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,159 @@
package com.water.dispatch.controller;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.water.common.core.result.R;
import com.water.dispatch.entity.CommandExecutionRecord;
import com.water.dispatch.entity.CommandTracking;
import com.water.dispatch.entity.DispatchCommand;
import com.water.dispatch.entity.dto.*;
import com.water.dispatch.service.CommandLedgerService;
import com.water.dispatch.service.CommandLifecycleService;
import com.water.dispatch.service.CommandTrackingService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Map;
/**
* 调度指令管理 - 全生命周期
*/
@Tag(name = "调度指令管理")
@RestController
@RequestMapping("/api/dispatch/command")
@RequiredArgsConstructor
public class DispatchCommandController {
private final CommandLifecycleService lifecycleService;
private final CommandLedgerService ledgerService;
private final CommandTrackingService trackingService;
// ==================== 生命周期操作 ====================
@Operation(summary = "1. 创建指令")
@PostMapping
public R<DispatchCommand> create(@RequestBody CommandCreateRequest req) {
return R.ok(lifecycleService.create(req));
}
@Operation(summary = "2. 下发指令")
@PostMapping("/{id}/issue")
public R<DispatchCommand> issue(@PathVariable Long id) {
return R.ok(lifecycleService.issue(id));
}
@Operation(summary = "3. 接收确认")
@PostMapping("/{id}/receive")
public R<DispatchCommand> receive(@PathVariable Long id,
@RequestParam(required = false) Long receiverId,
@RequestParam(required = false) String receiverName) {
return R.ok(lifecycleService.receive(id, receiverId, receiverName));
}
@Operation(summary = "4. 开始执行")
@PostMapping("/{id}/start")
public R<DispatchCommand> startExecution(@PathVariable Long id,
@RequestParam(required = false) Long executorId,
@RequestParam(required = false) String executorName) {
return R.ok(lifecycleService.startExecution(id, executorId, executorName));
}
@Operation(summary = "5. 执行进度反馈")
@PostMapping("/{id}/progress")
public R<DispatchCommand> reportProgress(@PathVariable Long id,
@RequestBody ExecutionRequest req) {
return R.ok(lifecycleService.reportProgress(id, req.getExecutorId(), req.getExecutorName(),
req.getDescription(), req.getProgress() != null ? req.getProgress() : 0));
}
@Operation(summary = "6. 完成指令")
@PostMapping("/{id}/complete")
public R<DispatchCommand> complete(@PathVariable Long id,
@RequestBody ExecutionRequest req) {
return R.ok(lifecycleService.complete(id, req.getExecutorId(), req.getExecutorName(), req.getExecuteResult()));
}
@Operation(summary = "7. 驳回指令")
@PostMapping("/{id}/reject")
public R<DispatchCommand> reject(@PathVariable Long id,
@RequestBody ExecutionRequest req) {
return R.ok(lifecycleService.reject(id, req.getExecutorId(), req.getExecutorName(), req.getRejectReason()));
}
@Operation(summary = "8. 取消指令")
@PostMapping("/{id}/cancel")
public R<DispatchCommand> cancel(@PathVariable Long id,
@RequestParam(required = false) Long operatorId,
@RequestParam(required = false) String operatorName,
@RequestParam(required = false) String reason) {
return R.ok(lifecycleService.cancel(id, operatorId, operatorName, reason));
}
// ==================== 查询 ====================
@Operation(summary = "9. 获取指令详情(按ID)")
@GetMapping("/{id}")
public R<DispatchCommand> getById(@PathVariable Long id) {
return R.ok(lifecycleService.getById(id));
}
@Operation(summary = "10. 根据指令编号获取详情")
@GetMapping("/no/{commandNo}")
public R<DispatchCommand> getByCommandNo(@PathVariable String commandNo) {
return R.ok(lifecycleService.getByCommandNo(commandNo));
}
// ==================== 台账 ====================
@Operation(summary = "11. 指令台账分页查询")
@GetMapping("/page")
public R<IPage<DispatchCommand>> queryPage(CommandQueryRequest req) {
return R.ok(ledgerService.queryPage(req));
}
@Operation(summary = "12. 指令台账列表查询")
@GetMapping("/list")
public R<List<DispatchCommand>> queryList(CommandQueryRequest req) {
return R.ok(ledgerService.queryList(req));
}
@Operation(summary = "13. 指令统计概览")
@GetMapping("/statistics")
public R<CommandStatVO> statistics() {
return R.ok(ledgerService.statistics());
}
@Operation(summary = "14. 指令数据导出")
@GetMapping("/export")
public R<List<DispatchCommand>> exportData(CommandQueryRequest req) {
return R.ok(ledgerService.exportData(req));
}
// ==================== 追踪 ====================
@Operation(summary = "15. 获取指令追踪时间线")
@GetMapping("/{id}/timeline")
public R<List<CommandTracking>> getTimeline(@PathVariable Long id) {
return R.ok(trackingService.getTimeline(id));
}
@Operation(summary = "16. 获取执行记录")
@GetMapping("/{id}/executions")
public R<List<CommandExecutionRecord>> getExecutions(@PathVariable Long id) {
return R.ok(trackingService.getExecutionRecords(id));
}
@Operation(summary = "17. 获取完整时间线(追踪+执行记录合并)")
@GetMapping("/{id}/full-timeline")
public R<List<Map<String, Object>>> getFullTimeline(@PathVariable Long id) {
return R.ok(trackingService.getFullTimeline(id));
}
@Operation(summary = "18. 按指令编号获取追踪时间线")
@GetMapping("/no/{commandNo}/timeline")
public R<List<CommandTracking>> getTimelineByCommandNo(@PathVariable String commandNo) {
return R.ok(trackingService.getTimelineByCommandNo(commandNo));
}
}
@@ -1,4 +1,5 @@
package com.water.dispatch.controller;
import com.water.common.core.result.R;
import com.water.dispatch.entity.*;
import com.water.dispatch.service.DispatchBizService;
@@ -6,19 +7,71 @@ import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.util.*;
@Tag(name="调度工作台") @RestController @RequestMapping("/dispatch") @RequiredArgsConstructor
@Tag(name = "调度工作台")
@RestController
@RequestMapping("/dispatch")
@RequiredArgsConstructor
public class DispatchController {
private final DispatchBizService svc;
@GetMapping("/duty/today") public R<List<DutySchedule>> todayDuty() { return R.ok(svc.getTodayDuty()); }
@PostMapping("/command") public R<Map<String,Object>> createCommand(@RequestBody Map<String,Object> req) { return R.ok(svc.createCommand(req)); }
@PostMapping("/command/{cmdNo}/issue") public R<Map<String,Object>> issue(@PathVariable String cmdNo) { return R.ok(svc.issueCommand(cmdNo)); }
@GetMapping("/command/list") public R<List<DispatchCommand>> listCommands(@RequestParam(required=false) Integer status) { return R.ok(svc.listCommands(status)); }
@PostMapping("/work-order") public R<Map<String,Object>> createWO(@RequestBody Map<String,Object> req) { return R.ok(svc.createWorkOrder(req)); }
@PutMapping("/work-order/{id}/status") public R<Map<String,Object>> updateWOStatus(@PathVariable Long id, @RequestParam int status) { return R.ok(svc.updateWorkOrderStatus(id, status)); }
@PostMapping("/duty-log") public R<String> addLog(@RequestParam Long scheduleId, @RequestParam Long userId, @RequestParam String type, @RequestParam String content) { svc.addDutyLog(scheduleId,userId,type,content); return R.ok("OK"); }
@GetMapping("/duty-log/{scheduleId}") public R<List<DutyLog>> getLogs(@PathVariable Long scheduleId) { return R.ok(svc.getDutyLogs(scheduleId)); }
@GetMapping("/strategy") public R<List<DispatchStrategy>> listStrategies(@RequestParam(required=false) String type) { return R.ok(svc.listStrategies(type)); }
@GetMapping("/emergency-plan") public R<List<EmergencyPlan>> listPlans(@RequestParam(required=false) String type) { return R.ok(svc.listPlans(type)); }
@PostMapping("/emergency/simulate") public R<Map<String,Object>> simulate(@RequestParam String type, @RequestParam double lng, @RequestParam double lat) { return R.ok(svc.simulateEmergency(type,lng,lat)); }
@GetMapping("/duty/today")
public R<List<DutySchedule>> todayDuty() {
return R.ok(svc.getTodayDuty());
}
@PostMapping("/command")
public R<Map<String, Object>> createCommand(@RequestBody Map<String, Object> req) {
return R.ok(svc.createCommand(req));
}
@PostMapping("/command/{cmdNo}/issue")
public R<Map<String, Object>> issue(@PathVariable String cmdNo) {
return R.ok(svc.issueCommand(cmdNo));
}
@GetMapping("/command/list")
public R<List<DispatchCommand>> listCommands(@RequestParam(required = false) String status) {
return R.ok(svc.listCommands(status));
}
@PostMapping("/work-order")
public R<Map<String, Object>> createWO(@RequestBody Map<String, Object> req) {
return R.ok(svc.createWorkOrder(req));
}
@PutMapping("/work-order/{id}/status")
public R<Map<String, Object>> updateWOStatus(@PathVariable Long id, @RequestParam int status) {
return R.ok(svc.updateWorkOrderStatus(id, status));
}
@PostMapping("/duty-log")
public R<String> addLog(@RequestParam Long scheduleId, @RequestParam Long userId,
@RequestParam String type, @RequestParam String content) {
svc.addDutyLog(scheduleId, userId, type, content);
return R.ok("OK");
}
@GetMapping("/duty-log/{scheduleId}")
public R<List<DutyLog>> getLogs(@PathVariable Long scheduleId) {
return R.ok(svc.getDutyLogs(scheduleId));
}
@GetMapping("/strategy")
public R<List<DispatchStrategy>> listStrategies(@RequestParam(required = false) String type) {
return R.ok(svc.listStrategies(type));
}
@GetMapping("/emergency-plan")
public R<List<EmergencyPlan>> listPlans(@RequestParam(required = false) String type) {
return R.ok(svc.listPlans(type));
}
@PostMapping("/emergency/simulate")
public R<Map<String, Object>> simulate(@RequestParam String type,
@RequestParam double lng, @RequestParam double lat) {
return R.ok(svc.simulateEmergency(type, lng, lat));
}
}
@@ -0,0 +1,43 @@
package com.water.dispatch.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.time.LocalDateTime;
/**
* 指令执行记录
*/
@Data
@TableName("disp_command_execution_record")
public class CommandExecutionRecord {
@TableId(type = IdType.AUTO)
private Long id;
/** 关联指令ID */
private Long commandId;
/** 关联指令编号 */
private String commandNo;
/** 执行人ID */
private Long executorId;
/** 执行人姓名 */
private String executorName;
/** 动作: RECEIVE / START / PROGRESS / COMPLETE / REJECT */
private String action;
/** 描述 */
private String description;
/** 附件(JSON array) */
private String attachments;
/** 进度 0-100 */
private Integer progress;
private LocalDateTime createdAt;
}
@@ -0,0 +1,43 @@
package com.water.dispatch.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.time.LocalDateTime;
/**
* 指令过程追踪(时间线)
*/
@Data
@TableName("disp_command_tracking")
public class CommandTracking {
@TableId(type = IdType.AUTO)
private Long id;
/** 关联指令ID */
private Long commandId;
/** 关联指令编号 */
private String commandNo;
/** 阶段: CREATED / ISSUED / RECEIVED / EXECUTING / COMPLETED / REJECTED / CANCELLED */
private String stage;
/** 操作人ID */
private Long operatorId;
/** 操作人姓名 */
private String operatorName;
/** 动作描述 */
private String actionDesc;
/** 来源状态 */
private String fromStatus;
/** 目标状态 */
private String toStatus;
private LocalDateTime createdAt;
}
@@ -6,7 +6,7 @@ import lombok.Data;
import java.time.LocalDateTime;
/**
* 调度指令 - 全生命周期: 下发→接收→执行→完成→驳回
* 调度指令 - 全生命周期: DRAFT→ISSUED→RECEIVED→EXECUTING→COMPLETED/REJECTED/CANCELLED
*/
@Data
@TableName("disp_dispatch_command")
@@ -30,6 +30,9 @@ public class DispatchCommand {
/** 优先级: LOW-低 MEDIUM-中 HIGH-高 URGENT-紧急 */
private String priority;
/** 状态: DRAFT/ISSUED/RECEIVED/EXECUTING/COMPLETED/REJECTED/CANCELLED */
private String status;
/** 下发人ID */
private Long issuerId;
@@ -45,8 +48,8 @@ public class DispatchCommand {
/** 关联设施ID */
private Long facilityId;
/** 状态: ISSUED-下发 RECEIVED-接收 EXECUTING-执行 COMPLETED-完成 REJECTED-驳回 CANCELLED-取消 */
private String status;
/** 截止时间 */
private LocalDateTime deadline;
/** 下发时间 */
private LocalDateTime issuedAt;
@@ -60,19 +63,23 @@ public class DispatchCommand {
/** 完成时间 */
private LocalDateTime completedAt;
/** 驳回时间 */
private LocalDateTime rejectedAt;
/** 驳回原因 */
private String rejectReason;
/** 执行结果 */
private String executeResult;
/** 截止时间 */
private LocalDateTime deadline;
/** 备注 */
private String remark;
@TableLogic
private Integer deleted;
private LocalDateTime createdAt;
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updatedAt;
}
@@ -0,0 +1,22 @@
package com.water.dispatch.entity.dto;
import lombok.Data;
import java.time.LocalDateTime;
/**
* 指令创建请求
*/
@Data
public class CommandCreateRequest {
private String title;
private String content;
private String commandType; // NORMAL / EMERGENCY / MAINTENANCE
private String priority; // LOW / MEDIUM / HIGH / URGENT
private Long issuerId;
private String issuerName;
private Long receiverId;
private String receiverName;
private Long facilityId;
private LocalDateTime deadline;
private String remark;
}
@@ -0,0 +1,22 @@
package com.water.dispatch.entity.dto;
import lombok.Data;
import java.time.LocalDateTime;
/**
* 指令台账查询请求
*/
@Data
public class CommandQueryRequest {
private String commandNo;
private String title;
private String commandType;
private String priority;
private String status;
private Long issuerId;
private Long receiverId;
private LocalDateTime startTime;
private LocalDateTime endTime;
private Integer pageNum = 1;
private Integer pageSize = 20;
}
@@ -0,0 +1,18 @@
package com.water.dispatch.entity.dto;
import lombok.Data;
import java.util.Map;
/**
* 指令统计VO
*/
@Data
public class CommandStatVO {
private long total;
private Map<String, Long> byStatus;
private Map<String, Long> byType;
private Map<String, Long> byPriority;
private long overdueCount;
private long todayCreated;
private long todayCompleted;
}
@@ -0,0 +1,19 @@
package com.water.dispatch.entity.dto;
import lombok.Data;
/**
* 执行反馈请求
*/
@Data
public class ExecutionRequest {
private Long commandId;
private Long executorId;
private String executorName;
private String action; // RECEIVE / START / PROGRESS / COMPLETE / REJECT
private String description;
private String attachments; // JSON array
private Integer progress;
private String rejectReason; // 驳回时填写
private String executeResult; // 完成时填写
}
@@ -0,0 +1,9 @@
package com.water.dispatch.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.water.dispatch.entity.CommandExecutionRecord;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface CommandExecutionRecordMapper extends BaseMapper<CommandExecutionRecord> {
}
@@ -0,0 +1,9 @@
package com.water.dispatch.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.water.dispatch.entity.CommandTracking;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface CommandTrackingMapper extends BaseMapper<CommandTracking> {
}
@@ -1,5 +1,98 @@
package com.water.dispatch.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.water.dispatch.entity.DispatchCommand;
import org.apache.ibatis.annotations.Mapper;
@Mapper public interface DispatchCommandMapper extends BaseMapper<DispatchCommand> {}
import org.apache.ibatis.annotations.*;
import java.util.List;
import java.util.Map;
@Mapper
public interface DispatchCommandMapper extends BaseMapper<DispatchCommand> {
/**
* 分页 + 多维度查询指令台账
*/
@SelectProvider(type = CommandSqlProvider.class, method = "buildLedgerQuery")
IPage<DispatchCommand> selectLedgerPage(
Page<?> page,
@Param("commandNo") String commandNo,
@Param("title") String title,
@Param("commandType") String commandType,
@Param("priority") String priority,
@Param("status") String status,
@Param("issuerId") Long issuerId,
@Param("receiverId") Long receiverId,
@Param("startTime") String startTime,
@Param("endTime") String endTime);
/**
* 按状态统计
*/
@Select("SELECT status, COUNT(*) as cnt FROM disp_dispatch_command WHERE deleted=0 GROUP BY status")
List<Map<String, Object>> countByStatus();
/**
* 按类型统计
*/
@Select("SELECT command_type, COUNT(*) as cnt FROM disp_dispatch_command WHERE deleted=0 GROUP BY command_type")
List<Map<String, Object>> countByType();
/**
* 按优先级统计
*/
@Select("SELECT priority, COUNT(*) as cnt FROM disp_dispatch_command WHERE deleted=0 GROUP BY priority")
List<Map<String, Object>> countByPriority();
/**
* 逾期指令数
*/
@Select("SELECT COUNT(*) FROM disp_dispatch_command WHERE deleted=0 AND status NOT IN ('COMPLETED','CANCELLED','REJECTED') AND deadline < NOW()")
long countOverdue();
class CommandSqlProvider {
public String buildLedgerQuery(
@Param("commandNo") String commandNo,
@Param("title") String title,
@Param("commandType") String commandType,
@Param("priority") String priority,
@Param("status") String status,
@Param("issuerId") Long issuerId,
@Param("receiverId") Long receiverId,
@Param("startTime") String startTime,
@Param("endTime") String endTime) {
StringBuilder sb = new StringBuilder("SELECT * FROM disp_dispatch_command WHERE deleted=0");
if (commandNo != null && !commandNo.isEmpty()) {
sb.append(" AND command_no LIKE '%").append(commandNo).append("%'");
}
if (title != null && !title.isEmpty()) {
sb.append(" AND title LIKE '%").append(title).append("%'");
}
if (commandType != null && !commandType.isEmpty()) {
sb.append(" AND command_type = '").append(commandType).append("'");
}
if (priority != null && !priority.isEmpty()) {
sb.append(" AND priority = '").append(priority).append("'");
}
if (status != null && !status.isEmpty()) {
sb.append(" AND status = '").append(status).append("'");
}
if (issuerId != null) {
sb.append(" AND issuer_id = ").append(issuerId);
}
if (receiverId != null) {
sb.append(" AND receiver_id = ").append(receiverId);
}
if (startTime != null && !startTime.isEmpty()) {
sb.append(" AND created_at >= '").append(startTime).append("'");
}
if (endTime != null && !endTime.isEmpty()) {
sb.append(" AND created_at <= '").append(endTime).append("'");
}
sb.append(" ORDER BY created_at DESC");
return sb.toString();
}
}
}
@@ -0,0 +1,125 @@
package com.water.dispatch.service;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.water.dispatch.entity.DispatchCommand;
import com.water.dispatch.entity.dto.CommandQueryRequest;
import com.water.dispatch.entity.dto.CommandStatVO;
import com.water.dispatch.mapper.DispatchCommandMapper;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 指令台账服务 - 多维度查询/统计/导出
*/
@Service
@RequiredArgsConstructor
public class CommandLedgerService {
private final DispatchCommandMapper commandMapper;
/**
* 分页多维度查询
*/
public IPage<DispatchCommand> queryPage(CommandQueryRequest req) {
Page<DispatchCommand> page = new Page<>(req.getPageNum(), req.getPageSize());
String startTime = req.getStartTime() != null ? req.getStartTime().toString() : null;
String endTime = req.getEndTime() != null ? req.getEndTime().toString() : null;
return commandMapper.selectLedgerPage(page,
req.getCommandNo(), req.getTitle(), req.getCommandType(),
req.getPriority(), req.getStatus(),
req.getIssuerId(), req.getReceiverId(),
startTime, endTime);
}
/**
* 列表查询(不分页)
*/
public List<DispatchCommand> queryList(CommandQueryRequest req) {
LambdaQueryWrapper<DispatchCommand> wrapper = new LambdaQueryWrapper<>();
if (req.getCommandNo() != null && !req.getCommandNo().isEmpty()) {
wrapper.like(DispatchCommand::getCommandNo, req.getCommandNo());
}
if (req.getTitle() != null && !req.getTitle().isEmpty()) {
wrapper.like(DispatchCommand::getTitle, req.getTitle());
}
if (req.getCommandType() != null && !req.getCommandType().isEmpty()) {
wrapper.eq(DispatchCommand::getCommandType, req.getCommandType());
}
if (req.getPriority() != null && !req.getPriority().isEmpty()) {
wrapper.eq(DispatchCommand::getPriority, req.getPriority());
}
if (req.getStatus() != null && !req.getStatus().isEmpty()) {
wrapper.eq(DispatchCommand::getStatus, req.getStatus());
}
if (req.getIssuerId() != null) {
wrapper.eq(DispatchCommand::getIssuerId, req.getIssuerId());
}
if (req.getReceiverId() != null) {
wrapper.eq(DispatchCommand::getReceiverId, req.getReceiverId());
}
if (req.getStartTime() != null) {
wrapper.ge(DispatchCommand::getCreatedAt, req.getStartTime());
}
if (req.getEndTime() != null) {
wrapper.le(DispatchCommand::getCreatedAt, req.getEndTime());
}
wrapper.orderByDesc(DispatchCommand::getCreatedAt);
return commandMapper.selectList(wrapper);
}
/**
* 统计概览
*/
public CommandStatVO statistics() {
CommandStatVO vo = new CommandStatVO();
vo.setTotal(commandMapper.selectCount(new LambdaQueryWrapper<>()));
Map<String, Long> byStatus = new HashMap<>();
commandMapper.countByStatus().forEach(m ->
byStatus.put(String.valueOf(m.get("status")), ((Number) m.get("cnt")).longValue()));
vo.setByStatus(byStatus);
Map<String, Long> byType = new HashMap<>();
commandMapper.countByType().forEach(m ->
byType.put(String.valueOf(m.get("command_type")), ((Number) m.get("cnt")).longValue()));
vo.setByType(byType);
Map<String, Long> byPriority = new HashMap<>();
commandMapper.countByPriority().forEach(m ->
byPriority.put(String.valueOf(m.get("priority")), ((Number) m.get("cnt")).longValue()));
vo.setByPriority(byPriority);
vo.setOverdueCount(commandMapper.countOverdue());
LocalDateTime todayStart = LocalDateTime.of(LocalDate.now(), LocalTime.MIN);
LocalDateTime todayEnd = LocalDateTime.of(LocalDate.now(), LocalTime.MAX);
vo.setTodayCreated(commandMapper.selectCount(
new LambdaQueryWrapper<DispatchCommand>()
.ge(DispatchCommand::getCreatedAt, todayStart)
.le(DispatchCommand::getCreatedAt, todayEnd)));
vo.setTodayCompleted(commandMapper.selectCount(
new LambdaQueryWrapper<DispatchCommand>()
.eq(DispatchCommand::getStatus, "COMPLETED")
.ge(DispatchCommand::getCompletedAt, todayStart)
.le(DispatchCommand::getCompletedAt, todayEnd)));
return vo;
}
/**
* 导出用数据(全量列表)
*/
public List<DispatchCommand> exportData(CommandQueryRequest req) {
return queryList(req);
}
}
@@ -0,0 +1,250 @@
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.entity.dto.ExecutionRequest;
import com.water.dispatch.mapper.CommandExecutionRecordMapper;
import com.water.dispatch.mapper.CommandTrackingMapper;
import com.water.dispatch.mapper.DispatchCommandMapper;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
/**
* 调度指令全生命周期服务
* DRAFT → ISSUED → RECEIVED → EXECUTING → COMPLETED / REJECTED / CANCELLED
*/
@Service
@RequiredArgsConstructor
public class CommandLifecycleService {
private final DispatchCommandMapper commandMapper;
private final CommandExecutionRecordMapper executionMapper;
private final CommandTrackingMapper trackingMapper;
/**
* 创建指令(DRAFT)
*/
@Transactional
public DispatchCommand create(CommandCreateRequest req) {
DispatchCommand cmd = new DispatchCommand();
cmd.setCommandNo(generateCommandNo());
cmd.setTitle(req.getTitle());
cmd.setContent(req.getContent());
cmd.setCommandType(req.getCommandType() != null ? req.getCommandType() : "NORMAL");
cmd.setPriority(req.getPriority() != null ? req.getPriority() : "MEDIUM");
cmd.setStatus("DRAFT");
cmd.setIssuerId(req.getIssuerId());
cmd.setIssuerName(req.getIssuerName());
cmd.setReceiverId(req.getReceiverId());
cmd.setReceiverName(req.getReceiverName());
cmd.setFacilityId(req.getFacilityId());
cmd.setDeadline(req.getDeadline());
cmd.setRemark(req.getRemark());
commandMapper.insert(cmd);
addTracking(cmd, "CREATED", null, "DRAFT", req.getIssuerId(), req.getIssuerName(), "创建指令");
return cmd;
}
/**
* 下发指令(DRAFT → ISSUED)
*/
@Transactional
public DispatchCommand issue(Long commandId) {
DispatchCommand cmd = getCommandOrThrow(commandId);
assertStatus(cmd, "DRAFT");
cmd.setStatus("ISSUED");
cmd.setIssuedAt(LocalDateTime.now());
commandMapper.updateById(cmd);
addTracking(cmd, "ISSUED", "DRAFT", "ISSUED", cmd.getIssuerId(), cmd.getIssuerName(), "下发指令");
return cmd;
}
/**
* 接收确认(ISSUED → RECEIVED)
*/
@Transactional
public DispatchCommand receive(Long commandId, Long receiverId, String receiverName) {
DispatchCommand cmd = getCommandOrThrow(commandId);
assertStatus(cmd, "ISSUED");
cmd.setStatus("RECEIVED");
cmd.setReceivedAt(LocalDateTime.now());
if (receiverId != null) {
cmd.setReceiverId(receiverId);
cmd.setReceiverName(receiverName);
}
commandMapper.updateById(cmd);
addExecution(cmd, receiverId, receiverName, "RECEIVE", "确认接收指令", null, 0);
addTracking(cmd, "RECEIVED", "ISSUED", "RECEIVED", receiverId, receiverName, "接收确认");
return cmd;
}
/**
* 开始执行(RECEIVED → EXECUTING)
*/
@Transactional
public DispatchCommand startExecution(Long commandId, Long executorId, String executorName) {
DispatchCommand cmd = getCommandOrThrow(commandId);
assertStatus(cmd, "RECEIVED");
cmd.setStatus("EXECUTING");
cmd.setExecutedAt(LocalDateTime.now());
commandMapper.updateById(cmd);
addExecution(cmd, executorId, executorName, "START", "开始执行", null, 10);
addTracking(cmd, "EXECUTING", "RECEIVED", "EXECUTING", executorId, executorName, "开始执行");
return cmd;
}
/**
* 执行进度反馈(EXECUTING stays)
*/
@Transactional
public DispatchCommand reportProgress(Long commandId, Long executorId, String executorName,
String description, int progress) {
DispatchCommand cmd = getCommandOrThrow(commandId);
assertStatus(cmd, "EXECUTING");
addExecution(cmd, executorId, executorName, "PROGRESS", description, null, progress);
return cmd;
}
/**
* 完成指令(EXECUTING → COMPLETED)
*/
@Transactional
public DispatchCommand complete(Long commandId, Long executorId, String executorName, String result) {
DispatchCommand cmd = getCommandOrThrow(commandId);
assertStatus(cmd, "EXECUTING");
cmd.setStatus("COMPLETED");
cmd.setCompletedAt(LocalDateTime.now());
cmd.setExecuteResult(result);
commandMapper.updateById(cmd);
addExecution(cmd, executorId, executorName, "COMPLETE", result, null, 100);
addTracking(cmd, "COMPLETED", "EXECUTING", "COMPLETED", executorId, executorName, "执行完成: " + result);
return cmd;
}
/**
* 驳回指令(任意活跃状态 → REJECTED)
*/
@Transactional
public DispatchCommand reject(Long commandId, Long operatorId, String operatorName, String reason) {
DispatchCommand cmd = getCommandOrThrow(commandId);
String fromStatus = cmd.getStatus();
if ("COMPLETED".equals(fromStatus) || "CANCELLED".equals(fromStatus) || "REJECTED".equals(fromStatus)) {
throw new BusinessException("当前状态不允许驳回: " + fromStatus);
}
cmd.setStatus("REJECTED");
cmd.setRejectedAt(LocalDateTime.now());
cmd.setRejectReason(reason);
commandMapper.updateById(cmd);
addExecution(cmd, operatorId, operatorName, "REJECT", reason, null, 0);
addTracking(cmd, "REJECTED", fromStatus, "REJECTED", operatorId, operatorName, "驳回: " + reason);
return cmd;
}
/**
* 取消指令
*/
@Transactional
public DispatchCommand cancel(Long commandId, Long operatorId, String operatorName, String reason) {
DispatchCommand cmd = getCommandOrThrow(commandId);
String fromStatus = cmd.getStatus();
if ("COMPLETED".equals(fromStatus) || "CANCELLED".equals(fromStatus)) {
throw new BusinessException("当前状态不允许取消: " + fromStatus);
}
cmd.setStatus("CANCELLED");
commandMapper.updateById(cmd);
addTracking(cmd, "CANCELLED", fromStatus, "CANCELLED", operatorId, operatorName,
"取消指令" + (reason != null ? ": " + reason : ""));
return cmd;
}
/**
* 获取指令详情
*/
public DispatchCommand getById(Long id) {
return getCommandOrThrow(id);
}
/**
* 根据编号获取
*/
public DispatchCommand getByCommandNo(String commandNo) {
DispatchCommand cmd = commandMapper.selectOne(
new LambdaQueryWrapper<DispatchCommand>()
.eq(DispatchCommand::getCommandNo, commandNo));
if (cmd == null) {
throw new BusinessException("指令不存在: " + commandNo);
}
return cmd;
}
// ========== Internal ==========
private DispatchCommand getCommandOrThrow(Long id) {
DispatchCommand cmd = commandMapper.selectById(id);
if (cmd == null) {
throw new BusinessException("指令不存在: " + id);
}
return cmd;
}
private void assertStatus(DispatchCommand cmd, String expected) {
if (!expected.equals(cmd.getStatus())) {
throw new BusinessException("指令状态不正确,期望: " + expected + ",实际: " + cmd.getStatus());
}
}
private String generateCommandNo() {
return "CMD-" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss"))
+ "-" + (int) (Math.random() * 9000 + 1000);
}
private void addTracking(DispatchCommand cmd, String stage, String fromStatus, String toStatus,
Long operatorId, String operatorName, String desc) {
CommandTracking t = new CommandTracking();
t.setCommandId(cmd.getId());
t.setCommandNo(cmd.getCommandNo());
t.setStage(stage);
t.setFromStatus(fromStatus);
t.setToStatus(toStatus);
t.setOperatorId(operatorId);
t.setOperatorName(operatorName);
t.setActionDesc(desc);
trackingMapper.insert(t);
}
private void addExecution(DispatchCommand cmd, Long executorId, String executorName,
String action, String description, String attachments, int progress) {
CommandExecutionRecord r = new CommandExecutionRecord();
r.setCommandId(cmd.getId());
r.setCommandNo(cmd.getCommandNo());
r.setExecutorId(executorId);
r.setExecutorName(executorName);
r.setAction(action);
r.setDescription(description);
r.setAttachments(attachments);
r.setProgress(progress);
executionMapper.insert(r);
}
}
@@ -0,0 +1,94 @@
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 lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.util.*;
/**
* 指令全过程追踪服务 - 时间线
*/
@Service
@RequiredArgsConstructor
public class CommandTrackingService {
private final CommandTrackingMapper trackingMapper;
private final CommandExecutionRecordMapper executionMapper;
/**
* 获取指令追踪时间线
*/
public List<CommandTracking> getTimeline(Long commandId) {
return trackingMapper.selectList(
new LambdaQueryWrapper<CommandTracking>()
.eq(CommandTracking::getCommandId, commandId)
.orderByAsc(CommandTracking::getCreatedAt));
}
/**
* 根据指令编号获取追踪时间线
*/
public List<CommandTracking> getTimelineByCommandNo(String commandNo) {
return trackingMapper.selectList(
new LambdaQueryWrapper<CommandTracking>()
.eq(CommandTracking::getCommandNo, commandNo)
.orderByAsc(CommandTracking::getCreatedAt));
}
/**
* 获取执行记录列表
*/
public List<CommandExecutionRecord> getExecutionRecords(Long commandId) {
return executionMapper.selectList(
new LambdaQueryWrapper<CommandExecutionRecord>()
.eq(CommandExecutionRecord::getCommandId, commandId)
.orderByAsc(CommandExecutionRecord::getCreatedAt));
}
/**
* 获取完整时间线(tracking + execution records 合并)
*/
public List<Map<String, Object>> getFullTimeline(Long commandId) {
List<CommandTracking> trackings = getTimeline(commandId);
List<CommandExecutionRecord> records = getExecutionRecords(commandId);
List<Map<String, Object>> timeline = new ArrayList<>();
for (CommandTracking t : trackings) {
Map<String, Object> item = new LinkedHashMap<>();
item.put("type", "TRACKING");
item.put("id", t.getId());
item.put("time", t.getCreatedAt());
item.put("stage", t.getStage());
item.put("operatorId", t.getOperatorId());
item.put("operatorName", t.getOperatorName());
item.put("description", t.getActionDesc());
item.put("fromStatus", t.getFromStatus());
item.put("toStatus", t.getToStatus());
timeline.add(item);
}
for (CommandExecutionRecord r : records) {
Map<String, Object> item = new LinkedHashMap<>();
item.put("type", "EXECUTION");
item.put("id", r.getId());
item.put("time", r.getCreatedAt());
item.put("action", r.getAction());
item.put("executorId", r.getExecutorId());
item.put("executorName", r.getExecutorName());
item.put("description", r.getDescription());
item.put("progress", r.getProgress());
item.put("attachments", r.getAttachments());
timeline.add(item);
}
// Sort by time
timeline.sort(Comparator.comparing(m -> (java.time.LocalDateTime) m.get("time")));
return timeline;
}
}
@@ -1,11 +1,19 @@
package com.water.dispatch.service;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.water.dispatch.entity.*;
import com.water.dispatch.mapper.*;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.time.*; import java.util.*;
@Service @RequiredArgsConstructor
import java.time.*;
import java.util.*;
/**
* 调度工作台 - 原有业务(值班/工单/策略/预案)
*/
@Service
@RequiredArgsConstructor
public class DispatchBizService {
private final DutyScheduleMapper dutyMapper;
private final DispatchCommandMapper cmdMapper;
@@ -16,66 +24,84 @@ public class DispatchBizService {
public List<DutySchedule> getTodayDuty() {
return dutyMapper.selectList(new LambdaQueryWrapper<DutySchedule>()
.eq(DutySchedule::getDutyDate, LocalDate.now()));
.eq(DutySchedule::getDutyDate, LocalDate.now()));
}
public Map<String,Object> createCommand(Map<String,Object> req) {
public Map<String, Object> createCommand(Map<String, Object> req) {
DispatchCommand c = new DispatchCommand();
c.setCmdNo("CMD-" + System.currentTimeMillis());
c.setTitle((String)req.get("title")); c.setContent((String)req.get("content"));
c.setType((String)req.getOrDefault("type","常规")); c.setStatus(0);
c.setCommandNo("CMD-" + System.currentTimeMillis());
c.setTitle((String) req.get("title"));
c.setContent((String) req.get("content"));
c.setCommandType((String) req.getOrDefault("commandType", "NORMAL"));
c.setPriority((String) req.getOrDefault("priority", "MEDIUM"));
c.setStatus("DRAFT");
cmdMapper.insert(c);
return Map.of("id",c.getId(),"cmdNo",c.getCmdNo());
return Map.of("id", c.getId(), "commandNo", c.getCommandNo());
}
public Map<String,Object> issueCommand(String cmdNo) {
public Map<String, Object> issueCommand(String commandNo) {
DispatchCommand c = cmdMapper.selectOne(new LambdaQueryWrapper<DispatchCommand>()
.eq(DispatchCommand::getCmdNo, cmdNo));
if(c==null) throw new RuntimeException("指令不存在");
c.setStatus(1); c.setIssuedTime(LocalDateTime.now());
.eq(DispatchCommand::getCommandNo, commandNo));
if (c == null) throw new RuntimeException("指令不存在");
c.setStatus("ISSUED");
c.setIssuedAt(LocalDateTime.now());
cmdMapper.updateById(c);
return Map.of("cmdNo",cmdNo,"status",1);
return Map.of("commandNo", commandNo, "status", c.getStatus());
}
public List<DispatchCommand> listCommands(Integer status) {
public List<DispatchCommand> listCommands(String status) {
return cmdMapper.selectList(new LambdaQueryWrapper<DispatchCommand>()
.eq(status!=null, DispatchCommand::getStatus, status));
.eq(status != null, DispatchCommand::getStatus, status));
}
public Map<String,Object> createWorkOrder(Map<String,Object> req) {
public Map<String, Object> createWorkOrder(Map<String, Object> req) {
WorkOrder w = new WorkOrder();
w.setOrderNo("WO-" + System.currentTimeMillis());
w.setTitle((String)req.get("title")); w.setDescription((String)req.get("description"));
w.setType((String)req.getOrDefault("type","维修")); w.setStatus(0);
w.setPriority((String)req.getOrDefault("priority","中"));
w.setTitle((String) req.get("title"));
w.setDescription((String) req.get("description"));
w.setType((String) req.getOrDefault("type", "维修"));
w.setStatus(0);
w.setPriority((String) req.getOrDefault("priority", "中"));
woMapper.insert(w);
return Map.of("id",w.getId(),"orderNo",w.getOrderNo());
return Map.of("id", w.getId(), "orderNo", w.getOrderNo());
}
public Map<String,Object> updateWorkOrderStatus(Long id, int status) {
public Map<String, Object> updateWorkOrderStatus(Long id, int status) {
WorkOrder w = woMapper.selectById(id);
if(w==null) throw new RuntimeException("工单不存在");
if (w == null) throw new RuntimeException("工单不存在");
w.setStatus(status);
if(status==2) w.setCompletedAt(LocalDateTime.now());
if (status == 2) w.setCompletedAt(LocalDateTime.now());
woMapper.updateById(w);
return Map.of("id",id,"status",status);
return Map.of("id", id, "status", status);
}
public void addDutyLog(Long scheduleId, Long userId, String type, String content) {
DutyLog l = new DutyLog();
l.setScheduleId(scheduleId); l.setUserId(userId);
l.setLogType(type); l.setContent(content);
l.setScheduleId(scheduleId);
l.setUserId(userId);
l.setLogType(type);
l.setContent(content);
logMapper.insert(l);
}
public List<DutyLog> getDutyLogs(Long scheduleId) {
return logMapper.selectList(new LambdaQueryWrapper<DutyLog>()
.eq(DutyLog::getScheduleId, scheduleId));
.eq(DutyLog::getScheduleId, scheduleId));
}
public List<DispatchStrategy> listStrategies(String type) {
return stratMapper.selectList(new LambdaQueryWrapper<DispatchStrategy>()
.eq(type!=null, DispatchStrategy::getType, type));
.eq(type != null, DispatchStrategy::getType, type));
}
public List<EmergencyPlan> listPlans(String type) {
return planMapper.selectList(new LambdaQueryWrapper<EmergencyPlan>()
.eq(type!=null, EmergencyPlan::getType, type));
.eq(type != null, EmergencyPlan::getType, type));
}
public Map<String,Object> simulateEmergency(String type, double lng, double lat) {
return Map.of("type",type,"lng",lng,"lat",lat,
"affectedArea","半径500米","affectedUsers",120,
"estimatedDuration","4小时");
public Map<String, Object> simulateEmergency(String type, double lng, double lat) {
return Map.of("type", type, "lng", lng, "lat", lat,
"affectedArea", "半径500米", "affectedUsers", 120,
"estimatedDuration", "4小时");
}
}
@@ -0,0 +1,73 @@
-- V2: 调度指令全生命周期管理
-- 新增/重建表,支持 创建→下发→接收确认→执行反馈→完成归档 全流程
-- 1. 调度指令主表(增强)
CREATE TABLE IF NOT EXISTS disp_dispatch_command (
id BIGSERIAL PRIMARY KEY,
command_no VARCHAR(64) NOT NULL UNIQUE,
title VARCHAR(200) NOT NULL,
content TEXT,
command_type VARCHAR(30) NOT NULL DEFAULT 'NORMAL', -- NORMAL / EMERGENCY / MAINTENANCE
priority VARCHAR(20) NOT NULL DEFAULT 'MEDIUM', -- LOW / MEDIUM / HIGH / URGENT
status VARCHAR(30) NOT NULL DEFAULT 'DRAFT', -- DRAFT / ISSUED / RECEIVED / EXECUTING / COMPLETED / REJECTED / CANCELLED
issuer_id BIGINT,
issuer_name VARCHAR(64),
receiver_id BIGINT,
receiver_name VARCHAR(64),
facility_id BIGINT,
deadline TIMESTAMP,
issued_at TIMESTAMP,
received_at TIMESTAMP,
executed_at TIMESTAMP,
completed_at TIMESTAMP,
rejected_at TIMESTAMP,
reject_reason TEXT,
execute_result TEXT,
remark TEXT,
deleted INT DEFAULT 0,
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);
CREATE INDEX idx_cmd_status ON disp_dispatch_command(status);
CREATE INDEX idx_cmd_type ON disp_dispatch_command(command_type);
CREATE INDEX idx_cmd_priority ON disp_dispatch_command(priority);
CREATE INDEX idx_cmd_issuer ON disp_dispatch_command(issuer_id);
CREATE INDEX idx_cmd_receiver ON disp_dispatch_command(receiver_id);
CREATE INDEX idx_cmd_deadline ON disp_dispatch_command(deadline);
CREATE INDEX idx_cmd_created_at ON disp_dispatch_command(created_at);
-- 2. 执行记录表
CREATE TABLE IF NOT EXISTS disp_command_execution_record (
id BIGSERIAL PRIMARY KEY,
command_id BIGINT NOT NULL,
command_no VARCHAR(64),
executor_id BIGINT NOT NULL,
executor_name VARCHAR(64),
action VARCHAR(30) NOT NULL, -- RECEIVE / START / PROGRESS / COMPLETE / REJECT
description TEXT,
attachments TEXT, -- JSON array of file URLs
progress INT DEFAULT 0, -- 0-100
created_at TIMESTAMP DEFAULT NOW()
);
CREATE INDEX idx_exec_cmd_id ON disp_command_execution_record(command_id);
CREATE INDEX idx_exec_cmd_no ON disp_command_execution_record(command_no);
-- 3. 过程追踪表(时间线)
CREATE TABLE IF NOT EXISTS disp_command_tracking (
id BIGSERIAL PRIMARY KEY,
command_id BIGINT NOT NULL,
command_no VARCHAR(64),
stage VARCHAR(30) NOT NULL, -- CREATED / ISSUED / RECEIVED / EXECUTING / COMPLETED / REJECTED / CANCELLED
operator_id BIGINT,
operator_name VARCHAR(64),
action_desc VARCHAR(500),
from_status VARCHAR(30),
to_status VARCHAR(30),
created_at TIMESTAMP DEFAULT NOW()
);
CREATE INDEX idx_track_cmd_id ON disp_command_tracking(command_id);
CREATE INDEX idx_track_cmd_no ON disp_command_tracking(command_no);
CREATE INDEX idx_track_stage ON disp_command_tracking(stage);
@@ -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());
}
}