fix: 优化excel综合导出水质方法

This commit is contained in:
tangwei 2026-08-05 15:21:28 +08:00
parent 3d952315a5
commit 616c1fbcba
17 changed files with 602 additions and 74 deletions

View File

@ -54,9 +54,9 @@ public class QgcExportController {
List<String> monthList = parseCsv(months); List<String> monthList = parseCsv(months);
List<String> stationList = parseCsv(stcd); List<String> stationList = parseCsv(stcd);
if (dataFields.isEmpty()) { // if (dataFields.isEmpty()) {
dataFields = Arrays.asList("v", "q", "z"); // dataFields = Arrays.asList("v", "q", "z");
} // }
if (tmDimension == null || tmDimension.isEmpty()) { if (tmDimension == null || tmDimension.isEmpty()) {
tmDimension = "month"; tmDimension = "month";
} }

View File

@ -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;
}

View File

@ -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;
}

View File

@ -0,0 +1,28 @@
package com.yfd.platform.qgc_export.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.yfd.platform.qgc_export.domain.SdWqR;
import org.apache.ibatis.annotations.Param;
import java.util.List;
import java.util.Map;
/**
* SD_WQ_R 水质小时数据 Mapper
*/
public interface SdWqRMapper extends BaseMapper<SdWqR> {
/**
* 批量查询测站在指定时间范围内的原始小时数据含达标标志
*
* @param stcdList 测站编码列表
* @param fieldName 查询字段名WTMP/PH/DOX/COND/TU
* @param startTime 起始时间
* @param endTime 结束时间
* @return [STCD, TM, VALUE, SFDB]
*/
List<Map<String, Object>> selectRawData(@Param("stcdList") List<String> stcdList,
@Param("fieldName") String fieldName,
@Param("startTime") String startTime,
@Param("endTime") String endTime);
}

View File

@ -0,0 +1,28 @@
package com.yfd.platform.qgc_export.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.yfd.platform.qgc_export.domain.SdWqdayS;
import org.apache.ibatis.annotations.Param;
import java.util.List;
import java.util.Map;
/**
* SD_WQDAY_S 水质日数据 Mapper
*/
public interface SdWqdaySMapper extends BaseMapper<SdWqdayS> {
/**
* 批量查询测站在指定时间范围内的原始日数据
*
* @param stcdList 测站编码列表
* @param fieldName 查询字段名WTMP/PH/DOX/COND/TU
* @param startTime 起始时间
* @param endTime 结束时间
* @return [STCD, DT, VALUE]
*/
List<Map<String, Object>> selectRawData(@Param("stcdList") List<String> stcdList,
@Param("fieldName") String fieldName,
@Param("startTime") String startTime,
@Param("endTime") String endTime);
}

View File

@ -0,0 +1,30 @@
package com.yfd.platform.qgc_export.processor;
import org.springframework.stereotype.Component;
/**
* 电导率(COND) 指标处理器
*/
@Component
public class CondProcessor extends WqIndicatorProcessor {
@Override
public String getFieldName() {
return "COND";
}
@Override
public String getSheetName() {
return "电导率(μS/cm)";
}
@Override
public int getDecimalPlaces() {
return 2;
}
@Override
public String getSttpType() {
return "WQ";
}
}

View File

@ -0,0 +1,30 @@
package com.yfd.platform.qgc_export.processor;
import org.springframework.stereotype.Component;
/**
* 溶解氧(DOX) 指标处理器
*/
@Component
public class DoxProcessor extends WqIndicatorProcessor {
@Override
public String getFieldName() {
return "DOX";
}
@Override
public String getSheetName() {
return "溶解氧(mg/L)";
}
@Override
public int getDecimalPlaces() {
return 2;
}
@Override
public String getSttpType() {
return "WQ";
}
}

View File

@ -24,7 +24,8 @@ public interface ExportIndicatorProcessor {
* 测站类型STTP用于区分同一电站下不同用途的测站 * 测站类型STTP用于区分同一电站下不同用途的测站
* <ul> * <ul>
* <li>V/Q/Z 河道水情 "ZQ"</li> * <li>V/Q/Z 河道水情 "ZQ"</li>
* <li>QEC 生态流量 待定 "SQ"</li> * <li>WQ 水质 null不区分类型取第一个测站</li>
* <li>QEC 生态流量 待定</li>
* </ul> * </ul>
* 返回 null 表示不按 STTP 过滤取该电站的第一个测站 * 返回 null 表示不按 STTP 过滤取该电站的第一个测站
*/ */
@ -32,6 +33,20 @@ public interface ExportIndicatorProcessor {
return null; return null;
} }
/**
* 数值保留的小数位数默认 3
*/
default int getDecimalPlaces() {
return 3;
}
/**
* 是否有小时达标率指标水质指标返回 true在月均下方追加一行"小时达标率"
*/
default boolean hasComplianceRate() {
return false;
}
/** /**
* 查询原始数据 * 查询原始数据
* *

View File

@ -13,6 +13,8 @@ public class IndicatorDataRow {
private Date tm; private Date tm;
/** 指标值 */ /** 指标值 */
private BigDecimal value; private BigDecimal value;
/** 是否达标仅水质指标使用0=不达标 1=达标) */
private Integer sfdb;
public IndicatorDataRow() {} public IndicatorDataRow() {}
@ -28,4 +30,6 @@ public class IndicatorDataRow {
public void setTm(Date tm) { this.tm = tm; } public void setTm(Date tm) { this.tm = tm; }
public BigDecimal getValue() { return value; } public BigDecimal getValue() { return value; }
public void setValue(BigDecimal value) { this.value = value; } public void setValue(BigDecimal value) { this.value = value; }
public Integer getSfdb() { return sfdb; }
public void setSfdb(Integer sfdb) { this.sfdb = sfdb; }
} }

View File

@ -0,0 +1,30 @@
package com.yfd.platform.qgc_export.processor;
import org.springframework.stereotype.Component;
/**
* PH 指标处理器
*/
@Component
public class PhProcessor extends WqIndicatorProcessor {
@Override
public String getFieldName() {
return "PH";
}
@Override
public String getSheetName() {
return "PH";
}
@Override
public int getDecimalPlaces() {
return 2;
}
@Override
public String getSttpType() {
return "WQ";
}
}

View File

@ -0,0 +1,30 @@
package com.yfd.platform.qgc_export.processor;
import org.springframework.stereotype.Component;
/**
* 浊度(TU) 指标处理器
*/
@Component
public class TuProcessor extends WqIndicatorProcessor {
@Override
public String getFieldName() {
return "TU";
}
@Override
public String getSheetName() {
return "浊度(NTU)";
}
@Override
public int getDecimalPlaces() {
return 2;
}
@Override
public String getSttpType() {
return "WQ";
}
}

View File

@ -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共用查询逻辑
* <p>
* 小时表查询额外提取 SFDB是否达标字段用于计算小时达标率
* 子类需标注 @Component 并提供 getFieldName/getSheetName/getDecimalPlaces
*/
public abstract class WqIndicatorProcessor implements ExportIndicatorProcessor {
protected SdWqRMapper sdWqRMapper;
protected SdWqdaySMapper sdWqdaySMapper;
private static final SimpleDateFormat SDF = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
public void setMappers(SdWqRMapper rMapper, SdWqdaySMapper sMapper) {
this.sdWqRMapper = rMapper;
this.sdWqdaySMapper = sMapper;
}
@Override
public String getSttpType() {
return null; // 水质不按STTP过滤取第一个测站
}
@Override
public boolean hasComplianceRate() {
return true;
}
@Override
public List<IndicatorDataRow> queryRawData(List<String> stcds, Date startTime, Date endTime, boolean isDaily) {
String startStr = SDF.format(startTime);
String endStr = SDF.format(endTime);
List<Map<String, Object>> rawList;
if (isDaily) {
rawList = sdWqdaySMapper.selectRawData(stcds, getFieldName(), startStr, endStr);
} else {
rawList = sdWqRMapper.selectRawData(stcds, getFieldName(), startStr, endStr);
}
List<IndicatorDataRow> rows = new ArrayList<>();
if (rawList != null) {
for (Map<String, Object> 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;
}
}

View File

@ -0,0 +1,31 @@
package com.yfd.platform.qgc_export.processor;
import org.springframework.stereotype.Component;
/**
* 水温(WTMP) 指标处理器
*/
@Component
public class WtmpProcessor extends WqIndicatorProcessor {
@Override
public String getFieldName() {
return "WTMP";
}
@Override
public String getSheetName() {
return "水温(℃)";
}
@Override
public int getDecimalPlaces() {
return 3;
}
@Override
public String getSttpType() {
return "WQ";
}
}

View File

@ -3,13 +3,10 @@ package com.yfd.platform.qgc_export.service.impl;
import com.yfd.platform.qgc_export.domain.StationMapping; import com.yfd.platform.qgc_export.domain.StationMapping;
import com.yfd.platform.qgc_export.mapper.SdRiverRMapper; import com.yfd.platform.qgc_export.mapper.SdRiverRMapper;
import com.yfd.platform.qgc_export.mapper.SdRiverdaySMapper; import com.yfd.platform.qgc_export.mapper.SdRiverdaySMapper;
import com.yfd.platform.qgc_export.mapper.SdWqRMapper;
import com.yfd.platform.qgc_export.mapper.SdWqdaySMapper;
import com.yfd.platform.qgc_export.mapper.StationMappingMapper; import com.yfd.platform.qgc_export.mapper.StationMappingMapper;
import com.yfd.platform.qgc_export.processor.ExportIndicatorProcessor; import com.yfd.platform.qgc_export.processor.*;
import com.yfd.platform.qgc_export.processor.IndicatorDataRow;
import com.yfd.platform.qgc_export.processor.RiverIndicatorProcessor;
import com.yfd.platform.qgc_export.processor.VProcessor;
import com.yfd.platform.qgc_export.processor.QProcessor;
import com.yfd.platform.qgc_export.processor.ZProcessor;
import com.yfd.platform.qgc_export.service.IQgcExportService; import com.yfd.platform.qgc_export.service.IQgcExportService;
import com.yfd.platform.utils.ExportZipUtil; import com.yfd.platform.utils.ExportZipUtil;
@ -30,27 +27,23 @@ import java.util.stream.Collectors;
* 综合导出服务实现月度报表 * 综合导出服务实现月度报表
* <p> * <p>
* 格式每月一个 Excel 文件 ZIP 下载 * 格式每月一个 Excel 文件 ZIP 下载
* 每个 Excel 含多个 Sheet每个指标一个 Sheet * 每个 Excel 含多个 Sheet顺序水温PH溶解氧电导率浊度流速流量水位
* *
* <h3>Sheet 结构</h3> * <h3>Sheet 结构</h3>
* <pre> * <pre>
* 日期 | 班多 | 羊曲 表头Row 0第一列=日期后面列=测站名称 * 日期 | 班多 | 羊曲 表头Row 0
* 01 | 2757.711 | 2709.839 日数据Row 1~31 * 01 | 2757.711 | 2709.839 日数据Row 1~31
* 02 | 2757.806 | 2709.794 * 02 | 2757.806 | 2709.794
* ... * ...
* 31 | 2757.751 | 2709.714 * 日均最低 | 2757.549 | 2709.565 汇总指标7~8行
* 日均最低 | 2757.549 | 2709.565 汇总指标7行
* 日均最高 | 2757.879 | 2709.903 * 日均最高 | 2757.879 | 2709.903
* 月内最低 | 2756.979 | 2709.463 * 月内最低 | 2756.979 | 2709.463
* 月内最低时间 | 31日 08时 | 27日 08时 * 月内最低时间 | 31日 08时 | 27日 08时
* 月内最高 | 2758.003 | 2709.983 * 月内最高 | 2758.003 | 2709.983
* 月内最高时间 | 12日 16时 | 24日 16时 * 月内最高时间 | 12日 16时 | 24日 16时
* 月均 | 2757.751 | 2757.751 * 月均 | 2757.751 | 2757.751
* 小时达标率 | 95.2% | 93.8% 仅水质指标有此行
* </pre> * </pre>
*
* <h3>电站测站映射STTP 类型区分</h3>
* 同一电站RSTCD V_MS_STBPRP_T 视图中可能有多个测站STCD
* 不同类型的指标需要不同类型的测站数据通过 {@link ExportIndicatorProcessor#getSttpType()} 区分
*/ */
@Service @Service
public class QgcExportServiceImpl implements IQgcExportService { public class QgcExportServiceImpl implements IQgcExportService {
@ -60,11 +53,11 @@ public class QgcExportServiceImpl implements IQgcExportService {
@Resource @Resource
private StationMappingMapper stationMappingMapper; private StationMappingMapper stationMappingMapper;
// V/Q/Z 相关
@Resource @Resource
private SdRiverRMapper sdRiverRMapper; private SdRiverRMapper sdRiverRMapper;
@Resource @Resource
private SdRiverdaySMapper sdRiverdaySMapper; private SdRiverdaySMapper sdRiverdaySMapper;
@Resource @Resource
private VProcessor vProcessor; private VProcessor vProcessor;
@Resource @Resource
@ -72,12 +65,29 @@ public class QgcExportServiceImpl implements IQgcExportService {
@Resource @Resource
private ZProcessor zProcessor; private ZProcessor zProcessor;
/** 时间格式dd日 HH时如 "31日 08时" */ // WQ 相关
@Resource
private SdWqRMapper sdWqRMapper;
@Resource
private SdWqdaySMapper sdWqdaySMapper;
@Resource
private WtmpProcessor wtmpProcessor;
@Resource
private PhProcessor phProcessor;
@Resource
private DoxProcessor doxProcessor;
@Resource
private CondProcessor condProcessor;
@Resource
private TuProcessor tuProcessor;
private static final SimpleDateFormat SDF_TIME = new SimpleDateFormat("dd日 HH时"); private static final SimpleDateFormat SDF_TIME = new SimpleDateFormat("dd日 HH时");
/** 汇总指标行标签7个 */ /** 基础汇总指标7个 */
private static final List<String> INDICATOR_LABELS = Collections.unmodifiableList( private static final List<String> BASE_LABELS = Collections.unmodifiableList(
Arrays.asList("日均最低", "日均最高", "月内最低", "月内最低时间", "月内最高", "月内最高时间", "月均")); Arrays.asList("日均最低", "日均最高", "月内最低", "月内最低时间", "月内最高", "月内最高时间", "月均"));
/** 小时达标率标签 */
private static final String LABEL_COMPLIANCE = "小时达标率";
@Override @Override
public void exportData(List<String> dataFields, List<String> months, public void exportData(List<String> dataFields, List<String> months,
@ -96,7 +106,7 @@ public class QgcExportServiceImpl implements IQgcExportService {
.filter(Objects::nonNull) .filter(Objects::nonNull)
.collect(Collectors.toCollection(LinkedHashSet::new)); .collect(Collectors.toCollection(LinkedHashSet::new));
// 3. 电站 测站映射 STTP 类型分别获取 // 3. 电站 测站映射
StationResolution stationRes = resolveStations(stationCodes, sttpTypes); StationResolution stationRes = resolveStations(stationCodes, sttpTypes);
if (stationRes.displayNames.isEmpty()) { if (stationRes.displayNames.isEmpty()) {
log.warn("未找到任何电站映射数据,导出取消"); log.warn("未找到任何电站映射数据,导出取消");
@ -142,18 +152,12 @@ public class QgcExportServiceImpl implements IQgcExportService {
// ======================== 构建 Sheet 数据 ======================== // ======================== 构建 Sheet 数据 ========================
/**
* 查询一个月的数据并组装为一个 Sheet 的全部行
* <ol>
* <li>日数据行01~31每天每电站一个值</li>
* <li>汇总指标行7行日均最低日均最高月内最低月内最低时间月内最高月内最高时间月均</li>
* </ol>
*/
private List<List<Object>> buildSheetRows( private List<List<Object>> buildSheetRows(
ExportIndicatorProcessor processor, MonthRange mr, ExportIndicatorProcessor processor, MonthRange mr,
List<String> stationOrder, StationResolution stationRes) { List<String> stationOrder, StationResolution stationRes) {
String sttp = processor.getSttpType(); String sttp = processor.getSttpType();
int decimals = processor.getDecimalPlaces();
// 构建该处理器对应的 RSTCDSTCD 映射 // 构建该处理器对应的 RSTCDSTCD 映射
Map<String, String> rstcdToStcd = new LinkedHashMap<>(); Map<String, String> rstcdToStcd = new LinkedHashMap<>();
@ -171,16 +175,22 @@ public class QgcExportServiceImpl implements IQgcExportService {
List<String> uniqueStcds = rstcdToStcd.values().stream().distinct().toList(); List<String> uniqueStcds = rstcdToStcd.values().stream().distinct().toList();
// 查询日表 + 小时表数据 // 查询日表 + 小时表
List<IndicatorDataRow> dailyRows = processor.queryRawData(uniqueStcds, mr.getStart(), mr.getEnd(), true); List<IndicatorDataRow> dailyRows = processor.queryRawData(uniqueStcds, mr.getStart(), mr.getEnd(), true);
List<IndicatorDataRow> hourlyRows = processor.queryRawData(uniqueStcds, mr.getStart(), mr.getEnd(), false); List<IndicatorDataRow> hourlyRows = processor.queryRawData(uniqueStcds, mr.getStart(), mr.getEnd(), false);
// 汇总聚合 // 汇总聚合含达标率
Map<String, Aggregation> aggMap = aggregate(dailyRows, hourlyRows, rstcdToStcd); Map<String, Aggregation> aggMap = aggregate(dailyRows, hourlyRows, rstcdToStcd, processor.hasComplianceRate());
// 日数据 (stcd, day) 索引 // 日数据索引
Map<String, Map<Integer, BigDecimal>> dailyByStcd = indexDailyByDay(dailyRows); Map<String, Map<Integer, BigDecimal>> dailyByStcd = indexDailyByDay(dailyRows);
// 指标标签
List<String> labels = new ArrayList<>(BASE_LABELS);
if (processor.hasComplianceRate()) {
labels.add(LABEL_COMPLIANCE);
}
List<List<Object>> rows = new ArrayList<>(); List<List<Object>> rows = new ArrayList<>();
// ---- 日数据行 ---- // ---- 日数据行 ----
@ -192,28 +202,29 @@ public class QgcExportServiceImpl implements IQgcExportService {
String stcd = rstcdToStcd.get(rstcd); String stcd = rstcdToStcd.get(rstcd);
Map<Integer, BigDecimal> dayMap = stcd != null ? dailyByStcd.get(stcd) : null; Map<Integer, BigDecimal> dayMap = stcd != null ? dailyByStcd.get(stcd) : null;
BigDecimal val = dayMap != null ? dayMap.get(d) : null; BigDecimal val = dayMap != null ? dayMap.get(d) : null;
row.add(formatOrEmpty(val)); row.add(formatOrEmpty(val, decimals));
} }
rows.add(row); rows.add(row);
} }
// ---- 汇总指标行 ---- // ---- 汇总指标行 ----
for (int i = 0; i < 7; i++) { for (int i = 0; i < labels.size(); i++) {
List<Object> row = new ArrayList<>(); List<Object> row = new ArrayList<>();
row.add(INDICATOR_LABELS.get(i)); row.add(labels.get(i));
for (String rstcd : stationOrder) { for (String rstcd : stationOrder) {
Aggregation agg = aggMap.get(rstcd); Aggregation agg = aggMap.get(rstcd);
switch (i) { switch (i) {
case 0: row.add(formatOrEmpty(agg != null ? agg.getDailyMin() : null)); break; case 0: row.add(formatOrEmpty(agg != null ? agg.getDailyMin() : null, decimals)); break;
case 1: row.add(formatOrEmpty(agg != null ? agg.getDailyMax() : null)); break; case 1: row.add(formatOrEmpty(agg != null ? agg.getDailyMax() : null, decimals)); break;
case 2: row.add(formatOrEmpty(agg != null ? agg.getExtremeMin() : null)); break; case 2: row.add(formatOrEmpty(agg != null ? agg.getExtremeMin() : null, decimals)); break;
case 3: row.add(agg != null && agg.getExtremeMinTime() != null case 3: row.add(agg != null && agg.getExtremeMinTime() != null
? SDF_TIME.format(agg.getExtremeMinTime()) : ""); break; ? SDF_TIME.format(agg.getExtremeMinTime()) : ""); break;
case 4: row.add(formatOrEmpty(agg != null ? agg.getExtremeMax() : null)); break; case 4: row.add(formatOrEmpty(agg != null ? agg.getExtremeMax() : null, decimals)); break;
case 5: row.add(agg != null && agg.getExtremeMaxTime() != null case 5: row.add(agg != null && agg.getExtremeMaxTime() != null
? SDF_TIME.format(agg.getExtremeMaxTime()) : ""); break; ? SDF_TIME.format(agg.getExtremeMaxTime()) : ""); break;
case 6: row.add(formatOrEmpty(agg != null ? agg.getMonthlyAvg() : null)); break; case 6: row.add(formatOrEmpty(agg != null ? agg.getMonthlyAvg() : null, decimals)); break;
case 7: row.add(formatComplianceRate(agg != null ? agg.getComplianceRate() : null)); break;
} }
} }
rows.add(row); rows.add(row);
@ -224,8 +235,19 @@ public class QgcExportServiceImpl implements IQgcExportService {
// ======================== 指标处理器匹配 ======================== // ======================== 指标处理器匹配 ========================
/**
* 注册顺序决定 Sheet Excel 中的顺序
* 水质指标水温PH溶解氧电导率浊度 河道水情流速流量水位
*/
private List<ExportIndicatorProcessor> resolveProcessors(List<String> dataFields) { private List<ExportIndicatorProcessor> resolveProcessors(List<String> dataFields) {
Map<String, ExportIndicatorProcessor> processorMap = new LinkedHashMap<>(); Map<String, ExportIndicatorProcessor> processorMap = new LinkedHashMap<>();
// WQ 在前
processorMap.put("wtmp", wtmpProcessor);
processorMap.put("ph", phProcessor);
processorMap.put("dox", doxProcessor);
processorMap.put("cond", condProcessor);
processorMap.put("tu", tuProcessor);
// V/Q/Z 在后
processorMap.put("v", vProcessor); processorMap.put("v", vProcessor);
processorMap.put("q", qProcessor); processorMap.put("q", qProcessor);
processorMap.put("z", zProcessor); processorMap.put("z", zProcessor);
@ -237,6 +259,8 @@ public class QgcExportServiceImpl implements IQgcExportService {
if (p != null) { if (p != null) {
if (p instanceof RiverIndicatorProcessor) { if (p instanceof RiverIndicatorProcessor) {
((RiverIndicatorProcessor) p).setMappers(sdRiverRMapper, sdRiverdaySMapper); ((RiverIndicatorProcessor) p).setMappers(sdRiverRMapper, sdRiverdaySMapper);
} else if (p instanceof WqIndicatorProcessor) {
((WqIndicatorProcessor) p).setMappers(sdWqRMapper, sdWqdaySMapper);
} }
result.add(p); result.add(p);
} }
@ -244,7 +268,7 @@ public class QgcExportServiceImpl implements IQgcExportService {
return result; return result;
} }
// ======================== 电站映射 STTP 类型区分 + 测站名称 ======================== // ======================== 电站映射 ========================
private StationResolution resolveStations(List<String> rstcds, Set<String> sttpTypes) { private StationResolution resolveStations(List<String> rstcds, Set<String> sttpTypes) {
StationResolution result = new StationResolution(); StationResolution result = new StationResolution();
@ -267,7 +291,7 @@ public class QgcExportServiceImpl implements IQgcExportService {
continue; continue;
} }
// 测站名称优先 ZQ 类型 STNM 任意 STNM ENNM RSTCD // 测站名称优先 ZQ 类型 STNM 任意 STNM ENNM RSTCD
String displayName = null; String displayName = null;
for (StationMapping m : rows) { for (StationMapping m : rows) {
if ("ZQ".equals(m.getSttp()) && m.getStnm() != null) { if ("ZQ".equals(m.getSttp()) && m.getStnm() != null) {
@ -288,7 +312,7 @@ public class QgcExportServiceImpl implements IQgcExportService {
} }
result.displayNames.put(rstcd, displayName); result.displayNames.put(rstcd, displayName);
// STTP STCD 映射每种 STTP 取第一个 // STTP STCD 映射
Map<String, String> sttpMap = new LinkedHashMap<>(); Map<String, String> sttpMap = new LinkedHashMap<>();
for (StationMapping m : rows) { for (StationMapping m : rows) {
String sttp = m.getSttp(); String sttp = m.getSttp();
@ -327,7 +351,6 @@ public class QgcExportServiceImpl implements IQgcExportService {
} }
} }
/** 计算月份的天数 */
private int getDaysInMonth(MonthRange mr) { private int getDaysInMonth(MonthRange mr) {
Calendar cal = Calendar.getInstance(); Calendar cal = Calendar.getInstance();
cal.setTime(mr.getEnd()); cal.setTime(mr.getEnd());
@ -339,7 +362,7 @@ public class QgcExportServiceImpl implements IQgcExportService {
private Map<String, Aggregation> aggregate( private Map<String, Aggregation> aggregate(
List<IndicatorDataRow> dailyRows, List<IndicatorDataRow> hourlyRows, List<IndicatorDataRow> dailyRows, List<IndicatorDataRow> hourlyRows,
Map<String, String> rstcdToStcd) { Map<String, String> rstcdToStcd, boolean calcCompliance) {
Map<String, Aggregation> result = new LinkedHashMap<>(); Map<String, Aggregation> result = new LinkedHashMap<>();
@ -347,7 +370,7 @@ public class QgcExportServiceImpl implements IQgcExportService {
String stcd = rstcdToStcd.get(rstcd); String stcd = rstcdToStcd.get(rstcd);
Aggregation agg = new Aggregation(); Aggregation agg = new Aggregation();
// 日均最低/最高/月均筛选该测站的所有日数据 // 日均最低/最高/月均
List<BigDecimal> dailyValues = dailyRows.stream() List<BigDecimal> dailyValues = dailyRows.stream()
.filter(r -> Objects.equals(r.getStcd(), stcd) && r.getValue() != null) .filter(r -> Objects.equals(r.getStcd(), stcd) && r.getValue() != null)
.map(IndicatorDataRow::getValue) .map(IndicatorDataRow::getValue)
@ -355,12 +378,11 @@ public class QgcExportServiceImpl implements IQgcExportService {
if (!dailyValues.isEmpty()) { if (!dailyValues.isEmpty()) {
agg.setDailyMin(Collections.min(dailyValues)); agg.setDailyMin(Collections.min(dailyValues));
agg.setDailyMax(Collections.max(dailyValues)); agg.setDailyMax(Collections.max(dailyValues));
// 月均 = 日均值的平均值
BigDecimal sum = dailyValues.stream().reduce(BigDecimal.ZERO, BigDecimal::add); BigDecimal sum = dailyValues.stream().reduce(BigDecimal.ZERO, BigDecimal::add);
agg.setMonthlyAvg(sum.divide(BigDecimal.valueOf(dailyValues.size()), 3, RoundingMode.HALF_UP)); agg.setMonthlyAvg(sum.divide(BigDecimal.valueOf(dailyValues.size()), 3, RoundingMode.HALF_UP));
} }
// 月内最低/最高及时间筛选该测站的所有小时数据 // 月内最低/最高及时间
List<IndicatorDataRow> hourlyStationData = hourlyRows.stream() List<IndicatorDataRow> hourlyStationData = hourlyRows.stream()
.filter(r -> Objects.equals(r.getStcd(), stcd) && r.getValue() != null) .filter(r -> Objects.equals(r.getStcd(), stcd) && r.getValue() != null)
.collect(Collectors.toList()); .collect(Collectors.toList());
@ -375,14 +397,22 @@ public class QgcExportServiceImpl implements IQgcExportService {
agg.setExtremeMaxTime(maxRow.getTm()); agg.setExtremeMaxTime(maxRow.getTm());
} }
// 小时达标率仅水质指标
if (calcCompliance) {
List<IndicatorDataRow> 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); result.put(rstcd, agg);
} }
return result; return result;
} }
/**
* 将日表数据按 (stcd day value) 索引供日数据行快速查找
*/
private Map<String, Map<Integer, BigDecimal>> indexDailyByDay(List<IndicatorDataRow> dailyRows) { private Map<String, Map<Integer, BigDecimal>> indexDailyByDay(List<IndicatorDataRow> dailyRows) {
Map<String, Map<Integer, BigDecimal>> result = new LinkedHashMap<>(); Map<String, Map<Integer, BigDecimal>> result = new LinkedHashMap<>();
for (IndicatorDataRow row : dailyRows) { for (IndicatorDataRow row : dailyRows) {
@ -395,9 +425,14 @@ public class QgcExportServiceImpl implements IQgcExportService {
return result; return result;
} }
private static String formatOrEmpty(BigDecimal value) { private static String formatOrEmpty(BigDecimal value, int decimals) {
if (value == null) return ""; if (value == null) return "";
return value.setScale(3, RoundingMode.HALF_UP).toPlainString(); return value.setScale(decimals, RoundingMode.HALF_UP).toPlainString();
}
private static String formatComplianceRate(BigDecimal value) {
if (value == null) return "";
return value.setScale(1, RoundingMode.HALF_UP).toPlainString() + "%";
} }
private static String getFileNamePrefix(String tmDimension) { private static String getFileNamePrefix(String tmDimension) {
@ -413,19 +448,22 @@ public class QgcExportServiceImpl implements IQgcExportService {
// ======================== 内部数据类 ======================== // ======================== 内部数据类 ========================
private static class StationResolution { private static class StationResolution {
/** RSTCD → 测站名称(列标题显示用) */
final Map<String, String> displayNames = new LinkedHashMap<>(); final Map<String, String> displayNames = new LinkedHashMap<>();
/** RSTCD → (STTP → STCD) */
final Map<String, Map<String, String>> sttpStcdMap = new LinkedHashMap<>(); final Map<String, Map<String, String>> sttpStcdMap = new LinkedHashMap<>();
String getDisplayName(String rstcd) { String getDisplayName(String rstcd) {
return displayNames.getOrDefault(rstcd, rstcd); return displayNames.getOrDefault(rstcd, rstcd);
} }
/**
* 获取指定 STTP 的测站编码sttp null 时返回第一个可用的 STCD
*/
String getStcd(String rstcd, String sttp) { String getStcd(String rstcd, String sttp) {
Map<String, String> inner = sttpStcdMap.get(rstcd); Map<String, String> inner = sttpStcdMap.get(rstcd);
if (inner == null || sttp == null) return null; if (inner == null || inner.isEmpty()) return null;
return inner.get(sttp); if (sttp != null) return inner.get(sttp);
// sttp null返回第一个可用的 STCD
return inner.values().iterator().next();
} }
} }
@ -441,12 +479,11 @@ public class QgcExportServiceImpl implements IQgcExportService {
private BigDecimal dailyMin; private BigDecimal dailyMin;
private BigDecimal dailyMax; private BigDecimal dailyMax;
private BigDecimal extremeMin; private BigDecimal extremeMin;
/** 月内最低发生时间 */
private Date extremeMinTime; private Date extremeMinTime;
private BigDecimal extremeMax; private BigDecimal extremeMax;
/** 月内最高发生时间 */
private Date extremeMaxTime; private Date extremeMaxTime;
/** 月均(所有日值的平均值) */
private BigDecimal monthlyAvg; private BigDecimal monthlyAvg;
/** 小时达标率(百分比,仅水质指标使用) */
private BigDecimal complianceRate;
} }
} }

View File

@ -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.ISdEngInfoBHService;
import com.yfd.platform.qgc_base.service.ISdHbrvDicService; import com.yfd.platform.qgc_base.service.ISdHbrvDicService;
import com.yfd.platform.system.domain.*; import com.yfd.platform.system.domain.*;
import com.yfd.platform.system.mapper.SysMenuMapper; import com.yfd.platform.system.mapper.*;
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.service.ISmsVerifyCodeService; import com.yfd.platform.system.service.ISmsVerifyCodeService;
import com.yfd.platform.system.service.ISysLogService; import com.yfd.platform.system.service.ISysLogService;
import com.yfd.platform.system.service.IUserService; import com.yfd.platform.system.service.IUserService;
@ -85,6 +82,10 @@ public class SmsVerifyCodeController {
@Resource @Resource
private ISdHbrvDicService hbrvDicService; private ISdHbrvDicService hbrvDicService;
@Resource
private SysUserMapper sysUserMapper;
@Value("${rsa.private_key}") @Value("${rsa.private_key}")
private String privateKey; private String privateKey;
private static final String URGE_CONTENT = "根据生态环境部要求,请贵电站尽快完成过鱼数据的报送工作,感谢支持。"; private static final String URGE_CONTENT = "根据生态环境部要求,请贵电站尽快完成过鱼数据的报送工作,感谢支持。";
@ -478,7 +479,7 @@ public class SmsVerifyCodeController {
return ResponseResult.error("验证码错误或已过期"); return ResponseResult.error("验证码错误或已过期");
} }
SysUser user = userService.getUserByPhone(phone, tenantId); SysUser user = userService.getUserByPhone(phone, null);
if (user == null) { if (user == null) {
return ResponseResult.error("该手机号未注册"); return ResponseResult.error("该手机号未注册");
} }
@ -500,19 +501,43 @@ public class SmsVerifyCodeController {
Authentication authenticate = new UsernamePasswordAuthenticationToken(user, null, authenticationToken.getAuthorities()); Authentication authenticate = new UsernamePasswordAuthenticationToken(user, null, authenticationToken.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authenticate); SecurityContextHolder.getContext().setAuthentication(authenticate);
LoginUser loginUser = new LoginUser(); // 判断是否为超级管理员LEVEL=1
loginUser.setUser(user); boolean isSuperAdmin = (isSuperAdmin(user.getId())||user.getUsertype()==0);
loginUser.setUsername(user.getUsername());
//Todo 根据用户查询权限信息 添加到LoginUser中 // 查询用户可访问的门户列表
List<String> 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<String> permissions; List<String> permissions;
if (isSuperAdmin) { if (isSuperAdmin) {
permissions = sysMenuMapper.selectPermsByUserId(user.getId(), null); permissions = sysMenuMapper.selectPermsByUserId(user.getId(), null);
} else { } else {
permissions = sysMenuMapper.selectPermsByUserId(user.getId(), tenantId); 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<String> permissions;
// if (isSuperAdmin) {
// permissions = sysMenuMapper.selectPermsByUserId(user.getId(), null);
// } else {
// permissions = sysMenuMapper.selectPermsByUserId(user.getId(), tenantId);
// }
// loginUser.setPermissions(permissions);
HttpServletRequest request = RequestHolder.getHttpServletRequest(); HttpServletRequest request = RequestHolder.getHttpServletRequest();
SysLog sysLog = new SysLog(); SysLog sysLog = new SysLog();
sysLog.setUsercode(user.getUsername()); sysLog.setUsercode(user.getUsername());
@ -549,4 +574,12 @@ public class SmsVerifyCodeController {
return ResponseResult.successData(map); return ResponseResult.successData(map);
} }
/**
* 判断用户是否为超级管理员拥有LEVEL=1的角色
*/
private boolean isSuperAdmin(String userId) {
String maxLevel = sysUserMapper.getMaxLevel(userId);
return "1".equals(maxLevel);
}
} }

View File

@ -0,0 +1,27 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.yfd.platform.qgc_export.mapper.SdWqRMapper">
<!--
水质小时数据动态字段查询
利用 STCD + TM + IS_DELETED 复合索引
-->
<select id="selectRawData" resultType="java.util.HashMap">
SELECT
STCD,
TM,
${fieldName} AS VALUE,
SFDB
FROM SD_WQ_R
WHERE IS_DELETED = 0
AND STCD IN
<foreach collection="stcdList" item="stcd" open="(" separator="," close=")">
#{stcd}
</foreach>
AND TM &gt;= TO_DATE(#{startTime}, 'YYYY-MM-DD HH24:MI:SS')
AND TM &lt; TO_DATE(#{endTime}, 'YYYY-MM-DD HH24:MI:SS')
AND ${fieldName} IS NOT NULL
</select>
</mapper>

View File

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.yfd.platform.qgc_export.mapper.SdWqdaySMapper">
<!--
水质日数据动态字段查询
利用 STCD + DT + IS_DELETED 复合索引
-->
<select id="selectRawData" resultType="java.util.HashMap">
SELECT
STCD,
DT,
${fieldName} AS VALUE
FROM SD_WQDAY_S
WHERE IS_DELETED = 0
AND STCD IN
<foreach collection="stcdList" item="stcd" open="(" separator="," close=")">
#{stcd}
</foreach>
AND DT &gt;= TO_DATE(#{startTime}, 'YYYY-MM-DD HH24:MI:SS')
AND DT &lt; TO_DATE(#{endTime}, 'YYYY-MM-DD HH24:MI:SS')
AND ${fieldName} IS NOT NULL
</select>
</mapper>