对降雨栅格添加websocket响应

This commit is contained in:
wzy-warehouse
2026-05-18 17:26:00 +08:00
parent 2fe0db36df
commit 9d95700787
9 changed files with 358 additions and 4 deletions
@@ -5,12 +5,14 @@ import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.SpringApplication; import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.scheduling.annotation.EnableScheduling;
/** /**
* 后台启动入口 * 后台启动入口
* 启动时过滤DataSourceAutoConfiguration,避免数据源自动配置 * 启动时过滤DataSourceAutoConfiguration,避免数据源自动配置
*/ */
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class) @SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
@EnableScheduling // 启用定时任务支持
@MapperScan("com.gis.xian.mapper") // 扫描MyBatis的Mapper接口 @MapperScan("com.gis.xian.mapper") // 扫描MyBatis的Mapper接口
public class XianApplication { public class XianApplication {
@@ -0,0 +1,25 @@
package com.gis.xian.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
/**
* 降雨网格配置属性类
* 从application.yml中读取rainfall.grid配置
*/
@Data
@Component
@ConfigurationProperties(prefix = "rainfall.grid")
public class RainfallGridProperties {
/**
* 降雨站点网格数据 Redis Key
*/
private String stationGrid;
/**
* 降雨站点标识符 Redis Key
*/
private String stationIdentifier;
}
@@ -0,0 +1,82 @@
package com.gis.xian.controller;
import com.gis.xian.domain.ApiResponse;
import com.gis.xian.service.RainfallGridService;
import com.gis.xian.vo.RainfallGridVo;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.SendTo;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.web.bind.annotation.RestController;
import java.util.concurrent.atomic.AtomicReference;
/**
* 降雨网格控制器
*/
@Slf4j
@RestController
public class RainfallGridController extends BaseController {
@Resource
private SimpMessagingTemplate messagingTemplate;
@Resource
private RainfallGridService rainfallGridService;
// 存储上一次的 ID,用于检测变化
private final AtomicReference<String> lastIdentifier = new AtomicReference<>(null);
/**
* 处理客户端订阅请求,首次订阅时返回当前数据
* 客户端发送任意消息到 /app/rainfall/grid 即可触发
*/
@MessageMapping("/rainfall/grid")
@SendTo("/topic/rainfall/grid/messages")
public ApiResponse<RainfallGridVo> handleSubscription() {
RainfallGridVo data = rainfallGridService.getRainfallGridData();
// 记录当前 identifier,用于后续变化检测
String currentIdentifier = rainfallGridService.getCurrentIdentifier();
if (currentIdentifier != null) {
lastIdentifier.set(currentIdentifier);
log.info("客户端订阅,记录初始 identifier: {}", currentIdentifier);
}
log.info("响应客户端订阅请求: id={}", data.getId());
return ApiResponse.ok(data);
}
/**
* 每秒检查 Redis 中的 identifier 是否变化,如果变化则推送新数据
*/
@Scheduled(fixedRate = 1000)
public void checkAndPushIfChanged() {
try {
String currentIdentifier = rainfallGridService.getCurrentIdentifier();
// 如果 identifier 为空,跳过
if (currentIdentifier == null) {
return;
}
// 检查 identifier 是否发生变化
String previousIdentifier = lastIdentifier.get();
if (previousIdentifier == null || !previousIdentifier.equals(currentIdentifier)) {
log.info("检测到 identifier 变化: {} -> {}", previousIdentifier, currentIdentifier);
// 获取最新数据并推送
RainfallGridVo data = rainfallGridService.getRainfallGridData();
messagingTemplate.convertAndSend("/topic/rainfall/grid/messages", ApiResponse.ok(data));
log.info("推送更新数据: id={}", data.getId());
// 更新记录的 identifier
lastIdentifier.set(currentIdentifier);
}
} catch (Exception e) {
log.error("检查并推送数据失败: {}", e.getMessage(), e);
}
}
}
@@ -0,0 +1,21 @@
package com.gis.xian.service;
import com.gis.xian.vo.RainfallGridVo;
/**
* 降雨网格服务接口
*/
public interface RainfallGridService {
/**
* 从 Redis 获取降雨网格数据
* @return 降雨网格数据
*/
RainfallGridVo getRainfallGridData();
/**
* 获取当前的降雨站点标识符
* @return 标识符
*/
String getCurrentIdentifier();
}
@@ -0,0 +1,173 @@
package com.gis.xian.service.impl;
import com.alibaba.fastjson2.JSON;
import com.gis.xian.config.RainfallGridProperties;
import com.gis.xian.service.RainfallGridService;
import com.gis.xian.service.ex.ServiceException;
import com.gis.xian.vo.RainfallGridVo;
import jakarta.annotation.Resource;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Map;
/**
* 降雨网格服务实现类
*/
@Service
public class RainfallGridServiceImpl implements RainfallGridService {
@Resource
private RedisTemplate<String, Object> redisTemplate;
@Resource
private RainfallGridProperties rainfallGridProperties;
@Override
public RainfallGridVo getRainfallGridData() {
try {
String redisKey = rainfallGridProperties.getStationGrid();
Object gridDataObj = redisTemplate.opsForValue().get(redisKey);
if (gridDataObj == null) {
throw new ServiceException("降雨网格数据不存在,Redis Key: " + redisKey);
}
// 将 Redis 中的 JSON 字符串解析为 Map
String gridDataJson;
if (gridDataObj instanceof String) {
gridDataJson = (String) gridDataObj;
} else {
gridDataJson = JSON.toJSONString(gridDataObj);
}
// 解析 JSON 为 Map(下划线命名)
Map<String, Object> gridDataMap = JSON.parseObject(gridDataJson, Map.class);
// 转换为 RainfallGridVo(小驼峰命名)
return convertToVo(gridDataMap);
} catch (Exception e) {
throw new ServiceException("解析降雨网格数据失败: " + e.getMessage(), e);
}
}
@Override
public String getCurrentIdentifier() {
try {
String redisKey = rainfallGridProperties.getStationIdentifier();
Object identifierObj = redisTemplate.opsForValue().get(redisKey);
if (identifierObj == null) {
return null;
}
return identifierObj.toString();
} catch (Exception e) {
throw new ServiceException("获取降雨站点标识符失败: " + e.getMessage(), e);
}
}
/**
* 将 Map 转换为 RainfallGridVo
*/
@SuppressWarnings("unchecked")
private RainfallGridVo convertToVo(Map<String, Object> gridDataMap) {
RainfallGridVo vo = new RainfallGridVo();
// 基本字段
vo.setId(getLongValue(gridDataMap, "id"));
vo.setPngPath((String) gridDataMap.get("png_path"));
vo.setResolution(getDoubleValue(gridDataMap, "resolution"));
vo.setStationCount(getIntValue(gridDataMap, "station_count"));
// 解析 query_time
String queryTimeStr = (String) gridDataMap.get("query_time");
if (queryTimeStr != null) {
vo.setQueryTime(LocalDateTime.parse(
queryTimeStr.replace(" ", "T"),
DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss")
));
}
// 解析 cesium_config
Map<String, Object> cesiumConfigMap = (Map<String, Object>) gridDataMap.get("cesium_config");
if (cesiumConfigMap != null) {
vo.setCesiumConfig(parseCesiumConfig(cesiumConfigMap));
}
return vo;
}
/**
* 解析 Cesium 配置
*/
@SuppressWarnings("unchecked")
private RainfallGridVo.CesiumConfig parseCesiumConfig(Map<String, Object> cesiumConfigMap) {
RainfallGridVo.CesiumConfig cesiumConfig = new RainfallGridVo.CesiumConfig();
cesiumConfig.setWidth(getDoubleValue(cesiumConfigMap, "width"));
cesiumConfig.setHeight(getDoubleValue(cesiumConfigMap, "height"));
// 解析 rectangle
Map<String, Object> rectangleMap = (Map<String, Object>) cesiumConfigMap.get("rectangle");
if (rectangleMap != null) {
cesiumConfig.setRectangle(parseRectangle(rectangleMap));
}
return cesiumConfig;
}
/**
* 解析矩形区域
*/
private RainfallGridVo.CesiumConfig.Rectangle parseRectangle(Map<String, Object> rectangleMap) {
RainfallGridVo.CesiumConfig.Rectangle rectangle = new RainfallGridVo.CesiumConfig.Rectangle();
rectangle.setWest(getDoubleValue(rectangleMap, "west"));
rectangle.setSouth(getDoubleValue(rectangleMap, "south"));
rectangle.setEast(getDoubleValue(rectangleMap, "east"));
rectangle.setNorth(getDoubleValue(rectangleMap, "north"));
return rectangle;
}
/**
* 辅助方法:从 Map 中获取 Long 值
*/
private Long getLongValue(Map<String, Object> map, String key) {
Object value = map.get(key);
if (value == null) {
return null;
}
if (value instanceof Number) {
return ((Number) value).longValue();
}
return Long.parseLong(value.toString());
}
/**
* 辅助方法:从 Map 中获取 Double 值
*/
private Double getDoubleValue(Map<String, Object> map, String key) {
Object value = map.get(key);
if (value == null) {
return null;
}
if (value instanceof Number) {
return ((Number) value).doubleValue();
}
return Double.parseDouble(value.toString());
}
/**
* 辅助方法:从 Map 中获取 Integer 值
*/
private Integer getIntValue(Map<String, Object> map, String key) {
Object value = map.get(key);
if (value == null) {
return null;
}
if (value instanceof Number) {
return ((Number) value).intValue();
}
return Integer.parseInt(value.toString());
}
}
@@ -0,0 +1,44 @@
package com.gis.xian.vo;
import lombok.Data;
import java.time.LocalDateTime;
@Data
public class RainfallGridVo {
public RainfallGridVo() {
this.cesiumConfig = new CesiumConfig();
}
private Long id;
private String pngPath;
private LocalDateTime queryTime;
private Double resolution;
private Integer stationCount;
private CesiumConfig cesiumConfig;
@Data
public static class CesiumConfig {
public CesiumConfig() {
this.rectangle = new Rectangle();
}
private Rectangle rectangle;
private Double width;
private Double height;
@Data
public static class Rectangle {
public Rectangle() {
this.west = 0.0;
this.south = 0.0;
this.east = 0.0;
this.north = 0.0;
}
private Double west;
private Double south;
private Double east;
private Double north;
}
}
}
+2 -2
View File
@@ -41,13 +41,13 @@ safety:
no-encrypt-paths: no-encrypt-paths:
- /crypto/sm2/public-key - /crypto/sm2/public-key
- /druid - /druid
- /algorithm-api/rainfall/grid - /websocket/info
- /websocket/** - /websocket/**
# 请求无需解密的路径 # 请求无需解密的路径
no-decrypt-paths: no-decrypt-paths:
- /crypto/sm2/public-key - /crypto/sm2/public-key
- /druid - /druid
- /algorithm-api/rainfall/grid - /websocket/info
- /websocket/** - /websocket/**
# 算法服务器配置 # 算法服务器配置
-2
View File
@@ -41,12 +41,10 @@ safety:
# 响应无需加密的路径 # 响应无需加密的路径
no-encrypt-paths: no-encrypt-paths:
- /crypto/sm2/public-key - /crypto/sm2/public-key
- /algorithm-api/rainfall/grid
- /websocket/** - /websocket/**
# 请求无需解密的路径 # 请求无需解密的路径
no-decrypt-paths: no-decrypt-paths:
- /crypto/sm2/public-key - /crypto/sm2/public-key
- /algorithm-api/rainfall/grid
- /websocket/** - /websocket/**
# 算法服务器配置 # 算法服务器配置
@@ -5,6 +5,15 @@ safety:
global: 'xian:sm2:keypair:global' global: 'xian:sm2:keypair:global'
# 降雨网格数据
rainfall:
grid:
# 降雨站点网格数据
station-grid: 'xian:rainfall:rain_station_grid'
# 降雨站点标识符
station-identifier: 'xian:rainfall:rain_station_identifier'
# 初始化数据存储 # 初始化数据存储
init: init:
data: data: