diff --git a/backend/src/main/java/com/yfd/platform/config/JobRunner.java b/backend/src/main/java/com/yfd/platform/config/JobRunner.java index 74bf8dbd..f6d180e5 100644 --- a/backend/src/main/java/com/yfd/platform/config/JobRunner.java +++ b/backend/src/main/java/com/yfd/platform/config/JobRunner.java @@ -15,7 +15,14 @@ */ package com.yfd.platform.config; +import cn.hutool.core.date.DateField; +import cn.hutool.core.date.DateUtil; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.yfd.platform.qgc_env.wq.entity.ao.JobBaseAo; +import com.yfd.platform.qgc_env.wq.service.EnvWqDataService; +import com.yfd.platform.qgc_job.service.IDwJobService; +import com.yfd.platform.qgc_job.service.IEqJobService; +import com.yfd.platform.qgc_job.service.IFhJobService; import com.yfd.platform.system.domain.QuartzJob; import com.yfd.platform.system.mapper.QuartzJobMapper; import com.yfd.platform.utils.QuartzManage; @@ -42,6 +49,10 @@ public class JobRunner implements ApplicationRunner { private static final Logger log = LoggerFactory.getLogger(JobRunner.class); private final QuartzJobMapper quartzJobMapper; private final QuartzManage quartzManage; + private final EnvWqDataService envWqDataService; + private final IEqJobService eqJobService; + private final IDwJobService dwJobService; + private final IFhJobService fhJobService; /** * 项目启动时重新激活启用的定时任务 @@ -55,5 +66,25 @@ public class JobRunner implements ApplicationRunner { quartzJobMapper.selectList(new LambdaQueryWrapper().eq(QuartzJob::getStatus, "1")); quartzJobs.forEach(quartzManage::addJob); log.info("--------------------定时任务注入完成---------------------"); + JobBaseAo ao = new JobBaseAo(); + //如果没有传递时间,则默认为上个月的开始到现在 + if (null == ao.getEndTime()){ + ao.setEndTime(DateUtil.date()); + } + if (null == ao.getStartTime()){ + ao.setStartTime(DateUtil.beginOfYear(DateUtil.offset(ao.getEndTime(), DateField.YEAR,-1))); + } + envWqDataService.wqLastData(ao); + log.info("--------------------wq最新一条数据---------------------"); + eqJobService.eqAnchorPointLastDate(ao); + log.info("--------------------eq最新一条数据---------------------"); + eqJobService.eqAnchorPointLastDate(ao); + log.info("--------------------eq最新一条数据---------------------"); + dwJobService.wtLastData(ao); + log.info("--------------------wt最新一条数据---------------------"); + dwJobService.wtvtPointData(ao); + log.info("--------------------wt最新一条数据---------------------"); + fhJobService.zqLastData(ao); + log.info("--------------------fh最新一条数据---------------------"); } } diff --git a/backend/src/main/java/com/yfd/platform/qgc_base/domain/vo/EngPointVo.java b/backend/src/main/java/com/yfd/platform/qgc_base/domain/vo/EngPointVo.java index c693d44d..ad4444c9 100644 --- a/backend/src/main/java/com/yfd/platform/qgc_base/domain/vo/EngPointVo.java +++ b/backend/src/main/java/com/yfd/platform/qgc_base/domain/vo/EngPointVo.java @@ -1,5 +1,6 @@ package com.yfd.platform.qgc_base.domain.vo; +import com.yfd.platform.qgc_eng.eq.entity.vo.LastTmEngEqDataVo; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; @@ -99,4 +100,7 @@ public class EngPointVo { @Schema(description = "基地排序") private Integer baseStepSort; + + @Schema(description="电站最新数据:入库流量和出库流量") + private LastTmEngEqDataVo lastTmEngEqDataVo; } \ No newline at end of file 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 45447ad9..d37e12a3 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 @@ -4,6 +4,7 @@ import cn.hutool.core.bean.BeanUtil; import cn.hutool.core.bean.copier.CopyOptions; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.date.DateUtil; +import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.StrUtil; import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableId; @@ -42,8 +43,10 @@ import com.yfd.platform.qgc_base.mapper.SdEngInfoBHMapper; import com.yfd.platform.qgc_base.service.IDataScopeFilterService; import com.yfd.platform.qgc_base.service.IMsOperationLogService; import com.yfd.platform.qgc_base.service.ISdEngInfoBHService; +import com.yfd.platform.qgc_eng.eq.entity.vo.LastTmEngEqDataVo; import com.yfd.platform.system.service.IAdminAuthService; import com.yfd.platform.utils.QgcQueryWrapperUtil; +import com.yfd.platform.utils.RedisCacheUtil; import com.yfd.platform.utils.SecurityUtils; import jakarta.annotation.Resource; import org.springframework.cache.annotation.CacheEvict; @@ -65,7 +68,8 @@ import java.util.stream.Collectors; public class SdEngInfoBHServiceImpl extends ServiceImpl implements ISdEngInfoBHService { private static final Map ENG_CODE_NAME_FIELD_MAP = new LinkedHashMap<>(); - + @Resource + private RedisCacheUtil redisCacheUtil; @Resource private IDataScopeFilterService dataScopeFilterService; @@ -1009,6 +1013,18 @@ 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.setStBaseInfo(buildStBaseInfo(stInfoMap)); + return result; + } + EngBaseInfoVo baseInfo = getStInfoByStcd(stcd); if (baseInfo == null) { return null; @@ -1035,6 +1051,138 @@ public class SdEngInfoBHServiceImpl extends ServiceImpl stInfoMap) { + EngStBaseInfoVo stBaseInfo = new EngStBaseInfoVo(); + stBaseInfo.setStcd(getMapString(stInfoMap, "stcd")); + stBaseInfo.setStnm(getMapString(stInfoMap, "stnm")); + stBaseInfo.setSttpCode(getMapString(stInfoMap, "sttpCode")); + stBaseInfo.setSttpName(getMapString(stInfoMap, "sttpName")); + stBaseInfo.setSttpFullPath(getMapString(stInfoMap, "sttpFullPath")); + stBaseInfo.setSttpTreeLevel(getMapInteger(stInfoMap, "sttpTreeLevel")); + stBaseInfo.setStCode(null); + stBaseInfo.setStName(null); + stBaseInfo.setBaseId(getMapString(stInfoMap, "baseId")); + stBaseInfo.setBaseName(getMapString(stInfoMap, "baseName")); + stBaseInfo.setHbrvcd(getMapString(stInfoMap, "hbrvcd")); + stBaseInfo.setHbrvcdName(getMapString(stInfoMap, "hbrvcdName")); + stBaseInfo.setRvcd(getMapString(stInfoMap, "rvcd")); + stBaseInfo.setRvcdName(getMapString(stInfoMap, "rvcdName")); + return stBaseInfo; + } + + private Map buildCamelCaseMsStbprpTMap(Map stInfoMap) { + LinkedHashMap result = new LinkedHashMap<>(); + List canonicalKeys = Arrays.asList( + "stcd", "stnm","ennm", "sttp", "sttpCode", "sttpName", "sttpFullPath", "sttpTreeLevel", + "rstcd", "baseId", "baseName", "hbrvcd", "hbrvcdName", "rvcd", "rvcdName", "addvcd","addvcdName","hycd","hynm","topHycd","topHynm", + "lgtd", "lttd", "stlc", "usfl", "dtin", "dtinTm", "bldsttCode", "introduce", "logo", + "inffile","bldsttCcodeName", "inv", "invinmn", "stdsdt", "pststdt", "pesstdt", "swdt", "jcdt", "wddt", + "orderIndex", "remark", "vlsr", "vlsrTm", "recordUser", "recordTime", "modifyUser", + "modifyTime", "isDeleted", "deleteUser", "deleteTime", "dataSource" + ); + for (String canonicalKey : canonicalKeys) { + result.put(canonicalKey, getMapValue(stInfoMap, canonicalKey)); + } + return result; + } + + private String getMapString(Map source, String key) { + Object value = getMapValue(source, key); + return value == null ? null : String.valueOf(value); + } + + private Integer getMapInteger(Map source, String key) { + Object value = getMapValue(source, key); + if (value == null) { + return null; + } + if (value instanceof Number number) { + return number.intValue(); + } + return Integer.parseInt(String.valueOf(value)); + } + + private Object getMapValue(Map source, String key) { + if (source == null || source.isEmpty() || StrUtil.isBlank(key)) { + return null; + } + if (source.containsKey(key)) { + return source.get(key); + } + String upperKey = key.toUpperCase(Locale.ROOT); + if (source.containsKey(upperKey)) { + return source.get(upperKey); + } + String lowerKey = key.toLowerCase(Locale.ROOT); + if (source.containsKey(lowerKey)) { + return source.get(lowerKey); + } + for (Map.Entry entry : source.entrySet()) { + if (entry.getKey() != null && entry.getKey().equalsIgnoreCase(key)) { + return entry.getValue(); + } + } + return null; + } + private String buildVmsstbprptViewSql() { return "SELECT " + "eng.STCD AS id, " + @@ -2713,9 +2861,9 @@ public class SdEngInfoBHServiceImpl extends ServiceImpl page = loadOptions == null ? null : QgcQueryWrapperUtil.buildPage(loadOptions, loadOptions.getSkip(), loadOptions.getTake()); List list = microservicDynamicSQLMapper.pageAllListWithResultType(page, sql.toString(), paramMap, EngPointVo.class); - + if(CollUtil.isNotEmpty(list)){ + for( EngPointVo engPointVo: list ){ + String stcd = engPointVo.getStcd(); + //查询最新一条数据的缓存 + LastTmEngEqDataVo lastTmEngEqDataVo = redisCacheUtil.fromJson(redisCacheUtil.getString("eqAnchorPointLastDate:"+stcd),LastTmEngEqDataVo.class); + if (ObjectUtil.isEmpty(lastTmEngEqDataVo)){ + lastTmEngEqDataVo = new LastTmEngEqDataVo(); + lastTmEngEqDataVo.setStcd(engPointVo.getStcd()); + }else{ + lastTmEngEqDataVo.setStcd(engPointVo.getStcd()); + } + // 设置电站名称、装机容量、正常蓄水位 + lastTmEngEqDataVo.setStnm(engPointVo.getEnnm()); + lastTmEngEqDataVo.setTtpwr(engPointVo.getTtpwr()); + lastTmEngEqDataVo.setNormz(engPointVo.getNormz()); + engPointVo.setLastTmEngEqDataVo(lastTmEngEqDataVo); + } + } result.setData(list); result.setTotal(page == null ? list.size() : page.getTotal()); return result; 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 53472205..b1801412 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 @@ -37,6 +37,7 @@ import com.yfd.platform.qgc_eng.eq.entity.vo.WbsbVo; import com.yfd.platform.qgc_eng.eq.entity.vo.StBaseInfo; import com.yfd.platform.qgc_eng.eq.entity.vo.StcdInfoVo; import com.yfd.platform.qgc_eng.eq.service.EngEqDataService; +import com.yfd.platform.qgc_env.wq.entity.vo.MsWarnRuleDetailBVo; import com.yfd.platform.qgc_env.wt.utils.SiteAvoidanceUtils; import com.yfd.platform.utils.QgcQueryWrapperUtil; import jakarta.annotation.Resource; @@ -6000,6 +6001,20 @@ public class EngEqDataServiceImpl implements EngEqDataService { resultList.add(eqdsVo); } + Map qecLimitQueryMap = new LinkedHashMap<>(); + Date currentDate = new Date(); + for (EngEqdsVo eqdsVo : resultList) { + if (eqdsVo == null || StrUtil.isBlank(eqdsVo.getRstcd()) || qecLimitQueryMap.containsKey(eqdsVo.getRstcd())) { + continue; + } + qecLimitQueryMap.put(eqdsVo.getRstcd(), currentDate); + } + Map qecLimitMap = getQecLimitMap(qecLimitQueryMap); + for (EngEqdsVo eqdsVo : resultList) { + MsWarnRuleDetailBVo ruleDetail = qecLimitMap.get(eqdsVo.getRstcd()); + eqdsVo.setQecLimit(ruleDetail == null ? null : ruleDetail.getMinVal()); + } + SiteAvoidanceUtils.calcSiteMapLev(resultList, EngEqdsVo::getStcd, EngEqdsVo::getLgtd, @@ -6011,4 +6026,152 @@ public class EngEqDataServiceImpl implements EngEqDataService { dataSourceResult.setTotal(page == null ? resultList.size() : page.getTotal()); return dataSourceResult; } + + private Map getQecLimitMap(Map stcdTmMap) { + Map resultMap = new HashMap<>(); + if (stcdTmMap == null || stcdTmMap.isEmpty()) { + return resultMap; + } + + Map paramMap = new HashMap<>(); + StringBuilder sql = new StringBuilder(); + sql.append("SELECT ") + .append("rel.STCD AS stcd, ") + .append("det.ID AS id, ") + .append("det.RULE_ID AS ruleId, ") + .append("det.TB_ID AS tbId, ") + .append("det.YS AS ys, ") + .append("det.TM AS tm, ") + .append("det.BEGIN_TIME AS beginTime, ") + .append("det.END_TIME AS endTime, ") + .append("det.MIN_VAL AS minVal, ") + .append("det.MAX_VAL AS maxVal, ") + .append("det.VLSR AS vlsr, ") + .append("det.VLSR_TM AS vlsrTm ") + .append("FROM MS_WARN_RULE_STBPRP_B rel ") + .append("INNER JOIN MS_WARN_RULE_B rule ON rule.ID = rel.RULE_ID ") + .append(" AND rule.RULE_TYPE = 'EQMN' ") + .append(" AND NVL(rule.IS_DELETED, 0) = 0 ") + .append("INNER JOIN MS_WARN_RULE_DETAIL_B det ON det.RULE_ID = rule.ID ") + .append(" AND det.YS = 'QEC' ") + .append(" AND NVL(det.IS_DELETED, 0) = 0 ") + .append("INNER JOIN ST_TB_B tb ON tb.ID = det.TB_ID ") + .append(" AND tb.TB_CODE = 'QEC_R' ") + .append(" AND NVL(tb.IS_DELETED, 0) = 0 ") + .append("WHERE NVL(rel.IS_DELETED, 0) = 0 "); + appendInCondition(sql, paramMap, "rel.STCD", stcdTmMap.keySet(), "qecLimitStcd"); + sql.append("ORDER BY rel.STCD, det.TM DESC, LPAD(det.BEGIN_TIME, 4, '0') DESC NULLS LAST, ") + .append("LPAD(det.END_TIME, 4, '0') DESC NULLS LAST"); + + List> ruleRows = microservicDynamicSQLMapper.pageAllList(null, sql.toString(), paramMap); + if (CollUtil.isEmpty(ruleRows)) { + return resultMap; + } + + Map> groupedRuleMap = new HashMap<>(); + for (Map row : ruleRows) { + String stcd = asString(row.get("STCD")); + if (StrUtil.isBlank(stcd)) { + continue; + } + groupedRuleMap.computeIfAbsent(stcd, key -> new ArrayList<>()).add(toMsWarnRuleDetailBVo(row)); + } + + for (Map.Entry entry : stcdTmMap.entrySet()) { + MsWarnRuleDetailBVo detail = resolveQecLimitDetail(entry.getValue(), groupedRuleMap.get(entry.getKey())); + if (detail != null) { + resultMap.put(entry.getKey(), detail); + } + } + return resultMap; + } + + private MsWarnRuleDetailBVo resolveQecLimitDetail(Date queryDate, List ruleRows) { + if (queryDate == null || CollUtil.isEmpty(ruleRows)) { + return null; + } + Date currentTm = null; + List currentRows = new ArrayList<>(); + String monthDay = DateTimeFormatter.ofPattern("MMdd").format( + queryDate.toInstant().atZone(ZoneId.systemDefault()).toLocalDate() + ); + for (MsWarnRuleDetailBVo row : ruleRows) { + Date tm = row.getTm(); + if (tm == null || queryDate.compareTo(tm) < 0) { + continue; + } + if (currentTm != null && !currentTm.equals(tm)) { + return matchQecLimitDetail(monthDay, currentRows); + } + currentTm = tm; + currentRows.add(row); + } + if (currentTm == null) { + return null; + } + return matchQecLimitDetail(monthDay, currentRows); + } + + private MsWarnRuleDetailBVo matchQecLimitDetail(String monthDay, List ruleRows) { + MsWarnRuleDetailBVo defaultValue = null; + for (MsWarnRuleDetailBVo row : ruleRows) { + if (row == null || row.getMinVal() == null) { + continue; + } + String beginTime = normalizeMonthDay(row.getBeginTime()); + String endTime = normalizeMonthDay(row.getEndTime()); + if (StrUtil.isNotBlank(beginTime) && StrUtil.isNotBlank(endTime)) { + if (isMonthDayInRange(monthDay, beginTime, endTime)) { + return row; + } + continue; + } + if (StrUtil.isBlank(beginTime) && StrUtil.isBlank(endTime)) { + defaultValue = row; + } + } + return defaultValue; + } + + private MsWarnRuleDetailBVo toMsWarnRuleDetailBVo(Map row) { + MsWarnRuleDetailBVo vo = new MsWarnRuleDetailBVo(); + vo.setId(asString(row.get("ID"))); + vo.setRuleId(asString(row.get("RULEID"))); + vo.setTbId(asString(row.get("TBID"))); + vo.setYs(asString(row.get("YS"))); + vo.setTm(toDate(row.get("TM"))); + vo.setBeginTime(asString(row.get("BEGINTIME"))); + vo.setEndTime(asString(row.get("ENDTIME"))); + vo.setMinVal(toBigDecimal(row.get("MINVAL"))); + vo.setMaxVal(toBigDecimal(row.get("MAXVAL"))); + vo.setVlsr(asString(row.get("VLSR"))); + vo.setVlsrTm(toDate(row.get("VLSRTM"))); + return vo; + } + + private void appendInCondition(StringBuilder sql, + Map paramMap, + String column, + Iterable values, + String prefix) { + if (values == null) { + return; + } + List placeholders = new ArrayList<>(); + int index = 0; + for (String value : values) { + if (StrUtil.isBlank(value)) { + continue; + } + String key = prefix + index++; + paramMap.put(key, value.trim()); + placeholders.add("#{map." + key + "}"); + } + if (CollUtil.isEmpty(placeholders)) { + return; + } + sql.append(" AND ").append(column).append(" IN (") + .append(String.join(", ", placeholders)) + .append(") "); + } } diff --git a/backend/src/main/java/com/yfd/platform/qgc_env/fb/service/impl/FbStationServiceImpl.java b/backend/src/main/java/com/yfd/platform/qgc_env/fb/service/impl/FbStationServiceImpl.java index 98201b27..af960fd2 100644 --- a/backend/src/main/java/com/yfd/platform/qgc_env/fb/service/impl/FbStationServiceImpl.java +++ b/backend/src/main/java/com/yfd/platform/qgc_env/fb/service/impl/FbStationServiceImpl.java @@ -4626,7 +4626,7 @@ public class FbStationServiceImpl implements FbStationService { .append("a.RSTCD AS rstcds, ") .append("a.BASE_ID AS baseId, ") .append("a.DTIN AS dtin, ") - .append("eng.ENNM AS ennm, ") + .append("COALESCE(eng.ENNM, '') AS ennm, ") .append("eng.RVCD AS rvcd, ") .append("eng.ADDVCD AS addvcd, ") .append("NVL(siteSort.SORT, 999999) AS siteStepSort, ") 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 01cd74b5..1ec35350 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 @@ -22,6 +22,7 @@ import com.yfd.platform.qgc_env.warn.entity.vo.StcdWarnStateVo; import com.yfd.platform.qgc_env.warn.service.WarnDataService; import com.yfd.platform.utils.QgcQueryWrapperUtil; import jakarta.annotation.Resource; +import org.springframework.cache.annotation.Cacheable; import org.springframework.stereotype.Service; import java.math.BigDecimal; @@ -260,6 +261,7 @@ public class WarnDataServiceImpl implements WarnDataService { } @Override + @Cacheable(cacheNames = "warn#3600", keyGenerator = "cacheKeyGenerator") public DataSourceResult getAlarmPointKendoListCust(DataSourceRequest dataSourceRequest) { DataSourceLoadOptionsBase loadOptions = dataSourceRequest == null ? null : dataSourceRequest.toDevRequest(); String startTimeText = loadOptions == null ? null : QgcQueryWrapperUtil.getFilterFieldValue(loadOptions, "startTime"); diff --git a/backend/src/main/java/com/yfd/platform/qgc_env/wq/entity/SdWqR.java b/backend/src/main/java/com/yfd/platform/qgc_env/wq/entity/SdWqR.java new file mode 100644 index 00000000..673d1656 --- /dev/null +++ b/backend/src/main/java/com/yfd/platform/qgc_env/wq/entity/SdWqR.java @@ -0,0 +1,310 @@ +package com.yfd.platform.qgc_env.wq.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.math.BigDecimal; +import java.util.Date; + +@Data +@TableName("SD_WQ_R") +@Schema(description = "水质监测数据表实体") +public class SdWqR { + + @TableId(value = "ID", type = IdType.ASSIGN_UUID) + @Schema(description = "主键") + private String id; + + @TableField("STCD") + @Schema(description = "站码") + private String stcd; + + @TableField("TM") + @Schema(description = "采样时间") + private Date tm; + + @TableField("WQGRD") + @Schema(description = "水质类别") + private String wqgrd; + + + @Schema(description = "水质类别") + @TableField(exist = false) + private String wqgrdName; + + @TableField("PERIOD") + @Schema(description = "丰平枯时期") + private String period; + + @TableField("SPSTLC") + @Schema(description = "水平位置") + private Long spstlc; + + @TableField("WTMP") + @Schema(description = "水温") + private BigDecimal wtmp; + + @TableField("PH") + @Schema(description = "PH") + private BigDecimal ph; + + @TableField("DOX") + @Schema(description = "溶解氧") + private BigDecimal dox; + + @TableField("CODMN") + @Schema(description = "高锰酸盐指数") + private BigDecimal codmn; + + @TableField("CODCR") + @Schema(description = "化学需氧量") + private BigDecimal codcr; + + @TableField("BOD5") + @Schema(description = "五日生化需氧量") + private BigDecimal bod5; + + @TableField("NH3N") + @Schema(description = "氨氮") + private BigDecimal nh3n; + + @TableField("TP") + @Schema(description = "总磷") + private BigDecimal tp; + + @TableField("TN") + @Schema(description = "总氮") + private BigDecimal tn; + + @TableField("CU") + @Schema(description = "铜") + private BigDecimal cu; + + @TableField("ZN") + @Schema(description = "锌") + private BigDecimal zn; + + @TableField("F") + @Schema(description = "氟化物") + private BigDecimal f; + + @TableField("SE") + @Schema(description = "硒") + private BigDecimal se; + + @TableField("ARS") + @Schema(description = "砷") + private BigDecimal ars; + + @TableField("HG") + @Schema(description = "汞") + private BigDecimal hg; + + @TableField("CD") + @Schema(description = "镉") + private BigDecimal cd; + + @TableField("CR6") + @Schema(description = "铬(六价)") + private BigDecimal cr6; + + @TableField("PB") + @Schema(description = "铅") + private BigDecimal pb; + + @TableField("CN") + @Schema(description = "氰化物") + private BigDecimal cn; + + @TableField("VLPH") + @Schema(description = "挥发酚") + private BigDecimal vlph; + + @TableField("OIL") + @Schema(description = "石油类") + private BigDecimal oil; + + @TableField("LAS") + @Schema(description = "阴离子表面活性剂") + private BigDecimal las; + + @TableField("S2") + @Schema(description = "硫化物") + private BigDecimal s2; + + @TableField("FCG") + @Schema(description = "粪大肠菌群") + private BigDecimal fcg; + + @TableField("CL") + @Schema(description = "氯化物") + private BigDecimal cl; + + @TableField("SO4") + @Schema(description = "硫酸盐") + private BigDecimal so4; + + @TableField("NO3") + @Schema(description = "硝氮/硝酸盐氮") + private BigDecimal no3; + + @TableField("THRD") + @Schema(description = "总硬度") + private BigDecimal thrd; + + @TableField("COND") + @Schema(description = "电导率") + private BigDecimal cond; + + @TableField("FE") + @Schema(description = "铁") + private BigDecimal fe; + + @TableField("MN") + @Schema(description = "锰") + private BigDecimal mn; + + @TableField("AL") + @Schema(description = "铝") + private BigDecimal al; + + @TableField("CHLA") + @Schema(description = "叶绿素a") + private BigDecimal chla; + + @TableField("CLARITY") + @Schema(description = "透明度") + private BigDecimal clarity; + + @TableField("TU") + @Schema(description = "浊度") + private BigDecimal tu; + + @TableField("CYANO") + @Schema(description = "蓝绿藻") + private BigDecimal cyano; + + @TableField("NI") + @Schema(description = "镍") + private BigDecimal ni; + + @TableField("TOD") + @Schema(description = "总氧/总需氧量") + private BigDecimal tod; + + @TableField("SFDB") + @Schema(description = "是否达标") + private Integer sfdb; + + @TableField("FID") + @Schema(description = "文件ID") + private String fid; + + @TableField("VLSR") + @Schema(description = "数据来源") + private String vlsr; + + @TableField("WQ_SFDBHN_YS") + @Schema(description = "水质不达标的要素") + private String wqSfdbhnYs; + + @TableField("REMARK") + @Schema(description = "备注") + private String remark; + + @TableField("TLI_SUM") + @Schema(description = "综合营养状态指数") + private BigDecimal tliSum; + + @TableField("TLI_SUM_LEVEL") + @Schema(description = "综合营养状态指数分级") + private String tliSumLevel; + + @TableField("CA") + @Schema(description = "钙") + private BigDecimal ca; + + @TableField("PO4") + @Schema(description = "磷酸盐") + private BigDecimal po4; + + @TableField("N_NO2") + @Schema(description = "亚硝酸盐氮(弃用)") + private BigDecimal nNo2; + + @TableField("NA") + @Schema(description = "钠") + private BigDecimal na; + + @TableField("K") + @Schema(description = "钾") + private BigDecimal k; + + @TableField("TOC") + @Schema(description = "总有机碳") + private BigDecimal toc; + + @TableField("OPOR") + @Schema(description = "有机磷农药") + private BigDecimal opor; + + @TableField("SS") + @Schema(description = "悬浮物") + private BigDecimal ss; + + @TableField("NO2") + @Schema(description = "亚硝酸盐氮") + private BigDecimal no2; + + @TableField("ORP") + @Schema(description = "氧化还原电位") + private BigDecimal orp; + + @TableField("FWGHTEMP") + @Schema(description = "FWGHTEMP") + private BigDecimal fwghtemp; + + @TableField("RECORD_USER") + @Schema(description = "创建人") + private String recordUser; + + @TableField("RECORD_TIME") + @Schema(description = "创建时间") + private Date recordTime; + + @TableField("MODIFY_USER") + @Schema(description = "更新人") + private String modifyUser; + + @TableField("MODIFY_TIME") + @Schema(description = "更新时间") + private Date modifyTime; + + @TableField("IS_DELETED") + @Schema(description = "是否已删除") + private Integer isDeleted; + + @TableField("DELETE_USER") + @Schema(description = "删除人") + private String deleteUser; + + @TableField("DELETE_TIME") + @Schema(description = "删除时间") + private Date deleteTime; + + @TableField(exist = false) + private Integer sfdbNew; + + @TableField(exist = false) + private String tmNew; + + @TableField(exist = false) + private String baseId; + + @TableField(exist = false) + private Integer dtinType; +} \ No newline at end of file diff --git a/backend/src/main/java/com/yfd/platform/qgc_env/wq/entity/ao/JobBaseAo.java b/backend/src/main/java/com/yfd/platform/qgc_env/wq/entity/ao/JobBaseAo.java new file mode 100644 index 00000000..0c2525af --- /dev/null +++ b/backend/src/main/java/com/yfd/platform/qgc_env/wq/entity/ao/JobBaseAo.java @@ -0,0 +1,139 @@ +package com.yfd.platform.qgc_env.wq.entity.ao; + +import com.fasterxml.jackson.annotation.JsonFormat; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import org.springframework.format.annotation.DateTimeFormat; + +import java.util.Calendar; +import java.util.Date; +import java.util.Set; + +@Data +@Schema(description = "定时任务基础参数") +public class JobBaseAo { + + public static final String JOB_TYPE_DAY = "day"; + public static final String JOB_TYPE_HOUR = "hour"; + + @Schema(description = "job类型 day=天 hour=小时", example = "hour", allowableValues = "day,hour") + private String jobType; + + @Schema(description = "只能是数字或特定字符串,为空=1。job类型为天即单位为天,类型为小时即单位小时 today=当天 am15=跨月15天取开始月份1号", example = "2") + private String jobStep; + + @Schema(description = "开始时间", example = "2022-08-01 00:00:00") + @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date startTime; + + @Schema(description = "结束时间", example = "2022-08-31 23:59:59") + @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date endTime; + + @Schema(description = "设施类型") + private String sttpCode; + + @Schema(description = "测站编码集合") + private Set stcds; + + @Schema(description = "备注") + private String remarks; + + public void initTime() { + Date startTime = this.getStartTime(); + Date endTime = this.getEndTime(); + if (startTime == null || endTime == null) { + Date now = new Date(); + String num = this.getJobStep() != null && !"".equals(this.getJobStep().trim()) ? this.getJobStep() : "1"; + if ("hour".equals(this.getJobType())) { + if ("today".equalsIgnoreCase(num)) { + startTime = getDayStartTime(now); + endTime = parseWholeMinuteEnd(now); + } else { + startTime = parseWholeMinuteStart(DateUtils.addHours(now, -Integer.parseInt(num))); + endTime = parseWholeMinuteEnd(now); + } + } else if ("day".equals(this.getJobType())) { + if ("today".equalsIgnoreCase(num)) { + startTime = getDayStartTime(now); + endTime = parseWholeMinuteEnd(now); + } else if (num.toLowerCase().startsWith("am")) { + num = num.replace("am", ""); + startTime = getMonthFirstDayStartTime(DateUtils.addDays(now, -Integer.parseInt(num))); + endTime = parseWholeMinuteEnd(now); + } else { + startTime = getDayStartTime(DateUtils.addDays(now, -Integer.parseInt(num))); + endTime = parseWholeMinuteEnd(now); + } + } + } + + if (startTime != null && endTime != null) { + if (startTime.getTime() > endTime.getTime()) { + throw new RuntimeException("开始时间不能大于结束时间"); + } else { + this.startTime = startTime; + this.endTime = endTime; + } + } else { + throw new RuntimeException("开始时间和结束时间不能为空"); + } + } + + private Date getMonthFirstDayStartTime(Date date) { + Calendar calendar = Calendar.getInstance(); + calendar.setTime(date); + calendar.set(Calendar.DAY_OF_MONTH, 1); + calendar.set(Calendar.HOUR_OF_DAY, 0); + calendar.set(Calendar.MINUTE, 0); + calendar.set(Calendar.SECOND, 0); + calendar.set(Calendar.MILLISECOND, 0); + return calendar.getTime(); + } + + private static Date getDayStartTime(Date date) { + Calendar calendar = Calendar.getInstance(); + calendar.setTime(date); + calendar.set(Calendar.HOUR_OF_DAY, 0); + calendar.set(Calendar.MINUTE, 0); + calendar.set(Calendar.SECOND, 0); + calendar.set(Calendar.MILLISECOND, 0); + return calendar.getTime(); + } + + private static Date parseWholeMinuteEnd(Date date) { + Calendar calendar = Calendar.getInstance(); + calendar.setTime(date); + calendar.set(Calendar.MINUTE, 59); + calendar.set(Calendar.SECOND, 59); + calendar.set(Calendar.MILLISECOND, 999); + return calendar.getTime(); + } + + private static Date parseWholeMinuteStart(Date date) { + Calendar calendar = Calendar.getInstance(); + calendar.setTime(date); + calendar.set(Calendar.HOUR_OF_DAY, 0); + calendar.set(Calendar.SECOND, 0); + calendar.set(Calendar.MILLISECOND, 0); + return calendar.getTime(); + } + + private static class DateUtils { + public static Date addHours(Date date, int hours) { + Calendar calendar = Calendar.getInstance(); + calendar.setTime(date); + calendar.add(Calendar.HOUR_OF_DAY, hours); + return calendar.getTime(); + } + + public static Date addDays(Date date, int days) { + Calendar calendar = Calendar.getInstance(); + calendar.setTime(date); + calendar.add(Calendar.DAY_OF_MONTH, days); + return calendar.getTime(); + } + } +} \ No newline at end of file diff --git a/backend/src/main/java/com/yfd/platform/qgc_env/wq/entity/vo/EnvWqDataVo.java b/backend/src/main/java/com/yfd/platform/qgc_env/wq/entity/vo/EnvWqDataVo.java index c7c8cd5b..88101692 100644 --- a/backend/src/main/java/com/yfd/platform/qgc_env/wq/entity/vo/EnvWqDataVo.java +++ b/backend/src/main/java/com/yfd/platform/qgc_env/wq/entity/vo/EnvWqDataVo.java @@ -1,5 +1,6 @@ package com.yfd.platform.qgc_env.wq.entity.vo; +import com.baomidou.mybatisplus.annotation.TableField; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; @@ -11,6 +12,8 @@ import java.util.Map; @Data @Schema(description = "水质实时数据") public class EnvWqDataVo { + @Schema(description = "id") + private String id; @Schema(description = "测站编码") private String stcd; @Schema(description = "电站编码") @@ -137,6 +140,34 @@ public class EnvWqDataVo { private Integer rstcdStepSort; @Schema(description = "测站排序") private Integer siteStepSort; + + @Schema(description = "创建人") + private String recordUser; + + + @Schema(description = "创建时间") + private Date recordTime; + + + @Schema(description = "更新人") + private String modifyUser; + + + @Schema(description = "更新时间") + private Date modifyTime; + + + @Schema(description = "是否已删除") + private Integer isDeleted; + + + @Schema(description = "删除人") + private String deleteUser; + + + @Schema(description = "删除时间") + private Date deleteTime; + @Schema(description = "水质要素最小限值集合") private List> min; @Schema(description = "水质要素最大限值集合") diff --git a/backend/src/main/java/com/yfd/platform/qgc_env/wq/mapper/WqJobMapper.java b/backend/src/main/java/com/yfd/platform/qgc_env/wq/mapper/WqJobMapper.java new file mode 100644 index 00000000..b1e778bd --- /dev/null +++ b/backend/src/main/java/com/yfd/platform/qgc_env/wq/mapper/WqJobMapper.java @@ -0,0 +1,18 @@ +package com.yfd.platform.qgc_env.wq.mapper; + +import com.yfd.platform.qgc_env.wq.entity.SdWqR; +import com.yfd.platform.qgc_env.wq.entity.ao.JobBaseAo; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +@Mapper +public interface WqJobMapper { + + List getWqLastData(@Param("ao") JobBaseAo ao); + + List getWqYsLastData(@Param("startTimeStr") String startTimeStr, @Param("endTimeStr") String endTimeStr); + + void mergeWqR(@Param("wqRs") List wqRs); +} \ No newline at end of file diff --git a/backend/src/main/java/com/yfd/platform/qgc_env/wq/service/EnvWqDataService.java b/backend/src/main/java/com/yfd/platform/qgc_env/wq/service/EnvWqDataService.java index f2ff1e22..0ba9565b 100644 --- a/backend/src/main/java/com/yfd/platform/qgc_env/wq/service/EnvWqDataService.java +++ b/backend/src/main/java/com/yfd/platform/qgc_env/wq/service/EnvWqDataService.java @@ -3,6 +3,7 @@ package com.yfd.platform.qgc_env.wq.service; import com.yfd.platform.common.DataSourceRequest; import com.yfd.platform.common.DataSourceResult; import com.yfd.platform.qgc_base.entity.vo.BatchDeleteAo; +import com.yfd.platform.qgc_env.wq.entity.ao.JobBaseAo; import com.yfd.platform.qgc_env.wq.entity.vo.WqBaseInfoVo; import com.yfd.platform.qgc_env.wq.entity.vo.EnvWqAnchorPointVo; @@ -45,4 +46,6 @@ public interface EnvWqDataService { boolean removeKendoByIds(BatchDeleteAo batchDeleteAo); DataSourceResult getAnchorPointKendoListCust(DataSourceRequest dataSourceRequest); + + void wqLastData(JobBaseAo ao); } diff --git a/backend/src/main/java/com/yfd/platform/qgc_env/wq/service/impl/EnvWqDataServiceImpl.java b/backend/src/main/java/com/yfd/platform/qgc_env/wq/service/impl/EnvWqDataServiceImpl.java index 4e9657f9..e45490d4 100644 --- a/backend/src/main/java/com/yfd/platform/qgc_env/wq/service/impl/EnvWqDataServiceImpl.java +++ b/backend/src/main/java/com/yfd/platform/qgc_env/wq/service/impl/EnvWqDataServiceImpl.java @@ -4,6 +4,7 @@ import cn.hutool.core.collection.CollUtil; import cn.hutool.core.date.DateUtil; import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.StrUtil; +import cn.hutool.json.JSONUtil; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.yfd.platform.common.*; import com.yfd.platform.common.exception.BizException; @@ -15,15 +16,16 @@ import com.yfd.platform.qgc_base.entity.vo.DataParam; import com.yfd.platform.qgc_base.mapper.MsOperationLogDetailMapper; import com.yfd.platform.qgc_base.mapper.MsOperationLogMapper; import com.yfd.platform.qgc_base.service.IDataScopeFilterService; +import com.yfd.platform.qgc_env.wq.entity.SdWqR; +import com.yfd.platform.qgc_env.wq.entity.ao.JobBaseAo; import com.yfd.platform.qgc_env.wq.entity.vo.*; +import com.yfd.platform.qgc_env.wq.mapper.WqJobMapper; import com.yfd.platform.qgc_env.wq.service.EnvWqDataService; import com.yfd.platform.qgc_env.wt.entity.vo.WbsbVo; import com.yfd.platform.qgc_env.wt.utils.SiteAvoidanceUtils; -import com.yfd.platform.utils.KendoUtil; -import com.yfd.platform.utils.QgcQueryWrapperUtil; -import com.yfd.platform.utils.RequestHolder; -import com.yfd.platform.utils.SecurityUtils; +import com.yfd.platform.utils.*; import jakarta.annotation.Resource; +import org.springframework.cache.annotation.Cacheable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.jdbc.core.JdbcTemplate; @@ -50,6 +52,9 @@ public class EnvWqDataServiceImpl implements EnvWqDataService { @Resource private IDataScopeFilterService dataScopeFilterService; + @Resource + private RedisCacheUtil redisCacheUtil; + private static final String WQ_HOUR_TABLE_NAME = "SD_WQ_R"; private static final String WQ_DAY_TABLE_NAME = "SD_WQDAY_S"; private static final String WQ_MONTH_TABLE_NAME = "SD_WQDRTP_S"; @@ -158,6 +163,9 @@ public class EnvWqDataServiceImpl implements EnvWqDataService { @Resource private MsOperationLogDetailMapper msOperationLogDetailMapper; + @Resource + private WqJobMapper wqJobMapper; + @Override public DataSourceResult processKendoList(DataSourceRequest dataSourceRequest) { boolean calculated = CommonConstant.CALCULATE_SUCCESS.equals( @@ -4549,6 +4557,7 @@ public class EnvWqDataServiceImpl implements EnvWqDataService { } @Override +// @Cacheable(cacheNames = "weCache#3600", keyGenerator = "cacheKeyGenerator") public DataSourceResult getAnchorPointKendoListCust(DataSourceRequest dataSourceRequest) { DataSourceResult dataSourceResult = new DataSourceResult<>(); dataSourceResult.setAggregates(new HashMap<>()); @@ -4575,6 +4584,15 @@ public class EnvWqDataServiceImpl implements EnvWqDataService { .append("t.ELEV AS dtmel, ") .append("t.STLC AS stlc, ") .append("t.WWQTG AS wwqtg, ") + .append("CASE t.WWQTG " + + " WHEN '1' THEN 'Ⅰ' " + + " WHEN '2' THEN 'Ⅱ' " + + " WHEN '3' THEN 'Ⅲ' " + + " WHEN '4' THEN 'Ⅳ' " + + " WHEN '5' THEN 'Ⅴ' " + + " WHEN '6' THEN 'V' " + + " ELSE '未知' " + + "END AS wwqtgName,") .append("t.DTIN_TYPE AS dtinType, ") .append("t.RSTCD AS rstcd, ") .append("t.FHSTCD AS fhstcd, ") @@ -4640,15 +4658,16 @@ public class EnvWqDataServiceImpl implements EnvWqDataService { .distinct() .collect(Collectors.toList()); - List wqDataList = queryWqDataByStcdBatch(stcdList); - Map wqDataMap = wqDataList.stream() - .collect(Collectors.toMap(EnvWqDataVo::getStcd, Function.identity(), (oldValue, newValue) -> newValue)); +// List wqDataList = queryWqDataByStcdBatch(stcdList); +// Map wqDataMap = wqDataList.stream() +// .collect(Collectors.toMap(EnvWqDataVo::getStcd, Function.identity(), (oldValue, newValue) -> newValue)); for (EnvWqAnchorPointVo anchorPointVo : wqVoList) { - EnvWqDataVo wqData = wqDataMap.get(anchorPointVo.getStcd()); + EnvWqDataVo wqData = redisCacheUtil.fromJson(redisCacheUtil.getString("wqLastDate:" + anchorPointVo.getStcd()), EnvWqDataVo.class); if (wqData != null) { anchorPointVo.setTm(wqData.getTm()); anchorPointVo.setWqGrd(wqData.getWqgrd()); + anchorPointVo.setWqGrdName(wqData.getWqgrdName()); anchorPointVo.setSfdb(wqData.getSfdb()); anchorPointVo.setWtmp(wqData.getWtmp()); anchorPointVo.setPh(wqData.getPh()); @@ -4730,7 +4749,7 @@ public class EnvWqDataServiceImpl implements EnvWqDataService { .map(s -> "'" + s + "'") .collect(Collectors.joining(",")); - String wqDataSql = "SELECT STCD, TM, WQGRD, SFDB, WTMP, PH, DOX, CODMN, CODCR, BOD5, NH3N, TP, TN, CU, ZN, F, SE, ARS, HG, CD, CR6, PB, CN, VLPH, OIL, LAS, S2, FCG, CL, SO4, NO3, THRD, COND, FE, MN, AL, CHLA, CLARITY, TU, CYANO, TOD " + + String wqDataSql = "SELECT STCD, TM, WQGRD,CASE WQGRD WHEN '1' THEN 'Ⅰ' WHEN '2' THEN 'Ⅱ' WHEN '3' THEN 'Ⅲ' WHEN '4' THEN 'Ⅳ' WHEN '5' THEN 'Ⅴ' WHEN '6' THEN 'V' ELSE '未知' END AS wqgrdName, SFDB, WTMP, PH, DOX, CODMN, CODCR, BOD5, NH3N, TP, TN, CU, ZN, F, SE, ARS, HG, CD, CR6, PB, CN, VLPH, OIL, LAS, S2, FCG, CL, SO4, NO3, THRD, COND, FE, MN, AL, CHLA, CLARITY, TU, CYANO, TOD " + "FROM ( " + " SELECT t.*, ROW_NUMBER() OVER (PARTITION BY t.STCD ORDER BY t.TM DESC) AS rn " + " FROM SD_WQ_R t " + @@ -4952,4 +4971,29 @@ public class EnvWqDataServiceImpl implements EnvWqDataService { } } } + + @Override + public void wqLastData(JobBaseAo ao) { + List wqDataVos = wqJobMapper.getWqLastData(ao); + String startTimeStr = DateUtil.format(ao.getStartTime(), "yyyy-MM-dd HH:mm:ss"); + String endTimeStr = DateUtil.format(ao.getEndTime(), "yyyy-MM-dd HH:mm:ss"); + List wqYsDataVos = wqJobMapper.getWqYsLastData(startTimeStr, endTimeStr); + if (CollectionUtils.isNotEmpty(wqYsDataVos)) { + for (SdWqR wqDataVo : wqYsDataVos) { + redisCacheUtil.setString("wqYsLastDate:" + wqDataVo.getStcd(), JSONUtil.toJsonStr(wqDataVo)); + } + } + if (CollUtil.isEmpty(wqDataVos)) { + return; + } + if (!wqDataVos.isEmpty()) { + List> newWqrListPartition = CollUtil.split(wqDataVos, 300); + for (List t : newWqrListPartition) { + wqJobMapper.mergeWqR(t); + } + } + for (SdWqR wqDataVo : wqDataVos) { + redisCacheUtil.setString("wqLastDate:" + wqDataVo.getStcd(), JSONUtil.toJsonStr(wqDataVo)); + } + } } 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 5aa6b96f..1e3d7581 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 @@ -27,7 +27,10 @@ import com.yfd.platform.qgc_env.wt.entity.vo.WaterTempMapVO; import com.yfd.platform.qgc_env.wt.mapper.SdWtMonitorMapper; import com.yfd.platform.qgc_env.wt.utils.SiteAvoidanceUtils; import com.yfd.platform.qgc_env.wt.service.SdWtMonitorService; +import com.yfd.platform.qgc_job.vo.WtLastDataVo; +import com.yfd.platform.qgc_job.vo.WtvtPointVo; import com.yfd.platform.utils.QgcQueryWrapperUtil; +import com.yfd.platform.utils.RedisCacheUtil; import com.yfd.platform.utils.SecurityUtils; import jakarta.annotation.Resource; import lombok.Data; @@ -63,6 +66,9 @@ public class SdWtMonitorServiceImpl implements SdWtMonitorService { @Resource private IDataScopeFilterService dataScopeFilterService; + @Resource + private RedisCacheUtil redisCacheUtil; + @Override public DataSourceResult getEvnmAutoMonitorList(DataSourceRequest dataSourceRequest) { DataSourceLoadOptionsBase loadOptions = dataSourceRequest == null ? null : dataSourceRequest.toDevRequest(); @@ -3044,6 +3050,19 @@ public class SdWtMonitorServiceImpl implements SdWtMonitorService { vo.setAnchoPointState("WT_2"); } } + + if(vo.getEnfc()==1){ + //垂向水温 + WtvtPointVo wtvtPointVo =redisCacheUtil.fromJson(redisCacheUtil.getString("wtvtPointData:" + vo.getStcd()), WtvtPointVo.class); + vo.setWtvtInfo(wtvtPointVo); + }else{ + //河道水温 + WtLastDataVo sdWtrvR = redisCacheUtil.fromJson(redisCacheUtil.getString("wtLastData:" + vo.getStcd()), WtLastDataVo.class); + if(sdWtrvR !=null){ + vo.setTm(sdWtrvR.getTm()); + vo.setTemperature(sdWtrvR.getWt()); + } + } } SiteAvoidanceUtils.calcSiteMapLev(resultList, diff --git a/backend/src/main/java/com/yfd/platform/qgc_env/wte/service/impl/WeFishServiceImpl.java b/backend/src/main/java/com/yfd/platform/qgc_env/wte/service/impl/WeFishServiceImpl.java index cfefb4b9..45316df3 100644 --- a/backend/src/main/java/com/yfd/platform/qgc_env/wte/service/impl/WeFishServiceImpl.java +++ b/backend/src/main/java/com/yfd/platform/qgc_env/wte/service/impl/WeFishServiceImpl.java @@ -16,6 +16,7 @@ import com.yfd.platform.qgc_env.wte.service.WeFishService; import com.yfd.platform.utils.KendoUtil; import com.yfd.platform.utils.QgcQueryWrapperUtil; import jakarta.annotation.Resource; +import org.springframework.cache.annotation.Cacheable; import org.springframework.stereotype.Service; import java.math.BigDecimal; @@ -1035,6 +1036,7 @@ public class WeFishServiceImpl implements WeFishService { } @Override + @Cacheable(cacheNames = "weCache#3600", keyGenerator = "cacheKeyGenerator") public DataSourceResult getQgcWeFishPoint(DataSourceRequest dataSourceRequest) { DataSourceLoadOptionsBase loadOptions = dataSourceRequest == null ? null : dataSourceRequest.toDevRequest(); String stcd = resolveWeFishStringFilterValue(dataSourceRequest, loadOptions, "stcd"); @@ -1067,6 +1069,7 @@ public class WeFishServiceImpl implements WeFishService { } @Override + @Cacheable(cacheNames = "weCache#3600", keyGenerator = "cacheKeyGenerator") public DataSourceResult getQgcWePoint(DataSourceRequest dataSourceRequest) { DataSourceResult result = new DataSourceResult<>(); result.setAggregates(new HashMap<>()); diff --git a/backend/src/main/java/com/yfd/platform/qgc_env/zq/service/impl/ZqMonitorServiceImpl.java b/backend/src/main/java/com/yfd/platform/qgc_env/zq/service/impl/ZqMonitorServiceImpl.java index 75b64aed..85743199 100644 --- a/backend/src/main/java/com/yfd/platform/qgc_env/zq/service/impl/ZqMonitorServiceImpl.java +++ b/backend/src/main/java/com/yfd/platform/qgc_env/zq/service/impl/ZqMonitorServiceImpl.java @@ -24,7 +24,9 @@ import com.yfd.platform.qgc_env.zq.entity.vo.ZqRiverDrtpDataVo; import com.yfd.platform.qgc_env.zq.entity.vo.ZqRiverDataVo; import com.yfd.platform.qgc_env.wt.utils.SiteAvoidanceUtils; import com.yfd.platform.qgc_env.zq.service.ZqMonitorService; +import com.yfd.platform.qgc_job.vo.ZqLastDataVo; import com.yfd.platform.utils.QgcQueryWrapperUtil; +import com.yfd.platform.utils.RedisCacheUtil; import com.yfd.platform.utils.SecurityUtils; import jakarta.annotation.Resource; import org.springframework.cache.annotation.Cacheable; @@ -50,7 +52,6 @@ public class ZqMonitorServiceImpl implements ZqMonitorService { private static final String ZQ_MONTH_TABLE_NAME = "SD_RIVERDRTP_S"; private static final Map ZQ_UPDATE_COLUMN_MAP = new LinkedHashMap<>(); private static final Map ZQ_FIELD_MEANING_MAP = new LinkedHashMap<>(); - static { ZQ_UPDATE_COLUMN_MAP.put("z", "Z"); ZQ_UPDATE_COLUMN_MAP.put("q", "Q"); @@ -90,6 +91,8 @@ public class ZqMonitorServiceImpl implements ZqMonitorService { @Resource private MsOperationLogDetailMapper msOperationLogDetailMapper; + @Resource + private RedisCacheUtil redisCacheUtil; @Override @Cacheable(cacheNames = "zqCache#7200", keyGenerator = "cacheKeyGenerator") @@ -2374,6 +2377,7 @@ public class ZqMonitorServiceImpl implements ZqMonitorService { } @Override + @Cacheable(cacheNames = "zqCache#3600", keyGenerator = "cacheKeyGenerator") public DataSourceResult getZqPointKendoListCust(DataSourceRequest dataSourceRequest) { DataSourceResult dataSourceResult = new DataSourceResult<>(); dataSourceResult.setAggregates(new HashMap<>()); @@ -2440,7 +2444,16 @@ public class ZqMonitorServiceImpl implements ZqMonitorService { Page page = QgcQueryWrapperUtil.buildPage(loadOptions, loadOptions.getSkip(), loadOptions.getTake()); List resultList = microservicDynamicSQLMapper.pageAllListWithResultType(page, sql.toString(), paramMap, ZqPoint.class); - + resultList.forEach(item->{ + //获取流量站最新数据 + ZqLastDataVo zqLastData = redisCacheUtil.fromJson(redisCacheUtil.getString("zqLastData:"+item.getStcd()), ZqLastDataVo.class); + if(zqLastData !=null){ + item.setTm(zqLastData.getTm()); + item.setQ(zqLastData.getQ()); + item.setZ(zqLastData.getZ()); + item.setV(zqLastData.getV()); + } + }); SiteAvoidanceUtils.calcSiteMapLev(resultList, ZqPoint::getStcd, ZqPoint::getLgtd, diff --git a/backend/src/main/java/com/yfd/platform/qgc_job/service/IDwJobService.java b/backend/src/main/java/com/yfd/platform/qgc_job/service/IDwJobService.java new file mode 100644 index 00000000..dff8a481 --- /dev/null +++ b/backend/src/main/java/com/yfd/platform/qgc_job/service/IDwJobService.java @@ -0,0 +1,10 @@ +package com.yfd.platform.qgc_job.service; + +import com.yfd.platform.qgc_env.wq.entity.ao.JobBaseAo; + +public interface IDwJobService { + + void wtLastData(JobBaseAo ao); + + void wtvtPointData(JobBaseAo ao); +} diff --git a/backend/src/main/java/com/yfd/platform/qgc_job/service/IEqJobService.java b/backend/src/main/java/com/yfd/platform/qgc_job/service/IEqJobService.java new file mode 100644 index 00000000..05285586 --- /dev/null +++ b/backend/src/main/java/com/yfd/platform/qgc_job/service/IEqJobService.java @@ -0,0 +1,8 @@ +package com.yfd.platform.qgc_job.service; + +import com.yfd.platform.qgc_env.wq.entity.ao.JobBaseAo; + +public interface IEqJobService { + + void eqAnchorPointLastDate(JobBaseAo ao); +} diff --git a/backend/src/main/java/com/yfd/platform/qgc_job/service/IFhJobService.java b/backend/src/main/java/com/yfd/platform/qgc_job/service/IFhJobService.java new file mode 100644 index 00000000..5b7b8f0b --- /dev/null +++ b/backend/src/main/java/com/yfd/platform/qgc_job/service/IFhJobService.java @@ -0,0 +1,8 @@ +package com.yfd.platform.qgc_job.service; + +import com.yfd.platform.qgc_env.wq.entity.ao.JobBaseAo; + +public interface IFhJobService { + + void zqLastData(JobBaseAo ao); +} diff --git a/backend/src/main/java/com/yfd/platform/qgc_job/service/impl/DwJobServiceImpl.java b/backend/src/main/java/com/yfd/platform/qgc_job/service/impl/DwJobServiceImpl.java new file mode 100644 index 00000000..894253ff --- /dev/null +++ b/backend/src/main/java/com/yfd/platform/qgc_job/service/impl/DwJobServiceImpl.java @@ -0,0 +1,291 @@ +package com.yfd.platform.qgc_job.service.impl; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.date.DateTime; +import cn.hutool.core.date.DateUtil; +import cn.hutool.json.JSONUtil; +import com.yfd.platform.common.MicroservicDynamicSQLMapper; +import com.yfd.platform.qgc_env.wq.entity.ao.JobBaseAo; +import com.yfd.platform.qgc_job.service.IDwJobService; +import com.yfd.platform.qgc_job.vo.WtLastDataVo; +import com.yfd.platform.qgc_job.vo.WtvtPointDataVo; +import com.yfd.platform.qgc_job.vo.WtvtPointVo; +import com.yfd.platform.utils.RedisCacheUtil; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.Date; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +@Slf4j +@Service +public class DwJobServiceImpl implements IDwJobService { + + private static final String WT_LAST_DATA_KEY_PREFIX = "wtLastData:"; + private static final String WTVT_POINT_DATA_KEY_PREFIX = "wtvtPointData:"; + + @Resource + private MicroservicDynamicSQLMapper microservicDynamicSQLMapper; + + @Resource + private RedisCacheUtil redisCacheUtil; + + @Override + public void wtLastData(JobBaseAo ao) { + if (ao == null) { + throw new RuntimeException("任务参数不能为空"); + } + ao.initTime(); + Date startTime = ao.getStartTime(); + Date endTime = ao.getEndTime(); + Set stcds = ao.getStcds(); + + List> dateList = getInterValDate(startTime, endTime); + Map lastDataMap = new LinkedHashMap<>(); + for (int index = dateList.size() - 1; index >= 0; index--) { + Map dateMap = dateList.get(index); + Date segmentStartTime = dateMap.get("startTime"); + Date segmentEndTime = dateMap.get("endTime"); + List wtLastDataList = getWtLastData(stcds, segmentStartTime, segmentEndTime); + for (WtLastDataVo wtLastDataVo : wtLastDataList) { + if (wtLastDataVo == null || wtLastDataVo.getStcd() == null || lastDataMap.containsKey(wtLastDataVo.getStcd())) { + continue; + } + lastDataMap.put(wtLastDataVo.getStcd(), wtLastDataVo); + } + } + + int count = 0; + for (Map.Entry entry : lastDataMap.entrySet()) { + String stcd = entry.getKey(); + WtLastDataVo wtLastDataVo = entry.getValue(); + WtLastDataVo redisData = redisCacheUtil.fromJson(redisCacheUtil.getString(WT_LAST_DATA_KEY_PREFIX + stcd), WtLastDataVo.class); + if (shouldRefresh(redisData, wtLastDataVo)) { + redisCacheUtil.setString(WT_LAST_DATA_KEY_PREFIX + stcd, JSONUtil.toJsonStr(wtLastDataVo)); + count++; + } else { + redisCacheUtil.delete(WT_LAST_DATA_KEY_PREFIX + stcd); + } + } + log.info("本次更新水温缓存{}条", count); + } + + @Override + public void wtvtPointData(JobBaseAo ao) { + if (ao == null) { + throw new RuntimeException("任务参数不能为空"); + } + ao.initTime(); + Date startTime = ao.getStartTime(); + Date endTime = ao.getEndTime(); + Set stcds = ao.getStcds(); + + List> dateList = getInterValDate(startTime, endTime); + Map> lastDataMap = new LinkedHashMap<>(); + for (int index = dateList.size() - 1; index >= 0; index--) { + Map dateMap = dateList.get(index); + Date segmentStartTime = dateMap.get("startTime"); + Date segmentEndTime = dateMap.get("endTime"); + List wtvtLastDataList = getWtvtPointLastData(stcds, segmentStartTime, segmentEndTime); + if (CollUtil.isEmpty(wtvtLastDataList)) { + continue; + } + Map> groupedMap = new LinkedHashMap<>(); + for (WtvtPointDataVo wtvtPointDataVo : wtvtLastDataList) { + if (wtvtPointDataVo == null || wtvtPointDataVo.getStcd() == null) { + continue; + } + groupedMap.computeIfAbsent(wtvtPointDataVo.getStcd(), key -> new ArrayList<>()).add(wtvtPointDataVo); + } + for (Map.Entry> entry : groupedMap.entrySet()) { + if (!lastDataMap.containsKey(entry.getKey())) { + lastDataMap.put(entry.getKey(), entry.getValue()); + } + } + } + + int count = 0; + for (Map.Entry> entry : lastDataMap.entrySet()) { + String stcd = entry.getKey(); + WtvtPointVo wtvtPointVo = buildWtvtPointVo(stcd, entry.getValue()); + WtvtPointVo redisData = redisCacheUtil.fromJson(redisCacheUtil.getString(WTVT_POINT_DATA_KEY_PREFIX + stcd), WtvtPointVo.class); + if (shouldRefresh(redisData, wtvtPointVo)) { + redisCacheUtil.setString(WTVT_POINT_DATA_KEY_PREFIX + wtvtPointVo.getStcd(), JSONUtil.toJsonStr(wtvtPointVo)); + count++; + } + } + log.info("本次更新垂向水温缓存{}条", count); + } + + private boolean shouldRefresh(WtLastDataVo redisData, WtLastDataVo currentData) { + if (currentData == null || currentData.getTm() == null) { + return false; + } + return redisData == null || redisData.getTm() == null || DateUtil.compare(redisData.getTm(), currentData.getTm()) < 0; + } + + private boolean shouldRefresh(WtvtPointVo redisData, WtvtPointVo currentData) { + if (currentData == null || currentData.getTm() == null) { + return false; + } + return redisData == null || redisData.getTm() == null || DateUtil.compare(redisData.getTm(), currentData.getTm()) < 0; + } + + private List getWtLastData(Set stcds, Date startTime, Date endTime) { + Map paramMap = new HashMap<>(); + paramMap.put("startTime", startTime); + paramMap.put("endTime", endTime); + + StringBuilder sql = new StringBuilder(); + sql.append("SELECT ") + .append(" data.ID AS id, ") + .append(" data.STCD AS stcd, ") + .append(" data.TM AS tm, ") + .append(" data.WT AS wt, ") + .append(" data.REMARK AS remark, ") + .append(" data.FID AS fid, ") + .append(" data.RECORD_USER AS recordUser, ") + .append(" data.RECORD_TIME AS recordTime, ") + .append(" data.MODIFY_USER AS modifyUser, ") + .append(" data.MODIFY_TIME AS modifyTime, ") + .append(" data.IS_DELETED AS isDeleted, ") + .append(" data.DELETE_USER AS deleteUser, ") + .append(" data.DELETE_TIME AS deleteTime ") + .append("FROM ( ") + .append(" SELECT t.*, ROW_NUMBER() OVER (PARTITION BY t.STCD ORDER BY t.TM DESC, t.RECORD_TIME DESC, t.ID DESC) AS rn ") + .append(" FROM SD_WTRV_R t ") + .append(" WHERE NVL(t.IS_DELETED, 0) = 0 ") + .append(" AND t.TM >= #{map.startTime} ") + .append(" AND t.TM <= #{map.endTime} "); + appendInCondition(sql, paramMap, "t.STCD", stcds, "wtLastDataStcd"); + sql.append(") data WHERE data.rn = 1"); + return microservicDynamicSQLMapper.getAllListWithResultType(sql.toString(), paramMap, WtLastDataVo.class); + } + + private List getWtvtPointLastData(Set stcds, Date startTime, Date endTime) { + Map paramMap = new HashMap<>(); + paramMap.put("startTime", startTime); + paramMap.put("endTime", endTime); + + StringBuilder sql = new StringBuilder(); + sql.append("WITH temp AS ( ") + .append(" SELECT STCD, VWT, TM, WTHG ") + .append(" FROM ( ") + .append(" SELECT STCD, VWT, TM, WTHG, DENSE_RANK() OVER (PARTITION BY STCD ORDER BY TM DESC) AS rn ") + .append(" FROM SD_WTVT_R ") + .append(" WHERE NVL(IS_DELETED, 0) = 0 ") + .append(" AND TM >= #{map.startTime} ") + .append(" AND TM <= #{map.endTime} "); + appendInCondition(sql, paramMap, "STCD", stcds, "wtvtPointDataStcd"); + sql.append(" ) ") + .append(" WHERE rn = 1 ") + .append(") ") + .append("SELECT ") + .append(" t1.STCD AS stcd, ") + .append(" t2.TM AS tm, ") + .append(" t1.AVGWT AS avgwt, ") + .append(" t2.VWT AS vwt, ") + .append(" t2.WTHG AS wthg, ") + .append(" t2.MINRN AS minrn, ") + .append(" t2.MAXRN AS maxrn ") + .append("FROM ( ") + .append(" SELECT STCD, ROUND(AVG(VWT), 2) AS AVGWT ") + .append(" FROM temp GROUP BY STCD ") + .append(") t1 ") + .append("INNER JOIN ( ") + .append(" SELECT * FROM ( ") + .append(" SELECT ") + .append(" STCD, VWT, TM, WTHG, ") + .append(" ROW_NUMBER() OVER (PARTITION BY STCD ORDER BY WTHG ASC) AS minrn, ") + .append(" ROW_NUMBER() OVER (PARTITION BY STCD ORDER BY WTHG DESC) AS maxrn ") + .append(" FROM temp ") + .append(" ) WHERE MINRN = 1 OR MAXRN = 1 ") + .append(") t2 ON t1.STCD = t2.STCD"); + return microservicDynamicSQLMapper.getAllListWithResultType(sql.toString(), paramMap, WtvtPointDataVo.class); + } + + private WtvtPointVo buildWtvtPointVo(String stcd, List wtvtPointDataVoList) { + WtvtPointVo wtvtPointVo = new WtvtPointVo(); + wtvtPointVo.setStcd(stcd); + if (CollUtil.isEmpty(wtvtPointDataVoList)) { + return wtvtPointVo; + } + for (WtvtPointDataVo wtvtPointDataVo : wtvtPointDataVoList) { + if (wtvtPointDataVo == null) { + continue; + } + wtvtPointVo.setTm(wtvtPointDataVo.getTm()); + wtvtPointVo.setAvgWt(wtvtPointDataVo.getAvgwt()); + if (Integer.valueOf(1).equals(wtvtPointDataVo.getMinrn())) { + wtvtPointVo.setMinWthg(wtvtPointDataVo.getWthg()); + wtvtPointVo.setMinWthgWt(wtvtPointDataVo.getVwt()); + } + if (Integer.valueOf(1).equals(wtvtPointDataVo.getMaxrn())) { + wtvtPointVo.setMaxWthg(wtvtPointDataVo.getWthg()); + wtvtPointVo.setMaxWthgWt(wtvtPointDataVo.getVwt()); + } + } + return wtvtPointVo; + } + + // 以指定时间间隔分隔开始时间和结束时间,保持旧平台 7 天分段口径 + private List> getInterValDate(Date startTime, Date endTime) { + int num = 7; + List> dateList = new ArrayList<>(); + long interval = DateUtil.betweenDay(startTime, endTime, false); + if (interval <= num) { + Map dateMap = new HashMap<>(); + dateMap.put("startTime", startTime); + dateMap.put("endTime", endTime); + dateList.add(dateMap); + return dateList; + } + while (startTime.getTime() < endTime.getTime()) { + Map dateMap = new HashMap<>(); + DateTime dateTime = DateUtil.offsetDay(startTime, num); + dateMap.put("startTime", startTime); + if (dateTime.getTime() < endTime.getTime()) { + dateMap.put("endTime", DateUtil.offsetSecond(dateTime, -1)); + } else { + dateMap.put("endTime", endTime); + } + dateList.add(dateMap); + startTime = dateTime; + } + dateList.sort(Comparator.comparing(item -> item.get("startTime"))); + return dateList; + } + + private void appendInCondition(StringBuilder sql, + Map paramMap, + String column, + Set values, + String prefix) { + if (CollUtil.isEmpty(values)) { + return; + } + List placeholders = new ArrayList<>(); + int index = 0; + for (String value : values) { + if (value == null || value.trim().isEmpty()) { + continue; + } + String key = prefix + index++; + paramMap.put(key, value.trim()); + placeholders.add("#{map." + key + "}"); + } + if (CollUtil.isEmpty(placeholders)) { + return; + } + sql.append(" AND ").append(column).append(" IN (") + .append(String.join(", ", placeholders)) + .append(") "); + } +} diff --git a/backend/src/main/java/com/yfd/platform/qgc_job/service/impl/EqJobServiceImpl.java b/backend/src/main/java/com/yfd/platform/qgc_job/service/impl/EqJobServiceImpl.java new file mode 100644 index 00000000..d5dcd9ea --- /dev/null +++ b/backend/src/main/java/com/yfd/platform/qgc_job/service/impl/EqJobServiceImpl.java @@ -0,0 +1,85 @@ +package com.yfd.platform.qgc_job.service.impl; + +import cn.hutool.json.JSONUtil; +import com.yfd.platform.common.MicroservicDynamicSQLMapper; +import com.yfd.platform.qgc_env.wq.entity.ao.JobBaseAo; +import com.yfd.platform.qgc_job.service.IEqJobService; +import com.yfd.platform.qgc_job.vo.EqAnchorPointLastDateVo; +import com.yfd.platform.utils.RedisCacheUtil; +import jakarta.annotation.Resource; +import org.springframework.stereotype.Service; + +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@Service +public class EqJobServiceImpl implements IEqJobService { + + @Resource + private MicroservicDynamicSQLMapper microservicDynamicSQLMapper; + + @Resource + private RedisCacheUtil redisCacheUtil; + + @Override + public void eqAnchorPointLastDate(JobBaseAo ao) { + Map paramMap = new HashMap<>(); + // 旧逻辑只按当前时间截取最新一条工程运行数据,ao 在此方法中不参与过滤 + paramMap.put("currentTime", new Date()); + + List eqDataVos = microservicDynamicSQLMapper.getAllListWithResultType( + buildEqAnchorPointLastDateSql(), + paramMap, + EqAnchorPointLastDateVo.class + ); + for (EqAnchorPointLastDateVo eqDataVo : eqDataVos) { + redisCacheUtil.setString("eqAnchorPointLastDate:" + eqDataVo.getStcd(), JSONUtil.toJsonStr(eqDataVo)); + } + } + + private String buildEqAnchorPointLastDateSql() { + return "SELECT " + + "run.STCD AS stcd, " + + "run.TM AS tm, " + + "run.QI AS qi, " + + "run.QO AS qo, " + + "run.RZ AS rz, " + + "run.DZ AS dz, " + + "qec.QEC_LIMIT AS qecLimit, " + + "qec.QEC_C AS qecC, " + + "qec.MWR_LIMIT AS mwrLimit, " + + "qec.MWR_C AS mwrC, " + + "NVL(qec.AVQ_LIMIT, eng.AVQ) AS avqLimit, " + + "qec.AVQ_C AS avqC, " + + "qecDetail.VLSR AS vlsr, " + + "qecDetail.VLSR_TM AS vlsrTm, " + + "mwrDetail.VLSR AS mwrVlsr, " + + "mwrDetail.VLSR_TM AS mwrVlsrTm " + + "FROM ( " + + " SELECT STCD, MAX(TM) AS TM " + + " FROM SD_HYDROPW_R " + + " WHERE NVL(IS_DELETED, 0) = 0 " + + " AND TM <= #{map.currentTime} " + + " GROUP BY STCD " + + ") lastRun " + + "INNER JOIN SD_HYDROPW_R run " + + " ON lastRun.STCD = run.STCD " + + " AND lastRun.TM = run.TM " + + " AND NVL(run.IS_DELETED, 0) = 0 " + + "LEFT JOIN SD_QEC_R qec " + + " ON qec.STCD = run.STCD " + + " AND qec.TM = run.TM " + + " AND NVL(qec.IS_DELETED, 0) = 0 " + + "LEFT JOIN SD_ENGINFO_B_H eng " + + " ON eng.STCD = run.STCD " + + " AND NVL(eng.IS_DELETED, 0) = 0 " + + "LEFT JOIN MS_WARN_RULE_DETAIL_B qecDetail " + + " ON qecDetail.ID = qec.QEC_RULE_DID " + + " AND NVL(qecDetail.IS_DELETED, 0) = 0 " + + "LEFT JOIN MS_WARN_RULE_DETAIL_B mwrDetail " + + " ON mwrDetail.ID = qec.QEC_MWRRULE_DID " + + " AND NVL(mwrDetail.IS_DELETED, 0) = 0"; + } +} diff --git a/backend/src/main/java/com/yfd/platform/qgc_job/service/impl/FhJobServiceImpl.java b/backend/src/main/java/com/yfd/platform/qgc_job/service/impl/FhJobServiceImpl.java new file mode 100644 index 00000000..149202f0 --- /dev/null +++ b/backend/src/main/java/com/yfd/platform/qgc_job/service/impl/FhJobServiceImpl.java @@ -0,0 +1,95 @@ +package com.yfd.platform.qgc_job.service.impl; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.json.JSONUtil; +import com.yfd.platform.common.MicroservicDynamicSQLMapper; +import com.yfd.platform.qgc_env.wq.entity.ao.JobBaseAo; +import com.yfd.platform.qgc_job.service.IFhJobService; +import com.yfd.platform.qgc_job.vo.ZqLastDataVo; +import com.yfd.platform.utils.RedisCacheUtil; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +@Slf4j +@Service +public class FhJobServiceImpl implements IFhJobService { + + private static final String ZQ_LAST_DATA_KEY_PREFIX = "zqLastData:"; + + @Resource + private MicroservicDynamicSQLMapper microservicDynamicSQLMapper; + + @Resource + private RedisCacheUtil redisCacheUtil; + + @Override + public void zqLastData(JobBaseAo ao) { + Set stcds = ao == null ? null : ao.getStcds(); + List zqDataVos = getZqLastData(stcds); + if (CollUtil.isEmpty(zqDataVos)) { + return; + } + for (ZqLastDataVo zqDataVo : zqDataVos) { + if (zqDataVo == null || zqDataVo.getStcd() == null) { + continue; + } + redisCacheUtil.setString(ZQ_LAST_DATA_KEY_PREFIX + zqDataVo.getStcd(), JSONUtil.toJsonStr(zqDataVo)); + } + log.info("本次更新河道水情缓存{}条", zqDataVos.size()); + } + + private List getZqLastData(Set stcds) { + Map paramMap = new HashMap<>(); + StringBuilder sql = new StringBuilder(); + sql.append("SELECT ") + .append("t.STCD AS stcd, ") + .append("t.Z AS z, ") + .append("t.Q AS q, ") + .append("t.V AS v, ") + .append("t.TM AS tm ") + .append("FROM SD_RIVER_R t ") + .append("INNER JOIN ( ") + .append(" SELECT STCD, MAX(TM) AS tm ") + .append(" FROM SD_RIVER_R ") + .append(" WHERE NVL(IS_DELETED, 0) = 0 "); + appendInCondition(sql, paramMap, "STCD", stcds, "zqLastDataInnerStcd"); + sql.append(" GROUP BY STCD ") + .append(") t1 ON t.STCD = t1.STCD AND t.TM = t1.tm ") + .append("WHERE NVL(t.IS_DELETED, 0) = 0 "); + appendInCondition(sql, paramMap, "t.STCD", stcds, "zqLastDataOuterStcd"); + return microservicDynamicSQLMapper.getAllListWithResultType(sql.toString(), paramMap, ZqLastDataVo.class); + } + + private void appendInCondition(StringBuilder sql, + Map paramMap, + String column, + Set values, + String prefix) { + if (CollUtil.isEmpty(values)) { + return; + } + List placeholders = new ArrayList<>(); + int index = 0; + for (String value : values) { + if (value == null || value.trim().isEmpty()) { + continue; + } + String key = prefix + index++; + paramMap.put(key, value.trim()); + placeholders.add("#{map." + key + "}"); + } + if (CollUtil.isEmpty(placeholders)) { + return; + } + sql.append(" AND ").append(column).append(" IN (") + .append(String.join(", ", placeholders)) + .append(") "); + } +} diff --git a/backend/src/main/java/com/yfd/platform/qgc_job/vo/EqAnchorPointLastDateVo.java b/backend/src/main/java/com/yfd/platform/qgc_job/vo/EqAnchorPointLastDateVo.java new file mode 100644 index 00000000..78889407 --- /dev/null +++ b/backend/src/main/java/com/yfd/platform/qgc_job/vo/EqAnchorPointLastDateVo.java @@ -0,0 +1,79 @@ +package com.yfd.platform.qgc_job.vo; + +import com.fasterxml.jackson.annotation.JsonFormat; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.io.Serializable; +import java.math.BigDecimal; +import java.util.Date; + +@Data +@Schema(description = "电站锚点最新缓存数据") +public class EqAnchorPointLastDateVo implements Serializable { + + private static final long serialVersionUID = 1L; + + @Schema(description = "电站编码") + private String stcd; + + @Schema(description = "电站名称") + private String stnm; + + @Schema(description = "数据时间") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") + private Date tm; + + @Schema(description = "装机容量") + private BigDecimal ttpwr; + + @Schema(description = "生态流量限值(环保部)") + private BigDecimal qecLimit; + + @Schema(description = "生态流量限值(水利部)") + private BigDecimal mwrLimit; + + @Schema(description = "生态流量限值(多年平均)") + private BigDecimal avqLimit; + + @Schema(description = "生态流量系数(环保部)") + private BigDecimal qecC; + + @Schema(description = "生态流量系数(水利部)") + private BigDecimal mwrC; + + @Schema(description = "生态流量系数(多年平均)") + private BigDecimal avqC; + + @Schema(description = "正常蓄水位") + private BigDecimal normz; + + @Schema(description = "死水位") + private BigDecimal ddz; + + @Schema(description = "入库流量") + private BigDecimal qi; + + @Schema(description = "出库流量") + private BigDecimal qo; + + @Schema(description = "坝上水位") + private BigDecimal rz; + + @Schema(description = "坝下水位") + private BigDecimal dz; + + @Schema(description = "规则来源") + private String vlsr; + + @Schema(description = "规则来源时间") + @JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8") + private Date vlsrTm; + + @Schema(description = "规则来源(水利部)") + private String mwrVlsr; + + @Schema(description = "规则来源时间(水利部)") + @JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8") + private Date mwrVlsrTm; +} diff --git a/backend/src/main/java/com/yfd/platform/qgc_job/vo/WtLastDataVo.java b/backend/src/main/java/com/yfd/platform/qgc_job/vo/WtLastDataVo.java new file mode 100644 index 00000000..0b2046b2 --- /dev/null +++ b/backend/src/main/java/com/yfd/platform/qgc_job/vo/WtLastDataVo.java @@ -0,0 +1,59 @@ +package com.yfd.platform.qgc_job.vo; + +import com.fasterxml.jackson.annotation.JsonFormat; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.io.Serializable; +import java.math.BigDecimal; +import java.util.Date; + +@Data +@Schema(description = "水温最新缓存数据") +public class WtLastDataVo implements Serializable { + + private static final long serialVersionUID = 1L; + + @Schema(description = "主键ID") + private String id; + + @Schema(description = "站点编码") + private String stcd; + + @Schema(description = "数据时间") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") + private Date tm; + + @Schema(description = "水温") + private BigDecimal wt; + + @Schema(description = "备注") + private String remark; + + @Schema(description = "附件ID") + private String fid; + + @Schema(description = "创建人") + private String recordUser; + + @Schema(description = "创建时间") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") + private Date recordTime; + + @Schema(description = "更新人") + private String modifyUser; + + @Schema(description = "更新时间") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") + private Date modifyTime; + + @Schema(description = "是否删除") + private Integer isDeleted; + + @Schema(description = "删除人") + private String deleteUser; + + @Schema(description = "删除时间") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") + private Date deleteTime; +} diff --git a/backend/src/main/java/com/yfd/platform/qgc_job/vo/WtvtPointDataVo.java b/backend/src/main/java/com/yfd/platform/qgc_job/vo/WtvtPointDataVo.java new file mode 100644 index 00000000..7af5d3ed --- /dev/null +++ b/backend/src/main/java/com/yfd/platform/qgc_job/vo/WtvtPointDataVo.java @@ -0,0 +1,37 @@ +package com.yfd.platform.qgc_job.vo; + +import com.fasterxml.jackson.annotation.JsonFormat; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.io.Serializable; +import java.util.Date; + +@Data +@Schema(description = "垂向水温锚点中间数据") +public class WtvtPointDataVo implements Serializable { + + private static final long serialVersionUID = 1L; + + @Schema(description = "站点编码") + private String stcd; + + @Schema(description = "数据时间") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") + private Date tm; + + @Schema(description = "平均水温") + private Double avgwt; + + @Schema(description = "水温") + private Double vwt; + + @Schema(description = "测量深度") + private Double wthg; + + @Schema(description = "最小深度行号") + private Integer minrn; + + @Schema(description = "最大深度行号") + private Integer maxrn; +} diff --git a/backend/src/main/java/com/yfd/platform/qgc_job/vo/WtvtPointVo.java b/backend/src/main/java/com/yfd/platform/qgc_job/vo/WtvtPointVo.java new file mode 100644 index 00000000..1ec24ba9 --- /dev/null +++ b/backend/src/main/java/com/yfd/platform/qgc_job/vo/WtvtPointVo.java @@ -0,0 +1,37 @@ +package com.yfd.platform.qgc_job.vo; + +import com.fasterxml.jackson.annotation.JsonFormat; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.io.Serializable; +import java.util.Date; + +@Data +@Schema(description = "垂向水温锚点缓存数据") +public class WtvtPointVo implements Serializable { + + private static final long serialVersionUID = 1L; + + @Schema(description = "站点编码") + private String stcd; + + @Schema(description = "数据时间") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") + private Date tm; + + @Schema(description = "平均温度") + private Double avgWt; + + @Schema(description = "最大测量深度") + private Double maxWthg; + + @Schema(description = "最大测量深度水温") + private Double maxWthgWt; + + @Schema(description = "最小测量深度") + private Double minWthg; + + @Schema(description = "最小测量深度水温") + private Double minWthgWt; +} diff --git a/backend/src/main/java/com/yfd/platform/qgc_job/vo/ZqLastDataVo.java b/backend/src/main/java/com/yfd/platform/qgc_job/vo/ZqLastDataVo.java new file mode 100644 index 00000000..9aff5c5b --- /dev/null +++ b/backend/src/main/java/com/yfd/platform/qgc_job/vo/ZqLastDataVo.java @@ -0,0 +1,27 @@ +package com.yfd.platform.qgc_job.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.math.BigDecimal; +import java.util.Date; + +@Data +@Schema(description = "河道水情最新缓存") +public class ZqLastDataVo { + + @Schema(description = "站码") + private String stcd; + + @Schema(description = "水位") + private BigDecimal z; + + @Schema(description = "流量") + private BigDecimal q; + + @Schema(description = "流速") + private BigDecimal v; + + @Schema(description = "时间") + private Date tm; +} diff --git a/backend/src/main/java/com/yfd/platform/utils/RedisCacheUtil.java b/backend/src/main/java/com/yfd/platform/utils/RedisCacheUtil.java index 2640763f..0afb6807 100644 --- a/backend/src/main/java/com/yfd/platform/utils/RedisCacheUtil.java +++ b/backend/src/main/java/com/yfd/platform/utils/RedisCacheUtil.java @@ -1,6 +1,9 @@ package com.yfd.platform.utils; +import com.fasterxml.jackson.databind.ObjectMapper; import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.stereotype.Component; @@ -12,8 +15,9 @@ import java.util.Set; import java.util.concurrent.TimeUnit; @Component +@Slf4j public class RedisCacheUtil { - + static final ObjectMapper objectMapper = new ObjectMapper(); @Resource private RedisTemplate redisTemplate; @@ -41,6 +45,21 @@ public class RedisCacheUtil { return (T) value; } + public T fromJson(String json, Class clazz) { + if (!StringUtils.isBlank(json) && clazz != null) { + try { + return (T)objectMapper.readValue(json, clazz); + } catch (Exception var4) { + if (log.isErrorEnabled()) { + log.error(var4.getMessage(), var4); + } + return null; + } + } else { + return null; + } + } + public Object get(String key) { return redisTemplate.opsForValue().get(key); } diff --git a/backend/src/main/resources/mapper/qgc_env/wq/WqJobMapper.xml b/backend/src/main/resources/mapper/qgc_env/wq/WqJobMapper.xml new file mode 100644 index 00000000..27da8341 --- /dev/null +++ b/backend/src/main/resources/mapper/qgc_env/wq/WqJobMapper.xml @@ -0,0 +1,56 @@ + + + + + + + + + + MERGE INTO SD_WQ_R T + USING ( + + SELECT #{wqR.id,jdbcType=VARCHAR} AS ID + , #{wqR.wqgrd,jdbcType=VARCHAR} AS WQGRD + , #{wqR.sfdb,jdbcType=NUMERIC} AS SFDB + , #{wqR.wqSfdbhnYs,jdbcType=VARCHAR} AS WQ_SFDBHN_YS + , SYSDATE AS MODIFY_TIME + FROM DUAL + + ) S + ON (T.ID = S.ID) + WHEN MATCHED THEN + UPDATE SET T.WQGRD = S.WQGRD, T.SFDB = S.SFDB, T.WQ_SFDBHN_YS = S.WQ_SFDBHN_YS, T.MODIFY_TIME = S.MODIFY_TIME + + + \ No newline at end of file diff --git a/frontend/.env.development b/frontend/.env.development index 5cf083ba..0eca7e7f 100644 --- a/frontend/.env.development +++ b/frontend/.env.development @@ -12,9 +12,9 @@ VITE_APP_BASE_API = '/dev-api' # VITE_APP_BASE_URL = 'http://172.16.21.142:8093' # VITE_APP_BASE_URL = 'http://172.16.21.142:8096' # 汤伟 -VITE_APP_BASE_URL = 'http://10.84.111.235:8093' +# VITE_APP_BASE_URL = 'http://10.84.111.235:8093' # 李林 -# VITE_APP_BASE_URL = 'http://10.84.111.25:8093' +VITE_APP_BASE_URL = 'http://10.84.111.25:8093' # 测试环境线上10.84.121.122: VITE_APP_TEST_ONLINE_URL = 'https://211.99.26.225:12122' diff --git a/frontend/docs/地图模块-3D Cesium实施计划.md b/frontend/docs/地图模块-3D Cesium实施计划.md new file mode 100644 index 00000000..404e6040 --- /dev/null +++ b/frontend/docs/地图模块-3D Cesium实施计划.md @@ -0,0 +1,417 @@ +# 地图模块 3D Cesium 实施计划 + +## 1. 目标 + +在现有地图体系下补齐 `3D(Cesium)` 视图能力,使其尽量复用 `2D(OpenLayers)` 的业务编排结果,满足以下要求: + +- 3D 默认进入中国视角,不再落到美国区域。 +- 3D 初始化时显示过渡动画,再定位到中国。 +- 3D 保留基础底图加载能力,但不提供底图切换面板。 +- 3D 支持锚点展示。 +- 3D 支持与 2D 一致的鼠标悬停 popup,样式复用现有 popup。 +- 3D 支持与 2D 一致的锚点点击行为,点击后打开同一套详情弹框逻辑。 +- 菜单切换、图层树勾选、图例勾选后,3D 锚点与 2D 一样联动变化。 +- 3D 必须复用 `eng_point` 的层级显示规则,中型水电站仅在 `ENG_POINT_MEDIUM_VISIBLE_ZOOM = 7.5` 以上显示。 + +## 2. 2D 现状梳理 + +当前 `2D` 的能力并不是散落在页面里,而是已经形成了比较清晰的分层: + +### 2.1 页面入口 + +- `src/components/gis/GisView.vue` +- 负责挂载地图容器、popup 容器、地图控制器、图例、图层树。 +- 页面只调用 `mapOrchestrator.mountView()` 和 `handlePageChange()`,不直接拼地图业务。 + +### 2.2 地图编排层 + +- `src/modules/map/application/map-orchestrator.ts` +- 负责页面初始化、菜单切换、图层配置加载、图例配置加载、基地切换、缩放联动。 +- 2D/3D 都应尽量复用这一层,避免在 Cesium 侧另起一套业务流程。 + +### 2.3 地图统一门面 + +- `src/components/gis/map.class.ts` +- 对外暴露统一接口,如: + - `addBaseDataLayer` + - `addInitDataLayer` + - `mdLayerTreeShowOrHidden` + - `setLegendPointVisible` + - `controlBaseLayerTreeShowAndHidden` + - `initPopupOverlay` + - `flyTopanto` +- 业务层只认这套接口,不应感知底层是 `OpenLayers` 还是 `Cesium`。 + +### 2.4 2D 地图能力落点 + +- `src/components/gis/map.ol.ts` +- 已实现的关键能力: + - 默认中国视角初始化 + - 基础底图加载与切换 + - 点位图层注册和显隐 + - 图例控制单类锚点显隐 + - 鼠标 hover popup + - click 打开详情弹窗 + - 基地裁切与底图遮罩 + - 缩放/移动后的批量 popup 和标签刷新 + +### 2.5 2D 锚点与 popup 支撑 + +- `src/components/gis/ol/point-layer-manager.ts` + - 负责将点位数据转成地图要素、分层、索引、图例显隐。 +- `src/components/gis/ol/popup-manager.ts` + - 负责 hover 命中、popup DOM 内容复用、批量 popup。 + +### 2.6 图层树和图例交互 + +- `src/components/mapController/LayerController.vue` + - 图层树勾选后走 `mapOrchestrator.applyLayerSelection()` +- `src/components/mapLegend/index.vue` + - 图例点击后走 `mapOrchestrator.toggleLegend()` / `toggleLegendBatch()` +- `src/store/modules/map.ts` + - 真正把缓存数据、图层状态、图例状态应用到地图实例 + - 也就是说,`3D` 只要把统一接口补齐,就能直接接住现有业务流 + +### 2.7 2D 已有特殊业务规则 + +- 锚点点击逻辑已在 `src/components/gis/map.ol.ts` 实现: + - 命中锚点后,`ylfb` 走 `ylfbModalVisible` + - 其他锚点走 `modalVisible` + - 标题和参数都直接取当前锚点数据 +- `eng_point` 的中型水电站显示阈值已在现有链路中实现: + - 阈值常量位于 `src/modules/map/application/map-orchestrator.ts` + - 过滤逻辑位于 `src/store/modules/map.ts` 的 `filterPointLayerDataForDisplay()` + - `3D` 不应重新发明一套规则,而应复用这套现有过滤结果 +- 部分点位会随着缩放级别提升显示得更多,不是简单的“图层开/关”: + - `eng_point` 的中型电站要到 `ENG_POINT_MEDIUM_VISIBLE_ZOOM = 7.5` 以上才显示 + - 水电开发页在缩放跨过 `12` 级时会自动补开一批动态图层 + - `2D` 内部还存在点位密度阈值、近邻点展开、批量 popup 阈值等缩放联动规则 +- `2D` 对锚点存在碰撞检测与密度控制,不是所有点始终全量显示: + - 图标和标签会分别经过碰撞计算 + - 低缩放下会因为密度、碰撞、区域裁切、图例状态而隐藏部分点 + - 高缩放下会逐步放开限制,显示更多锚点和标签 + +## 3. 3D 当前现状与缺口 + +### 3.1 已有内容 + +- `src/components/gis/map.cesium.ts` +- 当前只完成了最小化 `Cesium.Viewer` 初始化。 +- 已有基础缩放、截图、`flyTopanto()` 雏形。 + +### 3.2 当前主要缺口 + +- `init()` 里虽然定义了 `flyToChina()`,但没有真正执行,也没有 `resolve(viewer)`,初始化流程不完整。 +- 默认底图仍是 ArcGIS 全球影像,因此启动时容易落到美国默认视角。 +- `addBaseDataLayer()` 为空,无法接住当前 `GISMap/customBaseLayer` 逻辑。 +- `addInitDataLayer()` 为空,无法展示锚点。 +- `mdLayerTreeShowOrHidden()` 为空,菜单切换时 3D 锚点不会同步显隐。 +- `setLegendPointVisible()` 为空,图例勾选不会影响 3D 锚点。 +- `initPopupOverlay()` 为空,无法复用 2D popup 容器。 +- 缺少点击命中与详情弹框联动,3D 当前无法做到“点击锚点与 2D 同行为”。 +- `controlBaseLayerTreeShowAndHidden()` 为空,3D 基础底图和叠加层无法受现有图层树控制。 +- 尚未验证 `eng_point` 的缩放阈值规则在 3D 下是否能完全复用,存在实现时遗漏中型站点显隐的风险。 +- 目前没有承接 `2D` 中“放大到具体层级显示更多锚点”的缩放联动显示机制。 +- 目前没有承接 `2D` 中图标/标签的碰撞检测逻辑,后续若直接全量渲染,视觉效果会与 `2D` 明显不一致。 +- 当前没有建立 `Cesium entity / datasource / imageryLayer` 的注册表,后续联动无从下手。 + +## 4. 实施原则 + +- 不重做业务编排,优先复用 `map-orchestrator.ts` 和 `src/store/modules/map.ts`。 +- 不额外引入一套 3D 专用图例和图层状态,仍以现有 store 为唯一运行态来源。 +- 不单独复制 `eng_point` 的中型站显示规则,优先复用现有 `mapStore` 过滤结果。 +- 不单独新建一套点击弹框协议,优先保持与 `map.ol.ts` 相同的命中后行为。 +- 对“缩放越大显示越多锚点”这类规则,优先复用现有缩放阈值与显示门闩,不做拍脑袋的新规则。 +- 对碰撞检测,优先与 `2D` 保持同方向行为:低缩放控量,高缩放放开,而不是 3D 全量堆点。 +- 先做最小闭环,再逐步补能力: + - 第一步保证 3D 能正确初始化并定位中国。 + - 第二步保证基础底图能显示。 + - 第三步保证锚点能显示。 + - 第四步保证 popup、图层树、图例联动打通。 +- 3D 不做“基础地图切换 UI”,但底层仍要支持 `customBaseLayer + addBaseDataLayer(..., true)` 这一逻辑。 + +## 5. 分步骤实施计划 + +### Step 1:修正 3D 初始化并默认定位中国 + +目标: + +- 切到 3D 后稳定初始化 viewer。 +- 首屏出现一个轻量加载动画。 +- 初始化完成后飞到中国默认视角。 + +计划: + +- 修复 `map.cesium.ts` 的 `init()`: + - 保证初始化后一定 `resolve(this.viewer)`。 + - 执行 `flyToChina()`。 + - 补一个可控的启动过渡动画。 +- 统一中国默认参数: + - 中心经纬度建议复用 `104.5, 36.5` + - 相机高度先按全国视角收口,后续再微调 +- 如果切回 2D 再切到 3D,仍然要能正确重建 viewer。 + +验收: + +- 点击 `3D` 按钮后,不再出现美国默认视角。 +- 首屏有加载过渡。 +- 进入后稳定落在中国。 + +### Step 2:展示 3D 基础底图 + +目标: + +- 3D 不显示空球。 +- 保留“基础底图 + 3D 专用底图地址”能力。 +- 不提供底图切换面板,但底层能吃现有配置。 + +计划: + +- 在 `MapCesium` 内建立基础图层注册表。 +- 实现 `addBaseDataLayer(layer, isShow)`: + - 对 `customBaseLayer` 使用 `url_3d` + - 对 `BASEMAP-img`、`BASEMAP-white` 预留兼容能力 +- 实现 `controlBaseLayerTreeShowAndHidden()`: + - 让图层树勾选能控制 3D 底图显隐 +- 根据你的要求,UI 层先不开放 `BaseLayerSwitcher` 的 3D 切换行为,只保留默认基础底图加载。 + +验收: + +- 切到 3D 后可稳定看到基础底图。 +- 图层初始化时 `addBaseDataLayer(this.baseLayerConfig, checked, true)` 的思路在 3D 可落地。 + +### Step 3:打通 3D 锚点展示 + +目标: + +- 3D 能展示和 2D 同源的锚点数据。 +- 菜单切换后,3D 能跟随页面数据切换。 + +计划: + +- 在 `MapCesium` 内建立点位图层注册结构,建议按 `layerKey -> entity[]` 或 `CustomDataSource` 管理。 +- 实现 `addInitDataLayer(pointData, layerType)`: + - 复用当前点位数据结构 + - 解析 `lgtd/lttd/iconCode/titleName/stnm/anchoPointState/popupHtml` + - 复用现有图标资源路径 + - 确保 `eng_point` 仍按现有 `mapStore` 过滤后的数据展示,中型站点只在 `7.5` 以上层级出现 + - 为后续缩放驱动的增量显示预留运行态字段,不把所有点简单一次性硬渲染 +- 实现 `mdLayerTreeShowOrHidden(layerType, checked)`: + - 图层树控制整个点图层显隐 +- 实现 `removePointLayer()` / `hasLayer()` + - 便于后续页面切换和重载 + +验收: + +- 切到 3D 后当前页默认勾选图层的锚点能显示。 +- 切换菜单后,3D 锚点跟随变化。 +- 取消勾选图层后,对应锚点消失。 +- `eng_point` 在低于 `7.5` 时不显示中型水电站,高于 `7.5` 时正常显示。 + +### Step 4:打通 3D popup 与 hover + +目标: + +- 3D 鼠标悬停时显示与 2D 一样的 popup。 +- popup 样式复用现有 DOM 和样式,不再单独做一版。 + +计划: + +- 实现 `initPopupOverlay(popupContainer)` + - 继续复用 `GisView.vue` 中现有 popup 容器 +- 在 Cesium 中增加鼠标移动命中: + - 基于 `ScreenSpaceEventHandler` + - hover 到锚点时读取 entity 附带属性 +- popup 内容生成逻辑复用 2D 现有字段: + - 优先使用 `popupHtml` + - 否则走通用字段渲染 +- 处理离开锚点时隐藏 popup。 + +验收: + +- 鼠标移入锚点出现 popup。 +- 样式和 2D 保持一致。 +- 移出后 popup 正常消失。 + +### Step 5:补齐缩放驱动显示与碰撞检测 + +目标: + +- 3D 在低缩放下控制锚点数量,避免全量堆叠。 +- 3D 在放大到具体层级后,逐步显示更多锚点。 +- 3D 的图标/标签显示效果尽量接近 2D,不出现严重遮挡。 + +计划: + +- 先承接现有明确可复用的缩放规则: + - `eng_point` 的 `7.5` 阈值 + - 水电开发页的 `12` 级动态图层联动 +- 再补 3D 侧的显示门闩: + - 低缩放下根据缩放级别、图层状态、图例状态控制 entity 显隐 + - 高缩放下逐步放开限制,显示更多锚点 +- 为 3D 补一版轻量碰撞控制: + - 至少先控制标签碰撞,避免名称完全叠在一起 + - 如有必要,再补图标级碰撞或屏幕网格抽稀 +- 明确与 2D 对齐的目标不是“算法完全一样”,而是“视觉行为一致”: + - 低缩放控量 + - 高缩放增量显示 + - 关键锚点不被完全淹没 + +验收: + +- 3D 在全国视角下不会一次性挤满所有锚点。 +- 放大到具体层级后会看到更多锚点逐步出现。 +- 标签和锚点不会大面积重叠到不可读。 + +### Step 6:打通 3D 锚点点击详情 + +目标: + +- 3D 点击锚点后,行为与 2D 一致。 +- 继续复用现有详情弹框状态和参数结构。 + +计划: + +- 在 Cesium 中增加点击命中: + - 基于 `ScreenSpaceEventHandler` + - 命中锚点后读取 entity 绑定的原始业务数据 +- 复用 `2D` 当前点击行为: + - `sttpMap === 'ylfb'` 时打开 `ylfbModalVisible` + - 其他锚点打开 `modalVisible` + - `params`、`title` 赋值逻辑与 `map.ol.ts` 保持一致 +- 避免 hover popup 与 click 弹框互相干扰。 + +验收: + +- 点击 3D 锚点后可打开与 2D 一致的详情弹框。 +- `ylfb` 和普通锚点分支行为一致。 +- 点击空白区域不会误触发弹框。 + +### Step 7:打通图例联动 + +目标: + +- 图例勾选后,3D 只显示对应状态的锚点。 + +计划: + +- 实现 `setLegendPointVisible(layerKey, anchoPointState, checked)`: + - 复用 `anchoPointState` 作为图例匹配字段 + - entity 级别维护显示状态 +- 如果后续需要,也可补一个 3D 版本的图例索引表,避免每次全量遍历。 + +验收: + +- 图例单项勾选/取消时,3D 对应类别锚点同步显隐。 +- 图例分组勾选/取消时,3D 行为与 2D 保持一致。 + +### Step 8:补足收口与稳定性 + +目标: + +- 让 3D 能稳定参与现有地图生命周期。 + +计划: + +- 清理 Cesium 事件监听、viewer、entity、datasource,避免重复切换内存泄漏。 +- 检查 `switchView('2D' | '3D')` 前后 popup、底图、图层注册是否被正确重置。 +- 检查 hover、click、图例、图层树、缩放阈值这几条联动链路在 3D 是否完全闭环。 +- 视情况补最小诊断日志,确认菜单切换与图层联动没有漏项。 + +验收: + +- 2D/3D 多次来回切换不报错。 +- 菜单切换、图层切换、图例切换行为稳定。 + +## 6. 推荐落地顺序 + +按你的要求,建议严格按下面顺序推进: + +1. 默认定位到中国 + 启动动画 +2. 展示基础底图 +3. 展示锚点 +4. 补 hover popup +5. 补缩放驱动显示与碰撞检测 +6. 补锚点点击详情 +7. 补图层树联动 +8. 补图例联动 +9. 做切换稳定性收口 + +## 9. 必须补齐的隐藏逻辑清单 + +下面这些逻辑不是表面接口能直接看出来的,但如果 `3D` 不补,最终效果就会和 `2D` 明显不一致。 + +### 9.1 图层与菜单切换 + +- 菜单切换时,只隐藏旧页点图层,不隐藏底图。 +- 图层树勾选前要先经过互斥规则归一化: + - `rare_fish_point` 与 `fish_along_point` 互斥 + - `stinfo/stinfo_video_point` 与 `stinfo_ai_video_point` 互斥 + - `facilities` 与 `facilities_built` 两个分支互斥 +- 点图层取消勾选时只隐藏,不删除缓存;重新勾选时优先从缓存恢复。 + +### 9.2 图例与点内显隐 + +- 图例控制的是图层内部锚点是否可见,不是图层本身是否可见。 +- 图层恢复显示时,图例要先整层全关,再按当前 checked 状态逐项恢复。 +- 环保设施图例组当前是只展示不交互。 + +### 9.3 缩放驱动显示 + +- `eng_point` 的中型水电站只在 `ENG_POINT_MEDIUM_VISIBLE_ZOOM = 7.5` 以上显示。 +- 水电开发页缩放跨过 `12` 级时,会自动补开一批动态图层;缩回去时再自动收口。 +- `2D` 存在“缩放越大显示越多”的行为,不是所有锚点在所有层级都等量显示。 +- `2D` 还存在 hover、批量 popup、密度控制等缩放阈值: + - hover 低于一定缩放不触发 + - 高缩放会进入批量 popup 模式 + - 点位会随着缩放提升逐步放开显示限制 + +### 9.4 锚点碰撞与控量 + +- `2D` 中锚点是否可见,不只受图层开关控制,还受以下门闩共同影响: + - 图例可见性 + - 区域裁切可见性 + - 密度阈值 + - 图标碰撞结果 + - 标签碰撞结果 +- 低缩放下,为避免堆叠,部分锚点或标签会被主动隐藏。 +- 高缩放下,会逐步放开图标/标签限制,显示更多内容。 +- 因此 `3D` 不能简单把所有 entity 永久全开,否则视觉密度会明显失控。 + +### 9.5 Popup 与点击 + +- hover popup 不是全时开启的,有缩放门槛。 +- 点击和 hover 的命中对象是图标本体,不是任意文字区域。 +- `ylfb` 与普通锚点的点击详情弹框走不同业务分支。 + +### 9.6 基地与区域裁切 + +- 基地切换不仅影响搜索候选,还会影响地图上的点位可见范围。 +- 区域裁切会直接把区域外点位隐藏,不是只改底图遮罩。 + +### 9.7 筛选器特殊逻辑 + +- 装机容量筛选本质是批量切 `eng_point` 图例状态,不是简单切图层。 +- 鱼类分布模式会接管 `ylfb_point` 的数据、缓存、图例与显隐。 +- 时间筛选当前只重载指定时间相关图层,不是全量重载。 + +## 7. 预计修改文件 + +- `src/components/gis/map.cesium.ts` + - 3D 地图主体实现,主改动文件 +- `src/components/gis/map.class.ts` + - 视情况补齐 2D/3D 切换后 popup 初始化和服务切换细节 +- `src/components/gis/GisView.vue` + - 视情况控制 3D 下的底图切换器显示策略、加载态挂载位置 +- `src/components/mapController/index.vue` + - 若需要补 3D 初始化后的交互控制 + +## 8. 当前建议 + +建议先只做 `Step 1`,也就是: + +- 修正 Cesium 初始化完成逻辑 +- 增加 3D 启动加载动画 +- 默认飞到中国 + +这一步最小、最容易验证,也能先把“打开 3D 看起来不对”的核心问题解决掉。 diff --git a/frontend/docs/问题.md b/frontend/docs/问题.md deleted file mode 100644 index 64508204..00000000 --- a/frontend/docs/问题.md +++ /dev/null @@ -1,43 +0,0 @@ -# 问题汇总 - -> 本记录用于跟踪项目开发过程中发现的问题,按日期倒序排列,并标注每项问题的发现人及处理状态。 - ---- - -## 问题列表(2026-06-26) - -| 发现日期 | 解决人 | 编号 | 问题描述 | 状态 | 备注 | -| ---------- | ------ | ---- | ---------------------------------------------------------------------- | ------ | -------- | -| 2026-06-26 | 扈 | 1 | 基础信息图片展示是什么逻辑 | ✅ |
| -| 2026-06-26 | 扈 | 2 | 电站专题展示逻辑 | ✅ |
| -| 2026-06-26 |
| 3 | 实时视频回放 少接口 | ⚠ | 缺少接口 | -| 2026-06-26 |
| 4 | 预警提示 少接口 | ⚠ | 缺少接口 | -| 2026-06-26 | 扈 | 5 | 生态流量 达标率查询不对 | ✅ |
| -| 2026-06-26 | 王 | 6 | 鱼类适应性繁殖同期对比 NAN | ⚠ |
| -| 2026-06-26 | 扈 | 7 | 出库水温 综合分析 导出没做 | ✅ |
| -| 2026-06-26 |
| 8 | 栖息地-流量监测 没有水位视频和流量视频 字段没数据 | ⚠ |
| -| 2026-06-26 |
| 9 | 栖息地 水温、水质、流量没有数据没法测 | ⚠ |
| -| 2026-06-26 | 扈 | 10 | 鱼类增殖站 - 运行数据 要添加字典 | ✅ |
| -| 2026-06-26 | 扈 | 11 | 珍惜植物园 - 种植要求字段不知道 | ✅ |
| -| 2026-06-26 | 扈 | 12 | 水生调查断面 - 监测数据 不知道电导率,性腺发育期,早期资源量和种类 | ✅ |
| -| 2026-06-26 | 扈 | 13 | 水电告警情况 生态流量 不知道字段 接口传参有问题 | ✅ |
| -| 2026-06-26 |
| 14 | 地图抽吸 碰撞检测 放大到具体层级锚点抽吸了但是 popup 弹框还在显示 | ⚠ |
| -| 2026-06-26 |
| 15 | 地图比如积石峡 放大到层级被公伯峡隐藏掉了,然后在放大才显示 抽吸的问题 | ⚠ |
| -| 2026-06-26 |
| 16 | 运行情况 计划开始运行时间 接口没接 | ⚠ |
| -| 2026-07-01 | 王 | 17 | 所有弹框加拖动 | ✅ |
| -| 2026-07-01 | 王 | 18 | 所有弹框检查宽度有的是固定的 | ✅ |
| -| 2026-07-01 | 王 | 18 | 所有导出的添加 |
|
| -| 2026-07-02 | 扈 | 19 | 地图点击基地切换偶发性会全部展示出来所有锚点 |
|
| -| 2026-07-03 | 汤 | 20 | 图例数据关了选中配置需要添加一个字段是默认不显示 |
|
| - ---- - -## 状态说明 - -- ✅ – 已解决 / 已确认 -- ⚠ – 处理中 / 待解决 -- ❌ – 阻塞 / 未开始 - ---- - -、 diff --git a/frontend/src/api/mapModal/index.ts b/frontend/src/api/mapModal/index.ts index 3b38499a..e7b12bc6 100644 --- a/frontend/src/api/mapModal/index.ts +++ b/frontend/src/api/mapModal/index.ts @@ -62,7 +62,7 @@ export function checkAnalysisShow(data: any) { // 查询站点的关联子站点 (垂向水温、AI 识别) export function getStInfoByStcd(params: any) { return request({ - url: '/api/dec-lygk-base-server/base/msstbprpt/getStInfoByStcd', + url: '/eng/base/msstbprpt/getStInfoByStcd', method: 'get', params }); diff --git a/frontend/src/api/select/index.ts b/frontend/src/api/select/index.ts index d322e8e1..d52bcf9d 100644 --- a/frontend/src/api/select/index.ts +++ b/frontend/src/api/select/index.ts @@ -41,6 +41,14 @@ export function getEngInfoDropdown(data: any) { data }); } +// 根据河段获取电站 +export function getByRvcd(data: any) { + return request({ + url: '/env/engInfo/getByRvcd', + method: 'get', + params:data + }); +} // 根据id获取电站信息 export function getEngInfoById(params: any) { return request({ diff --git a/frontend/src/api/system/map/pushManagement/index.ts b/frontend/src/api/system/map/pushManagement/index.ts new file mode 100644 index 00000000..f6f3b6ae --- /dev/null +++ b/frontend/src/api/system/map/pushManagement/index.ts @@ -0,0 +1,100 @@ +import request from '@/utils/request'; + +// 获取列表 +export function getPushConfigList(data: any) { + return request({ + url: '/system/pushConfig/getPushConfigList', + method: 'post', + data + }); +} +//新增推送配置 +export function addPushConfig(data: any) { + return request({ + url: '/system/pushConfig/addPushConfig', + method: 'post', + data + }); +} + +//修改新增配置 +export function deletePushConfig(data: any) { + return request({ + url: '/system/pushConfig/deletePushConfig', + method: 'post', + data + }); +} +//删除新增配置 +export function updatePushConfig(data: any) { + return request({ + url: '/system/pushConfig/updatePushConfig', + method: 'post', + data + }); +} +//推送目标配置 --用户 +export function queryUsersList(data: any) { + return request({ + url: '/system/pushConfig/queryUsersList', + method: 'get', + params:data + }); +} +//推送目标配置 -- 角色 +export function queryRolesList(data: any) { + return request({ + url: '/system/pushConfig/queryRolesList', + method: 'get', + params:data + }); +} +//查询推送目标 +export function getPushTargets(data: any) { + return request({ + url: '/system/pushConfig/getPushTargets', + method: 'get', + params:data + }); +} +//新增推送目标 +export function savePushTargets(data: any) { + return request({ + url: '/system/pushConfig/savePushTargets', + method: 'post', + data + }); +} +//短信提醒状态修改 +export function setPushConfigSmsSwitch(data: any) { + return request({ + url: '/system/pushConfig/setPushConfigSmsSwitch', + method: 'post', + params:data + }); +} +//获取推送历史列表 +export function postPushHistoryList(data: any) { + return request({ + url: '/system/pushConfig/postPushHistoryList', + method: 'post', + data + }); +} + +//推送情况 +export function getPushHistoryDetailList(data: any) { + return request({ + url: '/system/pushConfig/getPushHistoryDetailList', + method: 'post', + data + }); +} +//s数量 +export function getPushHistoryDetailById(data: any) { + return request({ + url: '/system/pushConfig/getPushHistoryDetailById', + method: 'get', + params:data + }); +} \ No newline at end of file diff --git a/frontend/src/components/MapModal/components/BasicInfo.vue b/frontend/src/components/MapModal/components/BasicInfo.vue index 363ca155..93b8a5bb 100644 --- a/frontend/src/components/MapModal/components/BasicInfo.vue +++ b/frontend/src/components/MapModal/components/BasicInfo.vue @@ -115,7 +115,7 @@ @@ -592,7 +953,7 @@ onMounted(() => { } .login-wrapper { - position: relative; + position: relative; width: 100%; height: 72%; min-height: 600px; @@ -603,7 +964,7 @@ onMounted(() => { // 左侧背景区域 .left-section { - .slogan { + .slogan { position: absolute; top: 28%; left: 18%; @@ -635,10 +996,12 @@ onMounted(() => { /* 验证码布局 */ .captcha-row { display: flex; + .ant-row { display: flex; align-items: start; } + /* gap: 12px; align-items: center; */ diff --git a/frontend/src/views/system/map/components/TiltPhotoManagement/TiltPhotoManagementForm.vue b/frontend/src/views/system/map/components/TiltPhotoManagement/TiltPhotoManagementForm.vue index 32f1179d..7c2d4074 100644 --- a/frontend/src/views/system/map/components/TiltPhotoManagement/TiltPhotoManagementForm.vue +++ b/frontend/src/views/system/map/components/TiltPhotoManagement/TiltPhotoManagementForm.vue @@ -23,19 +23,33 @@ placeholder="请输入倾斜影像名称" /> - - + - - + + - + diff --git a/frontend/src/views/system/map/components/TiltPhotoManagement/TiltPhotoManagementSearch.vue b/frontend/src/views/system/map/components/TiltPhotoManagement/TiltPhotoManagementSearch.vue index 1a5623b8..e518bcb7 100644 --- a/frontend/src/views/system/map/components/TiltPhotoManagement/TiltPhotoManagementSearch.vue +++ b/frontend/src/views/system/map/components/TiltPhotoManagement/TiltPhotoManagementSearch.vue @@ -23,6 +23,8 @@ import BasicSearch from "@/components/BasicSearch/index.vue"; interface Props { handleAdd: () => void; + riverOptions: any[]; + riverOptionsLoad: boolean; } const props = defineProps(); @@ -34,6 +36,7 @@ const emit = defineEmits<{ const initSearchData = { stnm: "", + rvcd:"" }; const searchData = ref({ ...initSearchData }); @@ -47,13 +50,16 @@ const searchList: any = computed(() => [ }, }, { - type: "Select", - name: "hbrvcdName", - label: "所在河段", + type: 'Select', + name: 'rvcd', + label: '所在河段', + width: 300, fieldProps: { - allowClear: true, + allowClear: true }, - }, + options: props.riverOptions, + loading: props.riverOptionsLoad + } ]); const onSearchFinish = (values: any) => { diff --git a/frontend/src/views/system/map/components/TiltPhotoManagement/index.vue b/frontend/src/views/system/map/components/TiltPhotoManagement/index.vue index d2e4b4be..de08fae8 100644 --- a/frontend/src/views/system/map/components/TiltPhotoManagement/index.vue +++ b/frontend/src/views/system/map/components/TiltPhotoManagement/index.vue @@ -3,6 +3,8 @@
{ operator: 'contains', dataType: 'string', value: values.stnm + }, + { + field: 'rvcd', + operator: 'contains', + dataType: 'string', + value: values.rvcd } ].filter(Boolean) }; @@ -211,7 +223,20 @@ const handleEditSubmit = async (values: any) => { return; } }; -const initOption = async () => {}; +const riverOptions = ref([]); +const riverOptionsLoad = ref(false); +const initOption = () => { + const params = {}; + riverOptionsLoad.value = true; + getRvcdDropdown(params).then((res: any) => { + res.data.forEach((item: any) => { + item.value = item.rvcd; + item.label = item.rvnm; + }); + riverOptions.value = res.data || []; + riverOptionsLoad.value = false; + }); +}; onMounted(() => { initOption(); }); diff --git a/frontend/src/views/system/pushManagement/components/ConfigManagement/ConfigManagementForm.vue b/frontend/src/views/system/pushManagement/components/ConfigManagement/ConfigManagementForm.vue index 4835ade9..dba298b1 100644 --- a/frontend/src/views/system/pushManagement/components/ConfigManagement/ConfigManagementForm.vue +++ b/frontend/src/views/system/pushManagement/components/ConfigManagement/ConfigManagementForm.vue @@ -1,9 +1,9 @@ + + diff --git a/frontend/src/views/system/pushManagement/components/ConfigManagement/PushTargetModal.vue b/frontend/src/views/system/pushManagement/components/ConfigManagement/PushTargetModal.vue new file mode 100644 index 00000000..1fd92357 --- /dev/null +++ b/frontend/src/views/system/pushManagement/components/ConfigManagement/PushTargetModal.vue @@ -0,0 +1,198 @@ + + + + + diff --git a/frontend/src/views/system/pushManagement/components/ConfigManagement/index.vue b/frontend/src/views/system/pushManagement/components/ConfigManagement/index.vue index 64294dbc..46b833db 100644 --- a/frontend/src/views/system/pushManagement/components/ConfigManagement/index.vue +++ b/frontend/src/views/system/pushManagement/components/ConfigManagement/index.vue @@ -7,15 +7,31 @@ :handle-add="handleAdd" @reset="handleReset" @search-finish="onSearchFinish" - /> + /> --> +
新增
+ diff --git a/frontend/src/views/system/pushManagement/components/HistoryManagement/PushStatusModal/index.vue b/frontend/src/views/system/pushManagement/components/HistoryManagement/PushStatusModal/index.vue new file mode 100644 index 00000000..8d453b46 --- /dev/null +++ b/frontend/src/views/system/pushManagement/components/HistoryManagement/PushStatusModal/index.vue @@ -0,0 +1,206 @@ + + + + + diff --git a/frontend/src/views/system/pushManagement/components/HistoryManagement/index.vue b/frontend/src/views/system/pushManagement/components/HistoryManagement/index.vue index 372b78b1..265111ce 100644 --- a/frontend/src/views/system/pushManagement/components/HistoryManagement/index.vue +++ b/frontend/src/views/system/pushManagement/components/HistoryManagement/index.vue @@ -1,208 +1,161 @@