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:
2026-06-14 15:24:37 +08:00
parent 51c292f25b
commit 21fa7cffd2
19 changed files with 1480 additions and 52 deletions
@@ -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());
}
}