diff --git a/wm-dispatch/src/main/java/com/water/dispatch/controller/DispatchCommandController.java b/wm-dispatch/src/main/java/com/water/dispatch/controller/DispatchCommandController.java new file mode 100644 index 00000000..1b234b6c --- /dev/null +++ b/wm-dispatch/src/main/java/com/water/dispatch/controller/DispatchCommandController.java @@ -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 create(@RequestBody CommandCreateRequest req) { + return R.ok(lifecycleService.create(req)); + } + + @Operation(summary = "2. 下发指令") + @PostMapping("/{id}/issue") + public R issue(@PathVariable Long id) { + return R.ok(lifecycleService.issue(id)); + } + + @Operation(summary = "3. 接收确认") + @PostMapping("/{id}/receive") + public R 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 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 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 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 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 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 getById(@PathVariable Long id) { + return R.ok(lifecycleService.getById(id)); + } + + @Operation(summary = "10. 根据指令编号获取详情") + @GetMapping("/no/{commandNo}") + public R getByCommandNo(@PathVariable String commandNo) { + return R.ok(lifecycleService.getByCommandNo(commandNo)); + } + + // ==================== 台账 ==================== + + @Operation(summary = "11. 指令台账分页查询") + @GetMapping("/page") + public R> queryPage(CommandQueryRequest req) { + return R.ok(ledgerService.queryPage(req)); + } + + @Operation(summary = "12. 指令台账列表查询") + @GetMapping("/list") + public R> queryList(CommandQueryRequest req) { + return R.ok(ledgerService.queryList(req)); + } + + @Operation(summary = "13. 指令统计概览") + @GetMapping("/statistics") + public R statistics() { + return R.ok(ledgerService.statistics()); + } + + @Operation(summary = "14. 指令数据导出") + @GetMapping("/export") + public R> exportData(CommandQueryRequest req) { + return R.ok(ledgerService.exportData(req)); + } + + // ==================== 追踪 ==================== + + @Operation(summary = "15. 获取指令追踪时间线") + @GetMapping("/{id}/timeline") + public R> getTimeline(@PathVariable Long id) { + return R.ok(trackingService.getTimeline(id)); + } + + @Operation(summary = "16. 获取执行记录") + @GetMapping("/{id}/executions") + public R> getExecutions(@PathVariable Long id) { + return R.ok(trackingService.getExecutionRecords(id)); + } + + @Operation(summary = "17. 获取完整时间线(追踪+执行记录合并)") + @GetMapping("/{id}/full-timeline") + public R>> getFullTimeline(@PathVariable Long id) { + return R.ok(trackingService.getFullTimeline(id)); + } + + @Operation(summary = "18. 按指令编号获取追踪时间线") + @GetMapping("/no/{commandNo}/timeline") + public R> getTimelineByCommandNo(@PathVariable String commandNo) { + return R.ok(trackingService.getTimelineByCommandNo(commandNo)); + } +} diff --git a/wm-dispatch/src/main/java/com/water/dispatch/controller/DispatchController.java b/wm-dispatch/src/main/java/com/water/dispatch/controller/DispatchController.java index 169ab1f1..f7313aed 100644 --- a/wm-dispatch/src/main/java/com/water/dispatch/controller/DispatchController.java +++ b/wm-dispatch/src/main/java/com/water/dispatch/controller/DispatchController.java @@ -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> todayDuty() { return R.ok(svc.getTodayDuty()); } - @PostMapping("/command") public R> createCommand(@RequestBody Map req) { return R.ok(svc.createCommand(req)); } - @PostMapping("/command/{cmdNo}/issue") public R> issue(@PathVariable String cmdNo) { return R.ok(svc.issueCommand(cmdNo)); } - @GetMapping("/command/list") public R> listCommands(@RequestParam(required=false) Integer status) { return R.ok(svc.listCommands(status)); } - @PostMapping("/work-order") public R> createWO(@RequestBody Map req) { return R.ok(svc.createWorkOrder(req)); } - @PutMapping("/work-order/{id}/status") public R> updateWOStatus(@PathVariable Long id, @RequestParam int status) { return R.ok(svc.updateWorkOrderStatus(id, status)); } - @PostMapping("/duty-log") public R 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> getLogs(@PathVariable Long scheduleId) { return R.ok(svc.getDutyLogs(scheduleId)); } - @GetMapping("/strategy") public R> listStrategies(@RequestParam(required=false) String type) { return R.ok(svc.listStrategies(type)); } - @GetMapping("/emergency-plan") public R> listPlans(@RequestParam(required=false) String type) { return R.ok(svc.listPlans(type)); } - @PostMapping("/emergency/simulate") public R> simulate(@RequestParam String type, @RequestParam double lng, @RequestParam double lat) { return R.ok(svc.simulateEmergency(type,lng,lat)); } + + @GetMapping("/duty/today") + public R> todayDuty() { + return R.ok(svc.getTodayDuty()); + } + + @PostMapping("/command") + public R> createCommand(@RequestBody Map req) { + return R.ok(svc.createCommand(req)); + } + + @PostMapping("/command/{cmdNo}/issue") + public R> issue(@PathVariable String cmdNo) { + return R.ok(svc.issueCommand(cmdNo)); + } + + @GetMapping("/command/list") + public R> listCommands(@RequestParam(required = false) String status) { + return R.ok(svc.listCommands(status)); + } + + @PostMapping("/work-order") + public R> createWO(@RequestBody Map req) { + return R.ok(svc.createWorkOrder(req)); + } + + @PutMapping("/work-order/{id}/status") + public R> updateWOStatus(@PathVariable Long id, @RequestParam int status) { + return R.ok(svc.updateWorkOrderStatus(id, status)); + } + + @PostMapping("/duty-log") + public R 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> getLogs(@PathVariable Long scheduleId) { + return R.ok(svc.getDutyLogs(scheduleId)); + } + + @GetMapping("/strategy") + public R> listStrategies(@RequestParam(required = false) String type) { + return R.ok(svc.listStrategies(type)); + } + + @GetMapping("/emergency-plan") + public R> listPlans(@RequestParam(required = false) String type) { + return R.ok(svc.listPlans(type)); + } + + @PostMapping("/emergency/simulate") + public R> simulate(@RequestParam String type, + @RequestParam double lng, @RequestParam double lat) { + return R.ok(svc.simulateEmergency(type, lng, lat)); + } } diff --git a/wm-dispatch/src/main/java/com/water/dispatch/entity/CommandExecutionRecord.java b/wm-dispatch/src/main/java/com/water/dispatch/entity/CommandExecutionRecord.java new file mode 100644 index 00000000..aac59672 --- /dev/null +++ b/wm-dispatch/src/main/java/com/water/dispatch/entity/CommandExecutionRecord.java @@ -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; +} diff --git a/wm-dispatch/src/main/java/com/water/dispatch/entity/CommandTracking.java b/wm-dispatch/src/main/java/com/water/dispatch/entity/CommandTracking.java new file mode 100644 index 00000000..9d82b26c --- /dev/null +++ b/wm-dispatch/src/main/java/com/water/dispatch/entity/CommandTracking.java @@ -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; +} diff --git a/wm-dispatch/src/main/java/com/water/dispatch/entity/DispatchCommand.java b/wm-dispatch/src/main/java/com/water/dispatch/entity/DispatchCommand.java index 2a333701..c69b524d 100644 --- a/wm-dispatch/src/main/java/com/water/dispatch/entity/DispatchCommand.java +++ b/wm-dispatch/src/main/java/com/water/dispatch/entity/DispatchCommand.java @@ -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; } diff --git a/wm-dispatch/src/main/java/com/water/dispatch/entity/dto/CommandCreateRequest.java b/wm-dispatch/src/main/java/com/water/dispatch/entity/dto/CommandCreateRequest.java new file mode 100644 index 00000000..255429ac --- /dev/null +++ b/wm-dispatch/src/main/java/com/water/dispatch/entity/dto/CommandCreateRequest.java @@ -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; +} diff --git a/wm-dispatch/src/main/java/com/water/dispatch/entity/dto/CommandQueryRequest.java b/wm-dispatch/src/main/java/com/water/dispatch/entity/dto/CommandQueryRequest.java new file mode 100644 index 00000000..fe37a3c1 --- /dev/null +++ b/wm-dispatch/src/main/java/com/water/dispatch/entity/dto/CommandQueryRequest.java @@ -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; +} diff --git a/wm-dispatch/src/main/java/com/water/dispatch/entity/dto/CommandStatVO.java b/wm-dispatch/src/main/java/com/water/dispatch/entity/dto/CommandStatVO.java new file mode 100644 index 00000000..f6203a8c --- /dev/null +++ b/wm-dispatch/src/main/java/com/water/dispatch/entity/dto/CommandStatVO.java @@ -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 byStatus; + private Map byType; + private Map byPriority; + private long overdueCount; + private long todayCreated; + private long todayCompleted; +} diff --git a/wm-dispatch/src/main/java/com/water/dispatch/entity/dto/ExecutionRequest.java b/wm-dispatch/src/main/java/com/water/dispatch/entity/dto/ExecutionRequest.java new file mode 100644 index 00000000..188cada2 --- /dev/null +++ b/wm-dispatch/src/main/java/com/water/dispatch/entity/dto/ExecutionRequest.java @@ -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; // 完成时填写 +} diff --git a/wm-dispatch/src/main/java/com/water/dispatch/mapper/CommandExecutionRecordMapper.java b/wm-dispatch/src/main/java/com/water/dispatch/mapper/CommandExecutionRecordMapper.java new file mode 100644 index 00000000..dcc4112b --- /dev/null +++ b/wm-dispatch/src/main/java/com/water/dispatch/mapper/CommandExecutionRecordMapper.java @@ -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 { +} diff --git a/wm-dispatch/src/main/java/com/water/dispatch/mapper/CommandTrackingMapper.java b/wm-dispatch/src/main/java/com/water/dispatch/mapper/CommandTrackingMapper.java new file mode 100644 index 00000000..3911bdd1 --- /dev/null +++ b/wm-dispatch/src/main/java/com/water/dispatch/mapper/CommandTrackingMapper.java @@ -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 { +} diff --git a/wm-dispatch/src/main/java/com/water/dispatch/mapper/DispatchCommandMapper.java b/wm-dispatch/src/main/java/com/water/dispatch/mapper/DispatchCommandMapper.java index edb09f96..db1f8849 100644 --- a/wm-dispatch/src/main/java/com/water/dispatch/mapper/DispatchCommandMapper.java +++ b/wm-dispatch/src/main/java/com/water/dispatch/mapper/DispatchCommandMapper.java @@ -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 {} +import org.apache.ibatis.annotations.*; + +import java.util.List; +import java.util.Map; + +@Mapper +public interface DispatchCommandMapper extends BaseMapper { + + /** + * 分页 + 多维度查询指令台账 + */ + @SelectProvider(type = CommandSqlProvider.class, method = "buildLedgerQuery") + IPage 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> countByStatus(); + + /** + * 按类型统计 + */ + @Select("SELECT command_type, COUNT(*) as cnt FROM disp_dispatch_command WHERE deleted=0 GROUP BY command_type") + List> countByType(); + + /** + * 按优先级统计 + */ + @Select("SELECT priority, COUNT(*) as cnt FROM disp_dispatch_command WHERE deleted=0 GROUP BY priority") + List> 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(); + } + } +} diff --git a/wm-dispatch/src/main/java/com/water/dispatch/service/CommandLedgerService.java b/wm-dispatch/src/main/java/com/water/dispatch/service/CommandLedgerService.java new file mode 100644 index 00000000..c734a9fb --- /dev/null +++ b/wm-dispatch/src/main/java/com/water/dispatch/service/CommandLedgerService.java @@ -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 queryPage(CommandQueryRequest req) { + Page 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 queryList(CommandQueryRequest req) { + LambdaQueryWrapper 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 byStatus = new HashMap<>(); + commandMapper.countByStatus().forEach(m -> + byStatus.put(String.valueOf(m.get("status")), ((Number) m.get("cnt")).longValue())); + vo.setByStatus(byStatus); + + Map byType = new HashMap<>(); + commandMapper.countByType().forEach(m -> + byType.put(String.valueOf(m.get("command_type")), ((Number) m.get("cnt")).longValue())); + vo.setByType(byType); + + Map 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() + .ge(DispatchCommand::getCreatedAt, todayStart) + .le(DispatchCommand::getCreatedAt, todayEnd))); + vo.setTodayCompleted(commandMapper.selectCount( + new LambdaQueryWrapper() + .eq(DispatchCommand::getStatus, "COMPLETED") + .ge(DispatchCommand::getCompletedAt, todayStart) + .le(DispatchCommand::getCompletedAt, todayEnd))); + + return vo; + } + + /** + * 导出用数据(全量列表) + */ + public List exportData(CommandQueryRequest req) { + return queryList(req); + } +} diff --git a/wm-dispatch/src/main/java/com/water/dispatch/service/CommandLifecycleService.java b/wm-dispatch/src/main/java/com/water/dispatch/service/CommandLifecycleService.java new file mode 100644 index 00000000..24a2ce16 --- /dev/null +++ b/wm-dispatch/src/main/java/com/water/dispatch/service/CommandLifecycleService.java @@ -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() + .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); + } +} diff --git a/wm-dispatch/src/main/java/com/water/dispatch/service/CommandTrackingService.java b/wm-dispatch/src/main/java/com/water/dispatch/service/CommandTrackingService.java new file mode 100644 index 00000000..952b57f4 --- /dev/null +++ b/wm-dispatch/src/main/java/com/water/dispatch/service/CommandTrackingService.java @@ -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 getTimeline(Long commandId) { + return trackingMapper.selectList( + new LambdaQueryWrapper() + .eq(CommandTracking::getCommandId, commandId) + .orderByAsc(CommandTracking::getCreatedAt)); + } + + /** + * 根据指令编号获取追踪时间线 + */ + public List getTimelineByCommandNo(String commandNo) { + return trackingMapper.selectList( + new LambdaQueryWrapper() + .eq(CommandTracking::getCommandNo, commandNo) + .orderByAsc(CommandTracking::getCreatedAt)); + } + + /** + * 获取执行记录列表 + */ + public List getExecutionRecords(Long commandId) { + return executionMapper.selectList( + new LambdaQueryWrapper() + .eq(CommandExecutionRecord::getCommandId, commandId) + .orderByAsc(CommandExecutionRecord::getCreatedAt)); + } + + /** + * 获取完整时间线(tracking + execution records 合并) + */ + public List> getFullTimeline(Long commandId) { + List trackings = getTimeline(commandId); + List records = getExecutionRecords(commandId); + + List> timeline = new ArrayList<>(); + + for (CommandTracking t : trackings) { + Map 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 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; + } +} diff --git a/wm-dispatch/src/main/java/com/water/dispatch/service/DispatchBizService.java b/wm-dispatch/src/main/java/com/water/dispatch/service/DispatchBizService.java index 1fdaf727..487987d1 100644 --- a/wm-dispatch/src/main/java/com/water/dispatch/service/DispatchBizService.java +++ b/wm-dispatch/src/main/java/com/water/dispatch/service/DispatchBizService.java @@ -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 getTodayDuty() { return dutyMapper.selectList(new LambdaQueryWrapper() - .eq(DutySchedule::getDutyDate, LocalDate.now())); + .eq(DutySchedule::getDutyDate, LocalDate.now())); } - public Map createCommand(Map req) { + + public Map createCommand(Map 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 issueCommand(String cmdNo) { + + public Map issueCommand(String commandNo) { DispatchCommand c = cmdMapper.selectOne(new LambdaQueryWrapper() - .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 listCommands(Integer status) { + + public List listCommands(String status) { return cmdMapper.selectList(new LambdaQueryWrapper() - .eq(status!=null, DispatchCommand::getStatus, status)); + .eq(status != null, DispatchCommand::getStatus, status)); } - public Map createWorkOrder(Map req) { + + public Map createWorkOrder(Map 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 updateWorkOrderStatus(Long id, int status) { + + public Map 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 getDutyLogs(Long scheduleId) { return logMapper.selectList(new LambdaQueryWrapper() - .eq(DutyLog::getScheduleId, scheduleId)); + .eq(DutyLog::getScheduleId, scheduleId)); } + public List listStrategies(String type) { return stratMapper.selectList(new LambdaQueryWrapper() - .eq(type!=null, DispatchStrategy::getType, type)); + .eq(type != null, DispatchStrategy::getType, type)); } + public List listPlans(String type) { return planMapper.selectList(new LambdaQueryWrapper() - .eq(type!=null, EmergencyPlan::getType, type)); + .eq(type != null, EmergencyPlan::getType, type)); } - public Map simulateEmergency(String type, double lng, double lat) { - return Map.of("type",type,"lng",lng,"lat",lat, - "affectedArea","半径500米","affectedUsers",120, - "estimatedDuration","4小时"); + + public Map simulateEmergency(String type, double lng, double lat) { + return Map.of("type", type, "lng", lng, "lat", lat, + "affectedArea", "半径500米", "affectedUsers", 120, + "estimatedDuration", "4小时"); } } diff --git a/wm-dispatch/src/main/resources/db/V2__command_lifecycle.sql b/wm-dispatch/src/main/resources/db/V2__command_lifecycle.sql new file mode 100644 index 00000000..eb4074a5 --- /dev/null +++ b/wm-dispatch/src/main/resources/db/V2__command_lifecycle.sql @@ -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); diff --git a/wm-dispatch/src/test/java/com/water/dispatch/service/CommandLifecycleServiceTest.java b/wm-dispatch/src/test/java/com/water/dispatch/service/CommandLifecycleServiceTest.java new file mode 100644 index 00000000..8af0c32d --- /dev/null +++ b/wm-dispatch/src/test/java/com/water/dispatch/service/CommandLifecycleServiceTest.java @@ -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 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 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 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; + } +} diff --git a/wm-dispatch/src/test/java/com/water/dispatch/service/CommandTrackingServiceTest.java b/wm-dispatch/src/test/java/com/water/dispatch/service/CommandTrackingServiceTest.java new file mode 100644 index 00000000..7713816f --- /dev/null +++ b/wm-dispatch/src/test/java/com/water/dispatch/service/CommandTrackingServiceTest.java @@ -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 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 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> 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 result = trackingService.getTimelineByCommandNo("CMD-20260614-001"); + assertEquals(1, result.size()); + assertEquals("ISSUED", result.get(0).getStage()); + } +}