From 0c59afa49009598bf5170b7bd51da37e92c89b28 Mon Sep 17 00:00:00 2001 From: wzy-warehouse <18135009705@163.com> Date: Sun, 28 Jun 2026 15:57:59 +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?=E9=83=A8=E5=88=86=E5=86=85=E5=AE=B9=E5=BE=85=E5=AE=8C=E5=96=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/gis/xian/config/ReportProperties.java | 16 + .../com/gis/xian/dto/DisasterRiskData.java | 56 ++ .../xian/entity/XianInferenceResultFile.java | 26 + .../mapper/XianInferenceResultFileMapper.java | 19 + .../impl/IReportOutputServiceImpl.java | 526 ++++++++++++------ .../gis/xian/utils/ReportTableBuilder.java | 43 ++ src/main/resources/application-dev.yml | 2 + src/main/resources/application.yml | 16 +- .../mapper/XianHiddenDangerSpotsMapper.xml | 2 +- .../mapper/XianInferenceResultFileMapper.xml | 24 + .../customize/application-customize-dev.yml | 9 +- .../customize/application-customize-prod.yml | 10 +- 12 files changed, 586 insertions(+), 163 deletions(-) create mode 100644 src/main/java/com/gis/xian/dto/DisasterRiskData.java create mode 100644 src/main/java/com/gis/xian/entity/XianInferenceResultFile.java create mode 100644 src/main/java/com/gis/xian/mapper/XianInferenceResultFileMapper.java create mode 100644 src/main/java/com/gis/xian/utils/ReportTableBuilder.java create mode 100644 src/main/resources/com/gis/xian/mapper/XianInferenceResultFileMapper.xml diff --git a/src/main/java/com/gis/xian/config/ReportProperties.java b/src/main/java/com/gis/xian/config/ReportProperties.java index e851b01..a253291 100644 --- a/src/main/java/com/gis/xian/config/ReportProperties.java +++ b/src/main/java/com/gis/xian/config/ReportProperties.java @@ -4,13 +4,25 @@ 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 disasterTypes = new LinkedHashMap<>(); + + @Data + public static class ReportFiles { + /** + * 文件服务器前缀 + */ + private String serverUrl; + } @Data public static class DisasterCausingFactors { @@ -22,5 +34,9 @@ public class ReportProperties { public static class FactorConfig { private List type; private Integer number; + private String templatePath; + private String outputPath; + private String outputName; + } } diff --git a/src/main/java/com/gis/xian/dto/DisasterRiskData.java b/src/main/java/com/gis/xian/dto/DisasterRiskData.java new file mode 100644 index 0000000..8d7404f --- /dev/null +++ b/src/main/java/com/gis/xian/dto/DisasterRiskData.java @@ -0,0 +1,56 @@ +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 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; + /** 发生概率(小数 0-1) */ + private Double probability; + /** 灾害等级:高(>=70%) / 中(50%-70%) */ + private String riskLevel; + } +} diff --git a/src/main/java/com/gis/xian/entity/XianInferenceResultFile.java b/src/main/java/com/gis/xian/entity/XianInferenceResultFile.java new file mode 100644 index 0000000..ba25534 --- /dev/null +++ b/src/main/java/com/gis/xian/entity/XianInferenceResultFile.java @@ -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; +} diff --git a/src/main/java/com/gis/xian/mapper/XianInferenceResultFileMapper.java b/src/main/java/com/gis/xian/mapper/XianInferenceResultFileMapper.java new file mode 100644 index 0000000..65e2d1d --- /dev/null +++ b/src/main/java/com/gis/xian/mapper/XianInferenceResultFileMapper.java @@ -0,0 +1,19 @@ +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 selectByInferenceId(@Param("inferenceId") Long inferenceId); +} diff --git a/src/main/java/com/gis/xian/service/impl/IReportOutputServiceImpl.java b/src/main/java/com/gis/xian/service/impl/IReportOutputServiceImpl.java index e2beb3b..79aecb3 100644 --- a/src/main/java/com/gis/xian/service/impl/IReportOutputServiceImpl.java +++ b/src/main/java/com/gis/xian/service/impl/IReportOutputServiceImpl.java @@ -2,237 +2,445 @@ 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.service.ex.ReportParameterException; 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; -import java.util.stream.Stream; @Service @Slf4j public class IReportOutputServiceImpl implements ReportOutputService { + /** + * 暴雨灾害类型 → 模板变量后缀 + */ + private static final Map 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 xianInferenceResultMapper; + private XianInferenceResultMapper inferenceResultMapper; + + @Resource + private XianHiddenDangerSpotsMapper hiddenDangerSpotsMapper; + + @Resource + private XianRiskSpotsMapper riskSpotsMapper; + + @Resource + private XianInferenceResultFileMapper inferenceResultFileMapper; @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<>() { + // 获取模拟数据 + XianInferenceResult sim = inferenceResultMapper.selectById(id); + Map probabilities = JSON.parseObject(sim.getResult(), new TypeReference<>() { }); - // id - Map> ids = new HashMap<>(); - ids.put("high", new ArrayList<>()); - ids.put("middle", new ArrayList<>()); + // 获取所有隐患点详情 + List pointIds = extractPointIds(probabilities.keySet()); + List allSpots = pointIds.isEmpty() + ? Collections.emptyList() + : hiddenDangerSpotsMapper.getPointDetailByIds(pointIds); - 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; + // 按灾害类型分组并构建风险数据 + List files = inferenceResultFileMapper.selectByInferenceId(id); + List disasterDataList = buildDisasterDataList(allSpots, probabilities, files); + + // 获取降雨概况 + List rainData = fetchRainData(); + List districtNames = top3DistrictNames(rainData); + + // 获取风险统计 + int riskCount = sumCount(riskSpotsMapper.queryRiskNumberByDistrictName(districtNames)); + int hiddenCount = sumCount(hiddenDangerSpotsMapper.queryHiddenDangerNumberByDistrictName(districtNames)); + int predictPointNum = inferenceResultMapper.getTheNumberOfPredictedPoints(); + + // 构建模板数据模型 + Map model = buildTemplateModel(sim, disasterDataList, rainData, + districtNames, riskCount, hiddenCount, predictPointNum); + + // 渲染输出 + return renderReport( + reportProperties.getDisasterCausingFactors().getRainfall().getTemplatePath(), + Path.of(localPath, reportProperties.getDisasterCausingFactors().getRainfall().getOutputPath().replace("{id}", id.toString())).toString(), + reportProperties.getDisasterCausingFactors().getRainfall().getOutputName().replace("{currentTime}", System.currentTimeMillis() + ""), + model + ); } /** - * 获取前3个风险地区名称 - * - * @param rainData 降雨数据 - * @return 行政区名称 + * 从 JSONB key 集合中提取隐患点 ID */ - private List getDistrictNames(List rainData) { + private List extractPointIds(Set keys) { + return keys.stream() + .map(k -> Long.parseLong(k.split("_")[0])) + .distinct() + .collect(Collectors.toList()); + } + + /** + * 调用算法服务获取各区降雨概况 + */ + private List fetchRainData() { + ApiResponse> resp = algorithmClient.post( + "/rainfall/district-summary", + Map.of("inference_id", 1), + new ParameterizedTypeReference<>() { + }); + return resp != null ? resp.getData() : Collections.emptyList(); + } + + /** + * 降雨量前3行政区 + */ + private List top3DistrictNames(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); // 降序 - }) + .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()); } - 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()); - } - + /** + * 对DTO列表中的number字段求和 + */ + private int sumCount(List list) { + return list.stream().mapToInt(d -> Integer.parseInt(d.getNumber())).sum(); } /** - * 获取降雨量 - * - * @param rainData 降雨数据 - * @param districtNames 行政区名称 - * @return 降雨量 + * 将隐患点按disasterType分组,构建各类型的DisasterRiskData */ - 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) + private List buildDisasterDataList( + List allSpots, + Map probabilities, + List files) { + + // 按灾害类型分组 + Map> grouped = allSpots.stream() + .filter(s -> s.getDisasterType() != null) + .collect(Collectors.groupingBy(XianHiddenDangerSpots::getDisasterType, LinkedHashMap::new, Collectors.toList())); + + List result = new ArrayList<>(); + for (Map.Entry> 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 spots = entry.getValue().stream() + .map(spot -> { + Double prob = probabilities.get(spot.getId() + "_1"); + if (prob == null) return null; + String level = prob >= 0.70 ? "高" : "中"; + return new DisasterRiskData.SpotRisk() + .setPosition(spot.getPosition()) + .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); + + // 中高风险街道 + data.setHighRiskStreets(spots.stream() + .map(DisasterRiskData.SpotRisk::getPosition) + .collect(Collectors.joining("、"))); + + // 图片 + data.setImageUrl(findImage(files, disasterType)); + + result.add(data); + } + return result; + } + + /** + * 从文件列表中匹配图片 + */ + private String findImage(List files, String disasterType) { + String expectedName = String.format(IMAGE_NAME_PATTERN, disasterType); + return files.stream() + .filter(f -> expectedName.equals(f.getFileName())) + .findFirst() + .map(f -> (reportProperties.getFiles().getServerUrl() + "/" + f.getFilePath()).replace("\\", "/").replace("//", "/")) + .orElse(""); + } + + /** + * 构建模板数据模型 + * + * @param sim 模拟结果 + * @param disasterDataList 灾害数据 + * @param rainData 降雨数据 + * @param districtNames 灾害名称 + * @param riskCount 灾害数量 + * @param hiddenCount 隐患数量 + * @param predictPointNum 预测点数 + * @return 模板数据模型 + */ + private Map buildTemplateModel( + XianInferenceResult sim, + List disasterDataList, + List rainData, + List districtNames, + int riskCount, + int hiddenCount, + int predictPointNum) { + + Map 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("、", districtNames)); + m.put("持续时间", cond.getDuration() == null ? 72 : cond.getDuration()); + m.put("降雨量", buildRainfallStr(rainData, districtNames)); + m.put("降雨集中区域", districtNames.isEmpty() ? "" : districtNames.get(0)); + 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 rainData, List names) { + Map 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 hiddenDangerSpots 隐藏点信息 - * @return 暴雨Chain + * @param list 灾害数据 + * @return 灾害链 */ - private String getRainfallDisasterChain(List hiddenDangerSpots) { - return hiddenDangerSpots.stream() - .map(XianHiddenDangerSpots::getDisasterType) - .filter(Objects::nonNull) - .distinct() - .map(name -> "暴雨-" + name) + private String buildDisasterChain(String type, List list) { + return list.stream().filter(DisasterRiskData::hasData) + .map(d -> type + "-" + d.getDisasterType()) .collect(Collectors.joining("、")); } /** - * 获取暴雨灾害链位置 + * 构建高风险位置字符串 * - * @param hiddenDangerSpots 隐藏点信息 - * @return 暴雨灾害链位置 + * @param list 灾害数据 + * @return 高风险位置 */ - private String getRainfallDisasterChainPosition(List hiddenDangerSpots) { - return hiddenDangerSpots.stream() - .map(XianHiddenDangerSpots::getVillage) - .filter(Objects::nonNull) - .distinct() + private String buildRiskPosition(List list) { + return list.stream().filter(DisasterRiskData::hasData) + .map(DisasterRiskData::getHighRiskStreets) + .filter(s -> !s.isEmpty()) .collect(Collectors.joining("、")); } /** - * 获取次生灾害类型 + * 统计高风险数量 * - * @param hiddenDangerSpots 隐藏点信息 + * @param list 灾害数据 + * @return 高风险数量 + */ + private int countHighRisk(List list) { + return (int) list.stream().filter(DisasterRiskData::hasData) + .flatMap(d -> d.getSpots().stream()) + .filter(s -> "高".equals(s.getRiskLevel())) + .count(); + } + + /** + * 构建次生灾害类型字符串 + * + * @param list 灾害数据 * @return 次生灾害类型 */ - private String getSecondaryDisasterType(List hiddenDangerSpots) { - return hiddenDangerSpots.stream() - .map(XianHiddenDangerSpots::getDisasterType) - .filter(Objects::nonNull) + private String buildSecondaryTypes(List list) { + return list.stream().filter(DisasterRiskData::hasData) + .map(DisasterRiskData::getDisasterType) + .collect(Collectors.joining("、")); + } + + /** + * 构建所有高风险位置字符串 + * + * @param list 灾害数据 + * @return 所有高风险位置 + */ + private String buildAllHighRiskPosition(List list) { + return list.stream().filter(DisasterRiskData::hasData) + .flatMap(d -> d.getSpots().stream()) + .map(DisasterRiskData.SpotRisk::getPosition) .distinct() .collect(Collectors.joining("、")); } + + + /** + * 渲染报告 + * + * @param templatePath 模板路径 + * @param outputPath 输出路径 + * @param outputName 输出文件名 + * @param model 模板数据 + * @return 输出文件路径 + */ + private String renderReport(String templatePath, String outputPath, String outputName, Map 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; + } + } } diff --git a/src/main/java/com/gis/xian/utils/ReportTableBuilder.java b/src/main/java/com/gis/xian/utils/ReportTableBuilder.java new file mode 100644 index 0000000..cd99036 --- /dev/null +++ b/src/main/java/com/gis/xian/utils/ReportTableBuilder.java @@ -0,0 +1,43 @@ +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.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%"); + + /** + * 构建灾害风险表(四列:序号、位置、发生概率、灾害等级) + */ + public static TableRenderData buildRiskTable(String disasterType, List spots) { + RowRenderData header = Rows.of( + "序号", + "位置", + disasterType + "发生概率", + "灾害等级" + ).textBold().center().create(); + + Tables.TableBuilder builder = Tables.of(header); + AtomicInteger idx = new AtomicInteger(1); + for (DisasterRiskData.SpotRisk spot : spots) { + builder.addRow(Rows.of( + String.valueOf(idx.getAndIncrement()), + spot.getPosition(), + PCT_FMT.format(spot.getProbability()), + spot.getRiskLevel() + ).center().create()); + } + return builder.create(); + } +} diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml index b410d7c..8d64959 100644 --- a/src/main/resources/application-dev.yml +++ b/src/main/resources/application-dev.yml @@ -41,6 +41,7 @@ safety: - /druid - /websocket/info - /websocket/** + - /report/output/** # 请求无需解密的路径 no-decrypt-paths: @@ -48,4 +49,5 @@ safety: - /druid - /websocket/info - /websocket/** + - /report/output/** diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 0f00ec8..e95b9ed 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -16,10 +16,24 @@ mybatis-plus: # 报告配置 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 \ No newline at end of file + 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: 崩塌 \ 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 1481037..3532452 100644 --- a/src/main/resources/com/gis/xian/mapper/XianHiddenDangerSpotsMapper.xml +++ b/src/main/resources/com/gis/xian/mapper/XianHiddenDangerSpotsMapper.xml @@ -84,7 +84,7 @@ + SELECT file_path, file_name + FROM xian_inference_result_file + WHERE inference_id = #{inferenceId} + AND is_delete = 0 + ORDER BY create_time + + + diff --git a/src/main/resources/config/customize/application-customize-dev.yml b/src/main/resources/config/customize/application-customize-dev.yml index 2a91d8a..f5e0f15 100644 --- a/src/main/resources/config/customize/application-customize-dev.yml +++ b/src/main/resources/config/customize/application-customize-dev.yml @@ -6,4 +6,11 @@ algorithm: server: url: "http://localhost:8082" connect-timeout: 5 - read-timeout: 120 \ No newline at end of file + read-timeout: 120 + +# 文件配置 +files: + server: + url: "http://localhost:8082" + local: + path: "G:/files" \ 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 e7643a2..7ffc7a8 100644 --- a/src/main/resources/config/customize/application-customize-prod.yml +++ b/src/main/resources/config/customize/application-customize-prod.yml @@ -2,8 +2,16 @@ server: port: 8080 +# 算法服务器配置 algorithm: server: url: "http://localhost:8081" connect-timeout: 5 - read-timeout: 120 \ No newline at end of file + read-timeout: 120 + +# 文件配置 +files: + server: + url: "http://localhost:8082" + local: + path: "/data/files" \ No newline at end of file