Compare commits

22 Commits

Author SHA1 Message Date
7fb4c8e78a feat(mms-mapping): 添加ICD校验结果保存功能支持文件上传
- 在CsIcdPathController中添加POST接口支持multipart表单上传ICD文件
- 实现saveIcdCheckResultWithFile方法处理文件上传和校验结果保存
- 添加fillIcdFile方法处理ICD文件内容填充到参数对象
- 在CsIcdPathPO中将Icd_Content字段重命名为Icd并使用JsonNodeTypeHandler处理JSON转换
- 更新resultMap配置使用新的字段映射关系
- 修改ICD一致性校验服务的日志记录和校验逻辑
- 移除自动修正映射差异的功能,只保留一致性检查
- 优化测试用例验证ICD校验结果保存和文件上传功能
2026-06-16 13:25:12 +08:00
fd6e5097d7 feat(mms-mapping): 添加ICD一致性校验功能并重构设备类型管理
- 在MappingController中新增ICD一致性校验接口checkIcdJsonConsistency
- 添加IcdConsistencyCheckService服务实现ICD映射JSON一致性校验逻辑
- 添加IcdConsistencyCheckRequest和IcdConsistencyCheckResponse相关数据传输对象
- 在CsIcdPathPO中新增icdContent字段存储ICD内容字节数组
- 在CsIcdPathMapper中新增selectIcdPathList方法支持关键词搜索
- 移除设备类型相关的控制器、服务接口及实现类(MmsDeviceTypeController等)
- 更新.gitignore文件排除特定jar包路径
- 在pom.xml中添加device-types模块依赖和JNA库依赖
- 更新README.md文档添加device-types模块说明
- 重命名steady-DataView为steady-dataView模块名统一格式
2026-06-15 08:38:19 +08:00
周宇 蔡
1edee2bf12 fix(mms-mapping): 修复XML资源配置加载和JSON映射提取问题
- 删除冗余注释,优化代码可读性
- 增强extractMappingJson方法中的类型安全处理,支持字符串和对象类型的mappingJson字段
- 添加对象类型转换逻辑,确保返回正确格式的JSON字符串
2026-06-12 13:55:48 +08:00
b7b18dc325 调整parse函数解析失败的异常处理。现在不会直接抛出异常了。 2026-06-12 10:58:49 +08:00
a5a09022f2 添加了pqdif解析的基础功能,开放了一个基础解析接口。 2026-06-12 10:42:18 +08:00
4aca8ca2c4 Merge remote-tracking branch 'origin/main' 2026-06-12 10:41:11 +08:00
362bbf536f 添加了pqdif解析的基础功能,开放了一个基础解析接口。 2026-06-12 10:40:59 +08:00
周宇 蔡
89ec9e1fa3 Merge branch 'dev-czy' 2026-06-12 09:51:41 +08:00
周宇 蔡
1343d235c8 refactor(mms-mapping): 更新江苏配置模板并增强JSON到XML转换服务
- 将JiangSu_Config1.xml和JiangSu_Config2.xml中的Value标签注释格式统一
- 在JsonToXmlConversionService中改进规则匹配统计信息显示
- 添加XML模板中Value标签数量统计功能
- 实现匹配成功和未匹配Value标签的计数统计
- 增加未匹配标签的详细描述列表输出
- 新增countXmlValueTags方法用于统计XML中Value标签总数
- 新增countMatchedValueTags方法用于统计已匹配的Value标签数
- 新增findUnmatchedValueTags方法用于查找未匹配的Value标签描述
2026-06-12 09:50:51 +08:00
周宇 蔡
0cecf2d7a2 Merge branch 'dev-czy' 2026-06-12 08:53:25 +08:00
周宇 蔡
c0cf4de315 feat(mapping): 添加配置文件类型参数支持
- 在 generateXmlFromJson 方法中添加 configType 参数以支持不同配置文件
- 新增 loadXmlResources(Integer configType) 方法用于加载指定类型的XML资源
- 更新 JsonToXmlRequest 类添加 configType 属性定义
- 修改 MappingController 中的 getXmlFromJson 方法以传递配置类型参数
- 扩展 RuleBasedXmlMappingService 的 loadDefaultXmlFile 方法支持配置类型选择
- 在调试类 JsonToXmlDebugRunner 中设置默认使用配置类型2进行测试
2026-06-12 08:52:36 +08:00
212b69060c refactor(steady): 重构数据校验功能并新增PQDIF解析预留模块
- 将数据校验中的缺失率相关字段替换为数据完整性字段
- 新增数据校验任务删除功能及相应测试
- 在tools模块中添加parse-pqdif子模块作为PQDIF文件解析预留
- 更新README文档以反映新的模块结构和依赖关系
- 优化数据校验统计汇总逻辑和测试覆盖
- 在entrance模块中集成parse-pqdif依赖
- 重构数据校验服务层实现和数据对象映射
2026-06-12 08:41:11 +08:00
周宇 蔡
f7154db93d Merge branch 'dev-new' 2026-06-11 18:13:29 +08:00
周宇 蔡
557022d346 Merge branch 'main' of http://www.pqmcc.com:3000/ClientApps/CN_Tool 2026-06-11 18:09:02 +08:00
周宇 蔡
e43ab264e0 feat(mms-mapping): 添加江苏配置模板支持谐波数据
- 新增 JiangSu_Config1.xml 配置文件
- 配置历史稳态数据类型包括短时闪变、长时闪变、暂态数据
- 配置实时稳态数据类型支持电压、电流、功率等参数
- 设置谐波数据解析规则和通配符配置
- 配置电压电流功率等电气量的95值、平均值、最大值、最小值统计
- 添加谐波含有率、相角等谐波类数据配置
- 配置暂态事件解析规则和录波文件时间格式
- 设置 Kafka 消息发送相关参数和数据系数转换规则
2026-06-11 18:04:25 +08:00
1c979e248a feat(steady-checksquare): 新增数据校验功能模块
- 添加数据校验历史记录查询接口
- 实现数据校验任务创建功能
- 新增数据校验详情查询接口
- 添加谐波奇偶关系异常检测规则
- 实现数据校验明细数据结构
- 添加数据校验编号生成工具
- 优化InfluxDB查询组件并增加缓存机制
- 添加数据校验常量定义
- 实现数据校验值生成器中的派生字段处理逻辑
- 新增数据校验相关的VO、PO、DTO类
- 添加数据校验组件单元测试
2026-06-11 11:09:12 +08:00
36962221f5 feat(dbms): 增加数据库备份任务停止重启功能和MySQL支持
- 添加了备份任务停止和重启接口及实现
- 实现了对MySQL数据库的支持,包括数据库名配置
- 重构了数据库连接和备份操作的SPI架构
- 优化了备份文件删除逻辑,支持目录递归删除
- 增加了连接名称唯一性校验
- 完善了备份任务状态管理和错误处理机制
- 更新了数据库连接参数验证逻辑
2026-06-09 13:14:43 +08:00
周宇 蔡
24bdaa1ae9 Merge branch 'dev-czy'
# Conflicts:
#	tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/component/IcdToXmlResponseConverter.java
#	tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/component/JsonToXmlConversionService.java
#	tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/component/RuleBasedXmlMappingService.java
#	tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/pojo/bo/IcdToXmlGenerateResult.java
#	tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/pojo/vo/IcdToXmlResponse.java
#	tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/service/impl/IcdToXmlTaskAppService.java
2026-06-01 08:54:13 +08:00
5f6c10b9cb feat(add-ledger): 新增线路类型和设备单位管理功能
- 添加线路类型常量定义(主网0,配网1)及验证逻辑
- 新增设备单位查询和保存接口及实现
- 新增监测点限值查询接口
- 扩展AddLedgerDetailVO和AddLedgerLinePO实体类以支持线路类型字段
- 在测点保存时自动计算并保存限值信息
- 添加设备单位默认配置初始化逻辑
- 新增COverlimitUtil工具类用于限值计算
- 完善相关单元测试用例
2026-05-29 15:15:22 +08:00
周宇 蔡
58ca8b0c23 refactor(mms-mapping): 重构ICD到XML转换服务优化代码结构
- 移除IcdToXmlGenerateResult中未使用的iedName、ldInst、indexAnalysis、savedPath字段
- 注释掉相应的getter/setter方法减少代码冗余
- 重构IcdToXmlTaskAppService中generateFromIcd方法的业务逻辑
- 优化JsonToXmlConversionService的转换流程提高性能
- 添加详细的中文注释说明各个方法的功能和实现逻辑
- 调整规则匹配和XML生成的核心算法提升匹配准确性
- 修改未匹配规则的错误提示信息增加详细指标信息
2026-05-27 08:45:36 +08:00
周宇 蔡
8a92ff3be0 feat(mapping): 优化ICD到XML转换服务支持未匹配规则详情
- 实现convertFromJsonWithResult方法返回完整的转换结果
- 添加未匹配规则详细信息到生成结果的问题列表中
- 新增ConversionResult、RuleMatchingResult和UnmatchedRuleDetail数据结构
- 扩展应用规则逻辑以跟踪和报告未匹配的规则变体
- 重构buildXmlFromMapping方法以支持结果详情返回
- 更新控制台日志输出以显示规则匹配统计信息
2026-05-06 15:43:19 +08:00
周宇 蔡
ddadf26837 feat(mms-mapping): test中添加XML字符串返回功能并优化JSON转XML性能测试
- 在IcdToXmlGenerateResult和IcdToXmlResponse中新增mappingXml字段存储生成的XML字符串
- 修改IcdToXmlResponseConverter将XML内容从savedPath改为mappingXml字段
- 更新IcdToXmlTaskAppService将XML转换结果从保存路径改为直接返回XML内容
- 重构JsonToXmlConversionService移除临时文件创建,直接返回XML字符串
- 在JsonToXmlConversionService中添加性能监控日志输出
- 新增JsonToXmlDebugRunner用于本地调试JSON转XML功能
2026-05-06 14:31:18 +08:00
198 changed files with 15640 additions and 1047 deletions

3
.gitignore vendored
View File

@@ -5,6 +5,8 @@
target/ target/
logs/ logs/
docs/ docs/
.codex-tmp/
.docs/
# Log file # Log file
*.log *.log
@@ -21,6 +23,7 @@ docs/
*.ear *.ear
*.tar.gz *.tar.gz
*.rar *.rar
!tools/parse-pqdif/lib/pqdif-native-basic-bridge-1.0.0-jar-with-dependencies.jar
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
hs_err_pid* hs_err_pid*

View File

@@ -34,7 +34,9 @@ CN_Tool 是一个基于 Spring Boot 的多模块后端聚合工程,当前仓
- `activate-tool` - `activate-tool`
- `add-data` - `add-data`
- `add-ledger` - `add-ledger`
- `device-types`
- `mms-mapping` - `mms-mapping`
- `parse-pqdif`
- `wave-tool` - `wave-tool`
## 启动入口 ## 启动入口
@@ -43,7 +45,7 @@ CN_Tool 是一个基于 Spring Boot 的多模块后端聚合工程,当前仓
- `entrance/src/main/java/com/njcn/gather/EntranceApplication.java` - `entrance/src/main/java/com/njcn/gather/EntranceApplication.java`
`entrance` 模块聚合了 `system``disk-monitor``dbms``deploy``user``detection``activate-tool``add-data``add-ledger``wave-tool``mms-mapping`,是当前运行时主入口。 `entrance` 模块聚合了 `system``disk-monitor``dbms``deploy``user``detection``activate-tool``add-data``add-ledger``wave-tool``device-types``mms-mapping``parse-pqdif`,是当前运行时主入口。
## 技术基线 ## 技术基线
@@ -98,8 +100,12 @@ P0 已补齐基线文档,建议按以下顺序阅读:
- 当前提供电能质量 13 张表批量补数、任务状态查询和模板规则查询能力 - 当前提供电能质量 13 张表批量补数、任务状态查询和模板规则查询能力
- `tools/add-ledger` - `tools/add-ledger`
- 当前为数据台账工具预留空模块 - 当前为数据台账工具预留空模块
- `tools/device-types`
- 负责设备类型维护、ICD 校验结果保存和 PQDIF 校验预留入口
- `tools/mms-mapping` - `tools/mms-mapping`
- 负责 ICD 文件解析与 MMS 映射数据生成能力 - 负责 ICD 文件解析与 MMS 映射数据生成能力
- `tools/parse-pqdif`
- 当前为 PQDIF 文件解析能力预留空方法骨架
- `tools/wave-tool` - `tools/wave-tool`
- 负责波形文本解析与查看数据组装能力 - 负责波形文本解析与查看数据组装能力

View File

@@ -53,11 +53,21 @@
<artifactId>wave-tool</artifactId> <artifactId>wave-tool</artifactId>
<version>1.0.0</version> <version>1.0.0</version>
</dependency> </dependency>
<dependency>
<groupId>com.njcn.gather</groupId>
<artifactId>device-types</artifactId>
<version>1.0.0</version>
</dependency>
<dependency> <dependency>
<groupId>com.njcn.gather</groupId> <groupId>com.njcn.gather</groupId>
<artifactId>mms-mapping</artifactId> <artifactId>mms-mapping</artifactId>
<version>1.0.0</version> <version>1.0.0</version>
</dependency> </dependency>
<dependency>
<groupId>com.njcn.gather</groupId>
<artifactId>parse-pqdif</artifactId>
<version>1.0.0</version>
</dependency>
<dependency> <dependency>
<groupId>com.njcn.gather</groupId> <groupId>com.njcn.gather</groupId>
<artifactId>add-data</artifactId> <artifactId>add-data</artifactId>
@@ -75,7 +85,12 @@
</dependency> </dependency>
<dependency> <dependency>
<groupId>com.njcn.gather</groupId> <groupId>com.njcn.gather</groupId>
<artifactId>steady-DataView</artifactId> <artifactId>steady-dataView</artifactId>
<version>1.0.0</version>
</dependency>
<dependency>
<groupId>com.njcn.gather</groupId>
<artifactId>check-square</artifactId>
<version>1.0.0</version> <version>1.0.0</version>
</dependency> </dependency>
</dependencies> </dependencies>

View File

@@ -0,0 +1,45 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.njcn.gather</groupId>
<artifactId>steady</artifactId>
<version>1.0.0</version>
</parent>
<artifactId>check-square</artifactId>
<dependencies>
<dependency>
<groupId>com.njcn.gather</groupId>
<artifactId>steady-dataView</artifactId>
<version>1.0.0</version>
</dependency>
<dependency>
<groupId>com.njcn</groupId>
<artifactId>njcn-common</artifactId>
<version>0.0.1</version>
</dependency>
<dependency>
<groupId>com.njcn</groupId>
<artifactId>mybatis-plus</artifactId>
<version>0.0.1</version>
</dependency>
<dependency>
<groupId>com.njcn</groupId>
<artifactId>spingboot2.3.12</artifactId>
<version>2.3.12</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@@ -45,19 +45,6 @@ public class SteadyChecksquareCalculator {
return result; return result;
} }
public int maxContinuousMissingMinutes(List<SteadyChecksquareSegmentVO> segments) {
int result = 0;
if (segments == null) {
return result;
}
for (SteadyChecksquareSegmentVO segment : segments) {
if (segment != null && STATUS_MISSING.equals(segment.getStatus()) && segment.getDurationMinutes() != null) {
result = Math.max(result, segment.getDurationMinutes());
}
}
return result;
}
private SteadyChecksquareSegmentVO buildSegment(LocalDateTime startTime, LocalDateTime endTime, String status, private SteadyChecksquareSegmentVO buildSegment(LocalDateTime startTime, LocalDateTime endTime, String status,
int pointCount, int intervalMinutes) { int pointCount, int intervalMinutes) {
SteadyChecksquareSegmentVO segment = new SteadyChecksquareSegmentVO(); SteadyChecksquareSegmentVO segment = new SteadyChecksquareSegmentVO();

View File

@@ -0,0 +1,193 @@
package com.njcn.gather.steady.checksquare.component;
import com.njcn.gather.steady.checksquare.pojo.bo.SteadyChecksquareValuePointBO;
import com.njcn.gather.steady.checksquare.pojo.vo.SteadyChecksquareHarmonicParityDetailVO;
import com.njcn.gather.steady.checksquare.pojo.vo.SteadyChecksquareHarmonicParityRuleVO;
import com.njcn.gather.steady.datavie.pojo.bo.SteadyTrendIndicatorDefinitionBO;
import com.njcn.gather.steady.datavie.pojo.bo.SteadyTrendResolvedFieldBO;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
* 谐波偶次与局部奇次基线关系规则。
*/
@Component
@RequiredArgsConstructor
public class SteadyChecksquareHarmonicParityRuleComponent {
private static final DateTimeFormatter OUTPUT_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
private static final BigDecimal THRESHOLD_MULTIPLIER = new BigDecimal("2");
private static final BigDecimal EVEN_HARMONIC_DEADBAND_VALUE = new BigDecimal("0.1");
private static final int MIN_ODD_REFERENCE_COUNT = 2;
private final SteadyChecksquareInfluxQueryComponent influxQueryComponent;
public SteadyChecksquareHarmonicParityRuleVO check(String lineId, SteadyTrendIndicatorDefinitionBO indicator,
LocalDateTime startTime, LocalDateTime endTime,
int intervalMinutes) {
SteadyChecksquareHarmonicParityRuleVO result = new SteadyChecksquareHarmonicParityRuleVO();
if (!supportHarmonicParityRule(indicator)) {
return result;
}
for (String statType : indicator.getSupportStats()) {
for (String phase : indicator.getPhaseCodes()) {
Map<Integer, Map<LocalDateTime, BigDecimal>> valueMap = queryOrderValueMap(lineId, indicator, phase,
statType, startTime, endTime, intervalMinutes);
appendAbnormalDetails(result, phase, statType, indicator, valueMap);
}
}
result.setAbnormalPointCount(result.getAbnormalDetails().size());
result.setAbnormal(result.getAbnormalPointCount() > 0);
return result;
}
private boolean supportHarmonicParityRule(SteadyTrendIndicatorDefinitionBO indicator) {
return indicator != null && Boolean.TRUE.equals(indicator.getHarmonic())
&& indicator.getHarmonicOrderStart() != null && indicator.getHarmonicOrderEnd() != null;
}
private Map<Integer, Map<LocalDateTime, BigDecimal>> queryOrderValueMap(String lineId,
SteadyTrendIndicatorDefinitionBO indicator,
String phase, String statType,
LocalDateTime startTime,
LocalDateTime endTime,
int intervalMinutes) {
Map<Integer, Map<LocalDateTime, BigDecimal>> result = new LinkedHashMap<Integer, Map<LocalDateTime, BigDecimal>>();
List<SteadyTrendResolvedFieldBO> fields = new ArrayList<SteadyTrendResolvedFieldBO>();
for (int order = indicator.getHarmonicOrderStart(); order <= indicator.getHarmonicOrderEnd(); order++) {
fields.add(buildResolvedField(lineId, indicator, order, phase, statType));
}
Map<String, List<SteadyChecksquareValuePointBO>> fieldValueMap =
influxQueryComponent.queryValuePointMap(fields, startTime, endTime, intervalMinutes);
if (fieldValueMap == null) {
fieldValueMap = Collections.emptyMap();
}
for (int order = indicator.getHarmonicOrderStart(); order <= indicator.getHarmonicOrderEnd(); order++) {
result.put(order, toValueMap(fieldValueMap.get(indicator.getHarmonicFieldPrefix() + "_" + order)));
}
return result;
}
private void appendAbnormalDetails(SteadyChecksquareHarmonicParityRuleVO result, String phase, String statType,
SteadyTrendIndicatorDefinitionBO indicator,
Map<Integer, Map<LocalDateTime, BigDecimal>> valueMap) {
for (int order = firstEvenOrder(indicator.getHarmonicOrderStart()); order <= indicator.getHarmonicOrderEnd(); order += 2) {
Map<LocalDateTime, BigDecimal> evenValues = valueMap.get(order);
if (evenValues == null || evenValues.isEmpty()) {
continue;
}
for (Map.Entry<LocalDateTime, BigDecimal> entry : evenValues.entrySet()) {
appendAbnormalDetailIfNecessary(result, phase, statType, order, entry.getKey(), entry.getValue(), valueMap);
}
}
}
private void appendAbnormalDetailIfNecessary(SteadyChecksquareHarmonicParityRuleVO result, String phase,
String statType, int evenOrder, LocalDateTime time,
BigDecimal evenValue,
Map<Integer, Map<LocalDateTime, BigDecimal>> valueMap) {
if (evenValue == null || evenValue.compareTo(EVEN_HARMONIC_DEADBAND_VALUE) <= 0) {
return;
}
List<Integer> oddOrders = buildOddReferenceOrders(evenOrder);
List<BigDecimal> oddValues = new ArrayList<BigDecimal>();
List<Integer> effectiveOddOrders = new ArrayList<Integer>();
for (Integer oddOrder : oddOrders) {
Map<LocalDateTime, BigDecimal> values = valueMap.get(oddOrder);
BigDecimal oddValue = values == null ? null : values.get(time);
if (oddValue != null) {
effectiveOddOrders.add(oddOrder);
oddValues.add(oddValue);
}
}
if (oddValues.size() < MIN_ODD_REFERENCE_COUNT) {
return;
}
BigDecimal median = calculateMedian(oddValues);
if (median == null || evenValue.compareTo(median.multiply(THRESHOLD_MULTIPLIER)) <= 0) {
return;
}
result.getAbnormalDetails().add(buildDetail(time, phase, statType, evenOrder, evenValue,
effectiveOddOrders, oddValues, median));
}
private SteadyChecksquareHarmonicParityDetailVO buildDetail(LocalDateTime time, String phase, String statType,
Integer evenOrder, BigDecimal evenValue,
List<Integer> oddOrders, List<BigDecimal> oddValues,
BigDecimal median) {
SteadyChecksquareHarmonicParityDetailVO detail = new SteadyChecksquareHarmonicParityDetailVO();
detail.setTime(OUTPUT_TIME_FORMATTER.format(time));
detail.setPhase(phase);
detail.setStatType(statType);
detail.setEvenHarmonicOrder(evenOrder);
detail.setEvenValue(evenValue);
detail.setOddHarmonicOrders(new ArrayList<Integer>(oddOrders));
detail.setOddValues(new ArrayList<BigDecimal>(oddValues));
detail.setOddMedianValue(median);
detail.setThresholdMultiplier(THRESHOLD_MULTIPLIER);
return detail;
}
private List<Integer> buildOddReferenceOrders(int evenOrder) {
List<Integer> result = new ArrayList<Integer>();
result.add(evenOrder - 3);
result.add(evenOrder - 1);
result.add(evenOrder + 1);
result.add(evenOrder + 3);
return result;
}
private BigDecimal calculateMedian(List<BigDecimal> values) {
if (values == null || values.isEmpty()) {
return null;
}
List<BigDecimal> sorted = new ArrayList<BigDecimal>(values);
Collections.sort(sorted, Comparator.naturalOrder());
int middleIndex = sorted.size() / 2;
if (sorted.size() % 2 == 1) {
return sorted.get(middleIndex);
}
return sorted.get(middleIndex - 1).add(sorted.get(middleIndex)).divide(new BigDecimal("2"));
}
private int firstEvenOrder(int startOrder) {
return startOrder % 2 == 0 ? startOrder : startOrder + 1;
}
private Map<LocalDateTime, BigDecimal> toValueMap(List<SteadyChecksquareValuePointBO> points) {
Map<LocalDateTime, BigDecimal> result = new LinkedHashMap<LocalDateTime, BigDecimal>();
if (points == null || points.isEmpty()) {
return result;
}
for (SteadyChecksquareValuePointBO point : points) {
if (point != null && point.getTime() != null && point.getValue() != null) {
result.put(point.getTime(), point.getValue());
}
}
return result;
}
private SteadyTrendResolvedFieldBO buildResolvedField(String lineId, SteadyTrendIndicatorDefinitionBO indicator,
Integer harmonicOrder, String phase, String statType) {
SteadyTrendResolvedFieldBO field = new SteadyTrendResolvedFieldBO();
field.setMeasurement(indicator.getTableName());
field.setField(indicator.getHarmonicFieldPrefix() + "_" + harmonicOrder);
field.setLineId(lineId);
field.setIndicatorCode(indicator.getIndicatorCode());
field.setIndicatorName(indicator.getName());
field.setPhase(phase);
field.setStatType(statType);
field.setUnit(indicator.getUnit());
return field;
}
}

View File

@@ -0,0 +1,419 @@
package com.njcn.gather.steady.checksquare.component;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.njcn.common.pojo.enums.response.CommonResponseEnum;
import com.njcn.common.pojo.exception.BusinessException;
import com.njcn.gather.steady.checksquare.pojo.bo.SteadyChecksquareValuePointBO;
import com.njcn.gather.steady.datavie.config.SteadyInfluxDbProperties;
import com.njcn.gather.steady.datavie.pojo.bo.SteadyTrendResolvedFieldBO;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.math.BigDecimal;
import java.net.HttpURLConnection;
import java.net.URLEncoder;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* 数据校验 InfluxDB 查询组件。
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class SteadyChecksquareInfluxQueryComponent {
private static final DateTimeFormatter INFLUX_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss'Z'");
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
private static final int QUERY_WINDOW_DAYS = 1;
private static final ThreadLocal<Map<String, List<SteadyChecksquareValuePointBO>>> REQUEST_VALUE_CACHE =
new ThreadLocal<Map<String, List<SteadyChecksquareValuePointBO>>>();
private final SteadyInfluxDbProperties properties;
public void enableRequestCache() {
REQUEST_VALUE_CACHE.set(new LinkedHashMap<String, List<SteadyChecksquareValuePointBO>>());
}
public void clearRequestCache() {
REQUEST_VALUE_CACHE.remove();
}
public Set<LocalDateTime> queryExistingSlots(SteadyTrendResolvedFieldBO field, LocalDateTime startTime,
LocalDateTime endTime, int intervalMinutes) {
List<SteadyChecksquareValuePointBO> points = queryValuePoints(field, startTime, endTime, intervalMinutes);
Set<LocalDateTime> result = new HashSet<LocalDateTime>();
for (SteadyChecksquareValuePointBO point : points) {
if (point != null && point.getTime() != null) {
result.add(point.getTime());
}
}
return result;
}
public List<SteadyChecksquareValuePointBO> queryValuePoints(SteadyTrendResolvedFieldBO field, LocalDateTime startTime,
LocalDateTime endTime, int intervalMinutes) {
validateConfig();
String query = buildValuePointQuery(field, startTime, endTime);
String cacheKey = buildCacheKey(query, intervalMinutes);
Map<String, List<SteadyChecksquareValuePointBO>> cache = REQUEST_VALUE_CACHE.get();
if (cache != null && cache.containsKey(cacheKey)) {
return new ArrayList<SteadyChecksquareValuePointBO>(cache.get(cacheKey));
}
long startMillis = System.currentTimeMillis();
log.info("数据校验指标值 InfluxDB 查询开始measurement={}field={}lineId={}phase={}statType={}query={}",
field.getMeasurement(), field.getField(), field.getLineId(), field.getPhase(), field.getStatType(), query);
try {
List<SteadyChecksquareValuePointBO> points = queryValuePointsByWindow(field, startTime, endTime, intervalMinutes);
if (cache != null) {
cache.put(cacheKey, new ArrayList<SteadyChecksquareValuePointBO>(points));
}
log.info("数据校验指标值 InfluxDB 查询结束pointCount={}costMs={}", points.size(), System.currentTimeMillis() - startMillis);
return points;
} catch (RuntimeException ex) {
log.warn("数据校验指标值 InfluxDB 查询异常costMs={}error={}", System.currentTimeMillis() - startMillis, ex.getMessage());
throw ex;
}
}
public Map<String, List<SteadyChecksquareValuePointBO>> queryValuePointMap(List<SteadyTrendResolvedFieldBO> fields,
LocalDateTime startTime,
LocalDateTime endTime,
int intervalMinutes) {
Map<String, List<SteadyChecksquareValuePointBO>> result =
new LinkedHashMap<String, List<SteadyChecksquareValuePointBO>>();
if (fields == null || fields.isEmpty()) {
return result;
}
if (fields.size() == 1) {
SteadyTrendResolvedFieldBO field = fields.get(0);
result.put(field.getField(), queryValuePoints(field, startTime, endTime, intervalMinutes));
return result;
}
validateConfig();
Map<String, List<SteadyChecksquareValuePointBO>> cache = REQUEST_VALUE_CACHE.get();
List<SteadyTrendResolvedFieldBO> missingFields = new ArrayList<SteadyTrendResolvedFieldBO>();
for (SteadyTrendResolvedFieldBO field : fields) {
String cacheKey = buildCacheKey(buildValuePointQuery(field, startTime, endTime), intervalMinutes);
if (cache != null && cache.containsKey(cacheKey)) {
result.put(field.getField(), new ArrayList<SteadyChecksquareValuePointBO>(cache.get(cacheKey)));
} else {
missingFields.add(field);
}
}
if (!missingFields.isEmpty()) {
String query = buildBatchValuePointQuery(missingFields, startTime, endTime);
long startMillis = System.currentTimeMillis();
SteadyTrendResolvedFieldBO first = missingFields.get(0);
log.info("数据校验指标值 InfluxDB 批量查询开始measurement={}fieldCount={}lineId={}phase={}statType={}query={}",
first.getMeasurement(), missingFields.size(), first.getLineId(), first.getPhase(), first.getStatType(), query);
try {
Map<String, List<SteadyChecksquareValuePointBO>> queried =
queryBatchValuePointsByWindow(missingFields, startTime, endTime, intervalMinutes);
for (SteadyTrendResolvedFieldBO field : missingFields) {
List<SteadyChecksquareValuePointBO> points = queried.get(field.getField());
if (points == null) {
points = new ArrayList<SteadyChecksquareValuePointBO>();
}
result.put(field.getField(), points);
if (cache != null) {
String cacheKey = buildCacheKey(buildValuePointQuery(field, startTime, endTime), intervalMinutes);
cache.put(cacheKey, new ArrayList<SteadyChecksquareValuePointBO>(points));
}
}
log.info("数据校验指标值 InfluxDB 批量查询结束fieldCount={}costMs={}",
missingFields.size(), System.currentTimeMillis() - startMillis);
} catch (RuntimeException ex) {
log.warn("数据校验指标值 InfluxDB 批量查询异常fieldCount={}costMs={}error={}",
missingFields.size(), System.currentTimeMillis() - startMillis, ex.getMessage());
throw ex;
}
}
return result;
}
public String buildChecksquareQuery(SteadyTrendResolvedFieldBO field, LocalDateTime startTime, LocalDateTime endTime) {
return buildValuePointQuery(field, startTime, endTime);
}
private String buildCacheKey(String query, int intervalMinutes) {
return query + "|intervalMinutes=" + intervalMinutes;
}
private List<SteadyChecksquareValuePointBO> queryValuePointsByWindow(SteadyTrendResolvedFieldBO field,
LocalDateTime startTime,
LocalDateTime endTime,
int intervalMinutes) {
List<SteadyChecksquareValuePointBO> result = new ArrayList<SteadyChecksquareValuePointBO>();
LocalDateTime windowStart = startTime;
while (!windowStart.isAfter(endTime)) {
LocalDateTime windowEnd = min(windowStart.plusDays(QUERY_WINDOW_DAYS).minusNanos(1), endTime);
result.addAll(parseValuePoints(executeQuery(buildValuePointQuery(field, windowStart, windowEnd)), intervalMinutes));
windowStart = windowEnd.plusNanos(1);
}
return result;
}
private Map<String, List<SteadyChecksquareValuePointBO>> queryBatchValuePointsByWindow(List<SteadyTrendResolvedFieldBO> fields,
LocalDateTime startTime,
LocalDateTime endTime,
int intervalMinutes) {
Map<String, List<SteadyChecksquareValuePointBO>> result =
new LinkedHashMap<String, List<SteadyChecksquareValuePointBO>>();
for (SteadyTrendResolvedFieldBO field : fields) {
result.put(field.getField(), new ArrayList<SteadyChecksquareValuePointBO>());
}
LocalDateTime windowStart = startTime;
while (!windowStart.isAfter(endTime)) {
LocalDateTime windowEnd = min(windowStart.plusDays(QUERY_WINDOW_DAYS).minusNanos(1), endTime);
Map<String, List<SteadyChecksquareValuePointBO>> windowResult =
parseBatchValuePoints(executeQuery(buildBatchValuePointQuery(fields, windowStart, windowEnd)), intervalMinutes);
for (Map.Entry<String, List<SteadyChecksquareValuePointBO>> entry : windowResult.entrySet()) {
List<SteadyChecksquareValuePointBO> points = result.get(entry.getKey());
if (points == null) {
points = new ArrayList<SteadyChecksquareValuePointBO>();
result.put(entry.getKey(), points);
}
points.addAll(entry.getValue());
}
windowStart = windowEnd.plusNanos(1);
}
return result;
}
private LocalDateTime min(LocalDateTime first, LocalDateTime second) {
return first.isAfter(second) ? second : first;
}
public String buildValuePointQuery(SteadyTrendResolvedFieldBO field, LocalDateTime startTime, LocalDateTime endTime) {
StringBuilder sql = new StringBuilder();
sql.append("SELECT \"").append(field.getField()).append("\" AS \"value\"");
sql.append(" FROM \"").append(field.getMeasurement()).append("\"");
sql.append(" WHERE time >= '").append(INFLUX_TIME_FORMATTER.format(startTime)).append("'");
sql.append(" AND time <= '").append(INFLUX_TIME_FORMATTER.format(endTime)).append("'");
sql.append(" AND \"line_id\" = '").append(escapeTagValue(field.getLineId())).append("'");
sql.append(" AND \"phasic_type\" = '").append(escapeTagValue(field.getPhase())).append("'");
if (hasValueTypeTag(field.getMeasurement())) {
sql.append(" AND \"value_type\" = '").append(resolveValueType(field.getStatType())).append("'");
}
sql.append(" ORDER BY time ASC");
return sql.toString();
}
public String buildBatchValuePointQuery(List<SteadyTrendResolvedFieldBO> fields, LocalDateTime startTime, LocalDateTime endTime) {
SteadyTrendResolvedFieldBO first = fields.get(0);
StringBuilder sql = new StringBuilder("SELECT ");
for (int i = 0; i < fields.size(); i++) {
SteadyTrendResolvedFieldBO field = fields.get(i);
if (i > 0) {
sql.append(", ");
}
sql.append("\"").append(field.getField()).append("\" AS \"").append(field.getField()).append("\"");
}
sql.append(" FROM \"").append(first.getMeasurement()).append("\"");
sql.append(" WHERE time >= '").append(INFLUX_TIME_FORMATTER.format(startTime)).append("'");
sql.append(" AND time <= '").append(INFLUX_TIME_FORMATTER.format(endTime)).append("'");
sql.append(" AND \"line_id\" = '").append(escapeTagValue(first.getLineId())).append("'");
sql.append(" AND \"phasic_type\" = '").append(escapeTagValue(first.getPhase())).append("'");
if (hasValueTypeTag(first.getMeasurement())) {
sql.append(" AND \"value_type\" = '").append(resolveValueType(first.getStatType())).append("'");
}
sql.append(" ORDER BY time ASC");
return sql.toString();
}
private List<SteadyChecksquareValuePointBO> parseValuePoints(String body, int intervalMinutes) {
try {
JsonNode root = OBJECT_MAPPER.readTree(body);
JsonNode values = root.path("results").path(0).path("series").path(0).path("values");
List<SteadyChecksquareValuePointBO> result = new ArrayList<SteadyChecksquareValuePointBO>();
if (!values.isArray()) {
return result;
}
for (JsonNode value : values) {
if (value.size() < 2 || value.get(1).isNull()) {
continue;
}
LocalDateTime time = parseInfluxTime(value.get(0).asText());
if (time == null) {
continue;
}
SteadyChecksquareValuePointBO point = new SteadyChecksquareValuePointBO();
point.setTime(alignToPreviousSlot(time, intervalMinutes));
point.setValue(new BigDecimal(value.get(1).asText()));
result.add(point);
}
return result;
} catch (IOException ex) {
throw fail("InfluxDB 返回结果解析失败:" + ex.getMessage());
} catch (NumberFormatException ex) {
throw fail("InfluxDB 返回指标值格式不正确:" + ex.getMessage());
}
}
private Map<String, List<SteadyChecksquareValuePointBO>> parseBatchValuePoints(String body, int intervalMinutes) {
try {
JsonNode root = OBJECT_MAPPER.readTree(body);
JsonNode series = root.path("results").path(0).path("series").path(0);
JsonNode columns = series.path("columns");
JsonNode values = series.path("values");
Map<Integer, String> columnMap = new LinkedHashMap<Integer, String>();
Map<String, List<SteadyChecksquareValuePointBO>> result =
new LinkedHashMap<String, List<SteadyChecksquareValuePointBO>>();
if (!columns.isArray() || !values.isArray()) {
return result;
}
for (int i = 1; i < columns.size(); i++) {
String fieldName = columns.get(i).asText();
columnMap.put(i, fieldName);
result.put(fieldName, new ArrayList<SteadyChecksquareValuePointBO>());
}
for (JsonNode row : values) {
if (row.size() < 2) {
continue;
}
LocalDateTime time = parseInfluxTime(row.get(0).asText());
if (time == null) {
continue;
}
LocalDateTime slot = alignToPreviousSlot(time, intervalMinutes);
for (Map.Entry<Integer, String> entry : columnMap.entrySet()) {
JsonNode value = row.get(entry.getKey());
if (value == null || value.isNull()) {
continue;
}
SteadyChecksquareValuePointBO point = new SteadyChecksquareValuePointBO();
point.setTime(slot);
point.setValue(new BigDecimal(value.asText()));
result.get(entry.getValue()).add(point);
}
}
return result;
} catch (IOException ex) {
throw fail("InfluxDB 返回结果解析失败:" + ex.getMessage());
} catch (NumberFormatException ex) {
throw fail("InfluxDB 返回指标值格式不正确:" + ex.getMessage());
}
}
private LocalDateTime alignToPreviousSlot(LocalDateTime time, int intervalMinutes) {
LocalDateTime minuteFloor = time.withSecond(0).withNano(0);
int minuteOfDay = minuteFloor.getHour() * 60 + minuteFloor.getMinute();
int remainder = minuteOfDay % intervalMinutes;
return minuteFloor.minusMinutes(remainder);
}
private LocalDateTime parseInfluxTime(String value) {
try {
return OffsetDateTime.parse(value).withOffsetSameInstant(ZoneOffset.UTC).toLocalDateTime();
} catch (RuntimeException ex) {
return null;
}
}
private String executeQuery(String query) {
HttpURLConnection connection = null;
try {
URL url = new URL(buildQueryUrl(query));
connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("GET");
connection.setConnectTimeout(properties.getConnectTimeoutMs());
connection.setReadTimeout(properties.getReadTimeoutMs());
int status = connection.getResponseCode();
InputStream stream = status >= 200 && status < 300 ? connection.getInputStream() : connection.getErrorStream();
String body = readBody(stream);
if (status < 200 || status >= 300) {
throw fail("InfluxDB 查询失败:" + body);
}
return body;
} catch (IOException ex) {
throw fail("InfluxDB 查询异常:" + ex.getMessage());
} finally {
if (connection != null) {
connection.disconnect();
}
}
}
private String buildQueryUrl(String query) throws IOException {
StringBuilder url = new StringBuilder(trimRightSlash(properties.getUrl())).append("/query?");
url.append("db=").append(encode(properties.getDatabase()));
if (properties.getUsername() != null && !properties.getUsername().trim().isEmpty()) {
url.append("&u=").append(encode(properties.getUsername().trim()));
}
if (properties.getPassword() != null && !properties.getPassword().trim().isEmpty()) {
url.append("&p=").append(encode(properties.getPassword()));
}
url.append("&q=").append(encode(query));
return url.toString();
}
private void validateConfig() {
if (properties.getUrl() == null || properties.getUrl().trim().isEmpty()) {
throw fail("InfluxDB 地址未配置");
}
if (properties.getDatabase() == null || properties.getDatabase().trim().isEmpty()) {
throw fail("InfluxDB database 未配置");
}
}
private String readBody(InputStream stream) throws IOException {
if (stream == null) {
return "";
}
BufferedReader reader = new BufferedReader(new InputStreamReader(stream, StandardCharsets.UTF_8));
StringBuilder body = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
body.append(line);
}
return body.toString();
}
private String escapeTagValue(String value) {
return value == null ? "" : value.replace("\\", "\\\\").replace("'", "\\'");
}
private String resolveValueType(String statType) {
if (statType == null || statType.trim().isEmpty()) {
return "AVG";
}
return statType.trim().toUpperCase();
}
private boolean hasValueTypeTag(String measurement) {
return !"data_flicker".equals(measurement) && !"data_fluc".equals(measurement) && !"data_plt".equals(measurement);
}
private String trimRightSlash(String value) {
String text = value.trim();
while (text.endsWith("/")) {
text = text.substring(0, text.length() - 1);
}
return text;
}
private String encode(String value) throws IOException {
return URLEncoder.encode(value, StandardCharsets.UTF_8.name());
}
private BusinessException fail(String message) {
return new BusinessException(CommonResponseEnum.FAIL, message);
}
}

View File

@@ -0,0 +1,145 @@
package com.njcn.gather.steady.checksquare.component;
import com.njcn.gather.steady.checksquare.pojo.bo.SteadyChecksquareValuePointBO;
import com.njcn.gather.steady.checksquare.pojo.vo.SteadyChecksquareValueOrderDetailVO;
import com.njcn.gather.steady.checksquare.pojo.vo.SteadyChecksquareValueOrderRuleVO;
import com.njcn.gather.steady.datavie.pojo.bo.SteadyTrendIndicatorDefinitionBO;
import com.njcn.gather.steady.datavie.pojo.bo.SteadyTrendResolvedFieldBO;
import com.njcn.gather.steady.datavie.pojo.bo.SteadyTrendSeriesFieldBO;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
* 数据校验指标值大小关系规则。
*/
@Component
@RequiredArgsConstructor
public class SteadyChecksquareValueOrderRuleComponent {
private static final DateTimeFormatter OUTPUT_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
private static final List<String> REQUIRED_STATS = Collections.unmodifiableList(Arrays.asList("MAX", "CP95", "AVG", "MIN"));
private static final int ABNORMAL_THRESHOLD = 1;
private final SteadyChecksquareInfluxQueryComponent influxQueryComponent;
public SteadyChecksquareValueOrderRuleVO check(String lineId, SteadyTrendIndicatorDefinitionBO indicator,
Integer harmonicOrder, LocalDateTime startTime,
LocalDateTime endTime, int intervalMinutes) {
SteadyChecksquareValueOrderRuleVO result = new SteadyChecksquareValueOrderRuleVO();
if (!supportValueOrderRule(indicator)) {
return result;
}
for (String phase : indicator.getPhaseCodes()) {
Map<String, Map<LocalDateTime, BigDecimal>> statValueMap = queryStatValueMap(lineId, indicator,
harmonicOrder, phase, startTime, endTime, intervalMinutes);
appendAbnormalDetails(result, phase, harmonicOrder, statValueMap);
}
result.setAbnormalPointCount(result.getAbnormalDetails().size());
result.setAbnormal(result.getAbnormalPointCount() > ABNORMAL_THRESHOLD);
return result;
}
private boolean supportValueOrderRule(SteadyTrendIndicatorDefinitionBO indicator) {
return indicator != null && indicator.getSupportStats() != null && indicator.getSupportStats().containsAll(REQUIRED_STATS);
}
private Map<String, Map<LocalDateTime, BigDecimal>> queryStatValueMap(String lineId,
SteadyTrendIndicatorDefinitionBO indicator,
Integer harmonicOrder, String phase,
LocalDateTime startTime, LocalDateTime endTime,
int intervalMinutes) {
Map<String, Map<LocalDateTime, BigDecimal>> result = new LinkedHashMap<String, Map<LocalDateTime, BigDecimal>>();
for (String statType : REQUIRED_STATS) {
SteadyTrendResolvedFieldBO field = buildResolvedField(lineId, indicator, harmonicOrder, phase, statType);
result.put(statType, toValueMap(influxQueryComponent.queryValuePoints(field, startTime, endTime, intervalMinutes)));
}
return result;
}
private void appendAbnormalDetails(SteadyChecksquareValueOrderRuleVO result, String phase, Integer harmonicOrder,
Map<String, Map<LocalDateTime, BigDecimal>> statValueMap) {
Map<LocalDateTime, BigDecimal> maxValues = statValueMap.get("MAX");
Map<LocalDateTime, BigDecimal> cp95Values = statValueMap.get("CP95");
Map<LocalDateTime, BigDecimal> avgValues = statValueMap.get("AVG");
Map<LocalDateTime, BigDecimal> minValues = statValueMap.get("MIN");
if (maxValues == null || cp95Values == null || avgValues == null || minValues == null) {
return;
}
for (Map.Entry<LocalDateTime, BigDecimal> entry : maxValues.entrySet()) {
LocalDateTime time = entry.getKey();
BigDecimal maxValue = entry.getValue();
BigDecimal cp95Value = cp95Values.get(time);
BigDecimal avgValue = avgValues.get(time);
BigDecimal minValue = minValues.get(time);
// 缺少任一统计值时由缺数校验负责,不重复计入大小关系异常。
if (maxValue == null || cp95Value == null || avgValue == null || minValue == null) {
continue;
}
if (maxValue.compareTo(cp95Value) >= 0 && cp95Value.compareTo(avgValue) >= 0 && avgValue.compareTo(minValue) >= 0) {
continue;
}
result.getAbnormalDetails().add(buildDetail(time, phase, harmonicOrder, maxValue, minValue, avgValue, cp95Value));
}
}
private SteadyChecksquareValueOrderDetailVO buildDetail(LocalDateTime time, String phase, Integer harmonicOrder,
BigDecimal maxValue, BigDecimal minValue,
BigDecimal avgValue, BigDecimal cp95Value) {
SteadyChecksquareValueOrderDetailVO detail = new SteadyChecksquareValueOrderDetailVO();
detail.setTime(OUTPUT_TIME_FORMATTER.format(time));
detail.setPhase(phase);
detail.setHarmonicOrder(harmonicOrder);
detail.setMaxValue(maxValue);
detail.setMinValue(minValue);
detail.setAvgValue(avgValue);
detail.setCp95Value(cp95Value);
return detail;
}
private Map<LocalDateTime, BigDecimal> toValueMap(List<SteadyChecksquareValuePointBO> points) {
Map<LocalDateTime, BigDecimal> result = new LinkedHashMap<LocalDateTime, BigDecimal>();
if (points == null || points.isEmpty()) {
return result;
}
for (SteadyChecksquareValuePointBO point : points) {
if (point != null && point.getTime() != null && point.getValue() != null) {
result.put(point.getTime(), point.getValue());
}
}
return result;
}
private SteadyTrendResolvedFieldBO buildResolvedField(String lineId, SteadyTrendIndicatorDefinitionBO indicator,
Integer harmonicOrder, String phase, String statType) {
SteadyTrendResolvedFieldBO field = new SteadyTrendResolvedFieldBO();
field.setMeasurement(indicator.getTableName());
field.setField(resolveField(indicator, harmonicOrder));
field.setLineId(lineId);
field.setIndicatorCode(indicator.getIndicatorCode());
field.setIndicatorName(indicator.getName());
field.setPhase(phase);
field.setStatType(statType);
field.setUnit(indicator.getUnit());
return field;
}
private String resolveField(SteadyTrendIndicatorDefinitionBO indicator, Integer harmonicOrder) {
if (Boolean.TRUE.equals(indicator.getHarmonic())) {
return indicator.getHarmonicFieldPrefix() + "_" + harmonicOrder;
}
List<SteadyTrendSeriesFieldBO> fields = indicator.getSeriesFields();
if (fields == null || fields.isEmpty()) {
return "";
}
return fields.get(0).getField();
}
}

View File

@@ -0,0 +1,94 @@
package com.njcn.gather.steady.checksquare.controller;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.njcn.common.pojo.annotation.OperateInfo;
import com.njcn.common.pojo.constant.OperateType;
import com.njcn.common.pojo.enums.common.LogEnum;
import com.njcn.common.pojo.enums.response.CommonResponseEnum;
import com.njcn.common.pojo.response.HttpResult;
import com.njcn.common.utils.LogUtil;
import com.njcn.gather.steady.checksquare.pojo.param.SteadyChecksquareHistoryQueryParam;
import com.njcn.gather.steady.checksquare.pojo.param.SteadyChecksquareQueryParam;
import com.njcn.gather.steady.checksquare.pojo.vo.SteadyChecksquareItemDetailVO;
import com.njcn.gather.steady.checksquare.pojo.vo.SteadyChecksquareQueryVO;
import com.njcn.gather.steady.checksquare.pojo.vo.SteadyChecksquareTaskVO;
import com.njcn.gather.steady.checksquare.service.SteadyChecksquareService;
import com.njcn.web.controller.BaseController;
import com.njcn.web.utils.HttpResultUtil;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
/**
* 数据校验接口。
*/
@Slf4j
@Api(tags = "数据校验")
@RestController
@RequestMapping("/steady/checksquare")
@RequiredArgsConstructor
public class SteadyChecksquareController extends BaseController {
private final SteadyChecksquareService checksquareService;
@OperateInfo(info = LogEnum.BUSINESS_COMMON)
@ApiOperation("查询数据校验历史记录")
@PostMapping("/query")
public HttpResult<Page<SteadyChecksquareTaskVO>> query(@RequestBody @Validated SteadyChecksquareHistoryQueryParam param) {
String methodDescribe = getMethodDescribe("query");
LogUtil.njcnDebug(log, "{}开始查询数据校验历史记录param={}", methodDescribe, param);
Page<SteadyChecksquareTaskVO> result = checksquareService.query(param);
return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, result, methodDescribe);
}
@OperateInfo(info = LogEnum.BUSINESS_COMMON, operateType = OperateType.ADD)
@ApiOperation("新增数据校验记录")
@PostMapping("/create")
public HttpResult<SteadyChecksquareTaskVO> create(@RequestBody @Validated SteadyChecksquareQueryParam param) {
String methodDescribe = getMethodDescribe("create");
LogUtil.njcnDebug(log, "{}开始新增数据校验记录param={}", methodDescribe, param);
SteadyChecksquareTaskVO result = checksquareService.create(param);
return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, result, methodDescribe);
}
@OperateInfo(info = LogEnum.BUSINESS_COMMON, operateType = OperateType.DELETE)
@ApiOperation("删除数据校验任务")
@PostMapping("/delete")
public HttpResult<Boolean> delete(@RequestBody List<String> taskIds) {
String methodDescribe = getMethodDescribe("delete");
LogUtil.njcnDebug(log, "{}开始删除数据校验任务taskIds={}", methodDescribe, taskIds);
boolean result = checksquareService.delete(taskIds);
return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, result, methodDescribe);
}
@OperateInfo(info = LogEnum.BUSINESS_COMMON)
@ApiOperation("查询数据校验任务详情")
@GetMapping("/detail")
public HttpResult<SteadyChecksquareQueryVO> detail(@RequestParam("taskId") String taskId) {
String methodDescribe = getMethodDescribe("detail");
return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, checksquareService.detail(taskId), methodDescribe);
}
@OperateInfo(info = LogEnum.BUSINESS_COMMON)
@ApiOperation("查询数据校验检测项明细")
@GetMapping("/item-detail")
public HttpResult<SteadyChecksquareItemDetailVO> itemDetail(@RequestParam("itemId") String itemId,
@RequestParam("detailType") String detailType,
@RequestParam(value = "statType", required = false) String statType,
@RequestParam(value = "pageNum", required = false) Integer pageNum,
@RequestParam(value = "pageSize", required = false) Integer pageSize) {
String methodDescribe = getMethodDescribe("itemDetail");
return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS,
checksquareService.itemDetail(itemId, detailType, statType, pageNum, pageSize), methodDescribe);
}
}

View File

@@ -0,0 +1,10 @@
package com.njcn.gather.steady.checksquare.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.njcn.gather.steady.checksquare.pojo.po.SteadyChecksquareDetailPO;
/**
* 数据校验明细 Mapper。
*/
public interface SteadyChecksquareDetailMapper extends BaseMapper<SteadyChecksquareDetailPO> {
}

View File

@@ -0,0 +1,10 @@
package com.njcn.gather.steady.checksquare.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.njcn.gather.steady.checksquare.pojo.po.SteadyChecksquareItemPO;
/**
* 数据校验检测项 Mapper。
*/
public interface SteadyChecksquareItemMapper extends BaseMapper<SteadyChecksquareItemPO> {
}

View File

@@ -0,0 +1,10 @@
package com.njcn.gather.steady.checksquare.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.njcn.gather.steady.checksquare.pojo.po.SteadyChecksquareStatSummaryPO;
/**
* 数据校验统计摘要 Mapper。
*/
public interface SteadyChecksquareStatSummaryMapper extends BaseMapper<SteadyChecksquareStatSummaryPO> {
}

View File

@@ -0,0 +1,10 @@
package com.njcn.gather.steady.checksquare.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.njcn.gather.steady.checksquare.pojo.po.SteadyChecksquareTaskPO;
/**
* 数据校验任务 Mapper。
*/
public interface SteadyChecksquareTaskMapper extends BaseMapper<SteadyChecksquareTaskPO> {
}

View File

@@ -0,0 +1,22 @@
package com.njcn.gather.steady.checksquare.pojo.bo;
import lombok.Data;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
* 数据校验指标值时间点。
*/
@Data
public class SteadyChecksquareValuePointBO implements Serializable {
private static final long serialVersionUID = 1L;
/** 对齐后的统计时间。 */
private LocalDateTime time;
/** 指标值。 */
private BigDecimal value;
}

View File

@@ -0,0 +1,20 @@
package com.njcn.gather.steady.checksquare.pojo.constant;
/**
* 数据校验常量。
*/
public final class SteadyChecksquareConst {
public static final int STATE_DELETED = 0;
public static final int STATE_ENABLED = 1;
public static final String TASK_STATUS_RUNNING = "RUNNING";
public static final String TASK_STATUS_SUCCESS = "SUCCESS";
public static final String TASK_STATUS_FAIL = "FAIL";
public static final String DETAIL_TYPE_SEGMENT = "SEGMENT";
public static final String DETAIL_TYPE_VALUE_ORDER = "VALUE_ORDER";
public static final String DETAIL_TYPE_HARMONIC_PARITY = "HARMONIC_PARITY";
private SteadyChecksquareConst() {
}
}

View File

@@ -0,0 +1,31 @@
package com.njcn.gather.steady.checksquare.pojo.param;
import com.njcn.web.pojo.param.BaseParam;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 数据校验历史查询参数。
*/
@Data
@EqualsAndHashCode(callSuper = true)
@ApiModel("数据校验历史查询参数")
public class SteadyChecksquareHistoryQueryParam extends BaseParam {
@ApiModelProperty("监测点 ID")
private String lineId;
@ApiModelProperty("指标编码")
private String indicatorCode;
@ApiModelProperty("检测开始时间,格式 yyyy-MM-dd HH:mm:ss")
private String timeStart;
@ApiModelProperty("检测结束时间,格式 yyyy-MM-dd HH:mm:ss")
private String timeEnd;
@ApiModelProperty("是否存在异常")
private Boolean hasAbnormal;
}

View File

@@ -8,10 +8,10 @@ import java.io.Serializable;
import java.util.List; import java.util.List;
/** /**
* 数据校验查询参数 * 数据校验新增检测参数
*/ */
@Data @Data
@ApiModel("数据校验查询参数") @ApiModel("数据校验新增检测参数")
public class SteadyChecksquareQueryParam implements Serializable { public class SteadyChecksquareQueryParam implements Serializable {
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
@@ -27,7 +27,4 @@ public class SteadyChecksquareQueryParam implements Serializable {
@ApiModelProperty("结束时间,格式 yyyy-MM-dd HH:mm:ss") @ApiModelProperty("结束时间,格式 yyyy-MM-dd HH:mm:ss")
private String timeEnd; private String timeEnd;
@ApiModelProperty("谐波次数,谐波指标按请求次数查询")
private List<Integer> harmonicOrders;
} }

View File

@@ -0,0 +1,67 @@
package com.njcn.gather.steady.checksquare.pojo.po;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
* 数据校验明细。
*/
@Data
@TableName("steady_checksquare_detail")
public class SteadyChecksquareDetailPO implements Serializable {
private static final long serialVersionUID = 1L;
@TableId("id")
private String id;
@TableField("item_id")
private String itemId;
@TableField("detail_type")
private String detailType;
@TableField("stat_type")
private String statType;
@TableField("start_time")
private LocalDateTime startTime;
@TableField("end_time")
private LocalDateTime endTime;
@TableField("point_time")
private LocalDateTime pointTime;
@TableField("segment_status")
private String segmentStatus;
@TableField("missing_point_count")
private Integer missingPointCount;
@TableField("duration_minutes")
private Integer durationMinutes;
@TableField("phase")
private String phase;
@TableField("harmonic_order")
private Integer harmonicOrder;
@TableField("max_value")
private BigDecimal maxValue;
@TableField("min_value")
private BigDecimal minValue;
@TableField("avg_value")
private BigDecimal avgValue;
@TableField("cp95_value")
private BigDecimal cp95Value;
@TableField("even_harmonic_order")
private Integer evenHarmonicOrder;
@TableField("even_value")
private BigDecimal evenValue;
@TableField("odd_harmonic_orders_json")
private String oddHarmonicOrdersJson;
@TableField("odd_values_json")
private String oddValuesJson;
@TableField("odd_median_value")
private BigDecimal oddMedianValue;
@TableField("threshold_multiplier")
private BigDecimal thresholdMultiplier;
@TableField("create_time")
private LocalDateTime createTime;
}

View File

@@ -0,0 +1,61 @@
package com.njcn.gather.steady.checksquare.pojo.po;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
* 数据校验检测项。
*/
@Data
@TableName("steady_checksquare_item")
public class SteadyChecksquareItemPO implements Serializable {
private static final long serialVersionUID = 1L;
@TableId("id")
private String id;
@TableField("task_id")
private String taskId;
@TableField("item_key")
private String itemKey;
@TableField("indicator_code")
private String indicatorCode;
@TableField("indicator_name")
private String indicatorName;
@TableField("harmonic_order")
private Integer harmonicOrder;
@TableField("interval_minutes")
private Integer intervalMinutes;
@TableField("has_data")
private Integer hasData;
@TableField("expected_point_count")
private Integer expectedPointCount;
@TableField("actual_point_count")
private Integer actualPointCount;
@TableField("missing_point_count")
private Integer missingPointCount;
@TableField("data_integrity")
private BigDecimal dataIntegrity;
@TableField("data_integrity_text")
private String dataIntegrityText;
@TableField("abnormal")
private Integer abnormal;
@TableField("abnormal_point_count")
private Integer abnormalPointCount;
@TableField("harmonic_parity_abnormal")
private Integer harmonicParityAbnormal;
@TableField("harmonic_parity_abnormal_point_count")
private Integer harmonicParityAbnormalPointCount;
@TableField("state")
private Integer state;
@TableField("create_time")
private LocalDateTime createTime;
@TableField("update_time")
private LocalDateTime updateTime;
}

View File

@@ -0,0 +1,43 @@
package com.njcn.gather.steady.checksquare.pojo.po;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
* 数据校验统计摘要。
*/
@Data
@TableName("steady_checksquare_stat_summary")
public class SteadyChecksquareStatSummaryPO implements Serializable {
private static final long serialVersionUID = 1L;
@TableId("id")
private String id;
@TableField("item_id")
private String itemId;
@TableField("stat_type")
private String statType;
@TableField("supported")
private Integer supported;
@TableField("has_data")
private Integer hasData;
@TableField("expected_point_count")
private Integer expectedPointCount;
@TableField("actual_point_count")
private Integer actualPointCount;
@TableField("missing_point_count")
private Integer missingPointCount;
@TableField("data_integrity")
private BigDecimal dataIntegrity;
@TableField("data_integrity_text")
private String dataIntegrityText;
@TableField("create_time")
private LocalDateTime createTime;
}

View File

@@ -0,0 +1,59 @@
package com.njcn.gather.steady.checksquare.pojo.po;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
* 数据校验任务。
*/
@Data
@TableName("steady_checksquare_task")
public class SteadyChecksquareTaskPO implements Serializable {
private static final long serialVersionUID = 1L;
@TableId("id")
private String id;
@TableField("task_no")
private String taskNo;
@TableField("line_id")
private String lineId;
@TableField("line_name")
private String lineName;
@TableField("time_start")
private LocalDateTime timeStart;
@TableField("time_end")
private LocalDateTime timeEnd;
@TableField("interval_minutes")
private Integer intervalMinutes;
@TableField("indicator_codes_json")
private String indicatorCodesJson;
@TableField("indicator_codes_text")
private String indicatorCodesText;
@TableField("task_status")
private String taskStatus;
@TableField("item_count")
private Integer itemCount;
@TableField("abnormal_item_count")
private Integer abnormalItemCount;
@TableField("min_data_integrity")
private BigDecimal minDataIntegrity;
@TableField("result_message")
private String resultMessage;
@TableField("state")
private Integer state;
@TableField("create_by")
private String createBy;
@TableField("create_time")
private LocalDateTime createTime;
@TableField("update_by")
private String updateBy;
@TableField("update_time")
private LocalDateTime updateTime;
}

View File

@@ -0,0 +1,47 @@
package com.njcn.gather.steady.checksquare.pojo.vo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
/**
* 谐波奇偶关系异常明细。
*/
@Data
@ApiModel("谐波奇偶关系异常明细")
public class SteadyChecksquareHarmonicParityDetailVO implements Serializable {
private static final long serialVersionUID = 1L;
@ApiModelProperty("时间")
private String time;
@ApiModelProperty("相别")
private String phase;
@ApiModelProperty("统计类型")
private String statType;
@ApiModelProperty("偶次谐波次数")
private Integer evenHarmonicOrder;
@ApiModelProperty("偶次谐波值")
private BigDecimal evenValue;
@ApiModelProperty("参与比较的奇次谐波次数")
private List<Integer> oddHarmonicOrders = new ArrayList<Integer>();
@ApiModelProperty("参与比较的奇次谐波值")
private List<BigDecimal> oddValues = new ArrayList<BigDecimal>();
@ApiModelProperty("奇次谐波中位数")
private BigDecimal oddMedianValue;
@ApiModelProperty("异常阈值倍数")
private BigDecimal thresholdMultiplier;
}

View File

@@ -0,0 +1,23 @@
package com.njcn.gather.steady.checksquare.pojo.vo;
import lombok.Data;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
/**
* 谐波奇偶关系规则结果。
*/
@Data
public class SteadyChecksquareHarmonicParityRuleVO implements Serializable {
private static final long serialVersionUID = 1L;
private Boolean abnormal = false;
private Integer abnormalPointCount = 0;
private List<SteadyChecksquareHarmonicParityDetailVO> abnormalDetails =
new ArrayList<SteadyChecksquareHarmonicParityDetailVO>();
}

View File

@@ -0,0 +1,48 @@
package com.njcn.gather.steady.checksquare.pojo.vo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
/**
* 数据校验检测项明细。
*/
@Data
@ApiModel("数据校验检测项明细")
public class SteadyChecksquareItemDetailVO implements Serializable {
private static final long serialVersionUID = 1L;
@ApiModelProperty("检测项 ID")
private String itemId;
@ApiModelProperty("明细类型")
private String detailType;
@ApiModelProperty("统计类型")
private String statType;
@ApiModelProperty("当前页码;未分页查询时为空")
private Integer pageNum;
@ApiModelProperty("每页条数;未分页查询时为空")
private Integer pageSize;
@ApiModelProperty("总记录数;未分页查询时为空")
private Long total;
@ApiModelProperty("缺失区间")
private List<SteadyChecksquareSegmentVO> segments = new ArrayList<SteadyChecksquareSegmentVO>();
@ApiModelProperty("大小关系异常明细")
private List<SteadyChecksquareValueOrderDetailVO> valueOrderDetails =
new ArrayList<SteadyChecksquareValueOrderDetailVO>();
@ApiModelProperty("谐波奇偶关系异常明细")
private List<SteadyChecksquareHarmonicParityDetailVO> harmonicParityDetails =
new ArrayList<SteadyChecksquareHarmonicParityDetailVO>();
}

View File

@@ -18,6 +18,9 @@ public class SteadyChecksquareItemVO implements Serializable {
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
@ApiModelProperty("检测项 ID")
private String itemId;
@ApiModelProperty("校验项唯一键") @ApiModelProperty("校验项唯一键")
private String itemKey; private String itemKey;
@@ -45,14 +48,30 @@ public class SteadyChecksquareItemVO implements Serializable {
@ApiModelProperty("缺失点数") @ApiModelProperty("缺失点数")
private Integer missingPointCount; private Integer missingPointCount;
@ApiModelProperty("缺失率") @ApiModelProperty("数据完整性")
private BigDecimal missingRate; private BigDecimal dataIntegrity;
@ApiModelProperty("缺失率文本") @ApiModelProperty("数据完整性文本")
private String missingRateText; private String dataIntegrityText;
@ApiModelProperty("最大连续缺失时长,单位分钟") @ApiModelProperty("指标值大小关系是否异常")
private Integer maxContinuousMissingMinutes; private Boolean abnormal;
@ApiModelProperty("指标值大小关系异常累计值")
private Integer abnormalPointCount;
@ApiModelProperty("指标值大小关系异常明细")
private List<SteadyChecksquareValueOrderDetailVO> abnormalDetails = new ArrayList<SteadyChecksquareValueOrderDetailVO>();
@ApiModelProperty("谐波奇偶关系是否异常")
private Boolean harmonicParityAbnormal;
@ApiModelProperty("谐波奇偶关系异常累计值")
private Integer harmonicParityAbnormalPointCount;
@ApiModelProperty("谐波奇偶关系异常明细")
private List<SteadyChecksquareHarmonicParityDetailVO> harmonicParityAbnormalDetails =
new ArrayList<SteadyChecksquareHarmonicParityDetailVO>();
@ApiModelProperty("统计类型摘要") @ApiModelProperty("统计类型摘要")
private List<SteadyChecksquareStatSummaryVO> statSummaries = new ArrayList<SteadyChecksquareStatSummaryVO>(); private List<SteadyChecksquareStatSummaryVO> statSummaries = new ArrayList<SteadyChecksquareStatSummaryVO>();

View File

@@ -17,6 +17,12 @@ public class SteadyChecksquareQueryVO implements Serializable {
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
@ApiModelProperty("任务 ID")
private String taskId;
@ApiModelProperty("任务编号")
private String taskNo;
@ApiModelProperty("监测点 ID") @ApiModelProperty("监测点 ID")
private String lineId; private String lineId;

View File

@@ -24,6 +24,9 @@ public class SteadyChecksquareSegmentVO implements Serializable {
@ApiModelProperty("状态NORMAL/MISSING") @ApiModelProperty("状态NORMAL/MISSING")
private String status; private String status;
@ApiModelProperty("谐波次数")
private Integer harmonicOrder;
@ApiModelProperty("缺失点数") @ApiModelProperty("缺失点数")
private Integer missingPointCount; private Integer missingPointCount;

View File

@@ -34,12 +34,9 @@ public class SteadyChecksquareStatSummaryVO implements Serializable {
@ApiModelProperty("缺失点数") @ApiModelProperty("缺失点数")
private Integer missingPointCount; private Integer missingPointCount;
@ApiModelProperty("缺失率") @ApiModelProperty("数据完整性")
private BigDecimal missingRate; private BigDecimal dataIntegrity;
@ApiModelProperty("缺失率文本") @ApiModelProperty("数据完整性文本")
private String missingRateText; private String dataIntegrityText;
@ApiModelProperty("最大连续缺失时长,单位分钟")
private Integer maxContinuousMissingMinutes;
} }

View File

@@ -0,0 +1,54 @@
package com.njcn.gather.steady.checksquare.pojo.vo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.io.Serializable;
import java.math.BigDecimal;
/**
* 数据校验历史任务。
*/
@Data
@ApiModel("数据校验历史任务")
public class SteadyChecksquareTaskVO implements Serializable {
private static final long serialVersionUID = 1L;
@ApiModelProperty("任务 ID")
private String taskId;
@ApiModelProperty("任务编号")
private String taskNo;
@ApiModelProperty("监测点 ID")
private String lineId;
@ApiModelProperty("监测点名称")
private String lineName;
@ApiModelProperty("开始时间")
private String timeStart;
@ApiModelProperty("结束时间")
private String timeEnd;
@ApiModelProperty("统计间隔,单位分钟")
private Integer intervalMinutes;
@ApiModelProperty("任务状态")
private String taskStatus;
@ApiModelProperty("检测项数量")
private Integer itemCount;
@ApiModelProperty("异常检测项数量")
private Integer abnormalItemCount;
@ApiModelProperty("最低数据完整性")
private BigDecimal minDataIntegrity;
@ApiModelProperty("创建时间")
private String createTime;
}

View File

@@ -0,0 +1,39 @@
package com.njcn.gather.steady.checksquare.pojo.vo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.io.Serializable;
import java.math.BigDecimal;
/**
* 数据校验指标值大小关系异常明细。
*/
@Data
@ApiModel("数据校验指标值大小关系异常明细")
public class SteadyChecksquareValueOrderDetailVO implements Serializable {
private static final long serialVersionUID = 1L;
@ApiModelProperty("时间")
private String time;
@ApiModelProperty("相别")
private String phase;
@ApiModelProperty("谐波次数")
private Integer harmonicOrder;
@ApiModelProperty("最大值")
private BigDecimal maxValue;
@ApiModelProperty("最小值")
private BigDecimal minValue;
@ApiModelProperty("平均值")
private BigDecimal avgValue;
@ApiModelProperty("CP95 值")
private BigDecimal cp95Value;
}

View File

@@ -0,0 +1,22 @@
package com.njcn.gather.steady.checksquare.pojo.vo;
import lombok.Data;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
/**
* 数据校验指标值大小关系规则结果。
*/
@Data
public class SteadyChecksquareValueOrderRuleVO implements Serializable {
private static final long serialVersionUID = 1L;
private Boolean abnormal = false;
private Integer abnormalPointCount = 0;
private List<SteadyChecksquareValueOrderDetailVO> abnormalDetails = new ArrayList<SteadyChecksquareValueOrderDetailVO>();
}

View File

@@ -0,0 +1,10 @@
package com.njcn.gather.steady.checksquare.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.njcn.gather.steady.checksquare.pojo.po.SteadyChecksquareDetailPO;
/**
* 数据校验明细服务。
*/
public interface SteadyChecksquareDetailService extends IService<SteadyChecksquareDetailPO> {
}

View File

@@ -0,0 +1,10 @@
package com.njcn.gather.steady.checksquare.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.njcn.gather.steady.checksquare.pojo.po.SteadyChecksquareItemPO;
/**
* 数据校验检测项服务。
*/
public interface SteadyChecksquareItemService extends IService<SteadyChecksquareItemPO> {
}

View File

@@ -0,0 +1,29 @@
package com.njcn.gather.steady.checksquare.service;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.njcn.gather.steady.checksquare.pojo.param.SteadyChecksquareHistoryQueryParam;
import com.njcn.gather.steady.checksquare.pojo.param.SteadyChecksquareQueryParam;
import com.njcn.gather.steady.checksquare.pojo.vo.SteadyChecksquareItemDetailVO;
import com.njcn.gather.steady.checksquare.pojo.vo.SteadyChecksquareQueryVO;
import com.njcn.gather.steady.checksquare.pojo.vo.SteadyChecksquareTaskVO;
import java.util.List;
/**
* 数据校验服务。
*/
public interface SteadyChecksquareService {
Page<SteadyChecksquareTaskVO> query(SteadyChecksquareHistoryQueryParam param);
SteadyChecksquareTaskVO create(SteadyChecksquareQueryParam param);
boolean delete(List<String> taskIds);
SteadyChecksquareQueryVO detail(String taskId);
SteadyChecksquareItemDetailVO itemDetail(String itemId, String detailType, String statType);
SteadyChecksquareItemDetailVO itemDetail(String itemId, String detailType, String statType,
Integer pageNum, Integer pageSize);
}

View File

@@ -0,0 +1,10 @@
package com.njcn.gather.steady.checksquare.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.njcn.gather.steady.checksquare.pojo.po.SteadyChecksquareStatSummaryPO;
/**
* 数据校验统计摘要服务。
*/
public interface SteadyChecksquareStatSummaryService extends IService<SteadyChecksquareStatSummaryPO> {
}

View File

@@ -0,0 +1,10 @@
package com.njcn.gather.steady.checksquare.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.njcn.gather.steady.checksquare.pojo.po.SteadyChecksquareTaskPO;
/**
* 数据校验任务服务。
*/
public interface SteadyChecksquareTaskService extends IService<SteadyChecksquareTaskPO> {
}

View File

@@ -0,0 +1,15 @@
package com.njcn.gather.steady.checksquare.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.njcn.gather.steady.checksquare.mapper.SteadyChecksquareDetailMapper;
import com.njcn.gather.steady.checksquare.pojo.po.SteadyChecksquareDetailPO;
import com.njcn.gather.steady.checksquare.service.SteadyChecksquareDetailService;
import org.springframework.stereotype.Service;
/**
* 数据校验明细服务实现。
*/
@Service
public class SteadyChecksquareDetailServiceImpl extends ServiceImpl<SteadyChecksquareDetailMapper, SteadyChecksquareDetailPO>
implements SteadyChecksquareDetailService {
}

View File

@@ -0,0 +1,15 @@
package com.njcn.gather.steady.checksquare.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.njcn.gather.steady.checksquare.mapper.SteadyChecksquareItemMapper;
import com.njcn.gather.steady.checksquare.pojo.po.SteadyChecksquareItemPO;
import com.njcn.gather.steady.checksquare.service.SteadyChecksquareItemService;
import org.springframework.stereotype.Service;
/**
* 数据校验检测项服务实现。
*/
@Service
public class SteadyChecksquareItemServiceImpl extends ServiceImpl<SteadyChecksquareItemMapper, SteadyChecksquareItemPO>
implements SteadyChecksquareItemService {
}

View File

@@ -0,0 +1,16 @@
package com.njcn.gather.steady.checksquare.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.njcn.gather.steady.checksquare.mapper.SteadyChecksquareStatSummaryMapper;
import com.njcn.gather.steady.checksquare.pojo.po.SteadyChecksquareStatSummaryPO;
import com.njcn.gather.steady.checksquare.service.SteadyChecksquareStatSummaryService;
import org.springframework.stereotype.Service;
/**
* 数据校验统计摘要服务实现。
*/
@Service
public class SteadyChecksquareStatSummaryServiceImpl
extends ServiceImpl<SteadyChecksquareStatSummaryMapper, SteadyChecksquareStatSummaryPO>
implements SteadyChecksquareStatSummaryService {
}

View File

@@ -0,0 +1,15 @@
package com.njcn.gather.steady.checksquare.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.njcn.gather.steady.checksquare.mapper.SteadyChecksquareTaskMapper;
import com.njcn.gather.steady.checksquare.pojo.po.SteadyChecksquareTaskPO;
import com.njcn.gather.steady.checksquare.service.SteadyChecksquareTaskService;
import org.springframework.stereotype.Service;
/**
* 数据校验任务服务实现。
*/
@Service
public class SteadyChecksquareTaskServiceImpl extends ServiceImpl<SteadyChecksquareTaskMapper, SteadyChecksquareTaskPO>
implements SteadyChecksquareTaskService {
}

View File

@@ -0,0 +1,24 @@
package com.njcn.gather.steady.checksquare.util;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.UUID;
/**
* 数据校验编号工具。
*/
public final class SteadyChecksquareIdUtil {
private static final DateTimeFormatter TASK_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMddHHmmssSSS");
private SteadyChecksquareIdUtil() {
}
public static String uuid() {
return UUID.randomUUID().toString().replace("-", "");
}
public static String taskNo() {
return "CS" + LocalDateTime.now().format(TASK_FORMATTER);
}
}

View File

@@ -0,0 +1,102 @@
CREATE TABLE IF NOT EXISTS `steady_checksquare_task` (
`id` VARCHAR(64) NOT NULL COMMENT '主键',
`task_no` VARCHAR(64) NOT NULL COMMENT '检测任务编号',
`line_id` VARCHAR(64) NOT NULL COMMENT '监测点ID',
`line_name` VARCHAR(255) NULL COMMENT '监测点名称',
`time_start` DATETIME NOT NULL COMMENT '检测开始时间',
`time_end` DATETIME NOT NULL COMMENT '检测结束时间',
`interval_minutes` INT NULL COMMENT '默认统计间隔,单位分钟',
`indicator_codes_json` JSON NULL COMMENT '请求指标编码列表',
`indicator_codes_text` VARCHAR(2000) NULL COMMENT '请求指标编码检索文本,格式 |code1|code2|',
`task_status` VARCHAR(32) NOT NULL DEFAULT 'SUCCESS' COMMENT '任务状态RUNNING/SUCCESS/FAIL',
`item_count` INT NOT NULL DEFAULT 0 COMMENT '检测项数量',
`abnormal_item_count` INT NOT NULL DEFAULT 0 COMMENT '异常检测项数量',
`min_data_integrity` DECIMAL(12,6) NOT NULL DEFAULT 0.000000 COMMENT '最低数据完整性',
`result_message` VARCHAR(2000) NULL COMMENT '执行结果说明',
`state` TINYINT NOT NULL DEFAULT 1 COMMENT '状态0-删除1-正常',
`create_by` VARCHAR(64) NULL COMMENT '创建人',
`create_time` DATETIME NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_by` VARCHAR(64) NULL COMMENT '更新人',
`update_time` DATETIME NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_steady_checksquare_task_no` (`task_no`),
KEY `idx_steady_checksquare_task_line_time` (`line_id`, `time_start`, `time_end`),
KEY `idx_steady_checksquare_task_status` (`task_status`),
KEY `idx_steady_checksquare_task_indicator_text` (`indicator_codes_text`(255)),
KEY `idx_steady_checksquare_task_create_time` (`create_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='稳态数据校验任务表';
CREATE TABLE IF NOT EXISTS `steady_checksquare_item` (
`id` VARCHAR(64) NOT NULL COMMENT '主键',
`task_id` VARCHAR(64) NOT NULL COMMENT '检测任务ID',
`item_key` VARCHAR(255) NOT NULL COMMENT '检测项唯一键',
`indicator_code` VARCHAR(64) NOT NULL COMMENT '指标编码',
`indicator_name` VARCHAR(255) NULL COMMENT '指标名称',
`harmonic_order` INT NULL COMMENT '谐波次数;聚合项为空',
`interval_minutes` INT NULL COMMENT '当前检测项统计间隔,单位分钟',
`has_data` TINYINT NOT NULL DEFAULT 0 COMMENT '是否存在任意数据0-否1-是',
`expected_point_count` INT NOT NULL DEFAULT 0 COMMENT '期望点数',
`actual_point_count` INT NOT NULL DEFAULT 0 COMMENT '实际点数',
`missing_point_count` INT NOT NULL DEFAULT 0 COMMENT '缺失点数',
`data_integrity` DECIMAL(12,6) NOT NULL DEFAULT 0.000000 COMMENT '数据完整性',
`data_integrity_text` VARCHAR(32) NULL COMMENT '数据完整性文本',
`abnormal` TINYINT NOT NULL DEFAULT 0 COMMENT '指标值大小关系是否异常',
`abnormal_point_count` INT NOT NULL DEFAULT 0 COMMENT '大小关系异常点数',
`harmonic_parity_abnormal` TINYINT NOT NULL DEFAULT 0 COMMENT '谐波奇偶关系是否异常',
`harmonic_parity_abnormal_point_count` INT NOT NULL DEFAULT 0 COMMENT '谐波奇偶关系异常点数',
`state` TINYINT NOT NULL DEFAULT 1 COMMENT '状态0-删除1-正常',
`create_time` DATETIME NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` DATETIME NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_steady_checksquare_item` (`task_id`, `item_key`),
KEY `idx_steady_checksquare_item_indicator` (`indicator_code`),
KEY `idx_steady_checksquare_item_abnormal` (`abnormal`, `harmonic_parity_abnormal`),
KEY `idx_steady_checksquare_item_data_integrity` (`data_integrity`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='稳态数据校验检测项表';
CREATE TABLE IF NOT EXISTS `steady_checksquare_stat_summary` (
`id` VARCHAR(64) NOT NULL COMMENT '主键',
`item_id` VARCHAR(64) NOT NULL COMMENT '检测项ID',
`stat_type` VARCHAR(16) NOT NULL COMMENT '统计类型AVG/MAX/MIN/CP95',
`supported` TINYINT NOT NULL DEFAULT 1 COMMENT '是否支持',
`has_data` TINYINT NOT NULL DEFAULT 0 COMMENT '是否存在数据',
`expected_point_count` INT NOT NULL DEFAULT 0 COMMENT '期望点数',
`actual_point_count` INT NOT NULL DEFAULT 0 COMMENT '实际点数',
`missing_point_count` INT NOT NULL DEFAULT 0 COMMENT '缺失点数',
`data_integrity` DECIMAL(12,6) NOT NULL DEFAULT 0.000000 COMMENT '数据完整性',
`data_integrity_text` VARCHAR(32) NULL COMMENT '数据完整性文本',
`create_time` DATETIME NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_steady_checksquare_stat` (`item_id`, `stat_type`),
KEY `idx_steady_checksquare_stat_type` (`stat_type`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='稳态数据校验统计摘要表';
CREATE TABLE IF NOT EXISTS `steady_checksquare_detail` (
`id` VARCHAR(64) NOT NULL COMMENT '主键',
`item_id` VARCHAR(64) NOT NULL COMMENT '检测项ID',
`detail_type` VARCHAR(32) NOT NULL COMMENT '明细类型SEGMENT/VALUE_ORDER/HARMONIC_PARITY',
`stat_type` VARCHAR(16) NULL COMMENT '统计类型',
`start_time` DATETIME NULL COMMENT '区间开始时间',
`end_time` DATETIME NULL COMMENT '区间结束时间',
`point_time` DATETIME NULL COMMENT '异常点时间',
`segment_status` VARCHAR(16) NULL COMMENT '区间状态NORMAL/MISSING',
`missing_point_count` INT NULL COMMENT '缺失点数',
`duration_minutes` INT NULL COMMENT '持续时长,单位分钟',
`phase` VARCHAR(16) NULL COMMENT '相别',
`harmonic_order` INT NULL COMMENT '谐波次数',
`max_value` DECIMAL(24,8) NULL COMMENT '最大值',
`min_value` DECIMAL(24,8) NULL COMMENT '最小值',
`avg_value` DECIMAL(24,8) NULL COMMENT '平均值',
`cp95_value` DECIMAL(24,8) NULL COMMENT 'CP95值',
`even_harmonic_order` INT NULL COMMENT '偶次谐波次数',
`even_value` DECIMAL(24,8) NULL COMMENT '偶次谐波值',
`odd_harmonic_orders_json` JSON NULL COMMENT '参与比较的奇次谐波次数',
`odd_values_json` JSON NULL COMMENT '参与比较的奇次谐波值',
`odd_median_value` DECIMAL(24,8) NULL COMMENT '奇次谐波中位数',
`threshold_multiplier` DECIMAL(12,6) NULL COMMENT '异常阈值倍数',
`create_time` DATETIME NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (`id`),
KEY `idx_steady_checksquare_detail_item_type` (`item_id`, `detail_type`),
KEY `idx_steady_checksquare_detail_point_time` (`point_time`),
KEY `idx_steady_checksquare_detail_segment` (`start_time`, `end_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='稳态数据校验明细表';

View File

@@ -0,0 +1,14 @@
ALTER TABLE `steady_checksquare_task`
CHANGE COLUMN `max_missing_rate` `min_data_integrity` DECIMAL(12,6) NOT NULL DEFAULT 0.000000 COMMENT '最低数据完整性';
ALTER TABLE `steady_checksquare_item`
DROP INDEX `idx_steady_checksquare_item_missing_rate`,
CHANGE COLUMN `missing_rate` `data_integrity` DECIMAL(12,6) NOT NULL DEFAULT 0.000000 COMMENT '数据完整性',
CHANGE COLUMN `missing_rate_text` `data_integrity_text` VARCHAR(32) NULL COMMENT '数据完整性文本',
DROP COLUMN `max_continuous_missing_minutes`,
ADD KEY `idx_steady_checksquare_item_data_integrity` (`data_integrity`);
ALTER TABLE `steady_checksquare_stat_summary`
CHANGE COLUMN `missing_rate` `data_integrity` DECIMAL(12,6) NOT NULL DEFAULT 0.000000 COMMENT '数据完整性',
CHANGE COLUMN `missing_rate_text` `data_integrity_text` VARCHAR(32) NULL COMMENT '数据完整性文本',
DROP COLUMN `max_continuous_missing_minutes`;

View File

@@ -0,0 +1,147 @@
package com.njcn.gather.steady.checksquare.component;
import com.njcn.gather.steady.checksquare.pojo.bo.SteadyChecksquareValuePointBO;
import com.njcn.gather.steady.checksquare.pojo.vo.SteadyChecksquareHarmonicParityDetailVO;
import com.njcn.gather.steady.checksquare.pojo.vo.SteadyChecksquareHarmonicParityRuleVO;
import com.njcn.gather.steady.datavie.component.SteadyTrendIndicatorCatalog;
import com.njcn.gather.steady.datavie.pojo.bo.SteadyTrendIndicatorDefinitionBO;
import com.njcn.gather.steady.datavie.pojo.bo.SteadyTrendResolvedFieldBO;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
/**
* 谐波奇偶关系规则测试。
*/
class SteadyChecksquareHarmonicParityRuleComponentTest {
@Test
void shouldRecordAbnormalWhenEvenHarmonicExceedsOddMedianThreshold() {
SteadyChecksquareInfluxQueryComponent influxQueryComponent = mock(SteadyChecksquareInfluxQueryComponent.class);
SteadyChecksquareHarmonicParityRuleComponent component = new SteadyChecksquareHarmonicParityRuleComponent(influxQueryComponent);
LocalDateTime time = LocalDateTime.of(2026, 5, 1, 0, 0);
when(influxQueryComponent.queryValuePointMap(any(),
any(LocalDateTime.class), any(LocalDateTime.class), anyInt()))
.thenAnswer(invocation -> {
Map<String, List<SteadyChecksquareValuePointBO>> values = emptyBatchResult(invocation.getArgument(0));
putPoint(values, "v_3", time, "10");
putPoint(values, "v_4", time, "31");
putPoint(values, "v_5", time, "12");
putPoint(values, "v_7", time, "14");
return values;
});
SteadyTrendIndicatorDefinitionBO indicator = new SteadyTrendIndicatorCatalog().getIndicator("V_HARMONIC");
SteadyChecksquareHarmonicParityRuleVO result = component.check("line-001", indicator,
time, time, 1);
Assertions.assertEquals(Boolean.TRUE, result.getAbnormal());
Assertions.assertEquals(Integer.valueOf(1), result.getAbnormalPointCount());
SteadyChecksquareHarmonicParityDetailVO detail = result.getAbnormalDetails().get(0);
Assertions.assertEquals("2026-05-01 00:00:00", detail.getTime());
Assertions.assertEquals("A", detail.getPhase());
Assertions.assertEquals("AVG", detail.getStatType());
Assertions.assertEquals(Integer.valueOf(4), detail.getEvenHarmonicOrder());
Assertions.assertEquals(new BigDecimal("31"), detail.getEvenValue());
Assertions.assertEquals(Arrays.asList(3, 5, 7), detail.getOddHarmonicOrders());
Assertions.assertEquals(new BigDecimal("12"), detail.getOddMedianValue());
Assertions.assertEquals(new BigDecimal("2"), detail.getThresholdMultiplier());
}
@Test
void shouldSkipWhenOddReferenceCountLessThanTwo() {
SteadyChecksquareInfluxQueryComponent influxQueryComponent = mock(SteadyChecksquareInfluxQueryComponent.class);
SteadyChecksquareHarmonicParityRuleComponent component = new SteadyChecksquareHarmonicParityRuleComponent(influxQueryComponent);
LocalDateTime time = LocalDateTime.of(2026, 5, 1, 0, 0);
when(influxQueryComponent.queryValuePointMap(any(),
any(LocalDateTime.class), any(LocalDateTime.class), anyInt()))
.thenAnswer(invocation -> {
Map<String, List<SteadyChecksquareValuePointBO>> values = emptyBatchResult(invocation.getArgument(0));
putPoint(values, "v_2", time, "50");
putPoint(values, "v_3", time, "10");
return values;
});
SteadyTrendIndicatorDefinitionBO indicator = new SteadyTrendIndicatorCatalog().getIndicator("V_HARMONIC");
SteadyChecksquareHarmonicParityRuleVO result = component.check("line-001", indicator,
time, time, 1);
Assertions.assertEquals(Boolean.FALSE, result.getAbnormal());
Assertions.assertEquals(Integer.valueOf(0), result.getAbnormalPointCount());
Assertions.assertTrue(result.getAbnormalDetails().isEmpty());
}
@Test
void shouldSkipEvenHarmonicWhenValueNotGreaterThanDeadband() {
SteadyChecksquareInfluxQueryComponent influxQueryComponent = mock(SteadyChecksquareInfluxQueryComponent.class);
SteadyChecksquareHarmonicParityRuleComponent component = new SteadyChecksquareHarmonicParityRuleComponent(influxQueryComponent);
LocalDateTime time = LocalDateTime.of(2026, 5, 1, 0, 0);
when(influxQueryComponent.queryValuePointMap(any(),
any(LocalDateTime.class), any(LocalDateTime.class), anyInt()))
.thenAnswer(invocation -> {
Map<String, List<SteadyChecksquareValuePointBO>> values = emptyBatchResult(invocation.getArgument(0));
putPoint(values, "v_3", time, "0.01");
putPoint(values, "v_4", time, "0.10");
putPoint(values, "v_5", time, "0.02");
return values;
});
SteadyTrendIndicatorDefinitionBO indicator = new SteadyTrendIndicatorCatalog().getIndicator("V_HARMONIC");
SteadyChecksquareHarmonicParityRuleVO result = component.check("line-001", indicator,
time, time, 1);
Assertions.assertEquals(Boolean.FALSE, result.getAbnormal());
Assertions.assertEquals(Integer.valueOf(0), result.getAbnormalPointCount());
Assertions.assertTrue(result.getAbnormalDetails().isEmpty());
}
@Test
void shouldSkipNonHarmonicIndicator() {
SteadyChecksquareInfluxQueryComponent influxQueryComponent = mock(SteadyChecksquareInfluxQueryComponent.class);
SteadyChecksquareHarmonicParityRuleComponent component = new SteadyChecksquareHarmonicParityRuleComponent(influxQueryComponent);
SteadyTrendIndicatorDefinitionBO indicator = new SteadyTrendIndicatorCatalog().getIndicator("V_RMS");
SteadyChecksquareHarmonicParityRuleVO result = component.check("line-001", indicator,
LocalDateTime.of(2026, 5, 1, 0, 0),
LocalDateTime.of(2026, 5, 1, 0, 1), 1);
Assertions.assertEquals(Boolean.FALSE, result.getAbnormal());
Assertions.assertEquals(Integer.valueOf(0), result.getAbnormalPointCount());
Assertions.assertTrue(result.getAbnormalDetails().isEmpty());
}
private SteadyChecksquareValuePointBO point(LocalDateTime time, String value) {
SteadyChecksquareValuePointBO point = new SteadyChecksquareValuePointBO();
point.setTime(time);
point.setValue(new BigDecimal(value));
return point;
}
private Map<String, List<SteadyChecksquareValuePointBO>> emptyBatchResult(List<SteadyTrendResolvedFieldBO> fields) {
Map<String, List<SteadyChecksquareValuePointBO>> result =
new LinkedHashMap<String, List<SteadyChecksquareValuePointBO>>();
for (SteadyTrendResolvedFieldBO field : fields) {
result.put(field.getField(), Collections.<SteadyChecksquareValuePointBO>emptyList());
}
return result;
}
private void putPoint(Map<String, List<SteadyChecksquareValuePointBO>> values, String field,
LocalDateTime time, String value) {
if (values.containsKey(field)) {
values.put(field, Collections.singletonList(point(time, value)));
}
}
}

View File

@@ -0,0 +1,186 @@
package com.njcn.gather.steady.checksquare.component;
import com.njcn.gather.steady.datavie.config.SteadyInfluxDbProperties;
import com.njcn.gather.steady.datavie.pojo.bo.SteadyTrendResolvedFieldBO;
import com.sun.net.httpserver.HttpServer;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import java.net.InetSocketAddress;
import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
/**
* 数据校验 InfluxQL 构造契约测试。
*/
class SteadyChecksquareInfluxQueryComponentTest {
@Test
void shouldBuildChecksquareQueryWithoutQualityFlag() {
SteadyChecksquareInfluxQueryComponent component = new SteadyChecksquareInfluxQueryComponent(new SteadyInfluxDbProperties());
SteadyTrendResolvedFieldBO field = new SteadyTrendResolvedFieldBO();
field.setMeasurement("data_v");
field.setField("rms");
field.setLineId("line-001");
field.setPhase("A");
field.setStatType("AVG");
String query = component.buildChecksquareQuery(field,
LocalDateTime.of(2026, 5, 1, 0, 0, 0),
LocalDateTime.of(2026, 5, 1, 23, 59, 59));
Assertions.assertTrue(query.contains("SELECT \"rms\" AS \"value\""));
Assertions.assertTrue(query.contains("\"line_id\" = 'line-001'"));
Assertions.assertTrue(query.contains("\"phasic_type\" = 'A'"));
Assertions.assertTrue(query.contains("\"value_type\" = 'AVG'"));
Assertions.assertFalse(query.contains("quality_flag"));
Assertions.assertFalse(query.contains("GROUP BY time"));
}
@Test
void shouldBuildValuePointQueryWithStatTypeFilter() {
SteadyChecksquareInfluxQueryComponent component = new SteadyChecksquareInfluxQueryComponent(new SteadyInfluxDbProperties());
SteadyTrendResolvedFieldBO field = new SteadyTrendResolvedFieldBO();
field.setMeasurement("data_v");
field.setField("rms");
field.setLineId("line-001");
field.setPhase("A");
field.setStatType("CP95");
String query = component.buildValuePointQuery(field,
LocalDateTime.of(2026, 5, 1, 0, 0, 0),
LocalDateTime.of(2026, 5, 1, 23, 59, 59));
Assertions.assertTrue(query.contains("SELECT \"rms\" AS \"value\""));
Assertions.assertTrue(query.contains("\"line_id\" = 'line-001'"));
Assertions.assertTrue(query.contains("\"phasic_type\" = 'A'"));
Assertions.assertTrue(query.contains("\"value_type\" = 'CP95'"));
Assertions.assertTrue(query.endsWith("ORDER BY time ASC"));
}
@Test
void shouldReuseValuePointQueryWithinRequestCache() throws Exception {
AtomicInteger requestCount = new AtomicInteger();
HttpServer server = HttpServer.create(new InetSocketAddress(0), 0);
server.createContext("/query", exchange -> {
requestCount.incrementAndGet();
byte[] body = ("{\"results\":[{\"series\":[{\"values\":["
+ "[\"2026-05-01T00:00:00Z\",1.23],"
+ "[\"2026-05-01T00:01:00Z\",2.34]"
+ "]}]}]}").getBytes(StandardCharsets.UTF_8);
exchange.sendResponseHeaders(200, body.length);
exchange.getResponseBody().write(body);
exchange.close();
});
server.start();
try {
SteadyInfluxDbProperties properties = new SteadyInfluxDbProperties();
properties.setUrl("http://127.0.0.1:" + server.getAddress().getPort());
properties.setDatabase("steady");
SteadyChecksquareInfluxQueryComponent component = new SteadyChecksquareInfluxQueryComponent(properties);
SteadyTrendResolvedFieldBO field = new SteadyTrendResolvedFieldBO();
field.setMeasurement("data_v");
field.setField("rms");
field.setLineId("line-001");
field.setPhase("A");
field.setStatType("AVG");
component.enableRequestCache();
component.queryExistingSlots(field,
LocalDateTime.of(2026, 5, 1, 0, 0, 0),
LocalDateTime.of(2026, 5, 1, 0, 1, 0), 1);
component.queryValuePoints(field,
LocalDateTime.of(2026, 5, 1, 0, 0, 0),
LocalDateTime.of(2026, 5, 1, 0, 1, 0), 1);
component.clearRequestCache();
Assertions.assertEquals(1, requestCount.get());
} finally {
server.stop(0);
}
}
@Test
void shouldQueryMultipleValueFieldsOnce() throws Exception {
AtomicInteger requestCount = new AtomicInteger();
HttpServer server = HttpServer.create(new InetSocketAddress(0), 0);
server.createContext("/query", exchange -> {
requestCount.incrementAndGet();
byte[] body = ("{\"results\":[{\"series\":[{\"columns\":[\"time\",\"h_2\",\"h_3\"],\"values\":["
+ "[\"2026-05-01T00:00:00Z\",1.23,2.34],"
+ "[\"2026-05-01T00:01:00Z\",3.45,null]"
+ "]}]}]}").getBytes(StandardCharsets.UTF_8);
exchange.sendResponseHeaders(200, body.length);
exchange.getResponseBody().write(body);
exchange.close();
});
server.start();
try {
SteadyInfluxDbProperties properties = new SteadyInfluxDbProperties();
properties.setUrl("http://127.0.0.1:" + server.getAddress().getPort());
properties.setDatabase("steady");
SteadyChecksquareInfluxQueryComponent component = new SteadyChecksquareInfluxQueryComponent(properties);
SteadyTrendResolvedFieldBO h2 = buildField("h_2");
SteadyTrendResolvedFieldBO h3 = buildField("h_3");
component.enableRequestCache();
Map<String, java.util.List<com.njcn.gather.steady.checksquare.pojo.bo.SteadyChecksquareValuePointBO>> result =
component.queryValuePointMap(Arrays.asList(h2, h3),
LocalDateTime.of(2026, 5, 1, 0, 0, 0),
LocalDateTime.of(2026, 5, 1, 0, 1, 0), 1);
component.clearRequestCache();
Assertions.assertEquals(1, requestCount.get());
Assertions.assertEquals(2, result.get("h_2").size());
Assertions.assertEquals(1, result.get("h_3").size());
} finally {
server.stop(0);
}
}
@Test
void shouldSplitLongValuePointQueryByDay() throws Exception {
AtomicInteger requestCount = new AtomicInteger();
HttpServer server = HttpServer.create(new InetSocketAddress(0), 0);
server.createContext("/query", exchange -> {
int index = requestCount.incrementAndGet();
byte[] body = ("{\"results\":[{\"series\":[{\"values\":["
+ "[\"2026-05-0" + index + "T00:00:00Z\"," + index + "]"
+ "]}]}]}").getBytes(StandardCharsets.UTF_8);
exchange.sendResponseHeaders(200, body.length);
exchange.getResponseBody().write(body);
exchange.close();
});
server.start();
try {
SteadyInfluxDbProperties properties = new SteadyInfluxDbProperties();
properties.setUrl("http://127.0.0.1:" + server.getAddress().getPort());
properties.setDatabase("steady");
SteadyChecksquareInfluxQueryComponent component = new SteadyChecksquareInfluxQueryComponent(properties);
List<com.njcn.gather.steady.checksquare.pojo.bo.SteadyChecksquareValuePointBO> result =
component.queryValuePoints(buildField("h_2"),
LocalDateTime.of(2026, 5, 1, 0, 0, 0),
LocalDateTime.of(2026, 5, 3, 0, 0, 0), 1);
Assertions.assertEquals(3, requestCount.get());
Assertions.assertEquals(3, result.size());
} finally {
server.stop(0);
}
}
private SteadyTrendResolvedFieldBO buildField(String fieldName) {
SteadyTrendResolvedFieldBO field = new SteadyTrendResolvedFieldBO();
field.setMeasurement("data_harmonic");
field.setField(fieldName);
field.setLineId("line-001");
field.setPhase("A");
field.setStatType("AVG");
return field;
}
}

View File

@@ -0,0 +1,219 @@
package com.njcn.gather.steady.checksquare.component;
import com.njcn.gather.steady.checksquare.pojo.bo.SteadyChecksquareValuePointBO;
import com.njcn.gather.steady.checksquare.pojo.vo.SteadyChecksquareValueOrderRuleVO;
import com.njcn.gather.steady.datavie.pojo.bo.SteadyTrendIndicatorDefinitionBO;
import com.njcn.gather.steady.datavie.pojo.bo.SteadyTrendSeriesFieldBO;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.Collections;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
/**
* 数据校验指标值大小关系规则测试。
*/
class SteadyChecksquareValueOrderRuleComponentTest {
@Test
void shouldMarkIndicatorAbnormalWhenInvalidPointCountGreaterThanOne() {
SteadyChecksquareInfluxQueryComponent influxQueryComponent = mock(SteadyChecksquareInfluxQueryComponent.class);
SteadyChecksquareValueOrderRuleComponent component = new SteadyChecksquareValueOrderRuleComponent(influxQueryComponent);
LocalDateTime firstTime = LocalDateTime.of(2026, 5, 1, 0, 0);
LocalDateTime secondTime = LocalDateTime.of(2026, 5, 1, 0, 1);
when(influxQueryComponent.queryValuePoints(any(), any(LocalDateTime.class), any(LocalDateTime.class), eq(1)))
.thenAnswer(invocation -> {
String statType = invocation.getArgument(0, com.njcn.gather.steady.datavie.pojo.bo.SteadyTrendResolvedFieldBO.class).getStatType();
if ("MAX".equals(statType)) {
return Arrays.asList(point(firstTime, "8"), point(secondTime, "9"));
}
if ("CP95".equals(statType)) {
return Arrays.asList(point(firstTime, "9"), point(secondTime, "10"));
}
if ("AVG".equals(statType)) {
return Arrays.asList(point(firstTime, "7"), point(secondTime, "8"));
}
if ("MIN".equals(statType)) {
return Arrays.asList(point(firstTime, "1"), point(secondTime, "9"));
}
return Collections.emptyList();
});
SteadyChecksquareValueOrderRuleVO result = component.check("line-001", indicator(), null,
LocalDateTime.of(2026, 5, 1, 0, 0), LocalDateTime.of(2026, 5, 1, 0, 2), 1);
Assertions.assertEquals(Integer.valueOf(2), result.getAbnormalPointCount());
Assertions.assertEquals(Boolean.TRUE, result.getAbnormal());
Assertions.assertEquals(2, result.getAbnormalDetails().size());
Assertions.assertEquals("2026-05-01 00:00:00", result.getAbnormalDetails().get(0).getTime());
Assertions.assertEquals("A", result.getAbnormalDetails().get(0).getPhase());
Assertions.assertEquals(new BigDecimal("8"), result.getAbnormalDetails().get(0).getMaxValue());
Assertions.assertEquals(new BigDecimal("1"), result.getAbnormalDetails().get(0).getMinValue());
Assertions.assertEquals(new BigDecimal("7"), result.getAbnormalDetails().get(0).getAvgValue());
Assertions.assertEquals(new BigDecimal("9"), result.getAbnormalDetails().get(0).getCp95Value());
}
@Test
void shouldTreatEqualAdjacentStatValuesAsNormal() {
SteadyChecksquareInfluxQueryComponent influxQueryComponent = mock(SteadyChecksquareInfluxQueryComponent.class);
SteadyChecksquareValueOrderRuleComponent component = new SteadyChecksquareValueOrderRuleComponent(influxQueryComponent);
LocalDateTime time = LocalDateTime.of(2026, 5, 1, 0, 0);
when(influxQueryComponent.queryValuePoints(any(), any(LocalDateTime.class), any(LocalDateTime.class), eq(1)))
.thenAnswer(invocation -> {
String statType = invocation.getArgument(0, com.njcn.gather.steady.datavie.pojo.bo.SteadyTrendResolvedFieldBO.class).getStatType();
if ("MAX".equals(statType)) {
return Collections.singletonList(point(time, "10"));
}
if ("CP95".equals(statType)) {
return Collections.singletonList(point(time, "10"));
}
if ("AVG".equals(statType)) {
return Collections.singletonList(point(time, "8"));
}
if ("MIN".equals(statType)) {
return Collections.singletonList(point(time, "8"));
}
return Collections.emptyList();
});
SteadyChecksquareValueOrderRuleVO result = component.check("line-001", indicator(), null,
LocalDateTime.of(2026, 5, 1, 0, 0), LocalDateTime.of(2026, 5, 1, 0, 1), 1);
Assertions.assertEquals(Integer.valueOf(0), result.getAbnormalPointCount());
Assertions.assertEquals(Boolean.FALSE, result.getAbnormal());
Assertions.assertTrue(result.getAbnormalDetails().isEmpty());
}
@Test
void shouldNotMarkIndicatorAbnormalWhenOnlyOneInvalidPointExists() {
SteadyChecksquareInfluxQueryComponent influxQueryComponent = mock(SteadyChecksquareInfluxQueryComponent.class);
SteadyChecksquareValueOrderRuleComponent component = new SteadyChecksquareValueOrderRuleComponent(influxQueryComponent);
LocalDateTime time = LocalDateTime.of(2026, 5, 1, 0, 0);
when(influxQueryComponent.queryValuePoints(any(), any(LocalDateTime.class), any(LocalDateTime.class), eq(1)))
.thenAnswer(invocation -> {
String statType = invocation.getArgument(0, com.njcn.gather.steady.datavie.pojo.bo.SteadyTrendResolvedFieldBO.class).getStatType();
if ("MAX".equals(statType)) {
return Collections.singletonList(point(time, "8"));
}
if ("CP95".equals(statType)) {
return Collections.singletonList(point(time, "10"));
}
if ("AVG".equals(statType)) {
return Collections.singletonList(point(time, "8"));
}
if ("MIN".equals(statType)) {
return Collections.singletonList(point(time, "1"));
}
return Collections.emptyList();
});
SteadyChecksquareValueOrderRuleVO result = component.check("line-001", indicator(), null,
LocalDateTime.of(2026, 5, 1, 0, 0), LocalDateTime.of(2026, 5, 1, 0, 1), 1);
Assertions.assertEquals(Integer.valueOf(1), result.getAbnormalPointCount());
Assertions.assertEquals(Boolean.FALSE, result.getAbnormal());
Assertions.assertEquals(1, result.getAbnormalDetails().size());
}
@Test
void shouldFillHarmonicOrderInAbnormalDetailForHarmonicIndicator() {
SteadyChecksquareInfluxQueryComponent influxQueryComponent = mock(SteadyChecksquareInfluxQueryComponent.class);
SteadyChecksquareValueOrderRuleComponent component = new SteadyChecksquareValueOrderRuleComponent(influxQueryComponent);
LocalDateTime time = LocalDateTime.of(2026, 5, 1, 0, 0);
when(influxQueryComponent.queryValuePoints(any(), any(LocalDateTime.class), any(LocalDateTime.class), eq(1)))
.thenAnswer(invocation -> {
String statType = invocation.getArgument(0, com.njcn.gather.steady.datavie.pojo.bo.SteadyTrendResolvedFieldBO.class).getStatType();
if ("MAX".equals(statType)) {
return Collections.singletonList(point(time, "8"));
}
if ("CP95".equals(statType)) {
return Collections.singletonList(point(time, "10"));
}
if ("AVG".equals(statType)) {
return Collections.singletonList(point(time, "8"));
}
if ("MIN".equals(statType)) {
return Collections.singletonList(point(time, "1"));
}
return Collections.emptyList();
});
SteadyTrendIndicatorDefinitionBO indicator = indicator();
indicator.setHarmonic(true);
indicator.setHarmonicFieldPrefix("v");
SteadyChecksquareValueOrderRuleVO result = component.check("line-001", indicator, 5,
LocalDateTime.of(2026, 5, 1, 0, 0), LocalDateTime.of(2026, 5, 1, 0, 1), 1);
Assertions.assertEquals(1, result.getAbnormalDetails().size());
Assertions.assertEquals(Integer.valueOf(5), result.getAbnormalDetails().get(0).getHarmonicOrder());
}
@Test
void shouldSkipPointWhenAnyRequiredStatValueMissing() {
SteadyChecksquareInfluxQueryComponent influxQueryComponent = mock(SteadyChecksquareInfluxQueryComponent.class);
SteadyChecksquareValueOrderRuleComponent component = new SteadyChecksquareValueOrderRuleComponent(influxQueryComponent);
LocalDateTime time = LocalDateTime.of(2026, 5, 1, 0, 0);
when(influxQueryComponent.queryValuePoints(any(), any(LocalDateTime.class), any(LocalDateTime.class), eq(1)))
.thenAnswer(invocation -> {
String statType = invocation.getArgument(0, com.njcn.gather.steady.datavie.pojo.bo.SteadyTrendResolvedFieldBO.class).getStatType();
if ("MAX".equals(statType)) {
return Collections.singletonList(point(time, "10"));
}
if ("CP95".equals(statType)) {
return Collections.singletonList(point(time, "11"));
}
if ("MIN".equals(statType)) {
return Collections.singletonList(point(time, "1"));
}
return Collections.emptyList();
});
SteadyChecksquareValueOrderRuleVO result = component.check("line-001", indicator(), null,
LocalDateTime.of(2026, 5, 1, 0, 0), LocalDateTime.of(2026, 5, 1, 0, 1), 1);
Assertions.assertEquals(Integer.valueOf(0), result.getAbnormalPointCount());
Assertions.assertEquals(Boolean.FALSE, result.getAbnormal());
Assertions.assertTrue(result.getAbnormalDetails().isEmpty());
}
@Test
void shouldSkipIndicatorWhenNotAllFourStatsSupported() {
SteadyChecksquareInfluxQueryComponent influxQueryComponent = mock(SteadyChecksquareInfluxQueryComponent.class);
SteadyChecksquareValueOrderRuleComponent component = new SteadyChecksquareValueOrderRuleComponent(influxQueryComponent);
SteadyTrendIndicatorDefinitionBO indicator = indicator();
indicator.setSupportStats(Collections.singletonList("AVG"));
SteadyChecksquareValueOrderRuleVO result = component.check("line-001", indicator, null,
LocalDateTime.of(2026, 5, 1, 0, 0), LocalDateTime.of(2026, 5, 1, 0, 1), 1);
Assertions.assertEquals(Integer.valueOf(0), result.getAbnormalPointCount());
Assertions.assertEquals(Boolean.FALSE, result.getAbnormal());
Assertions.assertTrue(result.getAbnormalDetails().isEmpty());
}
private SteadyTrendIndicatorDefinitionBO indicator() {
SteadyTrendIndicatorDefinitionBO indicator = new SteadyTrendIndicatorDefinitionBO();
indicator.setIndicatorCode("V_RMS");
indicator.setName("相电压有效值");
indicator.setTableName("data_v");
indicator.setPhaseCodes(Collections.singletonList("A"));
indicator.setSeriesFields(Collections.singletonList(new SteadyTrendSeriesFieldBO("rms", "相电压有效值")));
indicator.setSupportStats(Arrays.asList("AVG", "MAX", "MIN", "CP95"));
indicator.setUnit("V");
return indicator;
}
private SteadyChecksquareValuePointBO point(LocalDateTime time, String value) {
SteadyChecksquareValuePointBO point = new SteadyChecksquareValuePointBO();
point.setTime(time);
point.setValue(new BigDecimal(value));
return point;
}
}

View File

@@ -0,0 +1,56 @@
package com.njcn.gather.steady.checksquare.controller;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.util.List;
/**
* 数据校验接口契约测试。
*/
class SteadyChecksquareControllerTest {
@Test
void shouldExposeChecksquareQueryEndpointInSeparateController() throws Exception {
RequestMapping requestMapping = SteadyChecksquareController.class.getAnnotation(RequestMapping.class);
Assertions.assertArrayEquals(new String[]{"/steady/checksquare"}, requestMapping.value());
Method queryMethod = SteadyChecksquareController.class.getDeclaredMethod("query", com.njcn.gather.steady.checksquare.pojo.param.SteadyChecksquareHistoryQueryParam.class);
PostMapping queryMapping = queryMethod.getAnnotation(PostMapping.class);
Assertions.assertArrayEquals(new String[]{"/query"}, queryMapping.value());
Method createMethod = SteadyChecksquareController.class.getDeclaredMethod("create", com.njcn.gather.steady.checksquare.pojo.param.SteadyChecksquareQueryParam.class);
PostMapping createMapping = createMethod.getAnnotation(PostMapping.class);
Assertions.assertArrayEquals(new String[]{"/create"}, createMapping.value());
Method detailMethod = SteadyChecksquareController.class.getDeclaredMethod("detail", String.class);
GetMapping detailMapping = detailMethod.getAnnotation(GetMapping.class);
Assertions.assertArrayEquals(new String[]{"/detail"}, detailMapping.value());
Method itemDetailMethod = SteadyChecksquareController.class.getDeclaredMethod("itemDetail",
String.class, String.class, String.class, Integer.class, Integer.class);
GetMapping itemDetailMapping = itemDetailMethod.getAnnotation(GetMapping.class);
Assertions.assertArrayEquals(new String[]{"/item-detail"}, itemDetailMapping.value());
Method deleteMethod = SteadyChecksquareController.class.getDeclaredMethod("delete", List.class);
PostMapping deleteMapping = deleteMethod.getAnnotation(PostMapping.class);
Assertions.assertArrayEquals(new String[]{"/delete"}, deleteMapping.value());
}
@Test
void shouldKeepCreateResponseAsTaskSummaryWithoutDetailItems() throws Exception {
Method createMethod = SteadyChecksquareController.class.getDeclaredMethod("create",
com.njcn.gather.steady.checksquare.pojo.param.SteadyChecksquareQueryParam.class);
ParameterizedType resultType = (ParameterizedType) createMethod.getGenericReturnType();
Assertions.assertEquals(com.njcn.gather.steady.checksquare.pojo.vo.SteadyChecksquareTaskVO.class,
resultType.getActualTypeArguments()[0]);
Assertions.assertThrows(NoSuchFieldException.class,
() -> com.njcn.gather.steady.checksquare.pojo.vo.SteadyChecksquareTaskVO.class.getDeclaredField("items"));
}
}

View File

@@ -19,7 +19,7 @@ class SteadyChecksquareQueryParamTest {
Assertions.assertNull(field("qualityFlag")); Assertions.assertNull(field("qualityFlag"));
Assertions.assertNull(field("statTypes")); Assertions.assertNull(field("statTypes"));
Assertions.assertNull(field("phases")); Assertions.assertNull(field("phases"));
Assertions.assertNotNull(field("harmonicOrders")); Assertions.assertNull(field("harmonicOrders"));
} }
private Field field(String name) { private Field field(String name) {

View File

@@ -0,0 +1,936 @@
package com.njcn.gather.steady.checksquare.service.impl;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.baomidou.mybatisplus.core.toolkit.support.SFunction;
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.njcn.gather.steady.checksquare.component.SteadyChecksquareCalculator;
import com.njcn.gather.steady.checksquare.component.SteadyChecksquareHarmonicParityRuleComponent;
import com.njcn.gather.steady.checksquare.component.SteadyChecksquareInfluxQueryComponent;
import com.njcn.gather.steady.checksquare.component.SteadyChecksquareValueOrderRuleComponent;
import com.njcn.gather.steady.checksquare.pojo.param.SteadyChecksquareQueryParam;
import com.njcn.gather.steady.checksquare.pojo.po.SteadyChecksquareDetailPO;
import com.njcn.gather.steady.checksquare.pojo.po.SteadyChecksquareItemPO;
import com.njcn.gather.steady.checksquare.pojo.po.SteadyChecksquareStatSummaryPO;
import com.njcn.gather.steady.checksquare.pojo.po.SteadyChecksquareTaskPO;
import com.njcn.gather.steady.checksquare.pojo.vo.SteadyChecksquareHarmonicParityDetailVO;
import com.njcn.gather.steady.checksquare.pojo.vo.SteadyChecksquareHarmonicParityRuleVO;
import com.njcn.gather.steady.checksquare.pojo.vo.SteadyChecksquareItemDetailVO;
import com.njcn.gather.steady.checksquare.pojo.vo.SteadyChecksquareItemVO;
import com.njcn.gather.steady.checksquare.pojo.vo.SteadyChecksquareQueryVO;
import com.njcn.gather.steady.checksquare.pojo.vo.SteadyChecksquareStatSummaryVO;
import com.njcn.gather.steady.checksquare.pojo.vo.SteadyChecksquareTaskVO;
import com.njcn.gather.steady.checksquare.pojo.vo.SteadyChecksquareValueOrderDetailVO;
import com.njcn.gather.steady.checksquare.pojo.vo.SteadyChecksquareValueOrderRuleVO;
import com.njcn.gather.steady.checksquare.service.SteadyChecksquareDetailService;
import com.njcn.gather.steady.checksquare.service.SteadyChecksquareItemService;
import com.njcn.gather.steady.checksquare.service.SteadyChecksquareStatSummaryService;
import com.njcn.gather.steady.checksquare.service.SteadyChecksquareTaskService;
import com.njcn.gather.steady.datavie.component.SteadyTrendIndicatorCatalog;
import com.njcn.gather.steady.datavie.pojo.bo.SteadyTrendIndicatorDefinitionBO;
import com.njcn.gather.steady.datavie.pojo.bo.SteadyTrendResolvedFieldBO;
import com.njcn.gather.tool.adddata.component.AddDataTimeSlotCalculator;
import com.njcn.gather.tool.addledger.pojo.vo.AddLedgerLinePathVO;
import com.njcn.gather.tool.addledger.service.AddLedgerService;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.apache.ibatis.builder.MapperBuilderAssistant;
import org.apache.ibatis.session.Configuration;
import org.springframework.transaction.annotation.Transactional;
import java.lang.reflect.Method;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
/**
* 鏁版嵁鏍¢獙鏈嶅姟娴嬭瘯銆? */
class SteadyChecksquareServiceImplTest {
@BeforeAll
static void initMybatisPlusTableInfo() {
MapperBuilderAssistant assistant = new MapperBuilderAssistant(new Configuration(), "");
TableInfoHelper.initTableInfo(assistant, SteadyChecksquareTaskPO.class);
TableInfoHelper.initTableInfo(assistant, SteadyChecksquareItemPO.class);
TableInfoHelper.initTableInfo(assistant, SteadyChecksquareDetailPO.class);
}
@Test
void shouldNotOpenTransactionAroundCreateCalculation() throws Exception {
Method createMethod = SteadyChecksquareServiceImpl.class.getMethod("create", SteadyChecksquareQueryParam.class);
Assertions.assertNull(createMethod.getAnnotation(Transactional.class));
}
@Test
void shouldRejectCreateWhenTimeRangeExceedsSevenDays() {
AddLedgerService addLedgerService = mock(AddLedgerService.class);
SteadyChecksquareTaskService taskService = mock(SteadyChecksquareTaskService.class);
LambdaQueryChainWrapper<SteadyChecksquareTaskPO> taskQuery = mock(LambdaQueryChainWrapper.class);
when(taskService.lambdaQuery()).thenReturn(taskQuery);
when(taskQuery.eq(any(), any())).thenReturn(taskQuery);
when(taskQuery.orderByDesc(any(SFunction.class))).thenReturn(taskQuery);
when(taskQuery.list()).thenReturn(Collections.emptyList());
SteadyChecksquareServiceImpl service = new SteadyChecksquareServiceImpl(new SteadyTrendIndicatorCatalog(),
mock(SteadyChecksquareInfluxQueryComponent.class), new SteadyChecksquareCalculator(),
mock(SteadyChecksquareValueOrderRuleComponent.class), mock(SteadyChecksquareHarmonicParityRuleComponent.class),
new AddDataTimeSlotCalculator(), addLedgerService, taskService,
mock(SteadyChecksquareItemService.class), mock(SteadyChecksquareStatSummaryService.class),
mock(SteadyChecksquareDetailService.class), new ObjectMapper());
AddLedgerLinePathVO linePath = new AddLedgerLinePathVO();
linePath.setLineId("line-001");
linePath.setLineName("line-001");
linePath.setLineInterval(1);
when(addLedgerService.listLinePathByLineIds(eq(Collections.singletonList("line-001"))))
.thenReturn(Collections.singletonMap("line-001", linePath));
SteadyChecksquareQueryParam param = new SteadyChecksquareQueryParam();
param.setLineId("line-001");
param.setIndicatorCodes(Collections.singletonList("V_RMS"));
param.setTimeStart("2026-05-01 00:00:00");
param.setTimeEnd("2026-05-08 00:01:00");
Assertions.assertThrows(RuntimeException.class, () -> service.create(param));
verify(taskService, never()).save(any());
}
@Test
void shouldNotRejectCreateByIndicatorCountWithinSevenDays() {
AddLedgerService addLedgerService = mock(AddLedgerService.class);
SteadyChecksquareTaskService taskService = mock(SteadyChecksquareTaskService.class);
SteadyChecksquareInfluxQueryComponent influxQueryComponent = mock(SteadyChecksquareInfluxQueryComponent.class);
SteadyChecksquareValueOrderRuleComponent valueOrderRuleComponent = mock(SteadyChecksquareValueOrderRuleComponent.class);
SteadyChecksquareHarmonicParityRuleComponent harmonicParityRuleComponent = mock(SteadyChecksquareHarmonicParityRuleComponent.class);
SteadyChecksquareItemService itemService = mock(SteadyChecksquareItemService.class);
SteadyChecksquareStatSummaryService statSummaryService = mock(SteadyChecksquareStatSummaryService.class);
SteadyChecksquareDetailService detailService = mock(SteadyChecksquareDetailService.class);
LambdaQueryChainWrapper<SteadyChecksquareTaskPO> taskQuery = mock(LambdaQueryChainWrapper.class);
when(taskService.lambdaQuery()).thenReturn(taskQuery);
when(taskQuery.eq(any(), any())).thenReturn(taskQuery);
when(taskQuery.orderByDesc(any(SFunction.class))).thenReturn(taskQuery);
when(taskQuery.list()).thenReturn(Collections.emptyList());
SteadyChecksquareServiceImpl service = new SteadyChecksquareServiceImpl(new SteadyTrendIndicatorCatalog(),
influxQueryComponent, new SteadyChecksquareCalculator(),
valueOrderRuleComponent, harmonicParityRuleComponent,
new AddDataTimeSlotCalculator(), addLedgerService, taskService,
itemService, statSummaryService, detailService, new ObjectMapper());
AddLedgerLinePathVO linePath = new AddLedgerLinePathVO();
linePath.setLineId("line-001");
linePath.setLineName("line-001");
linePath.setLineInterval(1);
when(addLedgerService.listLinePathByLineIds(eq(Collections.singletonList("line-001"))))
.thenReturn(Collections.singletonMap("line-001", linePath));
when(influxQueryComponent.queryExistingSlots(any(), any(LocalDateTime.class), any(LocalDateTime.class), anyInt()))
.thenReturn(new HashSet<LocalDateTime>());
when(valueOrderRuleComponent.check(any(), any(), any(), any(LocalDateTime.class), any(LocalDateTime.class), anyInt()))
.thenReturn(emptyRuleResult());
when(harmonicParityRuleComponent.check(any(), any(), any(LocalDateTime.class), any(LocalDateTime.class), anyInt()))
.thenReturn(emptyHarmonicParityRuleResult());
ArgumentCaptor<SteadyChecksquareTaskPO> taskCaptor = ArgumentCaptor.forClass(SteadyChecksquareTaskPO.class);
when(taskService.save(taskCaptor.capture())).thenReturn(true);
SteadyChecksquareQueryParam param = new SteadyChecksquareQueryParam();
param.setLineId("line-001");
param.setIndicatorCodes(Arrays.asList("V_RMS", "V_LINE_RMS", "FREQ", "I_RMS", "I_THD"));
param.setTimeStart("2026-05-01 00:00:00");
param.setTimeEnd("2026-05-03 23:59:00");
SteadyChecksquareTaskVO result = service.create(param);
Assertions.assertEquals(taskCaptor.getValue().getId(), result.getTaskId());
Assertions.assertEquals(Integer.valueOf(5), result.getItemCount());
verify(itemService).saveBatch(any());
}
@Test
void shouldReturnExistingTaskSummaryWhenCreateMatchesLineAndTime() {
SteadyChecksquareTaskService taskService = mock(SteadyChecksquareTaskService.class);
LambdaQueryChainWrapper<SteadyChecksquareTaskPO> taskQuery = mock(LambdaQueryChainWrapper.class);
SteadyChecksquareTaskPO task = new SteadyChecksquareTaskPO();
task.setId("task-001");
task.setTaskNo("CS202605010001");
task.setLineId("line-001");
task.setLineName("line-001");
task.setTimeStart(LocalDateTime.of(2026, 5, 1, 0, 0));
task.setTimeEnd(LocalDateTime.of(2026, 5, 1, 0, 1));
task.setIntervalMinutes(1);
task.setIndicatorCodesJson("[\"V_RMS\"]");
task.setTaskStatus("SUCCESS");
task.setItemCount(1);
task.setAbnormalItemCount(0);
task.setMinDataIntegrity(BigDecimal.ONE.setScale(6));
task.setCreateTime(LocalDateTime.of(2026, 5, 1, 1, 0));
task.setState(1);
when(taskService.lambdaQuery()).thenReturn(taskQuery);
when(taskQuery.eq(any(), any())).thenReturn(taskQuery);
when(taskQuery.orderByDesc(any(SFunction.class))).thenReturn(taskQuery);
when(taskQuery.list()).thenReturn(Collections.singletonList(task));
SteadyChecksquareServiceImpl service = new SteadyChecksquareServiceImpl(new SteadyTrendIndicatorCatalog(),
mock(SteadyChecksquareInfluxQueryComponent.class), new SteadyChecksquareCalculator(),
mock(SteadyChecksquareValueOrderRuleComponent.class), mock(SteadyChecksquareHarmonicParityRuleComponent.class),
new AddDataTimeSlotCalculator(), mock(AddLedgerService.class), taskService,
mock(SteadyChecksquareItemService.class), mock(SteadyChecksquareStatSummaryService.class),
mock(SteadyChecksquareDetailService.class), new ObjectMapper());
SteadyChecksquareQueryParam param = new SteadyChecksquareQueryParam();
param.setLineId("line-001");
param.setIndicatorCodes(Collections.singletonList("I_RMS"));
param.setTimeStart("2026-05-01 00:00:00");
param.setTimeEnd("2026-05-01 00:01:00");
SteadyChecksquareTaskVO result = service.create(param);
Assertions.assertEquals("task-001", result.getTaskId());
Assertions.assertEquals("CS202605010001", result.getTaskNo());
Assertions.assertEquals("SUCCESS", result.getTaskStatus());
verify(taskService, never()).save(any());
}
@Test
void shouldCreateTaskSynchronouslyAndReturnTaskSummary() {
AddLedgerService addLedgerService = mock(AddLedgerService.class);
SteadyChecksquareTaskService taskService = mock(SteadyChecksquareTaskService.class);
SteadyChecksquareInfluxQueryComponent influxQueryComponent = mock(SteadyChecksquareInfluxQueryComponent.class);
SteadyChecksquareValueOrderRuleComponent valueOrderRuleComponent = mock(SteadyChecksquareValueOrderRuleComponent.class);
SteadyChecksquareHarmonicParityRuleComponent harmonicParityRuleComponent = mock(SteadyChecksquareHarmonicParityRuleComponent.class);
SteadyChecksquareItemService itemService = mock(SteadyChecksquareItemService.class);
SteadyChecksquareStatSummaryService statSummaryService = mock(SteadyChecksquareStatSummaryService.class);
SteadyChecksquareDetailService detailService = mock(SteadyChecksquareDetailService.class);
LambdaQueryChainWrapper<SteadyChecksquareTaskPO> taskQuery = mock(LambdaQueryChainWrapper.class);
when(taskService.lambdaQuery()).thenReturn(taskQuery);
when(taskQuery.eq(any(), any())).thenReturn(taskQuery);
when(taskQuery.orderByDesc(any(SFunction.class))).thenReturn(taskQuery);
when(taskQuery.list()).thenReturn(Collections.emptyList());
SteadyChecksquareServiceImpl service = new SteadyChecksquareServiceImpl(new SteadyTrendIndicatorCatalog(),
influxQueryComponent, new SteadyChecksquareCalculator(),
valueOrderRuleComponent, harmonicParityRuleComponent,
new AddDataTimeSlotCalculator(), addLedgerService, taskService,
itemService, statSummaryService, detailService, new ObjectMapper());
AddLedgerLinePathVO linePath = new AddLedgerLinePathVO();
linePath.setLineId("line-001");
linePath.setLineName("line-001");
linePath.setLineInterval(1);
when(addLedgerService.listLinePathByLineIds(eq(Collections.singletonList("line-001"))))
.thenReturn(Collections.singletonMap("line-001", linePath));
when(influxQueryComponent.queryExistingSlots(any(), any(LocalDateTime.class), any(LocalDateTime.class), anyInt()))
.thenReturn(new HashSet<LocalDateTime>());
when(valueOrderRuleComponent.check(any(), any(), any(), any(LocalDateTime.class), any(LocalDateTime.class), anyInt()))
.thenReturn(emptyRuleResult());
when(harmonicParityRuleComponent.check(any(), any(), any(LocalDateTime.class), any(LocalDateTime.class), anyInt()))
.thenReturn(emptyHarmonicParityRuleResult());
ArgumentCaptor<SteadyChecksquareTaskPO> taskCaptor = ArgumentCaptor.forClass(SteadyChecksquareTaskPO.class);
when(taskService.save(taskCaptor.capture())).thenReturn(true);
SteadyChecksquareQueryParam param = new SteadyChecksquareQueryParam();
param.setLineId("line-001");
param.setIndicatorCodes(Collections.singletonList("V_RMS"));
param.setTimeStart("2026-05-01 00:00:00");
param.setTimeEnd("2026-05-01 00:01:00");
SteadyChecksquareTaskVO result = service.create(param);
Assertions.assertEquals(taskCaptor.getValue().getId(), result.getTaskId());
Assertions.assertEquals("SUCCESS", result.getTaskStatus());
Assertions.assertEquals(Integer.valueOf(1), result.getItemCount());
verify(itemService).saveBatch(any());
verify(statSummaryService).saveBatch(any());
}
@Test
void shouldUseFixedFlickerIntervalsPerIndicator() {
SteadyChecksquareInfluxQueryComponent influxQueryComponent = mock(SteadyChecksquareInfluxQueryComponent.class);
SteadyChecksquareValueOrderRuleComponent valueOrderRuleComponent = mock(SteadyChecksquareValueOrderRuleComponent.class);
SteadyChecksquareHarmonicParityRuleComponent harmonicParityRuleComponent = mock(SteadyChecksquareHarmonicParityRuleComponent.class);
AddLedgerService addLedgerService = mock(AddLedgerService.class);
SteadyChecksquareServiceImpl service = new SteadyChecksquareServiceImpl(new SteadyTrendIndicatorCatalog(),
influxQueryComponent, new SteadyChecksquareCalculator(), valueOrderRuleComponent, harmonicParityRuleComponent,
new AddDataTimeSlotCalculator(), addLedgerService, mock(SteadyChecksquareTaskService.class),
mock(SteadyChecksquareItemService.class), mock(SteadyChecksquareStatSummaryService.class),
mock(SteadyChecksquareDetailService.class), new ObjectMapper());
when(valueOrderRuleComponent.check(any(), any(), any(), any(LocalDateTime.class), any(LocalDateTime.class), anyInt()))
.thenReturn(emptyRuleResult());
when(harmonicParityRuleComponent.check(any(), any(), any(LocalDateTime.class), any(LocalDateTime.class), anyInt()))
.thenReturn(emptyHarmonicParityRuleResult());
AddLedgerLinePathVO linePath = new AddLedgerLinePathVO();
linePath.setLineId("line-001");
linePath.setLineName("杩涚嚎涓€");
linePath.setLineInterval(1);
when(addLedgerService.listLinePathByLineIds(eq(Collections.singletonList("line-001"))))
.thenReturn(Collections.singletonMap("line-001", linePath));
when(influxQueryComponent.queryExistingSlots(any(), any(LocalDateTime.class), any(LocalDateTime.class), eq(10)))
.thenReturn(new HashSet<LocalDateTime>(Arrays.asList(
LocalDateTime.of(2026, 5, 1, 0, 0),
LocalDateTime.of(2026, 5, 1, 0, 10))));
when(influxQueryComponent.queryExistingSlots(any(), any(LocalDateTime.class), any(LocalDateTime.class), eq(120)))
.thenReturn(new HashSet<LocalDateTime>(Collections.singletonList(
LocalDateTime.of(2026, 5, 1, 0, 0))));
SteadyChecksquareQueryParam param = new SteadyChecksquareQueryParam();
param.setLineId("line-001");
param.setIndicatorCodes(Arrays.asList("FLUC", "PST", "PLT"));
param.setTimeStart("2026-05-01 00:00:00");
param.setTimeEnd("2026-05-01 02:00:00");
SteadyChecksquareQueryVO result = calculate(service, param);
Assertions.assertEquals(Integer.valueOf(1), result.getIntervalMinutes());
Assertions.assertEquals(3, result.getItems().size());
assertItemInterval(result.getItems().get(0), "FLUC", 10, 13);
assertItemInterval(result.getItems().get(1), "PST", 10, 13);
assertItemInterval(result.getItems().get(2), "PLT", 120, 2);
}
@Test
void shouldAggregateAllHarmonicOrdersIntoIndicatorItem() {
SteadyChecksquareInfluxQueryComponent influxQueryComponent = mock(SteadyChecksquareInfluxQueryComponent.class);
SteadyChecksquareValueOrderRuleComponent valueOrderRuleComponent = mock(SteadyChecksquareValueOrderRuleComponent.class);
SteadyChecksquareHarmonicParityRuleComponent harmonicParityRuleComponent = mock(SteadyChecksquareHarmonicParityRuleComponent.class);
AddLedgerService addLedgerService = mock(AddLedgerService.class);
SteadyChecksquareServiceImpl service = new SteadyChecksquareServiceImpl(new SteadyTrendIndicatorCatalog(),
influxQueryComponent, new SteadyChecksquareCalculator(), valueOrderRuleComponent, harmonicParityRuleComponent,
new AddDataTimeSlotCalculator(), addLedgerService, mock(SteadyChecksquareTaskService.class),
mock(SteadyChecksquareItemService.class), mock(SteadyChecksquareStatSummaryService.class),
mock(SteadyChecksquareDetailService.class), new ObjectMapper());
when(valueOrderRuleComponent.check(any(), any(), any(), any(LocalDateTime.class), any(LocalDateTime.class), anyInt()))
.thenReturn(emptyRuleResult());
when(harmonicParityRuleComponent.check(any(), any(), any(LocalDateTime.class), any(LocalDateTime.class), anyInt()))
.thenReturn(emptyHarmonicParityRuleResult());
AddLedgerLinePathVO linePath = new AddLedgerLinePathVO();
linePath.setLineId("line-001");
linePath.setLineName("杩涚嚎涓€");
linePath.setLineInterval(1);
when(addLedgerService.listLinePathByLineIds(eq(Collections.singletonList("line-001"))))
.thenReturn(Collections.singletonMap("line-001", linePath));
when(influxQueryComponent.queryExistingSlots(any(), any(LocalDateTime.class), any(LocalDateTime.class), eq(1)))
.thenReturn(new HashSet<LocalDateTime>(Collections.singletonList(
LocalDateTime.of(2026, 5, 1, 0, 0))));
SteadyChecksquareQueryParam param = new SteadyChecksquareQueryParam();
param.setLineId("line-001");
param.setIndicatorCodes(Collections.singletonList("V_HARMONIC"));
param.setTimeStart("2026-05-01 00:00:00");
param.setTimeEnd("2026-05-01 00:01:00");
SteadyChecksquareQueryVO result = calculate(service, param);
Assertions.assertEquals(1, result.getItems().size());
Assertions.assertEquals("line-001|V_HARMONIC", result.getItems().get(0).getItemKey());
Assertions.assertNull(result.getItems().get(0).getHarmonicOrder());
Assertions.assertEquals(Integer.valueOf(2), result.getItems().get(0).getStatDetails().get(0).getSegments().get(0).getHarmonicOrder());
}
@Test
void shouldAverageHarmonicOrderResultsAndMarkAbnormalWhenAnyOrderAbnormal() {
SteadyChecksquareInfluxQueryComponent influxQueryComponent = mock(SteadyChecksquareInfluxQueryComponent.class);
SteadyChecksquareValueOrderRuleComponent valueOrderRuleComponent = mock(SteadyChecksquareValueOrderRuleComponent.class);
SteadyChecksquareHarmonicParityRuleComponent harmonicParityRuleComponent = mock(SteadyChecksquareHarmonicParityRuleComponent.class);
AddLedgerService addLedgerService = mock(AddLedgerService.class);
SteadyChecksquareServiceImpl service = new SteadyChecksquareServiceImpl(new SteadyTrendIndicatorCatalog(),
influxQueryComponent, new SteadyChecksquareCalculator(), valueOrderRuleComponent, harmonicParityRuleComponent,
new AddDataTimeSlotCalculator(), addLedgerService, mock(SteadyChecksquareTaskService.class),
mock(SteadyChecksquareItemService.class), mock(SteadyChecksquareStatSummaryService.class),
mock(SteadyChecksquareDetailService.class), new ObjectMapper());
when(valueOrderRuleComponent.check(any(), any(), any(), any(LocalDateTime.class), any(LocalDateTime.class), anyInt()))
.thenReturn(emptyRuleResult());
when(harmonicParityRuleComponent.check(any(), any(), any(LocalDateTime.class), any(LocalDateTime.class), anyInt()))
.thenReturn(emptyHarmonicParityRuleResult());
AddLedgerLinePathVO linePath = new AddLedgerLinePathVO();
linePath.setLineId("line-001");
linePath.setLineName("杩涚嚎涓€");
linePath.setLineInterval(1);
when(addLedgerService.listLinePathByLineIds(eq(Collections.singletonList("line-001"))))
.thenReturn(Collections.singletonMap("line-001", linePath));
when(influxQueryComponent.queryExistingSlots(any(), any(LocalDateTime.class), any(LocalDateTime.class), eq(1)))
.thenReturn(new HashSet<LocalDateTime>(Collections.singletonList(
LocalDateTime.of(2026, 5, 1, 0, 0))));
SteadyChecksquareValueOrderRuleVO normalRuleResult = new SteadyChecksquareValueOrderRuleVO();
SteadyChecksquareValueOrderRuleVO abnormalRuleResult = new SteadyChecksquareValueOrderRuleVO();
SteadyChecksquareValueOrderDetailVO abnormalDetail = new SteadyChecksquareValueOrderDetailVO();
abnormalDetail.setTime("2026-05-01 00:00:00");
abnormalDetail.setPhase("A");
abnormalDetail.setHarmonicOrder(2);
abnormalRuleResult.setAbnormal(true);
abnormalRuleResult.setAbnormalPointCount(4);
abnormalRuleResult.setAbnormalDetails(Collections.singletonList(abnormalDetail));
when(valueOrderRuleComponent.check(any(), any(), any(), any(LocalDateTime.class), any(LocalDateTime.class), anyInt()))
.thenReturn(normalRuleResult);
when(valueOrderRuleComponent.check(any(), any(), eq(2), any(LocalDateTime.class), any(LocalDateTime.class), anyInt()))
.thenReturn(abnormalRuleResult);
when(valueOrderRuleComponent.check(any(), any(), eq(3), any(LocalDateTime.class), any(LocalDateTime.class), anyInt()))
.thenReturn(normalRuleResult);
SteadyChecksquareQueryParam param = new SteadyChecksquareQueryParam();
param.setLineId("line-001");
param.setIndicatorCodes(Collections.singletonList("V_HARMONIC"));
param.setTimeStart("2026-05-01 00:00:00");
param.setTimeEnd("2026-05-01 00:01:00");
SteadyChecksquareQueryVO result = calculate(service, param);
List<SteadyChecksquareItemVO> items = result.getItems();
Assertions.assertEquals(1, items.size());
Assertions.assertEquals(Boolean.TRUE, items.get(0).getAbnormal());
Assertions.assertEquals(Integer.valueOf(1), items.get(0).getAbnormalPointCount());
Assertions.assertEquals(1, items.get(0).getAbnormalDetails().size());
Assertions.assertEquals(Integer.valueOf(2), items.get(0).getAbnormalDetails().get(0).getHarmonicOrder());
Assertions.assertEquals(Integer.valueOf(8), items.get(0).getExpectedPointCount());
Assertions.assertEquals(Integer.valueOf(4), items.get(0).getActualPointCount());
}
@Test
void shouldAssembleValueOrderRuleResultIntoItem() {
SteadyChecksquareInfluxQueryComponent influxQueryComponent = mock(SteadyChecksquareInfluxQueryComponent.class);
SteadyChecksquareValueOrderRuleComponent valueOrderRuleComponent = mock(SteadyChecksquareValueOrderRuleComponent.class);
SteadyChecksquareHarmonicParityRuleComponent harmonicParityRuleComponent = mock(SteadyChecksquareHarmonicParityRuleComponent.class);
AddLedgerService addLedgerService = mock(AddLedgerService.class);
SteadyChecksquareServiceImpl service = new SteadyChecksquareServiceImpl(new SteadyTrendIndicatorCatalog(),
influxQueryComponent, new SteadyChecksquareCalculator(), valueOrderRuleComponent, harmonicParityRuleComponent,
new AddDataTimeSlotCalculator(), addLedgerService, mock(SteadyChecksquareTaskService.class),
mock(SteadyChecksquareItemService.class), mock(SteadyChecksquareStatSummaryService.class),
mock(SteadyChecksquareDetailService.class), new ObjectMapper());
AddLedgerLinePathVO linePath = new AddLedgerLinePathVO();
linePath.setLineId("line-001");
linePath.setLineName("杩涚嚎涓€");
linePath.setLineInterval(1);
when(addLedgerService.listLinePathByLineIds(eq(Collections.singletonList("line-001"))))
.thenReturn(Collections.singletonMap("line-001", linePath));
when(influxQueryComponent.queryExistingSlots(any(), any(LocalDateTime.class), any(LocalDateTime.class), eq(1)))
.thenReturn(new HashSet<LocalDateTime>(Collections.singletonList(
LocalDateTime.of(2026, 5, 1, 0, 0))));
SteadyChecksquareValueOrderRuleVO ruleResult = new SteadyChecksquareValueOrderRuleVO();
SteadyChecksquareValueOrderDetailVO detail = new SteadyChecksquareValueOrderDetailVO();
detail.setTime("2026-05-01 00:00:00");
detail.setPhase("A");
ruleResult.setAbnormalPointCount(2);
ruleResult.setAbnormal(true);
ruleResult.setAbnormalDetails(Collections.singletonList(detail));
when(valueOrderRuleComponent.check(any(), any(), any(), any(LocalDateTime.class), any(LocalDateTime.class), eq(1)))
.thenReturn(ruleResult);
SteadyChecksquareQueryParam param = new SteadyChecksquareQueryParam();
param.setLineId("line-001");
param.setIndicatorCodes(Collections.singletonList("V_RMS"));
param.setTimeStart("2026-05-01 00:00:00");
param.setTimeEnd("2026-05-01 00:01:00");
SteadyChecksquareQueryVO result = calculate(service, param);
SteadyChecksquareItemVO item = result.getItems().get(0);
Assertions.assertEquals(Boolean.TRUE, item.getAbnormal());
Assertions.assertEquals(Integer.valueOf(2), item.getAbnormalPointCount());
Assertions.assertEquals(1, item.getAbnormalDetails().size());
Assertions.assertEquals("A", item.getAbnormalDetails().get(0).getPhase());
}
@Test
void shouldPrefetchNormalIndicatorFieldsByMeasurementPhaseAndStat() {
SteadyChecksquareInfluxQueryComponent influxQueryComponent = mock(SteadyChecksquareInfluxQueryComponent.class);
SteadyChecksquareValueOrderRuleComponent valueOrderRuleComponent = mock(SteadyChecksquareValueOrderRuleComponent.class);
SteadyChecksquareHarmonicParityRuleComponent harmonicParityRuleComponent = mock(SteadyChecksquareHarmonicParityRuleComponent.class);
AddLedgerService addLedgerService = mock(AddLedgerService.class);
SteadyChecksquareServiceImpl service = new SteadyChecksquareServiceImpl(new SteadyTrendIndicatorCatalog(),
influxQueryComponent, new SteadyChecksquareCalculator(), valueOrderRuleComponent, harmonicParityRuleComponent,
new AddDataTimeSlotCalculator(), addLedgerService, mock(SteadyChecksquareTaskService.class),
mock(SteadyChecksquareItemService.class), mock(SteadyChecksquareStatSummaryService.class),
mock(SteadyChecksquareDetailService.class), new ObjectMapper());
when(valueOrderRuleComponent.check(any(), any(), any(), any(LocalDateTime.class), any(LocalDateTime.class), anyInt()))
.thenReturn(emptyRuleResult());
when(harmonicParityRuleComponent.check(any(), any(), any(LocalDateTime.class), any(LocalDateTime.class), anyInt()))
.thenReturn(emptyHarmonicParityRuleResult());
AddLedgerLinePathVO linePath = new AddLedgerLinePathVO();
linePath.setLineId("line-001");
linePath.setLineName("杩涚嚎涓€");
linePath.setLineInterval(1);
when(addLedgerService.listLinePathByLineIds(eq(Collections.singletonList("line-001"))))
.thenReturn(Collections.singletonMap("line-001", linePath));
when(influxQueryComponent.queryExistingSlots(any(), any(LocalDateTime.class), any(LocalDateTime.class), eq(1)))
.thenReturn(new HashSet<LocalDateTime>(Collections.singletonList(
LocalDateTime.of(2026, 5, 1, 0, 0))));
SteadyChecksquareQueryParam param = new SteadyChecksquareQueryParam();
param.setLineId("line-001");
param.setIndicatorCodes(Arrays.asList("V_RMS", "V_LINE_RMS"));
param.setTimeStart("2026-05-01 00:00:00");
param.setTimeEnd("2026-05-01 00:01:00");
calculate(service, param);
ArgumentCaptor<List> captor = ArgumentCaptor.forClass(List.class);
verify(influxQueryComponent, times(12)).queryValuePointMap(captor.capture(),
any(LocalDateTime.class), any(LocalDateTime.class), eq(1));
boolean foundBatch = false;
for (List fields : captor.getAllValues()) {
if (fields.size() == 2) {
List<String> fieldNames = new ArrayList<String>();
for (Object field : fields) {
fieldNames.add(((SteadyTrendResolvedFieldBO) field).getField());
}
foundBatch = fieldNames.contains("rms") && fieldNames.contains("rms_lvr");
}
}
Assertions.assertTrue(foundBatch);
}
@Test
void shouldAssembleHarmonicParityRuleResultIntoAggregateItem() {
SteadyChecksquareInfluxQueryComponent influxQueryComponent = mock(SteadyChecksquareInfluxQueryComponent.class);
SteadyChecksquareValueOrderRuleComponent valueOrderRuleComponent = mock(SteadyChecksquareValueOrderRuleComponent.class);
SteadyChecksquareHarmonicParityRuleComponent harmonicParityRuleComponent = mock(SteadyChecksquareHarmonicParityRuleComponent.class);
AddLedgerService addLedgerService = mock(AddLedgerService.class);
SteadyChecksquareServiceImpl service = new SteadyChecksquareServiceImpl(new SteadyTrendIndicatorCatalog(),
influxQueryComponent, new SteadyChecksquareCalculator(), valueOrderRuleComponent, harmonicParityRuleComponent,
new AddDataTimeSlotCalculator(), addLedgerService, mock(SteadyChecksquareTaskService.class),
mock(SteadyChecksquareItemService.class), mock(SteadyChecksquareStatSummaryService.class),
mock(SteadyChecksquareDetailService.class), new ObjectMapper());
when(valueOrderRuleComponent.check(any(), any(), any(), any(LocalDateTime.class), any(LocalDateTime.class), anyInt()))
.thenReturn(emptyRuleResult());
AddLedgerLinePathVO linePath = new AddLedgerLinePathVO();
linePath.setLineId("line-001");
linePath.setLineName("杩涚嚎涓€");
linePath.setLineInterval(1);
when(addLedgerService.listLinePathByLineIds(eq(Collections.singletonList("line-001"))))
.thenReturn(Collections.singletonMap("line-001", linePath));
when(influxQueryComponent.queryExistingSlots(any(), any(LocalDateTime.class), any(LocalDateTime.class), eq(1)))
.thenReturn(new HashSet<LocalDateTime>(Collections.singletonList(
LocalDateTime.of(2026, 5, 1, 0, 0))));
SteadyChecksquareHarmonicParityRuleVO ruleResult = new SteadyChecksquareHarmonicParityRuleVO();
SteadyChecksquareHarmonicParityDetailVO detail = new SteadyChecksquareHarmonicParityDetailVO();
detail.setTime("2026-05-01 00:00:00");
detail.setPhase("A");
detail.setStatType("AVG");
detail.setEvenHarmonicOrder(4);
ruleResult.setAbnormal(true);
ruleResult.setAbnormalPointCount(1);
ruleResult.setAbnormalDetails(Collections.singletonList(detail));
when(harmonicParityRuleComponent.check(any(), any(), any(LocalDateTime.class), any(LocalDateTime.class), eq(1)))
.thenReturn(ruleResult);
SteadyChecksquareQueryParam param = new SteadyChecksquareQueryParam();
param.setLineId("line-001");
param.setIndicatorCodes(Collections.singletonList("V_HARMONIC"));
param.setTimeStart("2026-05-01 00:00:00");
param.setTimeEnd("2026-05-01 00:01:00");
SteadyChecksquareQueryVO result = calculate(service, param);
SteadyChecksquareItemVO item = result.getItems().get(0);
Assertions.assertNull(item.getHarmonicOrder());
Assertions.assertEquals(Boolean.TRUE, item.getHarmonicParityAbnormal());
Assertions.assertEquals(Integer.valueOf(1), item.getHarmonicParityAbnormalPointCount());
Assertions.assertEquals("AVG", item.getHarmonicParityAbnormalDetails().get(0).getStatType());
}
@Test
void shouldRejectUnsupportedItemDetailType() {
SteadyChecksquareItemService itemService = mock(SteadyChecksquareItemService.class);
SteadyChecksquareItemPO item = new SteadyChecksquareItemPO();
item.setId("item-001");
item.setState(1);
when(itemService.getById("item-001")).thenReturn(item);
SteadyChecksquareServiceImpl service = new SteadyChecksquareServiceImpl(new SteadyTrendIndicatorCatalog(),
mock(SteadyChecksquareInfluxQueryComponent.class), new SteadyChecksquareCalculator(),
mock(SteadyChecksquareValueOrderRuleComponent.class), mock(SteadyChecksquareHarmonicParityRuleComponent.class),
new AddDataTimeSlotCalculator(), mock(AddLedgerService.class), mock(SteadyChecksquareTaskService.class),
itemService, mock(SteadyChecksquareStatSummaryService.class),
mock(SteadyChecksquareDetailService.class), new ObjectMapper());
Assertions.assertThrows(RuntimeException.class, () -> service.itemDetail("item-001", "UNKNOWN", null));
}
@Test
void shouldLoadDetailSummariesInSingleBatch() {
SteadyChecksquareTaskService taskService = mock(SteadyChecksquareTaskService.class);
SteadyChecksquareItemService itemService = mock(SteadyChecksquareItemService.class);
SteadyChecksquareStatSummaryService statSummaryService = mock(SteadyChecksquareStatSummaryService.class);
LambdaQueryChainWrapper<SteadyChecksquareItemPO> itemQuery = mock(LambdaQueryChainWrapper.class);
LambdaQueryChainWrapper<SteadyChecksquareStatSummaryPO> summaryQuery = mock(LambdaQueryChainWrapper.class);
SteadyChecksquareTaskPO task = new SteadyChecksquareTaskPO();
task.setId("task-001");
task.setState(1);
task.setLineId("line-001");
task.setLineName("杩涚嚎涓€");
task.setTimeStart(LocalDateTime.of(2026, 5, 1, 0, 0));
task.setTimeEnd(LocalDateTime.of(2026, 5, 1, 0, 1));
task.setIntervalMinutes(1);
SteadyChecksquareItemPO item1 = buildItemPO("item-001", "V_RMS");
SteadyChecksquareItemPO item2 = buildItemPO("item-002", "FREQ");
SteadyChecksquareStatSummaryPO summary1 = buildSummaryPO("item-001", "AVG");
SteadyChecksquareStatSummaryPO summary2 = buildSummaryPO("item-002", "AVG");
when(taskService.getById("task-001")).thenReturn(task);
when(itemService.lambdaQuery()).thenReturn(itemQuery);
when(itemQuery.eq(any(), any())).thenReturn(itemQuery);
when(itemQuery.list()).thenReturn(Arrays.asList(item1, item2));
when(statSummaryService.lambdaQuery()).thenReturn(summaryQuery);
when(summaryQuery.in(any(), any(List.class))).thenReturn(summaryQuery);
when(summaryQuery.list()).thenReturn(Arrays.asList(summary1, summary2));
SteadyChecksquareServiceImpl service = new SteadyChecksquareServiceImpl(new SteadyTrendIndicatorCatalog(),
mock(SteadyChecksquareInfluxQueryComponent.class), new SteadyChecksquareCalculator(),
mock(SteadyChecksquareValueOrderRuleComponent.class), mock(SteadyChecksquareHarmonicParityRuleComponent.class),
new AddDataTimeSlotCalculator(), mock(AddLedgerService.class), taskService,
itemService, statSummaryService, mock(SteadyChecksquareDetailService.class), new ObjectMapper());
SteadyChecksquareQueryVO result = service.detail("task-001");
Assertions.assertEquals(2, result.getItems().size());
Assertions.assertEquals(1, result.getItems().get(0).getStatSummaries().size());
Assertions.assertEquals(1, result.getItems().get(1).getStatSummaries().size());
verify(statSummaryService, times(1)).lambdaQuery();
}
@Test
void shouldPageItemDetailWhenPageArgumentsPresent() {
SteadyChecksquareItemService itemService = mock(SteadyChecksquareItemService.class);
SteadyChecksquareDetailService detailService = mock(SteadyChecksquareDetailService.class);
SteadyChecksquareItemPO item = new SteadyChecksquareItemPO();
item.setId("item-001");
item.setState(1);
SteadyChecksquareDetailPO detail = new SteadyChecksquareDetailPO();
detail.setItemId("item-001");
detail.setDetailType("VALUE_ORDER");
detail.setPointTime(LocalDateTime.of(2026, 5, 1, 0, 0));
detail.setPhase("A");
Page<SteadyChecksquareDetailPO> page = new Page<SteadyChecksquareDetailPO>(2, 1);
page.setTotal(1);
page.setRecords(Collections.singletonList(detail));
when(itemService.getById("item-001")).thenReturn(item);
when(detailService.page(any(Page.class), any())).thenReturn(page);
SteadyChecksquareServiceImpl service = new SteadyChecksquareServiceImpl(new SteadyTrendIndicatorCatalog(),
mock(SteadyChecksquareInfluxQueryComponent.class), new SteadyChecksquareCalculator(),
mock(SteadyChecksquareValueOrderRuleComponent.class), mock(SteadyChecksquareHarmonicParityRuleComponent.class),
new AddDataTimeSlotCalculator(), mock(AddLedgerService.class), mock(SteadyChecksquareTaskService.class),
itemService, mock(SteadyChecksquareStatSummaryService.class),
detailService, new ObjectMapper());
SteadyChecksquareItemDetailVO result = service.itemDetail("item-001", "VALUE_ORDER", null, 2, 1);
Assertions.assertEquals(Integer.valueOf(2), result.getPageNum());
Assertions.assertEquals(Integer.valueOf(1), result.getPageSize());
Assertions.assertEquals(Long.valueOf(1L), result.getTotal());
Assertions.assertEquals(1, result.getValueOrderDetails().size());
verify(detailService).page(any(Page.class), any());
}
@Test
void shouldDeleteTasksAndItemsLogically() {
SteadyChecksquareTaskService taskService = mock(SteadyChecksquareTaskService.class);
SteadyChecksquareItemService itemService = mock(SteadyChecksquareItemService.class);
LambdaQueryChainWrapper<SteadyChecksquareTaskPO> taskQuery = mock(LambdaQueryChainWrapper.class);
SteadyChecksquareTaskPO task = new SteadyChecksquareTaskPO();
task.setId("task-001");
task.setState(1);
when(taskService.lambdaQuery()).thenReturn(taskQuery);
when(taskQuery.in(any(), any(List.class))).thenReturn(taskQuery);
when(taskQuery.eq(any(), any())).thenReturn(taskQuery);
when(taskQuery.list()).thenReturn(Collections.singletonList(task));
when(taskService.update(any())).thenReturn(true);
when(itemService.update(any())).thenReturn(true);
SteadyChecksquareServiceImpl service = new SteadyChecksquareServiceImpl(new SteadyTrendIndicatorCatalog(),
mock(SteadyChecksquareInfluxQueryComponent.class), new SteadyChecksquareCalculator(),
mock(SteadyChecksquareValueOrderRuleComponent.class), mock(SteadyChecksquareHarmonicParityRuleComponent.class),
new AddDataTimeSlotCalculator(), mock(AddLedgerService.class), taskService,
itemService, mock(SteadyChecksquareStatSummaryService.class),
mock(SteadyChecksquareDetailService.class), new ObjectMapper());
boolean result = service.delete(Collections.singletonList("task-001"));
Assertions.assertTrue(result);
verify(taskService).update(any());
verify(itemService).update(any());
}
@Test
void shouldSaveChecksquareResultsInBatch() {
SteadyChecksquareTaskService taskService = mock(SteadyChecksquareTaskService.class);
SteadyChecksquareItemService itemService = mock(SteadyChecksquareItemService.class);
SteadyChecksquareStatSummaryService statSummaryService = mock(SteadyChecksquareStatSummaryService.class);
SteadyChecksquareDetailService detailService = mock(SteadyChecksquareDetailService.class);
SteadyChecksquareServiceImpl service = new SteadyChecksquareServiceImpl(new SteadyTrendIndicatorCatalog(),
mock(SteadyChecksquareInfluxQueryComponent.class), new SteadyChecksquareCalculator(),
mock(SteadyChecksquareValueOrderRuleComponent.class), mock(SteadyChecksquareHarmonicParityRuleComponent.class),
new AddDataTimeSlotCalculator(), mock(AddLedgerService.class), taskService,
itemService, statSummaryService, detailService, new ObjectMapper());
SteadyChecksquareQueryParam param = new SteadyChecksquareQueryParam();
param.setIndicatorCodes(Collections.singletonList("V_RMS"));
SteadyChecksquareQueryVO result = new SteadyChecksquareQueryVO();
result.setLineId("line-001");
result.setLineName("杩涚嚎涓€");
result.setTimeStart("2026-05-01 00:00:00");
result.setTimeEnd("2026-05-01 00:01:00");
result.setIntervalMinutes(1);
SteadyChecksquareItemVO item = new SteadyChecksquareItemVO();
item.setItemKey("line-001|V_RMS");
item.setIndicatorCode("V_RMS");
item.setIndicatorName("鐩哥數鍘嬫湁鏁堝€?");
item.setIntervalMinutes(1);
item.setHasData(true);
item.setExpectedPointCount(2);
item.setActualPointCount(2);
item.setMissingPointCount(0);
item.setDataIntegrity(BigDecimal.ONE.setScale(6));
item.setDataIntegrityText("100.00%");
item.setAbnormal(false);
item.setAbnormalPointCount(0);
item.setHarmonicParityAbnormal(false);
item.setHarmonicParityAbnormalPointCount(0);
SteadyChecksquareStatSummaryVO summary = new SteadyChecksquareStatSummaryVO();
summary.setStatType("AVG");
summary.setSupported(true);
summary.setHasData(true);
summary.setExpectedPointCount(2);
summary.setActualPointCount(2);
summary.setMissingPointCount(0);
summary.setDataIntegrity(BigDecimal.ONE.setScale(6));
summary.setDataIntegrityText("100.00%");
item.getStatSummaries().add(summary);
result.getItems().add(item);
saveResult(service, param, result);
verify(taskService).save(any());
verify(itemService).saveBatch(any());
verify(statSummaryService).saveBatch(any());
}
@Test
void shouldSaveDetailResultsInChunks() {
SteadyChecksquareTaskService taskService = mock(SteadyChecksquareTaskService.class);
SteadyChecksquareItemService itemService = mock(SteadyChecksquareItemService.class);
SteadyChecksquareStatSummaryService statSummaryService = mock(SteadyChecksquareStatSummaryService.class);
SteadyChecksquareDetailService detailService = mock(SteadyChecksquareDetailService.class);
SteadyChecksquareServiceImpl service = new SteadyChecksquareServiceImpl(new SteadyTrendIndicatorCatalog(),
mock(SteadyChecksquareInfluxQueryComponent.class), new SteadyChecksquareCalculator(),
mock(SteadyChecksquareValueOrderRuleComponent.class), mock(SteadyChecksquareHarmonicParityRuleComponent.class),
new AddDataTimeSlotCalculator(), mock(AddLedgerService.class), taskService,
itemService, statSummaryService, detailService, new ObjectMapper());
SteadyChecksquareQueryParam param = new SteadyChecksquareQueryParam();
param.setIndicatorCodes(Collections.singletonList("V_RMS"));
SteadyChecksquareQueryVO result = new SteadyChecksquareQueryVO();
result.setLineId("line-001");
result.setLineName("line-001");
result.setTimeStart("2026-05-01 00:00:00");
result.setTimeEnd("2026-05-01 00:01:00");
result.setIntervalMinutes(1);
SteadyChecksquareItemVO item = buildOrderItem(true, BigDecimal.ONE.setScale(6));
item.setItemKey("line-001|V_RMS");
item.setIndicatorCode("V_RMS");
for (int i = 0; i < 1001; i++) {
SteadyChecksquareValueOrderDetailVO detail = new SteadyChecksquareValueOrderDetailVO();
detail.setTime("2026-05-01 00:00:00");
detail.setPhase("A");
item.getAbnormalDetails().add(detail);
}
result.getItems().add(item);
saveResult(service, param, result);
verify(detailService, times(2)).saveBatch(any());
}
@Test
void shouldCountNoDataItemAsAbnormalWhenSavingTask() {
SteadyChecksquareTaskService taskService = mock(SteadyChecksquareTaskService.class);
SteadyChecksquareServiceImpl service = new SteadyChecksquareServiceImpl(new SteadyTrendIndicatorCatalog(),
mock(SteadyChecksquareInfluxQueryComponent.class), new SteadyChecksquareCalculator(),
mock(SteadyChecksquareValueOrderRuleComponent.class), mock(SteadyChecksquareHarmonicParityRuleComponent.class),
new AddDataTimeSlotCalculator(), mock(AddLedgerService.class), taskService,
mock(SteadyChecksquareItemService.class), mock(SteadyChecksquareStatSummaryService.class),
mock(SteadyChecksquareDetailService.class), new ObjectMapper());
SteadyChecksquareQueryParam param = new SteadyChecksquareQueryParam();
param.setIndicatorCodes(Collections.singletonList("V_RMS"));
SteadyChecksquareQueryVO result = new SteadyChecksquareQueryVO();
result.setLineId("line-001");
result.setLineName("杩涚嚎涓€");
result.setTimeStart("2026-05-01 00:00:00");
result.setTimeEnd("2026-05-01 00:01:00");
result.setIntervalMinutes(1);
SteadyChecksquareItemVO item = new SteadyChecksquareItemVO();
item.setItemKey("line-001|V_RMS");
item.setIndicatorCode("V_RMS");
item.setHasData(false);
item.setExpectedPointCount(2);
item.setActualPointCount(0);
item.setMissingPointCount(2);
item.setDataIntegrity(BigDecimal.ZERO.setScale(6));
item.setDataIntegrityText("0.00%");
item.setAbnormal(false);
item.setHarmonicParityAbnormal(false);
result.getItems().add(item);
saveResult(service, param, result);
ArgumentCaptor<SteadyChecksquareTaskPO> captor = ArgumentCaptor.forClass(SteadyChecksquareTaskPO.class);
verify(taskService).save(captor.capture());
Assertions.assertEquals(Integer.valueOf(1), captor.getValue().getAbnormalItemCount());
Assertions.assertEquals(BigDecimal.ZERO.setScale(6), captor.getValue().getMinDataIntegrity());
}
@Test
void shouldMarkAggregateHarmonicItemNoDataWhenDataIntegrityIsZero() {
SteadyChecksquareServiceImpl service = newService();
SteadyTrendIndicatorDefinitionBO indicator = buildHarmonicIndicator();
SteadyChecksquareItemVO orderItem = buildOrderItem(true, BigDecimal.ZERO.setScale(6));
orderItem.getStatSummaries().add(buildSummaryVO(true, BigDecimal.ZERO.setScale(6)));
SteadyChecksquareItemVO result = aggregateHarmonicItems(service, indicator, Collections.singletonList(orderItem));
Assertions.assertEquals(BigDecimal.ZERO.setScale(6), result.getDataIntegrity());
Assertions.assertEquals(Boolean.FALSE, result.getHasData());
Assertions.assertEquals(Boolean.FALSE, result.getStatSummaries().get(0).getHasData());
}
@Test
void shouldMarkAggregateHarmonicItemHasDataWhenDataIntegrityIsGreaterThanZero() {
SteadyChecksquareServiceImpl service = newService();
SteadyTrendIndicatorDefinitionBO indicator = buildHarmonicIndicator();
SteadyChecksquareItemVO orderItem = buildOrderItem(false, new BigDecimal("0.500000"));
orderItem.getStatSummaries().add(buildSummaryVO(false, new BigDecimal("0.500000")));
SteadyChecksquareItemVO result = aggregateHarmonicItems(service, indicator, Collections.singletonList(orderItem));
Assertions.assertEquals(new BigDecimal("0.500000"), result.getDataIntegrity());
Assertions.assertEquals(Boolean.TRUE, result.getHasData());
Assertions.assertEquals(Boolean.TRUE, result.getStatSummaries().get(0).getHasData());
}
private void assertItemInterval(SteadyChecksquareItemVO item, String indicatorCode, int intervalMinutes, int expectedPointCount) {
Assertions.assertEquals(indicatorCode, item.getIndicatorCode());
Assertions.assertEquals(Integer.valueOf(intervalMinutes), item.getIntervalMinutes());
Assertions.assertEquals(Integer.valueOf(expectedPointCount), item.getExpectedPointCount());
}
private SteadyChecksquareServiceImpl newService() {
return new SteadyChecksquareServiceImpl(new SteadyTrendIndicatorCatalog(),
mock(SteadyChecksquareInfluxQueryComponent.class), new SteadyChecksquareCalculator(),
mock(SteadyChecksquareValueOrderRuleComponent.class), mock(SteadyChecksquareHarmonicParityRuleComponent.class),
new AddDataTimeSlotCalculator(), mock(AddLedgerService.class), mock(SteadyChecksquareTaskService.class),
mock(SteadyChecksquareItemService.class), mock(SteadyChecksquareStatSummaryService.class),
mock(SteadyChecksquareDetailService.class), new ObjectMapper());
}
private SteadyChecksquareQueryVO calculate(SteadyChecksquareServiceImpl service, SteadyChecksquareQueryParam param) {
try {
Method method = SteadyChecksquareServiceImpl.class.getDeclaredMethod("calculate", SteadyChecksquareQueryParam.class);
method.setAccessible(true);
return (SteadyChecksquareQueryVO) method.invoke(service, param);
} catch (Exception exception) {
throw new RuntimeException(exception);
}
}
private void saveResult(SteadyChecksquareServiceImpl service, SteadyChecksquareQueryParam param, SteadyChecksquareQueryVO result) {
try {
Method method = SteadyChecksquareServiceImpl.class.getDeclaredMethod("saveResult",
SteadyChecksquareQueryParam.class, SteadyChecksquareQueryVO.class);
method.setAccessible(true);
method.invoke(service, param, result);
} catch (Exception exception) {
throw new RuntimeException(exception);
}
}
private SteadyChecksquareItemVO aggregateHarmonicItems(SteadyChecksquareServiceImpl service,
SteadyTrendIndicatorDefinitionBO indicator,
List<SteadyChecksquareItemVO> orderItems) {
try {
Method method = SteadyChecksquareServiceImpl.class.getDeclaredMethod("aggregateHarmonicItems",
String.class, SteadyTrendIndicatorDefinitionBO.class, List.class, int.class);
method.setAccessible(true);
return (SteadyChecksquareItemVO) method.invoke(service, "line-001", indicator, orderItems, 1);
} catch (Exception exception) {
throw new RuntimeException(exception);
}
}
private SteadyTrendIndicatorDefinitionBO buildHarmonicIndicator() {
SteadyTrendIndicatorDefinitionBO indicator = new SteadyTrendIndicatorDefinitionBO();
indicator.setIndicatorCode("V_HARMONIC");
indicator.setName("V_HARMONIC");
indicator.setHarmonic(true);
return indicator;
}
private SteadyChecksquareItemVO buildOrderItem(boolean hasData, BigDecimal dataIntegrity) {
SteadyChecksquareItemVO item = new SteadyChecksquareItemVO();
item.setItemKey("line-001|V_HARMONIC|2");
item.setIndicatorCode("V_HARMONIC");
item.setIndicatorName("V_HARMONIC");
item.setHarmonicOrder(2);
item.setIntervalMinutes(1);
item.setHasData(hasData);
item.setExpectedPointCount(2);
item.setActualPointCount(dataIntegrity.compareTo(BigDecimal.ZERO) > 0 ? 1 : 0);
item.setMissingPointCount(dataIntegrity.compareTo(BigDecimal.ZERO) > 0 ? 1 : 2);
item.setDataIntegrity(dataIntegrity);
item.setAbnormal(false);
item.setAbnormalPointCount(0);
item.setHarmonicParityAbnormal(false);
item.setHarmonicParityAbnormalPointCount(0);
return item;
}
private SteadyChecksquareStatSummaryVO buildSummaryVO(boolean hasData, BigDecimal dataIntegrity) {
SteadyChecksquareStatSummaryVO summary = new SteadyChecksquareStatSummaryVO();
summary.setStatType("AVG");
summary.setSupported(true);
summary.setHasData(hasData);
summary.setExpectedPointCount(2);
summary.setActualPointCount(dataIntegrity.compareTo(BigDecimal.ZERO) > 0 ? 1 : 0);
summary.setMissingPointCount(dataIntegrity.compareTo(BigDecimal.ZERO) > 0 ? 1 : 2);
summary.setDataIntegrity(dataIntegrity);
return summary;
}
private SteadyChecksquareItemPO buildItemPO(String itemId, String indicatorCode) {
SteadyChecksquareItemPO item = new SteadyChecksquareItemPO();
item.setId(itemId);
item.setIndicatorCode(indicatorCode);
item.setIndicatorName(indicatorCode);
item.setState(1);
item.setHasData(1);
item.setExpectedPointCount(1);
item.setActualPointCount(1);
item.setMissingPointCount(0);
item.setDataIntegrity(BigDecimal.ONE.setScale(6));
item.setAbnormal(0);
item.setAbnormalPointCount(0);
item.setHarmonicParityAbnormal(0);
item.setHarmonicParityAbnormalPointCount(0);
return item;
}
private SteadyChecksquareStatSummaryPO buildSummaryPO(String itemId, String statType) {
SteadyChecksquareStatSummaryPO summary = new SteadyChecksquareStatSummaryPO();
summary.setItemId(itemId);
summary.setStatType(statType);
summary.setSupported(1);
summary.setHasData(1);
summary.setExpectedPointCount(1);
summary.setActualPointCount(1);
summary.setMissingPointCount(0);
summary.setDataIntegrity(BigDecimal.ONE.setScale(6));
return summary;
}
private SteadyChecksquareValueOrderRuleVO emptyRuleResult() {
return new SteadyChecksquareValueOrderRuleVO();
}
private SteadyChecksquareHarmonicParityRuleVO emptyHarmonicParityRuleResult() {
return new SteadyChecksquareHarmonicParityRuleVO();
}
}

View File

@@ -13,7 +13,8 @@
<packaging>pom</packaging> <packaging>pom</packaging>
<modules> <modules>
<module>steady-DataView</module> <module>steady-dataView</module>
<module>check-square</module>
</modules> </modules>
<properties> <properties>

View File

@@ -9,7 +9,7 @@
<version>1.0.0</version> <version>1.0.0</version>
</parent> </parent>
<artifactId>steady-DataView</artifactId> <artifactId>steady-dataView</artifactId>
<dependencies> <dependencies>
<dependency> <dependency>

View File

@@ -1,201 +0,0 @@
package com.njcn.gather.steady.checksquare.component;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.njcn.common.pojo.enums.response.CommonResponseEnum;
import com.njcn.common.pojo.exception.BusinessException;
import com.njcn.gather.steady.datavie.config.SteadyInfluxDbProperties;
import com.njcn.gather.steady.datavie.pojo.bo.SteadyTrendResolvedFieldBO;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URLEncoder;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.util.HashSet;
import java.util.Set;
/**
* 数据校验 InfluxDB 查询组件。
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class SteadyChecksquareInfluxQueryComponent {
private static final DateTimeFormatter INFLUX_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss'Z'");
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
private final SteadyInfluxDbProperties properties;
public Set<LocalDateTime> queryExistingSlots(SteadyTrendResolvedFieldBO field, LocalDateTime startTime,
LocalDateTime endTime, int intervalMinutes) {
validateConfig();
String query = buildChecksquareQuery(field, startTime, endTime);
long startMillis = System.currentTimeMillis();
log.info("数据校验 InfluxDB 查询开始measurement={}field={}lineId={}phase={}statType={}query={}",
field.getMeasurement(), field.getField(), field.getLineId(), field.getPhase(), field.getStatType(), query);
try {
String body = executeQuery(query);
Set<LocalDateTime> slots = parseExistingSlots(body, intervalMinutes);
log.info("数据校验 InfluxDB 查询结束slotCount={}costMs={}", slots.size(), System.currentTimeMillis() - startMillis);
return slots;
} catch (RuntimeException ex) {
log.warn("数据校验 InfluxDB 查询异常costMs={}error={}", System.currentTimeMillis() - startMillis, ex.getMessage());
throw ex;
}
}
public String buildChecksquareQuery(SteadyTrendResolvedFieldBO field, LocalDateTime startTime, LocalDateTime endTime) {
StringBuilder sql = new StringBuilder();
sql.append("SELECT \"").append(field.getField()).append("\" AS \"value\"");
sql.append(" FROM \"").append(field.getMeasurement()).append("\"");
sql.append(" WHERE time >= '").append(INFLUX_TIME_FORMATTER.format(startTime)).append("'");
sql.append(" AND time <= '").append(INFLUX_TIME_FORMATTER.format(endTime)).append("'");
sql.append(" AND \"line_id\" = '").append(escapeTagValue(field.getLineId())).append("'");
sql.append(" AND \"phasic_type\" = '").append(escapeTagValue(field.getPhase())).append("'");
if (hasValueTypeTag(field.getMeasurement())) {
sql.append(" AND \"value_type\" = '").append(resolveValueType(field.getStatType())).append("'");
}
sql.append(" ORDER BY time ASC");
return sql.toString();
}
private Set<LocalDateTime> parseExistingSlots(String body, int intervalMinutes) {
try {
JsonNode root = OBJECT_MAPPER.readTree(body);
JsonNode values = root.path("results").path(0).path("series").path(0).path("values");
Set<LocalDateTime> result = new HashSet<LocalDateTime>();
if (!values.isArray()) {
return result;
}
for (JsonNode value : values) {
if (value.size() < 2 || value.get(1).isNull()) {
continue;
}
LocalDateTime time = parseInfluxTime(value.get(0).asText());
if (time != null) {
result.add(alignToPreviousSlot(time, intervalMinutes));
}
}
return result;
} catch (IOException ex) {
throw fail("InfluxDB 返回结果解析失败:" + ex.getMessage());
}
}
private LocalDateTime alignToPreviousSlot(LocalDateTime time, int intervalMinutes) {
LocalDateTime minuteFloor = time.withSecond(0).withNano(0);
int minuteOfDay = minuteFloor.getHour() * 60 + minuteFloor.getMinute();
int remainder = minuteOfDay % intervalMinutes;
return minuteFloor.minusMinutes(remainder);
}
private LocalDateTime parseInfluxTime(String value) {
try {
return OffsetDateTime.parse(value).withOffsetSameInstant(ZoneOffset.UTC).toLocalDateTime();
} catch (RuntimeException ex) {
return null;
}
}
private String executeQuery(String query) {
HttpURLConnection connection = null;
try {
URL url = new URL(buildQueryUrl(query));
connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("GET");
connection.setConnectTimeout(properties.getConnectTimeoutMs());
connection.setReadTimeout(properties.getReadTimeoutMs());
int status = connection.getResponseCode();
InputStream stream = status >= 200 && status < 300 ? connection.getInputStream() : connection.getErrorStream();
String body = readBody(stream);
if (status < 200 || status >= 300) {
throw fail("InfluxDB 查询失败:" + body);
}
return body;
} catch (IOException ex) {
throw fail("InfluxDB 查询异常:" + ex.getMessage());
} finally {
if (connection != null) {
connection.disconnect();
}
}
}
private String buildQueryUrl(String query) throws IOException {
StringBuilder url = new StringBuilder(trimRightSlash(properties.getUrl())).append("/query?");
url.append("db=").append(encode(properties.getDatabase()));
if (properties.getUsername() != null && !properties.getUsername().trim().isEmpty()) {
url.append("&u=").append(encode(properties.getUsername().trim()));
}
if (properties.getPassword() != null && !properties.getPassword().trim().isEmpty()) {
url.append("&p=").append(encode(properties.getPassword()));
}
url.append("&q=").append(encode(query));
return url.toString();
}
private void validateConfig() {
if (properties.getUrl() == null || properties.getUrl().trim().isEmpty()) {
throw fail("InfluxDB 地址未配置");
}
if (properties.getDatabase() == null || properties.getDatabase().trim().isEmpty()) {
throw fail("InfluxDB database 未配置");
}
}
private String readBody(InputStream stream) throws IOException {
if (stream == null) {
return "";
}
BufferedReader reader = new BufferedReader(new InputStreamReader(stream, StandardCharsets.UTF_8));
StringBuilder body = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
body.append(line);
}
return body.toString();
}
private String escapeTagValue(String value) {
return value == null ? "" : value.replace("\\", "\\\\").replace("'", "\\'");
}
private String resolveValueType(String statType) {
if (statType == null || statType.trim().isEmpty()) {
return "AVG";
}
return statType.trim().toUpperCase();
}
private boolean hasValueTypeTag(String measurement) {
return !"data_flicker".equals(measurement) && !"data_fluc".equals(measurement) && !"data_plt".equals(measurement);
}
private String trimRightSlash(String value) {
String text = value.trim();
while (text.endsWith("/")) {
text = text.substring(0, text.length() - 1);
}
return text;
}
private String encode(String value) throws IOException {
return URLEncoder.encode(value, StandardCharsets.UTF_8.name());
}
private BusinessException fail(String message) {
return new BusinessException(CommonResponseEnum.FAIL, message);
}
}

View File

@@ -1,43 +0,0 @@
package com.njcn.gather.steady.checksquare.controller;
import com.njcn.common.pojo.annotation.OperateInfo;
import com.njcn.common.pojo.enums.common.LogEnum;
import com.njcn.common.pojo.enums.response.CommonResponseEnum;
import com.njcn.common.pojo.response.HttpResult;
import com.njcn.common.utils.LogUtil;
import com.njcn.gather.steady.checksquare.pojo.param.SteadyChecksquareQueryParam;
import com.njcn.gather.steady.checksquare.pojo.vo.SteadyChecksquareQueryVO;
import com.njcn.gather.steady.checksquare.service.SteadyChecksquareService;
import com.njcn.web.controller.BaseController;
import com.njcn.web.utils.HttpResultUtil;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 数据校验接口。
*/
@Slf4j
@Api(tags = "数据校验")
@RestController
@RequestMapping("/steady/data-view/checksquare")
@RequiredArgsConstructor
public class SteadyChecksquareController extends BaseController {
private final SteadyChecksquareService checksquareService;
@OperateInfo(info = LogEnum.BUSINESS_COMMON)
@ApiOperation("查询数据校验结果")
@PostMapping("/query")
public HttpResult<SteadyChecksquareQueryVO> query(@RequestBody SteadyChecksquareQueryParam param) {
String methodDescribe = getMethodDescribe("query");
LogUtil.njcnDebug(log, "{}开始查询数据校验结果param={}", methodDescribe, param);
SteadyChecksquareQueryVO result = checksquareService.query(param);
return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, result, methodDescribe);
}
}

View File

@@ -1,12 +0,0 @@
package com.njcn.gather.steady.checksquare.service;
import com.njcn.gather.steady.checksquare.pojo.param.SteadyChecksquareQueryParam;
import com.njcn.gather.steady.checksquare.pojo.vo.SteadyChecksquareQueryVO;
/**
* 数据校验服务。
*/
public interface SteadyChecksquareService {
SteadyChecksquareQueryVO query(SteadyChecksquareQueryParam param);
}

View File

@@ -1,349 +0,0 @@
package com.njcn.gather.steady.checksquare.service.impl;
import com.njcn.common.pojo.enums.response.CommonResponseEnum;
import com.njcn.common.pojo.exception.BusinessException;
import com.njcn.gather.steady.checksquare.component.SteadyChecksquareCalculator;
import com.njcn.gather.steady.checksquare.component.SteadyChecksquareInfluxQueryComponent;
import com.njcn.gather.steady.checksquare.pojo.param.SteadyChecksquareQueryParam;
import com.njcn.gather.steady.checksquare.pojo.vo.SteadyChecksquareItemVO;
import com.njcn.gather.steady.checksquare.pojo.vo.SteadyChecksquareQueryVO;
import com.njcn.gather.steady.checksquare.pojo.vo.SteadyChecksquareSegmentVO;
import com.njcn.gather.steady.checksquare.pojo.vo.SteadyChecksquareStatDetailVO;
import com.njcn.gather.steady.checksquare.pojo.vo.SteadyChecksquareStatSummaryVO;
import com.njcn.gather.steady.checksquare.service.SteadyChecksquareService;
import com.njcn.gather.steady.datavie.component.SteadyTrendIndicatorCatalog;
import com.njcn.gather.steady.datavie.pojo.bo.SteadyTrendIndicatorDefinitionBO;
import com.njcn.gather.steady.datavie.pojo.bo.SteadyTrendResolvedFieldBO;
import com.njcn.gather.steady.datavie.pojo.bo.SteadyTrendSeriesFieldBO;
import com.njcn.gather.tool.adddata.component.AddDataTimeSlotCalculator;
import com.njcn.gather.tool.addledger.pojo.constant.AddLedgerConst;
import com.njcn.gather.tool.addledger.pojo.vo.AddLedgerLinePathVO;
import com.njcn.gather.tool.addledger.service.AddLedgerService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* 数据校验服务实现。
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class SteadyChecksquareServiceImpl implements SteadyChecksquareService {
private static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
private static final String EMPTY_TEXT = "-";
private static final int FLICKER_SHORT_INTERVAL_MINUTES = 10;
private static final int FLICKER_LONG_INTERVAL_MINUTES = 120;
private final SteadyTrendIndicatorCatalog indicatorCatalog;
private final SteadyChecksquareInfluxQueryComponent influxQueryComponent;
private final SteadyChecksquareCalculator calculator;
private final AddDataTimeSlotCalculator timeSlotCalculator;
private final AddLedgerService addLedgerService;
@Override
public SteadyChecksquareQueryVO query(SteadyChecksquareQueryParam param) {
validateParam(param);
String lineId = trimToNull(param.getLineId());
LocalDateTime startTime = parseRequiredTime(param.getTimeStart(), "开始时间不能为空");
LocalDateTime endTime = parseRequiredTime(param.getTimeEnd(), "结束时间不能为空");
if (startTime.isAfter(endTime)) {
throw fail("开始时间不能大于结束时间");
}
AddLedgerLinePathVO linePath = requireLinePath(lineId);
int intervalMinutes = resolveIntervalMinutes(linePath);
SteadyChecksquareQueryVO result = new SteadyChecksquareQueryVO();
result.setLineId(lineId);
result.setLineName(trimToNull(linePath.getLineName()) == null ? EMPTY_TEXT : linePath.getLineName());
result.setTimeStart(param.getTimeStart());
result.setTimeEnd(param.getTimeEnd());
result.setIntervalMinutes(intervalMinutes);
long startMillis = System.currentTimeMillis();
List<String> indicatorCodes = normalizeTextList(param.getIndicatorCodes());
List<Integer> harmonicOrders = normalizeHarmonicOrders(param.getHarmonicOrders());
log.info("数据校验查询开始lineId={}indicatorCount={}timeStart={}timeEnd={}intervalMinutes={}",
lineId, indicatorCodes.size(), startTime, endTime, intervalMinutes);
for (String indicatorCode : indicatorCodes) {
SteadyTrendIndicatorDefinitionBO indicator = requireIndicator(indicatorCode);
int itemIntervalMinutes = resolveIndicatorIntervalMinutes(indicator, intervalMinutes);
List<LocalDateTime> itemSlots = timeSlotCalculator.buildTimeSlots(startTime, endTime, itemIntervalMinutes);
result.getItems().addAll(buildIndicatorItems(lineId, indicator, harmonicOrders, startTime, endTime, itemSlots, itemIntervalMinutes));
}
log.info("数据校验查询结束lineId={}itemCount={}costMs={}", lineId, result.getItems().size(), System.currentTimeMillis() - startMillis);
return result;
}
private List<SteadyChecksquareItemVO> buildIndicatorItems(String lineId, SteadyTrendIndicatorDefinitionBO indicator,
List<Integer> harmonicOrders,
LocalDateTime startTime, LocalDateTime endTime,
List<LocalDateTime> slots, int intervalMinutes) {
List<SteadyChecksquareItemVO> result = new ArrayList<SteadyChecksquareItemVO>();
if (Boolean.TRUE.equals(indicator.getHarmonic())) {
for (Integer order : requireValidHarmonicOrders(indicator, harmonicOrders)) {
result.add(buildItem(lineId, indicator, order, startTime, endTime, slots, intervalMinutes));
}
return result;
}
result.add(buildItem(lineId, indicator, null, startTime, endTime, slots, intervalMinutes));
return result;
}
private SteadyChecksquareItemVO buildItem(String lineId, SteadyTrendIndicatorDefinitionBO indicator, Integer harmonicOrder,
LocalDateTime startTime, LocalDateTime endTime,
List<LocalDateTime> slots, int intervalMinutes) {
SteadyChecksquareItemVO item = new SteadyChecksquareItemVO();
item.setItemKey(buildItemKey(lineId, indicator, harmonicOrder));
item.setIndicatorCode(indicator.getIndicatorCode());
item.setIndicatorName(indicator.getName());
item.setHarmonicOrder(harmonicOrder);
item.setIntervalMinutes(intervalMinutes);
int totalExpected = 0;
int totalActual = 0;
int maxContinuousMissingMinutes = 0;
boolean hasData = false;
for (String statType : indicator.getSupportStats()) {
Set<LocalDateTime> actualSlots = queryMergedActualSlots(lineId, indicator, harmonicOrder, statType, startTime, endTime, intervalMinutes);
Set<LocalDateTime> effectiveActualSlots = retainExpectedSlots(slots, actualSlots);
List<SteadyChecksquareSegmentVO> segments = calculator.buildSegments(slots, effectiveActualSlots, intervalMinutes);
SteadyChecksquareStatSummaryVO summary = buildSummary(statType, slots.size(), effectiveActualSlots.size(), segments);
SteadyChecksquareStatDetailVO detail = buildDetail(statType, segments);
item.getStatSummaries().add(summary);
item.getStatDetails().add(detail);
totalExpected += summary.getExpectedPointCount();
totalActual += summary.getActualPointCount();
maxContinuousMissingMinutes = Math.max(maxContinuousMissingMinutes, summary.getMaxContinuousMissingMinutes());
hasData = hasData || Boolean.TRUE.equals(summary.getHasData());
}
item.setHasData(hasData);
item.setExpectedPointCount(totalExpected);
item.setActualPointCount(totalActual);
item.setMissingPointCount(Math.max(0, totalExpected - totalActual));
item.setMissingRate(calculateRate(item.getMissingPointCount(), totalExpected));
item.setMissingRateText(formatRateText(item.getMissingRate()));
item.setMaxContinuousMissingMinutes(maxContinuousMissingMinutes);
return item;
}
private Set<LocalDateTime> queryMergedActualSlots(String lineId, SteadyTrendIndicatorDefinitionBO indicator, Integer harmonicOrder,
String statType, LocalDateTime startTime, LocalDateTime endTime,
int intervalMinutes) {
Set<LocalDateTime> result = new HashSet<LocalDateTime>();
for (String phase : indicator.getPhaseCodes()) {
SteadyTrendResolvedFieldBO field = buildResolvedField(lineId, indicator, harmonicOrder, phase, statType);
result.addAll(influxQueryComponent.queryExistingSlots(field, startTime, endTime, intervalMinutes));
}
return result;
}
private Set<LocalDateTime> retainExpectedSlots(List<LocalDateTime> slots, Set<LocalDateTime> actualSlots) {
Set<LocalDateTime> result = new HashSet<LocalDateTime>();
if (slots == null || actualSlots == null || actualSlots.isEmpty()) {
return result;
}
for (LocalDateTime slot : slots) {
if (actualSlots.contains(slot)) {
result.add(slot);
}
}
return result;
}
private SteadyTrendResolvedFieldBO buildResolvedField(String lineId, SteadyTrendIndicatorDefinitionBO indicator,
Integer harmonicOrder, String phase, String statType) {
SteadyTrendResolvedFieldBO field = new SteadyTrendResolvedFieldBO();
field.setMeasurement(indicator.getTableName());
field.setField(resolveField(indicator, harmonicOrder));
field.setLineId(lineId);
field.setIndicatorCode(indicator.getIndicatorCode());
field.setIndicatorName(indicator.getName());
field.setPhase(phase);
field.setStatType(statType);
field.setUnit(indicator.getUnit());
return field;
}
private String resolveField(SteadyTrendIndicatorDefinitionBO indicator, Integer harmonicOrder) {
if (Boolean.TRUE.equals(indicator.getHarmonic())) {
return indicator.getHarmonicFieldPrefix() + "_" + harmonicOrder;
}
List<SteadyTrendSeriesFieldBO> fields = indicator.getSeriesFields();
if (fields == null || fields.isEmpty()) {
throw fail("稳态指标不支持:" + indicator.getIndicatorCode());
}
return fields.get(0).getField();
}
private SteadyChecksquareStatSummaryVO buildSummary(String statType, int expectedCount, int actualCount,
List<SteadyChecksquareSegmentVO> segments) {
SteadyChecksquareStatSummaryVO summary = new SteadyChecksquareStatSummaryVO();
summary.setStatType(statType);
summary.setSupported(true);
summary.setHasData(actualCount > 0);
summary.setExpectedPointCount(expectedCount);
summary.setActualPointCount(actualCount);
summary.setMissingPointCount(Math.max(0, expectedCount - actualCount));
summary.setMissingRate(calculateRate(summary.getMissingPointCount(), expectedCount));
summary.setMissingRateText(formatRateText(summary.getMissingRate()));
summary.setMaxContinuousMissingMinutes(calculator.maxContinuousMissingMinutes(segments));
return summary;
}
private SteadyChecksquareStatDetailVO buildDetail(String statType, List<SteadyChecksquareSegmentVO> segments) {
SteadyChecksquareStatDetailVO detail = new SteadyChecksquareStatDetailVO();
detail.setStatType(statType);
detail.setSupported(true);
detail.setSegments(segments);
return detail;
}
private String buildItemKey(String lineId, SteadyTrendIndicatorDefinitionBO indicator, Integer harmonicOrder) {
if (harmonicOrder == null) {
return lineId + "|" + indicator.getIndicatorCode();
}
return lineId + "|" + indicator.getIndicatorCode() + "|" + harmonicOrder;
}
private void validateParam(SteadyChecksquareQueryParam param) {
if (param == null) {
throw fail("数据校验参数不能为空");
}
if (trimToNull(param.getLineId()) == null) {
throw fail("监测点 ID 不能为空");
}
if (normalizeTextList(param.getIndicatorCodes()).isEmpty()) {
throw fail("指标不能为空");
}
parseRequiredTime(param.getTimeStart(), "开始时间不能为空");
parseRequiredTime(param.getTimeEnd(), "结束时间不能为空");
}
private LocalDateTime parseRequiredTime(String time, String emptyMessage) {
String text = trimToNull(time);
if (text == null) {
throw fail(emptyMessage);
}
try {
return LocalDateTime.parse(text, TIME_FORMATTER);
} catch (DateTimeParseException ex) {
throw fail("时间格式不正确,仅支持 yyyy-MM-dd HH:mm:ss");
}
}
private AddLedgerLinePathVO requireLinePath(String lineId) {
Map<String, AddLedgerLinePathVO> linePathMap = addLedgerService.listLinePathByLineIds(Collections.singletonList(lineId));
AddLedgerLinePathVO linePath = linePathMap.get(lineId);
if (linePath == null) {
throw fail("监测点不存在或不可用");
}
return linePath;
}
private int resolveIntervalMinutes(AddLedgerLinePathVO linePath) {
Integer interval = linePath.getLineInterval();
if (interval == null || interval <= 0) {
return AddLedgerConst.LINE_INTERVAL_DEFAULT;
}
return interval;
}
private int resolveIndicatorIntervalMinutes(SteadyTrendIndicatorDefinitionBO indicator, int lineIntervalMinutes) {
String indicatorCode = indicator == null ? null : indicator.getIndicatorCode();
if ("FLUC".equals(indicatorCode) || "PST".equals(indicatorCode)) {
return FLICKER_SHORT_INTERVAL_MINUTES;
}
if ("PLT".equals(indicatorCode)) {
return FLICKER_LONG_INTERVAL_MINUTES;
}
return lineIntervalMinutes;
}
private SteadyTrendIndicatorDefinitionBO requireIndicator(String indicatorCode) {
SteadyTrendIndicatorDefinitionBO indicator = indicatorCatalog.getIndicator(indicatorCode);
if (indicator == null) {
throw fail("稳态指标不支持:" + indicatorCode);
}
return indicator;
}
private BigDecimal calculateRate(int missingCount, int expectedCount) {
if (expectedCount <= 0) {
return BigDecimal.ZERO.setScale(6, RoundingMode.HALF_UP);
}
return new BigDecimal(missingCount).divide(new BigDecimal(expectedCount), 6, RoundingMode.HALF_UP);
}
private String formatRateText(BigDecimal rate) {
if (rate == null) {
return null;
}
return rate.multiply(new BigDecimal("100")).setScale(2, RoundingMode.HALF_UP).toPlainString() + "%";
}
private List<String> normalizeTextList(List<String> values) {
if (values == null || values.isEmpty()) {
return new ArrayList<String>();
}
Set<String> result = new LinkedHashSet<String>();
for (String value : values) {
String text = trimToNull(value);
if (text != null) {
result.add(text);
}
}
return new ArrayList<String>(result);
}
private List<Integer> normalizeHarmonicOrders(List<Integer> values) {
if (values == null || values.isEmpty()) {
return new ArrayList<Integer>();
}
List<Integer> result = new ArrayList<Integer>();
for (Integer value : values) {
if (value != null && !result.contains(value)) {
result.add(value);
}
}
return result;
}
private List<Integer> requireValidHarmonicOrders(SteadyTrendIndicatorDefinitionBO indicator, List<Integer> harmonicOrders) {
if (harmonicOrders == null || harmonicOrders.isEmpty()) {
throw fail("谐波次数不能为空");
}
for (Integer order : harmonicOrders) {
if (order < indicator.getHarmonicOrderStart() || order > indicator.getHarmonicOrderEnd()) {
throw fail("谐波次数只能在 " + indicator.getHarmonicOrderStart() + "" + indicator.getHarmonicOrderEnd() + " 之间");
}
}
return harmonicOrders;
}
private String trimToNull(String value) {
if (value == null) {
return null;
}
String trimmed = value.trim();
return trimmed.isEmpty() ? null : trimmed;
}
private BusinessException fail(String message) {
return new BusinessException(CommonResponseEnum.FAIL, message);
}
}

View File

@@ -0,0 +1,41 @@
-- 稳态数据查看建议索引。
-- 本脚本不自动执行,请按数据库现状审阅后单独执行。
CREATE INDEX idx_data_v_time_line_phase
ON data_v (TIMEID, LINEID, PHASIC_TYPE);
CREATE INDEX idx_data_i_time_line_phase
ON data_i (TIMEID, LINEID, PHASIC_TYPE);
CREATE INDEX idx_data_flicker_time_line_phase
ON data_flicker (TIMEID, LINEID, PHASIC_TYPE);
CREATE INDEX idx_data_fluc_time_line_phase
ON data_fluc (TIMEID, LINEID, PHASIC_TYPE);
CREATE INDEX idx_data_harmphasic_i_time_line_phase
ON data_harmphasic_i (TIMEID, LINEID, PHASIC_TYPE);
CREATE INDEX idx_data_harmphasic_v_time_line_phase
ON data_harmphasic_v (TIMEID, LINEID, PHASIC_TYPE);
CREATE INDEX idx_data_harmpower_p_time_line_phase
ON data_harmpower_p (TIMEID, LINEID, PHASIC_TYPE);
CREATE INDEX idx_data_harmpower_q_time_line_phase
ON data_harmpower_q (TIMEID, LINEID, PHASIC_TYPE);
CREATE INDEX idx_data_harmpower_s_time_line_phase
ON data_harmpower_s (TIMEID, LINEID, PHASIC_TYPE);
CREATE INDEX idx_data_harmrate_i_time_line_phase
ON data_harmrate_i (TIMEID, LINEID, PHASIC_TYPE);
CREATE INDEX idx_data_harmrate_v_time_line_phase
ON data_harmrate_v (TIMEID, LINEID, PHASIC_TYPE);
CREATE INDEX idx_data_inharm_i_time_line_phase
ON data_inharm_i (TIMEID, LINEID, PHASIC_TYPE);
CREATE INDEX idx_data_plt_time_line_phase
ON data_plt (TIMEID, LINEID, PHASIC_TYPE);

View File

@@ -0,0 +1,42 @@
-- 稳态模块菜单图标修正脚本。
-- 本脚本不自动执行,请按数据库现状审阅后单独执行。
UPDATE sys_function
SET Icon = 'DataAnalysis'
WHERE State = 1
AND Type = 0
AND (
Name = '稳态模块'
OR Code IN ('steady', 'steadyModule', 'steadyDataView')
OR Path IN ('/steady', '/steadyDataView', '/steady/data-view')
);
UPDATE sys_function
SET Icon = 'DataBoard'
WHERE State = 1
AND Type = 0
AND (
Name = '稳态数据'
OR Code IN ('steadyData', 'steadyDataDetail')
OR Path IN ('/steady/data', '/steady/data-view/detail', '/steadyDataView/index')
);
UPDATE sys_function
SET Icon = 'TrendCharts'
WHERE State = 1
AND Type = 0
AND (
Name = '稳态趋势'
OR Code IN ('steadyTrend', 'steadyDataTrend')
OR Path IN ('/steady/trend', '/steady/data-view/trend', '/steadyTrend/index')
);
UPDATE sys_function
SET Icon = 'CircleCheck'
WHERE State = 1
AND Type = 0
AND (
Name = '数据验证'
OR Code IN ('dataValidation', 'steadyDataValidation')
OR Path IN ('/steady/data-validation', '/steady/data-view/validation', '/dataValidation/index')
);

View File

@@ -1,36 +0,0 @@
package com.njcn.gather.steady.checksquare.component;
import com.njcn.gather.steady.datavie.config.SteadyInfluxDbProperties;
import com.njcn.gather.steady.datavie.pojo.bo.SteadyTrendResolvedFieldBO;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import java.time.LocalDateTime;
/**
* 数据校验 InfluxQL 构造契约测试。
*/
class SteadyChecksquareInfluxQueryComponentTest {
@Test
void shouldBuildChecksquareQueryWithoutQualityFlag() {
SteadyChecksquareInfluxQueryComponent component = new SteadyChecksquareInfluxQueryComponent(new SteadyInfluxDbProperties());
SteadyTrendResolvedFieldBO field = new SteadyTrendResolvedFieldBO();
field.setMeasurement("data_v");
field.setField("rms");
field.setLineId("line-001");
field.setPhase("A");
field.setStatType("AVG");
String query = component.buildChecksquareQuery(field,
LocalDateTime.of(2026, 5, 1, 0, 0, 0),
LocalDateTime.of(2026, 5, 1, 23, 59, 59));
Assertions.assertTrue(query.contains("SELECT \"rms\" AS \"value\""));
Assertions.assertTrue(query.contains("\"line_id\" = 'line-001'"));
Assertions.assertTrue(query.contains("\"phasic_type\" = 'A'"));
Assertions.assertTrue(query.contains("\"value_type\" = 'AVG'"));
Assertions.assertFalse(query.contains("quality_flag"));
Assertions.assertFalse(query.contains("GROUP BY time"));
}
}

View File

@@ -1,24 +0,0 @@
package com.njcn.gather.steady.checksquare.controller;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import java.lang.reflect.Method;
/**
* 数据校验接口契约测试。
*/
class SteadyChecksquareControllerTest {
@Test
void shouldExposeChecksquareQueryEndpointInSeparateController() throws Exception {
RequestMapping requestMapping = SteadyChecksquareController.class.getAnnotation(RequestMapping.class);
Assertions.assertArrayEquals(new String[]{"/steady/data-view/checksquare"}, requestMapping.value());
Method method = SteadyChecksquareController.class.getDeclaredMethod("query", com.njcn.gather.steady.checksquare.pojo.param.SteadyChecksquareQueryParam.class);
PostMapping postMapping = method.getAnnotation(PostMapping.class);
Assertions.assertArrayEquals(new String[]{"/query"}, postMapping.value());
}
}

View File

@@ -1,130 +0,0 @@
package com.njcn.gather.steady.checksquare.service.impl;
import com.njcn.gather.steady.checksquare.component.SteadyChecksquareCalculator;
import com.njcn.gather.steady.checksquare.component.SteadyChecksquareInfluxQueryComponent;
import com.njcn.gather.steady.checksquare.pojo.param.SteadyChecksquareQueryParam;
import com.njcn.gather.steady.checksquare.pojo.vo.SteadyChecksquareItemVO;
import com.njcn.gather.steady.checksquare.pojo.vo.SteadyChecksquareQueryVO;
import com.njcn.gather.steady.datavie.component.SteadyTrendIndicatorCatalog;
import com.njcn.gather.tool.adddata.component.AddDataTimeSlotCalculator;
import com.njcn.gather.tool.addledger.pojo.vo.AddLedgerLinePathVO;
import com.njcn.gather.tool.addledger.service.AddLedgerService;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
/**
* 数据校验服务测试。
*/
class SteadyChecksquareServiceImplTest {
@Test
void shouldUseFixedFlickerIntervalsPerIndicator() {
SteadyChecksquareInfluxQueryComponent influxQueryComponent = mock(SteadyChecksquareInfluxQueryComponent.class);
AddLedgerService addLedgerService = mock(AddLedgerService.class);
SteadyChecksquareServiceImpl service = new SteadyChecksquareServiceImpl(new SteadyTrendIndicatorCatalog(),
influxQueryComponent, new SteadyChecksquareCalculator(), new AddDataTimeSlotCalculator(), addLedgerService);
AddLedgerLinePathVO linePath = new AddLedgerLinePathVO();
linePath.setLineId("line-001");
linePath.setLineName("进线一");
linePath.setLineInterval(1);
when(addLedgerService.listLinePathByLineIds(eq(Collections.singletonList("line-001"))))
.thenReturn(Collections.singletonMap("line-001", linePath));
when(influxQueryComponent.queryExistingSlots(any(), any(LocalDateTime.class), any(LocalDateTime.class), eq(10)))
.thenReturn(new HashSet<LocalDateTime>(Arrays.asList(
LocalDateTime.of(2026, 5, 1, 0, 0),
LocalDateTime.of(2026, 5, 1, 0, 10))));
when(influxQueryComponent.queryExistingSlots(any(), any(LocalDateTime.class), any(LocalDateTime.class), eq(120)))
.thenReturn(new HashSet<LocalDateTime>(Collections.singletonList(
LocalDateTime.of(2026, 5, 1, 0, 0))));
SteadyChecksquareQueryParam param = new SteadyChecksquareQueryParam();
param.setLineId("line-001");
param.setIndicatorCodes(Arrays.asList("FLUC", "PST", "PLT"));
param.setTimeStart("2026-05-01 00:00:00");
param.setTimeEnd("2026-05-01 02:00:00");
SteadyChecksquareQueryVO result = service.query(param);
Assertions.assertEquals(Integer.valueOf(1), result.getIntervalMinutes());
Assertions.assertEquals(3, result.getItems().size());
assertItemInterval(result.getItems().get(0), "FLUC", 10, 13);
assertItemInterval(result.getItems().get(1), "PST", 10, 13);
assertItemInterval(result.getItems().get(2), "PLT", 120, 2);
}
@Test
void shouldOnlyQueryRequestedHarmonicOrders() {
SteadyChecksquareInfluxQueryComponent influxQueryComponent = mock(SteadyChecksquareInfluxQueryComponent.class);
AddLedgerService addLedgerService = mock(AddLedgerService.class);
SteadyChecksquareServiceImpl service = new SteadyChecksquareServiceImpl(new SteadyTrendIndicatorCatalog(),
influxQueryComponent, new SteadyChecksquareCalculator(), new AddDataTimeSlotCalculator(), addLedgerService);
AddLedgerLinePathVO linePath = new AddLedgerLinePathVO();
linePath.setLineId("line-001");
linePath.setLineName("进线一");
linePath.setLineInterval(1);
when(addLedgerService.listLinePathByLineIds(eq(Collections.singletonList("line-001"))))
.thenReturn(Collections.singletonMap("line-001", linePath));
when(influxQueryComponent.queryExistingSlots(any(), any(LocalDateTime.class), any(LocalDateTime.class), eq(1)))
.thenReturn(new HashSet<LocalDateTime>(Collections.singletonList(
LocalDateTime.of(2026, 5, 1, 0, 0))));
SteadyChecksquareQueryParam param = new SteadyChecksquareQueryParam();
param.setLineId("line-001");
param.setIndicatorCodes(Collections.singletonList("V_HARMONIC"));
param.setHarmonicOrders(Collections.singletonList(5));
param.setTimeStart("2026-05-01 00:00:00");
param.setTimeEnd("2026-05-01 00:01:00");
SteadyChecksquareQueryVO result = service.query(param);
Assertions.assertEquals(1, result.getItems().size());
Assertions.assertEquals(Integer.valueOf(5), result.getItems().get(0).getHarmonicOrder());
}
@Test
void shouldKeepRequestedHarmonicOrdersDistinctAndOrdered() {
SteadyChecksquareInfluxQueryComponent influxQueryComponent = mock(SteadyChecksquareInfluxQueryComponent.class);
AddLedgerService addLedgerService = mock(AddLedgerService.class);
SteadyChecksquareServiceImpl service = new SteadyChecksquareServiceImpl(new SteadyTrendIndicatorCatalog(),
influxQueryComponent, new SteadyChecksquareCalculator(), new AddDataTimeSlotCalculator(), addLedgerService);
AddLedgerLinePathVO linePath = new AddLedgerLinePathVO();
linePath.setLineId("line-001");
linePath.setLineName("进线一");
linePath.setLineInterval(1);
when(addLedgerService.listLinePathByLineIds(eq(Collections.singletonList("line-001"))))
.thenReturn(Collections.singletonMap("line-001", linePath));
when(influxQueryComponent.queryExistingSlots(any(), any(LocalDateTime.class), any(LocalDateTime.class), eq(1)))
.thenReturn(new HashSet<LocalDateTime>());
SteadyChecksquareQueryParam param = new SteadyChecksquareQueryParam();
param.setLineId("line-001");
param.setIndicatorCodes(Collections.singletonList("V_HARMONIC"));
param.setHarmonicOrders(Arrays.asList(7, 5, 7));
param.setTimeStart("2026-05-01 00:00:00");
param.setTimeEnd("2026-05-01 00:01:00");
SteadyChecksquareQueryVO result = service.query(param);
List<SteadyChecksquareItemVO> items = result.getItems();
Assertions.assertEquals(2, items.size());
Assertions.assertEquals(Integer.valueOf(7), items.get(0).getHarmonicOrder());
Assertions.assertEquals(Integer.valueOf(5), items.get(1).getHarmonicOrder());
}
private void assertItemInterval(SteadyChecksquareItemVO item, String indicatorCode, int intervalMinutes, int expectedPointCount) {
Assertions.assertEquals(indicatorCode, item.getIndicatorCode());
Assertions.assertEquals(Integer.valueOf(intervalMinutes), item.getIntervalMinutes());
Assertions.assertEquals(Integer.valueOf(expectedPointCount), item.getExpectedPointCount());
}
}

View File

@@ -2,13 +2,100 @@
## 模块定位 ## 模块定位
`dbms``system-ops` 下的数据库监控模块,当前先提供数据库监控菜单对应的后端基础入口 `dbms``system-ops` 下的数据库运维模块,当前支持 Oracle、MySQL 两类数据库运维能力,其中 Oracle 支持 `DATA_PUMP``JDBC_EXPORT`MySQL 当前支持 `JDBC_EXPORT`
## 当前接口 ## 当前接口
- `GET /database/overview` - `GET /database/overview`
- 查询数据库监控基础信息。 - 查询数据库运维概览信息。
- `POST /database/connections/list`
- 查询数据库连接配置。
- `POST /database/connections/add`
- 新增 Oracle 数据库连接配置。
- `POST /database/connections/update`
- 修改 Oracle 数据库连接配置。
- `POST /database/connections/delete`
- 删除数据库连接配置。
- `POST /database/connections/test`
- 测试 Oracle 数据库连接。
- `POST /database/connections/tables`
- 查询 Oracle 表列表。
- `POST /database/backups/create`
- 创建备份任务,默认使用 `DATA_PUMP`,可选 `JDBC_EXPORT`
- `POST /database/backups/tasks/list`
- 查询备份任务列表。
- `GET /database/backups/tasks/status`
- 查询任务状态。
- `POST /database/backups/files/list`
- 查询备份文件记录。
- `POST /database/restores/create`
- 创建恢复任务。
- `GET /database/restores/tasks/status`
- 查询恢复任务状态。
- `POST /database/delete/backup-file`
- 删除备份文件,要求 `confirmText=确认删除`
- `POST /database/delete/task`
- 删除任务记录,要求 `confirmText=确认删除`
## 数据脚本
- `src/main/resources/sql/system-ops/system-ops-init.sql`
- 系统运维菜单初始化脚本。
- `src/main/resources/sql/system-ops/dbms-database-ops-init.sql`
- 数据库运维连接、任务、备份文件和恢复记录表结构。
## 配置项
建议通过环境配置覆盖:
```yaml
dbms:
backup:
storage-path: D:/dbms-backup
default-max-file-size-mb: 512
mysql-fetch-size: 1000
tools:
expdp-path:
impdp-path:
```
说明:
- `backup.storage-path`
- `JDBC_EXPORT` 生成的 CSV 和元数据 JSON 的受管根目录。
- `backup.default-max-file-size-mb`
- MySQL `JDBC_EXPORT` 默认分片大小,前端可通过 `maxFileSizeMb` 覆盖,默认 512MB。
- `backup.mysql-fetch-size`
- MySQL `JDBC_EXPORT` 流式读取批量大小,默认 1000。
- `tools.expdp-path``tools.impdp-path`
- Oracle Data Pump 工具路径;为空时尝试走系统 `PATH`
## 当前行为
- 当前能力矩阵如下:
| 数据库类型 | 连接测试 | 表列表 | JDBC_EXPORT | DATA_PUMP |
| --- | --- | --- | --- | --- |
| ORACLE | 支持 | 支持 | 支持 | 支持 |
| MYSQL | 支持 | 支持 | 支持 | 不支持 |
- 备份和恢复只允许基于已保存且连接可用的连接配置发起。
- 新增连接前的测试接口仍可传 `temporaryPassword` 做临时连通性测试。
- 备份任务异步执行,只有实际文件生成成功后才会写入 `dbms_backup_file` 记录。
- `JDBC_EXPORT` 当前会生成两类文件:
- MySQL `JDBC_EXPORT` 会按任务号创建独立备份目录,每张表独立 CSV默认按 512MB 分片:
- 数据分片文件:`<table>_part001_yyyyMMdd_<taskNo>.csv`
- 元数据文件:`mysql_jdbc_export_metadata_yyyyMMdd_<taskNo>.json`
- 备份任务支持停止和重新开始:
- `POST /database/backups/tasks/stop`
- `POST /database/backups/tasks/restart`
- `JDBC_EXPORT` 恢复依赖元数据文件,不再允许缺少元数据直接发起恢复。
- 删除备份文件时,会校验目标路径必须位于受管备份目录下,避免误删非备份文件。
## 当前限制 ## 当前限制
- 当前只完成后端基础入口,不包含真实数据库连接状态、容量或性能指标探测逻辑 - `DATA_PUMP` 仍依赖部署机器可执行 `expdp``impdp`,并且 Oracle 侧已准备好 `directory` 对象和权限
- 当前代码要求 `DATA_PUMP` 连接配置里补齐可管理的 `directoryPath`,否则虽然 Oracle 端可能已导出成功,后端无法安全管理文件记录与删除。
- `JDBC_EXPORT` 恢复当前仅覆盖表数据,不承诺恢复索引、约束、触发器、序列、存储过程、权限等数据库对象。
- `TIME_RANGE` 模式当前只在 `JDBC_EXPORT` 场景真正参与查询过滤;`DATA_PUMP` 尚未接入 Oracle `QUERY` 参数。
- MySQL `JDBC_EXPORT` 已实现按大小分片Oracle `JDBC_EXPORT` 仍沿用原单文件导出路径。
- 本轮仅完成代码路径和文档收口,未执行 `mvn` 编译、测试或真实库联调。

View File

@@ -26,5 +26,19 @@
<artifactId>spingboot2.3.12</artifactId> <artifactId>spingboot2.3.12</artifactId>
<version>2.3.12</version> <version>2.3.12</version>
</dependency> </dependency>
<dependency>
<groupId>com.njcn</groupId>
<artifactId>mybatis-plus</artifactId>
<version>0.0.1</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
</dependency>
<dependency>
<groupId>com.oracle</groupId>
<artifactId>ojdbc6</artifactId>
<version>11.2.0.3</version>
</dependency>
</dependencies> </dependencies>
</project> </project>

View File

@@ -0,0 +1,111 @@
package com.njcn.gather.systemops.database.component;
import cn.hutool.core.util.StrUtil;
import com.njcn.gather.systemops.database.config.DbmsProperties;
import com.njcn.gather.systemops.database.pojo.po.DatabaseConnection;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.List;
/**
* Oracle Data Pump 命令执行组件。
*/
@Component
@RequiredArgsConstructor
public class DataPumpCommandExecutor {
private final DbmsProperties dbmsProperties;
public CommandResult expdp(DatabaseConnection connection, String password, String directoryName,
String dumpFileName, String logFileName, List<String> tableNames) {
List<String> command = new ArrayList<>();
command.add(resolveTool(dbmsProperties.getTools().getExpdpPath(), "expdp"));
fillCommonArgs(command, connection, password, directoryName, dumpFileName, logFileName);
if (tableNames != null && !tableNames.isEmpty()) {
command.add("tables=" + connection.getSchemaName() + "." + String.join("," + connection.getSchemaName() + ".", tableNames));
}
return execute(command);
}
public CommandResult impdp(DatabaseConnection connection, String password, String directoryName,
String dumpFileName, String logFileName, String tableExistsAction) {
List<String> command = new ArrayList<>();
command.add(resolveTool(dbmsProperties.getTools().getImpdpPath(), "impdp"));
fillCommonArgs(command, connection, password, directoryName, dumpFileName, logFileName);
if (StrUtil.isNotBlank(tableExistsAction)) {
command.add("table_exists_action=" + tableExistsAction);
}
return execute(command);
}
private void fillCommonArgs(List<String> command, DatabaseConnection connection, String password,
String directoryName, String dumpFileName, String logFileName) {
command.add(connection.getUsername() + "/" + password + "@" + buildConnectIdentifier(connection));
command.add("directory=" + directoryName);
command.add("dumpfile=" + dumpFileName);
command.add("logfile=" + logFileName);
}
private String buildConnectIdentifier(DatabaseConnection connection) {
if (StrUtil.isNotBlank(connection.getServiceName())) {
return "//" + connection.getHost() + ":" + connection.getPort() + "/" + connection.getServiceName();
}
return connection.getHost() + ":" + connection.getPort() + ":" + connection.getSid();
}
private String resolveTool(String configuredPath, String defaultName) {
return StrUtil.blankToDefault(configuredPath, defaultName);
}
private CommandResult execute(List<String> command) {
CommandResult result = new CommandResult();
result.setCommand(maskPassword(command));
try {
Process process = new ProcessBuilder(command).redirectErrorStream(true).start();
StringBuilder output = new StringBuilder();
try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream(), Charset.defaultCharset()))) {
String line;
while ((line = reader.readLine()) != null) {
output.append(line).append(System.lineSeparator());
}
}
int exitCode = process.waitFor();
result.setExitCode(exitCode);
result.setOutput(output.toString());
result.setSuccess(exitCode == 0);
} catch (Exception exception) {
result.setExitCode(-1);
result.setOutput(exception.getMessage());
result.setSuccess(false);
}
return result;
}
private String maskPassword(List<String> command) {
if (command.size() < 2) {
return String.join(" ", command);
}
List<String> masked = new ArrayList<>(command);
String credential = masked.get(1);
int slashIndex = credential.indexOf('/');
int atIndex = credential.indexOf('@');
if (slashIndex > 0 && atIndex > slashIndex) {
masked.set(1, credential.substring(0, slashIndex + 1) + "******" + credential.substring(atIndex));
}
return String.join(" ", masked);
}
@Data
public static class CommandResult {
private Boolean success;
private Integer exitCode;
private String command;
private String output;
}
}

View File

@@ -0,0 +1,28 @@
package com.njcn.gather.systemops.database.component;
import cn.hutool.core.util.StrUtil;
import org.springframework.stereotype.Component;
/**
* 数据库连接密码处理组件。
*/
@Component
public class DatabasePasswordComponent {
public String encrypt(String plainText) {
if (StrUtil.isBlank(plainText)) {
return null;
}
return plainText;
}
/**
* 优先使用本次请求传入的临时密码;否则复用已保存的数据库密码。
*/
public String resolveRuntimePassword(String passwordCipher, String temporaryPassword) {
if (StrUtil.isNotBlank(temporaryPassword)) {
return temporaryPassword;
}
return StrUtil.isBlank(passwordCipher) ? null : passwordCipher;
}
}

View File

@@ -0,0 +1,541 @@
package com.njcn.gather.systemops.database.component;
import cn.hutool.core.util.StrUtil;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.njcn.gather.systemops.database.constant.DatabaseOpsConst;
import com.njcn.gather.systemops.database.pojo.param.DatabaseBackupParam;
import com.njcn.gather.systemops.database.util.DatabaseFileNameUtil;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.Statement;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.regex.Pattern;
import java.util.function.BooleanSupplier;
/**
* JDBC 表数据导出与恢复组件。
*/
@Component
@RequiredArgsConstructor
public class JdbcExportComponent {
private static final Pattern IDENTIFIER_PATTERN = Pattern.compile("^[A-Za-z][A-Za-z0-9_#$]*$");
private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
private final ObjectMapper objectMapper;
public void exportCsv(Connection jdbcConnection, String ownerName, DatabaseBackupParam.CreateParam param,
Path dataFilePath, Path metadataFilePath) throws Exception {
Files.createDirectories(dataFilePath.getParent());
if (metadataFilePath.getParent() != null) {
Files.createDirectories(metadataFilePath.getParent());
}
List<TableMetadata> metadataList = new ArrayList<>();
try (BufferedWriter writer = Files.newBufferedWriter(dataFilePath, StandardCharsets.UTF_8)) {
for (String tableName : param.getTargetNames()) {
metadataList.add(exportTable(jdbcConnection, ownerName, tableName, param, writer));
}
}
try (BufferedWriter metadataWriter = Files.newBufferedWriter(metadataFilePath, StandardCharsets.UTF_8)) {
objectMapper.writeValue(metadataWriter, metadataList);
}
}
public void importCsv(Connection jdbcConnection, Path dataFilePath, Path metadataFilePath, String dbType,
String restoreMode, String targetOwnerName) throws Exception {
String metadataText = new String(Files.readAllBytes(metadataFilePath), StandardCharsets.UTF_8);
if (metadataText.trim().startsWith("{")) {
importCsvV2(jdbcConnection, metadataFilePath, dbType, restoreMode, targetOwnerName);
return;
}
List<TableMetadata> metadataList = Arrays.asList(objectMapper.readValue(metadataFilePath.toFile(), TableMetadata[].class));
Map<String, TableMetadata> metadataMap = new LinkedHashMap<>();
for (TableMetadata metadata : metadataList) {
metadataMap.put(metadata.getFullTableName(), metadata);
}
jdbcConnection.setAutoCommit(false);
try (BufferedReader reader = Files.newBufferedReader(dataFilePath, StandardCharsets.UTF_8)) {
try {
String line;
TableMetadata currentMetadata = null;
List<String> currentColumns = null;
while ((line = reader.readLine()) != null) {
if (line.startsWith("-- TABLE ")) {
currentMetadata = metadataMap.get(line.substring("-- TABLE ".length()).trim());
if (currentMetadata == null) {
throw new IllegalArgumentException("未找到表元数据:" + line);
}
currentColumns = null;
prepareTargetTable(jdbcConnection, currentMetadata, dbType, restoreMode, targetOwnerName);
continue;
}
if (currentMetadata == null) {
continue;
}
if (currentColumns == null) {
currentColumns = parseCsvLine(line);
continue;
}
List<String> values = parseCsvLine(line);
insertRow(jdbcConnection, currentMetadata, currentColumns, values, dbType, restoreMode, targetOwnerName);
}
jdbcConnection.commit();
} catch (Exception exception) {
jdbcConnection.rollback();
throw exception;
}
}
}
public ExportManifest exportMysqlCsvV2(Connection jdbcConnection, String databaseName, String taskNo,
DatabaseBackupParam.CreateParam param, Path backupDirectory,
Path metadataFilePath, int fetchSize, long maxPartBytes,
BooleanSupplier cancelled) throws Exception {
Files.createDirectories(backupDirectory);
ExportManifest manifest = new ExportManifest();
manifest.setVersion(2);
manifest.setDbType("MYSQL");
manifest.setBackupStrategy("JDBC_EXPORT");
manifest.setTaskNo(taskNo);
manifest.setDatabaseName(databaseName);
List<TableExportMetadata> tableMetadataList = new ArrayList<>();
manifest.setTables(tableMetadataList);
for (String tableName : param.getTargetNames()) {
checkCancelled(cancelled, backupDirectory);
tableMetadataList.add(exportMysqlTableV2(jdbcConnection, tableName, param, backupDirectory, taskNo,
fetchSize, maxPartBytes, cancelled));
}
try (BufferedWriter metadataWriter = Files.newBufferedWriter(metadataFilePath, StandardCharsets.UTF_8)) {
objectMapper.writeValue(metadataWriter, manifest);
}
return manifest;
}
private TableMetadata exportTable(Connection connection, String ownerName, String tableName,
DatabaseBackupParam.CreateParam param, BufferedWriter writer) throws Exception {
String normalizedOwner = normalizeOwner(ownerName);
String normalizedTable = normalizeMysqlIdentifier(tableName);
String fullTableName = buildFullTableName(normalizedOwner, normalizedTable);
String querySql = buildQuerySql(fullTableName, param);
TableMetadata metadata = new TableMetadata();
metadata.setOwnerName(normalizedOwner);
metadata.setTableName(normalizedTable);
metadata.setFullTableName(fullTableName);
metadata.setTimeColumn(StrUtil.isBlank(param.getTimeColumn()) ? null : normalizeMysqlIdentifier(param.getTimeColumn()));
metadata.setStartTime(param.getStartTime() == null ? null : param.getStartTime().format(DATE_TIME_FORMATTER));
metadata.setEndTime(param.getEndTime() == null ? null : param.getEndTime().format(DATE_TIME_FORMATTER));
writer.write("-- TABLE " + fullTableName);
writer.newLine();
try (PreparedStatement statement = connection.prepareStatement(querySql)) {
fillQueryParams(statement, param);
try (ResultSet resultSet = statement.executeQuery()) {
ResultSetMetaData resultSetMetaData = resultSet.getMetaData();
int columnCount = resultSetMetaData.getColumnCount();
List<String> columnNames = new ArrayList<>();
List<String> columnTypes = new ArrayList<>();
for (int i = 1; i <= columnCount; i++) {
String columnName = resultSetMetaData.getColumnName(i);
columnNames.add(normalizeMysqlIdentifier(columnName));
columnTypes.add(resultSetMetaData.getColumnTypeName(i));
if (i > 1) {
writer.write(",");
}
writer.write(escape(columnName));
}
writer.newLine();
long rowCount = 0L;
while (resultSet.next()) {
for (int i = 1; i <= columnCount; i++) {
if (i > 1) {
writer.write(",");
}
writer.write(escape(resultSet.getString(i)));
}
writer.newLine();
rowCount++;
}
metadata.setColumnNames(columnNames);
metadata.setColumnTypes(columnTypes);
metadata.setRowCount(rowCount);
return metadata;
}
}
}
private TableExportMetadata exportMysqlTableV2(Connection connection, String tableName,
DatabaseBackupParam.CreateParam param, Path backupDirectory,
String taskNo, int fetchSize, long maxPartBytes,
BooleanSupplier cancelled) throws Exception {
String normalizedTable = normalizeIdentifier(tableName);
String querySql = buildQuerySql(normalizedTable, param);
TableExportMetadata metadata = new TableExportMetadata();
metadata.setTableName(normalizedTable);
metadata.setFullTableName(normalizedTable);
metadata.setTimeColumn(StrUtil.isBlank(param.getTimeColumn()) ? null : normalizeIdentifier(param.getTimeColumn()));
metadata.setStartTime(param.getStartTime() == null ? null : param.getStartTime().format(DATE_TIME_FORMATTER));
metadata.setEndTime(param.getEndTime() == null ? null : param.getEndTime().format(DATE_TIME_FORMATTER));
metadata.setColumns(new ArrayList<>());
metadata.setParts(new ArrayList<>());
try (PreparedStatement statement = connection.prepareStatement(querySql, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY)) {
statement.setFetchSize(fetchSize);
fillQueryParams(statement, param);
try (ResultSet resultSet = statement.executeQuery()) {
ResultSetMetaData resultSetMetaData = resultSet.getMetaData();
int columnCount = resultSetMetaData.getColumnCount();
List<String> columnNames = new ArrayList<>();
for (int i = 1; i <= columnCount; i++) {
String columnName = resultSetMetaData.getColumnName(i);
columnNames.add(columnName);
ColumnMetadata columnMetadata = new ColumnMetadata();
columnMetadata.setName(columnName);
columnMetadata.setType(resultSetMetaData.getColumnTypeName(i));
metadata.getColumns().add(columnMetadata);
}
PartWriter partWriter = openPartWriter(backupDirectory, normalizedTable, taskNo,
metadata.getParts().size() + 1, columnNames);
metadata.getParts().add(partWriter.getPart());
long totalRows = 0L;
try {
while (resultSet.next()) {
checkCancelled(cancelled, backupDirectory);
if (partWriter.shouldRotate(maxPartBytes)) {
partWriter.close();
partWriter = openPartWriter(backupDirectory, normalizedTable, taskNo,
metadata.getParts().size() + 1, columnNames);
metadata.getParts().add(partWriter.getPart());
}
partWriter.writeRow(resultSet, columnCount);
totalRows++;
}
} finally {
partWriter.close();
}
metadata.setRowCount(totalRows);
return metadata;
}
}
}
private String buildQuerySql(String fullTableName, DatabaseBackupParam.CreateParam param) {
StringBuilder sql = new StringBuilder("SELECT * FROM ").append(fullTableName);
if (param.getStartTime() != null && param.getEndTime() != null && StrUtil.isNotBlank(param.getTimeColumn())) {
sql.append(" WHERE ").append(normalizeIdentifier(param.getTimeColumn())).append(" BETWEEN ? AND ?");
}
return sql.toString();
}
private void fillQueryParams(PreparedStatement statement, DatabaseBackupParam.CreateParam param) throws Exception {
if (param.getStartTime() != null && param.getEndTime() != null && StrUtil.isNotBlank(param.getTimeColumn())) {
statement.setString(1, param.getStartTime().format(DATE_TIME_FORMATTER));
statement.setString(2, param.getEndTime().format(DATE_TIME_FORMATTER));
}
}
private void prepareTargetTable(Connection connection, TableMetadata metadata, String dbType, String restoreMode,
String targetOwnerName) throws Exception {
if (!"TRUNCATE".equalsIgnoreCase(restoreMode)
&& !("REPLACE".equalsIgnoreCase(restoreMode) && !isMysql(dbType))) {
return;
}
String fullTargetName = buildTargetTableName(metadata, targetOwnerName);
try (Statement statement = connection.createStatement()) {
statement.execute("TRUNCATE TABLE " + fullTargetName);
}
}
private void insertRow(Connection connection, TableMetadata metadata, List<String> columns,
List<String> values, String dbType, String restoreMode, String targetOwnerName) throws Exception {
String fullTargetName = buildTargetTableName(metadata, targetOwnerName);
StringBuilder placeholders = new StringBuilder();
for (int i = 0; i < columns.size(); i++) {
if (i > 0) {
placeholders.append(",");
}
placeholders.append("?");
}
String sql = buildInsertSql(dbType, restoreMode, fullTargetName, columns, placeholders.toString());
try (PreparedStatement statement = connection.prepareStatement(sql)) {
for (int i = 0; i < columns.size(); i++) {
statement.setString(i + 1, i < values.size() ? values.get(i) : null);
}
statement.executeUpdate();
}
}
private String buildInsertSql(String dbType, String restoreMode, String fullTargetName, List<String> columns,
String placeholders) {
String command = "INSERT INTO";
if (isMysql(dbType) && "SKIP".equalsIgnoreCase(restoreMode)) {
// MySQL 跳过重复主键行,避免普通恢复因历史数据重复而整体失败。
command = "INSERT IGNORE INTO";
} else if (isMysql(dbType) && "REPLACE".equalsIgnoreCase(restoreMode)) {
command = "REPLACE INTO";
}
return command + " " + fullTargetName + " (" + String.join(",", columns) + ") VALUES (" + placeholders + ")";
}
private void importCsvV2(Connection jdbcConnection, Path metadataFilePath, String dbType, String restoreMode,
String targetOwnerName) throws Exception {
ExportManifest manifest = objectMapper.readValue(metadataFilePath.toFile(), ExportManifest.class);
jdbcConnection.setAutoCommit(false);
try {
for (TableExportMetadata tableMetadata : manifest.getTables()) {
prepareTargetTable(jdbcConnection, toLegacyMetadata(tableMetadata), dbType, restoreMode, targetOwnerName);
for (FilePartMetadata part : tableMetadata.getParts()) {
importPart(jdbcConnection, metadataFilePath.getParent(), tableMetadata, part, dbType, restoreMode, targetOwnerName);
}
}
jdbcConnection.commit();
} catch (Exception exception) {
jdbcConnection.rollback();
throw exception;
}
}
private void importPart(Connection jdbcConnection, Path backupDirectory, TableExportMetadata tableMetadata,
FilePartMetadata part, String dbType, String restoreMode, String targetOwnerName) throws Exception {
Path partPath = backupDirectory.resolve(part.getFileName()).normalize();
if (!partPath.startsWith(backupDirectory.normalize())) {
throw new IllegalArgumentException("备份分片路径不在元数据目录内:" + part.getFileName());
}
try (BufferedReader reader = Files.newBufferedReader(partPath, StandardCharsets.UTF_8)) {
List<String> columns = null;
String line;
while ((line = reader.readLine()) != null) {
if (line.startsWith("-- TABLE ")) {
continue;
}
if (columns == null) {
columns = parseCsvLine(line);
continue;
}
List<String> values = parseCsvLine(line);
insertRow(jdbcConnection, toLegacyMetadata(tableMetadata), columns, values, dbType, restoreMode, targetOwnerName);
}
}
}
private boolean isMysql(String dbType) {
return DatabaseOpsConst.DB_TYPE_MYSQL.equalsIgnoreCase(dbType);
}
private PartWriter openPartWriter(Path backupDirectory, String tableName, String taskNo, int partIndex,
List<String> columnNames) throws IOException {
String rawName = tableName.toLowerCase(Locale.ROOT) + "_part" + String.format("%03d", partIndex) + ".csv";
String fileName = DatabaseFileNameUtil.appendTodayWithTask(rawName, taskNo);
Path filePath = backupDirectory.resolve(fileName).normalize();
BufferedWriter writer = Files.newBufferedWriter(filePath, StandardCharsets.UTF_8);
writer.write("-- TABLE " + tableName);
writer.newLine();
for (int i = 0; i < columnNames.size(); i++) {
if (i > 0) {
writer.write(",");
}
writer.write(escape(columnNames.get(i)));
}
writer.newLine();
FilePartMetadata part = new FilePartMetadata();
part.setFileName(fileName);
part.setFilePath(filePath.toString());
part.setRowCount(0L);
part.setFileSize(0L);
return new PartWriter(writer, filePath, part);
}
private TableMetadata toLegacyMetadata(TableExportMetadata metadata) {
TableMetadata legacy = new TableMetadata();
legacy.setOwnerName(null);
legacy.setTableName(metadata.getTableName());
legacy.setFullTableName(metadata.getFullTableName());
legacy.setTimeColumn(metadata.getTimeColumn());
legacy.setStartTime(metadata.getStartTime());
legacy.setEndTime(metadata.getEndTime());
legacy.setRowCount(metadata.getRowCount());
List<String> columnNames = new ArrayList<>();
List<String> columnTypes = new ArrayList<>();
for (ColumnMetadata column : metadata.getColumns()) {
columnNames.add(column.getName());
columnTypes.add(column.getType());
}
legacy.setColumnNames(columnNames);
legacy.setColumnTypes(columnTypes);
return legacy;
}
private void checkCancelled(BooleanSupplier cancelled, Path backupDirectory) {
if (cancelled != null && cancelled.getAsBoolean()) {
throw new IllegalStateException("备份任务已停止,已生成文件保留在:" + backupDirectory);
}
}
private String buildTargetTableName(TableMetadata metadata, String targetOwnerName) {
String owner = normalizeOwner(StrUtil.blankToDefault(targetOwnerName, metadata.getOwnerName()));
return buildFullTableName(owner, metadata.getTableName());
}
private String buildFullTableName(String ownerName, String tableName) {
if (StrUtil.isBlank(ownerName)) {
return tableName;
}
return ownerName + "." + tableName;
}
private String normalizeOwner(String ownerName) {
if (StrUtil.isBlank(ownerName)) {
return null;
}
return normalizeIdentifier(ownerName);
}
private List<String> parseCsvLine(String line) {
List<String> result = new ArrayList<>();
StringBuilder current = new StringBuilder();
boolean quoted = false;
for (int i = 0; i < line.length(); i++) {
char currentChar = line.charAt(i);
if (currentChar == '"') {
if (quoted && i + 1 < line.length() && line.charAt(i + 1) == '"') {
current.append('"');
i++;
} else {
quoted = !quoted;
}
continue;
}
if (currentChar == ',' && !quoted) {
result.add(current.toString());
current.setLength(0);
continue;
}
current.append(currentChar);
}
result.add(current.toString());
return result;
}
private String escape(String value) {
if (value == null) {
return "";
}
return "\"" + value.replace("\"", "\"\"") + "\"";
}
private String normalizeIdentifier(String value) {
if (value == null || !IDENTIFIER_PATTERN.matcher(value).matches()) {
throw new IllegalArgumentException("数据库对象名称格式不正确:" + value);
}
return value.trim().toUpperCase(Locale.ROOT);
}
private String normalizeMysqlIdentifier(String value) {
if (value == null || !IDENTIFIER_PATTERN.matcher(value).matches()) {
throw new IllegalArgumentException("数据库对象名称格式不正确:" + value);
}
return value.trim();
}
@Data
public static class TableMetadata {
private String ownerName;
private String tableName;
private String fullTableName;
private List<String> columnNames;
private List<String> columnTypes;
private String timeColumn;
private String startTime;
private String endTime;
private Long rowCount;
}
@Data
public static class ExportManifest {
private Integer version;
private String dbType;
private String backupStrategy;
private String taskNo;
private String databaseName;
private List<TableExportMetadata> tables;
}
@Data
public static class TableExportMetadata {
private String tableName;
private String fullTableName;
private String timeColumn;
private String startTime;
private String endTime;
private List<ColumnMetadata> columns;
private Long rowCount;
private List<FilePartMetadata> parts;
}
@Data
public static class ColumnMetadata {
private String name;
private String type;
}
@Data
public static class FilePartMetadata {
private String fileName;
private String filePath;
private Long rowCount;
private Long fileSize;
}
private class PartWriter {
private final BufferedWriter writer;
private final Path filePath;
private final FilePartMetadata part;
private PartWriter(BufferedWriter writer, Path filePath, FilePartMetadata part) {
this.writer = writer;
this.filePath = filePath;
this.part = part;
}
private FilePartMetadata getPart() {
return part;
}
private boolean shouldRotate(long maxPartBytes) throws IOException {
writer.flush();
return part.getRowCount() > 0 && Files.size(filePath) >= maxPartBytes;
}
private void writeRow(ResultSet resultSet, int columnCount) throws Exception {
for (int i = 1; i <= columnCount; i++) {
if (i > 1) {
writer.write(",");
}
writer.write(escape(resultSet.getString(i)));
}
writer.newLine();
part.setRowCount(part.getRowCount() + 1);
}
private void close() throws IOException {
writer.close();
part.setFileSize(Files.exists(filePath) ? Files.size(filePath) : 0L);
}
}
}

View File

@@ -0,0 +1,89 @@
package com.njcn.gather.systemops.database.component;
import cn.hutool.core.util.StrUtil;
import com.njcn.gather.systemops.database.constant.DatabaseOpsConst;
import com.njcn.gather.systemops.database.pojo.po.DatabaseConnection;
import com.njcn.gather.systemops.database.pojo.vo.DatabaseTableVO;
import com.njcn.gather.systemops.database.pojo.vo.DatabaseTestResultVO;
import org.springframework.stereotype.Component;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
/**
* Oracle JDBC 连接与元数据探测组件。
*/
@Component
public class OracleJdbcComponent {
public DatabaseTestResultVO test(DatabaseConnection connection, String password) {
DatabaseTestResultVO result = new DatabaseTestResultVO();
try (Connection ignored = openConnection(connection, password)) {
result.setSuccess(true);
result.setMessage("连接成功");
} catch (Exception exception) {
result.setSuccess(false);
result.setMessage(exception.getMessage());
}
return result;
}
public List<DatabaseTableVO> listTables(DatabaseConnection connection, String password, String schemaName) throws Exception {
String owner = StrUtil.blankToDefault(schemaName, connection.getSchemaName());
if (StrUtil.isBlank(owner)) {
owner = connection.getUsername();
}
owner = owner.trim().toUpperCase(Locale.ROOT);
String sql = "SELECT t.owner, t.table_name, t.num_rows, o.last_ddl_time, c.comments "
+ "FROM all_tables t "
+ "LEFT JOIN all_tab_comments c "
+ "ON t.owner = c.owner AND t.table_name = c.table_name "
+ "LEFT JOIN all_objects o "
+ "ON t.owner = o.owner AND t.table_name = o.object_name AND o.object_type = 'TABLE' "
+ "WHERE t.owner = ? ORDER BY t.table_name";
try (Connection jdbcConnection = openConnection(connection, password);
PreparedStatement statement = jdbcConnection.prepareStatement(sql)) {
statement.setString(1, owner);
try (ResultSet resultSet = statement.executeQuery()) {
List<DatabaseTableVO> result = new ArrayList<>();
while (resultSet.next()) {
DatabaseTableVO table = new DatabaseTableVO();
table.setOwner(resultSet.getString("owner"));
table.setTableName(resultSet.getString("table_name"));
table.setEngine(DatabaseOpsConst.DB_TYPE_ORACLE);
table.setTableRows(getLongValue(resultSet, "num_rows"));
Timestamp updateTime = resultSet.getTimestamp("last_ddl_time");
table.setUpdateTime(updateTime == null ? null : updateTime.toLocalDateTime());
table.setComments(resultSet.getString("comments"));
result.add(table);
}
return result;
}
}
}
private Long getLongValue(ResultSet resultSet, String columnName) throws Exception {
long value = resultSet.getLong(columnName);
return resultSet.wasNull() ? null : value;
}
public Connection openConnection(DatabaseConnection connection, String password) throws Exception {
if (StrUtil.isBlank(password)) {
throw new IllegalArgumentException("数据库密码不能为空");
}
return DriverManager.getConnection(buildJdbcUrl(connection), connection.getUsername(), password);
}
public String buildJdbcUrl(DatabaseConnection connection) {
if (DatabaseOpsConst.CONNECT_TYPE_SID.equalsIgnoreCase(connection.getConnectType())) {
return "jdbc:oracle:thin:@" + connection.getHost() + ":" + connection.getPort() + ":" + connection.getSid();
}
return "jdbc:oracle:thin:@//" + connection.getHost() + ":" + connection.getPort() + "/" + connection.getServiceName();
}
}

View File

@@ -0,0 +1,37 @@
package com.njcn.gather.systemops.database.config;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
/**
* 数据库运维后台任务线程池。
*/
@Slf4j
@Configuration
public class DbmsExecutorConfig {
@Bean(name = "dbmsTaskExecutorService", destroyMethod = "shutdown")
public ExecutorService dbmsTaskExecutorService() {
AtomicInteger threadIndex = new AtomicInteger(1);
return new ThreadPoolExecutor(
1,
1,
30,
TimeUnit.SECONDS,
new LinkedBlockingQueue<Runnable>(8),
runnable -> {
Thread thread = new Thread(runnable);
thread.setName("dbms-task-" + threadIndex.getAndIncrement());
return thread;
},
(runnable, executor) -> log.warn("数据库运维任务线程池已满,拒绝新的任务")
);
}
}

View File

@@ -0,0 +1,30 @@
package com.njcn.gather.systemops.database.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
/**
* 数据库运维配置。
*/
@Data
@Component
@ConfigurationProperties(prefix = "dbms")
public class DbmsProperties {
private Backup backup = new Backup();
private Tools tools = new Tools();
@Data
public static class Backup {
private String storagePath = "D:/dbms-backup";
private Integer defaultMaxFileSizeMb = 512;
private Integer mysqlFetchSize = 1000;
}
@Data
public static class Tools {
private String expdpPath;
private String impdpPath;
}
}

View File

@@ -0,0 +1,21 @@
package com.njcn.gather.systemops.database.constant;
/**
* 数据库运维常量。
*/
public final class DatabaseOpsConst {
public static final String DB_TYPE_ORACLE = "ORACLE";
public static final String DB_TYPE_MYSQL = "MYSQL";
public static final String CONNECT_TYPE_SERVICE_NAME = "SERVICE_NAME";
public static final String CONNECT_TYPE_SID = "SID";
public static final String CONFIRM_DELETE = "确认删除";
public static final String CONFIRM_OVERWRITE = "确认覆盖";
public static final int STATE_DELETED = 0;
public static final int STATE_ENABLED = 1;
public static final int SAVE_PASSWORD_YES = 1;
public static final int SAVE_PASSWORD_NO = 0;
private DatabaseOpsConst() {
}
}

View File

@@ -0,0 +1,87 @@
package com.njcn.gather.systemops.database.controller;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.njcn.common.pojo.annotation.OperateInfo;
import com.njcn.common.pojo.constant.OperateType;
import com.njcn.common.pojo.enums.common.LogEnum;
import com.njcn.common.pojo.enums.response.CommonResponseEnum;
import com.njcn.common.pojo.response.HttpResult;
import com.njcn.gather.systemops.database.pojo.param.DatabaseBackupParam;
import com.njcn.gather.systemops.database.pojo.vo.DatabaseBackupFileVO;
import com.njcn.gather.systemops.database.pojo.vo.DatabaseTaskCreateVO;
import com.njcn.gather.systemops.database.pojo.vo.DatabaseTaskVO;
import com.njcn.gather.systemops.database.service.DatabaseBackupFileService;
import com.njcn.gather.systemops.database.service.DatabaseOperationTaskService;
import com.njcn.web.controller.BaseController;
import com.njcn.web.utils.HttpResultUtil;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.RequiredArgsConstructor;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
/**
* 数据库备份接口。
*/
@Api(tags = "数据库备份")
@RestController
@RequestMapping("/database/backups")
@RequiredArgsConstructor
public class DatabaseBackupController extends BaseController {
private final DatabaseOperationTaskService databaseOperationTaskService;
private final DatabaseBackupFileService databaseBackupFileService;
@OperateInfo(info = LogEnum.BUSINESS_COMMON, operateType = OperateType.ADD)
@ApiOperation("创建备份任务")
@PostMapping("/create")
public HttpResult<DatabaseTaskCreateVO> create(@RequestBody @Validated DatabaseBackupParam.CreateParam param) {
String methodDescribe = getMethodDescribe("create");
return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, databaseOperationTaskService.createBackupTask(param), methodDescribe);
}
@OperateInfo(info = LogEnum.BUSINESS_COMMON)
@ApiOperation("查询备份任务")
@PostMapping("/tasks/list")
public HttpResult<Page<DatabaseTaskVO>> listTasks(@RequestBody @Validated DatabaseBackupParam.TaskQueryParam param) {
String methodDescribe = getMethodDescribe("listTasks");
return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, databaseOperationTaskService.listBackupTasks(param), methodDescribe);
}
@OperateInfo(info = LogEnum.BUSINESS_COMMON)
@ApiOperation("查询任务状态")
@GetMapping("/tasks/status")
public HttpResult<DatabaseTaskVO> status(@RequestParam("taskId") String taskId) {
String methodDescribe = getMethodDescribe("status");
return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, databaseOperationTaskService.getStatus(taskId), methodDescribe);
}
@OperateInfo(info = LogEnum.BUSINESS_COMMON, operateType = OperateType.UPDATE)
@ApiOperation("停止备份任务")
@PostMapping("/tasks/stop")
public HttpResult<Boolean> stop(@RequestBody @Validated DatabaseBackupParam.StopParam param) {
String methodDescribe = getMethodDescribe("stop");
return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, databaseOperationTaskService.stopBackupTask(param), methodDescribe);
}
@OperateInfo(info = LogEnum.BUSINESS_COMMON, operateType = OperateType.ADD)
@ApiOperation("重新开始备份任务")
@PostMapping("/tasks/restart")
public HttpResult<DatabaseTaskCreateVO> restart(@RequestBody @Validated DatabaseBackupParam.RestartParam param) {
String methodDescribe = getMethodDescribe("restart");
return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, databaseOperationTaskService.restartBackupTask(param), methodDescribe);
}
@OperateInfo(info = LogEnum.BUSINESS_COMMON)
@ApiOperation("查询备份文件")
@PostMapping("/files/list")
public HttpResult<Page<DatabaseBackupFileVO>> listFiles(@RequestBody @Validated DatabaseBackupParam.FileQueryParam param) {
String methodDescribe = getMethodDescribe("listFiles");
return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, databaseBackupFileService.listFiles(param), methodDescribe);
}
}

View File

@@ -0,0 +1,88 @@
package com.njcn.gather.systemops.database.controller;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.njcn.common.pojo.annotation.OperateInfo;
import com.njcn.common.pojo.constant.OperateType;
import com.njcn.common.pojo.enums.common.LogEnum;
import com.njcn.common.pojo.enums.response.CommonResponseEnum;
import com.njcn.common.pojo.response.HttpResult;
import com.njcn.gather.systemops.database.pojo.param.DatabaseConnectionParam;
import com.njcn.gather.systemops.database.pojo.vo.DatabaseConnectionVO;
import com.njcn.gather.systemops.database.pojo.vo.DatabaseTableVO;
import com.njcn.gather.systemops.database.pojo.vo.DatabaseTestResultVO;
import com.njcn.gather.systemops.database.service.DatabaseConnectionService;
import com.njcn.web.controller.BaseController;
import com.njcn.web.utils.HttpResultUtil;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.RequiredArgsConstructor;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
/**
* 数据库连接配置接口。
*/
@Api(tags = "数据库连接配置")
@RestController
@RequestMapping("/database/connections")
@RequiredArgsConstructor
public class DatabaseConnectionController extends BaseController {
private final DatabaseConnectionService databaseConnectionService;
@OperateInfo(info = LogEnum.BUSINESS_COMMON)
@ApiOperation("查询数据库连接配置")
@PostMapping("/list")
public HttpResult<Page<DatabaseConnectionVO>> list(@RequestBody @Validated DatabaseConnectionParam.QueryParam param) {
String methodDescribe = getMethodDescribe("list");
return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, databaseConnectionService.listConnections(param), methodDescribe);
}
@OperateInfo(info = LogEnum.BUSINESS_COMMON, operateType = OperateType.ADD)
@ApiOperation("新增数据库连接配置")
@PostMapping("/add")
public HttpResult<Boolean> add(@RequestBody @Validated DatabaseConnectionParam param) {
String methodDescribe = getMethodDescribe("add");
boolean result = databaseConnectionService.addConnection(param);
return HttpResultUtil.assembleCommonResponseResult(result ? CommonResponseEnum.SUCCESS : CommonResponseEnum.FAIL, result, methodDescribe);
}
@OperateInfo(info = LogEnum.BUSINESS_COMMON, operateType = OperateType.UPDATE)
@ApiOperation("修改数据库连接配置")
@PostMapping("/update")
public HttpResult<Boolean> update(@RequestBody @Validated DatabaseConnectionParam.UpdateParam param) {
String methodDescribe = getMethodDescribe("update");
boolean result = databaseConnectionService.updateConnection(param);
return HttpResultUtil.assembleCommonResponseResult(result ? CommonResponseEnum.SUCCESS : CommonResponseEnum.FAIL, result, methodDescribe);
}
@OperateInfo(info = LogEnum.BUSINESS_COMMON, operateType = OperateType.DELETE)
@ApiOperation("删除数据库连接配置")
@PostMapping("/delete")
public HttpResult<Boolean> delete(@RequestBody @Validated DatabaseConnectionParam.DeleteParam param) {
String methodDescribe = getMethodDescribe("delete");
boolean result = databaseConnectionService.deleteConnection(param);
return HttpResultUtil.assembleCommonResponseResult(result ? CommonResponseEnum.SUCCESS : CommonResponseEnum.FAIL, result, methodDescribe);
}
@OperateInfo(info = LogEnum.BUSINESS_COMMON)
@ApiOperation("测试数据库连接")
@PostMapping("/test")
public HttpResult<DatabaseTestResultVO> test(@RequestBody @Validated DatabaseConnectionParam.TestParam param) {
String methodDescribe = getMethodDescribe("test");
return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, databaseConnectionService.testConnection(param), methodDescribe);
}
@OperateInfo(info = LogEnum.BUSINESS_COMMON)
@ApiOperation("查询 Oracle 表列表")
@PostMapping("/tables")
public HttpResult<List<DatabaseTableVO>> tables(@RequestBody @Validated DatabaseConnectionParam.TablesParam param) {
String methodDescribe = getMethodDescribe("tables");
return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, databaseConnectionService.listTables(param), methodDescribe);
}
}

View File

@@ -0,0 +1,51 @@
package com.njcn.gather.systemops.database.controller;
import com.njcn.common.pojo.annotation.OperateInfo;
import com.njcn.common.pojo.constant.OperateType;
import com.njcn.common.pojo.enums.common.LogEnum;
import com.njcn.common.pojo.enums.response.CommonResponseEnum;
import com.njcn.common.pojo.response.HttpResult;
import com.njcn.gather.systemops.database.pojo.param.DatabaseDeleteParam;
import com.njcn.gather.systemops.database.service.DatabaseBackupFileService;
import com.njcn.gather.systemops.database.service.DatabaseOperationTaskService;
import com.njcn.web.controller.BaseController;
import com.njcn.web.utils.HttpResultUtil;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.RequiredArgsConstructor;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 数据库运维删除接口。
*/
@Api(tags = "数据库运维删除")
@RestController
@RequestMapping("/database/delete")
@RequiredArgsConstructor
public class DatabaseDeleteController extends BaseController {
private final DatabaseBackupFileService databaseBackupFileService;
private final DatabaseOperationTaskService databaseOperationTaskService;
@OperateInfo(info = LogEnum.BUSINESS_COMMON, operateType = OperateType.DELETE)
@ApiOperation("删除备份文件")
@PostMapping("/backup-file")
public HttpResult<Boolean> deleteBackupFile(@RequestBody @Validated DatabaseDeleteParam.BackupFileParam param) {
String methodDescribe = getMethodDescribe("deleteBackupFile");
boolean result = databaseBackupFileService.deleteBackupFile(param.getBackupFileId(), param.getConfirmText());
return HttpResultUtil.assembleCommonResponseResult(result ? CommonResponseEnum.SUCCESS : CommonResponseEnum.FAIL, result, methodDescribe);
}
@OperateInfo(info = LogEnum.BUSINESS_COMMON, operateType = OperateType.DELETE)
@ApiOperation("删除任务记录")
@PostMapping("/task")
public HttpResult<Boolean> deleteTask(@RequestBody @Validated DatabaseDeleteParam.TaskParam param) {
String methodDescribe = getMethodDescribe("deleteTask");
boolean result = databaseOperationTaskService.deleteTask(param.getTaskId(), param.getConfirmText());
return HttpResultUtil.assembleCommonResponseResult(result ? CommonResponseEnum.SUCCESS : CommonResponseEnum.FAIL, result, methodDescribe);
}
}

View File

@@ -0,0 +1,53 @@
package com.njcn.gather.systemops.database.controller;
import com.njcn.common.pojo.annotation.OperateInfo;
import com.njcn.common.pojo.constant.OperateType;
import com.njcn.common.pojo.enums.common.LogEnum;
import com.njcn.common.pojo.enums.response.CommonResponseEnum;
import com.njcn.common.pojo.response.HttpResult;
import com.njcn.gather.systemops.database.pojo.param.DatabaseRestoreParam;
import com.njcn.gather.systemops.database.pojo.vo.DatabaseTaskCreateVO;
import com.njcn.gather.systemops.database.pojo.vo.DatabaseTaskVO;
import com.njcn.gather.systemops.database.service.DatabaseOperationTaskService;
import com.njcn.gather.systemops.database.service.DatabaseRestoreService;
import com.njcn.web.controller.BaseController;
import com.njcn.web.utils.HttpResultUtil;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.RequiredArgsConstructor;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
/**
* 数据库恢复接口。
*/
@Api(tags = "数据库恢复")
@RestController
@RequestMapping("/database/restores")
@RequiredArgsConstructor
public class DatabaseRestoreController extends BaseController {
private final DatabaseRestoreService databaseRestoreService;
private final DatabaseOperationTaskService databaseOperationTaskService;
@OperateInfo(info = LogEnum.BUSINESS_COMMON, operateType = OperateType.ADD)
@ApiOperation("创建恢复任务")
@PostMapping("/create")
public HttpResult<DatabaseTaskCreateVO> create(@RequestBody @Validated DatabaseRestoreParam.CreateParam param) {
String methodDescribe = getMethodDescribe("create");
return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, databaseRestoreService.createRestoreTask(param), methodDescribe);
}
@OperateInfo(info = LogEnum.BUSINESS_COMMON)
@ApiOperation("查询恢复任务状态")
@GetMapping("/tasks/status")
public HttpResult<DatabaseTaskVO> status(@RequestParam("taskId") String taskId) {
String methodDescribe = getMethodDescribe("status");
return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, databaseOperationTaskService.getStatus(taskId), methodDescribe);
}
}

View File

@@ -0,0 +1,10 @@
package com.njcn.gather.systemops.database.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.njcn.gather.systemops.database.pojo.po.DatabaseBackupFile;
/**
* 数据库备份文件 Mapper。
*/
public interface DatabaseBackupFileMapper extends BaseMapper<DatabaseBackupFile> {
}

View File

@@ -0,0 +1,10 @@
package com.njcn.gather.systemops.database.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.njcn.gather.systemops.database.pojo.po.DatabaseConnection;
/**
* 数据库连接配置 Mapper。
*/
public interface DatabaseConnectionMapper extends BaseMapper<DatabaseConnection> {
}

View File

@@ -0,0 +1,10 @@
package com.njcn.gather.systemops.database.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.njcn.gather.systemops.database.pojo.po.DatabaseOperationTask;
/**
* 数据库运维任务 Mapper。
*/
public interface DatabaseOperationTaskMapper extends BaseMapper<DatabaseOperationTask> {
}

View File

@@ -0,0 +1,10 @@
package com.njcn.gather.systemops.database.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.njcn.gather.systemops.database.pojo.po.DatabaseRestoreRecord;
/**
* 数据库恢复记录 Mapper。
*/
public interface DatabaseRestoreRecordMapper extends BaseMapper<DatabaseRestoreRecord> {
}

View File

@@ -0,0 +1,10 @@
package com.njcn.gather.systemops.database.pojo.enums;
/**
* 备份模式。
*/
public enum BackupModeEnum {
FULL_TABLE,
TIME_RANGE,
SIZE_SPLIT
}

View File

@@ -0,0 +1,9 @@
package com.njcn.gather.systemops.database.pojo.enums;
/**
* 备份策略。
*/
public enum BackupStrategyEnum {
DATA_PUMP,
JDBC_EXPORT
}

View File

@@ -0,0 +1,10 @@
package com.njcn.gather.systemops.database.pojo.enums;
/**
* 备份文件格式。
*/
public enum FileFormatEnum {
DMP,
SQL,
CSV
}

View File

@@ -0,0 +1,10 @@
package com.njcn.gather.systemops.database.pojo.enums;
/**
* 数据库运维操作类型。
*/
public enum OperationTypeEnum {
BACKUP,
RESTORE,
DELETE
}

View File

@@ -0,0 +1,11 @@
package com.njcn.gather.systemops.database.pojo.enums;
/**
* 恢复模式。
*/
public enum RestoreModeEnum {
SKIP,
APPEND,
TRUNCATE,
REPLACE
}

View File

@@ -0,0 +1,12 @@
package com.njcn.gather.systemops.database.pojo.enums;
/**
* 运维任务状态。
*/
public enum TaskStatusEnum {
WAITING,
RUNNING,
SUCCESS,
FAIL,
CANCELLED
}

View File

@@ -0,0 +1,85 @@
package com.njcn.gather.systemops.database.pojo.param;
import com.njcn.web.pojo.param.BaseParam;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import javax.validation.constraints.NotBlank;
import java.time.LocalDateTime;
import java.util.List;
/**
* 数据库备份参数。
*/
public class DatabaseBackupParam {
@Data
@ApiModel("创建备份任务参数")
public static class CreateParam {
@ApiModelProperty("连接 ID")
@NotBlank(message = "连接 ID 不能为空")
private String connectionId;
@ApiModelProperty("备份策略DATA_PUMP、JDBC_EXPORT默认 DATA_PUMP")
private String backupStrategy;
@ApiModelProperty("Schema")
private String schemaName;
@ApiModelProperty("表名列表")
private List<String> targetNames;
@ApiModelProperty("备份模式FULL_TABLE、TIME_RANGE、SIZE_SPLIT")
private String backupMode;
@ApiModelProperty("时间字段")
private String timeColumn;
@ApiModelProperty("开始时间")
private LocalDateTime startTime;
@ApiModelProperty("结束时间")
private LocalDateTime endTime;
@ApiModelProperty("最大文件大小 MB")
private Integer maxFileSizeMb;
@ApiModelProperty("Oracle Directory 名称")
private String directoryName;
@ApiModelProperty("临时密码,不保存密码时传入")
private String temporaryPassword;
}
@Data
@EqualsAndHashCode(callSuper = true)
@ApiModel("备份任务查询参数")
public static class TaskQueryParam extends BaseParam {
@ApiModelProperty("连接 ID")
private String connectionId;
@ApiModelProperty("任务状态")
private String taskStatus;
}
@Data
@EqualsAndHashCode(callSuper = true)
@ApiModel("备份文件查询参数")
public static class FileQueryParam extends BaseParam {
@ApiModelProperty("连接 ID")
private String connectionId;
@ApiModelProperty("任务 ID")
private String taskId;
@ApiModelProperty("备份策略")
private String backupStrategy;
}
@Data
@ApiModel("停止备份任务参数")
public static class StopParam {
@ApiModelProperty("备份任务 ID")
@NotBlank(message = "备份任务 ID 不能为空")
private String taskId;
}
@Data
@ApiModel("重新开始备份任务参数")
public static class RestartParam {
@ApiModelProperty("备份任务 ID")
@NotBlank(message = "备份任务 ID 不能为空")
private String taskId;
@ApiModelProperty("临时密码,原连接未保存密码时传入")
private String temporaryPassword;
}
}

View File

@@ -0,0 +1,130 @@
package com.njcn.gather.systemops.database.pojo.param;
import com.fasterxml.jackson.annotation.JsonAlias;
import com.njcn.web.pojo.param.BaseParam;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
/**
* 数据库连接配置参数。
*/
@Data
@ApiModel("数据库连接配置参数")
public class DatabaseConnectionParam {
@ApiModelProperty("连接名称")
@NotBlank(message = "连接名称不能为空")
private String connectionName;
@ApiModelProperty("数据库类型ORACLE、MYSQL")
private String dbType;
@ApiModelProperty("数据库主机地址")
@NotBlank(message = "数据库主机地址不能为空")
private String host;
@ApiModelProperty("数据库端口")
@NotNull(message = "数据库端口不能为空")
private Integer port;
@ApiModelProperty("连接类型SERVICE_NAME、SID")
private String connectType;
@ApiModelProperty("服务名")
private String serviceName;
@ApiModelProperty("SID")
private String sid;
@ApiModelProperty("数据库名MySQL 使用")
private String databaseName;
@ApiModelProperty("Schema")
private String schemaName;
@ApiModelProperty("用户名")
@NotBlank(message = "用户名不能为空")
private String username;
@ApiModelProperty("密码")
private String password;
@ApiModelProperty("是否保存密码0-否1-是")
private Integer savePassword;
@ApiModelProperty("Oracle Directory 名称")
private String directoryName;
@ApiModelProperty("Oracle Directory 物理路径")
private String directoryPath;
@ApiModelProperty("扩展配置 JSON")
private String extraConfigJson;
@ApiModelProperty("备注")
private String remark;
@Data
@EqualsAndHashCode(callSuper = true)
@ApiModel("数据库连接更新参数")
public static class UpdateParam extends DatabaseConnectionParam {
@ApiModelProperty("连接 ID")
@NotBlank(message = "连接 ID 不能为空")
private String id;
}
@Data
@EqualsAndHashCode(callSuper = true)
@ApiModel("数据库连接查询参数")
public static class QueryParam extends BaseParam {
@ApiModelProperty("连接名称")
private String connectionName;
@ApiModelProperty("数据库类型")
private String dbType;
@ApiModelProperty("Schema")
private String schemaName;
}
@Data
@ApiModel("数据库连接删除参数")
public static class DeleteParam {
@ApiModelProperty("连接 ID")
@NotBlank(message = "连接 ID 不能为空")
private String id;
}
@Data
@ApiModel("数据库连接测试参数")
public static class TestParam {
@ApiModelProperty("连接 ID已有连接测试时传入")
private String connectionId;
@ApiModelProperty("临时连接参数,新增前测试时传入")
private DatabaseConnectionParam connection;
@ApiModelProperty("临时密码,测试时允许只传该字段而不写入 connection.password")
private String temporaryPassword;
}
@Data
@ApiModel("数据库表查询参数")
public static class TablesParam {
@ApiModelProperty("连接 ID")
@NotBlank(message = "连接 ID 不能为空")
private String connectionId;
@ApiModelProperty("临时密码,不保存密码时传入")
private String temporaryPassword;
@ApiModelProperty("兼容前端传入的运行时密码;为空时复用数据库 password_cipher")
private String password;
@JsonAlias("password_cipher")
@ApiModelProperty("兼容前端传入的已保存密码")
private String passwordCipher;
@ApiModelProperty("兼容前端传入的临时连接参数")
private DatabaseConnectionParam connection;
@ApiModelProperty("Schema 或数据库名,不传则使用连接默认值")
private String schemaName;
}
}

View File

@@ -0,0 +1,33 @@
package com.njcn.gather.systemops.database.pojo.param;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import javax.validation.constraints.NotBlank;
/**
* 数据库运维删除参数。
*/
public class DatabaseDeleteParam {
@Data
@ApiModel("删除备份文件参数")
public static class BackupFileParam {
@ApiModelProperty("备份文件 ID")
@NotBlank(message = "备份文件 ID 不能为空")
private String backupFileId;
@ApiModelProperty("确认文案")
private String confirmText;
}
@Data
@ApiModel("删除任务参数")
public static class TaskParam {
@ApiModelProperty("任务 ID")
@NotBlank(message = "任务 ID 不能为空")
private String taskId;
@ApiModelProperty("确认文案")
private String confirmText;
}
}

View File

@@ -0,0 +1,32 @@
package com.njcn.gather.systemops.database.pojo.param;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import javax.validation.constraints.NotBlank;
/**
* 数据库恢复参数。
*/
public class DatabaseRestoreParam {
@Data
@ApiModel("创建恢复任务参数")
public static class CreateParam {
@ApiModelProperty("目标连接 ID")
@NotBlank(message = "连接 ID 不能为空")
private String connectionId;
@ApiModelProperty("备份文件 ID")
@NotBlank(message = "备份文件 ID 不能为空")
private String backupFileId;
@ApiModelProperty("恢复模式SKIP、APPEND、TRUNCATE、REPLACE")
private String restoreMode;
@ApiModelProperty("目标 Schema")
private String targetSchemaName;
@ApiModelProperty("临时密码,不保存密码时传入")
private String temporaryPassword;
@ApiModelProperty("覆盖确认文案")
private String overwriteConfirmText;
}
}

View File

@@ -0,0 +1,71 @@
package com.njcn.gather.systemops.database.pojo.po;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
* 数据库备份文件记录。
*/
@Data
@TableName("dbms_backup_file")
public class DatabaseBackupFile implements Serializable {
private static final long serialVersionUID = 3119981982091873277L;
@TableId("id")
private String id;
@TableField("task_id")
private String taskId;
@TableField("connection_id")
private String connectionId;
@TableField("db_type")
private String dbType;
@TableField("backup_strategy")
private String backupStrategy;
@TableField("file_format")
private String fileFormat;
@TableField("schema_name")
private String schemaName;
@TableField("target_names_json")
private String targetNamesJson;
@TableField("backup_mode")
private String backupMode;
@TableField("backup_start_time")
private LocalDateTime backupStartTime;
@TableField("backup_end_time")
private LocalDateTime backupEndTime;
@TableField("time_column")
private String timeColumn;
@TableField("directory_name")
private String directoryName;
@TableField("dump_file_name")
private String dumpFileName;
@TableField("log_file_name")
private String logFileName;
@TableField("file_name")
private String fileName;
@TableField("file_path")
private String filePath;
@TableField("log_file_path")
private String logFilePath;
@TableField("metadata_file_path")
private String metadataFilePath;
@TableField("file_size")
private Long fileSize;
@TableField("checksum")
private String checksum;
@TableField("state")
private Integer state;
@TableField("create_by")
private String createBy;
@TableField("create_time")
private LocalDateTime createTime;
@TableField("update_by")
private String updateBy;
@TableField("update_time")
private LocalDateTime updateTime;
}

View File

@@ -0,0 +1,69 @@
package com.njcn.gather.systemops.database.pojo.po;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
* 数据库连接配置。
*/
@Data
@TableName("dbms_connection")
public class DatabaseConnection implements Serializable {
private static final long serialVersionUID = -5821519248914313778L;
@TableId("id")
private String id;
@TableField("connection_name")
private String connectionName;
@TableField("db_type")
private String dbType;
@TableField("host")
private String host;
@TableField("port")
private Integer port;
@TableField("connect_type")
private String connectType;
@TableField("service_name")
private String serviceName;
@TableField("sid")
private String sid;
@TableField("database_name")
private String databaseName;
@TableField("schema_name")
private String schemaName;
@TableField("username")
private String username;
@TableField("password_cipher")
private String passwordCipher;
@TableField("save_password")
private Integer savePassword;
@TableField("directory_name")
private String directoryName;
@TableField("directory_path")
private String directoryPath;
@TableField("extra_config_json")
private String extraConfigJson;
@TableField("remark")
private String remark;
@TableField("last_test_status")
private String lastTestStatus;
@TableField("last_test_message")
private String lastTestMessage;
@TableField("last_test_time")
private LocalDateTime lastTestTime;
@TableField("state")
private Integer state;
@TableField("create_by")
private String createBy;
@TableField("create_time")
private LocalDateTime createTime;
@TableField("update_by")
private String updateBy;
@TableField("update_time")
private LocalDateTime updateTime;
}

View File

@@ -0,0 +1,58 @@
package com.njcn.gather.systemops.database.pojo.po;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
* 数据库运维任务。
*/
@Data
@TableName("dbms_operation_task")
public class DatabaseOperationTask implements Serializable {
private static final long serialVersionUID = 1831235987236858769L;
@TableId("id")
private String id;
@TableField("task_no")
private String taskNo;
@TableField("connection_id")
private String connectionId;
@TableField("db_type")
private String dbType;
@TableField("operation_type")
private String operationType;
@TableField("backup_strategy")
private String backupStrategy;
@TableField("task_status")
private String taskStatus;
@TableField("schema_name")
private String schemaName;
@TableField("target_names_json")
private String targetNamesJson;
@TableField("request_param_json")
private String requestParamJson;
@TableField("result_message")
private String resultMessage;
@TableField("progress_percent")
private BigDecimal progressPercent;
@TableField("started_at")
private LocalDateTime startedAt;
@TableField("finished_at")
private LocalDateTime finishedAt;
@TableField("state")
private Integer state;
@TableField("create_by")
private String createBy;
@TableField("create_time")
private LocalDateTime createTime;
@TableField("update_by")
private String updateBy;
@TableField("update_time")
private LocalDateTime updateTime;
}

View File

@@ -0,0 +1,51 @@
package com.njcn.gather.systemops.database.pojo.po;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
* 数据库恢复记录。
*/
@Data
@TableName("dbms_restore_record")
public class DatabaseRestoreRecord implements Serializable {
private static final long serialVersionUID = -5638979151924581277L;
@TableId("id")
private String id;
@TableField("task_id")
private String taskId;
@TableField("backup_file_id")
private String backupFileId;
@TableField("connection_id")
private String connectionId;
@TableField("db_type")
private String dbType;
@TableField("restore_mode")
private String restoreMode;
@TableField("target_schema_name")
private String targetSchemaName;
@TableField("target_names_json")
private String targetNamesJson;
@TableField("table_exists_action")
private String tableExistsAction;
@TableField("overwrite_confirmed")
private Integer overwriteConfirmed;
@TableField("result_message")
private String resultMessage;
@TableField("state")
private Integer state;
@TableField("create_by")
private String createBy;
@TableField("create_time")
private LocalDateTime createTime;
@TableField("update_by")
private String updateBy;
@TableField("update_time")
private LocalDateTime updateTime;
}

View File

@@ -0,0 +1,29 @@
package com.njcn.gather.systemops.database.pojo.vo;
import lombok.Data;
import java.time.LocalDateTime;
/**
* 数据库备份文件响应。
*/
@Data
public class DatabaseBackupFileVO {
private String id;
private String taskId;
private String connectionId;
private String dbType;
private String backupStrategy;
private String fileFormat;
private String schemaName;
private String targetNamesJson;
private String backupMode;
private String fileName;
private String filePath;
private String logFileName;
private String logFilePath;
private Long fileSize;
private String checksum;
private Integer state;
private LocalDateTime createTime;
}

View File

@@ -0,0 +1,34 @@
package com.njcn.gather.systemops.database.pojo.vo;
import lombok.Data;
import java.time.LocalDateTime;
/**
* 数据库连接配置响应。
*/
@Data
public class DatabaseConnectionVO {
private String id;
private String connectionName;
private String dbType;
private String host;
private Integer port;
private String connectType;
private String serviceName;
private String sid;
private String databaseName;
private String schemaName;
private String username;
private Integer savePassword;
private String directoryName;
private String directoryPath;
private String extraConfigJson;
private String remark;
private String lastTestStatus;
private String lastTestMessage;
private LocalDateTime lastTestTime;
private Integer state;
private LocalDateTime createTime;
private LocalDateTime updateTime;
}

View File

@@ -0,0 +1,21 @@
package com.njcn.gather.systemops.database.pojo.vo;
import lombok.Data;
import java.time.LocalDateTime;
/**
* 数据库表信息。
*/
@Data
public class DatabaseTableVO {
private String owner;
private String tableName;
private Long autoIncrementValue = 0L;
private Long autoIncrement = 0L;
private LocalDateTime updateTime;
private Long dataLength;
private String engine;
private Long tableRows;
private String comments;
}

View File

@@ -0,0 +1,13 @@
package com.njcn.gather.systemops.database.pojo.vo;
import lombok.Data;
/**
* 运维任务创建结果。
*/
@Data
public class DatabaseTaskCreateVO {
private String taskId;
private String taskNo;
private String taskStatus;
}

View File

@@ -0,0 +1,28 @@
package com.njcn.gather.systemops.database.pojo.vo;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
* 数据库运维任务响应。
*/
@Data
public class DatabaseTaskVO {
private String id;
private String taskNo;
private String connectionId;
private String dbType;
private String operationType;
private String backupStrategy;
private String taskStatus;
private String schemaName;
private String targetNamesJson;
private String resultMessage;
private BigDecimal progressPercent;
private LocalDateTime startedAt;
private LocalDateTime finishedAt;
private LocalDateTime createTime;
private LocalDateTime updateTime;
}

View File

@@ -0,0 +1,12 @@
package com.njcn.gather.systemops.database.pojo.vo;
import lombok.Data;
/**
* 数据库连接测试结果。
*/
@Data
public class DatabaseTestResultVO {
private Boolean success;
private String message;
}

Some files were not shown because too many files have changed in this diff Show More