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 9cc53580..cc93de85 100644 --- a/backend/src/main/java/com/yfd/platform/config/SecurityConfig.java +++ b/backend/src/main/java/com/yfd/platform/config/SecurityConfig.java @@ -68,9 +68,10 @@ public class SecurityConfig { .requestMatchers("/dict/cache/**").permitAll() .requestMatchers("/sys/psbmodulelbb/**").permitAll() .requestMatchers("/base/operationLog/**").permitAll() - .requestMatchers("/system/**").permitAll() +// .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/config/SwaggerConfig.java b/backend/src/main/java/com/yfd/platform/config/SwaggerConfig.java index 8e63f008..3007f7e6 100644 --- a/backend/src/main/java/com/yfd/platform/config/SwaggerConfig.java +++ b/backend/src/main/java/com/yfd/platform/config/SwaggerConfig.java @@ -299,4 +299,12 @@ public class SwaggerConfig { .packagesToScan("com.yfd.platform.qgc_lygk.along.controller") .build(); } + + @Bean + public GroupedOpenApi groupExportApi() { + return GroupedOpenApi.builder() + .group("7. 综合导出") + .packagesToScan("com.yfd.platform.qgc_export.controller") + .build(); + } } \ No newline at end of file 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..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 @@ -14,8 +14,14 @@ public class EngVmsstbprptVo implements Serializable { private static final long serialVersionUID = 1L; private String id; + private String sttp; private String sttpCode; + private String sttpMap; + private String logo; 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..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 @@ -1001,68 +1001,269 @@ 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 +1271,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 +1541,7 @@ public class SdEngInfoBHServiceImpl extends ServiceImpl> 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_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_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/warn/service/impl/WarnDataServiceImpl.java b/backend/src/main/java/com/yfd/platform/qgc_env/warn/service/impl/WarnDataServiceImpl.java index e95e6602..cd11a195 100644 --- a/backend/src/main/java/com/yfd/platform/qgc_env/warn/service/impl/WarnDataServiceImpl.java +++ b/backend/src/main/java/com/yfd/platform/qgc_env/warn/service/impl/WarnDataServiceImpl.java @@ -1138,7 +1138,7 @@ public class WarnDataServiceImpl implements WarnDataService { "eng.LGTD AS lgtd, " + "eng.LTTD AS lttd, " + "eng.ELEV AS dtmel, " + - "eng.YRGEB AS ttpwr, " + + "pwr.TTPWR AS ttpwr, " + "'ENG' AS sttpMap, " + "CASE WHEN eng.PRSC IN (1, 2) THEN 1 WHEN eng.PRSC = 3 THEN 2 END AS endInstalledType, " + "CASE " + @@ -1153,6 +1153,7 @@ public class WarnDataServiceImpl implements WarnDataService { "NVL(rstSort.SORT, 999999) AS rstcdStepSort, " + "NVL(siteSort.SORT, 999999) AS siteStepSort " + "FROM SD_ENGINFO_B_H eng " + + "LEFT JOIN SD_PWR_B_H pwr ON pwr.STCD = eng.STCD AND NVL(pwr.IS_DELETED, 0) = 0 " + "LEFT JOIN SD_HYDROBASE hb ON hb.BASEID = eng.BASE_ID AND NVL(hb.IS_DELETED, 0) = 0 " + "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 ( " + 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_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/qgc_export/controller/QgcExportController.java b/backend/src/main/java/com/yfd/platform/qgc_export/controller/QgcExportController.java new file mode 100644 index 00000000..609cb3dc --- /dev/null +++ b/backend/src/main/java/com/yfd/platform/qgc_export/controller/QgcExportController.java @@ -0,0 +1,198 @@ +package com.yfd.platform.qgc_export.controller; + +import cn.hutool.core.util.StrUtil; +import com.yfd.platform.qgc_export.domain.ExportTask; +import com.yfd.platform.qgc_export.service.IQgcExportService; +import com.yfd.platform.qgc_export.service.ExportTaskService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.web.bind.annotation.*; + +import java.io.IOException; +import java.io.OutputStream; +import java.net.URLEncoder; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * 综合导出控制器 + */ +@RestController +@RequestMapping("/qgcExport") +@Tag(name = "综合导出") +public class QgcExportController { + + @Resource + private IQgcExportService qgcExportService; + + @Resource + private ExportTaskService exportTaskService; + + /** + * 原有同步导出接口(保留兼容) + */ + @PostMapping("/exportData") + @Operation(summary = "综合数据导出(ZIP 含多 Sheet Excel)— 同步返回文件流") + public void exportData(@RequestParam(required = false) String tmDimension, + @RequestParam(required = false) String dataField, + @RequestParam(required = false) String months, + @RequestParam(required = false) String stcd, + @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 (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); + } + + // ======================== 异步导出三接口 ======================== + + /** + * 接口一:创建导出任务,返回唯一任务 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(); + } + return Arrays.stream(value.split(",")) + .map(String::trim) + .filter(StrUtil::isNotBlank) + .collect(java.util.stream.Collectors.toList()); + } +} 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/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/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/SdRiverR.java b/backend/src/main/java/com/yfd/platform/qgc_export/domain/SdRiverR.java new file mode 100644 index 00000000..cf823808 --- /dev/null +++ b/backend/src/main/java/com/yfd/platform/qgc_export/domain/SdRiverR.java @@ -0,0 +1,69 @@ +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_RIVER_R + */ +@Data +@TableName("SD_RIVER_R") +public class SdRiverR implements Serializable { + + private static final long serialVersionUID = 1L; + + @TableId(type = IdType.INPUT) + private String id; + + /** 站码 */ + private String stcd; + + /** 时间 */ + private Date tm; + + /** 水位 (m) */ + private BigDecimal z; + + /** 流量 (m³/s) */ + private BigDecimal q; + + /** 流速 (m/s) */ + private BigDecimal v; + + /** 测流方法 */ + private String msqmt; + + /** 创建人 */ + private String recordUser; + + /** 创建时间 */ + private Date recordTime; + + /** 更新人 */ + private String modifyUser; + + /** 更新时间 */ + private Date modifyTime; + + /** 是否已删除: 0=未删除 1=已删除 */ + private Integer isDeleted; + + /** 删除人 */ + private String deleteUser; + + /** 删除时间 */ + private Date deleteTime; + + /** 附件ID */ + private String fid; + + /** 备注 */ + private String remark; +} diff --git a/backend/src/main/java/com/yfd/platform/qgc_export/domain/SdRiverdayS.java b/backend/src/main/java/com/yfd/platform/qgc_export/domain/SdRiverdayS.java new file mode 100644 index 00000000..2d2165a4 --- /dev/null +++ b/backend/src/main/java/com/yfd/platform/qgc_export/domain/SdRiverdayS.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_RIVERDAY_S + */ +@Data +@TableName("SD_RIVERDAY_S") +public class SdRiverdayS implements Serializable { + + private static final long serialVersionUID = 1L; + + @TableId(type = IdType.INPUT) + private String id; + + /** 站码 */ + private String stcd; + + /** 时间 */ + private Date dt; + + /** 水位 (m) */ + private BigDecimal z; + + /** 流量 (m³/s) */ + private BigDecimal q; + + /** 流速 (m/s) */ + private BigDecimal v; + + /** 测流方法 */ + private String msqmt; + + /** 创建人 */ + private String recordUser; + + /** 创建时间 */ + private Date recordTime; + + /** 更新人 */ + private String modifyUser; + + /** 更新时间 */ + private Date modifyTime; + + /** 是否已删除: 0=未删除 1=已删除 */ + private Integer isDeleted; + + /** 删除人 */ + private String deleteUser; + + /** 删除时间 */ + private Date deleteTime; +} 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/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/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/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/domain/StationMapping.java b/backend/src/main/java/com/yfd/platform/qgc_export/domain/StationMapping.java new file mode 100644 index 00000000..e360e38d --- /dev/null +++ b/backend/src/main/java/com/yfd/platform/qgc_export/domain/StationMapping.java @@ -0,0 +1,38 @@ +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 StationMapping implements Serializable { + + private static final long serialVersionUID = 1L; + + @TableField("STCD") + private String stcd; + + @TableField("RSTCD") + private String rstcd; + + @TableField("ENNM") + private String ennm; + + /** 测站类型(如 ZQ=河道水情, XX=生态流量等),用于区分同一电站下不同类型的测站 */ + @TableField("STTP") + private String sttp; + + /** 测站名称 */ + @TableField("STNM") + private String stnm; + + /** 所属水电基地名称 */ + @TableField("BASE_NAME") + private String baseName; +} 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/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/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/mapper/SdRiverRMapper.java b/backend/src/main/java/com/yfd/platform/qgc_export/mapper/SdRiverRMapper.java new file mode 100644 index 00000000..ed67b693 --- /dev/null +++ b/backend/src/main/java/com/yfd/platform/qgc_export/mapper/SdRiverRMapper.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.SdRiverR; +import org.apache.ibatis.annotations.Param; + +import java.util.List; +import java.util.Map; + +/** + * SD_RIVER_R 河道水情小时数据 Mapper + */ +public interface SdRiverRMapper extends BaseMapper { + + /** + * 批量查询站点在指定时间范围内的原始小时数据 + * + * @param stcdList 测站编码列表 + * @param fieldName 查询字段名(V/Q/Z) + * @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/SdRiverdaySMapper.java b/backend/src/main/java/com/yfd/platform/qgc_export/mapper/SdRiverdaySMapper.java new file mode 100644 index 00000000..9caf1cf1 --- /dev/null +++ b/backend/src/main/java/com/yfd/platform/qgc_export/mapper/SdRiverdaySMapper.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.SdRiverdayS; +import org.apache.ibatis.annotations.Param; + +import java.util.List; +import java.util.Map; + +/** + * SD_RIVERDAY_S 河道水情日数据 Mapper + */ +public interface SdRiverdaySMapper extends BaseMapper { + + /** + * 批量查询站点在指定时间范围内的原始日数据 + * + * @param stcdList 测站编码列表 + * @param fieldName 查询字段名(V/Q/Z) + * @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/mapper/SdWqRMapper.java b/backend/src/main/java/com/yfd/platform/qgc_export/mapper/SdWqRMapper.java new file mode 100644 index 00000000..433c6e62 --- /dev/null +++ b/backend/src/main/java/com/yfd/platform/qgc_export/mapper/SdWqRMapper.java @@ -0,0 +1,47 @@ +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); + + /** + * 查询水质达标数据(小时级),按 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); + + /** + * 查询 SD_WQ_R 表的字段注释(字段名 → 中文描述) + */ + List> selectColumnComments(); +} 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..ea974884 --- /dev/null +++ b/backend/src/main/java/com/yfd/platform/qgc_export/mapper/SdWqdaySMapper.java @@ -0,0 +1,35 @@ +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); + + /** + * 查询水质达标数据(日级),按 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/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/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/mapper/StationMappingMapper.java b/backend/src/main/java/com/yfd/platform/qgc_export/mapper/StationMappingMapper.java new file mode 100644 index 00000000..0a0bc456 --- /dev/null +++ b/backend/src/main/java/com/yfd/platform/qgc_export/mapper/StationMappingMapper.java @@ -0,0 +1,10 @@ +package com.yfd.platform.qgc_export.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.yfd.platform.qgc_export.domain.StationMapping; + +/** + * V_MS_STBPRP_T 视图 Mapper — 用于电站编码→测站编码转换 + */ +public interface StationMappingMapper extends BaseMapper { +} 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/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 new file mode 100644 index 00000000..6d5b4da5 --- /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 "水质(电导率)"; + } + + @Override + public int getDecimalPlaces() { + return 2; + } + + @Override + 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 new file mode 100644 index 00000000..023ce588 --- /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 "水质(溶解氧)"; + } + + @Override + public int getDecimalPlaces() { + return 2; + } + + @Override + 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 new file mode 100644 index 00000000..d8544f3a --- /dev/null +++ b/backend/src/main/java/com/yfd/platform/qgc_export/processor/DwtProcessor.java @@ -0,0 +1,27 @@ +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 "出库水温"; + } + + @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 new file mode 100644 index 00000000..87835dbf --- /dev/null +++ b/backend/src/main/java/com/yfd/platform/qgc_export/processor/DwtVariationProcessor.java @@ -0,0 +1,32 @@ +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; + } + + @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 new file mode 100644 index 00000000..a645d9de --- /dev/null +++ b/backend/src/main/java/com/yfd/platform/qgc_export/processor/DzProcessor.java @@ -0,0 +1,25 @@ +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 "坝下水位"; + } + + @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 new file mode 100644 index 00000000..da0e26bb --- /dev/null +++ b/backend/src/main/java/com/yfd/platform/qgc_export/processor/DzVariationProcessor.java @@ -0,0 +1,30 @@ +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; + } + + @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 new file mode 100644 index 00000000..c182b0c4 --- /dev/null +++ b/backend/src/main/java/com/yfd/platform/qgc_export/processor/ExportIndicatorProcessor.java @@ -0,0 +1,77 @@ +package com.yfd.platform.qgc_export.processor; + +import java.util.Date; +import java.util.List; + +/** + * 指标处理器接口 — 每种指标(v/q/z/wq/dwt/qec 等)实现此接口 + *

+ * 新增指标时:实现本接口 + 在 QgcExportServiceImpl.resolveProcessors() 中注入即可。 + */ +public interface ExportIndicatorProcessor { + + /** + * 指标字段名,与前端 dataField 中的值对应(如 "v", "q", "z", "qec") + */ + String getFieldName(); + + /** + * Excel Sheet 页中文标题(如 "流速(m/s)") + */ + String getSheetName(); + + /** + * 测站类型(STTP),用于区分同一电站下不同用途的测站。 + *

    + *
  • V/Q/Z 河道水情 → "ZQ"
  • + *
  • WQ 水质 → "WQ"
  • + *
  • DWT 出库水温 → "WTRV"
  • + *
  • RZ/DZ 坝上/坝下水位 → "ENG"
  • + *
  • QEC 生态流量 → 待定
  • + *
+ */ + default String getSttpType() { + return null; + } + + /** + * 数值保留的小数位数,默认 3 位 + */ + default int getDecimalPlaces() { + return 3; + } + + /** + * 是否有小时达标率指标(水质指标返回 true,在月均下方追加一行"小时达标率") + */ + default boolean hasComplianceRate() { + return false; + } + + /** + * 是否为日变幅格式(出库水温日变幅 Sheet)。 + * 日变幅格式:日数据行展示每日变幅(=当日最大-最小),汇总指标为"日变幅均值"和"日变幅最大值"。 + */ + default boolean isVariation() { + return false; + } + + /** + * Sheet 排序号,升序排列。 + * 数值越小越靠前,同组指标标准格式应排在日变幅格式之前。 + */ + default int getOrder() { + return 100; + } + + /** + * 查询原始数据 + * + * @param stcds 测站编码列表(已从电站编码转换,且已按 STTP 过滤) + * @param startTime 起始时间 + * @param endTime 结束时间 + * @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/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/IndicatorDataRow.java b/backend/src/main/java/com/yfd/platform/qgc_export/processor/IndicatorDataRow.java new file mode 100644 index 00000000..eca691d9 --- /dev/null +++ b/backend/src/main/java/com/yfd/platform/qgc_export/processor/IndicatorDataRow.java @@ -0,0 +1,35 @@ +package com.yfd.platform.qgc_export.processor; + +import java.math.BigDecimal; +import java.util.Date; + +/** + * 指标数据行 — 从数据库查询后的原始数据封装 + */ +public class IndicatorDataRow { + /** 测站编码 */ + private String stcd; + /** 数据时间 */ + private Date tm; + /** 指标值 */ + private BigDecimal value; + /** 是否达标(仅水质指标使用,0=不达标 1=达标) */ + private Integer sfdb; + + public IndicatorDataRow() {} + + public IndicatorDataRow(String stcd, Date tm, BigDecimal value) { + this.stcd = stcd; + this.tm = tm; + this.value = value; + } + + public String getStcd() { return stcd; } + public void setStcd(String stcd) { this.stcd = stcd; } + public Date getTm() { return tm; } + 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..8442bda4 --- /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 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 new file mode 100644 index 00000000..0446c615 --- /dev/null +++ b/backend/src/main/java/com/yfd/platform/qgc_export/processor/QProcessor.java @@ -0,0 +1,22 @@ +package com.yfd.platform.qgc_export.processor; + +import org.springframework.stereotype.Component; + +/** + * 流量(Q) 指标处理器 + */ +@Component +public class QProcessor extends RiverIndicatorProcessor { + + @Override + public String getFieldName() { return "Q"; } + + @Override + public String getSheetName() { return "流量(m³/s)"; } + + @Override + public String getSttpType() { return "ZQ"; } + + @Override + public int getOrder() { return 32; } +} 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/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/processor/RiverIndicatorProcessor.java b/backend/src/main/java/com/yfd/platform/qgc_export/processor/RiverIndicatorProcessor.java new file mode 100644 index 00000000..875ef01d --- /dev/null +++ b/backend/src/main/java/com/yfd/platform/qgc_export/processor/RiverIndicatorProcessor.java @@ -0,0 +1,58 @@ +package com.yfd.platform.qgc_export.processor; + +import com.yfd.platform.qgc_export.mapper.SdRiverRMapper; +import com.yfd.platform.qgc_export.mapper.SdRiverdaySMapper; + +import java.math.BigDecimal; +import java.text.SimpleDateFormat; +import java.util.*; + +/** + * 河道水情指标处理器基类 — V/Q/Z 三个指标共用查询逻辑 + *

+ * 子类(VProcessor/QProcessor/ZProcessor)需标注 @Component 并提供 getFieldName/getSheetName + */ +public abstract class RiverIndicatorProcessor implements ExportIndicatorProcessor { + + protected SdRiverRMapper sdRiverRMapper; + protected SdRiverdaySMapper sdRiverdaySMapper; + + private static final SimpleDateFormat SDF_DATETIME = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + + public void setMappers(SdRiverRMapper rMapper, SdRiverdaySMapper sMapper) { + this.sdRiverRMapper = rMapper; + this.sdRiverdaySMapper = sMapper; + } + + @Override + public List queryRawData(List stcds, Date startTime, Date endTime, boolean isDaily) { + String startStr = SDF_DATETIME.format(startTime); + String endStr = SDF_DATETIME.format(endTime); + + List> rawList; + if (isDaily) { + rawList = sdRiverdaySMapper.selectRawData(stcds, getFieldName(), startStr, endStr); + } else { + rawList = sdRiverRMapper.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 = null; + Object val = map.get("VALUE"); + if (val instanceof BigDecimal) { + value = (BigDecimal) val; + } else if (val instanceof Number) { + value = BigDecimal.valueOf(((Number) val).doubleValue()); + } + if (stcd != null && tm != null && value != null) { + rows.add(new IndicatorDataRow(stcd, tm, value)); + } + } + } + return rows; + } +} 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..6c7170ed --- /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 7; + } + + @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/processor/RzProcessor.java b/backend/src/main/java/com/yfd/platform/qgc_export/processor/RzProcessor.java new file mode 100644 index 00000000..42e4ccea --- /dev/null +++ b/backend/src/main/java/com/yfd/platform/qgc_export/processor/RzProcessor.java @@ -0,0 +1,25 @@ +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 "坝上水位"; + } + + @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 new file mode 100644 index 00000000..f74d3627 --- /dev/null +++ b/backend/src/main/java/com/yfd/platform/qgc_export/processor/RzVariationProcessor.java @@ -0,0 +1,30 @@ +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; + } + + @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 new file mode 100644 index 00000000..36e67231 --- /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 "水质(浊度)"; + } + + @Override + public int getDecimalPlaces() { + return 2; + } + + @Override + 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 new file mode 100644 index 00000000..330c68fb --- /dev/null +++ b/backend/src/main/java/com/yfd/platform/qgc_export/processor/VProcessor.java @@ -0,0 +1,22 @@ +package com.yfd.platform.qgc_export.processor; + +import org.springframework.stereotype.Component; + +/** + * 流速(V) 指标处理器 + */ +@Component +public class VProcessor extends RiverIndicatorProcessor { + + @Override + public String getFieldName() { return "V"; } + + @Override + public String getSheetName() { return "流速(m/s)"; } + + @Override + public String getSttpType() { return "ZQ"; } + + @Override + public int getOrder() { return 33; } +} 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..ad80b4c0 --- /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 "WQ"; + } + + @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..2217aa2d --- /dev/null +++ b/backend/src/main/java/com/yfd/platform/qgc_export/processor/WtmpProcessor.java @@ -0,0 +1,30 @@ +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 int getOrder() { + return 10; + } +} 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/processor/ZProcessor.java b/backend/src/main/java/com/yfd/platform/qgc_export/processor/ZProcessor.java new file mode 100644 index 00000000..abf293d8 --- /dev/null +++ b/backend/src/main/java/com/yfd/platform/qgc_export/processor/ZProcessor.java @@ -0,0 +1,22 @@ +package com.yfd.platform.qgc_export.processor; + +import org.springframework.stereotype.Component; + +/** + * 水位(Z) 指标处理器 + */ +@Component +public class ZProcessor extends RiverIndicatorProcessor { + + @Override + public String getFieldName() { return "Z"; } + + @Override + public String getSheetName() { return "水位(m)"; } + + @Override + public String getSttpType() { return "ZQ"; } + + @Override + public int getOrder() { return 31; } +} 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"; } + } +} 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 new file mode 100644 index 00000000..968a0856 --- /dev/null +++ b/backend/src/main/java/com/yfd/platform/qgc_export/service/IQgcExportService.java @@ -0,0 +1,42 @@ +package com.yfd.platform.qgc_export.service; + +import jakarta.servlet.http.HttpServletResponse; +import java.util.List; + +/** + * 综合导出服务接口 + */ +public interface IQgcExportService { + + /** + * 执行综合导出,生成 ZIP 并写入 HttpServletResponse + * + * @param dataFields 指标字段列表(如 ["v","q","z"]) + * @param months 月份列表(如 ["2026-08","2026-07"]) + * @param stationCodes 电站编码列表(RSTCD,前端传入) + * @param isDailyDimension 是否查日表 + * @param tmDimension 时间维度 (hour/day/month/year) + * @param response HttpServletResponse + */ + void exportData(List dataFields, + List months, + List stationCodes, + 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 new file mode 100644 index 00000000..e3d8497f --- /dev/null +++ b/backend/src/main/java/com/yfd/platform/qgc_export/service/impl/QgcExportServiceImpl.java @@ -0,0 +1,2134 @@ +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; +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.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.*; +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; +import jakarta.annotation.Resource; +import jakarta.servlet.http.HttpServletResponse; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Service; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.text.SimpleDateFormat; +import java.util.*; +import java.util.stream.Collectors; + +/** + * 综合导出服务实现(月度报表) + *

+ * 格式:每月一个 Excel 文件 → ZIP 下载。 + * 每个 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 结构

+ *
+ *     日期        | 班多      | 羊曲       ← 表头(Row 0)
+ *   01           | 2757.711  | 2709.839  ← 日数据(Row 1~31)
+ *   02           | 2757.806  | 2709.794
+ *   ...
+ *   日均最低      | 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%     ← 仅水质指标有此行
+ * 
+ */ +@Service +public class QgcExportServiceImpl implements IQgcExportService { + + private static final Logger log = LoggerFactory.getLogger(QgcExportServiceImpl.class); + + @Resource + private StationMappingMapper stationMappingMapper; + + // V/Q/Z 相关 + @Resource + private SdRiverRMapper sdRiverRMapper; + @Resource + private SdRiverdaySMapper sdRiverdaySMapper; + @Resource + private VProcessor vProcessor; + @Resource + private QProcessor qProcessor; + @Resource + private ZProcessor zProcessor; + + // WQ 相关 + @Resource + private SdWqRMapper sdWqRMapper; + @Resource + private SdWqdaySMapper sdWqdaySMapper; + @Resource + private WtmpProcessor wtmpProcessor; + @Resource + private CodcrProcessor codcrProcessor; + @Resource + private PhProcessor phProcessor; + @Resource + private DoxProcessor doxProcessor; + @Resource + private CondProcessor condProcessor; + @Resource + private TuProcessor tuProcessor; + + // DWT 出库水温相关 + @Resource + private SdWtrvRMapper sdWtrvRMapper; + @Resource + private SdWtrvdaySMapper sdWtrvdaySMapper; + @Resource + private DwtProcessor dwtProcessor; + @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; + + // QI/QO 出入库流量相关(STTP=ENG) + @Resource + private QiProcessor qiProcessor; + @Resource + private QoProcessor qoProcessor; + + // QEC 生态流量相关(STTP=ENG) + @Resource + private SdQecRMapper sdQecRMapper; + @Resource + private SdQecdaySMapper sdQecdaySMapper; + @Resource + private QecProcessor qecProcessor; + + @Resource + private EngEqDataService engEqDataService; + + @Resource + private MsAlongdetBMapper msAlongdetBMapper; + + @Resource + private SdWtvtRMapper sdWtvtRMapper; + @Resource + private SdWtvtdaySMapper sdWtvtdaySMapper; + + @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个) + */ + private static final List BASE_LABELS = Collections.unmodifiableList( + Arrays.asList("日均最低", "日均最高", "月内最低", "月内最低时间", "月内最高", "月内最高时间", "月均")); + /** + * 小时达标率标签 + */ + private static final String LABEL_COMPLIANCE = "小时达标率"; + + @Override + 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()) { + log.warn("未找到匹配的指标处理器,dataFields={}", dataFields); + return; + } + + // 2. 收集所有需要的 STTP 类型 + Set sttpTypes = processors.stream() + .map(ExportIndicatorProcessor::getSttpType) + .collect(Collectors.toCollection(LinkedHashSet::new)); + + // 3. 电站 → 测站映射 + StationResolution stationRes = resolveStations(stationCodes, sttpTypes); + if (stationRes.displayNames.isEmpty()) { + log.warn("未找到任何电站映射数据,导出取消"); + return; + } + + // 4. 电站顺序 + 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); + if (mr == null) continue; + + List sheets = new ArrayList<>(); + // 判断是否包含水质指标,后续用于追加 WQ 合规 Sheet + boolean hasWqIndicator = false; + for (ExportIndicatorProcessor processor : processors) { + List> rows; + // 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); + } + if (rows != null) { + List headers; + String sttp = processor.getSttpType(); + 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 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<>(); + 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); + } + } + } + } + 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} // 多年平均 + )); + } 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)) { + 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); + } + } + + if (!sheets.isEmpty()) { + excelList.add(new ExportZipUtil.ExcelData(month + "统计数据_" + timestamp, sheets)); + } + } + + if (excelList.isEmpty()) { + log.warn("没有可导出的数据"); + return; + } + + String zipFileName = getFileNamePrefix(tmDimension) + ".zip"; + ExportZipUtil.exportToResponse(response, zipFileName, excelList); + } + + // ======================== 构建 Sheet 数据 ======================== + + 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<>(); + 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; + } + + 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); + + // 汇总聚合(含达标率) + Map aggMap = aggregate(dailyRows, hourlyRows, rstcdToStcd, processor.hasComplianceRate()); + + // 日数据索引 + Map> dailyByStcd = indexDailyByDay(dailyRows); + + // 指标标签 + List labels = new ArrayList<>(BASE_LABELS); + if (processor.hasComplianceRate()) { + labels.add(LABEL_COMPLIANCE); + } + + 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 : rstcdToStcd.keySet()) { + String stcd = rstcdToStcd.get(rstcd); + Map dayMap = dailyByStcd.get(stcd); + BigDecimal val = dayMap != null ? dayMap.get(d) : null; + row.add(formatOrEmpty(val, decimals)); + } + rows.add(row); + } + + // ---- 汇总指标行 ---- + for (int i = 0; i < labels.size(); i++) { + List row = new ArrayList<>(); + row.add(labels.get(i)); + + 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; + } + } + rows.add(row); + } + + 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); // 子标题作为第一行 + + // 批量查询各测站的生态流量限值 + 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(rule != null && rule.getEqmnStr() != null ? rule.getEqmnStr() : ""); + row.add(s != null ? s.dailyHb : ""); + row.add(s != null ? s.hourlyHb : ""); + row.add(rule != null && rule.getVlsr() != null ? rule.getVlsr() : ""); + // 水利部 + row.add(displayName != null ? displayName : rstcd); + row.add(baseName); + row.add(rule != null && rule.getEqmnmwrStr() != null ? rule.getEqmnmwrStr() : ""); + row.add(s != null ? s.dailySl : ""); + row.add(s != null ? s.hourlySl : ""); + row.add(rule != null && rule.getMwrVlsr() != null ? rule.getMwrVlsr() : ""); + // 多年平均 + row.add(displayName != null ? displayName : rstcd); + row.add(baseName); + row.add(rule != null && rule.getAvqStr() != null ? rule.getAvqStr() : ""); + row.add(s != null ? s.dailyDn : ""); + row.add(s != null ? s.hourlyDn : ""); + row.add(rule != null && rule.getAvqVlsr() != null ? rule.getAvqVlsr() : ""); + + 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; + } + } + + // ======================== WQ 水质达标率 Sheet(双表头) ======================== + + /** + * 构建水质达标率 Sheet 的数据行(不含标题行,标题由调用方放入 headers)。 + *

+ * 返回的 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) { + + 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); + // 查询主要污染物(从小时表 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) { + hourlyMap.put((String) m.get("STCD"), m); + } + Map> dailyMap = new LinkedHashMap<>(); + for (Map m : dailyData) { + dailyMap.put((String) m.get("STCD"), m); + } + + // ======== 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; + String hourlyHb, hourlySl, hourlyDn; + } + + // ======================== 指标处理器匹配 ======================== + + /** + * 注册顺序决定 Sheet 在 Excel 中的顺序: + * 水质指标(水温→PH→溶解氧→电导率→浊度)→ 河道水情(流速→流量→水位) + *

+ * 支持别名:前端传 "wq" 等价于传 "wtmp,ph,dox,cond,tu"。 + * 前端传 "dwt" 展开为出库水温 + 出入库水温日变幅两个 Sheet。 + */ + private List resolveProcessors(List dataFields) { + 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); + processorMap.put("tu", tuProcessor); + // V/Q/Z 在后 + processorMap.put("v", vProcessor); + processorMap.put("q", qProcessor); + processorMap.put("z", zProcessor); + // 出入库流量(STTP=ENG,仅标准格式无日变幅) + processorMap.put("qi", qiProcessor); + processorMap.put("qo", qoProcessor); + // 生态流量(STTP=ENG,仅标准格式无日变幅) + processorMap.put("qec", qecProcessor); + // 垂向水温(STTP=WTVT, 双表头分层格式) + processorMap.put("rwt", rwtProcessor); + + // WQ 别名:展开为全部水质指标 + List expanded = new ArrayList<>(); + for (String field : dataFields) { + String lower = field.trim().toLowerCase(); + if ("wq".equals(lower)) { + expanded.addAll(List.of("wtmp", "codcr", "ph", "dox", "cond", "tu")); + } else { + expanded.add(lower); + } + } + + 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; + } + +// // 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); + 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) { + if (p instanceof RiverIndicatorProcessor) { + ((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); + } else if (p instanceof QecProcessor) { + ((QecProcessor) p).setMappers(sdQecRMapper, sdQecdaySMapper); + } else if (p instanceof RwtProcessor) { + ((RwtProcessor) p).setMappers(sdWtvtRMapper, sdWtvtdaySMapper); + } + result.add(p); + } + } + result.sort(Comparator.comparingInt(ExportIndicatorProcessor::getOrder)); + return result; + } + + // ======================== 电站映射 ======================== + + 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) + .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 (!"ENG".equals(m.getSttp()) && m.getRstcd() == null) { + continue; + } + byRstcd.computeIfAbsent(m.getRstcd(), k -> new ArrayList<>()).add(m); + } + + for (String rstcd : rstcds) { + List rows = byRstcd.get(rstcd); + if (rows == null || rows.isEmpty()) { + result.displayNames.put(rstcd, rstcd); + result.sttpStcdMap.put(rstcd, new LinkedHashMap<>()); + continue; + } + + // 默认测站名称:按请求的 STTP 类型优先级 → 任意 STNM → ENNM → RSTCD + String defaultDisplayName = null; + for (String sttp : sttpTypes) { + for (StationMapping m : rows) { + 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 (defaultDisplayName == null) { + defaultDisplayName = rows.get(0).getEnnm(); + } + if (defaultDisplayName == null) { + defaultDisplayName = rstcd; + } + 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<>(); + for (StationMapping m : rows) { + String sttp = m.getSttp(); + if (sttp != null && !sttpMap.containsKey(sttp)) { + sttpMap.put(sttp, m.getStcd()); + } + } + 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); + return result; + } + + // ======================== 月份范围 ======================== + + private MonthRange buildMonthRange(String month) { + try { + String[] parts = month.split("-"); + int year = Integer.parseInt(parts[0]); + int mon = Integer.parseInt(parts[1]); + + Calendar cal = Calendar.getInstance(); + cal.set(year, mon - 1, 1, 0, 0, 0); + cal.set(Calendar.MILLISECOND, 0); + Date start = cal.getTime(); + + cal.set(year, mon, 1, 0, 0, 0); + cal.set(Calendar.MILLISECOND, 0); + Date end = cal.getTime(); + + return new MonthRange(month, start, end); + } catch (Exception e) { + log.warn("解析月份失败: {}", month, e); + return null; + } + } + + 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( + List dailyRows, List hourlyRows, + Map rstcdToStcd, boolean calcCompliance) { + + Map result = new LinkedHashMap<>(); + + for (String rstcd : rstcdToStcd.keySet()) { + 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) + .collect(Collectors.toList()); + 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()); + if (!hourlyStationData.isEmpty()) { + IndicatorDataRow minRow = Collections.min(hourlyStationData, + Comparator.comparing(IndicatorDataRow::getValue)); + IndicatorDataRow maxRow = Collections.max(hourlyStationData, + Comparator.comparing(IndicatorDataRow::getValue)); + agg.setExtremeMin(minRow.getValue()); + agg.setExtremeMinTime(minRow.getTm()); + agg.setExtremeMax(maxRow.getValue()); + 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; + } + + 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 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 : rstcdToStcd.keySet()) { + String stcd = rstcdToStcd.get(rstcd); + Map dayMap = variationByStcd.get(stcd); + 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 : rstcdToStcd.keySet()) { + 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(); + } + + private static String formatComplianceRate(BigDecimal value) { + if (value == null) return ""; + return value.setScale(1, 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 { + /** + * 默认测站名称(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<>(); + /** + * 所属水电基地名称,RSTCD → {STTP → BASE_NAME} + */ + final Map> baseNameMap = new LinkedHashMap<>(); + + /** + * 获取默认测站名称 + */ + String getDisplayName(String rstcd) { + return displayNames.getOrDefault(rstcd, rstcd); + } + + /** + * 获取指定 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 null; +// return getDisplayName(rstcd); + } + + /** + * 获取指定 STTP 的测站编码。 + */ + String getStcd(String rstcd, String sttp) { + Map inner = sttpStcdMap.get(rstcd); + 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 + private static class MonthRange { + private final String label; + private final Date start; + private final Date end; + } + + @lombok.Data + private static class Aggregation { + private BigDecimal dailyMin; + private BigDecimal dailyMax; + private BigDecimal extremeMin; + private Date extremeMinTime; + 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()); + + // 预查询测站信息(年维度无关,一次查询即可) + // 先查所有电站名称(不限 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()) { + // 查询 V_MS_STBPRP_T 中 MWAY=2(自动站)的 WQ 测站 + List dbWqStations = yearStatMapper.selectList( + new LambdaQueryWrapper() + .eq(StationInfo::getSttp, "WQ") + .eq(StationInfo::getMway, 2) + .and(w -> w.in(StationInfo::getRstcd, stationCodes) + .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); + } + } + 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 ""; + } + + /** + * 按沿程排序测站(通过 STCD 匹配沿程配置表) + */ + private List sortStationsByOrder(List stations) { + // 收集所有测站的 STCD(沿程表匹配的是 STCD 测站编码) + List stcds = stations.stream().map(StationInfo::getStcd).filter(Objects::nonNull).distinct().toList(); + if (stcds.isEmpty()) return stations; + + // 查询沿程排序(MS_ALONGDET_B.STCD + MS_ALONG_B.ORDER_INDEX) + List> orderList = msAlongdetBMapper.selectAlongSortOrder(stcds); + Map orderMap = new LinkedHashMap<>(); + for (Map m : orderList) { + 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.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; + return 0; + }); + return sorted; + } + + // ======================== 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; + } + } +} 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/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/java/com/yfd/platform/system/service/impl/UserServiceImpl.java b/backend/src/main/java/com/yfd/platform/system/service/impl/UserServiceImpl.java index 96172af7..64e6220e 100644 --- a/backend/src/main/java/com/yfd/platform/system/service/impl/UserServiceImpl.java +++ b/backend/src/main/java/com/yfd/platform/system/service/impl/UserServiceImpl.java @@ -641,11 +641,38 @@ public class UserServiceImpl extends ServiceImpl impleme Page page) { Page mapPage = sysUserMapper.queryUsers(orgid, username, tenantId, page); - mapPage.getRecords().forEach(record -> { - String id = record.getId(); - List sysRoles = sysRoleMapper.getRoleByUserId(id); - record.setRoles(sysRoles); - }); + List records = mapPage.getRecords(); + if (records.isEmpty()) { + return mapPage; + } + + // 收集所有用户ID,一次批量查询角色(替代 N+1 查询) + List userIds = records.stream() + .map(SysUser::getId) + .collect(Collectors.toList()); + List> allRoleMaps = sysRoleMapper.getUserRolesByUserIds(userIds); + + // 按 userId 分组,将 Map 转为 SysRole 对象 + Map> userRoleMap = new HashMap<>(); + if (allRoleMaps != null) { + for (Map roleMap : allRoleMaps) { + String userId = (String) roleMap.get("userId"); + if (userId == null) continue; + SysRole role = new SysRole(); + role.setId((String) roleMap.get("id")); + role.setRolecode((String) roleMap.get("rolecode")); + role.setRolename((String) roleMap.get("rolename")); + role.setLevel((String) roleMap.get("level")); + role.setDescription((String) roleMap.get("description")); + role.setIsvaild((String) roleMap.get("isvaild")); + role.setOrgscope((String) roleMap.get("orgscope")); + role.setOptscope((String) roleMap.get("optscope")); + role.setBusscope((String) roleMap.get("busscope")); + userRoleMap.computeIfAbsent(userId, k -> new ArrayList<>()).add(role); + } + } + + records.forEach(r -> r.setRoles(userRoleMap.getOrDefault(r.getId(), Collections.emptyList()))); return mapPage; } diff --git a/backend/src/main/java/com/yfd/platform/utils/ExportZipUtil.java b/backend/src/main/java/com/yfd/platform/utils/ExportZipUtil.java new file mode 100644 index 00000000..6670a6af --- /dev/null +++ b/backend/src/main/java/com/yfd/platform/utils/ExportZipUtil.java @@ -0,0 +1,765 @@ +package com.yfd.platform.utils; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.apache.poi.ss.usermodel.*; +import org.apache.poi.ss.util.CellRangeAddress; +import org.apache.poi.xssf.streaming.SXSSFSheet; +import org.apache.poi.xssf.streaming.SXSSFWorkbook; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import jakarta.servlet.ServletOutputStream; +import jakarta.servlet.http.HttpServletResponse; +import java.io.*; +import java.net.URLEncoder; +import java.util.*; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** + * 综合导出工具类 — 将多维度数据导出为 ZIP 压缩包,内含若干 Excel (.xlsx) 文件,每个 Excel 支持多 Sheet 页。 + * + *

使用示例

+ *
{@code
+ * // 1. 构建 Sheet 数据
+ * SheetData stationSheet = new SheetData("电站基础信息",
+ *         Arrays.asList("测站编码", "测站名称", "经度", "纬度"),
+ *         stationRows);  // List>
+ *
+ * SheetData monitorSheet = new SheetData("监测数据",
+ *         Arrays.asList("时间", "水位", "流量"),
+ *         monitorRows);
+ *
+ * // 2. 构建 Excel 数据(一个 Excel = 多个 Sheet)
+ * ExcelData excel1 = new ExcelData("电站数据", Arrays.asList(stationSheet, monitorSheet));
+ * ExcelData excel2 = new ExcelData("流域数据", Arrays.asList(basinSheet));
+ *
+ * // 3. 导出到 HttpServletResponse
+ * ExportZipUtil.exportToResponse(response, "数据导出_20260731.zip", Arrays.asList(excel1, excel2));
+ *
+ * // 或导出到文件
+ * ExportZipUtil.exportToFile(new File("D:/export.zip"), Arrays.asList(excel1, excel2));
+ *
+ * // 类型安全的构建方式
+ * SheetData sheet = ExportZipUtil.fromTypedData("电站信息",
+ *         Arrays.asList("编码", "名称", "经度"),
+ *         Arrays.asList(Station::getStcd, Station::getStnm, Station::getLgtd),
+ *         stationList);
+ * }
+ * + * @author system + * @since 2026-07-31 + */ +public class ExportZipUtil { + + private static final Logger log = LoggerFactory.getLogger(ExportZipUtil.class); + + private ExportZipUtil() { + // 工具类,禁止实例化 + } + + // ======================== 数据模型 ======================== + + /** + * 单个 Sheet 页数据定义 + */ + @Data + @NoArgsConstructor + @AllArgsConstructor + public static class SheetData { + /** Sheet 页名称 */ + private String sheetName; + /** 表头列名集合 */ + 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; + } + } + + /** + * 单个 Excel 文件数据定义(一个 Excel = 多个 Sheet) + */ + @Data + @NoArgsConstructor + @AllArgsConstructor + public static class ExcelData { + /** Excel 文件名(不含 .xlsx 扩展名,在 ZIP 中自动添加) */ + private String fileName; + /** 该 Excel 包含的所有 Sheet 页 */ + private List sheets; + } + + // ======================== 分组表头数据模型 ======================== + + /** + * 带分组表头的 Sheet 页数据定义(支持合并单元格) + * + *

生成的 Excel 结构: + *

+     * Row 0: [rowHeaderLabel(合并)] [group0Name(合并 subCols 列)] [group1Name(合并 subCols 列)] ...
+     * Row 1: [                  ] [sub0] [sub1] ... [subN]       [sub0] [sub1] ... [subN]     ...
+     * Row 2+: rowLabel0          data...
+     * Row 3 : rowLabel1          data...
+     * 

+ * + *

使用示例

+ *
{@code
+     * GroupedSheetData sheet = new GroupedSheetData("流速(m/s)", "日期",
+     *         Arrays.asList("XX水电站", "YY水电站"),
+     *         Arrays.asList("日均最低", "日均最高", "月内最低", "月内最低时间", "月内最高", "月内最高时间"));
+     * // 每行:[rowLabel, g0_sub0, g0_sub1, ..., g0_sub5, g1_sub0, ...]
+     * sheet.getRows().add(Arrays.asList("2026-08", 0.5, 1.2, 0.3, "2026-08-15 03:00:00", 2.1, "...", ...));
+     * }
+ */ + @Data + @NoArgsConstructor + public static class GroupedSheetData { + /** Sheet 页名称 */ + private String sheetName; + /** 行标题列名(如"日期",该列在 Row0+Row1 纵向合并) */ + private String rowHeaderLabel; + /** 分组名称列表(如["XX水电站","YY水电站"]) */ + private List groupNames; + /** 子表头列表(每个分组下都有相同的子表头) */ + private List subHeaders; + /** 数据行:每行为 [rowLabel, group0.sub0, group0.sub1, ..., group0.subN, group1.sub0, ...] */ + private List> rows = new ArrayList<>(); + + public GroupedSheetData(String sheetName, String rowHeaderLabel, + List groupNames, List subHeaders) { + this.sheetName = sheetName; + this.rowHeaderLabel = rowHeaderLabel; + this.groupNames = groupNames; + this.subHeaders = subHeaders; + } + } + + // ======================== 核心导出方法 ======================== + + /** + * 导出 ZIP 压缩包到 HttpServletResponse(浏览器下载) + * + * @param response HttpServletResponse + * @param zipFileName 下载文件名(含 .zip 扩展名) + * @param excelList 要导出的 Excel 数据集合 + */ + public static void exportToResponse(HttpServletResponse response, String zipFileName, List excelList) { + response.setContentType("application/zip"); + response.setCharacterEncoding("UTF-8"); + try { + response.setHeader("Content-Disposition", + "attachment; filename=" + URLEncoder.encode(zipFileName, "UTF-8")); + } catch (UnsupportedEncodingException e) { + response.setHeader("Content-Disposition", "attachment; filename=export.zip"); + } + + try (ServletOutputStream os = response.getOutputStream()) { + exportToStream(os, excelList); + os.flush(); + } catch (IOException e) { + log.error("导出 ZIP 到 Response 失败", e); + throw new RuntimeException("导出失败: " + e.getMessage(), e); + } + } + + /** + * 导出 ZIP 压缩包到本地文件 + * + * @param targetFile 目标文件 + * @param excelList 要导出的 Excel 数据集合 + */ + public static void exportToFile(File targetFile, List excelList) { + try (FileOutputStream fos = new FileOutputStream(targetFile)) { + exportToStream(fos, excelList); + } catch (IOException e) { + log.error("导出 ZIP 到文件失败: {}", targetFile.getAbsolutePath(), e); + throw new RuntimeException("导出失败: " + e.getMessage(), e); + } + } + + /** + * 导出 ZIP 压缩包到任意 OutputStream + *

+ * 实现原理:先将每个 Excel 写入临时字节数组,再用内存 ZIP 打包,避免磁盘 IO。 + * + * @param os 目标输出流 + * @param excelList 要导出的 Excel 数据集合 + */ + public static void exportToStream(OutputStream os, List excelList) throws IOException { + if (excelList == null || excelList.isEmpty()) { + log.warn("导出数据为空,跳过 ZIP 生成"); + return; + } + + // 延迟创建 ZIP,累计所有 Excel 字节数组 + ByteArrayOutputStream zipBaos = new ByteArrayOutputStream(); + try (java.util.zip.ZipOutputStream zos = new java.util.zip.ZipOutputStream(zipBaos)) { + for (ExcelData excelData : excelList) { + if (excelData.getSheets() == null || excelData.getSheets().isEmpty()) { + continue; + } + byte[] excelBytes = workbookToBytes(createWorkbook(excelData)); + String entryName = sanitizeFileName(excelData.getFileName()) + ".xlsx"; + + java.util.zip.ZipEntry entry = new java.util.zip.ZipEntry(entryName); + zos.putNextEntry(entry); + zos.write(excelBytes); + zos.closeEntry(); + } + zos.finish(); + } + + os.write(zipBaos.toByteArray()); + } + + // ======================== 分组表头导出方法 ======================== + + /** + * 导出分组表头数据为 ZIP(每个 GroupedSheetData 生成一个独立的 Excel 文件在 ZIP 中) + * + * @param response HttpServletResponse + * @param zipFileName 下载文件名 + * @param sheets 分组表头数据列表(每个元素 = ZIP 中一个 .xlsx 文件) + */ + public static void exportGroupedToResponse(HttpServletResponse response, String zipFileName, + List sheets) { + response.setContentType("application/zip"); + response.setCharacterEncoding("UTF-8"); + try { + response.setHeader("Content-Disposition", + "attachment; filename=" + URLEncoder.encode(zipFileName, "UTF-8")); + } catch (UnsupportedEncodingException e) { + response.setHeader("Content-Disposition", "attachment; filename=export.zip"); + } + try (ServletOutputStream os = response.getOutputStream()) { + exportGroupedToStream(os, sheets); + os.flush(); + } catch (IOException e) { + log.error("导出分组 ZIP 到 Response 失败", e); + throw new RuntimeException("导出失败: " + e.getMessage(), e); + } + } + + /** + * 导出分组表头数据 ZIP 到任意 OutputStream + *

+ * 每个 GroupedSheetData 作为一个 .xlsx 文件打入 ZIP。 + */ + public static void exportGroupedToStream(OutputStream os, List sheets) throws IOException { + if (sheets == null || sheets.isEmpty()) { + log.warn("导出数据为空,跳过 ZIP 生成"); + return; + } + + ByteArrayOutputStream zipBaos = new ByteArrayOutputStream(); + try (java.util.zip.ZipOutputStream zos = new java.util.zip.ZipOutputStream(zipBaos)) { + for (GroupedSheetData sheet : sheets) { + SXSSFWorkbook workbook = new SXSSFWorkbook(100); + try { + fillGroupedSheet(workbook, sheet); + byte[] bytes = workbookToBytes(workbook); + String entryName = sanitizeFileName(sheet.getSheetName()) + ".xlsx"; + java.util.zip.ZipEntry entry = new java.util.zip.ZipEntry(entryName); + zos.putNextEntry(entry); + zos.write(bytes); + zos.closeEntry(); + } finally { + workbook.close(); + workbook.dispose(); + } + } + zos.finish(); + } + os.write(zipBaos.toByteArray()); + } + + /** + * 向 Workbook 中填充一个带分组表头的 Sheet + * + * @param workbook 目标 Workbook + * @param data 分组表头数据 + */ + public static void fillGroupedSheet(Workbook workbook, GroupedSheetData data) { + String name = safeName(data.getSheetName()); + Sheet sheet = workbook.createSheet(name); + if (sheet instanceof SXSSFSheet) { + ((SXSSFSheet) sheet).trackAllColumnsForAutoSizing(); + } + + CellStyle groupHeaderStyle = createHeaderStyle(workbook); + CellStyle subHeaderStyle = createSubHeaderStyle(workbook); + CellStyle dataStyle = createDataStyle(workbook); + + int groupCount = data.getGroupNames() != null ? data.getGroupNames().size() : 0; + int subCols = data.getSubHeaders() != null ? data.getSubHeaders().size() : 0; + int totalCols = 1 + groupCount * subCols; // 1 = rowHeaderLabel + + // Row 0: 分组表头 + Row groupRow = sheet.createRow(0); + groupRow.setHeightInPoints(24); + + Cell labelCell = groupRow.createCell(0); + labelCell.setCellValue(data.getRowHeaderLabel()); + labelCell.setCellStyle(groupHeaderStyle); + // 行标题列 Row0+Row1 合并 + if (data.getRowHeaderLabel() != null) { + sheet.addMergedRegion(new CellRangeAddress(0, 1, 0, 0)); + } + + int colIdx = 1; + for (int g = 0; g < groupCount; g++) { + String groupName = data.getGroupNames().get(g); + Cell gc = groupRow.createCell(colIdx); + gc.setCellValue(groupName); + gc.setCellStyle(groupHeaderStyle); + if (subCols > 1) { + sheet.addMergedRegion(new CellRangeAddress(0, 0, colIdx, colIdx + subCols - 1)); + } + colIdx += subCols; + } + + // Row 1: 子表头 + Row subRow = sheet.createRow(1); + subRow.setHeightInPoints(22); + colIdx = 1; + for (int g = 0; g < groupCount; g++) { + for (int s = 0; s < subCols; s++) { + Cell sc = subRow.createCell(colIdx++); + sc.setCellValue(data.getSubHeaders().get(s)); + sc.setCellStyle(subHeaderStyle); + } + } + + // Row 2+: 数据行 + if (data.getRows() != null) { + int rowIdx = 2; + for (List rowData : data.getRows()) { + Row dataRow = sheet.createRow(rowIdx++); + for (int c = 0; c < Math.min(rowData.size(), totalCols); c++) { + Cell cell = dataRow.createCell(c); + Object val = rowData.get(c); + if (c == 0) { + // 行标题列用居中对齐 + cell.setCellStyle(groupHeaderStyle); + } else { + cell.setCellStyle(dataStyle); + } + setCellValue(cell, val); + } + } + } + + // 自动列宽 + autoSizeColumns(sheet, totalCols); + + // 冻结前两行 + sheet.createFreezePane(0, 2); + } + + /** + * 创建子表头样式(浅蓝背景、加粗、居中、带边框) + */ + public static CellStyle createSubHeaderStyle(Workbook workbook) { + CellStyle style = workbook.createCellStyle(); + style.setFillForegroundColor(IndexedColors.LIGHT_CORNFLOWER_BLUE.getIndex()); + style.setFillPattern(FillPatternType.SOLID_FOREGROUND); + + Font font = workbook.createFont(); + font.setBold(true); + font.setFontHeightInPoints((short) 10); + style.setFont(font); + + style.setAlignment(HorizontalAlignment.CENTER); + style.setVerticalAlignment(VerticalAlignment.CENTER); + setThinBorder(style); + return style; + } + + // ======================== Excel 生成方法 ======================== + + /** + * 根据 ExcelData 创建一个完整的 Excel Workbook(含所有 Sheet) + *

+ * 使用 SXSSFWorkbook(流式写入),支持大数据量导出,内存占用可控。 + * + * @param excelData 单个 Excel 的数据定义 + * @return 填充完毕的 Workbook(调用方负责关闭以释放临时文件) + */ + public static Workbook createWorkbook(ExcelData excelData) { + SXSSFWorkbook workbook = new SXSSFWorkbook(100); // 内存中保留 100 行,其余写临时文件 + CellStyle headerStyle = createHeaderStyle(workbook); + CellStyle dataStyle = createDataStyle(workbook); + + for (SheetData sheetData : excelData.getSheets()) { + if (sheetData == null) { + continue; + } + fillSheet(workbook, sheetData, headerStyle, dataStyle); + } + + return workbook; + } + + /** + * 填充单个 Sheet 页的数据 + * + * @param workbook 目标 Workbook + * @param sheetData Sheet 数据定义 + * @param headerStyle 表头样式 + * @param dataStyle 数据行样式 + */ + public static void fillSheet(Workbook workbook, SheetData sheetData, CellStyle headerStyle, CellStyle dataStyle) { + String name = safeName(sheetData.getSheetName()); + Sheet sheet = workbook.createSheet(name); + + // SXSSFSheet 必须在写入数据前开启列追踪,否则 autoSizeColumn 会报错 + if (sheet instanceof SXSSFSheet) { + ((SXSSFSheet) sheet).trackAllColumnsForAutoSizing(); + } + + List headers = sheetData.getHeaders(); + List> rows = sheetData.getRows(); + int colCount = headers != null ? headers.size() : 0; + + // 设置列宽默认值(后续在数据写入后统一自动调整) + int currentRow = 0; + + // ---- 写表头 ---- + if (headers != null && !headers.isEmpty()) { + Row headerRow = sheet.createRow(currentRow++); + headerRow.setHeightInPoints(22); + for (int i = 0; i < colCount; i++) { + Cell cell = headerRow.createCell(i); + cell.setCellValue(headers.get(i)); + cell.setCellStyle(headerStyle); + } + } + + // ---- 写数据行 ---- + if (rows != null) { + 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(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) { + int freezeRows = 1 + sheetData.getHeaderRowCount(); + sheet.createFreezePane(0, Math.min(freezeRows, currentRow - 1)); + } + } + + // ======================== 样式定义 ======================== + + /** + * 创建表头样式(蓝色背景、白色加粗字体、居中对齐、带边框) + */ + public static CellStyle createHeaderStyle(Workbook workbook) { + CellStyle style = workbook.createCellStyle(); + + // 背景色:浅淡的苍白蓝 + style.setFillForegroundColor(IndexedColors.PALE_BLUE.getIndex()); + style.setFillPattern(FillPatternType.SOLID_FOREGROUND); + + // 字体:黑色加粗 + Font font = workbook.createFont(); + font.setBold(true); + font.setColor(IndexedColors.BLACK.getIndex()); + font.setFontHeightInPoints((short) 11); + style.setFont(font); + + // 对齐:居中 + style.setAlignment(HorizontalAlignment.CENTER); + style.setVerticalAlignment(VerticalAlignment.CENTER); + + // 边框 + setThinBorder(style); + + return style; + } + + /** + * 创建数据行样式(无背景、正常字体、左对齐、带边框) + */ + public static CellStyle createDataStyle(Workbook workbook) { + CellStyle style = workbook.createCellStyle(); + + style.setAlignment(HorizontalAlignment.LEFT); + style.setVerticalAlignment(VerticalAlignment.CENTER); + + // 自动换行 + style.setWrapText(true); + + setThinBorder(style); + + return style; + } + + /** + * 设置细线边框 + */ + private static void setThinBorder(CellStyle style) { + style.setBorderTop(BorderStyle.THIN); + style.setBorderRight(BorderStyle.THIN); + style.setBorderBottom(BorderStyle.THIN); + style.setBorderLeft(BorderStyle.THIN); + } + + // ======================== 列宽自动调整 ======================== + + /** + * 自动调整列宽(根据内容宽度) + *

+ * 遍历每一列,取表头和所有数据行中最宽的内容作为列宽基准。 + * + * @param sheet Sheet 对象 + * @param colCount 列数 + */ + public static void autoSizeColumns(Sheet sheet, int colCount) { + for (int i = 0; i < colCount; i++) { + sheet.autoSizeColumn(i); + // 自动列宽后留一点余量,避免内容紧贴边框 + int currentWidth = sheet.getColumnWidth(i); + if (currentWidth > 0) { + sheet.setColumnWidth(i, Math.min(currentWidth + 512, 255 * 256)); // 最大 255 字符宽 + } + } + } + + // ======================== 辅助方法 ======================== + + /** + * 类型安全方式构建 SheetData + *

+ * 适用于已有实体类 List 的场景,通过方法引用指定字段取值,避免手动逐字段提取。 + * + *

{@code
+     * SheetData data = ExportZipUtil.fromTypedData("电站信息",
+     *         Arrays.asList("编码", "名称", "经度", "纬度"),
+     *         Arrays.asList(Station::getStcd, Station::getStnm, Station::getLgtd, Station::getLttd),
+     *         stationList);
+     * }
+ * + * @param sheetName Sheet 名称 + * @param headers 表头列表 + * @param extractors 字段取值函数(与 headers 一一对应) + * @param data 实体数据集合 + * @param 实体类型 + * @return 构建好的 SheetData + */ + public static SheetData fromTypedData(String sheetName, + List headers, + List> extractors, + List data) { + List> rows = data.stream() + .map(entity -> extractors.stream() + .map(ext -> ext.apply(entity)) + .collect(Collectors.toList())) + .collect(Collectors.toList()); + return new SheetData(sheetName, headers, rows); + } + + /** + * 将 Workbook 写入字节数组并关闭 Workbook + */ + private static byte[] workbookToBytes(Workbook workbook) throws IOException { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + try { + workbook.write(baos); + } finally { + workbook.close(); + // SXSSFWorkbook 会清理临时文件 + if (workbook instanceof SXSSFWorkbook) { + ((SXSSFWorkbook) workbook).dispose(); + } + } + return baos.toByteArray(); + } + + /** + * 文件名非法字符过滤(Windows + 通用) + */ + private static String sanitizeFileName(String fileName) { + if (fileName == null || fileName.isEmpty()) { + return "sheet"; + } + return fileName.replaceAll("[\\\\/:*?\"<>|]", "_"); + } + + /** + * Sheet 名称安全处理(Excel Sheet 名最长 31 字符) + */ + private static String safeName(String name) { + if (name == null || name.isEmpty()) { + return "Sheet1"; + } + // Excel Sheet 名禁止包含 [ ] : * ? / \ + String safe = name.replaceAll("[\\[\\]:*?/\\\\]", "_"); + return safe.length() > 31 ? safe.substring(0, 31) : safe; + } + + /** + * 设置单元格值(自动适配类型) + */ + private static void setCellValue(Cell cell, Object value) { + if (value == null) { + cell.setCellValue(""); + } else if (value instanceof Number) { + cell.setCellValue(((Number) value).doubleValue()); + } else if (value instanceof Boolean) { + cell.setCellValue((Boolean) value); + } else if (value instanceof java.util.Date) { + cell.setCellValue((java.util.Date) value); + } else { + cell.setCellValue(String.valueOf(value)); + } + } + + // ======================== 测试方法 ======================== + + /** + * 本地运行测试,生成模拟数据 ZIP 文件到 D:/export_sample.zip + */ + public static void main(String[] args) { + // ---------- Excel 1:电站基础数据(1个Sheet:基础信息) ---------- + List> stationRows = new ArrayList<>(); + for (int i = 1; i <= 50; i++) { + stationRows.add(Arrays.asList( + "STCD" + String.format("%05d", i), // 测站编码 + "XX水电站" + i + "号", // 测站名称 + "长江流域", // 所在流域 + 110.123456 + Math.random() * 10, // 经度 + 30.654321 + Math.random() * 10, // 纬度 + "坝式水电站", // 工程类型 + Math.random() > 0.5 ? "运行中" : "在建", // 建设状态 + "省级", // 级别 + 2000 + (i % 24) // 建成时间 + )); + } + SheetData stationSheet = new SheetData("电站基础信息", + Arrays.asList("测站编码", "测站名称", "所在流域", "经度", "纬度", + "工程类型", "建设状态", "级别", "建成时间"), + stationRows); + + // ---------- Sheet 2:水位监测数据 ---------- + List> monitorRows = new ArrayList<>(); + String[] months = {"2026-01", "2026-02", "2026-03", "2026-04", "2026-05", "2026-06"}; + for (int i = 1; i <= 30; i++) { + for (String month : months) { + monitorRows.add(Arrays.asList( + "STCD" + String.format("%05d", i), + "XX水电站" + i + "号", + month, + String.format("%.2f", 100 + Math.random() * 50), // 水位(m) + String.format("%.2f", 500 + Math.random() * 2000), // 流量(m³/s) + String.format("%.1f", 15 + Math.random() * 20), // 水温(℃) + Math.random() > 0.8 ? "超标" : "正常" // 水质状态 + )); + } + } + SheetData monitorSheet = new SheetData("水位监测数据", + Arrays.asList("测站编码", "测站名称", "监测月份", "水位(m)", "流量(m³/s)", + "水温(℃)", "水质状态"), + monitorRows); + + // ---------- Excel 2:流域概况数据 ---------- + List> basinRows = new ArrayList<>(); + String[] basinNames = {"长江流域", "黄河流域", "珠江流域", "淮河流域", "松花江流域", "海河流域"}; + String[] provinces = {"湖北/湖南/四川", "河南/山东/陕西", "广东/广西", "安徽/江苏", "黑龙江/吉林", "河北/天津"}; + for (int i = 0; i < basinNames.length; i++) { + basinRows.add(Arrays.asList( + "RV" + String.format("%03d", i + 1), // 流域编码 + basinNames[i], // 流域名称 + i == 0 ? "一级" : "二级", // 级别 + provinces[i], // 覆盖省份 + 100 + Math.random() * 900, // 流域面积(万km²) + 500 + Math.random() * 5000, // 年径流量(亿m³) + (int) (1000 + Math.random() * 2000), // 电站数量 + "良好" // 生态状态 + )); + } + SheetData basinSheet = new SheetData("流域概况", + Arrays.asList("流域编码", "流域名称", "级别", "覆盖省份", "流域面积(万km²)", + "年径流量(亿m³)", "电站数量(个)", "生态状态"), + basinRows); + + // ---------- Sheet 4:鱼道设施数据 ---------- + List> fishwayRows = new ArrayList<>(); + String[] types = {"竖缝式鱼道", "池堰式鱼道", "升鱼机", "集运鱼船"}; + for (int i = 1; i <= 20; i++) { + fishwayRows.add(Arrays.asList( + "STCD" + String.format("%05d", i), + "XX水电站" + i + "号", + types[i % types.length], // 鱼道类型 + String.format("%.1f", 5 + Math.random() * 95), // 过鱼效率(%) + String.format("%.2f", 50 + Math.random() * 200), // 鱼道长度(m) + String.format("%.1f", 2 + Math.random() * 8), // 鱼道宽度(m) + Math.random() > 0.3 ? "运行中" : "检修中", // 运行状态 + "2025-0" + (1 + i % 9) + "-01" // 投运日期 + )); + } + SheetData fishwaySheet = new SheetData("鱼道设施", + Arrays.asList("电站编码", "电站名称", "鱼道类型", "过鱼效率(%)", + "鱼道长度(m)", "鱼道宽度(m)", "运行状态", "投运日期"), + fishwayRows); + + // ---------- 组装导出 ---------- + ExcelData excel1 = new ExcelData("电站综合数据", Arrays.asList(stationSheet, monitorSheet)); + ExcelData excel2 = new ExcelData("流域与鱼道数据", Arrays.asList(basinSheet, fishwaySheet)); + + String outputPath = "D:/export_sample.zip"; + try { + log.info("开始生成测试数据..."); + exportToFile(new File(outputPath), Arrays.asList(excel1, excel2)); + log.info("导出成功!文件路径: {}", outputPath); + System.out.println("导出成功!文件路径: " + outputPath); + } catch (Exception e) { + log.error("导出失败", e); + System.err.println("导出失败: " + e.getMessage()); + } + } +} 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_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 + A.RSTCD, + A.STCD, + A.SORT, + A.STTP, + S.STNM, + S.BASE_NAME + FROM MS_ALONGDET_B A + LEFT JOIN V_MS_STBPRP_T S ON S.STCD = A.STCD AND S.STTP = A.STTP + WHERE NVL(A.IS_DELETED, 0) = 0 + AND A.IO_DIRECTION = '1' + AND A.STTP = #{sttp} + AND A.RSTCD IN + + #{rstcd} + + ORDER BY A.SORT + + + + + + 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 @@ + + + + + + + + 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 @@ + + + + + + + + + + diff --git a/backend/src/main/resources/mapper/qgc_export/SdRiverRMapper.xml b/backend/src/main/resources/mapper/qgc_export/SdRiverRMapper.xml new file mode 100644 index 00000000..9a58d094 --- /dev/null +++ b/backend/src/main/resources/mapper/qgc_export/SdRiverRMapper.xml @@ -0,0 +1,26 @@ + + + + + + + + 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 @@ + + + + + + + + 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..c19016a0 --- /dev/null +++ b/backend/src/main/resources/mapper/qgc_export/SdWqRMapper.xml @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + + 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..3fbd4aa9 --- /dev/null +++ b/backend/src/main/resources/mapper/qgc_export/SdWqdaySMapper.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + 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 @@ + + + + + + + + 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 @@ + + + + + + + + + + + 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 @@ + + + + + + + + + + +