feat: add WCS dispatch outbox

This commit is contained in:
zgw
2026-07-16 09:39:36 +08:00
parent 3ac7305724
commit aa6728e52b
7 changed files with 691 additions and 3 deletions

View File

@@ -140,6 +140,18 @@ public class MydoJob extends BaseEntity
/** 通道匹配物料名称(非持久化字段) */
private String productName;
/** WCS 下发状态(非持久化字段) */
private String dispatchStatus;
/** WCS 自动重试次数(非持久化字段) */
private Integer autoRetryCount;
/** WCS 人工下发次数(非持久化字段) */
private Integer manualRetryCount;
/** WCS 最后一次下发错误(非持久化字段) */
private String lastDispatchError;
public void setJobId(String jobId)
{
this.jobId = jobId;
@@ -490,6 +502,46 @@ public class MydoJob extends BaseEntity
this.productName = productName;
}
public String getDispatchStatus()
{
return dispatchStatus;
}
public void setDispatchStatus(String dispatchStatus)
{
this.dispatchStatus = dispatchStatus;
}
public Integer getAutoRetryCount()
{
return autoRetryCount;
}
public void setAutoRetryCount(Integer autoRetryCount)
{
this.autoRetryCount = autoRetryCount;
}
public Integer getManualRetryCount()
{
return manualRetryCount;
}
public void setManualRetryCount(Integer manualRetryCount)
{
this.manualRetryCount = manualRetryCount;
}
public String getLastDispatchError()
{
return lastDispatchError;
}
public void setLastDispatchError(String lastDispatchError)
{
this.lastDispatchError = lastDispatchError;
}
@Override
public String toString() {
return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE)
@@ -529,6 +581,10 @@ public class MydoJob extends BaseEntity
.append("projectNo", getProjectNo())
.append("productId", getProductId())
.append("productName", getProductName())
.append("dispatchStatus", getDispatchStatus())
.append("autoRetryCount", getAutoRetryCount())
.append("manualRetryCount", getManualRetryCount())
.append("lastDispatchError", getLastDispatchError())
.toString();
}
}

View File

@@ -0,0 +1,141 @@
package com.ruoyi.out.dispatch.domain;
import java.io.Serializable;
import java.util.Date;
/**
* WCS 作业下发记录。
*/
public class WcsJobDispatch implements Serializable
{
private static final long serialVersionUID = 1L;
public static final String PENDING = "PENDING";
public static final String SENDING = "SENDING";
public static final String BLOCKED = "BLOCKED";
public static final String RETRY_WAIT = "RETRY_WAIT";
public static final String SUCCESS = "SUCCESS";
public static final String EXHAUSTED = "EXHAUSTED";
private String jobId;
private String dispatchStatus;
private Integer autoRetryCount;
private Integer manualRetryCount;
private String lastError;
private Date lastAttemptTime;
private Date nextRetryTime;
private Date successTime;
private Integer versionNo;
private Date createTime;
private Date updateTime;
public String getJobId()
{
return jobId;
}
public void setJobId(String jobId)
{
this.jobId = jobId;
}
public String getDispatchStatus()
{
return dispatchStatus;
}
public void setDispatchStatus(String dispatchStatus)
{
this.dispatchStatus = dispatchStatus;
}
public Integer getAutoRetryCount()
{
return autoRetryCount;
}
public void setAutoRetryCount(Integer autoRetryCount)
{
this.autoRetryCount = autoRetryCount;
}
public Integer getManualRetryCount()
{
return manualRetryCount;
}
public void setManualRetryCount(Integer manualRetryCount)
{
this.manualRetryCount = manualRetryCount;
}
public String getLastError()
{
return lastError;
}
public void setLastError(String lastError)
{
this.lastError = lastError;
}
public Date getLastAttemptTime()
{
return lastAttemptTime;
}
public void setLastAttemptTime(Date lastAttemptTime)
{
this.lastAttemptTime = lastAttemptTime;
}
public Date getNextRetryTime()
{
return nextRetryTime;
}
public void setNextRetryTime(Date nextRetryTime)
{
this.nextRetryTime = nextRetryTime;
}
public Date getSuccessTime()
{
return successTime;
}
public void setSuccessTime(Date successTime)
{
this.successTime = successTime;
}
public Integer getVersionNo()
{
return versionNo;
}
public void setVersionNo(Integer versionNo)
{
this.versionNo = versionNo;
}
public Date getCreateTime()
{
return createTime;
}
public void setCreateTime(Date createTime)
{
this.createTime = createTime;
}
public Date getUpdateTime()
{
return updateTime;
}
public void setUpdateTime(Date updateTime)
{
this.updateTime = updateTime;
}
}

View File

@@ -0,0 +1,29 @@
package com.ruoyi.out.dispatch.mapper;
import java.util.Date;
import java.util.List;
import java.util.Map;
import org.apache.ibatis.annotations.Param;
import com.ruoyi.out.dispatch.domain.WcsJobDispatch;
/**
* WCS 作业下发记录 Mapper。
*/
public interface WcsJobDispatchMapper
{
int insertIfAbsent(String jobId);
WcsJobDispatch selectByJobId(String jobId);
List<WcsJobDispatch> selectRetryCandidates(@Param("now") Date now, @Param("limit") int limit);
int claimForSend(Map<String, Object> params);
int markBlocked(Map<String, Object> params);
int markRetryWait(Map<String, Object> params);
int markSuccess(Map<String, Object> params);
int markExhausted(Map<String, Object> params);
}

View File

@@ -36,12 +36,20 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<result property="supplierName" column="supplier_name" />
<result property="customerId" column="customer_id" />
<result property="customerName" column="customer_name" />
<result property="dispatchStatus" column="dispatch_status" />
<result property="autoRetryCount" column="auto_retry_count" />
<result property="manualRetryCount" column="manual_retry_count" />
<result property="lastDispatchError" column="last_dispatch_error" />
</resultMap>
<sql id="selectMydoJobVo">
select job_id, status, task_no, in_or_out, execution_phase, tray_code, warehouse_id,
select mydo_job.job_id, status, task_no, in_or_out, execution_phase, tray_code, warehouse_id,
isnull(nullif(warehouse_name, ''), (select top 1 bw.warehouse_name from bas_warehouse bw where bw.warehouse_id = mydo_job.warehouse_id)) as warehouse_name,
roadway_id, cargo_space_id, cargo_space_name, target_roadway_id, target_cargo_space_id, target_cargo_space_name, line_type, priority, height, weight, create_time, start_time, finish_time, entrance_id, exit_id, dept_id, dept_name, user_id, user_name, supplier_id, supplier_name, customer_id, customer_name from mydo_job
roadway_id, cargo_space_id, cargo_space_name, target_roadway_id, target_cargo_space_id, target_cargo_space_name, line_type, priority, height, weight, mydo_job.create_time, start_time, finish_time, entrance_id, exit_id, dept_id, dept_name, user_id, user_name, supplier_id, supplier_name, customer_id, customer_name,
wjd.dispatch_status as dispatch_status, wjd.auto_retry_count as auto_retry_count,
wjd.manual_retry_count as manual_retry_count, wjd.last_error as last_dispatch_error
from mydo_job
left join wcs_job_dispatch wjd on wjd.job_id = mydo_job.job_id
</sql>
<sql id="selectMydoJobFinishVo">
select job_id, status, task_no, in_or_out, execution_phase, tray_code, warehouse_id,
@@ -79,7 +87,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<select id="selectMydoJobByJobId" parameterType="String" resultMap="MydoJobResult">
<include refid="selectMydoJobVo"/>
where job_id = #{jobId}
where mydo_job.job_id = #{jobId}
</select>
<select id="acquirePutOccupiedJobLock" parameterType="java.util.Map" resultType="java.lang.Integer">

View File

@@ -0,0 +1,93 @@
<?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.dispatch.mapper.WcsJobDispatchMapper">
<resultMap type="com.ruoyi.out.dispatch.domain.WcsJobDispatch" id="WcsJobDispatchResult">
<id property="jobId" column="job_id" />
<result property="dispatchStatus" column="dispatch_status" />
<result property="autoRetryCount" column="auto_retry_count" />
<result property="manualRetryCount" column="manual_retry_count" />
<result property="lastError" column="last_error" />
<result property="lastAttemptTime" column="last_attempt_time" />
<result property="nextRetryTime" column="next_retry_time" />
<result property="successTime" column="success_time" />
<result property="versionNo" column="version_no" />
<result property="createTime" column="create_time" />
<result property="updateTime" column="update_time" />
</resultMap>
<insert id="insertIfAbsent" parameterType="String">
if not exists (select 1 from wcs_job_dispatch with (updlock, holdlock) where job_id = #{jobId})
begin
insert into wcs_job_dispatch
(job_id, dispatch_status, auto_retry_count, manual_retry_count, version_no, create_time, update_time)
values
(#{jobId}, 'PENDING', 0, 0, 0, getdate(), getdate())
end
</insert>
<select id="selectByJobId" parameterType="String" resultMap="WcsJobDispatchResult">
select job_id, dispatch_status, auto_retry_count, manual_retry_count, last_error,
last_attempt_time, next_retry_time, success_time, version_no, create_time, update_time
from wcs_job_dispatch
where job_id = #{jobId}
</select>
<select id="selectRetryCandidates" resultMap="WcsJobDispatchResult">
select top (#{limit}) job_id, dispatch_status, auto_retry_count, manual_retry_count, last_error,
last_attempt_time, next_retry_time, success_time, version_no, create_time, update_time
from wcs_job_dispatch with (readpast)
where (dispatch_status = 'RETRY_WAIT'
and auto_retry_count &lt; 5
and (next_retry_time is null or next_retry_time &lt;= #{now}))
or dispatch_status = 'BLOCKED'
or (dispatch_status = 'SENDING'
and last_attempt_time &lt;= dateadd(minute, -5, #{now}))
order by create_time, job_id
</select>
<update id="claimForSend" parameterType="map">
update wcs_job_dispatch
set dispatch_status = 'SENDING',
auto_retry_count = auto_retry_count + case when #{trigger} = 'AUTO_RETRY' then 1 else 0 end,
manual_retry_count = manual_retry_count + case when #{trigger} = 'MANUAL' then 1 else 0 end,
last_attempt_time = #{attemptTime},
next_retry_time = null,
update_time = #{attemptTime},
version_no = version_no + 1
where job_id = #{jobId}
and version_no = #{versionNo}
and dispatch_status in ('PENDING', 'BLOCKED', 'RETRY_WAIT', 'EXHAUSTED', 'SENDING')
and (#{trigger} != 'AUTO_RETRY' or auto_retry_count &lt; 5)
</update>
<update id="markBlocked" parameterType="map">
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'
</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'
</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'
</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'
</update>
</mapper>

View File

@@ -0,0 +1,81 @@
package com.ruoyi.out.dispatch.mapper;
import org.junit.jupiter.api.Test;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.stream.Stream;
import static org.assertj.core.api.Assertions.assertThat;
class WcsJobDispatchMapperSqlTest
{
@Test
void dispatchMapperDefinesDurableRetryStateTransitions() throws Exception
{
String xml = Files.readString(resolvePath(
"src/main/resources/mapper/out/WcsJobDispatchMapper.xml",
"ruoyi-admin/src/main/resources/mapper/out/WcsJobDispatchMapper.xml"));
assertThat(xml)
.contains("PENDING")
.contains("SENDING")
.contains("BLOCKED")
.contains("RETRY_WAIT")
.contains("SUCCESS")
.contains("EXHAUSTED")
.contains("auto_retry_count &lt; 5")
.contains("dispatch_status = 'SENDING'")
.contains("version_no = version_no + 1")
.contains("selectRetryCandidates")
.contains("claimForSend")
.contains("markRetryWait")
.contains("markExhausted");
}
@Test
void migrationCreatesRetryTaskRelocationTypeAndDenseLaneMenu() throws Exception
{
String sql = Files.readString(resolvePath(
"../sql/20260716_dense_outbound_relocation_dispatch.sql",
"sql/20260716_dense_outbound_relocation_dispatch.sql"));
assertThat(sql)
.contains("CREATE TABLE dbo.wcs_job_dispatch")
.contains("IF NOT EXISTS")
.contains("auto_retry_count")
.contains("manual_retry_count")
.contains("last_error")
.contains("wcsOutboundRetryTask.retryFailedJobs")
.contains("0 0/1 * * * ?")
.contains("out:denseLaneOutbound:list")
.contains("out:denseLaneOutbound:preview")
.contains("out:denseLaneOutbound:generate")
.contains("out:job:pushWcs")
.contains("N'23'")
.contains("N'移库'");
}
@Test
void outboundJobQueriesExposeDispatchState() throws Exception
{
String xml = Files.readString(resolvePath(
"src/main/resources/mapper/in/MydoJobMapper.xml",
"ruoyi-admin/src/main/resources/mapper/in/MydoJobMapper.xml"));
assertThat(xml)
.contains("left join wcs_job_dispatch")
.contains("dispatchStatus")
.contains("autoRetryCount")
.contains("manualRetryCount")
.contains("lastDispatchError");
}
private Path resolvePath(String modulePath, String rootPath)
{
return Stream.of(Path.of(modulePath), Path.of(rootPath))
.filter(Files::exists)
.findFirst()
.orElseThrow();
}
}