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="priority != null "> and priority = #{priority}</if>
|
||||||
<if test="height != null "> and height = #{height}</if>
|
<if test="height != null "> and height = #{height}</if>
|
||||||
<if test="weight != null "> and weight = #{weight}</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="entranceId != null and entranceId != ''"> and entrance_id = #{entranceId}</if>
|
||||||
<if test="exitId != null and exitId != ''"> and exit_id = #{exitId}</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>
|
<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="priority != null "> and priority = #{priority}</if>
|
||||||
<if test="height != null "> and height = #{height}</if>
|
<if test="height != null "> and height = #{height}</if>
|
||||||
<if test="weight != null "> and weight = #{weight}</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="entranceId != null and entranceId != ''"> and entrance_id = #{entranceId}</if>
|
||||||
<if test="exitId != null and exitId != ''"> and exit_id = #{exitId}</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>
|
<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="priority != null "> and priority = #{priority}</if>
|
||||||
<if test="height != null "> and height = #{height}</if>
|
<if test="height != null "> and height = #{height}</if>
|
||||||
<if test="weight != null "> and weight = #{weight}</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="entranceId != null and entranceId != ''"> and entrance_id = #{entranceId}</if>
|
||||||
<if test="exitId != null and exitId != ''"> and exit_id = #{exitId}</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>
|
<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="priority != null "> and priority = #{priority}</if>
|
||||||
<if test="height != null "> and height = #{height}</if>
|
<if test="height != null "> and height = #{height}</if>
|
||||||
<if test="weight != null "> and weight = #{weight}</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="entranceId != null and entranceId != ''"> and entrance_id = #{entranceId}</if>
|
||||||
<if test="exitId != null and exitId != ''"> and exit_id = #{exitId}</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>
|
<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="priority != null "> and priority = #{priority}</if>
|
||||||
<if test="height != null "> and height = #{height}</if>
|
<if test="height != null "> and height = #{height}</if>
|
||||||
<if test="weight != null "> and weight = #{weight}</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="entranceId != null and entranceId != ''"> and entrance_id = #{entranceId}</if>
|
||||||
<if test="exitId != null and exitId != ''"> and exit_id = #{exitId}</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>
|
<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>");
|
.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
|
@Test
|
||||||
void denseInboundJobClaimIsAtomicAndLimitedToPendingWarehouse03Jobs() throws Exception {
|
void denseInboundJobClaimIsAtomicAndLimitedToPendingWarehouse03Jobs() throws Exception {
|
||||||
String xml = Files.readString(resolveMapperXml());
|
String xml = Files.readString(resolveMapperXml());
|
||||||
|
|||||||
@@ -180,6 +180,21 @@ class OutboundDispatchServiceImplTest
|
|||||||
verify(wcsClient, never()).sendRetrieveTask(any(MydoJob.class));
|
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
|
@Test
|
||||||
void blockedDenseJobDoesNotConsumeRetryOrCallWcs()
|
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-button>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :span="1.5">
|
<el-col :span="1.5">
|
||||||
<el-button
|
<el-tooltip content="手动下发 WCS" placement="top">
|
||||||
type="primary"
|
<span>
|
||||||
plain
|
<el-button
|
||||||
icon="el-icon-upload2"
|
type="primary"
|
||||||
size="mini"
|
plain
|
||||||
:disabled="single"
|
icon="el-icon-s-promotion"
|
||||||
@click="handlePushWcs"
|
size="mini"
|
||||||
v-hasPermi="['out:job:edit']"
|
:disabled="single || !canPushWcs(selectedRows[0])"
|
||||||
>推送WCS</el-button>
|
@click="handlePushWcs"
|
||||||
|
v-hasPermi="['out:job:pushWcs']"
|
||||||
|
>手动下发 WCS</el-button>
|
||||||
|
</span>
|
||||||
|
</el-tooltip>
|
||||||
</el-col>
|
</el-col>
|
||||||
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
|
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
|
||||||
</el-row>
|
</el-row>
|
||||||
@@ -154,6 +158,29 @@
|
|||||||
<span>{{ parseTime(scope.row.createTime, '{y}-{m}-{d} {h}:{i}:{s}') }}</span>
|
<span>{{ parseTime(scope.row.createTime, '{y}-{m}-{d} {h}:{i}:{s}') }}</span>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</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">
|
<el-table-column label="开始时间" align="center" prop="startTime" width="160">
|
||||||
<template slot-scope="scope">
|
<template slot-scope="scope">
|
||||||
<span>{{ parseTime(scope.row.startTime, '{y}-{m}-{d} {h}:{i}:{s}') }}</span>
|
<span>{{ parseTime(scope.row.startTime, '{y}-{m}-{d} {h}:{i}:{s}') }}</span>
|
||||||
@@ -239,6 +266,8 @@ export default {
|
|||||||
loading: true,
|
loading: true,
|
||||||
// 选中数组
|
// 选中数组
|
||||||
ids: [],
|
ids: [],
|
||||||
|
// 当前选中行
|
||||||
|
selectedRows: [],
|
||||||
// 非单个禁用
|
// 非单个禁用
|
||||||
single: true,
|
single: true,
|
||||||
// 显示搜索条件
|
// 显示搜索条件
|
||||||
@@ -290,6 +319,8 @@ export default {
|
|||||||
this.jobList = response.rows
|
this.jobList = response.rows
|
||||||
this.total = response.total
|
this.total = response.total
|
||||||
this.loading = false
|
this.loading = false
|
||||||
|
}).catch(() => {
|
||||||
|
this.loading = false
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
/** 搜索按钮操作 */
|
/** 搜索按钮操作 */
|
||||||
@@ -313,6 +344,7 @@ export default {
|
|||||||
// 多选框选中数据
|
// 多选框选中数据
|
||||||
handleSelectionChange(selection) {
|
handleSelectionChange(selection) {
|
||||||
this.ids = selection.map(item => item.jobId)
|
this.ids = selection.map(item => item.jobId)
|
||||||
|
this.selectedRows = selection
|
||||||
this.single = selection.length !== 1
|
this.single = selection.length !== 1
|
||||||
},
|
},
|
||||||
/** 手动作废按钮操作 */
|
/** 手动作废按钮操作 */
|
||||||
@@ -337,14 +369,59 @@ export default {
|
|||||||
},
|
},
|
||||||
/** 推送 WCS 按钮操作 */
|
/** 推送 WCS 按钮操作 */
|
||||||
handlePushWcs() {
|
handlePushWcs() {
|
||||||
const jobId = this.ids[0]
|
const row = this.selectedRows[0]
|
||||||
this.$modal.confirm('是否确认推送选中的出库作业到WCS?').then(() => {
|
if (!this.canPushWcs(row)) {
|
||||||
return pushJobToWcs(jobId)
|
this.$modal.msgWarning('当前作业状态不允许手动下发 WCS')
|
||||||
}).then(() => {
|
return
|
||||||
this.$modal.msgSuccess("推送成功")
|
}
|
||||||
|
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()
|
this.getList()
|
||||||
}).catch(() => {})
|
}).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) {
|
handleRowClick(row) {
|
||||||
this.currentJob = row
|
this.currentJob = row
|
||||||
@@ -368,3 +445,19 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</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