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 + 索引
This commit is contained in:
2026-06-15 12:50:41 +08:00
parent 9b11a3f7bf
commit f7e60cba23
20 changed files with 558 additions and 0 deletions
@@ -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<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)); }
@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)); }
@Operation(summary="详情") @GetMapping("/{id}") public R<PatrolDevice> detail(@PathVariable Long id) { return R.ok(svc.getDetail(id)); }
@Operation(summary="状态") @PutMapping("/{id}/status") public R<String> updateStatus(@PathVariable Long id, @RequestParam String status) { svc.updateStatus(id,status); return R.ok("OK"); }
@Operation(summary="维护") @PutMapping("/{id}/maintenance") public R<String> maintenance(@PathVariable Long id) { svc.recordMaintenance(id); return R.ok("OK"); }
@Operation(summary="统计") @GetMapping("/stats") public R<Map<String,Object>> stats() { return R.ok(svc.getStats()); }
@Operation(summary="删除") @DeleteMapping("/{id}") public R<String> delete(@PathVariable Long id) { svc.delete(id); return R.ok("OK"); }
}
@@ -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<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)); }
@Operation(summary="详情") @GetMapping("/{id}") public R<PatrolTask> detail(@PathVariable Long id) { return R.ok(svc.getDetail(id)); }
@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"))); }
@Operation(summary="开始") @PutMapping("/{id}/start") public R<String> start(@PathVariable Long id) { svc.startTask(id); return R.ok("OK"); }
@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"); }
@Operation(summary="统计") @GetMapping("/stats") public R<Map<String,Object>> stats() { return R.ok(svc.getStats()); }
}
@@ -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<Map<String,Object>> overview() { return R.ok(svc.getOverview()); }
@Operation(summary="月度统计") @GetMapping("/monthly") public R<Map<String,Object>> monthly(@RequestParam int year, @RequestParam int month) { return R.ok(svc.getMonthlyStats(year,month)); }
}
@@ -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<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)); }
@Operation(summary="轨迹") @GetMapping("/{taskId}") public R<List<PatrolTrackPoint>> getTrack(@PathVariable Long taskId) { return R.ok(svc.getTrack(taskId)); }
@Operation(summary="摘要") @GetMapping("/{taskId}/summary") public R<Map<String,Object>> summary(@PathVariable Long taskId) { return R.ok(svc.getTrackSummary(taskId)); }
@Operation(summary="导出") @GetMapping("/{taskId}/export") public R<List<Map<String,Object>>> export(@PathVariable Long taskId) { return R.ok(svc.exportTrack(taskId)); }
}
@@ -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<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"))); }
@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)); }
@Operation(summary="详情") @GetMapping("/{id}") public R<PatrolWorkOrder> detail(@PathVariable Long id) { return R.ok(svc.getDetail(id)); }
@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"); }
@Operation(summary="开始") @PutMapping("/{id}/start") public R<String> start(@PathVariable Long id) { svc.startProcess(id); return R.ok("OK"); }
@Operation(summary="解决") @PutMapping("/{id}/resolve") public R<String> resolve(@PathVariable Long id, @RequestParam String resolution) { svc.resolve(id,resolution); return R.ok("OK"); }
@Operation(summary="关闭") @PutMapping("/{id}/close") public R<String> close(@PathVariable Long id) { svc.close(id); return R.ok("OK"); }
@Operation(summary="统计") @GetMapping("/stats") public R<Map<String,Object>> stats() { return R.ok(svc.getStats()); }
}
@@ -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;
}
@@ -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;
}
@@ -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;
}
@@ -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;
}
@@ -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<PatrolDevice> {}
@@ -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<PatrolTask> {}
@@ -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<PatrolTrackPoint> {}
@@ -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<PatrolWorkOrder> {}
@@ -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<String, Object> list(String deviceType, String status, String keyword,
int page, int size) {
LambdaQueryWrapper<PatrolDevice> 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<PatrolDevice> records = deviceMapper.selectList(w);
Map<String, Object> 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<String, Object> getStats() {
Map<String, Object> r = new LinkedHashMap<>();
Long total = deviceMapper.selectCount(new LambdaQueryWrapper<>());
r.put("total", total);
LambdaQueryWrapper<PatrolDevice> normal = new LambdaQueryWrapper<PatrolDevice>().eq(PatrolDevice::getStatus, "normal");
r.put("normal", deviceMapper.selectCount(normal));
LambdaQueryWrapper<PatrolDevice> fault = new LambdaQueryWrapper<PatrolDevice>().eq(PatrolDevice::getStatus, "fault");
r.put("fault", deviceMapper.selectCount(fault));
LambdaQueryWrapper<PatrolDevice> offline = new LambdaQueryWrapper<PatrolDevice>().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);
}
}
@@ -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<String, Object> listTasks(String status, String workerName, String startDate, String endDate, int page, int size) {
LambdaQueryWrapper<PatrolTask> 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<PatrolTask> p = mapper.selectPage(new Page<>(page, size), w);
Map<String, Object> 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<String, Object> getStats() {
Map<String, Object> r = new LinkedHashMap<>();
r.put("total", mapper.selectCount(null));
r.put("completed", mapper.selectCount(new LambdaQueryWrapper<PatrolTask>().eq(PatrolTask::getStatus,"completed")));
r.put("inProgress", mapper.selectCount(new LambdaQueryWrapper<PatrolTask>().eq(PatrolTask::getStatus,"in_progress")));
r.put("pending", mapper.selectCount(new LambdaQueryWrapper<PatrolTask>().eq(PatrolTask::getStatus,"pending")));
return r;
}
}
@@ -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<String, Object> getOverview() {
LocalDate today = LocalDate.now();
Map<String, Object> 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<String, Object> getMonthlyStats(int year, int month) {
String s = String.format("%d-%02d-01", year, month), e = String.format("%d-%02d-31", year, month);
Map<String, Object> 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;
}
}
@@ -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<PatrolTrackPoint> getTrack(Long taskId) {
return mapper.selectList(new LambdaQueryWrapper<PatrolTrackPoint>().eq(PatrolTrackPoint::getTaskId, taskId).orderByAsc(PatrolTrackPoint::getTimestamp));
}
public Map<String, Object> getTrackSummary(Long taskId) {
List<PatrolTrackPoint> pts = getTrack(taskId);
Map<String, Object> 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<Map<String, Object>> exportTrack(Long taskId) {
List<Map<String, Object>> result = new ArrayList<>();
for (PatrolTrackPoint p : getTrack(taskId)) {
Map<String, Object> 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));
}
}
@@ -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<String, Object> list(String status, String severity, String keyword,
int page, int size) {
LambdaQueryWrapper<PatrolWorkOrder> 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<PatrolWorkOrder> records = woMapper.selectList(w);
Map<String, Object> 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<String, Object> getStats() {
Map<String, Object> 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<PatrolWorkOrder> w = new LambdaQueryWrapper<PatrolWorkOrder>()
.eq(PatrolWorkOrder::getStatus, s);
r.put(s, woMapper.selectCount(w));
}
return r;
}
}
@@ -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);
@@ -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()); }
}