Quellcode durchsuchen

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 适配新实体字段
bot_dev2 vor 5 Tagen
Ursprung
Commit
21fa7cffd2
19 geänderte Dateien mit 1480 neuen und 52 gelöschten Zeilen
  1. 159
    0
      wm-dispatch/src/main/java/com/water/dispatch/controller/DispatchCommandController.java
  2. 65
    12
      wm-dispatch/src/main/java/com/water/dispatch/controller/DispatchController.java
  3. 43
    0
      wm-dispatch/src/main/java/com/water/dispatch/entity/CommandExecutionRecord.java
  4. 43
    0
      wm-dispatch/src/main/java/com/water/dispatch/entity/CommandTracking.java
  5. 12
    5
      wm-dispatch/src/main/java/com/water/dispatch/entity/DispatchCommand.java
  6. 22
    0
      wm-dispatch/src/main/java/com/water/dispatch/entity/dto/CommandCreateRequest.java
  7. 22
    0
      wm-dispatch/src/main/java/com/water/dispatch/entity/dto/CommandQueryRequest.java
  8. 18
    0
      wm-dispatch/src/main/java/com/water/dispatch/entity/dto/CommandStatVO.java
  9. 19
    0
      wm-dispatch/src/main/java/com/water/dispatch/entity/dto/ExecutionRequest.java
  10. 9
    0
      wm-dispatch/src/main/java/com/water/dispatch/mapper/CommandExecutionRecordMapper.java
  11. 9
    0
      wm-dispatch/src/main/java/com/water/dispatch/mapper/CommandTrackingMapper.java
  12. 95
    2
      wm-dispatch/src/main/java/com/water/dispatch/mapper/DispatchCommandMapper.java
  13. 125
    0
      wm-dispatch/src/main/java/com/water/dispatch/service/CommandLedgerService.java
  14. 250
    0
      wm-dispatch/src/main/java/com/water/dispatch/service/CommandLifecycleService.java
  15. 94
    0
      wm-dispatch/src/main/java/com/water/dispatch/service/CommandTrackingService.java
  16. 59
    33
      wm-dispatch/src/main/java/com/water/dispatch/service/DispatchBizService.java
  17. 73
    0
      wm-dispatch/src/main/resources/db/V2__command_lifecycle.sql
  18. 248
    0
      wm-dispatch/src/test/java/com/water/dispatch/service/CommandLifecycleServiceTest.java
  19. 115
    0
      wm-dispatch/src/test/java/com/water/dispatch/service/CommandTrackingServiceTest.java

+ 159
- 0
wm-dispatch/src/main/java/com/water/dispatch/controller/DispatchCommandController.java Datei anzeigen

1
+package com.water.dispatch.controller;
2
+
3
+import com.baomidou.mybatisplus.core.metadata.IPage;
4
+import com.water.common.core.result.R;
5
+import com.water.dispatch.entity.CommandExecutionRecord;
6
+import com.water.dispatch.entity.CommandTracking;
7
+import com.water.dispatch.entity.DispatchCommand;
8
+import com.water.dispatch.entity.dto.*;
9
+import com.water.dispatch.service.CommandLedgerService;
10
+import com.water.dispatch.service.CommandLifecycleService;
11
+import com.water.dispatch.service.CommandTrackingService;
12
+import io.swagger.v3.oas.annotations.Operation;
13
+import io.swagger.v3.oas.annotations.tags.Tag;
14
+import lombok.RequiredArgsConstructor;
15
+import org.springframework.web.bind.annotation.*;
16
+
17
+import java.util.List;
18
+import java.util.Map;
19
+
20
+/**
21
+ * 调度指令管理 - 全生命周期
22
+ */
23
+@Tag(name = "调度指令管理")
24
+@RestController
25
+@RequestMapping("/api/dispatch/command")
26
+@RequiredArgsConstructor
27
+public class DispatchCommandController {
28
+
29
+    private final CommandLifecycleService lifecycleService;
30
+    private final CommandLedgerService ledgerService;
31
+    private final CommandTrackingService trackingService;
32
+
33
+    // ==================== 生命周期操作 ====================
34
+
35
+    @Operation(summary = "1. 创建指令")
36
+    @PostMapping
37
+    public R<DispatchCommand> create(@RequestBody CommandCreateRequest req) {
38
+        return R.ok(lifecycleService.create(req));
39
+    }
40
+
41
+    @Operation(summary = "2. 下发指令")
42
+    @PostMapping("/{id}/issue")
43
+    public R<DispatchCommand> issue(@PathVariable Long id) {
44
+        return R.ok(lifecycleService.issue(id));
45
+    }
46
+
47
+    @Operation(summary = "3. 接收确认")
48
+    @PostMapping("/{id}/receive")
49
+    public R<DispatchCommand> receive(@PathVariable Long id,
50
+                                      @RequestParam(required = false) Long receiverId,
51
+                                      @RequestParam(required = false) String receiverName) {
52
+        return R.ok(lifecycleService.receive(id, receiverId, receiverName));
53
+    }
54
+
55
+    @Operation(summary = "4. 开始执行")
56
+    @PostMapping("/{id}/start")
57
+    public R<DispatchCommand> startExecution(@PathVariable Long id,
58
+                                             @RequestParam(required = false) Long executorId,
59
+                                             @RequestParam(required = false) String executorName) {
60
+        return R.ok(lifecycleService.startExecution(id, executorId, executorName));
61
+    }
62
+
63
+    @Operation(summary = "5. 执行进度反馈")
64
+    @PostMapping("/{id}/progress")
65
+    public R<DispatchCommand> reportProgress(@PathVariable Long id,
66
+                                             @RequestBody ExecutionRequest req) {
67
+        return R.ok(lifecycleService.reportProgress(id, req.getExecutorId(), req.getExecutorName(),
68
+                req.getDescription(), req.getProgress() != null ? req.getProgress() : 0));
69
+    }
70
+
71
+    @Operation(summary = "6. 完成指令")
72
+    @PostMapping("/{id}/complete")
73
+    public R<DispatchCommand> complete(@PathVariable Long id,
74
+                                       @RequestBody ExecutionRequest req) {
75
+        return R.ok(lifecycleService.complete(id, req.getExecutorId(), req.getExecutorName(), req.getExecuteResult()));
76
+    }
77
+
78
+    @Operation(summary = "7. 驳回指令")
79
+    @PostMapping("/{id}/reject")
80
+    public R<DispatchCommand> reject(@PathVariable Long id,
81
+                                     @RequestBody ExecutionRequest req) {
82
+        return R.ok(lifecycleService.reject(id, req.getExecutorId(), req.getExecutorName(), req.getRejectReason()));
83
+    }
84
+
85
+    @Operation(summary = "8. 取消指令")
86
+    @PostMapping("/{id}/cancel")
87
+    public R<DispatchCommand> cancel(@PathVariable Long id,
88
+                                     @RequestParam(required = false) Long operatorId,
89
+                                     @RequestParam(required = false) String operatorName,
90
+                                     @RequestParam(required = false) String reason) {
91
+        return R.ok(lifecycleService.cancel(id, operatorId, operatorName, reason));
92
+    }
93
+
94
+    // ==================== 查询 ====================
95
+
96
+    @Operation(summary = "9. 获取指令详情(按ID)")
97
+    @GetMapping("/{id}")
98
+    public R<DispatchCommand> getById(@PathVariable Long id) {
99
+        return R.ok(lifecycleService.getById(id));
100
+    }
101
+
102
+    @Operation(summary = "10. 根据指令编号获取详情")
103
+    @GetMapping("/no/{commandNo}")
104
+    public R<DispatchCommand> getByCommandNo(@PathVariable String commandNo) {
105
+        return R.ok(lifecycleService.getByCommandNo(commandNo));
106
+    }
107
+
108
+    // ==================== 台账 ====================
109
+
110
+    @Operation(summary = "11. 指令台账分页查询")
111
+    @GetMapping("/page")
112
+    public R<IPage<DispatchCommand>> queryPage(CommandQueryRequest req) {
113
+        return R.ok(ledgerService.queryPage(req));
114
+    }
115
+
116
+    @Operation(summary = "12. 指令台账列表查询")
117
+    @GetMapping("/list")
118
+    public R<List<DispatchCommand>> queryList(CommandQueryRequest req) {
119
+        return R.ok(ledgerService.queryList(req));
120
+    }
121
+
122
+    @Operation(summary = "13. 指令统计概览")
123
+    @GetMapping("/statistics")
124
+    public R<CommandStatVO> statistics() {
125
+        return R.ok(ledgerService.statistics());
126
+    }
127
+
128
+    @Operation(summary = "14. 指令数据导出")
129
+    @GetMapping("/export")
130
+    public R<List<DispatchCommand>> exportData(CommandQueryRequest req) {
131
+        return R.ok(ledgerService.exportData(req));
132
+    }
133
+
134
+    // ==================== 追踪 ====================
135
+
136
+    @Operation(summary = "15. 获取指令追踪时间线")
137
+    @GetMapping("/{id}/timeline")
138
+    public R<List<CommandTracking>> getTimeline(@PathVariable Long id) {
139
+        return R.ok(trackingService.getTimeline(id));
140
+    }
141
+
142
+    @Operation(summary = "16. 获取执行记录")
143
+    @GetMapping("/{id}/executions")
144
+    public R<List<CommandExecutionRecord>> getExecutions(@PathVariable Long id) {
145
+        return R.ok(trackingService.getExecutionRecords(id));
146
+    }
147
+
148
+    @Operation(summary = "17. 获取完整时间线(追踪+执行记录合并)")
149
+    @GetMapping("/{id}/full-timeline")
150
+    public R<List<Map<String, Object>>> getFullTimeline(@PathVariable Long id) {
151
+        return R.ok(trackingService.getFullTimeline(id));
152
+    }
153
+
154
+    @Operation(summary = "18. 按指令编号获取追踪时间线")
155
+    @GetMapping("/no/{commandNo}/timeline")
156
+    public R<List<CommandTracking>> getTimelineByCommandNo(@PathVariable String commandNo) {
157
+        return R.ok(trackingService.getTimelineByCommandNo(commandNo));
158
+    }
159
+}

+ 65
- 12
wm-dispatch/src/main/java/com/water/dispatch/controller/DispatchController.java Datei anzeigen

1
 package com.water.dispatch.controller;
1
 package com.water.dispatch.controller;
2
+
2
 import com.water.common.core.result.R;
3
 import com.water.common.core.result.R;
3
 import com.water.dispatch.entity.*;
4
 import com.water.dispatch.entity.*;
4
 import com.water.dispatch.service.DispatchBizService;
5
 import com.water.dispatch.service.DispatchBizService;
6
 import io.swagger.v3.oas.annotations.tags.Tag;
7
 import io.swagger.v3.oas.annotations.tags.Tag;
7
 import lombok.RequiredArgsConstructor;
8
 import lombok.RequiredArgsConstructor;
8
 import org.springframework.web.bind.annotation.*;
9
 import org.springframework.web.bind.annotation.*;
10
+
9
 import java.util.*;
11
 import java.util.*;
10
-@Tag(name="调度工作台") @RestController @RequestMapping("/dispatch") @RequiredArgsConstructor
12
+
13
+@Tag(name = "调度工作台")
14
+@RestController
15
+@RequestMapping("/dispatch")
16
+@RequiredArgsConstructor
11
 public class DispatchController {
17
 public class DispatchController {
12
     private final DispatchBizService svc;
18
     private final DispatchBizService svc;
13
-    @GetMapping("/duty/today") public R<List<DutySchedule>> todayDuty() { return R.ok(svc.getTodayDuty()); }
14
-    @PostMapping("/command") public R<Map<String,Object>> createCommand(@RequestBody Map<String,Object> req) { return R.ok(svc.createCommand(req)); }
15
-    @PostMapping("/command/{cmdNo}/issue") public R<Map<String,Object>> issue(@PathVariable String cmdNo) { return R.ok(svc.issueCommand(cmdNo)); }
16
-    @GetMapping("/command/list") public R<List<DispatchCommand>> listCommands(@RequestParam(required=false) Integer status) { return R.ok(svc.listCommands(status)); }
17
-    @PostMapping("/work-order") public R<Map<String,Object>> createWO(@RequestBody Map<String,Object> req) { return R.ok(svc.createWorkOrder(req)); }
18
-    @PutMapping("/work-order/{id}/status") public R<Map<String,Object>> updateWOStatus(@PathVariable Long id, @RequestParam int status) { return R.ok(svc.updateWorkOrderStatus(id, status)); }
19
-    @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"); }
20
-    @GetMapping("/duty-log/{scheduleId}") public R<List<DutyLog>> getLogs(@PathVariable Long scheduleId) { return R.ok(svc.getDutyLogs(scheduleId)); }
21
-    @GetMapping("/strategy") public R<List<DispatchStrategy>> listStrategies(@RequestParam(required=false) String type) { return R.ok(svc.listStrategies(type)); }
22
-    @GetMapping("/emergency-plan") public R<List<EmergencyPlan>> listPlans(@RequestParam(required=false) String type) { return R.ok(svc.listPlans(type)); }
23
-    @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)); }
19
+
20
+    @GetMapping("/duty/today")
21
+    public R<List<DutySchedule>> todayDuty() {
22
+        return R.ok(svc.getTodayDuty());
23
+    }
24
+
25
+    @PostMapping("/command")
26
+    public R<Map<String, Object>> createCommand(@RequestBody Map<String, Object> req) {
27
+        return R.ok(svc.createCommand(req));
28
+    }
29
+
30
+    @PostMapping("/command/{cmdNo}/issue")
31
+    public R<Map<String, Object>> issue(@PathVariable String cmdNo) {
32
+        return R.ok(svc.issueCommand(cmdNo));
33
+    }
34
+
35
+    @GetMapping("/command/list")
36
+    public R<List<DispatchCommand>> listCommands(@RequestParam(required = false) String status) {
37
+        return R.ok(svc.listCommands(status));
38
+    }
39
+
40
+    @PostMapping("/work-order")
41
+    public R<Map<String, Object>> createWO(@RequestBody Map<String, Object> req) {
42
+        return R.ok(svc.createWorkOrder(req));
43
+    }
44
+
45
+    @PutMapping("/work-order/{id}/status")
46
+    public R<Map<String, Object>> updateWOStatus(@PathVariable Long id, @RequestParam int status) {
47
+        return R.ok(svc.updateWorkOrderStatus(id, status));
48
+    }
49
+
50
+    @PostMapping("/duty-log")
51
+    public R<String> addLog(@RequestParam Long scheduleId, @RequestParam Long userId,
52
+                            @RequestParam String type, @RequestParam String content) {
53
+        svc.addDutyLog(scheduleId, userId, type, content);
54
+        return R.ok("OK");
55
+    }
56
+
57
+    @GetMapping("/duty-log/{scheduleId}")
58
+    public R<List<DutyLog>> getLogs(@PathVariable Long scheduleId) {
59
+        return R.ok(svc.getDutyLogs(scheduleId));
60
+    }
61
+
62
+    @GetMapping("/strategy")
63
+    public R<List<DispatchStrategy>> listStrategies(@RequestParam(required = false) String type) {
64
+        return R.ok(svc.listStrategies(type));
65
+    }
66
+
67
+    @GetMapping("/emergency-plan")
68
+    public R<List<EmergencyPlan>> listPlans(@RequestParam(required = false) String type) {
69
+        return R.ok(svc.listPlans(type));
70
+    }
71
+
72
+    @PostMapping("/emergency/simulate")
73
+    public R<Map<String, Object>> simulate(@RequestParam String type,
74
+                                           @RequestParam double lng, @RequestParam double lat) {
75
+        return R.ok(svc.simulateEmergency(type, lng, lat));
76
+    }
24
 }
77
 }

+ 43
- 0
wm-dispatch/src/main/java/com/water/dispatch/entity/CommandExecutionRecord.java Datei anzeigen

1
+package com.water.dispatch.entity;
2
+
3
+import com.baomidou.mybatisplus.annotation.*;
4
+import lombok.Data;
5
+
6
+import java.time.LocalDateTime;
7
+
8
+/**
9
+ * 指令执行记录
10
+ */
11
+@Data
12
+@TableName("disp_command_execution_record")
13
+public class CommandExecutionRecord {
14
+
15
+    @TableId(type = IdType.AUTO)
16
+    private Long id;
17
+
18
+    /** 关联指令ID */
19
+    private Long commandId;
20
+
21
+    /** 关联指令编号 */
22
+    private String commandNo;
23
+
24
+    /** 执行人ID */
25
+    private Long executorId;
26
+
27
+    /** 执行人姓名 */
28
+    private String executorName;
29
+
30
+    /** 动作: RECEIVE / START / PROGRESS / COMPLETE / REJECT */
31
+    private String action;
32
+
33
+    /** 描述 */
34
+    private String description;
35
+
36
+    /** 附件(JSON array) */
37
+    private String attachments;
38
+
39
+    /** 进度 0-100 */
40
+    private Integer progress;
41
+
42
+    private LocalDateTime createdAt;
43
+}

+ 43
- 0
wm-dispatch/src/main/java/com/water/dispatch/entity/CommandTracking.java Datei anzeigen

1
+package com.water.dispatch.entity;
2
+
3
+import com.baomidou.mybatisplus.annotation.*;
4
+import lombok.Data;
5
+
6
+import java.time.LocalDateTime;
7
+
8
+/**
9
+ * 指令过程追踪(时间线)
10
+ */
11
+@Data
12
+@TableName("disp_command_tracking")
13
+public class CommandTracking {
14
+
15
+    @TableId(type = IdType.AUTO)
16
+    private Long id;
17
+
18
+    /** 关联指令ID */
19
+    private Long commandId;
20
+
21
+    /** 关联指令编号 */
22
+    private String commandNo;
23
+
24
+    /** 阶段: CREATED / ISSUED / RECEIVED / EXECUTING / COMPLETED / REJECTED / CANCELLED */
25
+    private String stage;
26
+
27
+    /** 操作人ID */
28
+    private Long operatorId;
29
+
30
+    /** 操作人姓名 */
31
+    private String operatorName;
32
+
33
+    /** 动作描述 */
34
+    private String actionDesc;
35
+
36
+    /** 来源状态 */
37
+    private String fromStatus;
38
+
39
+    /** 目标状态 */
40
+    private String toStatus;
41
+
42
+    private LocalDateTime createdAt;
43
+}

+ 12
- 5
wm-dispatch/src/main/java/com/water/dispatch/entity/DispatchCommand.java Datei anzeigen

6
 import java.time.LocalDateTime;
6
 import java.time.LocalDateTime;
7
 
7
 
8
 /**
8
 /**
9
- * 调度指令 - 全生命周期: 下发→接收→执行→完成→驳回
9
+ * 调度指令 - 全生命周期: DRAFT→ISSUED→RECEIVED→EXECUTING→COMPLETED/REJECTED/CANCELLED
10
  */
10
  */
11
 @Data
11
 @Data
12
 @TableName("disp_dispatch_command")
12
 @TableName("disp_dispatch_command")
30
     /** 优先级: LOW-低 MEDIUM-中 HIGH-高 URGENT-紧急 */
30
     /** 优先级: LOW-低 MEDIUM-中 HIGH-高 URGENT-紧急 */
31
     private String priority;
31
     private String priority;
32
 
32
 
33
+    /** 状态: DRAFT/ISSUED/RECEIVED/EXECUTING/COMPLETED/REJECTED/CANCELLED */
34
+    private String status;
35
+
33
     /** 下发人ID */
36
     /** 下发人ID */
34
     private Long issuerId;
37
     private Long issuerId;
35
 
38
 
45
     /** 关联设施ID */
48
     /** 关联设施ID */
46
     private Long facilityId;
49
     private Long facilityId;
47
 
50
 
48
-    /** 状态: ISSUED-下发 RECEIVED-接收 EXECUTING-执行 COMPLETED-完成 REJECTED-驳回 CANCELLED-取消 */
49
-    private String status;
51
+    /** 截止时间 */
52
+    private LocalDateTime deadline;
50
 
53
 
51
     /** 下发时间 */
54
     /** 下发时间 */
52
     private LocalDateTime issuedAt;
55
     private LocalDateTime issuedAt;
60
     /** 完成时间 */
63
     /** 完成时间 */
61
     private LocalDateTime completedAt;
64
     private LocalDateTime completedAt;
62
 
65
 
66
+    /** 驳回时间 */
67
+    private LocalDateTime rejectedAt;
68
+
63
     /** 驳回原因 */
69
     /** 驳回原因 */
64
     private String rejectReason;
70
     private String rejectReason;
65
 
71
 
66
     /** 执行结果 */
72
     /** 执行结果 */
67
     private String executeResult;
73
     private String executeResult;
68
 
74
 
69
-    /** 截止时间 */
70
-    private LocalDateTime deadline;
75
+    /** 备注 */
76
+    private String remark;
71
 
77
 
72
     @TableLogic
78
     @TableLogic
73
     private Integer deleted;
79
     private Integer deleted;
74
 
80
 
75
     private LocalDateTime createdAt;
81
     private LocalDateTime createdAt;
76
 
82
 
83
+    @TableField(fill = FieldFill.INSERT_UPDATE)
77
     private LocalDateTime updatedAt;
84
     private LocalDateTime updatedAt;
78
 }
85
 }

+ 22
- 0
wm-dispatch/src/main/java/com/water/dispatch/entity/dto/CommandCreateRequest.java Datei anzeigen

1
+package com.water.dispatch.entity.dto;
2
+
3
+import lombok.Data;
4
+import java.time.LocalDateTime;
5
+
6
+/**
7
+ * 指令创建请求
8
+ */
9
+@Data
10
+public class CommandCreateRequest {
11
+    private String title;
12
+    private String content;
13
+    private String commandType;  // NORMAL / EMERGENCY / MAINTENANCE
14
+    private String priority;     // LOW / MEDIUM / HIGH / URGENT
15
+    private Long issuerId;
16
+    private String issuerName;
17
+    private Long receiverId;
18
+    private String receiverName;
19
+    private Long facilityId;
20
+    private LocalDateTime deadline;
21
+    private String remark;
22
+}

+ 22
- 0
wm-dispatch/src/main/java/com/water/dispatch/entity/dto/CommandQueryRequest.java Datei anzeigen

1
+package com.water.dispatch.entity.dto;
2
+
3
+import lombok.Data;
4
+import java.time.LocalDateTime;
5
+
6
+/**
7
+ * 指令台账查询请求
8
+ */
9
+@Data
10
+public class CommandQueryRequest {
11
+    private String commandNo;
12
+    private String title;
13
+    private String commandType;
14
+    private String priority;
15
+    private String status;
16
+    private Long issuerId;
17
+    private Long receiverId;
18
+    private LocalDateTime startTime;
19
+    private LocalDateTime endTime;
20
+    private Integer pageNum = 1;
21
+    private Integer pageSize = 20;
22
+}

+ 18
- 0
wm-dispatch/src/main/java/com/water/dispatch/entity/dto/CommandStatVO.java Datei anzeigen

1
+package com.water.dispatch.entity.dto;
2
+
3
+import lombok.Data;
4
+import java.util.Map;
5
+
6
+/**
7
+ * 指令统计VO
8
+ */
9
+@Data
10
+public class CommandStatVO {
11
+    private long total;
12
+    private Map<String, Long> byStatus;
13
+    private Map<String, Long> byType;
14
+    private Map<String, Long> byPriority;
15
+    private long overdueCount;
16
+    private long todayCreated;
17
+    private long todayCompleted;
18
+}

+ 19
- 0
wm-dispatch/src/main/java/com/water/dispatch/entity/dto/ExecutionRequest.java Datei anzeigen

1
+package com.water.dispatch.entity.dto;
2
+
3
+import lombok.Data;
4
+
5
+/**
6
+ * 执行反馈请求
7
+ */
8
+@Data
9
+public class ExecutionRequest {
10
+    private Long commandId;
11
+    private Long executorId;
12
+    private String executorName;
13
+    private String action;       // RECEIVE / START / PROGRESS / COMPLETE / REJECT
14
+    private String description;
15
+    private String attachments;  // JSON array
16
+    private Integer progress;
17
+    private String rejectReason; // 驳回时填写
18
+    private String executeResult; // 完成时填写
19
+}

+ 9
- 0
wm-dispatch/src/main/java/com/water/dispatch/mapper/CommandExecutionRecordMapper.java Datei anzeigen

1
+package com.water.dispatch.mapper;
2
+
3
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
4
+import com.water.dispatch.entity.CommandExecutionRecord;
5
+import org.apache.ibatis.annotations.Mapper;
6
+
7
+@Mapper
8
+public interface CommandExecutionRecordMapper extends BaseMapper<CommandExecutionRecord> {
9
+}

+ 9
- 0
wm-dispatch/src/main/java/com/water/dispatch/mapper/CommandTrackingMapper.java Datei anzeigen

1
+package com.water.dispatch.mapper;
2
+
3
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
4
+import com.water.dispatch.entity.CommandTracking;
5
+import org.apache.ibatis.annotations.Mapper;
6
+
7
+@Mapper
8
+public interface CommandTrackingMapper extends BaseMapper<CommandTracking> {
9
+}

+ 95
- 2
wm-dispatch/src/main/java/com/water/dispatch/mapper/DispatchCommandMapper.java Datei anzeigen

1
 package com.water.dispatch.mapper;
1
 package com.water.dispatch.mapper;
2
+
2
 import com.baomidou.mybatisplus.core.mapper.BaseMapper;
3
 import com.baomidou.mybatisplus.core.mapper.BaseMapper;
4
+import com.baomidou.mybatisplus.core.metadata.IPage;
5
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
3
 import com.water.dispatch.entity.DispatchCommand;
6
 import com.water.dispatch.entity.DispatchCommand;
4
-import org.apache.ibatis.annotations.Mapper;
5
-@Mapper public interface DispatchCommandMapper extends BaseMapper<DispatchCommand> {}
7
+import org.apache.ibatis.annotations.*;
8
+
9
+import java.util.List;
10
+import java.util.Map;
11
+
12
+@Mapper
13
+public interface DispatchCommandMapper extends BaseMapper<DispatchCommand> {
14
+
15
+    /**
16
+     * 分页 + 多维度查询指令台账
17
+     */
18
+    @SelectProvider(type = CommandSqlProvider.class, method = "buildLedgerQuery")
19
+    IPage<DispatchCommand> selectLedgerPage(
20
+            Page<?> page,
21
+            @Param("commandNo") String commandNo,
22
+            @Param("title") String title,
23
+            @Param("commandType") String commandType,
24
+            @Param("priority") String priority,
25
+            @Param("status") String status,
26
+            @Param("issuerId") Long issuerId,
27
+            @Param("receiverId") Long receiverId,
28
+            @Param("startTime") String startTime,
29
+            @Param("endTime") String endTime);
30
+
31
+    /**
32
+     * 按状态统计
33
+     */
34
+    @Select("SELECT status, COUNT(*) as cnt FROM disp_dispatch_command WHERE deleted=0 GROUP BY status")
35
+    List<Map<String, Object>> countByStatus();
36
+
37
+    /**
38
+     * 按类型统计
39
+     */
40
+    @Select("SELECT command_type, COUNT(*) as cnt FROM disp_dispatch_command WHERE deleted=0 GROUP BY command_type")
41
+    List<Map<String, Object>> countByType();
42
+
43
+    /**
44
+     * 按优先级统计
45
+     */
46
+    @Select("SELECT priority, COUNT(*) as cnt FROM disp_dispatch_command WHERE deleted=0 GROUP BY priority")
47
+    List<Map<String, Object>> countByPriority();
48
+
49
+    /**
50
+     * 逾期指令数
51
+     */
52
+    @Select("SELECT COUNT(*) FROM disp_dispatch_command WHERE deleted=0 AND status NOT IN ('COMPLETED','CANCELLED','REJECTED') AND deadline < NOW()")
53
+    long countOverdue();
54
+
55
+    class CommandSqlProvider {
56
+        public String buildLedgerQuery(
57
+                @Param("commandNo") String commandNo,
58
+                @Param("title") String title,
59
+                @Param("commandType") String commandType,
60
+                @Param("priority") String priority,
61
+                @Param("status") String status,
62
+                @Param("issuerId") Long issuerId,
63
+                @Param("receiverId") Long receiverId,
64
+                @Param("startTime") String startTime,
65
+                @Param("endTime") String endTime) {
66
+            StringBuilder sb = new StringBuilder("SELECT * FROM disp_dispatch_command WHERE deleted=0");
67
+            if (commandNo != null && !commandNo.isEmpty()) {
68
+                sb.append(" AND command_no LIKE '%").append(commandNo).append("%'");
69
+            }
70
+            if (title != null && !title.isEmpty()) {
71
+                sb.append(" AND title LIKE '%").append(title).append("%'");
72
+            }
73
+            if (commandType != null && !commandType.isEmpty()) {
74
+                sb.append(" AND command_type = '").append(commandType).append("'");
75
+            }
76
+            if (priority != null && !priority.isEmpty()) {
77
+                sb.append(" AND priority = '").append(priority).append("'");
78
+            }
79
+            if (status != null && !status.isEmpty()) {
80
+                sb.append(" AND status = '").append(status).append("'");
81
+            }
82
+            if (issuerId != null) {
83
+                sb.append(" AND issuer_id = ").append(issuerId);
84
+            }
85
+            if (receiverId != null) {
86
+                sb.append(" AND receiver_id = ").append(receiverId);
87
+            }
88
+            if (startTime != null && !startTime.isEmpty()) {
89
+                sb.append(" AND created_at >= '").append(startTime).append("'");
90
+            }
91
+            if (endTime != null && !endTime.isEmpty()) {
92
+                sb.append(" AND created_at <= '").append(endTime).append("'");
93
+            }
94
+            sb.append(" ORDER BY created_at DESC");
95
+            return sb.toString();
96
+        }
97
+    }
98
+}

+ 125
- 0
wm-dispatch/src/main/java/com/water/dispatch/service/CommandLedgerService.java Datei anzeigen

1
+package com.water.dispatch.service;
2
+
3
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
4
+import com.baomidou.mybatisplus.core.metadata.IPage;
5
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
6
+import com.water.dispatch.entity.DispatchCommand;
7
+import com.water.dispatch.entity.dto.CommandQueryRequest;
8
+import com.water.dispatch.entity.dto.CommandStatVO;
9
+import com.water.dispatch.mapper.DispatchCommandMapper;
10
+import lombok.RequiredArgsConstructor;
11
+import org.springframework.stereotype.Service;
12
+
13
+import java.time.LocalDate;
14
+import java.time.LocalDateTime;
15
+import java.time.LocalTime;
16
+import java.util.HashMap;
17
+import java.util.List;
18
+import java.util.Map;
19
+
20
+/**
21
+ * 指令台账服务 - 多维度查询/统计/导出
22
+ */
23
+@Service
24
+@RequiredArgsConstructor
25
+public class CommandLedgerService {
26
+
27
+    private final DispatchCommandMapper commandMapper;
28
+
29
+    /**
30
+     * 分页多维度查询
31
+     */
32
+    public IPage<DispatchCommand> queryPage(CommandQueryRequest req) {
33
+        Page<DispatchCommand> page = new Page<>(req.getPageNum(), req.getPageSize());
34
+        String startTime = req.getStartTime() != null ? req.getStartTime().toString() : null;
35
+        String endTime = req.getEndTime() != null ? req.getEndTime().toString() : null;
36
+
37
+        return commandMapper.selectLedgerPage(page,
38
+                req.getCommandNo(), req.getTitle(), req.getCommandType(),
39
+                req.getPriority(), req.getStatus(),
40
+                req.getIssuerId(), req.getReceiverId(),
41
+                startTime, endTime);
42
+    }
43
+
44
+    /**
45
+     * 列表查询(不分页)
46
+     */
47
+    public List<DispatchCommand> queryList(CommandQueryRequest req) {
48
+        LambdaQueryWrapper<DispatchCommand> wrapper = new LambdaQueryWrapper<>();
49
+        if (req.getCommandNo() != null && !req.getCommandNo().isEmpty()) {
50
+            wrapper.like(DispatchCommand::getCommandNo, req.getCommandNo());
51
+        }
52
+        if (req.getTitle() != null && !req.getTitle().isEmpty()) {
53
+            wrapper.like(DispatchCommand::getTitle, req.getTitle());
54
+        }
55
+        if (req.getCommandType() != null && !req.getCommandType().isEmpty()) {
56
+            wrapper.eq(DispatchCommand::getCommandType, req.getCommandType());
57
+        }
58
+        if (req.getPriority() != null && !req.getPriority().isEmpty()) {
59
+            wrapper.eq(DispatchCommand::getPriority, req.getPriority());
60
+        }
61
+        if (req.getStatus() != null && !req.getStatus().isEmpty()) {
62
+            wrapper.eq(DispatchCommand::getStatus, req.getStatus());
63
+        }
64
+        if (req.getIssuerId() != null) {
65
+            wrapper.eq(DispatchCommand::getIssuerId, req.getIssuerId());
66
+        }
67
+        if (req.getReceiverId() != null) {
68
+            wrapper.eq(DispatchCommand::getReceiverId, req.getReceiverId());
69
+        }
70
+        if (req.getStartTime() != null) {
71
+            wrapper.ge(DispatchCommand::getCreatedAt, req.getStartTime());
72
+        }
73
+        if (req.getEndTime() != null) {
74
+            wrapper.le(DispatchCommand::getCreatedAt, req.getEndTime());
75
+        }
76
+        wrapper.orderByDesc(DispatchCommand::getCreatedAt);
77
+        return commandMapper.selectList(wrapper);
78
+    }
79
+
80
+    /**
81
+     * 统计概览
82
+     */
83
+    public CommandStatVO statistics() {
84
+        CommandStatVO vo = new CommandStatVO();
85
+        vo.setTotal(commandMapper.selectCount(new LambdaQueryWrapper<>()));
86
+
87
+        Map<String, Long> byStatus = new HashMap<>();
88
+        commandMapper.countByStatus().forEach(m ->
89
+                byStatus.put(String.valueOf(m.get("status")), ((Number) m.get("cnt")).longValue()));
90
+        vo.setByStatus(byStatus);
91
+
92
+        Map<String, Long> byType = new HashMap<>();
93
+        commandMapper.countByType().forEach(m ->
94
+                byType.put(String.valueOf(m.get("command_type")), ((Number) m.get("cnt")).longValue()));
95
+        vo.setByType(byType);
96
+
97
+        Map<String, Long> byPriority = new HashMap<>();
98
+        commandMapper.countByPriority().forEach(m ->
99
+                byPriority.put(String.valueOf(m.get("priority")), ((Number) m.get("cnt")).longValue()));
100
+        vo.setByPriority(byPriority);
101
+
102
+        vo.setOverdueCount(commandMapper.countOverdue());
103
+
104
+        LocalDateTime todayStart = LocalDateTime.of(LocalDate.now(), LocalTime.MIN);
105
+        LocalDateTime todayEnd = LocalDateTime.of(LocalDate.now(), LocalTime.MAX);
106
+        vo.setTodayCreated(commandMapper.selectCount(
107
+                new LambdaQueryWrapper<DispatchCommand>()
108
+                        .ge(DispatchCommand::getCreatedAt, todayStart)
109
+                        .le(DispatchCommand::getCreatedAt, todayEnd)));
110
+        vo.setTodayCompleted(commandMapper.selectCount(
111
+                new LambdaQueryWrapper<DispatchCommand>()
112
+                        .eq(DispatchCommand::getStatus, "COMPLETED")
113
+                        .ge(DispatchCommand::getCompletedAt, todayStart)
114
+                        .le(DispatchCommand::getCompletedAt, todayEnd)));
115
+
116
+        return vo;
117
+    }
118
+
119
+    /**
120
+     * 导出用数据(全量列表)
121
+     */
122
+    public List<DispatchCommand> exportData(CommandQueryRequest req) {
123
+        return queryList(req);
124
+    }
125
+}

+ 250
- 0
wm-dispatch/src/main/java/com/water/dispatch/service/CommandLifecycleService.java Datei anzeigen

1
+package com.water.dispatch.service;
2
+
3
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
4
+import com.water.common.core.exception.BusinessException;
5
+import com.water.dispatch.entity.CommandExecutionRecord;
6
+import com.water.dispatch.entity.CommandTracking;
7
+import com.water.dispatch.entity.DispatchCommand;
8
+import com.water.dispatch.entity.dto.CommandCreateRequest;
9
+import com.water.dispatch.entity.dto.ExecutionRequest;
10
+import com.water.dispatch.mapper.CommandExecutionRecordMapper;
11
+import com.water.dispatch.mapper.CommandTrackingMapper;
12
+import com.water.dispatch.mapper.DispatchCommandMapper;
13
+import lombok.RequiredArgsConstructor;
14
+import org.springframework.stereotype.Service;
15
+import org.springframework.transaction.annotation.Transactional;
16
+
17
+import java.time.LocalDateTime;
18
+import java.time.format.DateTimeFormatter;
19
+
20
+/**
21
+ * 调度指令全生命周期服务
22
+ * DRAFT → ISSUED → RECEIVED → EXECUTING → COMPLETED / REJECTED / CANCELLED
23
+ */
24
+@Service
25
+@RequiredArgsConstructor
26
+public class CommandLifecycleService {
27
+
28
+    private final DispatchCommandMapper commandMapper;
29
+    private final CommandExecutionRecordMapper executionMapper;
30
+    private final CommandTrackingMapper trackingMapper;
31
+
32
+    /**
33
+     * 创建指令(DRAFT)
34
+     */
35
+    @Transactional
36
+    public DispatchCommand create(CommandCreateRequest req) {
37
+        DispatchCommand cmd = new DispatchCommand();
38
+        cmd.setCommandNo(generateCommandNo());
39
+        cmd.setTitle(req.getTitle());
40
+        cmd.setContent(req.getContent());
41
+        cmd.setCommandType(req.getCommandType() != null ? req.getCommandType() : "NORMAL");
42
+        cmd.setPriority(req.getPriority() != null ? req.getPriority() : "MEDIUM");
43
+        cmd.setStatus("DRAFT");
44
+        cmd.setIssuerId(req.getIssuerId());
45
+        cmd.setIssuerName(req.getIssuerName());
46
+        cmd.setReceiverId(req.getReceiverId());
47
+        cmd.setReceiverName(req.getReceiverName());
48
+        cmd.setFacilityId(req.getFacilityId());
49
+        cmd.setDeadline(req.getDeadline());
50
+        cmd.setRemark(req.getRemark());
51
+        commandMapper.insert(cmd);
52
+
53
+        addTracking(cmd, "CREATED", null, "DRAFT", req.getIssuerId(), req.getIssuerName(), "创建指令");
54
+        return cmd;
55
+    }
56
+
57
+    /**
58
+     * 下发指令(DRAFT → ISSUED)
59
+     */
60
+    @Transactional
61
+    public DispatchCommand issue(Long commandId) {
62
+        DispatchCommand cmd = getCommandOrThrow(commandId);
63
+        assertStatus(cmd, "DRAFT");
64
+
65
+        cmd.setStatus("ISSUED");
66
+        cmd.setIssuedAt(LocalDateTime.now());
67
+        commandMapper.updateById(cmd);
68
+
69
+        addTracking(cmd, "ISSUED", "DRAFT", "ISSUED", cmd.getIssuerId(), cmd.getIssuerName(), "下发指令");
70
+        return cmd;
71
+    }
72
+
73
+    /**
74
+     * 接收确认(ISSUED → RECEIVED)
75
+     */
76
+    @Transactional
77
+    public DispatchCommand receive(Long commandId, Long receiverId, String receiverName) {
78
+        DispatchCommand cmd = getCommandOrThrow(commandId);
79
+        assertStatus(cmd, "ISSUED");
80
+
81
+        cmd.setStatus("RECEIVED");
82
+        cmd.setReceivedAt(LocalDateTime.now());
83
+        if (receiverId != null) {
84
+            cmd.setReceiverId(receiverId);
85
+            cmd.setReceiverName(receiverName);
86
+        }
87
+        commandMapper.updateById(cmd);
88
+
89
+        addExecution(cmd, receiverId, receiverName, "RECEIVE", "确认接收指令", null, 0);
90
+        addTracking(cmd, "RECEIVED", "ISSUED", "RECEIVED", receiverId, receiverName, "接收确认");
91
+        return cmd;
92
+    }
93
+
94
+    /**
95
+     * 开始执行(RECEIVED → EXECUTING)
96
+     */
97
+    @Transactional
98
+    public DispatchCommand startExecution(Long commandId, Long executorId, String executorName) {
99
+        DispatchCommand cmd = getCommandOrThrow(commandId);
100
+        assertStatus(cmd, "RECEIVED");
101
+
102
+        cmd.setStatus("EXECUTING");
103
+        cmd.setExecutedAt(LocalDateTime.now());
104
+        commandMapper.updateById(cmd);
105
+
106
+        addExecution(cmd, executorId, executorName, "START", "开始执行", null, 10);
107
+        addTracking(cmd, "EXECUTING", "RECEIVED", "EXECUTING", executorId, executorName, "开始执行");
108
+        return cmd;
109
+    }
110
+
111
+    /**
112
+     * 执行进度反馈(EXECUTING stays)
113
+     */
114
+    @Transactional
115
+    public DispatchCommand reportProgress(Long commandId, Long executorId, String executorName,
116
+                                          String description, int progress) {
117
+        DispatchCommand cmd = getCommandOrThrow(commandId);
118
+        assertStatus(cmd, "EXECUTING");
119
+
120
+        addExecution(cmd, executorId, executorName, "PROGRESS", description, null, progress);
121
+        return cmd;
122
+    }
123
+
124
+    /**
125
+     * 完成指令(EXECUTING → COMPLETED)
126
+     */
127
+    @Transactional
128
+    public DispatchCommand complete(Long commandId, Long executorId, String executorName, String result) {
129
+        DispatchCommand cmd = getCommandOrThrow(commandId);
130
+        assertStatus(cmd, "EXECUTING");
131
+
132
+        cmd.setStatus("COMPLETED");
133
+        cmd.setCompletedAt(LocalDateTime.now());
134
+        cmd.setExecuteResult(result);
135
+        commandMapper.updateById(cmd);
136
+
137
+        addExecution(cmd, executorId, executorName, "COMPLETE", result, null, 100);
138
+        addTracking(cmd, "COMPLETED", "EXECUTING", "COMPLETED", executorId, executorName, "执行完成: " + result);
139
+        return cmd;
140
+    }
141
+
142
+    /**
143
+     * 驳回指令(任意活跃状态 → REJECTED)
144
+     */
145
+    @Transactional
146
+    public DispatchCommand reject(Long commandId, Long operatorId, String operatorName, String reason) {
147
+        DispatchCommand cmd = getCommandOrThrow(commandId);
148
+        String fromStatus = cmd.getStatus();
149
+        if ("COMPLETED".equals(fromStatus) || "CANCELLED".equals(fromStatus) || "REJECTED".equals(fromStatus)) {
150
+            throw new BusinessException("当前状态不允许驳回: " + fromStatus);
151
+        }
152
+
153
+        cmd.setStatus("REJECTED");
154
+        cmd.setRejectedAt(LocalDateTime.now());
155
+        cmd.setRejectReason(reason);
156
+        commandMapper.updateById(cmd);
157
+
158
+        addExecution(cmd, operatorId, operatorName, "REJECT", reason, null, 0);
159
+        addTracking(cmd, "REJECTED", fromStatus, "REJECTED", operatorId, operatorName, "驳回: " + reason);
160
+        return cmd;
161
+    }
162
+
163
+    /**
164
+     * 取消指令
165
+     */
166
+    @Transactional
167
+    public DispatchCommand cancel(Long commandId, Long operatorId, String operatorName, String reason) {
168
+        DispatchCommand cmd = getCommandOrThrow(commandId);
169
+        String fromStatus = cmd.getStatus();
170
+        if ("COMPLETED".equals(fromStatus) || "CANCELLED".equals(fromStatus)) {
171
+            throw new BusinessException("当前状态不允许取消: " + fromStatus);
172
+        }
173
+
174
+        cmd.setStatus("CANCELLED");
175
+        commandMapper.updateById(cmd);
176
+
177
+        addTracking(cmd, "CANCELLED", fromStatus, "CANCELLED", operatorId, operatorName,
178
+                "取消指令" + (reason != null ? ": " + reason : ""));
179
+        return cmd;
180
+    }
181
+
182
+    /**
183
+     * 获取指令详情
184
+     */
185
+    public DispatchCommand getById(Long id) {
186
+        return getCommandOrThrow(id);
187
+    }
188
+
189
+    /**
190
+     * 根据编号获取
191
+     */
192
+    public DispatchCommand getByCommandNo(String commandNo) {
193
+        DispatchCommand cmd = commandMapper.selectOne(
194
+                new LambdaQueryWrapper<DispatchCommand>()
195
+                        .eq(DispatchCommand::getCommandNo, commandNo));
196
+        if (cmd == null) {
197
+            throw new BusinessException("指令不存在: " + commandNo);
198
+        }
199
+        return cmd;
200
+    }
201
+
202
+    // ========== Internal ==========
203
+
204
+    private DispatchCommand getCommandOrThrow(Long id) {
205
+        DispatchCommand cmd = commandMapper.selectById(id);
206
+        if (cmd == null) {
207
+            throw new BusinessException("指令不存在: " + id);
208
+        }
209
+        return cmd;
210
+    }
211
+
212
+    private void assertStatus(DispatchCommand cmd, String expected) {
213
+        if (!expected.equals(cmd.getStatus())) {
214
+            throw new BusinessException("指令状态不正确,期望: " + expected + ",实际: " + cmd.getStatus());
215
+        }
216
+    }
217
+
218
+    private String generateCommandNo() {
219
+        return "CMD-" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss"))
220
+                + "-" + (int) (Math.random() * 9000 + 1000);
221
+    }
222
+
223
+    private void addTracking(DispatchCommand cmd, String stage, String fromStatus, String toStatus,
224
+                             Long operatorId, String operatorName, String desc) {
225
+        CommandTracking t = new CommandTracking();
226
+        t.setCommandId(cmd.getId());
227
+        t.setCommandNo(cmd.getCommandNo());
228
+        t.setStage(stage);
229
+        t.setFromStatus(fromStatus);
230
+        t.setToStatus(toStatus);
231
+        t.setOperatorId(operatorId);
232
+        t.setOperatorName(operatorName);
233
+        t.setActionDesc(desc);
234
+        trackingMapper.insert(t);
235
+    }
236
+
237
+    private void addExecution(DispatchCommand cmd, Long executorId, String executorName,
238
+                              String action, String description, String attachments, int progress) {
239
+        CommandExecutionRecord r = new CommandExecutionRecord();
240
+        r.setCommandId(cmd.getId());
241
+        r.setCommandNo(cmd.getCommandNo());
242
+        r.setExecutorId(executorId);
243
+        r.setExecutorName(executorName);
244
+        r.setAction(action);
245
+        r.setDescription(description);
246
+        r.setAttachments(attachments);
247
+        r.setProgress(progress);
248
+        executionMapper.insert(r);
249
+    }
250
+}

+ 94
- 0
wm-dispatch/src/main/java/com/water/dispatch/service/CommandTrackingService.java Datei anzeigen

1
+package com.water.dispatch.service;
2
+
3
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
4
+import com.water.dispatch.entity.CommandExecutionRecord;
5
+import com.water.dispatch.entity.CommandTracking;
6
+import com.water.dispatch.mapper.CommandExecutionRecordMapper;
7
+import com.water.dispatch.mapper.CommandTrackingMapper;
8
+import lombok.RequiredArgsConstructor;
9
+import org.springframework.stereotype.Service;
10
+
11
+import java.util.*;
12
+
13
+/**
14
+ * 指令全过程追踪服务 - 时间线
15
+ */
16
+@Service
17
+@RequiredArgsConstructor
18
+public class CommandTrackingService {
19
+
20
+    private final CommandTrackingMapper trackingMapper;
21
+    private final CommandExecutionRecordMapper executionMapper;
22
+
23
+    /**
24
+     * 获取指令追踪时间线
25
+     */
26
+    public List<CommandTracking> getTimeline(Long commandId) {
27
+        return trackingMapper.selectList(
28
+                new LambdaQueryWrapper<CommandTracking>()
29
+                        .eq(CommandTracking::getCommandId, commandId)
30
+                        .orderByAsc(CommandTracking::getCreatedAt));
31
+    }
32
+
33
+    /**
34
+     * 根据指令编号获取追踪时间线
35
+     */
36
+    public List<CommandTracking> getTimelineByCommandNo(String commandNo) {
37
+        return trackingMapper.selectList(
38
+                new LambdaQueryWrapper<CommandTracking>()
39
+                        .eq(CommandTracking::getCommandNo, commandNo)
40
+                        .orderByAsc(CommandTracking::getCreatedAt));
41
+    }
42
+
43
+    /**
44
+     * 获取执行记录列表
45
+     */
46
+    public List<CommandExecutionRecord> getExecutionRecords(Long commandId) {
47
+        return executionMapper.selectList(
48
+                new LambdaQueryWrapper<CommandExecutionRecord>()
49
+                        .eq(CommandExecutionRecord::getCommandId, commandId)
50
+                        .orderByAsc(CommandExecutionRecord::getCreatedAt));
51
+    }
52
+
53
+    /**
54
+     * 获取完整时间线(tracking + execution records 合并)
55
+     */
56
+    public List<Map<String, Object>> getFullTimeline(Long commandId) {
57
+        List<CommandTracking> trackings = getTimeline(commandId);
58
+        List<CommandExecutionRecord> records = getExecutionRecords(commandId);
59
+
60
+        List<Map<String, Object>> timeline = new ArrayList<>();
61
+
62
+        for (CommandTracking t : trackings) {
63
+            Map<String, Object> item = new LinkedHashMap<>();
64
+            item.put("type", "TRACKING");
65
+            item.put("id", t.getId());
66
+            item.put("time", t.getCreatedAt());
67
+            item.put("stage", t.getStage());
68
+            item.put("operatorId", t.getOperatorId());
69
+            item.put("operatorName", t.getOperatorName());
70
+            item.put("description", t.getActionDesc());
71
+            item.put("fromStatus", t.getFromStatus());
72
+            item.put("toStatus", t.getToStatus());
73
+            timeline.add(item);
74
+        }
75
+
76
+        for (CommandExecutionRecord r : records) {
77
+            Map<String, Object> item = new LinkedHashMap<>();
78
+            item.put("type", "EXECUTION");
79
+            item.put("id", r.getId());
80
+            item.put("time", r.getCreatedAt());
81
+            item.put("action", r.getAction());
82
+            item.put("executorId", r.getExecutorId());
83
+            item.put("executorName", r.getExecutorName());
84
+            item.put("description", r.getDescription());
85
+            item.put("progress", r.getProgress());
86
+            item.put("attachments", r.getAttachments());
87
+            timeline.add(item);
88
+        }
89
+
90
+        // Sort by time
91
+        timeline.sort(Comparator.comparing(m -> (java.time.LocalDateTime) m.get("time")));
92
+        return timeline;
93
+    }
94
+}

+ 59
- 33
wm-dispatch/src/main/java/com/water/dispatch/service/DispatchBizService.java Datei anzeigen

1
 package com.water.dispatch.service;
1
 package com.water.dispatch.service;
2
+
2
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
3
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
3
 import com.water.dispatch.entity.*;
4
 import com.water.dispatch.entity.*;
4
 import com.water.dispatch.mapper.*;
5
 import com.water.dispatch.mapper.*;
5
 import lombok.RequiredArgsConstructor;
6
 import lombok.RequiredArgsConstructor;
6
 import org.springframework.stereotype.Service;
7
 import org.springframework.stereotype.Service;
7
-import java.time.*; import java.util.*;
8
-@Service @RequiredArgsConstructor
8
+
9
+import java.time.*;
10
+import java.util.*;
11
+
12
+/**
13
+ * 调度工作台 - 原有业务(值班/工单/策略/预案)
14
+ */
15
+@Service
16
+@RequiredArgsConstructor
9
 public class DispatchBizService {
17
 public class DispatchBizService {
10
     private final DutyScheduleMapper dutyMapper;
18
     private final DutyScheduleMapper dutyMapper;
11
     private final DispatchCommandMapper cmdMapper;
19
     private final DispatchCommandMapper cmdMapper;
16
 
24
 
17
     public List<DutySchedule> getTodayDuty() {
25
     public List<DutySchedule> getTodayDuty() {
18
         return dutyMapper.selectList(new LambdaQueryWrapper<DutySchedule>()
26
         return dutyMapper.selectList(new LambdaQueryWrapper<DutySchedule>()
19
-            .eq(DutySchedule::getDutyDate, LocalDate.now()));
27
+                .eq(DutySchedule::getDutyDate, LocalDate.now()));
20
     }
28
     }
21
-    public Map<String,Object> createCommand(Map<String,Object> req) {
29
+
30
+    public Map<String, Object> createCommand(Map<String, Object> req) {
22
         DispatchCommand c = new DispatchCommand();
31
         DispatchCommand c = new DispatchCommand();
23
-        c.setCmdNo("CMD-" + System.currentTimeMillis());
24
-        c.setTitle((String)req.get("title")); c.setContent((String)req.get("content"));
25
-        c.setType((String)req.getOrDefault("type","常规")); c.setStatus(0);
32
+        c.setCommandNo("CMD-" + System.currentTimeMillis());
33
+        c.setTitle((String) req.get("title"));
34
+        c.setContent((String) req.get("content"));
35
+        c.setCommandType((String) req.getOrDefault("commandType", "NORMAL"));
36
+        c.setPriority((String) req.getOrDefault("priority", "MEDIUM"));
37
+        c.setStatus("DRAFT");
26
         cmdMapper.insert(c);
38
         cmdMapper.insert(c);
27
-        return Map.of("id",c.getId(),"cmdNo",c.getCmdNo());
39
+        return Map.of("id", c.getId(), "commandNo", c.getCommandNo());
28
     }
40
     }
29
-    public Map<String,Object> issueCommand(String cmdNo) {
41
+
42
+    public Map<String, Object> issueCommand(String commandNo) {
30
         DispatchCommand c = cmdMapper.selectOne(new LambdaQueryWrapper<DispatchCommand>()
43
         DispatchCommand c = cmdMapper.selectOne(new LambdaQueryWrapper<DispatchCommand>()
31
-            .eq(DispatchCommand::getCmdNo, cmdNo));
32
-        if(c==null) throw new RuntimeException("指令不存在");
33
-        c.setStatus(1); c.setIssuedTime(LocalDateTime.now());
44
+                .eq(DispatchCommand::getCommandNo, commandNo));
45
+        if (c == null) throw new RuntimeException("指令不存在");
46
+        c.setStatus("ISSUED");
47
+        c.setIssuedAt(LocalDateTime.now());
34
         cmdMapper.updateById(c);
48
         cmdMapper.updateById(c);
35
-        return Map.of("cmdNo",cmdNo,"status",1);
49
+        return Map.of("commandNo", commandNo, "status", c.getStatus());
36
     }
50
     }
37
-    public List<DispatchCommand> listCommands(Integer status) {
51
+
52
+    public List<DispatchCommand> listCommands(String status) {
38
         return cmdMapper.selectList(new LambdaQueryWrapper<DispatchCommand>()
53
         return cmdMapper.selectList(new LambdaQueryWrapper<DispatchCommand>()
39
-            .eq(status!=null, DispatchCommand::getStatus, status));
54
+                .eq(status != null, DispatchCommand::getStatus, status));
40
     }
55
     }
41
-    public Map<String,Object> createWorkOrder(Map<String,Object> req) {
56
+
57
+    public Map<String, Object> createWorkOrder(Map<String, Object> req) {
42
         WorkOrder w = new WorkOrder();
58
         WorkOrder w = new WorkOrder();
43
         w.setOrderNo("WO-" + System.currentTimeMillis());
59
         w.setOrderNo("WO-" + System.currentTimeMillis());
44
-        w.setTitle((String)req.get("title")); w.setDescription((String)req.get("description"));
45
-        w.setType((String)req.getOrDefault("type","维修")); w.setStatus(0);
46
-        w.setPriority((String)req.getOrDefault("priority","中"));
60
+        w.setTitle((String) req.get("title"));
61
+        w.setDescription((String) req.get("description"));
62
+        w.setType((String) req.getOrDefault("type", "维修"));
63
+        w.setStatus(0);
64
+        w.setPriority((String) req.getOrDefault("priority", "中"));
47
         woMapper.insert(w);
65
         woMapper.insert(w);
48
-        return Map.of("id",w.getId(),"orderNo",w.getOrderNo());
66
+        return Map.of("id", w.getId(), "orderNo", w.getOrderNo());
49
     }
67
     }
50
-    public Map<String,Object> updateWorkOrderStatus(Long id, int status) {
68
+
69
+    public Map<String, Object> updateWorkOrderStatus(Long id, int status) {
51
         WorkOrder w = woMapper.selectById(id);
70
         WorkOrder w = woMapper.selectById(id);
52
-        if(w==null) throw new RuntimeException("工单不存在");
71
+        if (w == null) throw new RuntimeException("工单不存在");
53
         w.setStatus(status);
72
         w.setStatus(status);
54
-        if(status==2) w.setCompletedAt(LocalDateTime.now());
73
+        if (status == 2) w.setCompletedAt(LocalDateTime.now());
55
         woMapper.updateById(w);
74
         woMapper.updateById(w);
56
-        return Map.of("id",id,"status",status);
75
+        return Map.of("id", id, "status", status);
57
     }
76
     }
77
+
58
     public void addDutyLog(Long scheduleId, Long userId, String type, String content) {
78
     public void addDutyLog(Long scheduleId, Long userId, String type, String content) {
59
         DutyLog l = new DutyLog();
79
         DutyLog l = new DutyLog();
60
-        l.setScheduleId(scheduleId); l.setUserId(userId);
61
-        l.setLogType(type); l.setContent(content);
80
+        l.setScheduleId(scheduleId);
81
+        l.setUserId(userId);
82
+        l.setLogType(type);
83
+        l.setContent(content);
62
         logMapper.insert(l);
84
         logMapper.insert(l);
63
     }
85
     }
86
+
64
     public List<DutyLog> getDutyLogs(Long scheduleId) {
87
     public List<DutyLog> getDutyLogs(Long scheduleId) {
65
         return logMapper.selectList(new LambdaQueryWrapper<DutyLog>()
88
         return logMapper.selectList(new LambdaQueryWrapper<DutyLog>()
66
-            .eq(DutyLog::getScheduleId, scheduleId));
89
+                .eq(DutyLog::getScheduleId, scheduleId));
67
     }
90
     }
91
+
68
     public List<DispatchStrategy> listStrategies(String type) {
92
     public List<DispatchStrategy> listStrategies(String type) {
69
         return stratMapper.selectList(new LambdaQueryWrapper<DispatchStrategy>()
93
         return stratMapper.selectList(new LambdaQueryWrapper<DispatchStrategy>()
70
-            .eq(type!=null, DispatchStrategy::getType, type));
94
+                .eq(type != null, DispatchStrategy::getType, type));
71
     }
95
     }
96
+
72
     public List<EmergencyPlan> listPlans(String type) {
97
     public List<EmergencyPlan> listPlans(String type) {
73
         return planMapper.selectList(new LambdaQueryWrapper<EmergencyPlan>()
98
         return planMapper.selectList(new LambdaQueryWrapper<EmergencyPlan>()
74
-            .eq(type!=null, EmergencyPlan::getType, type));
99
+                .eq(type != null, EmergencyPlan::getType, type));
75
     }
100
     }
76
-    public Map<String,Object> simulateEmergency(String type, double lng, double lat) {
77
-        return Map.of("type",type,"lng",lng,"lat",lat,
78
-            "affectedArea","半径500米","affectedUsers",120,
79
-            "estimatedDuration","4小时");
101
+
102
+    public Map<String, Object> simulateEmergency(String type, double lng, double lat) {
103
+        return Map.of("type", type, "lng", lng, "lat", lat,
104
+                "affectedArea", "半径500米", "affectedUsers", 120,
105
+                "estimatedDuration", "4小时");
80
     }
106
     }
81
 }
107
 }

+ 73
- 0
wm-dispatch/src/main/resources/db/V2__command_lifecycle.sql Datei anzeigen

1
+-- V2: 调度指令全生命周期管理
2
+-- 新增/重建表,支持 创建→下发→接收确认→执行反馈→完成归档 全流程
3
+
4
+-- 1. 调度指令主表(增强)
5
+CREATE TABLE IF NOT EXISTS disp_dispatch_command (
6
+    id              BIGSERIAL PRIMARY KEY,
7
+    command_no      VARCHAR(64)  NOT NULL UNIQUE,
8
+    title           VARCHAR(200) NOT NULL,
9
+    content         TEXT,
10
+    command_type    VARCHAR(30)  NOT NULL DEFAULT 'NORMAL',   -- NORMAL / EMERGENCY / MAINTENANCE
11
+    priority        VARCHAR(20)  NOT NULL DEFAULT 'MEDIUM',   -- LOW / MEDIUM / HIGH / URGENT
12
+    status          VARCHAR(30)  NOT NULL DEFAULT 'DRAFT',    -- DRAFT / ISSUED / RECEIVED / EXECUTING / COMPLETED / REJECTED / CANCELLED
13
+    issuer_id       BIGINT,
14
+    issuer_name     VARCHAR(64),
15
+    receiver_id     BIGINT,
16
+    receiver_name   VARCHAR(64),
17
+    facility_id     BIGINT,
18
+    deadline        TIMESTAMP,
19
+    issued_at       TIMESTAMP,
20
+    received_at     TIMESTAMP,
21
+    executed_at     TIMESTAMP,
22
+    completed_at    TIMESTAMP,
23
+    rejected_at     TIMESTAMP,
24
+    reject_reason   TEXT,
25
+    execute_result  TEXT,
26
+    remark          TEXT,
27
+    deleted         INT DEFAULT 0,
28
+    created_at      TIMESTAMP DEFAULT NOW(),
29
+    updated_at      TIMESTAMP DEFAULT NOW()
30
+);
31
+
32
+CREATE INDEX idx_cmd_status      ON disp_dispatch_command(status);
33
+CREATE INDEX idx_cmd_type        ON disp_dispatch_command(command_type);
34
+CREATE INDEX idx_cmd_priority    ON disp_dispatch_command(priority);
35
+CREATE INDEX idx_cmd_issuer      ON disp_dispatch_command(issuer_id);
36
+CREATE INDEX idx_cmd_receiver    ON disp_dispatch_command(receiver_id);
37
+CREATE INDEX idx_cmd_deadline    ON disp_dispatch_command(deadline);
38
+CREATE INDEX idx_cmd_created_at  ON disp_dispatch_command(created_at);
39
+
40
+-- 2. 执行记录表
41
+CREATE TABLE IF NOT EXISTS disp_command_execution_record (
42
+    id              BIGSERIAL PRIMARY KEY,
43
+    command_id      BIGINT       NOT NULL,
44
+    command_no      VARCHAR(64),
45
+    executor_id     BIGINT       NOT NULL,
46
+    executor_name   VARCHAR(64),
47
+    action          VARCHAR(30)  NOT NULL,    -- RECEIVE / START / PROGRESS / COMPLETE / REJECT
48
+    description     TEXT,
49
+    attachments     TEXT,                      -- JSON array of file URLs
50
+    progress        INT DEFAULT 0,            -- 0-100
51
+    created_at      TIMESTAMP DEFAULT NOW()
52
+);
53
+
54
+CREATE INDEX idx_exec_cmd_id ON disp_command_execution_record(command_id);
55
+CREATE INDEX idx_exec_cmd_no ON disp_command_execution_record(command_no);
56
+
57
+-- 3. 过程追踪表(时间线)
58
+CREATE TABLE IF NOT EXISTS disp_command_tracking (
59
+    id              BIGSERIAL PRIMARY KEY,
60
+    command_id      BIGINT       NOT NULL,
61
+    command_no      VARCHAR(64),
62
+    stage           VARCHAR(30)  NOT NULL,    -- CREATED / ISSUED / RECEIVED / EXECUTING / COMPLETED / REJECTED / CANCELLED
63
+    operator_id     BIGINT,
64
+    operator_name   VARCHAR(64),
65
+    action_desc     VARCHAR(500),
66
+    from_status     VARCHAR(30),
67
+    to_status       VARCHAR(30),
68
+    created_at      TIMESTAMP DEFAULT NOW()
69
+);
70
+
71
+CREATE INDEX idx_track_cmd_id ON disp_command_tracking(command_id);
72
+CREATE INDEX idx_track_cmd_no ON disp_command_tracking(command_no);
73
+CREATE INDEX idx_track_stage  ON disp_command_tracking(stage);

+ 248
- 0
wm-dispatch/src/test/java/com/water/dispatch/service/CommandLifecycleServiceTest.java Datei anzeigen

1
+package com.water.dispatch.service;
2
+
3
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
4
+import com.water.common.core.exception.BusinessException;
5
+import com.water.dispatch.entity.CommandExecutionRecord;
6
+import com.water.dispatch.entity.CommandTracking;
7
+import com.water.dispatch.entity.DispatchCommand;
8
+import com.water.dispatch.entity.dto.CommandCreateRequest;
9
+import com.water.dispatch.mapper.CommandExecutionRecordMapper;
10
+import com.water.dispatch.mapper.CommandTrackingMapper;
11
+import com.water.dispatch.mapper.DispatchCommandMapper;
12
+import org.junit.jupiter.api.Test;
13
+import org.junit.jupiter.api.extension.ExtendWith;
14
+import org.mockito.ArgumentCaptor;
15
+import org.mockito.InjectMocks;
16
+import org.mockito.Mock;
17
+import org.mockito.junit.jupiter.MockitoExtension;
18
+
19
+import static org.junit.jupiter.api.Assertions.*;
20
+import static org.mockito.ArgumentMatchers.any;
21
+import static org.mockito.Mockito.*;
22
+
23
+@ExtendWith(MockitoExtension.class)
24
+class CommandLifecycleServiceTest {
25
+
26
+    @Mock
27
+    private DispatchCommandMapper commandMapper;
28
+    @Mock
29
+    private CommandExecutionRecordMapper executionMapper;
30
+    @Mock
31
+    private CommandTrackingMapper trackingMapper;
32
+
33
+    @InjectMocks
34
+    private CommandLifecycleService lifecycleService;
35
+
36
+    @Test
37
+    void testCreateCommand() {
38
+        when(commandMapper.insert(any())).thenReturn(1);
39
+        when(trackingMapper.insert(any())).thenReturn(1);
40
+
41
+        CommandCreateRequest req = new CommandCreateRequest();
42
+        req.setTitle("测试指令");
43
+        req.setContent("测试内容");
44
+        req.setCommandType("NORMAL");
45
+        req.setPriority("HIGH");
46
+        req.setIssuerId(1L);
47
+        req.setIssuerName("张三");
48
+
49
+        DispatchCommand result = lifecycleService.create(req);
50
+
51
+        assertNotNull(result.getCommandNo());
52
+        assertEquals("DRAFT", result.getStatus());
53
+        assertEquals("测试指令", result.getTitle());
54
+        assertEquals("NORMAL", result.getCommandType());
55
+        assertEquals("HIGH", result.getPriority());
56
+        verify(commandMapper).insert(any());
57
+        verify(trackingMapper).insert(any());
58
+    }
59
+
60
+    @Test
61
+    void testIssueCommand() {
62
+        DispatchCommand cmd = buildCommand(1L, "DRAFT");
63
+        when(commandMapper.selectById(1L)).thenReturn(cmd);
64
+        when(commandMapper.updateById(any())).thenReturn(1);
65
+        when(trackingMapper.insert(any())).thenReturn(1);
66
+
67
+        DispatchCommand result = lifecycleService.issue(1L);
68
+
69
+        assertEquals("ISSUED", result.getStatus());
70
+        assertNotNull(result.getIssuedAt());
71
+        verify(commandMapper).updateById(any());
72
+        verify(trackingMapper).insert(any());
73
+    }
74
+
75
+    @Test
76
+    void testIssueCommandWrongStatus() {
77
+        DispatchCommand cmd = buildCommand(1L, "ISSUED");
78
+        when(commandMapper.selectById(1L)).thenReturn(cmd);
79
+
80
+        assertThrows(BusinessException.class, () -> lifecycleService.issue(1L));
81
+    }
82
+
83
+    @Test
84
+    void testReceiveCommand() {
85
+        DispatchCommand cmd = buildCommand(1L, "ISSUED");
86
+        when(commandMapper.selectById(1L)).thenReturn(cmd);
87
+        when(commandMapper.updateById(any())).thenReturn(1);
88
+        when(executionMapper.insert(any())).thenReturn(1);
89
+        when(trackingMapper.insert(any())).thenReturn(1);
90
+
91
+        DispatchCommand result = lifecycleService.receive(1L, 2L, "李四");
92
+
93
+        assertEquals("RECEIVED", result.getStatus());
94
+        assertNotNull(result.getReceivedAt());
95
+        assertEquals(2L, result.getReceiverId());
96
+
97
+        ArgumentCaptor<CommandExecutionRecord> execCaptor = ArgumentCaptor.forClass(CommandExecutionRecord.class);
98
+        verify(executionMapper).insert(execCaptor.capture());
99
+        assertEquals("RECEIVE", execCaptor.getValue().getAction());
100
+    }
101
+
102
+    @Test
103
+    void testStartExecution() {
104
+        DispatchCommand cmd = buildCommand(1L, "RECEIVED");
105
+        when(commandMapper.selectById(1L)).thenReturn(cmd);
106
+        when(commandMapper.updateById(any())).thenReturn(1);
107
+        when(executionMapper.insert(any())).thenReturn(1);
108
+        when(trackingMapper.insert(any())).thenReturn(1);
109
+
110
+        DispatchCommand result = lifecycleService.startExecution(1L, 2L, "李四");
111
+
112
+        assertEquals("EXECUTING", result.getStatus());
113
+        assertNotNull(result.getExecutedAt());
114
+    }
115
+
116
+    @Test
117
+    void testCompleteCommand() {
118
+        DispatchCommand cmd = buildCommand(1L, "EXECUTING");
119
+        when(commandMapper.selectById(1L)).thenReturn(cmd);
120
+        when(commandMapper.updateById(any())).thenReturn(1);
121
+        when(executionMapper.insert(any())).thenReturn(1);
122
+        when(trackingMapper.insert(any())).thenReturn(1);
123
+
124
+        DispatchCommand result = lifecycleService.complete(1L, 2L, "李四", "已完成管道修复");
125
+
126
+        assertEquals("COMPLETED", result.getStatus());
127
+        assertNotNull(result.getCompletedAt());
128
+        assertEquals("已完成管道修复", result.getExecuteResult());
129
+
130
+        ArgumentCaptor<CommandExecutionRecord> execCaptor = ArgumentCaptor.forClass(CommandExecutionRecord.class);
131
+        verify(executionMapper).insert(execCaptor.capture());
132
+        assertEquals(100, execCaptor.getValue().getProgress());
133
+    }
134
+
135
+    @Test
136
+    void testRejectCommand() {
137
+        DispatchCommand cmd = buildCommand(1L, "ISSUED");
138
+        when(commandMapper.selectById(1L)).thenReturn(cmd);
139
+        when(commandMapper.updateById(any())).thenReturn(1);
140
+        when(executionMapper.insert(any())).thenReturn(1);
141
+        when(trackingMapper.insert(any())).thenReturn(1);
142
+
143
+        DispatchCommand result = lifecycleService.reject(1L, 2L, "李四", "信息不足,无法执行");
144
+
145
+        assertEquals("REJECTED", result.getStatus());
146
+        assertEquals("信息不足,无法执行", result.getRejectReason());
147
+        assertNotNull(result.getRejectedAt());
148
+    }
149
+
150
+    @Test
151
+    void testRejectCompletedCommand() {
152
+        DispatchCommand cmd = buildCommand(1L, "COMPLETED");
153
+        when(commandMapper.selectById(1L)).thenReturn(cmd);
154
+
155
+        assertThrows(BusinessException.class,
156
+                () -> lifecycleService.reject(1L, 2L, "李四", "原因"));
157
+    }
158
+
159
+    @Test
160
+    void testCancelCommand() {
161
+        DispatchCommand cmd = buildCommand(1L, "DRAFT");
162
+        when(commandMapper.selectById(1L)).thenReturn(cmd);
163
+        when(commandMapper.updateById(any())).thenReturn(1);
164
+        when(trackingMapper.insert(any())).thenReturn(1);
165
+
166
+        DispatchCommand result = lifecycleService.cancel(1L, 1L, "张三", "计划变更");
167
+
168
+        assertEquals("CANCELLED", result.getStatus());
169
+    }
170
+
171
+    @Test
172
+    void testCancelAlreadyCancelledCommand() {
173
+        DispatchCommand cmd = buildCommand(1L, "CANCELLED");
174
+        when(commandMapper.selectById(1L)).thenReturn(cmd);
175
+
176
+        assertThrows(BusinessException.class,
177
+                () -> lifecycleService.cancel(1L, 1L, "张三", "reason"));
178
+    }
179
+
180
+    @Test
181
+    void testCommandNotFound() {
182
+        when(commandMapper.selectById(999L)).thenReturn(null);
183
+        assertThrows(BusinessException.class, () -> lifecycleService.issue(999L));
184
+    }
185
+
186
+    @Test
187
+    void testReportProgress() {
188
+        DispatchCommand cmd = buildCommand(1L, "EXECUTING");
189
+        when(commandMapper.selectById(1L)).thenReturn(cmd);
190
+        when(executionMapper.insert(any())).thenReturn(1);
191
+
192
+        DispatchCommand result = lifecycleService.reportProgress(1L, 2L, "李四", "完成50%", 50);
193
+
194
+        assertEquals("EXECUTING", result.getStatus()); // status unchanged
195
+        ArgumentCaptor<CommandExecutionRecord> captor = ArgumentCaptor.forClass(CommandExecutionRecord.class);
196
+        verify(executionMapper).insert(captor.capture());
197
+        assertEquals("PROGRESS", captor.getValue().getAction());
198
+        assertEquals(50, captor.getValue().getProgress());
199
+    }
200
+
201
+    @Test
202
+    void testFullLifecycle() {
203
+        // Create
204
+        when(commandMapper.insert(any())).thenReturn(1);
205
+        when(trackingMapper.insert(any())).thenReturn(1);
206
+        when(executionMapper.insert(any())).thenReturn(1);
207
+
208
+        CommandCreateRequest req = new CommandCreateRequest();
209
+        req.setTitle("全流程测试");
210
+        req.setCommandType("EMERGENCY");
211
+        req.setPriority("URGENT");
212
+        req.setIssuerId(1L);
213
+        req.setIssuerName("张三");
214
+
215
+        DispatchCommand cmd = lifecycleService.create(req);
216
+        cmd.setId(100L);
217
+        assertEquals("DRAFT", cmd.getStatus());
218
+
219
+        // Issue
220
+        when(commandMapper.selectById(100L)).thenReturn(cmd);
221
+        when(commandMapper.updateById(any())).thenReturn(1);
222
+        lifecycleService.issue(100L);
223
+        assertEquals("ISSUED", cmd.getStatus());
224
+
225
+        // Receive
226
+        lifecycleService.receive(100L, 2L, "李四");
227
+        assertEquals("RECEIVED", cmd.getStatus());
228
+
229
+        // Start
230
+        lifecycleService.startExecution(100L, 2L, "李四");
231
+        assertEquals("EXECUTING", cmd.getStatus());
232
+
233
+        // Complete
234
+        lifecycleService.complete(100L, 2L, "李四", "抢修完成");
235
+        assertEquals("COMPLETED", cmd.getStatus());
236
+    }
237
+
238
+    private DispatchCommand buildCommand(Long id, String status) {
239
+        DispatchCommand cmd = new DispatchCommand();
240
+        cmd.setId(id);
241
+        cmd.setCommandNo("CMD-TEST-001");
242
+        cmd.setTitle("测试指令");
243
+        cmd.setStatus(status);
244
+        cmd.setIssuerId(1L);
245
+        cmd.setIssuerName("张三");
246
+        return cmd;
247
+    }
248
+}

+ 115
- 0
wm-dispatch/src/test/java/com/water/dispatch/service/CommandTrackingServiceTest.java Datei anzeigen

1
+package com.water.dispatch.service;
2
+
3
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
4
+import com.water.dispatch.entity.CommandExecutionRecord;
5
+import com.water.dispatch.entity.CommandTracking;
6
+import com.water.dispatch.mapper.CommandExecutionRecordMapper;
7
+import com.water.dispatch.mapper.CommandTrackingMapper;
8
+import org.junit.jupiter.api.Test;
9
+import org.junit.jupiter.api.extension.ExtendWith;
10
+import org.mockito.InjectMocks;
11
+import org.mockito.Mock;
12
+import org.mockito.junit.jupiter.MockitoExtension;
13
+
14
+import java.time.LocalDateTime;
15
+import java.util.List;
16
+import java.util.Map;
17
+
18
+import static org.junit.jupiter.api.Assertions.*;
19
+import static org.mockito.ArgumentMatchers.any;
20
+import static org.mockito.Mockito.when;
21
+
22
+@ExtendWith(MockitoExtension.class)
23
+class CommandTrackingServiceTest {
24
+
25
+    @Mock
26
+    private CommandTrackingMapper trackingMapper;
27
+    @Mock
28
+    private CommandExecutionRecordMapper executionMapper;
29
+
30
+    @InjectMocks
31
+    private CommandTrackingService trackingService;
32
+
33
+    @Test
34
+    void testGetTimeline() {
35
+        CommandTracking t1 = new CommandTracking();
36
+        t1.setId(1L);
37
+        t1.setCommandId(100L);
38
+        t1.setStage("CREATED");
39
+        t1.setCreatedAt(LocalDateTime.of(2026, 6, 14, 10, 0));
40
+
41
+        CommandTracking t2 = new CommandTracking();
42
+        t2.setId(2L);
43
+        t2.setCommandId(100L);
44
+        t2.setStage("ISSUED");
45
+        t2.setCreatedAt(LocalDateTime.of(2026, 6, 14, 10, 5));
46
+
47
+        when(trackingMapper.selectList(any(LambdaQueryWrapper.class)))
48
+                .thenReturn(List.of(t1, t2));
49
+
50
+        List<CommandTracking> result = trackingService.getTimeline(100L);
51
+        assertEquals(2, result.size());
52
+        assertEquals("CREATED", result.get(0).getStage());
53
+    }
54
+
55
+    @Test
56
+    void testGetExecutionRecords() {
57
+        CommandExecutionRecord r1 = new CommandExecutionRecord();
58
+        r1.setId(1L);
59
+        r1.setCommandId(100L);
60
+        r1.setAction("RECEIVE");
61
+        r1.setProgress(0);
62
+
63
+        when(executionMapper.selectList(any(LambdaQueryWrapper.class)))
64
+                .thenReturn(List.of(r1));
65
+
66
+        List<CommandExecutionRecord> result = trackingService.getExecutionRecords(100L);
67
+        assertEquals(1, result.size());
68
+        assertEquals("RECEIVE", result.get(0).getAction());
69
+    }
70
+
71
+    @Test
72
+    void testGetFullTimeline() {
73
+        CommandTracking t = new CommandTracking();
74
+        t.setId(1L);
75
+        t.setCommandId(100L);
76
+        t.setStage("CREATED");
77
+        t.setOperatorName("张三");
78
+        t.setActionDesc("创建指令");
79
+        t.setCreatedAt(LocalDateTime.of(2026, 6, 14, 10, 0));
80
+
81
+        CommandExecutionRecord r = new CommandExecutionRecord();
82
+        r.setId(1L);
83
+        r.setCommandId(100L);
84
+        r.setAction("RECEIVE");
85
+        r.setExecutorName("李四");
86
+        r.setDescription("确认接收");
87
+        r.setProgress(0);
88
+        r.setCreatedAt(LocalDateTime.of(2026, 6, 14, 10, 5));
89
+
90
+        when(trackingMapper.selectList(any(LambdaQueryWrapper.class)))
91
+                .thenReturn(List.of(t));
92
+        when(executionMapper.selectList(any(LambdaQueryWrapper.class)))
93
+                .thenReturn(List.of(r));
94
+
95
+        List<Map<String, Object>> timeline = trackingService.getFullTimeline(100L);
96
+        assertEquals(2, timeline.size());
97
+        assertEquals("TRACKING", timeline.get(0).get("type"));
98
+        assertEquals("EXECUTION", timeline.get(1).get("type"));
99
+    }
100
+
101
+    @Test
102
+    void testGetTimelineByCommandNo() {
103
+        CommandTracking t = new CommandTracking();
104
+        t.setCommandNo("CMD-20260614-001");
105
+        t.setStage("ISSUED");
106
+        t.setCreatedAt(LocalDateTime.now());
107
+
108
+        when(trackingMapper.selectList(any(LambdaQueryWrapper.class)))
109
+                .thenReturn(List.of(t));
110
+
111
+        List<CommandTracking> result = trackingService.getTimelineByCommandNo("CMD-20260614-001");
112
+        assertEquals(1, result.size());
113
+        assertEquals("ISSUED", result.get(0).getStage());
114
+    }
115
+}