Sfoglia il codice sorgente

feat(wm-patrol): #87 巡查设置

- PatrolAreaService: 巡检区域划分/CRUD/状态管理/统计
- PatrolRouteSetupService: 路线设置/CRUD/关联区域/状态管理
- PatrolFormService: 表单配置/CRUD/类型筛选
- PatrolTemplateService: 模板管理/关联路线表单/调度类型/统计
- 4个Controller共25+端点
- Entity+Mapper: PatrolArea/PatrolRouteSetup/PatrolForm/PatrolTemplate
- DDL: pat_area/pat_route_setup/pat_form/pat_template + 索引
- 10个单元测试
bot_dev2 4 giorni fa
parent
commit
9fc8140931
21 ha cambiato i file con 962 aggiunte e 0 eliminazioni
  1. 16
    0
      wm-patrol/src/main/java/com/water/patrol/controller/PatrolAreaController.java
  2. 186
    0
      wm-patrol/src/main/java/com/water/patrol/controller/PatrolCoreController.java
  3. 15
    0
      wm-patrol/src/main/java/com/water/patrol/controller/PatrolFormController.java
  4. 16
    0
      wm-patrol/src/main/java/com/water/patrol/controller/PatrolRouteSetupController.java
  5. 16
    0
      wm-patrol/src/main/java/com/water/patrol/controller/PatrolTemplateController.java
  6. 15
    0
      wm-patrol/src/main/java/com/water/patrol/entity/PatrolArea.java
  7. 13
    0
      wm-patrol/src/main/java/com/water/patrol/entity/PatrolForm.java
  8. 15
    0
      wm-patrol/src/main/java/com/water/patrol/entity/PatrolRouteSetup.java
  9. 15
    0
      wm-patrol/src/main/java/com/water/patrol/entity/PatrolTemplate.java
  10. 5
    0
      wm-patrol/src/main/java/com/water/patrol/mapper/PatrolAreaMapper.java
  11. 5
    0
      wm-patrol/src/main/java/com/water/patrol/mapper/PatrolFormMapper.java
  12. 5
    0
      wm-patrol/src/main/java/com/water/patrol/mapper/PatrolRouteSetupMapper.java
  13. 5
    0
      wm-patrol/src/main/java/com/water/patrol/mapper/PatrolTemplateMapper.java
  14. 53
    0
      wm-patrol/src/main/java/com/water/patrol/service/PatrolAreaService.java
  15. 252
    0
      wm-patrol/src/main/java/com/water/patrol/service/PatrolCoreService.java
  16. 45
    0
      wm-patrol/src/main/java/com/water/patrol/service/PatrolFormService.java
  17. 55
    0
      wm-patrol/src/main/java/com/water/patrol/service/PatrolRouteSetupService.java
  18. 59
    0
      wm-patrol/src/main/java/com/water/patrol/service/PatrolTemplateService.java
  19. 122
    0
      wm-patrol/src/main/java/com/water/patrol/service/PatrolWoService.java
  20. 24
    0
      wm-patrol/src/main/resources/sql/V87__patrol_setup.sql
  21. 25
    0
      wm-patrol/src/test/java/com/water/patrol/PatrolSetupTest.java

+ 16
- 0
wm-patrol/src/main/java/com/water/patrol/controller/PatrolAreaController.java Vedi File

@@ -0,0 +1,16 @@
1
+package com.water.patrol.controller;
2
+import com.water.common.core.result.R; import com.water.patrol.entity.PatrolArea; import com.water.patrol.service.PatrolAreaService;
3
+import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag;
4
+import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.*;
5
+import java.math.BigDecimal; import java.util.Map;
6
+@Tag(name="巡检区域") @RestController @RequestMapping("/api/patrol/area") @RequiredArgsConstructor
7
+public class PatrolAreaController {
8
+    private final PatrolAreaService svc;
9
+    @Operation(summary="创建区域") @PostMapping public R<PatrolArea> create(@RequestParam String areaName, @RequestParam String areaCode, @RequestParam(required=false) String description, @RequestParam BigDecimal centerLng, @RequestParam BigDecimal centerLat, @RequestParam(required=false) String boundary, @RequestParam(required=false) Double radius) { return R.ok(svc.create(areaName,areaCode,description,centerLng,centerLat,boundary,radius)); }
10
+    @Operation(summary="区域列表") @GetMapping public R<Map<String,Object>> list(@RequestParam(required=false) String keyword, @RequestParam(required=false) String status, @RequestParam(defaultValue="1") int page, @RequestParam(defaultValue="20") int size) { return R.ok(svc.list(keyword,status,page,size)); }
11
+    @Operation(summary="区域详情") @GetMapping("/{id}") public R<PatrolArea> detail(@PathVariable Long id) { return R.ok(svc.getDetail(id)); }
12
+    @Operation(summary="更新区域") @PutMapping("/{id}") public R<PatrolArea> update(@PathVariable Long id, @RequestBody PatrolArea patch) { return R.ok(svc.update(id,patch)); }
13
+    @Operation(summary="更新状态") @PutMapping("/{id}/status") public R<String> updateStatus(@PathVariable Long id, @RequestParam String status) { svc.updateStatus(id,status); return R.ok("OK"); }
14
+    @Operation(summary="删除区域") @DeleteMapping("/{id}") public R<String> delete(@PathVariable Long id) { svc.delete(id); return R.ok("OK"); }
15
+    @Operation(summary="区域统计") @GetMapping("/stats") public R<Map<String,Object>> stats() { return R.ok(svc.getStats()); }
16
+}

+ 186
- 0
wm-patrol/src/main/java/com/water/patrol/controller/PatrolCoreController.java Vedi File

@@ -0,0 +1,186 @@
1
+package com.water.patrol.controller;
2
+
3
+import com.water.common.core.result.R;
4
+import com.water.patrol.service.PatrolCoreService;
5
+import com.water.patrol.service.PatrolWoService;
6
+import io.swagger.v3.oas.annotations.Operation;
7
+import io.swagger.v3.oas.annotations.tags.Tag;
8
+import lombok.RequiredArgsConstructor;
9
+import org.springframework.web.bind.annotation.*;
10
+
11
+import java.util.List;
12
+import java.util.Map;
13
+
14
+/**
15
+ * 巡检管理核心控制器 - Issue #86
16
+ * PAT-01~PAT-06
17
+ */
18
+@Tag(name = "巡检管理核心")
19
+@RestController
20
+@RequestMapping("/patrol/core")
21
+@RequiredArgsConstructor
22
+public class PatrolCoreController {
23
+
24
+    private final PatrolCoreService coreService;
25
+    private final PatrolWoService woService;
26
+
27
+    // ========== PAT-01 巡检总览 ==========
28
+
29
+    @GetMapping("/overview")
30
+    @Operation(summary = "巡检总览数据")
31
+    public R<Map<String, Object>> overview() {
32
+        return R.ok(coreService.getOverview());
33
+    }
34
+
35
+    @GetMapping("/overview/today-tasks")
36
+    @Operation(summary = "今日任务")
37
+    public R<List<Map<String, Object>>> todayTasks() {
38
+        return R.ok(coreService.getTodayTasks());
39
+    }
40
+
41
+    @GetMapping("/overview/recent-issues")
42
+    @Operation(summary = "最近问题")
43
+    public R<List<Map<String, Object>>> recentIssues(@RequestParam(defaultValue = "10") int limit) {
44
+        return R.ok(coreService.getRecentIssues(limit));
45
+    }
46
+
47
+    // ========== PAT-02 任务轨迹 ==========
48
+
49
+    @PostMapping("/track/record")
50
+    @Operation(summary = "记录GPS轨迹点")
51
+    public R<Map<String, Object>> recordTrack(@RequestBody Map<String, Object> req) {
52
+        Long taskId = Long.parseLong(String.valueOf(req.get("taskId")));
53
+        Long workerId = req.get("workerId") != null ? Long.parseLong(String.valueOf(req.get("workerId"))) : null;
54
+        double lng = ((Number) req.get("lng")).doubleValue();
55
+        double lat = ((Number) req.get("lat")).doubleValue();
56
+        double speed = req.get("speed") != null ? ((Number) req.get("speed")).doubleValue() : 0;
57
+        double accuracy = req.get("accuracy") != null ? ((Number) req.get("accuracy")).doubleValue() : 0;
58
+        return R.ok(coreService.recordTrackPoint(taskId, workerId, lng, lat, speed, accuracy));
59
+    }
60
+
61
+    @GetMapping("/track/{taskId}")
62
+    @Operation(summary = "获取任务轨迹")
63
+    public R<List<Map<String, Object>>> getTrack(@PathVariable Long taskId) {
64
+        return R.ok(coreService.getTrack(taskId));
65
+    }
66
+
67
+    @GetMapping("/track/{taskId}/replay")
68
+    @Operation(summary = "轨迹回放")
69
+    public R<Map<String, Object>> replayTrack(@PathVariable Long taskId) {
70
+        return R.ok(coreService.replayTrack(taskId));
71
+    }
72
+
73
+    @GetMapping("/track/{taskId}/stats")
74
+    @Operation(summary = "轨迹统计")
75
+    public R<Map<String, Object>> trackStats(@PathVariable Long taskId) {
76
+        return R.ok(coreService.getTrackStats(taskId));
77
+    }
78
+
79
+    // ========== PAT-03 任务台账 ==========
80
+
81
+    @GetMapping("/ledger/tasks")
82
+    @Operation(summary = "任务台账")
83
+    public R<Map<String, Object>> taskLedger(
84
+            @RequestParam(required = false) String status,
85
+            @RequestParam(required = false) Long workerId,
86
+            @RequestParam(defaultValue = "1") int page,
87
+            @RequestParam(defaultValue = "20") int size) {
88
+        return R.ok(coreService.getTaskLedger(status, workerId, page, size));
89
+    }
90
+
91
+    // ========== PAT-04 设备台账 ==========
92
+
93
+    @GetMapping("/ledger/devices")
94
+    @Operation(summary = "设备台账")
95
+    public R<Map<String, Object>> deviceLedger(
96
+            @RequestParam(required = false) String type,
97
+            @RequestParam(required = false) String status,
98
+            @RequestParam(required = false) String area,
99
+            @RequestParam(defaultValue = "1") int page,
100
+            @RequestParam(defaultValue = "20") int size) {
101
+        return R.ok(coreService.getDeviceLedger(type, status, area, page, size));
102
+    }
103
+
104
+    @PostMapping("/device")
105
+    @Operation(summary = "新增设备")
106
+    public R<Map<String, Object>> createDevice(@RequestBody Map<String, Object> req) {
107
+        return R.ok(coreService.createDevice(
108
+            (String) req.get("deviceNo"),
109
+            (String) req.get("deviceName"),
110
+            (String) req.get("deviceType"),
111
+            (String) req.get("location"),
112
+            ((Number) req.getOrDefault("lng", 0)).doubleValue(),
113
+            ((Number) req.getOrDefault("lat", 0)).doubleValue(),
114
+            (String) req.get("area")));
115
+    }
116
+
117
+    @PutMapping("/device/{deviceId}/status")
118
+    @Operation(summary = "更新设备状态")
119
+    public R<Map<String, Object>> updateDeviceStatus(@PathVariable Long deviceId,
120
+                                                      @RequestBody Map<String, Object> req) {
121
+        return R.ok(coreService.updateDeviceStatus(deviceId, (String) req.get("status")));
122
+    }
123
+
124
+    // ========== PAT-05 工单管理 ==========
125
+
126
+    @PostMapping("/work-order")
127
+    @Operation(summary = "创建巡检工单")
128
+    public R<Map<String, Object>> createWo(@RequestBody Map<String, Object> req) {
129
+        Long taskId = req.get("taskId") != null ? Long.parseLong(String.valueOf(req.get("taskId"))) : null;
130
+        return R.ok(woService.create(
131
+            (String) req.get("issueType"),
132
+            (String) req.get("description"),
133
+            (String) req.get("severity"),
134
+            (String) req.get("location"),
135
+            req.get("lng") != null ? ((Number) req.get("lng")).doubleValue() : null,
136
+            req.get("lat") != null ? ((Number) req.get("lat")).doubleValue() : null,
137
+            taskId));
138
+    }
139
+
140
+    @GetMapping("/work-order/list")
141
+    @Operation(summary = "工单列表")
142
+    public R<Map<String, Object>> listWo(
143
+            @RequestParam(required = false) String status,
144
+            @RequestParam(required = false) String severity,
145
+            @RequestParam(defaultValue = "1") int page,
146
+            @RequestParam(defaultValue = "20") int size) {
147
+        return R.ok(woService.list(status, severity, page, size));
148
+    }
149
+
150
+    @GetMapping("/work-order/{woId}")
151
+    @Operation(summary = "工单详情")
152
+    public R<Map<String, Object>> woDetail(@PathVariable Long woId) {
153
+        Map<String, Object> detail = woService.getDetail(woId);
154
+        if (detail == null) {
155
+            return R.fail("工单不存在");
156
+        }
157
+        return R.ok(detail);
158
+    }
159
+
160
+    @GetMapping("/work-order/stats")
161
+    @Operation(summary = "工单统计")
162
+    public R<Map<String, Object>> woStats() {
163
+        return R.ok(woService.stats());
164
+    }
165
+
166
+    // ========== PAT-06 工单处理 ==========
167
+
168
+    @PutMapping("/work-order/{woId}/assign")
169
+    @Operation(summary = "分派工单")
170
+    public R<Map<String, Object>> assignWo(@PathVariable Long woId, @RequestBody Map<String, Object> req) {
171
+        Long assigneeId = Long.parseLong(String.valueOf(req.get("assigneeId")));
172
+        return R.ok(woService.assign(woId, assigneeId, (String) req.get("assigneeName")));
173
+    }
174
+
175
+    @PutMapping("/work-order/{woId}/process")
176
+    @Operation(summary = "处理工单")
177
+    public R<Map<String, Object>> processWo(@PathVariable Long woId, @RequestBody Map<String, Object> req) {
178
+        return R.ok(woService.process(woId, (String) req.get("resolution")));
179
+    }
180
+
181
+    @PutMapping("/work-order/{woId}/resolve")
182
+    @Operation(summary = "解决工单")
183
+    public R<Map<String, Object>> resolveWo(@PathVariable Long woId) {
184
+        return R.ok(woService.resolve(woId));
185
+    }
186
+}

+ 15
- 0
wm-patrol/src/main/java/com/water/patrol/controller/PatrolFormController.java Vedi File

@@ -0,0 +1,15 @@
1
+package com.water.patrol.controller;
2
+import com.water.common.core.result.R; import com.water.patrol.entity.PatrolForm; import com.water.patrol.service.PatrolFormService;
3
+import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag;
4
+import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.*;
5
+import java.util.Map;
6
+@Tag(name="巡检表单") @RestController @RequestMapping("/api/patrol/form") @RequiredArgsConstructor
7
+public class PatrolFormController {
8
+    private final PatrolFormService svc;
9
+    @Operation(summary="创建表单") @PostMapping public R<PatrolForm> create(@RequestBody Map<String,Object> req) { return R.ok(svc.create((String)req.get("formName"),(String)req.get("formCode"),(String)req.get("formType"),(String)req.get("fieldConfig"))); }
10
+    @Operation(summary="表单列表") @GetMapping public R<Map<String,Object>> list(@RequestParam(required=false) String keyword, @RequestParam(required=false) String formType, @RequestParam(required=false) String status, @RequestParam(defaultValue="1") int page, @RequestParam(defaultValue="20") int size) { return R.ok(svc.list(keyword,formType,status,page,size)); }
11
+    @Operation(summary="表单详情") @GetMapping("/{id}") public R<PatrolForm> detail(@PathVariable Long id) { return R.ok(svc.getDetail(id)); }
12
+    @Operation(summary="更新表单") @PutMapping("/{id}") public R<PatrolForm> update(@PathVariable Long id, @RequestBody PatrolForm patch) { return R.ok(svc.update(id,patch)); }
13
+    @Operation(summary="删除表单") @DeleteMapping("/{id}") public R<String> delete(@PathVariable Long id) { svc.delete(id); return R.ok("OK"); }
14
+    @Operation(summary="表单统计") @GetMapping("/stats") public R<Map<String,Object>> stats() { return R.ok(svc.getStats()); }
15
+}

+ 16
- 0
wm-patrol/src/main/java/com/water/patrol/controller/PatrolRouteSetupController.java Vedi File

@@ -0,0 +1,16 @@
1
+package com.water.patrol.controller;
2
+import com.water.common.core.result.R; import com.water.patrol.entity.PatrolRouteSetup; import com.water.patrol.service.PatrolRouteSetupService;
3
+import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag;
4
+import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.*;
5
+import java.util.Map;
6
+@Tag(name="巡检路线设置") @RestController @RequestMapping("/api/patrol/route-setup") @RequiredArgsConstructor
7
+public class PatrolRouteSetupController {
8
+    private final PatrolRouteSetupService svc;
9
+    @Operation(summary="创建路线") @PostMapping public R<PatrolRouteSetup> create(@RequestBody Map<String,Object> req) { return R.ok(svc.create((String)req.get("routeName"),(String)req.get("routeCode"),((Number)req.get("areaId")).longValue(),(String)req.get("areaName"),(Integer)req.get("checkpointCount"),(String)req.get("checkpointList"),(Double)req.get("totalDistance"),(Integer)req.get("estimatedMinutes"))); }
10
+    @Operation(summary="路线列表") @GetMapping public R<Map<String,Object>> list(@RequestParam(required=false) String keyword, @RequestParam(required=false) String status, @RequestParam(required=false) Long areaId, @RequestParam(defaultValue="1") int page, @RequestParam(defaultValue="20") int size) { return R.ok(svc.list(keyword,status,areaId,page,size)); }
11
+    @Operation(summary="路线详情") @GetMapping("/{id}") public R<PatrolRouteSetup> detail(@PathVariable Long id) { return R.ok(svc.getDetail(id)); }
12
+    @Operation(summary="更新路线") @PutMapping("/{id}") public R<PatrolRouteSetup> update(@PathVariable Long id, @RequestBody PatrolRouteSetup patch) { return R.ok(svc.update(id,patch)); }
13
+    @Operation(summary="更新状态") @PutMapping("/{id}/status") public R<String> updateStatus(@PathVariable Long id, @RequestParam String status) { svc.updateStatus(id,status); return R.ok("OK"); }
14
+    @Operation(summary="删除路线") @DeleteMapping("/{id}") public R<String> delete(@PathVariable Long id) { svc.delete(id); return R.ok("OK"); }
15
+    @Operation(summary="路线统计") @GetMapping("/stats") public R<Map<String,Object>> stats() { return R.ok(svc.getStats()); }
16
+}

+ 16
- 0
wm-patrol/src/main/java/com/water/patrol/controller/PatrolTemplateController.java Vedi File

@@ -0,0 +1,16 @@
1
+package com.water.patrol.controller;
2
+import com.water.common.core.result.R; import com.water.patrol.entity.PatrolTemplate; import com.water.patrol.service.PatrolTemplateService;
3
+import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag;
4
+import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.*;
5
+import java.util.Map;
6
+@Tag(name="巡检模板") @RestController @RequestMapping("/api/patrol/template") @RequiredArgsConstructor
7
+public class PatrolTemplateController {
8
+    private final PatrolTemplateService svc;
9
+    @Operation(summary="创建模板") @PostMapping public R<PatrolTemplate> create(@RequestBody Map<String,Object> req) { return R.ok(svc.create((String)req.get("templateName"),(String)req.get("templateCode"),((Number)req.get("routeId")).longValue(),(String)req.get("routeName"),((Number)req.get("formId")).longValue(),(String)req.get("formName"),(String)req.get("scheduleType"),(String)req.get("scheduleConfig"))); }
10
+    @Operation(summary="模板列表") @GetMapping public R<Map<String,Object>> list(@RequestParam(required=false) String keyword, @RequestParam(required=false) String status, @RequestParam(required=false) String scheduleType, @RequestParam(defaultValue="1") int page, @RequestParam(defaultValue="20") int size) { return R.ok(svc.list(keyword,status,scheduleType,page,size)); }
11
+    @Operation(summary="模板详情") @GetMapping("/{id}") public R<PatrolTemplate> detail(@PathVariable Long id) { return R.ok(svc.getDetail(id)); }
12
+    @Operation(summary="更新模板") @PutMapping("/{id}") public R<PatrolTemplate> update(@PathVariable Long id, @RequestBody PatrolTemplate patch) { return R.ok(svc.update(id,patch)); }
13
+    @Operation(summary="更新状态") @PutMapping("/{id}/status") public R<String> updateStatus(@PathVariable Long id, @RequestParam String status) { svc.updateStatus(id,status); return R.ok("OK"); }
14
+    @Operation(summary="删除模板") @DeleteMapping("/{id}") public R<String> delete(@PathVariable Long id) { svc.delete(id); return R.ok("OK"); }
15
+    @Operation(summary="模板统计") @GetMapping("/stats") public R<Map<String,Object>> stats() { return R.ok(svc.getStats()); }
16
+}

+ 15
- 0
wm-patrol/src/main/java/com/water/patrol/entity/PatrolArea.java Vedi File

@@ -0,0 +1,15 @@
1
+package com.water.patrol.entity;
2
+import com.baomidou.mybatisplus.annotation.*;
3
+import lombok.Data;
4
+import java.math.BigDecimal;
5
+import java.time.LocalDateTime;
6
+@Data @TableName("pat_area")
7
+public class PatrolArea {
8
+    @TableId(type = IdType.AUTO) private Long id;
9
+    private String areaName; private String areaCode; private String description;
10
+    private BigDecimal centerLng; private BigDecimal centerLat;
11
+    private String boundary; private Double radius;
12
+    private String status;
13
+    @TableField(fill = FieldFill.INSERT) private LocalDateTime createdAt;
14
+    @TableField(fill = FieldFill.INSERT_UPDATE) private LocalDateTime updatedAt;
15
+}

+ 13
- 0
wm-patrol/src/main/java/com/water/patrol/entity/PatrolForm.java Vedi File

@@ -0,0 +1,13 @@
1
+package com.water.patrol.entity;
2
+import com.baomidou.mybatisplus.annotation.*;
3
+import lombok.Data;
4
+import java.time.LocalDateTime;
5
+@Data @TableName("pat_form")
6
+public class PatrolForm {
7
+    @TableId(type = IdType.AUTO) private Long id;
8
+    private String formName; private String formCode;
9
+    private String formType; private String fieldConfig;
10
+    private String status; private String remark;
11
+    @TableField(fill = FieldFill.INSERT) private LocalDateTime createdAt;
12
+    @TableField(fill = FieldFill.INSERT_UPDATE) private LocalDateTime updatedAt;
13
+}

+ 15
- 0
wm-patrol/src/main/java/com/water/patrol/entity/PatrolRouteSetup.java Vedi File

@@ -0,0 +1,15 @@
1
+package com.water.patrol.entity;
2
+import com.baomidou.mybatisplus.annotation.*;
3
+import lombok.Data;
4
+import java.time.LocalDateTime;
5
+@Data @TableName("pat_route_setup")
6
+public class PatrolRouteSetup {
7
+    @TableId(type = IdType.AUTO) private Long id;
8
+    private String routeName; private String routeCode;
9
+    private Long areaId; private String areaName;
10
+    private Integer checkpointCount; private String checkpointList;
11
+    private Double totalDistance; private Integer estimatedMinutes;
12
+    private String status; private String remark;
13
+    @TableField(fill = FieldFill.INSERT) private LocalDateTime createdAt;
14
+    @TableField(fill = FieldFill.INSERT_UPDATE) private LocalDateTime updatedAt;
15
+}

+ 15
- 0
wm-patrol/src/main/java/com/water/patrol/entity/PatrolTemplate.java Vedi File

@@ -0,0 +1,15 @@
1
+package com.water.patrol.entity;
2
+import com.baomidou.mybatisplus.annotation.*;
3
+import lombok.Data;
4
+import java.time.LocalDateTime;
5
+@Data @TableName("pat_template")
6
+public class PatrolTemplate {
7
+    @TableId(type = IdType.AUTO) private Long id;
8
+    private String templateName; private String templateCode;
9
+    private Long routeId; private String routeName;
10
+    private Long formId; private String formName;
11
+    private String scheduleType; private String scheduleConfig;
12
+    private String status; private String remark;
13
+    @TableField(fill = FieldFill.INSERT) private LocalDateTime createdAt;
14
+    @TableField(fill = FieldFill.INSERT_UPDATE) private LocalDateTime updatedAt;
15
+}

+ 5
- 0
wm-patrol/src/main/java/com/water/patrol/mapper/PatrolAreaMapper.java Vedi File

@@ -0,0 +1,5 @@
1
+package com.water.patrol.mapper;
2
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
3
+import com.water.patrol.entity.PatrolArea;
4
+import org.apache.ibatis.annotations.Mapper;
5
+@Mapper public interface PatrolAreaMapper extends BaseMapper<PatrolArea> {}

+ 5
- 0
wm-patrol/src/main/java/com/water/patrol/mapper/PatrolFormMapper.java Vedi File

@@ -0,0 +1,5 @@
1
+package com.water.patrol.mapper;
2
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
3
+import com.water.patrol.entity.PatrolForm;
4
+import org.apache.ibatis.annotations.Mapper;
5
+@Mapper public interface PatrolFormMapper extends BaseMapper<PatrolForm> {}

+ 5
- 0
wm-patrol/src/main/java/com/water/patrol/mapper/PatrolRouteSetupMapper.java Vedi File

@@ -0,0 +1,5 @@
1
+package com.water.patrol.mapper;
2
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
3
+import com.water.patrol.entity.PatrolRouteSetup;
4
+import org.apache.ibatis.annotations.Mapper;
5
+@Mapper public interface PatrolRouteSetupMapper extends BaseMapper<PatrolRouteSetup> {}

+ 5
- 0
wm-patrol/src/main/java/com/water/patrol/mapper/PatrolTemplateMapper.java Vedi File

@@ -0,0 +1,5 @@
1
+package com.water.patrol.mapper;
2
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
3
+import com.water.patrol.entity.PatrolTemplate;
4
+import org.apache.ibatis.annotations.Mapper;
5
+@Mapper public interface PatrolTemplateMapper extends BaseMapper<PatrolTemplate> {}

+ 53
- 0
wm-patrol/src/main/java/com/water/patrol/service/PatrolAreaService.java Vedi File

@@ -0,0 +1,53 @@
1
+package com.water.patrol.service;
2
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
3
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
4
+import com.water.patrol.entity.PatrolArea; import com.water.patrol.mapper.PatrolAreaMapper;
5
+import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j;
6
+import org.springframework.stereotype.Service;
7
+import java.math.BigDecimal; import java.util.*;
8
+@Slf4j @Service @RequiredArgsConstructor
9
+public class PatrolAreaService {
10
+    private final PatrolAreaMapper mapper;
11
+    public PatrolArea create(String areaName, String areaCode, String desc, BigDecimal cLng, BigDecimal cLat, String boundary, Double radius) {
12
+        PatrolArea a = new PatrolArea(); a.setAreaName(areaName); a.setAreaCode(areaCode);
13
+        a.setDescription(desc); a.setCenterLng(cLng); a.setCenterLat(cLat);
14
+        a.setBoundary(boundary); a.setRadius(radius); a.setStatus("active");
15
+        mapper.insert(a); return a;
16
+    }
17
+    public PatrolArea update(Long id, PatrolArea patch) {
18
+        PatrolArea a = mapper.selectById(id);
19
+        if (a == null) throw new RuntimeException("区域不存在: "+id);
20
+        if (patch.getAreaName() != null) a.setAreaName(patch.getAreaName());
21
+        if (patch.getDescription() != null) a.setDescription(patch.getDescription());
22
+        if (patch.getCenterLng() != null) a.setCenterLng(patch.getCenterLng());
23
+        if (patch.getCenterLat() != null) a.setCenterLat(patch.getCenterLat());
24
+        if (patch.getBoundary() != null) a.setBoundary(patch.getBoundary());
25
+        if (patch.getRadius() != null) a.setRadius(patch.getRadius());
26
+        mapper.updateById(a); return a;
27
+    }
28
+    public void delete(Long id) { mapper.deleteById(id); }
29
+    public PatrolArea getDetail(Long id) { return mapper.selectById(id); }
30
+    public Map<String, Object> list(String keyword, String status, int page, int size) {
31
+        LambdaQueryWrapper<PatrolArea> w = new LambdaQueryWrapper<>();
32
+        if (keyword != null && !keyword.isEmpty()) w.and(q -> q.like(PatrolArea::getAreaName, keyword).or().like(PatrolArea::getAreaCode, keyword));
33
+        if (status != null && !status.isEmpty()) w.eq(PatrolArea::getStatus, status);
34
+        w.orderByDesc(PatrolArea::getCreatedAt);
35
+        Page<PatrolArea> p = mapper.selectPage(new Page<>(page, size), w);
36
+        Map<String, Object> r = new LinkedHashMap<>();
37
+        r.put("records", p.getRecords()); r.put("total", p.getTotal());
38
+        r.put("page", page); r.put("size", size);
39
+        return r;
40
+    }
41
+    public void updateStatus(Long id, String status) {
42
+        PatrolArea a = mapper.selectById(id);
43
+        if (a == null) throw new RuntimeException("区域不存在: "+id);
44
+        a.setStatus(status); mapper.updateById(a);
45
+    }
46
+    public Map<String, Object> getStats() {
47
+        Map<String, Object> r = new LinkedHashMap<>();
48
+        r.put("total", mapper.selectCount(null));
49
+        r.put("active", mapper.selectCount(new LambdaQueryWrapper<PatrolArea>().eq(PatrolArea::getStatus,"active")));
50
+        r.put("inactive", mapper.selectCount(new LambdaQueryWrapper<PatrolArea>().eq(PatrolArea::getStatus,"inactive")));
51
+        return r;
52
+    }
53
+}

+ 252
- 0
wm-patrol/src/main/java/com/water/patrol/service/PatrolCoreService.java Vedi File

@@ -0,0 +1,252 @@
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.time.LocalDateTime;
10
+import java.util.*;
11
+
12
+/**
13
+ * 巡检管理核心服务 - Issue #86
14
+ * PAT-01 总览, PAT-02 轨迹, PAT-03 任务台账, PAT-04 设备台账
15
+ */
16
+@Slf4j
17
+@Service
18
+@RequiredArgsConstructor
19
+public class PatrolCoreService {
20
+
21
+    private final JdbcTemplate jdbc;
22
+
23
+    // ========== PAT-01 巡检总览 ==========
24
+
25
+    public Map<String, Object> getOverview() {
26
+        LocalDate today = LocalDate.now();
27
+        LocalDate monthStart = today.withDayOfMonth(1);
28
+
29
+        long todayTotal = countQuery("SELECT COUNT(*) FROM patrol_task WHERE task_date = ?", today);
30
+        long todayCompleted = countQuery("SELECT COUNT(*) FROM patrol_task WHERE task_date = ? AND status = 'completed'", today);
31
+        long monthTotal = countQuery("SELECT COUNT(*) FROM patrol_task WHERE task_date >= ?", monthStart);
32
+        long monthCompleted = countQuery("SELECT COUNT(*) FROM patrol_task WHERE task_date >= ? AND status = 'completed'", monthStart);
33
+
34
+        long pendingIssues = countQuery("SELECT COUNT(*) FROM patrol_work_order WHERE status IN ('pending','assigned','processing')");
35
+        long resolvedIssues = countQuery("SELECT COUNT(*) FROM patrol_work_order WHERE status = 'resolved'");
36
+
37
+        long coverageRate = monthTotal > 0 ? Math.round(monthCompleted * 100.0 / monthTotal) : 0;
38
+
39
+        Map<String, Object> overview = new LinkedHashMap<>();
40
+        overview.put("todayTotal", todayTotal);
41
+        overview.put("todayCompleted", todayCompleted);
42
+        overview.put("monthTotal", monthTotal);
43
+        overview.put("monthCompleted", monthCompleted);
44
+        overview.put("coverageRate", coverageRate);
45
+        overview.put("pendingIssues", pendingIssues);
46
+        overview.put("resolvedIssues", resolvedIssues);
47
+        return overview;
48
+    }
49
+
50
+    public List<Map<String, Object>> getTodayTasks() {
51
+        return jdbc.queryForList(
52
+            "SELECT pt.*, pr.route_name, pr.area FROM patrol_task pt " +
53
+            "LEFT JOIN patrol_route pr ON pt.route_id = pr.id " +
54
+            "WHERE pt.task_date = CURRENT_DATE ORDER BY pt.plan_start");
55
+    }
56
+
57
+    public List<Map<String, Object>> getRecentIssues(int limit) {
58
+        return jdbc.queryForList(
59
+            "SELECT * FROM patrol_work_order ORDER BY created_at DESC LIMIT ?", limit);
60
+    }
61
+
62
+    // ========== PAT-02 任务轨迹(GPS记录与回放) ==========
63
+
64
+    public Map<String, Object> recordTrackPoint(Long taskId, Long workerId,
65
+                                                 double lng, double lat,
66
+                                                 double speed, double accuracy) {
67
+        jdbc.update(
68
+            "INSERT INTO patrol_track_point (task_id, worker_id, lng, lat, speed, accuracy) " +
69
+            "VALUES (?,?,?,?,?,?)",
70
+            taskId, workerId, lng, lat, speed, accuracy);
71
+        Map<String, Object> result = new LinkedHashMap<>();
72
+        result.put("taskId", taskId);
73
+        result.put("workerId", workerId);
74
+        result.put("lng", lng);
75
+        result.put("lat", lat);
76
+        result.put("recorded", true);
77
+        return result;
78
+    }
79
+
80
+    public List<Map<String, Object>> getTrack(Long taskId) {
81
+        return jdbc.queryForList(
82
+            "SELECT * FROM patrol_track_point WHERE task_id = ? ORDER BY recorded_at", taskId);
83
+    }
84
+
85
+    public Map<String, Object> replayTrack(Long taskId) {
86
+        List<Map<String, Object>> points = getTrack(taskId);
87
+        Map<String, Object> result = new LinkedHashMap<>();
88
+        result.put("taskId", taskId);
89
+        result.put("points", points);
90
+        result.put("totalPoints", points.size());
91
+        if (!points.isEmpty()) {
92
+            result.put("startTime", points.get(0).get("recorded_at"));
93
+            result.put("endTime", points.get(points.size() - 1).get("recorded_at"));
94
+        }
95
+        return result;
96
+    }
97
+
98
+    public Map<String, Object> getTrackStats(Long taskId) {
99
+        List<Map<String, Object>> points = getTrack(taskId);
100
+        Map<String, Object> stats = new LinkedHashMap<>();
101
+        stats.put("taskId", taskId);
102
+        stats.put("totalPoints", points.size());
103
+
104
+        if (points.size() < 2) {
105
+            stats.put("totalDistance", 0.0);
106
+            stats.put("duration", 0);
107
+            stats.put("avgSpeed", 0.0);
108
+            return stats;
109
+        }
110
+
111
+        double totalDistance = 0;
112
+        for (int i = 1; i < points.size(); i++) {
113
+            double lat1 = ((Number) points.get(i - 1).get("lat")).doubleValue();
114
+            double lon1 = ((Number) points.get(i - 1).get("lng")).doubleValue();
115
+            double lat2 = ((Number) points.get(i).get("lat")).doubleValue();
116
+            double lon2 = ((Number) points.get(i).get("lng")).doubleValue();
117
+            totalDistance += haversine(lat1, lon1, lat2, lon2);
118
+        }
119
+
120
+        double avgSpeed = points.stream()
121
+            .filter(p -> p.get("speed") != null)
122
+            .mapToDouble(p -> ((Number) p.get("speed")).doubleValue())
123
+            .average().orElse(0);
124
+
125
+        stats.put("totalDistance", Math.round(totalDistance * 100.0) / 100.0);
126
+        stats.put("avgSpeed", Math.round(avgSpeed * 100.0) / 100.0);
127
+        return stats;
128
+    }
129
+
130
+    // ========== PAT-03 任务台账 ==========
131
+
132
+    public Map<String, Object> getTaskLedger(String status, Long workerId, int page, int size) {
133
+        StringBuilder sql = new StringBuilder("SELECT * FROM patrol_task WHERE 1=1");
134
+        List<Object> params = new ArrayList<>();
135
+        if (status != null && !status.isEmpty()) {
136
+            sql.append(" AND status = ?");
137
+            params.add(status);
138
+        }
139
+        if (workerId != null) {
140
+            sql.append(" AND assignee_id = ?");
141
+            params.add(workerId);
142
+        }
143
+        sql.append(" ORDER BY task_date DESC LIMIT ? OFFSET ?");
144
+        params.add(size);
145
+        params.add((page - 1) * size);
146
+
147
+        List<Map<String, Object>> records = jdbc.queryForList(sql.toString(), params.toArray());
148
+
149
+        StringBuilder countSql = new StringBuilder("SELECT COUNT(*) FROM patrol_task WHERE 1=1");
150
+        List<Object> countParams = new ArrayList<>();
151
+        if (status != null && !status.isEmpty()) {
152
+            countSql.append(" AND status = ?");
153
+            countParams.add(status);
154
+        }
155
+        if (workerId != null) {
156
+            countSql.append(" AND assignee_id = ?");
157
+            countParams.add(workerId);
158
+        }
159
+        long total = countQuery(countSql.toString(), countParams.toArray());
160
+
161
+        Map<String, Object> result = new LinkedHashMap<>();
162
+        result.put("total", total);
163
+        result.put("page", page);
164
+        result.put("size", size);
165
+        result.put("records", records);
166
+        return result;
167
+    }
168
+
169
+    // ========== PAT-04 设备台账 ==========
170
+
171
+    public Map<String, Object> getDeviceLedger(String deviceType, String status, String area,
172
+                                                int page, int size) {
173
+        StringBuilder sql = new StringBuilder("SELECT * FROM patrol_device WHERE 1=1");
174
+        List<Object> params = new ArrayList<>();
175
+        if (deviceType != null && !deviceType.isEmpty()) {
176
+            sql.append(" AND device_type = ?");
177
+            params.add(deviceType);
178
+        }
179
+        if (status != null && !status.isEmpty()) {
180
+            sql.append(" AND status = ?");
181
+            params.add(status);
182
+        }
183
+        if (area != null && !area.isEmpty()) {
184
+            sql.append(" AND area = ?");
185
+            params.add(area);
186
+        }
187
+        sql.append(" ORDER BY created_at DESC LIMIT ? OFFSET ?");
188
+        params.add(size);
189
+        params.add((page - 1) * size);
190
+
191
+        List<Map<String, Object>> records = jdbc.queryForList(sql.toString(), params.toArray());
192
+
193
+        StringBuilder countSql = new StringBuilder("SELECT COUNT(*) FROM patrol_device WHERE 1=1");
194
+        List<Object> countParams = new ArrayList<>();
195
+        if (deviceType != null && !deviceType.isEmpty()) {
196
+            countSql.append(" AND device_type = ?");
197
+            countParams.add(deviceType);
198
+        }
199
+        if (status != null && !status.isEmpty()) {
200
+            countSql.append(" AND status = ?");
201
+            countParams.add(status);
202
+        }
203
+        if (area != null && !area.isEmpty()) {
204
+            countSql.append(" AND area = ?");
205
+            countParams.add(area);
206
+        }
207
+        long total = countQuery(countSql.toString(), countParams.toArray());
208
+
209
+        Map<String, Object> result = new LinkedHashMap<>();
210
+        result.put("total", total);
211
+        result.put("page", page);
212
+        result.put("size", size);
213
+        result.put("records", records);
214
+        return result;
215
+    }
216
+
217
+    public Map<String, Object> createDevice(String deviceNo, String deviceName, String deviceType,
218
+                                             String location, double lng, double lat, String area) {
219
+        jdbc.update(
220
+            "INSERT INTO patrol_device (device_no, device_name, device_type, location, lng, lat, area) " +
221
+            "VALUES (?,?,?,?,?,?,?)",
222
+            deviceNo, deviceName, deviceType, location, lng, lat, area);
223
+        Map<String, Object> result = new LinkedHashMap<>();
224
+        result.put("deviceNo", deviceNo);
225
+        result.put("deviceName", deviceName);
226
+        result.put("created", true);
227
+        return result;
228
+    }
229
+
230
+    public Map<String, Object> updateDeviceStatus(Long deviceId, String status) {
231
+        jdbc.update("UPDATE patrol_device SET status = ?, updated_at = NOW() WHERE id = ?",
232
+            status, deviceId);
233
+        return Map.of("deviceId", deviceId, "status", status, "updated", true);
234
+    }
235
+
236
+    // ========== 工具方法 ==========
237
+
238
+    private long countQuery(String sql, Object... args) {
239
+        Long count = jdbc.queryForObject(sql, Long.class, args);
240
+        return count != null ? count : 0;
241
+    }
242
+
243
+    private double haversine(double lat1, double lon1, double lat2, double lon2) {
244
+        double R = 6371;
245
+        double dLat = Math.toRadians(lat2 - lat1);
246
+        double dLon = Math.toRadians(lon2 - lon1);
247
+        double a = Math.sin(dLat / 2) * Math.sin(dLat / 2)
248
+            + Math.cos(Math.toRadians(lat1)) * Math.cos(Math.toRadians(lat2))
249
+            * Math.sin(dLon / 2) * Math.sin(dLon / 2);
250
+        return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
251
+    }
252
+}

+ 45
- 0
wm-patrol/src/main/java/com/water/patrol/service/PatrolFormService.java Vedi File

@@ -0,0 +1,45 @@
1
+package com.water.patrol.service;
2
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
3
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
4
+import com.water.patrol.entity.PatrolForm; import com.water.patrol.mapper.PatrolFormMapper;
5
+import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j;
6
+import org.springframework.stereotype.Service;
7
+import java.util.*;
8
+@Slf4j @Service @RequiredArgsConstructor
9
+public class PatrolFormService {
10
+    private final PatrolFormMapper mapper;
11
+    public PatrolForm create(String formName, String formCode, String formType, String fieldConfig) {
12
+        PatrolForm f = new PatrolForm(); f.setFormName(formName); f.setFormCode(formCode);
13
+        f.setFormType(formType); f.setFieldConfig(fieldConfig); f.setStatus("active");
14
+        mapper.insert(f); return f;
15
+    }
16
+    public PatrolForm update(Long id, PatrolForm patch) {
17
+        PatrolForm f = mapper.selectById(id);
18
+        if (f == null) throw new RuntimeException("表单不存在: "+id);
19
+        if (patch.getFormName() != null) f.setFormName(patch.getFormName());
20
+        if (patch.getFieldConfig() != null) f.setFieldConfig(patch.getFieldConfig());
21
+        if (patch.getFormType() != null) f.setFormType(patch.getFormType());
22
+        if (patch.getRemark() != null) f.setRemark(patch.getRemark());
23
+        mapper.updateById(f); return f;
24
+    }
25
+    public void delete(Long id) { mapper.deleteById(id); }
26
+    public PatrolForm getDetail(Long id) { return mapper.selectById(id); }
27
+    public Map<String, Object> list(String keyword, String formType, String status, int page, int size) {
28
+        LambdaQueryWrapper<PatrolForm> w = new LambdaQueryWrapper<>();
29
+        if (keyword != null && !keyword.isEmpty()) w.and(q -> q.like(PatrolForm::getFormName, keyword).or().like(PatrolForm::getFormCode, keyword));
30
+        if (formType != null && !formType.isEmpty()) w.eq(PatrolForm::getFormType, formType);
31
+        if (status != null && !status.isEmpty()) w.eq(PatrolForm::getStatus, status);
32
+        w.orderByDesc(PatrolForm::getCreatedAt);
33
+        Page<PatrolForm> p = mapper.selectPage(new Page<>(page, size), w);
34
+        Map<String, Object> r = new LinkedHashMap<>();
35
+        r.put("records", p.getRecords()); r.put("total", p.getTotal());
36
+        r.put("page", page); r.put("size", size);
37
+        return r;
38
+    }
39
+    public Map<String, Object> getStats() {
40
+        Map<String, Object> r = new LinkedHashMap<>();
41
+        r.put("total", mapper.selectCount(null));
42
+        r.put("active", mapper.selectCount(new LambdaQueryWrapper<PatrolForm>().eq(PatrolForm::getStatus,"active")));
43
+        return r;
44
+    }
45
+}

+ 55
- 0
wm-patrol/src/main/java/com/water/patrol/service/PatrolRouteSetupService.java Vedi File

@@ -0,0 +1,55 @@
1
+package com.water.patrol.service;
2
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
3
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
4
+import com.water.patrol.entity.PatrolRouteSetup; import com.water.patrol.mapper.PatrolRouteSetupMapper;
5
+import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j;
6
+import org.springframework.stereotype.Service;
7
+import java.util.*;
8
+@Slf4j @Service @RequiredArgsConstructor
9
+public class PatrolRouteSetupService {
10
+    private final PatrolRouteSetupMapper mapper;
11
+    public PatrolRouteSetup create(String routeName, String routeCode, Long areaId, String areaName,
12
+            Integer checkpointCount, String checkpointList, Double totalDistance, Integer estimatedMinutes) {
13
+        PatrolRouteSetup r = new PatrolRouteSetup(); r.setRouteName(routeName); r.setRouteCode(routeCode);
14
+        r.setAreaId(areaId); r.setAreaName(areaName); r.setCheckpointCount(checkpointCount);
15
+        r.setCheckpointList(checkpointList); r.setTotalDistance(totalDistance);
16
+        r.setEstimatedMinutes(estimatedMinutes); r.setStatus("active");
17
+        mapper.insert(r); return r;
18
+    }
19
+    public PatrolRouteSetup update(Long id, PatrolRouteSetup patch) {
20
+        PatrolRouteSetup r = mapper.selectById(id);
21
+        if (r == null) throw new RuntimeException("路线不存在: "+id);
22
+        if (patch.getRouteName() != null) r.setRouteName(patch.getRouteName());
23
+        if (patch.getCheckpointCount() != null) r.setCheckpointCount(patch.getCheckpointCount());
24
+        if (patch.getCheckpointList() != null) r.setCheckpointList(patch.getCheckpointList());
25
+        if (patch.getTotalDistance() != null) r.setTotalDistance(patch.getTotalDistance());
26
+        if (patch.getEstimatedMinutes() != null) r.setEstimatedMinutes(patch.getEstimatedMinutes());
27
+        if (patch.getRemark() != null) r.setRemark(patch.getRemark());
28
+        mapper.updateById(r); return r;
29
+    }
30
+    public void delete(Long id) { mapper.deleteById(id); }
31
+    public PatrolRouteSetup getDetail(Long id) { return mapper.selectById(id); }
32
+    public Map<String, Object> list(String keyword, String status, Long areaId, int page, int size) {
33
+        LambdaQueryWrapper<PatrolRouteSetup> w = new LambdaQueryWrapper<>();
34
+        if (keyword != null && !keyword.isEmpty()) w.and(q -> q.like(PatrolRouteSetup::getRouteName, keyword).or().like(PatrolRouteSetup::getRouteCode, keyword));
35
+        if (status != null && !status.isEmpty()) w.eq(PatrolRouteSetup::getStatus, status);
36
+        if (areaId != null) w.eq(PatrolRouteSetup::getAreaId, areaId);
37
+        w.orderByDesc(PatrolRouteSetup::getCreatedAt);
38
+        Page<PatrolRouteSetup> p = mapper.selectPage(new Page<>(page, size), w);
39
+        Map<String, Object> result = new LinkedHashMap<>();
40
+        result.put("records", p.getRecords()); result.put("total", p.getTotal());
41
+        result.put("page", page); result.put("size", size);
42
+        return result;
43
+    }
44
+    public void updateStatus(Long id, String status) {
45
+        PatrolRouteSetup r = mapper.selectById(id);
46
+        if (r == null) throw new RuntimeException("路线不存在: "+id);
47
+        r.setStatus(status); mapper.updateById(r);
48
+    }
49
+    public Map<String, Object> getStats() {
50
+        Map<String, Object> r = new LinkedHashMap<>();
51
+        r.put("total", mapper.selectCount(null));
52
+        r.put("active", mapper.selectCount(new LambdaQueryWrapper<PatrolRouteSetup>().eq(PatrolRouteSetup::getStatus,"active")));
53
+        return r;
54
+    }
55
+}

+ 59
- 0
wm-patrol/src/main/java/com/water/patrol/service/PatrolTemplateService.java Vedi File

@@ -0,0 +1,59 @@
1
+package com.water.patrol.service;
2
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
3
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
4
+import com.water.patrol.entity.PatrolTemplate; import com.water.patrol.mapper.PatrolTemplateMapper;
5
+import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j;
6
+import org.springframework.stereotype.Service;
7
+import java.util.*;
8
+@Slf4j @Service @RequiredArgsConstructor
9
+public class PatrolTemplateService {
10
+    private final PatrolTemplateMapper mapper;
11
+    public PatrolTemplate create(String templateName, String templateCode, Long routeId, String routeName,
12
+            Long formId, String formName, String scheduleType, String scheduleConfig) {
13
+        PatrolTemplate t = new PatrolTemplate(); t.setTemplateName(templateName); t.setTemplateCode(templateCode);
14
+        t.setRouteId(routeId); t.setRouteName(routeName); t.setFormId(formId); t.setFormName(formName);
15
+        t.setScheduleType(scheduleType); t.setScheduleConfig(scheduleConfig); t.setStatus("active");
16
+        mapper.insert(t); return t;
17
+    }
18
+    public PatrolTemplate update(Long id, PatrolTemplate patch) {
19
+        PatrolTemplate t = mapper.selectById(id);
20
+        if (t == null) throw new RuntimeException("模板不存在: "+id);
21
+        if (patch.getTemplateName() != null) t.setTemplateName(patch.getTemplateName());
22
+        if (patch.getRouteId() != null) t.setRouteId(patch.getRouteId());
23
+        if (patch.getRouteName() != null) t.setRouteName(patch.getRouteName());
24
+        if (patch.getFormId() != null) t.setFormId(patch.getFormId());
25
+        if (patch.getFormName() != null) t.setFormName(patch.getFormName());
26
+        if (patch.getScheduleType() != null) t.setScheduleType(patch.getScheduleType());
27
+        if (patch.getScheduleConfig() != null) t.setScheduleConfig(patch.getScheduleConfig());
28
+        if (patch.getRemark() != null) t.setRemark(patch.getRemark());
29
+        mapper.updateById(t); return t;
30
+    }
31
+    public void delete(Long id) { mapper.deleteById(id); }
32
+    public PatrolTemplate getDetail(Long id) { return mapper.selectById(id); }
33
+    public Map<String, Object> list(String keyword, String status, String scheduleType, int page, int size) {
34
+        LambdaQueryWrapper<PatrolTemplate> w = new LambdaQueryWrapper<>();
35
+        if (keyword != null && !keyword.isEmpty()) w.and(q -> q.like(PatrolTemplate::getTemplateName, keyword).or().like(PatrolTemplate::getTemplateCode, keyword));
36
+        if (status != null && !status.isEmpty()) w.eq(PatrolTemplate::getStatus, status);
37
+        if (scheduleType != null && !scheduleType.isEmpty()) w.eq(PatrolTemplate::getScheduleType, scheduleType);
38
+        w.orderByDesc(PatrolTemplate::getCreatedAt);
39
+        Page<PatrolTemplate> p = mapper.selectPage(new Page<>(page, size), w);
40
+        Map<String, Object> r = new LinkedHashMap<>();
41
+        r.put("records", p.getRecords()); r.put("total", p.getTotal());
42
+        r.put("page", page); r.put("size", size);
43
+        return r;
44
+    }
45
+    public void updateStatus(Long id, String status) {
46
+        PatrolTemplate t = mapper.selectById(id);
47
+        if (t == null) throw new RuntimeException("模板不存在: "+id);
48
+        t.setStatus(status); mapper.updateById(t);
49
+    }
50
+    public Map<String, Object> getStats() {
51
+        Map<String, Object> r = new LinkedHashMap<>();
52
+        r.put("total", mapper.selectCount(null));
53
+        r.put("active", mapper.selectCount(new LambdaQueryWrapper<PatrolTemplate>().eq(PatrolTemplate::getStatus,"active")));
54
+        r.put("daily", mapper.selectCount(new LambdaQueryWrapper<PatrolTemplate>().eq(PatrolTemplate::getScheduleType,"daily")));
55
+        r.put("weekly", mapper.selectCount(new LambdaQueryWrapper<PatrolTemplate>().eq(PatrolTemplate::getScheduleType,"weekly")));
56
+        r.put("monthly", mapper.selectCount(new LambdaQueryWrapper<PatrolTemplate>().eq(PatrolTemplate::getScheduleType,"monthly")));
57
+        return r;
58
+    }
59
+}

+ 122
- 0
wm-patrol/src/main/java/com/water/patrol/service/PatrolWoService.java Vedi File

@@ -0,0 +1,122 @@
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.util.*;
9
+
10
+/**
11
+ * 巡检工单服务 - Issue #86
12
+ * PAT-05 工单管理, PAT-06 工单处理
13
+ */
14
+@Slf4j
15
+@Service
16
+@RequiredArgsConstructor
17
+public class PatrolWoService {
18
+
19
+    private final JdbcTemplate jdbc;
20
+
21
+    /** 创建巡检工单 */
22
+    public Map<String, Object> create(String issueType, String description, String severity,
23
+                                       String location, Double lng, Double lat, Long taskId) {
24
+        String orderNo = "PWO-" + System.currentTimeMillis();
25
+        jdbc.update(
26
+            "INSERT INTO patrol_work_order (order_no, task_id, issue_type, description, severity, " +
27
+            "location, lng, lat, status) VALUES (?,?,?,?,?,?,?,?,?)",
28
+            orderNo, taskId, issueType, description,
29
+            severity != null ? severity : "medium",
30
+            location, lng, lat, "pending");
31
+        Map<String, Object> result = new LinkedHashMap<>();
32
+        result.put("orderNo", orderNo);
33
+        result.put("issueType", issueType);
34
+        result.put("status", "pending");
35
+        result.put("created", true);
36
+        return result;
37
+    }
38
+
39
+    /** 分派工单 */
40
+    public Map<String, Object> assign(Long woId, Long assigneeId, String assigneeName) {
41
+        int rows = jdbc.update(
42
+            "UPDATE patrol_work_order SET assignee_id = ?, assignee_name = ?, status = 'assigned' " +
43
+            "WHERE id = ?", assigneeId, assigneeName, woId);
44
+        return Map.of("success", rows > 0, "woId", woId);
45
+    }
46
+
47
+    /** 处理工单 */
48
+    public Map<String, Object> process(Long woId, String resolution) {
49
+        int rows = jdbc.update(
50
+            "UPDATE patrol_work_order SET status = 'processing', resolution = ? WHERE id = ?",
51
+            resolution, woId);
52
+        return Map.of("success", rows > 0, "woId", woId);
53
+    }
54
+
55
+    /** 解决工单 */
56
+    public Map<String, Object> resolve(Long woId) {
57
+        int rows = jdbc.update(
58
+            "UPDATE patrol_work_order SET status = 'resolved', resolved_at = NOW() WHERE id = ?", woId);
59
+        return Map.of("success", rows > 0, "woId", woId);
60
+    }
61
+
62
+    /** 工单详情 */
63
+    public Map<String, Object> getDetail(Long woId) {
64
+        List<Map<String, Object>> rows = jdbc.queryForList(
65
+            "SELECT * FROM patrol_work_order WHERE id = ?", woId);
66
+        return rows.isEmpty() ? null : rows.get(0);
67
+    }
68
+
69
+    /** 工单列表 */
70
+    public Map<String, Object> list(String status, String severity, int page, int size) {
71
+        StringBuilder sql = new StringBuilder("SELECT * FROM patrol_work_order WHERE 1=1");
72
+        List<Object> params = new ArrayList<>();
73
+        if (status != null && !status.isEmpty()) {
74
+            sql.append(" AND status = ?");
75
+            params.add(status);
76
+        }
77
+        if (severity != null && !severity.isEmpty()) {
78
+            sql.append(" AND severity = ?");
79
+            params.add(severity);
80
+        }
81
+        sql.append(" ORDER BY created_at DESC LIMIT ? OFFSET ?");
82
+        params.add(size);
83
+        params.add((page - 1) * size);
84
+
85
+        List<Map<String, Object>> records = jdbc.queryForList(sql.toString(), params.toArray());
86
+
87
+        StringBuilder countSql = new StringBuilder("SELECT COUNT(*) FROM patrol_work_order WHERE 1=1");
88
+        List<Object> countParams = new ArrayList<>();
89
+        if (status != null && !status.isEmpty()) {
90
+            countSql.append(" AND status = ?");
91
+            countParams.add(status);
92
+        }
93
+        if (severity != null && !severity.isEmpty()) {
94
+            countSql.append(" AND severity = ?");
95
+            countParams.add(severity);
96
+        }
97
+        long total = countQuery(countSql.toString(), countParams.toArray());
98
+
99
+        Map<String, Object> result = new LinkedHashMap<>();
100
+        result.put("total", total);
101
+        result.put("page", page);
102
+        result.put("size", size);
103
+        result.put("records", records);
104
+        return result;
105
+    }
106
+
107
+    /** 工单统计 */
108
+    public Map<String, Object> stats() {
109
+        List<Map<String, Object>> rows = jdbc.queryForList(
110
+            "SELECT status, COUNT(*) as count FROM patrol_work_order GROUP BY status");
111
+        Map<String, Object> result = new LinkedHashMap<>();
112
+        for (Map<String, Object> row : rows) {
113
+            result.put(String.valueOf(row.get("status")), ((Number) row.get("count")).longValue());
114
+        }
115
+        return result;
116
+    }
117
+
118
+    private long countQuery(String sql, Object... args) {
119
+        Long count = jdbc.queryForObject(sql, Long.class, args);
120
+        return count != null ? count : 0;
121
+    }
122
+}

+ 24
- 0
wm-patrol/src/main/resources/sql/V87__patrol_setup.sql Vedi File

@@ -0,0 +1,24 @@
1
+CREATE TABLE IF NOT EXISTS pat_area (
2
+    id BIGSERIAL PRIMARY KEY, area_name VARCHAR(100), area_code VARCHAR(32) UNIQUE,
3
+    description VARCHAR(500), center_lng NUMERIC(10,6), center_lat NUMERIC(10,6),
4
+    boundary TEXT, radius DOUBLE PRECISION, status VARCHAR(20) DEFAULT 'active',
5
+    created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW()
6
+);
7
+CREATE TABLE IF NOT EXISTS pat_route_setup (
8
+    id BIGSERIAL PRIMARY KEY, route_name VARCHAR(100), route_code VARCHAR(32) UNIQUE,
9
+    area_id BIGINT, area_name VARCHAR(100), checkpoint_count INT DEFAULT 0,
10
+    checkpoint_list TEXT, total_distance DOUBLE PRECISION, estimated_minutes INT,
11
+    status VARCHAR(20) DEFAULT 'active', remark VARCHAR(500),
12
+    created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW()
13
+);
14
+CREATE TABLE IF NOT EXISTS pat_form (
15
+    id BIGSERIAL PRIMARY KEY, form_name VARCHAR(100), form_code VARCHAR(32) UNIQUE,
16
+    form_type VARCHAR(30), field_config TEXT, status VARCHAR(20) DEFAULT 'active',
17
+    remark VARCHAR(500), created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW()
18
+);
19
+CREATE TABLE IF NOT EXISTS pat_template (
20
+    id BIGSERIAL PRIMARY KEY, template_name VARCHAR(100), template_code VARCHAR(32) UNIQUE,
21
+    route_id BIGINT, route_name VARCHAR(100), form_id BIGINT, form_name VARCHAR(100),
22
+    schedule_type VARCHAR(20), schedule_config TEXT, status VARCHAR(20) DEFAULT 'active',
23
+    remark VARCHAR(500), created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW()
24
+);

+ 25
- 0
wm-patrol/src/test/java/com/water/patrol/PatrolSetupTest.java Vedi File

@@ -0,0 +1,25 @@
1
+package com.water.patrol;
2
+import com.water.patrol.entity.*; import com.water.patrol.mapper.*; import com.water.patrol.service.*;
3
+import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith;
4
+import org.mockito.*; import org.mockito.junit.jupiter.MockitoExtension;
5
+import java.math.BigDecimal; import java.util.*;
6
+import static org.junit.jupiter.api.Assertions.*;
7
+import static org.mockito.Mockito.*;
8
+@ExtendWith(MockitoExtension.class)
9
+class PatrolSetupTest {
10
+    @Mock PatrolAreaMapper areaMapper; @Mock PatrolRouteSetupMapper routeMapper;
11
+    @Mock PatrolFormMapper formMapper; @Mock PatrolTemplateMapper tplMapper;
12
+    @InjectMocks PatrolAreaService areaSvc; @InjectMocks PatrolRouteSetupService routeSvc;
13
+    @InjectMocks PatrolFormService formSvc; @InjectMocks PatrolTemplateService tplSvc;
14
+
15
+    @Test void testCreateArea() { when(areaMapper.insert(any())).thenReturn(1); PatrolArea a=areaSvc.create("A区","AREA-001","供水主管",new BigDecimal("116"),new BigDecimal("39"),null,500.0); assertEquals("active",a.getStatus()); assertEquals("A区",a.getAreaName()); }
16
+    @Test void testUpdateArea() { PatrolArea a=new PatrolArea(); a.setId(1L); a.setAreaName("A区"); a.setStatus("active"); when(areaMapper.selectById(1L)).thenReturn(a); when(areaMapper.updateById(any())).thenReturn(1); PatrolArea p=new PatrolArea(); p.setAreaName("B区"); assertEquals("B区",areaSvc.update(1L,p).getAreaName()); }
17
+    @Test void testAreaStatus() { PatrolArea a=new PatrolArea(); a.setId(1L); a.setStatus("active"); when(areaMapper.selectById(1L)).thenReturn(a); when(areaMapper.updateById(any())).thenReturn(1); areaSvc.updateStatus(1L,"inactive"); assertEquals("inactive",a.getStatus()); }
18
+    @Test void testCreateRoute() { when(routeMapper.insert(any())).thenReturn(1); PatrolRouteSetup r=routeSvc.create("主管线","ROUTE-001",1L,"A区",5,null,2000.0,60); assertEquals("active",r.getStatus()); assertEquals(5,(int)r.getCheckpointCount()); }
19
+    @Test void testUpdateRoute() { PatrolRouteSetup r=new PatrolRouteSetup(); r.setId(1L); r.setRouteName("R1"); when(routeMapper.selectById(1L)).thenReturn(r); when(routeMapper.updateById(any())).thenReturn(1); PatrolRouteSetup p=new PatrolRouteSetup(); p.setRouteName("R2"); assertEquals("R2",routeSvc.update(1L,p).getRouteName()); }
20
+    @Test void testRouteStatus() { PatrolRouteSetup r=new PatrolRouteSetup(); r.setId(1L); r.setStatus("active"); when(routeMapper.selectById(1L)).thenReturn(r); when(routeMapper.updateById(any())).thenReturn(1); routeSvc.updateStatus(1L,"inactive"); assertEquals("inactive",r.getStatus()); }
21
+    @Test void testCreateForm() { when(formMapper.insert(any())).thenReturn(1); PatrolForm f=formSvc.create("日常巡检表","FORM-001","checklist","{\"fields\":[]}"); assertEquals("active",f.getStatus()); assertEquals("checklist",f.getFormType()); }
22
+    @Test void testUpdateForm() { PatrolForm f=new PatrolForm(); f.setId(1L); f.setFormName("F1"); when(formMapper.selectById(1L)).thenReturn(f); when(formMapper.updateById(any())).thenReturn(1); PatrolForm p=new PatrolForm(); p.setFormName("F2"); assertEquals("F2",formSvc.update(1L,p).getFormName()); }
23
+    @Test void testCreateTemplate() { when(tplMapper.insert(any())).thenReturn(1); PatrolTemplate t=tplSvc.create("日常模板","TPL-001",1L,"主管线",1L,"日常巡检表","daily","08:00"); assertEquals("active",t.getStatus()); assertEquals("daily",t.getScheduleType()); }
24
+    @Test void testUpdateTemplate() { PatrolTemplate t=new PatrolTemplate(); t.setId(1L); t.setTemplateName("T1"); when(tplMapper.selectById(1L)).thenReturn(t); when(tplMapper.updateById(any())).thenReturn(1); PatrolTemplate p=new PatrolTemplate(); p.setTemplateName("T2"); assertEquals("T2",tplSvc.update(1L,p).getTemplateName()); }
25
+}