Selaa lähdekoodia

feat(wm-patrol): #86 巡检管理核心

- PatrolOverviewService: 今日任务/完成率/异常数/在线巡检员/月度统计
- PatrolTrackService: GPS轨迹记录/回放/Haversine距离/导出
- PatrolLedgerService: 任务CRUD/分页查询/开始完成/统计
- PatrolDeviceService: 设备注册/状态/维护记录/分页/统计
- PatrolWorkOrderService: 工单创建/分配/处理/解决/关闭/统计
- 5个Controller共25+端点, 10个单元测试
- DDL: pat_track_point/pat_task/pat_device/pat_work_order + 索引
bot_dev2 4 päivää sitten
vanhempi
commit
637f2dd633
20 muutettua tiedostoa jossa 558 lisäystä ja 0 poistoa
  1. 16
    0
      wm-patrol/src/main/java/com/water/patrol/controller/PatrolDeviceController.java
  2. 15
    0
      wm-patrol/src/main/java/com/water/patrol/controller/PatrolLedgerController.java
  3. 11
    0
      wm-patrol/src/main/java/com/water/patrol/controller/PatrolOverviewController.java
  4. 13
    0
      wm-patrol/src/main/java/com/water/patrol/controller/PatrolTrackController.java
  5. 17
    0
      wm-patrol/src/main/java/com/water/patrol/controller/PatrolWoController.java
  6. 16
    0
      wm-patrol/src/main/java/com/water/patrol/entity/PatrolDevice.java
  7. 15
    0
      wm-patrol/src/main/java/com/water/patrol/entity/PatrolTask.java
  8. 12
    0
      wm-patrol/src/main/java/com/water/patrol/entity/PatrolTrackPoint.java
  9. 14
    0
      wm-patrol/src/main/java/com/water/patrol/entity/PatrolWorkOrder.java
  10. 5
    0
      wm-patrol/src/main/java/com/water/patrol/mapper/PatrolDeviceMapper.java
  11. 5
    0
      wm-patrol/src/main/java/com/water/patrol/mapper/PatrolTaskMapper.java
  12. 5
    0
      wm-patrol/src/main/java/com/water/patrol/mapper/PatrolTrackPointMapper.java
  13. 5
    0
      wm-patrol/src/main/java/com/water/patrol/mapper/PatrolWorkOrderMapper.java
  14. 106
    0
      wm-patrol/src/main/java/com/water/patrol/service/PatrolDeviceService.java
  15. 50
    0
      wm-patrol/src/main/java/com/water/patrol/service/PatrolLedgerService.java
  16. 30
    0
      wm-patrol/src/main/java/com/water/patrol/service/PatrolOverviewService.java
  17. 47
    0
      wm-patrol/src/main/java/com/water/patrol/service/PatrolTrackService.java
  18. 114
    0
      wm-patrol/src/main/java/com/water/patrol/service/PatrolWorkOrderService.java
  19. 35
    0
      wm-patrol/src/main/resources/sql/V86__patrol_core.sql
  20. 27
    0
      wm-patrol/src/test/java/com/water/patrol/PatrolCoreTest.java

+ 16
- 0
wm-patrol/src/main/java/com/water/patrol/controller/PatrolDeviceController.java Näytä tiedosto

1
+package com.water.patrol.controller;
2
+import com.water.common.core.result.R; import com.water.patrol.entity.PatrolDevice; import com.water.patrol.service.PatrolDeviceService;
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/device") @RequiredArgsConstructor
7
+public class PatrolDeviceController {
8
+    private final PatrolDeviceService svc;
9
+    @Operation(summary="注册") @PostMapping public R<PatrolDevice> create(@RequestParam String deviceNo, @RequestParam String deviceName, @RequestParam String deviceType, @RequestParam String location, @RequestParam BigDecimal lng, @RequestParam BigDecimal lat) { return R.ok(svc.create(deviceNo,deviceName,deviceType,location,lng,lat)); }
10
+    @Operation(summary="列表") @GetMapping public R<Map<String,Object>> list(@RequestParam(required=false) String deviceType, @RequestParam(required=false) String status, @RequestParam(required=false) String keyword, @RequestParam(defaultValue="1") int page, @RequestParam(defaultValue="20") int size) { return R.ok(svc.list(deviceType,status,keyword,page,size)); }
11
+    @Operation(summary="详情") @GetMapping("/{id}") public R<PatrolDevice> detail(@PathVariable Long id) { return R.ok(svc.getDetail(id)); }
12
+    @Operation(summary="状态") @PutMapping("/{id}/status") public R<String> updateStatus(@PathVariable Long id, @RequestParam String status) { svc.updateStatus(id,status); return R.ok("OK"); }
13
+    @Operation(summary="维护") @PutMapping("/{id}/maintenance") public R<String> maintenance(@PathVariable Long id) { svc.recordMaintenance(id); return R.ok("OK"); }
14
+    @Operation(summary="统计") @GetMapping("/stats") public R<Map<String,Object>> stats() { return R.ok(svc.getStats()); }
15
+    @Operation(summary="删除") @DeleteMapping("/{id}") public R<String> delete(@PathVariable Long id) { svc.delete(id); return R.ok("OK"); }
16
+}

+ 15
- 0
wm-patrol/src/main/java/com/water/patrol/controller/PatrolLedgerController.java Näytä tiedosto

1
+package com.water.patrol.controller;
2
+import com.water.common.core.result.R; import com.water.patrol.entity.PatrolTask; import com.water.patrol.service.PatrolLedgerService;
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/ledger") @RequiredArgsConstructor
7
+public class PatrolLedgerController {
8
+    private final PatrolLedgerService svc;
9
+    @Operation(summary="列表") @GetMapping public R<Map<String,Object>> list(@RequestParam(required=false) String status, @RequestParam(required=false) String workerName, @RequestParam(required=false) String startDate, @RequestParam(required=false) String endDate, @RequestParam(defaultValue="1") int page, @RequestParam(defaultValue="20") int size) { return R.ok(svc.listTasks(status,workerName,startDate,endDate,page,size)); }
10
+    @Operation(summary="详情") @GetMapping("/{id}") public R<PatrolTask> detail(@PathVariable Long id) { return R.ok(svc.getDetail(id)); }
11
+    @Operation(summary="创建") @PostMapping public R<PatrolTask> create(@RequestBody Map<String,Object> req) { return R.ok(svc.createTask((String)req.get("taskName"),((Number)req.get("routeId")).longValue(),((Number)req.get("workerId")).longValue(),(String)req.get("workerName"))); }
12
+    @Operation(summary="开始") @PutMapping("/{id}/start") public R<String> start(@PathVariable Long id) { svc.startTask(id); return R.ok("OK"); }
13
+    @Operation(summary="完成") @PutMapping("/{id}/complete") public R<String> complete(@PathVariable Long id, @RequestParam Integer completedCheckpoints, @RequestParam Double distance) { svc.completeTask(id,completedCheckpoints,distance); return R.ok("OK"); }
14
+    @Operation(summary="统计") @GetMapping("/stats") public R<Map<String,Object>> stats() { return R.ok(svc.getStats()); }
15
+}

+ 11
- 0
wm-patrol/src/main/java/com/water/patrol/controller/PatrolOverviewController.java Näytä tiedosto

1
+package com.water.patrol.controller;
2
+import com.water.common.core.result.R; import com.water.patrol.service.PatrolOverviewService;
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/overview") @RequiredArgsConstructor
7
+public class PatrolOverviewController {
8
+    private final PatrolOverviewService svc;
9
+    @Operation(summary="总览") @GetMapping public R<Map<String,Object>> overview() { return R.ok(svc.getOverview()); }
10
+    @Operation(summary="月度统计") @GetMapping("/monthly") public R<Map<String,Object>> monthly(@RequestParam int year, @RequestParam int month) { return R.ok(svc.getMonthlyStats(year,month)); }
11
+}

+ 13
- 0
wm-patrol/src/main/java/com/water/patrol/controller/PatrolTrackController.java Näytä tiedosto

1
+package com.water.patrol.controller;
2
+import com.water.common.core.result.R; import com.water.patrol.entity.PatrolTrackPoint; import com.water.patrol.service.PatrolTrackService;
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.*;
6
+@Tag(name="巡检轨迹") @RestController @RequestMapping("/api/patrol/track") @RequiredArgsConstructor
7
+public class PatrolTrackController {
8
+    private final PatrolTrackService svc;
9
+    @Operation(summary="记录GPS") @PostMapping public R<PatrolTrackPoint> record(@RequestParam Long taskId, @RequestParam Long workerId, @RequestParam BigDecimal lng, @RequestParam BigDecimal lat, @RequestParam(required=false) BigDecimal speed, @RequestParam(required=false) BigDecimal accuracy, @RequestParam(required=false) String address) { return R.ok(svc.recordPoint(taskId,workerId,lng,lat,speed,accuracy,address)); }
10
+    @Operation(summary="轨迹") @GetMapping("/{taskId}") public R<List<PatrolTrackPoint>> getTrack(@PathVariable Long taskId) { return R.ok(svc.getTrack(taskId)); }
11
+    @Operation(summary="摘要") @GetMapping("/{taskId}/summary") public R<Map<String,Object>> summary(@PathVariable Long taskId) { return R.ok(svc.getTrackSummary(taskId)); }
12
+    @Operation(summary="导出") @GetMapping("/{taskId}/export") public R<List<Map<String,Object>>> export(@PathVariable Long taskId) { return R.ok(svc.exportTrack(taskId)); }
13
+}

+ 17
- 0
wm-patrol/src/main/java/com/water/patrol/controller/PatrolWoController.java Näytä tiedosto

1
+package com.water.patrol.controller;
2
+import com.water.common.core.result.R; import com.water.patrol.entity.PatrolWorkOrder; import com.water.patrol.service.PatrolWorkOrderService;
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/work-order") @RequiredArgsConstructor
7
+public class PatrolWoController {
8
+    private final PatrolWorkOrderService svc;
9
+    @Operation(summary="创建") @PostMapping public R<PatrolWorkOrder> create(@RequestBody Map<String,Object> req) { return R.ok(svc.create(((Number)req.get("taskId")).longValue(),(String)req.get("issueType"),(String)req.get("description"),(String)req.get("photos"),(String)req.get("severity"))); }
10
+    @Operation(summary="列表") @GetMapping public R<Map<String,Object>> list(@RequestParam(required=false) String status, @RequestParam(required=false) String severity, @RequestParam(required=false) String keyword, @RequestParam(defaultValue="1") int page, @RequestParam(defaultValue="20") int size) { return R.ok(svc.list(status,severity,keyword,page,size)); }
11
+    @Operation(summary="详情") @GetMapping("/{id}") public R<PatrolWorkOrder> detail(@PathVariable Long id) { return R.ok(svc.getDetail(id)); }
12
+    @Operation(summary="分配") @PutMapping("/{id}/assign") public R<String> assign(@PathVariable Long id, @RequestParam Long assigneeId, @RequestParam String assigneeName) { svc.assign(id,assigneeId,assigneeName); return R.ok("OK"); }
13
+    @Operation(summary="开始") @PutMapping("/{id}/start") public R<String> start(@PathVariable Long id) { svc.startProcess(id); return R.ok("OK"); }
14
+    @Operation(summary="解决") @PutMapping("/{id}/resolve") public R<String> resolve(@PathVariable Long id, @RequestParam String resolution) { svc.resolve(id,resolution); return R.ok("OK"); }
15
+    @Operation(summary="关闭") @PutMapping("/{id}/close") public R<String> close(@PathVariable Long id) { svc.close(id); return R.ok("OK"); }
16
+    @Operation(summary="统计") @GetMapping("/stats") public R<Map<String,Object>> stats() { return R.ok(svc.getStats()); }
17
+}

+ 16
- 0
wm-patrol/src/main/java/com/water/patrol/entity/PatrolDevice.java Näytä tiedosto

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.LocalDate;
6
+import java.time.LocalDateTime;
7
+@Data @TableName("pat_device")
8
+public class PatrolDevice {
9
+    @TableId(type = IdType.AUTO) private Long id;
10
+    private String deviceNo; private String deviceName; private String deviceType;
11
+    private String location; private BigDecimal lng; private BigDecimal lat;
12
+    private String status; private LocalDate lastMaintenanceDate; private LocalDate nextMaintenanceDate;
13
+    private String remark;
14
+    @TableField(fill = FieldFill.INSERT) private LocalDateTime createdAt;
15
+    @TableField(fill = FieldFill.INSERT_UPDATE) private LocalDateTime updatedAt;
16
+}

+ 15
- 0
wm-patrol/src/main/java/com/water/patrol/entity/PatrolTask.java Näytä tiedosto

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_task")
6
+public class PatrolTask {
7
+    @TableId(type = IdType.AUTO) private Long id;
8
+    private String taskNo; private String taskName; private Long routeId;
9
+    private Long workerId; private String workerName; private String status;
10
+    private LocalDateTime startTime; private LocalDateTime endTime;
11
+    private Integer checkpoints; private Integer completedCheckpoints; private Integer abnormalCount;
12
+    private Double distance; private String remark;
13
+    @TableField(fill = FieldFill.INSERT) private LocalDateTime createdAt;
14
+    @TableField(fill = FieldFill.INSERT_UPDATE) private LocalDateTime updatedAt;
15
+}

+ 12
- 0
wm-patrol/src/main/java/com/water/patrol/entity/PatrolTrackPoint.java Näytä tiedosto

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_track_point")
7
+public class PatrolTrackPoint {
8
+    @TableId(type = IdType.AUTO) private Long id;
9
+    private Long taskId; private Long workerId;
10
+    private BigDecimal lng; private BigDecimal lat; private BigDecimal speed; private BigDecimal accuracy;
11
+    private String address; private LocalDateTime timestamp;
12
+}

+ 14
- 0
wm-patrol/src/main/java/com/water/patrol/entity/PatrolWorkOrder.java Näytä tiedosto

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_work_order")
6
+public class PatrolWorkOrder {
7
+    @TableId(type = IdType.AUTO) private Long id;
8
+    private String orderNo; private Long taskId; private String issueType;
9
+    private String description; private String photos; private String severity;
10
+    private Long assigneeId; private String assigneeName; private String status;
11
+    private String resolution; private LocalDateTime resolvedAt;
12
+    @TableField(fill = FieldFill.INSERT) private LocalDateTime createdAt;
13
+    @TableField(fill = FieldFill.INSERT_UPDATE) private LocalDateTime updatedAt;
14
+}

+ 5
- 0
wm-patrol/src/main/java/com/water/patrol/mapper/PatrolDeviceMapper.java Näytä tiedosto

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

+ 5
- 0
wm-patrol/src/main/java/com/water/patrol/mapper/PatrolTaskMapper.java Näytä tiedosto

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

+ 5
- 0
wm-patrol/src/main/java/com/water/patrol/mapper/PatrolTrackPointMapper.java Näytä tiedosto

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

+ 5
- 0
wm-patrol/src/main/java/com/water/patrol/mapper/PatrolWorkOrderMapper.java Näytä tiedosto

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

+ 106
- 0
wm-patrol/src/main/java/com/water/patrol/service/PatrolDeviceService.java Näytä tiedosto

1
+package com.water.patrol.service;
2
+
3
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
4
+import com.water.patrol.entity.PatrolDevice;
5
+import com.water.patrol.mapper.PatrolDeviceMapper;
6
+import lombok.RequiredArgsConstructor;
7
+import lombok.extern.slf4j.Slf4j;
8
+import org.springframework.stereotype.Service;
9
+import java.math.BigDecimal;
10
+import java.time.LocalDate;
11
+import java.time.LocalDateTime;
12
+import java.util.*;
13
+
14
+@Slf4j
15
+@Service
16
+@RequiredArgsConstructor
17
+public class PatrolDeviceService {
18
+    private final PatrolDeviceMapper deviceMapper;
19
+
20
+    public PatrolDevice create(String deviceNo, String deviceName, String deviceType,
21
+            String location, BigDecimal lng, BigDecimal lat) {
22
+        PatrolDevice d = new PatrolDevice();
23
+        d.setDeviceNo(deviceNo);
24
+        d.setDeviceName(deviceName);
25
+        d.setDeviceType(deviceType);
26
+        d.setLocation(location);
27
+        d.setLng(lng);
28
+        d.setLat(lat);
29
+        d.setStatus("normal");
30
+        d.setCreatedAt(LocalDateTime.now());
31
+        d.setUpdatedAt(LocalDateTime.now());
32
+        deviceMapper.insert(d);
33
+        log.info("Device created: {}", deviceNo);
34
+        return d;
35
+    }
36
+
37
+    public Map<String, Object> list(String deviceType, String status, String keyword,
38
+            int page, int size) {
39
+        LambdaQueryWrapper<PatrolDevice> w = new LambdaQueryWrapper<>();
40
+        if (deviceType != null && !deviceType.isEmpty()) w.eq(PatrolDevice::getDeviceType, deviceType);
41
+        if (status != null && !status.isEmpty()) w.eq(PatrolDevice::getStatus, status);
42
+        if (keyword != null && !keyword.isEmpty()) {
43
+            w.and(q -> q.like(PatrolDevice::getDeviceName, keyword)
44
+                .or().like(PatrolDevice::getDeviceNo, keyword)
45
+                .or().like(PatrolDevice::getLocation, keyword));
46
+        }
47
+        w.orderByDesc(PatrolDevice::getCreatedAt);
48
+
49
+        Long total = deviceMapper.selectCount(w);
50
+        w.last("LIMIT " + size + " OFFSET " + ((page - 1) * size));
51
+        List<PatrolDevice> records = deviceMapper.selectList(w);
52
+
53
+        Map<String, Object> result = new LinkedHashMap<>();
54
+        result.put("total", total);
55
+        result.put("page", page);
56
+        result.put("size", size);
57
+        result.put("records", records);
58
+        return result;
59
+    }
60
+
61
+    public PatrolDevice getDetail(Long id) {
62
+        return deviceMapper.selectById(id);
63
+    }
64
+
65
+    public void updateStatus(Long id, String status) {
66
+        PatrolDevice d = deviceMapper.selectById(id);
67
+        if (d == null) throw new RuntimeException("Device not found: " + id);
68
+        d.setStatus(status);
69
+        d.setUpdatedAt(LocalDateTime.now());
70
+        deviceMapper.updateById(d);
71
+        log.info("Device {} status -> {}", id, status);
72
+    }
73
+
74
+    public void recordMaintenance(Long id) {
75
+        PatrolDevice d = deviceMapper.selectById(id);
76
+        if (d == null) throw new RuntimeException("Device not found: " + id);
77
+        d.setLastMaintenanceDate(LocalDate.now());
78
+        d.setNextMaintenanceDate(LocalDate.now().plusMonths(6));
79
+        d.setStatus("normal");
80
+        d.setUpdatedAt(LocalDateTime.now());
81
+        deviceMapper.updateById(d);
82
+        log.info("Maintenance recorded for device {}", id);
83
+    }
84
+
85
+    public Map<String, Object> getStats() {
86
+        Map<String, Object> r = new LinkedHashMap<>();
87
+        Long total = deviceMapper.selectCount(new LambdaQueryWrapper<>());
88
+        r.put("total", total);
89
+
90
+        LambdaQueryWrapper<PatrolDevice> normal = new LambdaQueryWrapper<PatrolDevice>().eq(PatrolDevice::getStatus, "normal");
91
+        r.put("normal", deviceMapper.selectCount(normal));
92
+
93
+        LambdaQueryWrapper<PatrolDevice> fault = new LambdaQueryWrapper<PatrolDevice>().eq(PatrolDevice::getStatus, "fault");
94
+        r.put("fault", deviceMapper.selectCount(fault));
95
+
96
+        LambdaQueryWrapper<PatrolDevice> offline = new LambdaQueryWrapper<PatrolDevice>().eq(PatrolDevice::getStatus, "offline");
97
+        r.put("offline", deviceMapper.selectCount(offline));
98
+
99
+        return r;
100
+    }
101
+
102
+    public void delete(Long id) {
103
+        deviceMapper.deleteById(id);
104
+        log.info("Device deleted: {}", id);
105
+    }
106
+}

+ 50
- 0
wm-patrol/src/main/java/com/water/patrol/service/PatrolLedgerService.java Näytä tiedosto

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.PatrolTask; import com.water.patrol.mapper.PatrolTaskMapper;
5
+import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j;
6
+import org.springframework.stereotype.Service;
7
+import java.time.LocalDateTime; import java.util.*;
8
+@Slf4j @Service @RequiredArgsConstructor
9
+public class PatrolLedgerService {
10
+    private final PatrolTaskMapper mapper;
11
+    public Map<String, Object> listTasks(String status, String workerName, String startDate, String endDate, int page, int size) {
12
+        LambdaQueryWrapper<PatrolTask> w = new LambdaQueryWrapper<>();
13
+        if (status != null && !status.isEmpty()) w.eq(PatrolTask::getStatus, status);
14
+        if (workerName != null && !workerName.isEmpty()) w.like(PatrolTask::getWorkerName, workerName);
15
+        if (startDate != null && !startDate.isEmpty()) w.ge(PatrolTask::getCreatedAt, startDate+"T00:00:00");
16
+        if (endDate != null && !endDate.isEmpty()) w.le(PatrolTask::getCreatedAt, endDate+"T23:59:59");
17
+        w.orderByDesc(PatrolTask::getCreatedAt);
18
+        Page<PatrolTask> p = mapper.selectPage(new Page<>(page, size), w);
19
+        Map<String, Object> r = new LinkedHashMap<>();
20
+        r.put("records", p.getRecords()); r.put("total", p.getTotal());
21
+        r.put("page", page); r.put("size", size); r.put("pages", p.getPages());
22
+        return r;
23
+    }
24
+    public PatrolTask getDetail(Long id) { return mapper.selectById(id); }
25
+    public PatrolTask createTask(String taskName, Long routeId, Long workerId, String workerName) {
26
+        PatrolTask t = new PatrolTask(); t.setTaskNo("PAT-"+System.currentTimeMillis());
27
+        t.setTaskName(taskName); t.setRouteId(routeId); t.setWorkerId(workerId); t.setWorkerName(workerName);
28
+        t.setStatus("pending"); t.setCheckpoints(0); t.setCompletedCheckpoints(0);
29
+        t.setAbnormalCount(0); t.setDistance(0.0); mapper.insert(t); return t;
30
+    }
31
+    public void startTask(Long id) {
32
+        PatrolTask t = mapper.selectById(id);
33
+        if (t == null) throw new RuntimeException("任务不存在: "+id);
34
+        t.setStatus("in_progress"); t.setStartTime(LocalDateTime.now()); mapper.updateById(t);
35
+    }
36
+    public void completeTask(Long id, Integer cc, Double dist) {
37
+        PatrolTask t = mapper.selectById(id);
38
+        if (t == null) throw new RuntimeException("任务不存在: "+id);
39
+        t.setStatus("completed"); t.setEndTime(LocalDateTime.now());
40
+        t.setCompletedCheckpoints(cc); t.setDistance(dist); mapper.updateById(t);
41
+    }
42
+    public Map<String, Object> getStats() {
43
+        Map<String, Object> r = new LinkedHashMap<>();
44
+        r.put("total", mapper.selectCount(null));
45
+        r.put("completed", mapper.selectCount(new LambdaQueryWrapper<PatrolTask>().eq(PatrolTask::getStatus,"completed")));
46
+        r.put("inProgress", mapper.selectCount(new LambdaQueryWrapper<PatrolTask>().eq(PatrolTask::getStatus,"in_progress")));
47
+        r.put("pending", mapper.selectCount(new LambdaQueryWrapper<PatrolTask>().eq(PatrolTask::getStatus,"pending")));
48
+        return r;
49
+    }
50
+}

+ 30
- 0
wm-patrol/src/main/java/com/water/patrol/service/PatrolOverviewService.java Näytä tiedosto

1
+package com.water.patrol.service;
2
+import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j;
3
+import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Service;
4
+import java.time.LocalDate; import java.util.*;
5
+@Slf4j @Service @RequiredArgsConstructor
6
+public class PatrolOverviewService {
7
+    private final JdbcTemplate jdbc;
8
+    public Map<String, Object> getOverview() {
9
+        LocalDate today = LocalDate.now();
10
+        Map<String, Object> r = new LinkedHashMap<>();
11
+        r.put("todayTasks", jdbc.queryForObject("SELECT COUNT(*) FROM pat_task WHERE DATE(created_at)=?", Integer.class, today));
12
+        r.put("completedTasks", jdbc.queryForObject("SELECT COUNT(*) FROM pat_task WHERE DATE(created_at)=? AND status='completed'", Integer.class, today));
13
+        int t = (Integer) r.get("todayTasks"), c = (Integer) r.get("completedTasks");
14
+        r.put("completionRate", t > 0 ? Math.round(c * 100.0 / t) : 0);
15
+        r.put("abnormalCount", jdbc.queryForObject("SELECT COUNT(*) FROM pat_work_order WHERE DATE(created_at)=? AND status!='closed'", Integer.class, today));
16
+        r.put("onlineWorkers", jdbc.queryForObject("SELECT COUNT(DISTINCT worker_id) FROM pat_task WHERE status='in_progress'", Integer.class));
17
+        r.put("monthDistance", jdbc.queryForObject("SELECT COALESCE(SUM(distance),0) FROM pat_task WHERE DATE_PART('month',created_at)=DATE_PART('month',?::date)", Double.class, today.toString()));
18
+        r.put("pendingOrders", jdbc.queryForObject("SELECT COUNT(*) FROM pat_work_order WHERE status IN ('pending','assigned')", Integer.class));
19
+        return r;
20
+    }
21
+    public Map<String, Object> getMonthlyStats(int year, int month) {
22
+        String s = String.format("%d-%02d-01", year, month), e = String.format("%d-%02d-31", year, month);
23
+        Map<String, Object> r = new LinkedHashMap<>();
24
+        r.put("totalTasks", jdbc.queryForObject("SELECT COUNT(*) FROM pat_task WHERE created_at BETWEEN ?::date AND ?::date", Integer.class, s, e));
25
+        r.put("completedTasks", jdbc.queryForObject("SELECT COUNT(*) FROM pat_task WHERE created_at BETWEEN ?::date AND ?::date AND status='completed'", Integer.class, s, e));
26
+        r.put("totalDistance", jdbc.queryForObject("SELECT COALESCE(SUM(distance),0) FROM pat_task WHERE created_at BETWEEN ?::date AND ?::date", Double.class, s, e));
27
+        r.put("totalAbnormals", jdbc.queryForObject("SELECT COUNT(*) FROM pat_work_order WHERE created_at BETWEEN ?::date AND ?::date", Integer.class, s, e));
28
+        return r;
29
+    }
30
+}

+ 47
- 0
wm-patrol/src/main/java/com/water/patrol/service/PatrolTrackService.java Näytä tiedosto

1
+package com.water.patrol.service;
2
+import com.water.patrol.entity.PatrolTrackPoint; import com.water.patrol.mapper.PatrolTrackPointMapper;
3
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
4
+import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j;
5
+import org.springframework.stereotype.Service;
6
+import java.math.BigDecimal; import java.time.LocalDateTime; import java.util.*;
7
+@Slf4j @Service @RequiredArgsConstructor
8
+public class PatrolTrackService {
9
+    private final PatrolTrackPointMapper mapper;
10
+    public PatrolTrackPoint recordPoint(Long taskId, Long workerId, BigDecimal lng, BigDecimal lat, BigDecimal speed, BigDecimal accuracy, String address) {
11
+        PatrolTrackPoint p = new PatrolTrackPoint();
12
+        p.setTaskId(taskId); p.setWorkerId(workerId); p.setLng(lng); p.setLat(lat);
13
+        p.setSpeed(speed); p.setAccuracy(accuracy); p.setAddress(address); p.setTimestamp(LocalDateTime.now());
14
+        mapper.insert(p); return p;
15
+    }
16
+    public List<PatrolTrackPoint> getTrack(Long taskId) {
17
+        return mapper.selectList(new LambdaQueryWrapper<PatrolTrackPoint>().eq(PatrolTrackPoint::getTaskId, taskId).orderByAsc(PatrolTrackPoint::getTimestamp));
18
+    }
19
+    public Map<String, Object> getTrackSummary(Long taskId) {
20
+        List<PatrolTrackPoint> pts = getTrack(taskId);
21
+        Map<String, Object> r = new LinkedHashMap<>();
22
+        r.put("totalPoints", pts.size());
23
+        if (!pts.isEmpty()) {
24
+            r.put("startTime", pts.get(0).getTimestamp()); r.put("endTime", pts.get(pts.size()-1).getTimestamp());
25
+            double d = 0; for (int i = 1; i < pts.size(); i++) d += haversine(pts.get(i-1), pts.get(i));
26
+            r.put("totalDistance", Math.round(d));
27
+        }
28
+        return r;
29
+    }
30
+    public List<Map<String, Object>> exportTrack(Long taskId) {
31
+        List<Map<String, Object>> result = new ArrayList<>();
32
+        for (PatrolTrackPoint p : getTrack(taskId)) {
33
+            Map<String, Object> m = new LinkedHashMap<>();
34
+            m.put("lng", p.getLng()); m.put("lat", p.getLat()); m.put("speed", p.getSpeed());
35
+            m.put("timestamp", p.getTimestamp()); m.put("address", p.getAddress());
36
+            result.add(m);
37
+        }
38
+        return result;
39
+    }
40
+    private double haversine(PatrolTrackPoint a, PatrolTrackPoint b) {
41
+        double R=6371000, dLat=Math.toRadians(b.getLat().doubleValue()-a.getLat().doubleValue()),
42
+               dLng=Math.toRadians(b.getLng().doubleValue()-a.getLng().doubleValue()),
43
+               la1=Math.toRadians(a.getLat().doubleValue()), la2=Math.toRadians(b.getLat().doubleValue());
44
+        double x=Math.sin(dLat/2)*Math.sin(dLat/2)+Math.cos(la1)*Math.cos(la2)*Math.sin(dLng/2)*Math.sin(dLng/2);
45
+        return R*2*Math.atan2(Math.sqrt(x),Math.sqrt(1-x));
46
+    }
47
+}

+ 114
- 0
wm-patrol/src/main/java/com/water/patrol/service/PatrolWorkOrderService.java Näytä tiedosto

1
+package com.water.patrol.service;
2
+
3
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
4
+import com.water.patrol.entity.PatrolWorkOrder;
5
+import com.water.patrol.mapper.PatrolWorkOrderMapper;
6
+import lombok.RequiredArgsConstructor;
7
+import lombok.extern.slf4j.Slf4j;
8
+import org.springframework.stereotype.Service;
9
+import java.time.LocalDateTime;
10
+import java.util.*;
11
+
12
+@Slf4j
13
+@Service
14
+@RequiredArgsConstructor
15
+public class PatrolWorkOrderService {
16
+    private final PatrolWorkOrderMapper woMapper;
17
+
18
+    public PatrolWorkOrder create(Long taskId, String issueType, String description,
19
+            String photos, String severity) {
20
+        PatrolWorkOrder wo = new PatrolWorkOrder();
21
+        wo.setOrderNo("PWO-" + System.currentTimeMillis());
22
+        wo.setTaskId(taskId);
23
+        wo.setIssueType(issueType);
24
+        wo.setDescription(description);
25
+        wo.setPhotos(photos);
26
+        wo.setSeverity(severity != null ? severity : "medium");
27
+        wo.setStatus("pending");
28
+        wo.setCreatedAt(LocalDateTime.now());
29
+        wo.setUpdatedAt(LocalDateTime.now());
30
+        woMapper.insert(wo);
31
+        log.info("Work order created: {}", wo.getOrderNo());
32
+        return wo;
33
+    }
34
+
35
+    public Map<String, Object> list(String status, String severity, String keyword,
36
+            int page, int size) {
37
+        LambdaQueryWrapper<PatrolWorkOrder> w = new LambdaQueryWrapper<>();
38
+        if (status != null && !status.isEmpty()) w.eq(PatrolWorkOrder::getStatus, status);
39
+        if (severity != null && !severity.isEmpty()) w.eq(PatrolWorkOrder::getSeverity, severity);
40
+        if (keyword != null && !keyword.isEmpty()) {
41
+            w.and(q -> q.like(PatrolWorkOrder::getDescription, keyword)
42
+                .or().like(PatrolWorkOrder::getOrderNo, keyword));
43
+        }
44
+        w.orderByDesc(PatrolWorkOrder::getCreatedAt);
45
+
46
+        Long total = woMapper.selectCount(w);
47
+        w.last("LIMIT " + size + " OFFSET " + ((page - 1) * size));
48
+        List<PatrolWorkOrder> records = woMapper.selectList(w);
49
+
50
+        Map<String, Object> result = new LinkedHashMap<>();
51
+        result.put("total", total);
52
+        result.put("page", page);
53
+        result.put("size", size);
54
+        result.put("records", records);
55
+        return result;
56
+    }
57
+
58
+    public PatrolWorkOrder getDetail(Long id) {
59
+        return woMapper.selectById(id);
60
+    }
61
+
62
+    public void assign(Long id, Long assigneeId, String assigneeName) {
63
+        PatrolWorkOrder wo = woMapper.selectById(id);
64
+        if (wo == null) throw new RuntimeException("Work order not found: " + id);
65
+        wo.setAssigneeId(assigneeId);
66
+        wo.setAssigneeName(assigneeName);
67
+        wo.setStatus("assigned");
68
+        wo.setUpdatedAt(LocalDateTime.now());
69
+        woMapper.updateById(wo);
70
+        log.info("Work order {} assigned to {}", id, assigneeName);
71
+    }
72
+
73
+    public void startProcess(Long id) {
74
+        PatrolWorkOrder wo = woMapper.selectById(id);
75
+        if (wo == null) throw new RuntimeException("Work order not found: " + id);
76
+        wo.setStatus("in_progress");
77
+        wo.setUpdatedAt(LocalDateTime.now());
78
+        woMapper.updateById(wo);
79
+        log.info("Work order {} started", id);
80
+    }
81
+
82
+    public void resolve(Long id, String resolution) {
83
+        PatrolWorkOrder wo = woMapper.selectById(id);
84
+        if (wo == null) throw new RuntimeException("Work order not found: " + id);
85
+        wo.setStatus("resolved");
86
+        wo.setResolution(resolution);
87
+        wo.setResolvedAt(LocalDateTime.now());
88
+        wo.setUpdatedAt(LocalDateTime.now());
89
+        woMapper.updateById(wo);
90
+        log.info("Work order {} resolved", id);
91
+    }
92
+
93
+    public void close(Long id) {
94
+        PatrolWorkOrder wo = woMapper.selectById(id);
95
+        if (wo == null) throw new RuntimeException("Work order not found: " + id);
96
+        wo.setStatus("closed");
97
+        wo.setUpdatedAt(LocalDateTime.now());
98
+        woMapper.updateById(wo);
99
+        log.info("Work order {} closed", id);
100
+    }
101
+
102
+    public Map<String, Object> getStats() {
103
+        Map<String, Object> r = new LinkedHashMap<>();
104
+        Long total = woMapper.selectCount(new LambdaQueryWrapper<>());
105
+        r.put("total", total);
106
+
107
+        for (String s : List.of("pending", "assigned", "in_progress", "resolved", "closed")) {
108
+            LambdaQueryWrapper<PatrolWorkOrder> w = new LambdaQueryWrapper<PatrolWorkOrder>()
109
+                .eq(PatrolWorkOrder::getStatus, s);
110
+            r.put(s, woMapper.selectCount(w));
111
+        }
112
+        return r;
113
+    }
114
+}

+ 35
- 0
wm-patrol/src/main/resources/sql/V86__patrol_core.sql Näytä tiedosto

1
+CREATE TABLE IF NOT EXISTS pat_track_point (
2
+    id BIGSERIAL PRIMARY KEY, task_id BIGINT NOT NULL, worker_id BIGINT,
3
+    lng NUMERIC(10,6), lat NUMERIC(10,6), speed NUMERIC(8,2), accuracy NUMERIC(8,2),
4
+    address VARCHAR(200), timestamp TIMESTAMPTZ NOT NULL DEFAULT NOW()
5
+);
6
+CREATE INDEX IF NOT EXISTS idx_pat_track_task ON pat_track_point(task_id);
7
+
8
+CREATE TABLE IF NOT EXISTS pat_task (
9
+    id BIGSERIAL PRIMARY KEY, task_no VARCHAR(32) UNIQUE NOT NULL, task_name VARCHAR(100),
10
+    route_id BIGINT, worker_id BIGINT, worker_name VARCHAR(50), status VARCHAR(20) DEFAULT 'pending',
11
+    start_time TIMESTAMPTZ, end_time TIMESTAMPTZ, checkpoints INT DEFAULT 0,
12
+    completed_checkpoints INT DEFAULT 0, abnormal_count INT DEFAULT 0,
13
+    distance DOUBLE PRECISION DEFAULT 0, remark VARCHAR(500),
14
+    created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW()
15
+);
16
+CREATE INDEX IF NOT EXISTS idx_pat_task_status ON pat_task(status);
17
+CREATE INDEX IF NOT EXISTS idx_pat_task_worker ON pat_task(worker_id);
18
+
19
+CREATE TABLE IF NOT EXISTS pat_device (
20
+    id BIGSERIAL PRIMARY KEY, device_no VARCHAR(32) UNIQUE NOT NULL, device_name VARCHAR(100),
21
+    device_type VARCHAR(30), location VARCHAR(200), lng NUMERIC(10,6), lat NUMERIC(10,6),
22
+    status VARCHAR(20) DEFAULT 'normal', last_maintenance_date DATE, next_maintenance_date DATE,
23
+    remark VARCHAR(500), created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW()
24
+);
25
+CREATE INDEX IF NOT EXISTS idx_pat_device_type ON pat_device(device_type);
26
+CREATE INDEX IF NOT EXISTS idx_pat_device_status ON pat_device(status);
27
+
28
+CREATE TABLE IF NOT EXISTS pat_work_order (
29
+    id BIGSERIAL PRIMARY KEY, order_no VARCHAR(32) UNIQUE NOT NULL, task_id BIGINT,
30
+    issue_type VARCHAR(30), description TEXT, photos TEXT, severity VARCHAR(20) DEFAULT 'medium',
31
+    assignee_id BIGINT, assignee_name VARCHAR(50), status VARCHAR(20) DEFAULT 'pending',
32
+    resolution TEXT, resolved_at TIMESTAMPTZ, created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW()
33
+);
34
+CREATE INDEX IF NOT EXISTS idx_pat_wo_status ON pat_work_order(status);
35
+CREATE INDEX IF NOT EXISTS idx_pat_wo_task ON pat_work_order(task_id);

+ 27
- 0
wm-patrol/src/test/java/com/water/patrol/PatrolCoreTest.java Näytä tiedosto

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 org.springframework.jdbc.core.JdbcTemplate;
6
+import java.math.BigDecimal; import java.util.*;
7
+import static org.junit.jupiter.api.Assertions.*;
8
+import static org.mockito.Mockito.*;
9
+@ExtendWith(MockitoExtension.class)
10
+class PatrolCoreTest {
11
+    @Mock JdbcTemplate jdbc; @Mock PatrolTaskMapper taskMapper; @Mock PatrolTrackPointMapper trackMapper;
12
+    @Mock PatrolDeviceMapper deviceMapper; @Mock PatrolWorkOrderMapper woMapper;
13
+    @InjectMocks PatrolOverviewService overviewSvc; @InjectMocks PatrolLedgerService ledgerSvc;
14
+    @InjectMocks PatrolTrackService trackSvc; @InjectMocks PatrolDeviceService deviceSvc;
15
+    @InjectMocks PatrolWorkOrderService woSvc;
16
+
17
+    @Test void testOverview() { when(jdbc.queryForObject(anyString(),eq(Integer.class),any())).thenReturn(10); when(jdbc.queryForObject(anyString(),eq(Double.class),any())).thenReturn(5000.0); assertNotNull(overviewSvc.getOverview()); }
18
+    @Test void testCreateTask() { when(taskMapper.insert(any())).thenReturn(1); PatrolTask t=ledgerSvc.createTask("巡检",1L,100L,"张三"); assertTrue(t.getTaskNo().startsWith("PAT-")); assertEquals("pending",t.getStatus()); }
19
+    @Test void testStartTask() { PatrolTask t=new PatrolTask(); t.setId(1L); t.setStatus("pending"); when(taskMapper.selectById(1L)).thenReturn(t); when(taskMapper.updateById(any())).thenReturn(1); ledgerSvc.startTask(1L); assertEquals("in_progress",t.getStatus()); }
20
+    @Test void testRecordGps() { when(trackMapper.insert(any())).thenReturn(1); PatrolTrackPoint p=trackSvc.recordPoint(1L,100L,new BigDecimal("116.4"),new BigDecimal("39.9"),null,null,"test"); assertNotNull(p); assertEquals(1L,p.getTaskId()); }
21
+    @Test void testCreateDevice() { when(deviceMapper.insert(any())).thenReturn(1); PatrolDevice d=deviceSvc.create("DEV-001","阀门A","valve","A区",new BigDecimal("116"),new BigDecimal("39")); assertEquals("normal",d.getStatus()); }
22
+    @Test void testDeviceStatus() { PatrolDevice d=new PatrolDevice(); d.setId(1L); d.setStatus("normal"); when(deviceMapper.selectById(1L)).thenReturn(d); when(deviceMapper.updateById(any())).thenReturn(1); deviceSvc.updateStatus(1L,"fault"); assertEquals("fault",d.getStatus()); }
23
+    @Test void testCreateWO() { when(woMapper.insert(any())).thenReturn(1); PatrolWorkOrder wo=woSvc.create(1L,"leak","管道漏水",null,"high"); assertTrue(wo.getOrderNo().startsWith("PWO-")); assertEquals("high",wo.getSeverity()); }
24
+    @Test void testAssignWO() { PatrolWorkOrder wo=new PatrolWorkOrder(); wo.setId(1L); wo.setStatus("pending"); when(woMapper.selectById(1L)).thenReturn(wo); when(woMapper.updateById(any())).thenReturn(1); woSvc.assign(1L,200L,"李四"); assertEquals("assigned",wo.getStatus()); }
25
+    @Test void testResolveWO() { PatrolWorkOrder wo=new PatrolWorkOrder(); wo.setId(1L); wo.setStatus("in_progress"); when(woMapper.selectById(1L)).thenReturn(wo); when(woMapper.updateById(any())).thenReturn(1); woSvc.resolve(1L,"已修复"); assertEquals("resolved",wo.getStatus()); assertNotNull(wo.getResolvedAt()); }
26
+    @Test void testCloseWO() { PatrolWorkOrder wo=new PatrolWorkOrder(); wo.setId(1L); wo.setStatus("resolved"); when(woMapper.selectById(1L)).thenReturn(wo); when(woMapper.updateById(any())).thenReturn(1); woSvc.close(1L); assertEquals("closed",wo.getStatus()); }
27
+}