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 反向代理配置
This commit is contained in:
bot_pm
2026-06-14 13:18:19 +08:00
parent 575b2138c1
commit 0b8bad8879
20 changed files with 366 additions and 0 deletions
@@ -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);
}
}