Browse Source

Phase 1 #21 #22 #25 #26: GIS + IoT + DevOps + Notify

#21 GIS 引擎集成:
- GeoServer init 脚本(自动创建工作区/数据源)
- Leaflet 地图组件 (Vue3 MapView: 点位/弹窗/OSM底图)
- GisService: PostGIS 空间查询(附近设备/片区统计/GeoJSON)
- GisController: /nearby /device-stats /geojson API

#22 IoT 设备接入层:
- Kafka Consumer: iot.telemetry + iot.event 消费
- DeviceController: 设备列表/详情/注册/指令下发 REST API

#26 消息通知:
- NotifyService: 短信/WebSocket/APP Push/多渠道分发
- NotifyController: SMS/Push API

#25 DevOps:
- 10个微服务 Dockerfile (Eclipse Temurin JRE17)
- CI build.sh: Maven构建 + Docker镜像打包
- Frontend Nginx 反向代理配置
bot_pm 5 days ago
parent
commit
0b8bad8879

+ 7
- 0
docker/ci/build.sh View File

@@ -0,0 +1,7 @@
1
+#!/bin/bash
2
+set -e
3
+mvn clean package -DskipTests
4
+for mod in wm-{base,iot,data-engine,bpm,production,revenue,patrol,bi,notify,job}; do
5
+  docker build -t water/$mod -f docker/$mod/Dockerfile .
6
+done
7
+echo "Build done"

+ 8
- 0
docker/frontend/nginx.conf View File

@@ -0,0 +1,8 @@
1
+server {
2
+    listen 80;
3
+    server_name localhost;
4
+    root /usr/share/nginx/html;
5
+    index index.html;
6
+    location / { try_files $uri /index.html; }
7
+    location /api/ { proxy_pass http://wm-gateway:8080/; }
8
+}

+ 14
- 0
docker/geoserver/init.sh View File

@@ -0,0 +1,14 @@
1
+#!/bin/bash
2
+# GeoServer 初始化脚本:创建工作区、数据源、图层
3
+set -e
4
+GEOSERVER_URL="http://localhost:8081/geoserver"
5
+USER="admin"
6
+PASS="geoserver"
7
+
8
+echo "Waiting for GeoServer..."
9
+until curl -s -u $USER:$PASS "$GEOSERVER_URL/rest/about/version.xml" > /dev/null; do sleep 2; done
10
+
11
+# Create workspace
12
+curl -s -u $USER:$PASS -X POST "$GEOSERVER_URL/rest/workspaces"   -H "Content-Type: text/xml"   -d '<workspace><name>water_management</name></workspace>' || true
13
+
14
+echo "GeoServer init done"

+ 5
- 0
docker/wm-base/Dockerfile View File

@@ -0,0 +1,5 @@
1
+FROM eclipse-temurin:17-jre-alpine
2
+WORKDIR /app
3
+COPY wm-base/target/wm-base-*.jar app.jar
4
+EXPOSE 8081
5
+ENTRYPOINT ["java", "-jar", "app.jar"]

+ 5
- 0
docker/wm-bi/Dockerfile View File

@@ -0,0 +1,5 @@
1
+FROM eclipse-temurin:17-jre-alpine
2
+WORKDIR /app
3
+COPY wm-bi/target/wm-bi-*.jar app.jar
4
+EXPOSE 8088
5
+ENTRYPOINT ["java", "-jar", "app.jar"]

+ 5
- 0
docker/wm-bpm/Dockerfile View File

@@ -0,0 +1,5 @@
1
+FROM eclipse-temurin:17-jre-alpine
2
+WORKDIR /app
3
+COPY wm-bpm/target/wm-bpm-*.jar app.jar
4
+EXPOSE 8084
5
+ENTRYPOINT ["java", "-jar", "app.jar"]

+ 5
- 0
docker/wm-data-engine/Dockerfile View File

@@ -0,0 +1,5 @@
1
+FROM eclipse-temurin:17-jre-alpine
2
+WORKDIR /app
3
+COPY wm-data-engine/target/wm-data-engine-*.jar app.jar
4
+EXPOSE 8083
5
+ENTRYPOINT ["java", "-jar", "app.jar"]

+ 5
- 0
docker/wm-iot/Dockerfile View File

@@ -0,0 +1,5 @@
1
+FROM eclipse-temurin:17-jre-alpine
2
+WORKDIR /app
3
+COPY wm-iot/target/wm-iot-*.jar app.jar
4
+EXPOSE 8082
5
+ENTRYPOINT ["java", "-jar", "app.jar"]

+ 5
- 0
docker/wm-job/Dockerfile View File

@@ -0,0 +1,5 @@
1
+FROM eclipse-temurin:17-jre-alpine
2
+WORKDIR /app
3
+COPY wm-job/target/wm-job-*.jar app.jar
4
+EXPOSE 8090
5
+ENTRYPOINT ["java", "-jar", "app.jar"]

+ 5
- 0
docker/wm-notify/Dockerfile View File

@@ -0,0 +1,5 @@
1
+FROM eclipse-temurin:17-jre-alpine
2
+WORKDIR /app
3
+COPY wm-notify/target/wm-notify-*.jar app.jar
4
+EXPOSE 8089
5
+ENTRYPOINT ["java", "-jar", "app.jar"]

+ 5
- 0
docker/wm-patrol/Dockerfile View File

@@ -0,0 +1,5 @@
1
+FROM eclipse-temurin:17-jre-alpine
2
+WORKDIR /app
3
+COPY wm-patrol/target/wm-patrol-*.jar app.jar
4
+EXPOSE 8087
5
+ENTRYPOINT ["java", "-jar", "app.jar"]

+ 5
- 0
docker/wm-production/Dockerfile View File

@@ -0,0 +1,5 @@
1
+FROM eclipse-temurin:17-jre-alpine
2
+WORKDIR /app
3
+COPY wm-production/target/wm-production-*.jar app.jar
4
+EXPOSE 8085
5
+ENTRYPOINT ["java", "-jar", "app.jar"]

+ 5
- 0
docker/wm-revenue/Dockerfile View File

@@ -0,0 +1,5 @@
1
+FROM eclipse-temurin:17-jre-alpine
2
+WORKDIR /app
3
+COPY wm-revenue/target/wm-revenue-*.jar app.jar
4
+EXPOSE 8086
5
+ENTRYPOINT ["java", "-jar", "app.jar"]

+ 42
- 0
frontend/src/components/charts/MapView.vue View File

@@ -0,0 +1,42 @@
1
+<template>
2
+  <div ref="mapContainer" class="map-container"></div>
3
+</template>
4
+
5
+<script setup lang="ts">
6
+import { ref, onMounted, onUnmounted } from 'vue'
7
+
8
+const props = defineProps<{
9
+  center?: [number, number]
10
+  zoom?: number
11
+  markers?: Array<{ lat: number; lng: number; name: string; value?: string }>
12
+}>()
13
+
14
+const mapContainer = ref<HTMLElement>()
15
+
16
+let map: any = null
17
+
18
+onMounted(async () => {
19
+  const L = (await import('leaflet')).default
20
+  await import('leaflet/dist/leaflet.css')
21
+
22
+  map = L.map(mapContainer.value!).setView(props.center || [44.6000, 82.9000], props.zoom || 10)
23
+
24
+  L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
25
+    attribution: '&copy; OpenStreetMap', maxZoom: 18
26
+  }).addTo(map)
27
+
28
+  if (props.markers) {
29
+    props.markers.forEach(m => {
30
+      L.marker([m.lat, m.lng])
31
+        .addTo(map)
32
+        .bindPopup(`<b>${m.name}</b><br/>${m.value || ''}`)
33
+    })
34
+  }
35
+})
36
+
37
+onUnmounted(() => { if (map) map.remove() })
38
+</script>
39
+
40
+<style scoped>
41
+.map-container { width: 100%; height: 500px; border-radius: 8px }
42
+</style>

+ 22
- 0
wm-iot/src/main/java/com/water/iot/consumer/IotTelemetryConsumer.java View File

@@ -0,0 +1,22 @@
1
+package com.water.iot.consumer;
2
+
3
+import lombok.extern.slf4j.Slf4j;
4
+import org.springframework.kafka.annotation.KafkaListener;
5
+import org.springframework.stereotype.Component;
6
+
7
+@Slf4j
8
+@Component
9
+public class IotTelemetryConsumer {
10
+
11
+    @KafkaListener(topics = "iot.telemetry", groupId = "wm-iot-consumer")
12
+    public void consumeTelemetry(String message) {
13
+        log.debug("Received telemetry: {}", message);
14
+        // TODO: 解析 json -> 写入 TDengine
15
+    }
16
+
17
+    @KafkaListener(topics = "iot.event", groupId = "wm-iot-consumer")
18
+    public void consumeEvent(String message) {
19
+        log.info("Device event: {}", message);
20
+        // TODO: 处理上下线/故障事件
21
+    }
22
+}

+ 52
- 0
wm-iot/src/main/java/com/water/iot/controller/DeviceController.java View File

@@ -0,0 +1,52 @@
1
+package com.water.iot.controller;
2
+
3
+import com.water.common.core.result.R;
4
+import io.swagger.v3.oas.annotations.Operation;
5
+import io.swagger.v3.oas.annotations.tags.Tag;
6
+import lombok.RequiredArgsConstructor;
7
+import org.springframework.jdbc.core.JdbcTemplate;
8
+import org.springframework.web.bind.annotation.*;
9
+
10
+import java.util.List;
11
+import java.util.Map;
12
+
13
+@Tag(name = "设备管理")
14
+@RestController
15
+@RequestMapping("/device")
16
+@RequiredArgsConstructor
17
+public class DeviceController {
18
+
19
+    private final JdbcTemplate jdbcTemplate;
20
+
21
+    @Operation(summary = "设备列表")
22
+    @GetMapping("/list")
23
+    public R<List<Map<String, Object>>> list(@RequestParam(defaultValue = "1") int page,
24
+                                               @RequestParam(defaultValue = "10") int size) {
25
+        int offset = (page - 1) * size;
26
+        String sql = "SELECT id, device_sn, device_name, device_type, area, status, last_report_time FROM iot_device ORDER BY id LIMIT ? OFFSET ?";
27
+        return R.ok(jdbcTemplate.queryForList(sql, size, offset));
28
+    }
29
+
30
+    @Operation(summary = "设备详情")
31
+    @GetMapping("/{id}")
32
+    public R<Map<String, Object>> getById(@PathVariable Long id) {
33
+        return R.ok(jdbcTemplate.queryForMap("SELECT * FROM iot_device WHERE id = ?", id));
34
+    }
35
+
36
+    @Operation(summary = "注册设备")
37
+    @PostMapping
38
+    public R<String> register(@RequestBody Map<String, Object> body) {
39
+        jdbcTemplate.update(
40
+            "INSERT INTO iot_device (device_sn, device_name, device_type, area, loc_lng, loc_lat) VALUES (?,?,?,?,?,?)",
41
+            body.get("deviceSn"), body.get("deviceName"), body.get("deviceType"), body.get("area"),
42
+            body.get("lng"), body.get("lat"));
43
+        return R.ok("注册成功");
44
+    }
45
+
46
+    @Operation(summary = "下发指令")
47
+    @PostMapping("/{id}/command")
48
+    public R<String> sendCommand(@PathVariable Long id, @RequestBody Map<String, Object> cmd) {
49
+        // TODO: 实际指令通过 Kafka -> EMQX -> MQTT -> 设备
50
+        return R.ok("指令已下发");
51
+    }
52
+}

+ 39
- 0
wm-iot/src/main/java/com/water/iot/controller/GisController.java View File

@@ -0,0 +1,39 @@
1
+package com.water.iot.controller;
2
+
3
+import com.water.common.core.result.R;
4
+import com.water.iot.service.GisService;
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.List;
11
+import java.util.Map;
12
+
13
+@Tag(name = "GIS 地图服务")
14
+@RestController
15
+@RequestMapping("/gis")
16
+@RequiredArgsConstructor
17
+public class GisController {
18
+
19
+    private final GisService gisService;
20
+
21
+    @Operation(summary = "查询附近设备")
22
+    @GetMapping("/nearby")
23
+    public R<List<Map<String, Object>>> nearby(@RequestParam double lng, @RequestParam double lat,
24
+                                                @RequestParam(defaultValue = "5") double radius) {
25
+        return R.ok(gisService.findDevicesNearby(lng, lat, radius));
26
+    }
27
+
28
+    @Operation(summary = "片区设备统计")
29
+    @GetMapping("/device-stats")
30
+    public R<List<Map<String, Object>>> deviceStats() {
31
+        return R.ok(gisService.getDeviceStatsByArea());
32
+    }
33
+
34
+    @Operation(summary = "在线设备 GeoJSON")
35
+    @GetMapping("/geojson")
36
+    public R<String> geojson() {
37
+        return R.ok(gisService.getOnlineDevicesGeoJson());
38
+    }
39
+}

+ 65
- 0
wm-iot/src/main/java/com/water/iot/service/GisService.java View File

@@ -0,0 +1,65 @@
1
+package com.water.iot.service;
2
+
3
+import lombok.RequiredArgsConstructor;
4
+import org.springframework.jdbc.core.JdbcTemplate;
5
+import org.springframework.stereotype.Service;
6
+import java.util.List;
7
+import java.util.Map;
8
+
9
+@Service
10
+@RequiredArgsConstructor
11
+public class GisService {
12
+
13
+    private final JdbcTemplate jdbcTemplate;
14
+
15
+    /**
16
+     * 查询指定半径内的设备
17
+     */
18
+    public List<Map<String, Object>> findDevicesNearby(double lng, double lat, double radiusKm) {
19
+        String sql = """
20
+            SELECT id, device_sn, device_name, device_type, area,
21
+                   ST_Distance(geom::geography, ST_SetSRID(ST_MakePoint(?, ?), 4326)::geography) / 1000 AS distance_km,
22
+                   ST_X(geom) as lng, ST_Y(geom) as lat
23
+            FROM iot_device
24
+            WHERE ST_DWithin(geom::geography, ST_SetSRID(ST_MakePoint(?, ?), 4326)::geography, ? * 1000)
25
+            AND status = 'online'
26
+            ORDER BY distance_km
27
+            """;
28
+        return jdbcTemplate.queryForList(sql, lng, lat, lng, lat, radiusKm);
29
+    }
30
+
31
+    /**
32
+     * 查询片区内的设备统计
33
+     */
34
+    public List<Map<String, Object>> getDeviceStatsByArea() {
35
+        String sql = """
36
+            SELECT area, device_type, COUNT(*) as count
37
+            FROM iot_device
38
+            WHERE deleted = 0
39
+            GROUP BY area, device_type
40
+            ORDER BY area
41
+            """;
42
+        return jdbcTemplate.queryForList(sql);
43
+    }
44
+
45
+    /**
46
+     * 获取所有在线设备 GeoJSON
47
+     */
48
+    public String getOnlineDevicesGeoJson() {
49
+         String sql = """
50
+            SELECT json_build_object(
51
+                'type', 'FeatureCollection',
52
+                'features', json_agg(json_build_object(
53
+                    'type', 'Feature',
54
+                    'geometry', ST_AsGeoJSON(geom)::json,
55
+                    'properties', json_build_object(
56
+                        'id', id, 'name', device_name, 'type', device_type,
57
+                        'sn', device_sn, 'area', area, 'status', status
58
+                    )
59
+                ))
60
+            ) AS geojson
61
+            FROM iot_device WHERE status = 'online' AND geom IS NOT NULL
62
+            """;
63
+        return jdbcTemplate.queryForObject(sql, String.class);
64
+    }
65
+}

+ 34
- 0
wm-notify/src/main/java/com/water/notify/controller/NotifyController.java View File

@@ -0,0 +1,34 @@
1
+package com.water.notify.controller;
2
+
3
+import com.water.common.core.result.R;
4
+import com.water.notify.service.NotifyService;
5
+import io.swagger.v3.oas.annotations.tags.Tag;
6
+import lombok.RequiredArgsConstructor;
7
+import org.springframework.web.bind.annotation.*;
8
+
9
+import java.util.Map;
10
+
11
+@Tag(name = "消息通知")
12
+@RestController
13
+@RequestMapping("/notify")
14
+@RequiredArgsConstructor
15
+public class NotifyController {
16
+
17
+    private final NotifyService notifyService;
18
+
19
+    @PostMapping("/sms")
20
+    public R<String> sendSms(@RequestBody Map<String, String> req) {
21
+        notifyService.sendSms(req.get("phone"), req.get("content"));
22
+        return R.ok("短信发送成功");
23
+    }
24
+
25
+    @PostMapping("/push")
26
+    public R<String> push(@RequestBody Map<String, Object> req) {
27
+        notifyService.dispatch(
28
+            Long.parseLong(String.valueOf(req.get("schemeId"))),
29
+            Long.parseLong(String.valueOf(req.get("userId"))),
30
+            (String) req.get("title"),
31
+            (String) req.get("content"));
32
+        return R.ok("通知已分发");
33
+    }
34
+}

+ 33
- 0
wm-notify/src/main/java/com/water/notify/service/NotifyService.java View File

@@ -0,0 +1,33 @@
1
+package com.water.notify.service;
2
+
3
+import lombok.extern.slf4j.Slf4j;
4
+import org.springframework.stereotype.Service;
5
+
6
+@Slf4j
7
+@Service
8
+public class NotifyService {
9
+
10
+    /** 发送短信 */
11
+    public void sendSms(String phone, String content) {
12
+        log.info("Send SMS to {}: {}", phone, content);
13
+        // TODO: 集成阿里云/腾讯云短信 SDK
14
+    }
15
+
16
+    /** WebSocket 推送 */
17
+    public void pushWebSocket(Long userId, String message) {
18
+        log.info("Push WS to user {}: {}", userId, message);
19
+        // TODO: WebSocket session 管理
20
+    }
21
+
22
+    /** APP Push */
23
+    public void pushApp(Long userId, String title, String body) {
24
+        log.info("Push APP to user {}: {} - {}", userId, title, body);
25
+        // TODO: 极光推送
26
+    }
27
+
28
+    /** 按通知方案多渠道分发 */
29
+    public void dispatch(Long schemeId, Long userId, String title, String content) {
30
+        // TODO: 查询通知方案,按配置渠道分发
31
+        log.info("Notify dispatch: scheme={}, user={}, title={}", schemeId, userId, title);
32
+    }
33
+}