#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 反向代理配置
This commit is contained in:
@@ -0,0 +1,7 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
mvn clean package -DskipTests
|
||||||
|
for mod in wm-{base,iot,data-engine,bpm,production,revenue,patrol,bi,notify,job}; do
|
||||||
|
docker build -t water/$mod -f docker/$mod/Dockerfile .
|
||||||
|
done
|
||||||
|
echo "Build done"
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name localhost;
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
index index.html;
|
||||||
|
location / { try_files $uri /index.html; }
|
||||||
|
location /api/ { proxy_pass http://wm-gateway:8080/; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# GeoServer 初始化脚本:创建工作区、数据源、图层
|
||||||
|
set -e
|
||||||
|
GEOSERVER_URL="http://localhost:8081/geoserver"
|
||||||
|
USER="admin"
|
||||||
|
PASS="geoserver"
|
||||||
|
|
||||||
|
echo "Waiting for GeoServer..."
|
||||||
|
until curl -s -u $USER:$PASS "$GEOSERVER_URL/rest/about/version.xml" > /dev/null; do sleep 2; done
|
||||||
|
|
||||||
|
# Create workspace
|
||||||
|
curl -s -u $USER:$PASS -X POST "$GEOSERVER_URL/rest/workspaces" -H "Content-Type: text/xml" -d '<workspace><name>water_management</name></workspace>' || true
|
||||||
|
|
||||||
|
echo "GeoServer init done"
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
FROM eclipse-temurin:17-jre-alpine
|
||||||
|
WORKDIR /app
|
||||||
|
COPY wm-base/target/wm-base-*.jar app.jar
|
||||||
|
EXPOSE 8081
|
||||||
|
ENTRYPOINT ["java", "-jar", "app.jar"]
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
FROM eclipse-temurin:17-jre-alpine
|
||||||
|
WORKDIR /app
|
||||||
|
COPY wm-bi/target/wm-bi-*.jar app.jar
|
||||||
|
EXPOSE 8088
|
||||||
|
ENTRYPOINT ["java", "-jar", "app.jar"]
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
FROM eclipse-temurin:17-jre-alpine
|
||||||
|
WORKDIR /app
|
||||||
|
COPY wm-bpm/target/wm-bpm-*.jar app.jar
|
||||||
|
EXPOSE 8084
|
||||||
|
ENTRYPOINT ["java", "-jar", "app.jar"]
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
FROM eclipse-temurin:17-jre-alpine
|
||||||
|
WORKDIR /app
|
||||||
|
COPY wm-data-engine/target/wm-data-engine-*.jar app.jar
|
||||||
|
EXPOSE 8083
|
||||||
|
ENTRYPOINT ["java", "-jar", "app.jar"]
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
FROM eclipse-temurin:17-jre-alpine
|
||||||
|
WORKDIR /app
|
||||||
|
COPY wm-iot/target/wm-iot-*.jar app.jar
|
||||||
|
EXPOSE 8082
|
||||||
|
ENTRYPOINT ["java", "-jar", "app.jar"]
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
FROM eclipse-temurin:17-jre-alpine
|
||||||
|
WORKDIR /app
|
||||||
|
COPY wm-job/target/wm-job-*.jar app.jar
|
||||||
|
EXPOSE 8090
|
||||||
|
ENTRYPOINT ["java", "-jar", "app.jar"]
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
FROM eclipse-temurin:17-jre-alpine
|
||||||
|
WORKDIR /app
|
||||||
|
COPY wm-notify/target/wm-notify-*.jar app.jar
|
||||||
|
EXPOSE 8089
|
||||||
|
ENTRYPOINT ["java", "-jar", "app.jar"]
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
FROM eclipse-temurin:17-jre-alpine
|
||||||
|
WORKDIR /app
|
||||||
|
COPY wm-patrol/target/wm-patrol-*.jar app.jar
|
||||||
|
EXPOSE 8087
|
||||||
|
ENTRYPOINT ["java", "-jar", "app.jar"]
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
FROM eclipse-temurin:17-jre-alpine
|
||||||
|
WORKDIR /app
|
||||||
|
COPY wm-production/target/wm-production-*.jar app.jar
|
||||||
|
EXPOSE 8085
|
||||||
|
ENTRYPOINT ["java", "-jar", "app.jar"]
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
FROM eclipse-temurin:17-jre-alpine
|
||||||
|
WORKDIR /app
|
||||||
|
COPY wm-revenue/target/wm-revenue-*.jar app.jar
|
||||||
|
EXPOSE 8086
|
||||||
|
ENTRYPOINT ["java", "-jar", "app.jar"]
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
<template>
|
||||||
|
<div ref="mapContainer" class="map-container"></div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted, onUnmounted } from 'vue'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
center?: [number, number]
|
||||||
|
zoom?: number
|
||||||
|
markers?: Array<{ lat: number; lng: number; name: string; value?: string }>
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const mapContainer = ref<HTMLElement>()
|
||||||
|
|
||||||
|
let map: any = null
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
const L = (await import('leaflet')).default
|
||||||
|
await import('leaflet/dist/leaflet.css')
|
||||||
|
|
||||||
|
map = L.map(mapContainer.value!).setView(props.center || [44.6000, 82.9000], props.zoom || 10)
|
||||||
|
|
||||||
|
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||||
|
attribution: '© OpenStreetMap', maxZoom: 18
|
||||||
|
}).addTo(map)
|
||||||
|
|
||||||
|
if (props.markers) {
|
||||||
|
props.markers.forEach(m => {
|
||||||
|
L.marker([m.lat, m.lng])
|
||||||
|
.addTo(map)
|
||||||
|
.bindPopup(`<b>${m.name}</b><br/>${m.value || ''}`)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => { if (map) map.remove() })
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.map-container { width: 100%; height: 500px; border-radius: 8px }
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package com.water.iot.consumer;
|
||||||
|
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.kafka.annotation.KafkaListener;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@Component
|
||||||
|
public class IotTelemetryConsumer {
|
||||||
|
|
||||||
|
@KafkaListener(topics = "iot.telemetry", groupId = "wm-iot-consumer")
|
||||||
|
public void consumeTelemetry(String message) {
|
||||||
|
log.debug("Received telemetry: {}", message);
|
||||||
|
// TODO: 解析 json -> 写入 TDengine
|
||||||
|
}
|
||||||
|
|
||||||
|
@KafkaListener(topics = "iot.event", groupId = "wm-iot-consumer")
|
||||||
|
public void consumeEvent(String message) {
|
||||||
|
log.info("Device event: {}", message);
|
||||||
|
// TODO: 处理上下线/故障事件
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
package com.water.iot.controller;
|
||||||
|
|
||||||
|
import com.water.common.core.result.R;
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.jdbc.core.JdbcTemplate;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@Tag(name = "设备管理")
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/device")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class DeviceController {
|
||||||
|
|
||||||
|
private final JdbcTemplate jdbcTemplate;
|
||||||
|
|
||||||
|
@Operation(summary = "设备列表")
|
||||||
|
@GetMapping("/list")
|
||||||
|
public R<List<Map<String, Object>>> list(@RequestParam(defaultValue = "1") int page,
|
||||||
|
@RequestParam(defaultValue = "10") int size) {
|
||||||
|
int offset = (page - 1) * size;
|
||||||
|
String sql = "SELECT id, device_sn, device_name, device_type, area, status, last_report_time FROM iot_device ORDER BY id LIMIT ? OFFSET ?";
|
||||||
|
return R.ok(jdbcTemplate.queryForList(sql, size, offset));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "设备详情")
|
||||||
|
@GetMapping("/{id}")
|
||||||
|
public R<Map<String, Object>> getById(@PathVariable Long id) {
|
||||||
|
return R.ok(jdbcTemplate.queryForMap("SELECT * FROM iot_device WHERE id = ?", id));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "注册设备")
|
||||||
|
@PostMapping
|
||||||
|
public R<String> register(@RequestBody Map<String, Object> body) {
|
||||||
|
jdbcTemplate.update(
|
||||||
|
"INSERT INTO iot_device (device_sn, device_name, device_type, area, loc_lng, loc_lat) VALUES (?,?,?,?,?,?)",
|
||||||
|
body.get("deviceSn"), body.get("deviceName"), body.get("deviceType"), body.get("area"),
|
||||||
|
body.get("lng"), body.get("lat"));
|
||||||
|
return R.ok("注册成功");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "下发指令")
|
||||||
|
@PostMapping("/{id}/command")
|
||||||
|
public R<String> sendCommand(@PathVariable Long id, @RequestBody Map<String, Object> cmd) {
|
||||||
|
// TODO: 实际指令通过 Kafka -> EMQX -> MQTT -> 设备
|
||||||
|
return R.ok("指令已下发");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
package com.water.iot.controller;
|
||||||
|
|
||||||
|
import com.water.common.core.result.R;
|
||||||
|
import com.water.iot.service.GisService;
|
||||||
|
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.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@Tag(name = "GIS 地图服务")
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/gis")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class GisController {
|
||||||
|
|
||||||
|
private final GisService gisService;
|
||||||
|
|
||||||
|
@Operation(summary = "查询附近设备")
|
||||||
|
@GetMapping("/nearby")
|
||||||
|
public R<List<Map<String, Object>>> nearby(@RequestParam double lng, @RequestParam double lat,
|
||||||
|
@RequestParam(defaultValue = "5") double radius) {
|
||||||
|
return R.ok(gisService.findDevicesNearby(lng, lat, radius));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "片区设备统计")
|
||||||
|
@GetMapping("/device-stats")
|
||||||
|
public R<List<Map<String, Object>>> deviceStats() {
|
||||||
|
return R.ok(gisService.getDeviceStatsByArea());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "在线设备 GeoJSON")
|
||||||
|
@GetMapping("/geojson")
|
||||||
|
public R<String> geojson() {
|
||||||
|
return R.ok(gisService.getOnlineDevicesGeoJson());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
package com.water.iot.service;
|
||||||
|
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.jdbc.core.JdbcTemplate;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class GisService {
|
||||||
|
|
||||||
|
private final JdbcTemplate jdbcTemplate;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询指定半径内的设备
|
||||||
|
*/
|
||||||
|
public List<Map<String, Object>> findDevicesNearby(double lng, double lat, double radiusKm) {
|
||||||
|
String sql = """
|
||||||
|
SELECT id, device_sn, device_name, device_type, area,
|
||||||
|
ST_Distance(geom::geography, ST_SetSRID(ST_MakePoint(?, ?), 4326)::geography) / 1000 AS distance_km,
|
||||||
|
ST_X(geom) as lng, ST_Y(geom) as lat
|
||||||
|
FROM iot_device
|
||||||
|
WHERE ST_DWithin(geom::geography, ST_SetSRID(ST_MakePoint(?, ?), 4326)::geography, ? * 1000)
|
||||||
|
AND status = 'online'
|
||||||
|
ORDER BY distance_km
|
||||||
|
""";
|
||||||
|
return jdbcTemplate.queryForList(sql, lng, lat, lng, lat, radiusKm);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询片区内的设备统计
|
||||||
|
*/
|
||||||
|
public List<Map<String, Object>> getDeviceStatsByArea() {
|
||||||
|
String sql = """
|
||||||
|
SELECT area, device_type, COUNT(*) as count
|
||||||
|
FROM iot_device
|
||||||
|
WHERE deleted = 0
|
||||||
|
GROUP BY area, device_type
|
||||||
|
ORDER BY area
|
||||||
|
""";
|
||||||
|
return jdbcTemplate.queryForList(sql);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取所有在线设备 GeoJSON
|
||||||
|
*/
|
||||||
|
public String getOnlineDevicesGeoJson() {
|
||||||
|
String sql = """
|
||||||
|
SELECT json_build_object(
|
||||||
|
'type', 'FeatureCollection',
|
||||||
|
'features', json_agg(json_build_object(
|
||||||
|
'type', 'Feature',
|
||||||
|
'geometry', ST_AsGeoJSON(geom)::json,
|
||||||
|
'properties', json_build_object(
|
||||||
|
'id', id, 'name', device_name, 'type', device_type,
|
||||||
|
'sn', device_sn, 'area', area, 'status', status
|
||||||
|
)
|
||||||
|
))
|
||||||
|
) AS geojson
|
||||||
|
FROM iot_device WHERE status = 'online' AND geom IS NOT NULL
|
||||||
|
""";
|
||||||
|
return jdbcTemplate.queryForObject(sql, String.class);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
package com.water.notify.controller;
|
||||||
|
|
||||||
|
import com.water.common.core.result.R;
|
||||||
|
import com.water.notify.service.NotifyService;
|
||||||
|
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("/notify")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class NotifyController {
|
||||||
|
|
||||||
|
private final NotifyService notifyService;
|
||||||
|
|
||||||
|
@PostMapping("/sms")
|
||||||
|
public R<String> sendSms(@RequestBody Map<String, String> req) {
|
||||||
|
notifyService.sendSms(req.get("phone"), req.get("content"));
|
||||||
|
return R.ok("短信发送成功");
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/push")
|
||||||
|
public R<String> push(@RequestBody Map<String, Object> req) {
|
||||||
|
notifyService.dispatch(
|
||||||
|
Long.parseLong(String.valueOf(req.get("schemeId"))),
|
||||||
|
Long.parseLong(String.valueOf(req.get("userId"))),
|
||||||
|
(String) req.get("title"),
|
||||||
|
(String) req.get("content"));
|
||||||
|
return R.ok("通知已分发");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package com.water.notify.service;
|
||||||
|
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
public class NotifyService {
|
||||||
|
|
||||||
|
/** 发送短信 */
|
||||||
|
public void sendSms(String phone, String content) {
|
||||||
|
log.info("Send SMS to {}: {}", phone, content);
|
||||||
|
// TODO: 集成阿里云/腾讯云短信 SDK
|
||||||
|
}
|
||||||
|
|
||||||
|
/** WebSocket 推送 */
|
||||||
|
public void pushWebSocket(Long userId, String message) {
|
||||||
|
log.info("Push WS to user {}: {}", userId, message);
|
||||||
|
// TODO: WebSocket session 管理
|
||||||
|
}
|
||||||
|
|
||||||
|
/** APP Push */
|
||||||
|
public void pushApp(Long userId, String title, String body) {
|
||||||
|
log.info("Push APP to user {}: {} - {}", userId, title, body);
|
||||||
|
// TODO: 极光推送
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 按通知方案多渠道分发 */
|
||||||
|
public void dispatch(Long schemeId, Long userId, String title, String content) {
|
||||||
|
// TODO: 查询通知方案,按配置渠道分发
|
||||||
|
log.info("Notify dispatch: scheme={}, user={}, title={}", schemeId, userId, title);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user