From d35eb47d736db5e8312327905cc8be2f2e7aa079 Mon Sep 17 00:00:00 2001 From: tangwei Date: Tue, 4 Aug 2026 17:19:25 +0800 Subject: [PATCH 01/28] =?UTF-8?q?fix:=20=E4=BC=98=E5=8C=96=E9=A1=B5?= =?UTF-8?q?=E9=9D=A2bug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../impl/TreeStructureServiceImpl.java | 84 +++++----- .../vd/service/impl/VdMonitorServiceImpl.java | 5 +- .../service/impl/SdWtMonitorServiceImpl.java | 2 +- .../service/impl/SysMenuServiceImpl.java | 65 ++++++-- .../mapper/qgc_base/SdRvcdDicMapper.xml | 154 ++++++++++-------- 5 files changed, 182 insertions(+), 128 deletions(-) diff --git a/backend/src/main/java/com/yfd/platform/qgc_base/service/impl/TreeStructureServiceImpl.java b/backend/src/main/java/com/yfd/platform/qgc_base/service/impl/TreeStructureServiceImpl.java index 18a0ec2e..e14679f3 100644 --- a/backend/src/main/java/com/yfd/platform/qgc_base/service/impl/TreeStructureServiceImpl.java +++ b/backend/src/main/java/com/yfd/platform/qgc_base/service/impl/TreeStructureServiceImpl.java @@ -1,5 +1,6 @@ package com.yfd.platform.qgc_base.service.impl; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.yfd.platform.qgc_base.domain.SdEngInfoBH; import com.yfd.platform.qgc_base.domain.SdHydrobase; import com.yfd.platform.qgc_base.domain.SdHbrvDic; @@ -363,25 +364,40 @@ public class TreeStructureServiceImpl implements ITreeStructureService { .collect(Collectors.toList()); } - // 2. 预加载所有流域下的电站(按流域分组) - Map> rvcdEngMap = new HashMap<>(); - for (SdRvcdDic rvcdDic : rvcdList) { - List engList = engInfoBHMapper.selectByRvcd(null,rvcdDic.getRvcd()); + if (rvcdList.isEmpty()) { + return result; + } - // 如果提供了电站名称参数,进行模糊匹配筛选 - if (StringUtils.hasText(engName) && engList != null) { - engList = engList.stream() - .filter(eng -> eng.getEnnm() != null && eng.getEnnm().contains(engName)) - .collect(Collectors.toList()); - } + // 2. 收集所有流域编码,一次批量查询替代 N 次循环查询 + List rvcdCodeList = rvcdList.stream() + .map(SdRvcdDic::getRvcd) + .collect(Collectors.toList()); + List allEngList = engInfoBHMapper.selectList(new LambdaQueryWrapper().in(SdEngInfoBH::getReachcd, rvcdCodeList)); +// List allEngList = engInfoBHMapper.selectByRvcdList(rvcdCodeList); - if (engList != null && !engList.isEmpty()) { - rvcdEngMap.put(rvcdDic.getRvcd(), engList); + // 3. 按流域分组,同时在内存中过滤电站名称 + Map> rvcdEngMap = new LinkedHashMap<>(); + if (allEngList != null) { + for (SdEngInfoBH eng : allEngList) { + String engRvcd = eng.getReachcd(); + if (!StringUtils.hasText(engRvcd) || !rvcdCodeList.contains(engRvcd)) { + continue; + } + if (StringUtils.hasText(engName) && eng.getEnnm() != null + && !eng.getEnnm().contains(engName)) { + continue; + } + rvcdEngMap.computeIfAbsent(engRvcd, k -> new ArrayList<>()).add(eng); } } - // 3. 构建二级树形结构(流域 -> 电站) + // 4. 构建二级树形结构(流域 -> 电站) for (SdRvcdDic rvcdDic : rvcdList) { + List engList = rvcdEngMap.get(rvcdDic.getRvcd()); + if (engList == null || engList.isEmpty()) { + continue; + } + Map rvcdNode = new LinkedHashMap<>(); rvcdNode.put("type", "RVCD"); rvcdNode.put("code", rvcdDic.getRvcd()); @@ -389,37 +405,25 @@ public class TreeStructureServiceImpl implements ITreeStructureService { rvcdNode.put("path", rvcdDic.getPath()); rvcdNode.put("orgLevel", 0); rvcdNode.put("grd", rvcdDic.getGrd()); -// rvcdNode.put("lgtd", rvcdDic.getLgtd()); -// rvcdNode.put("lttd", rvcdDic.getLttd()); rvcdNode.put("parentId", "0"); - // 获取该流域下的所有电站 - List engList = rvcdEngMap.get(rvcdDic.getRvcd()); - List> engChildren = new ArrayList<>(); - - if (engList != null && !engList.isEmpty()) { - for (SdEngInfoBH eng : engList) { - Map engNode = new LinkedHashMap<>(); - engNode.put("type", "STATION"); - engNode.put("code", eng.getStcd()); - engNode.put("name", eng.getEnnm()); -// engNode.put("lgtd", eng.getLgtd()); -// engNode.put("lttd", eng.getLttd()); - engNode.put("elev", eng.getElev()); - engNode.put("usfl", eng.getUsfl()); - engNode.put("bldstt", eng.getBldstt()); - engNode.put("engtp", eng.getEngtp()); - engNode.put("parentId", rvcdDic.getRvcd()); - engNode.put("orgLevel", 1); - engChildren.add(engNode); - } + List> engChildren = new ArrayList<>(engList.size()); + for (SdEngInfoBH eng : engList) { + Map engNode = new LinkedHashMap<>(); + engNode.put("type", "STATION"); + engNode.put("code", eng.getStcd()); + engNode.put("name", eng.getEnnm()); + engNode.put("elev", eng.getElev()); + engNode.put("usfl", eng.getUsfl()); + engNode.put("bldstt", eng.getBldstt()); + engNode.put("engtp", eng.getEngtp()); + engNode.put("parentId", rvcdDic.getRvcd()); + engNode.put("orgLevel", 1); + engChildren.add(engNode); } - // 只有当该流域下有电站时才添加该流域节点 - if (!engChildren.isEmpty()) { - rvcdNode.put("children", engChildren); - result.add(rvcdNode); - } + rvcdNode.put("children", engChildren); + result.add(rvcdNode); } return result; diff --git a/backend/src/main/java/com/yfd/platform/qgc_env/vd/service/impl/VdMonitorServiceImpl.java b/backend/src/main/java/com/yfd/platform/qgc_env/vd/service/impl/VdMonitorServiceImpl.java index 173950ba..a49616a6 100644 --- a/backend/src/main/java/com/yfd/platform/qgc_env/vd/service/impl/VdMonitorServiceImpl.java +++ b/backend/src/main/java/com/yfd/platform/qgc_env/vd/service/impl/VdMonitorServiceImpl.java @@ -531,14 +531,15 @@ public class VdMonitorServiceImpl implements VdMonitorService { if (StrUtil.isNotBlank(filterSql)) { sql.append(" AND ").append(filterSql).append(" "); } - sql.append(buildVdPointOrderBySql(dataSourceRequest == null ? null : dataSourceRequest.getSort())); + DataSourceResult result = new DataSourceResult<>(); - DataScopeApplyResult scopeApplyResult = dataScopeFilterService.appendCurrentUserStationScope(sql, paramMap, "t.RSTCD"); + DataScopeApplyResult scopeApplyResult = dataScopeFilterService.appendCurrentUserStationScope(sql, paramMap, "t.rstcds"); if (scopeApplyResult.isEmptyResult()) { result.setData(new ArrayList<>()); result.setTotal(0L); return result; } + sql.append(buildVdPointOrderBySql(dataSourceRequest == null ? null : dataSourceRequest.getSort())); List list = microservicDynamicSQLMapper.pageAllListWithResultType( page, sql.toString(), paramMap, VdPointVo.class); result.setData(list); diff --git a/backend/src/main/java/com/yfd/platform/qgc_env/wt/service/impl/SdWtMonitorServiceImpl.java b/backend/src/main/java/com/yfd/platform/qgc_env/wt/service/impl/SdWtMonitorServiceImpl.java index f50e5292..2f825c50 100644 --- a/backend/src/main/java/com/yfd/platform/qgc_env/wt/service/impl/SdWtMonitorServiceImpl.java +++ b/backend/src/main/java/com/yfd/platform/qgc_env/wt/service/impl/SdWtMonitorServiceImpl.java @@ -3033,7 +3033,7 @@ public class SdWtMonitorServiceImpl implements SdWtMonitorService { // paramMap.put("dtinType", dtinType); // } - DataScopeApplyResult scopeApplyResult = dataScopeFilterService.appendCurrentUserStationScope(sql, paramMap, "t.RSTCD"); + DataScopeApplyResult scopeApplyResult = dataScopeFilterService.appendCurrentUserStationScope(sql, paramMap, "wt.RSTCD"); if (scopeApplyResult.isEmptyResult()) { dataSourceResult.setData(new ArrayList<>()); dataSourceResult.setTotal(0L); diff --git a/backend/src/main/java/com/yfd/platform/system/service/impl/SysMenuServiceImpl.java b/backend/src/main/java/com/yfd/platform/system/service/impl/SysMenuServiceImpl.java index 32c2229b..e7e6c97b 100644 --- a/backend/src/main/java/com/yfd/platform/system/service/impl/SysMenuServiceImpl.java +++ b/backend/src/main/java/com/yfd/platform/system/service/impl/SysMenuServiceImpl.java @@ -76,30 +76,65 @@ public class SysMenuServiceImpl extends ServiceImpl impl String name, String isdisplay, String tenantId) { - List> mapList=null; - List> listMap =new ArrayList<>(); - if(StrUtil.isEmpty(name)){//不带名称查询,返回树结构 + if (StrUtil.isEmpty(name)) { + // 一次查询该系统下所有菜单,内存中构建树(替代递归逐层查 DB) QueryWrapper queryWrapper = new QueryWrapper<>(); - queryWrapper.eq("parentid", "0").eq("systemcode", systemcode) + queryWrapper.eq("systemcode", systemcode) .eq(StrUtil.isNotBlank(tenantId), "tenant_id", tenantId) .orderByAsc("orderno"); - mapList = this.listMaps(queryWrapper); - listMap.addAll(ObjectConverterUtil.convertMapFieldsToEntityFormat(SysMenu.class, mapList)); - for (int i = 0; i < listMap.size(); i++) { - //查询下一子集 - List> childList = child(listMap.get(i).get( - "id").toString(), systemcode, name, null, null, tenantId); - listMap.get(i).put("children", childList); //添加新列 子集 + List> allMenus = this.listMaps(queryWrapper); + List> allConverted = ObjectConverterUtil.convertMapFieldsToEntityFormat(SysMenu.class, allMenus); + + // 构建 id -> node 索引,并预初始化 children 空列表 + Map> nodeMap = new LinkedHashMap<>(); + for (Map menu : allConverted) { + menu.put("children", new ArrayList<>()); + nodeMap.put(String.valueOf(menu.get("id")), menu); } - }else{ //根据菜单名称查询,直接返回类别 + + // 单次遍历挂载子节点到父节点 + List> roots = new ArrayList<>(); + for (Map menu : allConverted) { + String parentId = String.valueOf(menu.get("parentid")); + Map parent = nodeMap.get(parentId); + if (parent != null) { +// @SuppressWarnings("unchecked") + List> children = (List>) parent.get("children"); + children.add(menu); + } else { + roots.add(menu); + } + } + + // 递归按 orderno 排序 + sortTreeByOrderno(roots); + return roots; + } else { + // 根据菜单名称查询,直接返回列表 QueryWrapper queryWrapper = new QueryWrapper<>(); queryWrapper.like("name", name).eq("systemcode", systemcode) .eq(StrUtil.isNotBlank(tenantId), "tenant_id", tenantId) .orderByAsc("name"); - mapList = this.listMaps(queryWrapper); - listMap.addAll(ObjectConverterUtil.convertMapFieldsToEntityFormat(SysMenu.class, mapList)); + List> mapList = this.listMaps(queryWrapper); + return ObjectConverterUtil.convertMapFieldsToEntityFormat(SysMenu.class, mapList); + } + } + + /** + * 递归按 orderno 排序树节点(保持与原 child() 相同的排序语义) + */ + private void sortTreeByOrderno(List> nodes) { + nodes.sort(Comparator.comparingInt(m -> { + Object orderno = m.get("orderno"); + return orderno instanceof Number ? ((Number) orderno).intValue() : 0; + })); + for (Map node : nodes) { + @SuppressWarnings("unchecked") + List> children = (List>) node.get("children"); + if (children != null && !children.isEmpty()) { + sortTreeByOrderno(children); + } } - return listMap; } /*********************************** diff --git a/backend/src/main/resources/mapper/qgc_base/SdRvcdDicMapper.xml b/backend/src/main/resources/mapper/qgc_base/SdRvcdDicMapper.xml index 3d40173f..5084ec4a 100644 --- a/backend/src/main/resources/mapper/qgc_base/SdRvcdDicMapper.xml +++ b/backend/src/main/resources/mapper/qgc_base/SdRvcdDicMapper.xml @@ -53,83 +53,97 @@ ORDER BY ORDER_INDEX ASC + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + SELECT + STCD, + TM, + ${fieldName} AS VALUE + FROM SD_RIVER_R + WHERE IS_DELETED = 0 + AND STCD IN + + #{stcd} + + AND TM >= TO_DATE(#{startTime}, 'YYYY-MM-DD HH24:MI:SS') + AND TM < TO_DATE(#{endTime}, 'YYYY-MM-DD HH24:MI:SS') + AND ${fieldName} IS NOT NULL + + + diff --git a/backend/src/main/resources/mapper/qgc_export/SdRiverdaySMapper.xml b/backend/src/main/resources/mapper/qgc_export/SdRiverdaySMapper.xml new file mode 100644 index 00000000..13899c3d --- /dev/null +++ b/backend/src/main/resources/mapper/qgc_export/SdRiverdaySMapper.xml @@ -0,0 +1,26 @@ + + + + + + + + From d6d8c87b194cc93eb738856737e4a8ebb1b62504 Mon Sep 17 00:00:00 2001 From: tangwei Date: Wed, 5 Aug 2026 13:50:39 +0800 Subject: [PATCH 05/28] =?UTF-8?q?fix:=20=E4=BC=98=E5=8C=96excel=E7=BB=BC?= =?UTF-8?q?=E5=90=88=E5=AF=BC=E5=87=BA=E6=B5=81=E9=87=8F=E3=80=81=E6=B0=B4?= =?UTF-8?q?=E4=BD=8D=E3=80=81=E6=B5=81=E9=80=9F=E6=96=B9=E6=B3=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../qgc_export/domain/StationMapping.java | 4 + .../processor/ExportIndicatorProcessor.java | 22 +- .../qgc_export/processor/QProcessor.java | 5 + .../qgc_export/processor/VProcessor.java | 5 + .../qgc_export/processor/ZProcessor.java | 5 + .../service/impl/QgcExportServiceImpl.java | 193 +++++++++++++----- 6 files changed, 178 insertions(+), 56 deletions(-) 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 6b6990c8..61af0b13 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 @@ -23,4 +23,8 @@ public class StationMapping implements Serializable { @TableField("ENNM") private String ennm; + + /** 测站类型(如 ZQ=河道水情, XX=生态流量等),用于区分同一电站下不同类型的测站 */ + @TableField("STTP") + private String sttp; } diff --git a/backend/src/main/java/com/yfd/platform/qgc_export/processor/ExportIndicatorProcessor.java b/backend/src/main/java/com/yfd/platform/qgc_export/processor/ExportIndicatorProcessor.java index 83ac09a3..f86de229 100644 --- a/backend/src/main/java/com/yfd/platform/qgc_export/processor/ExportIndicatorProcessor.java +++ b/backend/src/main/java/com/yfd/platform/qgc_export/processor/ExportIndicatorProcessor.java @@ -4,14 +4,14 @@ import java.util.Date; import java.util.List; /** - * 指标处理器接口 — 每种指标(v/q/z/wq/dwt 等)实现此接口 + * 指标处理器接口 — 每种指标(v/q/z/wq/dwt/qec 等)实现此接口 *

- * 新增指标时:实现本接口 + 在 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),用于区分同一电站下不同用途的测站。 + *

    + *
  • V/Q/Z 河道水情 → "ZQ"
  • + *
  • QEC 生态流量 → 待定(如 "SQ")
  • + *
+ * 返回 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 queryRawData(List stcds, Date startTime, Date endTime, boolean isDaily); diff --git a/backend/src/main/java/com/yfd/platform/qgc_export/processor/QProcessor.java b/backend/src/main/java/com/yfd/platform/qgc_export/processor/QProcessor.java index 81680f10..031d7361 100644 --- a/backend/src/main/java/com/yfd/platform/qgc_export/processor/QProcessor.java +++ b/backend/src/main/java/com/yfd/platform/qgc_export/processor/QProcessor.java @@ -17,4 +17,9 @@ public class QProcessor extends RiverIndicatorProcessor { public String getSheetName() { return "流量(m³/s)"; } + + @Override + public String getSttpType() { + return "ZQ"; + } } diff --git a/backend/src/main/java/com/yfd/platform/qgc_export/processor/VProcessor.java b/backend/src/main/java/com/yfd/platform/qgc_export/processor/VProcessor.java index 3897e376..74438880 100644 --- a/backend/src/main/java/com/yfd/platform/qgc_export/processor/VProcessor.java +++ b/backend/src/main/java/com/yfd/platform/qgc_export/processor/VProcessor.java @@ -17,4 +17,9 @@ public class VProcessor extends RiverIndicatorProcessor { public String getSheetName() { return "流速(m/s)"; } + + @Override + public String getSttpType() { + return "ZQ"; + } } diff --git a/backend/src/main/java/com/yfd/platform/qgc_export/processor/ZProcessor.java b/backend/src/main/java/com/yfd/platform/qgc_export/processor/ZProcessor.java index 6aeb9903..32e5e8b3 100644 --- a/backend/src/main/java/com/yfd/platform/qgc_export/processor/ZProcessor.java +++ b/backend/src/main/java/com/yfd/platform/qgc_export/processor/ZProcessor.java @@ -17,4 +17,9 @@ public class ZProcessor extends RiverIndicatorProcessor { public String getSheetName() { return "水位(m)"; } + + @Override + public String getSttpType() { + return "ZQ"; + } } 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 24d2949d..ea60381c 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 @@ -31,6 +31,16 @@ import java.util.stream.Collectors; *

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

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

+ * 同一电站(RSTCD)在 V_MS_STBPRP_T 视图中可能有多个测站(STCD), + * 不同类型的指标需要不同类型的测站数据。通过 {@link ExportIndicatorProcessor#getSttpType()} + * 区分: + *
    + *
  • V/Q/Z 河道水情 → STTP="ZQ"
  • + *
  • QEC 生态流量 → STTP=待定
  • + *
+ * *
  *     日期        | 电站A    | 电站B
  *   日均最低      | 0.523    | 0.612
@@ -71,39 +81,32 @@ public class QgcExportServiceImpl implements IQgcExportService {
     public void exportData(List dataFields, List months,
                            List stationCodes, boolean isDailyDimension,
                            String tmDimension, HttpServletResponse response) {
-        // 1. 电站 → 测站映射 + 电站名称
-        Map stationInfoMap = resolveStations(stationCodes);
-        if (stationInfoMap.isEmpty()) {
-            log.warn("未找到任何电站映射数据,导出取消");
-            return;
-        }
-
-        // 2. 指标处理器匹配
+        // 1. 指标处理器匹配
         List processors = resolveProcessors(dataFields);
         if (processors.isEmpty()) {
             log.warn("未找到匹配的指标处理器,dataFields={}", dataFields);
             return;
         }
 
-        // 3. 收集电站名称 + 测站列表(保持顺序)
-        List stationOrder = new ArrayList<>(stationInfoMap.keySet());
-        List stationNames = stationOrder.stream()
-                .map(k -> stationInfoMap.get(k).getStationName())
-                .toList();
-        List stcds = stationInfoMap.values().stream()
-                .map(StationInfo::getStcd)
+        // 2. 收集所有需要的 STTP 类型
+        Set sttpTypes = processors.stream()
+                .map(ExportIndicatorProcessor::getSttpType)
                 .filter(Objects::nonNull)
-                .distinct()
-                .collect(Collectors.toList());
+                .collect(Collectors.toCollection(LinkedHashSet::new));
 
-        // 4. rstcd → stcd 映射(用于聚合时找数据)
-        Map rstcdToStcd = new LinkedHashMap<>();
-        for (Map.Entry 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 stationOrder = new ArrayList<>(stationRes.stationNames.keySet());
+        List stationNames = stationOrder.stream()
+                .map(stationRes::getStationName)
+                .toList();
+
         // 5. 每月一个 ExcelData
         List excelList = new ArrayList<>();
         for (String month : months) {
@@ -112,9 +115,9 @@ public class QgcExportServiceImpl implements IQgcExportService {
 
             List sheets = new ArrayList<>();
             for (ExportIndicatorProcessor processor : processors) {
-                // 查询 + 聚合这一个月的数据
+                // 查询 + 聚合这一个月的数据(使用该处理器对应的 STTP 测站)
                 List> rows = buildIndicatorRows(
-                        processor, mr, stcds, rstcdToStcd, stationOrder);
+                        processor, mr, stationOrder, stationRes);
                 if (rows != null) {
                     List 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
      * 

- * 返回的每一行 = [指标名, station1值, station2值, ...] + * 使用 processor.getSttpType() 映射到正确的测站编码。 */ private List> buildIndicatorRows( ExportIndicatorProcessor processor, MonthRange mr, - List stcds, Map rstcdToStcd, - List stationOrder) { + List stationOrder, StationResolution stationRes) { - if (stcds.isEmpty()) return null; + String sttp = processor.getSttpType(); + + // 构建该处理器对应的 RSTCD→STCD 映射 + Map 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 uniqueStcds = rstcdToStcd.values().stream().distinct().toList(); // 查询日表数据(用于日均最低/最高) - List dailyRows = processor.queryRawData(stcds, mr.getStart(), mr.getEnd(), true); + List dailyRows = processor.queryRawData(uniqueStcds, mr.getStart(), mr.getEnd(), true); // 查询小时表数据(用于月内最低/最高) - List hourlyRows = processor.queryRawData(stcds, mr.getStart(), mr.getEnd(), false); + List hourlyRows = processor.queryRawData(uniqueStcds, mr.getStart(), mr.getEnd(), false); // 按电站聚合 Map aggMap = aggregate(dailyRows, hourlyRows, rstcdToStcd); @@ -184,42 +215,88 @@ public class QgcExportServiceImpl implements IQgcExportService { // ======================== 指标处理器匹配 ======================== + /** + * 根据前端 dataField 匹配处理器。 + *

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

    + *
  • {@link RiverIndicatorProcessor} 子类需要调用 setMappers() 注入 Mapper
  • + *
  • 独立实现的 {@link ExportIndicatorProcessor}(如 QEC)自行管理 Mapper
  • + *
+ */ private List resolveProcessors(List dataFields) { - Map riverMap = new LinkedHashMap<>(); - riverMap.put("v", vProcessor); - riverMap.put("q", qProcessor); - riverMap.put("z", zProcessor); + 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(); - 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 resolveStations(List rstcds) { - Map result = new LinkedHashMap<>(); + /** + * 查询 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; List mappings = stationMappingMapper.selectList( new LambdaQueryWrapper().in(StationMapping::getRstcd, rstcds)); + // 按 RSTCD 分组 + Map> 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 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 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 stationNames = new LinkedHashMap<>(); + /** RSTCD → (STTP → STCD) */ + final Map> sttpStcdMap = new LinkedHashMap<>(); + + String getStationName(String rstcd) { + return stationNames.getOrDefault(rstcd, rstcd); + } + + String getStcd(String rstcd, String sttp) { + Map inner = sttpStcdMap.get(rstcd); + if (inner == null || sttp == null) return null; + return inner.get(sttp); + } } @lombok.Data From 3d952315a59a66c02baf2f25b3fe3123c073445e Mon Sep 17 00:00:00 2001 From: tangwei Date: Wed, 5 Aug 2026 14:25:59 +0800 Subject: [PATCH 06/28] =?UTF-8?q?fix:=20=E4=BC=98=E5=8C=96excel=E7=BB=BC?= =?UTF-8?q?=E5=90=88=E5=AF=BC=E5=87=BA=E6=B5=81=E9=87=8F=E3=80=81=E6=B0=B4?= =?UTF-8?q?=E4=BD=8D=E3=80=81=E6=B5=81=E9=80=9F=E6=96=B9=E6=B3=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../qgc_export/domain/StationMapping.java | 4 + .../service/impl/QgcExportServiceImpl.java | 246 ++++++++++-------- 2 files changed, 147 insertions(+), 103 deletions(-) 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()} - * 区分: - *
    - *
  • V/Q/Z 河道水情 → STTP="ZQ"
  • - *
  • QEC 生态流量 → STTP=待定
  • - *
- * - *
- *     日期        | 电站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; } } From 616c1fbcbaa41c97ede9b9904f77af7e5739f368 Mon Sep 17 00:00:00 2001 From: tangwei Date: Wed, 5 Aug 2026 15:21:28 +0800 Subject: [PATCH 07/28] =?UTF-8?q?fix:=20=E4=BC=98=E5=8C=96excel=E7=BB=BC?= =?UTF-8?q?=E5=90=88=E5=AF=BC=E5=87=BA=E6=B0=B4=E8=B4=A8=E6=96=B9=E6=B3=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/QgcExportController.java | 6 +- .../yfd/platform/qgc_export/domain/SdWqR.java | 51 ++++++ .../platform/qgc_export/domain/SdWqdayS.java | 51 ++++++ .../qgc_export/mapper/SdWqRMapper.java | 28 ++++ .../qgc_export/mapper/SdWqdaySMapper.java | 28 ++++ .../qgc_export/processor/CondProcessor.java | 30 ++++ .../qgc_export/processor/DoxProcessor.java | 30 ++++ .../processor/ExportIndicatorProcessor.java | 17 +- .../processor/IndicatorDataRow.java | 4 + .../qgc_export/processor/PhProcessor.java | 30 ++++ .../qgc_export/processor/TuProcessor.java | 30 ++++ .../processor/WqIndicatorProcessor.java | 77 +++++++++ .../qgc_export/processor/WtmpProcessor.java | 31 ++++ .../service/impl/QgcExportServiceImpl.java | 155 +++++++++++------- .../controller/SmsVerifyCodeController.java | 55 +++++-- .../mapper/qgc_export/SdWqRMapper.xml | 27 +++ .../mapper/qgc_export/SdWqdaySMapper.xml | 26 +++ 17 files changed, 602 insertions(+), 74 deletions(-) create mode 100644 backend/src/main/java/com/yfd/platform/qgc_export/domain/SdWqR.java create mode 100644 backend/src/main/java/com/yfd/platform/qgc_export/domain/SdWqdayS.java create mode 100644 backend/src/main/java/com/yfd/platform/qgc_export/mapper/SdWqRMapper.java create mode 100644 backend/src/main/java/com/yfd/platform/qgc_export/mapper/SdWqdaySMapper.java create mode 100644 backend/src/main/java/com/yfd/platform/qgc_export/processor/CondProcessor.java create mode 100644 backend/src/main/java/com/yfd/platform/qgc_export/processor/DoxProcessor.java create mode 100644 backend/src/main/java/com/yfd/platform/qgc_export/processor/PhProcessor.java create mode 100644 backend/src/main/java/com/yfd/platform/qgc_export/processor/TuProcessor.java create mode 100644 backend/src/main/java/com/yfd/platform/qgc_export/processor/WqIndicatorProcessor.java create mode 100644 backend/src/main/java/com/yfd/platform/qgc_export/processor/WtmpProcessor.java create mode 100644 backend/src/main/resources/mapper/qgc_export/SdWqRMapper.xml create mode 100644 backend/src/main/resources/mapper/qgc_export/SdWqdaySMapper.xml 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 f879110f..cf225a43 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 @@ -54,9 +54,9 @@ public class QgcExportController { List monthList = parseCsv(months); List stationList = parseCsv(stcd); - if (dataFields.isEmpty()) { - dataFields = Arrays.asList("v", "q", "z"); - } +// if (dataFields.isEmpty()) { +// dataFields = Arrays.asList("v", "q", "z"); +// } if (tmDimension == null || tmDimension.isEmpty()) { tmDimension = "month"; } diff --git a/backend/src/main/java/com/yfd/platform/qgc_export/domain/SdWqR.java b/backend/src/main/java/com/yfd/platform/qgc_export/domain/SdWqR.java new file mode 100644 index 00000000..f97d85f7 --- /dev/null +++ b/backend/src/main/java/com/yfd/platform/qgc_export/domain/SdWqR.java @@ -0,0 +1,51 @@ +package com.yfd.platform.qgc_export.domain; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +import java.io.Serializable; +import java.math.BigDecimal; +import java.util.Date; + +/** + * 水质监测数据表(小时级) + * 对应 Oracle 表: QGC_REFA.SD_WQ_R + */ +@Data +@TableName("SD_WQ_R") +public class SdWqR implements Serializable { + + private static final long serialVersionUID = 1L; + + @TableId(type = IdType.INPUT) + private String id; + + /** 站码 */ + private String stcd; + + /** 采样时间 */ + private Date tm; + + /** 水温 (℃) */ + private BigDecimal wtmp; + + /** PH */ + private BigDecimal ph; + + /** 溶解氧 (mg/L) */ + private BigDecimal dox; + + /** 电导率 (μS/cm) */ + private BigDecimal cond; + + /** 浊度 (NTU) */ + private BigDecimal tu; + + /** 是否达标: 0=不达标 1=达标 */ + private Integer sfdb; + + /** 是否已删除 */ + private Integer isDeleted; +} diff --git a/backend/src/main/java/com/yfd/platform/qgc_export/domain/SdWqdayS.java b/backend/src/main/java/com/yfd/platform/qgc_export/domain/SdWqdayS.java new file mode 100644 index 00000000..0db66633 --- /dev/null +++ b/backend/src/main/java/com/yfd/platform/qgc_export/domain/SdWqdayS.java @@ -0,0 +1,51 @@ +package com.yfd.platform.qgc_export.domain; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +import java.io.Serializable; +import java.math.BigDecimal; +import java.util.Date; + +/** + * 水质监测日数据表 + * 对应 Oracle 表: QGC_REFA.SD_WQDAY_S + */ +@Data +@TableName("SD_WQDAY_S") +public class SdWqdayS implements Serializable { + + private static final long serialVersionUID = 1L; + + @TableId(type = IdType.INPUT) + private String id; + + /** 站码 */ + private String stcd; + + /** 时间 */ + private Date dt; + + /** 水温 (℃) */ + private BigDecimal wtmp; + + /** PH */ + private BigDecimal ph; + + /** 溶解氧 (mg/L) */ + private BigDecimal dox; + + /** 电导率 (μS/cm) */ + private BigDecimal cond; + + /** 浊度 (NTU) */ + private BigDecimal tu; + + /** 是否达标 */ + private Integer sfdb; + + /** 是否已删除 */ + private Integer isDeleted; +} diff --git a/backend/src/main/java/com/yfd/platform/qgc_export/mapper/SdWqRMapper.java b/backend/src/main/java/com/yfd/platform/qgc_export/mapper/SdWqRMapper.java new file mode 100644 index 00000000..12f6f401 --- /dev/null +++ b/backend/src/main/java/com/yfd/platform/qgc_export/mapper/SdWqRMapper.java @@ -0,0 +1,28 @@ +package com.yfd.platform.qgc_export.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.yfd.platform.qgc_export.domain.SdWqR; +import org.apache.ibatis.annotations.Param; + +import java.util.List; +import java.util.Map; + +/** + * SD_WQ_R 水质小时数据 Mapper + */ +public interface SdWqRMapper extends BaseMapper { + + /** + * 批量查询测站在指定时间范围内的原始小时数据(含达标标志) + * + * @param stcdList 测站编码列表 + * @param fieldName 查询字段名(WTMP/PH/DOX/COND/TU) + * @param startTime 起始时间 + * @param endTime 结束时间 + * @return [STCD, TM, VALUE, SFDB] + */ + List> selectRawData(@Param("stcdList") List stcdList, + @Param("fieldName") String fieldName, + @Param("startTime") String startTime, + @Param("endTime") String endTime); +} diff --git a/backend/src/main/java/com/yfd/platform/qgc_export/mapper/SdWqdaySMapper.java b/backend/src/main/java/com/yfd/platform/qgc_export/mapper/SdWqdaySMapper.java new file mode 100644 index 00000000..2a2dd55e --- /dev/null +++ b/backend/src/main/java/com/yfd/platform/qgc_export/mapper/SdWqdaySMapper.java @@ -0,0 +1,28 @@ +package com.yfd.platform.qgc_export.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.yfd.platform.qgc_export.domain.SdWqdayS; +import org.apache.ibatis.annotations.Param; + +import java.util.List; +import java.util.Map; + +/** + * SD_WQDAY_S 水质日数据 Mapper + */ +public interface SdWqdaySMapper extends BaseMapper { + + /** + * 批量查询测站在指定时间范围内的原始日数据 + * + * @param stcdList 测站编码列表 + * @param fieldName 查询字段名(WTMP/PH/DOX/COND/TU) + * @param startTime 起始时间 + * @param endTime 结束时间 + * @return [STCD, DT, VALUE] + */ + List> selectRawData(@Param("stcdList") List stcdList, + @Param("fieldName") String fieldName, + @Param("startTime") String startTime, + @Param("endTime") String endTime); +} diff --git a/backend/src/main/java/com/yfd/platform/qgc_export/processor/CondProcessor.java b/backend/src/main/java/com/yfd/platform/qgc_export/processor/CondProcessor.java new file mode 100644 index 00000000..f79e6921 --- /dev/null +++ b/backend/src/main/java/com/yfd/platform/qgc_export/processor/CondProcessor.java @@ -0,0 +1,30 @@ +package com.yfd.platform.qgc_export.processor; + +import org.springframework.stereotype.Component; + +/** + * 电导率(COND) 指标处理器 + */ +@Component +public class CondProcessor extends WqIndicatorProcessor { + + @Override + public String getFieldName() { + return "COND"; + } + + @Override + public String getSheetName() { + return "电导率(μS/cm)"; + } + + @Override + public int getDecimalPlaces() { + return 2; + } + + @Override + public String getSttpType() { + return "WQ"; + } +} diff --git a/backend/src/main/java/com/yfd/platform/qgc_export/processor/DoxProcessor.java b/backend/src/main/java/com/yfd/platform/qgc_export/processor/DoxProcessor.java new file mode 100644 index 00000000..61adcc37 --- /dev/null +++ b/backend/src/main/java/com/yfd/platform/qgc_export/processor/DoxProcessor.java @@ -0,0 +1,30 @@ +package com.yfd.platform.qgc_export.processor; + +import org.springframework.stereotype.Component; + +/** + * 溶解氧(DOX) 指标处理器 + */ +@Component +public class DoxProcessor extends WqIndicatorProcessor { + + @Override + public String getFieldName() { + return "DOX"; + } + + @Override + public String getSheetName() { + return "溶解氧(mg/L)"; + } + + @Override + public int getDecimalPlaces() { + return 2; + } + + @Override + public String getSttpType() { + return "WQ"; + } +} diff --git a/backend/src/main/java/com/yfd/platform/qgc_export/processor/ExportIndicatorProcessor.java b/backend/src/main/java/com/yfd/platform/qgc_export/processor/ExportIndicatorProcessor.java index f86de229..ac7db929 100644 --- a/backend/src/main/java/com/yfd/platform/qgc_export/processor/ExportIndicatorProcessor.java +++ b/backend/src/main/java/com/yfd/platform/qgc_export/processor/ExportIndicatorProcessor.java @@ -24,7 +24,8 @@ public interface ExportIndicatorProcessor { * 测站类型(STTP),用于区分同一电站下不同用途的测站。 *
    *
  • V/Q/Z 河道水情 → "ZQ"
  • - *
  • QEC 生态流量 → 待定(如 "SQ")
  • + *
  • WQ 水质 → null(不区分类型,取第一个测站)
  • + *
  • QEC 生态流量 → 待定
  • *
* 返回 null 表示不按 STTP 过滤,取该电站的第一个测站。 */ @@ -32,6 +33,20 @@ public interface ExportIndicatorProcessor { return null; } + /** + * 数值保留的小数位数,默认 3 位 + */ + default int getDecimalPlaces() { + return 3; + } + + /** + * 是否有小时达标率指标(水质指标返回 true,在月均下方追加一行"小时达标率") + */ + default boolean hasComplianceRate() { + return false; + } + /** * 查询原始数据 * diff --git a/backend/src/main/java/com/yfd/platform/qgc_export/processor/IndicatorDataRow.java b/backend/src/main/java/com/yfd/platform/qgc_export/processor/IndicatorDataRow.java index 828c5b7c..eca691d9 100644 --- a/backend/src/main/java/com/yfd/platform/qgc_export/processor/IndicatorDataRow.java +++ b/backend/src/main/java/com/yfd/platform/qgc_export/processor/IndicatorDataRow.java @@ -13,6 +13,8 @@ public class IndicatorDataRow { private Date tm; /** 指标值 */ private BigDecimal value; + /** 是否达标(仅水质指标使用,0=不达标 1=达标) */ + private Integer sfdb; public IndicatorDataRow() {} @@ -28,4 +30,6 @@ public class IndicatorDataRow { public void setTm(Date tm) { this.tm = tm; } public BigDecimal getValue() { return value; } public void setValue(BigDecimal value) { this.value = value; } + public Integer getSfdb() { return sfdb; } + public void setSfdb(Integer sfdb) { this.sfdb = sfdb; } } diff --git a/backend/src/main/java/com/yfd/platform/qgc_export/processor/PhProcessor.java b/backend/src/main/java/com/yfd/platform/qgc_export/processor/PhProcessor.java new file mode 100644 index 00000000..1eca08f7 --- /dev/null +++ b/backend/src/main/java/com/yfd/platform/qgc_export/processor/PhProcessor.java @@ -0,0 +1,30 @@ +package com.yfd.platform.qgc_export.processor; + +import org.springframework.stereotype.Component; + +/** + * PH 指标处理器 + */ +@Component +public class PhProcessor extends WqIndicatorProcessor { + + @Override + public String getFieldName() { + return "PH"; + } + + @Override + public String getSheetName() { + return "PH"; + } + + @Override + public int getDecimalPlaces() { + return 2; + } + + @Override + public String getSttpType() { + return "WQ"; + } +} diff --git a/backend/src/main/java/com/yfd/platform/qgc_export/processor/TuProcessor.java b/backend/src/main/java/com/yfd/platform/qgc_export/processor/TuProcessor.java new file mode 100644 index 00000000..f92a3b0d --- /dev/null +++ b/backend/src/main/java/com/yfd/platform/qgc_export/processor/TuProcessor.java @@ -0,0 +1,30 @@ +package com.yfd.platform.qgc_export.processor; + +import org.springframework.stereotype.Component; + +/** + * 浊度(TU) 指标处理器 + */ +@Component +public class TuProcessor extends WqIndicatorProcessor { + + @Override + public String getFieldName() { + return "TU"; + } + + @Override + public String getSheetName() { + return "浊度(NTU)"; + } + + @Override + public int getDecimalPlaces() { + return 2; + } + + @Override + public String getSttpType() { + return "WQ"; + } +} diff --git a/backend/src/main/java/com/yfd/platform/qgc_export/processor/WqIndicatorProcessor.java b/backend/src/main/java/com/yfd/platform/qgc_export/processor/WqIndicatorProcessor.java new file mode 100644 index 00000000..c0bde5df --- /dev/null +++ b/backend/src/main/java/com/yfd/platform/qgc_export/processor/WqIndicatorProcessor.java @@ -0,0 +1,77 @@ +package com.yfd.platform.qgc_export.processor; + +import com.yfd.platform.qgc_export.mapper.SdWqRMapper; +import com.yfd.platform.qgc_export.mapper.SdWqdaySMapper; + +import java.math.BigDecimal; +import java.text.SimpleDateFormat; +import java.util.*; + +/** + * 水质指标处理器基类 — 5个水质指标(WTMP/PH/DOX/COND/TU)共用查询逻辑 + *

+ * 小时表查询额外提取 SFDB(是否达标)字段,用于计算小时达标率。 + * 子类需标注 @Component 并提供 getFieldName/getSheetName/getDecimalPlaces。 + */ +public abstract class WqIndicatorProcessor implements ExportIndicatorProcessor { + + protected SdWqRMapper sdWqRMapper; + protected SdWqdaySMapper sdWqdaySMapper; + + private static final SimpleDateFormat SDF = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + + public void setMappers(SdWqRMapper rMapper, SdWqdaySMapper sMapper) { + this.sdWqRMapper = rMapper; + this.sdWqdaySMapper = sMapper; + } + + @Override + public String getSttpType() { + return null; // 水质不按STTP过滤,取第一个测站 + } + + @Override + public boolean hasComplianceRate() { + return true; + } + + @Override + public List queryRawData(List stcds, Date startTime, Date endTime, boolean isDaily) { + String startStr = SDF.format(startTime); + String endStr = SDF.format(endTime); + + List> rawList; + if (isDaily) { + rawList = sdWqdaySMapper.selectRawData(stcds, getFieldName(), startStr, endStr); + } else { + rawList = sdWqRMapper.selectRawData(stcds, getFieldName(), startStr, endStr); + } + + List rows = new ArrayList<>(); + if (rawList != null) { + for (Map map : rawList) { + String stcd = (String) map.get("STCD"); + Date tm = (Date) map.get(isDaily ? "DT" : "TM"); + BigDecimal value = toBigDecimal(map.get("VALUE")); + if (stcd != null && tm != null && value != null) { + IndicatorDataRow row = new IndicatorDataRow(stcd, tm, value); + // 小时表额外携带 SFDB(达标标志) + if (!isDaily) { + Object sfdbObj = map.get("SFDB"); + if (sfdbObj instanceof Number) { + row.setSfdb(((Number) sfdbObj).intValue()); + } + } + rows.add(row); + } + } + } + return rows; + } + + private BigDecimal toBigDecimal(Object val) { + if (val instanceof BigDecimal) return (BigDecimal) val; + if (val instanceof Number) return BigDecimal.valueOf(((Number) val).doubleValue()); + return null; + } +} diff --git a/backend/src/main/java/com/yfd/platform/qgc_export/processor/WtmpProcessor.java b/backend/src/main/java/com/yfd/platform/qgc_export/processor/WtmpProcessor.java new file mode 100644 index 00000000..7a136d59 --- /dev/null +++ b/backend/src/main/java/com/yfd/platform/qgc_export/processor/WtmpProcessor.java @@ -0,0 +1,31 @@ +package com.yfd.platform.qgc_export.processor; + +import org.springframework.stereotype.Component; + +/** + * 水温(WTMP) 指标处理器 + */ +@Component +public class WtmpProcessor extends WqIndicatorProcessor { + + @Override + public String getFieldName() { + return "WTMP"; + } + + @Override + public String getSheetName() { + return "水温(℃)"; + } + + @Override + public int getDecimalPlaces() { + return 3; + } + + @Override + public String getSttpType() { + return "WQ"; + } + +} 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 5ad37af1..af085dc1 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 @@ -3,13 +3,10 @@ package com.yfd.platform.qgc_export.service.impl; import com.yfd.platform.qgc_export.domain.StationMapping; import com.yfd.platform.qgc_export.mapper.SdRiverRMapper; import com.yfd.platform.qgc_export.mapper.SdRiverdaySMapper; +import com.yfd.platform.qgc_export.mapper.SdWqRMapper; +import com.yfd.platform.qgc_export.mapper.SdWqdaySMapper; import com.yfd.platform.qgc_export.mapper.StationMappingMapper; -import com.yfd.platform.qgc_export.processor.ExportIndicatorProcessor; -import com.yfd.platform.qgc_export.processor.IndicatorDataRow; -import com.yfd.platform.qgc_export.processor.RiverIndicatorProcessor; -import com.yfd.platform.qgc_export.processor.VProcessor; -import com.yfd.platform.qgc_export.processor.QProcessor; -import com.yfd.platform.qgc_export.processor.ZProcessor; +import com.yfd.platform.qgc_export.processor.*; import com.yfd.platform.qgc_export.service.IQgcExportService; import com.yfd.platform.utils.ExportZipUtil; @@ -30,27 +27,23 @@ import java.util.stream.Collectors; * 综合导出服务实现(月度报表) *

* 格式:每月一个 Excel 文件 → ZIP 下载。 - * 每个 Excel 含多个 Sheet(每个指标一个 Sheet)。 + * 每个 Excel 含多个 Sheet,顺序:水温→PH→溶解氧→电导率→浊度→流速→流量→水位。 * *

Sheet 结构

*
- *     日期        | 班多      | 羊曲       ← 表头(Row 0:第一列=日期,后面列=测站名称)
- *   01           | 2757.711  | 2709.839  ← 每日数据(Row 1~31)
+ *     日期        | 班多      | 羊曲       ← 表头(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.549  | 2709.565  ← 汇总指标(7~8行)
  *   日均最高      | 2757.879  | 2709.903
  *   月内最低      | 2756.979  | 2709.463
  *   月内最低时间  | 31日 08时  | 27日 08时
  *   月内最高      | 2758.003  | 2709.983
  *   月内最高时间  | 12日 16时  | 24日 16时
  *   月均         | 2757.751  | 2757.751
+ *   小时达标率    | 95.2%     | 93.8%     ← 仅水质指标有此行
  * 
- * - *

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

- * 同一电站(RSTCD)在 V_MS_STBPRP_T 视图中可能有多个测站(STCD), - * 不同类型的指标需要不同类型的测站数据。通过 {@link ExportIndicatorProcessor#getSttpType()} 区分。 */ @Service public class QgcExportServiceImpl implements IQgcExportService { @@ -60,11 +53,11 @@ public class QgcExportServiceImpl implements IQgcExportService { @Resource private StationMappingMapper stationMappingMapper; + // V/Q/Z 相关 @Resource private SdRiverRMapper sdRiverRMapper; @Resource private SdRiverdaySMapper sdRiverdaySMapper; - @Resource private VProcessor vProcessor; @Resource @@ -72,12 +65,29 @@ public class QgcExportServiceImpl implements IQgcExportService { @Resource private ZProcessor zProcessor; - /** 时间格式:dd日 HH时(如 "31日 08时") */ + // WQ 相关 + @Resource + private SdWqRMapper sdWqRMapper; + @Resource + private SdWqdaySMapper sdWqdaySMapper; + @Resource + private WtmpProcessor wtmpProcessor; + @Resource + private PhProcessor phProcessor; + @Resource + private DoxProcessor doxProcessor; + @Resource + private CondProcessor condProcessor; + @Resource + private TuProcessor tuProcessor; + private static final SimpleDateFormat SDF_TIME = new SimpleDateFormat("dd日 HH时"); - /** 汇总指标行标签(7个) */ - private static final List INDICATOR_LABELS = Collections.unmodifiableList( + /** 基础汇总指标(7个) */ + private static final List BASE_LABELS = Collections.unmodifiableList( Arrays.asList("日均最低", "日均最高", "月内最低", "月内最低时间", "月内最高", "月内最高时间", "月均")); + /** 小时达标率标签 */ + private static final String LABEL_COMPLIANCE = "小时达标率"; @Override public void exportData(List dataFields, List months, @@ -96,7 +106,7 @@ public class QgcExportServiceImpl implements IQgcExportService { .filter(Objects::nonNull) .collect(Collectors.toCollection(LinkedHashSet::new)); - // 3. 电站 → 测站映射(按 STTP 类型分别获取) + // 3. 电站 → 测站映射 StationResolution stationRes = resolveStations(stationCodes, sttpTypes); if (stationRes.displayNames.isEmpty()) { log.warn("未找到任何电站映射数据,导出取消"); @@ -142,18 +152,12 @@ public class QgcExportServiceImpl implements IQgcExportService { // ======================== 构建 Sheet 数据 ======================== - /** - * 查询一个月的数据并组装为一个 Sheet 的全部行: - *
    - *
  1. 日数据行(01~31):每天每电站一个值
  2. - *
  3. 汇总指标行(7行):日均最低、日均最高、月内最低、月内最低时间、月内最高、月内最高时间、月均
  4. - *
- */ private List> buildSheetRows( ExportIndicatorProcessor processor, MonthRange mr, List stationOrder, StationResolution stationRes) { String sttp = processor.getSttpType(); + int decimals = processor.getDecimalPlaces(); // 构建该处理器对应的 RSTCD→STCD 映射 Map rstcdToStcd = new LinkedHashMap<>(); @@ -171,16 +175,22 @@ public class QgcExportServiceImpl implements IQgcExportService { 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); + // 汇总聚合(含达标率) + Map aggMap = aggregate(dailyRows, hourlyRows, rstcdToStcd, processor.hasComplianceRate()); - // 日数据按 (stcd, day) 索引 + // 日数据索引 Map> dailyByStcd = indexDailyByDay(dailyRows); + // 指标标签 + List labels = new ArrayList<>(BASE_LABELS); + if (processor.hasComplianceRate()) { + labels.add(LABEL_COMPLIANCE); + } + List> rows = new ArrayList<>(); // ---- 日数据行 ---- @@ -192,28 +202,29 @@ public class QgcExportServiceImpl implements IQgcExportService { 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)); + row.add(formatOrEmpty(val, decimals)); } rows.add(row); } // ---- 汇总指标行 ---- - for (int i = 0; i < 7; i++) { + for (int i = 0; i < labels.size(); i++) { List row = new ArrayList<>(); - row.add(INDICATOR_LABELS.get(i)); + row.add(labels.get(i)); for (String rstcd : stationOrder) { Aggregation agg = aggMap.get(rstcd); switch (i) { - 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 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)); 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)); 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); @@ -224,8 +235,19 @@ public class QgcExportServiceImpl implements IQgcExportService { // ======================== 指标处理器匹配 ======================== + /** + * 注册顺序决定 Sheet 在 Excel 中的顺序: + * 水质指标(水温→PH→溶解氧→电导率→浊度)→ 河道水情(流速→流量→水位) + */ private List resolveProcessors(List dataFields) { Map processorMap = new LinkedHashMap<>(); + // WQ 在前 + processorMap.put("wtmp", wtmpProcessor); + processorMap.put("ph", phProcessor); + processorMap.put("dox", doxProcessor); + processorMap.put("cond", condProcessor); + processorMap.put("tu", tuProcessor); + // V/Q/Z 在后 processorMap.put("v", vProcessor); processorMap.put("q", qProcessor); processorMap.put("z", zProcessor); @@ -237,6 +259,8 @@ public class QgcExportServiceImpl implements IQgcExportService { if (p != null) { if (p instanceof RiverIndicatorProcessor) { ((RiverIndicatorProcessor) p).setMappers(sdRiverRMapper, sdRiverdaySMapper); + } else if (p instanceof WqIndicatorProcessor) { + ((WqIndicatorProcessor) p).setMappers(sdWqRMapper, sdWqdaySMapper); } result.add(p); } @@ -244,7 +268,7 @@ public class QgcExportServiceImpl implements IQgcExportService { return result; } - // ======================== 电站映射(按 STTP 类型区分 + 测站名称) ======================== + // ======================== 电站映射 ======================== private StationResolution resolveStations(List rstcds, Set sttpTypes) { StationResolution result = new StationResolution(); @@ -267,7 +291,7 @@ public class QgcExportServiceImpl implements IQgcExportService { continue; } - // 测站名称:优先 ZQ 类型的 STNM → 任意 STNM → ENNM → RSTCD + // 测站名称:优先 ZQ 类型 STNM → 任意 STNM → ENNM → RSTCD String displayName = null; for (StationMapping m : rows) { if ("ZQ".equals(m.getSttp()) && m.getStnm() != null) { @@ -288,7 +312,7 @@ public class QgcExportServiceImpl implements IQgcExportService { } result.displayNames.put(rstcd, displayName); - // STTP → STCD 映射:每种 STTP 取第一个 + // STTP → STCD 映射 Map sttpMap = new LinkedHashMap<>(); for (StationMapping m : rows) { String sttp = m.getSttp(); @@ -327,7 +351,6 @@ public class QgcExportServiceImpl implements IQgcExportService { } } - /** 计算月份的天数 */ private int getDaysInMonth(MonthRange mr) { Calendar cal = Calendar.getInstance(); cal.setTime(mr.getEnd()); @@ -339,7 +362,7 @@ public class QgcExportServiceImpl implements IQgcExportService { private Map aggregate( List dailyRows, List hourlyRows, - Map rstcdToStcd) { + Map rstcdToStcd, boolean calcCompliance) { Map result = new LinkedHashMap<>(); @@ -347,7 +370,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) @@ -355,12 +378,11 @@ 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()); @@ -375,14 +397,22 @@ public class QgcExportServiceImpl implements IQgcExportService { agg.setExtremeMaxTime(maxRow.getTm()); } + // 小时达标率(仅水质指标) + if (calcCompliance) { + List sfdbRows = hourlyStationData.stream() + .filter(r -> r.getSfdb() != null) + .collect(Collectors.toList()); + if (!sfdbRows.isEmpty()) { + long compliant = sfdbRows.stream().filter(r -> r.getSfdb() == 1).count(); + agg.setComplianceRate(BigDecimal.valueOf(compliant * 100.0 / sfdbRows.size())); + } + } + result.put(rstcd, agg); } return result; } - /** - * 将日表数据按 (stcd → day → value) 索引,供日数据行快速查找 - */ private Map> indexDailyByDay(List dailyRows) { Map> result = new LinkedHashMap<>(); for (IndicatorDataRow row : dailyRows) { @@ -395,9 +425,14 @@ public class QgcExportServiceImpl implements IQgcExportService { return result; } - private static String formatOrEmpty(BigDecimal value) { + private static String formatOrEmpty(BigDecimal value, int decimals) { if (value == null) return ""; - return value.setScale(3, RoundingMode.HALF_UP).toPlainString(); + return value.setScale(decimals, RoundingMode.HALF_UP).toPlainString(); + } + + private static String formatComplianceRate(BigDecimal value) { + if (value == null) return ""; + return value.setScale(1, RoundingMode.HALF_UP).toPlainString() + "%"; } private static String getFileNamePrefix(String tmDimension) { @@ -413,19 +448,22 @@ public class QgcExportServiceImpl implements IQgcExportService { // ======================== 内部数据类 ======================== private static class StationResolution { - /** RSTCD → 测站名称(列标题显示用) */ final Map displayNames = new LinkedHashMap<>(); - /** RSTCD → (STTP → STCD) */ final Map> sttpStcdMap = new LinkedHashMap<>(); String getDisplayName(String rstcd) { return displayNames.getOrDefault(rstcd, rstcd); } + /** + * 获取指定 STTP 的测站编码。sttp 为 null 时返回第一个可用的 STCD。 + */ String getStcd(String rstcd, String sttp) { Map inner = sttpStcdMap.get(rstcd); - if (inner == null || sttp == null) return null; - return inner.get(sttp); + if (inner == null || inner.isEmpty()) return null; + if (sttp != null) return inner.get(sttp); + // sttp 为 null:返回第一个可用的 STCD + return inner.values().iterator().next(); } } @@ -441,12 +479,11 @@ public class QgcExportServiceImpl implements IQgcExportService { private BigDecimal dailyMin; private BigDecimal dailyMax; private BigDecimal extremeMin; - /** 月内最低发生时间 */ private Date extremeMinTime; private BigDecimal extremeMax; - /** 月内最高发生时间 */ private Date extremeMaxTime; - /** 月均(所有日值的平均值) */ private BigDecimal monthlyAvg; + /** 小时达标率(百分比,仅水质指标使用) */ + private BigDecimal complianceRate; } } diff --git a/backend/src/main/java/com/yfd/platform/system/controller/SmsVerifyCodeController.java b/backend/src/main/java/com/yfd/platform/system/controller/SmsVerifyCodeController.java index 06e5d6e9..dc4dda21 100644 --- a/backend/src/main/java/com/yfd/platform/system/controller/SmsVerifyCodeController.java +++ b/backend/src/main/java/com/yfd/platform/system/controller/SmsVerifyCodeController.java @@ -12,10 +12,7 @@ import com.yfd.platform.qgc_base.domain.SdEngInfoBH; import com.yfd.platform.qgc_base.service.ISdEngInfoBHService; import com.yfd.platform.qgc_base.service.ISdHbrvDicService; import com.yfd.platform.system.domain.*; -import com.yfd.platform.system.mapper.SysMenuMapper; -import com.yfd.platform.system.mapper.SysOrganizationMapper; -import com.yfd.platform.system.mapper.SysRoleMapper; -import com.yfd.platform.system.mapper.SysUserTenantMapper; +import com.yfd.platform.system.mapper.*; import com.yfd.platform.system.service.ISmsVerifyCodeService; import com.yfd.platform.system.service.ISysLogService; import com.yfd.platform.system.service.IUserService; @@ -85,6 +82,10 @@ public class SmsVerifyCodeController { @Resource private ISdHbrvDicService hbrvDicService; + @Resource + private SysUserMapper sysUserMapper; + + @Value("${rsa.private_key}") private String privateKey; private static final String URGE_CONTENT = "根据生态环境部要求,请贵电站尽快完成过鱼数据的报送工作,感谢支持。"; @@ -478,7 +479,7 @@ public class SmsVerifyCodeController { return ResponseResult.error("验证码错误或已过期"); } - SysUser user = userService.getUserByPhone(phone, tenantId); + SysUser user = userService.getUserByPhone(phone, null); if (user == null) { return ResponseResult.error("该手机号未注册"); } @@ -500,19 +501,43 @@ public class SmsVerifyCodeController { Authentication authenticate = new UsernamePasswordAuthenticationToken(user, null, authenticationToken.getAuthorities()); SecurityContextHolder.getContext().setAuthentication(authenticate); - LoginUser loginUser = new LoginUser(); - loginUser.setUser(user); - loginUser.setUsername(user.getUsername()); - //Todo 根据用户查询权限信息 添加到LoginUser中 + // 判断是否为超级管理员(LEVEL=1) + boolean isSuperAdmin = (isSuperAdmin(user.getId())||user.getUsertype()==0); + + // 查询用户可访问的门户列表 + List accessibleTenantIds = sysUserTenantMapper.getTenantIdsByUserId(user.getId()); + + // 非超级管理员必须指定门户,且必须在可访问列表中 + if (!isSuperAdmin) { + if (StrUtil.isBlank(tenantId)) { + throw new RuntimeException("用户无权限访问该平台"); + } + if (accessibleTenantIds == null || !accessibleTenantIds.contains(tenantId)) { + throw new RuntimeException("用户无权限访问该平台"); + } + } + // 加载权限:超级管理员加载全部权限,普通用户按门户过滤 - boolean isSuperAdmin = userService.isSuperAdmin(user.getId()); List permissions; if (isSuperAdmin) { permissions = sysMenuMapper.selectPermsByUserId(user.getId(), null); } else { permissions = sysMenuMapper.selectPermsByUserId(user.getId(), tenantId); } - loginUser.setPermissions(permissions); + + LoginUser loginUser = new LoginUser(user, permissions, tenantId, accessibleTenantIds, isSuperAdmin); + +// LoginUser loginUser = new LoginUser(); +// loginUser.setUser(user); +// loginUser.setUsername(user.getUsername()); +// boolean isSuperAdmin = userService.isSuperAdmin(user.getId()); +// List permissions; +// if (isSuperAdmin) { +// permissions = sysMenuMapper.selectPermsByUserId(user.getId(), null); +// } else { +// permissions = sysMenuMapper.selectPermsByUserId(user.getId(), tenantId); +// } +// loginUser.setPermissions(permissions); HttpServletRequest request = RequestHolder.getHttpServletRequest(); SysLog sysLog = new SysLog(); sysLog.setUsercode(user.getUsername()); @@ -549,4 +574,12 @@ public class SmsVerifyCodeController { return ResponseResult.successData(map); } + + /** + * 判断用户是否为超级管理员(拥有LEVEL=1的角色) + */ + private boolean isSuperAdmin(String userId) { + String maxLevel = sysUserMapper.getMaxLevel(userId); + return "1".equals(maxLevel); + } } \ No newline at end of file diff --git a/backend/src/main/resources/mapper/qgc_export/SdWqRMapper.xml b/backend/src/main/resources/mapper/qgc_export/SdWqRMapper.xml new file mode 100644 index 00000000..3026c9fb --- /dev/null +++ b/backend/src/main/resources/mapper/qgc_export/SdWqRMapper.xml @@ -0,0 +1,27 @@ + + + + + + + + diff --git a/backend/src/main/resources/mapper/qgc_export/SdWqdaySMapper.xml b/backend/src/main/resources/mapper/qgc_export/SdWqdaySMapper.xml new file mode 100644 index 00000000..4541f84e --- /dev/null +++ b/backend/src/main/resources/mapper/qgc_export/SdWqdaySMapper.xml @@ -0,0 +1,26 @@ + + + + + + + + From f96d86b1c1a8ff2b54cabe18e32a5e50edebd125 Mon Sep 17 00:00:00 2001 From: tangwei Date: Wed, 5 Aug 2026 15:25:19 +0800 Subject: [PATCH 08/28] =?UTF-8?q?fix:=20=E4=BC=98=E5=8C=96excel=E7=BB=BC?= =?UTF-8?q?=E5=90=88=E5=AF=BC=E5=87=BA=E6=B0=B4=E8=B4=A8=E6=8C=87=E6=A0=87?= =?UTF-8?q?=E7=A9=BA=E5=80=BC=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/impl/QgcExportServiceImpl.java | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) 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 af085dc1..10542479 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 @@ -238,6 +238,8 @@ public class QgcExportServiceImpl implements IQgcExportService { /** * 注册顺序决定 Sheet 在 Excel 中的顺序: * 水质指标(水温→PH→溶解氧→电导率→浊度)→ 河道水情(流速→流量→水位) + *

+ * 支持别名:前端传 "wq" 等价于传 "wtmp,ph,dox,cond,tu"。 */ private List resolveProcessors(List dataFields) { Map processorMap = new LinkedHashMap<>(); @@ -252,10 +254,20 @@ public class QgcExportServiceImpl implements IQgcExportService { processorMap.put("q", qProcessor); processorMap.put("z", zProcessor); - List result = new ArrayList<>(); + // WQ 别名:展开为全部水质指标 + List expanded = new ArrayList<>(); for (String field : dataFields) { String lower = field.trim().toLowerCase(); - ExportIndicatorProcessor p = processorMap.get(lower); + if ("wq".equals(lower)) { + expanded.addAll(List.of("wtmp", "ph", "dox", "cond", "tu")); + } else { + expanded.add(lower); + } + } + + List result = new ArrayList<>(); + for (String field : expanded) { + ExportIndicatorProcessor p = processorMap.get(field); if (p != null) { if (p instanceof RiverIndicatorProcessor) { ((RiverIndicatorProcessor) p).setMappers(sdRiverRMapper, sdRiverdaySMapper); From 356c4f89ca076dc2e35cf918e1f93ac0bfab83a3 Mon Sep 17 00:00:00 2001 From: tangwei Date: Wed, 5 Aug 2026 16:16:30 +0800 Subject: [PATCH 09/28] =?UTF-8?q?fix:=20=E4=BC=98=E5=8C=96excel=E7=BB=BC?= =?UTF-8?q?=E5=90=88=E5=AF=BC=E5=87=BA=E6=B0=B4=E8=B4=A8=E6=B5=8B=E7=AB=99?= =?UTF-8?q?=E9=94=99=E8=AF=AF=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../processor/ExportIndicatorProcessor.java | 3 +- .../processor/WqIndicatorProcessor.java | 2 +- .../service/impl/QgcExportServiceImpl.java | 79 +++++++++++++------ 3 files changed, 56 insertions(+), 28 deletions(-) diff --git a/backend/src/main/java/com/yfd/platform/qgc_export/processor/ExportIndicatorProcessor.java b/backend/src/main/java/com/yfd/platform/qgc_export/processor/ExportIndicatorProcessor.java index ac7db929..5b0af248 100644 --- a/backend/src/main/java/com/yfd/platform/qgc_export/processor/ExportIndicatorProcessor.java +++ b/backend/src/main/java/com/yfd/platform/qgc_export/processor/ExportIndicatorProcessor.java @@ -24,10 +24,9 @@ public interface ExportIndicatorProcessor { * 测站类型(STTP),用于区分同一电站下不同用途的测站。 *

    *
  • V/Q/Z 河道水情 → "ZQ"
  • - *
  • WQ 水质 → null(不区分类型,取第一个测站)
  • + *
  • WQ 水质 → "WQ"
  • *
  • QEC 生态流量 → 待定
  • *
- * 返回 null 表示不按 STTP 过滤,取该电站的第一个测站。 */ default String getSttpType() { return null; diff --git a/backend/src/main/java/com/yfd/platform/qgc_export/processor/WqIndicatorProcessor.java b/backend/src/main/java/com/yfd/platform/qgc_export/processor/WqIndicatorProcessor.java index c0bde5df..ad80b4c0 100644 --- a/backend/src/main/java/com/yfd/platform/qgc_export/processor/WqIndicatorProcessor.java +++ b/backend/src/main/java/com/yfd/platform/qgc_export/processor/WqIndicatorProcessor.java @@ -27,7 +27,7 @@ public abstract class WqIndicatorProcessor implements ExportIndicatorProcessor { @Override public String getSttpType() { - return null; // 水质不按STTP过滤,取第一个测站 + return "WQ"; } @Override 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 10542479..9cbc4a56 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 @@ -103,7 +103,6 @@ public class QgcExportServiceImpl implements IQgcExportService { // 2. 收集所有需要的 STTP 类型 Set sttpTypes = processors.stream() .map(ExportIndicatorProcessor::getSttpType) - .filter(Objects::nonNull) .collect(Collectors.toCollection(LinkedHashSet::new)); // 3. 电站 → 测站映射 @@ -113,11 +112,8 @@ public class QgcExportServiceImpl implements IQgcExportService { return; } - // 4. 电站顺序 + 列标题(测站名称) + // 4. 电站顺序 List stationOrder = new ArrayList<>(stationRes.displayNames.keySet()); - List headerNames = stationOrder.stream() - .map(stationRes::getDisplayName) - .toList(); // 5. 每月一个 ExcelData List excelList = new ArrayList<>(); @@ -129,9 +125,13 @@ public class QgcExportServiceImpl implements IQgcExportService { for (ExportIndicatorProcessor processor : processors) { List> rows = buildSheetRows(processor, mr, stationOrder, stationRes); if (rows != null) { + // 按当前处理器 STTP 类型获取对应的测站名称 + String sttp = processor.getSttpType(); List headers = new ArrayList<>(); headers.add("日期"); - headers.addAll(headerNames); + for (String rstcd : stationOrder) { + headers.add(stationRes.getDisplayName(rstcd, sttp)); + } sheets.add(new ExportZipUtil.SheetData(processor.getSheetName(), headers, rows)); } } @@ -303,26 +303,39 @@ public class QgcExportServiceImpl implements IQgcExportService { continue; } - // 测站名称:优先 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) { + // 默认测站名称:按请求的 STTP 类型优先级 → 任意 STNM → ENNM → RSTCD + String defaultDisplayName = null; + for (String sttp : sttpTypes) { for (StationMapping m : rows) { - if (m.getStnm() != null) { displayName = m.getStnm(); break; } + if (sttp.equals(m.getSttp()) && m.getStnm() != null) { + defaultDisplayName = m.getStnm(); + break; + } + } + if (defaultDisplayName != null) break; + } + if (defaultDisplayName == null) { + for (StationMapping m : rows) { + if (m.getStnm() != null) { defaultDisplayName = m.getStnm(); break; } } } - if (displayName == null) { - displayName = rows.get(0).getEnnm(); + if (defaultDisplayName == null) { + defaultDisplayName = rows.get(0).getEnnm(); } - if (displayName == null) { - displayName = rstcd; + if (defaultDisplayName == null) { + defaultDisplayName = rstcd; } - result.displayNames.put(rstcd, displayName); + result.displayNames.put(rstcd, defaultDisplayName); + + // 按 STTP 类型分别记录测站名称(水质 Sheet 用 WQ 名称,水情 Sheet 用 ZQ 名称) + Map sttpNames = new LinkedHashMap<>(); + for (StationMapping m : rows) { + String sttp = m.getSttp(); + if (sttp != null && m.getStnm() != null && !sttpNames.containsKey(sttp)) { + sttpNames.put(sttp, m.getStnm()); + } + } + result.sttpDisplayNames.put(rstcd, sttpNames); // STTP → STCD 映射 Map sttpMap = new LinkedHashMap<>(); @@ -460,22 +473,38 @@ public class QgcExportServiceImpl implements IQgcExportService { // ======================== 内部数据类 ======================== private static class StationResolution { + /** 默认测站名称(fallback),RSTCD → displayName */ final Map displayNames = new LinkedHashMap<>(); + /** 按 STTP 分组的测站名称,RSTCD → {STTP → displayName} */ + final Map> sttpDisplayNames = new LinkedHashMap<>(); + /** STTP → STCD 映射,RSTCD → {STTP → STCD} */ final Map> sttpStcdMap = new LinkedHashMap<>(); + /** 获取默认测站名称 */ String getDisplayName(String rstcd) { return displayNames.getOrDefault(rstcd, rstcd); } /** - * 获取指定 STTP 的测站编码。sttp 为 null 时返回第一个可用的 STCD。 + * 获取指定 STTP 类型的测站名称。 + * 优先返回该 STTP 对应的名称,若无则返回默认名称。 + */ + String getDisplayName(String rstcd, String sttp) { + Map inner = sttpDisplayNames.get(rstcd); + if (inner != null) { + String name = inner.get(sttp); + if (name != null) return name; + } + return getDisplayName(rstcd); + } + + /** + * 获取指定 STTP 的测站编码。 */ String getStcd(String rstcd, String sttp) { Map inner = sttpStcdMap.get(rstcd); if (inner == null || inner.isEmpty()) return null; - if (sttp != null) return inner.get(sttp); - // sttp 为 null:返回第一个可用的 STCD - return inner.values().iterator().next(); + return inner.get(sttp); } } From 9d45d85fc6fd12621518d2ff7b697f5df3ec7281 Mon Sep 17 00:00:00 2001 From: tangwei Date: Wed, 5 Aug 2026 16:41:05 +0800 Subject: [PATCH 10/28] =?UTF-8?q?fix:=20=E5=A2=9E=E5=8A=A0excel=E7=BB=BC?= =?UTF-8?q?=E5=90=88=E5=AF=BC=E5=87=BA=E5=87=BA=E5=BA=93=E6=B0=B4=E6=B8=A9?= =?UTF-8?q?=E6=95=B0=E6=8D=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../platform/qgc_export/domain/SdWtrvR.java | 63 +++++++++ .../qgc_export/domain/SdWtrvdayS.java | 54 ++++++++ .../qgc_export/mapper/SdWtrvRMapper.java | 26 ++++ .../qgc_export/mapper/SdWtrvdaySMapper.java | 26 ++++ .../qgc_export/processor/DwtProcessor.java | 22 ++++ .../processor/DwtVariationProcessor.java | 27 ++++ .../processor/ExportIndicatorProcessor.java | 9 ++ .../processor/WtrvIndicatorProcessor.java | 68 ++++++++++ .../service/impl/QgcExportServiceImpl.java | 121 +++++++++++++++++- .../src/main/resources/application-prod.yml | 8 +- .../mapper/qgc_export/SdWtrvRMapper.xml | 23 ++++ .../mapper/qgc_export/SdWtrvdaySMapper.xml | 23 ++++ 12 files changed, 465 insertions(+), 5 deletions(-) create mode 100644 backend/src/main/java/com/yfd/platform/qgc_export/domain/SdWtrvR.java create mode 100644 backend/src/main/java/com/yfd/platform/qgc_export/domain/SdWtrvdayS.java create mode 100644 backend/src/main/java/com/yfd/platform/qgc_export/mapper/SdWtrvRMapper.java create mode 100644 backend/src/main/java/com/yfd/platform/qgc_export/mapper/SdWtrvdaySMapper.java create mode 100644 backend/src/main/java/com/yfd/platform/qgc_export/processor/DwtProcessor.java create mode 100644 backend/src/main/java/com/yfd/platform/qgc_export/processor/DwtVariationProcessor.java create mode 100644 backend/src/main/java/com/yfd/platform/qgc_export/processor/WtrvIndicatorProcessor.java create mode 100644 backend/src/main/resources/mapper/qgc_export/SdWtrvRMapper.xml create mode 100644 backend/src/main/resources/mapper/qgc_export/SdWtrvdaySMapper.xml diff --git a/backend/src/main/java/com/yfd/platform/qgc_export/domain/SdWtrvR.java b/backend/src/main/java/com/yfd/platform/qgc_export/domain/SdWtrvR.java new file mode 100644 index 00000000..4017bbd9 --- /dev/null +++ b/backend/src/main/java/com/yfd/platform/qgc_export/domain/SdWtrvR.java @@ -0,0 +1,63 @@ +package com.yfd.platform.qgc_export.domain; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +import java.io.Serializable; +import java.math.BigDecimal; +import java.util.Date; + +/** + * 河道水温监测数据表(小时级) + * 对应 Oracle 表: QGC_REFA.SD_WTRV_R + */ +@Data +@TableName("SD_WTRV_R") +public class SdWtrvR implements Serializable { + + private static final long serialVersionUID = 1L; + + @TableId(type = IdType.INPUT) + private String id; + + /** 水温站编码 */ + private String stcd; + + /** 数据时间 */ + private Date tm; + + /** 水温 (℃) */ + private BigDecimal wt; + + /** 备注 */ + private String remark; + + /** 附件ID */ + private String fid; + + /** 所属水生生态调查数据表ID */ + private String werId; + + /** 创建人 */ + private String recordUser; + + /** 创建时间 */ + private Date recordTime; + + /** 更新人 */ + private String modifyUser; + + /** 更新时间 */ + private Date modifyTime; + + /** 是否已删除 */ + private Integer isDeleted; + + /** 删除人 */ + private String deleteUser; + + /** 删除时间 */ + private Date deleteTime; +} diff --git a/backend/src/main/java/com/yfd/platform/qgc_export/domain/SdWtrvdayS.java b/backend/src/main/java/com/yfd/platform/qgc_export/domain/SdWtrvdayS.java new file mode 100644 index 00000000..66328c2a --- /dev/null +++ b/backend/src/main/java/com/yfd/platform/qgc_export/domain/SdWtrvdayS.java @@ -0,0 +1,54 @@ +package com.yfd.platform.qgc_export.domain; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +import java.io.Serializable; +import java.math.BigDecimal; +import java.util.Date; + +/** + * 河道水温监测数据日统计表 + * 对应 Oracle 表: QGC_REFA.SD_WTRVDAY_S + */ +@Data +@TableName("SD_WTRVDAY_S") +public class SdWtrvdayS implements Serializable { + + private static final long serialVersionUID = 1L; + + @TableId(type = IdType.INPUT) + private String id; + + /** 水温站编码 */ + private String stcd; + + /** 数据时间 */ + private Date dt; + + /** 水温 (℃) */ + private BigDecimal wt; + + /** 创建人 */ + private String recordUser; + + /** 创建时间 */ + private Date recordTime; + + /** 更新人 */ + private String modifyUser; + + /** 更新时间 */ + private Date modifyTime; + + /** 是否已删除 */ + private Integer isDeleted; + + /** 删除人 */ + private String deleteUser; + + /** 删除时间 */ + private Date deleteTime; +} diff --git a/backend/src/main/java/com/yfd/platform/qgc_export/mapper/SdWtrvRMapper.java b/backend/src/main/java/com/yfd/platform/qgc_export/mapper/SdWtrvRMapper.java new file mode 100644 index 00000000..32ba2d9b --- /dev/null +++ b/backend/src/main/java/com/yfd/platform/qgc_export/mapper/SdWtrvRMapper.java @@ -0,0 +1,26 @@ +package com.yfd.platform.qgc_export.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.yfd.platform.qgc_export.domain.SdWtrvR; +import org.apache.ibatis.annotations.Param; + +import java.util.List; +import java.util.Map; + +/** + * SD_WTRV_R 河道水温小时数据 Mapper + */ +public interface SdWtrvRMapper extends BaseMapper { + + /** + * 批量查询测站在指定时间范围内的原始小时数据 + * + * @param stcdList 测站编码列表 + * @param startTime 起始时间 + * @param endTime 结束时间 + * @return [STCD, TM, VALUE] + */ + List> selectRawData(@Param("stcdList") List stcdList, + @Param("startTime") String startTime, + @Param("endTime") String endTime); +} diff --git a/backend/src/main/java/com/yfd/platform/qgc_export/mapper/SdWtrvdaySMapper.java b/backend/src/main/java/com/yfd/platform/qgc_export/mapper/SdWtrvdaySMapper.java new file mode 100644 index 00000000..434cc04b --- /dev/null +++ b/backend/src/main/java/com/yfd/platform/qgc_export/mapper/SdWtrvdaySMapper.java @@ -0,0 +1,26 @@ +package com.yfd.platform.qgc_export.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.yfd.platform.qgc_export.domain.SdWtrvdayS; +import org.apache.ibatis.annotations.Param; + +import java.util.List; +import java.util.Map; + +/** + * SD_WTRVDAY_S 河道水温日数据 Mapper + */ +public interface SdWtrvdaySMapper extends BaseMapper { + + /** + * 批量查询测站在指定时间范围内的原始日数据 + * + * @param stcdList 测站编码列表 + * @param startTime 起始时间 + * @param endTime 结束时间 + * @return [STCD, DT, VALUE] + */ + List> selectRawData(@Param("stcdList") List stcdList, + @Param("startTime") String startTime, + @Param("endTime") String endTime); +} diff --git a/backend/src/main/java/com/yfd/platform/qgc_export/processor/DwtProcessor.java b/backend/src/main/java/com/yfd/platform/qgc_export/processor/DwtProcessor.java new file mode 100644 index 00000000..87b7f550 --- /dev/null +++ b/backend/src/main/java/com/yfd/platform/qgc_export/processor/DwtProcessor.java @@ -0,0 +1,22 @@ +package com.yfd.platform.qgc_export.processor; + +import org.springframework.stereotype.Component; + +/** + * 出库水温(DWT) 标准格式处理器 + *

+ * Sheet 格式与水情指标一致:日数据(01-31) + 7个汇总指标。 + */ +@Component +public class DwtProcessor extends WtrvIndicatorProcessor { + + @Override + public String getFieldName() { + return "DWT"; + } + + @Override + public String getSheetName() { + return "出库水温"; + } +} diff --git a/backend/src/main/java/com/yfd/platform/qgc_export/processor/DwtVariationProcessor.java b/backend/src/main/java/com/yfd/platform/qgc_export/processor/DwtVariationProcessor.java new file mode 100644 index 00000000..46ac84d5 --- /dev/null +++ b/backend/src/main/java/com/yfd/platform/qgc_export/processor/DwtVariationProcessor.java @@ -0,0 +1,27 @@ +package com.yfd.platform.qgc_export.processor; + +import org.springframework.stereotype.Component; + +/** + * 出入库水温日变幅 处理器 + *

+ * 日变幅格式:日数据行展示每日变幅(当日最大-最小),汇总指标为"日变幅均值"和"日变幅最大值"。 + */ +@Component +public class DwtVariationProcessor extends WtrvIndicatorProcessor { + + @Override + public String getFieldName() { + return "DWT"; + } + + @Override + public String getSheetName() { + return "出入库水温日变幅"; + } + + @Override + public boolean isVariation() { + return true; + } +} diff --git a/backend/src/main/java/com/yfd/platform/qgc_export/processor/ExportIndicatorProcessor.java b/backend/src/main/java/com/yfd/platform/qgc_export/processor/ExportIndicatorProcessor.java index 5b0af248..997e411b 100644 --- a/backend/src/main/java/com/yfd/platform/qgc_export/processor/ExportIndicatorProcessor.java +++ b/backend/src/main/java/com/yfd/platform/qgc_export/processor/ExportIndicatorProcessor.java @@ -25,6 +25,7 @@ public interface ExportIndicatorProcessor { *

    *
  • V/Q/Z 河道水情 → "ZQ"
  • *
  • WQ 水质 → "WQ"
  • + *
  • DWT 出库水温 → "WTRV"
  • *
  • QEC 生态流量 → 待定
  • *
*/ @@ -46,6 +47,14 @@ public interface ExportIndicatorProcessor { return false; } + /** + * 是否为日变幅格式(出库水温日变幅 Sheet)。 + * 日变幅格式:日数据行展示每日变幅(=当日最大-最小),汇总指标为"日变幅均值"和"日变幅最大值"。 + */ + default boolean isVariation() { + return false; + } + /** * 查询原始数据 * diff --git a/backend/src/main/java/com/yfd/platform/qgc_export/processor/WtrvIndicatorProcessor.java b/backend/src/main/java/com/yfd/platform/qgc_export/processor/WtrvIndicatorProcessor.java new file mode 100644 index 00000000..ca4f9a3b --- /dev/null +++ b/backend/src/main/java/com/yfd/platform/qgc_export/processor/WtrvIndicatorProcessor.java @@ -0,0 +1,68 @@ +package com.yfd.platform.qgc_export.processor; + +import com.yfd.platform.qgc_export.mapper.SdWtrvRMapper; +import com.yfd.platform.qgc_export.mapper.SdWtrvdaySMapper; + +import java.math.BigDecimal; +import java.text.SimpleDateFormat; +import java.util.*; + +/** + * 河道水温指标处理器基类 — DWT(出库水温)使用 SD_WTRV_R / SD_WTRVDAY_S 表 + *

+ * 子类标注 @Component,getSttpType() 固定返回 "WTRV"。 + */ +public abstract class WtrvIndicatorProcessor implements ExportIndicatorProcessor { + + protected SdWtrvRMapper sdWtrvRMapper; + protected SdWtrvdaySMapper sdWtrvdaySMapper; + + private static final SimpleDateFormat SDF = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + + public void setMappers(SdWtrvRMapper rMapper, SdWtrvdaySMapper sMapper) { + this.sdWtrvRMapper = rMapper; + this.sdWtrvdaySMapper = sMapper; + } + + @Override + public String getSttpType() { + return "WTRV"; + } + + @Override + public int getDecimalPlaces() { + return 3; + } + + @Override + public List queryRawData(List stcds, Date startTime, Date endTime, boolean isDaily) { + String startStr = SDF.format(startTime); + String endStr = SDF.format(endTime); + + List> rawList; + if (isDaily) { + rawList = sdWtrvdaySMapper.selectRawData(stcds, startStr, endStr); + } else { + rawList = sdWtrvRMapper.selectRawData(stcds, startStr, endStr); + } + + List rows = new ArrayList<>(); + if (rawList != null) { + for (Map map : rawList) { + String stcd = (String) map.get("STCD"); + Date tm = (Date) map.get(isDaily ? "DT" : "TM"); + BigDecimal value = toBigDecimal(map.get("VALUE")); + if (stcd != null && tm != null && value != null) { + rows.add(new IndicatorDataRow(stcd, tm, value)); + } + } + } + return rows; + } + + private BigDecimal toBigDecimal(Object val) { + if (val instanceof BigDecimal) return (BigDecimal) val; + if (val instanceof Number) return BigDecimal.valueOf(((Number) val).doubleValue()); + return null; + } +} 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 9cbc4a56..4657094a 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 @@ -5,6 +5,8 @@ import com.yfd.platform.qgc_export.mapper.SdRiverRMapper; import com.yfd.platform.qgc_export.mapper.SdRiverdaySMapper; import com.yfd.platform.qgc_export.mapper.SdWqRMapper; import com.yfd.platform.qgc_export.mapper.SdWqdaySMapper; +import com.yfd.platform.qgc_export.mapper.SdWtrvRMapper; +import com.yfd.platform.qgc_export.mapper.SdWtrvdaySMapper; import com.yfd.platform.qgc_export.mapper.StationMappingMapper; import com.yfd.platform.qgc_export.processor.*; import com.yfd.platform.qgc_export.service.IQgcExportService; @@ -81,6 +83,16 @@ public class QgcExportServiceImpl implements IQgcExportService { @Resource private TuProcessor tuProcessor; + // DWT 出库水温相关 + @Resource + private SdWtrvRMapper sdWtrvRMapper; + @Resource + private SdWtrvdaySMapper sdWtrvdaySMapper; + @Resource + private DwtProcessor dwtProcessor; + @Resource + private DwtVariationProcessor dwtVariationProcessor; + private static final SimpleDateFormat SDF_TIME = new SimpleDateFormat("dd日 HH时"); /** 基础汇总指标(7个) */ @@ -175,7 +187,12 @@ public class QgcExportServiceImpl implements IQgcExportService { List uniqueStcds = rstcdToStcd.values().stream().distinct().toList(); - // 查询日表 + 小时表 + // ---- 日变幅格式(出入库水温日变幅) ---- + if (processor.isVariation()) { + return buildVariationRows(processor, mr, stationOrder, rstcdToStcd, uniqueStcds, decimals); + } + + // ---- 标准格式 ---- List dailyRows = processor.queryRawData(uniqueStcds, mr.getStart(), mr.getEnd(), true); List hourlyRows = processor.queryRawData(uniqueStcds, mr.getStart(), mr.getEnd(), false); @@ -240,6 +257,7 @@ public class QgcExportServiceImpl implements IQgcExportService { * 水质指标(水温→PH→溶解氧→电导率→浊度)→ 河道水情(流速→流量→水位) *

* 支持别名:前端传 "wq" 等价于传 "wtmp,ph,dox,cond,tu"。 + * 前端传 "dwt" 展开为出库水温 + 出入库水温日变幅两个 Sheet。 */ private List resolveProcessors(List dataFields) { Map processorMap = new LinkedHashMap<>(); @@ -267,6 +285,15 @@ public class QgcExportServiceImpl implements IQgcExportService { List result = new ArrayList<>(); for (String field : expanded) { + // DWT 出库水温 → 展开为标准格式 + 日变幅两个 Sheet + if ("dwt".equals(field)) { + dwtProcessor.setMappers(sdWtrvRMapper, sdWtrvdaySMapper); + dwtVariationProcessor.setMappers(sdWtrvRMapper, sdWtrvdaySMapper); + result.add(dwtProcessor); + result.add(dwtVariationProcessor); + continue; + } + ExportIndicatorProcessor p = processorMap.get(field); if (p != null) { if (p instanceof RiverIndicatorProcessor) { @@ -450,6 +477,98 @@ public class QgcExportServiceImpl implements IQgcExportService { return result; } + // ======================== 日变幅格式(出入库水温日变幅) ======================== + + /** 日变幅汇总标签 */ + private static final List VARIATION_LABELS = List.of("日变幅均值", "日变幅最大值"); + + /** + * 构建日变幅格式的 Sheet 数据。 + *

+ * 日数据行:从小时表计算每日变幅(当日最大 - 最小)。 + * 汇总行:日变幅均值、日变幅最大值。 + */ + private List> buildVariationRows( + ExportIndicatorProcessor processor, MonthRange mr, + List stationOrder, Map rstcdToStcd, + List uniqueStcds, int decimals) { + + // 仅查询小时表,按天分组计算日变幅(当日最大-最小) + List hourlyRows = processor.queryRawData(uniqueStcds, mr.getStart(), mr.getEnd(), false); + + // STCD → {day → 日变幅} + Map> variationByStcd = new LinkedHashMap<>(); + // STCD → {day → List} 临时分组 + Map>> hourlyByStcdDay = new LinkedHashMap<>(); + for (IndicatorDataRow row : hourlyRows) { + 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); + hourlyByStcdDay + .computeIfAbsent(row.getStcd(), k -> new LinkedHashMap<>()) + .computeIfAbsent(day, k -> new ArrayList<>()) + .add(row.getValue()); + } + // 计算每日变幅 = max - min + for (Map.Entry>> stcdEntry : hourlyByStcdDay.entrySet()) { + Map dayVariation = new LinkedHashMap<>(); + for (Map.Entry> dayEntry : stcdEntry.getValue().entrySet()) { + List values = dayEntry.getValue(); + if (values.size() >= 2) { + BigDecimal max = Collections.max(values); + BigDecimal min = Collections.min(values); + dayVariation.put(dayEntry.getKey(), max.subtract(min)); + } + } + if (!dayVariation.isEmpty()) { + variationByStcd.put(stcdEntry.getKey(), dayVariation); + } + } + + // RSTCD → [日变幅均值, 日变幅最大值] + Map variationAgg = new LinkedHashMap<>(); + for (Map.Entry entry : rstcdToStcd.entrySet()) { + Map dayMap = variationByStcd.get(entry.getValue()); + if (dayMap != null && !dayMap.isEmpty()) { + List values = new ArrayList<>(dayMap.values()); + BigDecimal sum = values.stream().reduce(BigDecimal.ZERO, BigDecimal::add); + BigDecimal avg = sum.divide(BigDecimal.valueOf(values.size()), 3, RoundingMode.HALF_UP); + BigDecimal max = Collections.max(values); + variationAgg.put(entry.getKey(), new BigDecimal[]{avg, max}); + } + } + + List> rows = new ArrayList<>(); + + // ---- 日数据行(日变幅) ---- + int daysInMonth = getDaysInMonth(mr); + for (int d = 1; d <= daysInMonth; d++) { + List row = new ArrayList<>(); + row.add(String.format("%02d", d)); + for (String rstcd : stationOrder) { + String stcd = rstcdToStcd.get(rstcd); + Map dayMap = stcd != null ? variationByStcd.get(stcd) : null; + BigDecimal val = dayMap != null ? dayMap.get(d) : null; + row.add(formatOrEmpty(val, decimals)); + } + rows.add(row); + } + + // ---- 汇总行 ---- + for (int i = 0; i < VARIATION_LABELS.size(); i++) { + List row = new ArrayList<>(); + row.add(VARIATION_LABELS.get(i)); + for (String rstcd : stationOrder) { + BigDecimal[] agg = variationAgg.get(rstcd); + row.add(formatOrEmpty(agg != null ? agg[i] : null, decimals)); + } + rows.add(row); + } + + return rows; + } + private static String formatOrEmpty(BigDecimal value, int decimals) { if (value == null) return ""; return value.setScale(decimals, RoundingMode.HALF_UP).toPlainString(); diff --git a/backend/src/main/resources/application-prod.yml b/backend/src/main/resources/application-prod.yml index 98127603..b7c9abc9 100644 --- a/backend/src/main/resources/application-prod.yml +++ b/backend/src/main/resources/application-prod.yml @@ -13,8 +13,8 @@ spring: # ==================== 达梦(DM) 数据源 ==================== dm-master: driverClassName: dm.jdbc.driver.DmDriver - url: "${DB_DM_MASTER_URL:jdbc:dm://172.16.21.143:5236/QGC_REFA}" - username: "${DB_DM_MASTER_USERNAME:QGC_REFA}" + url: "${DB_DM_MASTER_URL:jdbc:dm://172.16.21.143:5236/QGC_REFA_TEST}" + username: "${DB_DM_MASTER_USERNAME:QGC_REFA_TEST}" password: "${DB_DM_MASTER_PASSWORD:Y4M4K1oCkL8U}" initial-size: 5 min-idle: 5 @@ -42,8 +42,8 @@ spring: max-pool-prepared-statement-per-connection-size: 100 dm-slave: driverClassName: dm.jdbc.driver.DmDriver - url: "${DB_DM_SLAVE_URL:jdbc:dm://172.16.21.143:5236/QGC_REFA}" - username: "${DB_DM_SLAVE_USERNAME:QGC_REFA}" + url: "${DB_DM_SLAVE_URL:jdbc:dm://172.16.21.143:5236/QGC_REFA_TEST}" + username: "${DB_DM_SLAVE_USERNAME:QGC_REFA_TEST}" password: "${DB_DM_SLAVE_PASSWORD:Y4M4K1oCkL8U}" initial-size: 5 min-idle: 5 diff --git a/backend/src/main/resources/mapper/qgc_export/SdWtrvRMapper.xml b/backend/src/main/resources/mapper/qgc_export/SdWtrvRMapper.xml new file mode 100644 index 00000000..04c8cbe9 --- /dev/null +++ b/backend/src/main/resources/mapper/qgc_export/SdWtrvRMapper.xml @@ -0,0 +1,23 @@ + + + + + + + + diff --git a/backend/src/main/resources/mapper/qgc_export/SdWtrvdaySMapper.xml b/backend/src/main/resources/mapper/qgc_export/SdWtrvdaySMapper.xml new file mode 100644 index 00000000..50f6af6a --- /dev/null +++ b/backend/src/main/resources/mapper/qgc_export/SdWtrvdaySMapper.xml @@ -0,0 +1,23 @@ + + + + + + + + From c17bf6fd08c078dfb3c20d27ab4a7a7fb2453616 Mon Sep 17 00:00:00 2001 From: tangwei Date: Wed, 5 Aug 2026 17:12:47 +0800 Subject: [PATCH 11/28] =?UTF-8?q?fix:=20=E5=A2=9E=E5=8A=A0excel=E7=BB=BC?= =?UTF-8?q?=E5=90=88=E5=AF=BC=E5=87=BA=E5=9D=9D=E4=B8=8A=E3=80=81=E5=9D=9D?= =?UTF-8?q?=E4=B8=8B=E6=B0=B4=E4=BD=8D=E6=95=B0=E6=8D=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../qgc_export/domain/SdHydropwR.java | 89 +++++++++++++++++++ .../qgc_export/domain/SdHydropwdayS.java | 72 +++++++++++++++ .../qgc_export/mapper/SdHydropwRMapper.java | 28 ++++++ .../mapper/SdHydropwdaySMapper.java | 28 ++++++ .../qgc_export/processor/DzProcessor.java | 20 +++++ .../processor/DzVariationProcessor.java | 25 ++++++ .../processor/ExportIndicatorProcessor.java | 1 + .../processor/HydropwIndicatorProcessor.java | 69 ++++++++++++++ .../qgc_export/processor/RzProcessor.java | 20 +++++ .../processor/RzVariationProcessor.java | 25 ++++++ .../service/impl/QgcExportServiceImpl.java | 60 ++++++++++++- .../mapper/qgc_export/SdHydropwRMapper.xml | 23 +++++ .../mapper/qgc_export/SdHydropwdaySMapper.xml | 23 +++++ 13 files changed, 480 insertions(+), 3 deletions(-) create mode 100644 backend/src/main/java/com/yfd/platform/qgc_export/domain/SdHydropwR.java create mode 100644 backend/src/main/java/com/yfd/platform/qgc_export/domain/SdHydropwdayS.java create mode 100644 backend/src/main/java/com/yfd/platform/qgc_export/mapper/SdHydropwRMapper.java create mode 100644 backend/src/main/java/com/yfd/platform/qgc_export/mapper/SdHydropwdaySMapper.java create mode 100644 backend/src/main/java/com/yfd/platform/qgc_export/processor/DzProcessor.java create mode 100644 backend/src/main/java/com/yfd/platform/qgc_export/processor/DzVariationProcessor.java create mode 100644 backend/src/main/java/com/yfd/platform/qgc_export/processor/HydropwIndicatorProcessor.java create mode 100644 backend/src/main/java/com/yfd/platform/qgc_export/processor/RzProcessor.java create mode 100644 backend/src/main/java/com/yfd/platform/qgc_export/processor/RzVariationProcessor.java create mode 100644 backend/src/main/resources/mapper/qgc_export/SdHydropwRMapper.xml create mode 100644 backend/src/main/resources/mapper/qgc_export/SdHydropwdaySMapper.xml diff --git a/backend/src/main/java/com/yfd/platform/qgc_export/domain/SdHydropwR.java b/backend/src/main/java/com/yfd/platform/qgc_export/domain/SdHydropwR.java new file mode 100644 index 00000000..7264ddcc --- /dev/null +++ b/backend/src/main/java/com/yfd/platform/qgc_export/domain/SdHydropwR.java @@ -0,0 +1,89 @@ +package com.yfd.platform.qgc_export.domain; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +import java.io.Serializable; +import java.math.BigDecimal; +import java.util.Date; + +/** + * 电站运行数据表(小时级) + * 对应 Oracle 表: QGC_REFA.SD_HYDROPW_R + */ +@Data +@TableName("SD_HYDROPW_R") +public class SdHydropwR implements Serializable { + + private static final long serialVersionUID = 1L; + + @TableId(type = IdType.INPUT) + private String id; + + /** 水电工程编码 */ + private String stcd; + + /** 数据时间 */ + private Date tm; + + /** 坝上水位 (m) */ + private BigDecimal rz; + + /** 蓄水量 */ + private BigDecimal w; + + /** 坝下水位 (m) */ + private BigDecimal dz; + + /** 入库流量 */ + private BigDecimal qi; + + /** 出库流量 */ + private BigDecimal qo; + + /** 发电流量 */ + private BigDecimal qn; + + /** 泄洪流量 */ + private BigDecimal qgt; + + /** 生态流量 */ + private BigDecimal qec; + + /** 灌溉流量 */ + private BigDecimal qc; + + /** 发电量 */ + private BigDecimal ge; + + /** 平均出力 */ + private BigDecimal na; + + /** 生态流量是否达标 */ + private Integer sfdb; + + /** 泄洪水量 */ + private BigDecimal gtw; + + /** 发电水量 */ + private BigDecimal wge; + + /** 区间流量 */ + private BigDecimal qr; + + /** 耗水率 */ + private BigDecimal wr; + + /** 记录人等... */ + private String recordUser; + private Date recordTime; + private String modifyUser; + private Date modifyTime; + private Integer isDeleted; + private String deleteUser; + private Date deleteTime; + private String fid; + private String remark; +} diff --git a/backend/src/main/java/com/yfd/platform/qgc_export/domain/SdHydropwdayS.java b/backend/src/main/java/com/yfd/platform/qgc_export/domain/SdHydropwdayS.java new file mode 100644 index 00000000..85b5f78d --- /dev/null +++ b/backend/src/main/java/com/yfd/platform/qgc_export/domain/SdHydropwdayS.java @@ -0,0 +1,72 @@ +package com.yfd.platform.qgc_export.domain; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +import java.io.Serializable; +import java.math.BigDecimal; +import java.util.Date; + +/** + * 电站运行数据日统计表 + * 对应 Oracle 表: QGC_REFA.SD_HYDROPWDAY_S + */ +@Data +@TableName("SD_HYDROPWDAY_S") +public class SdHydropwdayS implements Serializable { + + private static final long serialVersionUID = 1L; + + @TableId(type = IdType.INPUT) + private String id; + + /** 水电工程编码 */ + private String stcd; + + /** 时间 */ + private Date dt; + + /** 坝上水位 (m) */ + private BigDecimal rz; + + /** 蓄水量 */ + private BigDecimal w; + + /** 坝下水位 (m) */ + private BigDecimal dz; + + /** 入库流量 */ + private BigDecimal qi; + + /** 出库流量 */ + private BigDecimal qo; + + /** 发电流量 */ + private BigDecimal qn; + + /** 泄洪流量 */ + private BigDecimal qgt; + + /** 生态流量 */ + private BigDecimal qec; + + /** 灌溉流量 */ + private BigDecimal qc; + + /** 发电量 */ + private BigDecimal ge; + + /** 平均出力 */ + private BigDecimal na; + + /** 记录人等... */ + private String recordUser; + private Date recordTime; + private String modifyUser; + private Date modifyTime; + private Integer isDeleted; + private String deleteUser; + private Date deleteTime; +} diff --git a/backend/src/main/java/com/yfd/platform/qgc_export/mapper/SdHydropwRMapper.java b/backend/src/main/java/com/yfd/platform/qgc_export/mapper/SdHydropwRMapper.java new file mode 100644 index 00000000..6b8faf0e --- /dev/null +++ b/backend/src/main/java/com/yfd/platform/qgc_export/mapper/SdHydropwRMapper.java @@ -0,0 +1,28 @@ +package com.yfd.platform.qgc_export.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.yfd.platform.qgc_export.domain.SdHydropwR; +import org.apache.ibatis.annotations.Param; + +import java.util.List; +import java.util.Map; + +/** + * SD_HYDROPW_R 电站运行小时数据 Mapper + */ +public interface SdHydropwRMapper extends BaseMapper { + + /** + * 批量查询测站在指定时间范围内的原始小时数据(动态字段) + * + * @param stcdList 测站编码列表 + * @param fieldName 查询字段名(RZ/DZ) + * @param startTime 起始时间 + * @param endTime 结束时间 + * @return [STCD, TM, VALUE] + */ + List> selectRawData(@Param("stcdList") List stcdList, + @Param("fieldName") String fieldName, + @Param("startTime") String startTime, + @Param("endTime") String endTime); +} diff --git a/backend/src/main/java/com/yfd/platform/qgc_export/mapper/SdHydropwdaySMapper.java b/backend/src/main/java/com/yfd/platform/qgc_export/mapper/SdHydropwdaySMapper.java new file mode 100644 index 00000000..5757b572 --- /dev/null +++ b/backend/src/main/java/com/yfd/platform/qgc_export/mapper/SdHydropwdaySMapper.java @@ -0,0 +1,28 @@ +package com.yfd.platform.qgc_export.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.yfd.platform.qgc_export.domain.SdHydropwdayS; +import org.apache.ibatis.annotations.Param; + +import java.util.List; +import java.util.Map; + +/** + * SD_HYDROPWDAY_S 电站运行日数据 Mapper + */ +public interface SdHydropwdaySMapper extends BaseMapper { + + /** + * 批量查询测站在指定时间范围内的原始日数据(动态字段) + * + * @param stcdList 测站编码列表 + * @param fieldName 查询字段名(RZ/DZ) + * @param startTime 起始时间 + * @param endTime 结束时间 + * @return [STCD, DT, VALUE] + */ + List> selectRawData(@Param("stcdList") List stcdList, + @Param("fieldName") String fieldName, + @Param("startTime") String startTime, + @Param("endTime") String endTime); +} diff --git a/backend/src/main/java/com/yfd/platform/qgc_export/processor/DzProcessor.java b/backend/src/main/java/com/yfd/platform/qgc_export/processor/DzProcessor.java new file mode 100644 index 00000000..83a8c58e --- /dev/null +++ b/backend/src/main/java/com/yfd/platform/qgc_export/processor/DzProcessor.java @@ -0,0 +1,20 @@ +package com.yfd.platform.qgc_export.processor; + +import org.springframework.stereotype.Component; + +/** + * 坝下水位(DZ) 标准格式处理器 + */ +@Component +public class DzProcessor extends HydropwIndicatorProcessor { + + @Override + public String getFieldName() { + return "DZ"; + } + + @Override + public String getSheetName() { + return "坝下水位"; + } +} diff --git a/backend/src/main/java/com/yfd/platform/qgc_export/processor/DzVariationProcessor.java b/backend/src/main/java/com/yfd/platform/qgc_export/processor/DzVariationProcessor.java new file mode 100644 index 00000000..f34ce869 --- /dev/null +++ b/backend/src/main/java/com/yfd/platform/qgc_export/processor/DzVariationProcessor.java @@ -0,0 +1,25 @@ +package com.yfd.platform.qgc_export.processor; + +import org.springframework.stereotype.Component; + +/** + * 坝下水位(DZ) 日变幅格式处理器 + */ +@Component +public class DzVariationProcessor extends HydropwIndicatorProcessor { + + @Override + public String getFieldName() { + return "DZ"; + } + + @Override + public String getSheetName() { + return "坝下水位日变幅"; + } + + @Override + public boolean isVariation() { + return true; + } +} diff --git a/backend/src/main/java/com/yfd/platform/qgc_export/processor/ExportIndicatorProcessor.java b/backend/src/main/java/com/yfd/platform/qgc_export/processor/ExportIndicatorProcessor.java index 997e411b..c24728f9 100644 --- a/backend/src/main/java/com/yfd/platform/qgc_export/processor/ExportIndicatorProcessor.java +++ b/backend/src/main/java/com/yfd/platform/qgc_export/processor/ExportIndicatorProcessor.java @@ -26,6 +26,7 @@ public interface ExportIndicatorProcessor { *
  • V/Q/Z 河道水情 → "ZQ"
  • *
  • WQ 水质 → "WQ"
  • *
  • DWT 出库水温 → "WTRV"
  • + *
  • RZ/DZ 坝上/坝下水位 → "ENG"
  • *
  • QEC 生态流量 → 待定
  • * */ diff --git a/backend/src/main/java/com/yfd/platform/qgc_export/processor/HydropwIndicatorProcessor.java b/backend/src/main/java/com/yfd/platform/qgc_export/processor/HydropwIndicatorProcessor.java new file mode 100644 index 00000000..b9712dcb --- /dev/null +++ b/backend/src/main/java/com/yfd/platform/qgc_export/processor/HydropwIndicatorProcessor.java @@ -0,0 +1,69 @@ +package com.yfd.platform.qgc_export.processor; + +import com.yfd.platform.qgc_export.mapper.SdHydropwRMapper; +import com.yfd.platform.qgc_export.mapper.SdHydropwdaySMapper; + +import java.math.BigDecimal; +import java.text.SimpleDateFormat; +import java.util.*; + +/** + * 电站运行指标处理器基类 — RZ(坝上水位)/DZ(坝下水位) 使用 SD_HYDROPW_R / SD_HYDROPWDAY_S 表 + *

    + * 子类标注 @Component,getSttpType() 固定返回 "ENG"。 + * 动态字段名由子类 getFieldName() 返回。 + */ +public abstract class HydropwIndicatorProcessor implements ExportIndicatorProcessor { + + protected SdHydropwRMapper sdHydropwRMapper; + protected SdHydropwdaySMapper sdHydropwdaySMapper; + + private static final SimpleDateFormat SDF = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + + public void setMappers(SdHydropwRMapper rMapper, SdHydropwdaySMapper sMapper) { + this.sdHydropwRMapper = rMapper; + this.sdHydropwdaySMapper = sMapper; + } + + @Override + public String getSttpType() { + return "ENG"; + } + + @Override + public int getDecimalPlaces() { + return 3; + } + + @Override + public List queryRawData(List stcds, Date startTime, Date endTime, boolean isDaily) { + String startStr = SDF.format(startTime); + String endStr = SDF.format(endTime); + + List> rawList; + if (isDaily) { + rawList = sdHydropwdaySMapper.selectRawData(stcds, getFieldName(), startStr, endStr); + } else { + rawList = sdHydropwRMapper.selectRawData(stcds, getFieldName(), startStr, endStr); + } + + List rows = new ArrayList<>(); + if (rawList != null) { + for (Map map : rawList) { + String stcd = (String) map.get("STCD"); + Date tm = (Date) map.get(isDaily ? "DT" : "TM"); + BigDecimal value = toBigDecimal(map.get("VALUE")); + if (stcd != null && tm != null && value != null) { + rows.add(new IndicatorDataRow(stcd, tm, value)); + } + } + } + return rows; + } + + private BigDecimal toBigDecimal(Object val) { + if (val instanceof BigDecimal) return (BigDecimal) val; + if (val instanceof Number) return BigDecimal.valueOf(((Number) val).doubleValue()); + return null; + } +} diff --git a/backend/src/main/java/com/yfd/platform/qgc_export/processor/RzProcessor.java b/backend/src/main/java/com/yfd/platform/qgc_export/processor/RzProcessor.java new file mode 100644 index 00000000..c00dd40f --- /dev/null +++ b/backend/src/main/java/com/yfd/platform/qgc_export/processor/RzProcessor.java @@ -0,0 +1,20 @@ +package com.yfd.platform.qgc_export.processor; + +import org.springframework.stereotype.Component; + +/** + * 坝上水位(RZ) 标准格式处理器 + */ +@Component +public class RzProcessor extends HydropwIndicatorProcessor { + + @Override + public String getFieldName() { + return "RZ"; + } + + @Override + public String getSheetName() { + return "坝上水位"; + } +} diff --git a/backend/src/main/java/com/yfd/platform/qgc_export/processor/RzVariationProcessor.java b/backend/src/main/java/com/yfd/platform/qgc_export/processor/RzVariationProcessor.java new file mode 100644 index 00000000..f7ed04d9 --- /dev/null +++ b/backend/src/main/java/com/yfd/platform/qgc_export/processor/RzVariationProcessor.java @@ -0,0 +1,25 @@ +package com.yfd.platform.qgc_export.processor; + +import org.springframework.stereotype.Component; + +/** + * 坝上水位(RZ) 日变幅格式处理器 + */ +@Component +public class RzVariationProcessor extends HydropwIndicatorProcessor { + + @Override + public String getFieldName() { + return "RZ"; + } + + @Override + public String getSheetName() { + return "坝上水位日变幅"; + } + + @Override + public boolean isVariation() { + return true; + } +} 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 4657094a..e21b57ed 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 @@ -7,6 +7,8 @@ import com.yfd.platform.qgc_export.mapper.SdWqRMapper; import com.yfd.platform.qgc_export.mapper.SdWqdaySMapper; import com.yfd.platform.qgc_export.mapper.SdWtrvRMapper; import com.yfd.platform.qgc_export.mapper.SdWtrvdaySMapper; +import com.yfd.platform.qgc_export.mapper.SdHydropwRMapper; +import com.yfd.platform.qgc_export.mapper.SdHydropwdaySMapper; import com.yfd.platform.qgc_export.mapper.StationMappingMapper; import com.yfd.platform.qgc_export.processor.*; import com.yfd.platform.qgc_export.service.IQgcExportService; @@ -29,7 +31,16 @@ import java.util.stream.Collectors; * 综合导出服务实现(月度报表) *

    * 格式:每月一个 Excel 文件 → ZIP 下载。 - * 每个 Excel 含多个 Sheet,顺序:水温→PH→溶解氧→电导率→浊度→流速→流量→水位。 + * 每个 Excel 含多个 Sheet,顺序由前端 dataField 参数决定。 + * + *

    支持的指标

    + *
      + *
    • WQ: wtmp/ph/dox/cond/tu → STTP=WQ, 8个汇总指标(含小时达标率)
    • + *
    • 河道水情: v/q/z → STTP=ZQ, 7个汇总指标
    • + *
    • 出库水温: dwt → STTP=WTRV, 2个Sheet(标准+日变幅)
    • + *
    • 坝上水位: rz → STTP=ENG, 2个Sheet(标准+日变幅)
    • + *
    • 坝下水位: dz → STTP=ENG, 2个Sheet(标准+日变幅)
    • + *
    * *

    Sheet 结构

    *
    @@ -93,6 +104,20 @@ public class QgcExportServiceImpl implements IQgcExportService {
         @Resource
         private DwtVariationProcessor dwtVariationProcessor;
     
    +    // RZ/DZ 坝上/坝下水位相关(STTP=ENG)
    +    @Resource
    +    private SdHydropwRMapper sdHydropwRMapper;
    +    @Resource
    +    private SdHydropwdaySMapper sdHydropwdaySMapper;
    +    @Resource
    +    private RzProcessor rzProcessor;
    +    @Resource
    +    private RzVariationProcessor rzVariationProcessor;
    +    @Resource
    +    private DzProcessor dzProcessor;
    +    @Resource
    +    private DzVariationProcessor dzVariationProcessor;
    +
         private static final SimpleDateFormat SDF_TIME = new SimpleDateFormat("dd日 HH时");
     
         /** 基础汇总指标(7个) */
    @@ -293,6 +318,22 @@ public class QgcExportServiceImpl implements IQgcExportService {
                     result.add(dwtVariationProcessor);
                     continue;
                 }
    +            // RZ 坝上水位 → 展开为标准格式 + 日变幅两个 Sheet
    +            if ("rz".equals(field)) {
    +                rzProcessor.setMappers(sdHydropwRMapper, sdHydropwdaySMapper);
    +                rzVariationProcessor.setMappers(sdHydropwRMapper, sdHydropwdaySMapper);
    +                result.add(rzProcessor);
    +                result.add(rzVariationProcessor);
    +                continue;
    +            }
    +            // DZ 坝下水位 → 展开为标准格式 + 日变幅两个 Sheet
    +            if ("dz".equals(field)) {
    +                dzProcessor.setMappers(sdHydropwRMapper, sdHydropwdaySMapper);
    +                dzVariationProcessor.setMappers(sdHydropwRMapper, sdHydropwdaySMapper);
    +                result.add(dzProcessor);
    +                result.add(dzVariationProcessor);
    +                continue;
    +            }
     
                 ExportIndicatorProcessor p = processorMap.get(field);
                 if (p != null) {
    @@ -314,11 +355,24 @@ public class QgcExportServiceImpl implements IQgcExportService {
             if (rstcds == null || rstcds.isEmpty()) return result;
     
             List mappings = stationMappingMapper.selectList(
    -                new LambdaQueryWrapper().in(StationMapping::getRstcd, rstcds));
    +                new LambdaQueryWrapper()
    +                        .in(StationMapping::getRstcd, rstcds)
    +                        .or()
    +                        .in(StationMapping::getStcd, rstcds)
    +        );
     
    +        // 添加逻辑:如果 sttp 是 "ENG",则 rstcd 赋值为 stcd
    +        mappings.forEach(mapping -> {
    +            if ("ENG".equals(mapping.getSttp())) {
    +                mapping.setRstcd(mapping.getStcd());
    +            }
    +        });
             Map> byRstcd = new LinkedHashMap<>();
    +
             for (StationMapping m : mappings) {
    -            if (m.getRstcd() == null) continue;
    +            if (!"ENG".equals(m.getSttp()) && m.getRstcd() == null) {
    +                continue;
    +            }
                 byRstcd.computeIfAbsent(m.getRstcd(), k -> new ArrayList<>()).add(m);
             }
     
    diff --git a/backend/src/main/resources/mapper/qgc_export/SdHydropwRMapper.xml b/backend/src/main/resources/mapper/qgc_export/SdHydropwRMapper.xml
    new file mode 100644
    index 00000000..7b39431f
    --- /dev/null
    +++ b/backend/src/main/resources/mapper/qgc_export/SdHydropwRMapper.xml
    @@ -0,0 +1,23 @@
    +
    +
    +
    +
    +    
    +    
    +
    +
    diff --git a/backend/src/main/resources/mapper/qgc_export/SdHydropwdaySMapper.xml b/backend/src/main/resources/mapper/qgc_export/SdHydropwdaySMapper.xml
    new file mode 100644
    index 00000000..abb89678
    --- /dev/null
    +++ b/backend/src/main/resources/mapper/qgc_export/SdHydropwdaySMapper.xml
    @@ -0,0 +1,23 @@
    +
    +
    +
    +
    +    
    +    
    +
    +
    
    From bc1910b8d2183b2b08fdd5ae84c92236b0a4f865 Mon Sep 17 00:00:00 2001
    From: tangwei 
    Date: Wed, 5 Aug 2026 17:18:34 +0800
    Subject: [PATCH 12/28] =?UTF-8?q?fix:=20=E4=BC=98=E5=8C=96=E6=B0=B4?=
     =?UTF-8?q?=E8=B4=A8=E6=8C=87=E6=A0=87sheet=E5=90=8D=E7=A7=B0?=
    MIME-Version: 1.0
    Content-Type: text/plain; charset=UTF-8
    Content-Transfer-Encoding: 8bit
    
    ---
     .../com/yfd/platform/qgc_export/processor/CondProcessor.java    | 2 +-
     .../com/yfd/platform/qgc_export/processor/DoxProcessor.java     | 2 +-
     .../java/com/yfd/platform/qgc_export/processor/PhProcessor.java | 2 +-
     .../java/com/yfd/platform/qgc_export/processor/TuProcessor.java | 2 +-
     .../com/yfd/platform/qgc_export/processor/WtmpProcessor.java    | 2 +-
     5 files changed, 5 insertions(+), 5 deletions(-)
    
    diff --git a/backend/src/main/java/com/yfd/platform/qgc_export/processor/CondProcessor.java b/backend/src/main/java/com/yfd/platform/qgc_export/processor/CondProcessor.java
    index f79e6921..f9b70e9d 100644
    --- a/backend/src/main/java/com/yfd/platform/qgc_export/processor/CondProcessor.java
    +++ b/backend/src/main/java/com/yfd/platform/qgc_export/processor/CondProcessor.java
    @@ -15,7 +15,7 @@ public class CondProcessor extends WqIndicatorProcessor {
     
         @Override
         public String getSheetName() {
    -        return "电导率(μS/cm)";
    +        return "水质(电导率)";
         }
     
         @Override
    diff --git a/backend/src/main/java/com/yfd/platform/qgc_export/processor/DoxProcessor.java b/backend/src/main/java/com/yfd/platform/qgc_export/processor/DoxProcessor.java
    index 61adcc37..5d3723da 100644
    --- a/backend/src/main/java/com/yfd/platform/qgc_export/processor/DoxProcessor.java
    +++ b/backend/src/main/java/com/yfd/platform/qgc_export/processor/DoxProcessor.java
    @@ -15,7 +15,7 @@ public class DoxProcessor extends WqIndicatorProcessor {
     
         @Override
         public String getSheetName() {
    -        return "溶解氧(mg/L)";
    +        return "水质(溶解氧)";
         }
     
         @Override
    diff --git a/backend/src/main/java/com/yfd/platform/qgc_export/processor/PhProcessor.java b/backend/src/main/java/com/yfd/platform/qgc_export/processor/PhProcessor.java
    index 1eca08f7..e4c123c2 100644
    --- a/backend/src/main/java/com/yfd/platform/qgc_export/processor/PhProcessor.java
    +++ b/backend/src/main/java/com/yfd/platform/qgc_export/processor/PhProcessor.java
    @@ -15,7 +15,7 @@ public class PhProcessor extends WqIndicatorProcessor {
     
         @Override
         public String getSheetName() {
    -        return "PH";
    +        return "水质(PH)";
         }
     
         @Override
    diff --git a/backend/src/main/java/com/yfd/platform/qgc_export/processor/TuProcessor.java b/backend/src/main/java/com/yfd/platform/qgc_export/processor/TuProcessor.java
    index f92a3b0d..babef596 100644
    --- a/backend/src/main/java/com/yfd/platform/qgc_export/processor/TuProcessor.java
    +++ b/backend/src/main/java/com/yfd/platform/qgc_export/processor/TuProcessor.java
    @@ -15,7 +15,7 @@ public class TuProcessor extends WqIndicatorProcessor {
     
         @Override
         public String getSheetName() {
    -        return "浊度(NTU)";
    +        return "水质(浊度)";
         }
     
         @Override
    diff --git a/backend/src/main/java/com/yfd/platform/qgc_export/processor/WtmpProcessor.java b/backend/src/main/java/com/yfd/platform/qgc_export/processor/WtmpProcessor.java
    index 7a136d59..c18a1357 100644
    --- a/backend/src/main/java/com/yfd/platform/qgc_export/processor/WtmpProcessor.java
    +++ b/backend/src/main/java/com/yfd/platform/qgc_export/processor/WtmpProcessor.java
    @@ -15,7 +15,7 @@ public class WtmpProcessor extends WqIndicatorProcessor {
     
         @Override
         public String getSheetName() {
    -        return "水温(℃)";
    +        return "水质(水温)";
         }
     
         @Override
    
    From d5549e1541fc6364d11f8303d57253fa3c50f52a Mon Sep 17 00:00:00 2001
    From: tangwei 
    Date: Wed, 5 Aug 2026 17:43:06 +0800
    Subject: [PATCH 13/28] =?UTF-8?q?fix:=20=E5=A2=9E=E5=8A=A0sheet=E9=A1=B5?=
     =?UTF-8?q?=E6=8E=92=E5=BA=8F=E5=92=8C=E5=8C=96=E5=AD=A6=E9=9C=80=E6=B0=A7?=
     =?UTF-8?q?=E9=87=8F=E6=8C=87=E6=A0=87?=
    MIME-Version: 1.0
    Content-Type: text/plain; charset=UTF-8
    Content-Transfer-Encoding: 8bit
    
    ---
     .../qgc_export/processor/CodcrProcessor.java  | 30 +++++++++++++++++++
     .../qgc_export/processor/CondProcessor.java   |  4 +--
     .../qgc_export/processor/DoxProcessor.java    |  4 +--
     .../qgc_export/processor/DwtProcessor.java    |  5 ++++
     .../processor/DwtVariationProcessor.java      |  5 ++++
     .../qgc_export/processor/DzProcessor.java     |  5 ++++
     .../processor/DzVariationProcessor.java       |  5 ++++
     .../processor/ExportIndicatorProcessor.java   |  8 +++++
     .../qgc_export/processor/PhProcessor.java     |  4 +--
     .../qgc_export/processor/QProcessor.java      | 15 ++++------
     .../qgc_export/processor/RzProcessor.java     |  5 ++++
     .../processor/RzVariationProcessor.java       |  5 ++++
     .../qgc_export/processor/TuProcessor.java     |  4 +--
     .../qgc_export/processor/VProcessor.java      | 15 ++++------
     .../qgc_export/processor/WtmpProcessor.java   |  5 ++--
     .../qgc_export/processor/ZProcessor.java      | 15 ++++------
     .../service/impl/QgcExportServiceImpl.java    |  6 +++-
     17 files changed, 101 insertions(+), 39 deletions(-)
     create mode 100644 backend/src/main/java/com/yfd/platform/qgc_export/processor/CodcrProcessor.java
    
    diff --git a/backend/src/main/java/com/yfd/platform/qgc_export/processor/CodcrProcessor.java b/backend/src/main/java/com/yfd/platform/qgc_export/processor/CodcrProcessor.java
    new file mode 100644
    index 00000000..11e8a591
    --- /dev/null
    +++ b/backend/src/main/java/com/yfd/platform/qgc_export/processor/CodcrProcessor.java
    @@ -0,0 +1,30 @@
    +package com.yfd.platform.qgc_export.processor;
    +
    +import org.springframework.stereotype.Component;
    +
    +/**
    + * 电导率(COND) 指标处理器
    + */
    +@Component
    +public class CodcrProcessor extends WqIndicatorProcessor {
    +
    +    @Override
    +    public String getFieldName() {
    +        return "CODCR";
    +    }
    +
    +    @Override
    +    public String getSheetName() {
    +        return "水质(化学需氧量)";
    +    }
    +
    +    @Override
    +    public int getDecimalPlaces() {
    +        return 2;
    +    }
    +
    +    @Override
    +    public int getOrder() {
    +        return 12;
    +    }
    +}
    diff --git a/backend/src/main/java/com/yfd/platform/qgc_export/processor/CondProcessor.java b/backend/src/main/java/com/yfd/platform/qgc_export/processor/CondProcessor.java
    index f9b70e9d..6d5b4da5 100644
    --- a/backend/src/main/java/com/yfd/platform/qgc_export/processor/CondProcessor.java
    +++ b/backend/src/main/java/com/yfd/platform/qgc_export/processor/CondProcessor.java
    @@ -24,7 +24,7 @@ public class CondProcessor extends WqIndicatorProcessor {
         }
     
         @Override
    -    public String getSttpType() {
    -        return "WQ";
    +    public int getOrder() {
    +        return 13;
         }
     }
    diff --git a/backend/src/main/java/com/yfd/platform/qgc_export/processor/DoxProcessor.java b/backend/src/main/java/com/yfd/platform/qgc_export/processor/DoxProcessor.java
    index 5d3723da..023ce588 100644
    --- a/backend/src/main/java/com/yfd/platform/qgc_export/processor/DoxProcessor.java
    +++ b/backend/src/main/java/com/yfd/platform/qgc_export/processor/DoxProcessor.java
    @@ -24,7 +24,7 @@ public class DoxProcessor extends WqIndicatorProcessor {
         }
     
         @Override
    -    public String getSttpType() {
    -        return "WQ";
    +    public int getOrder() {
    +        return 11;
         }
     }
    diff --git a/backend/src/main/java/com/yfd/platform/qgc_export/processor/DwtProcessor.java b/backend/src/main/java/com/yfd/platform/qgc_export/processor/DwtProcessor.java
    index 87b7f550..d8544f3a 100644
    --- a/backend/src/main/java/com/yfd/platform/qgc_export/processor/DwtProcessor.java
    +++ b/backend/src/main/java/com/yfd/platform/qgc_export/processor/DwtProcessor.java
    @@ -19,4 +19,9 @@ public class DwtProcessor extends WtrvIndicatorProcessor {
         public String getSheetName() {
             return "出库水温";
         }
    +
    +    @Override
    +    public int getOrder() {
    +        return 8;
    +    }
     }
    diff --git a/backend/src/main/java/com/yfd/platform/qgc_export/processor/DwtVariationProcessor.java b/backend/src/main/java/com/yfd/platform/qgc_export/processor/DwtVariationProcessor.java
    index 46ac84d5..87835dbf 100644
    --- a/backend/src/main/java/com/yfd/platform/qgc_export/processor/DwtVariationProcessor.java
    +++ b/backend/src/main/java/com/yfd/platform/qgc_export/processor/DwtVariationProcessor.java
    @@ -24,4 +24,9 @@ public class DwtVariationProcessor extends WtrvIndicatorProcessor {
         public boolean isVariation() {
             return true;
         }
    +
    +    @Override
    +    public int getOrder() {
    +        return 9;
    +    }
     }
    diff --git a/backend/src/main/java/com/yfd/platform/qgc_export/processor/DzProcessor.java b/backend/src/main/java/com/yfd/platform/qgc_export/processor/DzProcessor.java
    index 83a8c58e..a645d9de 100644
    --- a/backend/src/main/java/com/yfd/platform/qgc_export/processor/DzProcessor.java
    +++ b/backend/src/main/java/com/yfd/platform/qgc_export/processor/DzProcessor.java
    @@ -17,4 +17,9 @@ public class DzProcessor extends HydropwIndicatorProcessor {
         public String getSheetName() {
             return "坝下水位";
         }
    +
    +    @Override
    +    public int getOrder() {
    +        return 3;
    +    }
     }
    diff --git a/backend/src/main/java/com/yfd/platform/qgc_export/processor/DzVariationProcessor.java b/backend/src/main/java/com/yfd/platform/qgc_export/processor/DzVariationProcessor.java
    index f34ce869..da0e26bb 100644
    --- a/backend/src/main/java/com/yfd/platform/qgc_export/processor/DzVariationProcessor.java
    +++ b/backend/src/main/java/com/yfd/platform/qgc_export/processor/DzVariationProcessor.java
    @@ -22,4 +22,9 @@ public class DzVariationProcessor extends HydropwIndicatorProcessor {
         public boolean isVariation() {
             return true;
         }
    +
    +    @Override
    +    public int getOrder() {
    +        return 4;
    +    }
     }
    diff --git a/backend/src/main/java/com/yfd/platform/qgc_export/processor/ExportIndicatorProcessor.java b/backend/src/main/java/com/yfd/platform/qgc_export/processor/ExportIndicatorProcessor.java
    index c24728f9..c182b0c4 100644
    --- a/backend/src/main/java/com/yfd/platform/qgc_export/processor/ExportIndicatorProcessor.java
    +++ b/backend/src/main/java/com/yfd/platform/qgc_export/processor/ExportIndicatorProcessor.java
    @@ -56,6 +56,14 @@ public interface ExportIndicatorProcessor {
             return false;
         }
     
    +    /**
    +     * Sheet 排序号,升序排列。
    +     * 数值越小越靠前,同组指标标准格式应排在日变幅格式之前。
    +     */
    +    default int getOrder() {
    +        return 100;
    +    }
    +
         /**
          * 查询原始数据
          *
    diff --git a/backend/src/main/java/com/yfd/platform/qgc_export/processor/PhProcessor.java b/backend/src/main/java/com/yfd/platform/qgc_export/processor/PhProcessor.java
    index e4c123c2..8442bda4 100644
    --- a/backend/src/main/java/com/yfd/platform/qgc_export/processor/PhProcessor.java
    +++ b/backend/src/main/java/com/yfd/platform/qgc_export/processor/PhProcessor.java
    @@ -24,7 +24,7 @@ public class PhProcessor extends WqIndicatorProcessor {
         }
     
         @Override
    -    public String getSttpType() {
    -        return "WQ";
    +    public int getOrder() {
    +        return 15;
         }
     }
    diff --git a/backend/src/main/java/com/yfd/platform/qgc_export/processor/QProcessor.java b/backend/src/main/java/com/yfd/platform/qgc_export/processor/QProcessor.java
    index 031d7361..0446c615 100644
    --- a/backend/src/main/java/com/yfd/platform/qgc_export/processor/QProcessor.java
    +++ b/backend/src/main/java/com/yfd/platform/qgc_export/processor/QProcessor.java
    @@ -9,17 +9,14 @@ import org.springframework.stereotype.Component;
     public class QProcessor extends RiverIndicatorProcessor {
     
         @Override
    -    public String getFieldName() {
    -        return "Q";
    -    }
    +    public String getFieldName() { return "Q"; }
     
         @Override
    -    public String getSheetName() {
    -        return "流量(m³/s)";
    -    }
    +    public String getSheetName() { return "流量(m³/s)"; }
     
         @Override
    -    public String getSttpType() {
    -        return "ZQ";
    -    }
    +    public String getSttpType() { return "ZQ"; }
    +
    +    @Override
    +    public int getOrder() { return 32; }
     }
    diff --git a/backend/src/main/java/com/yfd/platform/qgc_export/processor/RzProcessor.java b/backend/src/main/java/com/yfd/platform/qgc_export/processor/RzProcessor.java
    index c00dd40f..42e4ccea 100644
    --- a/backend/src/main/java/com/yfd/platform/qgc_export/processor/RzProcessor.java
    +++ b/backend/src/main/java/com/yfd/platform/qgc_export/processor/RzProcessor.java
    @@ -17,4 +17,9 @@ public class RzProcessor extends HydropwIndicatorProcessor {
         public String getSheetName() {
             return "坝上水位";
         }
    +
    +    @Override
    +    public int getOrder() {
    +        return 1;
    +    }
     }
    diff --git a/backend/src/main/java/com/yfd/platform/qgc_export/processor/RzVariationProcessor.java b/backend/src/main/java/com/yfd/platform/qgc_export/processor/RzVariationProcessor.java
    index f7ed04d9..f74d3627 100644
    --- a/backend/src/main/java/com/yfd/platform/qgc_export/processor/RzVariationProcessor.java
    +++ b/backend/src/main/java/com/yfd/platform/qgc_export/processor/RzVariationProcessor.java
    @@ -22,4 +22,9 @@ public class RzVariationProcessor extends HydropwIndicatorProcessor {
         public boolean isVariation() {
             return true;
         }
    +
    +    @Override
    +    public int getOrder() {
    +        return 2;
    +    }
     }
    diff --git a/backend/src/main/java/com/yfd/platform/qgc_export/processor/TuProcessor.java b/backend/src/main/java/com/yfd/platform/qgc_export/processor/TuProcessor.java
    index babef596..36e67231 100644
    --- a/backend/src/main/java/com/yfd/platform/qgc_export/processor/TuProcessor.java
    +++ b/backend/src/main/java/com/yfd/platform/qgc_export/processor/TuProcessor.java
    @@ -24,7 +24,7 @@ public class TuProcessor extends WqIndicatorProcessor {
         }
     
         @Override
    -    public String getSttpType() {
    -        return "WQ";
    +    public int getOrder() {
    +        return 14;
         }
     }
    diff --git a/backend/src/main/java/com/yfd/platform/qgc_export/processor/VProcessor.java b/backend/src/main/java/com/yfd/platform/qgc_export/processor/VProcessor.java
    index 74438880..330c68fb 100644
    --- a/backend/src/main/java/com/yfd/platform/qgc_export/processor/VProcessor.java
    +++ b/backend/src/main/java/com/yfd/platform/qgc_export/processor/VProcessor.java
    @@ -9,17 +9,14 @@ import org.springframework.stereotype.Component;
     public class VProcessor extends RiverIndicatorProcessor {
     
         @Override
    -    public String getFieldName() {
    -        return "V";
    -    }
    +    public String getFieldName() { return "V"; }
     
         @Override
    -    public String getSheetName() {
    -        return "流速(m/s)";
    -    }
    +    public String getSheetName() { return "流速(m/s)"; }
     
         @Override
    -    public String getSttpType() {
    -        return "ZQ";
    -    }
    +    public String getSttpType() { return "ZQ"; }
    +
    +    @Override
    +    public int getOrder() { return 33; }
     }
    diff --git a/backend/src/main/java/com/yfd/platform/qgc_export/processor/WtmpProcessor.java b/backend/src/main/java/com/yfd/platform/qgc_export/processor/WtmpProcessor.java
    index c18a1357..2217aa2d 100644
    --- a/backend/src/main/java/com/yfd/platform/qgc_export/processor/WtmpProcessor.java
    +++ b/backend/src/main/java/com/yfd/platform/qgc_export/processor/WtmpProcessor.java
    @@ -24,8 +24,7 @@ public class WtmpProcessor extends WqIndicatorProcessor {
         }
     
         @Override
    -    public String getSttpType() {
    -        return "WQ";
    +    public int getOrder() {
    +        return 10;
         }
    -
     }
    diff --git a/backend/src/main/java/com/yfd/platform/qgc_export/processor/ZProcessor.java b/backend/src/main/java/com/yfd/platform/qgc_export/processor/ZProcessor.java
    index 32e5e8b3..abf293d8 100644
    --- a/backend/src/main/java/com/yfd/platform/qgc_export/processor/ZProcessor.java
    +++ b/backend/src/main/java/com/yfd/platform/qgc_export/processor/ZProcessor.java
    @@ -9,17 +9,14 @@ import org.springframework.stereotype.Component;
     public class ZProcessor extends RiverIndicatorProcessor {
     
         @Override
    -    public String getFieldName() {
    -        return "Z";
    -    }
    +    public String getFieldName() { return "Z"; }
     
         @Override
    -    public String getSheetName() {
    -        return "水位(m)";
    -    }
    +    public String getSheetName() { return "水位(m)"; }
     
         @Override
    -    public String getSttpType() {
    -        return "ZQ";
    -    }
    +    public String getSttpType() { return "ZQ"; }
    +
    +    @Override
    +    public int getOrder() { return 31; }
     }
    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 e21b57ed..bfd3ff67 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
    @@ -86,6 +86,8 @@ public class QgcExportServiceImpl implements IQgcExportService {
         @Resource
         private WtmpProcessor wtmpProcessor;
         @Resource
    +    private CodcrProcessor codcrProcessor;
    +    @Resource
         private PhProcessor phProcessor;
         @Resource
         private DoxProcessor doxProcessor;
    @@ -288,6 +290,7 @@ public class QgcExportServiceImpl implements IQgcExportService {
             Map processorMap = new LinkedHashMap<>();
             // WQ 在前
             processorMap.put("wtmp", wtmpProcessor);
    +        processorMap.put("codcr", codcrProcessor);
             processorMap.put("ph", phProcessor);
             processorMap.put("dox", doxProcessor);
             processorMap.put("cond", condProcessor);
    @@ -302,7 +305,7 @@ public class QgcExportServiceImpl implements IQgcExportService {
             for (String field : dataFields) {
                 String lower = field.trim().toLowerCase();
                 if ("wq".equals(lower)) {
    -                expanded.addAll(List.of("wtmp", "ph", "dox", "cond", "tu"));
    +                expanded.addAll(List.of("wtmp","codcr", "ph", "dox", "cond", "tu"));
                 } else {
                     expanded.add(lower);
                 }
    @@ -345,6 +348,7 @@ public class QgcExportServiceImpl implements IQgcExportService {
                     result.add(p);
                 }
             }
    +        result.sort(Comparator.comparingInt(ExportIndicatorProcessor::getOrder));
             return result;
         }
     
    
    From c62f3cee5e0dcbe7a42328a3ca76397763e1143e Mon Sep 17 00:00:00 2001
    From: tangwei 
    Date: Wed, 5 Aug 2026 18:13:16 +0800
    Subject: [PATCH 14/28] =?UTF-8?q?fix:=20=E5=A2=9E=E5=8A=A0=E5=87=BA?=
     =?UTF-8?q?=E5=85=A5=E5=BA=93=E6=B5=81=E9=87=8F=E6=95=B0=E6=8D=AE=E7=BB=BC?=
     =?UTF-8?q?=E5=90=88=E5=AF=BC=E5=87=BA?=
    MIME-Version: 1.0
    Content-Type: text/plain; charset=UTF-8
    Content-Transfer-Encoding: 8bit
    
    ---
     .../qgc_export/processor/QiProcessor.java     | 25 +++++++++++++++++++
     .../qgc_export/processor/QoProcessor.java     | 25 +++++++++++++++++++
     .../service/impl/QgcExportServiceImpl.java    | 24 ++++++++++++++++++
     3 files changed, 74 insertions(+)
     create mode 100644 backend/src/main/java/com/yfd/platform/qgc_export/processor/QiProcessor.java
     create mode 100644 backend/src/main/java/com/yfd/platform/qgc_export/processor/QoProcessor.java
    
    diff --git a/backend/src/main/java/com/yfd/platform/qgc_export/processor/QiProcessor.java b/backend/src/main/java/com/yfd/platform/qgc_export/processor/QiProcessor.java
    new file mode 100644
    index 00000000..efeac096
    --- /dev/null
    +++ b/backend/src/main/java/com/yfd/platform/qgc_export/processor/QiProcessor.java
    @@ -0,0 +1,25 @@
    +package com.yfd.platform.qgc_export.processor;
    +
    +import org.springframework.stereotype.Component;
    +
    +/**
    + * 入库流量(QI) 处理器
    + */
    +@Component
    +public class QiProcessor extends HydropwIndicatorProcessor {
    +
    +    @Override
    +    public String getFieldName() {
    +        return "QI";
    +    }
    +
    +    @Override
    +    public String getSheetName() {
    +        return "入库流量";
    +    }
    +
    +    @Override
    +    public int getOrder() {
    +        return 5;
    +    }
    +}
    diff --git a/backend/src/main/java/com/yfd/platform/qgc_export/processor/QoProcessor.java b/backend/src/main/java/com/yfd/platform/qgc_export/processor/QoProcessor.java
    new file mode 100644
    index 00000000..e6481449
    --- /dev/null
    +++ b/backend/src/main/java/com/yfd/platform/qgc_export/processor/QoProcessor.java
    @@ -0,0 +1,25 @@
    +package com.yfd.platform.qgc_export.processor;
    +
    +import org.springframework.stereotype.Component;
    +
    +/**
    + * 出库流量(QO) 处理器
    + */
    +@Component
    +public class QoProcessor extends HydropwIndicatorProcessor {
    +
    +    @Override
    +    public String getFieldName() {
    +        return "QO";
    +    }
    +
    +    @Override
    +    public String getSheetName() {
    +        return "出库流量";
    +    }
    +
    +    @Override
    +    public int getOrder() {
    +        return 6;
    +    }
    +}
    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 bfd3ff67..cd3a3c41 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
    @@ -120,6 +120,12 @@ public class QgcExportServiceImpl implements IQgcExportService {
         @Resource
         private DzVariationProcessor dzVariationProcessor;
     
    +    // QI/QO 出入库流量相关(STTP=ENG)
    +    @Resource
    +    private QiProcessor qiProcessor;
    +    @Resource
    +    private QoProcessor qoProcessor;
    +
         private static final SimpleDateFormat SDF_TIME = new SimpleDateFormat("dd日 HH时");
     
         /** 基础汇总指标(7个) */
    @@ -299,6 +305,9 @@ public class QgcExportServiceImpl implements IQgcExportService {
             processorMap.put("v", vProcessor);
             processorMap.put("q", qProcessor);
             processorMap.put("z", zProcessor);
    +        // 出入库流量(STTP=ENG,仅标准格式无日变幅)
    +        processorMap.put("qi", qiProcessor);
    +        processorMap.put("qo", qoProcessor);
     
             // WQ 别名:展开为全部水质指标
             List expanded = new ArrayList<>();
    @@ -321,6 +330,19 @@ public class QgcExportServiceImpl implements IQgcExportService {
                     result.add(dwtVariationProcessor);
                     continue;
                 }
    +
    +//            // RZ 坝上水位 → 展开为标准格式 + 日变幅两个 Sheet
    +//            if ("qi".equals(field)) {
    +//                qiProcessor.setMappers(sdHydropwRMapper, sdHydropwdaySMapper);
    +//                result.add(qiProcessor);
    +//                continue;
    +//            }
    +//            // DZ 坝下水位 → 展开为标准格式 + 日变幅两个 Sheet
    +//            if ("qo".equals(field)) {
    +//                qoProcessor.setMappers(sdHydropwRMapper, sdHydropwdaySMapper);
    +//                result.add(qoProcessor);
    +//                continue;
    +//            }
                 // RZ 坝上水位 → 展开为标准格式 + 日变幅两个 Sheet
                 if ("rz".equals(field)) {
                     rzProcessor.setMappers(sdHydropwRMapper, sdHydropwdaySMapper);
    @@ -344,6 +366,8 @@ public class QgcExportServiceImpl implements IQgcExportService {
                         ((RiverIndicatorProcessor) p).setMappers(sdRiverRMapper, sdRiverdaySMapper);
                     } else if (p instanceof WqIndicatorProcessor) {
                         ((WqIndicatorProcessor) p).setMappers(sdWqRMapper, sdWqdaySMapper);
    +                } else if (p instanceof HydropwIndicatorProcessor) {
    +                    ((HydropwIndicatorProcessor) p).setMappers(sdHydropwRMapper, sdHydropwdaySMapper);
                     }
                     result.add(p);
                 }
    
    From ac4bf3465dd15d7123afdd09e7e4192d43911656 Mon Sep 17 00:00:00 2001
    From: tangwei 
    Date: Wed, 5 Aug 2026 18:32:02 +0800
    Subject: [PATCH 15/28] =?UTF-8?q?fix:=20=E4=BC=98=E5=8C=96excel=E5=AF=BC?=
     =?UTF-8?q?=E5=87=BA=E6=A0=87=E9=A2=98=E6=A0=B7=E5=BC=8F?=
    MIME-Version: 1.0
    Content-Type: text/plain; charset=UTF-8
    Content-Transfer-Encoding: 8bit
    
    ---
     .../main/java/com/yfd/platform/utils/ExportZipUtil.java   | 8 ++++----
     1 file changed, 4 insertions(+), 4 deletions(-)
    
    diff --git a/backend/src/main/java/com/yfd/platform/utils/ExportZipUtil.java b/backend/src/main/java/com/yfd/platform/utils/ExportZipUtil.java
    index dc6d248d..e0583236 100644
    --- a/backend/src/main/java/com/yfd/platform/utils/ExportZipUtil.java
    +++ b/backend/src/main/java/com/yfd/platform/utils/ExportZipUtil.java
    @@ -466,14 +466,14 @@ public class ExportZipUtil {
         public static CellStyle createHeaderStyle(Workbook workbook) {
             CellStyle style = workbook.createCellStyle();
     
    -        // 背景色:深蓝
    -        style.setFillForegroundColor(IndexedColors.ROYAL_BLUE.getIndex());
    +        // 背景色:浅淡的苍白蓝
    +        style.setFillForegroundColor(IndexedColors.PALE_BLUE.getIndex());
             style.setFillPattern(FillPatternType.SOLID_FOREGROUND);
     
    -        // 字体:白色加粗
    +        // 字体:黑色加粗
             Font font = workbook.createFont();
             font.setBold(true);
    -        font.setColor(IndexedColors.WHITE.getIndex());
    +        font.setColor(IndexedColors.BLACK.getIndex());
             font.setFontHeightInPoints((short) 11);
             style.setFont(font);
     
    
    From 2837e9e28d5042948f62cd4a9cce06fdf4bd986f Mon Sep 17 00:00:00 2001
    From: tangwei 
    Date: Thu, 6 Aug 2026 09:52:48 +0800
    Subject: [PATCH 16/28] =?UTF-8?q?fix:=20=E4=BC=98=E5=8C=96=E7=BB=BC?=
     =?UTF-8?q?=E5=90=88=E5=AF=BC=E5=87=BA=E9=80=BB=E8=BE=91?=
    MIME-Version: 1.0
    Content-Type: text/plain; charset=UTF-8
    Content-Transfer-Encoding: 8bit
    
    ---
     .../platform/qgc_export/domain/SdQecR.java    |  50 ++++
     .../platform/qgc_export/domain/SdQecdayS.java |  50 ++++
     .../qgc_export/domain/StationMapping.java     |   4 +
     .../qgc_export/mapper/SdQecRMapper.java       |  25 ++
     .../qgc_export/mapper/SdQecdaySMapper.java    |  25 ++
     .../qgc_export/processor/QecProcessor.java    |  86 +++++++
     .../service/impl/QgcExportServiceImpl.java    | 229 ++++++++++++++++--
     .../com/yfd/platform/utils/ExportZipUtil.java |  43 +++-
     .../mapper/qgc_export/SdQecRMapper.xml        |  39 +++
     .../mapper/qgc_export/SdQecdaySMapper.xml     |  39 +++
     10 files changed, 571 insertions(+), 19 deletions(-)
     create mode 100644 backend/src/main/java/com/yfd/platform/qgc_export/domain/SdQecR.java
     create mode 100644 backend/src/main/java/com/yfd/platform/qgc_export/domain/SdQecdayS.java
     create mode 100644 backend/src/main/java/com/yfd/platform/qgc_export/mapper/SdQecRMapper.java
     create mode 100644 backend/src/main/java/com/yfd/platform/qgc_export/mapper/SdQecdaySMapper.java
     create mode 100644 backend/src/main/java/com/yfd/platform/qgc_export/processor/QecProcessor.java
     create mode 100644 backend/src/main/resources/mapper/qgc_export/SdQecRMapper.xml
     create mode 100644 backend/src/main/resources/mapper/qgc_export/SdQecdaySMapper.xml
    
    diff --git a/backend/src/main/java/com/yfd/platform/qgc_export/domain/SdQecR.java b/backend/src/main/java/com/yfd/platform/qgc_export/domain/SdQecR.java
    new file mode 100644
    index 00000000..b4cecc24
    --- /dev/null
    +++ b/backend/src/main/java/com/yfd/platform/qgc_export/domain/SdQecR.java
    @@ -0,0 +1,50 @@
    +package com.yfd.platform.qgc_export.domain;
    +
    +import com.baomidou.mybatisplus.annotation.IdType;
    +import com.baomidou.mybatisplus.annotation.TableId;
    +import com.baomidou.mybatisplus.annotation.TableName;
    +import lombok.Data;
    +
    +import java.io.Serializable;
    +import java.math.BigDecimal;
    +import java.util.Date;
    +
    +/**
    + * 生态流量监测数据表(小时级)
    + * 对应 Oracle 表: QGC_REFA.SD_QEC_R
    + */
    +@Data
    +@TableName("SD_QEC_R")
    +public class SdQecR implements Serializable {
    +
    +    private static final long serialVersionUID = 1L;
    +
    +    @TableId(type = IdType.INPUT)
    +    private String id;
    +
    +    /** 水电工程编码 */
    +    private String stcd;
    +
    +    /** 数据识别时间 */
    +    private Date tm;
    +
    +    /** 生态流量 (m³/s) */
    +    private BigDecimal qec;
    +
    +    /** AI识别生态流量值 */
    +    private BigDecimal waterflow;
    +
    +    /** 生态流量泄放方式 */
    +    private String tpqec;
    +
    +    /** 生态流量是否达标 */
    +    private Integer sfdb;
    +
    +    private String recordUser;
    +    private Date recordTime;
    +    private String modifyUser;
    +    private Date modifyTime;
    +    private Integer isDeleted;
    +    private String deleteUser;
    +    private Date deleteTime;
    +}
    diff --git a/backend/src/main/java/com/yfd/platform/qgc_export/domain/SdQecdayS.java b/backend/src/main/java/com/yfd/platform/qgc_export/domain/SdQecdayS.java
    new file mode 100644
    index 00000000..81f88638
    --- /dev/null
    +++ b/backend/src/main/java/com/yfd/platform/qgc_export/domain/SdQecdayS.java
    @@ -0,0 +1,50 @@
    +package com.yfd.platform.qgc_export.domain;
    +
    +import com.baomidou.mybatisplus.annotation.IdType;
    +import com.baomidou.mybatisplus.annotation.TableId;
    +import com.baomidou.mybatisplus.annotation.TableName;
    +import lombok.Data;
    +
    +import java.io.Serializable;
    +import java.math.BigDecimal;
    +import java.util.Date;
    +
    +/**
    + * 生态流量监测数据日统计表
    + * 对应 Oracle 表: QGC_REFA.SD_QECDAY_S
    + */
    +@Data
    +@TableName("SD_QECDAY_S")
    +public class SdQecdayS implements Serializable {
    +
    +    private static final long serialVersionUID = 1L;
    +
    +    @TableId(type = IdType.INPUT)
    +    private String id;
    +
    +    /** 水电工程编码 */
    +    private String stcd;
    +
    +    /** 数据识别时间 */
    +    private Date dt;
    +
    +    /** 生态流量 (m³/s) */
    +    private BigDecimal qec;
    +
    +    /** AI识别生态流量值 */
    +    private BigDecimal waterflow;
    +
    +    /** 生态流量泄放方式 */
    +    private String tpqec;
    +
    +    /** 生态流量是否达标 */
    +    private Integer sfdb;
    +
    +    private String recordUser;
    +    private Date recordTime;
    +    private String modifyUser;
    +    private Date modifyTime;
    +    private Integer isDeleted;
    +    private String deleteUser;
    +    private Date deleteTime;
    +}
    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 c8f58ee6..e360e38d 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
    @@ -31,4 +31,8 @@ public class StationMapping implements Serializable {
         /** 测站名称 */
         @TableField("STNM")
         private String stnm;
    +
    +    /** 所属水电基地名称 */
    +    @TableField("BASE_NAME")
    +    private String baseName;
     }
    diff --git a/backend/src/main/java/com/yfd/platform/qgc_export/mapper/SdQecRMapper.java b/backend/src/main/java/com/yfd/platform/qgc_export/mapper/SdQecRMapper.java
    new file mode 100644
    index 00000000..8f6e1077
    --- /dev/null
    +++ b/backend/src/main/java/com/yfd/platform/qgc_export/mapper/SdQecRMapper.java
    @@ -0,0 +1,25 @@
    +package com.yfd.platform.qgc_export.mapper;
    +
    +import com.baomidou.mybatisplus.core.mapper.BaseMapper;
    +import com.yfd.platform.qgc_export.domain.SdQecR;
    +import org.apache.ibatis.annotations.Param;
    +
    +import java.util.List;
    +import java.util.Map;
    +
    +/**
    + * SD_QEC_R 生态流量小时数据 Mapper
    + */
    +public interface SdQecRMapper extends BaseMapper {
    +
    +    List> selectRawData(@Param("stcdList") List stcdList,
    +                                            @Param("startTime") String startTime,
    +                                            @Param("endTime") String endTime);
    +
    +    /**
    +     * 查询生态流量达标数据(小时级),返回三种 SFDB 字段
    +     */
    +    List> selectComplianceData(@Param("stcdList") List stcdList,
    +                                                   @Param("startTime") String startTime,
    +                                                   @Param("endTime") String endTime);
    +}
    diff --git a/backend/src/main/java/com/yfd/platform/qgc_export/mapper/SdQecdaySMapper.java b/backend/src/main/java/com/yfd/platform/qgc_export/mapper/SdQecdaySMapper.java
    new file mode 100644
    index 00000000..608acf4f
    --- /dev/null
    +++ b/backend/src/main/java/com/yfd/platform/qgc_export/mapper/SdQecdaySMapper.java
    @@ -0,0 +1,25 @@
    +package com.yfd.platform.qgc_export.mapper;
    +
    +import com.baomidou.mybatisplus.core.mapper.BaseMapper;
    +import com.yfd.platform.qgc_export.domain.SdQecdayS;
    +import org.apache.ibatis.annotations.Param;
    +
    +import java.util.List;
    +import java.util.Map;
    +
    +/**
    + * SD_QECDAY_S 生态流量日数据 Mapper
    + */
    +public interface SdQecdaySMapper extends BaseMapper {
    +
    +    List> selectRawData(@Param("stcdList") List stcdList,
    +                                            @Param("startTime") String startTime,
    +                                            @Param("endTime") String endTime);
    +
    +    /**
    +     * 查询生态流量达标数据(日级),返回三种 SFDB 字段
    +     */
    +    List> selectComplianceData(@Param("stcdList") List stcdList,
    +                                                   @Param("startTime") String startTime,
    +                                                   @Param("endTime") String endTime);
    +}
    diff --git a/backend/src/main/java/com/yfd/platform/qgc_export/processor/QecProcessor.java b/backend/src/main/java/com/yfd/platform/qgc_export/processor/QecProcessor.java
    new file mode 100644
    index 00000000..cdf2470f
    --- /dev/null
    +++ b/backend/src/main/java/com/yfd/platform/qgc_export/processor/QecProcessor.java
    @@ -0,0 +1,86 @@
    +package com.yfd.platform.qgc_export.processor;
    +
    +import com.yfd.platform.qgc_export.mapper.SdQecRMapper;
    +import com.yfd.platform.qgc_export.mapper.SdQecdaySMapper;
    +
    +import org.springframework.stereotype.Component;
    +
    +import java.math.BigDecimal;
    +import java.text.SimpleDateFormat;
    +import java.util.*;
    +
    +/**
    + * 生态流量(QEC) 处理器
    + * 

    + * 数据来源: SD_QEC_R / SD_QECDAY_S,STTP="ENG"。 + */ +@Component +public class QecProcessor implements ExportIndicatorProcessor { + + private SdQecRMapper sdQecRMapper; + private SdQecdaySMapper sdQecdaySMapper; + + private static final SimpleDateFormat SDF = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + + public void setMappers(SdQecRMapper rMapper, SdQecdaySMapper sMapper) { + this.sdQecRMapper = rMapper; + this.sdQecdaySMapper = sMapper; + } + + @Override + public String getFieldName() { + return "QEC"; + } + + @Override + public String getSheetName() { + return "生态流量"; + } + + @Override + public String getSttpType() { + return "ENG"; + } + + @Override + public int getDecimalPlaces() { + return 3; + } + + @Override + public int getOrder() { + return 37; + } + + @Override + public List queryRawData(List stcds, Date startTime, Date endTime, boolean isDaily) { + String startStr = SDF.format(startTime); + String endStr = SDF.format(endTime); + + List> rawList; + if (isDaily) { + rawList = sdQecdaySMapper.selectRawData(stcds, startStr, endStr); + } else { + rawList = sdQecRMapper.selectRawData(stcds, startStr, endStr); + } + + List rows = new ArrayList<>(); + if (rawList != null) { + for (Map map : rawList) { + String stcd = (String) map.get("STCD"); + Date tm = (Date) map.get(isDaily ? "DT" : "TM"); + BigDecimal value = toBigDecimal(map.get("VALUE")); + if (stcd != null && tm != null && value != null) { + rows.add(new IndicatorDataRow(stcd, tm, value)); + } + } + } + return rows; + } + + private BigDecimal toBigDecimal(Object val) { + if (val instanceof BigDecimal) return (BigDecimal) val; + if (val instanceof Number) return BigDecimal.valueOf(((Number) val).doubleValue()); + return null; + } +} 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 cd3a3c41..bb1a2e9f 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 @@ -1,5 +1,6 @@ package com.yfd.platform.qgc_export.service.impl; +import cn.hutool.core.util.StrUtil; import com.yfd.platform.qgc_export.domain.StationMapping; import com.yfd.platform.qgc_export.mapper.SdRiverRMapper; import com.yfd.platform.qgc_export.mapper.SdRiverdaySMapper; @@ -9,6 +10,8 @@ import com.yfd.platform.qgc_export.mapper.SdWtrvRMapper; import com.yfd.platform.qgc_export.mapper.SdWtrvdaySMapper; import com.yfd.platform.qgc_export.mapper.SdHydropwRMapper; import com.yfd.platform.qgc_export.mapper.SdHydropwdaySMapper; +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.processor.*; import com.yfd.platform.qgc_export.service.IQgcExportService; @@ -126,7 +129,16 @@ public class QgcExportServiceImpl implements IQgcExportService { @Resource private QoProcessor qoProcessor; + // QEC 生态流量相关(STTP=ENG) + @Resource + private SdQecRMapper sdQecRMapper; + @Resource + private SdQecdaySMapper sdQecdaySMapper; + @Resource + private QecProcessor qecProcessor; + private static final SimpleDateFormat SDF_TIME = new SimpleDateFormat("dd日 HH时"); + private static final SimpleDateFormat SDF = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); /** 基础汇总指标(7个) */ private static final List BASE_LABELS = Collections.unmodifiableList( @@ -161,6 +173,7 @@ public class QgcExportServiceImpl implements IQgcExportService { List stationOrder = new ArrayList<>(stationRes.displayNames.keySet()); // 5. 每月一个 ExcelData + String timestamp = String.valueOf(System.currentTimeMillis()); List excelList = new ArrayList<>(); for (String month : months) { MonthRange mr = buildMonthRange(month); @@ -168,21 +181,53 @@ public class QgcExportServiceImpl implements IQgcExportService { List sheets = new ArrayList<>(); for (ExportIndicatorProcessor processor : processors) { - List> rows = buildSheetRows(processor, mr, stationOrder, stationRes); + List> rows; + // QEC 自定义三栏达标率格式 + if (processor instanceof QecProcessor) { + rows = buildQecSheetRows(mr, stationOrder, stationRes); + } else { + rows = buildSheetRows(processor, mr, stationOrder, stationRes); + } if (rows != null) { - // 按当前处理器 STTP 类型获取对应的测站名称 + List headers; String sttp = processor.getSttpType(); - List headers = new ArrayList<>(); - headers.add("日期"); - for (String rstcd : stationOrder) { - headers.add(stationRes.getDisplayName(rstcd, sttp)); + if (processor instanceof QecProcessor) { + // 分部标题作为列标题(合并单元格) + String[] sectionTitles = {"生态流量达标率(环保部)", "生态流量达标率(水利部)", "生态流量达标率(多年平均)"}; + headers = new ArrayList<>(); + for (String title : sectionTitles) { + headers.add(title); + for (int i = 1; i < 6; i++) headers.add(""); + } + } else { + // 仅包含当前处理器 STTP 类型有对应测站的电站 + headers = new ArrayList<>(); + headers.add("日期"); + for (String rstcd : stationOrder) { + if (stationRes.getStcd(rstcd, sttp) != null) { + String displayName = stationRes.getDisplayName(rstcd, sttp); + if (StrUtil.isNotBlank(displayName)) { + headers.add(displayName); + } + } + } } - sheets.add(new ExportZipUtil.SheetData(processor.getSheetName(), headers, rows)); + ExportZipUtil.SheetData sd = new ExportZipUtil.SheetData(processor.getSheetName(), headers, rows); + if (processor instanceof QecProcessor) { + // 分部标题合并单元格(Row 0 是 headers),子标题行用 subHeaderStyle + sd.setHeaderRowCount(1); + sd.setMergedRegions(Arrays.asList( + new int[]{0, 0, 0, 5}, // 环保部 + new int[]{0, 0, 6, 11}, // 水利部 + new int[]{0, 0, 12, 17} // 多年平均 + )); + } + sheets.add(sd); } } if (!sheets.isEmpty()) { - excelList.add(new ExportZipUtil.ExcelData(month, sheets)); + excelList.add(new ExportZipUtil.ExcelData(month + "统计数据_" + timestamp, sheets)); } } @@ -248,9 +293,9 @@ public class QgcExportServiceImpl implements IQgcExportService { for (int d = 1; d <= daysInMonth; d++) { List row = new ArrayList<>(); row.add(String.format("%02d", d)); - for (String rstcd : stationOrder) { + for (String rstcd : rstcdToStcd.keySet()) { String stcd = rstcdToStcd.get(rstcd); - Map dayMap = stcd != null ? dailyByStcd.get(stcd) : null; + Map dayMap = dailyByStcd.get(stcd); BigDecimal val = dayMap != null ? dayMap.get(d) : null; row.add(formatOrEmpty(val, decimals)); } @@ -262,7 +307,7 @@ public class QgcExportServiceImpl implements IQgcExportService { List row = new ArrayList<>(); row.add(labels.get(i)); - for (String rstcd : stationOrder) { + for (String rstcd : rstcdToStcd.keySet()) { Aggregation agg = aggMap.get(rstcd); switch (i) { case 0: row.add(formatOrEmpty(agg != null ? agg.getDailyMin() : null, decimals)); break; @@ -283,6 +328,133 @@ public class QgcExportServiceImpl implements IQgcExportService { return rows; } + // ======================== QEC 生态流量三栏达标率 Sheet ======================== + + /** + * 构建生态流量三栏达标率 Sheet 的数据行(不含标题行,标题由调用方放入 headers)。 + *

    + * 返回的 rows[0] 是子标题行(电站名称|所属水电基地|...),后续为电站数据行。 + */ + private List> buildQecSheetRows( + MonthRange mr, List stationOrder, StationResolution stationRes) { + + String sttp = "ENG"; + Map 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=ENG 的测站数据"); + return null; + } + + List uniqueStcds = rstcdToStcd.values().stream().distinct().toList(); + + // 查询日达标数据 + 小时达标数据 + String startStr = SDF.format(mr.getStart()); + String endStr = SDF.format(mr.getEnd()); + List> dailyList = sdQecdaySMapper.selectComplianceData(uniqueStcds, startStr, endStr); + List> hourlyList = sdQecRMapper.selectComplianceData(uniqueStcds, startStr, endStr); + + // 按 STCD 分组计算达标率 + Map statsByStcd = new LinkedHashMap<>(); + for (String stcd : uniqueStcds) { + ComplianceStats s = new ComplianceStats(); + List> dayForStcd = dailyList.stream() + .filter(m -> stcd.equals(m.get("STCD"))).toList(); + s.dailyHb = calcComplianceRate(dayForStcd, "SFDB"); + s.dailySl = calcComplianceRate(dayForStcd, "MWR_SFDB"); + s.dailyDn = calcComplianceRate(dayForStcd, "AVQ_SFDB"); + + List> hourForStcd = hourlyList.stream() + .filter(m -> stcd.equals(m.get("STCD"))).toList(); + s.hourlyHb = calcComplianceRate(hourForStcd, "SFDB"); + s.hourlySl = calcComplianceRate(hourForStcd, "MWR_SFDB"); + s.hourlyDn = calcComplianceRate(hourForStcd, "AVQ_SFDB"); + + statsByStcd.put(stcd, s); + } + + // ======== Row 0: 子标题行 ======== + List subHeaders = Arrays.asList("电站名称", "所属水电基地", "生态流量限值", "日达标率", "小时达标率", "备注"); + List subRow = new ArrayList<>(); + subRow.addAll(subHeaders); + subRow.addAll(subHeaders); + subRow.addAll(subHeaders); + + // ======== 数据行 ======== + List> rows = new ArrayList<>(); + rows.add(subRow); // 子标题作为第一行 + + for (String rstcd : stationOrder) { + String stcd = rstcdToStcd.get(rstcd); + String displayName = stationRes.getDisplayName(rstcd, sttp); + String baseName = stationRes.getBaseName(rstcd, sttp); + ComplianceStats s = stcd != null ? statsByStcd.get(stcd) : null; + + List row = new ArrayList<>(); + // 环保部 + row.add(displayName != null ? displayName : rstcd); + row.add(baseName); + row.add(""); // 生态流量限值(暂空) + row.add(s != null ? s.dailyHb : ""); + row.add(s != null ? s.hourlyHb : ""); + row.add(""); // 备注(暂空) + // 水利部 + row.add(displayName != null ? displayName : rstcd); + row.add(baseName); + row.add(""); + row.add(s != null ? s.dailySl : ""); + row.add(s != null ? s.hourlySl : ""); + row.add(""); + // 多年平均 + row.add(displayName != null ? displayName : rstcd); + row.add(baseName); + row.add(""); + row.add(s != null ? s.dailyDn : ""); + row.add(s != null ? s.hourlyDn : ""); + row.add(""); + + rows.add(row); + } + + return rows; + } + + /** + * 计算达标率:SFDB=1 达标, SFDB=0 不达标, SFDB=2/3 不参与计算 + */ + private String calcComplianceRate(List> records, String sfdbField) { + long total = 0; + long compliant = 0; + for (Map m : records) { + Object v = m.get(sfdbField); + if (v == null) continue; + int sfdb = toInt(v); + if (sfdb == 0 || sfdb == 1) { + total++; + if (sfdb == 1) compliant++; + } + } + if (total == 0) return ""; + BigDecimal rate = BigDecimal.valueOf(compliant * 100.0 / total); + return rate.setScale(1, RoundingMode.HALF_UP).toString() + "%"; + } + + private int toInt(Object v) { + if (v instanceof Number) return ((Number) v).intValue(); + try { return Integer.parseInt(v.toString()); } catch (Exception e) { return -1; } + } + + /** 分部达标统计 */ + private static class ComplianceStats { + String dailyHb, dailySl, dailyDn; + String hourlyHb, hourlySl, hourlyDn; + } + // ======================== 指标处理器匹配 ======================== /** @@ -308,6 +480,8 @@ public class QgcExportServiceImpl implements IQgcExportService { // 出入库流量(STTP=ENG,仅标准格式无日变幅) processorMap.put("qi", qiProcessor); processorMap.put("qo", qoProcessor); + // 生态流量(STTP=ENG,仅标准格式无日变幅) + processorMap.put("qec", qecProcessor); // WQ 别名:展开为全部水质指标 List expanded = new ArrayList<>(); @@ -368,6 +542,8 @@ public class QgcExportServiceImpl implements IQgcExportService { ((WqIndicatorProcessor) p).setMappers(sdWqRMapper, sdWqdaySMapper); } else if (p instanceof HydropwIndicatorProcessor) { ((HydropwIndicatorProcessor) p).setMappers(sdHydropwRMapper, sdHydropwdaySMapper); + } else if (p instanceof QecProcessor) { + ((QecProcessor) p).setMappers(sdQecRMapper, sdQecdaySMapper); } result.add(p); } @@ -455,6 +631,16 @@ public class QgcExportServiceImpl implements IQgcExportService { } } result.sttpStcdMap.put(rstcd, sttpMap); + + // STTP → BASE_NAME 映射(所属水电基地) + Map baseNames = new LinkedHashMap<>(); + for (StationMapping m : rows) { + String sttp = m.getSttp(); + if (sttp != null && m.getBaseName() != null && !baseNames.containsKey(sttp)) { + baseNames.put(sttp, m.getBaseName()); + } + } + result.baseNameMap.put(rstcd, baseNames); } log.info("电站映射解析完成,RSTCD数量={}, STTP类型={}", result.displayNames.size(), sttpTypes); @@ -628,9 +814,9 @@ public class QgcExportServiceImpl implements IQgcExportService { for (int d = 1; d <= daysInMonth; d++) { List row = new ArrayList<>(); row.add(String.format("%02d", d)); - for (String rstcd : stationOrder) { + for (String rstcd : rstcdToStcd.keySet()) { String stcd = rstcdToStcd.get(rstcd); - Map dayMap = stcd != null ? variationByStcd.get(stcd) : null; + Map dayMap = variationByStcd.get(stcd); BigDecimal val = dayMap != null ? dayMap.get(d) : null; row.add(formatOrEmpty(val, decimals)); } @@ -641,7 +827,7 @@ public class QgcExportServiceImpl implements IQgcExportService { for (int i = 0; i < VARIATION_LABELS.size(); i++) { List row = new ArrayList<>(); row.add(VARIATION_LABELS.get(i)); - for (String rstcd : stationOrder) { + for (String rstcd : rstcdToStcd.keySet()) { BigDecimal[] agg = variationAgg.get(rstcd); row.add(formatOrEmpty(agg != null ? agg[i] : null, decimals)); } @@ -680,6 +866,8 @@ public class QgcExportServiceImpl implements IQgcExportService { final Map> sttpDisplayNames = new LinkedHashMap<>(); /** STTP → STCD 映射,RSTCD → {STTP → STCD} */ final Map> sttpStcdMap = new LinkedHashMap<>(); + /** 所属水电基地名称,RSTCD → {STTP → BASE_NAME} */ + final Map> baseNameMap = new LinkedHashMap<>(); /** 获取默认测站名称 */ String getDisplayName(String rstcd) { @@ -696,7 +884,8 @@ public class QgcExportServiceImpl implements IQgcExportService { String name = inner.get(sttp); if (name != null) return name; } - return getDisplayName(rstcd); + return null; +// return getDisplayName(rstcd); } /** @@ -707,6 +896,16 @@ public class QgcExportServiceImpl implements IQgcExportService { if (inner == null || inner.isEmpty()) return null; return inner.get(sttp); } + + /** 获取指定 STTP 的水电基地名称 */ + String getBaseName(String rstcd, String sttp) { + Map inner = baseNameMap.get(rstcd); + if (inner != null && sttp != null) { + String name = inner.get(sttp); + if (name != null) return name; + } + return ""; + } } @lombok.Data diff --git a/backend/src/main/java/com/yfd/platform/utils/ExportZipUtil.java b/backend/src/main/java/com/yfd/platform/utils/ExportZipUtil.java index e0583236..6670a6af 100644 --- a/backend/src/main/java/com/yfd/platform/utils/ExportZipUtil.java +++ b/backend/src/main/java/com/yfd/platform/utils/ExportZipUtil.java @@ -75,6 +75,23 @@ public class ExportZipUtil { private List headers; /** 数据行集合(每行是一个 Object 列表,与 headers 一一对应) */ private List> rows; + /** + * 需要合并的区域列表,每项 int[4] = {firstRow, lastRow, firstCol, lastCol}(0-based)。 + * 在数据写入后由 fillSheet 统一合并。 + */ + private List mergedRegions; + /** + * rows 中前 headerRowCount 行使用双层标题样式: + * 第 0 行用 headerStyle,第 1 行用 subHeaderStyle,以此类推。 + * 默认 0 表示全部行使用 dataStyle。 + */ + private int headerRowCount; + + public SheetData(String sheetName, List headers, List> rows) { + this.sheetName = sheetName; + this.headers = headers; + this.rows = rows; + } } /** @@ -438,23 +455,41 @@ public class ExportZipUtil { // ---- 写数据行 ---- if (rows != null) { - for (List rowData : rows) { + CellStyle subHeaderStyle = createSubHeaderStyle(workbook); + for (int r = 0; r < rows.size(); r++) { + List rowData = rows.get(r); Row dataRow = sheet.createRow(currentRow++); + int hc = sheetData.getHeaderRowCount(); + CellStyle rowStyle; + if (r < hc) { + // 多层标题:第0行=headerStyle, 第1行=subHeaderStyle, 后续=subHeaderStyle + rowStyle = r == 0 ? headerStyle : subHeaderStyle; + } else { + rowStyle = dataStyle; + } for (int i = 0; i < Math.min(rowData.size(), colCount); i++) { Cell cell = dataRow.createCell(i); Object value = rowData.get(i); setCellValue(cell, value); - cell.setCellStyle(dataStyle); + cell.setCellStyle(rowStyle); } } } + // ---- 合并单元格 ---- + if (sheetData.getMergedRegions() != null) { + for (int[] region : sheetData.getMergedRegions()) { + sheet.addMergedRegion(new CellRangeAddress(region[0], region[1], region[2], region[3])); + } + } + // ---- 自动调整列宽 ---- autoSizeColumns(sheet, colCount); - // 冻结首行(表头固定) + // 冻结表头行 if (currentRow > 1) { - sheet.createFreezePane(0, 1); + int freezeRows = 1 + sheetData.getHeaderRowCount(); + sheet.createFreezePane(0, Math.min(freezeRows, currentRow - 1)); } } diff --git a/backend/src/main/resources/mapper/qgc_export/SdQecRMapper.xml b/backend/src/main/resources/mapper/qgc_export/SdQecRMapper.xml new file mode 100644 index 00000000..90df1d34 --- /dev/null +++ b/backend/src/main/resources/mapper/qgc_export/SdQecRMapper.xml @@ -0,0 +1,39 @@ + + + + + + + + + + diff --git a/backend/src/main/resources/mapper/qgc_export/SdQecdaySMapper.xml b/backend/src/main/resources/mapper/qgc_export/SdQecdaySMapper.xml new file mode 100644 index 00000000..1d7da108 --- /dev/null +++ b/backend/src/main/resources/mapper/qgc_export/SdQecdaySMapper.xml @@ -0,0 +1,39 @@ + + + + + + + + + + From 73ae21a62b449c6e8a614ea36826b5e8f15bd852 Mon Sep 17 00:00:00 2001 From: tangwei Date: Thu, 6 Aug 2026 10:42:58 +0800 Subject: [PATCH 17/28] =?UTF-8?q?fix:=20=E6=96=B0=E5=A2=9E=E7=BB=BC?= =?UTF-8?q?=E5=90=88=E5=AF=BC=E5=87=BA=E6=B0=B4=E8=B4=A8=E8=BE=BE=E6=A0=87?= =?UTF-8?q?=E7=8E=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../qgc_export/mapper/SdWqRMapper.java | 14 ++ .../qgc_export/mapper/SdWqdaySMapper.java | 7 + .../service/impl/QgcExportServiceImpl.java | 149 ++++++++++++++++++ .../mapper/qgc_export/SdWqRMapper.xml | 35 ++++ .../mapper/qgc_export/SdWqdaySMapper.xml | 18 +++ 5 files changed, 223 insertions(+) diff --git a/backend/src/main/java/com/yfd/platform/qgc_export/mapper/SdWqRMapper.java b/backend/src/main/java/com/yfd/platform/qgc_export/mapper/SdWqRMapper.java index 12f6f401..aa9cb6b2 100644 --- a/backend/src/main/java/com/yfd/platform/qgc_export/mapper/SdWqRMapper.java +++ b/backend/src/main/java/com/yfd/platform/qgc_export/mapper/SdWqRMapper.java @@ -25,4 +25,18 @@ public interface SdWqRMapper extends BaseMapper { @Param("fieldName") String fieldName, @Param("startTime") String startTime, @Param("endTime") String endTime); + + /** + * 查询水质达标数据(小时级),按 STCD 聚合 SFDB=1 达标数、SFDB IN (0,1) 总数 + */ + List> selectComplianceData(@Param("stcdList") List stcdList, + @Param("startTime") String startTime, + @Param("endTime") String endTime); + + /** + * 查询各测站不达标要素(SFDB=0 时的 WQ_SFDBHN_YS) + */ + List> selectPollutants(@Param("stcdList") List stcdList, + @Param("startTime") String startTime, + @Param("endTime") String endTime); } diff --git a/backend/src/main/java/com/yfd/platform/qgc_export/mapper/SdWqdaySMapper.java b/backend/src/main/java/com/yfd/platform/qgc_export/mapper/SdWqdaySMapper.java index 2a2dd55e..ea974884 100644 --- a/backend/src/main/java/com/yfd/platform/qgc_export/mapper/SdWqdaySMapper.java +++ b/backend/src/main/java/com/yfd/platform/qgc_export/mapper/SdWqdaySMapper.java @@ -25,4 +25,11 @@ public interface SdWqdaySMapper extends BaseMapper { @Param("fieldName") String fieldName, @Param("startTime") String startTime, @Param("endTime") String endTime); + + /** + * 查询水质达标数据(日级),按 STCD 聚合 SFDB=1 达标数、SFDB IN (0,1) 总数 + */ + List> selectComplianceData(@Param("stcdList") List stcdList, + @Param("startTime") String startTime, + @Param("endTime") String endTime); } 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 bb1a2e9f..bffd473d 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 @@ -180,6 +180,8 @@ public class QgcExportServiceImpl implements IQgcExportService { if (mr == null) continue; List sheets = new ArrayList<>(); + // 判断是否包含水质指标,后续用于追加 WQ 合规 Sheet + boolean hasWqIndicator = false; for (ExportIndicatorProcessor processor : processors) { List> rows; // QEC 自定义三栏达标率格式 @@ -223,6 +225,33 @@ public class QgcExportServiceImpl implements IQgcExportService { )); } sheets.add(sd); + if ("WQ".equals(sttp)) { + hasWqIndicator = true; + } + } + } + + // 若包含水质指标,追加 WQ 合规 Sheet + if (hasWqIndicator) { + List> complianceRows = buildWqComplianceSheet(mr, stationOrder, stationRes); + if (complianceRows != null) { + List complianceHeaders = new ArrayList<>(); + complianceHeaders.add("水质站"); + complianceHeaders.add("小时"); + complianceHeaders.add(""); + complianceHeaders.add(""); + complianceHeaders.add("日均"); + complianceHeaders.add(""); + complianceHeaders.add(""); + complianceHeaders.add("主要污染物"); + ExportZipUtil.SheetData sd = new ExportZipUtil.SheetData("水质达标率", complianceHeaders, complianceRows); + sd.setHeaderRowCount(1); + sd.setMergedRegions(Arrays.asList( + new int[]{0, 1, 0, 0}, // "水质站" 合并 row 0-1 + new int[]{0, 0, 1, 3}, // "小时" 合并 col 1-3 + new int[]{0, 0, 4, 6} // "日均" 合并 col 4-6 + )); + sheets.add(sd); } } @@ -449,6 +478,126 @@ public class QgcExportServiceImpl implements IQgcExportService { try { return Integer.parseInt(v.toString()); } catch (Exception e) { return -1; } } + // ======================== WQ 水质达标率 Sheet(双表头) ======================== + + /** + * 构建水质达标率 Sheet 的数据行(不含标题行,标题由调用方放入 headers)。 + *

    + * 返回的 rows[0] 是子标题行(|达标数|总数|达标率(%)|达标数|总数|达标率(%)|),后续为电站数据行。 + */ + private List> buildWqComplianceSheet( + MonthRange mr, List stationOrder, StationResolution stationRes) { + + String sttp = "WQ"; + Map 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=WQ 的测站数据"); + return null; + } + + List uniqueStcds = rstcdToStcd.values().stream().distinct().toList(); + String startStr = SDF.format(mr.getStart()); + String endStr = SDF.format(mr.getEnd()); + + // 查询小时达标数据 + List> hourlyData = sdWqRMapper.selectComplianceData(uniqueStcds, startStr, endStr); + // 查询日均达标数据 + List> dailyData = sdWqdaySMapper.selectComplianceData(uniqueStcds, startStr, endStr); + // 查询主要污染物 + List> pollutantData = sdWqRMapper.selectPollutants(uniqueStcds, startStr, endStr); + + // 按 STCD 索引达标统计 + Map> hourlyMap = new LinkedHashMap<>(); + for (Map m : hourlyData) { + hourlyMap.put((String) m.get("STCD"), m); + } + Map> dailyMap = new LinkedHashMap<>(); + for (Map m : dailyData) { + dailyMap.put((String) m.get("STCD"), m); + } + // 按 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()) { + pollutantSetMap.computeIfAbsent(stcd, k -> new LinkedHashSet<>()).add(pol); + } + } + + // ======== Row 0: 子标题行 ======== + List subRow = new ArrayList<>(); + subRow.add(""); + subRow.add("达标数"); + subRow.add("总数"); + subRow.add("达标率(%)"); + subRow.add("达标数"); + subRow.add("总数"); + subRow.add("达标率(%)"); + subRow.add(""); + + List> rows = new ArrayList<>(); + rows.add(subRow); + + // ======== 数据行 ======== + for (String rstcd : stationOrder) { + String stcd = rstcdToStcd.get(rstcd); + if (stcd == null) continue; + + String displayName = stationRes.getDisplayName(rstcd, sttp); + Map hStat = hourlyMap.get(stcd); + Map dStat = dailyMap.get(stcd); + + List row = new ArrayList<>(); + row.add(displayName != null ? displayName : rstcd); + + // 小时达标 + appendComplianceCell(row, hStat); + // 日均达标 + appendComplianceCell(row, dStat); + + // 主要污染物 + Set pollutants = pollutantSetMap.get(stcd); + row.add(pollutants != null && !pollutants.isEmpty() + ? String.join(",", pollutants) : ""); + + rows.add(row); + } + + return rows; + } + + /** + * 将达标统计 Map 的达标数/总数/达标率追加到 row + */ + private void appendComplianceCell(List row, Map stat) { + if (stat == null) { + row.add(""); + row.add(""); + row.add(""); + return; + } + Object compliantObj = stat.get("COMPLIANT_CNT"); + Object totalObj = stat.get("TOTAL_CNT"); + long compliant = compliantObj instanceof Number ? ((Number) compliantObj).longValue() : 0; + long total = totalObj instanceof Number ? ((Number) totalObj).longValue() : 0; + + row.add(compliant); + row.add(total); + if (total > 0) { + BigDecimal rate = BigDecimal.valueOf(compliant * 100.0 / total); + row.add(rate.setScale(2, RoundingMode.HALF_UP).toPlainString()); + } else { + row.add(""); + } + } + /** 分部达标统计 */ private static class ComplianceStats { String dailyHb, dailySl, dailyDn; diff --git a/backend/src/main/resources/mapper/qgc_export/SdWqRMapper.xml b/backend/src/main/resources/mapper/qgc_export/SdWqRMapper.xml index 3026c9fb..987b757e 100644 --- a/backend/src/main/resources/mapper/qgc_export/SdWqRMapper.xml +++ b/backend/src/main/resources/mapper/qgc_export/SdWqRMapper.xml @@ -24,4 +24,39 @@ AND ${fieldName} IS NOT NULL + + + + + + diff --git a/backend/src/main/resources/mapper/qgc_export/SdWqdaySMapper.xml b/backend/src/main/resources/mapper/qgc_export/SdWqdaySMapper.xml index 4541f84e..3fbd4aa9 100644 --- a/backend/src/main/resources/mapper/qgc_export/SdWqdaySMapper.xml +++ b/backend/src/main/resources/mapper/qgc_export/SdWqdaySMapper.xml @@ -23,4 +23,22 @@ AND ${fieldName} IS NOT NULL + + + From 94fde50aade07f6e192f01aa633366afd5256f5f Mon Sep 17 00:00:00 2001 From: tangwei Date: Thu, 6 Aug 2026 13:46:22 +0800 Subject: [PATCH 18/28] =?UTF-8?q?fix:=20=E4=BC=98=E5=8C=96=E7=BB=BC?= =?UTF-8?q?=E5=90=88=E5=AF=BC=E5=87=BA=E9=99=90=E5=80=BC=E5=92=8C=E5=A4=87?= =?UTF-8?q?=E6=B3=A8=E8=BF=94=E5=9B=9E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../yfd/platform/config/SecurityConfig.java | 2 +- .../qgc_eng/eq/service/EngEqDataService.java | 21 +++ .../eq/service/impl/EngEqDataServiceImpl.java | 140 ++++++++++++++++++ .../service/impl/QgcExportServiceImpl.java | 21 ++- 4 files changed, 177 insertions(+), 7 deletions(-) diff --git a/backend/src/main/java/com/yfd/platform/config/SecurityConfig.java b/backend/src/main/java/com/yfd/platform/config/SecurityConfig.java index f8e9fd41..cc93de85 100644 --- a/backend/src/main/java/com/yfd/platform/config/SecurityConfig.java +++ b/backend/src/main/java/com/yfd/platform/config/SecurityConfig.java @@ -71,7 +71,7 @@ public class SecurityConfig { // .requestMatchers("/system/**").permitAll() .requestMatchers("/qgcExport/**").permitAll() // .requestMatchers("/eng/**").permitAll() -// .requestMatchers("/eq/**").permitAll() + .requestMatchers("/eq/**").permitAll() // .requestMatchers("/env/**").permitAll() // .requestMatchers("/warn/**").permitAll() // .requestMatchers("/threedroamb/**").permitAll() diff --git a/backend/src/main/java/com/yfd/platform/qgc_eng/eq/service/EngEqDataService.java b/backend/src/main/java/com/yfd/platform/qgc_eng/eq/service/EngEqDataService.java index 260c2bba..6dbcc43f 100644 --- a/backend/src/main/java/com/yfd/platform/qgc_eng/eq/service/EngEqDataService.java +++ b/backend/src/main/java/com/yfd/platform/qgc_eng/eq/service/EngEqDataService.java @@ -24,6 +24,7 @@ import com.yfd.platform.qgc_eng.eq.entity.vo.StcdInfoVo; import com.yfd.platform.qgc_eng.eq.entity.vo.WbsbVo; import java.util.List; +import java.util.Map; public interface EngEqDataService { @@ -53,6 +54,26 @@ public interface EngEqDataService { EngEqRuleVo getEngLimit(DataSourceRequest dataSourceRequest); + /** + * 根据测站编码和时间范围查询生态流量限值及来源 + * + * @param stcd 测站编码 + * @param startTime 开始时间 yyyy-MM-dd HH:mm:ss + * @param endTime 结束时间 yyyy-MM-dd HH:mm:ss + * @return 生态流量限值信息(含环保部/水利部限值及来源、多年平均流量及来源) + */ + EngEqRuleVo getEngLimitByTime(String stcd, String startTime, String endTime); + + /** + * 批量查询生态流量限值及来源 + * + * @param stcdList 测站编码列表 + * @param startTime 开始时间 yyyy-MM-dd HH:mm:ss + * @param endTime 结束时间 yyyy-MM-dd HH:mm:ss + * @return STCD → 生态流量限值信息 + */ + Map getEngLimitByTimeBatch(List stcdList, String startTime, String endTime); + QgcQecStaticVo getQgcStaticData(DataSourceRequest dataSourceRequest, String type); DataSourceResult getWbsbList(DataSourceRequest dataSourceRequest); diff --git a/backend/src/main/java/com/yfd/platform/qgc_eng/eq/service/impl/EngEqDataServiceImpl.java b/backend/src/main/java/com/yfd/platform/qgc_eng/eq/service/impl/EngEqDataServiceImpl.java index c0a8d4e5..6f7d0543 100644 --- a/backend/src/main/java/com/yfd/platform/qgc_eng/eq/service/impl/EngEqDataServiceImpl.java +++ b/backend/src/main/java/com/yfd/platform/qgc_eng/eq/service/impl/EngEqDataServiceImpl.java @@ -47,9 +47,11 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.LinkedHashMap; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Set; import java.util.Date; import java.math.BigDecimal; import java.math.RoundingMode; @@ -57,6 +59,7 @@ import java.time.LocalDate; import java.time.LocalDateTime; import java.time.ZoneId; import java.time.format.DateTimeFormatter; +import java.util.stream.Collectors; @Service public class EngEqDataServiceImpl implements EngEqDataService { @@ -978,6 +981,143 @@ public class EngEqDataServiceImpl implements EngEqDataService { return vo; } + @Override + public EngEqRuleVo getEngLimitByTime(String stcd, String startTime, String endTime) { + EngEqRuleVo vo = new EngEqRuleVo().setStcd(stcd); + + // 1. 查询电站信息(多年平均流量) + Map stationParamMap = new HashMap<>(); + stationParamMap.put("stcd", stcd); + List> stationList = microservicDynamicSQLMapper.pageAllList( + null, + "SELECT STCD, ENNM AS STNM, AVQ, AVQ_SRC FROM SD_ENGINFO_B_H " + + "WHERE STCD = #{map.stcd} AND NVL(IS_DELETED, 0) = 0", + stationParamMap + ); + if (CollUtil.isNotEmpty(stationList)) { + Map station = stationList.get(0); + vo.setStnm(asString(station.get("STNM"))); + BigDecimal avq = toBigDecimal(station.get("AVQ")); + if (avq != null) { + vo.setAvqStr(stripTrailingZeros(avq)); + } + String avqSrc = asString(station.get("AVQ_SRC")); + if (StrUtil.isNotBlank(avqSrc)) { + vo.setAvqVlsr(avqSrc); + } + } + + // 2. 查询环保部限值规则 + List> eqmnList = queryEngLimitRuleDetails(stcd, startTime, endTime, "EQMN"); + vo.setEqmnStr(buildEngLimitValue(eqmnList)); + vo.setVlsr(buildEngLimitVlsr(eqmnList)); + + // 3. 查询水利部限值规则 + List> eqmnMwrList = queryEngLimitRuleDetails(stcd, startTime, endTime, "EQMNMWR"); + vo.setEqmnmwrStr(buildEngLimitValue(eqmnMwrList)); + vo.setMwrVlsr(buildEngLimitVlsr(eqmnMwrList)); + + return vo; + } + + @Override + public Map getEngLimitByTimeBatch(List stcdList, String startTime, String endTime) { + Map result = new LinkedHashMap<>(); + if (CollUtil.isEmpty(stcdList)) { + return result; + } + + // 把列表转为 ('A','B','C') 格式的字符串 + String stcdStr = stcdList.stream() + .map(s -> "'" + s + "'") // 手动加单引号 + .collect(Collectors.joining(",")); + + String sql = "SELECT STCD, ENNM AS STNM, AVQ, AVQ_SRC FROM SD_ENGINFO_B_H " + + "WHERE STCD IN (" + stcdStr + ") " + + "AND NVL(IS_DELETED, 0) = 0"; + +// 此时 paramMap 甚至可以传空,因为值已经写死在 SQL 里了 + List> stationList = microservicDynamicSQLMapper.pageAllList(null, sql, new HashMap<>()); + Map> stationMap = new LinkedHashMap<>(); + if (CollUtil.isNotEmpty(stationList)) { + for (Map row : stationList) { + stationMap.put(asString(row.get("STCD")), row); + } + } + + // 2. 逐个查询限值规则并组装结果 + for (String stcd : stcdList) { + EngEqRuleVo vo = new EngEqRuleVo().setStcd(stcd); + + Map station = stationMap.get(stcd); + if (station != null) { + vo.setStnm(asString(station.get("STNM"))); + BigDecimal avq = toBigDecimal(station.get("AVQ")); + if (avq != null) { + vo.setAvqStr(stripTrailingZeros(avq)); + } + String avqSrc = asString(station.get("AVQ_SRC")); + if (StrUtil.isNotBlank(avqSrc)) { + vo.setAvqVlsr(avqSrc); + } + } + + List> eqmnList = queryEngLimitRuleDetails(stcd, startTime, endTime, "EQMN"); + vo.setEqmnStr(buildEngLimitValue(eqmnList)); + vo.setVlsr(buildEngLimitVlsr(eqmnList)); + + List> eqmnMwrList = queryEngLimitRuleDetails(stcd, startTime, endTime, "EQMNMWR"); + vo.setEqmnmwrStr(buildEngLimitValue(eqmnMwrList)); + vo.setMwrVlsr(buildEngLimitVlsr(eqmnMwrList)); + + result.put(stcd, vo); + } + return result; + } + + /** + * 构建生态流量限值展示(不含前缀,仅时间段和限值) + */ + private String buildEngLimitValue(List> detailList) { + if (CollUtil.isEmpty(detailList)) { + return "无限值要求"; + } + List parts = new ArrayList<>(); + for (Map row : detailList) { + String beginTime = asString(row.get("BEGIN_TIME")); + String endTime = asString(row.get("END_TIME")); + BigDecimal minVal = toBigDecimal(row.get("MIN_VAL")); + if (StrUtil.length(beginTime) != 4 || StrUtil.length(endTime) != 4 || minVal == null) { + continue; + } + parts.add(String.format("%s月%s日~%s月%s日限值为:%s", + beginTime.substring(0, 2), beginTime.substring(2, 4), + endTime.substring(0, 2), endTime.substring(2, 4), + stripTrailingZeros(minVal))); + } + if (parts.isEmpty()) { + return "无限值要求"; + } + return String.join(";", parts); + } + + /** + * 提取限值来源(VLSR),去重后用逗号拼接 + */ + private String buildEngLimitVlsr(List> detailList) { + if (CollUtil.isEmpty(detailList)) { + return ""; + } + Set vlsrSet = new LinkedHashSet<>(); + for (Map row : detailList) { + String vlsr = asString(row.get("VLSR")); + if (StrUtil.isNotBlank(vlsr)) { + vlsrSet.add(vlsr); + } + } + return vlsrSet.isEmpty() ? "" : String.join(",", vlsrSet); + } + @Override public DataSourceResult getFidKendoListCust(DataSourceRequest dataSourceRequest) { DataSourceLoadOptionsBase loadOptions = dataSourceRequest == null ? null : dataSourceRequest.toDevRequest(); 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 bffd473d..9fb70f3e 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 @@ -15,6 +15,8 @@ import com.yfd.platform.qgc_export.mapper.SdQecdaySMapper; import com.yfd.platform.qgc_export.mapper.StationMappingMapper; import com.yfd.platform.qgc_export.processor.*; import com.yfd.platform.qgc_export.service.IQgcExportService; +import com.yfd.platform.qgc_eng.eq.entity.vo.EngEqRuleVo; +import com.yfd.platform.qgc_eng.eq.service.EngEqDataService; import com.yfd.platform.utils.ExportZipUtil; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; @@ -137,6 +139,9 @@ public class QgcExportServiceImpl implements IQgcExportService { @Resource private QecProcessor qecProcessor; + @Resource + private EngEqDataService engEqDataService; + private static final SimpleDateFormat SDF_TIME = new SimpleDateFormat("dd日 HH时"); private static final SimpleDateFormat SDF = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); @@ -418,34 +423,38 @@ public class QgcExportServiceImpl implements IQgcExportService { List> rows = new ArrayList<>(); rows.add(subRow); // 子标题作为第一行 + // 批量查询各测站的生态流量限值 + Map limitMap = engEqDataService.getEngLimitByTimeBatch(uniqueStcds, startStr, endStr); + for (String rstcd : stationOrder) { String stcd = rstcdToStcd.get(rstcd); String displayName = stationRes.getDisplayName(rstcd, sttp); String baseName = stationRes.getBaseName(rstcd, sttp); ComplianceStats s = stcd != null ? statsByStcd.get(stcd) : null; + EngEqRuleVo rule = stcd != null ? limitMap.get(stcd) : null; List row = new ArrayList<>(); // 环保部 row.add(displayName != null ? displayName : rstcd); row.add(baseName); - row.add(""); // 生态流量限值(暂空) + row.add(rule != null && rule.getEqmnStr() != null ? rule.getEqmnStr() : ""); row.add(s != null ? s.dailyHb : ""); row.add(s != null ? s.hourlyHb : ""); - row.add(""); // 备注(暂空) + row.add(rule != null && rule.getVlsr() != null ? rule.getVlsr() : ""); // 水利部 row.add(displayName != null ? displayName : rstcd); row.add(baseName); - row.add(""); + row.add(rule != null && rule.getEqmnmwrStr() != null ? rule.getEqmnmwrStr() : ""); row.add(s != null ? s.dailySl : ""); row.add(s != null ? s.hourlySl : ""); - row.add(""); + row.add(rule != null && rule.getMwrVlsr() != null ? rule.getMwrVlsr() : ""); // 多年平均 row.add(displayName != null ? displayName : rstcd); row.add(baseName); - row.add(""); + row.add(rule != null && rule.getAvqStr() != null ? rule.getAvqStr() : ""); row.add(s != null ? s.dailyDn : ""); row.add(s != null ? s.hourlyDn : ""); - row.add(""); + row.add(rule != null && rule.getAvqVlsr() != null ? rule.getAvqVlsr() : ""); rows.add(row); } From 062590a0320c631a1a0d5ac921d132388785db59 Mon Sep 17 00:00:00 2001 From: tangwei Date: Thu, 6 Aug 2026 16:08:01 +0800 Subject: [PATCH 19/28] =?UTF-8?q?fix:=20=E4=BC=98=E5=8C=96=E6=B0=B4?= =?UTF-8?q?=E8=B4=A8=E8=BE=BE=E6=A0=87=E7=8E=87=E7=9A=84=E6=B1=A1=E6=9F=93?= =?UTF-8?q?=E7=89=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../qgc_export/mapper/SdWqRMapper.java | 5 +++ .../service/impl/QgcExportServiceImpl.java | 39 ++++++++++++++----- .../mapper/qgc_export/SdWqRMapper.xml | 9 +++++ 3 files changed, 43 insertions(+), 10 deletions(-) diff --git a/backend/src/main/java/com/yfd/platform/qgc_export/mapper/SdWqRMapper.java b/backend/src/main/java/com/yfd/platform/qgc_export/mapper/SdWqRMapper.java index aa9cb6b2..433c6e62 100644 --- a/backend/src/main/java/com/yfd/platform/qgc_export/mapper/SdWqRMapper.java +++ b/backend/src/main/java/com/yfd/platform/qgc_export/mapper/SdWqRMapper.java @@ -39,4 +39,9 @@ public interface SdWqRMapper extends BaseMapper { List> selectPollutants(@Param("stcdList") List stcdList, @Param("startTime") String startTime, @Param("endTime") String endTime); + + /** + * 查询 SD_WQ_R 表的字段注释(字段名 → 中文描述) + */ + List> selectColumnComments(); } 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 9fb70f3e..05089b7e 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 @@ -518,9 +518,37 @@ public class QgcExportServiceImpl implements IQgcExportService { List> hourlyData = sdWqRMapper.selectComplianceData(uniqueStcds, startStr, endStr); // 查询日均达标数据 List> dailyData = sdWqdaySMapper.selectComplianceData(uniqueStcds, startStr, endStr); - // 查询主要污染物 + // 查询主要污染物(从小时表 WQ_SFDBHN_YS 获取) List> pollutantData = sdWqRMapper.selectPollutants(uniqueStcds, startStr, endStr); + // 查询要素中文名映射(通过 ST_TB_YS_B.YS_SHOW_NAME,COLUMN_NAME → 显示名) + 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); + } + } + } + } + // 按 STCD 索引达标统计 Map> hourlyMap = new LinkedHashMap<>(); for (Map m : hourlyData) { @@ -530,15 +558,6 @@ public class QgcExportServiceImpl implements IQgcExportService { for (Map m : dailyData) { dailyMap.put((String) m.get("STCD"), m); } - // 按 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()) { - pollutantSetMap.computeIfAbsent(stcd, k -> new LinkedHashSet<>()).add(pol); - } - } // ======== Row 0: 子标题行 ======== List subRow = new ArrayList<>(); diff --git a/backend/src/main/resources/mapper/qgc_export/SdWqRMapper.xml b/backend/src/main/resources/mapper/qgc_export/SdWqRMapper.xml index 987b757e..c19016a0 100644 --- a/backend/src/main/resources/mapper/qgc_export/SdWqRMapper.xml +++ b/backend/src/main/resources/mapper/qgc_export/SdWqRMapper.xml @@ -59,4 +59,13 @@ AND TM < TO_DATE(#{endTime}, 'YYYY-MM-DD HH24:MI:SS') + + + From a4f0d34776002c2267915e02f6f3116e279c40a9 Mon Sep 17 00:00:00 2001 From: tangwei Date: Thu, 6 Aug 2026 17:11:09 +0800 Subject: [PATCH 20/28] =?UTF-8?q?fix:=20=E4=BC=98=E5=8C=96sheet=E7=94=B5?= =?UTF-8?q?=E7=AB=99=E6=8E=92=E5=BA=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../qgc_export/mapper/MsAlongdetBMapper.java | 25 +++++++++ .../service/impl/QgcExportServiceImpl.java | 53 +++++++++++++++++-- .../mapper/qgc_export/MsAlongdetBMapper.xml | 42 +++++++++++++++ 3 files changed, 115 insertions(+), 5 deletions(-) create mode 100644 backend/src/main/java/com/yfd/platform/qgc_export/mapper/MsAlongdetBMapper.java create mode 100644 backend/src/main/resources/mapper/qgc_export/MsAlongdetBMapper.xml diff --git a/backend/src/main/java/com/yfd/platform/qgc_export/mapper/MsAlongdetBMapper.java b/backend/src/main/java/com/yfd/platform/qgc_export/mapper/MsAlongdetBMapper.java new file mode 100644 index 00000000..1d39f300 --- /dev/null +++ b/backend/src/main/java/com/yfd/platform/qgc_export/mapper/MsAlongdetBMapper.java @@ -0,0 +1,25 @@ +package com.yfd.platform.qgc_export.mapper; + +import org.apache.ibatis.annotations.Param; + +import java.util.List; +import java.util.Map; + +/** + * MS_ALONGDET_B 沿程配置详情表 Mapper + */ +public interface MsAlongdetBMapper { + + /** + * 查询指定 STTP 类型的出库沿程配置(IO_DIRECTION=1),按 SORT 排序。 + */ + List> selectAlongStations(@Param("sttp") String sttp, + @Param("rstcdList") List rstcdList); + + /** + * 查询所有沿程配置的电站排序(MS_ALONG_B JOIN MS_ALONGDET_B), + * 按 MS_ALONG_B.ORDER_INDEX, MS_ALONGDET_B.SORT 排序。 + * 返回 RSTCD, ORDER_INDEX。 + */ + List> selectAlongSortOrder(@Param("rstcdList") List rstcdList); +} 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 05089b7e..637a7fbf 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 @@ -13,6 +13,7 @@ import com.yfd.platform.qgc_export.mapper.SdHydropwdaySMapper; 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.processor.*; import com.yfd.platform.qgc_export.service.IQgcExportService; import com.yfd.platform.qgc_eng.eq.entity.vo.EngEqRuleVo; @@ -142,6 +143,9 @@ public class QgcExportServiceImpl implements IQgcExportService { @Resource private EngEqDataService engEqDataService; + @Resource + private MsAlongdetBMapper msAlongdetBMapper; + private static final SimpleDateFormat SDF_TIME = new SimpleDateFormat("dd日 HH时"); private static final SimpleDateFormat SDF = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); @@ -174,10 +178,8 @@ public class QgcExportServiceImpl implements IQgcExportService { return; } - // 4. 电站顺序 - List stationOrder = new ArrayList<>(stationRes.displayNames.keySet()); - - // 5. 每月一个 ExcelData + // 4. 电站顺序:优先按沿程配置 MS_ALONG_B.ORDER_INDEX 排序,其余保持前端传入顺序 + List stationOrder = buildAlongSortedOrder(stationCodes, stationRes); String timestamp = String.valueOf(System.currentTimeMillis()); List excelList = new ArrayList<>(); for (String month : months) { @@ -188,6 +190,7 @@ public class QgcExportServiceImpl implements IQgcExportService { // 判断是否包含水质指标,后续用于追加 WQ 合规 Sheet boolean hasWqIndicator = false; for (ExportIndicatorProcessor processor : processors) { + String sttp = processor.getSttpType(); List> rows; // QEC 自定义三栏达标率格式 if (processor instanceof QecProcessor) { @@ -197,7 +200,6 @@ public class QgcExportServiceImpl implements IQgcExportService { } if (rows != null) { List headers; - String sttp = processor.getSttpType(); if (processor instanceof QecProcessor) { // 分部标题作为列标题(合并单元格) String[] sectionTitles = {"生态流量达标率(环保部)", "生态流量达标率(水利部)", "生态流量达标率(多年平均)"}; @@ -824,6 +826,47 @@ public class QgcExportServiceImpl implements IQgcExportService { return result; } + /** + * 按沿程配置 MS_ALONG_B.ORDER_INDEX 排序电站列表。 + * 有沿程配置的电站排前面,无配置的保持原有顺序排后面。 + */ + private List buildAlongSortedOrder(List rstcds, StationResolution stationRes) { + if (rstcds.isEmpty()) return Collections.emptyList(); + + // 查询沿程配置:MS_ALONGDET_B JOIN MS_ALONG_B,按 ORDER_INDEX, SORT 排序 + List> alongList = msAlongdetBMapper.selectAlongSortOrder(rstcds); + if (alongList.isEmpty()) { + return new ArrayList<>(stationRes.displayNames.keySet()); + } + + // 按 ORDER_INDEX 分组去重,保留每站的最小 ORDER_INDEX + Map rstcdOrderMap = new LinkedHashMap<>(); + for (Map row : alongList) { + String rstcd = (String) row.get("RSTCD"); + Object orderIdx = row.get("ORDER_INDEX"); + if (rstcd != null && orderIdx != null && !rstcdOrderMap.containsKey(rstcd)) { + rstcdOrderMap.put(rstcd, ((Number) orderIdx).intValue()); + } + } + + // 有沿程配置的电站按 ORDER_INDEX 升序,无配置的排后面 + List alongSorted = new ArrayList<>(); + List remaining = new ArrayList<>(); + for (String rstcd : stationRes.displayNames.keySet()) { + if (rstcdOrderMap.containsKey(rstcd)) { + alongSorted.add(rstcd); + } else { + remaining.add(rstcd); + } + } + alongSorted.sort(Comparator.comparingInt(rstcdOrderMap::get)); + + List result = new ArrayList<>(alongSorted); + result.addAll(remaining); + log.info("沿程排序完成,有沿程={}, 无沿程={}", alongSorted.size(), remaining.size()); + return result; + } + // ======================== 月份范围 ======================== private MonthRange buildMonthRange(String month) { diff --git a/backend/src/main/resources/mapper/qgc_export/MsAlongdetBMapper.xml b/backend/src/main/resources/mapper/qgc_export/MsAlongdetBMapper.xml new file mode 100644 index 00000000..0500cd83 --- /dev/null +++ b/backend/src/main/resources/mapper/qgc_export/MsAlongdetBMapper.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + From 97982109ff5dcdbcbf137974924d1f1a86409878 Mon Sep 17 00:00:00 2001 From: tangwei Date: Thu, 6 Aug 2026 17:18:52 +0800 Subject: [PATCH 21/28] =?UTF-8?q?fix:=20=E5=9B=9E=E9=80=80=E4=BB=A3?= =?UTF-8?q?=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/impl/QgcExportServiceImpl.java | 53 ++----------------- 1 file changed, 5 insertions(+), 48 deletions(-) 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 637a7fbf..05089b7e 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 @@ -13,7 +13,6 @@ import com.yfd.platform.qgc_export.mapper.SdHydropwdaySMapper; 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.processor.*; import com.yfd.platform.qgc_export.service.IQgcExportService; import com.yfd.platform.qgc_eng.eq.entity.vo.EngEqRuleVo; @@ -143,9 +142,6 @@ public class QgcExportServiceImpl implements IQgcExportService { @Resource private EngEqDataService engEqDataService; - @Resource - private MsAlongdetBMapper msAlongdetBMapper; - private static final SimpleDateFormat SDF_TIME = new SimpleDateFormat("dd日 HH时"); private static final SimpleDateFormat SDF = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); @@ -178,8 +174,10 @@ public class QgcExportServiceImpl implements IQgcExportService { return; } - // 4. 电站顺序:优先按沿程配置 MS_ALONG_B.ORDER_INDEX 排序,其余保持前端传入顺序 - List stationOrder = buildAlongSortedOrder(stationCodes, stationRes); + // 4. 电站顺序 + List stationOrder = new ArrayList<>(stationRes.displayNames.keySet()); + + // 5. 每月一个 ExcelData String timestamp = String.valueOf(System.currentTimeMillis()); List excelList = new ArrayList<>(); for (String month : months) { @@ -190,7 +188,6 @@ public class QgcExportServiceImpl implements IQgcExportService { // 判断是否包含水质指标,后续用于追加 WQ 合规 Sheet boolean hasWqIndicator = false; for (ExportIndicatorProcessor processor : processors) { - String sttp = processor.getSttpType(); List> rows; // QEC 自定义三栏达标率格式 if (processor instanceof QecProcessor) { @@ -200,6 +197,7 @@ public class QgcExportServiceImpl implements IQgcExportService { } if (rows != null) { List headers; + String sttp = processor.getSttpType(); if (processor instanceof QecProcessor) { // 分部标题作为列标题(合并单元格) String[] sectionTitles = {"生态流量达标率(环保部)", "生态流量达标率(水利部)", "生态流量达标率(多年平均)"}; @@ -826,47 +824,6 @@ public class QgcExportServiceImpl implements IQgcExportService { return result; } - /** - * 按沿程配置 MS_ALONG_B.ORDER_INDEX 排序电站列表。 - * 有沿程配置的电站排前面,无配置的保持原有顺序排后面。 - */ - private List buildAlongSortedOrder(List rstcds, StationResolution stationRes) { - if (rstcds.isEmpty()) return Collections.emptyList(); - - // 查询沿程配置:MS_ALONGDET_B JOIN MS_ALONG_B,按 ORDER_INDEX, SORT 排序 - List> alongList = msAlongdetBMapper.selectAlongSortOrder(rstcds); - if (alongList.isEmpty()) { - return new ArrayList<>(stationRes.displayNames.keySet()); - } - - // 按 ORDER_INDEX 分组去重,保留每站的最小 ORDER_INDEX - Map rstcdOrderMap = new LinkedHashMap<>(); - for (Map row : alongList) { - String rstcd = (String) row.get("RSTCD"); - Object orderIdx = row.get("ORDER_INDEX"); - if (rstcd != null && orderIdx != null && !rstcdOrderMap.containsKey(rstcd)) { - rstcdOrderMap.put(rstcd, ((Number) orderIdx).intValue()); - } - } - - // 有沿程配置的电站按 ORDER_INDEX 升序,无配置的排后面 - List alongSorted = new ArrayList<>(); - List remaining = new ArrayList<>(); - for (String rstcd : stationRes.displayNames.keySet()) { - if (rstcdOrderMap.containsKey(rstcd)) { - alongSorted.add(rstcd); - } else { - remaining.add(rstcd); - } - } - alongSorted.sort(Comparator.comparingInt(rstcdOrderMap::get)); - - List result = new ArrayList<>(alongSorted); - result.addAll(remaining); - log.info("沿程排序完成,有沿程={}, 无沿程={}", alongSorted.size(), remaining.size()); - return result; - } - // ======================== 月份范围 ======================== private MonthRange buildMonthRange(String month) { From f2564ceca9b1f37bc2a35e3fdc58eed5ad43eee3 Mon Sep 17 00:00:00 2001 From: tangwei Date: Thu, 6 Aug 2026 17:50:06 +0800 Subject: [PATCH 22/28] =?UTF-8?q?fix:=20=E5=A2=9E=E5=8A=A0=E5=9E=82?= =?UTF-8?q?=E5=90=91=E6=B0=B4=E6=B8=A9=E5=AF=BC=E5=87=BAsheet=E5=8A=9F?= =?UTF-8?q?=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../qgc_env/wt/mapper/SdWtvtRMapper.java | 17 ++ .../qgc_export/domain/SdWtvtdayS.java | 27 +++ .../qgc_export/mapper/SdWtvtdaySMapper.java | 28 +++ .../qgc_export/processor/RwtProcessor.java | 60 +++++ .../service/impl/QgcExportServiceImpl.java | 227 ++++++++++++++++++ .../mapper/qgc_export/SdWtvtdaySMapper.xml | 42 ++++ 6 files changed, 401 insertions(+) create mode 100644 backend/src/main/java/com/yfd/platform/qgc_export/domain/SdWtvtdayS.java create mode 100644 backend/src/main/java/com/yfd/platform/qgc_export/mapper/SdWtvtdaySMapper.java create mode 100644 backend/src/main/java/com/yfd/platform/qgc_export/processor/RwtProcessor.java create mode 100644 backend/src/main/resources/mapper/qgc_export/SdWtvtdaySMapper.xml diff --git a/backend/src/main/java/com/yfd/platform/qgc_env/wt/mapper/SdWtvtRMapper.java b/backend/src/main/java/com/yfd/platform/qgc_env/wt/mapper/SdWtvtRMapper.java index 8a514c00..8c4fb63d 100644 --- a/backend/src/main/java/com/yfd/platform/qgc_env/wt/mapper/SdWtvtRMapper.java +++ b/backend/src/main/java/com/yfd/platform/qgc_env/wt/mapper/SdWtvtRMapper.java @@ -7,6 +7,7 @@ import org.apache.ibatis.annotations.Param; import org.apache.ibatis.annotations.Select; import java.util.List; +import java.util.Map; /** * 垂向水温年份数据Mapper @@ -28,4 +29,20 @@ public interface SdWtvtRMapper extends BaseMapper { "ORDER BY STCD, TO_CHAR(TM, 'yyyy') DESC" + "") List getWtrvDefaultYear(@Param("stcd") String stcd); + + /** + * 查询各测站水深对应的高程(HGT),按 STCD、WTHG 分组取最大值 + */ + @Select("") + List> selectDepthElevation(@Param("stcdList") List stcdList); } diff --git a/backend/src/main/java/com/yfd/platform/qgc_export/domain/SdWtvtdayS.java b/backend/src/main/java/com/yfd/platform/qgc_export/domain/SdWtvtdayS.java new file mode 100644 index 00000000..5cb01453 --- /dev/null +++ b/backend/src/main/java/com/yfd/platform/qgc_export/domain/SdWtvtdayS.java @@ -0,0 +1,27 @@ +package com.yfd.platform.qgc_export.domain; + +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +import java.io.Serializable; +import java.math.BigDecimal; +import java.util.Date; + +/** + * SD_WTVTDAY_S 垂向水温日统计表 + */ +@Data +@TableName("SD_WTVTDAY_S") +public class SdWtvtdayS implements Serializable { + private static final long serialVersionUID = 1L; + + private String id; + private String stcd; + private Date dt; + private BigDecimal wthg; // 水深 + private BigDecimal vwt; // 水温 + private String recordUser; + private Date recordTime; + private Date modifyTime; + private Integer isDeleted; +} diff --git a/backend/src/main/java/com/yfd/platform/qgc_export/mapper/SdWtvtdaySMapper.java b/backend/src/main/java/com/yfd/platform/qgc_export/mapper/SdWtvtdaySMapper.java new file mode 100644 index 00000000..dad13645 --- /dev/null +++ b/backend/src/main/java/com/yfd/platform/qgc_export/mapper/SdWtvtdaySMapper.java @@ -0,0 +1,28 @@ +package com.yfd.platform.qgc_export.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.yfd.platform.qgc_export.domain.SdWtvtdayS; +import org.apache.ibatis.annotations.Param; + +import java.util.List; +import java.util.Map; + +/** + * SD_WTVTDAY_S 垂向水温日数据 Mapper + */ +public interface SdWtvtdaySMapper extends BaseMapper { + + /** + * 查询月内每日垂向水温数据(含 EXTRACT(DAY FROM DT)),用于按指定日期筛选 + */ + List> selectDailyData(@Param("stcdList") List stcdList, + @Param("startTime") String startTime, + @Param("endTime") String endTime); + + /** + * 查询月均垂向水温,按 STCD、WTHG 分组 + */ + List> selectMonthlyAvg(@Param("stcdList") List stcdList, + @Param("startTime") String startTime, + @Param("endTime") String endTime); +} diff --git a/backend/src/main/java/com/yfd/platform/qgc_export/processor/RwtProcessor.java b/backend/src/main/java/com/yfd/platform/qgc_export/processor/RwtProcessor.java new file mode 100644 index 00000000..930d91b9 --- /dev/null +++ b/backend/src/main/java/com/yfd/platform/qgc_export/processor/RwtProcessor.java @@ -0,0 +1,60 @@ +package com.yfd.platform.qgc_export.processor; + +import com.yfd.platform.qgc_env.wt.mapper.SdWtvtRMapper; +import com.yfd.platform.qgc_export.mapper.SdWtvtdaySMapper; +import org.springframework.stereotype.Component; + +import java.util.Collections; +import java.util.Date; +import java.util.List; + +/** + * 垂向水温(RWT) 处理器 — 双表头格式,按水深分层展示 + *

    + * STTP: WTVT, 使用 SD_WTVT_R / SD_WTVTDAY_S 表 + */ +@Component +public class RwtProcessor implements ExportIndicatorProcessor { + + private SdWtvtRMapper sdWtvtRMapper; + private SdWtvtdaySMapper sdWtvtdaySMapper; + + public void setMappers(SdWtvtRMapper rMapper, SdWtvtdaySMapper sMapper) { + this.sdWtvtRMapper = rMapper; + this.sdWtvtdaySMapper = sMapper; + } + + public SdWtvtRMapper getSdWtvtRMapper() { return sdWtvtRMapper; } + public SdWtvtdaySMapper getSdWtvtdaySMapper() { return sdWtvtdaySMapper; } + + @Override + public String getFieldName() { + return "rwt"; + } + + @Override + public String getSheetName() { + return "垂向水温"; + } + + @Override + public String getSttpType() { + return "WTVT"; + } + + @Override + public int getDecimalPlaces() { + return 2; + } + + @Override + public int getOrder() { + return 11; + } + + @Override + public List queryRawData(List stcds, Date startTime, Date endTime, boolean isDaily) { + // RWT 使用自定义 Sheet 构建,不走标准数据查询流程 + return Collections.emptyList(); + } +} 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 05089b7e..51069e4b 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 @@ -13,6 +13,9 @@ import com.yfd.platform.qgc_export.mapper.SdHydropwdaySMapper; 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_env.wt.mapper.SdWtvtRMapper; +import com.yfd.platform.qgc_export.mapper.SdWtvtdaySMapper; import com.yfd.platform.qgc_export.processor.*; import com.yfd.platform.qgc_export.service.IQgcExportService; import com.yfd.platform.qgc_eng.eq.entity.vo.EngEqRuleVo; @@ -142,6 +145,17 @@ public class QgcExportServiceImpl implements IQgcExportService { @Resource private EngEqDataService engEqDataService; + @Resource + private MsAlongdetBMapper msAlongdetBMapper; + + @Resource + private SdWtvtRMapper sdWtvtRMapper; + @Resource + private SdWtvtdaySMapper sdWtvtdaySMapper; + + @Resource + private RwtProcessor rwtProcessor; + private static final SimpleDateFormat SDF_TIME = new SimpleDateFormat("dd日 HH时"); private static final SimpleDateFormat SDF = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); @@ -192,6 +206,8 @@ public class QgcExportServiceImpl implements IQgcExportService { // QEC 自定义三栏达标率格式 if (processor instanceof QecProcessor) { rows = buildQecSheetRows(mr, stationOrder, stationRes); + } else if (processor instanceof RwtProcessor) { + rows = buildRwtSheetRows(mr, stationOrder, stationRes); } else { rows = buildSheetRows(processor, mr, stationOrder, stationRes); } @@ -206,6 +222,15 @@ public class QgcExportServiceImpl implements IQgcExportService { headers.add(title); for (int i = 1; i < 6; i++) headers.add(""); } + } else if (processor instanceof RwtProcessor) { + // 垂向水温双表头:测站名称 | 水深 | 5日 | | 15日 | | 25日 | | 月均 | + 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(""); } else { // 仅包含当前处理器 STTP 类型有对应测站的电站 headers = new ArrayList<>(); @@ -228,6 +253,31 @@ public class QgcExportServiceImpl implements IQgcExportService { new int[]{0, 0, 6, 11}, // 水利部 new int[]{0, 0, 12, 17} // 多年平均 )); + } else if (processor instanceof RwtProcessor) { + // 垂向水温双表头合并单元格 + sd.setHeaderRowCount(1); + List regions = new ArrayList<>(Arrays.asList( + new int[]{0, 1, 0, 0}, // 测站名称 header + new int[]{0, 1, 1, 1}, // 水深(m) header + new int[]{0, 0, 2, 3}, // 5日 + new int[]{0, 0, 4, 5}, // 15日 + new int[]{0, 0, 6, 7}, // 25日 + new int[]{0, 0, 8, 9} // 月均 + )); + // 数据行测站名称相同合并(rows[0] 是子标题行,数据从 rows[1] 开始,sheet 行号 = 2 + i) + int dataStartRow = 2; // headers(1) + subheader(1) + for (int i = 1; i < rows.size(); ) { + String name = rows.get(i).get(0).toString(); + int j = i + 1; + while (j < rows.size() && name.equals(rows.get(j).get(0).toString())) { + j++; + } + if (j - i > 1) { + regions.add(new int[]{dataStartRow + i - 1, dataStartRow + j - 2, 0, 0}); + } + i = j; + } + sd.setMergedRegions(regions); } sheets.add(sd); if ("WQ".equals(sttp)) { @@ -494,6 +544,179 @@ public class QgcExportServiceImpl implements IQgcExportService { *

    * 返回的 rows[0] 是子标题行(|达标数|总数|达标率(%)|达标数|总数|达标率(%)|),后续为电站数据行。 */ + + // ======================== RWT 垂向水温 ======================== + + /** + * 构建垂向水温(RWT) Sheet 数据 — 双表头格式,按测站和水深分层 + */ + private List> buildRwtSheetRows(MonthRange mr, List stationOrder, + StationResolution stationRes) { + List stcdList = new ArrayList<>(); + // WTVT 测站→显示名 映射 + Map stcdDisplayMap = new LinkedHashMap<>(); + for (String rstcd : stationOrder) { + String stcd = stationRes.getStcd(rstcd, "WTVT"); + if (stcd != null) { + stcdList.add(stcd); + String displayName = stationRes.getDisplayName(rstcd, "WTVT"); + stcdDisplayMap.put(stcd, StrUtil.isNotBlank(displayName) ? displayName : stcd); + } + } + if (stcdList.isEmpty()) return null; + + String startStr = SDF.format(mr.getStart()); + String endStr = SDF.format(mr.getEnd()); + + // 1. 查询日数据(含 EXTRACT(DAY FROM DT)) + List> dailyData = sdWtvtdaySMapper.selectDailyData(stcdList, startStr, endStr); + // 2. 查询月均 + List> monthlyData = sdWtvtdaySMapper.selectMonthlyAvg(stcdList, startStr, endStr); + // 3. 查询高程 + List> elevationData = sdWtvtRMapper.selectDepthElevation(stcdList); + + // 按 (STCD, WTHG) 构建日水温索引 + Map> dayVwtMap = new LinkedHashMap<>(); + Map wthgOrderKey = new LinkedHashMap<>(); + for (Map row : dailyData) { + String stcd = (String) row.get("STCD"); + Object wthgObj = row.get("WTHG"); + Object vwtObj = row.get("VWT"); + Object dayNumObj = row.get("DAY_NUM"); + if (stcd == null || wthgObj == null || vwtObj == null || dayNumObj == null) continue; + + String wthg = formatNumber(wthgObj); + int dayNum = ((Number) dayNumObj).intValue(); + BigDecimal vwt = toBigDecimal(vwtObj); + + String key = stcd + "|" + wthg; + dayVwtMap.computeIfAbsent(key, k -> new LinkedHashMap<>()) + .putIfAbsent(String.valueOf(dayNum), vwt); + wthgOrderKey.putIfAbsent(wthg, toBigDecimal(wthgObj)); + } + + // 月均索引 + Map monthlyAvgMap = new LinkedHashMap<>(); + for (Map row : monthlyData) { + String stcd = (String) row.get("STCD"); + Object wthgObj = row.get("WTHG"); + Object vwtObj = row.get("VWT"); + if (stcd == null || wthgObj == null || vwtObj == null) continue; + + String wthg = formatNumber(wthgObj); + String key = stcd + "|" + wthg; + monthlyAvgMap.put(key, toBigDecimal(vwtObj)); + wthgOrderKey.putIfAbsent(wthg, toBigDecimal(wthgObj)); + } + + // 高程索引 + Map elevationMap = new LinkedHashMap<>(); + for (Map row : elevationData) { + String stcd = (String) row.get("STCD"); + Object wthgObj = row.get("WTHG"); + Object hgtObj = row.get("HGT"); + if (stcd == null || wthgObj == null || hgtObj == null) continue; + + String wthg = formatNumber(wthgObj); + elevationMap.put(stcd + "|" + wthg, toBigDecimal(hgtObj)); + } + + // 收集所有 (STCD, WTHG) 组合,按测站顺序和 WTHG 升序排列 + Set allKeys = new LinkedHashSet<>(); + allKeys.addAll(dayVwtMap.keySet()); + allKeys.addAll(monthlyAvgMap.keySet()); + + List sortedKeys = new ArrayList<>(); + for (String stcd : stcdList) { + String prefix = stcd + "|"; + List stcdKeys = new ArrayList<>(); + for (String key : allKeys) { + if (key.startsWith(prefix)) { + stcdKeys.add(key.substring(prefix.length())); + } + } + stcdKeys.sort(Comparator.comparing(k -> wthgOrderKey.getOrDefault(k, BigDecimal.ZERO))); + for (String wthg : stcdKeys) { + sortedKeys.add(prefix + wthg); + } + } + + // 构建数据行 + 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)"); + rows.add(subHeaderRow); + + for (String key : sortedKeys) { + int sep = key.indexOf('|'); + String stcd = key.substring(0, sep); + String wthg = key.substring(sep + 1); + + Map dayMap = dayVwtMap.getOrDefault(key, Collections.emptyMap()); + BigDecimal elev = elevationMap.get(key); + BigDecimal monthlyAvg = monthlyAvgMap.get(key); + + List row = new ArrayList<>(); + row.add(stcdDisplayMap.getOrDefault(stcd, stcd)); + row.add(formatNumberCell(wthg)); + row.add(formatTemp(dayMap.get("5"))); + row.add(formatElev(elev)); + row.add(formatTemp(dayMap.get("15"))); + row.add(formatElev(elev)); + row.add(formatTemp(dayMap.get("25"))); + row.add(formatElev(elev)); + row.add(formatTemp(monthlyAvg)); + row.add(formatElev(elev)); + rows.add(row); + } + + return rows; + } + + private String formatNumberCell(String numStr) { + BigDecimal val = toBigDecimal(numStr); + return val != null ? stripTrailingZeros(val) : numStr; + } + + private String formatTemp(BigDecimal val) { + if (val == null) return ""; + return val.setScale(2, java.math.RoundingMode.HALF_UP).toPlainString(); + } + + private String formatElev(BigDecimal val) { + if (val == null) return ""; + return stripTrailingZeros(val); + } + + private String formatNumber(Object obj) { + if (obj instanceof BigDecimal) return stripTrailingZeros((BigDecimal) obj); + if (obj instanceof Number) return String.valueOf(obj); + return obj.toString(); + } + + private BigDecimal toBigDecimal(Object val) { + if (val instanceof BigDecimal) return (BigDecimal) val; + if (val instanceof Number) return BigDecimal.valueOf(((Number) val).doubleValue()); + if (val instanceof String) { + try { + return new BigDecimal((String) val); + } catch (NumberFormatException e) { + return null; + } + } + return null; + } + + private String stripTrailingZeros(BigDecimal val) { + if (val == null) return ""; + return val.stripTrailingZeros().toPlainString(); + } + private List> buildWqComplianceSheet( MonthRange mr, List stationOrder, StationResolution stationRes) { @@ -659,6 +882,8 @@ public class QgcExportServiceImpl implements IQgcExportService { processorMap.put("qo", qoProcessor); // 生态流量(STTP=ENG,仅标准格式无日变幅) processorMap.put("qec", qecProcessor); + // 垂向水温(STTP=WTVT, 双表头分层格式) + processorMap.put("rwt", rwtProcessor); // WQ 别名:展开为全部水质指标 List expanded = new ArrayList<>(); @@ -721,6 +946,8 @@ public class QgcExportServiceImpl implements IQgcExportService { ((HydropwIndicatorProcessor) p).setMappers(sdHydropwRMapper, sdHydropwdaySMapper); } else if (p instanceof QecProcessor) { ((QecProcessor) p).setMappers(sdQecRMapper, sdQecdaySMapper); + } else if (p instanceof RwtProcessor) { + ((RwtProcessor) p).setMappers(sdWtvtRMapper, sdWtvtdaySMapper); } result.add(p); } diff --git a/backend/src/main/resources/mapper/qgc_export/SdWtvtdaySMapper.xml b/backend/src/main/resources/mapper/qgc_export/SdWtvtdaySMapper.xml new file mode 100644 index 00000000..f92b48b7 --- /dev/null +++ b/backend/src/main/resources/mapper/qgc_export/SdWtvtdaySMapper.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + From 29105cf1f5ef18efe4b1ecf9f5179ad936813c6e Mon Sep 17 00:00:00 2001 From: tangwei Date: Thu, 6 Aug 2026 18:32:23 +0800 Subject: [PATCH 23/28] =?UTF-8?q?fix:=20=E5=A2=9E=E5=8A=A0=E5=B0=8F?= =?UTF-8?q?=E6=97=B6=E5=92=8C=E6=97=A5=E8=A1=A8=E5=AF=BC=E5=87=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/impl/QgcExportServiceImpl.java | 197 ++++++++++++++++++ 1 file changed, 197 insertions(+) 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 51069e4b..d2e15999 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 @@ -169,6 +169,12 @@ public class QgcExportServiceImpl implements IQgcExportService { public void exportData(List dataFields, List months, List stationCodes, boolean isDailyDimension, String tmDimension, HttpServletResponse response) { + // hour/day 模式:仅导出原始数据,不做聚合处理 + if ("hour".equals(tmDimension) || "day".equals(tmDimension)) { + exportRawData(dataFields, months, stationCodes, tmDimension, response); + return; + } + // 1. 指标处理器匹配 List processors = resolveProcessors(dataFields); if (processors.isEmpty()) { @@ -1331,4 +1337,195 @@ public class QgcExportServiceImpl implements IQgcExportService { /** 小时达标率(百分比,仅水质指标使用) */ private BigDecimal complianceRate; } + + // ======================== hour/day 原始数据导出 ======================== + + /** + * hour/day 模式:仅导出原始数据,不做汇总/达标率等聚合处理 + */ + private void exportRawData(List dataFields, List months, + List stationCodes, String tmDimension, + HttpServletResponse response) { + if (months.isEmpty()) { + log.warn("hour/day 模式需要传入时间范围参数"); + return; + } + boolean isDaily = "day".equals(tmDimension); + + // 解析月份字段(如 "2026-05")→ 当月起止时间 + String monthStr = months.get(0); + String startTime, endTime; + if (monthStr.matches("\\d{4}-\\d{2}")) { + // 月份格式:YYYY-MM → 当月1日 ~ 下月1日 + startTime = monthStr + "-01 00:00:00"; + // 计算下月 + int year = Integer.parseInt(monthStr.substring(0, 4)); + int month = Integer.parseInt(monthStr.substring(5, 7)); + if (month == 12) { + endTime = (year + 1) + "-01-01 00:00:00"; + } else { + endTime = year + "-" + String.format("%02d", month + 1) + "-01 00:00:00"; + } + } else { + startTime = monthStr; + endTime = months.size() >= 2 ? months.get(1) : monthStr; + if (!startTime.contains(":")) startTime += " 00:00:00"; + if (!endTime.contains(":")) endTime += " 23:59:59"; + } + + // 1. 指标处理器(去掉日变幅 processor) + List processors = resolveProcessors(dataFields).stream() + .filter(p -> !p.isVariation()) + .toList(); + if (processors.isEmpty()) { + log.warn("未找到匹配的指标处理器"); + return; + } + + // 2. 电站映射 + Set sttpTypes = processors.stream() + .map(ExportIndicatorProcessor::getSttpType) + .collect(Collectors.toCollection(LinkedHashSet::new)); + StationResolution stationRes = resolveStations(stationCodes, sttpTypes); + if (stationRes.displayNames.isEmpty()) return; + + List stationOrder = new ArrayList<>(stationRes.displayNames.keySet()); + + // 3. 文件名 + String timestamp = String.valueOf(System.currentTimeMillis()); + String excelFileName = monthStr + (isDaily ? "日数据_" : "小时数据_") + timestamp; + String zipFileName = getFileNamePrefix(tmDimension) + ".zip"; + + // 4. 构建 Sheet + List sheets = new ArrayList<>(); + for (ExportIndicatorProcessor processor : processors) { + String sttp = processor.getSttpType(); + List stcds = new ArrayList<>(); + Map stcdDisplayMap = new LinkedHashMap<>(); + for (String rstcd : stationOrder) { + String stcd = stationRes.getStcd(rstcd, sttp); + if (stcd != null) { + stcds.add(stcd); + String name = stationRes.getDisplayName(rstcd, sttp); + stcdDisplayMap.put(stcd, StrUtil.isNotBlank(name) ? name : stcd); + } + } + if (stcds.isEmpty()) continue; + + List> rows; + List headers; + if (processor instanceof RwtProcessor) { + rows = buildRwtRawSheetRows(stcds, stcdDisplayMap, startTime, endTime); + headers = Arrays.asList("测站", "时间", "水深(m)", "水温(℃)"); + } else { + rows = buildRawSheetRows(processor, stcds, stcdDisplayMap, startTime, endTime, isDaily); + if (rows == null) continue; + // 表头:时间 | 测站1 | 测站2 | ... + headers = new ArrayList<>(); + headers.add("时间"); + for (String stcd : stcds) { + headers.add(stcdDisplayMap.getOrDefault(stcd, stcd)); + } + } + + ExportZipUtil.SheetData sd = new ExportZipUtil.SheetData(processor.getSheetName(), headers, rows); + sheets.add(sd); + } + + if (sheets.isEmpty()) { + log.warn("hour/day 模式:没有可用数据"); + return; + } + + // 5. ZIP 导出 + try { + ExportZipUtil.exportToResponse(response, zipFileName, + Collections.singletonList(new ExportZipUtil.ExcelData(excelFileName, sheets))); + } catch (Exception e) { + log.error("hour/day 导出失败", e); + } + } + + /** + * 构建标准格式原始数据行(非 RWT) + * 每行 = 一个时间点,列 = 各测站的原始值 + */ + private List> buildRawSheetRows(ExportIndicatorProcessor processor, + List stcds, + Map stcdDisplayMap, + String startTime, String endTime, + boolean isDaily) { + try { + Date start = SDF.parse(startTime); + Date end = SDF.parse(endTime); + List rawData = processor.queryRawData(stcds, start, end, isDaily); + if (rawData == null || rawData.isEmpty()) return null; + + // 按时间分组:timeKey → (stcd → value) + Map> timeMap = new LinkedHashMap<>(); + SimpleDateFormat timeFmt = new SimpleDateFormat(isDaily ? "yyyy-MM-dd" : "yyyy-MM-dd HH:mm:ss"); + for (IndicatorDataRow row : rawData) { + String timeKey = timeFmt.format(row.getTm()); + timeMap.computeIfAbsent(timeKey, k -> new LinkedHashMap<>()) + .put(row.getStcd(), row.getValue()); + } + + // 构建行数据(按时间排序,避免 LinkedHashMap 插入顺序导致乱序) + List> rows = new ArrayList<>(); + List sortedTimes = new ArrayList<>(timeMap.keySet()); + Collections.sort(sortedTimes); + for (String timeKey : sortedTimes) { + List row = new ArrayList<>(); + row.add(timeKey); + Map stcdValMap = timeMap.get(timeKey); + for (String stcd : stcds) { + BigDecimal val = stcdValMap.get(stcd); + row.add(val != null ? formatTemp(val) : ""); + } + rows.add(row); + } + return rows; + } catch (Exception e) { + log.error("构建原始数据 Sheet 失败: {}", processor.getFieldName(), e); + return null; + } + } + + /** + * 构建 RWT 垂向水温原始数据行 + * 格式:测站 | 时间 | 水深(m) | 水温(℃) + */ + private List> buildRwtRawSheetRows(List stcds, + Map stcdDisplayMap, + String startTime, String endTime) { + try { + List> rawList = sdWtvtdaySMapper.selectDailyData(stcds, startTime, endTime); + if (rawList == null || rawList.isEmpty()) return null; + + SimpleDateFormat dateFmt = new SimpleDateFormat("yyyy-MM-dd"); + List> rows = new ArrayList<>(); + for (Map map : rawList) { + String stcd = (String) map.get("STCD"); + Object wthgObj = map.get("WTHG"); + Object vwtObj = map.get("VWT"); + Object dayNumObj = map.get("DAY_NUM"); + if (stcd == null || vwtObj == null) continue; + + String displayName = stcdDisplayMap.getOrDefault(stcd, stcd); + String depth = wthgObj != null ? formatNumberCell(formatNumber(wthgObj)) : ""; + BigDecimal vwt = toBigDecimal(vwtObj); + + List row = new ArrayList<>(); + row.add(displayName); + row.add(dayNumObj != null ? "第" + dayNumObj + "日" : ""); + row.add(depth); + row.add(formatTemp(vwt)); + rows.add(row); + } + return rows; + } catch (Exception e) { + log.error("构建 RWT 原始数据失败", e); + return null; + } + } } From b19db9e8d2ea6201a211ba1ae4bd93dfdc91c328 Mon Sep 17 00:00:00 2001 From: tangwei Date: Fri, 7 Aug 2026 09:46:02 +0800 Subject: [PATCH 24/28] =?UTF-8?q?fix:=20=E5=A2=9E=E5=8A=A0=E5=B9=B4?= =?UTF-8?q?=E5=BA=A6=E5=AF=BC=E5=87=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/QgcExportController.java | 15 +- .../qgc_export/domain/StationInfo.java | 56 ++ .../qgc_export/mapper/YearStatMapper.java | 27 + .../qgc_export/service/IQgcExportService.java | 15 + .../service/impl/QgcExportServiceImpl.java | 641 ++++++++++++++++-- .../mapper/qgc_export/YearStatMapper.xml | 49 ++ 6 files changed, 758 insertions(+), 45 deletions(-) create mode 100644 backend/src/main/java/com/yfd/platform/qgc_export/domain/StationInfo.java create mode 100644 backend/src/main/java/com/yfd/platform/qgc_export/mapper/YearStatMapper.java create mode 100644 backend/src/main/resources/mapper/qgc_export/YearStatMapper.xml 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 @@ + + + + + + + + + + + From 7095dd459ef7b7bdc63a73590af8e4da6a6b63e9 Mon Sep 17 00:00:00 2001 From: tangwei Date: Fri, 7 Aug 2026 10:31:40 +0800 Subject: [PATCH 25/28] =?UTF-8?q?fix:=20=E4=BC=98=E5=8C=96=E4=BB=A3?= =?UTF-8?q?=E7=A0=81bug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../qgc_base/domain/vo/EngStInfoResultVo.java | 2 +- .../qgc_base/domain/vo/EngVmsstbprptVo.java | 4 + .../qgc_base/service/ISdEngInfoBHService.java | 2 +- .../service/impl/SdEngInfoBHServiceImpl.java | 312 ++++++++++++++---- .../service/impl/WarnDataServiceImpl.java | 3 +- .../service/impl/QgcExportServiceImpl.java | 29 +- 6 files changed, 282 insertions(+), 70 deletions(-) diff --git a/backend/src/main/java/com/yfd/platform/qgc_base/domain/vo/EngStInfoResultVo.java b/backend/src/main/java/com/yfd/platform/qgc_base/domain/vo/EngStInfoResultVo.java index 27e6ca72..a409c84a 100644 --- a/backend/src/main/java/com/yfd/platform/qgc_base/domain/vo/EngStInfoResultVo.java +++ b/backend/src/main/java/com/yfd/platform/qgc_base/domain/vo/EngStInfoResultVo.java @@ -13,7 +13,7 @@ import java.util.Map; public class EngStInfoResultVo { @Schema(description = "主信息兼容对象") - private Map msStbprpT = new LinkedHashMap<>(); + private EngVmsstbprptVo msStbprpT; @Schema(description = "测值列表") private List stcdValVoList = new ArrayList<>(); diff --git a/backend/src/main/java/com/yfd/platform/qgc_base/domain/vo/EngVmsstbprptVo.java b/backend/src/main/java/com/yfd/platform/qgc_base/domain/vo/EngVmsstbprptVo.java index 0554dae5..e82d20ac 100644 --- a/backend/src/main/java/com/yfd/platform/qgc_base/domain/vo/EngVmsstbprptVo.java +++ b/backend/src/main/java/com/yfd/platform/qgc_base/domain/vo/EngVmsstbprptVo.java @@ -14,8 +14,12 @@ public class EngVmsstbprptVo implements Serializable { private static final long serialVersionUID = 1L; private String id; + private String sttp; private String sttpCode; private String sttpName; + private String sttpFullPath; + private String sttpTreeLevel; + private String rvcdFullPath; private String stcd; private String stnm; private String ennm; diff --git a/backend/src/main/java/com/yfd/platform/qgc_base/service/ISdEngInfoBHService.java b/backend/src/main/java/com/yfd/platform/qgc_base/service/ISdEngInfoBHService.java index 026a2fc8..88af134a 100644 --- a/backend/src/main/java/com/yfd/platform/qgc_base/service/ISdEngInfoBHService.java +++ b/backend/src/main/java/com/yfd/platform/qgc_base/service/ISdEngInfoBHService.java @@ -80,7 +80,7 @@ public interface ISdEngInfoBHService extends IService { List selectRegDropdown(SdEngInfoBHRequest sdEngInfoBHRequest); - EngBaseInfoVo getStInfoByStcd(String stcd); + EngVmsstbprptVo getStInfoByStcd(String stcd); EngStInfoResultVo getStcdInfo(String stcd); diff --git a/backend/src/main/java/com/yfd/platform/qgc_base/service/impl/SdEngInfoBHServiceImpl.java b/backend/src/main/java/com/yfd/platform/qgc_base/service/impl/SdEngInfoBHServiceImpl.java index 01c9ef84..cec79c4d 100644 --- a/backend/src/main/java/com/yfd/platform/qgc_base/service/impl/SdEngInfoBHServiceImpl.java +++ b/backend/src/main/java/com/yfd/platform/qgc_base/service/impl/SdEngInfoBHServiceImpl.java @@ -1001,68 +1001,267 @@ public class SdEngInfoBHServiceImpl extends ServiceImpl 0\n" + + " GROUP BY STCD, TM ) temp\n" + + " GROUP BY STCD) b ON b.STCD = eng.STCD " + + "LEFT JOIN MS_ALONG_B along ON along.BASEID = eng.BASE_ID AND along.CODE = 'common' AND NVL(along.IS_DELETED, 0) = 0 " + + "LEFT JOIN ( " + + " SELECT det.SORT,cfg.BASEID, cfg.RVCD, det.STCD " + + " FROM MS_ALONGDET_B det " + + " INNER JOIN MS_ALONG_B cfg ON det.ALONG_ID = cfg.ID " + + " WHERE NVL(det.IS_DELETED, 0) = 0 AND NVL(cfg.IS_DELETED, 0) = 0 AND cfg.CODE = 'common' " + + ") rstSort ON rstSort.BASEID = eng.BASE_ID AND rstSort.STCD = eng.STCD " + + "LEFT JOIN ( " + + " SELECT det.SORT,cfg.BASEID, cfg.RVCD, det.STCD " + + " FROM MS_ALONGDET_B det " + + " INNER JOIN MS_ALONG_B cfg ON det.ALONG_ID = cfg.ID " + + " WHERE NVL(det.IS_DELETED, 0) = 0 AND NVL(cfg.IS_DELETED, 0) = 0 AND cfg.CODE = 'common' " + + ") siteSort ON siteSort.BASEID = eng.BASE_ID AND siteSort.STCD = eng.STCD " + + "WHERE NVL(eng.IS_DELETED, 0) = 0 "; } @Override @@ -1070,27 +1269,27 @@ public class SdEngInfoBHServiceImpl extends ServiceImpl stInfoMap = microservicDynamicSQLMapper.getOneBySql(buildStcdInfoViewSql(), Collections.singletonMap("stcd", stcd)); if (stInfoMap != null && !stInfoMap.isEmpty()) { EngStInfoResultVo result = new EngStInfoResultVo(); - result.setMsStbprpT(buildCamelCaseMsStbprpTMap(stInfoMap)); + result.setMsStbprpT(baseInfo); result.setStBaseInfo(buildStBaseInfo(stInfoMap)); return result; } - EngBaseInfoVo baseInfo = getStInfoByStcd(stcd); - if (baseInfo == null) { - return null; - } EngStBaseInfoVo stBaseInfo = new EngStBaseInfoVo(); stBaseInfo.setStcd(baseInfo.getStcd()); stBaseInfo.setStnm(baseInfo.getStnm()); stBaseInfo.setSttpCode(baseInfo.getSttpCode()); stBaseInfo.setSttpName(baseInfo.getSttpName()); - stBaseInfo.setSttpFullPath(baseInfo.getSttpFullPath()); - stBaseInfo.setSttpTreeLevel(baseInfo.getSttpTreeLevel()); +// stBaseInfo.setSttpFullPath(baseInfo.getSttpFullPath()); +// stBaseInfo.setSttpTreeLevel(baseInfo.getSttpTreeLevel()); stBaseInfo.setStCode(null); stBaseInfo.setStName(null); stBaseInfo.setBaseId(baseInfo.getBaseId()); @@ -1340,7 +1539,7 @@ public class SdEngInfoBHServiceImpl extends ServiceImpl sortStationsByOrder(List stations) { - // 按 RSTCD 收集沿程排序 - List rstcds = stations.stream().map(StationInfo::getRstcd).filter(Objects::nonNull).distinct().toList(); - if (rstcds.isEmpty()) return stations; + // 收集所有测站的 STCD(沿程表匹配的是 STCD 测站编码) + List stcds = stations.stream().map(StationInfo::getStcd).filter(Objects::nonNull).distinct().toList(); + if (stcds.isEmpty()) return stations; - List> orderList = msAlongdetBMapper.selectAlongSortOrder(rstcds); + // 查询沿程排序(MS_ALONGDET_B.STCD + MS_ALONG_B.ORDER_INDEX) + List> orderList = msAlongdetBMapper.selectAlongSortOrder(stcds); 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()); + String stcd = (String) m.get("STCD"); + Object orderObj = m.get("ORDER_INDEX"); + if (stcd != null && orderObj instanceof Number) { + orderMap.put(stcd, ((Number) orderObj).intValue()); } } + if (orderMap.isEmpty()) { + // 无沿程配置时按电站名称排序 + stations.sort(Comparator.comparing(s -> s.getEnnm() != null ? s.getEnnm() : s.getStnm() != null ? s.getStnm() : "")); + return stations; + } + List sorted = new ArrayList<>(stations); sorted.sort((a, b) -> { - Integer oa = orderMap.get(a.getRstcd()); - Integer ob = orderMap.get(b.getRstcd()); + Integer oa = orderMap.get(a.getStcd()); + Integer ob = orderMap.get(b.getStcd()); if (oa != null && ob != null) return oa.compareTo(ob); if (oa != null) return -1; if (ob != null) return 1; From 0dea0c9ee00582509d853ce718a85975044b8d70 Mon Sep 17 00:00:00 2001 From: tangwei Date: Fri, 7 Aug 2026 10:57:14 +0800 Subject: [PATCH 26/28] =?UTF-8?q?fix:=20=E6=B0=B4=E8=B4=A8=E8=BE=BE?= =?UTF-8?q?=E6=A0=87=E7=BB=9F=E8=AE=A1=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/impl/QgcExportServiceImpl.java | 41 ++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) 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 68c0cba9..e3d8497f 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 @@ -1413,10 +1413,28 @@ public class QgcExportServiceImpl implements IQgcExportService { String timestamp = String.valueOf(System.currentTimeMillis()); // 预查询测站信息(年维度无关,一次查询即可) + // 先查所有电站名称(不限 STTP),用于占位电站显示名称 + Map allStationNames = new LinkedHashMap<>(); + if (!stationCodes.isEmpty()) { + List allStations = yearStatMapper.selectList( + new LambdaQueryWrapper() + .and(w -> w.in(StationInfo::getRstcd, stationCodes) + .or() + .in(StationInfo::getStcd, stationCodes)) + ); + for (StationInfo s : allStations) { + String key = s.getRstcd() != null ? s.getRstcd() : s.getStcd(); + // 取第一个非空名称:ENNM > STNM > key + String name = s.getEnnm() != null ? s.getEnnm() : s.getStnm(); + allStationNames.putIfAbsent(key, name != null ? name : key); + } + } + List wqStations = null; List engStations = null; if (hasWq && !stationCodes.isEmpty()) { - wqStations = yearStatMapper.selectList( + // 查询 V_MS_STBPRP_T 中 MWAY=2(自动站)的 WQ 测站 + List dbWqStations = yearStatMapper.selectList( new LambdaQueryWrapper() .eq(StationInfo::getSttp, "WQ") .eq(StationInfo::getMway, 2) @@ -1424,6 +1442,27 @@ public class QgcExportServiceImpl implements IQgcExportService { .or() .in(StationInfo::getStcd, stationCodes)) ); + // 按前端传的 stationCodes 顺序构建完整列表(无数据的电站也显示空行) + Map rstcdMap = new LinkedHashMap<>(); + for (StationInfo s : dbWqStations) { + String key = s.getRstcd() != null ? s.getRstcd() : s.getStcd(); + rstcdMap.putIfAbsent(key, s); + } + wqStations = new ArrayList<>(); + for (String code : stationCodes) { + StationInfo found = rstcdMap.get(code); + if (found != null) { + wqStations.add(found); + } else { + // 无 WQ 自动站数据的电站,从名称映射中取名称 + String name = allStationNames.getOrDefault(code, code); + StationInfo placeholder = new StationInfo(); + placeholder.setRstcd(code); + placeholder.setStcd(code); + placeholder.setEnnm(name); + wqStations.add(placeholder); + } + } if (!wqStations.isEmpty()) { wqStations = sortStationsByOrder(wqStations); } From c84a4054539d82c40b783d68b0df6bd11fdfcb23 Mon Sep 17 00:00:00 2001 From: tangwei Date: Fri, 7 Aug 2026 11:19:23 +0800 Subject: [PATCH 27/28] =?UTF-8?q?fix:=20=E5=9E=82=E5=90=91=E6=B0=B4?= =?UTF-8?q?=E6=B8=A9sheet=E9=A1=B5=E9=A1=BA=E5=BA=8F=E6=9B=B4=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/yfd/platform/qgc_export/processor/RwtProcessor.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/main/java/com/yfd/platform/qgc_export/processor/RwtProcessor.java b/backend/src/main/java/com/yfd/platform/qgc_export/processor/RwtProcessor.java index 930d91b9..6c7170ed 100644 --- a/backend/src/main/java/com/yfd/platform/qgc_export/processor/RwtProcessor.java +++ b/backend/src/main/java/com/yfd/platform/qgc_export/processor/RwtProcessor.java @@ -49,7 +49,7 @@ public class RwtProcessor implements ExportIndicatorProcessor { @Override public int getOrder() { - return 11; + return 7; } @Override From 766899bb510e7c5c38a58a146fccb23c32aee990 Mon Sep 17 00:00:00 2001 From: tangwei Date: Fri, 7 Aug 2026 13:38:56 +0800 Subject: [PATCH 28/28] =?UTF-8?q?fix:=20=E4=BC=98=E5=8C=96=E7=BB=BC?= =?UTF-8?q?=E5=90=88=E5=AF=BC=E5=87=BA=EF=BC=8C=E5=A2=9E=E5=8A=A0=E4=BA=86?= =?UTF-8?q?=E5=BC=82=E6=AD=A5=E5=AF=BC=E5=87=BA=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../qgc_base/domain/vo/EngVmsstbprptVo.java | 2 + .../service/impl/SdEngInfoBHServiceImpl.java | 2 + .../controller/QgcExportController.java | 148 ++++++++++-- .../qgc_export/domain/ExportTask.java | 58 +++++ .../qgc_export/service/ExportTaskService.java | 227 ++++++++++++++++++ 5 files changed, 419 insertions(+), 18 deletions(-) create mode 100644 backend/src/main/java/com/yfd/platform/qgc_export/domain/ExportTask.java create mode 100644 backend/src/main/java/com/yfd/platform/qgc_export/service/ExportTaskService.java diff --git a/backend/src/main/java/com/yfd/platform/qgc_base/domain/vo/EngVmsstbprptVo.java b/backend/src/main/java/com/yfd/platform/qgc_base/domain/vo/EngVmsstbprptVo.java index e82d20ac..efd19e93 100644 --- a/backend/src/main/java/com/yfd/platform/qgc_base/domain/vo/EngVmsstbprptVo.java +++ b/backend/src/main/java/com/yfd/platform/qgc_base/domain/vo/EngVmsstbprptVo.java @@ -16,6 +16,8 @@ public class EngVmsstbprptVo implements Serializable { private String id; private String sttp; private String sttpCode; + private String sttpMap; + private String logo; private String sttpName; private String sttpFullPath; private String sttpTreeLevel; diff --git a/backend/src/main/java/com/yfd/platform/qgc_base/service/impl/SdEngInfoBHServiceImpl.java b/backend/src/main/java/com/yfd/platform/qgc_base/service/impl/SdEngInfoBHServiceImpl.java index cec79c4d..03472886 100644 --- a/backend/src/main/java/com/yfd/platform/qgc_base/service/impl/SdEngInfoBHServiceImpl.java +++ b/backend/src/main/java/com/yfd/platform/qgc_base/service/impl/SdEngInfoBHServiceImpl.java @@ -1079,6 +1079,7 @@ public class SdEngInfoBHServiceImpl extends ServiceImpl - * 前端传参示例: - *
    {@code
    - * POST /qgcExport/exportData
    - * {
    - *   "tmDimension": "month",
    - *   "dataField": "v,q,z",
    - *   "months": "2026-08,2026-07",
    - *   "stcd": "00001,00002",
    - *   "sysId": "qgc",
    - *   "isDailyDimension": true,
    - *   "isCalcQec": true
    - * }
    - * }
    */ @RestController @RequestMapping("/qgcExport") @@ -38,8 +30,14 @@ public class QgcExportController { @Resource private IQgcExportService qgcExportService; + @Resource + private ExportTaskService exportTaskService; + + /** + * 原有同步导出接口(保留兼容) + */ @PostMapping("/exportData") - @Operation(summary = "综合数据导出(ZIP 含多 Sheet Excel)") + @Operation(summary = "综合数据导出(ZIP 含多 Sheet Excel)— 同步返回文件流") public void exportData(@RequestParam(required = false) String tmDimension, @RequestParam(required = false) String dataField, @RequestParam(required = false) String months, @@ -52,7 +50,6 @@ public class QgcExportController { @RequestParam(required = false) String baseId, HttpServletResponse response) { - // 解析逗号分隔参数 List dataFields = parseCsv(dataField); List monthList = parseCsv(months); List stationList = parseCsv(stcd); @@ -62,7 +59,6 @@ public class QgcExportController { tmDimension = "month"; } - // 年度导出分支 if ("year".equals(tmDimension)) { List yearStatTypeList = parseCsv(yearStatType); List yearList = parseCsv(year); @@ -74,6 +70,122 @@ public class QgcExportController { isDailyDimension, tmDimension, response); } + // ======================== 异步导出三接口 ======================== + + /** + * 接口一:创建导出任务,返回唯一任务 ID + */ + @PostMapping("/create") + @Operation(summary = "创建异步导出任务,返回 taskId") + public Map createTask(@RequestParam(required = false) String tmDimension, + @RequestParam(required = false) String dataField, + @RequestParam(required = false) String months, + @RequestParam(required = false) String stcd, + @RequestParam(defaultValue = "true") boolean isDailyDimension, + @RequestParam(required = false) String yearStatType, + @RequestParam(required = false) String year, + @RequestParam(required = false) String baseId) { + + List dataFields = parseCsv(dataField); + List monthList = parseCsv(months); + List stationList = parseCsv(stcd); + List baseIdList = parseCsv(baseId); + + if (tmDimension == null || tmDimension.isEmpty()) { + tmDimension = "month"; + } + + String taskId; + if ("year".equals(tmDimension)) { + List yearStatTypeList = parseCsv(yearStatType); + List yearList = parseCsv(year); + taskId = exportTaskService.submitYearExport(yearStatTypeList, yearList, stationList, baseIdList); + } else { + taskId = exportTaskService.submitMonthExport(dataFields, monthList, stationList, + isDailyDimension, tmDimension); + } + + Map result = new HashMap<>(); + result.put("taskId", taskId); + return result; + } + + /** + * 接口二:查询任务状态 + */ + @GetMapping("/status") + @Operation(summary = "查询异步导出任务状态") + public Map getStatus(@RequestParam String taskId) { + ExportTask task = exportTaskService.getTask(taskId); + Map result = new HashMap<>(); + if (task == null) { + result.put("exists", false); + return result; + } + result.put("exists", true); + result.put("taskId", task.getTaskId()); + result.put("status", task.getStatus().name()); + result.put("statusDesc", task.getStatus().getDesc()); + if (task.getErrorMessage() != null) { + result.put("errorMessage", task.getErrorMessage()); + } + return result; + } + + /** + * 接口三:下载导出文件(参数与 /exportData 一致 + taskId,文件名根据 tmDimension 生成) + */ + @PostMapping("/download") + @Operation(summary = "下载异步导出的 ZIP 文件") + public void download(@RequestParam String taskId, + @RequestParam(required = false) String tmDimension, + @RequestParam(required = false) String dataField, + @RequestParam(required = false) String months, + @RequestParam(required = false) String stcd, + @RequestParam(defaultValue = "true") boolean isDailyDimension, + @RequestParam(required = false) String yearStatType, + @RequestParam(required = false) String year, + @RequestParam(required = false) String baseId, + HttpServletResponse response) { + ExportTask task = exportTaskService.getTask(taskId); + if (task == null || task.getStatus() != ExportTask.Status.DONE) { + response.setStatus(404); + return; + } + + if (tmDimension == null || tmDimension.isEmpty()) { + tmDimension = "month"; + } + + String zipFileName = getFileNamePrefix(tmDimension) + ".zip"; + response.setContentType("application/zip"); + response.setCharacterEncoding("UTF-8"); + try { + response.setHeader("Content-Disposition", + "attachment; filename=" + URLEncoder.encode(zipFileName, "UTF-8")); + } catch (IOException e) { + response.setHeader("Content-Disposition", "attachment; filename=" + zipFileName); + } + + try (OutputStream os = response.getOutputStream()) { + exportTaskService.downloadTask(taskId, os); + } catch (IOException e) { + response.setStatus(500); + } + } + + // ======================== 工具方法 ======================== + + private static String getFileNamePrefix(String tmDimension) { + return switch (tmDimension) { + case "year" -> "年度报表"; + case "month" -> "月度报表"; + case "day" -> "日度报表"; + case "hour" -> "小时报表"; + default -> "未知报表"; + }; + } + private List parseCsv(String value) { if (StrUtil.isBlank(value)) { return Collections.emptyList(); diff --git a/backend/src/main/java/com/yfd/platform/qgc_export/domain/ExportTask.java b/backend/src/main/java/com/yfd/platform/qgc_export/domain/ExportTask.java new file mode 100644 index 00000000..60e7a9bb --- /dev/null +++ b/backend/src/main/java/com/yfd/platform/qgc_export/domain/ExportTask.java @@ -0,0 +1,58 @@ +package com.yfd.platform.qgc_export.domain; + +import java.io.File; + +/** + * 导出任务 — 用于异步导出 + */ +public class ExportTask { + + public enum Status { + CREATED("已创建"), + PROCESSING("处理中"), + DONE("已完成"), + ERROR("失败"); + + private final String desc; + + Status(String desc) { this.desc = desc; } + + public String getDesc() { return desc; } + } + + /** 任务唯一 ID */ + private String taskId; + /** 任务状态 */ + private Status status; + /** 导出完成的临时文件 */ + private File file; + /** ZIP 文件名 */ + private String fileName; + /** 错误信息(status=ERROR 时) */ + private String errorMessage; + /** 创建时间戳 */ + private long createTime; + + public ExportTask(String taskId) { + this.taskId = taskId; + this.status = Status.CREATED; + this.createTime = System.currentTimeMillis(); + } + + public String getTaskId() { return taskId; } + public void setTaskId(String taskId) { this.taskId = taskId; } + + public Status getStatus() { return status; } + public void setStatus(Status status) { this.status = status; } + + public File getFile() { return file; } + public void setFile(File file) { this.file = file; } + + public String getFileName() { return fileName; } + public void setFileName(String fileName) { this.fileName = fileName; } + + public String getErrorMessage() { return errorMessage; } + public void setErrorMessage(String errorMessage) { this.errorMessage = errorMessage; } + + public long getCreateTime() { return createTime; } +} diff --git a/backend/src/main/java/com/yfd/platform/qgc_export/service/ExportTaskService.java b/backend/src/main/java/com/yfd/platform/qgc_export/service/ExportTaskService.java new file mode 100644 index 00000000..c606d326 --- /dev/null +++ b/backend/src/main/java/com/yfd/platform/qgc_export/service/ExportTaskService.java @@ -0,0 +1,227 @@ +package com.yfd.platform.qgc_export.service; + +import com.yfd.platform.qgc_export.domain.ExportTask; +import jakarta.annotation.Resource; +import jakarta.servlet.ServletOutputStream; +import jakarta.servlet.WriteListener; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpServletResponseWrapper; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Service; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.nio.file.Files; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; + +/** + * 导出任务管理服务 — 异步导出 + 状态查询 + 文件下载 + */ +@Service +public class ExportTaskService { + + private static final Logger log = LoggerFactory.getLogger(ExportTaskService.class); + + @Resource + private IQgcExportService qgcExportService; + + /** 任务缓存(taskId → task) */ + private final Map taskMap = new ConcurrentHashMap<>(); + + /** + * 创建月度/小时/日导出任务,返回任务 ID + */ + public String submitMonthExport(List dataFields, List months, + List stationCodes, boolean isDailyDimension, + String tmDimension) { + String taskId = UUID.randomUUID().toString().replace("-", ""); + ExportTask task = new ExportTask(taskId); + task.setStatus(ExportTask.Status.PROCESSING); + taskMap.put(taskId, task); + + CompletableFuture.runAsync(() -> { + File tempFile = null; + try { + tempFile = File.createTempFile("qgc_export_", ".zip"); + FileOutputStream fos = new FileOutputStream(tempFile); + CapturingResponse capturing = new CapturingResponse(fos); + qgcExportService.exportData(dataFields, months, stationCodes, + isDailyDimension, tmDimension, capturing); + capturing.flushAndClose(); + fos.close(); + task.setFile(tempFile); + task.setStatus(ExportTask.Status.DONE); + log.info("导出任务 {} 完成,文件: {}", taskId, tempFile.getAbsolutePath()); + } catch (Exception e) { + log.error("导出任务 {} 失败", taskId, e); + task.setErrorMessage(e.getMessage()); + task.setStatus(ExportTask.Status.ERROR); + if (tempFile != null) { + try { Files.deleteIfExists(tempFile.toPath()); } catch (IOException ignored) {} + } + } + }); + + return taskId; + } + + /** + * 创建年度导出任务,返回任务 ID + */ + public String submitYearExport(List yearStatType, List years, + List stationCodes, List baseIds) { + String taskId = UUID.randomUUID().toString().replace("-", ""); + ExportTask task = new ExportTask(taskId); + task.setStatus(ExportTask.Status.PROCESSING); + taskMap.put(taskId, task); + + CompletableFuture.runAsync(() -> { + File tempFile = null; + try { + tempFile = File.createTempFile("qgc_export_", ".zip"); + FileOutputStream fos = new FileOutputStream(tempFile); + CapturingResponse capturing = new CapturingResponse(fos); + qgcExportService.exportYearData(yearStatType, years, stationCodes, + baseIds, capturing); + capturing.flushAndClose(); + fos.close(); + task.setFile(tempFile); + task.setStatus(ExportTask.Status.DONE); + log.info("年度导出任务 {} 完成,文件: {}", taskId, tempFile.getAbsolutePath()); + } catch (Exception e) { + log.error("年度导出任务 {} 失败", taskId, e); + task.setErrorMessage(e.getMessage()); + task.setStatus(ExportTask.Status.ERROR); + if (tempFile != null) { + try { Files.deleteIfExists(tempFile.toPath()); } catch (IOException ignored) {} + } + } + }); + + return taskId; + } + + /** + * 查询任务状态 + */ + public ExportTask getTask(String taskId) { + return taskMap.get(taskId); + } + + /** + * 将任务文件写入 OutputStream(下载),写完后删除临时文件和任务缓存 + */ + public void downloadTask(String taskId, OutputStream os) throws IOException { + ExportTask task = taskMap.get(taskId); + if (task == null || task.getStatus() != ExportTask.Status.DONE || task.getFile() == null) { + throw new IllegalStateException("任务未完成或文件不存在"); + } + try (FileInputStream fis = new FileInputStream(task.getFile())) { + byte[] buf = new byte[8192]; + int len; + while ((len = fis.read(buf)) != -1) { + os.write(buf, 0, len); + } + } finally { + try { Files.deleteIfExists(task.getFile().toPath()); } catch (IOException e) { + log.warn("删除临时文件失败: {}", task.getFile(), e); + } + taskMap.remove(taskId); + } + } + + /** + * 清理超时任务(可由定时任务调用) + */ + public void cleanExpiredTasks(long timeoutMs) { + long now = System.currentTimeMillis(); + taskMap.entrySet().removeIf(entry -> { + ExportTask task = entry.getValue(); + if (now - task.getCreateTime() > timeoutMs) { + if (task.getFile() != null) { + try { Files.deleteIfExists(task.getFile().toPath()); } catch (IOException ignored) {} + } + return true; + } + return false; + }); + } + + // ==================== 内部类 ==================== + + /** + * 最小化 HttpServletResponse — 基于 HttpServletResponseWrapper,仅重写导出用到的 5 个方法 + */ + private static class CapturingResponse extends HttpServletResponseWrapper { + private final FileOutputStream fos; + private final ServletOutputStream sos; + + /** 兜底 response,仅用于满足 super() 非 null 约束,实际所有调用都被本类重写 */ + private static final HttpServletResponse NOOP = new HttpServletResponse() { + @Override public ServletOutputStream getOutputStream() { throw new UnsupportedOperationException(); } + @Override public java.io.PrintWriter getWriter() { throw new UnsupportedOperationException(); } + @Override public String getCharacterEncoding() { return null; } + @Override public String getContentType() { return null; } + @Override public void setCharacterEncoding(String charset) {} + @Override public void setContentLength(int len) {} + @Override public void setContentLengthLong(long len) {} + @Override public void setContentType(String type) {} + @Override public void setBufferSize(int size) {} + @Override public int getBufferSize() { return 0; } + @Override public void flushBuffer() {} + @Override public void resetBuffer() {} + @Override public boolean isCommitted() { return false; } + @Override public void reset() {} + @Override public void setLocale(java.util.Locale loc) {} + @Override public java.util.Locale getLocale() { return java.util.Locale.getDefault(); } + @Override public void addCookie(jakarta.servlet.http.Cookie cookie) {} + @Override public boolean containsHeader(String name) { return false; } + @Override public String encodeURL(String url) { return url; } + @Override public String encodeRedirectURL(String url) { return url; } + @Override public void sendError(int sc, String msg) {} + @Override public void sendError(int sc) {} + @Override public void sendRedirect(String location) {} + @Override public void sendRedirect(String location, int sc, boolean clearBuffer) {} + @Override public void setDateHeader(String name, long date) {} + @Override public void addDateHeader(String name, long date) {} + @Override public void setHeader(String name, String value) {} + @Override public void addHeader(String name, String value) {} + @Override public void setIntHeader(String name, int value) {} + @Override public void addIntHeader(String name, int value) {} + @Override public void setStatus(int sc) {} + @Override public int getStatus() { return 200; } + @Override public String getHeader(String name) { return null; } + @Override public java.util.Collection getHeaders(String name) { return java.util.Collections.emptyList(); } + @Override public java.util.Collection getHeaderNames() { return java.util.Collections.emptyList(); } + }; + + CapturingResponse(FileOutputStream fos) { + super(NOOP); + this.fos = fos; + this.sos = new ServletOutputStream() { + @Override public void write(int b) throws IOException { fos.write(b); } + @Override public boolean isReady() { return true; } + @Override public void setWriteListener(WriteListener listener) {} + }; + } + + void flushAndClose() throws IOException { + sos.flush(); + fos.flush(); + } + + @Override public ServletOutputStream getOutputStream() { return sos; } + @Override public void setContentType(String type) {} + @Override public void setCharacterEncoding(String charset) {} + @Override public void setHeader(String name, String value) {} + @Override public String getCharacterEncoding() { return "UTF-8"; } + } +}