From 2ef96f812f6d4233867fc4ebc9de5f2b4f62a822 Mon Sep 17 00:00:00 2001 From: wzy-warehouse <18135009705@163.com> Date: Sun, 28 Jun 2026 11:27:33 +0800 Subject: [PATCH] =?UTF-8?q?=E9=83=A8=E5=88=86=E6=9A=B4=E9=9B=A8=E7=81=BE?= =?UTF-8?q?=E5=AE=B3=E9=93=BE=E6=8A=A5=E5=91=8A=E4=BA=A7=E5=87=BA=EF=BC=8C?= =?UTF-8?q?=E8=A1=A8=E6=A0=BC=E4=BF=A1=E6=81=AF=E5=89=8D=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pom.xml | 8 + .../com/gis/xian/config/AlgorithmClient.java | 182 ++++++++++++++ .../config/AlgorithmServerProperties.java | 10 + .../com/gis/xian/config/ReportProperties.java | 26 ++ .../controller/AlgorithmProxyController.java | 199 +-------------- .../controller/ReportOutputController.java | 27 ++ .../java/com/gis/xian/dto/BaseCondition.java | 67 +++++ .../RainfallDistrictSummaryResponseDTO.java | 30 +++ .../gis/xian/dto/RiskAndHiddenSpotDTO.java | 9 + .../gis/xian/entity/XianInferenceResult.java | 188 +------------- .../com/gis/xian/entity/XianMeteorology.java | 134 ++++++++++ .../mapper/XianHiddenDangerSpotsMapper.java | 19 ++ .../mapper/XianInferenceResultMapper.java | 19 +- .../xian/mapper/XianMeteorologyMapper.java | 11 + .../gis/xian/mapper/XianRiskSpotsMapper.java | 21 +- .../gis/xian/service/ReportOutputService.java | 10 + .../service/ex/ReportParameterException.java | 22 ++ .../impl/IReportOutputServiceImpl.java | 238 ++++++++++++++++++ .../com/gis/xian/task/InitializeData.java | 2 +- .../com/gis/xian/utils/DateTimeUtils.java | 18 ++ src/main/resources/application.yml | 10 + .../mapper/XianHiddenDangerSpotsMapper.xml | 77 ++++-- .../xian/mapper/XianInferenceResultMapper.xml | 43 +++- .../gis/xian/mapper/XianMeteorologyMapper.xml | 29 +++ .../gis/xian/mapper/XianRiskSpotsMapper.xml | 66 +++-- .../customize/application-customize-dev.yml | 4 +- .../customize/application-customize-prod.yml | 4 +- .../template/rainfall/rainfall-template.docx | Bin 0 -> 19874 bytes .../template/rainfall/~$infall-template.docx | Bin 0 -> 162 bytes 29 files changed, 1048 insertions(+), 425 deletions(-) create mode 100644 src/main/java/com/gis/xian/config/AlgorithmClient.java create mode 100644 src/main/java/com/gis/xian/config/ReportProperties.java create mode 100644 src/main/java/com/gis/xian/controller/ReportOutputController.java create mode 100644 src/main/java/com/gis/xian/dto/BaseCondition.java create mode 100644 src/main/java/com/gis/xian/dto/RainfallDistrictSummaryResponseDTO.java create mode 100644 src/main/java/com/gis/xian/dto/RiskAndHiddenSpotDTO.java create mode 100644 src/main/java/com/gis/xian/entity/XianMeteorology.java create mode 100644 src/main/java/com/gis/xian/mapper/XianMeteorologyMapper.java create mode 100644 src/main/java/com/gis/xian/service/ReportOutputService.java create mode 100644 src/main/java/com/gis/xian/service/ex/ReportParameterException.java create mode 100644 src/main/java/com/gis/xian/service/impl/IReportOutputServiceImpl.java create mode 100644 src/main/java/com/gis/xian/utils/DateTimeUtils.java create mode 100644 src/main/resources/com/gis/xian/mapper/XianMeteorologyMapper.xml create mode 100644 src/main/resources/template/rainfall/rainfall-template.docx create mode 100644 src/main/resources/template/rainfall/~$infall-template.docx diff --git a/pom.xml b/pom.xml index 766d675..303ae36 100644 --- a/pom.xml +++ b/pom.xml @@ -13,6 +13,7 @@ 1.82 2.0.60 3.5.9 + 1.12.2 @@ -120,6 +121,13 @@ spring-boot-starter-aop + + + com.deepoove + poi-tl + ${poi-tl-version} + + org.springframework.boot diff --git a/src/main/java/com/gis/xian/config/AlgorithmClient.java b/src/main/java/com/gis/xian/config/AlgorithmClient.java new file mode 100644 index 0000000..5a597ca --- /dev/null +++ b/src/main/java/com/gis/xian/config/AlgorithmClient.java @@ -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 proxyGet(HttpServletRequest request) { + return proxyRequest(request, HttpMethod.GET); + } + + public ApiResponse proxyPost(HttpServletRequest request) { + return proxyRequest(request, HttpMethod.POST); + } + + public ApiResponse proxyPut(HttpServletRequest request) { + return proxyRequest(request, HttpMethod.PUT); + } + + public ApiResponse proxyDelete(HttpServletRequest request) { + return proxyRequest(request, HttpMethod.DELETE); + } + + public ApiResponse proxyPatch(HttpServletRequest request) { + return proxyRequest(request, HttpMethod.PATCH); + } + + @SuppressWarnings("unchecked") + private ApiResponse 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 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 get(String path, Class responseType) { + String url = props.getUrl() + path; + log.info("GET {}", url); + return restClient.get().uri(url).retrieve().body(responseType); + } + + public T get(String path, ParameterizedTypeReference responseType) { + String url = props.getUrl() + path; + log.info("GET {}", url); + return restClient.get().uri(url).retrieve().body(responseType); + } + + public T post(String path, Object body, Class responseType) { + String url = props.getUrl() + path; + log.info("POST {} body={}", url, body); + return restClient.post().uri(url).body(body).retrieve().body(responseType); + } + + public T post(String path, Object body, ParameterizedTypeReference 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 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 parseResponseBody(String body) { + if (body == null || body.isEmpty()) { + return ApiResponse.ok(null); + } + try { + Map 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); + } +} diff --git a/src/main/java/com/gis/xian/config/AlgorithmServerProperties.java b/src/main/java/com/gis/xian/config/AlgorithmServerProperties.java index 10cdbf8..94c5f5e 100644 --- a/src/main/java/com/gis/xian/config/AlgorithmServerProperties.java +++ b/src/main/java/com/gis/xian/config/AlgorithmServerProperties.java @@ -16,4 +16,14 @@ public class AlgorithmServerProperties { * 算法服务器地址 */ private String url; + + /** + * 连接超时(秒) + */ + private int connectTimeout; + + /** + * 读取超时(秒) + */ + private int readTimeout; } diff --git a/src/main/java/com/gis/xian/config/ReportProperties.java b/src/main/java/com/gis/xian/config/ReportProperties.java new file mode 100644 index 0000000..e851b01 --- /dev/null +++ b/src/main/java/com/gis/xian/config/ReportProperties.java @@ -0,0 +1,26 @@ +package com.gis.xian.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +import java.util.List; + +@Data +@Component +@ConfigurationProperties(prefix = "report") +public class ReportProperties { + private DisasterCausingFactors disasterCausingFactors; + + @Data + public static class DisasterCausingFactors { + private FactorConfig rainfall; + private FactorConfig earthquake; + } + + @Data + public static class FactorConfig { + private List type; + private Integer number; + } +} diff --git a/src/main/java/com/gis/xian/controller/AlgorithmProxyController.java b/src/main/java/com/gis/xian/controller/AlgorithmProxyController.java index 9d405d9..368c8db 100644 --- a/src/main/java/com/gis/xian/controller/AlgorithmProxyController.java +++ b/src/main/java/com/gis/xian/controller/AlgorithmProxyController.java @@ -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 proxyGet(HttpServletRequest request) { - return proxyRequest(request, HttpMethod.GET); + return algorithmClient.proxyGet(request); } - /** - * 处理所有 POST 请求 - */ @PostMapping("/**") - public ApiResponse proxyPost(HttpServletRequest request) throws IOException { - return proxyRequest(request, HttpMethod.POST); + public ApiResponse proxyPost(HttpServletRequest request) { + return algorithmClient.proxyPost(request); } - /** - * 处理所有 PUT 请求 - */ @PutMapping("/**") - public ApiResponse proxyPut(HttpServletRequest request) throws IOException { - return proxyRequest(request, HttpMethod.PUT); + public ApiResponse proxyPut(HttpServletRequest request) { + return algorithmClient.proxyPut(request); } - /** - * 处理所有 DELETE 请求 - */ @DeleteMapping("/**") public ApiResponse proxyDelete(HttpServletRequest request) { - return proxyRequest(request, HttpMethod.DELETE); + return algorithmClient.proxyDelete(request); } - /** - * 处理所有 PATCH 请求 - */ @PatchMapping("/**") - public ApiResponse proxyPatch(HttpServletRequest request) throws IOException { - return proxyRequest(request, HttpMethod.PATCH); - } - - /** - * 通用的请求代理方法 - */ - @SuppressWarnings("unchecked") - private ApiResponse 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 entity = new HttpEntity<>(requestBody, headers); - - // 发送请求到算法服务器 - ResponseEntity 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 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 headerNames = request.getHeaderNames(); - while (headerNames.hasMoreElements()) { - String headerName = headerNames.nextElement(); - // 排除一些不应该转发的头 - if (!shouldExcludeHeader(headerName)) { - Enumeration 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 proxyPatch(HttpServletRequest request) { + return algorithmClient.proxyPatch(request); } } diff --git a/src/main/java/com/gis/xian/controller/ReportOutputController.java b/src/main/java/com/gis/xian/controller/ReportOutputController.java new file mode 100644 index 0000000..d84fb34 --- /dev/null +++ b/src/main/java/com/gis/xian/controller/ReportOutputController.java @@ -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 rainfall(@PathVariable String id) { + return ApiResponse.ok(reportOutputService.outputRainReport(Long.parseLong(id))); + } +} diff --git a/src/main/java/com/gis/xian/dto/BaseCondition.java b/src/main/java/com/gis/xian/dto/BaseCondition.java new file mode 100644 index 0000000..cac3f73 --- /dev/null +++ b/src/main/java/com/gis/xian/dto/BaseCondition.java @@ -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 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; +} diff --git a/src/main/java/com/gis/xian/dto/RainfallDistrictSummaryResponseDTO.java b/src/main/java/com/gis/xian/dto/RainfallDistrictSummaryResponseDTO.java new file mode 100644 index 0000000..345913e --- /dev/null +++ b/src/main/java/com/gis/xian/dto/RainfallDistrictSummaryResponseDTO.java @@ -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; +} diff --git a/src/main/java/com/gis/xian/dto/RiskAndHiddenSpotDTO.java b/src/main/java/com/gis/xian/dto/RiskAndHiddenSpotDTO.java new file mode 100644 index 0000000..1666ec7 --- /dev/null +++ b/src/main/java/com/gis/xian/dto/RiskAndHiddenSpotDTO.java @@ -0,0 +1,9 @@ +package com.gis.xian.dto; + +import lombok.Data; + +@Data +public class RiskAndHiddenSpotDTO { + private String districtName; + private String number; +} diff --git a/src/main/java/com/gis/xian/entity/XianInferenceResult.java b/src/main/java/com/gis/xian/entity/XianInferenceResult.java index 21bf03d..397a3fb 100644 --- a/src/main/java/com/gis/xian/entity/XianInferenceResult.java +++ b/src/main/java/com/gis/xian/entity/XianInferenceResult.java @@ -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: 已删除) + * 是否删除(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(); - } } diff --git a/src/main/java/com/gis/xian/entity/XianMeteorology.java b/src/main/java/com/gis/xian/entity/XianMeteorology.java new file mode 100644 index 0000000..2e09f8b --- /dev/null +++ b/src/main/java/com/gis/xian/entity/XianMeteorology.java @@ -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; +} \ No newline at end of file diff --git a/src/main/java/com/gis/xian/mapper/XianHiddenDangerSpotsMapper.java b/src/main/java/com/gis/xian/mapper/XianHiddenDangerSpotsMapper.java index b1beddc..1ab8677 100644 --- a/src/main/java/com/gis/xian/mapper/XianHiddenDangerSpotsMapper.java +++ b/src/main/java/com/gis/xian/mapper/XianHiddenDangerSpotsMapper.java @@ -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 queryHiddenDangerNumberByDistrictName(List districtNames); + + /** + * 根据id获取隐患点详情 + * + * @param ids 隐患点id列表 + * @return 隐患点详情 + */ + List getPointDetailByIds(List ids); } diff --git a/src/main/java/com/gis/xian/mapper/XianInferenceResultMapper.java b/src/main/java/com/gis/xian/mapper/XianInferenceResultMapper.java index 41b4f02..898fdb7 100644 --- a/src/main/java/com/gis/xian/mapper/XianInferenceResultMapper.java +++ b/src/main/java/com/gis/xian/mapper/XianInferenceResultMapper.java @@ -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 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(); } diff --git a/src/main/java/com/gis/xian/mapper/XianMeteorologyMapper.java b/src/main/java/com/gis/xian/mapper/XianMeteorologyMapper.java new file mode 100644 index 0000000..befe2ad --- /dev/null +++ b/src/main/java/com/gis/xian/mapper/XianMeteorologyMapper.java @@ -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 { + +} diff --git a/src/main/java/com/gis/xian/mapper/XianRiskSpotsMapper.java b/src/main/java/com/gis/xian/mapper/XianRiskSpotsMapper.java index f688c75..2a1d19f 100644 --- a/src/main/java/com/gis/xian/mapper/XianRiskSpotsMapper.java +++ b/src/main/java/com/gis/xian/mapper/XianRiskSpotsMapper.java @@ -1,18 +1,20 @@ package com.gis.xian.mapper; +import com.gis.xian.dto.RiskAndHiddenSpotDTO; import com.gis.xian.entity.XianRiskSpots; import java.util.List; /** -* @author wzy -* @description 针对表【xian_risk_spots(地质灾害风险区)】的数据库操作Mapper -* @createDate 2026-04-11 10:38:29 -* @Entity com.gis.xian.entity.XianRiskSpots -*/ + * @author wzy + * @description 针对表【xian_risk_spots(地质灾害风险区)】的数据库操作Mapper + * @createDate 2026-04-11 10:38:29 + * @Entity com.gis.xian.entity.XianRiskSpots + */ public interface XianRiskSpotsMapper { /** * 获取所有风险点基础信息 + * * @return 风险点基础列表 */ List getBasePoints(); @@ -20,10 +22,19 @@ public interface XianRiskSpotsMapper { /** * 根据id获取风险点详情 + * * @param id 风险点id * @return 风险点详情 */ XianRiskSpots getPointDetailById(Long id); + + /** + * 根据行政区获取风险点数量 + * + * @param districtNames 行政名称列表 + * @return 风险点数量 + */ + List queryRiskNumberByDistrictName(List districtNames); } diff --git a/src/main/java/com/gis/xian/service/ReportOutputService.java b/src/main/java/com/gis/xian/service/ReportOutputService.java new file mode 100644 index 0000000..bbe9048 --- /dev/null +++ b/src/main/java/com/gis/xian/service/ReportOutputService.java @@ -0,0 +1,10 @@ +package com.gis.xian.service; + +public interface ReportOutputService { + /** + * 产出暴雨报告 + * @param id 模拟id + * @return 报告链接 + */ + String outputRainReport(Long id); +} diff --git a/src/main/java/com/gis/xian/service/ex/ReportParameterException.java b/src/main/java/com/gis/xian/service/ex/ReportParameterException.java new file mode 100644 index 0000000..3d2eeed --- /dev/null +++ b/src/main/java/com/gis/xian/service/ex/ReportParameterException.java @@ -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); + } +} diff --git a/src/main/java/com/gis/xian/service/impl/IReportOutputServiceImpl.java b/src/main/java/com/gis/xian/service/impl/IReportOutputServiceImpl.java new file mode 100644 index 0000000..e2beb3b --- /dev/null +++ b/src/main/java/com/gis/xian/service/impl/IReportOutputServiceImpl.java @@ -0,0 +1,238 @@ +package com.gis.xian.service.impl; + +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.TypeReference; +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.RainfallDistrictSummaryResponseDTO; +import com.gis.xian.dto.RiskAndHiddenSpotDTO; +import com.gis.xian.entity.XianHiddenDangerSpots; +import com.gis.xian.entity.XianInferenceResult; +import com.gis.xian.mapper.XianHiddenDangerSpotsMapper; +import com.gis.xian.mapper.XianInferenceResultMapper; +import com.gis.xian.mapper.XianRiskSpotsMapper; +import com.gis.xian.service.ReportOutputService; +import com.gis.xian.service.ex.ReportParameterException; +import com.gis.xian.utils.DateTimeUtils; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.stereotype.Service; + +import java.util.*; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +@Service +@Slf4j +public class IReportOutputServiceImpl implements ReportOutputService { + + @Resource + private XianInferenceResultMapper xianInferenceResultMapper; + + @Resource + private AlgorithmClient algorithmClient; + + @Resource + private XianRiskSpotsMapper xianRiskSpotsMapper; + + @Resource + private XianHiddenDangerSpotsMapper xianHiddenDangerSpotsMapper; + + @Resource + private ReportProperties reportProperties; + + @Override + public String outputRainReport(Long id) { + // 存储替换模版的map + Map replaceTemplateMap = new HashMap<>(); + + // 中高风险隐患点id + Map> hiddenDangerSpotsIds = null; + + // 获取模拟信息 + XianInferenceResult simulationInfo = xianInferenceResultMapper.selectById(id); + + // 隐患点id + hiddenDangerSpotsIds = getHiddenDangerSpotsIds(simulationInfo); + + // 获取高风险隐患点信息 + List highHiddenDangerSpots = xianHiddenDangerSpotsMapper.getPointDetailByIds(hiddenDangerSpotsIds.get("high")); + + // 获取中风险隐患点信息 + List middleHiddenDangerSpots = xianHiddenDangerSpotsMapper.getPointDetailByIds(hiddenDangerSpotsIds.get("middle")); + + // 获取降雨信息 + ApiResponse> rainData = algorithmClient.post("/rainfall/district-summary", Map.of("inference_id", 1), + new ParameterizedTypeReference<>() { + }); + + // 获取行政区划名称 + List districtNames = getDistrictNames(rainData.getData()); + + // 获取风险区和隐患点数量 + List riskNumbers = xianRiskSpotsMapper.queryRiskNumberByDistrictName(districtNames); + List hiddenDangerNumbers = xianHiddenDangerSpotsMapper.queryHiddenDangerNumberByDistrictName(districtNames); + + // 获取预测点数量 + Integer predictPointNum = xianInferenceResultMapper.getTheNumberOfPredictedPoints(); + + // 生成降雨报告模版 + generateRainReportTemplate(replaceTemplateMap, simulationInfo, highHiddenDangerSpots, middleHiddenDangerSpots, rainData.getData(), districtNames, riskNumbers, hiddenDangerNumbers, predictPointNum); + System.out.println(replaceTemplateMap); + return ""; + } + + /** + * 获取隐患点id + * + * @param simulationInfo 模拟信息 + * @return 隐患点id + */ + private Map> getHiddenDangerSpotsIds(XianInferenceResult simulationInfo) { + // 预测结果 + Map probabilities = JSON.parseObject(simulationInfo.getResult(), new TypeReference<>() { + }); + + // id + Map> ids = new HashMap<>(); + ids.put("high", new ArrayList<>()); + ids.put("middle", new ArrayList<>()); + + for (String point : probabilities.keySet()) { + Long id = Long.parseLong(point.split("_")[0]); + if (probabilities.get(point) >= 70) { + ids.get("high").add(id); + } else if (probabilities.get(point) >= 50) { + ids.get("middle").add(id); + } + } + return ids; + } + + /** + * 获取前3个风险地区名称 + * + * @param rainData 降雨数据 + * @return 行政区名称 + */ + private List getDistrictNames(List rainData) { + return rainData.stream() + .filter(dto -> dto.getRainfall() != null && !dto.getRainfall().isEmpty()) + // 按降雨量降序排序 + .sorted((dto1, dto2) -> { + double r1 = Double.parseDouble(dto1.getRainfall()); + double r2 = Double.parseDouble(dto2.getRainfall()); + return Double.compare(r2, r1); // 降序 + }) + .limit(3) + .map(RainfallDistrictSummaryResponseDTO::getDistrictName) + .collect(Collectors.toList()); + } + + private void generateRainReportTemplate( + Map m, + XianInferenceResult simulationInfo, + List highHiddenDangerSpots, + List middleHiddenDangerSpots, + List rainData, + List districtNames, + List riskNumbers, + List hiddenDangerNumbers, + Integer predictPointNum + ) { + + try { + BaseCondition condition = JSON.parseObject(simulationInfo.getCondition(), BaseCondition.class); + List hiddenDangerSpots = Stream.concat(highHiddenDangerSpots.stream(), middleHiddenDangerSpots.stream()).collect(Collectors.toList()); + + m.put("报告时间", DateTimeUtils.datetimeFormat(simulationInfo.getOccurredTime(), "MM月dd日HH时mm分")); + m.put("降雨时间", DateTimeUtils.datetimeFormat(simulationInfo.getOccurredTime(), "yyyy年MM月dd日HH时")); + m.put("降雨地区", String.join("、", districtNames)); + m.put("持续时间", condition.getDuration() == null ? 72 : condition.getDuration()); + m.put("降雨量", getRainfall(rainData, districtNames)); + m.put("降雨集中区域", districtNames.isEmpty() ? "" : districtNames.get(0)); + m.put("风险区数量", riskNumbers.size()); + m.put("隐患点数量", hiddenDangerNumbers.size()); + m.put("致灾因子", String.join("、", reportProperties.getDisasterCausingFactors().getRainfall().getType()) + "等"); + m.put("致灾因子数量", reportProperties.getDisasterCausingFactors().getRainfall().getNumber()); + m.put("预测点数量", predictPointNum); + m.put("灾害链", getRainfallDisasterChain(hiddenDangerSpots)); + m.put("具体风险地区", getRainfallDisasterChainPosition(hiddenDangerSpots)); + m.put("高风险区域数量", highHiddenDangerSpots.size()); + m.put("次生灾害类型", getSecondaryDisasterType(highHiddenDangerSpots)); + m.put("hasDisasters", !hiddenDangerSpots.isEmpty()); + m.put("hasLandslide", highHiddenDangerSpots.stream().anyMatch(d -> "滑坡".equals(d.getDisasterType()))); + m.put("滑坡中高风险街道", highHiddenDangerSpots.stream().filter(d -> "滑坡".equals(d.getDisasterType())).map(XianHiddenDangerSpots::getVillage).collect(Collectors.joining("、"))); + } catch (Exception e) { + log.error("生成暴雨报告出错:{}", e.getMessage()); + throw new ReportParameterException("生成暴雨报告出错:" + e.getMessage()); + } + + } + + /** + * 获取降雨量 + * + * @param rainData 降雨数据 + * @param districtNames 行政区名称 + * @return 降雨量 + */ + private String getRainfall(List rainData, List districtNames) { + Map map = rainData.stream() + .collect(Collectors.toMap( + RainfallDistrictSummaryResponseDTO::getDistrictName, + RainfallDistrictSummaryResponseDTO::getRainfall, + (v1, v2) -> v1 + )); + return districtNames.stream() + .map(map::get) + .filter(Objects::nonNull) + .collect(Collectors.joining("mm、")) + "mm"; + } + + /** + * 获取暴雨灾害链 + * + * @param hiddenDangerSpots 隐藏点信息 + * @return 暴雨Chain + */ + private String getRainfallDisasterChain(List hiddenDangerSpots) { + return hiddenDangerSpots.stream() + .map(XianHiddenDangerSpots::getDisasterType) + .filter(Objects::nonNull) + .distinct() + .map(name -> "暴雨-" + name) + .collect(Collectors.joining("、")); + } + + /** + * 获取暴雨灾害链位置 + * + * @param hiddenDangerSpots 隐藏点信息 + * @return 暴雨灾害链位置 + */ + private String getRainfallDisasterChainPosition(List hiddenDangerSpots) { + return hiddenDangerSpots.stream() + .map(XianHiddenDangerSpots::getVillage) + .filter(Objects::nonNull) + .distinct() + .collect(Collectors.joining("、")); + } + + /** + * 获取次生灾害类型 + * + * @param hiddenDangerSpots 隐藏点信息 + * @return 次生灾害类型 + */ + private String getSecondaryDisasterType(List hiddenDangerSpots) { + return hiddenDangerSpots.stream() + .map(XianHiddenDangerSpots::getDisasterType) + .filter(Objects::nonNull) + .distinct() + .collect(Collectors.joining("、")); + } +} diff --git a/src/main/java/com/gis/xian/task/InitializeData.java b/src/main/java/com/gis/xian/task/InitializeData.java index 76cacf8..a4029b6 100644 --- a/src/main/java/com/gis/xian/task/InitializeData.java +++ b/src/main/java/com/gis/xian/task/InitializeData.java @@ -19,7 +19,7 @@ import java.util.concurrent.Executor; /** * 初始化数据 */ -@Component +//@Component @Slf4j public class InitializeData { diff --git a/src/main/java/com/gis/xian/utils/DateTimeUtils.java b/src/main/java/com/gis/xian/utils/DateTimeUtils.java new file mode 100644 index 0000000..895ed1b --- /dev/null +++ b/src/main/java/com/gis/xian/utils/DateTimeUtils.java @@ -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)); + } +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index dc6cdec..0f00ec8 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -13,3 +13,13 @@ mybatis-plus: map-underscore-to-camel-case: true # 添加 TypeHandler 扫描包路径 type-handlers-package: com.gis.xian.handler + +# 报告配置 +report: + disaster-causing-factors: + rainfall: + type: [ "降雨强度", "持续时间", "累计降雨量", "高程", "坡度", "坡向", "土壤分类", "岩性", "土地利用类型", "不透水率", "植被覆盖指数", "距河道距离", "管网密度" ] + number: 19 + earthquake: + type: [ "震度", "震源深度", "距离震中位置", "高程", "坡度", "坡向", "土壤分类", "岩性", "土地利用类型", "植被覆盖指数", "距离断裂带距离" ] + number: 17 \ No newline at end of file diff --git a/src/main/resources/com/gis/xian/mapper/XianHiddenDangerSpotsMapper.xml b/src/main/resources/com/gis/xian/mapper/XianHiddenDangerSpotsMapper.xml index ca52c6f..1481037 100644 --- a/src/main/resources/com/gis/xian/mapper/XianHiddenDangerSpotsMapper.xml +++ b/src/main/resources/com/gis/xian/mapper/XianHiddenDangerSpotsMapper.xml @@ -5,24 +5,29 @@ - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + @@ -55,10 +60,42 @@ + + + + diff --git a/src/main/resources/com/gis/xian/mapper/XianInferenceResultMapper.xml b/src/main/resources/com/gis/xian/mapper/XianInferenceResultMapper.xml index 59e530a..9da40c7 100644 --- a/src/main/resources/com/gis/xian/mapper/XianInferenceResultMapper.xml +++ b/src/main/resources/com/gis/xian/mapper/XianInferenceResultMapper.xml @@ -5,16 +5,15 @@ - - - - - - - - - - + + + + + + + + + @@ -25,4 +24,28 @@ + + + + \ No newline at end of file diff --git a/src/main/resources/com/gis/xian/mapper/XianMeteorologyMapper.xml b/src/main/resources/com/gis/xian/mapper/XianMeteorologyMapper.xml new file mode 100644 index 0000000..a9366ad --- /dev/null +++ b/src/main/resources/com/gis/xian/mapper/XianMeteorologyMapper.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/com/gis/xian/mapper/XianRiskSpotsMapper.xml b/src/main/resources/com/gis/xian/mapper/XianRiskSpotsMapper.xml index 7a367fe..491a6c5 100644 --- a/src/main/resources/com/gis/xian/mapper/XianRiskSpotsMapper.xml +++ b/src/main/resources/com/gis/xian/mapper/XianRiskSpotsMapper.xml @@ -5,28 +5,33 @@ - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -39,11 +44,26 @@ + diff --git a/src/main/resources/config/customize/application-customize-dev.yml b/src/main/resources/config/customize/application-customize-dev.yml index 9912192..2a91d8a 100644 --- a/src/main/resources/config/customize/application-customize-dev.yml +++ b/src/main/resources/config/customize/application-customize-dev.yml @@ -4,4 +4,6 @@ server: algorithm: server: - url: "http://localhost:8082" \ No newline at end of file + url: "http://localhost:8082" + connect-timeout: 5 + read-timeout: 120 \ No newline at end of file diff --git a/src/main/resources/config/customize/application-customize-prod.yml b/src/main/resources/config/customize/application-customize-prod.yml index ee478bc..e7643a2 100644 --- a/src/main/resources/config/customize/application-customize-prod.yml +++ b/src/main/resources/config/customize/application-customize-prod.yml @@ -4,4 +4,6 @@ server: algorithm: server: - url: "http://localhost:8081" \ No newline at end of file + url: "http://localhost:8081" + connect-timeout: 5 + read-timeout: 120 \ No newline at end of file diff --git a/src/main/resources/template/rainfall/rainfall-template.docx b/src/main/resources/template/rainfall/rainfall-template.docx new file mode 100644 index 0000000000000000000000000000000000000000..17d3649bd98d1a2bed4b06647768f7fd22b4d8c8 GIT binary patch literal 19874 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