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

This commit is contained in:
tangwei 2026-08-05 13:50:39 +08:00
parent 06684578f8
commit d6d8c87b19
6 changed files with 178 additions and 56 deletions

View File

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

View File

@ -4,14 +4,14 @@ import java.util.Date;
import java.util.List;
/**
* 指标处理器接口 每种指标v/q/z/wq/dwt 实现此接口
* 指标处理器接口 每种指标v/q/z/wq/dwt/qec 实现此接口
* <p>
* 新增指标时实现本接口 + QgcExportServiceImpl 中注入即可
* 新增指标时实现本接口 + QgcExportServiceImpl.resolveProcessors() 中注入即可
*/
public interface ExportIndicatorProcessor {
/**
* 指标字段名与前端 dataField 中的值对应 "v", "q", "z"
* 指标字段名与前端 dataField 中的值对应 "v", "q", "z", "qec"
*/
String getFieldName();
@ -20,13 +20,25 @@ public interface ExportIndicatorProcessor {
*/
String getSheetName();
/**
* 测站类型STTP用于区分同一电站下不同用途的测站
* <ul>
* <li>V/Q/Z 河道水情 "ZQ"</li>
* <li>QEC 生态流量 待定 "SQ"</li>
* </ul>
* 返回 null 表示不按 STTP 过滤取该电站的第一个测站
*/
default String getSttpType() {
return null;
}
/**
* 查询原始数据
*
* @param stcds 测站编码列表已从电站编码转换
* @param stcds 测站编码列表已从电站编码转换且已按 STTP 过滤
* @param startTime 起始时间
* @param endTime 结束时间
* @param isDaily 是否查询日表true=SD_RIVERDAY_S, false=SD_RIVER_R
* @param isDaily 是否查询日表true=日表, false=小时表
* @return 原始数据行列表
*/
List<IndicatorDataRow> queryRawData(List<String> stcds, Date startTime, Date endTime, boolean isDaily);

View File

@ -17,4 +17,9 @@ public class QProcessor extends RiverIndicatorProcessor {
public String getSheetName() {
return "流量(m³/s)";
}
@Override
public String getSttpType() {
return "ZQ";
}
}

View File

@ -17,4 +17,9 @@ public class VProcessor extends RiverIndicatorProcessor {
public String getSheetName() {
return "流速(m/s)";
}
@Override
public String getSttpType() {
return "ZQ";
}
}

View File

@ -17,4 +17,9 @@ public class ZProcessor extends RiverIndicatorProcessor {
public String getSheetName() {
return "水位(m)";
}
@Override
public String getSttpType() {
return "ZQ";
}
}

View File

@ -31,6 +31,16 @@ import java.util.stream.Collectors;
* <p>
* 格式每月一个 Excel 文件 ZIP 下载
* 每个 Excel 含多个 Sheet每个指标一个 Sheet每行是一个统计指标
*
* <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
@ -71,39 +81,32 @@ public class QgcExportServiceImpl implements IQgcExportService {
public void exportData(List<String> dataFields, List<String> months,
List<String> stationCodes, boolean isDailyDimension,
String tmDimension, HttpServletResponse response) {
// 1. 电站 测站映射 + 电站名称
Map<String, StationInfo> stationInfoMap = resolveStations(stationCodes);
if (stationInfoMap.isEmpty()) {
log.warn("未找到任何电站映射数据,导出取消");
return;
}
// 2. 指标处理器匹配
// 1. 指标处理器匹配
List<ExportIndicatorProcessor> processors = resolveProcessors(dataFields);
if (processors.isEmpty()) {
log.warn("未找到匹配的指标处理器dataFields={}", dataFields);
return;
}
// 3. 收集电站名称 + 测站列表保持顺序
List<String> stationOrder = new ArrayList<>(stationInfoMap.keySet());
List<String> stationNames = stationOrder.stream()
.map(k -> stationInfoMap.get(k).getStationName())
.toList();
List<String> stcds = stationInfoMap.values().stream()
.map(StationInfo::getStcd)
// 2. 收集所有需要的 STTP 类型
Set<String> sttpTypes = processors.stream()
.map(ExportIndicatorProcessor::getSttpType)
.filter(Objects::nonNull)
.distinct()
.collect(Collectors.toList());
.collect(Collectors.toCollection(LinkedHashSet::new));
// 4. rstcd stcd 映射用于聚合时找数据
Map<String, String> rstcdToStcd = new LinkedHashMap<>();
for (Map.Entry<String, StationInfo> e : stationInfoMap.entrySet()) {
if (e.getValue().getStcd() != null) {
rstcdToStcd.put(e.getKey(), e.getValue().getStcd());
}
// 3. 电站 测站映射 STTP 类型分别获取
StationResolution stationRes = resolveStations(stationCodes, sttpTypes);
if (stationRes.stationNames.isEmpty()) {
log.warn("未找到任何电站映射数据,导出取消");
return;
}
// 4. 电站顺序 + 名称
List<String> stationOrder = new ArrayList<>(stationRes.stationNames.keySet());
List<String> stationNames = stationOrder.stream()
.map(stationRes::getStationName)
.toList();
// 5. 每月一个 ExcelData
List<ExportZipUtil.ExcelData> excelList = new ArrayList<>();
for (String month : months) {
@ -112,9 +115,9 @@ public class QgcExportServiceImpl implements IQgcExportService {
List<ExportZipUtil.SheetData> sheets = new ArrayList<>();
for (ExportIndicatorProcessor processor : processors) {
// 查询 + 聚合这一个月的数据
// 查询 + 聚合这一个月的数据使用该处理器对应的 STTP 测站
List<List<Object>> rows = buildIndicatorRows(
processor, mr, stcds, rstcdToStcd, stationOrder);
processor, mr, stationOrder, stationRes);
if (rows != null) {
List<String> headers = new ArrayList<>();
headers.add("日期"); // 表头第一列 = 日期
@ -132,30 +135,58 @@ public class QgcExportServiceImpl implements IQgcExportService {
log.warn("没有可导出的数据");
return;
}
String fileNamePrefix = getFileNamePrefix(tmDimension);
// 6. 委托 ExportZipUtil 导出 ZIP
String zipFileName = "综合数据导出_" + System.currentTimeMillis() + ".zip";
String zipFileName = fileNamePrefix + ".zip";
// String zipFileName = fileNamePrefix + System.currentTimeMillis() + ".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 数据 ========================
/**
* 查询并聚合一个月的数据组装成 SheetData rows
* <p>
* 返回的每一行 = [指标名, station1值, station2值, ...]
* 使用 processor.getSttpType() 映射到正确的测站编码
*/
private List<List<Object>> buildIndicatorRows(
ExportIndicatorProcessor processor, MonthRange mr,
List<String> stcds, Map<String, String> rstcdToStcd,
List<String> stationOrder) {
List<String> stationOrder, StationResolution stationRes) {
if (stcds.isEmpty()) return null;
String sttp = processor.getSttpType();
// 构建该处理器对应的 RSTCDSTCD 映射
Map<String, String> rstcdToStcd = new LinkedHashMap<>();
for (String rstcd : stationOrder) {
String stcd = stationRes.getStcd(rstcd, sttp);
if (stcd != null) {
rstcdToStcd.put(rstcd, stcd);
}
}
if (rstcdToStcd.isEmpty()) {
log.warn("指标 {} 未找到 STTP={} 的测站数据", processor.getFieldName(), sttp);
return null;
}
// 去重 STCD 用于查询
List<String> uniqueStcds = rstcdToStcd.values().stream().distinct().toList();
// 查询日表数据用于日均最低/最高
List<IndicatorDataRow> dailyRows = processor.queryRawData(stcds, mr.getStart(), mr.getEnd(), true);
List<IndicatorDataRow> dailyRows = processor.queryRawData(uniqueStcds, mr.getStart(), mr.getEnd(), true);
// 查询小时表数据用于月内最低/最高
List<IndicatorDataRow> hourlyRows = processor.queryRawData(stcds, mr.getStart(), mr.getEnd(), false);
List<IndicatorDataRow> hourlyRows = processor.queryRawData(uniqueStcds, mr.getStart(), mr.getEnd(), false);
// 按电站聚合
Map<String, Aggregation> aggMap = aggregate(dailyRows, hourlyRows, rstcdToStcd);
@ -184,42 +215,88 @@ public class QgcExportServiceImpl implements IQgcExportService {
// ======================== 指标处理器匹配 ========================
/**
* 根据前端 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, RiverIndicatorProcessor> riverMap = new LinkedHashMap<>();
riverMap.put("v", vProcessor);
riverMap.put("q", qProcessor);
riverMap.put("z", zProcessor);
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();
RiverIndicatorProcessor p = riverMap.get(lower);
ExportIndicatorProcessor p = processorMap.get(lower);
if (p != null) {
p.setMappers(sdRiverRMapper, sdRiverdaySMapper);
// RiverIndicatorProcessor 需要注入 Mapper Spring Bean手动设置
if (p instanceof RiverIndicatorProcessor) {
((RiverIndicatorProcessor) p).setMappers(sdRiverRMapper, sdRiverdaySMapper);
}
result.add(p);
}
}
return result;
}
// ======================== 电站映射 ========================
// ======================== 电站映射 STTP 类型区分 ========================
private Map<String, StationInfo> resolveStations(List<String> rstcds) {
Map<String, StationInfo> result = new LinkedHashMap<>();
/**
* 查询 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;
List<StationMapping> mappings = stationMappingMapper.selectList(
new LambdaQueryWrapper<StationMapping>().in(StationMapping::getRstcd, rstcds));
// RSTCD 分组
Map<String, List<StationMapping>> byRstcd = new LinkedHashMap<>();
for (StationMapping m : mappings) {
String rstcd = m.getRstcd();
if (rstcd == null || result.containsKey(rstcd)) continue;
result.put(rstcd, new StationInfo(m.getStcd(),
m.getEnnm() != null ? m.getEnnm() : rstcd));
if (m.getRstcd() == null) continue;
byRstcd.computeIfAbsent(m.getRstcd(), k -> new ArrayList<>()).add(m);
}
for (String rstcd : rstcds) {
result.putIfAbsent(rstcd, new StationInfo(null, rstcd));
List<StationMapping> rows = byRstcd.get(rstcd);
if (rows == null || rows.isEmpty()) {
// 无映射数据电站名回退为编码
result.stationNames.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);
// STTP STCD 映射每种 STTP 取第一个
Map<String, String> sttpMap = new LinkedHashMap<>();
for (StationMapping m : rows) {
String sttp = m.getSttp();
if (sttp != null && !sttpMap.containsKey(sttp)) {
sttpMap.put(sttp, m.getStcd());
}
}
result.sttpStcdMap.put(rstcd, sttpMap);
}
log.info("电站映射解析完成RSTCD数量={}, STTP类型={}", result.stationNames.size(), sttpTypes);
return result;
}
@ -296,10 +373,24 @@ public class QgcExportServiceImpl implements IQgcExportService {
// ======================== 内部数据类 ========================
@lombok.Data
private static class StationInfo {
private final String stcd;
private final String stationName;
/**
* 电站测站映射解析结果
*/
private static class StationResolution {
/** RSTCD → 电站中文名 */
final Map<String, String> stationNames = new LinkedHashMap<>();
/** RSTCD → (STTP → STCD) */
final Map<String, Map<String, String>> sttpStcdMap = new LinkedHashMap<>();
String getStationName(String rstcd) {
return stationNames.getOrDefault(rstcd, rstcd);
}
String getStcd(String rstcd, String sttp) {
Map<String, String> inner = sttpStcdMap.get(rstcd);
if (inner == null || sttp == null) return null;
return inner.get(sttp);
}
}
@lombok.Data