diff --git a/backend/src/main/java/com/yfd/platform/qgc_export/domain/StationMapping.java b/backend/src/main/java/com/yfd/platform/qgc_export/domain/StationMapping.java index 61af0b13..c8f58ee6 100644 --- a/backend/src/main/java/com/yfd/platform/qgc_export/domain/StationMapping.java +++ b/backend/src/main/java/com/yfd/platform/qgc_export/domain/StationMapping.java @@ -27,4 +27,8 @@ public class StationMapping implements Serializable { /** 测站类型(如 ZQ=河道水情, XX=生态流量等),用于区分同一电站下不同类型的测站 */ @TableField("STTP") private String sttp; + + /** 测站名称 */ + @TableField("STNM") + private String stnm; } 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 ea60381c..5ad37af1 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 @@ -27,29 +27,30 @@ import java.util.*; import java.util.stream.Collectors; /** - * 综合导出服务实现 + * 综合导出服务实现(月度报表) *

* 格式:每月一个 Excel 文件 → ZIP 下载。 - * 每个 Excel 含多个 Sheet(每个指标一个 Sheet),每行是一个统计指标。 + * 每个 Excel 含多个 Sheet(每个指标一个 Sheet)。 + * + *

Sheet 结构

+ *
+ *     日期        | 班多      | 羊曲       ← 表头(Row 0:第一列=日期,后面列=测站名称)
+ *   01           | 2757.711  | 2709.839  ← 每日数据(Row 1~31)
+ *   02           | 2757.806  | 2709.794
+ *   ...
+ *   31           | 2757.751  | 2709.714
+ *   日均最低      | 2757.549  | 2709.565  ← 汇总指标(7行)
+ *   日均最高      | 2757.879  | 2709.903
+ *   月内最低      | 2756.979  | 2709.463
+ *   月内最低时间  | 31日 08时  | 27日 08时
+ *   月内最高      | 2758.003  | 2709.983
+ *   月内最高时间  | 12日 16时  | 24日 16时
+ *   月均         | 2757.751  | 2757.751
+ * 
* *

电站→测站映射(STTP 类型区分)

* 同一电站(RSTCD)在 V_MS_STBPRP_T 视图中可能有多个测站(STCD), - * 不同类型的指标需要不同类型的测站数据。通过 {@link ExportIndicatorProcessor#getSttpType()} - * 区分: - * - * - *
- *     日期        | 电站A    | 电站B
- *   日均最低      | 0.523    | 0.612
- *   日均最高      | 1.234    | 1.456
- *   月内最低      | 0.412    | 0.523
- *   月内最低时间  | 2026-08-15 03:00:00 | 2026-08-14 05:30:00
- *   月内最高      | 2.156    | 2.345
- *   月内最高时间  | 2026-08-03 12:00:00 | 2026-08-20 18:45:00
- * 
+ * 不同类型的指标需要不同类型的测站数据。通过 {@link ExportIndicatorProcessor#getSttpType()} 区分。 */ @Service public class QgcExportServiceImpl implements IQgcExportService { @@ -71,11 +72,12 @@ public class QgcExportServiceImpl implements IQgcExportService { @Resource private ZProcessor zProcessor; - private static final SimpleDateFormat SDF_DATETIME = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + /** 时间格式:dd日 HH时(如 "31日 08时") */ + private static final SimpleDateFormat SDF_TIME = new SimpleDateFormat("dd日 HH时"); - /** 行标题:6 个统计指标 */ + /** 汇总指标行标签(7个) */ private static final List INDICATOR_LABELS = Collections.unmodifiableList( - Arrays.asList("日均最低", "日均最高", "月内最低", "月内最低时间", "月内最高", "月内最高时间")); + Arrays.asList("日均最低", "日均最高", "月内最低", "月内最低时间", "月内最高", "月内最高时间", "月均")); @Override public void exportData(List dataFields, List months, @@ -96,15 +98,15 @@ public class QgcExportServiceImpl implements IQgcExportService { // 3. 电站 → 测站映射(按 STTP 类型分别获取) StationResolution stationRes = resolveStations(stationCodes, sttpTypes); - if (stationRes.stationNames.isEmpty()) { + if (stationRes.displayNames.isEmpty()) { log.warn("未找到任何电站映射数据,导出取消"); return; } - // 4. 电站顺序 + 名称 - List stationOrder = new ArrayList<>(stationRes.stationNames.keySet()); - List stationNames = stationOrder.stream() - .map(stationRes::getStationName) + // 4. 电站顺序 + 列标题(测站名称) + List stationOrder = new ArrayList<>(stationRes.displayNames.keySet()); + List headerNames = stationOrder.stream() + .map(stationRes::getDisplayName) .toList(); // 5. 每月一个 ExcelData @@ -115,13 +117,11 @@ public class QgcExportServiceImpl implements IQgcExportService { List sheets = new ArrayList<>(); for (ExportIndicatorProcessor processor : processors) { - // 查询 + 聚合这一个月的数据(使用该处理器对应的 STTP 测站) - List> rows = buildIndicatorRows( - processor, mr, stationOrder, stationRes); + List> rows = buildSheetRows(processor, mr, stationOrder, stationRes); if (rows != null) { List headers = new ArrayList<>(); - headers.add("日期"); // 表头第一列 = 日期 - headers.addAll(stationNames); + headers.add("日期"); + headers.addAll(headerNames); sheets.add(new ExportZipUtil.SheetData(processor.getSheetName(), headers, rows)); } } @@ -135,32 +135,21 @@ public class QgcExportServiceImpl implements IQgcExportService { log.warn("没有可导出的数据"); return; } - String fileNamePrefix = getFileNamePrefix(tmDimension); - // 6. 委托 ExportZipUtil 导出 ZIP - String zipFileName = fileNamePrefix + ".zip"; -// String zipFileName = fileNamePrefix + System.currentTimeMillis() + ".zip"; + + String zipFileName = getFileNamePrefix(tmDimension) + ".zip"; ExportZipUtil.exportToResponse(response, zipFileName, excelList); } - // 根据维度类型返回文件名称前缀 - private static String getFileNamePrefix(String tmDimension) { - return switch (tmDimension) { - case "year" -> "年度报表"; - case "month" -> "月度报表"; - case "day" -> "日度报表"; - case "hour" -> "小时报表"; - default -> "未知"; - }; - } - - // ======================== 构建单个指标的 Sheet 数据 ======================== + // ======================== 构建 Sheet 数据 ======================== /** - * 查询并聚合一个月的数据,组装成 SheetData 的 rows - *

- * 使用 processor.getSttpType() 映射到正确的测站编码。 + * 查询一个月的数据并组装为一个 Sheet 的全部行: + *

    + *
  1. 日数据行(01~31):每天每电站一个值
  2. + *
  3. 汇总指标行(7行):日均最低、日均最高、月内最低、月内最低时间、月内最高、月内最高时间、月均
  4. + *
*/ - private List> buildIndicatorRows( + private List> buildSheetRows( ExportIndicatorProcessor processor, MonthRange mr, List stationOrder, StationResolution stationRes) { @@ -180,22 +169,38 @@ public class QgcExportServiceImpl implements IQgcExportService { return null; } - // 去重 STCD 用于查询 List uniqueStcds = rstcdToStcd.values().stream().distinct().toList(); - // 查询日表数据(用于日均最低/最高) + // 查询日表 + 小时表数据 List dailyRows = processor.queryRawData(uniqueStcds, mr.getStart(), mr.getEnd(), true); - // 查询小时表数据(用于月内最低/最高) List hourlyRows = processor.queryRawData(uniqueStcds, mr.getStart(), mr.getEnd(), false); - // 按电站聚合 + // 汇总聚合 Map aggMap = aggregate(dailyRows, hourlyRows, rstcdToStcd); - // 组装 6 行 - List> rows = new ArrayList<>(6); - for (int i = 0; i < 6; i++) { + // 日数据按 (stcd, day) 索引 + Map> dailyByStcd = indexDailyByDay(dailyRows); + + List> rows = new ArrayList<>(); + + // ---- 日数据行 ---- + int daysInMonth = getDaysInMonth(mr); + for (int d = 1; d <= daysInMonth; d++) { List row = new ArrayList<>(); - row.add(INDICATOR_LABELS.get(i)); // 第一列:指标名称 + row.add(String.format("%02d", d)); + for (String rstcd : stationOrder) { + String stcd = rstcdToStcd.get(rstcd); + Map dayMap = stcd != null ? dailyByStcd.get(stcd) : null; + BigDecimal val = dayMap != null ? dayMap.get(d) : null; + row.add(formatOrEmpty(val)); + } + rows.add(row); + } + + // ---- 汇总指标行 ---- + for (int i = 0; i < 7; i++) { + List row = new ArrayList<>(); + row.add(INDICATOR_LABELS.get(i)); for (String rstcd : stationOrder) { Aggregation agg = aggMap.get(rstcd); @@ -203,40 +208,33 @@ public class QgcExportServiceImpl implements IQgcExportService { case 0: row.add(formatOrEmpty(agg != null ? agg.getDailyMin() : null)); break; case 1: row.add(formatOrEmpty(agg != null ? agg.getDailyMax() : null)); break; case 2: row.add(formatOrEmpty(agg != null ? agg.getExtremeMin() : null)); break; - case 3: row.add(agg != null && agg.getExtremeMinTime() != null ? agg.getExtremeMinTime() : ""); 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)); break; - case 5: row.add(agg != null && agg.getExtremeMaxTime() != null ? agg.getExtremeMaxTime() : ""); 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)); break; } } rows.add(row); } + return rows; } // ======================== 指标处理器匹配 ======================== - /** - * 根据前端 dataField 匹配处理器。 - *

- * 新增指标时只需在这里注册一行即可,Controller 无需改动。 - *

    - *
  • {@link RiverIndicatorProcessor} 子类需要调用 setMappers() 注入 Mapper
  • - *
  • 独立实现的 {@link ExportIndicatorProcessor}(如 QEC)自行管理 Mapper
  • - *
- */ private List resolveProcessors(List dataFields) { Map processorMap = new LinkedHashMap<>(); processorMap.put("v", vProcessor); processorMap.put("q", qProcessor); processorMap.put("z", zProcessor); - // 未来新增: processorMap.put("qec", qecProcessor); List result = new ArrayList<>(); for (String field : dataFields) { String lower = field.trim().toLowerCase(); ExportIndicatorProcessor p = processorMap.get(lower); if (p != null) { - // RiverIndicatorProcessor 需要注入 Mapper(非 Spring Bean,手动设置) if (p instanceof RiverIndicatorProcessor) { ((RiverIndicatorProcessor) p).setMappers(sdRiverRMapper, sdRiverdaySMapper); } @@ -246,18 +244,8 @@ public class QgcExportServiceImpl implements IQgcExportService { return result; } - // ======================== 电站映射(按 STTP 类型区分) ======================== + // ======================== 电站映射(按 STTP 类型区分 + 测站名称) ======================== - /** - * 查询 V_MS_STBPRP_T 视图,同时获取: - *
    - *
  1. 电站中文名(ENNM):首次出现时记录,所有 STTP 共用
  2. - *
  3. STTP → STCD 映射:每个 RSTCD 下按 STTP 类型取第一个测站编码
  4. - *
- * - * @param rstcds 前端传入的电站编码列表 - * @param sttpTypes 需要的测站类型集合(从 processors 收集) - */ private StationResolution resolveStations(List rstcds, Set sttpTypes) { StationResolution result = new StationResolution(); if (rstcds == null || rstcds.isEmpty()) return result; @@ -265,7 +253,6 @@ public class QgcExportServiceImpl implements IQgcExportService { List mappings = stationMappingMapper.selectList( new LambdaQueryWrapper().in(StationMapping::getRstcd, rstcds)); - // 按 RSTCD 分组 Map> byRstcd = new LinkedHashMap<>(); for (StationMapping m : mappings) { if (m.getRstcd() == null) continue; @@ -275,15 +262,31 @@ public class QgcExportServiceImpl implements IQgcExportService { for (String rstcd : rstcds) { List rows = byRstcd.get(rstcd); if (rows == null || rows.isEmpty()) { - // 无映射数据,电站名回退为编码 - result.stationNames.put(rstcd, rstcd); + result.displayNames.put(rstcd, rstcd); result.sttpStcdMap.put(rstcd, new LinkedHashMap<>()); continue; } - // 电站中文名:取第一条记录(所有 STTP 共用同一个名称) - String ennm = rows.get(0).getEnnm(); - result.stationNames.put(rstcd, ennm != null ? ennm : rstcd); + // 测站名称:优先 ZQ 类型的 STNM → 任意 STNM → ENNM → RSTCD + String displayName = null; + for (StationMapping m : rows) { + if ("ZQ".equals(m.getSttp()) && m.getStnm() != null) { + displayName = m.getStnm(); + break; + } + } + if (displayName == null) { + for (StationMapping m : rows) { + if (m.getStnm() != null) { displayName = m.getStnm(); break; } + } + } + if (displayName == null) { + displayName = rows.get(0).getEnnm(); + } + if (displayName == null) { + displayName = rstcd; + } + result.displayNames.put(rstcd, displayName); // STTP → STCD 映射:每种 STTP 取第一个 Map sttpMap = new LinkedHashMap<>(); @@ -296,7 +299,7 @@ public class QgcExportServiceImpl implements IQgcExportService { result.sttpStcdMap.put(rstcd, sttpMap); } - log.info("电站映射解析完成,RSTCD数量={}, STTP类型={}", result.stationNames.size(), sttpTypes); + log.info("电站映射解析完成,RSTCD数量={}, STTP类型={}", result.displayNames.size(), sttpTypes); return result; } @@ -324,6 +327,14 @@ public class QgcExportServiceImpl implements IQgcExportService { } } + /** 计算月份的天数 */ + private int getDaysInMonth(MonthRange mr) { + Calendar cal = Calendar.getInstance(); + cal.setTime(mr.getEnd()); + cal.add(Calendar.DAY_OF_MONTH, -1); + return cal.get(Calendar.DAY_OF_MONTH); + } + // ======================== 数据聚合 ======================== private Map aggregate( @@ -336,7 +347,7 @@ public class QgcExportServiceImpl implements IQgcExportService { String stcd = rstcdToStcd.get(rstcd); Aggregation agg = new Aggregation(); - // 日均最低/最高:筛选该测站的所有日数据 + // 日均最低/最高/月均:筛选该测站的所有日数据 List dailyValues = dailyRows.stream() .filter(r -> Objects.equals(r.getStcd(), stcd) && r.getValue() != null) .map(IndicatorDataRow::getValue) @@ -344,9 +355,12 @@ public class QgcExportServiceImpl implements IQgcExportService { if (!dailyValues.isEmpty()) { agg.setDailyMin(Collections.min(dailyValues)); agg.setDailyMax(Collections.max(dailyValues)); + // 月均 = 日均值的平均值 + BigDecimal sum = dailyValues.stream().reduce(BigDecimal.ZERO, BigDecimal::add); + agg.setMonthlyAvg(sum.divide(BigDecimal.valueOf(dailyValues.size()), 3, RoundingMode.HALF_UP)); } - // 月内最低/最高:筛选该测站的所有小时数据 + // 月内最低/最高及时间:筛选该测站的所有小时数据 List hourlyStationData = hourlyRows.stream() .filter(r -> Objects.equals(r.getStcd(), stcd) && r.getValue() != null) .collect(Collectors.toList()); @@ -356,9 +370,9 @@ public class QgcExportServiceImpl implements IQgcExportService { IndicatorDataRow maxRow = Collections.max(hourlyStationData, Comparator.comparing(IndicatorDataRow::getValue)); agg.setExtremeMin(minRow.getValue()); - agg.setExtremeMinTime(SDF_DATETIME.format(minRow.getTm())); + agg.setExtremeMinTime(minRow.getTm()); agg.setExtremeMax(maxRow.getValue()); - agg.setExtremeMaxTime(SDF_DATETIME.format(maxRow.getTm())); + agg.setExtremeMaxTime(maxRow.getTm()); } result.put(rstcd, agg); @@ -366,24 +380,46 @@ public class QgcExportServiceImpl implements IQgcExportService { return result; } + /** + * 将日表数据按 (stcd → day → value) 索引,供日数据行快速查找 + */ + private Map> indexDailyByDay(List dailyRows) { + Map> result = new LinkedHashMap<>(); + for (IndicatorDataRow row : dailyRows) { + if (row.getStcd() == null || row.getTm() == null || row.getValue() == null) continue; + Calendar cal = Calendar.getInstance(); + cal.setTime(row.getTm()); + int day = cal.get(Calendar.DAY_OF_MONTH); + result.computeIfAbsent(row.getStcd(), k -> new LinkedHashMap<>()).putIfAbsent(day, row.getValue()); + } + return result; + } + private static String formatOrEmpty(BigDecimal value) { if (value == null) return ""; return value.setScale(3, RoundingMode.HALF_UP).toPlainString(); } + private static String getFileNamePrefix(String tmDimension) { + return switch (tmDimension) { + case "year" -> "年度报表"; + case "month" -> "月度报表"; + case "day" -> "日度报表"; + case "hour" -> "小时报表"; + default -> "未知报表"; + }; + } + // ======================== 内部数据类 ======================== - /** - * 电站→测站映射解析结果 - */ private static class StationResolution { - /** RSTCD → 电站中文名 */ - final Map stationNames = new LinkedHashMap<>(); + /** RSTCD → 测站名称(列标题显示用) */ + final Map displayNames = new LinkedHashMap<>(); /** RSTCD → (STTP → STCD) */ final Map> sttpStcdMap = new LinkedHashMap<>(); - String getStationName(String rstcd) { - return stationNames.getOrDefault(rstcd, rstcd); + String getDisplayName(String rstcd) { + return displayNames.getOrDefault(rstcd, rstcd); } String getStcd(String rstcd, String sttp) { @@ -405,8 +441,12 @@ public class QgcExportServiceImpl implements IQgcExportService { private BigDecimal dailyMin; private BigDecimal dailyMax; private BigDecimal extremeMin; - private String extremeMinTime; + /** 月内最低发生时间 */ + private Date extremeMinTime; private BigDecimal extremeMax; - private String extremeMaxTime; + /** 月内最高发生时间 */ + private Date extremeMaxTime; + /** 月均(所有日值的平均值) */ + private BigDecimal monthlyAvg; } }