소스 검색

feat(wm-patrol): #76 巡检执行(检查项/GPS轨迹/异常上报)

- Entity: PatrolExecution, CheckItemRecord, GpsTrackPoint, PatrolIssue
- DTO: StartExecutionRequest, CompleteExecutionRequest, CheckItemRecordRequest, GpsTrackBatchRequest, PatrolIssueRequest
- Mapper: PatrolExecutionMapper, CheckItemRecordMapper, GpsTrackPointMapper, PatrolIssueMapper
- Service: ExecutionService (执行流程), TrackService (GPS轨迹), IssueService (异常上报)
- Controller: ExecutionController 22 endpoints at /api/patrol/execution/*
- DDL: V2__patrol_execution.sql (4 tables with indexes)
- Config: MyBatisPlusConfig with pagination + MetaObjectHandler
- Unit tests: ExecutionServiceTest(7), TrackServiceTest(6), IssueServiceTest(5) = 18 test cases
bot_dev2 5 일 전
부모
커밋
3d5e7ca35a
38개의 변경된 파일3836개의 추가작업 그리고 195개의 파일을 삭제
  1. 111
    0
      db/postgresql/V2__patrol_execution.sql
  2. 2
    0
      wm-patrol/src/main/java/com/water/patrol/PatrolApplication.java
  3. 38
    0
      wm-patrol/src/main/java/com/water/patrol/config/MyBatisPlusConfig.java
  4. 197
    0
      wm-patrol/src/main/java/com/water/patrol/controller/ExecutionController.java
  5. 287
    77
      wm-patrol/src/main/java/com/water/patrol/controller/PatrolController.java
  6. 79
    0
      wm-patrol/src/main/java/com/water/patrol/entity/CheckItemRecord.java
  7. 54
    0
      wm-patrol/src/main/java/com/water/patrol/entity/GpsTrackPoint.java
  8. 53
    0
      wm-patrol/src/main/java/com/water/patrol/entity/PatrolCheckpoint.java
  9. 90
    0
      wm-patrol/src/main/java/com/water/patrol/entity/PatrolExecution.java
  10. 84
    0
      wm-patrol/src/main/java/com/water/patrol/entity/PatrolIssue.java
  11. 53
    0
      wm-patrol/src/main/java/com/water/patrol/entity/PatrolRoute.java
  12. 75
    0
      wm-patrol/src/main/java/com/water/patrol/entity/PatrolTask.java
  13. 41
    0
      wm-patrol/src/main/java/com/water/patrol/entity/PatrolWorker.java
  14. 64
    0
      wm-patrol/src/main/java/com/water/patrol/entity/dto/CheckItemRecordRequest.java
  15. 27
    0
      wm-patrol/src/main/java/com/water/patrol/entity/dto/CompleteExecutionRequest.java
  16. 52
    0
      wm-patrol/src/main/java/com/water/patrol/entity/dto/GpsTrackBatchRequest.java
  17. 49
    0
      wm-patrol/src/main/java/com/water/patrol/entity/dto/PatrolIssueRequest.java
  18. 42
    0
      wm-patrol/src/main/java/com/water/patrol/entity/dto/StartExecutionRequest.java
  19. 18
    0
      wm-patrol/src/main/java/com/water/patrol/mapper/CheckItemRecordMapper.java
  20. 17
    0
      wm-patrol/src/main/java/com/water/patrol/mapper/GpsTrackPointMapper.java
  21. 19
    0
      wm-patrol/src/main/java/com/water/patrol/mapper/PatrolCheckpointMapper.java
  22. 24
    0
      wm-patrol/src/main/java/com/water/patrol/mapper/PatrolExecutionMapper.java
  23. 18
    0
      wm-patrol/src/main/java/com/water/patrol/mapper/PatrolIssueMapper.java
  24. 20
    0
      wm-patrol/src/main/java/com/water/patrol/mapper/PatrolRouteMapper.java
  25. 25
    0
      wm-patrol/src/main/java/com/water/patrol/mapper/PatrolTaskMapper.java
  26. 20
    0
      wm-patrol/src/main/java/com/water/patrol/mapper/PatrolWorkerMapper.java
  27. 262
    0
      wm-patrol/src/main/java/com/water/patrol/service/ExecutionService.java
  28. 135
    0
      wm-patrol/src/main/java/com/water/patrol/service/IssueService.java
  29. 0
    118
      wm-patrol/src/main/java/com/water/patrol/service/PatrolService.java
  30. 292
    0
      wm-patrol/src/main/java/com/water/patrol/service/RouteService.java
  31. 362
    0
      wm-patrol/src/main/java/com/water/patrol/service/TaskService.java
  32. 176
    0
      wm-patrol/src/main/java/com/water/patrol/service/TrackService.java
  33. 178
    0
      wm-patrol/src/main/java/com/water/patrol/service/WorkerService.java
  34. 170
    0
      wm-patrol/src/main/resources/db/V1__patrol_route.sql
  35. 211
    0
      wm-patrol/src/test/java/com/water/patrol/service/ExecutionServiceTest.java
  36. 138
    0
      wm-patrol/src/test/java/com/water/patrol/service/IssueServiceTest.java
  37. 201
    0
      wm-patrol/src/test/java/com/water/patrol/service/PatrolServiceTest.java
  38. 152
    0
      wm-patrol/src/test/java/com/water/patrol/service/TrackServiceTest.java

+ 111
- 0
db/postgresql/V2__patrol_execution.sql 파일 보기

@@ -0,0 +1,111 @@
1
+-- =============================================
2
+-- 智慧水务管理系统 - PostgreSQL DDL
3
+-- 版本: V2
4
+-- 描述: 巡检执行模块(检查项/GPS轨迹/异常上报)
5
+-- =============================================
6
+
7
+-- ==================== 巡检执行表 ====================
8
+CREATE TABLE IF NOT EXISTS patrol_execution (
9
+    id              BIGSERIAL PRIMARY KEY,
10
+    task_id         BIGINT NOT NULL REFERENCES patrol_task(id),
11
+    inspector_id    BIGINT NOT NULL REFERENCES patrol_worker(id),
12
+    inspector_name  VARCHAR(50),
13
+    status          VARCHAR(20) NOT NULL DEFAULT 'pending',     -- pending/in_progress/completed/aborted
14
+    start_time      TIMESTAMP,
15
+    end_time        TIMESTAMP,
16
+    total_check_items   INT DEFAULT 0,
17
+    completed_check_items INT DEFAULT 0,
18
+    issue_count     INT DEFAULT 0,
19
+    total_distance  DECIMAL(10,2) DEFAULT 0,                    -- 巡检总距离(km)
20
+    start_lng       DOUBLE PRECISION,
21
+    start_lat       DOUBLE PRECISION,
22
+    end_lng         DOUBLE PRECISION,
23
+    end_lat         DOUBLE PRECISION,
24
+    remark          VARCHAR(500),
25
+    deleted         SMALLINT DEFAULT 0,
26
+    created_at      TIMESTAMP DEFAULT NOW(),
27
+    updated_at      TIMESTAMP DEFAULT NOW()
28
+);
29
+COMMENT ON TABLE patrol_execution IS '巡检执行表';
30
+COMMENT ON COLUMN patrol_execution.status IS '状态: pending(待开始)/in_progress(进行中)/completed(已完成)/aborted(已中止)';
31
+COMMENT ON COLUMN patrol_execution.total_distance IS '巡检总距离(km)';
32
+CREATE INDEX IF NOT EXISTS idx_patrol_execution_task ON patrol_execution(task_id);
33
+CREATE INDEX IF NOT EXISTS idx_patrol_execution_inspector ON patrol_execution(inspector_id);
34
+CREATE INDEX IF NOT EXISTS idx_patrol_execution_status ON patrol_execution(status);
35
+CREATE INDEX IF NOT EXISTS idx_patrol_execution_start_time ON patrol_execution(start_time);
36
+
37
+-- ==================== 检查项记录表 ====================
38
+CREATE TABLE IF NOT EXISTS check_item_record (
39
+    id              BIGSERIAL PRIMARY KEY,
40
+    execution_id    BIGINT NOT NULL REFERENCES patrol_execution(id),
41
+    point_seq       INT,                                        -- 检查点序号
42
+    point_name      VARCHAR(100),                               -- 检查点名称
43
+    item_name       VARCHAR(200),                               -- 检查项名称
44
+    check_status    VARCHAR(20) NOT NULL DEFAULT 'normal',      -- normal/abnormal/skipped
45
+    record_value    DOUBLE PRECISION,                           -- 数值录入
46
+    value_unit      VARCHAR(20),                                -- 数值单位
47
+    description     TEXT,                                       -- 文字描述
48
+    photo_urls      TEXT,                                       -- 照片URL列表(逗号分隔)
49
+    remark          VARCHAR(500),
50
+    record_time     TIMESTAMP NOT NULL DEFAULT NOW(),
51
+    lng             DOUBLE PRECISION,
52
+    lat             DOUBLE PRECISION,
53
+    deleted         SMALLINT DEFAULT 0,
54
+    created_at      TIMESTAMP DEFAULT NOW(),
55
+    updated_at      TIMESTAMP DEFAULT NOW()
56
+);
57
+COMMENT ON TABLE check_item_record IS '检查项记录表';
58
+COMMENT ON COLUMN check_item_record.check_status IS '状态: normal(正常)/abnormal(异常)/skipped(跳过)';
59
+CREATE INDEX IF NOT EXISTS idx_check_item_record_execution ON check_item_record(execution_id);
60
+CREATE INDEX IF NOT EXISTS idx_check_item_record_point ON check_item_record(execution_id, point_seq);
61
+CREATE INDEX IF NOT EXISTS idx_check_item_record_status ON check_item_record(check_status);
62
+
63
+-- ==================== GPS轨迹点表 ====================
64
+CREATE TABLE IF NOT EXISTS gps_track_point (
65
+    id              BIGSERIAL PRIMARY KEY,
66
+    execution_id    BIGINT NOT NULL REFERENCES patrol_execution(id),
67
+    lng             DOUBLE PRECISION NOT NULL,
68
+    lat             DOUBLE PRECISION NOT NULL,
69
+    altitude        DOUBLE PRECISION,                           -- 海拔(米)
70
+    speed           DOUBLE PRECISION,                           -- 速度(m/s)
71
+    bearing         DOUBLE PRECISION,                           -- 方向(角度, 0-360)
72
+    accuracy        DOUBLE PRECISION,                           -- 精度(米)
73
+    record_time     TIMESTAMP NOT NULL DEFAULT NOW(),
74
+    deleted         SMALLINT DEFAULT 0,
75
+    created_at      TIMESTAMP DEFAULT NOW(),
76
+    updated_at      TIMESTAMP DEFAULT NOW()
77
+);
78
+COMMENT ON TABLE gps_track_point IS 'GPS轨迹点表';
79
+CREATE INDEX IF NOT EXISTS idx_gps_track_execution ON gps_track_point(execution_id);
80
+CREATE INDEX IF NOT EXISTS idx_gps_track_time ON gps_track_point(execution_id, record_time);
81
+
82
+-- ==================== 巡检问题表 ====================
83
+CREATE TABLE IF NOT EXISTS patrol_issue (
84
+    id              BIGSERIAL PRIMARY KEY,
85
+    execution_id    BIGINT NOT NULL REFERENCES patrol_execution(id),
86
+    check_record_id BIGINT REFERENCES check_item_record(id),    -- 关联检查项记录(可选)
87
+    issue_type      VARCHAR(30) NOT NULL,                       -- leak/damage/pollution/illegal/other
88
+    severity        VARCHAR(20) NOT NULL DEFAULT 'medium',      -- low/medium/high/critical
89
+    description     TEXT NOT NULL,
90
+    photo_urls      TEXT,                                       -- 照片URL列表(逗号分隔)
91
+    lng             DOUBLE PRECISION,
92
+    lat             DOUBLE PRECISION,
93
+    address         VARCHAR(300),                               -- 地址描述
94
+    handle_status   VARCHAR(20) NOT NULL DEFAULT 'pending',     -- pending/processing/resolved/closed
95
+    work_order_id   BIGINT,                                     -- 关联工单ID
96
+    reporter_id     BIGINT NOT NULL,
97
+    reporter_name   VARCHAR(50),
98
+    report_time     TIMESTAMP NOT NULL DEFAULT NOW(),
99
+    deleted         SMALLINT DEFAULT 0,
100
+    created_at      TIMESTAMP DEFAULT NOW(),
101
+    updated_at      TIMESTAMP DEFAULT NOW()
102
+);
103
+COMMENT ON TABLE patrol_issue IS '巡检问题表';
104
+COMMENT ON COLUMN patrol_issue.issue_type IS '问题类型: leak(漏水)/damage(损坏)/pollution(污染)/illegal(违规)/other(其他)';
105
+COMMENT ON COLUMN patrol_issue.severity IS '严重程度: low(低)/medium(中)/high(高)/critical(紧急)';
106
+COMMENT ON COLUMN patrol_issue.handle_status IS '处理状态: pending(待处理)/processing(处理中)/resolved(已解决)/closed(已关闭)';
107
+CREATE INDEX IF NOT EXISTS idx_patrol_issue_execution ON patrol_issue(execution_id);
108
+CREATE INDEX IF NOT EXISTS idx_patrol_issue_type ON patrol_issue(issue_type);
109
+CREATE INDEX IF NOT EXISTS idx_patrol_issue_severity ON patrol_issue(severity);
110
+CREATE INDEX IF NOT EXISTS idx_patrol_issue_handle_status ON patrol_issue(handle_status);
111
+CREATE INDEX IF NOT EXISTS idx_patrol_issue_report_time ON patrol_issue(report_time);

+ 2
- 0
wm-patrol/src/main/java/com/water/patrol/PatrolApplication.java 파일 보기

@@ -1,9 +1,11 @@
1 1
 package com.water.patrol;
2 2
 
3
+import org.mybatis.spring.annotation.MapperScan;
3 4
 import org.springframework.boot.SpringApplication;
4 5
 import org.springframework.boot.autoconfigure.SpringBootApplication;
5 6
 
6 7
 @SpringBootApplication
8
+@MapperScan("com.water.patrol.mapper")
7 9
 public class PatrolApplication {
8 10
     public static void main(String[] args) {
9 11
         SpringApplication.run(PatrolApplication.class, args);

+ 38
- 0
wm-patrol/src/main/java/com/water/patrol/config/MyBatisPlusConfig.java 파일 보기

@@ -0,0 +1,38 @@
1
+package com.water.patrol.config;
2
+
3
+import com.baomidou.mybatisplus.annotation.DbType;
4
+import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
5
+import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
6
+import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
7
+import org.apache.ibatis.reflection.MetaObject;
8
+import org.springframework.context.annotation.Bean;
9
+import org.springframework.context.annotation.Configuration;
10
+
11
+import java.time.LocalDateTime;
12
+
13
+@Configuration
14
+public class MyBatisPlusConfig {
15
+
16
+    @Bean
17
+    public MybatisPlusInterceptor mybatisPlusInterceptor() {
18
+        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
19
+        interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.POSTGRE_SQL));
20
+        return interceptor;
21
+    }
22
+
23
+    @Bean
24
+    public MetaObjectHandler metaObjectHandler() {
25
+        return new MetaObjectHandler() {
26
+            @Override
27
+            public void insertFill(MetaObject metaObject) {
28
+                this.strictInsertFill(metaObject, "createdAt", LocalDateTime::now, LocalDateTime.class);
29
+                this.strictInsertFill(metaObject, "updatedAt", LocalDateTime::now, LocalDateTime.class);
30
+            }
31
+
32
+            @Override
33
+            public void updateFill(MetaObject metaObject) {
34
+                this.strictUpdateFill(metaObject, "updatedAt", LocalDateTime::now, LocalDateTime.class);
35
+            }
36
+        };
37
+    }
38
+}

+ 197
- 0
wm-patrol/src/main/java/com/water/patrol/controller/ExecutionController.java 파일 보기

@@ -0,0 +1,197 @@
1
+package com.water.patrol.controller;
2
+
3
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
4
+import com.water.common.core.result.R;
5
+import com.water.patrol.entity.*;
6
+import com.water.patrol.entity.dto.*;
7
+import com.water.patrol.service.ExecutionService;
8
+import com.water.patrol.service.IssueService;
9
+import com.water.patrol.service.TrackService;
10
+import io.swagger.v3.oas.annotations.Operation;
11
+import io.swagger.v3.oas.annotations.tags.Tag;
12
+import lombok.RequiredArgsConstructor;
13
+import org.springframework.web.bind.annotation.*;
14
+
15
+import java.util.List;
16
+import java.util.Map;
17
+
18
+@Tag(name = "巡检执行管理")
19
+@RestController
20
+@RequestMapping("/api/patrol/execution")
21
+@RequiredArgsConstructor
22
+public class ExecutionController {
23
+
24
+    private final ExecutionService executionService;
25
+    private final TrackService trackService;
26
+    private final IssueService issueService;
27
+
28
+    // ========== 巡检执行 ==========
29
+
30
+    @Operation(summary = "开始巡检")
31
+    @PostMapping("/start")
32
+    public R<PatrolExecution> startExecution(@RequestBody StartExecutionRequest request) {
33
+        return R.ok(executionService.startExecution(request));
34
+    }
35
+
36
+    @Operation(summary = "获取执行详情")
37
+    @GetMapping("/{id}")
38
+    public R<PatrolExecution> getExecution(@PathVariable Long id) {
39
+        return R.ok(executionService.getExecution(id));
40
+    }
41
+
42
+    @Operation(summary = "获取某任务的执行列表")
43
+    @GetMapping("/task/{taskId}")
44
+    public R<List<PatrolExecution>> getExecutionsByTask(@PathVariable Long taskId) {
45
+        return R.ok(executionService.getExecutionsByTaskId(taskId));
46
+    }
47
+
48
+    @Operation(summary = "分页查询执行列表")
49
+    @GetMapping("/list")
50
+    public R<Page<PatrolExecution>> listExecutions(
51
+            @RequestParam(defaultValue = "1") Integer pageNum,
52
+            @RequestParam(defaultValue = "10") Integer pageSize,
53
+            @RequestParam(required = false) String status,
54
+            @RequestParam(required = false) Long inspectorId) {
55
+        return R.ok(executionService.listExecutions(pageNum, pageSize, status, inspectorId));
56
+    }
57
+
58
+    @Operation(summary = "完成巡检")
59
+    @PutMapping("/{id}/complete")
60
+    public R<PatrolExecution> completeExecution(@PathVariable Long id,
61
+                                                 @RequestBody CompleteExecutionRequest request) {
62
+        return R.ok(executionService.completeExecution(id, request));
63
+    }
64
+
65
+    @Operation(summary = "中止巡检")
66
+    @PutMapping("/{id}/abort")
67
+    public R<PatrolExecution> abortExecution(@PathVariable Long id,
68
+                                              @RequestParam(required = false) String reason) {
69
+        return R.ok(executionService.abortExecution(id, reason));
70
+    }
71
+
72
+    @Operation(summary = "获取执行统计")
73
+    @GetMapping("/stats")
74
+    public R<Map<String, Object>> getStats(@RequestParam String startDate,
75
+                                            @RequestParam String endDate) {
76
+        return R.ok(executionService.getExecutionStats(startDate, endDate));
77
+    }
78
+
79
+    // ========== 检查项记录 ==========
80
+
81
+    @Operation(summary = "提交检查项记录")
82
+    @PostMapping("/{id}/check-item")
83
+    public R<CheckItemRecord> submitCheckItem(@PathVariable Long id,
84
+                                               @RequestBody CheckItemRecordRequest request) {
85
+        return R.ok(executionService.submitCheckItem(id, request));
86
+    }
87
+
88
+    @Operation(summary = "批量提交检查项记录")
89
+    @PostMapping("/{id}/check-item/batch")
90
+    public R<Map<String, Object>> batchSubmitCheckItems(@PathVariable Long id,
91
+                                                         @RequestBody List<CheckItemRecordRequest> items) {
92
+        int count = executionService.batchSubmitCheckItems(id, items);
93
+        return R.ok(Map.of("executionId", id, "recorded", count));
94
+    }
95
+
96
+    @Operation(summary = "获取检查项记录列表")
97
+    @GetMapping("/{id}/check-item/list")
98
+    public R<List<CheckItemRecord>> getCheckItems(@PathVariable Long id) {
99
+        return R.ok(executionService.getCheckItemRecords(id));
100
+    }
101
+
102
+    @Operation(summary = "获取检查项状态汇总")
103
+    @GetMapping("/{id}/check-item/summary")
104
+    public R<List<Map<String, Object>>> getCheckItemSummary(@PathVariable Long id) {
105
+        return R.ok(executionService.getCheckItemStatusSummary(id));
106
+    }
107
+
108
+    // ========== GPS轨迹 ==========
109
+
110
+    @Operation(summary = "记录GPS轨迹点")
111
+    @PostMapping("/{id}/track")
112
+    public R<GpsTrackPoint> recordTrackPoint(@PathVariable Long id,
113
+                                              @RequestParam Double lng,
114
+                                              @RequestParam Double lat,
115
+                                              @RequestParam(required = false) Double altitude,
116
+                                              @RequestParam(required = false) Double speed,
117
+                                              @RequestParam(required = false) Double bearing,
118
+                                              @RequestParam(required = false) Double accuracy) {
119
+        return R.ok(trackService.recordTrackPoint(id, lng, lat, altitude, speed, bearing, accuracy));
120
+    }
121
+
122
+    @Operation(summary = "批量记录GPS轨迹点")
123
+    @PostMapping("/{id}/track/batch")
124
+    public R<Map<String, Object>> batchRecordTrack(@PathVariable Long id,
125
+                                                    @RequestBody GpsTrackBatchRequest request) {
126
+        int count = trackService.batchRecordTrackPoints(id, request);
127
+        return R.ok(Map.of("executionId", id, "recorded", count));
128
+    }
129
+
130
+    @Operation(summary = "获取轨迹回放数据")
131
+    @GetMapping("/{id}/track")
132
+    public R<List<GpsTrackPoint>> getTrackPoints(@PathVariable Long id) {
133
+        return R.ok(trackService.getTrackPoints(id));
134
+    }
135
+
136
+    @Operation(summary = "获取轨迹摘要(地图展示用)")
137
+    @GetMapping("/{id}/track/summary")
138
+    public R<Map<String, Object>> getTrackSummary(@PathVariable Long id) {
139
+        return R.ok(trackService.getTrackSummary(id));
140
+    }
141
+
142
+    @Operation(summary = "获取最新位置")
143
+    @GetMapping("/{id}/track/latest")
144
+    public R<GpsTrackPoint> getLatestPosition(@PathVariable Long id) {
145
+        return R.ok(trackService.getLatestPosition(id));
146
+    }
147
+
148
+    // ========== 异常上报 ==========
149
+
150
+    @Operation(summary = "上报问题")
151
+    @PostMapping("/{id}/issue")
152
+    public R<PatrolIssue> reportIssue(@PathVariable Long id,
153
+                                       @RequestBody PatrolIssueRequest request) {
154
+        return R.ok(issueService.reportIssue(id, request));
155
+    }
156
+
157
+    @Operation(summary = "获取问题详情")
158
+    @GetMapping("/issue/{issueId}")
159
+    public R<PatrolIssue> getIssue(@PathVariable Long issueId) {
160
+        return R.ok(issueService.getIssue(issueId));
161
+    }
162
+
163
+    @Operation(summary = "获取执行的问题列表")
164
+    @GetMapping("/{id}/issue/list")
165
+    public R<List<PatrolIssue>> getIssues(@PathVariable Long id) {
166
+        return R.ok(issueService.getIssuesByExecutionId(id));
167
+    }
168
+
169
+    @Operation(summary = "更新问题处理状态")
170
+    @PutMapping("/issue/{issueId}/status")
171
+    public R<PatrolIssue> updateIssueStatus(@PathVariable Long issueId,
172
+                                             @RequestParam String handleStatus) {
173
+        return R.ok(issueService.updateIssueStatus(issueId, handleStatus));
174
+    }
175
+
176
+    @Operation(summary = "关联工单")
177
+    @PutMapping("/issue/{issueId}/link-work-order")
178
+    public R<PatrolIssue> linkWorkOrder(@PathVariable Long issueId,
179
+                                         @RequestParam Long workOrderId) {
180
+        return R.ok(issueService.linkWorkOrder(issueId, workOrderId));
181
+    }
182
+
183
+    @Operation(summary = "获取问题汇总")
184
+    @GetMapping("/{id}/issue/summary")
185
+    public R<List<Map<String, Object>>> getIssueSummary(@PathVariable Long id) {
186
+        return R.ok(issueService.getIssueSummary(id));
187
+    }
188
+
189
+    @Operation(summary = "查询问题列表(按类型/状态筛选)")
190
+    @GetMapping("/issue/query")
191
+    public R<List<PatrolIssue>> queryIssues(
192
+            @RequestParam(required = false) String issueType,
193
+            @RequestParam(required = false) String handleStatus,
194
+            @RequestParam(required = false) Long executionId) {
195
+        return R.ok(issueService.queryIssues(issueType, handleStatus, executionId));
196
+    }
197
+}

+ 287
- 77
wm-patrol/src/main/java/com/water/patrol/controller/PatrolController.java 파일 보기

@@ -1,100 +1,310 @@
1 1
 package com.water.patrol.controller;
2 2
 
3
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
3 4
 import com.water.common.core.result.R;
4
-import com.water.patrol.service.PatrolService;
5
+import com.water.patrol.entity.PatrolCheckpoint;
6
+import com.water.patrol.entity.PatrolRoute;
7
+import com.water.patrol.entity.PatrolTask;
8
+import com.water.patrol.entity.PatrolWorker;
9
+import com.water.patrol.service.RouteService;
10
+import com.water.patrol.service.TaskService;
11
+import com.water.patrol.service.WorkerService;
5 12
 import io.swagger.v3.oas.annotations.Operation;
6 13
 import io.swagger.v3.oas.annotations.tags.Tag;
7 14
 import lombok.RequiredArgsConstructor;
15
+import org.springframework.format.annotation.DateTimeFormat;
8 16
 import org.springframework.web.bind.annotation.*;
9 17
 
10 18
 import java.time.LocalDate;
11
-import java.util.*;
19
+import java.util.List;
20
+import java.util.Map;
12 21
 
13
-@Tag(name = "巡检管理")
22
+/**
23
+ * 巡检路线管理与任务分派 REST API
24
+ */
25
+@Tag(name = "巡检路线管理与任务分派")
14 26
 @RestController
15
-@RequestMapping("/patrol")
27
+@RequestMapping("/api/patrol")
16 28
 @RequiredArgsConstructor
17 29
 public class PatrolController {
18 30
 
19
-    private final PatrolService patrolService;
31
+    private final RouteService routeService;
32
+    private final TaskService taskService;
33
+    private final WorkerService workerService;
20 34
 
21
-    // ---- 路线 ----
35
+    // ==================== 路线管理 ====================
36
+
37
+    @Operation(summary = "分页查询巡检路线")
38
+    @GetMapping("/route/page")
39
+    public R<Page<PatrolRoute>> routePage(
40
+            @RequestParam(defaultValue = "1") int current,
41
+            @RequestParam(defaultValue = "10") int size,
42
+            @RequestParam(required = false) String keyword,
43
+            @RequestParam(required = false) String area,
44
+            @RequestParam(required = false) Integer status) {
45
+        return R.ok(routeService.page(current, size, keyword, area, status));
46
+    }
47
+
48
+    @Operation(summary = "查询路线详情")
49
+    @GetMapping("/route/{id}")
50
+    public R<PatrolRoute> routeDetail(@PathVariable Long id) {
51
+        PatrolRoute route = routeService.getById(id);
52
+        return route != null ? R.ok(route) : R.fail(404, "路线不存在");
53
+    }
54
+
55
+    @Operation(summary = "创建巡检路线")
22 56
     @PostMapping("/route")
23
-    public R<Map<String, Object>> createRoute(@RequestBody Map<String, Object> req) {
24
-        @SuppressWarnings("unchecked")
25
-        List<Map<String, Object>> points = (List<Map<String, Object>>) req.getOrDefault("points", List.of());
26
-        return R.ok(patrolService.createRoute(
27
-            (String) req.get("routeName"), (String) req.get("area"),
28
-            points, (int) req.getOrDefault("estimDuration", 60)));
57
+    public R<PatrolRoute> createRoute(@RequestBody PatrolRoute route) {
58
+        return R.ok(routeService.create(route));
29 59
     }
30 60
 
31
-    @GetMapping("/route/list")
32
-    public R<List<Map<String, Object>>> routes(@RequestParam String area) {
33
-        return R.ok(patrolService.getRoutes(area));
61
+    @Operation(summary = "更新巡检路线")
62
+    @PutMapping("/route/{id}")
63
+    public R<String> updateRoute(@PathVariable Long id, @RequestBody PatrolRoute route) {
64
+        route.setId(id);
65
+        return routeService.update(route) ? R.ok("更新成功") : R.fail("更新失败");
34 66
     }
35 67
 
36
-    // ---- 任务 ----
68
+    @Operation(summary = "删除巡检路线")
69
+    @DeleteMapping("/route/{id}")
70
+    public R<String> deleteRoute(@PathVariable Long id) {
71
+        return routeService.delete(id) ? R.ok("删除成功") : R.fail("删除失败");
72
+    }
73
+
74
+    @Operation(summary = "启用/停用路线")
75
+    @PutMapping("/route/{id}/status")
76
+    public R<String> toggleRouteStatus(@PathVariable Long id, @RequestParam int status) {
77
+        return routeService.toggleStatus(id, status) ? R.ok("操作成功") : R.fail("操作失败");
78
+    }
79
+
80
+    @Operation(summary = "按区域统计路线数")
81
+    @GetMapping("/route/stats")
82
+    public R<List<Map<String, Object>>> routeStats() {
83
+        return R.ok(routeService.countByArea());
84
+    }
85
+
86
+    // ==================== 检查点管理 ====================
87
+
88
+    @Operation(summary = "查询路线检查点列表")
89
+    @GetMapping("/route/{routeId}/checkpoints")
90
+    public R<List<PatrolCheckpoint>> checkpoints(@PathVariable Long routeId) {
91
+        return R.ok(routeService.getCheckpoints(routeId));
92
+    }
93
+
94
+    @Operation(summary = "批量设置路线检查点")
95
+    @PostMapping("/route/{routeId}/checkpoints")
96
+    public R<List<PatrolCheckpoint>> setCheckpoints(
97
+            @PathVariable Long routeId,
98
+            @RequestBody List<PatrolCheckpoint> checkpoints) {
99
+        return R.ok(routeService.setCheckpoints(routeId, checkpoints));
100
+    }
101
+
102
+    @Operation(summary = "添加单个检查点")
103
+    @PostMapping("/checkpoint")
104
+    public R<PatrolCheckpoint> addCheckpoint(@RequestBody PatrolCheckpoint checkpoint) {
105
+        return R.ok(routeService.addCheckpoint(checkpoint));
106
+    }
107
+
108
+    @Operation(summary = "删除检查点")
109
+    @DeleteMapping("/checkpoint/{id}")
110
+    public R<String> deleteCheckpoint(@PathVariable Long id) {
111
+        return routeService.deleteCheckpoint(id) ? R.ok("删除成功") : R.fail("删除失败");
112
+    }
113
+
114
+    @Operation(summary = "路线优化 - 计算最优巡检路径")
115
+    @PostMapping("/route/{id}/optimize")
116
+    public R<Map<String, Object>> optimizeRoute(@PathVariable Long id) {
117
+        return R.ok(routeService.optimizeRoute(id));
118
+    }
119
+
120
+    // ==================== 任务管理 ====================
121
+
122
+    @Operation(summary = "分页查询巡检任务")
123
+    @GetMapping("/task/page")
124
+    public R<Page<PatrolTask>> taskPage(
125
+            @RequestParam(defaultValue = "1") int current,
126
+            @RequestParam(defaultValue = "10") int size,
127
+            @RequestParam(required = false) String keyword,
128
+            @RequestParam(required = false) String status,
129
+            @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate startDate,
130
+            @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate endDate,
131
+            @RequestParam(required = false) Long workerId) {
132
+        return R.ok(taskService.page(current, size, keyword, status, startDate, endDate, workerId));
133
+    }
134
+
135
+    @Operation(summary = "查询任务详情")
136
+    @GetMapping("/task/{id}")
137
+    public R<PatrolTask> taskDetail(@PathVariable Long id) {
138
+        PatrolTask task = taskService.getById(id);
139
+        return task != null ? R.ok(task) : R.fail(404, "任务不存在");
140
+    }
141
+
142
+    @Operation(summary = "创建巡检任务")
37 143
     @PostMapping("/task")
38
-    public R<Map<String, Object>> createTask(@RequestBody Map<String, Object> req) {
39
-        return R.ok(patrolService.createTask(
40
-            Long.parseLong(String.valueOf(req.get("routeId"))),
41
-            Long.parseLong(String.valueOf(req.get("assigneeId"))),
42
-            (String) req.get("taskDate")));
43
-    }
44
-
45
-    @GetMapping("/task/today")
46
-    public R<List<Map<String, Object>>> todayTasks(@RequestParam Long userId) {
47
-        return R.ok(patrolService.getTodayTasks(userId));
48
-    }
49
-
50
-    @PutMapping("/task/{id}/start")
51
-    public R<Map<String, Object>> startTask(@PathVariable Long id) {
52
-        return R.ok(patrolService.startTask(id));
53
-    }
54
-
55
-    @PutMapping("/task/{id}/complete")
56
-    public R<Map<String, Object>> completeTask(@PathVariable Long id, @RequestParam double distance) {
57
-        return R.ok(patrolService.completeTask(id, distance));
58
-    }
59
-
60
-    // ---- 巡检记录 ----
61
-    @PostMapping("/record")
62
-    public R<Map<String, Object>> record(@RequestBody Map<String, Object> req) {
63
-        @SuppressWarnings("unchecked")
64
-        List<Map<String, Object>> items = (List<Map<String, Object>>) req.getOrDefault("checkItems", List.of());
65
-        return R.ok(patrolService.recordCheck(
66
-            Long.parseLong(String.valueOf(req.get("taskId"))),
67
-            (int) req.get("pointSeq"),
68
-            req.get("deviceId") != null ? Long.parseLong(String.valueOf(req.get("deviceId"))) : null,
69
-            items,
70
-            ((Number) req.get("lng")).doubleValue(),
71
-            ((Number) req.get("lat")).doubleValue()));
72
-    }
73
-
74
-    @GetMapping("/record/list/{taskId}")
75
-    public R<List<Map<String, Object>>> records(@PathVariable Long taskId) {
76
-        return R.ok(patrolService.getTaskRecords(taskId));
77
-    }
78
-
79
-    // ---- 问题上报 ----
80
-    @PostMapping("/issue/report")
81
-    public R<Map<String, Object>> reportIssue(@RequestBody Map<String, Object> req) {
82
-        @SuppressWarnings("unchecked")
83
-        List<String> photos = (List<String>) req.getOrDefault("photoUrls", List.of());
84
-        return R.ok(patrolService.reportIssue(
85
-            Long.parseLong(String.valueOf(req.get("taskId"))),
86
-            req.get("deviceId") != null ? Long.parseLong(String.valueOf(req.get("deviceId"))) : null,
87
-            (String) req.get("issueType"), (String) req.get("description"),
88
-            photos,
89
-            ((Number) req.get("lng")).doubleValue(),
90
-            ((Number) req.get("lat")).doubleValue()));
91
-    }
92
-
93
-    // ---- 统计 ----
94
-    @GetMapping("/stats")
95
-    public R<Map<String, Object>> stats(@RequestParam String area,
96
-                                         @RequestParam String start,
97
-                                         @RequestParam String end) {
98
-        return R.ok(patrolService.getStats(area, LocalDate.parse(start), LocalDate.parse(end)));
144
+    public R<PatrolTask> createTask(@RequestBody PatrolTask task) {
145
+        return R.ok(taskService.create(task));
146
+    }
147
+
148
+    @Operation(summary = "更新巡检任务")
149
+    @PutMapping("/task/{id}")
150
+    public R<String> updateTask(@PathVariable Long id, @RequestBody PatrolTask task) {
151
+        task.setId(id);
152
+        return taskService.update(task) ? R.ok("更新成功") : R.fail("更新失败");
153
+    }
154
+
155
+    @Operation(summary = "删除巡检任务")
156
+    @DeleteMapping("/task/{id}")
157
+    public R<String> deleteTask(@PathVariable Long id) {
158
+        return taskService.delete(id) ? R.ok("删除成功") : R.fail("删除失败");
159
+    }
160
+
161
+    // ==================== 任务分派 ====================
162
+
163
+    @Operation(summary = "手动分派任务给巡检员")
164
+    @PostMapping("/task/{id}/assign")
165
+    public R<String> assignTask(@PathVariable Long id, @RequestParam Long workerId) {
166
+        return taskService.assign(id, workerId) ? R.ok("分派成功") : R.fail("分派失败");
167
+    }
168
+
169
+    @Operation(summary = "自动分派任务(选择负荷最低的巡检员)")
170
+    @PostMapping("/task/{id}/auto-assign")
171
+    public R<Map<String, Object>> autoAssignTask(@PathVariable Long id) {
172
+        return R.ok(taskService.autoAssign(id));
173
+    }
174
+
175
+    @Operation(summary = "批量自动分派指定日期的待分派任务")
176
+    @PostMapping("/task/batch-auto-assign")
177
+    public R<Map<String, Object>> batchAutoAssign(
178
+            @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate date) {
179
+        return R.ok(taskService.batchAutoAssign(date));
180
+    }
181
+
182
+    @Operation(summary = "开始任务")
183
+    @PostMapping("/task/{id}/start")
184
+    public R<String> startTask(@PathVariable Long id) {
185
+        return taskService.startTask(id) ? R.ok("任务已开始") : R.fail("操作失败");
186
+    }
187
+
188
+    @Operation(summary = "完成任务")
189
+    @PostMapping("/task/{id}/complete")
190
+    public R<String> completeTask(@PathVariable Long id, @RequestParam(required = false) Double distance) {
191
+        return taskService.completeTask(id, distance != null ? distance : 0.0)
192
+                ? R.ok("任务已完成") : R.fail("操作失败");
193
+    }
194
+
195
+    @Operation(summary = "取消任务")
196
+    @PostMapping("/task/{id}/cancel")
197
+    public R<String> cancelTask(@PathVariable Long id, @RequestBody(required = false) Map<String, String> body) {
198
+        String reason = body != null ? body.getOrDefault("reason", "") : "";
199
+        return taskService.cancelTask(id, reason) ? R.ok("任务已取消") : R.fail("操作失败");
200
+    }
201
+
202
+    // ==================== 周期任务 ====================
203
+
204
+    @Operation(summary = "创建周期任务(日/周/月)")
205
+    @PostMapping("/task/cycle")
206
+    public R<Map<String, Object>> createCycleTask(@RequestBody Map<String, Object> body) {
207
+        PatrolTask template = new PatrolTask();
208
+        template.setTaskName((String) body.get("taskName"));
209
+        if (body.get("routeId") != null) {
210
+            template.setRouteId(((Number) body.get("routeId")).longValue());
211
+        }
212
+        if (body.get("workerId") != null) {
213
+            template.setWorkerId(((Number) body.get("workerId")).longValue());
214
+        }
215
+        if (body.get("priority") != null) {
216
+            template.setPriority(((Number) body.get("priority")).intValue());
217
+        }
218
+        template.setRemark((String) body.get("remark"));
219
+
220
+        String cycleType = (String) body.getOrDefault("cycleType", "daily");
221
+        String startDateStr = (String) body.get("startDate");
222
+        LocalDate startDate = startDateStr != null ? LocalDate.parse(startDateStr) : LocalDate.now();
223
+        int cycles = body.get("cycles") != null ? ((Number) body.get("cycles")).intValue() : 7;
224
+
225
+        return R.ok(taskService.createCycleTask(template, cycleType, startDate, cycles));
226
+    }
227
+
228
+    // ==================== 巡检人员管理 ====================
229
+
230
+    @Operation(summary = "分页查询巡检员")
231
+    @GetMapping("/worker/page")
232
+    public R<Page<PatrolWorker>> workerPage(
233
+            @RequestParam(defaultValue = "1") int current,
234
+            @RequestParam(defaultValue = "10") int size,
235
+            @RequestParam(required = false) String keyword,
236
+            @RequestParam(required = false) Integer status) {
237
+        return R.ok(workerService.page(current, size, keyword, status));
238
+    }
239
+
240
+    @Operation(summary = "查询巡检员详情")
241
+    @GetMapping("/worker/{id}")
242
+    public R<PatrolWorker> workerDetail(@PathVariable Long id) {
243
+        PatrolWorker worker = workerService.getById(id);
244
+        return worker != null ? R.ok(worker) : R.fail(404, "巡检员不存在");
245
+    }
246
+
247
+    @Operation(summary = "创建巡检员")
248
+    @PostMapping("/worker")
249
+    public R<PatrolWorker> createWorker(@RequestBody PatrolWorker worker) {
250
+        return R.ok(workerService.create(worker));
251
+    }
252
+
253
+    @Operation(summary = "更新巡检员")
254
+    @PutMapping("/worker/{id}")
255
+    public R<String> updateWorker(@PathVariable Long id, @RequestBody PatrolWorker worker) {
256
+        worker.setId(id);
257
+        return workerService.update(worker) ? R.ok("更新成功") : R.fail("更新失败");
258
+    }
259
+
260
+    @Operation(summary = "删除巡检员")
261
+    @DeleteMapping("/worker/{id}")
262
+    public R<String> deleteWorker(@PathVariable Long id) {
263
+        return workerService.delete(id) ? R.ok("删除成功") : R.fail("删除失败");
264
+    }
265
+
266
+    @Operation(summary = "按技能标签筛选巡检员")
267
+    @GetMapping("/worker/skill")
268
+    public R<List<PatrolWorker>> workersBySkill(@RequestParam String tag) {
269
+        return R.ok(workerService.findBySkill(tag));
270
+    }
271
+
272
+    @Operation(summary = "更新巡检员技能标签")
273
+    @PutMapping("/worker/{id}/skills")
274
+    public R<String> updateSkills(@PathVariable Long id, @RequestBody Map<String, String> body) {
275
+        String skillTags = body.getOrDefault("skillTags", "");
276
+        return workerService.updateSkillTags(id, skillTags) ? R.ok("更新成功") : R.fail("更新失败");
277
+    }
278
+
279
+    @Operation(summary = "查询巡检员工作负荷")
280
+    @GetMapping("/worker/{id}/workload")
281
+    public R<Map<String, Object>> workerWorkload(
282
+            @PathVariable Long id,
283
+            @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate startDate,
284
+            @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate endDate) {
285
+        return R.ok(workerService.getWorkload(id, startDate, endDate));
286
+    }
287
+
288
+    @Operation(summary = "巡检员工作负荷概览(指定日期)")
289
+    @GetMapping("/worker/workload-overview")
290
+    public R<List<Map<String, Object>>> workloadOverview(
291
+            @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate date) {
292
+        return R.ok(workerService.getWorkloadOverview(date));
293
+    }
294
+
295
+    @Operation(summary = "按部门统计巡检员人数")
296
+    @GetMapping("/worker/stats")
297
+    public R<List<Map<String, Object>>> workerStats() {
298
+        return R.ok(workerService.countByDept());
299
+    }
300
+
301
+    // ==================== 综合统计 ====================
302
+
303
+    @Operation(summary = "任务统计看板")
304
+    @GetMapping("/task/stats")
305
+    public R<Map<String, Object>> taskStats(
306
+            @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate startDate,
307
+            @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate endDate) {
308
+        return R.ok(taskService.getTaskStats(startDate, endDate));
99 309
     }
100 310
 }

+ 79
- 0
wm-patrol/src/main/java/com/water/patrol/entity/CheckItemRecord.java 파일 보기

@@ -0,0 +1,79 @@
1
+package com.water.patrol.entity;
2
+
3
+import com.baomidou.mybatisplus.annotation.*;
4
+import com.water.common.core.entity.BaseEntity;
5
+import lombok.Data;
6
+import lombok.EqualsAndHashCode;
7
+
8
+import java.time.LocalDateTime;
9
+
10
+@Data
11
+@EqualsAndHashCode(callSuper = true)
12
+@TableName("check_item_record")
13
+public class CheckItemRecord extends BaseEntity {
14
+
15
+    /**
16
+     * 关联执行ID
17
+     */
18
+    private Long executionId;
19
+
20
+    /**
21
+     * 检查点序号
22
+     */
23
+    private Integer pointSeq;
24
+
25
+    /**
26
+     * 检查点名称
27
+     */
28
+    private String pointName;
29
+
30
+    /**
31
+     * 检查项名称
32
+     */
33
+    private String itemName;
34
+
35
+    /**
36
+     * 检查状态: normal/abnormal/skipped
37
+     */
38
+    private String checkStatus;
39
+
40
+    /**
41
+     * 数值录入值
42
+     */
43
+    private Double recordValue;
44
+
45
+    /**
46
+     * 数值单位
47
+     */
48
+    private String valueUnit;
49
+
50
+    /**
51
+     * 文字描述
52
+     */
53
+    private String description;
54
+
55
+    /**
56
+     * 照片URL列表(JSON数组)
57
+     */
58
+    private String photoUrls;
59
+
60
+    /**
61
+     * 备注
62
+     */
63
+    private String remark;
64
+
65
+    /**
66
+     * 记录时间
67
+     */
68
+    private LocalDateTime recordTime;
69
+
70
+    /**
71
+     * 经度
72
+     */
73
+    private Double lng;
74
+
75
+    /**
76
+     * 纬度
77
+     */
78
+    private Double lat;
79
+}

+ 54
- 0
wm-patrol/src/main/java/com/water/patrol/entity/GpsTrackPoint.java 파일 보기

@@ -0,0 +1,54 @@
1
+package com.water.patrol.entity;
2
+
3
+import com.baomidou.mybatisplus.annotation.*;
4
+import com.water.common.core.entity.BaseEntity;
5
+import lombok.Data;
6
+import lombok.EqualsAndHashCode;
7
+
8
+import java.time.LocalDateTime;
9
+
10
+@Data
11
+@EqualsAndHashCode(callSuper = true)
12
+@TableName("gps_track_point")
13
+public class GpsTrackPoint extends BaseEntity {
14
+
15
+    /**
16
+     * 关联执行ID
17
+     */
18
+    private Long executionId;
19
+
20
+    /**
21
+     * 经度
22
+     */
23
+    private Double lng;
24
+
25
+    /**
26
+     * 纬度
27
+     */
28
+    private Double lat;
29
+
30
+    /**
31
+     * 海拔(米)
32
+     */
33
+    private Double altitude;
34
+
35
+    /**
36
+     * 速度(m/s)
37
+     */
38
+    private Double speed;
39
+
40
+    /**
41
+     * 方向(角度, 0-360)
42
+     */
43
+    private Double bearing;
44
+
45
+    /**
46
+     * 精度(米)
47
+     */
48
+    private Double accuracy;
49
+
50
+    /**
51
+     * 记录时间
52
+     */
53
+    private LocalDateTime recordTime;
54
+}

+ 53
- 0
wm-patrol/src/main/java/com/water/patrol/entity/PatrolCheckpoint.java 파일 보기

@@ -0,0 +1,53 @@
1
+package com.water.patrol.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("patrol_checkpoint")
13
+public class PatrolCheckpoint {
14
+
15
+    @TableId(type = IdType.AUTO)
16
+    private Long id;
17
+
18
+    /** 所属路线ID */
19
+    private Long routeId;
20
+
21
+    /** 检查点顺序 */
22
+    private Integer seq;
23
+
24
+    /** 检查点名称 */
25
+    private String name;
26
+
27
+    /** 检查类型: visual/device/read */
28
+    private String checkType;
29
+
30
+    /** 关联设备ID(可选) */
31
+    private Long deviceId;
32
+
33
+    /** 经度 */
34
+    private Double lng;
35
+
36
+    /** 纬度 */
37
+    private Double lat;
38
+
39
+    /** 检查项列表(JSON) */
40
+    private String checkItems;
41
+
42
+    /** 是否必检: 1=必检 0=选检 */
43
+    private Integer required;
44
+
45
+    @TableField(fill = FieldFill.INSERT)
46
+    private LocalDateTime createdTime;
47
+
48
+    @TableField(fill = FieldFill.INSERT_UPDATE)
49
+    private LocalDateTime updatedTime;
50
+
51
+    @TableLogic
52
+    private Integer deleted;
53
+}

+ 90
- 0
wm-patrol/src/main/java/com/water/patrol/entity/PatrolExecution.java 파일 보기

@@ -0,0 +1,90 @@
1
+package com.water.patrol.entity;
2
+
3
+import com.baomidou.mybatisplus.annotation.*;
4
+import com.water.common.core.entity.BaseEntity;
5
+import lombok.Data;
6
+import lombok.EqualsAndHashCode;
7
+
8
+import java.math.BigDecimal;
9
+import java.time.LocalDateTime;
10
+
11
+@Data
12
+@EqualsAndHashCode(callSuper = true)
13
+@TableName("patrol_execution")
14
+public class PatrolExecution extends BaseEntity {
15
+
16
+    /**
17
+     * 关联巡检任务ID
18
+     */
19
+    private Long taskId;
20
+
21
+    /**
22
+     * 巡检员ID
23
+     */
24
+    private Long inspectorId;
25
+
26
+    /**
27
+     * 巡检员姓名
28
+     */
29
+    private String inspectorName;
30
+
31
+    /**
32
+     * 执行状态: pending/in_progress/completed/aborted
33
+     */
34
+    private String status;
35
+
36
+    /**
37
+     * 实际开始时间
38
+     */
39
+    private LocalDateTime startTime;
40
+
41
+    /**
42
+     * 实际结束时间
43
+     */
44
+    private LocalDateTime endTime;
45
+
46
+    /**
47
+     * 总检查项数
48
+     */
49
+    private Integer totalCheckItems;
50
+
51
+    /**
52
+     * 已完成检查项数
53
+     */
54
+    private Integer completedCheckItems;
55
+
56
+    /**
57
+     * 异常数量
58
+     */
59
+    private Integer issueCount;
60
+
61
+    /**
62
+     * 巡检总距离(km)
63
+     */
64
+    private BigDecimal totalDistance;
65
+
66
+    /**
67
+     * 开始经度
68
+     */
69
+    private Double startLng;
70
+
71
+    /**
72
+     * 开始纬度
73
+     */
74
+    private Double startLat;
75
+
76
+    /**
77
+     * 结束经度
78
+     */
79
+    private Double endLng;
80
+
81
+    /**
82
+     * 结束纬度
83
+     */
84
+    private Double endLat;
85
+
86
+    /**
87
+     * 备注
88
+     */
89
+    private String remark;
90
+}

+ 84
- 0
wm-patrol/src/main/java/com/water/patrol/entity/PatrolIssue.java 파일 보기

@@ -0,0 +1,84 @@
1
+package com.water.patrol.entity;
2
+
3
+import com.baomidou.mybatisplus.annotation.*;
4
+import com.water.common.core.entity.BaseEntity;
5
+import lombok.Data;
6
+import lombok.EqualsAndHashCode;
7
+
8
+import java.time.LocalDateTime;
9
+
10
+@Data
11
+@EqualsAndHashCode(callSuper = true)
12
+@TableName("patrol_issue")
13
+public class PatrolIssue extends BaseEntity {
14
+
15
+    /**
16
+     * 关联执行ID
17
+     */
18
+    private Long executionId;
19
+
20
+    /**
21
+     * 关联检查项记录ID(可选)
22
+     */
23
+    private Long checkRecordId;
24
+
25
+    /**
26
+     * 问题类型: leak/damage/pollution/illegal/other
27
+     */
28
+    private String issueType;
29
+
30
+    /**
31
+     * 严重程度: low/medium/high/critical
32
+     */
33
+    private String severity;
34
+
35
+    /**
36
+     * 问题描述
37
+     */
38
+    private String description;
39
+
40
+    /**
41
+     * 照片URL列表(JSON数组)
42
+     */
43
+    private String photoUrls;
44
+
45
+    /**
46
+     * 经度
47
+     */
48
+    private Double lng;
49
+
50
+    /**
51
+     * 纬度
52
+     */
53
+    private Double lat;
54
+
55
+    /**
56
+     * 地址描述
57
+     */
58
+    private String address;
59
+
60
+    /**
61
+     * 处理状态: pending/processing/resolved/closed
62
+     */
63
+    private String handleStatus;
64
+
65
+    /**
66
+     * 关联工单ID
67
+     */
68
+    private Long workOrderId;
69
+
70
+    /**
71
+     * 上报人ID
72
+     */
73
+    private Long reporterId;
74
+
75
+    /**
76
+     * 上报人姓名
77
+     */
78
+    private String reporterName;
79
+
80
+    /**
81
+     * 上报时间
82
+     */
83
+    private LocalDateTime reportTime;
84
+}

+ 53
- 0
wm-patrol/src/main/java/com/water/patrol/entity/PatrolRoute.java 파일 보기

@@ -0,0 +1,53 @@
1
+package com.water.patrol.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("patrol_route")
13
+public class PatrolRoute {
14
+
15
+    @TableId(type = IdType.AUTO)
16
+    private Long id;
17
+
18
+    /** 路线名称 */
19
+    private String routeName;
20
+
21
+    /** 路线编码 */
22
+    private String routeCode;
23
+
24
+    /** 所属区域 */
25
+    private String area;
26
+
27
+    /** 路线描述 */
28
+    private String description;
29
+
30
+    /** 总距离(公里) */
31
+    private Double totalDistance;
32
+
33
+    /** 预估时长(分钟) */
34
+    private Integer estimDuration;
35
+
36
+    /** 状态: 1=启用 0=停用 */
37
+    private Integer status;
38
+
39
+    /** 创建人 */
40
+    private Long createdBy;
41
+
42
+    /** 更新人 */
43
+    private Long updatedBy;
44
+
45
+    @TableField(fill = FieldFill.INSERT)
46
+    private LocalDateTime createdTime;
47
+
48
+    @TableField(fill = FieldFill.INSERT_UPDATE)
49
+    private LocalDateTime updatedTime;
50
+
51
+    @TableLogic
52
+    private Integer deleted;
53
+}

+ 75
- 0
wm-patrol/src/main/java/com/water/patrol/entity/PatrolTask.java 파일 보기

@@ -0,0 +1,75 @@
1
+package com.water.patrol.entity;
2
+
3
+import com.baomidou.mybatisplus.annotation.*;
4
+import lombok.Data;
5
+
6
+import java.time.LocalDate;
7
+import java.time.LocalDateTime;
8
+
9
+/**
10
+ * 巡检任务实体
11
+ */
12
+@Data
13
+@TableName("patrol_task")
14
+public class PatrolTask {
15
+
16
+    @TableId(type = IdType.AUTO)
17
+    private Long id;
18
+
19
+    /** 任务名称 */
20
+    private String taskName;
21
+
22
+    /** 路线ID */
23
+    private Long routeId;
24
+
25
+    /** 巡检员ID */
26
+    private Long workerId;
27
+
28
+    /** 任务日期 */
29
+    private LocalDate taskDate;
30
+
31
+    /** 周期类型: daily/weekly/monthly/once */
32
+    private String cycleType;
33
+
34
+    /** 计划开始时间 */
35
+    private LocalDateTime planStart;
36
+
37
+    /** 计划结束时间 */
38
+    private LocalDateTime planEnd;
39
+
40
+    /** 实际开始时间 */
41
+    private LocalDateTime actualStart;
42
+
43
+    /** 实际结束时间 */
44
+    private LocalDateTime actualEnd;
45
+
46
+    /** 状态: pending/assigned/in_progress/completed/cancelled */
47
+    private String status;
48
+
49
+    /** 优先级 */
50
+    private Integer priority;
51
+
52
+    /** 巡检距离(公里) */
53
+    private Double distance;
54
+
55
+    /** 已完成检查点数 */
56
+    private Integer checkpointDone;
57
+
58
+    /** 总检查点数 */
59
+    private Integer checkpointTotal;
60
+
61
+    /** 备注 */
62
+    private String remark;
63
+
64
+    /** 创建人 */
65
+    private Long createdBy;
66
+
67
+    @TableField(fill = FieldFill.INSERT)
68
+    private LocalDateTime createdTime;
69
+
70
+    @TableField(fill = FieldFill.INSERT_UPDATE)
71
+    private LocalDateTime updatedTime;
72
+
73
+    @TableLogic
74
+    private Integer deleted;
75
+}

+ 41
- 0
wm-patrol/src/main/java/com/water/patrol/entity/PatrolWorker.java 파일 보기

@@ -0,0 +1,41 @@
1
+package com.water.patrol.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("patrol_worker")
13
+public class PatrolWorker {
14
+
15
+    @TableId(type = IdType.AUTO)
16
+    private Long id;
17
+
18
+    /** 姓名 */
19
+    private String name;
20
+
21
+    /** 联系电话 */
22
+    private String phone;
23
+
24
+    /** 部门 */
25
+    private String dept;
26
+
27
+    /** 技能标签(JSON数组) */
28
+    private String skillTags;
29
+
30
+    /** 状态: 1=在岗 0=休假 -1=离职 */
31
+    private Integer status;
32
+
33
+    @TableField(fill = FieldFill.INSERT)
34
+    private LocalDateTime createdTime;
35
+
36
+    @TableField(fill = FieldFill.INSERT_UPDATE)
37
+    private LocalDateTime updatedTime;
38
+
39
+    @TableLogic
40
+    private Integer deleted;
41
+}

+ 64
- 0
wm-patrol/src/main/java/com/water/patrol/entity/dto/CheckItemRecordRequest.java 파일 보기

@@ -0,0 +1,64 @@
1
+package com.water.patrol.entity.dto;
2
+
3
+import lombok.Data;
4
+
5
+import java.util.List;
6
+
7
+@Data
8
+public class CheckItemRecordRequest {
9
+
10
+    /**
11
+     * 检查点序号
12
+     */
13
+    private Integer pointSeq;
14
+
15
+    /**
16
+     * 检查点名称
17
+     */
18
+    private String pointName;
19
+
20
+    /**
21
+     * 检查项名称
22
+     */
23
+    private String itemName;
24
+
25
+    /**
26
+     * 检查状态: normal/abnormal/skipped
27
+     */
28
+    private String checkStatus;
29
+
30
+    /**
31
+     * 数值录入值
32
+     */
33
+    private Double recordValue;
34
+
35
+    /**
36
+     * 数值单位
37
+     */
38
+    private String valueUnit;
39
+
40
+    /**
41
+     * 文字描述
42
+     */
43
+    private String description;
44
+
45
+    /**
46
+     * 照片URL列表
47
+     */
48
+    private List<String> photoUrls;
49
+
50
+    /**
51
+     * 备注
52
+     */
53
+    private String remark;
54
+
55
+    /**
56
+     * 经度
57
+     */
58
+    private Double lng;
59
+
60
+    /**
61
+     * 纬度
62
+     */
63
+    private Double lat;
64
+}

+ 27
- 0
wm-patrol/src/main/java/com/water/patrol/entity/dto/CompleteExecutionRequest.java 파일 보기

@@ -0,0 +1,27 @@
1
+package com.water.patrol.entity.dto;
2
+
3
+import lombok.Data;
4
+
5
+@Data
6
+public class CompleteExecutionRequest {
7
+
8
+    /**
9
+     * 结束经度
10
+     */
11
+    private Double endLng;
12
+
13
+    /**
14
+     * 结束纬度
15
+     */
16
+    private Double endLat;
17
+
18
+    /**
19
+     * 巡检总距离(km)
20
+     */
21
+    private Double totalDistance;
22
+
23
+    /**
24
+     * 备注
25
+     */
26
+    private String remark;
27
+}

+ 52
- 0
wm-patrol/src/main/java/com/water/patrol/entity/dto/GpsTrackBatchRequest.java 파일 보기

@@ -0,0 +1,52 @@
1
+package com.water.patrol.entity.dto;
2
+
3
+import lombok.Data;
4
+
5
+import java.util.List;
6
+
7
+@Data
8
+public class GpsTrackBatchRequest {
9
+
10
+    /**
11
+     * GPS轨迹点列表
12
+     */
13
+    private List<GpsTrackPointDto> points;
14
+
15
+    @Data
16
+    public static class GpsTrackPointDto {
17
+        /**
18
+         * 经度
19
+         */
20
+        private Double lng;
21
+
22
+        /**
23
+         * 纬度
24
+         */
25
+        private Double lat;
26
+
27
+        /**
28
+         * 海拔(米)
29
+         */
30
+        private Double altitude;
31
+
32
+        /**
33
+         * 速度(m/s)
34
+         */
35
+        private Double speed;
36
+
37
+        /**
38
+         * 方向(角度)
39
+         */
40
+        private Double bearing;
41
+
42
+        /**
43
+         * 精度(米)
44
+         */
45
+        private Double accuracy;
46
+
47
+        /**
48
+         * 记录时间(ISO格式)
49
+         */
50
+        private String recordTime;
51
+    }
52
+}

+ 49
- 0
wm-patrol/src/main/java/com/water/patrol/entity/dto/PatrolIssueRequest.java 파일 보기

@@ -0,0 +1,49 @@
1
+package com.water.patrol.entity.dto;
2
+
3
+import lombok.Data;
4
+
5
+import java.util.List;
6
+
7
+@Data
8
+public class PatrolIssueRequest {
9
+
10
+    /**
11
+     * 关联检查项记录ID(可选)
12
+     */
13
+    private Long checkRecordId;
14
+
15
+    /**
16
+     * 问题类型: leak/damage/pollution/illegal/other
17
+     */
18
+    private String issueType;
19
+
20
+    /**
21
+     * 严重程度: low/medium/high/critical
22
+     */
23
+    private String severity;
24
+
25
+    /**
26
+     * 问题描述
27
+     */
28
+    private String description;
29
+
30
+    /**
31
+     * 照片URL列表
32
+     */
33
+    private List<String> photoUrls;
34
+
35
+    /**
36
+     * 经度
37
+     */
38
+    private Double lng;
39
+
40
+    /**
41
+     * 纬度
42
+     */
43
+    private Double lat;
44
+
45
+    /**
46
+     * 地址描述
47
+     */
48
+    private String address;
49
+}

+ 42
- 0
wm-patrol/src/main/java/com/water/patrol/entity/dto/StartExecutionRequest.java 파일 보기

@@ -0,0 +1,42 @@
1
+package com.water.patrol.entity.dto;
2
+
3
+import lombok.Data;
4
+
5
+@Data
6
+public class StartExecutionRequest {
7
+
8
+    /**
9
+     * 巡检任务ID
10
+     */
11
+    private Long taskId;
12
+
13
+    /**
14
+     * 巡检员ID
15
+     */
16
+    private Long inspectorId;
17
+
18
+    /**
19
+     * 巡检员姓名
20
+     */
21
+    private String inspectorName;
22
+
23
+    /**
24
+     * 开始经度
25
+     */
26
+    private Double startLng;
27
+
28
+    /**
29
+     * 开始纬度
30
+     */
31
+    private Double startLat;
32
+
33
+    /**
34
+     * 总检查项数
35
+     */
36
+    private Integer totalCheckItems;
37
+
38
+    /**
39
+     * 备注
40
+     */
41
+    private String remark;
42
+}

+ 18
- 0
wm-patrol/src/main/java/com/water/patrol/mapper/CheckItemRecordMapper.java 파일 보기

@@ -0,0 +1,18 @@
1
+package com.water.patrol.mapper;
2
+
3
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
4
+import com.water.patrol.entity.CheckItemRecord;
5
+import org.apache.ibatis.annotations.Mapper;
6
+import org.apache.ibatis.annotations.Param;
7
+import org.apache.ibatis.annotations.Select;
8
+
9
+import java.util.List;
10
+import java.util.Map;
11
+
12
+@Mapper
13
+public interface CheckItemRecordMapper extends BaseMapper<CheckItemRecord> {
14
+
15
+    @Select("SELECT check_status, COUNT(*) as count FROM check_item_record " +
16
+            "WHERE execution_id = #{executionId} AND deleted = 0 GROUP BY check_status")
17
+    List<Map<String, Object>> selectStatusSummary(@Param("executionId") Long executionId);
18
+}

+ 17
- 0
wm-patrol/src/main/java/com/water/patrol/mapper/GpsTrackPointMapper.java 파일 보기

@@ -0,0 +1,17 @@
1
+package com.water.patrol.mapper;
2
+
3
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
4
+import com.water.patrol.entity.GpsTrackPoint;
5
+import org.apache.ibatis.annotations.Mapper;
6
+import org.apache.ibatis.annotations.Param;
7
+import org.apache.ibatis.annotations.Select;
8
+
9
+import java.util.List;
10
+
11
+@Mapper
12
+public interface GpsTrackPointMapper extends BaseMapper<GpsTrackPoint> {
13
+
14
+    @Select("SELECT * FROM gps_track_point WHERE execution_id = #{executionId} " +
15
+            "AND deleted = 0 ORDER BY record_time ASC")
16
+    List<GpsTrackPoint> selectTrackByExecutionId(@Param("executionId") Long executionId);
17
+}

+ 19
- 0
wm-patrol/src/main/java/com/water/patrol/mapper/PatrolCheckpointMapper.java 파일 보기

@@ -0,0 +1,19 @@
1
+package com.water.patrol.mapper;
2
+
3
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
4
+import com.water.patrol.entity.PatrolCheckpoint;
5
+import org.apache.ibatis.annotations.Mapper;
6
+import org.apache.ibatis.annotations.Param;
7
+import org.apache.ibatis.annotations.Select;
8
+
9
+import java.util.List;
10
+
11
+@Mapper
12
+public interface PatrolCheckpointMapper extends BaseMapper<PatrolCheckpoint> {
13
+
14
+    @Select("SELECT * FROM patrol_checkpoint WHERE route_id = #{routeId} AND deleted = 0 ORDER BY seq")
15
+    List<PatrolCheckpoint> findByRouteId(@Param("routeId") Long routeId);
16
+
17
+    @Select("SELECT COUNT(*) FROM patrol_checkpoint WHERE route_id = #{routeId} AND deleted = 0")
18
+    int countByRouteId(@Param("routeId") Long routeId);
19
+}

+ 24
- 0
wm-patrol/src/main/java/com/water/patrol/mapper/PatrolExecutionMapper.java 파일 보기

@@ -0,0 +1,24 @@
1
+package com.water.patrol.mapper;
2
+
3
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
4
+import com.water.patrol.entity.PatrolExecution;
5
+import org.apache.ibatis.annotations.Mapper;
6
+import org.apache.ibatis.annotations.Param;
7
+import org.apache.ibatis.annotations.Select;
8
+
9
+import java.util.Map;
10
+
11
+@Mapper
12
+public interface PatrolExecutionMapper extends BaseMapper<PatrolExecution> {
13
+
14
+    @Select("SELECT " +
15
+            "COUNT(*) as total_executions, " +
16
+            "COUNT(CASE WHEN status = 'completed' THEN 1 END) as completed_count, " +
17
+            "COUNT(CASE WHEN status = 'in_progress' THEN 1 END) as in_progress_count, " +
18
+            "COALESCE(SUM(issue_count), 0) as total_issues, " +
19
+            "COALESCE(SUM(total_distance), 0) as total_distance " +
20
+            "FROM patrol_execution WHERE deleted = 0 AND task_id IN " +
21
+            "(SELECT id FROM patrol_task WHERE task_date BETWEEN #{startDate} AND #{endDate})")
22
+    Map<String, Object> selectExecutionStats(@Param("startDate") String startDate,
23
+                                             @Param("endDate") String endDate);
24
+}

+ 18
- 0
wm-patrol/src/main/java/com/water/patrol/mapper/PatrolIssueMapper.java 파일 보기

@@ -0,0 +1,18 @@
1
+package com.water.patrol.mapper;
2
+
3
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
4
+import com.water.patrol.entity.PatrolIssue;
5
+import org.apache.ibatis.annotations.Mapper;
6
+import org.apache.ibatis.annotations.Param;
7
+import org.apache.ibatis.annotations.Select;
8
+
9
+import java.util.List;
10
+import java.util.Map;
11
+
12
+@Mapper
13
+public interface PatrolIssueMapper extends BaseMapper<PatrolIssue> {
14
+
15
+    @Select("SELECT issue_type, severity, COUNT(*) as count FROM patrol_issue " +
16
+            "WHERE execution_id = #{executionId} AND deleted = 0 GROUP BY issue_type, severity")
17
+    List<Map<String, Object>> selectIssueSummary(@Param("executionId") Long executionId);
18
+}

+ 20
- 0
wm-patrol/src/main/java/com/water/patrol/mapper/PatrolRouteMapper.java 파일 보기

@@ -0,0 +1,20 @@
1
+package com.water.patrol.mapper;
2
+
3
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
4
+import com.water.patrol.entity.PatrolRoute;
5
+import org.apache.ibatis.annotations.Mapper;
6
+import org.apache.ibatis.annotations.Param;
7
+import org.apache.ibatis.annotations.Select;
8
+
9
+import java.util.List;
10
+import java.util.Map;
11
+
12
+@Mapper
13
+public interface PatrolRouteMapper extends BaseMapper<PatrolRoute> {
14
+
15
+    @Select("SELECT area, COUNT(*) as count FROM patrol_route WHERE status = 1 AND deleted = 0 GROUP BY area")
16
+    List<Map<String, Object>> countByArea();
17
+
18
+    @Select("SELECT * FROM patrol_route WHERE area = #{area} AND status = 1 AND deleted = 0 ORDER BY created_time DESC")
19
+    List<PatrolRoute> findByArea(@Param("area") String area);
20
+}

+ 25
- 0
wm-patrol/src/main/java/com/water/patrol/mapper/PatrolTaskMapper.java 파일 보기

@@ -0,0 +1,25 @@
1
+package com.water.patrol.mapper;
2
+
3
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
4
+import com.water.patrol.entity.PatrolTask;
5
+import org.apache.ibatis.annotations.Mapper;
6
+import org.apache.ibatis.annotations.Param;
7
+import org.apache.ibatis.annotations.Select;
8
+
9
+import java.time.LocalDate;
10
+import java.util.List;
11
+import java.util.Map;
12
+
13
+@Mapper
14
+public interface PatrolTaskMapper extends BaseMapper<PatrolTask> {
15
+
16
+    @Select("SELECT status, COUNT(*) as count FROM patrol_task WHERE task_date BETWEEN #{start} AND #{end} AND deleted = 0 GROUP BY status")
17
+    List<Map<String, Object>> countByStatusInRange(@Param("start") LocalDate start, @Param("end") LocalDate end);
18
+
19
+    @Select("SELECT worker_id, COUNT(*) as total, SUM(CASE WHEN status='completed' THEN 1 ELSE 0 END) as completed " +
20
+            "FROM patrol_task WHERE task_date BETWEEN #{start} AND #{end} AND deleted = 0 GROUP BY worker_id")
21
+    List<Map<String, Object>> workloadByWorker(@Param("start") LocalDate start, @Param("end") LocalDate end);
22
+
23
+    @Select("SELECT * FROM patrol_task WHERE task_date = #{date} AND worker_id = #{workerId} AND deleted = 0 ORDER BY plan_start")
24
+    List<PatrolTask> findByDateAndWorker(@Param("date") LocalDate date, @Param("workerId") Long workerId);
25
+}

+ 20
- 0
wm-patrol/src/main/java/com/water/patrol/mapper/PatrolWorkerMapper.java 파일 보기

@@ -0,0 +1,20 @@
1
+package com.water.patrol.mapper;
2
+
3
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
4
+import com.water.patrol.entity.PatrolWorker;
5
+import org.apache.ibatis.annotations.Mapper;
6
+import org.apache.ibatis.annotations.Param;
7
+import org.apache.ibatis.annotations.Select;
8
+
9
+import java.util.List;
10
+import java.util.Map;
11
+
12
+@Mapper
13
+public interface PatrolWorkerMapper extends BaseMapper<PatrolWorker> {
14
+
15
+    @Select("SELECT * FROM patrol_worker WHERE status = 1 AND deleted = 0 ORDER BY name")
16
+    List<PatrolWorker> findActive();
17
+
18
+    @Select("SELECT dept, COUNT(*) as count FROM patrol_worker WHERE status = 1 AND deleted = 0 GROUP BY dept")
19
+    List<Map<String, Object>> countByDept();
20
+}

+ 262
- 0
wm-patrol/src/main/java/com/water/patrol/service/ExecutionService.java 파일 보기

@@ -0,0 +1,262 @@
1
+package com.water.patrol.service;
2
+
3
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
4
+import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
5
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
6
+import com.water.patrol.entity.CheckItemRecord;
7
+import com.water.patrol.entity.PatrolExecution;
8
+import com.water.patrol.entity.dto.CheckItemRecordRequest;
9
+import com.water.patrol.entity.dto.CompleteExecutionRequest;
10
+import com.water.patrol.entity.dto.StartExecutionRequest;
11
+import com.water.patrol.mapper.CheckItemRecordMapper;
12
+import com.water.patrol.mapper.PatrolExecutionMapper;
13
+import lombok.RequiredArgsConstructor;
14
+import lombok.extern.slf4j.Slf4j;
15
+import org.springframework.stereotype.Service;
16
+import org.springframework.transaction.annotation.Transactional;
17
+
18
+import java.math.BigDecimal;
19
+import java.time.LocalDateTime;
20
+import java.util.List;
21
+import java.util.Map;
22
+
23
+@Slf4j
24
+@Service
25
+@RequiredArgsConstructor
26
+public class ExecutionService {
27
+
28
+    private final PatrolExecutionMapper executionMapper;
29
+    private final CheckItemRecordMapper checkItemRecordMapper;
30
+
31
+    /**
32
+     * 开始巡检执行
33
+     */
34
+    @Transactional
35
+    public PatrolExecution startExecution(StartExecutionRequest request) {
36
+        // Check if there's already an active execution for this task
37
+        LambdaQueryWrapper<PatrolExecution> wrapper = new LambdaQueryWrapper<>();
38
+        wrapper.eq(PatrolExecution::getTaskId, request.getTaskId())
39
+               .in(PatrolExecution::getStatus, "pending", "in_progress");
40
+        PatrolExecution existing = executionMapper.selectOne(wrapper);
41
+        if (existing != null) {
42
+            throw new RuntimeException("该任务已有进行中的巡检执行(ID:" + existing.getId() + ")");
43
+        }
44
+
45
+        PatrolExecution execution = new PatrolExecution();
46
+        execution.setTaskId(request.getTaskId());
47
+        execution.setInspectorId(request.getInspectorId());
48
+        execution.setInspectorName(request.getInspectorName());
49
+        execution.setStatus("in_progress");
50
+        execution.setStartTime(LocalDateTime.now());
51
+        execution.setTotalCheckItems(request.getTotalCheckItems() != null ? request.getTotalCheckItems() : 0);
52
+        execution.setCompletedCheckItems(0);
53
+        execution.setIssueCount(0);
54
+        execution.setTotalDistance(BigDecimal.ZERO);
55
+        execution.setStartLng(request.getStartLng());
56
+        execution.setStartLat(request.getStartLat());
57
+        execution.setRemark(request.getRemark());
58
+        executionMapper.insert(execution);
59
+
60
+        log.info("Patrol execution started: id={}, taskId={}, inspector={}",
61
+                execution.getId(), execution.getTaskId(), execution.getInspectorName());
62
+        return execution;
63
+    }
64
+
65
+    /**
66
+     * 获取执行详情
67
+     */
68
+    public PatrolExecution getExecution(Long executionId) {
69
+        PatrolExecution execution = executionMapper.selectById(executionId);
70
+        if (execution == null) {
71
+            throw new RuntimeException("巡检执行不存在: " + executionId);
72
+        }
73
+        return execution;
74
+    }
75
+
76
+    /**
77
+     * 获取某任务的执行列表
78
+     */
79
+    public List<PatrolExecution> getExecutionsByTaskId(Long taskId) {
80
+        LambdaQueryWrapper<PatrolExecution> wrapper = new LambdaQueryWrapper<>();
81
+        wrapper.eq(PatrolExecution::getTaskId, taskId)
82
+               .orderByDesc(PatrolExecution::getStartTime);
83
+        return executionMapper.selectList(wrapper);
84
+    }
85
+
86
+    /**
87
+     * 分页查询执行列表
88
+     */
89
+    public Page<PatrolExecution> listExecutions(Integer pageNum, Integer pageSize,
90
+                                                  String status, Long inspectorId) {
91
+        LambdaQueryWrapper<PatrolExecution> wrapper = new LambdaQueryWrapper<>();
92
+        if (status != null && !status.isEmpty()) {
93
+            wrapper.eq(PatrolExecution::getStatus, status);
94
+        }
95
+        if (inspectorId != null) {
96
+            wrapper.eq(PatrolExecution::getInspectorId, inspectorId);
97
+        }
98
+        wrapper.orderByDesc(PatrolExecution::getStartTime);
99
+        return executionMapper.selectPage(new Page<>(pageNum, pageSize), wrapper);
100
+    }
101
+
102
+    /**
103
+     * 提交检查项记录
104
+     */
105
+    @Transactional
106
+    public CheckItemRecord submitCheckItem(Long executionId, CheckItemRecordRequest request) {
107
+        PatrolExecution execution = getExecution(executionId);
108
+        if (!"in_progress".equals(execution.getStatus())) {
109
+            throw new RuntimeException("巡检执行不在进行中状态");
110
+        }
111
+
112
+        CheckItemRecord record = new CheckItemRecord();
113
+        record.setExecutionId(executionId);
114
+        record.setPointSeq(request.getPointSeq());
115
+        record.setPointName(request.getPointName());
116
+        record.setItemName(request.getItemName());
117
+        record.setCheckStatus(request.getCheckStatus());
118
+        record.setRecordValue(request.getRecordValue());
119
+        record.setValueUnit(request.getValueUnit());
120
+        record.setDescription(request.getDescription());
121
+        record.setRemark(request.getRemark());
122
+        record.setLng(request.getLng());
123
+        record.setLat(request.getLat());
124
+        record.setRecordTime(LocalDateTime.now());
125
+
126
+        // Serialize photo URLs
127
+        if (request.getPhotoUrls() != null && !request.getPhotoUrls().isEmpty()) {
128
+            record.setPhotoUrls(String.join(",", request.getPhotoUrls()));
129
+        }
130
+
131
+        checkItemRecordMapper.insert(record);
132
+
133
+        // Update completed check items count
134
+        execution.setCompletedCheckItems(execution.getCompletedCheckItems() + 1);
135
+        executionMapper.updateById(execution);
136
+
137
+        log.info("Check item recorded: executionId={}, point={}, item={}, status={}",
138
+                executionId, request.getPointName(), request.getItemName(), request.getCheckStatus());
139
+        return record;
140
+    }
141
+
142
+    /**
143
+     * 批量提交检查项记录
144
+     */
145
+    @Transactional
146
+    public int batchSubmitCheckItems(Long executionId, List<CheckItemRecordRequest> items) {
147
+        PatrolExecution execution = getExecution(executionId);
148
+        if (!"in_progress".equals(execution.getStatus())) {
149
+            throw new RuntimeException("巡检执行不在进行中状态");
150
+        }
151
+
152
+        int count = 0;
153
+        for (CheckItemRecordRequest request : items) {
154
+            CheckItemRecord record = new CheckItemRecord();
155
+            record.setExecutionId(executionId);
156
+            record.setPointSeq(request.getPointSeq());
157
+            record.setPointName(request.getPointName());
158
+            record.setItemName(request.getItemName());
159
+            record.setCheckStatus(request.getCheckStatus());
160
+            record.setRecordValue(request.getRecordValue());
161
+            record.setValueUnit(request.getValueUnit());
162
+            record.setDescription(request.getDescription());
163
+            record.setRemark(request.getRemark());
164
+            record.setLng(request.getLng());
165
+            record.setLat(request.getLat());
166
+            record.setRecordTime(LocalDateTime.now());
167
+            if (request.getPhotoUrls() != null && !request.getPhotoUrls().isEmpty()) {
168
+                record.setPhotoUrls(String.join(",", request.getPhotoUrls()));
169
+            }
170
+            checkItemRecordMapper.insert(record);
171
+            count++;
172
+        }
173
+
174
+        // Update count
175
+        execution.setCompletedCheckItems(execution.getCompletedCheckItems() + count);
176
+        executionMapper.updateById(execution);
177
+        return count;
178
+    }
179
+
180
+    /**
181
+     * 获取执行的检查项记录列表
182
+     */
183
+    public List<CheckItemRecord> getCheckItemRecords(Long executionId) {
184
+        LambdaQueryWrapper<CheckItemRecord> wrapper = new LambdaQueryWrapper<>();
185
+        wrapper.eq(CheckItemRecord::getExecutionId, executionId)
186
+               .orderByAsc(CheckItemRecord::getPointSeq)
187
+               .orderByAsc(CheckItemRecord::getRecordTime);
188
+        return checkItemRecordMapper.selectList(wrapper);
189
+    }
190
+
191
+    /**
192
+     * 获取检查项状态汇总
193
+     */
194
+    public List<Map<String, Object>> getCheckItemStatusSummary(Long executionId) {
195
+        return checkItemRecordMapper.selectStatusSummary(executionId);
196
+    }
197
+
198
+    /**
199
+     * 完成巡检执行
200
+     */
201
+    @Transactional
202
+    public PatrolExecution completeExecution(Long executionId, CompleteExecutionRequest request) {
203
+        PatrolExecution execution = getExecution(executionId);
204
+        if (!"in_progress".equals(execution.getStatus())) {
205
+            throw new RuntimeException("巡检执行不在进行中状态,无法完成");
206
+        }
207
+
208
+        execution.setStatus("completed");
209
+        execution.setEndTime(LocalDateTime.now());
210
+        if (request.getEndLng() != null) {
211
+            execution.setEndLng(request.getEndLng());
212
+        }
213
+        if (request.getEndLat() != null) {
214
+            execution.setEndLat(request.getEndLat());
215
+        }
216
+        if (request.getTotalDistance() != null) {
217
+            execution.setTotalDistance(BigDecimal.valueOf(request.getTotalDistance()));
218
+        }
219
+        if (request.getRemark() != null) {
220
+            execution.setRemark(request.getRemark());
221
+        }
222
+        executionMapper.updateById(execution);
223
+
224
+        log.info("Patrol execution completed: id={}, distance={}", executionId, execution.getTotalDistance());
225
+        return execution;
226
+    }
227
+
228
+    /**
229
+     * 中止巡检执行
230
+     */
231
+    @Transactional
232
+    public PatrolExecution abortExecution(Long executionId, String reason) {
233
+        PatrolExecution execution = getExecution(executionId);
234
+        if ("completed".equals(execution.getStatus())) {
235
+            throw new RuntimeException("已完成的巡检执行无法中止");
236
+        }
237
+
238
+        execution.setStatus("aborted");
239
+        execution.setEndTime(LocalDateTime.now());
240
+        execution.setRemark(reason);
241
+        executionMapper.updateById(execution);
242
+
243
+        log.info("Patrol execution aborted: id={}, reason={}", executionId, reason);
244
+        return execution;
245
+    }
246
+
247
+    /**
248
+     * 更新异常数量(由IssueService调用)
249
+     */
250
+    public void incrementIssueCount(Long executionId) {
251
+        PatrolExecution execution = getExecution(executionId);
252
+        execution.setIssueCount(execution.getIssueCount() + 1);
253
+        executionMapper.updateById(execution);
254
+    }
255
+
256
+    /**
257
+     * 获取执行统计
258
+     */
259
+    public Map<String, Object> getExecutionStats(String startDate, String endDate) {
260
+        return executionMapper.selectExecutionStats(startDate, endDate);
261
+    }
262
+}

+ 135
- 0
wm-patrol/src/main/java/com/water/patrol/service/IssueService.java 파일 보기

@@ -0,0 +1,135 @@
1
+package com.water.patrol.service;
2
+
3
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
4
+import com.water.patrol.entity.PatrolExecution;
5
+import com.water.patrol.entity.PatrolIssue;
6
+import com.water.patrol.entity.dto.PatrolIssueRequest;
7
+import com.water.patrol.mapper.PatrolIssueMapper;
8
+import lombok.RequiredArgsConstructor;
9
+import lombok.extern.slf4j.Slf4j;
10
+import org.springframework.stereotype.Service;
11
+import org.springframework.transaction.annotation.Transactional;
12
+
13
+import java.time.LocalDateTime;
14
+import java.util.List;
15
+import java.util.Map;
16
+
17
+@Slf4j
18
+@Service
19
+@RequiredArgsConstructor
20
+public class IssueService {
21
+
22
+    private final PatrolIssueMapper issueMapper;
23
+    private final ExecutionService executionService;
24
+
25
+    /**
26
+     * 上报问题
27
+     */
28
+    @Transactional
29
+    public PatrolIssue reportIssue(Long executionId, PatrolIssueRequest request) {
30
+        // Verify execution exists and is in progress
31
+        PatrolExecution execution = executionService.getExecution(executionId);
32
+        if (!"in_progress".equals(execution.getStatus())) {
33
+            throw new RuntimeException("巡检执行不在进行中状态,无法上报问题");
34
+        }
35
+
36
+        PatrolIssue issue = new PatrolIssue();
37
+        issue.setExecutionId(executionId);
38
+        issue.setCheckRecordId(request.getCheckRecordId());
39
+        issue.setIssueType(request.getIssueType());
40
+        issue.setSeverity(request.getSeverity());
41
+        issue.setDescription(request.getDescription());
42
+        issue.setLng(request.getLng());
43
+        issue.setLat(request.getLat());
44
+        issue.setAddress(request.getAddress());
45
+        issue.setHandleStatus("pending");
46
+        issue.setReporterId(execution.getInspectorId());
47
+        issue.setReporterName(execution.getInspectorName());
48
+        issue.setReportTime(LocalDateTime.now());
49
+
50
+        // Serialize photo URLs
51
+        if (request.getPhotoUrls() != null && !request.getPhotoUrls().isEmpty()) {
52
+            issue.setPhotoUrls(String.join(",", request.getPhotoUrls()));
53
+        }
54
+
55
+        issueMapper.insert(issue);
56
+
57
+        // Update issue count on execution
58
+        executionService.incrementIssueCount(executionId);
59
+
60
+        log.info("Issue reported: executionId={}, type={}, severity={}",
61
+                executionId, request.getIssueType(), request.getSeverity());
62
+        return issue;
63
+    }
64
+
65
+    /**
66
+     * 获取问题详情
67
+     */
68
+    public PatrolIssue getIssue(Long issueId) {
69
+        PatrolIssue issue = issueMapper.selectById(issueId);
70
+        if (issue == null) {
71
+            throw new RuntimeException("问题不存在: " + issueId);
72
+        }
73
+        return issue;
74
+    }
75
+
76
+    /**
77
+     * 获取某执行的所有问题
78
+     */
79
+    public List<PatrolIssue> getIssuesByExecutionId(Long executionId) {
80
+        LambdaQueryWrapper<PatrolIssue> wrapper = new LambdaQueryWrapper<>();
81
+        wrapper.eq(PatrolIssue::getExecutionId, executionId)
82
+               .orderByDesc(PatrolIssue::getReportTime);
83
+        return issueMapper.selectList(wrapper);
84
+    }
85
+
86
+    /**
87
+     * 更新问题处理状态
88
+     */
89
+    @Transactional
90
+    public PatrolIssue updateIssueStatus(Long issueId, String handleStatus) {
91
+        PatrolIssue issue = getIssue(issueId);
92
+        issue.setHandleStatus(handleStatus);
93
+        issueMapper.updateById(issue);
94
+        log.info("Issue status updated: id={}, status={}", issueId, handleStatus);
95
+        return issue;
96
+    }
97
+
98
+    /**
99
+     * 关联工单
100
+     */
101
+    @Transactional
102
+    public PatrolIssue linkWorkOrder(Long issueId, Long workOrderId) {
103
+        PatrolIssue issue = getIssue(issueId);
104
+        issue.setWorkOrderId(workOrderId);
105
+        issue.setHandleStatus("processing");
106
+        issueMapper.updateById(issue);
107
+        log.info("Issue linked to work order: issueId={}, workOrderId={}", issueId, workOrderId);
108
+        return issue;
109
+    }
110
+
111
+    /**
112
+     * 获取问题汇总
113
+     */
114
+    public List<Map<String, Object>> getIssueSummary(Long executionId) {
115
+        return issueMapper.selectIssueSummary(executionId);
116
+    }
117
+
118
+    /**
119
+     * 按类型和状态查询问题列表
120
+     */
121
+    public List<PatrolIssue> queryIssues(String issueType, String handleStatus, Long executionId) {
122
+        LambdaQueryWrapper<PatrolIssue> wrapper = new LambdaQueryWrapper<>();
123
+        if (issueType != null && !issueType.isEmpty()) {
124
+            wrapper.eq(PatrolIssue::getIssueType, issueType);
125
+        }
126
+        if (handleStatus != null && !handleStatus.isEmpty()) {
127
+            wrapper.eq(PatrolIssue::getHandleStatus, handleStatus);
128
+        }
129
+        if (executionId != null) {
130
+            wrapper.eq(PatrolIssue::getExecutionId, executionId);
131
+        }
132
+        wrapper.orderByDesc(PatrolIssue::getReportTime);
133
+        return issueMapper.selectList(wrapper);
134
+    }
135
+}

+ 0
- 118
wm-patrol/src/main/java/com/water/patrol/service/PatrolService.java 파일 보기

@@ -1,118 +0,0 @@
1
-package com.water.patrol.service;
2
-
3
-import lombok.RequiredArgsConstructor;
4
-import lombok.extern.slf4j.Slf4j;
5
-import org.springframework.jdbc.core.JdbcTemplate;
6
-import org.springframework.stereotype.Service;
7
-
8
-import java.time.LocalDate;
9
-import java.util.*;
10
-
11
-@Slf4j
12
-@Service
13
-@RequiredArgsConstructor
14
-public class PatrolService {
15
-
16
-    private final JdbcTemplate jdbc;
17
-
18
-    // ========== 路线管理 ==========
19
-    public Map<String, Object> createRoute(String routeName, String area, List<Map<String, Object>> points, int estimDuration) {
20
-        jdbc.update("INSERT INTO patrol_route (route_name, area, route_points, estim_duration) VALUES (?,?,?::jsonb,?)",
21
-            routeName, area, points.toString(), estimDuration);
22
-        return Map.of("routeName", routeName, "area", area, "points", points.size());
23
-    }
24
-
25
-    public List<Map<String, Object>> getRoutes(String area) {
26
-        return jdbc.queryForList("SELECT * FROM patrol_route WHERE area = ? AND status = 1", area);
27
-    }
28
-
29
-    // ========== 任务管理 ==========
30
-    public Map<String, Object> createTask(Long routeId, Long assigneeId, String taskDate) {
31
-        jdbc.update(
32
-            "INSERT INTO patrol_task (route_id, assignee_id, task_name, task_date, plan_start, plan_end, status) " +
33
-            "SELECT ?, ?, route_name, ?, CAST(? AS TIMESTAMP), CAST(? AS TIMESTAMP) + (estim_duration || ' minutes')::INTERVAL, 'pending' " +
34
-            "FROM patrol_route WHERE id = ?",
35
-            routeId, assigneeId, taskDate, taskDate + " 09:00:00", taskDate + " 09:00:00", routeId);
36
-        return Map.of("routeId", routeId, "assigneeId", assigneeId, "date", taskDate, "status", "created");
37
-    }
38
-
39
-    public List<Map<String, Object>> getTodayTasks(Long userId) {
40
-        return jdbc.queryForList(
41
-            "SELECT pt.*, pr.route_name, pr.area FROM patrol_task pt " +
42
-            "LEFT JOIN patrol_route pr ON pt.route_id = pr.id " +
43
-            "WHERE pt.task_date = CURRENT_DATE AND pt.assignee_id = ? " +
44
-            "ORDER BY pt.plan_start", userId);
45
-    }
46
-
47
-    public Map<String, Object> startTask(Long taskId) {
48
-        jdbc.update("UPDATE patrol_task SET status = 'in_progress', actual_start = NOW() WHERE id = ?", taskId);
49
-        return Map.of("taskId", taskId, "status", "in_progress", "startedAt", new Date());
50
-    }
51
-
52
-    public Map<String, Object> completeTask(Long taskId, double distance) {
53
-        jdbc.update(
54
-            "UPDATE patrol_task SET status = 'completed', actual_end = NOW(), distance = ? WHERE id = ?",
55
-            distance, taskId);
56
-        return Map.of("taskId", taskId, "status", "completed", "distance", distance);
57
-    }
58
-
59
-    // ========== 巡检记录 ==========
60
-    public Map<String, Object> recordCheck(Long taskId, int pointSeq, Long deviceId,
61
-                                            List<Map<String, Object>> checkItems,
62
-                                            double lng, double lat) {
63
-        jdbc.update(
64
-            "INSERT INTO patrol_record (task_id, point_seq, device_id, check_items, gps_lng, gps_lat, record_time) " +
65
-            "VALUES (?,?,?,?::jsonb,?,?,NOW())",
66
-            taskId, pointSeq, deviceId, checkItems.toString(), lng, lat);
67
-        return Map.of("taskId", taskId, "pointSeq", pointSeq, "recorded", true);
68
-    }
69
-
70
-    public List<Map<String, Object>> getTaskRecords(Long taskId) {
71
-        return jdbc.queryForList(
72
-            "SELECT * FROM patrol_record WHERE task_id = ? ORDER BY point_seq", taskId);
73
-    }
74
-
75
-    // ========== 问题上报(巡检APP) ==========
76
-    public Map<String, Object> reportIssue(Long taskId, Long deviceId, String issueType,
77
-                                            String description, List<String> photoUrls,
78
-                                            double lng, double lat) {
79
-        // 自动创建工单
80
-        jdbc.update(
81
-            "INSERT INTO patrol_task (task_name, assignee_id, task_date, status) " +
82
-            "SELECT CONCAT('问题处理: ', ?), assignee_id, CURRENT_DATE, 'pending' FROM patrol_task WHERE id = ?",
83
-            issueType + ": " + description.substring(0, Math.min(description.length(), 50)), taskId);
84
-
85
-        log.info("Issue reported: type={} desc={}", issueType, description);
86
-        return Map.of("reported", true, "issueType", issueType, "photos", photoUrls);
87
-    }
88
-
89
-    // ========== 统计分析 ==========
90
-    public Map<String, Object> getStats(String area, LocalDate start, LocalDate end) {
91
-        Map<String, Object> stats = new LinkedHashMap<>();
92
-
93
-        // 任务执行率
94
-        stats.put("completionRate", jdbc.queryForMap(
95
-            "SELECT COUNT(*) as total, SUM(CASE WHEN status='completed' THEN 1 ELSE 0 END) as completed " +
96
-            "FROM patrol_task WHERE task_date BETWEEN ? AND ?", start, end));
97
-
98
-        // 人员里程
99
-        stats.put("personDistance", jdbc.queryForList(
100
-            "SELECT u.real_name, SUM(pt.distance) as total_km " +
101
-            "FROM patrol_task pt JOIN sys_user u ON pt.assignee_id = u.id " +
102
-            "WHERE pt.task_date BETWEEN ? AND ? GROUP BY u.id, u.real_name", start, end));
103
-
104
-        // 巡检工作量
105
-        stats.put("workload", jdbc.queryForList(
106
-            "SELECT task_date, COUNT(*) as tasks, SUM(distance) as total_km " +
107
-            "FROM patrol_task WHERE task_date BETWEEN ? AND ? GROUP BY task_date ORDER BY task_date",
108
-            start, end));
109
-
110
-        // 问题分类统计
111
-        stats.put("issueStats", jdbc.queryForList(
112
-            "SELECT SUBSTRING(task_name FROM '^[^:]+') as issue_type, COUNT(*) as count " +
113
-            "FROM patrol_task WHERE task_name LIKE '%问题处理:%' AND task_date BETWEEN ? AND ? GROUP BY 1",
114
-            start, end));
115
-
116
-        return stats;
117
-    }
118
-}

+ 292
- 0
wm-patrol/src/main/java/com/water/patrol/service/RouteService.java 파일 보기

@@ -0,0 +1,292 @@
1
+package com.water.patrol.service;
2
+
3
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
4
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
5
+import com.water.patrol.entity.PatrolCheckpoint;
6
+import com.water.patrol.entity.PatrolRoute;
7
+import com.water.patrol.mapper.PatrolCheckpointMapper;
8
+import com.water.patrol.mapper.PatrolRouteMapper;
9
+import lombok.RequiredArgsConstructor;
10
+import lombok.extern.slf4j.Slf4j;
11
+import org.springframework.stereotype.Service;
12
+import org.springframework.transaction.annotation.Transactional;
13
+
14
+import java.time.LocalDateTime;
15
+import java.util.*;
16
+
17
+/**
18
+ * 巡检路线管理服务 - 路线CRUD + 检查点设置 + 路线优化
19
+ */
20
+@Slf4j
21
+@Service
22
+@RequiredArgsConstructor
23
+public class RouteService {
24
+
25
+    private final PatrolRouteMapper routeMapper;
26
+    private final PatrolCheckpointMapper checkpointMapper;
27
+
28
+    // ==================== 路线 CRUD ====================
29
+
30
+    /**
31
+     * 分页查询路线
32
+     */
33
+    public Page<PatrolRoute> page(int current, int size, String keyword, String area, Integer status) {
34
+        LambdaQueryWrapper<PatrolRoute> wrapper = new LambdaQueryWrapper<>();
35
+        if (keyword != null && !keyword.isEmpty()) {
36
+            wrapper.and(w -> w.like(PatrolRoute::getRouteName, keyword)
37
+                    .or().like(PatrolRoute::getRouteCode, keyword)
38
+                    .or().like(PatrolRoute::getArea, keyword));
39
+        }
40
+        if (area != null && !area.isEmpty()) {
41
+            wrapper.eq(PatrolRoute::getArea, area);
42
+        }
43
+        if (status != null) {
44
+            wrapper.eq(PatrolRoute::getStatus, status);
45
+        }
46
+        wrapper.orderByDesc(PatrolRoute::getCreatedTime);
47
+        return routeMapper.selectPage(new Page<>(current, size), wrapper);
48
+    }
49
+
50
+    /**
51
+     * 查询路线详情
52
+     */
53
+    public PatrolRoute getById(Long id) {
54
+        return routeMapper.selectById(id);
55
+    }
56
+
57
+    /**
58
+     * 创建路线
59
+     */
60
+    @Transactional
61
+    public PatrolRoute create(PatrolRoute route) {
62
+        route.setCreatedTime(LocalDateTime.now());
63
+        route.setUpdatedTime(LocalDateTime.now());
64
+        route.setDeleted(0);
65
+        if (route.getStatus() == null) route.setStatus(1);
66
+        if (route.getEstimDuration() == null) route.setEstimDuration(60);
67
+        if (route.getTotalDistance() == null) route.setTotalDistance(0.0);
68
+        routeMapper.insert(route);
69
+        return route;
70
+    }
71
+
72
+    /**
73
+     * 更新路线
74
+     */
75
+    @Transactional
76
+    public boolean update(PatrolRoute route) {
77
+        route.setUpdatedTime(LocalDateTime.now());
78
+        return routeMapper.updateById(route) > 0;
79
+    }
80
+
81
+    /**
82
+     * 删除路线(逻辑删除)
83
+     */
84
+    @Transactional
85
+    public boolean delete(Long id) {
86
+        return routeMapper.deleteById(id) > 0;
87
+    }
88
+
89
+    /**
90
+     * 启用/停用路线
91
+     */
92
+    @Transactional
93
+    public boolean toggleStatus(Long id, int status) {
94
+        PatrolRoute route = new PatrolRoute();
95
+        route.setId(id);
96
+        route.setStatus(status);
97
+        route.setUpdatedTime(LocalDateTime.now());
98
+        return routeMapper.updateById(route) > 0;
99
+    }
100
+
101
+    /**
102
+     * 按区域统计路线数
103
+     */
104
+    public List<Map<String, Object>> countByArea() {
105
+        return routeMapper.countByArea();
106
+    }
107
+
108
+    // ==================== 检查点管理 ====================
109
+
110
+    /**
111
+     * 查询路线的所有检查点
112
+     */
113
+    public List<PatrolCheckpoint> getCheckpoints(Long routeId) {
114
+        return checkpointMapper.findByRouteId(routeId);
115
+    }
116
+
117
+    /**
118
+     * 批量设置路线检查点(替换模式)
119
+     */
120
+    @Transactional
121
+    public List<PatrolCheckpoint> setCheckpoints(Long routeId, List<PatrolCheckpoint> checkpoints) {
122
+        // 先删除旧检查点(逻辑删除)
123
+        LambdaQueryWrapper<PatrolCheckpoint> wrapper = new LambdaQueryWrapper<>();
124
+        wrapper.eq(PatrolCheckpoint::getRouteId, routeId);
125
+        checkpointMapper.delete(wrapper);
126
+
127
+        // 批量插入新检查点
128
+        List<PatrolCheckpoint> saved = new ArrayList<>();
129
+        for (int i = 0; i < checkpoints.size(); i++) {
130
+            PatrolCheckpoint cp = checkpoints.get(i);
131
+            cp.setId(null);
132
+            cp.setRouteId(routeId);
133
+            cp.setSeq(i + 1);
134
+            cp.setDeleted(0);
135
+            cp.setCreatedTime(LocalDateTime.now());
136
+            cp.setUpdatedTime(LocalDateTime.now());
137
+            if (cp.getRequired() == null) cp.setRequired(1);
138
+            checkpointMapper.insert(cp);
139
+            saved.add(cp);
140
+        }
141
+
142
+        // 更新路线的检查点数量信息
143
+        updateRouteDistanceAndDuration(routeId);
144
+
145
+        return saved;
146
+    }
147
+
148
+    /**
149
+     * 添加单个检查点
150
+     */
151
+    @Transactional
152
+    public PatrolCheckpoint addCheckpoint(PatrolCheckpoint checkpoint) {
153
+        checkpoint.setDeleted(0);
154
+        checkpoint.setCreatedTime(LocalDateTime.now());
155
+        checkpoint.setUpdatedTime(LocalDateTime.now());
156
+        if (checkpoint.getRequired() == null) checkpoint.setRequired(1);
157
+        // 自动设置序号为最后
158
+        int count = checkpointMapper.countByRouteId(checkpoint.getRouteId());
159
+        checkpoint.setSeq(count + 1);
160
+        checkpointMapper.insert(checkpoint);
161
+        return checkpoint;
162
+    }
163
+
164
+    /**
165
+     * 删除检查点
166
+     */
167
+    @Transactional
168
+    public boolean deleteCheckpoint(Long id) {
169
+        return checkpointMapper.deleteById(id) > 0;
170
+    }
171
+
172
+    // ==================== 路线优化 ====================
173
+
174
+    /**
175
+     * 路线优化 - 根据检查点坐标计算最优距离(贪心最近邻)
176
+     * 返回优化后的距离和顺序建议
177
+     */
178
+    public Map<String, Object> optimizeRoute(Long routeId) {
179
+        List<PatrolCheckpoint> checkpoints = checkpointMapper.findByRouteId(routeId);
180
+        if (checkpoints.size() < 2) {
181
+            return Map.of("optimized", false, "reason", "检查点数量不足,无需优化");
182
+        }
183
+
184
+        // 贪心最近邻算法优化顺序
185
+        List<PatrolCheckpoint> optimized = nearestNeighborOptimize(checkpoints);
186
+
187
+        double totalDistance = calculateTotalDistance(optimized);
188
+
189
+        // 重新排列序号
190
+        for (int i = 0; i < optimized.size(); i++) {
191
+            PatrolCheckpoint cp = optimized.get(i);
192
+            PatrolCheckpoint update = new PatrolCheckpoint();
193
+            update.setId(cp.getId());
194
+            update.setSeq(i + 1);
195
+            update.setUpdatedTime(LocalDateTime.now());
196
+            checkpointMapper.updateById(update);
197
+        }
198
+
199
+        // 更新路线距离
200
+        PatrolRoute routeUpdate = new PatrolRoute();
201
+        routeUpdate.setId(routeId);
202
+        routeUpdate.setTotalDistance(Math.round(totalDistance * 100.0) / 100.0);
203
+        routeUpdate.setEstimDuration((int) (totalDistance / 4.0 * 60)); // 按4km/h步行速度估算
204
+        routeUpdate.setUpdatedTime(LocalDateTime.now());
205
+        routeMapper.updateById(routeUpdate);
206
+
207
+        Map<String, Object> result = new LinkedHashMap<>();
208
+        result.put("optimized", true);
209
+        result.put("checkpointCount", optimized.size());
210
+        result.put("totalDistanceKm", Math.round(totalDistance * 100.0) / 100.0);
211
+        result.put("estimatedMinutes", (int) (totalDistance / 4.0 * 60));
212
+        result.put("optimizedOrder", optimized.stream().map(PatrolCheckpoint::getName).toList());
213
+        return result;
214
+    }
215
+
216
+    /**
217
+     * 最近邻优化
218
+     */
219
+    private List<PatrolCheckpoint> nearestNeighborOptimize(List<PatrolCheckpoint> checkpoints) {
220
+        List<PatrolCheckpoint> remaining = new ArrayList<>(checkpoints);
221
+        List<PatrolCheckpoint> result = new ArrayList<>();
222
+
223
+        // 从第一个检查点开始
224
+        PatrolCheckpoint current = remaining.remove(0);
225
+        result.add(current);
226
+
227
+        while (!remaining.isEmpty()) {
228
+            PatrolCheckpoint nearest = null;
229
+            double minDist = Double.MAX_VALUE;
230
+            for (PatrolCheckpoint cp : remaining) {
231
+                double dist = haversine(
232
+                        current.getLat() != null ? current.getLat() : 0,
233
+                        current.getLng() != null ? current.getLng() : 0,
234
+                        cp.getLat() != null ? cp.getLat() : 0,
235
+                        cp.getLng() != null ? cp.getLng() : 0);
236
+                if (dist < minDist) {
237
+                    minDist = dist;
238
+                    nearest = cp;
239
+                }
240
+            }
241
+            remaining.remove(nearest);
242
+            result.add(nearest);
243
+            current = nearest;
244
+        }
245
+        return result;
246
+    }
247
+
248
+    /**
249
+     * 计算总距离
250
+     */
251
+    private double calculateTotalDistance(List<PatrolCheckpoint> checkpoints) {
252
+        double total = 0;
253
+        for (int i = 1; i < checkpoints.size(); i++) {
254
+            PatrolCheckpoint prev = checkpoints.get(i - 1);
255
+            PatrolCheckpoint curr = checkpoints.get(i);
256
+            total += haversine(
257
+                    prev.getLat() != null ? prev.getLat() : 0,
258
+                    prev.getLng() != null ? prev.getLng() : 0,
259
+                    curr.getLat() != null ? curr.getLat() : 0,
260
+                    curr.getLng() != null ? curr.getLng() : 0);
261
+        }
262
+        return total;
263
+    }
264
+
265
+    /**
266
+     * Haversine公式计算两点距离(km)
267
+     */
268
+    double haversine(double lat1, double lng1, double lat2, double lng2) {
269
+        final double R = 6371.0;
270
+        double dLat = Math.toRadians(lat2 - lat1);
271
+        double dLng = Math.toRadians(lng2 - lng1);
272
+        double a = Math.sin(dLat / 2) * Math.sin(dLat / 2)
273
+                + Math.cos(Math.toRadians(lat1)) * Math.cos(Math.toRadians(lat2))
274
+                * Math.sin(dLng / 2) * Math.sin(dLng / 2);
275
+        double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
276
+        return R * c;
277
+    }
278
+
279
+    /**
280
+     * 更新路线距离和时长估算
281
+     */
282
+    private void updateRouteDistanceAndDuration(Long routeId) {
283
+        List<PatrolCheckpoint> checkpoints = checkpointMapper.findByRouteId(routeId);
284
+        double distance = calculateTotalDistance(checkpoints);
285
+        PatrolRoute route = new PatrolRoute();
286
+        route.setId(routeId);
287
+        route.setTotalDistance(Math.round(distance * 100.0) / 100.0);
288
+        route.setEstimDuration((int) (distance / 4.0 * 60));
289
+        route.setUpdatedTime(LocalDateTime.now());
290
+        routeMapper.updateById(route);
291
+    }
292
+}

+ 362
- 0
wm-patrol/src/main/java/com/water/patrol/service/TaskService.java 파일 보기

@@ -0,0 +1,362 @@
1
+package com.water.patrol.service;
2
+
3
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
4
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
5
+import com.water.patrol.entity.PatrolCheckpoint;
6
+import com.water.patrol.entity.PatrolRoute;
7
+import com.water.patrol.entity.PatrolTask;
8
+import com.water.patrol.entity.PatrolWorker;
9
+import com.water.patrol.mapper.PatrolCheckpointMapper;
10
+import com.water.patrol.mapper.PatrolRouteMapper;
11
+import com.water.patrol.mapper.PatrolTaskMapper;
12
+import com.water.patrol.mapper.PatrolWorkerMapper;
13
+import lombok.RequiredArgsConstructor;
14
+import lombok.extern.slf4j.Slf4j;
15
+import org.springframework.stereotype.Service;
16
+import org.springframework.transaction.annotation.Transactional;
17
+
18
+import java.time.DayOfWeek;
19
+import java.time.LocalDate;
20
+import java.time.LocalDateTime;
21
+import java.time.LocalTime;
22
+import java.time.temporal.TemporalAdjusters;
23
+import java.util.*;
24
+
25
+/**
26
+ * 巡检任务管理服务 - 任务创建 + 自动/手动分派 + 周期任务
27
+ */
28
+@Slf4j
29
+@Service
30
+@RequiredArgsConstructor
31
+public class TaskService {
32
+
33
+    private final PatrolTaskMapper taskMapper;
34
+    private final PatrolRouteMapper routeMapper;
35
+    private final PatrolWorkerMapper workerMapper;
36
+    private final PatrolCheckpointMapper checkpointMapper;
37
+
38
+    // ==================== 任务 CRUD ====================
39
+
40
+    /**
41
+     * 分页查询任务
42
+     */
43
+    public Page<PatrolTask> page(int current, int size, String keyword, String status,
44
+                                  LocalDate startDate, LocalDate endDate, Long workerId) {
45
+        LambdaQueryWrapper<PatrolTask> wrapper = new LambdaQueryWrapper<>();
46
+        if (keyword != null && !keyword.isEmpty()) {
47
+            wrapper.like(PatrolTask::getTaskName, keyword);
48
+        }
49
+        if (status != null && !status.isEmpty()) {
50
+            wrapper.eq(PatrolTask::getStatus, status);
51
+        }
52
+        if (startDate != null) {
53
+            wrapper.ge(PatrolTask::getTaskDate, startDate);
54
+        }
55
+        if (endDate != null) {
56
+            wrapper.le(PatrolTask::getTaskDate, endDate);
57
+        }
58
+        if (workerId != null) {
59
+            wrapper.eq(PatrolTask::getWorkerId, workerId);
60
+        }
61
+        wrapper.orderByDesc(PatrolTask::getTaskDate).orderByAsc(PatrolTask::getPlanStart);
62
+        return taskMapper.selectPage(new Page<>(current, size), wrapper);
63
+    }
64
+
65
+    /**
66
+     * 查询任务详情
67
+     */
68
+    public PatrolTask getById(Long id) {
69
+        return taskMapper.selectById(id);
70
+    }
71
+
72
+    /**
73
+     * 创建任务
74
+     */
75
+    @Transactional
76
+    public PatrolTask create(PatrolTask task) {
77
+        task.setCreatedTime(LocalDateTime.now());
78
+        task.setUpdatedTime(LocalDateTime.now());
79
+        task.setDeleted(0);
80
+        if (task.getStatus() == null) task.setStatus("pending");
81
+        if (task.getPriority() == null) task.setPriority(0);
82
+        if (task.getDistance() == null) task.setDistance(0.0);
83
+        if (task.getCheckpointDone() == null) task.setCheckpointDone(0);
84
+
85
+        // 如果关联了路线,自动设置检查点总数和计划时间
86
+        if (task.getRouteId() != null) {
87
+            int cpCount = checkpointMapper.countByRouteId(task.getRouteId());
88
+            task.setCheckpointTotal(cpCount);
89
+
90
+            PatrolRoute route = routeMapper.selectById(task.getRouteId());
91
+            if (route != null && task.getTaskName() == null) {
92
+                task.setTaskName("巡检: " + route.getRouteName());
93
+            }
94
+        }
95
+        if (task.getCheckpointTotal() == null) task.setCheckpointTotal(0);
96
+
97
+        if (task.getTaskDate() != null) {
98
+            LocalDateTime dateStart = task.getTaskDate().atTime(9, 0);
99
+            if (task.getPlanStart() == null) task.setPlanStart(dateStart);
100
+            if (task.getPlanEnd() == null) {
101
+                int duration = 60;
102
+                if (task.getRouteId() != null) {
103
+                    PatrolRoute route = routeMapper.selectById(task.getRouteId());
104
+                    if (route != null && route.getEstimDuration() != null) {
105
+                        duration = route.getEstimDuration();
106
+                    }
107
+                }
108
+                task.setPlanEnd(dateStart.plusMinutes(duration));
109
+            }
110
+        }
111
+
112
+        taskMapper.insert(task);
113
+        return task;
114
+    }
115
+
116
+    /**
117
+     * 更新任务
118
+     */
119
+    @Transactional
120
+    public boolean update(PatrolTask task) {
121
+        task.setUpdatedTime(LocalDateTime.now());
122
+        return taskMapper.updateById(task) > 0;
123
+    }
124
+
125
+    /**
126
+     * 删除任务
127
+     */
128
+    @Transactional
129
+    public boolean delete(Long id) {
130
+        return taskMapper.deleteById(id) > 0;
131
+    }
132
+
133
+    // ==================== 任务分派 ====================
134
+
135
+    /**
136
+     * 手动分派任务给巡检员
137
+     */
138
+    @Transactional
139
+    public boolean assign(Long taskId, Long workerId) {
140
+        PatrolTask task = new PatrolTask();
141
+        task.setId(taskId);
142
+        task.setWorkerId(workerId);
143
+        task.setStatus("assigned");
144
+        task.setUpdatedTime(LocalDateTime.now());
145
+        return taskMapper.updateById(task) > 0;
146
+    }
147
+
148
+    /**
149
+     * 自动分派 - 选择当前负荷最低的可用巡检员
150
+     */
151
+    @Transactional
152
+    public Map<String, Object> autoAssign(Long taskId) {
153
+        PatrolTask task = taskMapper.selectById(taskId);
154
+        if (task == null) {
155
+            return Map.of("success", false, "reason", "任务不存在");
156
+        }
157
+
158
+        // 查找在岗的巡检员
159
+        List<PatrolWorker> workers = workerMapper.findActive();
160
+        if (workers.isEmpty()) {
161
+            return Map.of("success", false, "reason", "无可用巡检员");
162
+        }
163
+
164
+        // 计算每个巡检员当天的任务负荷
165
+        PatrolWorker bestWorker = null;
166
+        int minLoad = Integer.MAX_VALUE;
167
+
168
+        for (PatrolWorker worker : workers) {
169
+            List<PatrolTask> workerTasks = taskMapper.findByDateAndWorker(task.getTaskDate(), worker.getId());
170
+            if (workerTasks.size() < minLoad) {
171
+                minLoad = workerTasks.size();
172
+                bestWorker = worker;
173
+            }
174
+        }
175
+
176
+        if (bestWorker == null) {
177
+            return Map.of("success", false, "reason", "无法选择巡检员");
178
+        }
179
+
180
+        // 分派任务
181
+        assign(taskId, bestWorker.getId());
182
+
183
+        Map<String, Object> result = new LinkedHashMap<>();
184
+        result.put("success", true);
185
+        result.put("taskId", taskId);
186
+        result.put("workerId", bestWorker.getId());
187
+        result.put("workerName", bestWorker.getName());
188
+        result.put("currentLoad", minLoad);
189
+        return result;
190
+    }
191
+
192
+    /**
193
+     * 批量自动分派 - 对指定日期的所有pending任务进行自动分派
194
+     */
195
+    @Transactional
196
+    public Map<String, Object> batchAutoAssign(LocalDate date) {
197
+        LambdaQueryWrapper<PatrolTask> wrapper = new LambdaQueryWrapper<>();
198
+        wrapper.eq(PatrolTask::getStatus, "pending");
199
+        wrapper.eq(PatrolTask::getTaskDate, date);
200
+        wrapper.eq(PatrolTask::getDeleted, 0);
201
+        List<PatrolTask> pendingTasks = taskMapper.selectList(wrapper);
202
+
203
+        int assigned = 0;
204
+        for (PatrolTask task : pendingTasks) {
205
+            Map<String, Object> result = autoAssign(task.getId());
206
+            if (Boolean.TRUE.equals(result.get("success"))) {
207
+                assigned++;
208
+            }
209
+        }
210
+
211
+        return Map.of("total", pendingTasks.size(), "assigned", assigned, "date", date.toString());
212
+    }
213
+
214
+    /**
215
+     * 开始任务
216
+     */
217
+    @Transactional
218
+    public boolean startTask(Long taskId) {
219
+        PatrolTask task = new PatrolTask();
220
+        task.setId(taskId);
221
+        task.setStatus("in_progress");
222
+        task.setActualStart(LocalDateTime.now());
223
+        task.setUpdatedTime(LocalDateTime.now());
224
+        return taskMapper.updateById(task) > 0;
225
+    }
226
+
227
+    /**
228
+     * 完成任务
229
+     */
230
+    @Transactional
231
+    public boolean completeTask(Long taskId, Double distance) {
232
+        PatrolTask task = new PatrolTask();
233
+        task.setId(taskId);
234
+        task.setStatus("completed");
235
+        task.setActualEnd(LocalDateTime.now());
236
+        task.setDistance(distance);
237
+        task.setUpdatedTime(LocalDateTime.now());
238
+        return taskMapper.updateById(task) > 0;
239
+    }
240
+
241
+    /**
242
+     * 取消任务
243
+     */
244
+    @Transactional
245
+    public boolean cancelTask(Long taskId, String reason) {
246
+        PatrolTask task = new PatrolTask();
247
+        task.setId(taskId);
248
+        task.setStatus("cancelled");
249
+        task.setRemark(reason);
250
+        task.setUpdatedTime(LocalDateTime.now());
251
+        return taskMapper.updateById(task) > 0;
252
+    }
253
+
254
+    // ==================== 周期任务 ====================
255
+
256
+    /**
257
+     * 创建周期任务(日/周/月)
258
+     * 从指定开始日期起,生成N个周期的任务
259
+     */
260
+    @Transactional
261
+    public Map<String, Object> createCycleTask(PatrolTask template, String cycleType,
262
+                                                 LocalDate startDate, int cycles) {
263
+        List<LocalDate> dates = generateCycleDates(cycleType, startDate, cycles);
264
+        List<PatrolTask> createdTasks = new ArrayList<>();
265
+
266
+        for (LocalDate date : dates) {
267
+            PatrolTask task = new PatrolTask();
268
+            task.setTaskName(template.getTaskName());
269
+            task.setRouteId(template.getRouteId());
270
+            task.setWorkerId(template.getWorkerId());
271
+            task.setTaskDate(date);
272
+            task.setCycleType(cycleType);
273
+            task.setPriority(template.getPriority());
274
+            task.setRemark(template.getRemark());
275
+
276
+            // 直接创建
277
+            task.setCreatedTime(LocalDateTime.now());
278
+            task.setUpdatedTime(LocalDateTime.now());
279
+            task.setDeleted(0);
280
+            task.setStatus(template.getWorkerId() != null ? "assigned" : "pending");
281
+            if (task.getPriority() == null) task.setPriority(0);
282
+            task.setDistance(0.0);
283
+            task.setCheckpointDone(0);
284
+
285
+            if (task.getRouteId() != null) {
286
+                int cpCount = checkpointMapper.countByRouteId(task.getRouteId());
287
+                task.setCheckpointTotal(cpCount);
288
+            }
289
+            if (task.getCheckpointTotal() == null) task.setCheckpointTotal(0);
290
+
291
+            LocalDateTime dateStart = date.atTime(9, 0);
292
+            task.setPlanStart(dateStart);
293
+            int duration = 60;
294
+            if (task.getRouteId() != null) {
295
+                PatrolRoute route = routeMapper.selectById(task.getRouteId());
296
+                if (route != null && route.getEstimDuration() != null) {
297
+                    duration = route.getEstimDuration();
298
+                }
299
+            }
300
+            task.setPlanEnd(dateStart.plusMinutes(duration));
301
+
302
+            taskMapper.insert(task);
303
+            createdTasks.add(task);
304
+        }
305
+
306
+        Map<String, Object> result = new LinkedHashMap<>();
307
+        result.put("cycleType", cycleType);
308
+        result.put("startDate", startDate.toString());
309
+        result.put("cycles", cycles);
310
+        result.put("created", createdTasks.size());
311
+        result.put("tasks", createdTasks.stream().map(t -> Map.of("id", t.getId(), "date", t.getTaskDate().toString())).toList());
312
+        return result;
313
+    }
314
+
315
+    /**
316
+     * 根据周期类型生成日期列表
317
+     */
318
+    List<LocalDate> generateCycleDates(String cycleType, LocalDate startDate, int cycles) {
319
+        List<LocalDate> dates = new ArrayList<>();
320
+        LocalDate current = startDate;
321
+
322
+        for (int i = 0; i < cycles; i++) {
323
+            dates.add(current);
324
+            switch (cycleType.toLowerCase()) {
325
+                case "daily" -> current = current.plusDays(1);
326
+                case "weekly" -> current = current.plusWeeks(1);
327
+                case "monthly" -> current = current.plusMonths(1);
328
+                default -> current = current.plusDays(1);
329
+            }
330
+        }
331
+        return dates;
332
+    }
333
+
334
+    // ==================== 统计 ====================
335
+
336
+    /**
337
+     * 任务统计
338
+     */
339
+    public Map<String, Object> getTaskStats(LocalDate startDate, LocalDate endDate) {
340
+        Map<String, Object> stats = new LinkedHashMap<>();
341
+
342
+        // 按状态统计
343
+        List<Map<String, Object>> statusCounts = taskMapper.countByStatusInRange(startDate, endDate);
344
+        stats.put("byStatus", statusCounts);
345
+
346
+        // 人员工作量
347
+        List<Map<String, Object>> workload = taskMapper.workloadByWorker(startDate, endDate);
348
+        stats.put("workload", workload);
349
+
350
+        // 总计
351
+        int total = statusCounts.stream().mapToInt(m -> ((Number) m.get("count")).intValue()).sum();
352
+        int completed = statusCounts.stream()
353
+                .filter(m -> "completed".equals(m.get("status")))
354
+                .mapToInt(m -> ((Number) m.get("count")).intValue())
355
+                .sum();
356
+        stats.put("total", total);
357
+        stats.put("completed", completed);
358
+        stats.put("completionRate", total > 0 ? Math.round(completed * 100.0 / total) : 0);
359
+
360
+        return stats;
361
+    }
362
+}

+ 176
- 0
wm-patrol/src/main/java/com/water/patrol/service/TrackService.java 파일 보기

@@ -0,0 +1,176 @@
1
+package com.water.patrol.service;
2
+
3
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
4
+import com.water.patrol.entity.GpsTrackPoint;
5
+import com.water.patrol.entity.dto.GpsTrackBatchRequest;
6
+import com.water.patrol.mapper.GpsTrackPointMapper;
7
+import lombok.RequiredArgsConstructor;
8
+import lombok.extern.slf4j.Slf4j;
9
+import org.springframework.stereotype.Service;
10
+import org.springframework.transaction.annotation.Transactional;
11
+
12
+import java.time.LocalDateTime;
13
+import java.time.format.DateTimeFormatter;
14
+import java.util.*;
15
+import java.util.stream.Collectors;
16
+
17
+@Slf4j
18
+@Service
19
+@RequiredArgsConstructor
20
+public class TrackService {
21
+
22
+    private final GpsTrackPointMapper trackPointMapper;
23
+
24
+    /**
25
+     * 记录单个GPS轨迹点
26
+     */
27
+    public GpsTrackPoint recordTrackPoint(Long executionId, Double lng, Double lat,
28
+                                           Double altitude, Double speed,
29
+                                           Double bearing, Double accuracy) {
30
+        GpsTrackPoint point = new GpsTrackPoint();
31
+        point.setExecutionId(executionId);
32
+        point.setLng(lng);
33
+        point.setLat(lat);
34
+        point.setAltitude(altitude);
35
+        point.setSpeed(speed);
36
+        point.setBearing(bearing);
37
+        point.setAccuracy(accuracy);
38
+        point.setRecordTime(LocalDateTime.now());
39
+        trackPointMapper.insert(point);
40
+        return point;
41
+    }
42
+
43
+    /**
44
+     * 批量记录GPS轨迹点
45
+     */
46
+    @Transactional
47
+    public int batchRecordTrackPoints(Long executionId, GpsTrackBatchRequest request) {
48
+        if (request.getPoints() == null || request.getPoints().isEmpty()) {
49
+            return 0;
50
+        }
51
+
52
+        DateTimeFormatter formatter = DateTimeFormatter.ISO_DATE_TIME;
53
+        int count = 0;
54
+        for (GpsTrackBatchRequest.GpsTrackPointDto dto : request.getPoints()) {
55
+            GpsTrackPoint point = new GpsTrackPoint();
56
+            point.setExecutionId(executionId);
57
+            point.setLng(dto.getLng());
58
+            point.setLat(dto.getLat());
59
+            point.setAltitude(dto.getAltitude());
60
+            point.setSpeed(dto.getSpeed());
61
+            point.setBearing(dto.getBearing());
62
+            point.setAccuracy(dto.getAccuracy());
63
+
64
+            if (dto.getRecordTime() != null && !dto.getRecordTime().isEmpty()) {
65
+                point.setRecordTime(LocalDateTime.parse(dto.getRecordTime(), formatter));
66
+            } else {
67
+                point.setRecordTime(LocalDateTime.now());
68
+            }
69
+
70
+            trackPointMapper.insert(point);
71
+            count++;
72
+        }
73
+
74
+        log.info("Batch recorded {} GPS track points for execution {}", count, executionId);
75
+        return count;
76
+    }
77
+
78
+    /**
79
+     * 获取完整轨迹(用于回放)
80
+     */
81
+    public List<GpsTrackPoint> getTrackPoints(Long executionId) {
82
+        return trackPointMapper.selectTrackByExecutionId(executionId);
83
+    }
84
+
85
+    /**
86
+     * 获取轨迹摘要数据(用于地图展示,减少数据量)
87
+     */
88
+    public Map<String, Object> getTrackSummary(Long executionId) {
89
+        List<GpsTrackPoint> points = getTrackPoints(executionId);
90
+
91
+        Map<String, Object> summary = new LinkedHashMap<>();
92
+        summary.put("executionId", executionId);
93
+        summary.put("totalPoints", points.size());
94
+
95
+        if (points.isEmpty()) {
96
+            summary.put("distance", 0.0);
97
+            summary.put("duration", 0);
98
+            summary.put("avgSpeed", 0.0);
99
+            summary.put("points", List.of());
100
+            return summary;
101
+        }
102
+
103
+        // Calculate total distance using Haversine formula
104
+        double totalDistance = 0.0;
105
+        for (int i = 1; i < points.size(); i++) {
106
+            totalDistance += haversineDistance(
107
+                    points.get(i - 1).getLat(), points.get(i - 1).getLng(),
108
+                    points.get(i).getLat(), points.get(i).getLng());
109
+        }
110
+
111
+        // Duration in minutes
112
+        LocalDateTime start = points.get(0).getRecordTime();
113
+        LocalDateTime end = points.get(points.size() - 1).getRecordTime();
114
+        long durationMinutes = java.time.Duration.between(start, end).toMinutes();
115
+
116
+        // Average speed
117
+        double avgSpeed = points.stream()
118
+                .filter(p -> p.getSpeed() != null)
119
+                .mapToDouble(GpsTrackPoint::getSpeed)
120
+                .average()
121
+                .orElse(0.0);
122
+
123
+        // Simplify points for map display (keep every Nth point)
124
+        int step = Math.max(1, points.size() / 200);
125
+        List<Map<String, Object>> simplifiedPoints = new ArrayList<>();
126
+        for (int i = 0; i < points.size(); i += step) {
127
+            GpsTrackPoint p = points.get(i);
128
+            Map<String, Object> sp = new LinkedHashMap<>();
129
+            sp.put("lng", p.getLng());
130
+            sp.put("lat", p.getLat());
131
+            sp.put("t", p.getRecordTime() != null ? p.getRecordTime().toString() : null);
132
+            simplifiedPoints.add(sp);
133
+        }
134
+        // Always include last point
135
+        if (points.size() % step != 1) {
136
+            GpsTrackPoint last = points.get(points.size() - 1);
137
+            Map<String, Object> sp = new LinkedHashMap<>();
138
+            sp.put("lng", last.getLng());
139
+            sp.put("lat", last.getLat());
140
+            sp.put("t", last.getRecordTime() != null ? last.getRecordTime().toString() : null);
141
+            simplifiedPoints.add(sp);
142
+        }
143
+
144
+        summary.put("distance", Math.round(totalDistance * 1000.0) / 1000.0); // km
145
+        summary.put("duration", durationMinutes);
146
+        summary.put("avgSpeed", Math.round(avgSpeed * 100.0) / 100.0);
147
+        summary.put("points", simplifiedPoints);
148
+
149
+        return summary;
150
+    }
151
+
152
+    /**
153
+     * 获取最新位置
154
+     */
155
+    public GpsTrackPoint getLatestPosition(Long executionId) {
156
+        LambdaQueryWrapper<GpsTrackPoint> wrapper = new LambdaQueryWrapper<>();
157
+        wrapper.eq(GpsTrackPoint::getExecutionId, executionId)
158
+               .orderByDesc(GpsTrackPoint::getRecordTime)
159
+               .last("LIMIT 1");
160
+        return trackPointMapper.selectOne(wrapper);
161
+    }
162
+
163
+    /**
164
+     * Haversine公式计算两点距离(km)
165
+     */
166
+    private double haversineDistance(double lat1, double lng1, double lat2, double lng2) {
167
+        final double R = 6371.0; // Earth radius in km
168
+        double dLat = Math.toRadians(lat2 - lat1);
169
+        double dLng = Math.toRadians(lng2 - lng1);
170
+        double a = Math.sin(dLat / 2) * Math.sin(dLat / 2)
171
+                + Math.cos(Math.toRadians(lat1)) * Math.cos(Math.toRadians(lat2))
172
+                * Math.sin(dLng / 2) * Math.sin(dLng / 2);
173
+        double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
174
+        return R * c;
175
+    }
176
+}

+ 178
- 0
wm-patrol/src/main/java/com/water/patrol/service/WorkerService.java 파일 보기

@@ -0,0 +1,178 @@
1
+package com.water.patrol.service;
2
+
3
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
4
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
5
+import com.water.patrol.entity.PatrolTask;
6
+import com.water.patrol.entity.PatrolWorker;
7
+import com.water.patrol.mapper.PatrolTaskMapper;
8
+import com.water.patrol.mapper.PatrolWorkerMapper;
9
+import lombok.RequiredArgsConstructor;
10
+import lombok.extern.slf4j.Slf4j;
11
+import org.springframework.stereotype.Service;
12
+import org.springframework.transaction.annotation.Transactional;
13
+
14
+import java.time.LocalDate;
15
+import java.time.LocalDateTime;
16
+import java.util.*;
17
+import java.util.stream.Collectors;
18
+
19
+/**
20
+ * 巡检人员管理服务 - 巡检员信息 + 技能标签 + 工作负荷
21
+ */
22
+@Slf4j
23
+@Service
24
+@RequiredArgsConstructor
25
+public class WorkerService {
26
+
27
+    private final PatrolWorkerMapper workerMapper;
28
+    private final PatrolTaskMapper taskMapper;
29
+
30
+    // ==================== 人员 CRUD ====================
31
+
32
+    /**
33
+     * 分页查询巡检员
34
+     */
35
+    public Page<PatrolWorker> page(int current, int size, String keyword, Integer status) {
36
+        LambdaQueryWrapper<PatrolWorker> wrapper = new LambdaQueryWrapper<>();
37
+        if (keyword != null && !keyword.isEmpty()) {
38
+            wrapper.and(w -> w.like(PatrolWorker::getName, keyword)
39
+                    .or().like(PatrolWorker::getPhone, keyword)
40
+                    .or().like(PatrolWorker::getDept, keyword));
41
+        }
42
+        if (status != null) {
43
+            wrapper.eq(PatrolWorker::getStatus, status);
44
+        }
45
+        wrapper.orderByAsc(PatrolWorker::getName);
46
+        return workerMapper.selectPage(new Page<>(current, size), wrapper);
47
+    }
48
+
49
+    /**
50
+     * 查询巡检员详情
51
+     */
52
+    public PatrolWorker getById(Long id) {
53
+        return workerMapper.selectById(id);
54
+    }
55
+
56
+    /**
57
+     * 创建巡检员
58
+     */
59
+    @Transactional
60
+    public PatrolWorker create(PatrolWorker worker) {
61
+        worker.setCreatedTime(LocalDateTime.now());
62
+        worker.setUpdatedTime(LocalDateTime.now());
63
+        worker.setDeleted(0);
64
+        if (worker.getStatus() == null) worker.setStatus(1);
65
+        workerMapper.insert(worker);
66
+        return worker;
67
+    }
68
+
69
+    /**
70
+     * 更新巡检员
71
+     */
72
+    @Transactional
73
+    public boolean update(PatrolWorker worker) {
74
+        worker.setUpdatedTime(LocalDateTime.now());
75
+        return workerMapper.updateById(worker) > 0;
76
+    }
77
+
78
+    /**
79
+     * 删除巡检员(逻辑删除)
80
+     */
81
+    @Transactional
82
+    public boolean delete(Long id) {
83
+        return workerMapper.deleteById(id) > 0;
84
+    }
85
+
86
+    /**
87
+     * 查询所有在岗巡检员
88
+     */
89
+    public List<PatrolWorker> findActive() {
90
+        return workerMapper.findActive();
91
+    }
92
+
93
+    // ==================== 技能标签 ====================
94
+
95
+    /**
96
+     * 按技能筛选巡检员
97
+     */
98
+    public List<PatrolWorker> findBySkill(String skillTag) {
99
+        LambdaQueryWrapper<PatrolWorker> wrapper = new LambdaQueryWrapper<>();
100
+        wrapper.like(PatrolWorker::getSkillTags, skillTag);
101
+        wrapper.eq(PatrolWorker::getStatus, 1);
102
+        wrapper.eq(PatrolWorker::getDeleted, 0);
103
+        return workerMapper.selectList(wrapper);
104
+    }
105
+
106
+    /**
107
+     * 更新技能标签
108
+     */
109
+    @Transactional
110
+    public boolean updateSkillTags(Long workerId, String skillTags) {
111
+        PatrolWorker worker = new PatrolWorker();
112
+        worker.setId(workerId);
113
+        worker.setSkillTags(skillTags);
114
+        worker.setUpdatedTime(LocalDateTime.now());
115
+        return workerMapper.updateById(worker) > 0;
116
+    }
117
+
118
+    // ==================== 工作负荷 ====================
119
+
120
+    /**
121
+     * 查询巡检员工作负荷(指定日期范围)
122
+     */
123
+    public Map<String, Object> getWorkload(Long workerId, LocalDate startDate, LocalDate endDate) {
124
+        LambdaQueryWrapper<PatrolTask> wrapper = new LambdaQueryWrapper<>();
125
+        wrapper.eq(PatrolTask::getWorkerId, workerId);
126
+        wrapper.ge(PatrolTask::getTaskDate, startDate);
127
+        wrapper.le(PatrolTask::getTaskDate, endDate);
128
+        wrapper.eq(PatrolTask::getDeleted, 0);
129
+        List<PatrolTask> tasks = taskMapper.selectList(wrapper);
130
+
131
+        Map<String, Object> result = new LinkedHashMap<>();
132
+        result.put("workerId", workerId);
133
+        result.put("startDate", startDate.toString());
134
+        result.put("endDate", endDate.toString());
135
+        result.put("totalTasks", tasks.size());
136
+        result.put("completed", tasks.stream().filter(t -> "completed".equals(t.getStatus())).count());
137
+        result.put("inProgress", tasks.stream().filter(t -> "in_progress".equals(t.getStatus())).count());
138
+        result.put("pending", tasks.stream().filter(t -> "pending".equals(t.getStatus()) || "assigned".equals(t.getStatus())).count());
139
+        result.put("totalDistance", tasks.stream()
140
+                .filter(t -> t.getDistance() != null)
141
+                .mapToDouble(PatrolTask::getDistance)
142
+                .sum());
143
+        result.put("tasks", tasks);
144
+        return result;
145
+    }
146
+
147
+    /**
148
+     * 查询所有巡检员负荷概览
149
+     */
150
+    public List<Map<String, Object>> getWorkloadOverview(LocalDate date) {
151
+        List<PatrolWorker> workers = workerMapper.findActive();
152
+        List<Map<String, Object>> overview = new ArrayList<>();
153
+
154
+        for (PatrolWorker worker : workers) {
155
+            List<PatrolTask> dayTasks = taskMapper.findByDateAndWorker(date, worker.getId());
156
+            Map<String, Object> item = new LinkedHashMap<>();
157
+            item.put("workerId", worker.getId());
158
+            item.put("workerName", worker.getName());
159
+            item.put("dept", worker.getDept());
160
+            item.put("taskCount", dayTasks.size());
161
+            item.put("completed", dayTasks.stream().filter(t -> "completed".equals(t.getStatus())).count());
162
+            item.put("totalDistance", dayTasks.stream()
163
+                    .filter(t -> t.getDistance() != null)
164
+                    .mapToDouble(PatrolTask::getDistance)
165
+                    .sum());
166
+            overview.add(item);
167
+        }
168
+
169
+        return overview;
170
+    }
171
+
172
+    /**
173
+     * 按部门统计人数
174
+     */
175
+    public List<Map<String, Object>> countByDept() {
176
+        return workerMapper.countByDept();
177
+    }
178
+}

+ 170
- 0
wm-patrol/src/main/resources/db/V1__patrol_route.sql 파일 보기

@@ -0,0 +1,170 @@
1
+-- ============================================================
2
+-- 巡检路线管理与任务分派 DDL
3
+-- ============================================================
4
+
5
+-- 巡检人员表
6
+CREATE TABLE IF NOT EXISTS patrol_worker (
7
+    id          BIGSERIAL PRIMARY KEY,
8
+    name        VARCHAR(50)  NOT NULL,
9
+    phone       VARCHAR(20),
10
+    dept        VARCHAR(100),
11
+    skill_tags  VARCHAR(500),           -- JSON数组: ["管道","阀门","水表"]
12
+    status      INT DEFAULT 1,          -- 1=在岗 0=休假 -1=离职
13
+    created_time TIMESTAMP DEFAULT NOW(),
14
+    updated_time TIMESTAMP DEFAULT NOW(),
15
+    deleted     INT DEFAULT 0
16
+);
17
+
18
+-- 巡检路线表
19
+CREATE TABLE IF NOT EXISTS patrol_route (
20
+    id             BIGSERIAL PRIMARY KEY,
21
+    route_name     VARCHAR(200) NOT NULL,
22
+    route_code     VARCHAR(50),
23
+    area           VARCHAR(100),
24
+    description    TEXT,
25
+    total_distance DOUBLE PRECISION DEFAULT 0,  -- 公里
26
+    estim_duration INT DEFAULT 60,              -- 分钟
27
+    status         INT DEFAULT 1,               -- 1=启用 0=停用
28
+    created_by     BIGINT,
29
+    updated_by     BIGINT,
30
+    created_time   TIMESTAMP DEFAULT NOW(),
31
+    updated_time   TIMESTAMP DEFAULT NOW(),
32
+    deleted        INT DEFAULT 0
33
+);
34
+
35
+-- 巡检检查点表
36
+CREATE TABLE IF NOT EXISTS patrol_checkpoint (
37
+    id            BIGSERIAL PRIMARY KEY,
38
+    route_id      BIGINT NOT NULL REFERENCES patrol_route(id),
39
+    seq           INT NOT NULL,                  -- 检查点顺序
40
+    name          VARCHAR(200) NOT NULL,
41
+    check_type    VARCHAR(50),                   -- 检查类型: visual/device/read
42
+    device_id     BIGINT,                        -- 关联设备ID(可选)
43
+    lng           DOUBLE PRECISION,
44
+    lat           DOUBLE PRECISION,
45
+    check_items   TEXT,                          -- JSON数组: 检查项列表
46
+    required      INT DEFAULT 1,                 -- 1=必检 0=选检
47
+    created_time  TIMESTAMP DEFAULT NOW(),
48
+    updated_time  TIMESTAMP DEFAULT NOW(),
49
+    deleted       INT DEFAULT 0
50
+);
51
+CREATE INDEX IF NOT EXISTS idx_checkpoint_route ON patrol_checkpoint(route_id);
52
+
53
+-- 巡检任务表
54
+CREATE TABLE IF NOT EXISTS patrol_task (
55
+    id              BIGSERIAL PRIMARY KEY,
56
+    task_name       VARCHAR(200) NOT NULL,
57
+    route_id        BIGINT REFERENCES patrol_route(id),
58
+    worker_id       BIGINT REFERENCES patrol_worker(id),
59
+    task_date       DATE NOT NULL,
60
+    cycle_type      VARCHAR(20),                -- daily/weekly/monthly/once
61
+    plan_start      TIMESTAMP,
62
+    plan_end        TIMESTAMP,
63
+    actual_start    TIMESTAMP,
64
+    actual_end      TIMESTAMP,
65
+    status          VARCHAR(20) DEFAULT 'pending', -- pending/assigned/in_progress/completed/cancelled
66
+    priority        INT DEFAULT 0,
67
+    distance        DOUBLE PRECISION DEFAULT 0,
68
+    checkpoint_done INT DEFAULT 0,              -- 已完成检查点数
69
+    checkpoint_total INT DEFAULT 0,             -- 总检查点数
70
+    remark          TEXT,
71
+    created_by      BIGINT,
72
+    created_time    TIMESTAMP DEFAULT NOW(),
73
+    updated_time    TIMESTAMP DEFAULT NOW(),
74
+    deleted         INT DEFAULT 0
75
+);
76
+CREATE INDEX IF NOT EXISTS idx_task_date ON patrol_task(task_date);
77
+CREATE INDEX IF NOT EXISTS idx_task_worker ON patrol_task(worker_id);
78
+CREATE INDEX IF NOT EXISTS idx_task_route ON patrol_task(route_id);
79
+CREATE INDEX IF NOT EXISTS idx_task_status ON patrol_task(status);
80
+
81
+-- 巡检执行记录表
82
+CREATE TABLE IF NOT EXISTS patrol_execution (
83
+    id                  BIGSERIAL PRIMARY KEY,
84
+    task_id             BIGINT REFERENCES patrol_task(id),
85
+    inspector_id        BIGINT,
86
+    inspector_name      VARCHAR(50),
87
+    status              VARCHAR(20) DEFAULT 'pending',  -- pending/in_progress/completed/aborted
88
+    start_time          TIMESTAMP,
89
+    end_time            TIMESTAMP,
90
+    total_check_items   INT DEFAULT 0,
91
+    completed_check_items INT DEFAULT 0,
92
+    issue_count         INT DEFAULT 0,
93
+    total_distance      NUMERIC(10,3) DEFAULT 0,
94
+    start_lng           DOUBLE PRECISION,
95
+    start_lat           DOUBLE PRECISION,
96
+    end_lng             DOUBLE PRECISION,
97
+    end_lat             DOUBLE PRECISION,
98
+    remark              TEXT,
99
+    created_at          TIMESTAMP DEFAULT NOW(),
100
+    updated_at          TIMESTAMP DEFAULT NOW(),
101
+    deleted             INT DEFAULT 0
102
+);
103
+CREATE INDEX IF NOT EXISTS idx_exec_task ON patrol_execution(task_id);
104
+CREATE INDEX IF NOT EXISTS idx_exec_inspector ON patrol_execution(inspector_id);
105
+CREATE INDEX IF NOT EXISTS idx_exec_status ON patrol_execution(status);
106
+
107
+-- 检查项记录表
108
+CREATE TABLE IF NOT EXISTS check_item_record (
109
+    id              BIGSERIAL PRIMARY KEY,
110
+    execution_id    BIGINT NOT NULL REFERENCES patrol_execution(id),
111
+    point_seq       INT,
112
+    point_name      VARCHAR(200),
113
+    item_name       VARCHAR(200),
114
+    check_status    VARCHAR(20),          -- normal/abnormal/skipped
115
+    record_value    DOUBLE PRECISION,
116
+    value_unit      VARCHAR(20),
117
+    description     TEXT,
118
+    photo_urls      TEXT,                 -- JSON数组
119
+    remark          TEXT,
120
+    record_time     TIMESTAMP,
121
+    lng             DOUBLE PRECISION,
122
+    lat             DOUBLE PRECISION,
123
+    created_at      TIMESTAMP DEFAULT NOW(),
124
+    updated_at      TIMESTAMP DEFAULT NOW(),
125
+    deleted         INT DEFAULT 0
126
+);
127
+CREATE INDEX IF NOT EXISTS idx_check_record_exec ON check_item_record(execution_id);
128
+
129
+-- GPS轨迹点表
130
+CREATE TABLE IF NOT EXISTS gps_track_point (
131
+    id              BIGSERIAL PRIMARY KEY,
132
+    execution_id    BIGINT NOT NULL REFERENCES patrol_execution(id),
133
+    lng             DOUBLE PRECISION NOT NULL,
134
+    lat             DOUBLE PRECISION NOT NULL,
135
+    altitude        DOUBLE PRECISION,
136
+    speed           DOUBLE PRECISION,
137
+    bearing         DOUBLE PRECISION,
138
+    accuracy        DOUBLE PRECISION,
139
+    record_time     TIMESTAMP DEFAULT NOW(),
140
+    created_at      TIMESTAMP DEFAULT NOW(),
141
+    updated_at      TIMESTAMP DEFAULT NOW(),
142
+    deleted         INT DEFAULT 0
143
+);
144
+CREATE INDEX IF NOT EXISTS idx_track_exec ON gps_track_point(execution_id);
145
+CREATE INDEX IF NOT EXISTS idx_track_time ON gps_track_point(record_time);
146
+
147
+-- 巡检问题上报表
148
+CREATE TABLE IF NOT EXISTS patrol_issue (
149
+    id              BIGSERIAL PRIMARY KEY,
150
+    execution_id    BIGINT NOT NULL REFERENCES patrol_execution(id),
151
+    check_record_id BIGINT,
152
+    issue_type      VARCHAR(30),          -- leak/damage/pollution/illegal/other
153
+    severity        VARCHAR(20),          -- low/medium/high/critical
154
+    description     TEXT,
155
+    photo_urls      TEXT,                 -- JSON数组
156
+    lng             DOUBLE PRECISION,
157
+    lat             DOUBLE PRECISION,
158
+    address         VARCHAR(500),
159
+    handle_status   VARCHAR(20) DEFAULT 'pending',  -- pending/processing/resolved/closed
160
+    work_order_id   BIGINT,
161
+    reporter_id     BIGINT,
162
+    reporter_name   VARCHAR(50),
163
+    report_time     TIMESTAMP DEFAULT NOW(),
164
+    created_at      TIMESTAMP DEFAULT NOW(),
165
+    updated_at      TIMESTAMP DEFAULT NOW(),
166
+    deleted         INT DEFAULT 0
167
+);
168
+CREATE INDEX IF NOT EXISTS idx_issue_exec ON patrol_issue(execution_id);
169
+CREATE INDEX IF NOT EXISTS idx_issue_status ON patrol_issue(handle_status);
170
+CREATE INDEX IF NOT EXISTS idx_issue_type ON patrol_issue(issue_type);

+ 211
- 0
wm-patrol/src/test/java/com/water/patrol/service/ExecutionServiceTest.java 파일 보기

@@ -0,0 +1,211 @@
1
+package com.water.patrol.service;
2
+
3
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
4
+import com.water.patrol.entity.CheckItemRecord;
5
+import com.water.patrol.entity.PatrolExecution;
6
+import com.water.patrol.entity.dto.CheckItemRecordRequest;
7
+import com.water.patrol.entity.dto.CompleteExecutionRequest;
8
+import com.water.patrol.entity.dto.StartExecutionRequest;
9
+import com.water.patrol.mapper.CheckItemRecordMapper;
10
+import com.water.patrol.mapper.PatrolExecutionMapper;
11
+import org.junit.jupiter.api.BeforeEach;
12
+import org.junit.jupiter.api.DisplayName;
13
+import org.junit.jupiter.api.Test;
14
+import org.junit.jupiter.api.extension.ExtendWith;
15
+import org.mockito.ArgumentCaptor;
16
+import org.mockito.InjectMocks;
17
+import org.mockito.Mock;
18
+import org.mockito.junit.jupiter.MockitoExtension;
19
+
20
+import java.math.BigDecimal;
21
+import java.time.LocalDateTime;
22
+import java.util.Arrays;
23
+import java.util.List;
24
+
25
+import static org.junit.jupiter.api.Assertions.*;
26
+import static org.mockito.ArgumentMatchers.any;
27
+import static org.mockito.Mockito.*;
28
+
29
+@ExtendWith(MockitoExtension.class)
30
+class ExecutionServiceTest {
31
+
32
+    @Mock
33
+    private PatrolExecutionMapper executionMapper;
34
+
35
+    @Mock
36
+    private CheckItemRecordMapper checkItemRecordMapper;
37
+
38
+    @InjectMocks
39
+    private ExecutionService executionService;
40
+
41
+    private StartExecutionRequest startRequest;
42
+
43
+    @BeforeEach
44
+    void setUp() {
45
+        startRequest = new StartExecutionRequest();
46
+        startRequest.setTaskId(1L);
47
+        startRequest.setInspectorId(100L);
48
+        startRequest.setInspectorName("张三");
49
+        startRequest.setStartLng(86.6134);
50
+        startRequest.setStartLat(44.8256);
51
+        startRequest.setTotalCheckItems(10);
52
+    }
53
+
54
+    @Test
55
+    @DisplayName("开始巡检执行 - 正常创建")
56
+    void startExecution_success() {
57
+        when(executionMapper.selectOne(any(LambdaQueryWrapper.class))).thenReturn(null);
58
+        when(executionMapper.insert(any(PatrolExecution.class))).thenReturn(1);
59
+
60
+        PatrolExecution result = executionService.startExecution(startRequest);
61
+
62
+        assertNotNull(result);
63
+        assertEquals(1L, result.getTaskId());
64
+        assertEquals("张三", result.getInspectorName());
65
+        assertEquals("in_progress", result.getStatus());
66
+        assertEquals(10, result.getTotalCheckItems());
67
+        assertEquals(0, result.getCompletedCheckItems());
68
+        assertEquals(0, result.getIssueCount());
69
+        assertNotNull(result.getStartTime());
70
+
71
+        ArgumentCaptor<PatrolExecution> captor = ArgumentCaptor.forClass(PatrolExecution.class);
72
+        verify(executionMapper).insert(captor.capture());
73
+        assertEquals("in_progress", captor.getValue().getStatus());
74
+    }
75
+
76
+    @Test
77
+    @DisplayName("开始巡检执行 - 已有进行中的执行应抛异常")
78
+    void startExecution_duplicateExecution_throwsException() {
79
+        PatrolExecution existing = new PatrolExecution();
80
+        existing.setId(99L);
81
+        existing.setTaskId(1L);
82
+        existing.setStatus("in_progress");
83
+
84
+        when(executionMapper.selectOne(any(LambdaQueryWrapper.class))).thenReturn(existing);
85
+
86
+        RuntimeException ex = assertThrows(RuntimeException.class,
87
+                () -> executionService.startExecution(startRequest));
88
+        assertTrue(ex.getMessage().contains("已有进行中的巡检执行"));
89
+    }
90
+
91
+    @Test
92
+    @DisplayName("提交检查项记录 - 正常记录并更新计数")
93
+    void submitCheckItem_success() {
94
+        PatrolExecution execution = buildInProgressExecution();
95
+        when(executionMapper.selectById(1L)).thenReturn(execution);
96
+        when(checkItemRecordMapper.insert(any(CheckItemRecord.class))).thenReturn(1);
97
+        when(executionMapper.updateById(any(PatrolExecution.class))).thenReturn(1);
98
+
99
+        CheckItemRecordRequest req = new CheckItemRecordRequest();
100
+        req.setPointSeq(1);
101
+        req.setPointName("1号阀门");
102
+        req.setItemName("阀门状态");
103
+        req.setCheckStatus("normal");
104
+        req.setDescription("运行正常");
105
+        req.setLng(86.6135);
106
+        req.setLat(44.8257);
107
+        req.setPhotoUrls(Arrays.asList("http://img.example.com/1.jpg", "http://img.example.com/2.jpg"));
108
+
109
+        CheckItemRecord result = executionService.submitCheckItem(1L, req);
110
+
111
+        assertNotNull(result);
112
+        assertEquals("normal", result.getCheckStatus());
113
+        assertEquals(1, result.getPointSeq());
114
+        assertNotNull(result.getRecordTime());
115
+        assertEquals(1, execution.getCompletedCheckItems());
116
+
117
+        verify(checkItemRecordMapper).insert(any(CheckItemRecord.class));
118
+        verify(executionMapper).updateById(any(PatrolExecution.class));
119
+    }
120
+
121
+    @Test
122
+    @DisplayName("提交检查项记录 - 执行非进行中应抛异常")
123
+    void submitCheckItem_notInProgress_throwsException() {
124
+        PatrolExecution execution = buildInProgressExecution();
125
+        execution.setStatus("completed");
126
+        when(executionMapper.selectById(1L)).thenReturn(execution);
127
+
128
+        CheckItemRecordRequest req = new CheckItemRecordRequest();
129
+        req.setCheckStatus("normal");
130
+
131
+        RuntimeException ex = assertThrows(RuntimeException.class,
132
+                () -> executionService.submitCheckItem(1L, req));
133
+        assertTrue(ex.getMessage().contains("不在进行中状态"));
134
+    }
135
+
136
+    @Test
137
+    @DisplayName("完成巡检执行 - 正常完成")
138
+    void completeExecution_success() {
139
+        PatrolExecution execution = buildInProgressExecution();
140
+        when(executionMapper.selectById(1L)).thenReturn(execution);
141
+        when(executionMapper.updateById(any(PatrolExecution.class))).thenReturn(1);
142
+
143
+        CompleteExecutionRequest req = new CompleteExecutionRequest();
144
+        req.setEndLng(86.6200);
145
+        req.setEndLat(44.8300);
146
+        req.setTotalDistance(3.5);
147
+        req.setRemark("巡检顺利完成");
148
+
149
+        PatrolExecution result = executionService.completeExecution(1L, req);
150
+
151
+        assertEquals("completed", result.getStatus());
152
+        assertNotNull(result.getEndTime());
153
+        assertEquals(86.6200, result.getEndLng());
154
+        assertEquals(44.8300, result.getEndLat());
155
+        assertEquals(BigDecimal.valueOf(3.5), result.getTotalDistance());
156
+    }
157
+
158
+    @Test
159
+    @DisplayName("中止巡检执行 - 正常中止")
160
+    void abortExecution_success() {
161
+        PatrolExecution execution = buildInProgressExecution();
162
+        when(executionMapper.selectById(1L)).thenReturn(execution);
163
+        when(executionMapper.updateById(any(PatrolExecution.class))).thenReturn(1);
164
+
165
+        PatrolExecution result = executionService.abortExecution(1L, "天气原因中止");
166
+
167
+        assertEquals("aborted", result.getStatus());
168
+        assertNotNull(result.getEndTime());
169
+        assertEquals("天气原因中止", result.getRemark());
170
+    }
171
+
172
+    @Test
173
+    @DisplayName("批量提交检查项 - 正确更新计数")
174
+    void batchSubmitCheckItems_success() {
175
+        PatrolExecution execution = buildInProgressExecution();
176
+        when(executionMapper.selectById(1L)).thenReturn(execution);
177
+        when(checkItemRecordMapper.insert(any(CheckItemRecord.class))).thenReturn(1);
178
+        when(executionMapper.updateById(any(PatrolExecution.class))).thenReturn(1);
179
+
180
+        CheckItemRecordRequest item1 = new CheckItemRecordRequest();
181
+        item1.setPointSeq(1);
182
+        item1.setItemName("阀门1");
183
+        item1.setCheckStatus("normal");
184
+
185
+        CheckItemRecordRequest item2 = new CheckItemRecordRequest();
186
+        item2.setPointSeq(2);
187
+        item2.setItemName("阀门2");
188
+        item2.setCheckStatus("abnormal");
189
+
190
+        int count = executionService.batchSubmitCheckItems(1L, List.of(item1, item2));
191
+
192
+        assertEquals(2, count);
193
+        assertEquals(2, execution.getCompletedCheckItems());
194
+        verify(checkItemRecordMapper, times(2)).insert(any(CheckItemRecord.class));
195
+    }
196
+
197
+    private PatrolExecution buildInProgressExecution() {
198
+        PatrolExecution execution = new PatrolExecution();
199
+        execution.setId(1L);
200
+        execution.setTaskId(1L);
201
+        execution.setInspectorId(100L);
202
+        execution.setInspectorName("张三");
203
+        execution.setStatus("in_progress");
204
+        execution.setStartTime(LocalDateTime.now());
205
+        execution.setTotalCheckItems(10);
206
+        execution.setCompletedCheckItems(0);
207
+        execution.setIssueCount(0);
208
+        execution.setTotalDistance(BigDecimal.ZERO);
209
+        return execution;
210
+    }
211
+}

+ 138
- 0
wm-patrol/src/test/java/com/water/patrol/service/IssueServiceTest.java 파일 보기

@@ -0,0 +1,138 @@
1
+package com.water.patrol.service;
2
+
3
+import com.water.patrol.entity.PatrolExecution;
4
+import com.water.patrol.entity.PatrolIssue;
5
+import com.water.patrol.entity.dto.PatrolIssueRequest;
6
+import com.water.patrol.mapper.PatrolIssueMapper;
7
+import org.junit.jupiter.api.DisplayName;
8
+import org.junit.jupiter.api.Test;
9
+import org.junit.jupiter.api.extension.ExtendWith;
10
+import org.mockito.ArgumentCaptor;
11
+import org.mockito.InjectMocks;
12
+import org.mockito.Mock;
13
+import org.mockito.junit.jupiter.MockitoExtension;
14
+
15
+import java.math.BigDecimal;
16
+import java.time.LocalDateTime;
17
+import java.util.Arrays;
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 IssueServiceTest {
25
+
26
+    @Mock
27
+    private PatrolIssueMapper issueMapper;
28
+
29
+    @Mock
30
+    private ExecutionService executionService;
31
+
32
+    @InjectMocks
33
+    private IssueService issueService;
34
+
35
+    @Test
36
+    @DisplayName("上报问题 - 正常上报并关联执行")
37
+    void reportIssue_success() {
38
+        PatrolExecution execution = buildInProgressExecution();
39
+        when(executionService.getExecution(1L)).thenReturn(execution);
40
+        when(issueMapper.insert(any(PatrolIssue.class))).thenReturn(1);
41
+
42
+        PatrolIssueRequest request = new PatrolIssueRequest();
43
+        request.setIssueType("leak");
44
+        request.setSeverity("high");
45
+        request.setDescription("发现管道漏水,水流较大");
46
+        request.setLng(86.6135);
47
+        request.setLat(44.8257);
48
+        request.setAddress("XX路与YY路交叉口");
49
+        request.setPhotoUrls(Arrays.asList("http://img.example.com/leak1.jpg", "http://img.example.com/leak2.jpg"));
50
+
51
+        PatrolIssue result = issueService.reportIssue(1L, request);
52
+
53
+        assertNotNull(result);
54
+        assertEquals("leak", result.getIssueType());
55
+        assertEquals("high", result.getSeverity());
56
+        assertEquals("pending", result.getHandleStatus());
57
+        assertEquals(100L, result.getReporterId());
58
+        assertEquals("张三", result.getReporterName());
59
+        assertNotNull(result.getReportTime());
60
+
61
+        // Verify issue count was incremented
62
+        verify(executionService).incrementIssueCount(1L);
63
+    }
64
+
65
+    @Test
66
+    @DisplayName("上报问题 - 执行非进行中应抛异常")
67
+    void reportIssue_notInProgress_throwsException() {
68
+        PatrolExecution execution = buildInProgressExecution();
69
+        execution.setStatus("completed");
70
+        when(executionService.getExecution(1L)).thenReturn(execution);
71
+
72
+        PatrolIssueRequest request = new PatrolIssueRequest();
73
+        request.setIssueType("leak");
74
+        request.setSeverity("high");
75
+        request.setDescription("test");
76
+
77
+        RuntimeException ex = assertThrows(RuntimeException.class,
78
+                () -> issueService.reportIssue(1L, request));
79
+        assertTrue(ex.getMessage().contains("不在进行中状态"));
80
+    }
81
+
82
+    @Test
83
+    @DisplayName("关联工单 - 状态更新为处理中")
84
+    void linkWorkOrder_success() {
85
+        PatrolIssue issue = new PatrolIssue();
86
+        issue.setId(1L);
87
+        issue.setExecutionId(1L);
88
+        issue.setHandleStatus("pending");
89
+
90
+        when(issueMapper.selectById(1L)).thenReturn(issue);
91
+        when(issueMapper.updateById(any(PatrolIssue.class))).thenReturn(1);
92
+
93
+        PatrolIssue result = issueService.linkWorkOrder(1L, 500L);
94
+
95
+        assertEquals(500L, result.getWorkOrderId());
96
+        assertEquals("processing", result.getHandleStatus());
97
+    }
98
+
99
+    @Test
100
+    @DisplayName("更新问题处理状态")
101
+    void updateIssueStatus_success() {
102
+        PatrolIssue issue = new PatrolIssue();
103
+        issue.setId(1L);
104
+        issue.setHandleStatus("processing");
105
+
106
+        when(issueMapper.selectById(1L)).thenReturn(issue);
107
+        when(issueMapper.updateById(any(PatrolIssue.class))).thenReturn(1);
108
+
109
+        PatrolIssue result = issueService.updateIssueStatus(1L, "resolved");
110
+
111
+        assertEquals("resolved", result.getHandleStatus());
112
+    }
113
+
114
+    @Test
115
+    @DisplayName("获取问题详情 - 不存在应抛异常")
116
+    void getIssue_notFound_throwsException() {
117
+        when(issueMapper.selectById(999L)).thenReturn(null);
118
+
119
+        RuntimeException ex = assertThrows(RuntimeException.class,
120
+                () -> issueService.getIssue(999L));
121
+        assertTrue(ex.getMessage().contains("问题不存在"));
122
+    }
123
+
124
+    private PatrolExecution buildInProgressExecution() {
125
+        PatrolExecution execution = new PatrolExecution();
126
+        execution.setId(1L);
127
+        execution.setTaskId(1L);
128
+        execution.setInspectorId(100L);
129
+        execution.setInspectorName("张三");
130
+        execution.setStatus("in_progress");
131
+        execution.setStartTime(LocalDateTime.now());
132
+        execution.setTotalCheckItems(10);
133
+        execution.setCompletedCheckItems(0);
134
+        execution.setIssueCount(0);
135
+        execution.setTotalDistance(BigDecimal.ZERO);
136
+        return execution;
137
+    }
138
+}

+ 201
- 0
wm-patrol/src/test/java/com/water/patrol/service/PatrolServiceTest.java 파일 보기

@@ -0,0 +1,201 @@
1
+package com.water.patrol.service;
2
+
3
+import com.water.patrol.entity.PatrolCheckpoint;
4
+import com.water.patrol.entity.PatrolTask;
5
+import org.junit.jupiter.api.BeforeEach;
6
+import org.junit.jupiter.api.DisplayName;
7
+import org.junit.jupiter.api.Test;
8
+
9
+import java.time.LocalDate;
10
+import java.util.List;
11
+
12
+import static org.junit.jupiter.api.Assertions.*;
13
+
14
+/**
15
+ * 巡检模块核心逻辑单元测试
16
+ * 测试路线优化(haversine距离计算)、周期任务日期生成、任务状态流转等纯逻辑
17
+ */
18
+class PatrolServiceTest {
19
+
20
+    private RouteService routeService;
21
+    private TaskService taskService;
22
+
23
+    @BeforeEach
24
+    void setUp() {
25
+        // RouteService需要mapper但haversine是纯方法,传null即可
26
+        routeService = new RouteService(null, null);
27
+        taskService = new TaskService(null, null, null, null);
28
+    }
29
+
30
+    // ==================== Haversine 距离计算测试 ====================
31
+
32
+    @Test
33
+    @DisplayName("测试Haversine距离 - 相同坐标应为0")
34
+    void testHaversine_samePoint() {
35
+        double dist = routeService.haversine(44.85, 82.07, 44.85, 82.07);
36
+        assertEquals(0.0, dist, 0.001);
37
+    }
38
+
39
+    @Test
40
+    @DisplayName("测试Haversine距离 - 已知距离的两个城市(北京-上海约1068km)")
41
+    void testHaversine_beijingToShanghai() {
42
+        // 北京(39.9, 116.4) -> 上海(31.2, 121.5)
43
+        double dist = routeService.haversine(39.9, 116.4, 31.2, 121.5);
44
+        assertTrue(dist > 1000 && dist < 1150,
45
+                "北京到上海应在1000-1150km范围,实际: " + dist);
46
+    }
47
+
48
+    @Test
49
+    @DisplayName("测试Haversine距离 - 短距离(巡检场景,几百米)")
50
+    void testHaversine_shortDistance() {
51
+        // 两个相距约500m的坐标点
52
+        double dist = routeService.haversine(44.850, 82.070, 44.854, 82.073);
53
+        assertTrue(dist > 0.3 && dist < 0.6,
54
+                "短距离应在300m-600m范围,实际: " + dist + "km");
55
+    }
56
+
57
+    @Test
58
+    @DisplayName("测试Haversine对称性 - A到B等于B到A")
59
+    void testHaversine_symmetric() {
60
+        double d1 = routeService.haversine(44.85, 82.07, 44.86, 82.08);
61
+        double d2 = routeService.haversine(44.86, 82.08, 44.85, 82.07);
62
+        assertEquals(d1, d2, 0.0001);
63
+    }
64
+
65
+    // ==================== 周期任务日期生成测试 ====================
66
+
67
+    @Test
68
+    @DisplayName("测试日周期 - 生成7天日期")
69
+    void testGenerateCycleDates_daily() {
70
+        LocalDate start = LocalDate.of(2026, 6, 1);
71
+        List<LocalDate> dates = taskService.generateCycleDates("daily", start, 7);
72
+        assertEquals(7, dates.size());
73
+        assertEquals(start, dates.get(0));
74
+        assertEquals(start.plusDays(6), dates.get(6));
75
+        // 验证连续日期
76
+        for (int i = 1; i < dates.size(); i++) {
77
+            assertEquals(dates.get(i - 1).plusDays(1), dates.get(i));
78
+        }
79
+    }
80
+
81
+    @Test
82
+    @DisplayName("测试周周期 - 生成4周日期")
83
+    void testGenerateCycleDates_weekly() {
84
+        LocalDate start = LocalDate.of(2026, 6, 1);
85
+        List<LocalDate> dates = taskService.generateCycleDates("weekly", start, 4);
86
+        assertEquals(4, dates.size());
87
+        assertEquals(start, dates.get(0));
88
+        assertEquals(start.plusWeeks(3), dates.get(3));
89
+        // 验证每周间隔7天
90
+        for (int i = 1; i < dates.size(); i++) {
91
+            assertEquals(7, dates.get(i).toEpochDay() - dates.get(i - 1).toEpochDay());
92
+        }
93
+    }
94
+
95
+    @Test
96
+    @DisplayName("测试月周期 - 生成3个月日期")
97
+    void testGenerateCycleDates_monthly() {
98
+        LocalDate start = LocalDate.of(2026, 1, 15);
99
+        List<LocalDate> dates = taskService.generateCycleDates("monthly", start, 3);
100
+        assertEquals(3, dates.size());
101
+        assertEquals(start, dates.get(0));
102
+        assertEquals(LocalDate.of(2026, 2, 15), dates.get(1));
103
+        assertEquals(LocalDate.of(2026, 3, 15), dates.get(2));
104
+    }
105
+
106
+    @Test
107
+    @DisplayName("测试月周期 - 跨年末")
108
+    void testGenerateCycleDates_monthlyCrossYear() {
109
+        LocalDate start = LocalDate.of(2026, 11, 1);
110
+        List<LocalDate> dates = taskService.generateCycleDates("monthly", start, 4);
111
+        assertEquals(4, dates.size());
112
+        assertEquals(LocalDate.of(2026, 11, 1), dates.get(0));
113
+        assertEquals(LocalDate.of(2026, 12, 1), dates.get(1));
114
+        assertEquals(LocalDate.of(2027, 1, 1), dates.get(2));
115
+        assertEquals(LocalDate.of(2027, 2, 1), dates.get(3));
116
+    }
117
+
118
+    @Test
119
+    @DisplayName("测试单周期 - 只生成1个日期")
120
+    void testGenerateCycleDates_single() {
121
+        LocalDate start = LocalDate.of(2026, 6, 14);
122
+        List<LocalDate> dates = taskService.generateCycleDates("daily", start, 1);
123
+        assertEquals(1, dates.size());
124
+        assertEquals(start, dates.get(0));
125
+    }
126
+
127
+    // ==================== 巡检任务实体逻辑测试 ====================
128
+
129
+    @Test
130
+    @DisplayName("测试PatrolTask实体 - 默认值设置")
131
+    void testPatrolTask_defaults() {
132
+        PatrolTask task = new PatrolTask();
133
+        task.setTaskName("测试任务");
134
+        task.setTaskDate(LocalDate.of(2026, 6, 14));
135
+        task.setRouteId(1L);
136
+        task.setStatus("pending");
137
+
138
+        assertNotNull(task.getTaskName());
139
+        assertEquals("pending", task.getStatus());
140
+        assertEquals(LocalDate.of(2026, 6, 14), task.getTaskDate());
141
+    }
142
+
143
+    @Test
144
+    @DisplayName("测试PatrolTask状态流转 - pending -> assigned -> in_progress -> completed")
145
+    void testPatrolTask_statusFlow() {
146
+        PatrolTask task = new PatrolTask();
147
+        task.setStatus("pending");
148
+        assertEquals("pending", task.getStatus());
149
+
150
+        task.setStatus("assigned");
151
+        assertEquals("assigned", task.getStatus());
152
+
153
+        task.setStatus("in_progress");
154
+        assertEquals("in_progress", task.getStatus());
155
+
156
+        task.setStatus("completed");
157
+        assertEquals("completed", task.getStatus());
158
+    }
159
+
160
+    // ==================== 检查点实体测试 ====================
161
+
162
+    @Test
163
+    @DisplayName("测试PatrolCheckpoint - 检查点顺序排列")
164
+    void testCheckpoint_ordering() {
165
+        List<PatrolCheckpoint> checkpoints = List.of(
166
+                createCheckpoint(3, "C"),
167
+                createCheckpoint(1, "A"),
168
+                createCheckpoint(2, "B")
169
+        );
170
+
171
+        List<PatrolCheckpoint> sorted = checkpoints.stream()
172
+                .sorted((a, b) -> Integer.compare(a.getSeq(), b.getSeq()))
173
+                .toList();
174
+
175
+        assertEquals("A", sorted.get(0).getName());
176
+        assertEquals("B", sorted.get(1).getName());
177
+        assertEquals("C", sorted.get(2).getName());
178
+    }
179
+
180
+    @Test
181
+    @DisplayName("测试检查点坐标有效性")
182
+    void testCheckpoint_coordinates() {
183
+        PatrolCheckpoint cp = new PatrolCheckpoint();
184
+        cp.setLng(82.07);
185
+        cp.setLat(44.85);
186
+        cp.setName("检查点A");
187
+        cp.setCheckType("visual");
188
+
189
+        assertNotNull(cp.getLng());
190
+        assertNotNull(cp.getLat());
191
+        assertTrue(cp.getLng() > 0 && cp.getLng() < 180);
192
+        assertTrue(cp.getLat() > 0 && cp.getLat() < 90);
193
+    }
194
+
195
+    private PatrolCheckpoint createCheckpoint(int seq, String name) {
196
+        PatrolCheckpoint cp = new PatrolCheckpoint();
197
+        cp.setSeq(seq);
198
+        cp.setName(name);
199
+        return cp;
200
+    }
201
+}

+ 152
- 0
wm-patrol/src/test/java/com/water/patrol/service/TrackServiceTest.java 파일 보기

@@ -0,0 +1,152 @@
1
+package com.water.patrol.service;
2
+
3
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
4
+import com.water.patrol.entity.GpsTrackPoint;
5
+import com.water.patrol.entity.dto.GpsTrackBatchRequest;
6
+import com.water.patrol.mapper.GpsTrackPointMapper;
7
+import org.junit.jupiter.api.DisplayName;
8
+import org.junit.jupiter.api.Test;
9
+import org.junit.jupiter.api.extension.ExtendWith;
10
+import org.mockito.ArgumentCaptor;
11
+import org.mockito.InjectMocks;
12
+import org.mockito.Mock;
13
+import org.mockito.junit.jupiter.MockitoExtension;
14
+
15
+import java.time.LocalDateTime;
16
+import java.util.ArrayList;
17
+import java.util.List;
18
+import java.util.Map;
19
+
20
+import static org.junit.jupiter.api.Assertions.*;
21
+import static org.mockito.ArgumentMatchers.any;
22
+import static org.mockito.Mockito.*;
23
+
24
+@ExtendWith(MockitoExtension.class)
25
+class TrackServiceTest {
26
+
27
+    @Mock
28
+    private GpsTrackPointMapper trackPointMapper;
29
+
30
+    @InjectMocks
31
+    private TrackService trackService;
32
+
33
+    @Test
34
+    @DisplayName("记录单个GPS轨迹点")
35
+    void recordTrackPoint_success() {
36
+        when(trackPointMapper.insert(any(GpsTrackPoint.class))).thenReturn(1);
37
+
38
+        GpsTrackPoint result = trackService.recordTrackPoint(
39
+                1L, 86.6134, 44.8256, 500.0, 1.5, 180.0, 10.0);
40
+
41
+        assertNotNull(result);
42
+        assertEquals(1L, result.getExecutionId());
43
+        assertEquals(86.6134, result.getLng());
44
+        assertEquals(44.8256, result.getLat());
45
+        assertEquals(1.5, result.getSpeed());
46
+        assertNotNull(result.getRecordTime());
47
+
48
+        verify(trackPointMapper).insert(any(GpsTrackPoint.class));
49
+    }
50
+
51
+    @Test
52
+    @DisplayName("批量记录GPS轨迹点")
53
+    void batchRecordTrackPoints_success() {
54
+        when(trackPointMapper.insert(any(GpsTrackPoint.class))).thenReturn(1);
55
+
56
+        GpsTrackBatchRequest request = new GpsTrackBatchRequest();
57
+        List<GpsTrackBatchRequest.GpsTrackPointDto> points = new ArrayList<>();
58
+
59
+        GpsTrackBatchRequest.GpsTrackPointDto p1 = new GpsTrackBatchRequest.GpsTrackPointDto();
60
+        p1.setLng(86.6134);
61
+        p1.setLat(44.8256);
62
+        p1.setSpeed(1.2);
63
+        p1.setRecordTime("2025-06-14T10:00:00");
64
+        points.add(p1);
65
+
66
+        GpsTrackBatchRequest.GpsTrackPointDto p2 = new GpsTrackBatchRequest.GpsTrackPointDto();
67
+        p2.setLng(86.6135);
68
+        p2.setLat(44.8257);
69
+        p2.setSpeed(1.3);
70
+        p2.setRecordTime("2025-06-14T10:00:10");
71
+        points.add(p2);
72
+
73
+        request.setPoints(points);
74
+
75
+        int count = trackService.batchRecordTrackPoints(1L, request);
76
+
77
+        assertEquals(2, count);
78
+        verify(trackPointMapper, times(2)).insert(any(GpsTrackPoint.class));
79
+    }
80
+
81
+    @Test
82
+    @DisplayName("批量记录空列表返回0")
83
+    void batchRecordTrackPoints_emptyList() {
84
+        GpsTrackBatchRequest request = new GpsTrackBatchRequest();
85
+        request.setPoints(List.of());
86
+
87
+        int count = trackService.batchRecordTrackPoints(1L, request);
88
+
89
+        assertEquals(0, count);
90
+        verify(trackPointMapper, never()).insert(any());
91
+    }
92
+
93
+    @Test
94
+    @DisplayName("获取轨迹摘要 - 空轨迹")
95
+    void getTrackSummary_emptyTrack() {
96
+        when(trackPointMapper.selectTrackByExecutionId(1L)).thenReturn(List.of());
97
+
98
+        Map<String, Object> summary = trackService.getTrackSummary(1L);
99
+
100
+        assertEquals(1L, summary.get("executionId"));
101
+        assertEquals(0, summary.get("totalPoints"));
102
+        assertEquals(0.0, summary.get("distance"));
103
+        assertEquals(0, summary.get("duration"));
104
+    }
105
+
106
+    @Test
107
+    @DisplayName("获取轨迹摘要 - 包含距离计算")
108
+    void getTrackSummary_withDistance() {
109
+        List<GpsTrackPoint> points = new ArrayList<>();
110
+
111
+        GpsTrackPoint p1 = new GpsTrackPoint();
112
+        p1.setLng(86.6134);
113
+        p1.setLat(44.8256);
114
+        p1.setSpeed(1.2);
115
+        p1.setRecordTime(LocalDateTime.of(2025, 6, 14, 10, 0, 0));
116
+        points.add(p1);
117
+
118
+        GpsTrackPoint p2 = new GpsTrackPoint();
119
+        p2.setLng(86.6144);
120
+        p2.setLat(44.8266);
121
+        p2.setSpeed(1.5);
122
+        p2.setRecordTime(LocalDateTime.of(2025, 6, 14, 10, 30, 0));
123
+        points.add(p2);
124
+
125
+        when(trackPointMapper.selectTrackByExecutionId(1L)).thenReturn(points);
126
+
127
+        Map<String, Object> summary = trackService.getTrackSummary(1L);
128
+
129
+        assertEquals(2, summary.get("totalPoints"));
130
+        assertTrue((double) summary.get("distance") > 0);
131
+        assertEquals(30L, summary.get("duration"));
132
+        assertTrue((double) summary.get("avgSpeed") > 0);
133
+    }
134
+
135
+    @Test
136
+    @DisplayName("获取最新位置")
137
+    void getLatestPosition_success() {
138
+        GpsTrackPoint latest = new GpsTrackPoint();
139
+        latest.setExecutionId(1L);
140
+        latest.setLng(86.6200);
141
+        latest.setLat(44.8300);
142
+        latest.setRecordTime(LocalDateTime.now());
143
+
144
+        when(trackPointMapper.selectOne(any(LambdaQueryWrapper.class))).thenReturn(latest);
145
+
146
+        GpsTrackPoint result = trackService.getLatestPosition(1L);
147
+
148
+        assertNotNull(result);
149
+        assertEquals(86.6200, result.getLng());
150
+        assertEquals(44.8300, result.getLat());
151
+    }
152
+}