From f7e60cba23453298b610167297815de5ccd78334 Mon Sep 17 00:00:00 2001 From: bot_dev2 Date: Mon, 15 Jun 2026 12:50:21 +0800 Subject: [PATCH] =?UTF-8?q?feat(wm-patrol):=20#86=20=E5=B7=A1=E6=A3=80?= =?UTF-8?q?=E7=AE=A1=E7=90=86=E6=A0=B8=E5=BF=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - PatrolOverviewService: 今日任务/完成率/异常数/在线巡检员/月度统计 - PatrolTrackService: GPS轨迹记录/回放/Haversine距离/导出 - PatrolLedgerService: 任务CRUD/分页查询/开始完成/统计 - PatrolDeviceService: 设备注册/状态/维护记录/分页/统计 - PatrolWorkOrderService: 工单创建/分配/处理/解决/关闭/统计 - 5个Controller共25+端点, 10个单元测试 - DDL: pat_track_point/pat_task/pat_device/pat_work_order + 索引 --- .../controller/PatrolDeviceController.java | 16 +++ .../controller/PatrolLedgerController.java | 15 +++ .../controller/PatrolOverviewController.java | 11 ++ .../controller/PatrolTrackController.java | 13 ++ .../patrol/controller/PatrolWoController.java | 17 +++ .../com/water/patrol/entity/PatrolDevice.java | 16 +++ .../com/water/patrol/entity/PatrolTask.java | 15 +++ .../water/patrol/entity/PatrolTrackPoint.java | 12 ++ .../water/patrol/entity/PatrolWorkOrder.java | 14 +++ .../patrol/mapper/PatrolDeviceMapper.java | 5 + .../water/patrol/mapper/PatrolTaskMapper.java | 5 + .../patrol/mapper/PatrolTrackPointMapper.java | 5 + .../patrol/mapper/PatrolWorkOrderMapper.java | 5 + .../patrol/service/PatrolDeviceService.java | 106 ++++++++++++++++ .../patrol/service/PatrolLedgerService.java | 50 ++++++++ .../patrol/service/PatrolOverviewService.java | 30 +++++ .../patrol/service/PatrolTrackService.java | 47 ++++++++ .../service/PatrolWorkOrderService.java | 114 ++++++++++++++++++ .../main/resources/sql/V86__patrol_core.sql | 35 ++++++ .../java/com/water/patrol/PatrolCoreTest.java | 27 +++++ 20 files changed, 558 insertions(+) create mode 100644 wm-patrol/src/main/java/com/water/patrol/controller/PatrolDeviceController.java create mode 100644 wm-patrol/src/main/java/com/water/patrol/controller/PatrolLedgerController.java create mode 100644 wm-patrol/src/main/java/com/water/patrol/controller/PatrolOverviewController.java create mode 100644 wm-patrol/src/main/java/com/water/patrol/controller/PatrolTrackController.java create mode 100644 wm-patrol/src/main/java/com/water/patrol/controller/PatrolWoController.java create mode 100644 wm-patrol/src/main/java/com/water/patrol/entity/PatrolDevice.java create mode 100644 wm-patrol/src/main/java/com/water/patrol/entity/PatrolTask.java create mode 100644 wm-patrol/src/main/java/com/water/patrol/entity/PatrolTrackPoint.java create mode 100644 wm-patrol/src/main/java/com/water/patrol/entity/PatrolWorkOrder.java create mode 100644 wm-patrol/src/main/java/com/water/patrol/mapper/PatrolDeviceMapper.java create mode 100644 wm-patrol/src/main/java/com/water/patrol/mapper/PatrolTaskMapper.java create mode 100644 wm-patrol/src/main/java/com/water/patrol/mapper/PatrolTrackPointMapper.java create mode 100644 wm-patrol/src/main/java/com/water/patrol/mapper/PatrolWorkOrderMapper.java create mode 100644 wm-patrol/src/main/java/com/water/patrol/service/PatrolDeviceService.java create mode 100644 wm-patrol/src/main/java/com/water/patrol/service/PatrolLedgerService.java create mode 100644 wm-patrol/src/main/java/com/water/patrol/service/PatrolOverviewService.java create mode 100644 wm-patrol/src/main/java/com/water/patrol/service/PatrolTrackService.java create mode 100644 wm-patrol/src/main/java/com/water/patrol/service/PatrolWorkOrderService.java create mode 100644 wm-patrol/src/main/resources/sql/V86__patrol_core.sql create mode 100644 wm-patrol/src/test/java/com/water/patrol/PatrolCoreTest.java diff --git a/wm-patrol/src/main/java/com/water/patrol/controller/PatrolDeviceController.java b/wm-patrol/src/main/java/com/water/patrol/controller/PatrolDeviceController.java new file mode 100644 index 00000000..ddacf33d --- /dev/null +++ b/wm-patrol/src/main/java/com/water/patrol/controller/PatrolDeviceController.java @@ -0,0 +1,16 @@ +package com.water.patrol.controller; +import com.water.common.core.result.R; import com.water.patrol.entity.PatrolDevice; import com.water.patrol.service.PatrolDeviceService; +import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.*; +import java.math.BigDecimal; import java.util.Map; +@Tag(name="设备台账") @RestController @RequestMapping("/api/patrol/device") @RequiredArgsConstructor +public class PatrolDeviceController { + private final PatrolDeviceService svc; + @Operation(summary="注册") @PostMapping public R 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)); } + @Operation(summary="列表") @GetMapping public R> 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)); } + @Operation(summary="详情") @GetMapping("/{id}") public R detail(@PathVariable Long id) { return R.ok(svc.getDetail(id)); } + @Operation(summary="状态") @PutMapping("/{id}/status") public R updateStatus(@PathVariable Long id, @RequestParam String status) { svc.updateStatus(id,status); return R.ok("OK"); } + @Operation(summary="维护") @PutMapping("/{id}/maintenance") public R maintenance(@PathVariable Long id) { svc.recordMaintenance(id); return R.ok("OK"); } + @Operation(summary="统计") @GetMapping("/stats") public R> stats() { return R.ok(svc.getStats()); } + @Operation(summary="删除") @DeleteMapping("/{id}") public R delete(@PathVariable Long id) { svc.delete(id); return R.ok("OK"); } +} diff --git a/wm-patrol/src/main/java/com/water/patrol/controller/PatrolLedgerController.java b/wm-patrol/src/main/java/com/water/patrol/controller/PatrolLedgerController.java new file mode 100644 index 00000000..351d0be6 --- /dev/null +++ b/wm-patrol/src/main/java/com/water/patrol/controller/PatrolLedgerController.java @@ -0,0 +1,15 @@ +package com.water.patrol.controller; +import com.water.common.core.result.R; import com.water.patrol.entity.PatrolTask; import com.water.patrol.service.PatrolLedgerService; +import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.*; +import java.util.Map; +@Tag(name="巡检台账") @RestController @RequestMapping("/api/patrol/ledger") @RequiredArgsConstructor +public class PatrolLedgerController { + private final PatrolLedgerService svc; + @Operation(summary="列表") @GetMapping public R> 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)); } + @Operation(summary="详情") @GetMapping("/{id}") public R detail(@PathVariable Long id) { return R.ok(svc.getDetail(id)); } + @Operation(summary="创建") @PostMapping public R create(@RequestBody Map req) { return R.ok(svc.createTask((String)req.get("taskName"),((Number)req.get("routeId")).longValue(),((Number)req.get("workerId")).longValue(),(String)req.get("workerName"))); } + @Operation(summary="开始") @PutMapping("/{id}/start") public R start(@PathVariable Long id) { svc.startTask(id); return R.ok("OK"); } + @Operation(summary="完成") @PutMapping("/{id}/complete") public R complete(@PathVariable Long id, @RequestParam Integer completedCheckpoints, @RequestParam Double distance) { svc.completeTask(id,completedCheckpoints,distance); return R.ok("OK"); } + @Operation(summary="统计") @GetMapping("/stats") public R> stats() { return R.ok(svc.getStats()); } +} diff --git a/wm-patrol/src/main/java/com/water/patrol/controller/PatrolOverviewController.java b/wm-patrol/src/main/java/com/water/patrol/controller/PatrolOverviewController.java new file mode 100644 index 00000000..56cbfa83 --- /dev/null +++ b/wm-patrol/src/main/java/com/water/patrol/controller/PatrolOverviewController.java @@ -0,0 +1,11 @@ +package com.water.patrol.controller; +import com.water.common.core.result.R; import com.water.patrol.service.PatrolOverviewService; +import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.*; +import java.util.Map; +@Tag(name="巡检总览") @RestController @RequestMapping("/api/patrol/overview") @RequiredArgsConstructor +public class PatrolOverviewController { + private final PatrolOverviewService svc; + @Operation(summary="总览") @GetMapping public R> overview() { return R.ok(svc.getOverview()); } + @Operation(summary="月度统计") @GetMapping("/monthly") public R> monthly(@RequestParam int year, @RequestParam int month) { return R.ok(svc.getMonthlyStats(year,month)); } +} diff --git a/wm-patrol/src/main/java/com/water/patrol/controller/PatrolTrackController.java b/wm-patrol/src/main/java/com/water/patrol/controller/PatrolTrackController.java new file mode 100644 index 00000000..36250826 --- /dev/null +++ b/wm-patrol/src/main/java/com/water/patrol/controller/PatrolTrackController.java @@ -0,0 +1,13 @@ +package com.water.patrol.controller; +import com.water.common.core.result.R; import com.water.patrol.entity.PatrolTrackPoint; import com.water.patrol.service.PatrolTrackService; +import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.*; +import java.math.BigDecimal; import java.util.*; +@Tag(name="巡检轨迹") @RestController @RequestMapping("/api/patrol/track") @RequiredArgsConstructor +public class PatrolTrackController { + private final PatrolTrackService svc; + @Operation(summary="记录GPS") @PostMapping public R 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)); } + @Operation(summary="轨迹") @GetMapping("/{taskId}") public R> getTrack(@PathVariable Long taskId) { return R.ok(svc.getTrack(taskId)); } + @Operation(summary="摘要") @GetMapping("/{taskId}/summary") public R> summary(@PathVariable Long taskId) { return R.ok(svc.getTrackSummary(taskId)); } + @Operation(summary="导出") @GetMapping("/{taskId}/export") public R>> export(@PathVariable Long taskId) { return R.ok(svc.exportTrack(taskId)); } +} diff --git a/wm-patrol/src/main/java/com/water/patrol/controller/PatrolWoController.java b/wm-patrol/src/main/java/com/water/patrol/controller/PatrolWoController.java new file mode 100644 index 00000000..3692a997 --- /dev/null +++ b/wm-patrol/src/main/java/com/water/patrol/controller/PatrolWoController.java @@ -0,0 +1,17 @@ +package com.water.patrol.controller; +import com.water.common.core.result.R; import com.water.patrol.entity.PatrolWorkOrder; import com.water.patrol.service.PatrolWorkOrderService; +import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.*; +import java.util.Map; +@Tag(name="巡检工单") @RestController @RequestMapping("/api/patrol/work-order") @RequiredArgsConstructor +public class PatrolWoController { + private final PatrolWorkOrderService svc; + @Operation(summary="创建") @PostMapping public R create(@RequestBody Map 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"))); } + @Operation(summary="列表") @GetMapping public R> 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)); } + @Operation(summary="详情") @GetMapping("/{id}") public R detail(@PathVariable Long id) { return R.ok(svc.getDetail(id)); } + @Operation(summary="分配") @PutMapping("/{id}/assign") public R assign(@PathVariable Long id, @RequestParam Long assigneeId, @RequestParam String assigneeName) { svc.assign(id,assigneeId,assigneeName); return R.ok("OK"); } + @Operation(summary="开始") @PutMapping("/{id}/start") public R start(@PathVariable Long id) { svc.startProcess(id); return R.ok("OK"); } + @Operation(summary="解决") @PutMapping("/{id}/resolve") public R resolve(@PathVariable Long id, @RequestParam String resolution) { svc.resolve(id,resolution); return R.ok("OK"); } + @Operation(summary="关闭") @PutMapping("/{id}/close") public R close(@PathVariable Long id) { svc.close(id); return R.ok("OK"); } + @Operation(summary="统计") @GetMapping("/stats") public R> stats() { return R.ok(svc.getStats()); } +} diff --git a/wm-patrol/src/main/java/com/water/patrol/entity/PatrolDevice.java b/wm-patrol/src/main/java/com/water/patrol/entity/PatrolDevice.java new file mode 100644 index 00000000..106b36d1 --- /dev/null +++ b/wm-patrol/src/main/java/com/water/patrol/entity/PatrolDevice.java @@ -0,0 +1,16 @@ +package com.water.patrol.entity; +import com.baomidou.mybatisplus.annotation.*; +import lombok.Data; +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalDateTime; +@Data @TableName("pat_device") +public class PatrolDevice { + @TableId(type = IdType.AUTO) private Long id; + private String deviceNo; private String deviceName; private String deviceType; + private String location; private BigDecimal lng; private BigDecimal lat; + private String status; private LocalDate lastMaintenanceDate; private LocalDate nextMaintenanceDate; + private String remark; + @TableField(fill = FieldFill.INSERT) private LocalDateTime createdAt; + @TableField(fill = FieldFill.INSERT_UPDATE) private LocalDateTime updatedAt; +} diff --git a/wm-patrol/src/main/java/com/water/patrol/entity/PatrolTask.java b/wm-patrol/src/main/java/com/water/patrol/entity/PatrolTask.java new file mode 100644 index 00000000..7e66981c --- /dev/null +++ b/wm-patrol/src/main/java/com/water/patrol/entity/PatrolTask.java @@ -0,0 +1,15 @@ +package com.water.patrol.entity; +import com.baomidou.mybatisplus.annotation.*; +import lombok.Data; +import java.time.LocalDateTime; +@Data @TableName("pat_task") +public class PatrolTask { + @TableId(type = IdType.AUTO) private Long id; + private String taskNo; private String taskName; private Long routeId; + private Long workerId; private String workerName; private String status; + private LocalDateTime startTime; private LocalDateTime endTime; + private Integer checkpoints; private Integer completedCheckpoints; private Integer abnormalCount; + private Double distance; private String remark; + @TableField(fill = FieldFill.INSERT) private LocalDateTime createdAt; + @TableField(fill = FieldFill.INSERT_UPDATE) private LocalDateTime updatedAt; +} diff --git a/wm-patrol/src/main/java/com/water/patrol/entity/PatrolTrackPoint.java b/wm-patrol/src/main/java/com/water/patrol/entity/PatrolTrackPoint.java new file mode 100644 index 00000000..ab0a37a9 --- /dev/null +++ b/wm-patrol/src/main/java/com/water/patrol/entity/PatrolTrackPoint.java @@ -0,0 +1,12 @@ +package com.water.patrol.entity; +import com.baomidou.mybatisplus.annotation.*; +import lombok.Data; +import java.math.BigDecimal; +import java.time.LocalDateTime; +@Data @TableName("pat_track_point") +public class PatrolTrackPoint { + @TableId(type = IdType.AUTO) private Long id; + private Long taskId; private Long workerId; + private BigDecimal lng; private BigDecimal lat; private BigDecimal speed; private BigDecimal accuracy; + private String address; private LocalDateTime timestamp; +} diff --git a/wm-patrol/src/main/java/com/water/patrol/entity/PatrolWorkOrder.java b/wm-patrol/src/main/java/com/water/patrol/entity/PatrolWorkOrder.java new file mode 100644 index 00000000..e91af4db --- /dev/null +++ b/wm-patrol/src/main/java/com/water/patrol/entity/PatrolWorkOrder.java @@ -0,0 +1,14 @@ +package com.water.patrol.entity; +import com.baomidou.mybatisplus.annotation.*; +import lombok.Data; +import java.time.LocalDateTime; +@Data @TableName("pat_work_order") +public class PatrolWorkOrder { + @TableId(type = IdType.AUTO) private Long id; + private String orderNo; private Long taskId; private String issueType; + private String description; private String photos; private String severity; + private Long assigneeId; private String assigneeName; private String status; + private String resolution; private LocalDateTime resolvedAt; + @TableField(fill = FieldFill.INSERT) private LocalDateTime createdAt; + @TableField(fill = FieldFill.INSERT_UPDATE) private LocalDateTime updatedAt; +} diff --git a/wm-patrol/src/main/java/com/water/patrol/mapper/PatrolDeviceMapper.java b/wm-patrol/src/main/java/com/water/patrol/mapper/PatrolDeviceMapper.java new file mode 100644 index 00000000..a91b8512 --- /dev/null +++ b/wm-patrol/src/main/java/com/water/patrol/mapper/PatrolDeviceMapper.java @@ -0,0 +1,5 @@ +package com.water.patrol.mapper; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.water.patrol.entity.PatrolDevice; +import org.apache.ibatis.annotations.Mapper; +@Mapper public interface PatrolDeviceMapper extends BaseMapper {} diff --git a/wm-patrol/src/main/java/com/water/patrol/mapper/PatrolTaskMapper.java b/wm-patrol/src/main/java/com/water/patrol/mapper/PatrolTaskMapper.java new file mode 100644 index 00000000..5675cc57 --- /dev/null +++ b/wm-patrol/src/main/java/com/water/patrol/mapper/PatrolTaskMapper.java @@ -0,0 +1,5 @@ +package com.water.patrol.mapper; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.water.patrol.entity.PatrolTask; +import org.apache.ibatis.annotations.Mapper; +@Mapper public interface PatrolTaskMapper extends BaseMapper {} diff --git a/wm-patrol/src/main/java/com/water/patrol/mapper/PatrolTrackPointMapper.java b/wm-patrol/src/main/java/com/water/patrol/mapper/PatrolTrackPointMapper.java new file mode 100644 index 00000000..cc98d106 --- /dev/null +++ b/wm-patrol/src/main/java/com/water/patrol/mapper/PatrolTrackPointMapper.java @@ -0,0 +1,5 @@ +package com.water.patrol.mapper; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.water.patrol.entity.PatrolTrackPoint; +import org.apache.ibatis.annotations.Mapper; +@Mapper public interface PatrolTrackPointMapper extends BaseMapper {} diff --git a/wm-patrol/src/main/java/com/water/patrol/mapper/PatrolWorkOrderMapper.java b/wm-patrol/src/main/java/com/water/patrol/mapper/PatrolWorkOrderMapper.java new file mode 100644 index 00000000..9000361c --- /dev/null +++ b/wm-patrol/src/main/java/com/water/patrol/mapper/PatrolWorkOrderMapper.java @@ -0,0 +1,5 @@ +package com.water.patrol.mapper; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.water.patrol.entity.PatrolWorkOrder; +import org.apache.ibatis.annotations.Mapper; +@Mapper public interface PatrolWorkOrderMapper extends BaseMapper {} diff --git a/wm-patrol/src/main/java/com/water/patrol/service/PatrolDeviceService.java b/wm-patrol/src/main/java/com/water/patrol/service/PatrolDeviceService.java new file mode 100644 index 00000000..65a3b466 --- /dev/null +++ b/wm-patrol/src/main/java/com/water/patrol/service/PatrolDeviceService.java @@ -0,0 +1,106 @@ +package com.water.patrol.service; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.water.patrol.entity.PatrolDevice; +import com.water.patrol.mapper.PatrolDeviceMapper; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.*; + +@Slf4j +@Service +@RequiredArgsConstructor +public class PatrolDeviceService { + private final PatrolDeviceMapper deviceMapper; + + public PatrolDevice create(String deviceNo, String deviceName, String deviceType, + String location, BigDecimal lng, BigDecimal lat) { + PatrolDevice d = new PatrolDevice(); + d.setDeviceNo(deviceNo); + d.setDeviceName(deviceName); + d.setDeviceType(deviceType); + d.setLocation(location); + d.setLng(lng); + d.setLat(lat); + d.setStatus("normal"); + d.setCreatedAt(LocalDateTime.now()); + d.setUpdatedAt(LocalDateTime.now()); + deviceMapper.insert(d); + log.info("Device created: {}", deviceNo); + return d; + } + + public Map list(String deviceType, String status, String keyword, + int page, int size) { + LambdaQueryWrapper w = new LambdaQueryWrapper<>(); + if (deviceType != null && !deviceType.isEmpty()) w.eq(PatrolDevice::getDeviceType, deviceType); + if (status != null && !status.isEmpty()) w.eq(PatrolDevice::getStatus, status); + if (keyword != null && !keyword.isEmpty()) { + w.and(q -> q.like(PatrolDevice::getDeviceName, keyword) + .or().like(PatrolDevice::getDeviceNo, keyword) + .or().like(PatrolDevice::getLocation, keyword)); + } + w.orderByDesc(PatrolDevice::getCreatedAt); + + Long total = deviceMapper.selectCount(w); + w.last("LIMIT " + size + " OFFSET " + ((page - 1) * size)); + List records = deviceMapper.selectList(w); + + Map result = new LinkedHashMap<>(); + result.put("total", total); + result.put("page", page); + result.put("size", size); + result.put("records", records); + return result; + } + + public PatrolDevice getDetail(Long id) { + return deviceMapper.selectById(id); + } + + public void updateStatus(Long id, String status) { + PatrolDevice d = deviceMapper.selectById(id); + if (d == null) throw new RuntimeException("Device not found: " + id); + d.setStatus(status); + d.setUpdatedAt(LocalDateTime.now()); + deviceMapper.updateById(d); + log.info("Device {} status -> {}", id, status); + } + + public void recordMaintenance(Long id) { + PatrolDevice d = deviceMapper.selectById(id); + if (d == null) throw new RuntimeException("Device not found: " + id); + d.setLastMaintenanceDate(LocalDate.now()); + d.setNextMaintenanceDate(LocalDate.now().plusMonths(6)); + d.setStatus("normal"); + d.setUpdatedAt(LocalDateTime.now()); + deviceMapper.updateById(d); + log.info("Maintenance recorded for device {}", id); + } + + public Map getStats() { + Map r = new LinkedHashMap<>(); + Long total = deviceMapper.selectCount(new LambdaQueryWrapper<>()); + r.put("total", total); + + LambdaQueryWrapper normal = new LambdaQueryWrapper().eq(PatrolDevice::getStatus, "normal"); + r.put("normal", deviceMapper.selectCount(normal)); + + LambdaQueryWrapper fault = new LambdaQueryWrapper().eq(PatrolDevice::getStatus, "fault"); + r.put("fault", deviceMapper.selectCount(fault)); + + LambdaQueryWrapper offline = new LambdaQueryWrapper().eq(PatrolDevice::getStatus, "offline"); + r.put("offline", deviceMapper.selectCount(offline)); + + return r; + } + + public void delete(Long id) { + deviceMapper.deleteById(id); + log.info("Device deleted: {}", id); + } +} diff --git a/wm-patrol/src/main/java/com/water/patrol/service/PatrolLedgerService.java b/wm-patrol/src/main/java/com/water/patrol/service/PatrolLedgerService.java new file mode 100644 index 00000000..05888c92 --- /dev/null +++ b/wm-patrol/src/main/java/com/water/patrol/service/PatrolLedgerService.java @@ -0,0 +1,50 @@ +package com.water.patrol.service; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.water.patrol.entity.PatrolTask; import com.water.patrol.mapper.PatrolTaskMapper; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import java.time.LocalDateTime; import java.util.*; +@Slf4j @Service @RequiredArgsConstructor +public class PatrolLedgerService { + private final PatrolTaskMapper mapper; + public Map listTasks(String status, String workerName, String startDate, String endDate, int page, int size) { + LambdaQueryWrapper w = new LambdaQueryWrapper<>(); + if (status != null && !status.isEmpty()) w.eq(PatrolTask::getStatus, status); + if (workerName != null && !workerName.isEmpty()) w.like(PatrolTask::getWorkerName, workerName); + if (startDate != null && !startDate.isEmpty()) w.ge(PatrolTask::getCreatedAt, startDate+"T00:00:00"); + if (endDate != null && !endDate.isEmpty()) w.le(PatrolTask::getCreatedAt, endDate+"T23:59:59"); + w.orderByDesc(PatrolTask::getCreatedAt); + Page p = mapper.selectPage(new Page<>(page, size), w); + Map r = new LinkedHashMap<>(); + r.put("records", p.getRecords()); r.put("total", p.getTotal()); + r.put("page", page); r.put("size", size); r.put("pages", p.getPages()); + return r; + } + public PatrolTask getDetail(Long id) { return mapper.selectById(id); } + public PatrolTask createTask(String taskName, Long routeId, Long workerId, String workerName) { + PatrolTask t = new PatrolTask(); t.setTaskNo("PAT-"+System.currentTimeMillis()); + t.setTaskName(taskName); t.setRouteId(routeId); t.setWorkerId(workerId); t.setWorkerName(workerName); + t.setStatus("pending"); t.setCheckpoints(0); t.setCompletedCheckpoints(0); + t.setAbnormalCount(0); t.setDistance(0.0); mapper.insert(t); return t; + } + public void startTask(Long id) { + PatrolTask t = mapper.selectById(id); + if (t == null) throw new RuntimeException("任务不存在: "+id); + t.setStatus("in_progress"); t.setStartTime(LocalDateTime.now()); mapper.updateById(t); + } + public void completeTask(Long id, Integer cc, Double dist) { + PatrolTask t = mapper.selectById(id); + if (t == null) throw new RuntimeException("任务不存在: "+id); + t.setStatus("completed"); t.setEndTime(LocalDateTime.now()); + t.setCompletedCheckpoints(cc); t.setDistance(dist); mapper.updateById(t); + } + public Map getStats() { + Map r = new LinkedHashMap<>(); + r.put("total", mapper.selectCount(null)); + r.put("completed", mapper.selectCount(new LambdaQueryWrapper().eq(PatrolTask::getStatus,"completed"))); + r.put("inProgress", mapper.selectCount(new LambdaQueryWrapper().eq(PatrolTask::getStatus,"in_progress"))); + r.put("pending", mapper.selectCount(new LambdaQueryWrapper().eq(PatrolTask::getStatus,"pending"))); + return r; + } +} diff --git a/wm-patrol/src/main/java/com/water/patrol/service/PatrolOverviewService.java b/wm-patrol/src/main/java/com/water/patrol/service/PatrolOverviewService.java new file mode 100644 index 00000000..f9487742 --- /dev/null +++ b/wm-patrol/src/main/java/com/water/patrol/service/PatrolOverviewService.java @@ -0,0 +1,30 @@ +package com.water.patrol.service; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Service; +import java.time.LocalDate; import java.util.*; +@Slf4j @Service @RequiredArgsConstructor +public class PatrolOverviewService { + private final JdbcTemplate jdbc; + public Map getOverview() { + LocalDate today = LocalDate.now(); + Map r = new LinkedHashMap<>(); + r.put("todayTasks", jdbc.queryForObject("SELECT COUNT(*) FROM pat_task WHERE DATE(created_at)=?", Integer.class, today)); + r.put("completedTasks", jdbc.queryForObject("SELECT COUNT(*) FROM pat_task WHERE DATE(created_at)=? AND status='completed'", Integer.class, today)); + int t = (Integer) r.get("todayTasks"), c = (Integer) r.get("completedTasks"); + r.put("completionRate", t > 0 ? Math.round(c * 100.0 / t) : 0); + r.put("abnormalCount", jdbc.queryForObject("SELECT COUNT(*) FROM pat_work_order WHERE DATE(created_at)=? AND status!='closed'", Integer.class, today)); + r.put("onlineWorkers", jdbc.queryForObject("SELECT COUNT(DISTINCT worker_id) FROM pat_task WHERE status='in_progress'", Integer.class)); + 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())); + r.put("pendingOrders", jdbc.queryForObject("SELECT COUNT(*) FROM pat_work_order WHERE status IN ('pending','assigned')", Integer.class)); + return r; + } + public Map getMonthlyStats(int year, int month) { + String s = String.format("%d-%02d-01", year, month), e = String.format("%d-%02d-31", year, month); + Map r = new LinkedHashMap<>(); + r.put("totalTasks", jdbc.queryForObject("SELECT COUNT(*) FROM pat_task WHERE created_at BETWEEN ?::date AND ?::date", Integer.class, s, e)); + r.put("completedTasks", jdbc.queryForObject("SELECT COUNT(*) FROM pat_task WHERE created_at BETWEEN ?::date AND ?::date AND status='completed'", Integer.class, s, e)); + r.put("totalDistance", jdbc.queryForObject("SELECT COALESCE(SUM(distance),0) FROM pat_task WHERE created_at BETWEEN ?::date AND ?::date", Double.class, s, e)); + r.put("totalAbnormals", jdbc.queryForObject("SELECT COUNT(*) FROM pat_work_order WHERE created_at BETWEEN ?::date AND ?::date", Integer.class, s, e)); + return r; + } +} diff --git a/wm-patrol/src/main/java/com/water/patrol/service/PatrolTrackService.java b/wm-patrol/src/main/java/com/water/patrol/service/PatrolTrackService.java new file mode 100644 index 00000000..3d9f84b2 --- /dev/null +++ b/wm-patrol/src/main/java/com/water/patrol/service/PatrolTrackService.java @@ -0,0 +1,47 @@ +package com.water.patrol.service; +import com.water.patrol.entity.PatrolTrackPoint; import com.water.patrol.mapper.PatrolTrackPointMapper; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import java.math.BigDecimal; import java.time.LocalDateTime; import java.util.*; +@Slf4j @Service @RequiredArgsConstructor +public class PatrolTrackService { + private final PatrolTrackPointMapper mapper; + public PatrolTrackPoint recordPoint(Long taskId, Long workerId, BigDecimal lng, BigDecimal lat, BigDecimal speed, BigDecimal accuracy, String address) { + PatrolTrackPoint p = new PatrolTrackPoint(); + p.setTaskId(taskId); p.setWorkerId(workerId); p.setLng(lng); p.setLat(lat); + p.setSpeed(speed); p.setAccuracy(accuracy); p.setAddress(address); p.setTimestamp(LocalDateTime.now()); + mapper.insert(p); return p; + } + public List getTrack(Long taskId) { + return mapper.selectList(new LambdaQueryWrapper().eq(PatrolTrackPoint::getTaskId, taskId).orderByAsc(PatrolTrackPoint::getTimestamp)); + } + public Map getTrackSummary(Long taskId) { + List pts = getTrack(taskId); + Map r = new LinkedHashMap<>(); + r.put("totalPoints", pts.size()); + if (!pts.isEmpty()) { + r.put("startTime", pts.get(0).getTimestamp()); r.put("endTime", pts.get(pts.size()-1).getTimestamp()); + double d = 0; for (int i = 1; i < pts.size(); i++) d += haversine(pts.get(i-1), pts.get(i)); + r.put("totalDistance", Math.round(d)); + } + return r; + } + public List> exportTrack(Long taskId) { + List> result = new ArrayList<>(); + for (PatrolTrackPoint p : getTrack(taskId)) { + Map m = new LinkedHashMap<>(); + m.put("lng", p.getLng()); m.put("lat", p.getLat()); m.put("speed", p.getSpeed()); + m.put("timestamp", p.getTimestamp()); m.put("address", p.getAddress()); + result.add(m); + } + return result; + } + private double haversine(PatrolTrackPoint a, PatrolTrackPoint b) { + double R=6371000, dLat=Math.toRadians(b.getLat().doubleValue()-a.getLat().doubleValue()), + dLng=Math.toRadians(b.getLng().doubleValue()-a.getLng().doubleValue()), + la1=Math.toRadians(a.getLat().doubleValue()), la2=Math.toRadians(b.getLat().doubleValue()); + double x=Math.sin(dLat/2)*Math.sin(dLat/2)+Math.cos(la1)*Math.cos(la2)*Math.sin(dLng/2)*Math.sin(dLng/2); + return R*2*Math.atan2(Math.sqrt(x),Math.sqrt(1-x)); + } +} diff --git a/wm-patrol/src/main/java/com/water/patrol/service/PatrolWorkOrderService.java b/wm-patrol/src/main/java/com/water/patrol/service/PatrolWorkOrderService.java new file mode 100644 index 00000000..dd4cae09 --- /dev/null +++ b/wm-patrol/src/main/java/com/water/patrol/service/PatrolWorkOrderService.java @@ -0,0 +1,114 @@ +package com.water.patrol.service; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.water.patrol.entity.PatrolWorkOrder; +import com.water.patrol.mapper.PatrolWorkOrderMapper; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import java.time.LocalDateTime; +import java.util.*; + +@Slf4j +@Service +@RequiredArgsConstructor +public class PatrolWorkOrderService { + private final PatrolWorkOrderMapper woMapper; + + public PatrolWorkOrder create(Long taskId, String issueType, String description, + String photos, String severity) { + PatrolWorkOrder wo = new PatrolWorkOrder(); + wo.setOrderNo("PWO-" + System.currentTimeMillis()); + wo.setTaskId(taskId); + wo.setIssueType(issueType); + wo.setDescription(description); + wo.setPhotos(photos); + wo.setSeverity(severity != null ? severity : "medium"); + wo.setStatus("pending"); + wo.setCreatedAt(LocalDateTime.now()); + wo.setUpdatedAt(LocalDateTime.now()); + woMapper.insert(wo); + log.info("Work order created: {}", wo.getOrderNo()); + return wo; + } + + public Map list(String status, String severity, String keyword, + int page, int size) { + LambdaQueryWrapper w = new LambdaQueryWrapper<>(); + if (status != null && !status.isEmpty()) w.eq(PatrolWorkOrder::getStatus, status); + if (severity != null && !severity.isEmpty()) w.eq(PatrolWorkOrder::getSeverity, severity); + if (keyword != null && !keyword.isEmpty()) { + w.and(q -> q.like(PatrolWorkOrder::getDescription, keyword) + .or().like(PatrolWorkOrder::getOrderNo, keyword)); + } + w.orderByDesc(PatrolWorkOrder::getCreatedAt); + + Long total = woMapper.selectCount(w); + w.last("LIMIT " + size + " OFFSET " + ((page - 1) * size)); + List records = woMapper.selectList(w); + + Map result = new LinkedHashMap<>(); + result.put("total", total); + result.put("page", page); + result.put("size", size); + result.put("records", records); + return result; + } + + public PatrolWorkOrder getDetail(Long id) { + return woMapper.selectById(id); + } + + public void assign(Long id, Long assigneeId, String assigneeName) { + PatrolWorkOrder wo = woMapper.selectById(id); + if (wo == null) throw new RuntimeException("Work order not found: " + id); + wo.setAssigneeId(assigneeId); + wo.setAssigneeName(assigneeName); + wo.setStatus("assigned"); + wo.setUpdatedAt(LocalDateTime.now()); + woMapper.updateById(wo); + log.info("Work order {} assigned to {}", id, assigneeName); + } + + public void startProcess(Long id) { + PatrolWorkOrder wo = woMapper.selectById(id); + if (wo == null) throw new RuntimeException("Work order not found: " + id); + wo.setStatus("in_progress"); + wo.setUpdatedAt(LocalDateTime.now()); + woMapper.updateById(wo); + log.info("Work order {} started", id); + } + + public void resolve(Long id, String resolution) { + PatrolWorkOrder wo = woMapper.selectById(id); + if (wo == null) throw new RuntimeException("Work order not found: " + id); + wo.setStatus("resolved"); + wo.setResolution(resolution); + wo.setResolvedAt(LocalDateTime.now()); + wo.setUpdatedAt(LocalDateTime.now()); + woMapper.updateById(wo); + log.info("Work order {} resolved", id); + } + + public void close(Long id) { + PatrolWorkOrder wo = woMapper.selectById(id); + if (wo == null) throw new RuntimeException("Work order not found: " + id); + wo.setStatus("closed"); + wo.setUpdatedAt(LocalDateTime.now()); + woMapper.updateById(wo); + log.info("Work order {} closed", id); + } + + public Map getStats() { + Map r = new LinkedHashMap<>(); + Long total = woMapper.selectCount(new LambdaQueryWrapper<>()); + r.put("total", total); + + for (String s : List.of("pending", "assigned", "in_progress", "resolved", "closed")) { + LambdaQueryWrapper w = new LambdaQueryWrapper() + .eq(PatrolWorkOrder::getStatus, s); + r.put(s, woMapper.selectCount(w)); + } + return r; + } +} diff --git a/wm-patrol/src/main/resources/sql/V86__patrol_core.sql b/wm-patrol/src/main/resources/sql/V86__patrol_core.sql new file mode 100644 index 00000000..d67a310e --- /dev/null +++ b/wm-patrol/src/main/resources/sql/V86__patrol_core.sql @@ -0,0 +1,35 @@ +CREATE TABLE IF NOT EXISTS pat_track_point ( + id BIGSERIAL PRIMARY KEY, task_id BIGINT NOT NULL, worker_id BIGINT, + lng NUMERIC(10,6), lat NUMERIC(10,6), speed NUMERIC(8,2), accuracy NUMERIC(8,2), + address VARCHAR(200), timestamp TIMESTAMPTZ NOT NULL DEFAULT NOW() +); +CREATE INDEX IF NOT EXISTS idx_pat_track_task ON pat_track_point(task_id); + +CREATE TABLE IF NOT EXISTS pat_task ( + id BIGSERIAL PRIMARY KEY, task_no VARCHAR(32) UNIQUE NOT NULL, task_name VARCHAR(100), + route_id BIGINT, worker_id BIGINT, worker_name VARCHAR(50), status VARCHAR(20) DEFAULT 'pending', + start_time TIMESTAMPTZ, end_time TIMESTAMPTZ, checkpoints INT DEFAULT 0, + completed_checkpoints INT DEFAULT 0, abnormal_count INT DEFAULT 0, + distance DOUBLE PRECISION DEFAULT 0, remark VARCHAR(500), + created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW() +); +CREATE INDEX IF NOT EXISTS idx_pat_task_status ON pat_task(status); +CREATE INDEX IF NOT EXISTS idx_pat_task_worker ON pat_task(worker_id); + +CREATE TABLE IF NOT EXISTS pat_device ( + id BIGSERIAL PRIMARY KEY, device_no VARCHAR(32) UNIQUE NOT NULL, device_name VARCHAR(100), + device_type VARCHAR(30), location VARCHAR(200), lng NUMERIC(10,6), lat NUMERIC(10,6), + status VARCHAR(20) DEFAULT 'normal', last_maintenance_date DATE, next_maintenance_date DATE, + remark VARCHAR(500), created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW() +); +CREATE INDEX IF NOT EXISTS idx_pat_device_type ON pat_device(device_type); +CREATE INDEX IF NOT EXISTS idx_pat_device_status ON pat_device(status); + +CREATE TABLE IF NOT EXISTS pat_work_order ( + id BIGSERIAL PRIMARY KEY, order_no VARCHAR(32) UNIQUE NOT NULL, task_id BIGINT, + issue_type VARCHAR(30), description TEXT, photos TEXT, severity VARCHAR(20) DEFAULT 'medium', + assignee_id BIGINT, assignee_name VARCHAR(50), status VARCHAR(20) DEFAULT 'pending', + resolution TEXT, resolved_at TIMESTAMPTZ, created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW() +); +CREATE INDEX IF NOT EXISTS idx_pat_wo_status ON pat_work_order(status); +CREATE INDEX IF NOT EXISTS idx_pat_wo_task ON pat_work_order(task_id); diff --git a/wm-patrol/src/test/java/com/water/patrol/PatrolCoreTest.java b/wm-patrol/src/test/java/com/water/patrol/PatrolCoreTest.java new file mode 100644 index 00000000..3b42700c --- /dev/null +++ b/wm-patrol/src/test/java/com/water/patrol/PatrolCoreTest.java @@ -0,0 +1,27 @@ +package com.water.patrol; +import com.water.patrol.entity.*; import com.water.patrol.mapper.*; import com.water.patrol.service.*; +import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.*; import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.jdbc.core.JdbcTemplate; +import java.math.BigDecimal; import java.util.*; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; +@ExtendWith(MockitoExtension.class) +class PatrolCoreTest { + @Mock JdbcTemplate jdbc; @Mock PatrolTaskMapper taskMapper; @Mock PatrolTrackPointMapper trackMapper; + @Mock PatrolDeviceMapper deviceMapper; @Mock PatrolWorkOrderMapper woMapper; + @InjectMocks PatrolOverviewService overviewSvc; @InjectMocks PatrolLedgerService ledgerSvc; + @InjectMocks PatrolTrackService trackSvc; @InjectMocks PatrolDeviceService deviceSvc; + @InjectMocks PatrolWorkOrderService woSvc; + + @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()); } + @Test void testCreateTask() { when(taskMapper.insert(any())).thenReturn(1); PatrolTask t=ledgerSvc.createTask("巡检",1L,100L,"张三"); assertTrue(t.getTaskNo().startsWith("PAT-")); assertEquals("pending",t.getStatus()); } + @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()); } + @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()); } + @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()); } + @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()); } + @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()); } + @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()); } + @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()); } + @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()); } +}