feat(wm-dispatch): #69 调度指令全生命周期管理
- 增强 DispatchCommand 实体,支持全生命周期状态流转 - 新增 CommandExecutionRecord(执行记录)、CommandTracking(过程追踪)实体 - 新增 DTO: CommandCreateRequest, CommandQueryRequest, ExecutionRequest, CommandStatVO - 新增 Mapper: CommandExecutionRecordMapper, CommandTrackingMapper, 增强 DispatchCommandMapper(分页+统计) - 新增 Service: - CommandLifecycleService: DRAFT→ISSUED→RECEIVED→EXECUTING→COMPLETED/REJECTED/CANCELLED 全流程 - CommandLedgerService: 指令台账(多维度查询/统计/导出) - CommandTrackingService: 全过程追踪时间线 - 新增 DispatchCommandController: 18个 RESTful 端点 (/api/dispatch/command/*) - DDL: V2__command_lifecycle.sql(3张表 + 索引) - 单元测试: CommandLifecycleServiceTest(13个), CommandTrackingServiceTest(4个) - 同步修复 DispatchBizService/DispatchController 适配新实体字段
This commit is contained in:
+248
@@ -0,0 +1,248 @@
|
||||
package com.water.dispatch.service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.water.common.core.exception.BusinessException;
|
||||
import com.water.dispatch.entity.CommandExecutionRecord;
|
||||
import com.water.dispatch.entity.CommandTracking;
|
||||
import com.water.dispatch.entity.DispatchCommand;
|
||||
import com.water.dispatch.entity.dto.CommandCreateRequest;
|
||||
import com.water.dispatch.mapper.CommandExecutionRecordMapper;
|
||||
import com.water.dispatch.mapper.CommandTrackingMapper;
|
||||
import com.water.dispatch.mapper.DispatchCommandMapper;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class CommandLifecycleServiceTest {
|
||||
|
||||
@Mock
|
||||
private DispatchCommandMapper commandMapper;
|
||||
@Mock
|
||||
private CommandExecutionRecordMapper executionMapper;
|
||||
@Mock
|
||||
private CommandTrackingMapper trackingMapper;
|
||||
|
||||
@InjectMocks
|
||||
private CommandLifecycleService lifecycleService;
|
||||
|
||||
@Test
|
||||
void testCreateCommand() {
|
||||
when(commandMapper.insert(any())).thenReturn(1);
|
||||
when(trackingMapper.insert(any())).thenReturn(1);
|
||||
|
||||
CommandCreateRequest req = new CommandCreateRequest();
|
||||
req.setTitle("测试指令");
|
||||
req.setContent("测试内容");
|
||||
req.setCommandType("NORMAL");
|
||||
req.setPriority("HIGH");
|
||||
req.setIssuerId(1L);
|
||||
req.setIssuerName("张三");
|
||||
|
||||
DispatchCommand result = lifecycleService.create(req);
|
||||
|
||||
assertNotNull(result.getCommandNo());
|
||||
assertEquals("DRAFT", result.getStatus());
|
||||
assertEquals("测试指令", result.getTitle());
|
||||
assertEquals("NORMAL", result.getCommandType());
|
||||
assertEquals("HIGH", result.getPriority());
|
||||
verify(commandMapper).insert(any());
|
||||
verify(trackingMapper).insert(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testIssueCommand() {
|
||||
DispatchCommand cmd = buildCommand(1L, "DRAFT");
|
||||
when(commandMapper.selectById(1L)).thenReturn(cmd);
|
||||
when(commandMapper.updateById(any())).thenReturn(1);
|
||||
when(trackingMapper.insert(any())).thenReturn(1);
|
||||
|
||||
DispatchCommand result = lifecycleService.issue(1L);
|
||||
|
||||
assertEquals("ISSUED", result.getStatus());
|
||||
assertNotNull(result.getIssuedAt());
|
||||
verify(commandMapper).updateById(any());
|
||||
verify(trackingMapper).insert(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testIssueCommandWrongStatus() {
|
||||
DispatchCommand cmd = buildCommand(1L, "ISSUED");
|
||||
when(commandMapper.selectById(1L)).thenReturn(cmd);
|
||||
|
||||
assertThrows(BusinessException.class, () -> lifecycleService.issue(1L));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testReceiveCommand() {
|
||||
DispatchCommand cmd = buildCommand(1L, "ISSUED");
|
||||
when(commandMapper.selectById(1L)).thenReturn(cmd);
|
||||
when(commandMapper.updateById(any())).thenReturn(1);
|
||||
when(executionMapper.insert(any())).thenReturn(1);
|
||||
when(trackingMapper.insert(any())).thenReturn(1);
|
||||
|
||||
DispatchCommand result = lifecycleService.receive(1L, 2L, "李四");
|
||||
|
||||
assertEquals("RECEIVED", result.getStatus());
|
||||
assertNotNull(result.getReceivedAt());
|
||||
assertEquals(2L, result.getReceiverId());
|
||||
|
||||
ArgumentCaptor<CommandExecutionRecord> execCaptor = ArgumentCaptor.forClass(CommandExecutionRecord.class);
|
||||
verify(executionMapper).insert(execCaptor.capture());
|
||||
assertEquals("RECEIVE", execCaptor.getValue().getAction());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testStartExecution() {
|
||||
DispatchCommand cmd = buildCommand(1L, "RECEIVED");
|
||||
when(commandMapper.selectById(1L)).thenReturn(cmd);
|
||||
when(commandMapper.updateById(any())).thenReturn(1);
|
||||
when(executionMapper.insert(any())).thenReturn(1);
|
||||
when(trackingMapper.insert(any())).thenReturn(1);
|
||||
|
||||
DispatchCommand result = lifecycleService.startExecution(1L, 2L, "李四");
|
||||
|
||||
assertEquals("EXECUTING", result.getStatus());
|
||||
assertNotNull(result.getExecutedAt());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCompleteCommand() {
|
||||
DispatchCommand cmd = buildCommand(1L, "EXECUTING");
|
||||
when(commandMapper.selectById(1L)).thenReturn(cmd);
|
||||
when(commandMapper.updateById(any())).thenReturn(1);
|
||||
when(executionMapper.insert(any())).thenReturn(1);
|
||||
when(trackingMapper.insert(any())).thenReturn(1);
|
||||
|
||||
DispatchCommand result = lifecycleService.complete(1L, 2L, "李四", "已完成管道修复");
|
||||
|
||||
assertEquals("COMPLETED", result.getStatus());
|
||||
assertNotNull(result.getCompletedAt());
|
||||
assertEquals("已完成管道修复", result.getExecuteResult());
|
||||
|
||||
ArgumentCaptor<CommandExecutionRecord> execCaptor = ArgumentCaptor.forClass(CommandExecutionRecord.class);
|
||||
verify(executionMapper).insert(execCaptor.capture());
|
||||
assertEquals(100, execCaptor.getValue().getProgress());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRejectCommand() {
|
||||
DispatchCommand cmd = buildCommand(1L, "ISSUED");
|
||||
when(commandMapper.selectById(1L)).thenReturn(cmd);
|
||||
when(commandMapper.updateById(any())).thenReturn(1);
|
||||
when(executionMapper.insert(any())).thenReturn(1);
|
||||
when(trackingMapper.insert(any())).thenReturn(1);
|
||||
|
||||
DispatchCommand result = lifecycleService.reject(1L, 2L, "李四", "信息不足,无法执行");
|
||||
|
||||
assertEquals("REJECTED", result.getStatus());
|
||||
assertEquals("信息不足,无法执行", result.getRejectReason());
|
||||
assertNotNull(result.getRejectedAt());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRejectCompletedCommand() {
|
||||
DispatchCommand cmd = buildCommand(1L, "COMPLETED");
|
||||
when(commandMapper.selectById(1L)).thenReturn(cmd);
|
||||
|
||||
assertThrows(BusinessException.class,
|
||||
() -> lifecycleService.reject(1L, 2L, "李四", "原因"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCancelCommand() {
|
||||
DispatchCommand cmd = buildCommand(1L, "DRAFT");
|
||||
when(commandMapper.selectById(1L)).thenReturn(cmd);
|
||||
when(commandMapper.updateById(any())).thenReturn(1);
|
||||
when(trackingMapper.insert(any())).thenReturn(1);
|
||||
|
||||
DispatchCommand result = lifecycleService.cancel(1L, 1L, "张三", "计划变更");
|
||||
|
||||
assertEquals("CANCELLED", result.getStatus());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCancelAlreadyCancelledCommand() {
|
||||
DispatchCommand cmd = buildCommand(1L, "CANCELLED");
|
||||
when(commandMapper.selectById(1L)).thenReturn(cmd);
|
||||
|
||||
assertThrows(BusinessException.class,
|
||||
() -> lifecycleService.cancel(1L, 1L, "张三", "reason"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCommandNotFound() {
|
||||
when(commandMapper.selectById(999L)).thenReturn(null);
|
||||
assertThrows(BusinessException.class, () -> lifecycleService.issue(999L));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testReportProgress() {
|
||||
DispatchCommand cmd = buildCommand(1L, "EXECUTING");
|
||||
when(commandMapper.selectById(1L)).thenReturn(cmd);
|
||||
when(executionMapper.insert(any())).thenReturn(1);
|
||||
|
||||
DispatchCommand result = lifecycleService.reportProgress(1L, 2L, "李四", "完成50%", 50);
|
||||
|
||||
assertEquals("EXECUTING", result.getStatus()); // status unchanged
|
||||
ArgumentCaptor<CommandExecutionRecord> captor = ArgumentCaptor.forClass(CommandExecutionRecord.class);
|
||||
verify(executionMapper).insert(captor.capture());
|
||||
assertEquals("PROGRESS", captor.getValue().getAction());
|
||||
assertEquals(50, captor.getValue().getProgress());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFullLifecycle() {
|
||||
// Create
|
||||
when(commandMapper.insert(any())).thenReturn(1);
|
||||
when(trackingMapper.insert(any())).thenReturn(1);
|
||||
when(executionMapper.insert(any())).thenReturn(1);
|
||||
|
||||
CommandCreateRequest req = new CommandCreateRequest();
|
||||
req.setTitle("全流程测试");
|
||||
req.setCommandType("EMERGENCY");
|
||||
req.setPriority("URGENT");
|
||||
req.setIssuerId(1L);
|
||||
req.setIssuerName("张三");
|
||||
|
||||
DispatchCommand cmd = lifecycleService.create(req);
|
||||
cmd.setId(100L);
|
||||
assertEquals("DRAFT", cmd.getStatus());
|
||||
|
||||
// Issue
|
||||
when(commandMapper.selectById(100L)).thenReturn(cmd);
|
||||
when(commandMapper.updateById(any())).thenReturn(1);
|
||||
lifecycleService.issue(100L);
|
||||
assertEquals("ISSUED", cmd.getStatus());
|
||||
|
||||
// Receive
|
||||
lifecycleService.receive(100L, 2L, "李四");
|
||||
assertEquals("RECEIVED", cmd.getStatus());
|
||||
|
||||
// Start
|
||||
lifecycleService.startExecution(100L, 2L, "李四");
|
||||
assertEquals("EXECUTING", cmd.getStatus());
|
||||
|
||||
// Complete
|
||||
lifecycleService.complete(100L, 2L, "李四", "抢修完成");
|
||||
assertEquals("COMPLETED", cmd.getStatus());
|
||||
}
|
||||
|
||||
private DispatchCommand buildCommand(Long id, String status) {
|
||||
DispatchCommand cmd = new DispatchCommand();
|
||||
cmd.setId(id);
|
||||
cmd.setCommandNo("CMD-TEST-001");
|
||||
cmd.setTitle("测试指令");
|
||||
cmd.setStatus(status);
|
||||
cmd.setIssuerId(1L);
|
||||
cmd.setIssuerName("张三");
|
||||
return cmd;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
package com.water.dispatch.service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.water.dispatch.entity.CommandExecutionRecord;
|
||||
import com.water.dispatch.entity.CommandTracking;
|
||||
import com.water.dispatch.mapper.CommandExecutionRecordMapper;
|
||||
import com.water.dispatch.mapper.CommandTrackingMapper;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class CommandTrackingServiceTest {
|
||||
|
||||
@Mock
|
||||
private CommandTrackingMapper trackingMapper;
|
||||
@Mock
|
||||
private CommandExecutionRecordMapper executionMapper;
|
||||
|
||||
@InjectMocks
|
||||
private CommandTrackingService trackingService;
|
||||
|
||||
@Test
|
||||
void testGetTimeline() {
|
||||
CommandTracking t1 = new CommandTracking();
|
||||
t1.setId(1L);
|
||||
t1.setCommandId(100L);
|
||||
t1.setStage("CREATED");
|
||||
t1.setCreatedAt(LocalDateTime.of(2026, 6, 14, 10, 0));
|
||||
|
||||
CommandTracking t2 = new CommandTracking();
|
||||
t2.setId(2L);
|
||||
t2.setCommandId(100L);
|
||||
t2.setStage("ISSUED");
|
||||
t2.setCreatedAt(LocalDateTime.of(2026, 6, 14, 10, 5));
|
||||
|
||||
when(trackingMapper.selectList(any(LambdaQueryWrapper.class)))
|
||||
.thenReturn(List.of(t1, t2));
|
||||
|
||||
List<CommandTracking> result = trackingService.getTimeline(100L);
|
||||
assertEquals(2, result.size());
|
||||
assertEquals("CREATED", result.get(0).getStage());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetExecutionRecords() {
|
||||
CommandExecutionRecord r1 = new CommandExecutionRecord();
|
||||
r1.setId(1L);
|
||||
r1.setCommandId(100L);
|
||||
r1.setAction("RECEIVE");
|
||||
r1.setProgress(0);
|
||||
|
||||
when(executionMapper.selectList(any(LambdaQueryWrapper.class)))
|
||||
.thenReturn(List.of(r1));
|
||||
|
||||
List<CommandExecutionRecord> result = trackingService.getExecutionRecords(100L);
|
||||
assertEquals(1, result.size());
|
||||
assertEquals("RECEIVE", result.get(0).getAction());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetFullTimeline() {
|
||||
CommandTracking t = new CommandTracking();
|
||||
t.setId(1L);
|
||||
t.setCommandId(100L);
|
||||
t.setStage("CREATED");
|
||||
t.setOperatorName("张三");
|
||||
t.setActionDesc("创建指令");
|
||||
t.setCreatedAt(LocalDateTime.of(2026, 6, 14, 10, 0));
|
||||
|
||||
CommandExecutionRecord r = new CommandExecutionRecord();
|
||||
r.setId(1L);
|
||||
r.setCommandId(100L);
|
||||
r.setAction("RECEIVE");
|
||||
r.setExecutorName("李四");
|
||||
r.setDescription("确认接收");
|
||||
r.setProgress(0);
|
||||
r.setCreatedAt(LocalDateTime.of(2026, 6, 14, 10, 5));
|
||||
|
||||
when(trackingMapper.selectList(any(LambdaQueryWrapper.class)))
|
||||
.thenReturn(List.of(t));
|
||||
when(executionMapper.selectList(any(LambdaQueryWrapper.class)))
|
||||
.thenReturn(List.of(r));
|
||||
|
||||
List<Map<String, Object>> timeline = trackingService.getFullTimeline(100L);
|
||||
assertEquals(2, timeline.size());
|
||||
assertEquals("TRACKING", timeline.get(0).get("type"));
|
||||
assertEquals("EXECUTION", timeline.get(1).get("type"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetTimelineByCommandNo() {
|
||||
CommandTracking t = new CommandTracking();
|
||||
t.setCommandNo("CMD-20260614-001");
|
||||
t.setStage("ISSUED");
|
||||
t.setCreatedAt(LocalDateTime.now());
|
||||
|
||||
when(trackingMapper.selectList(any(LambdaQueryWrapper.class)))
|
||||
.thenReturn(List.of(t));
|
||||
|
||||
List<CommandTracking> result = trackingService.getTimelineByCommandNo("CMD-20260614-001");
|
||||
assertEquals(1, result.size());
|
||||
assertEquals("ISSUED", result.get(0).getStage());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user