diff --git a/backend/src/main/java/com/yfd/platform/qgc_export/controller/QgcExportController.java b/backend/src/main/java/com/yfd/platform/qgc_export/controller/QgcExportController.java index cf225a43..4f5d59bf 100644 --- a/backend/src/main/java/com/yfd/platform/qgc_export/controller/QgcExportController.java +++ b/backend/src/main/java/com/yfd/platform/qgc_export/controller/QgcExportController.java @@ -47,20 +47,29 @@ public class QgcExportController { @RequestParam(required = false) String sysId, @RequestParam(defaultValue = "true") boolean isDailyDimension, @RequestParam(defaultValue = "false") boolean isCalcQec, + @RequestParam(required = false) String yearStatType, + @RequestParam(required = false) String year, + @RequestParam(required = false) String baseId, HttpServletResponse response) { // 解析逗号分隔参数 List dataFields = parseCsv(dataField); List monthList = parseCsv(months); List stationList = parseCsv(stcd); + List baseIdList = parseCsv(baseId); -// if (dataFields.isEmpty()) { -// dataFields = Arrays.asList("v", "q", "z"); -// } if (tmDimension == null || tmDimension.isEmpty()) { tmDimension = "month"; } + // 年度导出分支 + if ("year".equals(tmDimension)) { + List yearStatTypeList = parseCsv(yearStatType); + List yearList = parseCsv(year); + qgcExportService.exportYearData(yearStatTypeList, yearList, stationList, baseIdList, response); + return; + } + qgcExportService.exportData(dataFields, monthList, stationList, isDailyDimension, tmDimension, response); } diff --git a/backend/src/main/java/com/yfd/platform/qgc_export/domain/StationInfo.java b/backend/src/main/java/com/yfd/platform/qgc_export/domain/StationInfo.java new file mode 100644 index 00000000..bcb86b51 --- /dev/null +++ b/backend/src/main/java/com/yfd/platform/qgc_export/domain/StationInfo.java @@ -0,0 +1,56 @@ +package com.yfd.platform.qgc_export.domain; + +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +import java.io.Serializable; + +/** + * V_MS_STBPRP_T 视图扩展映射 — 用于年度导出时获取流域、基地等信息 + */ +@Data +@TableName("V_MS_STBPRP_T") +public class StationInfo implements Serializable { + + private static final long serialVersionUID = 1L; + + @TableField("STCD") + private String stcd; + + @TableField("STNM") + private String stnm; + + @TableField("ENNM") + private String ennm; + + @TableField("STTP") + private String sttp; + + @TableField("RSTCD") + private String rstcd; + + @TableField("BASE_ID") + private String baseId; + + @TableField("BASE_NAME") + private String baseName; + + @TableField("HBRVCD_NAME") + private String hbrvcdName; + + @TableField("RVCD_NAME") + private String rvcdName; + + @TableField("REACHCD") + private String reachcd; + + @TableField("REACHCD_NAME") + private String reachcdName; + + @TableField("HYNM") + private String hynm; + + @TableField("MWAY") + private Integer mway; +} diff --git a/backend/src/main/java/com/yfd/platform/qgc_export/mapper/YearStatMapper.java b/backend/src/main/java/com/yfd/platform/qgc_export/mapper/YearStatMapper.java new file mode 100644 index 00000000..59b0e530 --- /dev/null +++ b/backend/src/main/java/com/yfd/platform/qgc_export/mapper/YearStatMapper.java @@ -0,0 +1,27 @@ +package com.yfd.platform.qgc_export.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.yfd.platform.qgc_export.domain.StationInfo; +import org.apache.ibatis.annotations.Param; + +import java.util.List; +import java.util.Map; + +/** + * 年度统计导出 Mapper + */ +public interface YearStatMapper extends BaseMapper { + + /** + * 查询生态流量月度达标数据(SD_QECDRTP_S DRTP='MON') + * 返回 STCD、三部分达标数 + */ + List> selectQecMonthlyCompliance(@Param("stcdList") List stcdList, + @Param("year") int year); + + /** + * 查询生态流量月度达标数据(按 BASE_ID 分组) + */ + List> selectQecMonthlyComplianceByBase(@Param("baseIdList") List baseIdList, + @Param("year") int year); +} diff --git a/backend/src/main/java/com/yfd/platform/qgc_export/service/IQgcExportService.java b/backend/src/main/java/com/yfd/platform/qgc_export/service/IQgcExportService.java index 53f49973..968a0856 100644 --- a/backend/src/main/java/com/yfd/platform/qgc_export/service/IQgcExportService.java +++ b/backend/src/main/java/com/yfd/platform/qgc_export/service/IQgcExportService.java @@ -24,4 +24,19 @@ public interface IQgcExportService { boolean isDailyDimension, String tmDimension, HttpServletResponse response); + + /** + * 年度统计导出(水质达标率 + 生态流量达标率) + * + * @param yearStatType 统计指标类型(wq, qec) + * @param years 年份列表(如 ["2026","2025"]) + * @param stationCodes 电站编码列表(RSTCD) + * @param baseIds 水电基地编码列表(BASE_ID) + * @param response HttpServletResponse + */ + void exportYearData(List yearStatType, + List years, + List stationCodes, + List baseIds, + HttpServletResponse response); } diff --git a/backend/src/main/java/com/yfd/platform/qgc_export/service/impl/QgcExportServiceImpl.java b/backend/src/main/java/com/yfd/platform/qgc_export/service/impl/QgcExportServiceImpl.java index d2e15999..6ae1cd0a 100644 --- a/backend/src/main/java/com/yfd/platform/qgc_export/service/impl/QgcExportServiceImpl.java +++ b/backend/src/main/java/com/yfd/platform/qgc_export/service/impl/QgcExportServiceImpl.java @@ -14,6 +14,8 @@ import com.yfd.platform.qgc_export.mapper.SdQecRMapper; import com.yfd.platform.qgc_export.mapper.SdQecdaySMapper; import com.yfd.platform.qgc_export.mapper.StationMappingMapper; import com.yfd.platform.qgc_export.mapper.MsAlongdetBMapper; +import com.yfd.platform.qgc_export.mapper.YearStatMapper; +import com.yfd.platform.qgc_export.domain.StationInfo; import com.yfd.platform.qgc_env.wt.mapper.SdWtvtRMapper; import com.yfd.platform.qgc_export.mapper.SdWtvtdaySMapper; import com.yfd.platform.qgc_export.processor.*; @@ -156,13 +158,20 @@ public class QgcExportServiceImpl implements IQgcExportService { @Resource private RwtProcessor rwtProcessor; + @Resource + private YearStatMapper yearStatMapper; + private static final SimpleDateFormat SDF_TIME = new SimpleDateFormat("dd日 HH时"); private static final SimpleDateFormat SDF = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); - /** 基础汇总指标(7个) */ + /** + * 基础汇总指标(7个) + */ private static final List BASE_LABELS = Collections.unmodifiableList( Arrays.asList("日均最低", "日均最高", "月内最低", "月内最低时间", "月内最高", "月内最高时间", "月均")); - /** 小时达标率标签 */ + /** + * 小时达标率标签 + */ private static final String LABEL_COMPLIANCE = "小时达标率"; @Override @@ -233,10 +242,14 @@ public class QgcExportServiceImpl implements IQgcExportService { headers = new ArrayList<>(); headers.add("测站名称"); headers.add("水深(m)"); - headers.add("5日"); headers.add(""); - headers.add("15日"); headers.add(""); - headers.add("25日"); headers.add(""); - headers.add("月均"); headers.add(""); + headers.add("5日"); + headers.add(""); + headers.add("15日"); + headers.add(""); + headers.add("25日"); + headers.add(""); + headers.add("月均"); + headers.add(""); } else { // 仅包含当前处理器 STTP 类型有对应测站的电站 headers = new ArrayList<>(); @@ -400,16 +413,32 @@ public class QgcExportServiceImpl implements IQgcExportService { for (String rstcd : rstcdToStcd.keySet()) { Aggregation agg = aggMap.get(rstcd); switch (i) { - case 0: row.add(formatOrEmpty(agg != null ? agg.getDailyMin() : null, decimals)); break; - case 1: row.add(formatOrEmpty(agg != null ? agg.getDailyMax() : null, decimals)); break; - case 2: row.add(formatOrEmpty(agg != null ? agg.getExtremeMin() : null, decimals)); break; - case 3: row.add(agg != null && agg.getExtremeMinTime() != null - ? SDF_TIME.format(agg.getExtremeMinTime()) : ""); break; - case 4: row.add(formatOrEmpty(agg != null ? agg.getExtremeMax() : null, decimals)); break; - case 5: row.add(agg != null && agg.getExtremeMaxTime() != null - ? SDF_TIME.format(agg.getExtremeMaxTime()) : ""); break; - case 6: row.add(formatOrEmpty(agg != null ? agg.getMonthlyAvg() : null, decimals)); break; - case 7: row.add(formatComplianceRate(agg != null ? agg.getComplianceRate() : null)); break; + case 0: + row.add(formatOrEmpty(agg != null ? agg.getDailyMin() : null, decimals)); + break; + case 1: + row.add(formatOrEmpty(agg != null ? agg.getDailyMax() : null, decimals)); + break; + case 2: + row.add(formatOrEmpty(agg != null ? agg.getExtremeMin() : null, decimals)); + break; + case 3: + row.add(agg != null && agg.getExtremeMinTime() != null + ? SDF_TIME.format(agg.getExtremeMinTime()) : ""); + break; + case 4: + row.add(formatOrEmpty(agg != null ? agg.getExtremeMax() : null, decimals)); + break; + case 5: + row.add(agg != null && agg.getExtremeMaxTime() != null + ? SDF_TIME.format(agg.getExtremeMaxTime()) : ""); + break; + case 6: + row.add(formatOrEmpty(agg != null ? agg.getMonthlyAvg() : null, decimals)); + break; + case 7: + row.add(formatComplianceRate(agg != null ? agg.getComplianceRate() : null)); + break; } } rows.add(row); @@ -540,7 +569,11 @@ public class QgcExportServiceImpl implements IQgcExportService { private int toInt(Object v) { if (v instanceof Number) return ((Number) v).intValue(); - try { return Integer.parseInt(v.toString()); } catch (Exception e) { return -1; } + try { + return Integer.parseInt(v.toString()); + } catch (Exception e) { + return -1; + } } // ======================== WQ 水质达标率 Sheet(双表头) ======================== @@ -557,7 +590,7 @@ public class QgcExportServiceImpl implements IQgcExportService { * 构建垂向水温(RWT) Sheet 数据 — 双表头格式,按测站和水深分层 */ private List> buildRwtSheetRows(MonthRange mr, List stationOrder, - StationResolution stationRes) { + StationResolution stationRes) { List stcdList = new ArrayList<>(); // WTVT 测站→显示名 映射 Map stcdDisplayMap = new LinkedHashMap<>(); @@ -597,7 +630,7 @@ public class QgcExportServiceImpl implements IQgcExportService { String key = stcd + "|" + wthg; dayVwtMap.computeIfAbsent(key, k -> new LinkedHashMap<>()) - .putIfAbsent(String.valueOf(dayNum), vwt); + .putIfAbsent(String.valueOf(dayNum), vwt); wthgOrderKey.putIfAbsent(wthg, toBigDecimal(wthgObj)); } @@ -651,11 +684,16 @@ public class QgcExportServiceImpl implements IQgcExportService { List> rows = new ArrayList<>(); // 子标题行(Row 0) List subHeaderRow = new ArrayList<>(); - subHeaderRow.add(""); subHeaderRow.add(""); - subHeaderRow.add("水温(℃)"); subHeaderRow.add("相应高程(m)"); - subHeaderRow.add("水温(℃)"); subHeaderRow.add("相应高程(m)"); - subHeaderRow.add("水温(℃)"); subHeaderRow.add("相应高程(m)"); - subHeaderRow.add("水温(℃)"); subHeaderRow.add("相应高程(m)"); + subHeaderRow.add(""); + subHeaderRow.add(""); + subHeaderRow.add("水温(℃)"); + subHeaderRow.add("相应高程(m)"); + subHeaderRow.add("水温(℃)"); + subHeaderRow.add("相应高程(m)"); + subHeaderRow.add("水温(℃)"); + subHeaderRow.add("相应高程(m)"); + subHeaderRow.add("水温(℃)"); + subHeaderRow.add("相应高程(m)"); rows.add(subHeaderRow); for (String key : sortedKeys) { @@ -855,7 +893,9 @@ public class QgcExportServiceImpl implements IQgcExportService { } } - /** 分部达标统计 */ + /** + * 分部达标统计 + */ private static class ComplianceStats { String dailyHb, dailySl, dailyDn; String hourlyHb, hourlySl, hourlyDn; @@ -896,7 +936,7 @@ public class QgcExportServiceImpl implements IQgcExportService { for (String field : dataFields) { String lower = field.trim().toLowerCase(); if ("wq".equals(lower)) { - expanded.addAll(List.of("wtmp","codcr", "ph", "dox", "cond", "tu")); + expanded.addAll(List.of("wtmp", "codcr", "ph", "dox", "cond", "tu")); } else { expanded.add(lower); } @@ -1011,7 +1051,10 @@ public class QgcExportServiceImpl implements IQgcExportService { } if (defaultDisplayName == null) { for (StationMapping m : rows) { - if (m.getStnm() != null) { defaultDisplayName = m.getStnm(); break; } + if (m.getStnm() != null) { + defaultDisplayName = m.getStnm(); + break; + } } } if (defaultDisplayName == null) { @@ -1157,7 +1200,9 @@ public class QgcExportServiceImpl implements IQgcExportService { // ======================== 日变幅格式(出入库水温日变幅) ======================== - /** 日变幅汇总标签 */ + /** + * 日变幅汇总标签 + */ private static final List VARIATION_LABELS = List.of("日变幅均值", "日变幅最大值"); /** @@ -1270,16 +1315,26 @@ public class QgcExportServiceImpl implements IQgcExportService { // ======================== 内部数据类 ======================== private static class StationResolution { - /** 默认测站名称(fallback),RSTCD → displayName */ + /** + * 默认测站名称(fallback),RSTCD → displayName + */ final Map displayNames = new LinkedHashMap<>(); - /** 按 STTP 分组的测站名称,RSTCD → {STTP → displayName} */ + /** + * 按 STTP 分组的测站名称,RSTCD → {STTP → displayName} + */ final Map> sttpDisplayNames = new LinkedHashMap<>(); - /** STTP → STCD 映射,RSTCD → {STTP → STCD} */ + /** + * STTP → STCD 映射,RSTCD → {STTP → STCD} + */ final Map> sttpStcdMap = new LinkedHashMap<>(); - /** 所属水电基地名称,RSTCD → {STTP → BASE_NAME} */ + /** + * 所属水电基地名称,RSTCD → {STTP → BASE_NAME} + */ final Map> baseNameMap = new LinkedHashMap<>(); - /** 获取默认测站名称 */ + /** + * 获取默认测站名称 + */ String getDisplayName(String rstcd) { return displayNames.getOrDefault(rstcd, rstcd); } @@ -1307,7 +1362,9 @@ public class QgcExportServiceImpl implements IQgcExportService { return inner.get(sttp); } - /** 获取指定 STTP 的水电基地名称 */ + /** + * 获取指定 STTP 的水电基地名称 + */ String getBaseName(String rstcd, String sttp) { Map inner = baseNameMap.get(rstcd); if (inner != null && sttp != null) { @@ -1334,10 +1391,510 @@ public class QgcExportServiceImpl implements IQgcExportService { private BigDecimal extremeMax; private Date extremeMaxTime; private BigDecimal monthlyAvg; - /** 小时达标率(百分比,仅水质指标使用) */ + /** + * 小时达标率(百分比,仅水质指标使用) + */ private BigDecimal complianceRate; } + // ======================== 年度统计导出 ======================== + + @Override + public void exportYearData(List yearStatType, List years, + List stationCodes, List baseIds, + HttpServletResponse response) { + if ((years == null || years.isEmpty()) || (stationCodes.isEmpty() && baseIds.isEmpty())) { + log.warn("年度导出需要传入年份和电站/基地编码"); + return; + } + + boolean hasWq = yearStatType.contains("wq"); + boolean hasQec = yearStatType.contains("qec"); + String timestamp = String.valueOf(System.currentTimeMillis()); + + // 预查询测站信息(年维度无关,一次查询即可) + List wqStations = null; + List engStations = null; + if (hasWq && !stationCodes.isEmpty()) { + wqStations = yearStatMapper.selectList( + new LambdaQueryWrapper() + .eq(StationInfo::getSttp, "WQ") + .eq(StationInfo::getMway, 2) + .and(w -> w.in(StationInfo::getRstcd, stationCodes) + .or() + .in(StationInfo::getStcd, stationCodes)) + ); + if (!wqStations.isEmpty()) { + wqStations = sortStationsByOrder(wqStations); + } + } + if (hasQec && !stationCodes.isEmpty()) { + engStations = yearStatMapper.selectList( + new LambdaQueryWrapper() + .eq(StationInfo::getSttp, "ENG") + .and(w -> w.in(StationInfo::getRstcd, stationCodes) + .or() + .in(StationInfo::getStcd, stationCodes)) + ); + if (!engStations.isEmpty()) { + engStations = sortStationsByOrder(engStations); + } + } + + // 多年份:每年一个 Excel 文件 + List excelList = new ArrayList<>(); + for (String yearStr : years) { + int year = Integer.parseInt(yearStr); + List sheets = new ArrayList<>(); + + // WQ + if (wqStations != null && !wqStations.isEmpty()) { + ExportZipUtil.SheetData wqSheet = buildYearWqSheet(wqStations, year); + if (wqSheet != null) sheets.add(wqSheet); + } + + // QEC 电站维度 + if (engStations != null && !engStations.isEmpty()) { + ExportZipUtil.SheetData qecEpaSheet = buildYearQecStationSheet(engStations, year, "QEC"); + ExportZipUtil.SheetData qecMwrSheet = buildYearQecStationSheet(engStations, year, "MWR"); + ExportZipUtil.SheetData qecAvqSheet = buildYearQecStationSheet(engStations, year, "AVQ"); + if (qecEpaSheet != null) sheets.add(qecEpaSheet); + if (qecMwrSheet != null) sheets.add(qecMwrSheet); + if (qecAvqSheet != null) sheets.add(qecAvqSheet); + } + + // QEC 水电基地维度 + if (!baseIds.isEmpty()) { + ExportZipUtil.SheetData baseEpaSheet = buildYearQecBaseSheet(baseIds, year, "QEC"); + ExportZipUtil.SheetData baseMwrSheet = buildYearQecBaseSheet(baseIds, year, "MWR"); + ExportZipUtil.SheetData baseAvqSheet = buildYearQecBaseSheet(baseIds, year, "AVQ"); + if (baseEpaSheet != null) sheets.add(baseEpaSheet); + if (baseMwrSheet != null) sheets.add(baseMwrSheet); + if (baseAvqSheet != null) sheets.add(baseAvqSheet); + } + + if (!sheets.isEmpty()) { + excelList.add(new ExportZipUtil.ExcelData(year + "年度报表_" + timestamp, sheets)); + } + } + + if (excelList.isEmpty()) { + log.warn("年度导出没有可用的数据"); + return; + } + + String zipFileName = "年度报表.zip"; + ExportZipUtil.exportToResponse(response, zipFileName, excelList); + } + + /** + * 构建年度水质达标率 Sheet + * 格式:电站 | 所在流域 | 小时尺度(%) | 日均尺度(%) | 主要污染物 + */ + private ExportZipUtil.SheetData buildYearWqSheet(List wqStations, int year) { + List stcdList = wqStations.stream().map(StationInfo::getStcd).distinct().toList(); + if (stcdList.isEmpty()) return null; + + String startTime = year + "-01-01 00:00:00"; + String endTime = (year + 1) + "-01-01 00:00:00"; + + // 查询小时/日均达标数据 + List> hourlyData = sdWqRMapper.selectComplianceData(stcdList, startTime, endTime); + List> dailyData = sdWqdaySMapper.selectComplianceData(stcdList, startTime, endTime); + // 查询主要污染物 + List> pollutantData = sdWqRMapper.selectPollutants(stcdList, startTime, endTime); + // 查询要素中文名 + Map colCommentMap = new LinkedHashMap<>(); + List> colComments = sdWqRMapper.selectColumnComments(); + for (Map cm : colComments) { + String colName = (String) cm.get("COLUMN_NAME"); + String comment = (String) cm.get("COMMENTS"); + if (colName != null && comment != null && !comment.isEmpty()) { + colCommentMap.put(colName, comment); + } + } + + // 按 STCD 聚合污染物 + Map> pollutantSetMap = new LinkedHashMap<>(); + for (Map m : pollutantData) { + String stcd = (String) m.get("STCD"); + String pol = (String) m.get("POLLUTANT"); + if (pol != null && !pol.isEmpty()) { + Set set = pollutantSetMap.computeIfAbsent(stcd, k -> new LinkedHashSet<>()); + for (String field : pol.split(",")) { + String f = field.trim(); + if (!f.isEmpty()) { + String desc = colCommentMap.getOrDefault(f, f); + set.add(desc); + } + } + } + } + + // 索引达标统计 + Map> hourlyMap = indexByStcd(hourlyData); + Map> dailyMap = indexByStcd(dailyData); + + // 表头 + List headers = Arrays.asList("电站", "所在流域", "小时尺度(%)", "日均尺度(%)", "主要污染物"); + + // 数据行 + List> rows = new ArrayList<>(); + for (StationInfo station : wqStations) { + List row = new ArrayList<>(); + row.add(station.getEnnm() != null ? station.getEnnm() : station.getStnm()); + row.add(buildBasinPath(station)); + // 小时达标率 + row.add(calcComplianceRate(hourlyMap.get(station.getStcd()))); + // 日均达标率 + row.add(calcComplianceRate(dailyMap.get(station.getStcd()))); + // 主要污染物 + Set pollutants = pollutantSetMap.get(station.getStcd()); + row.add(pollutants != null && !pollutants.isEmpty() ? String.join(", ", pollutants) : ""); + rows.add(row); + } + + return new ExportZipUtil.SheetData("电站-自动站水质达标率", headers, rows); + } + + /** + * 构建年度生态流量电站维度 Sheet(环保部/水利部/多年平均) + * 格式:电站 | 所在流域 | 所在水电基地 | 小时尺度(%) | 日尺度(%) | 月尺度(%) + */ + private ExportZipUtil.SheetData buildYearQecStationSheet(List engStations, int year, String dept) { + List stcdList = engStations.stream().map(StationInfo::getStcd).distinct().toList(); + if (stcdList.isEmpty()) return null; + + String startTime = year + "-01-01 00:00:00"; + String endTime = (year + 1) + "-01-01 00:00:00"; + + // 查询三种维度达标数据 + List> hourlyData = sdQecRMapper.selectComplianceData(stcdList, startTime, endTime); + List> dailyData = sdQecdaySMapper.selectComplianceData(stcdList, startTime, endTime); + List> monthlyData = yearStatMapper.selectQecMonthlyCompliance(stcdList, year); + + // 按 STCD 聚合小时/日数据(原始数据是逐行明细,需聚合) + Map hourlyAgg = aggregateQecCompliance(hourlyData, dept); + Map dailyAgg = aggregateQecCompliance(dailyData, dept); + Map> monthlyMap = indexByStcd(monthlyData); + + // 部门名称和对应的 SFDB 字段 + String deptName; + String sfdbField; + String compliantField; + String totalField; + switch (dept) { + case "MWR": + deptName = "水利部"; + sfdbField = "MWR_SFDB"; + compliantField = "MWR_COMPLIANT"; + totalField = "MWR_TOTAL"; + break; + case "AVQ": + deptName = "多年平均"; + sfdbField = "AVQ_SFDB"; + compliantField = "AVQ_COMPLIANT"; + totalField = "AVQ_TOTAL"; + break; + default: // QEC 环保部 + deptName = "环保部"; + sfdbField = "SFDB"; + compliantField = "QEC_COMPLIANT"; + totalField = "QEC_TOTAL"; + break; + } + + List headers = Arrays.asList("电站", "所在流域", "所在水电基地", "小时尺度(%)", "日尺度(%)", "月尺度(%)"); + List> rows = new ArrayList<>(); + + for (StationInfo station : engStations) { + List row = new ArrayList<>(); + row.add(station.getEnnm() != null ? station.getEnnm() : station.getStnm()); + row.add(buildBasinPath(station)); + row.add(station.getBaseName() != null ? station.getBaseName() : ""); + // 小时达标率 + long[] hAgg = hourlyAgg.get(station.getStcd()); + row.add(formatRate(hAgg != null ? hAgg[0] : 0, hAgg != null ? hAgg[1] : 0)); + // 日达标率 + long[] dAgg = dailyAgg.get(station.getStcd()); + row.add(formatRate(dAgg != null ? dAgg[0] : 0, dAgg != null ? dAgg[1] : 0)); + // 月度达标率 + row.add(calcMonthlyComplianceRate(monthlyMap.get(station.getStcd()), compliantField, totalField)); + rows.add(row); + } + + return new ExportZipUtil.SheetData("电站-生态流量达标率(" + deptName + ")", headers, rows); + } + + /** + * 构建年度生态流量水电基地维度 Sheet + * 格式:水电基地 | 小时尺度(%) | 日尺度(%) | 月尺度(%) + */ + private ExportZipUtil.SheetData buildYearQecBaseSheet(List baseIds, int year, String dept) { + if (baseIds.isEmpty()) return null; + + // 查询该 BASE_ID 下的所有 ENG 测站 + List baseStations = yearStatMapper.selectList( + new LambdaQueryWrapper() + .eq(StationInfo::getSttp, "ENG") + .in(StationInfo::getBaseId, baseIds) + ); + if (baseStations.isEmpty()) return null; + + // 按 BASE_ID 分组 + Map> byBase = new LinkedHashMap<>(); + Map baseNameMap = new LinkedHashMap<>(); + for (StationInfo s : baseStations) { + String bid = s.getBaseId(); + if (bid == null) continue; + byBase.computeIfAbsent(bid, k -> new ArrayList<>()).add(s); + if (s.getBaseName() != null) baseNameMap.put(bid, s.getBaseName()); + } + + // 收集所有 STCD + List allStcds = baseStations.stream().map(StationInfo::getStcd).distinct().toList(); + String startTime = year + "-01-01 00:00:00"; + String endTime = (year + 1) + "-01-01 00:00:00"; + + // 查询达标数据 + List> hourlyData = sdQecRMapper.selectComplianceData(allStcds, startTime, endTime); + List> dailyData = sdQecdaySMapper.selectComplianceData(allStcds, startTime, endTime); + List> monthlyData = yearStatMapper.selectQecMonthlyComplianceByBase(baseIds, year); + + Map> monthlyMap = new LinkedHashMap<>(); + for (Map m : monthlyData) { + String bid = (String) m.get("BASE_ID"); + if (bid != null) monthlyMap.put(bid, m); + } + + String deptName; + String compliantField; + String totalField; + switch (dept) { + case "MWR": + deptName = "水利部"; + compliantField = "MWR_COMPLIANT"; + totalField = "MWR_TOTAL"; + break; + case "AVQ": + deptName = "多年平均"; + compliantField = "AVQ_COMPLIANT"; + totalField = "AVQ_TOTAL"; + break; + default: + deptName = "环保部"; + compliantField = "QEC_COMPLIANT"; + totalField = "QEC_TOTAL"; + break; + } + + List headers = Arrays.asList("水电基地", "小时尺度(%)", "日尺度(%)", "月尺度(%)"); + List> rows = new ArrayList<>(); + + for (String baseId : baseIds) { + List sts = byBase.get(baseId); + if (sts == null || sts.isEmpty()) continue; + + List baseStcds = sts.stream().map(StationInfo::getStcd).distinct().toList(); + + // 按 BASE 聚合达标率 + long hourCompliant = 0, hourTotal = 0; + long dayCompliant = 0, dayTotal = 0; + + for (Map m : hourlyData) { + if (baseStcds.contains(m.get("STCD"))) { + hourCompliant += countCompliant(m, "SFDB", "MWR_SFDB", "AVQ_SFDB", dept); + hourTotal += countTotal(m, "SFDB", "MWR_SFDB", "AVQ_SFDB", dept); + } + } + for (Map m : dailyData) { + if (baseStcds.contains(m.get("STCD"))) { + dayCompliant += countCompliant(m, "SFDB", "MWR_SFDB", "AVQ_SFDB", dept); + dayTotal += countTotal(m, "SFDB", "MWR_SFDB", "AVQ_SFDB", dept); + } + } + + List row = new ArrayList<>(); + row.add(baseNameMap.getOrDefault(baseId, baseId)); + row.add(formatRate(hourCompliant, hourTotal)); + row.add(formatRate(dayCompliant, dayTotal)); + + // 月度达标率(直接从 SQL 聚合结果获取) + Map mStat = monthlyMap.get(baseId); + if (mStat != null) { + Object cObj = mStat.get(compliantField); + Object tObj = mStat.get(totalField); + long c = cObj instanceof Number ? ((Number) cObj).longValue() : 0; + long t = tObj instanceof Number ? ((Number) tObj).longValue() : 0; + row.add(formatRate(c, t)); + } else { + row.add(""); + } + rows.add(row); + } + + return new ExportZipUtil.SheetData("水电基地-生态流量达标率(" + deptName + ")", headers, rows); + } + + // ======================== 年度导出辅助方法 ======================== + + /** + * 按 STCD 索引 Map 列表 + */ + private Map> indexByStcd(List> data) { + Map> result = new LinkedHashMap<>(); + for (Map m : data) { + String stcd = (String) m.get("STCD"); + if (stcd != null) result.put(stcd, m); + } + return result; + } + + /** + * 构建流域路径:一级流域-二级流域-水系 + */ + private String buildBasinPath(StationInfo station) { +// StringBuilder sb = new StringBuilder(); +// if (station.getHbrvcdName() != null) sb.append(station.getHbrvcdName()); +// if (station.getRvcdName() != null) { +// if (sb.length() > 0) sb.append("-"); +// sb.append(station.getRvcdName()); +// } +// if (station.getHynm() != null) { +// if (sb.length() > 0) sb.append("-"); +// sb.append(station.getHynm()); +// } +// return sb.toString(); + return station.getReachcdName(); + } + + /** + * 计算水质达标率(%, 保留2位小数) + */ + private String calcComplianceRate(Map stat) { + if (stat == null) return ""; + Object cObj = stat.get("COMPLIANT_CNT"); + Object tObj = stat.get("TOTAL_CNT"); + long compliant = cObj instanceof Number ? ((Number) cObj).longValue() : 0; + long total = tObj instanceof Number ? ((Number) tObj).longValue() : 0; + return formatRate(compliant, total); + } + + /** + * 计算 QEC 部门达标率(从明细数据 SFDB/MWR_SFDB/AVQ_SFDB) + */ + private String calcQecComplianceRate(Map stat, String dept) { + if (stat == null) return ""; + long compliant = countCompliant(stat, "SFDB", "MWR_SFDB", "AVQ_SFDB", dept); + long total = countTotal(stat, "SFDB", "MWR_SFDB", "AVQ_SFDB", dept); + return formatRate(compliant, total); + } + + /** + * 聚合 QEC 达标明细数据:按 STCD 汇总达标数/总数 + * + * @return Map 其中 [0]=compliant, [1]=total + */ + private Map aggregateQecCompliance(List> data, String dept) { + Map result = new LinkedHashMap<>(); + for (Map m : data) { + String stcd = (String) m.get("STCD"); + if (stcd == null) continue; + result.computeIfAbsent(stcd, k -> new long[2]); + long[] arr = result.get(stcd); + arr[0] += countCompliant(m, "SFDB", "MWR_SFDB", "AVQ_SFDB", dept); + arr[1] += countTotal(m, "SFDB", "MWR_SFDB", "AVQ_SFDB", dept); + } + return result; + } + + private long countCompliant(Map map, String qecField, String mwrField, String avqField, String dept) { + String field; + switch (dept) { + case "MWR": + field = mwrField; + break; + case "AVQ": + field = avqField; + break; + default: + field = qecField; + } + Object val = map.get(field); + if (val instanceof Number && ((Number) val).intValue() == 1) return 1; + return 0; + } + + private long countTotal(Map map, String qecField, String mwrField, String avqField, String dept) { + String field; + switch (dept) { + case "MWR": + field = mwrField; + break; + case "AVQ": + field = avqField; + break; + default: + field = qecField; + } + Object val = map.get(field); + if (val instanceof Number) { + int v = ((Number) val).intValue(); + if (v == 0 || v == 1) return 1; + } + return 0; + } + + /** + * 计算月度达标率(从聚合结果 COMPLIANT/TOTAL 字段) + */ + private String calcMonthlyComplianceRate(Map stat, String compliantField, String totalField) { + if (stat == null) return ""; + Object cObj = stat.get(compliantField); + Object tObj = stat.get(totalField); + long c = cObj instanceof Number ? ((Number) cObj).longValue() : 0; + long t = tObj instanceof Number ? ((Number) tObj).longValue() : 0; + return formatRate(c, t); + } + + private String formatRate(long compliant, long total) { + if (total > 0) { + BigDecimal rate = BigDecimal.valueOf(compliant * 100.0 / total); + return rate.setScale(2, RoundingMode.HALF_UP).toPlainString(); + } + return ""; + } + + /** + * 按沿程排序测站 + */ + private List sortStationsByOrder(List stations) { + // 按 RSTCD 收集沿程排序 + List rstcds = stations.stream().map(StationInfo::getRstcd).filter(Objects::nonNull).distinct().toList(); + if (rstcds.isEmpty()) return stations; + + List> orderList = msAlongdetBMapper.selectAlongSortOrder(rstcds); + Map orderMap = new LinkedHashMap<>(); + for (Map m : orderList) { + String rstcd = (String) m.get("RSTCD"); + Object sortObj = m.get("SORT"); + if (rstcd != null && sortObj instanceof Number) { + orderMap.put(rstcd, ((Number) sortObj).intValue()); + } + } + + List sorted = new ArrayList<>(stations); + sorted.sort((a, b) -> { + Integer oa = orderMap.get(a.getRstcd()); + Integer ob = orderMap.get(b.getRstcd()); + if (oa != null && ob != null) return oa.compareTo(ob); + if (oa != null) return -1; + if (ob != null) return 1; + return 0; + }); + return sorted; + } + // ======================== hour/day 原始数据导出 ======================== /** @@ -1451,10 +2008,10 @@ public class QgcExportServiceImpl implements IQgcExportService { * 每行 = 一个时间点,列 = 各测站的原始值 */ private List> buildRawSheetRows(ExportIndicatorProcessor processor, - List stcds, - Map stcdDisplayMap, - String startTime, String endTime, - boolean isDaily) { + List stcds, + Map stcdDisplayMap, + String startTime, String endTime, + boolean isDaily) { try { Date start = SDF.parse(startTime); Date end = SDF.parse(endTime); @@ -1467,7 +2024,7 @@ public class QgcExportServiceImpl implements IQgcExportService { for (IndicatorDataRow row : rawData) { String timeKey = timeFmt.format(row.getTm()); timeMap.computeIfAbsent(timeKey, k -> new LinkedHashMap<>()) - .put(row.getStcd(), row.getValue()); + .put(row.getStcd(), row.getValue()); } // 构建行数据(按时间排序,避免 LinkedHashMap 插入顺序导致乱序) @@ -1496,8 +2053,8 @@ public class QgcExportServiceImpl implements IQgcExportService { * 格式:测站 | 时间 | 水深(m) | 水温(℃) */ private List> buildRwtRawSheetRows(List stcds, - Map stcdDisplayMap, - String startTime, String endTime) { + Map stcdDisplayMap, + String startTime, String endTime) { try { List> rawList = sdWtvtdaySMapper.selectDailyData(stcds, startTime, endTime); if (rawList == null || rawList.isEmpty()) return null; diff --git a/backend/src/main/resources/mapper/qgc_export/YearStatMapper.xml b/backend/src/main/resources/mapper/qgc_export/YearStatMapper.xml new file mode 100644 index 00000000..f401ca7d --- /dev/null +++ b/backend/src/main/resources/mapper/qgc_export/YearStatMapper.xml @@ -0,0 +1,49 @@ + + + + + + + + + + +