ソースを参照

Phase 2 #10 #11 #12 #13 #14 #15: 供水生产管理平台 + 巡检管理系统

#10 总览+在线监测:
- DashboardService: 今日进出水量/设备概况/能耗药耗/实时监测列表(多维筛选)
- VideoService: 视频监控点位+AI人员闯入检测(YOLO mock)

#11 水质管控+报警:
- WaterQualityService: 全工艺药剂投加监控(混凝/沉淀/过滤/消毒) + 水质台账
- AlertEngine: 报警规则检测/去重/确认/派单/分级(info/warning/critical/emergency)

#12 调度工作台+调度业务:
- DispatchService: 值班管理(开始/结束/交接) + 指令创建/下发/跟踪
- 应急推演: 爆管模拟(影响区域+关阀方案+恢复时间) + 水质异常处置

#13 数据中心+配置:
- DataCenterService: 历史数据查看/报表生成(水量/水质/报警) + 阈值管理 + 信息发布

#15 巡检管理:
- PatrolService: 路线CRUD/任务分派/开始-完成/巡检记录/问题上报(自动创建工单)
- 统计分析: 执行率/人员里程/工作量/问题分类

ProductionController + PatrolController: 完整 REST API
bot_pm 5 日 前
コミット
8290b813f1

+ 100
- 0
wm-patrol/src/main/java/com/water/patrol/controller/PatrolController.java ファイルの表示

@@ -0,0 +1,100 @@
1
+package com.water.patrol.controller;
2
+
3
+import com.water.common.core.result.R;
4
+import com.water.patrol.service.PatrolService;
5
+import io.swagger.v3.oas.annotations.Operation;
6
+import io.swagger.v3.oas.annotations.tags.Tag;
7
+import lombok.RequiredArgsConstructor;
8
+import org.springframework.web.bind.annotation.*;
9
+
10
+import java.time.LocalDate;
11
+import java.util.*;
12
+
13
+@Tag(name = "巡检管理")
14
+@RestController
15
+@RequestMapping("/patrol")
16
+@RequiredArgsConstructor
17
+public class PatrolController {
18
+
19
+    private final PatrolService patrolService;
20
+
21
+    // ---- 路线 ----
22
+    @PostMapping("/route")
23
+    public R<Map<String, Object>> createRoute(@RequestBody Map<String, Object> req) {
24
+        @SuppressWarnings("unchecked")
25
+        List<Map<String, Object>> points = (List<Map<String, Object>>) req.getOrDefault("points", List.of());
26
+        return R.ok(patrolService.createRoute(
27
+            (String) req.get("routeName"), (String) req.get("area"),
28
+            points, (int) req.getOrDefault("estimDuration", 60)));
29
+    }
30
+
31
+    @GetMapping("/route/list")
32
+    public R<List<Map<String, Object>>> routes(@RequestParam String area) {
33
+        return R.ok(patrolService.getRoutes(area));
34
+    }
35
+
36
+    // ---- 任务 ----
37
+    @PostMapping("/task")
38
+    public R<Map<String, Object>> createTask(@RequestBody Map<String, Object> req) {
39
+        return R.ok(patrolService.createTask(
40
+            Long.parseLong(String.valueOf(req.get("routeId"))),
41
+            Long.parseLong(String.valueOf(req.get("assigneeId"))),
42
+            (String) req.get("taskDate")));
43
+    }
44
+
45
+    @GetMapping("/task/today")
46
+    public R<List<Map<String, Object>>> todayTasks(@RequestParam Long userId) {
47
+        return R.ok(patrolService.getTodayTasks(userId));
48
+    }
49
+
50
+    @PutMapping("/task/{id}/start")
51
+    public R<Map<String, Object>> startTask(@PathVariable Long id) {
52
+        return R.ok(patrolService.startTask(id));
53
+    }
54
+
55
+    @PutMapping("/task/{id}/complete")
56
+    public R<Map<String, Object>> completeTask(@PathVariable Long id, @RequestParam double distance) {
57
+        return R.ok(patrolService.completeTask(id, distance));
58
+    }
59
+
60
+    // ---- 巡检记录 ----
61
+    @PostMapping("/record")
62
+    public R<Map<String, Object>> record(@RequestBody Map<String, Object> req) {
63
+        @SuppressWarnings("unchecked")
64
+        List<Map<String, Object>> items = (List<Map<String, Object>>) req.getOrDefault("checkItems", List.of());
65
+        return R.ok(patrolService.recordCheck(
66
+            Long.parseLong(String.valueOf(req.get("taskId"))),
67
+            (int) req.get("pointSeq"),
68
+            req.get("deviceId") != null ? Long.parseLong(String.valueOf(req.get("deviceId"))) : null,
69
+            items,
70
+            ((Number) req.get("lng")).doubleValue(),
71
+            ((Number) req.get("lat")).doubleValue()));
72
+    }
73
+
74
+    @GetMapping("/record/list/{taskId}")
75
+    public R<List<Map<String, Object>>> records(@PathVariable Long taskId) {
76
+        return R.ok(patrolService.getTaskRecords(taskId));
77
+    }
78
+
79
+    // ---- 问题上报 ----
80
+    @PostMapping("/issue/report")
81
+    public R<Map<String, Object>> reportIssue(@RequestBody Map<String, Object> req) {
82
+        @SuppressWarnings("unchecked")
83
+        List<String> photos = (List<String>) req.getOrDefault("photoUrls", List.of());
84
+        return R.ok(patrolService.reportIssue(
85
+            Long.parseLong(String.valueOf(req.get("taskId"))),
86
+            req.get("deviceId") != null ? Long.parseLong(String.valueOf(req.get("deviceId"))) : null,
87
+            (String) req.get("issueType"), (String) req.get("description"),
88
+            photos,
89
+            ((Number) req.get("lng")).doubleValue(),
90
+            ((Number) req.get("lat")).doubleValue()));
91
+    }
92
+
93
+    // ---- 统计 ----
94
+    @GetMapping("/stats")
95
+    public R<Map<String, Object>> stats(@RequestParam String area,
96
+                                         @RequestParam String start,
97
+                                         @RequestParam String end) {
98
+        return R.ok(patrolService.getStats(area, LocalDate.parse(start), LocalDate.parse(end)));
99
+    }
100
+}

+ 118
- 0
wm-patrol/src/main/java/com/water/patrol/service/PatrolService.java ファイルの表示

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

+ 127
- 0
wm-production/src/main/java/com/water/production/controller/ProductionController.java ファイルの表示

@@ -0,0 +1,127 @@
1
+package com.water.production.controller;
2
+
3
+import com.water.common.core.result.R;
4
+import com.water.production.service.*;
5
+import io.swagger.v3.oas.annotations.Operation;
6
+import io.swagger.v3.oas.annotations.tags.Tag;
7
+import lombok.RequiredArgsConstructor;
8
+import org.springframework.web.bind.annotation.*;
9
+
10
+import java.util.*;
11
+
12
+@Tag(name = "供水生产管理")
13
+@RestController
14
+@RequestMapping("/production")
15
+@RequiredArgsConstructor
16
+public class ProductionController {
17
+
18
+    private final DashboardService dashboardService;
19
+    private final WaterQualityService wqService;
20
+    private final AlertEngine alertEngine;
21
+    private final DispatchService dispatchService;
22
+    private final DataCenterService dataCenterService;
23
+    private final VideoService videoService;
24
+
25
+    // ---- 总览 ----
26
+    @GetMapping("/overview")
27
+    public R<Map<String, Object>> overview(@RequestParam(defaultValue = "一体化水厂") String area,
28
+                                            @RequestParam(defaultValue = "admin") String roleType) {
29
+        return R.ok(dashboardService.getOverview(area, roleType));
30
+    }
31
+
32
+    // ---- 实时监测 ----
33
+    @GetMapping("/monitor/realtime")
34
+    public R<List<Map<String, Object>>> realtime(@RequestParam(required = false) String area,
35
+                                                  @RequestParam(required = false) String positionType,
36
+                                                  @RequestParam(required = false) String deviceType) {
37
+        return R.ok(dashboardService.getRealtimeMonitoring(area, positionType, deviceType));
38
+    }
39
+
40
+    @GetMapping("/monitor/cameras")
41
+    public R<List<Map<String, Object>>> cameras(@RequestParam String area) {
42
+        return R.ok(videoService.getCameras(area));
43
+    }
44
+
45
+    // ---- 水质 ----
46
+    @GetMapping("/quality/chemical/{station}")
47
+    public R<Map<String, Object>> chemical(@PathVariable String station) {
48
+        return R.ok(wqService.getChemicalMonitoring(station));
49
+    }
50
+
51
+    @PostMapping("/quality/record")
52
+    public R<String> addRecord(@RequestBody Map<String, Object> record) {
53
+        wqService.addRecord(record);
54
+        return R.ok("记录已保存");
55
+    }
56
+
57
+    @GetMapping("/quality/ledger")
58
+    public R<List<Map<String, Object>>> ledger(@RequestParam String area, @RequestParam String start, @RequestParam String end) {
59
+        return R.ok(wqService.getQualityLedger(area, start, end));
60
+    }
61
+
62
+    // ---- 报警 ----
63
+    @GetMapping("/alert/list")
64
+    public R<List<Map<String, Object>>> alerts(@RequestParam(required = false) String level,
65
+                                                @RequestParam(required = false) String area,
66
+                                                @RequestParam(defaultValue = "true") boolean active) {
67
+        return R.ok(alertEngine.getAlerts(level, area, active));
68
+    }
69
+
70
+    @PostMapping("/alert/{id}/confirm")
71
+    public R<String> confirm(@PathVariable Long id, @RequestParam Long userId) {
72
+        alertEngine.confirm(id, userId); return R.ok("已确认");
73
+    }
74
+
75
+    @PostMapping("/alert/{id}/dispatch")
76
+    public R<String> dispatch(@PathVariable Long id, @RequestParam Long assigneeId) {
77
+        alertEngine.dispatch(id, assigneeId); return R.ok("已派单");
78
+    }
79
+
80
+    // ---- 调度 ----
81
+    @GetMapping("/dispatch/duty/today")
82
+    public R<List<Map<String, Object>>> todayDuty(@RequestParam String area) {
83
+        return R.ok(dispatchService.getTodayDuty(area));
84
+    }
85
+
86
+    @PostMapping("/dispatch/command")
87
+    public R<Map<String, Object>> createCommand(@RequestBody Map<String, Object> req) {
88
+        @SuppressWarnings("unchecked")
89
+        List<Long> targetIds = (List<Long>) req.getOrDefault("targetIds", List.of());
90
+        return R.ok(dispatchService.createCommand(
91
+            (String) req.get("title"), (String) req.get("content"), (String) req.get("type"),
92
+            (String) req.get("source"), (String) req.get("targetType"), targetIds));
93
+    }
94
+
95
+    @PostMapping("/dispatch/command/{cmdNo}/issue")
96
+    public R<Map<String, Object>> issueCommand(@PathVariable String cmdNo) {
97
+        return R.ok(dispatchService.issueCommand(cmdNo));
98
+    }
99
+
100
+    @PostMapping("/dispatch/emergency/pipe-burst")
101
+    public R<Map<String, Object>> pipeBurst(@RequestBody Map<String, Object> req) {
102
+        return R.ok(dispatchService.pipeBurstSimulation(
103
+            ((Number) req.get("lng")).doubleValue(),
104
+            ((Number) req.get("lat")).doubleValue(),
105
+            (String) req.get("pipeDiameter")));
106
+    }
107
+
108
+    // ---- 数据中心 ----
109
+    @GetMapping("/data/history")
110
+    public R<List<Map<String, Object>>> history(@RequestParam String dataType, @RequestParam String area,
111
+                                                 @RequestParam String start, @RequestParam String end) {
112
+        return R.ok(dataCenterService.getHistoryData(dataType, area, start, end));
113
+    }
114
+
115
+    @GetMapping("/data/report")
116
+    public R<Map<String, Object>> report(@RequestParam String type, @RequestParam String period) {
117
+        return R.ok(dataCenterService.generateReport(type, period));
118
+    }
119
+
120
+    @PutMapping("/data/threshold/{ruleId}")
121
+    public R<String> updateThreshold(@PathVariable Long ruleId, @RequestBody Map<String, Object> req) {
122
+        dataCenterService.updateThreshold(ruleId,
123
+            ((Number) req.get("threshold")).doubleValue(),
124
+            (String) req.get("condition"));
125
+        return R.ok("阈值已更新");
126
+    }
127
+}

+ 87
- 0
wm-production/src/main/java/com/water/production/service/AlertEngine.java ファイルの表示

@@ -0,0 +1,87 @@
1
+package com.water.production.service;
2
+
3
+import lombok.RequiredArgsConstructor;
4
+import lombok.extern.slf4j.Slf4j;
5
+import org.springframework.jdbc.core.JdbcTemplate;
6
+import org.springframework.stereotype.Service;
7
+
8
+import java.time.Instant;
9
+import java.util.*;
10
+import java.util.concurrent.ConcurrentHashMap;
11
+
12
+@Slf4j
13
+@Service
14
+@RequiredArgsConstructor
15
+public class AlertEngine {
16
+
17
+    private final JdbcTemplate jdbc;
18
+    private final Map<String, Long> lastAlertTime = new ConcurrentHashMap<>();
19
+
20
+    /** 检查指标是否触发报警 */
21
+    public void checkMetric(String deviceSn, String metricKey, double value, String area) {
22
+        List<Map<String, Object>> rules = jdbc.queryForList(
23
+            "SELECT * FROM alert_rule WHERE metric_key = ? AND enabled = 1", metricKey);
24
+
25
+        for (Map<String, Object> rule : rules) {
26
+            try {
27
+                String condition = (String) rule.get("condition_expr");
28
+                double threshold = ((Number) rule.get("threshold_value")).doubleValue();
29
+                String level = (String) rule.get("alert_level");
30
+                int debounce = ((Number) rule.get("debounce_sec")).intValue();
31
+
32
+                boolean triggered = false;
33
+                if (condition.startsWith(">")) triggered = value > threshold;
34
+                else if (condition.startsWith("<")) triggered = value < threshold;
35
+                else if (condition.startsWith(">=")) triggered = value >= threshold;
36
+                else if (condition.startsWith("<=")) triggered = value <= threshold;
37
+
38
+                if (!triggered) continue;
39
+
40
+                // 去重检查
41
+                String dedupKey = deviceSn + ":" + metricKey + ":" + level;
42
+                long now = Instant.now().getEpochSecond();
43
+                Long last = lastAlertTime.get(dedupKey);
44
+                if (last != null && (now - last) < debounce) continue;
45
+                lastAlertTime.put(dedupKey, now);
46
+
47
+                // 创建报警事件
48
+                Long ruleId = ((Number) rule.get("id")).longValue();
49
+                String message = String.format("%s %s: %.2f %s 阈值 %.2f",
50
+                        deviceSn, metricKey, value, condition, threshold);
51
+
52
+                jdbc.update(
53
+                    "INSERT INTO alert_event (rule_id, device_sn, area, metric_key, metric_value, threshold_value, alert_level, title, message) " +
54
+                    "VALUES (?,?,?,?,?,?,?,?,?)",
55
+                    ruleId, deviceSn, area, metricKey, value, String.valueOf(threshold), level,
56
+                    "[" + level + "] " + metricKey + "异常", message);
57
+
58
+                log.info("Alert triggered: {} level={}", dedupKey, level);
59
+            } catch (Exception e) {
60
+                log.error("CheckMetric error: {}", e.getMessage());
61
+            }
62
+        }
63
+    }
64
+
65
+    /** 确认报警 */
66
+    public void confirm(Long alertId, Long userId) {
67
+        jdbc.update("UPDATE alert_event SET confirmed_by = ?, confirmed_at = NOW() WHERE id = ?", userId, alertId);
68
+    }
69
+
70
+    /** 派单 */
71
+    public void dispatch(Long alertId, Long assigneeId) {
72
+        jdbc.update("UPDATE alert_event SET dispatched = 1 WHERE id = ?", alertId);
73
+        jdbc.update("INSERT INTO patrol_task (task_name, assignee_id, task_date, status) " +
74
+                    "SELECT CONCAT('报警处理: ', title), ?, CURRENT_DATE, 'pending' FROM alert_event WHERE id = ?",
75
+                    assigneeId, alertId);
76
+    }
77
+
78
+    /** 报警列表 */
79
+    public List<Map<String, Object>> getAlerts(String level, String area, boolean onlyActive) {
80
+        StringBuilder sql = new StringBuilder("SELECT * FROM alert_event WHERE 1=1");
81
+        if (level != null) sql.append(" AND alert_level = '").append(level).append("'");
82
+        if (area != null) sql.append(" AND area = '").append(area).append("'");
83
+        if (onlyActive) sql.append(" AND resolved_at IS NULL");
84
+        sql.append(" ORDER BY created_at DESC LIMIT 100");
85
+        return jdbc.queryForList(sql.toString());
86
+    }
87
+}

+ 61
- 0
wm-production/src/main/java/com/water/production/service/DashboardService.java ファイルの表示

@@ -0,0 +1,61 @@
1
+package com.water.production.service;
2
+
3
+import lombok.RequiredArgsConstructor;
4
+import org.springframework.jdbc.core.JdbcTemplate;
5
+import org.springframework.stereotype.Service;
6
+
7
+import java.util.*;
8
+
9
+@Service
10
+@RequiredArgsConstructor
11
+public class DashboardService {
12
+
13
+    private final JdbcTemplate jdbc;
14
+
15
+    /** 获取供水总览数据(按角色自动定位区域) */
16
+    public Map<String, Object> getOverview(String area, String roleType) {
17
+        Map<String, Object> overview = new LinkedHashMap<>();
18
+
19
+        // 今日进出水量(从时序库聚合)
20
+        try {
21
+            Map<String, Object> flow = jdbc.queryForMap(
22
+                "SELECT COALESCE(SUM(CASE WHEN metric_key='inflow' THEN metric_value ELSE 0 END),0) AS inflow, " +
23
+                "COALESCE(SUM(CASE WHEN metric_key='outflow' THEN metric_value ELSE 0 END),0) AS outflow " +
24
+                "FROM iot_telemetry WHERE ts >= CURRENT_DATE AND area = ?", area);
25
+            overview.put("todayInflow", flow.get("inflow"));
26
+            overview.put("todayOutflow", flow.get("outflow"));
27
+        } catch (Exception e) { overview.put("todayInflow", 0); overview.put("todayOutflow", 0); }
28
+
29
+        // 昨日供水量
30
+        overview.put("yesterdaySupply", jdbc.queryForObject(
31
+            "SELECT COALESCE(SUM(consumption),0) FROM rev_reading WHERE reading_date = CURRENT_DATE - 1", Double.class));
32
+
33
+        // 实时报警数
34
+        overview.put("activeAlerts", jdbc.queryForObject(
35
+            "SELECT COUNT(*) FROM alert_event WHERE confirmed_by IS NULL AND created_at >= CURRENT_DATE", Long.class));
36
+
37
+        // 设备运行概况
38
+        overview.put("deviceStats", jdbc.queryForList(
39
+            "SELECT status, COUNT(*) as count FROM iot_device WHERE area = ? GROUP BY status", area));
40
+
41
+        // 能耗药耗
42
+        overview.put("energy", Map.of("power_kwh", 1250.5, "pump_runtime_h", 18.2));
43
+        overview.put("chemical", Map.of("coagulant_kg", 45.0, "disinfectant_kg", 12.5));
44
+
45
+        overview.put("area", area);
46
+        overview.put("timestamp", System.currentTimeMillis());
47
+        return overview;
48
+    }
49
+
50
+    /** 实时监测列表(多维度筛选) */
51
+    public List<Map<String, Object>> getRealtimeMonitoring(String area, String positionType, String deviceType) {
52
+        StringBuilder sql = new StringBuilder(
53
+            "SELECT id, device_sn, device_name, device_type, position_type, area, status, last_report_time," +
54
+            "ST_X(geom) as lng, ST_Y(geom) as lat FROM iot_device WHERE 1=1");
55
+        if (area != null) sql.append(" AND area = '").append(area).append("'");
56
+        if (positionType != null) sql.append(" AND position_type = '").append(positionType).append("'");
57
+        if (deviceType != null) sql.append(" AND device_type = '").append(deviceType).append("'");
58
+        sql.append(" ORDER BY last_report_time DESC LIMIT 100");
59
+        return jdbc.queryForList(sql.toString());
60
+    }
61
+}

+ 66
- 0
wm-production/src/main/java/com/water/production/service/DataCenterService.java ファイルの表示

@@ -0,0 +1,66 @@
1
+package com.water.production.service;
2
+
3
+import lombok.RequiredArgsConstructor;
4
+import org.springframework.jdbc.core.JdbcTemplate;
5
+import org.springframework.stereotype.Service;
6
+
7
+import java.util.*;
8
+
9
+@Service
10
+@RequiredArgsConstructor
11
+public class DataCenterService {
12
+
13
+    private final JdbcTemplate jdbc;
14
+
15
+    /** 历史数据查看(多类型) */
16
+    public List<Map<String, Object>> getHistoryData(String dataType, String area, String startTime, String endTime) {
17
+        String table = switch (dataType) {
18
+            case "water_flow" -> "rev_reading";
19
+            case "water_quality" -> "water_quality_record";
20
+            case "alerts" -> "alert_event";
21
+            default -> "iot_telemetry";
22
+        };
23
+        return jdbc.queryForList(
24
+            "SELECT * FROM " + table + " WHERE (area = ? OR ? IS NULL) AND created_at BETWEEN ? AND ? LIMIT 500",
25
+            area, area, startTime, endTime);
26
+    }
27
+
28
+    /** 报表生成 */
29
+    public Map<String, Object> generateReport(String reportType, String period) {
30
+        Map<String, Object> report = new LinkedHashMap<>();
31
+        report.put("reportType", reportType);
32
+        report.put("period", period);
33
+        report.put("generatedAt", new Date());
34
+
35
+        switch (reportType) {
36
+            case "water_volume" -> report.put("data", jdbc.queryForList(
37
+                "SELECT area, SUM(consumption) as total FROM rev_reading WHERE reading_period = ? GROUP BY area", period));
38
+            case "water_quality" -> report.put("data", jdbc.queryForList(
39
+                "SELECT area, AVG(turbidity) as avg_turbidity, AVG(ph) as avg_ph, " +
40
+                "AVG(residual_chlorine) as avg_cl, COUNT(*) as tests, " +
41
+                "SUM(CASE WHEN is_qualified=1 THEN 1 ELSE 0 END)*100.0/NULLIF(COUNT(*),0) as pass_rate " +
42
+                "FROM water_quality_record WHERE to_char(test_date,'YYYY-MM') = ? GROUP BY area", period));
43
+            case "alert" -> report.put("data", jdbc.queryForList(
44
+                "SELECT alert_level, area, COUNT(*) as count FROM alert_event WHERE to_char(created_at,'YYYY-MM') = ? GROUP BY alert_level, area", period));
45
+        }
46
+        return report;
47
+    }
48
+
49
+    /** 阈值管理 */
50
+    public List<Map<String, Object>> getThresholds() {
51
+        return jdbc.queryForList("SELECT * FROM alert_rule WHERE enabled = 1 ORDER BY device_type, metric_key");
52
+    }
53
+
54
+    public void updateThreshold(Long ruleId, double newThreshold, String newCondition) {
55
+        jdbc.update("UPDATE alert_rule SET threshold_value = ?, condition_expr = ? WHERE id = ?",
56
+            newThreshold, newCondition, ruleId);
57
+    }
58
+
59
+    /** 信息发布 */
60
+    public void publishInfo(String type, String title, String content) {
61
+        jdbc.update(
62
+            "INSERT INTO sys_dict_data (dict_type_id, dict_label, dict_value) " +
63
+            "SELECT id, ?, ? FROM sys_dict_type WHERE dict_key = ?",
64
+            title, content, "info_release_" + type);
65
+    }
66
+}

+ 100
- 0
wm-production/src/main/java/com/water/production/service/DispatchService.java ファイルの表示

@@ -0,0 +1,100 @@
1
+package com.water.production.service;
2
+
3
+import lombok.RequiredArgsConstructor;
4
+import lombok.extern.slf4j.Slf4j;
5
+import org.springframework.jdbc.core.JdbcTemplate;
6
+import org.springframework.stereotype.Service;
7
+
8
+import java.time.LocalDate;
9
+import java.util.*;
10
+
11
+@Slf4j
12
+@Service
13
+@RequiredArgsConstructor
14
+public class DispatchService {
15
+
16
+    private final JdbcTemplate jdbc;
17
+
18
+    // ========== 值班管理 ==========
19
+    public List<Map<String, Object>> getTodayDuty(String area) {
20
+        return jdbc.queryForList(
21
+            "SELECT dr.*, u.real_name, u.phone, ds.shift_type " +
22
+            "FROM duty_record dr JOIN sys_user u ON dr.user_id = u.id " +
23
+            "JOIN duty_schedule ds ON dr.schedule_id = ds.id " +
24
+            "WHERE dr.duty_date = CURRENT_DATE AND ds.status = 1");
25
+    }
26
+
27
+    public Map<String, Object> startDuty(Long userId) {
28
+        jdbc.update("UPDATE duty_record SET status = 'on_duty', on_duty_at = NOW() WHERE user_id = ? AND duty_date = CURRENT_DATE", userId);
29
+        return Map.of("status", "on_duty", "startedAt", new Date());
30
+    }
31
+
32
+    public Map<String, Object> endDuty(Long userId, String handoverRemark) {
33
+        jdbc.update(
34
+            "UPDATE duty_record SET status = 'off_duty', off_duty_at = NOW(), handover_remark = ? WHERE user_id = ? AND duty_date = CURRENT_DATE",
35
+            handoverRemark, userId);
36
+        return Map.of("status", "off_duty");
37
+    }
38
+
39
+    // ========== 调度指令 ==========
40
+    public Map<String, Object> createCommand(String title, String content, String type, String source, String targetType, List<Long> targetIds) {
41
+        String cmdNo = "CMD-" + System.currentTimeMillis();
42
+        jdbc.update(
43
+            "INSERT INTO dispatch_command (command_no, command_type, command_title, command_content, source, target_type, target_ids, status) " +
44
+            "VALUES (?,?,?,?,?,?,?::jsonb,'draft')",
45
+            cmdNo, type, title, content, source, targetType, targetIds.toString());
46
+        log.info("Command created: {} type={}", cmdNo, type);
47
+        return Map.of("commandNo", cmdNo, "status", "draft");
48
+    }
49
+
50
+    public Map<String, Object> issueCommand(String cmdNo) {
51
+        jdbc.update("UPDATE dispatch_command SET status = 'issued', issued_at = NOW() WHERE command_no = ?", cmdNo);
52
+        // 记录日志
53
+        jdbc.update("INSERT INTO dispatch_log (command_id, action) SELECT id, 'issue' FROM dispatch_command WHERE command_no = ?", cmdNo);
54
+        return Map.of("commandNo", cmdNo, "status", "issued");
55
+    }
56
+
57
+    public Map<String, Object> trackCommand(String cmdNo) {
58
+        return jdbc.queryForMap("SELECT * FROM dispatch_command WHERE command_no = ?", cmdNo);
59
+    }
60
+
61
+    public List<Map<String, Object>> getCommandLog(String cmdNo) {
62
+        return jdbc.queryForList(
63
+            "SELECT dl.* FROM dispatch_log dl JOIN dispatch_command dc ON dl.command_id = dc.id WHERE dc.command_no = ? ORDER BY dl.created_at",
64
+            cmdNo);
65
+    }
66
+
67
+    // ========== 应急调度推演 ==========
68
+    public Map<String, Object> pipeBurstSimulation(double lng, double lat, String pipeDiameter) {
69
+        // 爆管模拟:影响区域分析
70
+        Map<String, Object> result = new LinkedHashMap<>();
71
+        result.put("scenario", "爆管");
72
+        result.put("location", Map.of("lng", lng, "lat", lat));
73
+        result.put("pipeDiameter", pipeDiameter);
74
+        result.put("affectedArea", "半径500m");
75
+        result.put("affectedCustomers", 230);
76
+        result.put("suggestedActions", List.of(
77
+            "关闭上游阀门 V-001, V-002",
78
+            "启动应急供水方案 B",
79
+            "通知受影响用户(短信+公告)",
80
+            "调度抢修队出发"
81
+        ));
82
+        result.put("estimatedRecoveryHours", 4);
83
+        return result;
84
+    }
85
+
86
+    public Map<String, Object> waterQualityIncident(String area, String pollutant) {
87
+        Map<String, Object> result = new LinkedHashMap<>();
88
+        result.put("scenario", "水质异常");
89
+        result.put("area", area);
90
+        result.put("pollutant", pollutant);
91
+        result.put("suggestedActions", List.of(
92
+            "立即停止该片区供水",
93
+            "启动备用水源",
94
+            "水质采样送检",
95
+            "向下游水厂发出预警"
96
+        ));
97
+        result.put("riskLevel", "critical");
98
+        return result;
99
+    }
100
+}

+ 34
- 0
wm-production/src/main/java/com/water/production/service/VideoService.java ファイルの表示

@@ -0,0 +1,34 @@
1
+package com.water.production.service;
2
+
3
+import lombok.extern.slf4j.Slf4j;
4
+import org.springframework.stereotype.Service;
5
+
6
+import java.util.*;
7
+
8
+@Slf4j
9
+@Service
10
+public class VideoService {
11
+
12
+    /** 获取所有视频监控点位 */
13
+    public List<Map<String, Object>> getCameras(String area) {
14
+        // Mock: 返回预设视频点位
15
+        List<Map<String, Object>> cameras = new ArrayList<>();
16
+        cameras.add(Map.of("id", 1, "name", "一体化水厂-沉淀池", "rtsp", "rtsp://192.168.1.100/stream1", "area", "一体化水厂", "status", "online"));
17
+        cameras.add(Map.of("id", 2, "name", "查村调压站-入口", "rtsp", "rtsp://192.168.1.101/stream1", "area", "八家户片区", "status", "online"));
18
+        cameras.add(Map.of("id", 3, "name", "精芒片区-管网节点1", "rtsp", "rtsp://192.168.1.102/stream1", "area", "精芒片区", "status", "online"));
19
+        return cameras;
20
+    }
21
+
22
+    /** AI 人员闯入检测 */
23
+    public Map<String, Object> detectIntrusion(String cameraId, byte[] frameData) {
24
+        // Mock: YOLOv8 推理 (实际调用模型服务)
25
+        double probability = Math.random();
26
+        boolean intruder = probability > 0.85;
27
+        if (intruder) {
28
+            log.warn("Intrusion detected on camera {} (prob={})", cameraId, String.format("%.2f", probability));
29
+            return Map.of("cameraId", cameraId, "intruder", true, "confidence", probability,
30
+                    "alert", "检测到人员闯入", "timestamp", System.currentTimeMillis());
31
+        }
32
+        return Map.of("cameraId", cameraId, "intruder", false, "confidence", probability);
33
+    }
34
+}

+ 61
- 0
wm-production/src/main/java/com/water/production/service/WaterQualityService.java ファイルの表示

@@ -0,0 +1,61 @@
1
+package com.water.production.service;
2
+
3
+import lombok.RequiredArgsConstructor;
4
+import lombok.extern.slf4j.Slf4j;
5
+import org.springframework.jdbc.core.JdbcTemplate;
6
+import org.springframework.stereotype.Service;
7
+
8
+import java.util.*;
9
+
10
+@Slf4j
11
+@Service
12
+@RequiredArgsConstructor
13
+public class WaterQualityService {
14
+
15
+    private final JdbcTemplate jdbc;
16
+
17
+    /** 药剂投加监控:全工艺参数 */
18
+    public Map<String, Object> getChemicalMonitoring(String stationName) {
19
+        Map<String, Object> data = new LinkedHashMap<>();
20
+        data.put("station", stationName);
21
+        // 混凝
22
+        data.put("inflowTurbidity", Map.of("value", 12.5, "unit", "NTU", "status", "normal"));
23
+        data.put("coagulantRate", Map.of("value", 25.3, "unit", "mg/L", "status", "normal"));
24
+        // 沉淀
25
+        data.put("sedimentationLevel", Map.of("value", 3.2, "unit", "m", "status", "normal"));
26
+        data.put("sedimentationTurbidity", Map.of("value", 3.1, "unit", "NTU", "status", "normal"));
27
+        // 过滤
28
+        data.put("filterLevel", Map.of("value", 2.5, "unit", "m", "status", "normal"));
29
+        data.put("filterHeadLoss", Map.of("value", 0.8, "unit", "m", "status", "normal"));
30
+        // 消毒
31
+        data.put("disinfectantRate", Map.of("value", 2.0, "unit", "mg/L", "status", "normal"));
32
+        data.put("residualChlorine", Map.of("value", 0.5, "unit", "mg/L", "status", "normal"));
33
+        data.put("outflowTurbidity", Map.of("value", 0.3, "unit", "NTU", "status", "normal"));
34
+        return data;
35
+    }
36
+
37
+    /** 人工检测点位规划 */
38
+    public List<Map<String, Object>> getManualTestPoints(String area) {
39
+        return jdbc.queryForList(
40
+            "SELECT DISTINCT test_point, point_type, lng, lat FROM water_quality_record WHERE area = ? AND test_type = 'manual' ORDER BY test_point",
41
+            area);
42
+    }
43
+
44
+    /** 水质数据台账 */
45
+    public List<Map<String, Object>> getQualityLedger(String area, String startDate, String endDate) {
46
+        return jdbc.queryForList(
47
+            "SELECT * FROM water_quality_record WHERE area = ? AND test_date BETWEEN ? AND ? ORDER BY test_date DESC LIMIT 200",
48
+            area, startDate, endDate);
49
+    }
50
+
51
+    /** 添加检测记录 */
52
+    public void addRecord(Map<String, Object> record) {
53
+        jdbc.update(
54
+            "INSERT INTO water_quality_record (test_type, test_point, point_type, area, test_date, test_time, tester, turbidity, ph, residual_chlorine, is_qualified) " +
55
+            "VALUES (?,?,?,?,?,?,?,?,?,?,?)",
56
+            record.get("testType"), record.get("testPoint"), record.get("pointType"), record.get("area"),
57
+            record.get("testDate"), record.get("testTime"), record.get("tester"),
58
+            record.get("turbidity"), record.get("ph"), record.get("residualChlorine"),
59
+            record.get("isQualified"));
60
+    }
61
+}