15 Commits

Author SHA1 Message Date
wzy-warehouse d343b3e64f redis单独配置 2026-06-28 17:39:30 +08:00
wzy-warehouse f95ff8c293 修改为24小时 2026-06-28 17:33:24 +08:00
wzy-warehouse ad05e64685 修改山洪表格显示以及模版简单修改 2026-06-28 17:08:25 +08:00
wzy-warehouse 9390c5dc85 修改导出功能 2026-06-28 17:01:22 +08:00
wzy-warehouse c9cad1e366 修复图片显示 2026-06-28 16:44:43 +08:00
wzy-warehouse 4262ec24ad 修复位置信息 2026-06-28 16:40:56 +08:00
wzy-warehouse 1264070023 完善表格显示以及风险点隐患点计数 2026-06-28 16:31:10 +08:00
wzy-warehouse 30acf435ee 部分暴雨灾害链报告产出,部分内容待完善 2026-06-28 16:16:51 +08:00
wzy-warehouse 0c59afa490 部分暴雨灾害链报告产出,部分内容待完善 2026-06-28 15:57:59 +08:00
wzy-warehouse 2ef96f812f 部分暴雨灾害链报告产出,表格信息前。 2026-06-28 11:27:33 +08:00
wzy-warehouse 20bfead821 删除不必要的依赖和文件 2026-06-26 09:31:02 +08:00
wzy-warehouse 0d952e0c98 恢复误删除文件 2026-06-25 21:54:32 +08:00
wzy-warehouse d2950ffdd3 删除不必要的文件 2026-06-24 10:24:35 +08:00
wzy-warehouse 71ae772ede Merge remote-tracking branch 'refs/remotes/origin/main' into wzy 2026-06-24 09:54:27 +08:00
zxyroy d37d9c2361 修改后端readme 2026-06-17 22:10:41 +08:00
45 changed files with 1645 additions and 1503 deletions
+2 -2
View File
@@ -1,5 +1,5 @@
# basic_template_not_login_back
开发基本模版——后端
# xian_api_new
新版西安项目——后端
# basic_template_not_login_back
+8 -40
View File
@@ -13,6 +13,7 @@
<bcprov-jdk15to18.version>1.82</bcprov-jdk15to18.version>
<fastjson.version>2.0.60</fastjson.version>
<mybatis-plus.version>3.5.9</mybatis-plus.version>
<poi-tl-version>1.12.2</poi-tl-version>
</properties>
<parent>
@@ -120,6 +121,13 @@
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!-- word模板 -->
<dependency>
<groupId>com.deepoove</groupId>
<artifactId>poi-tl</artifactId>
<version>${poi-tl-version}</version>
</dependency>
<!-- 测试框架 -->
<dependency>
<groupId>org.springframework.boot</groupId>
@@ -127,46 +135,6 @@
<scope>test</scope>
</dependency>
<!-- GIS空间数据处理工具 -->
<dependency>
<groupId>org.locationtech.jts</groupId>
<artifactId>jts-core</artifactId>
<version>1.19.0</version>
</dependency>
<!-- PostGIS TypeHandler 支持 -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-typehandlers-jsr310</artifactId>
<version>1.0.2</version>
</dependency>
<!-- PostGIS JTS 支持 -->
<dependency>
<groupId>net.postgis</groupId>
<artifactId>postgis-jdbc</artifactId>
<version>2021.1.0</version>
</dependency>
<dependency>
<groupId>org.hibernate.orm</groupId>
<artifactId>hibernate-spatial</artifactId>
<version>6.2.0.Final</version>
</dependency>
<!-- 坐标投影转换 -->
<dependency>
<groupId>org.locationtech.proj4j</groupId>
<artifactId>proj4j</artifactId>
<version>1.1.4</version>
</dependency>
<!--常用工具类 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
</dependencies>
<profiles>
@@ -0,0 +1,182 @@
package com.gis.xian.config;
import com.alibaba.fastjson2.JSON;
import com.gis.xian.domain.ApiResponse;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.stereotype.Component;
import org.springframework.util.StreamUtils;
import org.springframework.web.client.RestClient;
import java.util.Collections;
import java.util.Enumeration;
import java.util.Map;
/**
* 算法服务 HTTP 客户端
*
* 两种使用方式:
* 1. Controller 透传前端请求:{@link #proxyRequest(HttpServletRequest, HttpMethod)}
* 2. Service 直接调用:{@link #get(String, Class)} / {@link #post(String, Object, Class)}
*/
@Slf4j
@Component
public class AlgorithmClient {
@Resource
private AlgorithmServerProperties props;
private RestClient restClient;
@PostConstruct
public void init() {
SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
factory.setConnectTimeout(props.getConnectTimeout() * 1000);
factory.setReadTimeout(props.getReadTimeout() * 1000);
this.restClient = RestClient.builder().requestFactory(factory).build();
log.info("AlgorithmClient 初始化: url={}, connectTimeout={}s, readTimeout={}s",
props.getUrl(), props.getConnectTimeout(), props.getReadTimeout());
}
// ================================================================
// 前端透传:完整代理 HTTP 请求
// ================================================================
public ApiResponse<Object> proxyGet(HttpServletRequest request) {
return proxyRequest(request, HttpMethod.GET);
}
public ApiResponse<Object> proxyPost(HttpServletRequest request) {
return proxyRequest(request, HttpMethod.POST);
}
public ApiResponse<Object> proxyPut(HttpServletRequest request) {
return proxyRequest(request, HttpMethod.PUT);
}
public ApiResponse<Object> proxyDelete(HttpServletRequest request) {
return proxyRequest(request, HttpMethod.DELETE);
}
public ApiResponse<Object> proxyPatch(HttpServletRequest request) {
return proxyRequest(request, HttpMethod.PATCH);
}
@SuppressWarnings("unchecked")
private ApiResponse<Object> proxyRequest(HttpServletRequest request, HttpMethod httpMethod) {
long startTime = System.currentTimeMillis();
try {
String targetUrl = buildTargetUrl(request);
log.info("代理请求: {} -> {}", request.getRequestURI(), targetUrl);
RestClient.RequestBodySpec spec = restClient
.method(httpMethod)
.uri(targetUrl)
.headers(h -> copyRequestHeaders(request, h));
if (httpMethod == HttpMethod.POST || httpMethod == HttpMethod.PUT || httpMethod == HttpMethod.PATCH) {
String contentType = request.getContentType();
byte[] bodyBytes = StreamUtils.copyToByteArray(request.getInputStream());
if (bodyBytes.length > 0) {
if (contentType != null) {
spec.contentType(MediaType.parseMediaType(contentType));
}
spec.body(bodyBytes);
}
}
ResponseEntity<String> response = spec.retrieve().toEntity(String.class);
long elapsed = System.currentTimeMillis() - startTime;
log.info("代理完成: {} -> {}, {}ms, HTTP {}", request.getRequestURI(), targetUrl, elapsed, response.getStatusCode().value());
return parseResponseBody(response.getBody());
} catch (Exception e) {
long elapsed = System.currentTimeMillis() - startTime;
log.error("代理失败: {}, {}ms, {}", request.getRequestURI(), elapsed, e.getMessage(), e);
return ApiResponse.error(502, "算法服务调用失败: " + e.getMessage(), null);
}
}
// ================================================================
// 后端直接调用(类型安全)
// ================================================================
public <T> T get(String path, Class<T> responseType) {
String url = props.getUrl() + path;
log.info("GET {}", url);
return restClient.get().uri(url).retrieve().body(responseType);
}
public <T> T get(String path, ParameterizedTypeReference<T> responseType) {
String url = props.getUrl() + path;
log.info("GET {}", url);
return restClient.get().uri(url).retrieve().body(responseType);
}
public <T> T post(String path, Object body, Class<T> responseType) {
String url = props.getUrl() + path;
log.info("POST {} body={}", url, body);
return restClient.post().uri(url).body(body).retrieve().body(responseType);
}
public <T> T post(String path, Object body, ParameterizedTypeReference<T> responseType) {
String url = props.getUrl() + path;
log.info("POST {} body={}", url, body);
return restClient.post().uri(url).body(body).retrieve().body(responseType);
}
// ================================================================
// 内部工具方法
// ================================================================
private String buildTargetUrl(HttpServletRequest request) {
String path = request.getRequestURI().substring("/algorithm-api".length());
if (!path.startsWith("/")) {
path = "/" + path;
}
String baseUrl = props.getUrl();
if (baseUrl.endsWith("/") && path.startsWith("/")) {
baseUrl = baseUrl.substring(0, baseUrl.length() - 1);
}
String queryString = request.getQueryString();
return queryString != null ? baseUrl + path + "?" + queryString : baseUrl + path;
}
private void copyRequestHeaders(HttpServletRequest request, HttpHeaders headers) {
Enumeration<String> names = request.getHeaderNames();
while (names.hasMoreElements()) {
String name = names.nextElement();
if (shouldExcludeHeader(name)) continue;
headers.addAll(name, Collections.list(request.getHeaders(name)));
}
}
private boolean shouldExcludeHeader(String name) {
String lower = name.toLowerCase();
return lower.equals("host") || lower.equals("content-length") || lower.equals("transfer-encoding");
}
@SuppressWarnings("unchecked")
private ApiResponse<Object> parseResponseBody(String body) {
if (body == null || body.isEmpty()) {
return ApiResponse.ok(null);
}
try {
Map<String, Object> map = JSON.parseObject(body, Map.class);
if (map.containsKey("code") && map.containsKey("message")) {
return new ApiResponse<>((Integer) map.get("code"), (String) map.get("message"), map.get("data"));
}
} catch (Exception ignored) {
}
return ApiResponse.ok((Object) body);
}
}
@@ -16,4 +16,14 @@ public class AlgorithmServerProperties {
* 算法服务器地址
*/
private String url;
/**
* 连接超时(秒)
*/
private int connectTimeout;
/**
* 读取超时(秒)
*/
private int readTimeout;
}
@@ -0,0 +1,36 @@
package com.gis.xian.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
/**
* 基础信息点 Redis Key 配置属性
*/
@Data
@Component
@ConfigurationProperties(prefix = "init.data.base-points")
public class BasePointsRedisProperties {
private HiddenDanger hiddenDanger;
private String risk;
private String hospitals;
private String dangerousSource;
private String emergencyShelter;
private String firefighter;
private String storePoints;
private String school;
private String bridge;
private String reservoir;
private String subway;
@Data
public static class HiddenDanger {
private String all;
private String landslide;
private String collapse;
private String debrisFlow;
private String flashFlood;
private String waterLogging;
}
}
@@ -0,0 +1,42 @@
package com.gis.xian.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
@Data
@Component
@ConfigurationProperties(prefix = "report")
public class ReportProperties {
private DisasterCausingFactors disasterCausingFactors;
private ReportFiles files;
private Map<String, String> disasterTypes = new LinkedHashMap<>();
@Data
public static class ReportFiles {
/**
* 文件服务器前缀
*/
private String serverUrl;
}
@Data
public static class DisasterCausingFactors {
private FactorConfig rainfall;
private FactorConfig earthquake;
}
@Data
public static class FactorConfig {
private List<String> type;
private Integer number;
private String templatePath;
private String outputPath;
private String outputName;
}
}
@@ -1,138 +0,0 @@
package com.gis.xian.config.typehandler;
import net.postgis.jdbc.PGgeometry;
import org.apache.ibatis.type.BaseTypeHandler;
import org.apache.ibatis.type.JdbcType;
import org.apache.ibatis.type.MappedJdbcTypes;
import org.apache.ibatis.type.MappedTypes;
import org.locationtech.jts.geom.Geometry;
import org.locationtech.jts.geom.GeometryFactory;
import org.locationtech.jts.geom.PrecisionModel;
import org.locationtech.jts.io.WKTReader;
import org.locationtech.jts.io.WKTWriter;
import org.postgresql.util.PGobject;
import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
/**
* 修复编译报错版:PostgreSQL geometry → JTS Geometry 类型处理器
* 解决 JTS Geometry 与 PostGIS Geometry 类的冲突问题
*/
@MappedTypes(Geometry.class) // 实体类字段类型(JTS 的 Geometry
@MappedJdbcTypes(JdbcType.OTHER) // 数据库 geometry 对应 JDBC 类型 OTHER
public class GeometryTypeHandler extends BaseTypeHandler<Geometry> {
// WKT 解析器(JTS → 数据库,线程安全)
private static final WKTReader WKT_READER = new WKTReader();
// WKT 生成器(数据库 → JTS,线程安全)
private static final WKTWriter WKT_WRITER = new WKTWriter();
private static final GeometryFactory GEOMETRY_FACTORY = new GeometryFactory(
new PrecisionModel(PrecisionModel.FLOATING),
4490
);
@Override
public void setNonNullParameter(PreparedStatement ps, int i, Geometry parameter, JdbcType jdbcType) throws SQLException {
try {
// 将 JTS 的 Geometry 转为标准 WKT 字符串
String wktText = WKT_WRITER.write(parameter);
// 用 WKT 字符串创建 PGgeometry 对象
PGgeometry pgGeometry = new PGgeometry(wktText);
// 存入 PreparedStatement
ps.setObject(i, pgGeometry);
} catch (Exception e) {
throw new SQLException("将 JTS Geometry 转为 PGgeometry 失败", e);
}
}
@Override
public Geometry getNullableResult(ResultSet rs, String columnName) throws SQLException {
return convertToGeometry(rs.getObject(columnName));
}
@Override
public Geometry getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
return convertToGeometry(rs.getObject(columnIndex));
}
@Override
public Geometry getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
return convertToGeometry(cs.getObject(columnIndex));
}
private Geometry convertToGeometry(Object value) {
// 处理 null 值
if (value == null) {
return null;
}
String wktText = null;
try {
// 兼容 PostGIS 的 PGgeometry 格式
if (value instanceof PGgeometry) {
PGgeometry pgGeometry = (PGgeometry) value;
// 转为标准 WKT 文本
wktText = pgGeometry.toString();
}
// 兼容 PostgreSQL 的 PGobject 格式
else if (value instanceof PGobject) {
PGobject pgObject = (PGobject) value;
String pgValue = pgObject.getValue();
if (pgValue != null && !pgValue.trim().isEmpty()) {
wktText = new PGgeometry(pgValue).toString();
}
}
// 兼容纯 WKT 字符串格式
else if (value instanceof String) {
wktText = (String) value;
}
// 未知格式直接返回 null
else {
return null;
}
// 通用清洗 WKT 字符串
String pureWkt = cleanWkt(wktText);
if (pureWkt != null && !pureWkt.trim().isEmpty() && !"NULL".equalsIgnoreCase(pureWkt)) {
Geometry geometry = WKT_READER.read(pureWkt);
// 给几何对象设置 SRID=4490
geometry.setSRID(4490);
return geometry;
}
return null;
} catch (Exception e) {
System.err.println("解析 geometry 字段失败,原始 WKT" + wktText + ",异常:" + e.getMessage());
return null;
}
}
/**
* 通用版 WKT 清洗方法:支持 Point、LINESTRING 等常见几何类型
*/
private String cleanWkt(String wkt) {
if (wkt == null) {
return null;
}
// 步骤1:去除所有不可见字符(换行、回车、制表符)
String cleaned = wkt.replaceAll("[\n\r\t]", "");
// 步骤2:去除 SRID 前缀(如 "SRID=4326;"
cleaned = cleaned.replaceAll("^SRID=\\d+;", "");
// 步骤3:将多个连续空格替换为单个空格(避免坐标间多空格干扰)
cleaned = cleaned.replaceAll("\\s+", " ");
// 步骤4:去除几何类型与括号间的多余空格(适配 Point、LINESTRING
cleaned = cleaned.replaceAll("(POINT|LINESTRING|POLYGON)\\s*\\(", "$1(");
cleaned = cleaned.replaceAll("\\)\\s*$", ")");
// 步骤5:去除首尾多余空格
cleaned = cleaned.trim();
// 步骤6:验证是否为支持的几何类型(可根据需求扩展)
if (!cleaned.startsWith("POINT(") && !cleaned.startsWith("LINESTRING(") && !cleaned.startsWith("POLYGON(")) {
System.err.println("不支持的几何类型,WKT" + cleaned);
return null;
}
return cleaned;
}
}
@@ -1,24 +1,14 @@
package com.gis.xian.controller;
import com.alibaba.fastjson2.JSON;
import com.gis.xian.config.AlgorithmServerProperties;
import com.gis.xian.domain.ApiResponse;
import com.gis.xian.config.AlgorithmClient;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.*;
import org.springframework.util.StreamUtils;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.client.RestTemplate;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Enumeration;
import java.util.Map;
/**
* 算法代理控制器
* 所有以 /algorithm-api 开头的请求都会转发到算法服务器
* 算法代理控制器 — 前端请求透传到算法服务器
*/
@RestController
@RequestMapping("/algorithm-api")
@@ -26,197 +16,30 @@ import java.util.Map;
public class AlgorithmProxyController extends BaseController {
@Resource
private AlgorithmServerProperties algorithmServerProperties;
private AlgorithmClient algorithmClient;
private final RestTemplate restTemplate = new RestTemplate();
/**
* 处理所有 GET 请求
*/
@GetMapping("/**")
public ApiResponse<Object> proxyGet(HttpServletRequest request) {
return proxyRequest(request, HttpMethod.GET);
return algorithmClient.proxyGet(request);
}
/**
* 处理所有 POST 请求
*/
@PostMapping("/**")
public ApiResponse<Object> proxyPost(HttpServletRequest request) throws IOException {
return proxyRequest(request, HttpMethod.POST);
public ApiResponse<Object> proxyPost(HttpServletRequest request) {
return algorithmClient.proxyPost(request);
}
/**
* 处理所有 PUT 请求
*/
@PutMapping("/**")
public ApiResponse<Object> proxyPut(HttpServletRequest request) throws IOException {
return proxyRequest(request, HttpMethod.PUT);
public ApiResponse<Object> proxyPut(HttpServletRequest request) {
return algorithmClient.proxyPut(request);
}
/**
* 处理所有 DELETE 请求
*/
@DeleteMapping("/**")
public ApiResponse<Object> proxyDelete(HttpServletRequest request) {
return proxyRequest(request, HttpMethod.DELETE);
return algorithmClient.proxyDelete(request);
}
/**
* 处理所有 PATCH 请求
*/
@PatchMapping("/**")
public ApiResponse<Object> proxyPatch(HttpServletRequest request) throws IOException {
return proxyRequest(request, HttpMethod.PATCH);
}
/**
* 通用的请求代理方法
*/
@SuppressWarnings("unchecked")
private ApiResponse<Object> proxyRequest(HttpServletRequest request, HttpMethod httpMethod) {
long startTime = System.currentTimeMillis();
try {
// 构建目标 URL
String targetUrl = buildTargetUrl(request);
log.info("代理请求: {} -> {}", request.getRequestURI(), targetUrl);
// 构建请求头
HttpHeaders headers = buildRequestHeaders(request);
// 构建请求体
Object requestBody = null;
if (httpMethod == HttpMethod.POST || httpMethod == HttpMethod.PUT || httpMethod == HttpMethod.PATCH) {
String contentType = request.getContentType();
if (contentType != null && !contentType.isEmpty()) {
headers.setContentType(MediaType.parseMediaType(contentType));
// 读取请求体
byte[] bodyBytes = StreamUtils.copyToByteArray(request.getInputStream());
if (bodyBytes.length > 0) {
// 尝试解析为 JSON,如果不是 JSON 则直接使用字节数组
if (contentType.contains("application/json")) {
String jsonBody = new String(bodyBytes, StandardCharsets.UTF_8);
try {
requestBody = JSON.parse(jsonBody);
} catch (Exception e) {
requestBody = jsonBody;
}
} else {
requestBody = bodyBytes;
}
}
}
}
// 创建 HTTP 实体
HttpEntity<Object> entity = new HttpEntity<>(requestBody, headers);
// 发送请求到算法服务器
ResponseEntity<String> response = restTemplate.exchange(
targetUrl,
httpMethod,
entity,
String.class
);
// 解析响应
long endTime = System.currentTimeMillis();
log.info("代理请求完成: {} -> {}, 耗时: {}ms, 状态码: {}",
request.getRequestURI(), targetUrl, (endTime - startTime), response.getStatusCode());
// 尝试将响应解析为 ApiResponse
String responseBody = response.getBody();
if (responseBody != null && !responseBody.isEmpty()) {
try {
// 尝试解析为 ApiResponse 格式
Map<String, Object> responseMap = JSON.parseObject(responseBody, Map.class);
if (responseMap.containsKey("code") && responseMap.containsKey("message")) {
// 如果已经是 ApiResponse 格式,直接返回
Integer code = (Integer) responseMap.get("code");
String message = (String) responseMap.get("message");
Object data = responseMap.get("data");
return new ApiResponse<>(code, message, data);
}
} catch (Exception e) {
log.debug("响应不是标准 ApiResponse 格式,将作为数据返回");
}
// 如果不是 ApiResponse 格式,将整个响应作为 data 返回
return ApiResponse.ok((Object) responseBody);
}
return ApiResponse.ok((Object) null);
} catch (Exception e) {
long endTime = System.currentTimeMillis();
log.error("代理请求失败: {} , 耗时: {}ms, 错误: {}",
request.getRequestURI(), (endTime - startTime), e.getMessage(), e);
return ApiResponse.error(502, "算法服务调用失败: " + e.getMessage(), null);
}
}
/**
* 构建目标 URL
*/
private String buildTargetUrl(HttpServletRequest request) {
// 获取原始请求路径,去掉 /algorithm-api 前缀
String requestUri = request.getRequestURI();
String path = requestUri.substring("/algorithm-api".length());
// 确保路径以 / 开头
if (!path.startsWith("/")) {
path = "/" + path;
}
// 拼接算法服务器地址和路径
String baseUrl = algorithmServerProperties.getUrl();
if (baseUrl.endsWith("/") && path.startsWith("/")) {
baseUrl = baseUrl.substring(0, baseUrl.length() - 1);
}
String targetUrl = baseUrl + path;
// 添加查询参数
String queryString = request.getQueryString();
if (queryString != null && !queryString.isEmpty()) {
targetUrl += "?" + queryString;
}
return targetUrl;
}
/**
* 构建请求头
*/
private HttpHeaders buildRequestHeaders(HttpServletRequest request) {
HttpHeaders headers = new HttpHeaders();
// 复制所有请求头(排除一些特定的头)
Enumeration<String> headerNames = request.getHeaderNames();
while (headerNames.hasMoreElements()) {
String headerName = headerNames.nextElement();
// 排除一些不应该转发的头
if (!shouldExcludeHeader(headerName)) {
Enumeration<String> headerValues = request.getHeaders(headerName);
while (headerValues.hasMoreElements()) {
headers.add(headerName, headerValues.nextElement());
}
}
}
return headers;
}
/**
* 判断是否应该排除该请求头
*/
private boolean shouldExcludeHeader(String headerName) {
String lowerName = headerName.toLowerCase();
// 排除 Host、Content-Length 等头
return lowerName.equals("host") ||
lowerName.equals("content-length") ||
lowerName.equals("transfer-encoding");
public ApiResponse<Object> proxyPatch(HttpServletRequest request) {
return algorithmClient.proxyPatch(request);
}
}
@@ -1,25 +0,0 @@
package com.gis.xian.controller;
import com.gis.xian.service.IFeignService;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
/**
* 专题图触发接口
* 统一入口,接收 simulationId 和 type,调用 Python 端处理
*/
@Slf4j
@RestController
@RequestMapping("/open")
public class QgisController {
@Resource
private IFeignService feignService;
@PostMapping("/qgis/trigger")
public void trigger(@RequestParam String simulationId, @RequestParam String type) {
log.info("收到专题图触发请求: simulationId={}, type={}", simulationId, type);
feignService.trigger(simulationId, type);
}
}
@@ -0,0 +1,27 @@
package com.gis.xian.controller;
import com.gis.xian.domain.ApiResponse;
import com.gis.xian.service.ReportOutputService;
import jakarta.annotation.Resource;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/report/output")
public class ReportOutputController {
@Resource
private ReportOutputService reportOutputService;
/**
* 输出暴雨报告
* @param id 模拟id
* @return 暴雨报告链接
*/
@PostMapping("/rainfall/{id}")
public ApiResponse<String> rainfall(@PathVariable String id) {
return ApiResponse.ok(reportOutputService.outputRainReport(Long.parseLong(id)));
}
}
@@ -0,0 +1,67 @@
package com.gis.xian.dto;
import com.alibaba.fastjson.annotation.JSONField;
import com.fasterxml.jackson.databind.PropertyNamingStrategies;
import com.fasterxml.jackson.databind.annotation.JsonNaming;
import lombok.Data;
import java.time.LocalDateTime;
import java.util.List;
/**
* 预测条件基类 — 对应 xian_inference_result.condition JSONB 字段
*/
@Data
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
public class BaseCondition {
/**
* 点位ID列表
*/
@JSONField(name = "point_ids")
private List<Integer> pointIds;
/**
* 行政区划代码
*/
@JSONField(name = "region_code")
private String regionCode;
/**
* 事件发生时间
*/
@JSONField(name = "occurred_time", format = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime occurredTime;
/**
* 震级 (Richter)
*/
private Float magnitude;
/**
* 震源深度 (km)
*/
private Float depth;
/**
* 震中经度
*/
@JSONField(name = "epicenter_lon")
private Float epicenterLon;
/**
* 震中纬度
*/
@JSONField(name = "epicenter_lat")
private Float epicenterLat;
/**
* 累计降雨量(mm)
*/
private Float rainfall;
/**
* 降雨持续时间(h)
*/
private Float duration;
}
@@ -0,0 +1,60 @@
package com.gis.xian.dto;
import lombok.Data;
import lombok.experimental.Accessors;
import java.util.ArrayList;
import java.util.List;
/**
* 灾害风险数据——按灾害类型聚合,用于填充报告模板
*/
@Data
@Accessors(chain = true)
public class DisasterRiskData {
/** 灾害类型中文名(滑坡、泥石流、山洪、内涝、崩塌) */
private String disasterType;
/** 模板条件变量后缀(Landslide、DebrisFlow、TorrentialFlood、WaterLogging、Collapse */
private String conditionSuffix;
/** 中高风险街道(逗号分隔) */
private String highRiskStreets;
/** 影响人数下限(暂空,待公式计算) */
private String influenceLower = "";
/** 影响人数上限(暂空,待公式计算) */
private String influenceUpper = "";
/** 隐患点人口分布图完整 URL */
private String imageUrl;
/** 风险明细列表 */
private List<SpotRisk> spots = new ArrayList<>();
/**
* 是否有该类型的数据
*/
public boolean hasData() {
return spots != null && !spots.isEmpty();
}
@Data
@Accessors(chain = true)
public static class SpotRisk {
/** 序号 */
private int index;
/** 位置 */
private String position;
/** 灾害点名称 */
private String disasterName;
/** 乡镇/街道 */
private String village;
/** 发生概率(小数 0-1 */
private Double probability;
/** 灾害等级:高(>=70%) / 中(50%-70%) */
private String riskLevel;
}
}
@@ -0,0 +1,30 @@
package com.gis.xian.dto;
import com.alibaba.fastjson.annotation.JSONField;
import com.fasterxml.jackson.databind.PropertyNamingStrategies;
import com.fasterxml.jackson.databind.annotation.JsonNaming;
import lombok.Data;
@Data
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
public class RainfallDistrictSummaryResponseDTO {
/**
* 行政区名称
*/
@JSONField(name = "district_name")
private String districtName;
/**
* 行政区编码
*/
@JSONField(name = "district_code")
private String districtCode;
/**
* 雨量
*/
private String rainfall;
/**
* 持续时间
*/
@JSONField(name = "duration_hours")
private String durationHours;
}
@@ -0,0 +1,9 @@
package com.gis.xian.dto;
import lombok.Data;
@Data
public class RiskAndHiddenSpotDTO {
private String districtName;
private String number;
}
@@ -1,11 +1,15 @@
package com.gis.xian.entity;
import java.util.Date;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.time.LocalDateTime;
/**
* 推理结果表
* @TableName xian_inference_result
*/
@Data
@TableName(value = "xian_inference_result", autoResultMap = true)
public class XianInferenceResult {
/**
@@ -19,14 +23,14 @@ public class XianInferenceResult {
private String name;
/**
* 事件类型
* 事件类型rainfall / earthquake
*/
private String eventType;
/**
* 发生时间
*/
private Date occurredTime;
private LocalDateTime occurredTime;
/**
* 操作类型
@@ -36,186 +40,20 @@ public class XianInferenceResult {
/**
* 推理结果(JSONB
*/
private Object result;
private String result;
/**
* 条件(JSONB
* 预测条件(JSONB
*/
private Object condition;
/**
* 文件路径(JSONB
*/
private Object filePath;
private String condition;
/**
* 创建时间
*/
private Date createTime;
private LocalDateTime createTime;
/**
* 是否删除(0:未删除, 1:已删除)
*/
private Integer isDelete;
// 构造方法
public XianInferenceResult() {
}
public XianInferenceResult(Long id, String name, String eventType, Date occurredTime,
String operationType, Object result, Object condition,
Object filePath, Date createTime, Integer isDelete) {
this.id = id;
this.name = name;
this.eventType = eventType;
this.occurredTime = occurredTime;
this.operationType = operationType;
this.result = result;
this.condition = condition;
this.filePath = filePath;
this.createTime = createTime;
this.isDelete = isDelete;
}
// Getter 和 Setter 方法
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getEventType() {
return eventType;
}
public void setEventType(String eventType) {
this.eventType = eventType;
}
public Date getOccurredTime() {
return occurredTime;
}
public void setOccurredTime(Date occurredTime) {
this.occurredTime = occurredTime;
}
public String getOperationType() {
return operationType;
}
public void setOperationType(String operationType) {
this.operationType = operationType;
}
public Object getResult() {
return result;
}
public void setResult(Object result) {
this.result = result;
}
public Object getCondition() {
return condition;
}
public void setCondition(Object condition) {
this.condition = condition;
}
public Object getFilePath() {
return filePath;
}
public void setFilePath(Object filePath) {
this.filePath = filePath;
}
public Date getCreateTime() {
return createTime;
}
public void setCreateTime(Date createTime) {
this.createTime = createTime;
}
public Integer getIsDelete() {
return isDelete;
}
public void setIsDelete(Integer isDelete) {
this.isDelete = isDelete;
}
@Override
public boolean equals(Object that) {
if (this == that) {
return true;
}
if (that == null) {
return false;
}
if (getClass() != that.getClass()) {
return false;
}
XianInferenceResult other = (XianInferenceResult) that;
return (this.getId() == null ? other.getId() == null : this.getId().equals(other.getId()))
&& (this.getName() == null ? other.getName() == null : this.getName().equals(other.getName()))
&& (this.getEventType() == null ? other.getEventType() == null : this.getEventType().equals(other.getEventType()))
&& (this.getOccurredTime() == null ? other.getOccurredTime() == null : this.getOccurredTime().equals(other.getOccurredTime()))
&& (this.getOperationType() == null ? other.getOperationType() == null : this.getOperationType().equals(other.getOperationType()))
&& (this.getResult() == null ? other.getResult() == null : this.getResult().equals(other.getResult()))
&& (this.getCondition() == null ? other.getCondition() == null : this.getCondition().equals(other.getCondition()))
&& (this.getFilePath() == null ? other.getFilePath() == null : this.getFilePath().equals(other.getFilePath()))
&& (this.getCreateTime() == null ? other.getCreateTime() == null : this.getCreateTime().equals(other.getCreateTime()))
&& (this.getIsDelete() == null ? other.getIsDelete() == null : this.getIsDelete().equals(other.getIsDelete()));
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((getId() == null) ? 0 : getId().hashCode());
result = prime * result + ((getName() == null) ? 0 : getName().hashCode());
result = prime * result + ((getEventType() == null) ? 0 : getEventType().hashCode());
result = prime * result + ((getOccurredTime() == null) ? 0 : getOccurredTime().hashCode());
result = prime * result + ((getOperationType() == null) ? 0 : getOperationType().hashCode());
result = prime * result + ((getResult() == null) ? 0 : getResult().hashCode());
result = prime * result + ((getCondition() == null) ? 0 : getCondition().hashCode());
result = prime * result + ((getFilePath() == null) ? 0 : getFilePath().hashCode());
result = prime * result + ((getCreateTime() == null) ? 0 : getCreateTime().hashCode());
result = prime * result + ((getIsDelete() == null) ? 0 : getIsDelete().hashCode());
return result;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append(getClass().getSimpleName());
sb.append(" [");
sb.append("Hash = ").append(hashCode());
sb.append(", id=").append(id);
sb.append(", name=").append(name);
sb.append(", eventType=").append(eventType);
sb.append(", occurredTime=").append(occurredTime);
sb.append(", operationType=").append(operationType);
sb.append(", result=").append(result);
sb.append(", condition=").append(condition);
sb.append(", filePath=").append(filePath);
sb.append(", createTime=").append(createTime);
sb.append(", isDelete=").append(isDelete);
sb.append("]");
return sb.toString();
}
}
@@ -0,0 +1,26 @@
package com.gis.xian.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.time.LocalDateTime;
/**
* 推理结果文件表
*/
@Data
@TableName(value = "xian_inference_result_file", autoResultMap = true)
public class XianInferenceResultFile {
private Long id;
private Long inferenceId;
private String filePath;
private String fileName;
private LocalDateTime createTime;
private Integer isDelete;
}
@@ -0,0 +1,134 @@
package com.gis.xian.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import java.io.Serializable;
import java.time.LocalDateTime;
import lombok.Data;
/**
* 西安市气象局-实况-小时实况
* @TableName xian_meteorology
*/
@TableName(value ="xian_meteorology")
@Data
public class XianMeteorology implements Serializable {
/**
* 主键ID,自增
*/
@TableId(value = "id")
private Long id;
/**
* 区站名称
*/
@TableField(value = "station_name")
private String stationName;
/**
* 水平能见度(人工)
*/
@TableField(value = "visibility")
private Double visibility;
/**
* 区域编码
*/
@TableField(value = "area_code")
private String areaCode;
/**
* 最大风速
*/
@TableField(value = "max_wind_speed")
private Double maxWindSpeed;
/**
* 过去1小时降水量,单位:毫米
*/
@TableField(value = "rainfall_1h")
private Double rainfall1h;
/**
* 气压
*/
@TableField(value = "pressure")
private Double pressure;
/**
* 最大风速的风向
*/
@TableField(value = "max_wind_direction")
private Double maxWindDirection;
/**
* 极大风速的风向
*/
@TableField(value = "inst_max_wind_direction")
private Double instMaxWindDirection;
/**
* 区站号
*/
@TableField(value = "station_id")
private String stationId;
/**
* 温度/气温,单位:摄氏度
*/
@TableField(value = "temperature")
private Double temperature;
/**
* 相对湿度
*/
@TableField(value = "relative_humidity")
private Double relativeHumidity;
/**
* 日期(加8小时为数据对应时间)
*/
@TableField(value = "datetime")
private Long datetime;
/**
* 经度
*/
@TableField(value = "lon")
private Double lon;
/**
* 纬度
*/
@TableField(value = "lat")
private Double lat;
/**
* 地理位置点
*/
@TableField(value = "geom")
private Object geom;
/**
* 创建时间
*/
@TableField(value = "create_time")
private LocalDateTime createTime;
/**
* 更新时间
*/
@TableField(value = "update_time")
private LocalDateTime updateTime;
/**
* 逻辑删除标识,0未删除,1已删除
*/
@TableField(value = "is_delete")
private Integer isDelete;
@TableField(exist = false)
private static final long serialVersionUID = 1L;
}
@@ -1,5 +1,6 @@
package com.gis.xian.mapper;
import com.gis.xian.dto.RiskAndHiddenSpotDTO;
import com.gis.xian.entity.XianHiddenDangerSpots;
import java.util.List;
@@ -13,6 +14,7 @@ import java.util.List;
public interface XianHiddenDangerSpotsMapper {
/**
* 获取所有基础点:滑坡、泥石流、山洪、内涝
*
* @param disasterType 具体灾害类型(landslide-滑坡, debris_flow-泥石流, flash_flood-山洪, water_logging-内涝),可选
* @return 基础点列表
*/
@@ -20,10 +22,27 @@ public interface XianHiddenDangerSpotsMapper {
/**
* 根据id获取隐患点详情
*
* @param id 隐患点id
* @return 隐患点详情
*/
XianHiddenDangerSpots getPointDetailById(Long id);
/**
* 根据行政区获取隐患点数量
*
* @param districtNames 行政名称列表
* @return 隐患点数量
*/
List<RiskAndHiddenSpotDTO> queryHiddenDangerNumberByDistrictName(List<String> districtNames);
/**
* 根据id获取隐患点详情
*
* @param ids 隐患点id列表
* @return 隐患点详情
*/
List<XianHiddenDangerSpots> getPointDetailByIds(List<Long> ids);
}
@@ -0,0 +1,26 @@
package com.gis.xian.mapper;
import com.gis.xian.entity.XianInferenceResultFile;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
* @description 针对表【xian_inference_result_file(推理结果文件表)】的数据库操作Mapper
*/
@Mapper
public interface XianInferenceResultFileMapper {
/**
* 根据推理结果id获取文件列表
*/
List<XianInferenceResultFile> selectByInferenceId(@Param("inferenceId") Long inferenceId);
/**
* 插入或更新(按 inference_id + file_name 唯一约束 upsert
*/
int upsert(@Param("inferenceId") Long inferenceId,
@Param("filePath") String filePath,
@Param("fileName") String fileName);
}
@@ -1,5 +1,6 @@
package com.gis.xian.mapper;
import com.gis.xian.entity.XianInferenceResult;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
@@ -13,9 +14,25 @@ import org.apache.ibatis.annotations.Param;
public interface XianInferenceResultMapper {
/**
* 根据id和pointId获取概率
*
* @param id 预测结果id
* @param pointId 隐患点/风险点id和类型
* @return 预测概率
*/
String getProbabilityByIdAndPointId(@Param("id") Long id, @Param("pointId") String pointId);
/**
* 根据id获取预测结果
*
* @param id 预测结果id
* @return 预测结果
*/
XianInferenceResult selectById(Long id);
/**
* 获取预测结果数量
*
* @return 预测结果数量
*/
Integer getTheNumberOfPredictedPoints();
}
@@ -0,0 +1,11 @@
package com.gis.xian.mapper;
/**
* @author wzy
* @description 针对表【xian_meteorology(西安市气象局-实况-小时实况)】的数据库操作Mapper
* @createDate 2026-06-27 10:28:34
* @Entity com.gis.xian.entity.XianMeteorology
*/
public interface XianMeteorologyMapper {
}
@@ -1,5 +1,6 @@
package com.gis.xian.mapper;
import com.gis.xian.dto.RiskAndHiddenSpotDTO;
import com.gis.xian.entity.XianRiskSpots;
import java.util.List;
@@ -13,6 +14,7 @@ import java.util.List;
public interface XianRiskSpotsMapper {
/**
* 获取所有风险点基础信息
*
* @return 风险点基础列表
*/
List<XianRiskSpots> getBasePoints();
@@ -20,10 +22,19 @@ public interface XianRiskSpotsMapper {
/**
* 根据id获取风险点详情
*
* @param id 风险点id
* @return 风险点详情
*/
XianRiskSpots getPointDetailById(Long id);
/**
* 根据行政区获取风险点数量
*
* @param districtNames 行政名称列表
* @return 风险点数量
*/
List<RiskAndHiddenSpotDTO> queryRiskNumberByDistrictName(List<String> districtNames);
}
@@ -1,15 +0,0 @@
package com.gis.xian.service;
/**
* 专题图触发接口
* 只负责调用 Python 端,不返回结果
*/
public interface IFeignService {
/**
* 触发专题图生成
* @param simulationId 模拟ID
* @param type 灾害类型(earthquake / rain
*/
void trigger(String simulationId, String type);
}
@@ -0,0 +1,10 @@
package com.gis.xian.service;
public interface ReportOutputService {
/**
* 产出暴雨报告
* @param id 模拟id
* @return 报告链接
*/
String outputRainReport(Long id);
}
@@ -0,0 +1,22 @@
package com.gis.xian.service.ex;
public class ReportParameterException extends ServiceException {
public ReportParameterException() {
}
public ReportParameterException(String message) {
super(message);
}
public ReportParameterException(String message, Throwable cause) {
super(message, cause);
}
public ReportParameterException(Throwable cause) {
super(cause);
}
public ReportParameterException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
super(message, cause, enableSuppression, writableStackTrace);
}
}
@@ -1,49 +0,0 @@
package com.gis.xian.service.impl;
import com.gis.xian.service.IFeignService;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestClient;
import java.util.Map;
/**
* 专题图触发服务
* 只负责调用 Python QGIS 服务,不处理计算和存库
*/
@Slf4j
@Service
public class FeignServiceImpl implements IFeignService {
@Resource
private RestClient.Builder restClientBuilder;
@Value("${qgis.url}")
private String qgisUrl;
@Override
public void trigger(String simulationId, String type) {
if (simulationId == null || simulationId.isBlank()) {
log.error("触发参数为空,simulationId={}", simulationId);
return;
}
log.info("触发专题图生成: simulationId={}, type={}", simulationId, type);
try {
RestClient client = restClientBuilder.build();
String result = client.post()
.uri(qgisUrl)
.contentType(MediaType.APPLICATION_JSON)
.body(Map.of("simulationId", simulationId, "type", type))
.retrieve()
.body(String.class);
log.info("Python 端响应: {}", result);
} catch (Exception e) {
log.error("调用 Python QGIS 服务失败: {}", e.getMessage(), e);
}
}
}
@@ -0,0 +1,482 @@
package com.gis.xian.service.impl;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.TypeReference;
import com.deepoove.poi.XWPFTemplate;
import com.deepoove.poi.config.Configure;
import com.deepoove.poi.data.PictureRenderData;
import com.deepoove.poi.data.Pictures;
import com.deepoove.poi.policy.TableRenderPolicy;
import com.gis.xian.config.AlgorithmClient;
import com.gis.xian.config.ReportProperties;
import com.gis.xian.domain.ApiResponse;
import com.gis.xian.dto.BaseCondition;
import com.gis.xian.dto.DisasterRiskData;
import com.gis.xian.dto.RainfallDistrictSummaryResponseDTO;
import com.gis.xian.dto.RiskAndHiddenSpotDTO;
import com.gis.xian.entity.XianHiddenDangerSpots;
import com.gis.xian.entity.XianInferenceResult;
import com.gis.xian.entity.XianInferenceResultFile;
import com.gis.xian.mapper.XianHiddenDangerSpotsMapper;
import com.gis.xian.mapper.XianInferenceResultFileMapper;
import com.gis.xian.mapper.XianInferenceResultMapper;
import com.gis.xian.mapper.XianRiskSpotsMapper;
import com.gis.xian.service.ReportOutputService;
import com.gis.xian.utils.DateTimeUtils;
import com.gis.xian.utils.ReportTableBuilder;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.stereotype.Service;
import java.io.FileOutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
@Service
@Slf4j
public class IReportOutputServiceImpl implements ReportOutputService {
/**
* 暴雨灾害类型 → 模板变量后缀
*/
private static final Map<String, String> DISASTER_CONDITION_MAP = new LinkedHashMap<>();
static {
DISASTER_CONDITION_MAP.put("滑坡", "Landslide");
DISASTER_CONDITION_MAP.put("泥石流", "DebrisFlow");
DISASTER_CONDITION_MAP.put("山洪", "TorrentialFlood");
DISASTER_CONDITION_MAP.put("内涝", "WaterLogging");
DISASTER_CONDITION_MAP.put("崩塌", "Collapse");
}
/**
* 图片命名模式
*/
private static final String IMAGE_NAME_PATTERN = "暴雨%s潜在隐患点及人口分布图.jpg";
@Value("${files.local.path}")
private String localPath;
@Value("${files.server.url}")
private String fileServerPath;
@Resource
private XianInferenceResultMapper inferenceResultMapper;
@Resource
private XianHiddenDangerSpotsMapper hiddenDangerSpotsMapper;
@Resource
private XianRiskSpotsMapper riskSpotsMapper;
@Resource
private XianInferenceResultFileMapper inferenceResultFileMapper;
@Resource
private AlgorithmClient algorithmClient;
@Resource
private ReportProperties reportProperties;
@Override
public String outputRainReport(Long id) {
// 获取模拟数据
XianInferenceResult sim = inferenceResultMapper.selectById(id);
Map<String, Double> probabilities = JSON.parseObject(sim.getResult(), new TypeReference<>() {
});
// 获取所有隐患点详情
List<Long> pointIds = extractPointIds(probabilities.keySet());
List<XianHiddenDangerSpots> allSpots = pointIds.isEmpty()
? Collections.emptyList()
: hiddenDangerSpotsMapper.getPointDetailByIds(pointIds);
// 按灾害类型分组并构建风险数据
List<XianInferenceResultFile> files = inferenceResultFileMapper.selectByInferenceId(id);
List<DisasterRiskData> disasterDataList = buildDisasterDataList(allSpots, probabilities, files);
// 获取降雨概况
List<RainfallDistrictSummaryResponseDTO> rainData = fetchRainData();
List<String> allDistricts = allDistrictNames(rainData);
List<String> top3Districts = top3DistrictNames(rainData);
// 获取风险统计
int riskCount = sumCount(riskSpotsMapper.queryRiskNumberByDistrictName(top3Districts));
int hiddenCount = sumCount(hiddenDangerSpotsMapper.queryHiddenDangerNumberByDistrictName(top3Districts));
int predictPointNum = sumCount(riskSpotsMapper.queryRiskNumberByDistrictName(allDistricts))
+ sumCount(hiddenDangerSpotsMapper.queryHiddenDangerNumberByDistrictName(allDistricts));
// 构建模板数据模型
Map<String, Object> model = buildTemplateModel(sim, disasterDataList, rainData,
allDistricts, top3Districts, riskCount, hiddenCount, predictPointNum);
// 渲染输出
String templatePath = reportProperties.getDisasterCausingFactors().getRainfall().getTemplatePath();
String outputDir = Path.of(localPath,
reportProperties.getDisasterCausingFactors().getRainfall().getOutputPath()
.replace("{id}", id.toString())).toString();
String outputName = reportProperties.getDisasterCausingFactors().getRainfall().getOutputName()
.replace("{currentTime}", String.valueOf(System.currentTimeMillis()));
String fullPath = renderReport(templatePath, outputDir, outputName, model);
// 写入文件记录
String baseName = outputName.replaceAll("_\\d+(\\.\\w+)$", "$1");
String relativePath = fullPath.substring(localPath.length()).replace("\\", "/").replaceAll("^/+", "");
inferenceResultFileMapper.upsert(id, relativePath, baseName);
log.info("文件记录已写入: inference_id={}, file_name={}, file_path={}", id, baseName, relativePath);
return relativePath;
}
/**
* 从 JSONB key 集合中提取隐患点 ID
*/
private List<Long> extractPointIds(Set<String> keys) {
return keys.stream()
.map(k -> Long.parseLong(k.split("_")[0]))
.distinct()
.collect(Collectors.toList());
}
/**
* 调用算法服务获取各区降雨概况
*/
private List<RainfallDistrictSummaryResponseDTO> fetchRainData() {
ApiResponse<List<RainfallDistrictSummaryResponseDTO>> resp = algorithmClient.post(
"/rainfall/district-summary",
Map.of("inference_id", 1),
new ParameterizedTypeReference<>() {
});
return resp != null ? resp.getData() : Collections.emptyList();
}
/**
* 全部行政区名称(按降雨量降序)
*/
private List<String> allDistrictNames(List<RainfallDistrictSummaryResponseDTO> rainData) {
return rainData.stream()
.filter(d -> d.getDistrictName() != null && !d.getDistrictName().isEmpty())
.filter(d -> d.getRainfall() != null && !d.getRainfall().isEmpty())
.sorted((a, b) -> Double.compare(
Double.parseDouble(b.getRainfall()),
Double.parseDouble(a.getRainfall())))
.map(RainfallDistrictSummaryResponseDTO::getDistrictName)
.collect(Collectors.toList());
}
/**
* 降雨量前3行政区
*/
private List<String> top3DistrictNames(List<RainfallDistrictSummaryResponseDTO> rainData) {
return rainData.stream()
.filter(d -> d.getRainfall() != null && !d.getRainfall().isEmpty())
.sorted((a, b) -> Double.compare(
Double.parseDouble(b.getRainfall()),
Double.parseDouble(a.getRainfall())))
.limit(3)
.map(RainfallDistrictSummaryResponseDTO::getDistrictName)
.collect(Collectors.toList());
}
/**
* 对DTO列表中的number字段求和
*/
private int sumCount(List<RiskAndHiddenSpotDTO> list) {
return list.stream().mapToInt(d -> Integer.parseInt(d.getNumber())).sum();
}
/**
* 将隐患点按disasterType分组,构建各类型的DisasterRiskData
*/
private List<DisasterRiskData> buildDisasterDataList(
List<XianHiddenDangerSpots> allSpots,
Map<String, Double> probabilities,
List<XianInferenceResultFile> files) {
// 按灾害类型分组
Map<String, List<XianHiddenDangerSpots>> grouped = allSpots.stream()
.filter(s -> s.getDisasterType() != null)
.collect(Collectors.groupingBy(XianHiddenDangerSpots::getDisasterType, LinkedHashMap::new, Collectors.toList()));
List<DisasterRiskData> result = new ArrayList<>();
for (Map.Entry<String, List<XianHiddenDangerSpots>> entry : grouped.entrySet()) {
String disasterType = entry.getKey();
String suffix = DISASTER_CONDITION_MAP.get(disasterType);
if (suffix == null) continue;
DisasterRiskData data = new DisasterRiskData()
.setDisasterType(disasterType)
.setConditionSuffix(suffix)
.setInfluenceLower("")
.setInfluenceUpper("");
// 构建风险明细(按概率降序)
List<DisasterRiskData.SpotRisk> spots = entry.getValue().stream()
.map(spot -> {
Double prob = probabilities.get(spot.getId() + "_1");
if (prob == null) return null;
String level = prob >= 70 ? "" : "";
return new DisasterRiskData.SpotRisk()
.setPosition(spot.getPosition())
.setDisasterName(spot.getDisasterName())
.setVillage(spot.getVillage())
.setProbability(prob)
.setRiskLevel(level);
})
.filter(Objects::nonNull)
.sorted(Comparator.comparing(DisasterRiskData.SpotRisk::getProbability).reversed())
.collect(Collectors.toList());
// 重新编号
AtomicInteger reIdx = new AtomicInteger(1);
spots.forEach(s -> s.setIndex(reIdx.getAndIncrement()));
data.setSpots(spots);
// 中高风险街道(village 去重)
data.setHighRiskStreets(spots.stream()
.map(DisasterRiskData.SpotRisk::getVillage)
.filter(Objects::nonNull)
.distinct()
.collect(Collectors.joining("")));
// 图片
data.setImageUrl(findImage(files, disasterType));
result.add(data);
}
return result;
}
/**
* 从文件列表中匹配图片
*/
private String findImage(List<XianInferenceResultFile> files, String disasterType) {
String expectedName = String.format(IMAGE_NAME_PATTERN, disasterType);
return files.stream()
.filter(f -> expectedName.equals(f.getFileName()))
.findFirst()
.map(f -> {
String server = reportProperties.getFiles().getServerUrl().replaceAll("/+$", "");
String path = f.getFilePath().replace("\\", "/").replaceAll("^/+", "");
return server + "/" + path;
})
.orElse("");
}
/**
* 构建模板数据模型
*
* @param sim 模拟结果
* @param disasterDataList 灾害数据
* @param rainData 降雨数据
* @param allDistricts 所有灾害名称
* @param top3Districts top3灾害名称
* @param riskCount 灾害数量
* @param hiddenCount 隐患数量
* @param predictPointNum 预测点数
* @return 模板数据模型
*/
private Map<String, Object> buildTemplateModel(
XianInferenceResult sim,
List<DisasterRiskData> disasterDataList,
List<RainfallDistrictSummaryResponseDTO> rainData,
List<String> allDistricts,
List<String> top3Districts,
int riskCount,
int hiddenCount,
int predictPointNum) {
Map<String, Object> m = new HashMap<>();
BaseCondition cond = JSON.parseObject(sim.getCondition(), BaseCondition.class);
// 基础文本
m.put("报告时间", DateTimeUtils.datetimeFormat(sim.getOccurredTime(), "MM月dd日HH时mm分"));
m.put("降雨时间", DateTimeUtils.datetimeFormat(sim.getOccurredTime(), "yyyy年MM月dd日HH时"));
m.put("降雨地区", String.join("", allDistricts));
m.put("持续时间", cond.getDuration() == null ? 24 : cond.getDuration());
m.put("降雨量", buildRainfallStr(rainData, allDistricts));
m.put("降雨集中区域", String.join("", top3Districts));
m.put("风险区数量", riskCount);
m.put("隐患点数量", hiddenCount);
m.put("致灾因子", String.join("", reportProperties.getDisasterCausingFactors().getRainfall().getType()) + "");
m.put("致灾因子数量", reportProperties.getDisasterCausingFactors().getRainfall().getNumber());
m.put("预测点数量", predictPointNum);
m.put("灾害链", buildDisasterChain("暴雨", disasterDataList));
m.put("具体风险地区", buildRiskPosition(disasterDataList));
m.put("高风险区域数量", countHighRisk(disasterDataList));
m.put("次生灾害类型", buildSecondaryTypes(disasterDataList));
// 所有灾害类型默认值
for (String typeName : DISASTER_CONDITION_MAP.keySet()) {
String suffix = DISASTER_CONDITION_MAP.get(typeName);
m.put("has" + suffix, false);
}
// 各灾害类型(覆盖有数据的)
for (DisasterRiskData data : disasterDataList) {
String suffix = data.getConditionSuffix();
boolean has = data.hasData();
m.put("has" + suffix, has);
if (has) {
m.put(data.getDisasterType() + "中高风险街道", data.getHighRiskStreets());
m.put(data.getDisasterType() + "风险表",
ReportTableBuilder.buildRiskTable(data.getDisasterType(), data.getSpots()));
m.put(data.getDisasterType() + "隐患点人口分布图",
downloadImage(data.getImageUrl()));
m.put(data.getDisasterType() + "影响人数下限", data.getInfluenceLower());
m.put(data.getDisasterType() + "影响人数上限", data.getInfluenceUpper());
}
}
// 全局条件
boolean hasAny = disasterDataList.stream().anyMatch(DisasterRiskData::hasData);
m.put("hasDisasters", hasAny);
// 应急处置建议
m.put("高风险街道", buildAllHighRiskPosition(disasterDataList));
return m;
}
/**
* 构建降雨量字符串
*
* @param rainData 降雨数据
* @param names 位置
* @return 降雨量字符串
*/
private String buildRainfallStr(List<RainfallDistrictSummaryResponseDTO> rainData, List<String> names) {
Map<String, String> map = rainData.stream().collect(Collectors.toMap(
RainfallDistrictSummaryResponseDTO::getDistrictName,
RainfallDistrictSummaryResponseDTO::getRainfall,
(a, b) -> a));
return names.stream().map(map::get).filter(Objects::nonNull)
.collect(Collectors.joining("mm、")) + "mm";
}
/**
* 构建灾害链字符串
*
* @param list 灾害数据
* @return 灾害链
*/
private String buildDisasterChain(String type, List<DisasterRiskData> list) {
return list.stream().filter(DisasterRiskData::hasData)
.map(d -> type + "-" + d.getDisasterType())
.collect(Collectors.joining(""));
}
/**
* 构建高风险位置字符串
*
* @param list 灾害数据
* @return 高风险位置
*/
private String buildRiskPosition(List<DisasterRiskData> list) {
return list.stream().filter(DisasterRiskData::hasData)
.map(DisasterRiskData::getHighRiskStreets)
.filter(s -> !s.isEmpty())
.collect(Collectors.joining(""));
}
/**
* 统计高风险数量
*
* @param list 灾害数据
* @return 高风险数量
*/
private int countHighRisk(List<DisasterRiskData> list) {
return (int) list.stream().filter(DisasterRiskData::hasData)
.flatMap(d -> d.getSpots().stream())
.filter(s -> "".equals(s.getRiskLevel()))
.count();
}
/**
* 构建次生灾害类型字符串
*
* @param list 灾害数据
* @return 次生灾害类型
*/
private String buildSecondaryTypes(List<DisasterRiskData> list) {
return list.stream().filter(DisasterRiskData::hasData)
.map(DisasterRiskData::getDisasterType)
.collect(Collectors.joining(""));
}
/**
* 构建所有高风险位置字符串
*
* @param list 灾害数据
* @return 所有高风险位置
*/
private String buildAllHighRiskPosition(List<DisasterRiskData> list) {
return list.stream().filter(DisasterRiskData::hasData)
.flatMap(d -> d.getSpots().stream())
.map(DisasterRiskData.SpotRisk::getVillage)
.filter(Objects::nonNull)
.distinct()
.collect(Collectors.joining(""));
}
/**
* 渲染报告
*
* @param templatePath 模板路径
* @param outputPath 输出路径
* @param outputName 输出文件名
* @param model 模板数据
* @return 输出文件路径
*/
private String renderReport(String templatePath, String outputPath, String outputName, Map<String, Object> model) {
// 注册表格策略
Configure config = Configure.builder()
.bind("滑坡风险表", new TableRenderPolicy())
.bind("泥石流风险表", new TableRenderPolicy())
.bind("山洪风险表", new TableRenderPolicy())
.bind("内涝风险表", new TableRenderPolicy())
.bind("崩塌风险表", new TableRenderPolicy())
.build();
try {
Path dir = Path.of(outputPath);
if (!Files.exists(dir)) Files.createDirectories(dir);
Path output = dir.resolve(outputName);
XWPFTemplate template = XWPFTemplate.compile(Path.of(templatePath).toFile(), config)
.render(model);
try (FileOutputStream fos = new FileOutputStream(output.toFile())) {
template.write(fos);
}
template.close();
log.info("报告生成成功: {}", Path.of(outputPath, outputName).toString());
return output.toString();
} catch (Exception e) {
log.error("生成暴雨报告失败", e);
throw new RuntimeException("生成暴雨报告失败: " + e.getMessage(), e);
}
}
/**
* 从 URL 下载图片
*/
private PictureRenderData downloadImage(String imageUrl) {
if (imageUrl == null || imageUrl.isEmpty()) return null;
try {
return Pictures.ofUrl(imageUrl).size(500, 375).create();
} catch (Exception e) {
log.warn("下载图片失败, 使用占位: {}", imageUrl, e);
return null;
}
}
}
@@ -5,7 +5,8 @@ import com.gis.xian.mapper.*;
import com.gis.xian.vo.*;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import com.gis.xian.config.BasePointsRedisProperties;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.event.EventListener;
import org.springframework.data.redis.core.RedisTemplate;
@@ -13,6 +14,7 @@ import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
/**
* 初始化数据
@@ -57,53 +59,12 @@ public class InitializeData {
@Resource
RedisTemplate<String, Object> redisTemplate;
@Value("${init.data.base-points.hidden-danger.all}")
private String allBasePointsKey;
@Resource
@Qualifier("xianPool")
private Executor xianPoolExecutor;
@Value("${init.data.base-points.hidden-danger.landslide}")
private String landslideKey;
@Value("${init.data.base-points.hidden-danger.collapse}")
private String collapseKey;
@Value("${init.data.base-points.hidden-danger.debris-flow}")
private String debrisFlowKey;
@Value("${init.data.base-points.hidden-danger.flash-flood}")
private String flashFloodKey;
@Value("${init.data.base-points.hidden-danger.water-logging}")
private String waterLoggingKey;
@Value("${init.data.base-points.risk}")
private String riskBasePointsKey;
@Value("${init.data.base-points.hospitals}")
private String hospitalsBasePointsKey;
@Value("${init.data.base-points.dangerous-source}")
private String dangerousSourceBasePointsKey;
@Value("${init.data.base-points.emergency-shelter}")
private String emergencyShelterBasePointsKey;
@Value("${init.data.base-points.firefighter}")
private String firefighterBasePointsKey;
@Value("${init.data.base-points.store-points}")
private String storePointsBasePointsKey;
@Value("${init.data.base-points.school}")
private String schoolBasePointsKey;
@Value("${init.data.base-points.bridge}")
private String bridgeBasePointsKey;
@Value("${init.data.base-points.reservoir}")
private String reservoirBasePointsKey;
@Value("${init.data.base-points.subway}")
private String subwayBasePointsKey;
@Resource
private BasePointsRedisProperties basePoints;
@EventListener(ApplicationReadyEvent.class)
@Async("xianPool")
@@ -113,158 +74,158 @@ public class InitializeData {
// 隐患点 - 总体
CompletableFuture<Void> allFuture = CompletableFuture.runAsync(() -> {
redisTemplate.opsForValue().set(allBasePointsKey, JSON.toJSONString(
redisTemplate.opsForValue().set(basePoints.getHiddenDanger().getAll(), JSON.toJSONString(
XianHiddenDangerSpotsBasePointVo.entity2Vo(
xianHiddenDangerSpotsMapper.getBasePoints(null))
)
);
log.info("加载隐患点信息(总体)并写入redis完成");
});
}, xianPoolExecutor);
// 隐患点 - 滑坡
CompletableFuture<Void> landslideFuture = CompletableFuture.runAsync(() -> {
redisTemplate.opsForValue().set(landslideKey, JSON.toJSONString(
redisTemplate.opsForValue().set(basePoints.getHiddenDanger().getLandslide(), JSON.toJSONString(
XianHiddenDangerSpotsBasePointVo.entity2Vo(
xianHiddenDangerSpotsMapper.getBasePoints("landslide"))
)
);
log.info("加载隐患点信息(滑坡)并写入redis完成");
});
}, xianPoolExecutor);
// 隐患点 - 崩塌
CompletableFuture<Void> collapseFuture = CompletableFuture.runAsync(() -> {
redisTemplate.opsForValue().set(collapseKey, JSON.toJSONString(
redisTemplate.opsForValue().set(basePoints.getHiddenDanger().getCollapse(), JSON.toJSONString(
XianHiddenDangerSpotsBasePointVo.entity2Vo(
xianHiddenDangerSpotsMapper.getBasePoints("collapse"))
)
);
log.info("加载隐患点信息(崩塌)并写入redis完成");
});
}, xianPoolExecutor);
// 隐患点 - 泥石流
CompletableFuture<Void> debrisFlowFuture = CompletableFuture.runAsync(() -> {
redisTemplate.opsForValue().set(debrisFlowKey, JSON.toJSONString(
redisTemplate.opsForValue().set(basePoints.getHiddenDanger().getDebrisFlow(), JSON.toJSONString(
XianHiddenDangerSpotsBasePointVo.entity2Vo(
xianHiddenDangerSpotsMapper.getBasePoints("debris_flow"))
)
);
log.info("加载隐患点信息(泥石流)并写入redis完成");
});
}, xianPoolExecutor);
// 隐患点 - 山洪
CompletableFuture<Void> flashFloodFuture = CompletableFuture.runAsync(() -> {
redisTemplate.opsForValue().set(flashFloodKey, JSON.toJSONString(
redisTemplate.opsForValue().set(basePoints.getHiddenDanger().getFlashFlood(), JSON.toJSONString(
XianHiddenDangerSpotsBasePointVo.entity2Vo(
xianHiddenDangerSpotsMapper.getBasePoints("flash_flood"))
)
);
log.info("加载隐患点信息(山洪)并写入redis完成");
});
}, xianPoolExecutor);
// 隐患点 - 内涝
CompletableFuture<Void> waterLoggingFuture = CompletableFuture.runAsync(() -> {
redisTemplate.opsForValue().set(waterLoggingKey, JSON.toJSONString(
redisTemplate.opsForValue().set(basePoints.getHiddenDanger().getWaterLogging(), JSON.toJSONString(
XianHiddenDangerSpotsBasePointVo.entity2Vo(
xianHiddenDangerSpotsMapper.getBasePoints("water_logging"))
)
);
log.info("加载隐患点信息(内涝)并写入redis完成");
});
}, xianPoolExecutor);
CompletableFuture<Void> riskFuture = CompletableFuture.runAsync(() -> {
redisTemplate.opsForValue().set(riskBasePointsKey, JSON.toJSONString(
redisTemplate.opsForValue().set(basePoints.getRisk(), JSON.toJSONString(
XianRiskSpotsBasePointVo.entity2Vo(
xianRiskSpotsMapper.getBasePoints())
)
);
log.info("加载风险点基本信息写入redis完成");
});
}, xianPoolExecutor);
CompletableFuture<Void> hospitalsFuture = CompletableFuture.runAsync(() -> {
redisTemplate.opsForValue().set(hospitalsBasePointsKey, JSON.toJSONString(
redisTemplate.opsForValue().set(basePoints.getHospitals(), JSON.toJSONString(
XianHospitalsBasePointVo.entity2Vo(
xianHospitalsMapper.getBasePoints())
)
);
log.info("加载医院基本信息写入redis完成");
});
}, xianPoolExecutor);
CompletableFuture<Void> dangerousSourceFuture = CompletableFuture.runAsync(() -> {
redisTemplate.opsForValue().set(dangerousSourceBasePointsKey, JSON.toJSONString(
redisTemplate.opsForValue().set(basePoints.getDangerousSource(), JSON.toJSONString(
XianDangerousSourceBasePointVo.entity2Vo(
xianDangerousSourceMapper.getBasePoints())
)
);
log.info("加载危险源基本信息写入redis完成");
});
}, xianPoolExecutor);
CompletableFuture<Void> emergencyShelterFuture = CompletableFuture.runAsync(() -> {
redisTemplate.opsForValue().set(emergencyShelterBasePointsKey, JSON.toJSONString(
redisTemplate.opsForValue().set(basePoints.getEmergencyShelter(), JSON.toJSONString(
XianEmergencyShelterBasePointVo.entity2Vo(
xianEmergencyShelterMapper.getBasePoints())
)
);
log.info("加载应急避难所基本信息写入redis完成");
});
}, xianPoolExecutor);
CompletableFuture<Void> firefighterFuture = CompletableFuture.runAsync(() -> {
redisTemplate.opsForValue().set(firefighterBasePointsKey, JSON.toJSONString(
redisTemplate.opsForValue().set(basePoints.getFirefighter(), JSON.toJSONString(
XianFirefighterBasePointVo.entity2Vo(
xianFirefighterMapper.getBasePoints())
)
);
log.info("加载消防站基本信息写入redis完成");
});
}, xianPoolExecutor);
CompletableFuture<Void> storePointsFuture = CompletableFuture.runAsync(() -> {
redisTemplate.opsForValue().set(storePointsBasePointsKey, JSON.toJSONString(
redisTemplate.opsForValue().set(basePoints.getStorePoints(), JSON.toJSONString(
XianStorePointsBasePointVo.entity2Vo(
xianStorePointsMapper.getBasePoints())
)
);
log.info("加载物资储备点基本信息写入redis完成");
});
}, xianPoolExecutor);
CompletableFuture<Void> schoolFuture = CompletableFuture.runAsync(() -> {
redisTemplate.opsForValue().set(schoolBasePointsKey, JSON.toJSONString(
redisTemplate.opsForValue().set(basePoints.getSchool(), JSON.toJSONString(
XianSchoolBasePointVo.entity2Vo(
xianSchoolMapper.getBasePoints())
)
);
log.info("加载学校基本信息写入redis完成");
});
}, xianPoolExecutor);
CompletableFuture<Void> bridgeFuture = CompletableFuture.runAsync(() -> {
redisTemplate.opsForValue().set(bridgeBasePointsKey, JSON.toJSONString(
redisTemplate.opsForValue().set(basePoints.getBridge(), JSON.toJSONString(
XianBridgeBasePointVo.entity2Vo(
xianBridgeMapper.getBasePoints())
)
);
log.info("加载桥梁基本信息写入redis完成");
});
}, xianPoolExecutor);
CompletableFuture<Void> reservoirFuture = CompletableFuture.runAsync(() -> {
redisTemplate.opsForValue().set(reservoirBasePointsKey, JSON.toJSONString(
redisTemplate.opsForValue().set(basePoints.getReservoir(), JSON.toJSONString(
XianReservoirListBasePointVo.entity2Vo(
xianReservoirListMapper.getBasePoints())
)
);
log.info("加载水库基本信息写入redis完成");
});
}, xianPoolExecutor);
CompletableFuture<Void> subwayFuture = CompletableFuture.runAsync(() -> {
redisTemplate.opsForValue().set(subwayBasePointsKey, JSON.toJSONString(
redisTemplate.opsForValue().set(basePoints.getSubway(), JSON.toJSONString(
XianSubwayStationsBasePointVo.entity2Vo(
xianSubwayStationsMapper.getBasePoints())
)
);
log.info("加载地铁站点基本信息写入redis完成");
});
}, xianPoolExecutor);
// 等待所有任务完成
CompletableFuture.allOf(
allFuture, landslideFuture, debrisFlowFuture,
allFuture, landslideFuture, collapseFuture, debrisFlowFuture,
flashFloodFuture, waterLoggingFuture,
riskFuture, hospitalsFuture,
dangerousSourceFuture, emergencyShelterFuture, firefighterFuture, storePointsFuture, schoolFuture,
@@ -0,0 +1,18 @@
package com.gis.xian.utils;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
public class DateTimeUtils {
/**
* 格式化日期时间
*
* @param datetime 日期时间
* @param format 格式
* @return 格式化后的日期时间
*/
public static String datetimeFormat(LocalDateTime datetime, String format) {
return datetime.format(DateTimeFormatter.ofPattern(format));
}
}
@@ -0,0 +1,67 @@
package com.gis.xian.utils;
import com.deepoove.poi.data.RowRenderData;
import com.deepoove.poi.data.Rows;
import com.deepoove.poi.data.TableRenderData;
import com.deepoove.poi.data.Tables;
import com.deepoove.poi.data.style.TableStyle;
import com.gis.xian.dto.DisasterRiskData;
import java.text.DecimalFormat;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
/**
* poi-tl 报告表格构建工具
*/
public class ReportTableBuilder {
private static final DecimalFormat PCT_FMT = new DecimalFormat("0.00");
/**
* 小四号 = 12pt = 24 half-pts
*/
private static final int XIAO_SI_FONT = 10;
/**
* 列宽(twips):序号窄 / 位置最宽 / 概率中等 / 等级中等
*/
private static final int[] COL_WIDTHS = {700, 5000, 1500, 1500};
/**
* 表格总宽 = 列宽之和
*/
private static final String TABLE_WIDTH = "8700";
/**
* 构建灾害风险表(四列:序号、位置、发生概率、灾害等级)
*/
public static TableRenderData buildRiskTable(String disasterType, List<DisasterRiskData.SpotRisk> spots) {
// 山洪第二列用灾害点名称,其他用位置
boolean useDisasterName = "山洪".equals(disasterType);
String col2Header = useDisasterName ? "灾害名称" : "位置";
RowRenderData header = Rows.of(
"序号",
col2Header,
disasterType + "发生概率",
"灾害等级"
).textFontFamily("黑体").textFontSize(XIAO_SI_FONT).textBold().center().verticalCenter().create();
Tables.TableBuilder builder = Tables.of(header);
AtomicInteger idx = new AtomicInteger(1);
for (DisasterRiskData.SpotRisk spot : spots) {
String col2 = useDisasterName ? spot.getDisasterName() : spot.getPosition();
builder.addRow(Rows.of(
String.valueOf(idx.getAndIncrement()),
col2,
PCT_FMT.format(spot.getProbability()) + "%",
spot.getRiskLevel()
).textFontFamily("仿宋_GB2312").textFontSize(XIAO_SI_FONT).center().verticalCenter().create());
}
TableRenderData table = builder.create();
TableStyle style = new TableStyle();
style.setWidth(TABLE_WIDTH);
style.setColWidths(COL_WIDTHS);
table.setTableStyle(style);
return table;
}
}
@@ -1,682 +0,0 @@
package com.gis.xian.utils;
import org.springframework.util.AntPathMatcher;
import java.util.*;
/**
* @author zzw
* @description: 字符串工具类 (基于RuoYi风格扩展)
* @date 2026/5/26 上午9:25
*/
public class StringUtils extends org.apache.commons.lang3.StringUtils {
/** 空字符串 */
private static final String NULLSTR = "";
/** 下划线 */
private static final char SEPARATOR = '_';
/** 星号 */
private static final char ASTERISK = '*';
/**
* 获取参数不为空值
*
* @param value defaultValue 要判断的value
* @return value 返回值
*/
public static <T> T nvl(T value, T defaultValue)
{
return value != null ? value : defaultValue;
}
/**
* * 判断一个Collection是否为空, 包含ListSetQueue
*
* @param coll 要判断的Collection
* @return true:为空 false:非空
*/
public static boolean isEmpty(Collection<?> coll)
{
return isNull(coll) || coll.isEmpty();
}
/**
* * 判断一个Collection是否非空,包含ListSetQueue
*
* @param coll 要判断的Collection
* @return true:非空 false:空
*/
public static boolean isNotEmpty(Collection<?> coll)
{
return !isEmpty(coll);
}
/**
* * 判断一个对象数组是否为空
*
* @param objects 要判断的对象数组
** @return true:为空 false:非空
*/
public static boolean isEmpty(Object[] objects)
{
return isNull(objects) || (objects.length == 0);
}
/**
* * 判断一个对象数组是否非空
*
* @param objects 要判断的对象数组
* @return true:非空 false:空
*/
public static boolean isNotEmpty(Object[] objects)
{
return !isEmpty(objects);
}
/**
* * 判断一个Map是否为空
*
* @param map 要判断的Map
* @return true:为空 false:非空
*/
public static boolean isEmpty(Map<?, ?> map)
{
return isNull(map) || map.isEmpty();
}
/**
* * 判断一个Map是否为空
*
* @param map 要判断的Map
* @return true:非空 false:空
*/
public static boolean isNotEmpty(Map<?, ?> map)
{
return !isEmpty(map);
}
/**
* * 判断一个字符串是否为空串
*
* @param str String
* @return true:为空 false:非空
*/
public static boolean isEmpty(String str)
{
return isNull(str) || NULLSTR.equals(str.trim());
}
/**
* * 判断一个字符串是否为非空串
*
* @param str String
* @return true:非空串 false:空串
*/
public static boolean isNotEmpty(String str)
{
return !isEmpty(str);
}
/**
* * 判断一个对象是否为空
*
* @param object Object
* @return true:为空 false:非空
*/
public static boolean isNull(Object object)
{
return object == null;
}
/**
* * 判断一个对象是否非空
*
* @param object Object
* @return true:非空 false:空
*/
public static boolean isNotNull(Object object)
{
return !isNull(object);
}
/**
* * 判断一个对象是否是数组类型(Java基本型别的数组)
*
* @param object 对象
* @return true:是数组 false:不是数组
*/
public static boolean isArray(Object object)
{
return isNotNull(object) && object.getClass().isArray();
}
/**
* 去空格
*/
public static String trim(String str)
{
return (str == null ? "" : str.trim());
}
/**
* 替换指定字符串的指定区间内字符为"*"
*
* @param str 字符串
* @param startInclude 开始位置(包含)
* @param endExclude 结束位置(不包含)
* @return 替换后的字符串
*/
public static String hide(CharSequence str, int startInclude, int endExclude)
{
if (isEmpty(str))
{
return NULLSTR;
}
final int strLength = str.length();
if (startInclude > strLength)
{
return NULLSTR;
}
if (endExclude > strLength)
{
endExclude = strLength;
}
if (startInclude > endExclude)
{
// 如果起始位置大于结束位置,不替换
return NULLSTR;
}
final char[] chars = new char[strLength];
for (int i = 0; i < strLength; i++)
{
if (i >= startInclude && i < endExclude)
{
chars[i] = ASTERISK;
}
else
{
chars[i] = str.charAt(i);
}
}
return new String(chars);
}
/**
* 截取字符串
*
* @param str 字符串
* @param start 开始
* @return 结果
*/
public static String substring(final String str, int start)
{
if (str == null)
{
return NULLSTR;
}
if (start < 0)
{
start = str.length() + start;
}
if (start < 0)
{
start = 0;
}
if (start > str.length())
{
return NULLSTR;
}
return str.substring(start);
}
/**
* 截取字符串
*
* @param str 字符串
* @param start 开始
* @param end 结束
* @return 结果
*/
public static String substring(final String str, int start, int end)
{
if (str == null)
{
return NULLSTR;
}
if (end < 0)
{
end = str.length() + end;
}
if (start < 0)
{
start = str.length() + start;
}
if (end > str.length())
{
end = str.length();
}
if (start > end)
{
return NULLSTR;
}
if (start < 0)
{
start = 0;
}
if (end < 0)
{
end = 0;
}
return str.substring(start, end);
}
/**
* 在字符串中查找第一个出现的 `open` 和最后一个出现的 `close` 之间的子字符串
*
* @param str 要截取的字符串
* @param open 起始字符串
* @param close 结束字符串
* @return 截取结果
*/
public static String substringBetweenLast(final String str, final String open, final String close)
{
if (isEmpty(str) || isEmpty(open) || isEmpty(close))
{
return NULLSTR;
}
final int start = str.indexOf(open);
if (start != INDEX_NOT_FOUND)
{
final int end = str.lastIndexOf(close);
if (end != INDEX_NOT_FOUND)
{
return str.substring(start + open.length(), end);
}
}
return NULLSTR;
}
/**
* 判断是否为空,并且不是空白字符
*
* @param str 要判断的value
* @return 结果
*/
public static boolean hasText(String str)
{
return (str != null && !str.isEmpty() && containsText(str));
}
private static boolean containsText(CharSequence str)
{
int strLen = str.length();
for (int i = 0; i < strLen; i++)
{
if (!Character.isWhitespace(str.charAt(i)))
{
return true;
}
}
return false;
}
/**
* 字符串转set
*
* @param str 字符串
* @param sep 分隔符
* @return set集合
*/
public static final Set<String> str2Set(String str, String sep)
{
return new HashSet<String>(str2List(str, sep, true, false));
}
/**
* 字符串转list
*
* @param str 字符串
* @param sep 分隔符
* @return list集合
*/
public static final List<String> str2List(String str, String sep)
{
return str2List(str, sep, true, false);
}
/**
* 字符串转list
*
* @param str 字符串
* @param sep 分隔符
* @param filterBlank 过滤纯空白
* @param trim 去掉首尾空白
* @return list集合
*/
public static final List<String> str2List(String str, String sep, boolean filterBlank, boolean trim)
{
List<String> list = new ArrayList<String>();
if (StringUtils.isEmpty(str))
{
return list;
}
// 过滤空白字符串
if (filterBlank && StringUtils.isBlank(str))
{
return list;
}
String[] split = str.split(sep);
for (String string : split)
{
if (filterBlank && StringUtils.isBlank(string))
{
continue;
}
if (trim)
{
string = string.trim();
}
list.add(string);
}
return list;
}
/**
* 判断给定的collection列表中是否包含数组array 判断给定的数组array中是否包含给定的元素value
*
* @param collection 给定的集合
* @param array 给定的数组
* @return boolean 结果
*/
public static boolean containsAny(Collection<String> collection, String... array)
{
if (isEmpty(collection) || isEmpty(array))
{
return false;
}
else
{
for (String str : array)
{
if (collection.contains(str))
{
return true;
}
}
return false;
}
}
/**
* 查找指定字符串是否包含指定字符串列表中的任意一个字符串同时串忽略大小写
*
* @param cs 指定字符串
* @param searchCharSequences 需要检查的字符串数组
* @return 是否包含任意一个字符串
*/
public static boolean containsAnyIgnoreCase(CharSequence cs, CharSequence... searchCharSequences)
{
if (isEmpty(cs) || isEmpty(searchCharSequences))
{
return false;
}
for (CharSequence testStr : searchCharSequences)
{
if (containsIgnoreCase(cs, testStr))
{
return true;
}
}
return false;
}
/**
* 驼峰转下划线命名
*/
public static String toUnderScoreCase(String str)
{
if (str == null)
{
return null;
}
StringBuilder sb = new StringBuilder();
// 前置字符是否大写
boolean preCharIsUpperCase = true;
// 当前字符是否大写
boolean curreCharIsUpperCase = true;
// 下一字符是否大写
boolean nexteCharIsUpperCase = true;
for (int i = 0; i < str.length(); i++)
{
char c = str.charAt(i);
if (i > 0)
{
preCharIsUpperCase = Character.isUpperCase(str.charAt(i - 1));
}
else
{
preCharIsUpperCase = false;
}
curreCharIsUpperCase = Character.isUpperCase(c);
if (i < (str.length() - 1))
{
nexteCharIsUpperCase = Character.isUpperCase(str.charAt(i + 1));
}
if (preCharIsUpperCase && curreCharIsUpperCase && !nexteCharIsUpperCase)
{
sb.append(SEPARATOR);
}
else if ((i != 0 && !preCharIsUpperCase) && curreCharIsUpperCase)
{
sb.append(SEPARATOR);
}
sb.append(Character.toLowerCase(c));
}
return sb.toString();
}
/**
* 是否包含字符串
*
* @param str 验证字符串
* @param strs 字符串组
* @return 包含返回true
*/
public static boolean inStringIgnoreCase(String str, String... strs)
{
if (str != null && strs != null)
{
for (String s : strs)
{
if (str.equalsIgnoreCase(trim(s)))
{
return true;
}
}
}
return false;
}
/**
* 将下划线大写方式命名的字符串转换为驼峰式。如果转换前的下划线大写方式命名的字符串为空,则返回空字符串。 例如:HELLO_WORLD->HelloWorld
*
* @param name 转换前的下划线大写方式命名的字符串
* @return 转换后的驼峰式命名的字符串
*/
public static String convertToCamelCase(String name)
{
StringBuilder result = new StringBuilder();
// 快速检查
if (name == null || name.isEmpty())
{
// 没必要转换
return "";
}
else if (!name.contains("_"))
{
// 不含下划线,仅将首字母大写
return name.substring(0, 1).toUpperCase() + name.substring(1);
}
// 用下划线将原始字符串分割
String[] camels = name.split("_");
for (String camel : camels)
{
// 跳过原始字符串中开头、结尾的下换线或双重下划线
if (camel.isEmpty())
{
continue;
}
// 首字母大写
result.append(camel.substring(0, 1).toUpperCase());
result.append(camel.substring(1).toLowerCase());
}
return result.toString();
}
/**
* 驼峰式命名法
* 例如:user_name->userName
*/
public static String toCamelCase(String s)
{
if (s == null)
{
return null;
}
if (s.indexOf(SEPARATOR) == -1)
{
return s;
}
s = s.toLowerCase();
StringBuilder sb = new StringBuilder(s.length());
boolean upperCase = false;
for (int i = 0; i < s.length(); i++)
{
char c = s.charAt(i);
if (c == SEPARATOR)
{
upperCase = true;
}
else if (upperCase)
{
sb.append(Character.toUpperCase(c));
upperCase = false;
}
else
{
sb.append(c);
}
}
return sb.toString();
}
/**
* 查找指定字符串是否匹配指定字符串列表中的任意一个字符串
*
* @param str 指定字符串
* @param strs 需要检查的字符串数组
* @return 是否匹配
*/
public static boolean matches(String str, List<String> strs)
{
if (isEmpty(str) || isEmpty(strs))
{
return false;
}
for (String pattern : strs)
{
if (isMatch(pattern, str))
{
return true;
}
}
return false;
}
/**
* 判断url是否与规则配置:
* ? 表示单个字符;
* * 表示一层路径内的任意字符串,不可跨层级;
* ** 表示任意层路径;
*
* @param pattern 匹配规则
* @param url 需要匹配的url
* @return
*/
public static boolean isMatch(String pattern, String url)
{
AntPathMatcher matcher = new AntPathMatcher();
return matcher.match(pattern, url);
}
@SuppressWarnings("unchecked")
public static <T> T cast(Object obj)
{
return (T) obj;
}
/**
* 数字左边补齐0,使之达到指定长度。注意,如果数字转换为字符串后,长度大于size,则只保留 最后size个字符。
*
* @param num 数字对象
* @param size 字符串指定长度
* @return 返回数字的字符串格式,该字符串为指定长度。
*/
public static final String padl(final Number num, final int size)
{
return padl(num.toString(), size, '0');
}
/**
* 字符串左补齐。如果原始字符串s长度大于size,则只保留最后size个字符。
*
* @param s 原始字符串
* @param size 字符串指定长度
* @param c 用于补齐的字符
* @return 返回指定长度的字符串,由原字符串左补齐或截取得到。
*/
public static final String padl(final String s, final int size, final char c)
{
final StringBuilder sb = new StringBuilder(size);
if (s != null)
{
final int len = s.length();
if (s.length() <= size)
{
for (int i = size - len; i > 0; i--)
{
sb.append(c);
}
sb.append(s);
}
else
{
return s.substring(len - size, len);
}
}
else
{
for (int i = size; i > 0; i--)
{
sb.append(c);
}
}
return sb.toString();
}
}
+3 -12
View File
@@ -1,13 +1,8 @@
# 端口
server:
port: 8081
# 开发环境配置
spring:
config:
import:
- classpath:config/database/application-database-dev.yml
- classpath:config/qgis/application-qgis-dev.yml
- classpath:config/customize/application-customize-dev.yml
@@ -46,17 +41,13 @@ safety:
- /druid
- /websocket/info
- /websocket/**
- /open/**
- /report/output/**
# 请求无需解密的路径
no-decrypt-paths:
- /crypto/sm2/public-key
- /druid
- /websocket/info
- /websocket/**
- /open/**
- /report/output/**
# 算法服务器配置
algorithm:
server:
# 开发环境算法服务器地址
url: http://localhost:8082
-11
View File
@@ -1,13 +1,8 @@
# 端口
server:
port: 8080
# 生产环境配置
spring:
config:
import:
- classpath:config/database/application-database-prod.yml
- classpath:config/qgis/application-qgis-prod.yml
- classpath:config/customize/application-customize-prod.yml
# redis
@@ -49,9 +44,3 @@ safety:
no-decrypt-paths:
- /crypto/sm2/public-key
- /websocket/**
# 算法服务器配置
algorithm:
server:
# 生产环境算法服务器地址
url: http://localhost:8081
+24
View File
@@ -13,3 +13,27 @@ mybatis-plus:
map-underscore-to-camel-case: true
# 添加 TypeHandler 扫描包路径
type-handlers-package: com.gis.xian.handler
# 报告配置
report:
files:
server-url: ${files.server.url:http://localhost:8082}
disaster-causing-factors:
rainfall:
type: [ "降雨强度", "持续时间", "累计降雨量", "高程", "坡度", "坡向", "土壤分类", "岩性", "土地利用类型", "不透水率", "植被覆盖指数", "距河道距离", "管网密度" ]
number: 19
template-path: "src/main/resources/template/rainfall/rainfall-template.docx"
output-path: "/xian/report/rainfall/{id}"
output-name: "暴雨应急预评估报告_{currentTime}.docx"
earthquake:
type: [ "震度", "震源深度", "距离震中位置", "高程", "坡度", "坡向", "土壤分类", "岩性", "土地利用类型", "植被覆盖指数", "距离断裂带距离" ]
number: 17
template-path: "src/main/resources/template/earthquake/earthquake-template.docx"
output-path: "/xian/report/earthquake/{id}"
output-name: "地震应急预评估报告_{currentTime}.docx"
disaster-types:
landslide: 滑坡
debrisFlow: 泥石流
torrentialFlood: 山洪
waterLogging: 内涝
collapse: 崩塌
@@ -25,6 +25,11 @@
<result property="isDelete" column="is_delete"/>
</resultMap>
<resultMap id="RiskAndHiddenSpotDTOMap" type="com.gis.xian.dto.RiskAndHiddenSpotDTO">
<result property="districtName" column="county"/>
<result property="number" column="number"/>
</resultMap>
<!-- 获取所有基础点:滑坡、崩塌、泥石流、山洪、内涝 -->
<select id="getBasePoints" resultMap="XianHiddenDangerSpotsResultMap">
SELECT id, disaster_name, lon, lat, disaster_type FROM xian_hidden_danger_spots
@@ -55,10 +60,42 @@
<!-- 根据id获取隐患点详情 -->
<select id="getPointDetailById" resultMap="XianHiddenDangerSpotsResultMap">
SELECT id, field_code, disaster_name, position, disaster_type, scale_grade, risk_grade FROM xian_hidden_danger_spots
SELECT id, field_code, disaster_name, position, disaster_type, scale_grade, risk_grade FROM
xian_hidden_danger_spots
<where>
id = #{id}
</where>
AND is_delete = 0
</where>
</select>
<select id="queryHiddenDangerNumberByDistrictName" resultMap="RiskAndHiddenSpotDTOMap">
SELECT county, COUNT(*) AS number FROM xian_hidden_danger_spots
<where>
is_delete = 0
<if test="districtNames != null and districtNames.size() > 0">
AND county IN(
<foreach collection="districtNames" item="districtName" separator=",">
#{districtName}
</foreach>
)
</if>
</where>
GROUP BY county
</select>
<select id="getPointDetailByIds" resultMap="XianHiddenDangerSpotsResultMap">
SELECT id, disaster_name, disaster_type, village, position, risk_grade FROM
xian_hidden_danger_spots
<where>
<if test="ids != null and ids.size() > 0">
id IN(
<foreach collection="ids" item="id" separator=",">
#{id}
</foreach>
)
</if>
AND is_delete = 0
</where>
</select>
</mapper>
@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.gis.xian.mapper.XianInferenceResultFileMapper">
<resultMap id="XianInferenceResultFileResultMap" type="com.gis.xian.entity.XianInferenceResultFile">
<id property="id" column="id"/>
<result property="inferenceId" column="inference_id"/>
<result property="filePath" column="file_path"/>
<result property="fileName" column="file_name"/>
<result property="createTime" column="create_time"/>
<result property="isDelete" column="is_delete"/>
</resultMap>
<select id="selectByInferenceId" resultMap="XianInferenceResultFileResultMap">
SELECT file_path, file_name
FROM xian_inference_result_file
WHERE inference_id = #{inferenceId}
AND is_delete = 0
ORDER BY create_time
</select>
<insert id="upsert">
INSERT INTO xian_inference_result_file (inference_id, file_path, file_name, create_time, is_delete)
VALUES (#{inferenceId}, #{filePath}, #{fileName}, NOW(), 0)
ON CONFLICT (inference_id, file_name)
DO UPDATE SET file_path = #{filePath}, create_time = NOW()
</insert>
</mapper>
@@ -12,7 +12,6 @@
<result property="operationType" column="operation_type"/>
<result property="result" column="result"/>
<result property="condition" column="condition"/>
<result property="filePath" column="file_path" />
<result property="createTime" column="create_time"/>
<result property="isDelete" column="is_delete"/>
</resultMap>
@@ -25,4 +24,28 @@
</where>
</select>
<select id="selectById" resultMap="XianInferenceResultResultMap">
SELECT
id, name, event_type, occurred_time, operation_type,
(
SELECT jsonb_object_agg(key, value)
FROM jsonb_each(result)
WHERE value::numeric >= 50
AND key LIKE '%\_1' ESCAPE '\'
) AS result
, condition
FROM xian_inference_result
<where>
id = #{id}
AND is_delete = 0
</where>
</select>
<select id="getTheNumberOfPredictedPoints" resultType="java.lang.Integer">
SELECT COUNT(*) FROM xian_inference_result
<where>
is_delete = 0
</where>
</select>
</mapper>
@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.gis.xian.mapper.XianMeteorologyMapper">
<resultMap id="BaseResultMap" type="com.gis.xian.entity.XianMeteorology">
<id property="id" column="id" jdbcType="BIGINT"/>
<result property="stationName" column="station_name" jdbcType="VARCHAR"/>
<result property="visibility" column="visibility" jdbcType="DOUBLE"/>
<result property="areaCode" column="area_code" jdbcType="VARCHAR"/>
<result property="maxWindSpeed" column="max_wind_speed" jdbcType="DOUBLE"/>
<result property="rainfall1h" column="rainfall_1h" jdbcType="DOUBLE"/>
<result property="pressure" column="pressure" jdbcType="DOUBLE"/>
<result property="maxWindDirection" column="max_wind_direction" jdbcType="DOUBLE"/>
<result property="instMaxWindDirection" column="inst_max_wind_direction" jdbcType="DOUBLE"/>
<result property="stationId" column="station_id" jdbcType="VARCHAR"/>
<result property="temperature" column="temperature" jdbcType="DOUBLE"/>
<result property="relativeHumidity" column="relative_humidity" jdbcType="DOUBLE"/>
<result property="datetime" column="datetime" jdbcType="BIGINT"/>
<result property="lon" column="lon" jdbcType="DOUBLE"/>
<result property="lat" column="lat" jdbcType="DOUBLE"/>
<result property="geom" column="geom" jdbcType="OTHER"/>
<result property="createTime" column="create_time" jdbcType="TIMESTAMP"/>
<result property="updateTime" column="update_time" jdbcType="TIMESTAMP"/>
<result property="isDelete" column="is_delete" jdbcType="SMALLINT"/>
</resultMap>
</mapper>
@@ -29,6 +29,11 @@
<result property="isDelete" column="is_delete"/>
</resultMap>
<resultMap id="RiskAndHiddenSpotDTOMap" type="com.gis.xian.dto.RiskAndHiddenSpotDTO">
<result property="districtName" column="county"/>
<result property="number" column="number"/>
</resultMap>
<!-- 获取所有风险点基础信息 -->
<select id="getBasePoints" resultMap="XianRiskSpotsResultMap">
SELECT id, risk_name, lon, lat FROM xian_risk_spots
@@ -39,11 +44,26 @@
<!-- 根据id获取风险点信息 -->
<select id="getPointDetailById" resultMap="XianRiskSpotsResultMap">
SELECT id, risk_name, unit_code, position, resident_counts, risk_property, permanent_population, housing, inspector_name, inspector_tele, lon, lat FROM xian_risk_spots
SELECT id, risk_name, unit_code, position, resident_counts, risk_property, permanent_population, housing,
inspector_name, inspector_tele, lon, lat FROM xian_risk_spots
<where>
id = #{id} AND is_delete = 0
</where>
</select>
<select id="queryRiskNumberByDistrictName" resultMap="RiskAndHiddenSpotDTOMap">
SELECT county, COUNT(*) AS number FROM xian_risk_spots
<where>
is_delete = 0
<if test="districtNames != null and districtNames.size() > 0">
AND county IN(
<foreach collection="districtNames" item="districtName" separator=",">
#{districtName}
</foreach>
)
</if>
</where>
GROUP BY county
</select>
</mapper>
@@ -1,8 +1,16 @@
# qgis配置
qgis:
# 地震专题图模板路径
eq-maps-template-path: D:/代码/xian_api_new/src/main/resources/template/qgis-template/eq/
# 暴雨专题图模板路径
rain-maps-template-path: D:/代码/xian_api_new/src/main/resources/template/qgis-template/rain/
# QGIS基础路径
base-path: F:/files/xian/maps/
# 端口
server:
port: 8081
algorithm:
server:
url: "http://localhost:8082"
connect-timeout: 5
read-timeout: 120
# 文件配置
files:
server:
url: "http://localhost:8083"
local:
path: "G:/files"
@@ -1,8 +1,17 @@
# qgis配置
qgis:
# 地震专题图模板路径
eq-maps-template-path: D:/代码/xian_api_new/src/main/resources/template/qgis-template/eq/
# 暴雨专题图模板路径
rain-maps-template-path: D:/代码/xian_api_new/src/main/resources/template/qgis-template/rain/
# QGIS基础路径
base-path: F:/files/xian/maps/
# 端口
server:
port: 8080
# 算法服务器配置
algorithm:
server:
url: "http://localhost:8081"
connect-timeout: 5
read-timeout: 120
# 文件配置
files:
server:
url: "http://localhost:8082"
local:
path: "/data/files"
@@ -1,3 +0,0 @@
# qgis配置
qgis:
url: http://localhost:18998/qgis/make/map
@@ -1,3 +0,0 @@
# qgis配置
qgis:
url: http://localhost:18998/qgis/make/map