feat: add manual WCS dispatch action
This commit is contained in:
@@ -74,7 +74,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
<if test="priority != null "> and priority = #{priority}</if>
|
||||
<if test="height != null "> and height = #{height}</if>
|
||||
<if test="weight != null "> and weight = #{weight}</if>
|
||||
<if test="params.beginCreateTime != null and params.beginCreateTime != '' and params.endCreateTime != null and params.endCreateTime != ''"> and create_time between #{params.beginCreateTime} and #{params.endCreateTime}</if>
|
||||
<if test="params.beginCreateTime != null and params.beginCreateTime != '' and params.endCreateTime != null and params.endCreateTime != ''"> and mydo_job.create_time between #{params.beginCreateTime} and #{params.endCreateTime}</if>
|
||||
<if test="entranceId != null and entranceId != ''"> and entrance_id = #{entranceId}</if>
|
||||
<if test="exitId != null and exitId != ''"> and exit_id = #{exitId}</if>
|
||||
<if test="deptName != null and deptName != ''"> and dept_name like concat('%', #{deptName}, '%')</if>
|
||||
@@ -430,7 +430,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
<if test="priority != null "> and priority = #{priority}</if>
|
||||
<if test="height != null "> and height = #{height}</if>
|
||||
<if test="weight != null "> and weight = #{weight}</if>
|
||||
<if test="params.beginCreateTime != null and params.beginCreateTime != '' and params.endCreateTime != null and params.endCreateTime != ''"> and create_time between #{params.beginCreateTime} and #{params.endCreateTime}</if>
|
||||
<if test="params.beginCreateTime != null and params.beginCreateTime != '' and params.endCreateTime != null and params.endCreateTime != ''"> and mydo_job.create_time between #{params.beginCreateTime} and #{params.endCreateTime}</if>
|
||||
<if test="entranceId != null and entranceId != ''"> and entrance_id = #{entranceId}</if>
|
||||
<if test="exitId != null and exitId != ''"> and exit_id = #{exitId}</if>
|
||||
<if test="deptName != null and deptName != ''"> and dept_name like concat('%', #{deptName}, '%')</if>
|
||||
@@ -484,7 +484,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
<if test="priority != null "> and priority = #{priority}</if>
|
||||
<if test="height != null "> and height = #{height}</if>
|
||||
<if test="weight != null "> and weight = #{weight}</if>
|
||||
<if test="params.beginCreateTime != null and params.beginCreateTime != '' and params.endCreateTime != null and params.endCreateTime != ''"> and create_time between #{params.beginCreateTime} and #{params.endCreateTime}</if>
|
||||
<if test="params.beginCreateTime != null and params.beginCreateTime != '' and params.endCreateTime != null and params.endCreateTime != ''"> and mydo_job.create_time between #{params.beginCreateTime} and #{params.endCreateTime}</if>
|
||||
<if test="entranceId != null and entranceId != ''"> and entrance_id = #{entranceId}</if>
|
||||
<if test="exitId != null and exitId != ''"> and exit_id = #{exitId}</if>
|
||||
<if test="deptName != null and deptName != ''"> and dept_name like concat('%', #{deptName}, '%')</if>
|
||||
@@ -594,7 +594,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
<if test="priority != null "> and priority = #{priority}</if>
|
||||
<if test="height != null "> and height = #{height}</if>
|
||||
<if test="weight != null "> and weight = #{weight}</if>
|
||||
<if test="params.beginCreateTime != null and params.beginCreateTime != '' and params.endCreateTime != null and params.endCreateTime != ''"> and create_time between #{params.beginCreateTime} and #{params.endCreateTime}</if>
|
||||
<if test="params.beginCreateTime != null and params.beginCreateTime != '' and params.endCreateTime != null and params.endCreateTime != ''"> and mydo_job.create_time between #{params.beginCreateTime} and #{params.endCreateTime}</if>
|
||||
<if test="entranceId != null and entranceId != ''"> and entrance_id = #{entranceId}</if>
|
||||
<if test="exitId != null and exitId != ''"> and exit_id = #{exitId}</if>
|
||||
<if test="deptName != null and deptName != ''"> and dept_name like concat('%', #{deptName}, '%')</if>
|
||||
@@ -622,7 +622,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
<if test="priority != null "> and priority = #{priority}</if>
|
||||
<if test="height != null "> and height = #{height}</if>
|
||||
<if test="weight != null "> and weight = #{weight}</if>
|
||||
<if test="params.beginCreateTime != null and params.beginCreateTime != '' and params.endCreateTime != null and params.endCreateTime != ''"> and create_time between #{params.beginCreateTime} and #{params.endCreateTime}</if>
|
||||
<if test="params.beginCreateTime != null and params.beginCreateTime != '' and params.endCreateTime != null and params.endCreateTime != ''"> and mydo_job.create_time between #{params.beginCreateTime} and #{params.endCreateTime}</if>
|
||||
<if test="entranceId != null and entranceId != ''"> and entrance_id = #{entranceId}</if>
|
||||
<if test="exitId != null and exitId != ''"> and exit_id = #{exitId}</if>
|
||||
<if test="deptName != null and deptName != ''"> and dept_name like concat('%', #{deptName}, '%')</if>
|
||||
|
||||
@@ -88,6 +88,19 @@ class MydoJobMapperSqlTest {
|
||||
.contains("<if test=\"warehouseId != null and warehouseId != ''\"> and warehouse_id = #{warehouseId}</if>");
|
||||
}
|
||||
|
||||
@Test
|
||||
void outboundListQualifiesCreateTimeAfterJoiningDispatchOutbox() throws Exception {
|
||||
String xml = Files.readString(resolveMapperXml());
|
||||
for (String selectId : new String[] {"selectMydoJobList", "selectInMydoJobList",
|
||||
"selectInventoryInMydoJobList", "selectOutMydoJobList", "selectInventoryOutMydoJobList"}) {
|
||||
String activeList = section(xml, "<select id=\"" + selectId + "\"", "</select>");
|
||||
assertThat(activeList)
|
||||
.as(selectId)
|
||||
.contains("mydo_job.create_time between #{params.beginCreateTime} and #{params.endCreateTime}")
|
||||
.doesNotContain("> and create_time between #{params.beginCreateTime}");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void denseInboundJobClaimIsAtomicAndLimitedToPendingWarehouse03Jobs() throws Exception {
|
||||
String xml = Files.readString(resolveMapperXml());
|
||||
|
||||
@@ -180,6 +180,21 @@ class OutboundDispatchServiceImplTest
|
||||
verify(wcsClient, never()).sendRetrieveTask(any(MydoJob.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void completedJobCannotBeManuallyDispatched()
|
||||
{
|
||||
MydoJob job = jobOfType("CK-COMPLETED", 21L);
|
||||
job.setStatus("4");
|
||||
when(jobMapper.selectMydoJobByJobId("CK-COMPLETED")).thenReturn(job);
|
||||
|
||||
AjaxResult result = service.dispatch("CK-COMPLETED", DispatchTrigger.MANUAL);
|
||||
|
||||
assertThat(result.isSuccess()).isFalse();
|
||||
assertThat(result.get("msg")).asString().contains("状态不允许");
|
||||
verify(dispatchMapper, never()).insertIfAbsent(any(String.class));
|
||||
verify(wcsClient, never()).sendRetrieveTask(any(MydoJob.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void blockedDenseJobDoesNotConsumeRetryOrCallWcs()
|
||||
{
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
package com.ruoyi.out.job.controller;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.stream.Stream;
|
||||
import com.ruoyi.common.core.domain.AjaxResult;
|
||||
import com.ruoyi.out.dispatch.domain.DispatchTrigger;
|
||||
import com.ruoyi.out.dispatch.service.IOutboundDispatchService;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
class OutMydoJobDispatchControllerTest
|
||||
{
|
||||
@Test
|
||||
void manualEndpointUsesDedicatedPermissionAndManualTrigger() throws Exception
|
||||
{
|
||||
Method endpoint = OutMydoJobController.class.getMethod("pushWcs", String.class);
|
||||
assertThat(endpoint.getAnnotation(PreAuthorize.class).value())
|
||||
.isEqualTo("@ss.hasPermi('out:job:pushWcs')");
|
||||
|
||||
IOutboundDispatchService dispatchService = mock(IOutboundDispatchService.class);
|
||||
when(dispatchService.dispatch("CK-1", DispatchTrigger.MANUAL))
|
||||
.thenReturn(AjaxResult.success("已下发"));
|
||||
OutMydoJobController controller = new OutMydoJobController();
|
||||
ReflectionTestUtils.setField(controller, "outboundDispatchService", dispatchService);
|
||||
|
||||
assertThat(controller.pushWcs("CK-1").isSuccess()).isTrue();
|
||||
verify(dispatchService).dispatch("CK-1", DispatchTrigger.MANUAL);
|
||||
}
|
||||
|
||||
@Test
|
||||
void jobPageShowsDispatchStateAndRestrictsManualAction() throws Exception
|
||||
{
|
||||
String vue = Files.readString(resolve("ruoyi-ui/src/views/out/job/index.vue"));
|
||||
String api = Files.readString(resolve("ruoyi-ui/src/api/out/job.js"));
|
||||
|
||||
assertThat(vue)
|
||||
.contains("prop=\"dispatchStatus\"")
|
||||
.contains("prop=\"autoRetryCount\"")
|
||||
.contains("lastDispatchError")
|
||||
.contains("out:job:pushWcs")
|
||||
.contains("canPushWcs")
|
||||
.contains("手动下发 WCS")
|
||||
.doesNotContain("v-hasPermi=\"['out:job:edit']\"");
|
||||
assertThat(api).contains("/out/job/pushWcs/");
|
||||
}
|
||||
|
||||
private Path resolve(String path)
|
||||
{
|
||||
return Stream.of(Path.of(path), Path.of("..", path))
|
||||
.filter(Files::exists)
|
||||
.findFirst()
|
||||
.orElseThrow();
|
||||
}
|
||||
}
|
||||
@@ -109,15 +109,19 @@
|
||||
>手动完成</el-button>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-button
|
||||
type="primary"
|
||||
plain
|
||||
icon="el-icon-upload2"
|
||||
size="mini"
|
||||
:disabled="single"
|
||||
@click="handlePushWcs"
|
||||
v-hasPermi="['out:job:edit']"
|
||||
>推送WCS</el-button>
|
||||
<el-tooltip content="手动下发 WCS" placement="top">
|
||||
<span>
|
||||
<el-button
|
||||
type="primary"
|
||||
plain
|
||||
icon="el-icon-s-promotion"
|
||||
size="mini"
|
||||
:disabled="single || !canPushWcs(selectedRows[0])"
|
||||
@click="handlePushWcs"
|
||||
v-hasPermi="['out:job:pushWcs']"
|
||||
>手动下发 WCS</el-button>
|
||||
</span>
|
||||
</el-tooltip>
|
||||
</el-col>
|
||||
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
|
||||
</el-row>
|
||||
@@ -154,6 +158,29 @@
|
||||
<span>{{ parseTime(scope.row.createTime, '{y}-{m}-{d} {h}:{i}:{s}') }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="WCS状态" align="center" prop="dispatchStatus" width="112">
|
||||
<template slot-scope="scope">
|
||||
<el-tag :type="dispatchStatusType(scope.row.dispatchStatus)" size="mini" effect="plain">
|
||||
{{ dispatchStatusLabel(scope.row.dispatchStatus) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="自动重试" align="center" prop="autoRetryCount" width="88">
|
||||
<template slot-scope="scope">{{ scope.row.autoRetryCount || 0 }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="最后下发错误" align="left" prop="lastDispatchError" min-width="160">
|
||||
<template slot-scope="scope">
|
||||
<el-tooltip
|
||||
v-if="scope.row.lastDispatchError"
|
||||
:content="scope.row.lastDispatchError"
|
||||
placement="top"
|
||||
effect="dark"
|
||||
>
|
||||
<span class="dispatch-error">{{ scope.row.lastDispatchError }}</span>
|
||||
</el-tooltip>
|
||||
<span v-else class="dispatch-empty">-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="开始时间" align="center" prop="startTime" width="160">
|
||||
<template slot-scope="scope">
|
||||
<span>{{ parseTime(scope.row.startTime, '{y}-{m}-{d} {h}:{i}:{s}') }}</span>
|
||||
@@ -239,6 +266,8 @@ export default {
|
||||
loading: true,
|
||||
// 选中数组
|
||||
ids: [],
|
||||
// 当前选中行
|
||||
selectedRows: [],
|
||||
// 非单个禁用
|
||||
single: true,
|
||||
// 显示搜索条件
|
||||
@@ -290,6 +319,8 @@ export default {
|
||||
this.jobList = response.rows
|
||||
this.total = response.total
|
||||
this.loading = false
|
||||
}).catch(() => {
|
||||
this.loading = false
|
||||
})
|
||||
},
|
||||
/** 搜索按钮操作 */
|
||||
@@ -313,6 +344,7 @@ export default {
|
||||
// 多选框选中数据
|
||||
handleSelectionChange(selection) {
|
||||
this.ids = selection.map(item => item.jobId)
|
||||
this.selectedRows = selection
|
||||
this.single = selection.length !== 1
|
||||
},
|
||||
/** 手动作废按钮操作 */
|
||||
@@ -337,14 +369,59 @@ export default {
|
||||
},
|
||||
/** 推送 WCS 按钮操作 */
|
||||
handlePushWcs() {
|
||||
const jobId = this.ids[0]
|
||||
this.$modal.confirm('是否确认推送选中的出库作业到WCS?').then(() => {
|
||||
return pushJobToWcs(jobId)
|
||||
}).then(() => {
|
||||
this.$modal.msgSuccess("推送成功")
|
||||
const row = this.selectedRows[0]
|
||||
if (!this.canPushWcs(row)) {
|
||||
this.$modal.msgWarning('当前作业状态不允许手动下发 WCS')
|
||||
return
|
||||
}
|
||||
const autoRetries = Number(row.autoRetryCount || 0)
|
||||
const manualRetries = Number(row.manualRetryCount || 0)
|
||||
const lastError = row.lastDispatchError || '无'
|
||||
const message = `确认手动下发作业 ${row.jobId} 到 WCS?自动重试 ${autoRetries} 次,人工下发 ${manualRetries} 次;最后错误:${lastError}`
|
||||
this.$modal.confirm(message).then(() => {
|
||||
return pushJobToWcs(row.jobId)
|
||||
}).then(response => {
|
||||
const resultMessage = response.msg || 'WCS手动下发请求已处理'
|
||||
if (resultMessage.includes('下发成功')) {
|
||||
this.$modal.msgSuccess(resultMessage)
|
||||
} else {
|
||||
this.$modal.msgWarning(resultMessage)
|
||||
}
|
||||
this.getList()
|
||||
}).catch(() => {})
|
||||
},
|
||||
canPushWcs(row) {
|
||||
if (!row || String(row.status) !== '1') {
|
||||
return false
|
||||
}
|
||||
return ['PENDING', 'RETRY_WAIT', 'EXHAUSTED', 'BLOCKED']
|
||||
.includes(this.normalizeDispatchStatus(row.dispatchStatus))
|
||||
},
|
||||
normalizeDispatchStatus(status) {
|
||||
return status || 'PENDING'
|
||||
},
|
||||
dispatchStatusLabel(status) {
|
||||
const labels = {
|
||||
PENDING: '待下发',
|
||||
SENDING: '下发中',
|
||||
BLOCKED: '通道阻挡',
|
||||
RETRY_WAIT: '等待重试',
|
||||
SUCCESS: '已下发',
|
||||
EXHAUSTED: '重试耗尽'
|
||||
}
|
||||
return labels[this.normalizeDispatchStatus(status)] || status || '待下发'
|
||||
},
|
||||
dispatchStatusType(status) {
|
||||
const types = {
|
||||
PENDING: 'info',
|
||||
SENDING: 'warning',
|
||||
BLOCKED: 'warning',
|
||||
RETRY_WAIT: 'warning',
|
||||
SUCCESS: 'success',
|
||||
EXHAUSTED: 'danger'
|
||||
}
|
||||
return types[this.normalizeDispatchStatus(status)] || 'info'
|
||||
},
|
||||
/** 点击主作业行 —— 联动刷新子作业 */
|
||||
handleRowClick(row) {
|
||||
this.currentJob = row
|
||||
@@ -368,3 +445,19 @@ export default {
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.dispatch-error {
|
||||
display: inline-block;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
color: #f56c6c;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.dispatch-empty {
|
||||
color: #c0c4cc;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user