feat(ems): 新增位移专属看板功能并优化告警阈值处理

refactor(ems): 重构告警阈值相关字段注释与逻辑,移除废弃的alarmLevel字段

fix(common): 修复Excel字典表达式解析时的空指针问题

test(admin): 添加报警迁移脚本的契约测试

perf(ems): 优化位移看板查询性能,新增分表查询与抽样功能

docs(ems): 完善告警阈值字段的文档注释

chore: 统一日期解析逻辑至DateUtils工具类

style: 清理部分文件中的无用导入
main
zch 2 months ago
parent e4d24627f4
commit 7c3b0a1411

@ -0,0 +1,41 @@
package org.dromara.test;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import static org.junit.jupiter.api.Assertions.assertTrue;
/**
*
*
*
*
*/
@DisplayName("报警迁移验证脚本契约")
public class AlarmMigrationContractUnitTest {
@Test
@DisplayName("验证报警迁移脚本包含可执行建表和样例数据")
public void shouldContainAlarmMigrationSeedAndSchema() throws IOException {
Path scriptPath = Path.of("script", "sql", "update", "update_5.6.0_alarm_validation.sql");
String sql = Files.readString(scriptPath, StandardCharsets.UTF_8);
assertTrue(sql.contains("CREATE TABLE [dbo].[ems_alarm_notify_group]"), "应包含通知组建表语句");
assertTrue(sql.contains("CREATE TABLE [dbo].[ems_alarm_notify_group_user]"), "应包含通知组成员建表语句");
assertTrue(sql.contains("CREATE TABLE [dbo].[ems_alarm_push_log]"), "应包含推送日志建表语句");
assertTrue(sql.contains("INSERT INTO [dbo].[ems_record_alarm_rule]"), "应包含报警规则样例数据");
assertTrue(sql.contains("INSERT INTO [dbo].[ems_alarm_action_step]"), "应包含措施步骤样例数据");
assertTrue(sql.contains("INSERT INTO [dbo].[ems_alarm_action_step_image]"), "应包含步骤图片样例数据");
assertTrue(sql.contains("INSERT INTO [dbo].[ems_record_alarm_data]"), "应包含告警记录样例数据");
assertTrue(sql.contains("INSERT INTO [dbo].[ems_alarm_push_log]"), "应包含推送日志样例数据");
assertTrue(sql.contains("'PENDING'"), "应保留待推送状态样例");
assertTrue(sql.contains("'SUCCESS'"), "应保留推送成功状态样例");
assertTrue(sql.contains("'FAILED'"), "应保留推送失败状态样例");
assertTrue(sql.contains("TODO"), "应保留未彻底解决的 DDL 兼容 TODO");
}
}

@ -111,7 +111,17 @@ public class ExcelDownHandler implements SheetWriteHandler {
} else if (StringUtils.isNotBlank(converterExp)) {
// 如果指定了确切的值,则直接解析确切的值
List<String> strList = StringUtils.splitList(converterExp, format.separator());
options = StreamUtils.toList(strList, s -> StringUtils.split(s, "=")[1]);
options = StreamUtils.toList(strList, s -> {
// readConverterExp 同时被导入导出共用,格式不完整时这里直接忽略,避免整个导出链路被错误注解拖垮。
String[] pair = s.split("=", 2);
if (pair.length < 2 || StringUtils.isBlank(pair[1])) {
log.warn("忽略非法的Excel字典表达式字段配置, field={}, expression={}", field.getName(), s);
return null;
}
return pair[1];
}).stream()
.filter(StringUtils::isNotBlank)
.toList();
}
} else if (field.isAnnotationPresent(ExcelEnumFormat.class)) {
// 否则如果指定了@ExcelEnumFormat则使用枚举的逻辑

@ -32,6 +32,12 @@
<artifactId>jackson-datatype-jsr310</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

@ -1,11 +1,9 @@
package org.dromara.common.json.handler;
import cn.hutool.core.date.DateTime;
import cn.hutool.core.date.DateUtil;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import org.dromara.common.core.utils.ObjectUtils;
import org.dromara.common.core.utils.DateUtils;
import java.io.IOException;
import java.util.Date;
@ -27,11 +25,8 @@ public class CustomDateDeserializer extends JsonDeserializer<Date> {
*/
@Override
public Date deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
DateTime parse = DateUtil.parse(p.getText());
if (ObjectUtils.isNull(parse)) {
return null;
}
return parse.toJdkDate();
// JSON 日期入参和 GET 参数统一走项目白名单格式解析,避免同一时间字符串在不同入口出现不一致行为。
return DateUtils.parseDate(p.getText());
}
}

@ -1,8 +1,6 @@
package org.dromara.common.web.config;
import cn.hutool.core.date.DateTime;
import cn.hutool.core.date.DateUtil;
import org.dromara.common.core.utils.ObjectUtils;
import org.dromara.common.core.utils.DateUtils;
import org.dromara.common.web.handler.GlobalExceptionHandler;
import org.dromara.common.web.interceptor.PlusWebInvokeTimeInterceptor;
import org.springframework.boot.autoconfigure.AutoConfiguration;
@ -34,13 +32,10 @@ public class ResourcesConfig implements WebMvcConfigurer {
@Override
public void addFormatters(FormatterRegistry registry) {
// 全局日期格式转换配置
registry.addConverter(String.class, Date.class, source -> {
DateTime parse = DateUtil.parse(source);
if (ObjectUtils.isNull(parse)) {
return null;
}
return parse.toJdkDate();
});
registry.addConverter(String.class, Date.class, source ->
// 这里显式复用项目统一的白名单时间格式解析,避免 Hutool 自动猜格式时引入额外时区扫描和运行时噪声。
DateUtils.parseDate(source)
);
}
@Override

@ -59,12 +59,21 @@ public class EmsMonitorMetricThreshold extends TenantEntity {
private BigDecimal alarmLower;
/**
*
* Hysteresis /
* <p>线</p>
* <p> alarmUpper / alarmLower
* (alarmUpper - hysteresis) (alarmLower + hysteresis) </p>
* <p>alarmUpper = 100hysteresis = 5 100 95 </p>
* <p> null 0 </p>
*/
private BigDecimal hysteresis;
/**
*
* Duration Seconds
* <p></p>
* <p> durationSec </p>
* <p>durationSec = 30 28 30 30 </p>
* <p> null 0 </p>
*/
private Integer durationSec;

@ -60,12 +60,18 @@ public class EmsMonitorMetricThresholdBo extends BaseEntity {
private BigDecimal alarmLower;
/**
*
* Hysteresis /
* <p>线</p>
* <p>alarmUpper = 100hysteresis = 5 100 95 </p>
* <p> null 0 </p>
*/
private BigDecimal hysteresis;
/**
*
* Duration Seconds
* <p></p>
* <p>durationSec = 30 30 </p>
* <p> null 0 </p>
*/
private Integer durationSec;

@ -78,13 +78,19 @@ public class EmsMonitorMetricThresholdVo implements Serializable {
private BigDecimal alarmLower;
/**
*
* Hysteresis /
* <p>线</p>
* <p>alarmUpper = 100hysteresis = 5 100 95 </p>
* <p> null 0 </p>
*/
@ExcelProperty(value = "回差值")
private BigDecimal hysteresis;
/**
*
* Duration Seconds
* <p></p>
* <p>durationSec = 30 30 </p>
* <p> null 0 </p>
*/
@ExcelProperty(value = "持续触发秒数")
private Integer durationSec;

@ -1,18 +1,14 @@
package org.dromara.ems.base.domain.vo;
import java.util.Date;
import com.fasterxml.jackson.annotation.JsonFormat;
import org.dromara.ems.base.domain.EmsReportPeriodSummary;
import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
import cn.idev.excel.annotation.ExcelProperty;
import org.dromara.common.excel.annotation.ExcelDictFormat;
import org.dromara.common.excel.convert.ExcelDictConvert;
import io.github.linpeilie.annotations.AutoMapper;
import lombok.Data;
import org.dromara.ems.base.domain.EmsReportPeriodSummary;
import java.io.Serial;
import java.math.BigDecimal;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.Date;
@ -118,8 +114,8 @@ public class EmsReportPeriodSummaryVo implements Serializable {
/**
*
*/
@ExcelProperty(value = "超限时长", converter = ExcelDictConvert.class)
@ExcelDictFormat(readConverterExp = "秒=")
// 这里直接把单位放进表头,避免把“秒”误当成字典表达式参与下拉解析,导致导出阶段崩溃。
@ExcelProperty(value = "超限时长(秒)")
private Integer overLimitDuration;
/**

@ -127,7 +127,6 @@ public class EmsMonitorMetricThresholdServiceImpl implements IEmsMonitorMetricTh
queryWrapper.eq(bo.getAlarmLower() != null, "t.alarm_lower", bo.getAlarmLower());
queryWrapper.eq(bo.getHysteresis() != null, "t.hysteresis", bo.getHysteresis());
queryWrapper.eq(bo.getDurationSec() != null, "t.duration_sec", bo.getDurationSec());
queryWrapper.eq(StringUtils.isNotBlank(bo.getAlarmLevel()), "t.alarm_level", bo.getAlarmLevel());
queryWrapper.eq(bo.getNotifyGroupId() != null, "t.notify_group_id", bo.getNotifyGroupId());
queryWrapper.eq(StringUtils.isNotBlank(bo.getIsEnable()), "t.is_enable", bo.getIsEnable());
return queryWrapper;
@ -167,6 +166,8 @@ public class EmsMonitorMetricThresholdServiceImpl implements IEmsMonitorMetricTh
*
*/
private void validEntityBeforeSave(EmsMonitorMetricThreshold entity){
// 告警级别已经从前端配置面收敛,这里统一清空,避免历史请求继续把停用字段写回数据库。
entity.setAlarmLevel(null);
//TODO 做一些数据校验,如唯一约束
}

@ -99,9 +99,26 @@ public class EmsRecordAlarmRule extends BaseEntity
@ExcelProperty(value = "恢复下限")
private BigDecimal recoverLower;
/**
* Hysteresis /
* <p> alarmUpper / alarmLower 线
* / alarm flapping</p>
* <p> (alarmUpper - hysteresis)
* (alarmLower + hysteresis) </p>
* <p>alarmUpper = 100hysteresis = 5 100 95 </p>
* <p> null 0 </p>
*/
@ExcelProperty(value = "回差")
private BigDecimal hysteresis;
/**
* Duration Seconds
* <p>
* transient spike</p>
* <p> durationSec </p>
* <p>durationSec = 30 28 30 30 </p>
* <p> null 0 </p>
*/
@ExcelProperty(value = "持续触发秒数")
private Integer durationSec;

@ -1014,12 +1014,10 @@ public class EmsRecordAlarmDataServiceImpl implements IEmsRecordAlarmDataService
if (ObjectUtil.isEmpty(alarmData.getMetricCode())) {
alarmData.setMetricCode(matchedRule.getMetricCode());
}
if (ObjectUtil.isEmpty(alarmData.getAlarmLevel())) {
alarmData.setAlarmLevel(matchedRule.getAlarmLevel());
// 告警级别已从规则配置入口收敛,落库链路不再从规则表回填,避免历史字段继续影响实时告警口径。
if (ObjectUtil.isEmpty(alarmData.getNotifyUser())) {
alarmData.setNotifyUser(matchedRule.getNotifyUser());
}
if (ObjectUtil.isEmpty(alarmData.getNotifyUser())) {
alarmData.setNotifyUser(matchedRule.getNotifyUser());
}
if (alarmData.getThresholdValue() == null) {
if (matchedRule.getTriggerValue() != null) {
alarmData.setThresholdValue(matchedRule.getTriggerValue());
@ -1066,7 +1064,6 @@ public class EmsRecordAlarmDataServiceImpl implements IEmsRecordAlarmDataService
pushLog.setAlarmObjId(alarmData.getObjId());
pushLog.setChannelType(resolveChannelType(target));
pushLog.setTargetValue(target);
pushLog.setAlarmLevel(alarmData.getAlarmLevel());
pushLog.setPushContent(alarmData.getAlarmContent());
pushLog.setPushStatus(EmsAlarmPushStatusConstants.PENDING);
pushLog.setResponseMsg("已生成待推送日志,等待真实推送执行链接管");

@ -1,11 +1,89 @@
package org.dromara.ems.report.domain.bo;
import lombok.Data;
import java.math.BigDecimal;
import java.util.List;
/**
*
*
* <p>
*
* displacementBoard Controller / Service </p>
* <p> {@link VibrationBoardQueryBo}
* 1. BO vibrationParam
* 2. BO Controller / Service / Mapper </p>
*
* <p>highThreshold / warningThreshold / minContinuousSamples /
* rapidRiseThreshold / stddevThreshold
* 7 BO</p>
*/
public class DisplacementBoardQueryBo extends VibrationBoardQueryBo {
@Data
public class DisplacementBoardQueryBo {
/**
*
* <p>
* Service monitorIds IN SQL </p>
*/
private String monitorId;
/**
*
* <p>
* monitorId Service </p>
*/
private List<String> monitorIds;
/**
* yyyy-MM-dd HH:mm:ss
* <p> Service fail fast
* 24 </p>
*/
private String beginRecordTime;
/**
* yyyy-MM-dd HH:mm:ss
* <p> beginRecordTime 90 </p>
*/
private String endRecordTime;
/**
*
* <p> 1 1 ROW_NUMBER SQL
* Java
* 1440</p>
*/
private Integer samplingInterval;
/**
* 使
* <p>300um
* </p>
*/
private BigDecimal highThreshold;
/**
* 使
* <p></p>
*/
private BigDecimal warningThreshold;
/**
* 使
* <p> 3 3 </p>
*/
private Integer minContinuousSamples;
/**
* 使
* <p>
* </p>
*/
private BigDecimal rapidRiseThreshold;
/**
* 使
* <p>
* </p>
*/
private BigDecimal stddevThreshold;
}

@ -0,0 +1,63 @@
package org.dromara.ems.report.mapper;
import org.apache.ibatis.annotations.Param;
import org.dromara.ems.record.domain.RecordIotenvInstant;
import org.dromara.ems.report.domain.bo.DisplacementBoardQueryBo;
import java.util.List;
/**
* Mapper
*
* <p> Mapper {@link VibrationBoardMapper}
*
* 1. SQL vibrationDisplacement Java
* 2. monitor_type=10 UNION ALL ROW_NUMBER
* XML SQL </p>
*
* <p> 4 / / /
* SQL WHERE vibration_displacement IS NOT NULL AND > 0
* SQL /</p>
*/
public interface DisplacementBoardMapper {
/**
*
* <p> &lt;= 1
* XML INNER JOIN monitor_type=10 + vibration_displacement
* </p>
*
* @param tableNames
* @param query
*/
List<RecordIotenvInstant> selectRawData(@Param("tableNames") List<String> tableNames,
@Param("query") DisplacementBoardQueryBo query);
/**
*
* <p> &gt; 1 DB ROW_NUMBER
* +
* DB </p>
*
* @param tableNames
* @param query
*/
List<RecordIotenvInstant> selectSampledData(@Param("tableNames") List<String> tableNames,
@Param("query") DisplacementBoardQueryBo query);
/**
*
* <p> SQL <strong></strong>
* sampleCount </p>
*/
List<RecordIotenvInstant> selectQualityRawData(@Param("tableNames") List<String> tableNames,
@Param("query") DisplacementBoardQueryBo query);
/**
*
* <p> &gt; 1 DB
* </p>
*/
List<RecordIotenvInstant> selectQualitySampledData(@Param("tableNames") List<String> tableNames,
@Param("query") DisplacementBoardQueryBo query);
}

@ -93,7 +93,6 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="recoverLower != null "> and RAL.recover_lower = #{recoverLower}</if>
<if test="hysteresis != null "> and RAL.hysteresis = #{hysteresis}</if>
<if test="durationSec != null "> and RAL.duration_sec = #{durationSec}</if>
<if test="alarmLevel != null and alarmLevel != ''"> and RAL.alarm_level = #{alarmLevel}</if>
<if test="notifyGroupId != null "> and RAL.notify_group_id = #{notifyGroupId}</if>
<if test="isEnable != null and isEnable != ''"> and RAL.is_enable = #{isEnable}</if>
<if test="notifyUser != null and notifyUser != ''"> and RAL.notify_user = #{notifyUser}</if>
@ -152,7 +151,6 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="recoverLower != null">recover_lower,</if>
<if test="hysteresis != null">hysteresis,</if>
<if test="durationSec != null">duration_sec,</if>
<if test="alarmLevel != null">alarm_level,</if>
<if test="notifyGroupId != null">notify_group_id,</if>
<if test="isEnable != null">is_enable,</if>
<if test="notifyUser != null">notify_user,</if>
@ -180,7 +178,6 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="recoverLower != null">#{recoverLower},</if>
<if test="hysteresis != null">#{hysteresis},</if>
<if test="durationSec != null">#{durationSec},</if>
<if test="alarmLevel != null">#{alarmLevel},</if>
<if test="notifyGroupId != null">#{notifyGroupId},</if>
<if test="isEnable != null">#{isEnable},</if>
<if test="notifyUser != null">#{notifyUser},</if>
@ -212,7 +209,6 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="recoverLower != null">recover_lower = #{recoverLower},</if>
<if test="hysteresis != null">hysteresis = #{hysteresis},</if>
<if test="durationSec != null">duration_sec = #{durationSec},</if>
<if test="alarmLevel != null">alarm_level = #{alarmLevel},</if>
<if test="notifyGroupId != null">notify_group_id = #{notifyGroupId},</if>
<if test="isEnable != null">is_enable = #{isEnable},</if>
<if test="notifyUser != null">notify_user = #{notifyUser},</if>

@ -0,0 +1,246 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.dromara.ems.report.mapper.DisplacementBoardMapper">
<!--
位移专属看板 XML与振动通用 XML 解耦。
所有分析类 SQL 在 WHERE 中固定过滤 vibration_displacement IS NOT NULL AND > 0
避免先查四维指标再在 Java 层裁剪成位移数据,减少数据库 I/O 与内存压力。
保留 RecordIotenvInstant 作为结果映射,是因为位移看板在聚合层仍要读取
monitor_id / monitor_name / recode_time / vibration_displacement 等字段,
建新实体反而需要同步维护多个一致性,收益不大。
-->
<resultMap type="org.dromara.ems.record.domain.RecordIotenvInstant" id="DisplacementBoardResult">
<result property="objid" column="objid"/>
<result property="monitorId" column="monitorId"/>
<result property="monitorCode" column="monitor_code"/>
<result property="monitorName" column="monitor_name"/>
<result property="vibrationDisplacement" column="vibration_displacement"/>
<result property="collectTime" column="collectTime"/>
<result property="recodeTime" column="recodeTime"/>
</resultMap>
<!--
位移看板只需要位移相关字段,避免把四维振动字段一并 SELECT 浪费 IO
monitor_name 通过 INNER JOIN ems_base_monitor_info 获取,
COALESCE 兜底避免主数据未维护导致前端报错。
-->
<sql id="selectColumns">
t.objid,
t.monitorId,
t.vibration_displacement,
t.collectTime,
t.recodeTime,
COALESCE(ebmi.monitor_name, t.monitorId) AS monitor_name,
t.monitorId AS monitor_code
</sql>
<!--
分析类 WHERE
1. 时间范围(参数化绑定 #{} 防注入)
2. 设备过滤(支持单设备 / 多设备 IN 列表)
3. vibration_displacement 有效值过滤——位移看板分析类 SQL 的专属口径
注意:不做上限截断,高位位移本身就是业务风险信号。
-->
<sql id="baseWhere">
t.recodeTime &gt;= #{query.beginRecordTime}
AND t.recodeTime &lt;= #{query.endRecordTime}
<choose>
<when test="query.monitorId != null and query.monitorId != ''">
AND t.monitorId = #{query.monitorId}
</when>
<when test="query.monitorIds != null and query.monitorIds.size() > 0">
AND t.monitorId IN
<foreach collection="query.monitorIds" item="monitorId" open="(" separator="," close=")">
#{monitorId}
</foreach>
</when>
</choose>
AND t.vibration_displacement IS NOT NULL
AND t.vibration_displacement &gt; 0
</sql>
<!--
质量页 WHERE仅按时间/设备过滤,不做位移有效值过滤,
避免质量页分母被提前缩小导致覆盖率虚高。
-->
<sql id="baseWhereQuality">
t.recodeTime &gt;= #{query.beginRecordTime}
AND t.recodeTime &lt;= #{query.endRecordTime}
<choose>
<when test="query.monitorId != null and query.monitorId != ''">
AND t.monitorId = #{query.monitorId}
</when>
<when test="query.monitorIds != null and query.monitorIds.size() > 0">
AND t.monitorId IN
<foreach collection="query.monitorIds" item="monitorId" open="(" separator="," close=")">
#{monitorId}
</foreach>
</when>
</choose>
</sql>
<!--
原始查询UNION ALL 多日分表 → 子查询 → 排序。
INNER JOIN monitor_type = 10 确保只查振动类设备(现场物理意义:振动位移传感器挂在 type=10 下)。
${tableName} 虽然是拼接而非参数化,但 Service 已做白名单正则校验TABLE_NAME_PATTERN
此处是唯一允许 ${} 的位置。
-->
<sql id="rawPageQuery">
SELECT *
FROM (
<foreach collection="tableNames" item="tableName" separator=" UNION ALL ">
SELECT
<include refid="selectColumns"/>
FROM ${tableName} t
INNER JOIN ems_base_monitor_info ebmi
ON t.monitorId = ebmi.monitor_code
AND ebmi.monitor_type = 10
<where>
<include refid="baseWhere"/>
</where>
</foreach>
) displacement_data
<!-- 排序:先设备、再时间、最后主键,保证 Java 层“取最新值”“相邻点差值”算法稳定 -->
ORDER BY displacement_data.monitorId ASC, displacement_data.recodeTime ASC, displacement_data.objid ASC
</sql>
<!--
抽样查询:使用 ROW_NUMBER 窗口函数按「设备 + 时间桶」分区,每个桶只保留最新一条记录。
时间桶宽度 = samplingInterval 分钟,通过 DATEDIFF/TIMESTAMPDIFF 计算桶编号。
多数据库兼容:通过 _databaseId 判断 MySQL/PostgreSQL/SQL Server 语法差异。
-->
<sql id="samplingPageQuery">
WITH sampled AS (
<foreach collection="tableNames" item="tableName" separator=" UNION ALL ">
SELECT
<include refid="selectColumns"/>,
ROW_NUMBER() OVER (
PARTITION BY t.monitorId,
<choose>
<when test="_databaseId == 'mysql'">
CAST(TIMESTAMPDIFF(MINUTE, '2000-01-01 00:00:00', t.recodeTime) / #{query.samplingInterval} AS SIGNED)
</when>
<when test="_databaseId == 'postgresql' or _databaseId == 'PostgreSQL'">
CAST(EXTRACT(EPOCH FROM (t.recodeTime - TIMESTAMP '2000-01-01 00:00:00')) / 60 / #{query.samplingInterval} AS BIGINT)
</when>
<otherwise>
CAST(DATEDIFF(MINUTE, '2000-01-01 00:00:00', t.recodeTime) / #{query.samplingInterval} AS BIGINT)
</otherwise>
</choose>
ORDER BY t.recodeTime DESC, t.objid DESC
) AS rn
FROM ${tableName} t
INNER JOIN ems_base_monitor_info ebmi
ON t.monitorId = ebmi.monitor_code
AND ebmi.monitor_type = 10
<where>
<include refid="baseWhere"/>
</where>
</foreach>
)
SELECT
objid,
monitorId,
monitor_code,
monitor_name,
vibration_displacement,
collectTime,
recodeTime
FROM sampled
<!-- rn = 1 为每个“设备+时间桶”内最新的一条,效果等价于“每 N 分钟取一个位移采样点” -->
WHERE rn = 1
ORDER BY monitorId ASC, recodeTime ASC, objid ASC
</sql>
<!--
质量页原始查询:不按位移有效值过滤,
保证分母代表“本次查询实际采到的样本总数”。
-->
<sql id="rawQualityQuery">
SELECT *
FROM (
<foreach collection="tableNames" item="tableName" separator=" UNION ALL ">
SELECT
<include refid="selectColumns"/>
FROM ${tableName} t
INNER JOIN ems_base_monitor_info ebmi
ON t.monitorId = ebmi.monitor_code
AND ebmi.monitor_type = 10
<where>
<include refid="baseWhereQuality"/>
</where>
</foreach>
) displacement_quality_data
ORDER BY displacement_quality_data.monitorId ASC, displacement_quality_data.recodeTime ASC, displacement_quality_data.objid ASC
</sql>
<!--
质量页抽样查询:仅做时间桶抽样,不做位移有效值过滤,
避免质量统计在 SQL 层被“分析口径”提前污染。
-->
<sql id="samplingQualityQuery">
WITH sampled AS (
<foreach collection="tableNames" item="tableName" separator=" UNION ALL ">
SELECT
<include refid="selectColumns"/>,
ROW_NUMBER() OVER (
PARTITION BY t.monitorId,
<choose>
<when test="_databaseId == 'mysql'">
CAST(TIMESTAMPDIFF(MINUTE, '2000-01-01 00:00:00', t.recodeTime) / #{query.samplingInterval} AS SIGNED)
</when>
<when test="_databaseId == 'postgresql' or _databaseId == 'PostgreSQL'">
CAST(EXTRACT(EPOCH FROM (t.recodeTime - TIMESTAMP '2000-01-01 00:00:00')) / 60 / #{query.samplingInterval} AS BIGINT)
</when>
<otherwise>
CAST(DATEDIFF(MINUTE, '2000-01-01 00:00:00', t.recodeTime) / #{query.samplingInterval} AS BIGINT)
</otherwise>
</choose>
ORDER BY t.recodeTime DESC, t.objid DESC
) AS rn
FROM ${tableName} t
INNER JOIN ems_base_monitor_info ebmi
ON t.monitorId = ebmi.monitor_code
AND ebmi.monitor_type = 10
<where>
<include refid="baseWhereQuality"/>
</where>
</foreach>
)
SELECT
objid,
monitorId,
monitor_code,
monitor_name,
vibration_displacement,
collectTime,
recodeTime
FROM sampled
WHERE rn = 1
ORDER BY monitorId ASC, recodeTime ASC, objid ASC
</sql>
<!-- 分析类原始明细samplingInterval <= 1 时使用) -->
<select id="selectRawData" resultMap="DisplacementBoardResult">
<include refid="rawPageQuery"/>
</select>
<!-- 分析类抽样明细samplingInterval > 1 时使用DB 层降采样) -->
<select id="selectSampledData" resultMap="DisplacementBoardResult">
<include refid="samplingPageQuery"/>
</select>
<!-- 质量页原始样本samplingInterval <= 1 时使用) -->
<select id="selectQualityRawData" resultMap="DisplacementBoardResult">
<include refid="rawQualityQuery"/>
</select>
<!-- 质量页抽样样本samplingInterval > 1 时使用) -->
<select id="selectQualitySampledData" resultMap="DisplacementBoardResult">
<include refid="samplingQualityQuery"/>
</select>
</mapper>
Loading…
Cancel
Save