diff --git a/docker/ci/build.sh b/docker/ci/build.sh new file mode 100644 index 00000000..7c963a3a --- /dev/null +++ b/docker/ci/build.sh @@ -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" diff --git a/docker/frontend/nginx.conf b/docker/frontend/nginx.conf new file mode 100644 index 00000000..d75850d9 --- /dev/null +++ b/docker/frontend/nginx.conf @@ -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/; } +} diff --git a/docker/geoserver/init.sh b/docker/geoserver/init.sh new file mode 100644 index 00000000..efdeddf9 --- /dev/null +++ b/docker/geoserver/init.sh @@ -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 'water_management' || true + +echo "GeoServer init done" diff --git a/docker/wm-base/Dockerfile b/docker/wm-base/Dockerfile new file mode 100644 index 00000000..d6d5f60e --- /dev/null +++ b/docker/wm-base/Dockerfile @@ -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"] diff --git a/docker/wm-bi/Dockerfile b/docker/wm-bi/Dockerfile new file mode 100644 index 00000000..dcf58e47 --- /dev/null +++ b/docker/wm-bi/Dockerfile @@ -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"] diff --git a/docker/wm-bpm/Dockerfile b/docker/wm-bpm/Dockerfile new file mode 100644 index 00000000..323c1d09 --- /dev/null +++ b/docker/wm-bpm/Dockerfile @@ -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"] diff --git a/docker/wm-data-engine/Dockerfile b/docker/wm-data-engine/Dockerfile new file mode 100644 index 00000000..d9b54e08 --- /dev/null +++ b/docker/wm-data-engine/Dockerfile @@ -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"] diff --git a/docker/wm-iot/Dockerfile b/docker/wm-iot/Dockerfile new file mode 100644 index 00000000..5b3adcf2 --- /dev/null +++ b/docker/wm-iot/Dockerfile @@ -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"] diff --git a/docker/wm-job/Dockerfile b/docker/wm-job/Dockerfile new file mode 100644 index 00000000..cdcc5020 --- /dev/null +++ b/docker/wm-job/Dockerfile @@ -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"] diff --git a/docker/wm-notify/Dockerfile b/docker/wm-notify/Dockerfile new file mode 100644 index 00000000..b093a9f8 --- /dev/null +++ b/docker/wm-notify/Dockerfile @@ -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"] diff --git a/docker/wm-patrol/Dockerfile b/docker/wm-patrol/Dockerfile new file mode 100644 index 00000000..365890a8 --- /dev/null +++ b/docker/wm-patrol/Dockerfile @@ -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"] diff --git a/docker/wm-production/Dockerfile b/docker/wm-production/Dockerfile new file mode 100644 index 00000000..7f652e0d --- /dev/null +++ b/docker/wm-production/Dockerfile @@ -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"] diff --git a/docker/wm-revenue/Dockerfile b/docker/wm-revenue/Dockerfile new file mode 100644 index 00000000..5c0ab69b --- /dev/null +++ b/docker/wm-revenue/Dockerfile @@ -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"] diff --git a/frontend/src/components/charts/MapView.vue b/frontend/src/components/charts/MapView.vue new file mode 100644 index 00000000..6f41536d --- /dev/null +++ b/frontend/src/components/charts/MapView.vue @@ -0,0 +1,42 @@ + + + + + diff --git a/wm-iot/src/main/java/com/water/iot/consumer/IotTelemetryConsumer.java b/wm-iot/src/main/java/com/water/iot/consumer/IotTelemetryConsumer.java new file mode 100644 index 00000000..8a2bc53c --- /dev/null +++ b/wm-iot/src/main/java/com/water/iot/consumer/IotTelemetryConsumer.java @@ -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: 处理上下线/故障事件 + } +} diff --git a/wm-iot/src/main/java/com/water/iot/controller/DeviceController.java b/wm-iot/src/main/java/com/water/iot/controller/DeviceController.java new file mode 100644 index 00000000..5b3702bd --- /dev/null +++ b/wm-iot/src/main/java/com/water/iot/controller/DeviceController.java @@ -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(@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> getById(@PathVariable Long id) { + return R.ok(jdbcTemplate.queryForMap("SELECT * FROM iot_device WHERE id = ?", id)); + } + + @Operation(summary = "注册设备") + @PostMapping + public R register(@RequestBody Map 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 sendCommand(@PathVariable Long id, @RequestBody Map cmd) { + // TODO: 实际指令通过 Kafka -> EMQX -> MQTT -> 设备 + return R.ok("指令已下发"); + } +} diff --git a/wm-iot/src/main/java/com/water/iot/controller/GisController.java b/wm-iot/src/main/java/com/water/iot/controller/GisController.java new file mode 100644 index 00000000..b35cb9a3 --- /dev/null +++ b/wm-iot/src/main/java/com/water/iot/controller/GisController.java @@ -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>> 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>> deviceStats() { + return R.ok(gisService.getDeviceStatsByArea()); + } + + @Operation(summary = "在线设备 GeoJSON") + @GetMapping("/geojson") + public R geojson() { + return R.ok(gisService.getOnlineDevicesGeoJson()); + } +} diff --git a/wm-iot/src/main/java/com/water/iot/service/GisService.java b/wm-iot/src/main/java/com/water/iot/service/GisService.java new file mode 100644 index 00000000..a5dbd384 --- /dev/null +++ b/wm-iot/src/main/java/com/water/iot/service/GisService.java @@ -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> 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> 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); + } +} diff --git a/wm-notify/src/main/java/com/water/notify/controller/NotifyController.java b/wm-notify/src/main/java/com/water/notify/controller/NotifyController.java new file mode 100644 index 00000000..abde2526 --- /dev/null +++ b/wm-notify/src/main/java/com/water/notify/controller/NotifyController.java @@ -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 sendSms(@RequestBody Map req) { + notifyService.sendSms(req.get("phone"), req.get("content")); + return R.ok("短信发送成功"); + } + + @PostMapping("/push") + public R push(@RequestBody Map 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("通知已分发"); + } +} diff --git a/wm-notify/src/main/java/com/water/notify/service/NotifyService.java b/wm-notify/src/main/java/com/water/notify/service/NotifyService.java new file mode 100644 index 00000000..345010a2 --- /dev/null +++ b/wm-notify/src/main/java/com/water/notify/service/NotifyService.java @@ -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); + } +}