fix: 优化excel综合导出流量、水位、流速方法

This commit is contained in:
tangwei 2026-08-05 14:25:59 +08:00
parent d6d8c87b19
commit 3d952315a5
2 changed files with 147 additions and 103 deletions

View File

@ -27,4 +27,8 @@ public class StationMapping implements Serializable {
/** 测站类型(如 ZQ=河道水情, XX=生态流量等),用于区分同一电站下不同类型的测站 */
@TableField("STTP")
private String sttp;
/** 测站名称 */
@TableField("STNM")
private String stnm;
}

View File

@ -27,29 +27,30 @@ import java.util.*;
import java.util.stream.Collectors;
/**
* 综合导出服务实现
* 综合导出服务实现月度报表
* <p>
* 格式每月一个 Excel 文件 ZIP 下载
* 每个 Excel 含多个 Sheet每个指标一个 Sheet每行是一个统计指标
* 每个 Excel 含多个 Sheet每个指标一个 Sheet
*
* <h3>Sheet 结构</h3>
* <pre>
* 日期 | 班多 | 羊曲 表头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
* </pre>
*
* <h3>电站测站映射STTP 类型区分</h3>
* 同一电站RSTCD V_MS_STBPRP_T 视图中可能有多个测站STCD
* 不同类型的指标需要不同类型的测站数据通过 {@link ExportIndicatorProcessor#getSttpType()}
* 区分
* <ul>
* <li>V/Q/Z 河道水情 STTP="ZQ"</li>
* <li>QEC 生态流量 STTP=待定</li>
* </ul>
*
* <pre>
* 日期 | 电站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
* </pre>
* 不同类型的指标需要不同类型的测站数据通过 {@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<String> INDICATOR_LABELS = Collections.unmodifiableList(
Arrays.asList("日均最低", "日均最高", "月内最低", "月内最低时间", "月内最高", "月内最高时间"));
Arrays.asList("日均最低", "日均最高", "月内最低", "月内最低时间", "月内最高", "月内最高时间", "月均"));
@Override
public void exportData(List<String> dataFields, List<String> 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<String> stationOrder = new ArrayList<>(stationRes.stationNames.keySet());
List<String> stationNames = stationOrder.stream()
.map(stationRes::getStationName)
// 4. 电站顺序 + 列标题测站名称
List<String> stationOrder = new ArrayList<>(stationRes.displayNames.keySet());
List<String> headerNames = stationOrder.stream()
.map(stationRes::getDisplayName)
.toList();
// 5. 每月一个 ExcelData
@ -115,13 +117,11 @@ public class QgcExportServiceImpl implements IQgcExportService {
List<ExportZipUtil.SheetData> sheets = new ArrayList<>();
for (ExportIndicatorProcessor processor : processors) {
// 查询 + 聚合这一个月的数据使用该处理器对应的 STTP 测站
List<List<Object>> rows = buildIndicatorRows(
processor, mr, stationOrder, stationRes);
List<List<Object>> rows = buildSheetRows(processor, mr, stationOrder, stationRes);
if (rows != null) {
List<String> 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
* <p>
* 使用 processor.getSttpType() 映射到正确的测站编码
* 查询一个月的数据并组装为一个 Sheet 的全部行
* <ol>
* <li>日数据行01~31每天每电站一个值</li>
* <li>汇总指标行7行日均最低日均最高月内最低月内最低时间月内最高月内最高时间月均</li>
* </ol>
*/
private List<List<Object>> buildIndicatorRows(
private List<List<Object>> buildSheetRows(
ExportIndicatorProcessor processor, MonthRange mr,
List<String> stationOrder, StationResolution stationRes) {
@ -180,22 +169,38 @@ public class QgcExportServiceImpl implements IQgcExportService {
return null;
}
// 去重 STCD 用于查询
List<String> uniqueStcds = rstcdToStcd.values().stream().distinct().toList();
// 查询日表数据用于日均最低/最高
// 查询日表 + 小时表数据
List<IndicatorDataRow> dailyRows = processor.queryRawData(uniqueStcds, mr.getStart(), mr.getEnd(), true);
// 查询小时表数据用于月内最低/最高
List<IndicatorDataRow> hourlyRows = processor.queryRawData(uniqueStcds, mr.getStart(), mr.getEnd(), false);
// 按电站聚合
// 汇总聚合
Map<String, Aggregation> aggMap = aggregate(dailyRows, hourlyRows, rstcdToStcd);
// 组装 6
List<List<Object>> rows = new ArrayList<>(6);
for (int i = 0; i < 6; i++) {
// 日数据按 (stcd, day) 索引
Map<String, Map<Integer, BigDecimal>> dailyByStcd = indexDailyByDay(dailyRows);
List<List<Object>> rows = new ArrayList<>();
// ---- 日数据行 ----
int daysInMonth = getDaysInMonth(mr);
for (int d = 1; d <= daysInMonth; d++) {
List<Object> 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<Integer, BigDecimal> 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<Object> 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 匹配处理器
* <p>
* 新增指标时只需在这里注册一行即可Controller 无需改动
* <ul>
* <li>{@link RiverIndicatorProcessor} 子类需要调用 setMappers() 注入 Mapper</li>
* <li>独立实现的 {@link ExportIndicatorProcessor} QEC自行管理 Mapper</li>
* </ul>
*/
private List<ExportIndicatorProcessor> resolveProcessors(List<String> dataFields) {
Map<String, ExportIndicatorProcessor> processorMap = new LinkedHashMap<>();
processorMap.put("v", vProcessor);
processorMap.put("q", qProcessor);
processorMap.put("z", zProcessor);
// 未来新增: processorMap.put("qec", qecProcessor);
List<ExportIndicatorProcessor> 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 视图同时获取
* <ol>
* <li>电站中文名ENNM首次出现时记录所有 STTP 共用</li>
* <li>STTP STCD 映射每个 RSTCD 下按 STTP 类型取第一个测站编码</li>
* </ol>
*
* @param rstcds 前端传入的电站编码列表
* @param sttpTypes 需要的测站类型集合 processors 收集
*/
private StationResolution resolveStations(List<String> rstcds, Set<String> sttpTypes) {
StationResolution result = new StationResolution();
if (rstcds == null || rstcds.isEmpty()) return result;
@ -265,7 +253,6 @@ public class QgcExportServiceImpl implements IQgcExportService {
List<StationMapping> mappings = stationMappingMapper.selectList(
new LambdaQueryWrapper<StationMapping>().in(StationMapping::getRstcd, rstcds));
// RSTCD 分组
Map<String, List<StationMapping>> 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<StationMapping> 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<String, String> 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<String, Aggregation> aggregate(
@ -336,7 +347,7 @@ public class QgcExportServiceImpl implements IQgcExportService {
String stcd = rstcdToStcd.get(rstcd);
Aggregation agg = new Aggregation();
// 日均最低/最高筛选该测站的所有日数据
// 日均最低/最高/月均筛选该测站的所有日数据
List<BigDecimal> 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<IndicatorDataRow> 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<String, Map<Integer, BigDecimal>> indexDailyByDay(List<IndicatorDataRow> dailyRows) {
Map<String, Map<Integer, BigDecimal>> 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<String, String> stationNames = new LinkedHashMap<>();
/** RSTCD → 测站名称(列标题显示用) */
final Map<String, String> displayNames = new LinkedHashMap<>();
/** RSTCD → (STTP → STCD) */
final Map<String, Map<String, String>> 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;
}
}