35 Commits

Author SHA1 Message Date
周宇 蔡
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
66d351afe4 feat(steady): 新增数据校验功能并优化稳态趋势查询
- 在 AddLedgerLineMapper.xml 中添加 lineInterval 字段映射
- 在 AddLedgerLinePathVO 中添加 lineInterval 属性用于存储统计间隔
- 为稳态趋势查询服务添加详细的执行日志记录和性能监控
- 重构 InfluxDB 查询组件,添加诊断信息构建方法和异常处理
- 限制谐波次数最大展示数量从 6 个调整为 3 个
- 新增数据校验相关组件、控制器和服务实现
- 实现数据连续性检查和缺失数据统计功能
- 添加数据校验查询参数和返回结果的数据结构定义
- 完善相关单元测试确保功能正确性
2026-05-27 08:04:49 +08:00
e5369fef5a docs: 规划deploy Linux远程运维实现 2026-05-21 21:13:17 +08:00
ba8bc43377 docs: 设计deploy Linux远程运维方案 2026-05-21 16:44:42 +08:00
9a9614a9e5 feat(system-ops): 新增系统运维模块及稳态数据视图优化
- 添加 system-ops 模块及其子模块 dbms 和 deploy
- 实现数据库监控和系统部署的基础接口和服务
- 更新项目依赖配置和文档说明
- 优化稳态数据视图中线电压相位显示逻辑
- 完善线电压指标的相位解析和测试验证
2026-05-21 14:08:15 +08:00
89efc55119 fix(tools): 修复台账节点查询和数据补录功能
- 修复 QUALITYFLAG 字段默认值从 1 改为 0
- 添加 selectNodeById 查询方法用于精确节点查找
- 重构 requireLedger 方法增加节点名称参数和详细错误提示
- 新增 levelName 辅助方法统一层级名称显示
- 更新 InfluxDB 配置地址从 192.168.1.68 改为 127.0.0.1
- 扩展 add-data 模块支持 InfluxDB 数据补录功能
- 新增 AddDataInfluxTaskController 提供 InfluxDB 补数任务接口
- 实现 AddDataInfluxFieldMapper 完成字段到 InfluxDB 测量值映射
- 添加 AddDataInfluxTaskExecutor 处理 InfluxDB 异步补数任务
- 更新 README 文档说明 InfluxDB 写入功能和配置要求
2026-05-20 08:33:37 +08:00
bff89bede0 微调 2026-05-18 16:42:43 +08:00
1ee94208ae feat(event): 添加暂态事件波形查看与导出功能
- 新增 getTransientEventWave 接口用于查看暂态事件波形
- 新增 exportTransientEventWaves 接口用于批量导出暂态事件波形
- 添加 EventWaveExportParam 参数类支持波形导出
- 在 EventListMapper 中增加 selectTransientDetailsByIds 查询方法
- 更新事件列表查询参数支持毫秒级时间格式
- 移除事件描述模糊查询条件优化查询性能
- 添加波形导出相关的常量和工具类集成
2026-05-18 16:31:01 +08:00
38f910fccd feat(event): 添加暂态事件波形查看与导出功能
- 新增 getTransientEventWave 接口用于查看暂态事件波形
- 新增 exportTransientEventWaves 接口用于批量导出暂态事件波形
- 添加 EventWaveExportParam 参数类支持波形导出
- 在 EventListMapper 中增加 selectTransientDetailsByIds 查询方法
- 更新事件列表查询参数支持毫秒级时间格式
- 移除事件描述模糊查询条件优化查询性能
- 添加波形导出相关的常量和工具类集成
2026-05-18 08:45:05 +08:00
90219a3daf 我叫圣文 2026-05-15 16:37:22 +08:00
41c915d1df steady: 补充指标相别展示规则 2026-05-14 15:52:32 +08:00
81d7b213b8 steady: 增加趋势查看设计方案 2026-05-14 15:30:25 +08:00
a8a57e882f feat(filter): 添加事件列表接口到认证白名单
- 在 AuthGlobalFilter 中添加 /event/list/transient/page 接口到白名单
- 在 AuthGlobalFilter 中添加 /event/list/transient/page/debug 接口到白名单
- 新增 DATA_FLICKER.sql 数据库脚本文件,包含电能质量相关数据表结构
2026-05-14 09:12:16 +08:00
b56116264c feat(wave-tool): 添加波形解析结果统计信息并优化数值精度
- 在 WaveComtradeResultVO 中新增 totalChannels、phaseCount 和 unit 字段用于展示统计信息
- 实现波形时间和幅值的小数位统一保留 3 位精度处理
- 添加了对 COMTRADE 配置文件中的变比进行优先使用的逻辑
- 实现了波形单位识别逻辑,支持 kV/A、kV、A 等单位显示
- 更新了文档中关于 PT/CT 变比的说明,明确优先使用 cfg 模拟量通道的变比
- 添加 spring-boot-starter-test 依赖以支持测试功能
2026-05-11 16:32:25 +08:00
6f5d8dc45a feat(event): 新增暂态事件列表功能
- 新增事件列表控制器,提供分页查询、详情查看和导出功能
- 实现事件列表服务和数据访问层,支持多条件查询
- 添加台账链路查询功能,关联工程、项目、设备和监测点信息
- 实现事件数据的Excel导出功能,包含完整的字段映射
- 添加相关索引优化查询性能
- 集成台账服务,支持通过台账路径筛选事件数据
2026-05-09 13:16:56 +08:00
c3b074db26 feat(add-ledger): 新增数据台账管理功能
- 初始化台账设备相关字典类型和数据
- 添加数据台账常量定义类
- 实现台账配置控制器提供增删改查接口
- 创建台账节点详情、树形结构等视图对象
- 开发工程、设备、测点等数据访问层
- 添加台账ID生成和线路号处理工具类
- 实现台账软删除和查询功能
- 添加测点线路号可用性检查机制
2026-05-09 07:52:04 +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
328 changed files with 28104 additions and 935 deletions

2
.gitignore vendored
View File

@@ -5,6 +5,8 @@
target/
logs/
docs/
.codex-tmp/
.docs/
# Log file
*.log

View File

@@ -18,6 +18,7 @@
- 只清理自己造成的问题:可以删除因本次修改而产生的未使用 `import`、变量或方法;不要删除仓库中原本就存在的死代码,除非用户明确要求。
- 先定义验证方式:执行方案里要写清楚“改哪里、怎么判断改对了”;默认通过代码路径、配置一致性和受影响范围检查进行验证。
- 除非用户明确要求,否则不执行任何 `mvn` 编译、打包、测试或其他构建命令。
- 所有导出或生成文件名统一追加日期,格式为“原文件名 + `_` + `yyyyMMdd` + 原扩展名”,例如 `暂态事件列表_20260516.xlsx`;新增下载导出、临时生成或落盘保存文件时,优先复用目标模块已有文件名工具,不要在业务代码中散落手写日期拼接。
## 项目结构与模块划分
`CN_Tool` 是一个 Maven 多模块后端项目,根目录的 [`pom.xml`](C:/code/gitea/cn_tool/CN_Tool/pom.xml) 聚合了 `entrance``system``user``detection``tools`
@@ -40,6 +41,8 @@ Java 源码位于 `src/main/java`,配置文件位于 `src/main/resources`My
- 不要假设运行时存在自动数据库迁移;如果代码依赖新表、新字段或新索引,必须同步补齐对应 SQL 与文档说明。
- SQL 脚本应放在目标模块的 `src/main/resources/sql/...` 下,并保持可审阅、可单独执行、语义清晰。
- 变更缓存、日志、审计相关逻辑时,优先沿用现有机制,不要绕开现有登录上下文、缓存约定和审计字段填充方式。
- 涉及 `sys_dict_type``sys_dict_data` 的字典类型编码、字典数据编码或固定字典数据 ID 时,必须统一维护在 `system/src/main/java/com/njcn/gather/system/pojo/constant/DictConst.java`后续新增使用点也要先补常量再引用不要在业务代码、SQL 或文档中散落硬编码。
- 新增或保留字典初始化 SQL 前,必须先确认对应字典类型在后端代码、配置或明确页面契约中确实被使用;未使用的字典类型和字典数据不要写入 `sys_dict_type``sys_dict_data` 初始化脚本。
## 注释与编码
- 新增或修改代码时,关键字段、关键分支、关键约束和非直观实现应补充简洁中文注释。

View File

@@ -15,6 +15,7 @@ CN_Tool 是一个基于 Spring Boot 的多模块后端聚合工程,当前仓
- `entrance`
- `system`
- `systemmonitor`
- `system-ops`
- `user`
- `detection`
- `tools`
@@ -23,11 +24,18 @@ CN_Tool 是一个基于 Spring Boot 的多模块后端聚合工程,当前仓
- `disk-monitor`
其中 `system-ops` 当前包含:
- `dbms`
- `deploy`
其中 `tools` 当前包含:
- `activate-tool`
- `add-data`
- `add-ledger`
- `mms-mapping`
- `parse-pqdif`
- `wave-tool`
## 启动入口
@@ -36,7 +44,7 @@ CN_Tool 是一个基于 Spring Boot 的多模块后端聚合工程,当前仓
- `entrance/src/main/java/com/njcn/gather/EntranceApplication.java`
`entrance` 模块聚合了 `system``disk-monitor``user``detection``activate-tool``add-data``wave-tool``mms-mapping`,是当前运行时主入口。
`entrance` 模块聚合了 `system``disk-monitor``dbms``deploy``user``detection``activate-tool``add-data``add-ledger``wave-tool``mms-mapping``parse-pqdif`,是当前运行时主入口。
## 技术基线
@@ -79,14 +87,22 @@ P0 已补齐基线文档,建议按以下顺序阅读:
- 负责字典、日志、系统配置、注册资源相关能力
- `systemmonitor/disk-monitor`
- 负责磁盘监控相关能力的独立扩展实现
- `system-ops/dbms`
- 负责系统运维下数据库监控基础入口
- `system-ops/deploy`
- 负责系统运维下系统部署基础入口
- `detection`
- 当前以通信基础设施为主,包含 WebSocket / Netty 相关组件
- `tools/activate-tool`
- 负责激活码生成、激活码验证、许可证读取等能力
- `tools/add-data`
- 当前提供电能质量 13 张表批量补数、任务状态查询和模板规则查询能力
- `tools/add-ledger`
- 当前为数据台账工具预留空模块
- `tools/mms-mapping`
- 负责 ICD 文件解析与 MMS 映射数据生成能力
- `tools/parse-pqdif`
- 当前为 PQDIF 文件解析能力预留空方法骨架
- `tools/wave-tool`
- 负责波形文本解析与查看数据组装能力

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,433 @@
# steady-DataView 稳态历史趋势设计方案
## 目标
`steady/steady-DataView` 中新增稳态历史趋势查看能力,用于展示 InfluxDB 中的稳态数据历史趋势图。数据结构与 `tools/add-data` 中的 13 张 `data_*` 表字段保持一致,但查询数据源改为 InfluxDB 时序库。
本方案只设计后端接口、查询模型、页面交互和性能策略,不包含代码实现。
## 当前上下文
- `steady-DataView` 当前已有 `/steady/data-view/page``/detail``/templates`,主要面向 MySQL 稳态数据分页、详情和模板查询。
- `tools/add-data` 已维护 13 张稳态数据表定义,字段包含基础字段 `TIMEID``LINEID``PHASIC_TYPE``QUALITYFLAG`,以及业务值字段和 `_MAX``_MIN``_CP95` 等统计字段。
- `tools/add-ledger` 已提供台账树能力,层级固定为:工程 -> 项目 -> 设备 -> 监测点。
- `tools/wave-tool` 已有趋势图点位返回结构,可参考 `points``sampled``displayPointCount` 等概念。
## 总体方案
新增趋势查看能力时,`steady-DataView` 负责统一编排:
1.`add-ledger` 复用台账层级,生成趋势页面左侧监测点设备树。
2. 从稳态指标目录生成指标树,指标范围覆盖电压、电流、频率、谐波、功率、闪变。
3. 将页面选择的监测点、指标、相别、统计类型、时间范围转换成 InfluxDB 查询。
4. 返回趋势图可直接消费的多序列点位数据。
5. 按天和分桶粒度动态加载,避免大范围一次性返回明细点。
保留现有分页和详情接口,不在本次设计中移除。
## 左侧设备树
页面左侧展示台账监测点设备树,口径参照 `add-ledger`
```text
工程(设备数 / 监测点数)
项目(设备数 / 监测点数)
设备(监测点数)
监测点
```
数量统计口径:
- 台账节点:`cs_ledger.State = 1`
- 设备:`cs_equipment_delivery.run_status <> 0`
- 监测点:`cs_line.status = 1`
- 设备数:当前节点下有效设备数量
- 监测点数:当前节点下有效监测点数量
建议新增趋势专用接口:
```text
GET /steady/data-view/ledger-tree
```
返回字段:
```json
{
"id": "project-001",
"parentId": "engineering-001",
"name": "10kV一号站",
"level": 1,
"sort": 0,
"deviceCount": 4,
"lineCount": 28,
"selectable": false,
"children": []
}
```
节点选择规则:
- 监测点节点可直接作为趋势查询的 `lineId`
- 设备节点选中时,等价于选中其下全部有效监测点。
- 工程、项目节点选中时,等价于选中其下全部有效监测点,但需要做数量限制。
- 搜索设备名或监测点名时,应保留完整父级路径,避免返回孤立节点。
## 指标树
新增趋势指标树接口:
```text
GET /steady/data-view/indicator-tree
```
指标分组沿用用户提供的结构:
- 电压趋势
- 电流趋势
- 频率趋势
- 谐波趋势
- 功率趋势
- 闪变趋势
后端不直接散落前端 JS 节点,而是维护统一指标目录。每个叶子指标建议包含:
```json
{
"indicatorCode": "V_RMS",
"name": "相电压有效值",
"groupCode": "VOLTAGE",
"tableName": "data_v",
"baseFields": ["RMS"],
"phaseCodes": ["A", "B", "C"],
"seriesFields": [
{
"field": "RMS",
"name": "相电压有效值"
}
],
"supportStats": ["AVG", "MAX", "MIN", "CP95"],
"harmonic": false,
"harmonicOrderStart": null,
"harmonicOrderEnd": null,
"unit": "V"
}
```
普通指标通过 `baseFields` 映射到 InfluxDB field。`phaseCodes` 用于约束 `PHASIC_TYPE` 查询条件,`seriesFields` 用于描述一个指标最终生成几条曲线。谐波指标通过 `fieldPrefix + harmonicOrder + statSuffix` 生成字段,如 `V_5``V_5_MAX``V_5_MIN``V_5_CP95`
部分指标只支持 `T` 相,不能套用全局 A/B/C/T 相别选择。指标树只负责选择业务指标,相别选择由当前指标目录动态驱动:
- `phaseCodes = ["A", "B", "C"]` 的指标,页面展示 A/B/C 相别选择,默认可全选。
- `phaseCodes = ["T"]` 的指标,页面固定为 T 相,可隐藏相别选择或显示为“总量/三相统计”。
- 单监测点多指标查询时,每个指标按自己的 `phaseCodes` 生成曲线,不强制取多个指标的相别交集。
- 多监测点单指标查询时,按该指标的 `phaseCodes` 展示相别选择。
`T` 相不一定只对应一条曲线。例如线电压有效值使用 `PHASIC_TYPE = 'T'` 过滤,但会展开 `RMSAB``RMSBC``RMSCA` 三条曲线:
```json
{
"indicatorCode": "V_LINE_RMS",
"name": "线电压有效值",
"tableName": "data_v",
"phaseCodes": ["T"],
"seriesFields": [
{
"field": "RMSAB",
"name": "AB线电压"
},
{
"field": "RMSBC",
"name": "BC线电压"
},
{
"field": "RMSCA",
"name": "CA线电压"
}
],
"supportStats": ["AVG", "MAX", "MIN", "CP95"],
"unit": "V"
}
```
## InfluxDB 配置
InfluxDB 连接做成配置项,不在代码中写死。
```yaml
steady:
influxdb:
url: http://192.168.1.103:18086
database: pqsbase
username: admin
password: ${STEADY_INFLUXDB_PASSWORD:}
ssl: false
connect-timeout-ms: 5000
read-timeout-ms: 30000
```
本地密码可配置为 `123456`,但不建议提交明文密码到仓库配置文件。
从截图判断当前更接近 InfluxDB 1.x 连接方式,查询方案优先按 InfluxQL 设计。如实际为 InfluxDB 2.x需要将 `database` 改为 `bucket/org/token` 模型。
## InfluxDB 数据模型
建议保持与 `add-data` 表结构一致:
- measurement沿用表名`data_v``data_i``data_harmrate_v`
- timeInfluxDB 内置时间,等价原 `TIMEID`
- tag`LINEID``PHASIC_TYPE``QUALITYFLAG`
- field沿用原业务字段`RMS``RMS_MAX``RMS_MIN``RMS_CP95`
查询时必须使用 `AddDataTableRegistry` 或趋势指标目录做 measurement 和 field 白名单校验,禁止前端传任意表名或字段名直接拼接查询。
## 趋势查询接口
建议新增:
```text
POST /steady/data-view/trend/query
POST /steady/data-view/trend/day
POST /steady/data-view/trend/summary
```
`trend/query` 用于首次查询,返回图表元数据、首屏数据和可加载日期范围。
`trend/day` 用于按天动态加载。前端切换日期、拖动缩放或需要补齐某天数据时调用。
请求示例:
```json
{
"lineIds": ["line-001"],
"indicatorCodes": ["V_RMS", "FREQ"],
"statTypes": ["AVG", "MAX", "MIN", "CP95"],
"phases": ["A", "B", "C"],
"timeStart": "2026-05-01 00:00:00",
"timeEnd": "2026-05-01 23:59:59",
"bucket": "10m",
"qualityFlag": 1
}
```
返回示例:
```json
{
"sampled": true,
"bucket": "10m",
"sourcePointCount": 86400,
"displayPointCount": 144,
"series": [
{
"seriesKey": "line-001|V_RMS|A|AVG",
"lineId": "line-001",
"lineName": "进线一",
"indicatorCode": "V_RMS",
"indicatorName": "相电压有效值",
"phase": "A",
"statType": "AVG",
"unit": "V",
"points": [
{
"time": "2026-05-01 00:00:00",
"value": 220.1
}
]
}
]
}
```
`trend/summary` 返回当前查询范围或当前可视窗口内的统计值:
```json
{
"items": [
{
"seriesKey": "line-001|V_RMS|A",
"max": 232.1,
"avg": 220.4,
"min": 214.8,
"cp95": 228.7
}
]
}
```
## 统计口径
统计类型包括:
- `MAX`:最大值
- `AVG`:平均值
- `MIN`:最小值
- `CP95`95% 概率大值
优先读取已有统计字段:
- `AVG` 读取基础字段,如 `RMS``FREQ``V_5`
- `MAX` 读取 `_MAX` 字段
- `MIN` 读取 `_MIN` 字段
- `CP95` 读取 `_CP95` 字段
如果某些表没有独立统计字段,如部分闪变和波动字段,则接口应在指标目录中标明不支持的统计类型,前端禁用对应选项。仅在明确需要时才使用 InfluxDB 动态聚合兜底。
## 查询模式限制
支持两种主要查询模式:
1. 一个监测点,多指标
2. 多个监测点,一个指标
为了保证图表可读性和查询效率,建议限制:
- 普通趋势:最多 8 个监测点或 8 个指标。
- 多监测点查询时,默认只允许 1 个指标。
- 单监测点查询时,可选择多个指标。
- 多指标混查时,每个指标使用自己的相别配置,避免 A/B/C 指标与 T 相指标交集为空导致无法查询。
- 单次序列数量建议不超过 24 条,超过时提示缩小监测点、指标、相别或统计类型范围。
## 谐波展示方案
谐波类包含 2~50 次谐波,不适合一次性全部画趋势线。建议拆成三种展示:
1. 趋势图模式
- 用户选择谐波类型后,再选择 1 到 6 个谐波次数。
- 只展示被选次数随时间变化。
- 适合观察 3、5、7、11、13 次等重点谐波。
2. 谐波谱模式
- 用户在趋势图上选中某个时间点。
- 展示该时刻 2~50 次谐波柱状图。
- 适合查看单个时刻的谐波分布。
3. TopN 模式
- 后端按当前时间范围统计每个谐波次数的 `MAX``CP95`
- 默认返回 Top 10。
- 适合快速定位主要谐波次数。
谐波趋势默认禁止直接加载 2~50 全部次数。必须指定 `harmonicOrders` 或使用 `topN`
## 按天动态加载
前端首次查询时只加载当前可视范围或默认第一天数据。用户滚动、缩放、切换日期时按天请求。
后端按时间范围自动给出建议 bucket
- 范围小于 6 小时:`1m`
- 6 小时到 1 天:`5m``10m`
- 1 天到 7 天:按天加载,每天 `10m``30m`
- 大于 7 天:优先按天加载摘要,进入某天后再加载细节
接口返回 `sampled``sourcePointCount``displayPointCount`,前端可提示当前是否已下采样。
## 缓存策略
建议增加短期查询缓存,缓存 key 包含:
```text
lineIds + indicatorCodes + phases + statTypes + day + bucket + qualityFlag
```
缓存粒度以单日为主,避免一个大时间范围缓存过大。缓存只用于趋势点位,不缓存配置密码和连接对象以外的敏感信息。
## 页面布局
推荐布局:
```text
顶部:时间范围 / 相别 / 统计类型 / 查询 / 重置
左侧:
台账监测点树
- 工程、项目、设备、监测点
- 显示设备数、监测点数
- 支持搜索与勾选
左侧下方或中左:
稳态指标树
- 电压趋势
- 电流趋势
- 频率趋势
- 谐波趋势
- 功率趋势
- 闪变趋势
中间:
趋势图
- 折线图
- 图例开关
- tooltip
- 缩放联动
- 按天动态加载
右侧:
当前范围统计
- 最大值
- 平均值
- 最小值
- CP95
```
谐波类指标被选中时,趋势图上方增加:
- 谐波次数选择器
- 趋势 / 谱图 / TopN 切换
## 后端模块划分
建议在 `steady-DataView` 中按职责拆分:
- `SteadyDataViewLedgerController`:趋势页面台账树
- `SteadyDataViewIndicatorController`:指标树
- `SteadyDataViewTrendController`:趋势查询、按天加载、统计摘要
- `SteadyDataViewLedgerService`:复用 add-ledger 口径生成带数量的树
- `SteadyDataViewIndicatorService`:维护指标目录和字段映射
- `SteadyDataViewTrendService`:参数校验、查询编排、结果组装
- `SteadyInfluxQueryComponent`InfluxDB 查询封装
- `SteadyTrendFieldResolver`:指标到 measurement/field 的白名单转换
保留现有 `SteadyDataViewController` 的分页、详情、模板职责,不继续把所有新接口堆到一个大 Controller。
## 验证方式
方案落地后,默认验证不执行 Maven 构建,除非用户明确要求。
建议检查:
1. 配置检查InfluxDB 配置项可被 Spring Boot 读取,密码未硬编码提交。
2. 指标映射检查:所有指标都能解析到合法 measurement 和 field。
3. 台账树检查:设备数、监测点数与 add-ledger 有效数据口径一致。
4. 查询限制检查:超出监测点、指标、谐波次数限制时返回明确提示。
5. 查询语句检查InfluxDB 查询必须带时间范围、`LINEID` 和必要 tag 条件。
6. 趋势返回检查:返回 series、points、统计摘要结构稳定前端可直接绘图。
7. 谐波检查:趋势模式不默认返回 2~50 全部次数,谱图和 TopN 按独立接口或模式返回。
## 分期建议
第一期:
- InfluxDB 可配置连接
- 台账监测点树,包含设备数和监测点数
- 稳态指标树
- 普通指标趋势查询
- 最大值、平均值、最小值、CP95
- 按天动态加载
- 谐波趋势支持手动选择次数
第二期:
- 谐波谱图
- 谐波 TopN
- 查询缓存优化
- 大范围摘要视图
- 前端交互细节优化
## 待确认事项
1. InfluxDB 版本是否确定为 1.x。
2. 生产环境 InfluxDB measurement 是否完全沿用 `data_*` 表名。
3. `QUALITYFLAG` 在 InfluxDB 中是否作为 tag 保存。
4. 谐波相角、间谐波等字段命名是否与 `add-data` 当前 SQL 完全一致。
5. 前端是否已有可复用的 ECharts 趋势图组件,还是只参考 waveform 返回结构重新接入。

View File

@@ -0,0 +1,514 @@
# Linux 服务器部署运维设计
## 1. 背景
`system-ops/deploy` 当前只提供系统部署菜单的基础入口:
- `GET /deploy/overview`
- `DeployController`
- `DeployService`
- `DeployOverviewVO`
本次需求是在 `deploy` 模块中补充 Linux 服务器远程运维能力。用户可以维护 Linux 服务器连接配置,基于 SSH/SFTP 连接服务器,完成远程文件上传、下载和基础命令终端操作。命令终端目标体验接近 Xshell 的基础能力。
当前仓库没有前端代码,本设计只定义页面布局、接口契约、后端模块拆分、数据存储和验证方式,不实现真实前端页面。
## 2. 范围确认
本期只支持 Linux 服务器。
本期包含:
- Linux SSH 连接配置的新增、编辑、删除、查询。
- SSH 连接测试。
- SFTP 文件列表、上传、下载、删除、新建目录。
- SSH Shell 基础命令交互。
- 前端单页运维工作台布局设计。
- 连接配置使用文件方式存储,不新建数据库表。
本期不包含:
- Windows 服务器。
- FTP 协议。
- 数据库存储连接配置。
- 部署任务编排。
- 命令审批、命令黑名单、命令历史。
- 批量文件压缩下载。
- 数据库专用客户端封装。
- Maven 编译、打包、测试。
说明:需求中提到的 “FPT” 本期按 Linux 服务器常用能力理解为 SFTP。SFTP 复用 SSH 账号、密码和端口,比单独 FTP 更适合本期场景。
## 3. 总体方案
推荐采用 “SSH/SFTP + WebSocket 终端” 方案:
- 服务器连接配置保存到本地 JSON 文件。
- 后端通过 SSH 建立 Linux 连接。
- 文件操作通过 SFTP 通道完成。
- 终端操作通过 SSH Shell 通道完成。
- 前端通过 WebSocket 与后端交换终端输入输出。
该方案可以复用同一份服务器连接配置,不需要引入 Windows 远程协议,也能满足类 Xshell 的基础交互需求。
## 4. 前端页面布局
页面路径建议沿用当前菜单路径:
```text
/systemOps/deploy
```
页面采用三块工作区:
- 左侧:服务器列表。
- 中间:远程文件管理。
- 右侧:连接详情和快捷操作。
- 底部SSH 终端区。
推荐布局:
```text
┌──────────────────────────────────────────────────────────────┐
│ 顶部工具栏:新增连接 测试连接 刷新 当前连接状态 │
├──────────────┬──────────────────────────────┬────────────────┤
│ 服务器列表 │ 远程文件管理 │ 连接详情/操作 │
│ │ │ │
│ Linux-测试 │ 路径栏:/opt/app │ 主机/IP │
│ Linux-生产 │ 上传 下载 新建目录 删除 刷新 │ 用户名 │
│ │ │ 端口 │
│ │ 文件表格 │ 测试连接 │
│ │ │ 打开终端 │
├──────────────┴──────────────────────────────┴────────────────┤
│ 终端 TabsLinux-测试 │
│ $ pwd │
│ /opt/app │
└──────────────────────────────────────────────────────────────┘
```
### 4.1 服务器列表
左侧服务器列表用于选择当前操作目标。
展示字段:
| 字段 | 说明 |
|---|---|
| 名称 | 服务器显示名称 |
| 主机地址 | IP 或域名 |
| SSH 端口 | 默认 22 |
| 连接状态 | 未测试、连接成功、连接失败 |
交互:
- 支持按名称、主机地址搜索。
- 点击服务器后加载连接详情,并将文件管理区切换到该服务器。
- 列表项提供编辑、删除、测试连接入口。
- 删除连接前必须二次确认。
### 4.2 连接配置弹窗
新增和编辑使用同一个弹窗。
字段:
| 字段 | 是否必填 | 说明 |
|---|---|---|
| 名称 | 是 | 页面展示名称 |
| 主机地址 | 是 | Linux 服务器 IP 或域名 |
| SSH 端口 | 是 | 默认 22范围 1-65535 |
| 用户名 | 是 | SSH 登录用户 |
| 密码 | 新增必填 | 编辑时留空表示不修改 |
| 备注 | 否 | 环境说明 |
按钮:
- 测试连接。
- 保存。
- 取消。
密码规则:
- 新增连接时密码必填。
- 编辑连接时密码不回显。
- 编辑时密码为空表示沿用原密码。
- 查询列表和详情接口均不返回密码。
### 4.3 远程文件管理
中间文件管理区基于当前选中的服务器工作。
顶部路径栏:
- 展示当前远程目录,例如 `/opt/app`
- 支持返回上级目录。
- 支持点击面包屑跳转到上级路径。
工具栏:
- 上传。
- 下载。
- 新建目录。
- 删除。
- 刷新。
文件表格字段:
| 字段 | 说明 |
|---|---|
| 名称 | 文件或目录名称 |
| 类型 | 文件、目录、软链接 |
| 大小 | 文件大小,目录可为空 |
| 权限 | Linux 权限字符串 |
| 修改时间 | 远程文件修改时间 |
交互规则:
- 双击目录进入下级目录。
- 下载只支持普通文件。
- 删除文件或目录前必须二次确认。
- 本期支持单文件上传和单文件下载。
- 上传目标目录为当前路径。
- 下载目录、批量压缩下载不在本期范围。
### 4.4 SSH 终端区
底部终端区用于执行 Linux 命令。
交互规则:
- 点击“打开终端”后创建 SSH Shell 会话。
- 前端输入通过 WebSocket 实时发送给后端。
- 后端将 Shell 输出通过 WebSocket 实时推送给前端。
- 本期建议限制为每台服务器最多一个终端会话。
- 关闭终端 Tab 时通知后端释放 SSH 会话。
- 终端断开后显示状态,不自动重连。
用户可以在终端中自行执行数据库命令,例如:
```bash
mysql -uroot -p
psql -h 127.0.0.1 -U postgres
redis-cli
```
后端不解析数据库命令,也不保存命令历史。
## 5. 后端结构设计
`system-ops/deploy` 模块内按职责新增类,保留现有 `DeployController``/deploy/overview`
建议结构:
```text
system-ops/deploy/src/main/java/com/njcn/gather/systemops/deploy/
├── config/
├── controller/
├── pojo/param/
├── pojo/vo/
├── pojo/dto/
├── repository/
├── service/
├── service/impl/
└── websocket/
```
职责拆分:
| 类 | 职责 |
|---|---|
| `DeployServerController` | 连接配置查询、新增、编辑、删除、测试连接 |
| `DeployFileController` | SFTP 文件列表、上传、下载、删除、新建目录 |
| `DeployTerminalWebSocketHandler` | SSH 终端 WebSocket 输入输出转发 |
| `DeployServerConfigService` | 连接配置业务校验和编排 |
| `DeployServerConfigRepository` | JSON 文件读写 |
| `DeploySftpService` | SFTP 文件操作 |
| `DeploySshTerminalService` | SSH Shell 会话创建、输入、输出、关闭 |
| `DeployCryptoService` | 密码加密和解密 |
| `DeployProperties` | deploy 配置项绑定 |
## 6. 连接配置存储
连接配置不入库,使用 JSON 文件落盘。存储目录通过配置指定。
建议配置:
```yaml
deploy:
storage-dir: ${log.homeDir}/deploy
terminal-idle-timeout-minutes: 30
```
`deploy.crypto-key` 不建议在默认 `application.yml` 中配置明文值。后续实现时可通过环境覆盖或外部配置提供,业务代码只读取配置,不写死密钥。
落盘文件:
```text
D:\logs\deploy\deploy-server-connections.json
```
JSON 结构:
```json
{
"servers": [
{
"id": "uuid",
"name": "测试服务器",
"host": "192.168.1.10",
"sshPort": 22,
"username": "root",
"password": "加密密文",
"description": "测试环境",
"createdTime": "2026-05-21 14:00:00",
"updatedTime": "2026-05-21 14:00:00"
}
]
}
```
写文件规则:
- 启动时如果文件不存在,自动创建空配置文件。
- 读写方法集中在 `DeployServerConfigRepository`
- 写入时先写临时文件,再替换正式文件,避免进程中断导致 JSON 损坏。
- 保存和删除操作需要加进程内锁,避免并发写入互相覆盖。
密码规则:
- 密码必须加密后落盘。
- 接口返回不包含密码。
- 日志不打印密码。
- 优先复用项目已有加密能力;如没有合适工具,则在 `deploy` 内封装 AES 加解密组件。
- 加密密钥通过配置提供,不在业务代码中硬编码。
## 7. 接口设计
接口风格沿用当前仓库常见写法:查询和变更优先使用 `POST`,返回 `HttpResult<T>`
### 7.1 连接配置接口
| 方法 | 路径 | 说明 |
|---|---|---|
| `POST` | `/deploy/server/list` | 查询服务器连接配置列表 |
| `POST` | `/deploy/server/add` | 新增服务器连接配置 |
| `POST` | `/deploy/server/update` | 修改服务器连接配置 |
| `POST` | `/deploy/server/delete` | 删除服务器连接配置 |
| `POST` | `/deploy/server/test` | 测试 SSH 连接 |
列表返回字段:
| 字段 | 说明 |
|---|---|
| `id` | 连接 ID |
| `name` | 服务器名称 |
| `host` | 主机地址 |
| `sshPort` | SSH 端口 |
| `username` | 用户名 |
| `description` | 备注 |
| `createdTime` | 创建时间 |
| `updatedTime` | 更新时间 |
新增参数:
| 字段 | 是否必填 |
|---|---|
| `name` | 是 |
| `host` | 是 |
| `sshPort` | 是 |
| `username` | 是 |
| `password` | 是 |
| `description` | 否 |
编辑参数:
| 字段 | 是否必填 | 说明 |
|---|---|---|
| `id` | 是 | 连接 ID |
| `name` | 是 | 服务器名称 |
| `host` | 是 | 主机地址 |
| `sshPort` | 是 | SSH 端口 |
| `username` | 是 | 用户名 |
| `password` | 否 | 为空表示不修改 |
| `description` | 否 | 备注 |
### 7.2 文件接口
| 方法 | 路径 | 说明 |
|---|---|---|
| `POST` | `/deploy/file/list` | 查询远程目录文件列表 |
| `POST` | `/deploy/file/mkdir` | 新建远程目录 |
| `POST` | `/deploy/file/delete` | 删除远程文件或目录 |
| `POST` | `/deploy/file/upload` | 上传本地文件到远程目录 |
| `POST` | `/deploy/file/download` | 下载远程普通文件 |
文件列表参数:
| 字段 | 是否必填 | 说明 |
|---|---|---|
| `serverId` | 是 | 服务器连接 ID |
| `path` | 是 | 远程目录路径 |
文件列表返回字段:
| 字段 | 说明 |
|---|---|
| `name` | 文件名 |
| `path` | 完整路径 |
| `type` | `FILE``DIRECTORY``LINK` |
| `size` | 文件大小 |
| `permissions` | 权限字符串 |
| `modifiedTime` | 修改时间 |
下载接口直接写入 `HttpServletResponse`。下载文件名沿用远程文件名,不追加日期;仓库“导出或生成文件追加日期”的规则适用于后端生成或导出文件,本功能是下载远程已有文件,不改变原文件名。
### 7.3 终端 WebSocket
终端连接:
```text
WebSocket /deploy/terminal?serverId={serverId}
```
前端发送输入:
```json
{
"type": "input",
"data": "ls -la\n"
}
```
前端发送窗口大小:
```json
{
"type": "resize",
"cols": 120,
"rows": 30
}
```
后端输出:
```json
{
"type": "output",
"data": "total 20\r\n..."
}
```
后端状态:
```json
{
"type": "status",
"status": "CONNECTED"
}
```
异常消息:
```json
{
"type": "error",
"message": "SSH连接失败"
}
```
## 8. 参数校验
后端至少补充以下校验:
- 服务器名称不能为空。
- 主机地址不能为空。
- SSH 端口范围为 `1-65535`
- 用户名不能为空。
- 新增连接时密码不能为空。
- 编辑连接时 `id` 必须存在。
- 删除连接时 `id` 必须存在。
- 同一主机、端口、用户名组合不建议重复保存。
- 文件路径不能为空。
- 文件上传目标必须是远程目录。
- 下载目标必须是远程普通文件。
- 删除路径不能为空,不能删除空路径或根目录 `/`
- 新建目录名称不能为空,不能包含路径分隔符。
## 9. 安全与资源控制
安全规则:
- 密码不明文落盘。
- 接口返回不包含密码。
- 日志不打印密码、终端输入内容、文件内容。
- 终端不保存命令历史。
- 文件路径需要做基础规范化,避免空路径、非法路径和目录穿越。
- 下载只允许下载普通文件。
资源规则:
- SSH 连接测试设置连接超时,例如 5 秒。
- SFTP 操作每次请求创建短连接,操作完成后释放。
- 终端会话保持长连接,关闭 WebSocket 后释放 SSH Session 和 Channel。
- 终端会话设置空闲超时,默认 30 分钟。
- 本期每台服务器最多保留一个终端会话。
## 10. 依赖建议
后续实现 SSH/SFTP 时建议优先选择 Java 8 可用、项目易接入的 SSH 客户端库,例如 JSch 或 sshj。
选择标准:
- 支持 SSH 密码登录。
- 支持 SFTP 文件操作。
- 支持 Shell Channel。
- 能在 Spring Boot 2.3 和 Java 8 下稳定使用。
最终依赖需要写入 `system-ops/deploy/pom.xml`,不影响其他模块。
## 11. 错误处理
连接测试需要区分常见错误:
| 场景 | 返回说明 |
|---|---|
| 主机不可达 | 连接服务器失败 |
| 端口不通 | SSH端口连接失败 |
| 账号或密码错误 | SSH认证失败 |
| SFTP 打开失败 | 文件通道打开失败 |
| 终端打开失败 | Shell通道打开失败 |
接口层仍使用项目现有 `HttpResult``CommonResponseEnum` 风格。具体错误文案由 Service 返回给 Controller不新增全局异常体系。
## 12. 验证方式
默认不执行 Maven 编译、打包、测试命令。后续实现完成后按以下方式验证:
- 检查 `deploy` 新增代码只位于 `system-ops/deploy`
- 新增连接后,接口返回和 JSON 文件内容一致。
- 编辑连接时密码留空不会覆盖原密码。
- 删除连接后JSON 文件同步移除对应记录。
- 查询接口不返回密码。
- JSON 文件中密码不是明文。
- 测试连接能识别成功、主机不可达、端口不通、账号密码错误。
- 文件列表能展示远程目录内容。
- 上传文件后远程目录可见。
- 下载普通文件内容与远程文件一致。
- 删除文件或目录后远程路径不存在。
- 新建目录后远程路径存在。
- 终端能打开 Linux Shell执行 `pwd``ls -la``mysql --version` 等基础命令。
- 关闭终端后,后端 SSH 会话被释放。
## 13. 后续扩展
后续如需求增加,可以在当前方案基础上扩展:
- SSH 私钥登录。
- 多终端 Tab。
- 命令审计和历史记录。
- 命令黑名单或审批。
- 部署脚本编排。
- 文件批量上传和批量下载。
- Windows WinRM 或 PowerShell Remoting。
这些能力不进入本期实现,避免当前 `deploy` 模块从基础入口一次扩张为完整运维平台。

View File

@@ -21,6 +21,16 @@
<artifactId>disk-monitor</artifactId>
<version>1.0.0</version>
</dependency>
<dependency>
<groupId>com.njcn.gather</groupId>
<artifactId>dbms</artifactId>
<version>1.0.0</version>
</dependency>
<dependency>
<groupId>com.njcn.gather</groupId>
<artifactId>deploy</artifactId>
<version>1.0.0</version>
</dependency>
<dependency>
<groupId>com.njcn.gather</groupId>
<artifactId>detection</artifactId>
@@ -48,11 +58,31 @@
<artifactId>mms-mapping</artifactId>
<version>1.0.0</version>
</dependency>
<dependency>
<groupId>com.njcn.gather</groupId>
<artifactId>parse-pqdif</artifactId>
<version>1.0.0</version>
</dependency>
<dependency>
<groupId>com.njcn.gather</groupId>
<artifactId>add-data</artifactId>
<version>1.0.0</version>
</dependency>
<dependency>
<groupId>com.njcn.gather</groupId>
<artifactId>add-ledger</artifactId>
<version>1.0.0</version>
</dependency>
<dependency>
<groupId>com.njcn.gather</groupId>
<artifactId>event-list</artifactId>
<version>1.0.0</version>
</dependency>
<dependency>
<groupId>com.njcn.gather</groupId>
<artifactId>steady-DataView</artifactId>
<version>1.0.0</version>
</dependency>
</dependencies>
<build>

View File

@@ -48,6 +48,16 @@ socket:
webSocket:
port: 7777
steady:
influxdb:
url: http://127.0.0.1:18086
database: pqsbase
username: admin
password: 123456
ssl: false
connect-timeout-ms: 5000
read-timeout-ms: 30000
log:
homeDir: D:\logs
commonLevel: info

View File

@@ -0,0 +1,317 @@
# event-list API 调试文档
## 1. 基础信息
- 模块:`event/event-list`
- 控制器:`EventListController`
- 接口前缀:`/event/list`
- 本地默认地址:`http://localhost:18192`
- Content-Type`application/json`
- 认证:除 `/admin/login``/admin/getPublicKey`、Swagger 资源外,请求需要携带登录后的 `Authorization` 头。
```text
Authorization: Bearer <accessToken>
```
说明:
- `event-list` 当前提供暂态事件分页查询、详情查询和导出能力。
- 事件数据来自 `r_mp_event_detail`
- 工程、项目、设备、监测点名称由 `tools/add-ledger` 内部服务按监测点 ID 批量补齐。
- 本文示例中的业务数据为调试示例,实际值以数据库为准。
## 2. 通用响应
分页和详情接口统一返回 `HttpResult` 包装结果。外层字段由公共组件序列化,常见结构如下:
```json
{
"code": "000000",
"message": "成功",
"data": {}
}
```
调试时重点查看:
- `code`:业务响应码。
- `message`:业务响应消息。
- `data`:接口返回数据。
导出接口直接返回 Excel 文件流,不返回 `HttpResult` JSON。
## 3. 分页查询暂态事件列表
### 3.1 接口信息
- 路径:`POST /event/list/transient/page`
- 完整地址:`http://localhost:18192/event/list/transient/page`
- 控制器方法:`EventListController#pageTransientEvents`
- 服务方法:`EventListService#pageTransientEvents`
- 返回:`HttpResult<Page<EventListVO>>`
- 默认排序:`start_time DESC, event_id DESC`
### 3.2 请求示例
```json
{
"pageNum": 1,
"pageSize": 10,
"startTimeStart": "2026-05-01 00:00:00",
"startTimeEnd": "2026-05-09 23:59:59",
"eventType": "VOLTAGE_SAG",
"phase": "A",
"eventDescribe": "暂降",
"durationMin": 0.02,
"durationMax": 10,
"featureAmplitudeMin": 10,
"featureAmplitudeMax": 90,
"fileFlag": 1,
"dealFlag": 0,
"lineIds": [
"line-001"
],
"engineeringName": "示例工程",
"projectName": "示例项目",
"equipmentName": "示例设备",
"lineName": "示例测点"
}
```
分页字段继承公共 `BaseParam`,调试时按项目现有分页参数约定传入。示例使用 `pageNum``pageSize`
### 3.3 请求字段
| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `pageNum` | Number | 否 | 页码,来自公共分页参数 |
| `pageSize` | Number | 否 | 每页条数,来自公共分页参数 |
| `startTimeStart` | String | 否 | 发生时刻开始,格式 `yyyy-MM-dd HH:mm:ss` |
| `startTimeEnd` | String | 否 | 发生时刻结束,格式 `yyyy-MM-dd HH:mm:ss` |
| `eventType` | String | 否 | 事件类型,对应 `r_mp_event_detail.event_type` |
| `phase` | String | 否 | 相别,对应 `r_mp_event_detail.phase` |
| `eventDescribe` | String | 否 | 事件描述关键字,按 `LIKE` 查询 |
| `durationMin` | Number | 否 | 持续时间下限,单位秒 |
| `durationMax` | Number | 否 | 持续时间上限,单位秒 |
| `featureAmplitudeMin` | Number | 否 | 暂降/暂升幅值下限 |
| `featureAmplitudeMax` | Number | 否 | 暂降/暂升幅值上限 |
| `fileFlag` | Number | 否 | 波形文件状态:`0` 未招,`1` 已招 |
| `dealFlag` | Number | 否 | 处理状态:`0` 未处理,`1` 已处理,`2` 已处理无结果,`3` 计算失败 |
| `lineIds` | Array<String> | 否 | 监测点 ID 列表,最多 1000 个 |
| `engineeringName` | String | 否 | 工程名称关键字,通过 `add-ledger` 反查监测点 |
| `projectName` | String | 否 | 项目名称关键字,通过 `add-ledger` 反查监测点 |
| `equipmentName` | String | 否 | 设备名称关键字,通过 `add-ledger` 反查监测点 |
| `lineName` | String | 否 | 监测点名称关键字,通过 `add-ledger` 反查监测点 |
时间字段支持以下输入格式:
- `yyyy-MM-dd HH:mm:ss`
- `yyyy-MM-dd HH:mm:ss.SSS`
- `yyyy-MM-dd'T'HH:mm:ss`
- `yyyy-MM-dd'T'HH:mm:ss.SSS`
如果不传 `startTimeStart`,后端默认取当前月 1 日 `00:00:00`。如果不传 `startTimeEnd`,后端默认取当前时间。
### 3.4 响应示例
```json
{
"code": "000000",
"message": "成功",
"data": {
"records": [
{
"eventId": "event-001",
"measurementPointId": "line-001",
"eventType": "VOLTAGE_SAG",
"equipmentName": "示例设备",
"engineeringName": "示例工程",
"projectName": "示例项目",
"startTime": "2026-05-09 10:20:30",
"lineName": "示例测点",
"eventDescribe": "电压暂降",
"sagsource": "上游",
"phase": "A",
"duration": 0.12,
"featureAmplitude": 65.5,
"wavePath": "D:/wave/event-001.cfg",
"fileFlag": 1,
"dealFlag": 0,
"createTime": "2026-05-09 10:21:00"
}
],
"total": 1,
"size": 10,
"current": 1,
"pages": 1
}
}
```
`Page` 对象可能包含 MyBatis-Plus 的其他分页字段,调试时主要关注 `records``total``size``current``pages`
### 3.5 curl 示例
```powershell
curl.exe -X POST "http://localhost:18192/event/list/transient/page" `
-H "Content-Type: application/json" `
-H "Authorization: Bearer <accessToken>" `
-d "{\"pageNum\":1,\"pageSize\":10,\"startTimeStart\":\"2026-05-01 00:00:00\",\"startTimeEnd\":\"2026-05-09 23:59:59\",\"eventDescribe\":\"暂降\"}"
```
## 4. 查询暂态事件详情
### 4.1 接口信息
- 路径:`GET /event/list/transient/{eventId}`
- 完整地址:`http://localhost:18192/event/list/transient/{eventId}`
- 控制器方法:`EventListController#getTransientEventDetail`
- 服务方法:`EventListService#getTransientEventDetail`
- 返回:`HttpResult<EventListVO>`
### 4.2 请求参数
| 参数 | 位置 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- | --- |
| `eventId` | Path | String | 是 | 暂态事件 ID对应 `r_mp_event_detail.event_id` |
### 4.3 响应示例
```json
{
"code": "000000",
"message": "成功",
"data": {
"eventId": "event-001",
"measurementPointId": "line-001",
"eventType": "VOLTAGE_SAG",
"equipmentName": "示例设备",
"engineeringName": "示例工程",
"projectName": "示例项目",
"startTime": "2026-05-09 10:20:30",
"lineName": "示例测点",
"eventDescribe": "电压暂降",
"sagsource": "上游",
"phase": "A",
"duration": 0.12,
"featureAmplitude": 65.5,
"wavePath": "D:/wave/event-001.cfg",
"fileFlag": 1,
"dealFlag": 0,
"createTime": "2026-05-09 10:21:00"
}
}
```
### 4.4 curl 示例
```powershell
curl.exe -X GET "http://localhost:18192/event/list/transient/event-001" `
-H "Authorization: Bearer <accessToken>"
```
## 5. 导出暂态事件列表
### 5.1 接口信息
- 路径:`POST /event/list/transient/export`
- 完整地址:`http://localhost:18192/event/list/transient/export`
- 控制器方法:`EventListController#exportTransientEvents`
- 服务方法:`EventListService#exportTransientEvents`
- 返回Excel 文件流
- 文件名:`暂态事件列表.xlsx`
- Sheet 名称:`暂态事件列表`
导出复用分页查询的筛选条件,但不使用分页结果。当前同步导出最多允许 5000 条,超过时返回业务错误。
### 5.2 请求示例
```json
{
"startTimeStart": "2026-05-01 00:00:00",
"startTimeEnd": "2026-05-09 23:59:59",
"eventDescribe": "暂降",
"fileFlag": 1
}
```
### 5.3 导出字段
| Excel 列名 | 响应字段 | 说明 |
| --- | --- | --- |
| 设备名称 | `equipmentName` | 由 `add-ledger` 按监测点补齐 |
| 工程名称 | `engineeringName` | 由 `add-ledger` 按监测点补齐 |
| 项目名称 | `projectName` | 由 `add-ledger` 按监测点补齐 |
| 发生时刻 | `startTime` | 格式 `yyyy-MM-dd HH:mm:ss` |
| 监测点名称 | `lineName` | 由 `add-ledger` 按监测点补齐 |
| 事件描述 | `eventDescribe` | 为空时使用 `eventType` |
| 事件发生位置 | `sagsource` | 为空时返回 `-` |
| 相别 | `phase` | 为空时返回 `-` |
| 持续时间(s) | `duration` | 单位秒 |
| 暂降/暂升幅值(%) | `featureAmplitude` | 原值导出 |
### 5.4 curl 示例
```powershell
curl.exe -X POST "http://localhost:18192/event/list/transient/export" `
-H "Content-Type: application/json" `
-H "Authorization: Bearer <accessToken>" `
-d "{\"startTimeStart\":\"2026-05-01 00:00:00\",\"startTimeEnd\":\"2026-05-09 23:59:59\",\"eventDescribe\":\"暂降\"}" `
-o "D:/temp/暂态事件列表.xlsx"
```
## 6. 返回字段说明
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `eventId` | String | 事件 ID |
| `measurementPointId` | String | 监测点 ID |
| `eventType` | String | 事件类型 |
| `equipmentName` | String | 设备名称,台账缺失时为 `-` |
| `engineeringName` | String | 工程名称,台账缺失时为 `-` |
| `projectName` | String | 项目名称,台账缺失时为 `-` |
| `startTime` | String | 发生时刻,格式 `yyyy-MM-dd HH:mm:ss` |
| `lineName` | String | 监测点名称,台账缺失时为 `-` |
| `eventDescribe` | String | 事件描述,空值时返回 `eventType` |
| `sagsource` | String | 事件发生位置,空值时为 `-` |
| `phase` | String | 相别,空值时为 `-` |
| `duration` | Number | 持续时间,单位秒 |
| `featureAmplitude` | Number | 暂降/暂升幅值 |
| `wavePath` | String | 波形文件路径 |
| `fileFlag` | Number | 波形文件状态:`0` 未招,`1` 已招 |
| `dealFlag` | Number | 处理状态:`0` 未处理,`1` 已处理,`2` 已处理无结果,`3` 计算失败 |
| `createTime` | String | 创建时间,格式 `yyyy-MM-dd HH:mm:ss` |
## 7. 常见错误场景
| 场景 | 后端提示 |
| --- | --- |
| 详情接口 `eventId` 为空 | `事件 ID 不能为空` |
| 详情数据不存在 | `暂态事件不存在` |
| 开始时间大于结束时间 | `开始时间不能大于结束时间` |
| 时间格式无法解析 | `时间格式不正确,仅支持 yyyy-MM-dd HH:mm:ss` |
| 持续时间下限大于上限 | `持续时间下限不能大于上限` |
| 幅值下限大于上限 | `幅值下限不能大于上限` |
| `fileFlag` 不为 `0``1` | `波形文件状态只能是 0 或 1` |
| `dealFlag` 不在 `0-3` 范围内 | `处理状态只能是 0、1、2、3` |
| `lineIds` 超过 1000 个 | `监测点 ID 查询数量不能超过 1000 个` |
| 台账关键字匹配监测点超过 1000 个 | `台账检索匹配监测点过多,请缩小查询条件` |
| 导出超过 5000 条 | `导出数据超过 5000 条,请缩小查询条件` |
未携带有效 `Authorization` 时,全局认证过滤器会返回 token 解析相关错误。
## 8. 调试注意事项
- 先登录获取 `accessToken`,再调试 `event-list` 接口。
- 分页查询不传时间范围时,后端默认查当前月 1 日到当前时间。
- 台账关键字筛选会先通过 `add-ledger` 查询匹配监测点,再查询事件表。
- 如果台账关键字命中范围过大,需要缩小工程、项目、设备或测点关键字。
- 导出接口建议始终传入明确时间范围,避免超过 5000 条限制。
- 如查询性能不稳定,可先检查是否已按需执行 `event/event-list/src/main/resources/sql/event-list/event-list-index.sql` 中的建议索引。
## 9. 当前限制
- 本文档只补充 API 调试说明,未改动业务代码。
- 当前未执行 `mvn` 编译、打包、测试或真实接口联调。
- 响应外层 `HttpResult` 字段以公共组件实际序列化结果为准。
- 分页参数字段以公共 `BaseParam` 实际定义和前端现有调用约定为准。

60
event/event-list/pom.xml Normal file
View File

@@ -0,0 +1,60 @@
<?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>event</artifactId>
<version>1.0.0</version>
</parent>
<artifactId>event-list</artifactId>
<dependencies>
<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>com.njcn.gather</groupId>
<artifactId>add-ledger</artifactId>
<version>1.0.0</version>
</dependency>
<dependency>
<groupId>com.njcn.gather</groupId>
<artifactId>system</artifactId>
<version>1.0.0</version>
</dependency>
<dependency>
<groupId>com.njcn.gather</groupId>
<artifactId>wave-tool</artifactId>
<version>1.0.0</version>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>4.1.2</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,26 @@
package com.njcn.gather.event.eventlist.config;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import java.io.IOException;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
/**
* 暂态事件时间字段按毫秒输出,保持与数据库 datetime(3) 精度一致。
*/
public class EventMillisecondTimeSerializer extends JsonSerializer<LocalDateTime> {
private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS");
@Override
public void serialize(LocalDateTime value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
if (value == null) {
gen.writeNull();
return;
}
gen.writeString(FORMATTER.format(value));
}
}

View File

@@ -0,0 +1,86 @@
package com.njcn.gather.event.eventlist.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.event.eventlist.pojo.param.EventListQueryParam;
import com.njcn.gather.event.eventlist.pojo.param.EventWaveExportParam;
import com.njcn.gather.event.eventlist.pojo.vo.EventListVO;
import com.njcn.gather.event.eventlist.service.EventListService;
import com.njcn.gather.tool.wave.pojo.vo.WaveComtradeResultVO;
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.PathVariable;
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;
/**
* 事件列表接口预留入口。
*/
@Validated
@Slf4j
@Api(tags = "事件列表")
@RestController
@RequestMapping("/event/list")
@RequiredArgsConstructor
public class EventListController extends BaseController {
/** 事件列表服务。 */
private final EventListService eventListService;
@OperateInfo(info = LogEnum.BUSINESS_COMMON)
@ApiOperation("分页查询暂态事件列表")
@PostMapping("/transient/page")
public HttpResult<Page<EventListVO>> pageTransientEvents(@RequestBody EventListQueryParam param) {
String methodDescribe = getMethodDescribe("pageTransientEvents");
LogUtil.njcnDebug(log, "{}开始分页查询暂态事件列表param={}", methodDescribe, param);
Page<EventListVO> result = eventListService.pageTransientEvents(param);
return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, result, methodDescribe);
}
@OperateInfo(info = LogEnum.BUSINESS_COMMON)
@ApiOperation("查询暂态事件详情")
@GetMapping("/transient/{eventId}")
public HttpResult<EventListVO> getTransientEventDetail(@PathVariable("eventId") String eventId) {
String methodDescribe = getMethodDescribe("getTransientEventDetail");
LogUtil.njcnDebug(log, "{}开始查询暂态事件详情eventId={}", methodDescribe, eventId);
EventListVO result = eventListService.getTransientEventDetail(eventId);
return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, result, methodDescribe);
}
@OperateInfo(info = LogEnum.BUSINESS_COMMON, operateType = OperateType.DOWNLOAD)
@ApiOperation("导出暂态事件列表")
@PostMapping("/transient/export")
public void exportTransientEvents(@RequestBody EventListQueryParam param) {
eventListService.exportTransientEvents(param);
}
@OperateInfo(info = LogEnum.BUSINESS_COMMON)
@ApiOperation("查看暂态事件波形")
@GetMapping("/transient/{eventId}/wave")
public HttpResult<WaveComtradeResultVO> getTransientEventWave(@PathVariable("eventId") String eventId) {
String methodDescribe = getMethodDescribe("getTransientEventWave");
LogUtil.njcnDebug(log, "{}开始查看暂态事件波形eventId={}", methodDescribe, eventId);
WaveComtradeResultVO result = eventListService.getTransientEventWave(eventId);
return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, result, methodDescribe);
}
@OperateInfo(info = LogEnum.BUSINESS_COMMON, operateType = OperateType.DOWNLOAD)
@ApiOperation("导出选中暂态事件波形")
@PostMapping("/transient/wave/export")
public void exportTransientEventWaves(@RequestBody EventWaveExportParam param) {
eventListService.exportTransientEventWaves(param);
}
}

View File

@@ -0,0 +1,23 @@
package com.njcn.gather.event.eventlist.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.njcn.gather.event.eventlist.pojo.param.EventListQueryParam;
import com.njcn.gather.event.eventlist.pojo.po.MpEventDetailPO;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
* 暂态事件列表 Mapper。
*/
public interface EventListMapper extends BaseMapper<MpEventDetailPO> {
Page<MpEventDetailPO> selectTransientPage(Page<MpEventDetailPO> page, @Param("param") EventListQueryParam param);
List<MpEventDetailPO> selectTransientExportList(@Param("param") EventListQueryParam param, @Param("limit") Integer limit);
MpEventDetailPO selectTransientDetail(@Param("eventId") String eventId);
List<MpEventDetailPO> selectTransientDetailsByIds(@Param("eventIds") List<String> eventIds);
}

View File

@@ -0,0 +1,111 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.njcn.gather.event.eventlist.mapper.EventListMapper">
<sql id="EventDetailColumns">
event_id,
measurement_point_id,
start_time,
event_type,
advance_reason,
advance_type,
feature_amplitude,
duration,
eventass_index,
dq_time,
deal_time,
num,
file_flag,
deal_flag,
first_time,
first_type,
first_ms,
energy,
severity,
sagsource,
phase,
event_describe,
wave_path,
create_time,
transient_value,
verify_reason,
verify_reason_detail
</sql>
<sql id="TransientWhere">
<where>
<if test="param.startTimeStart != null and param.startTimeStart != ''">
AND start_time &gt;= #{param.startTimeStart}
</if>
<if test="param.startTimeEnd != null and param.startTimeEnd != ''">
AND start_time &lt;= #{param.startTimeEnd}
</if>
<if test="param.eventType != null and param.eventType != ''">
AND event_type = #{param.eventType}
</if>
<if test="param.phase != null and param.phase != ''">
AND phase = #{param.phase}
</if>
<if test="param.durationMin != null">
AND duration &gt;= #{param.durationMin}
</if>
<if test="param.durationMax != null">
AND duration &lt;= #{param.durationMax}
</if>
<if test="param.featureAmplitudeMin != null">
AND feature_amplitude &gt;= #{param.featureAmplitudeMin}
</if>
<if test="param.featureAmplitudeMax != null">
AND feature_amplitude &lt;= #{param.featureAmplitudeMax}
</if>
<if test="param.fileFlag != null">
AND file_flag = #{param.fileFlag}
</if>
<if test="param.dealFlag != null">
AND deal_flag = #{param.dealFlag}
</if>
<if test="param.lineIds != null and param.lineIds.size() &gt; 0">
AND measurement_point_id IN
<foreach collection="param.lineIds" item="lineId" open="(" separator="," close=")">
#{lineId}
</foreach>
</if>
</where>
</sql>
<select id="selectTransientPage" resultType="com.njcn.gather.event.eventlist.pojo.po.MpEventDetailPO">
SELECT
<include refid="EventDetailColumns"/>
FROM r_mp_event_detail
<include refid="TransientWhere"/>
ORDER BY start_time DESC, event_id DESC
</select>
<select id="selectTransientExportList" resultType="com.njcn.gather.event.eventlist.pojo.po.MpEventDetailPO">
SELECT
<include refid="EventDetailColumns"/>
FROM r_mp_event_detail
<include refid="TransientWhere"/>
ORDER BY start_time DESC, event_id DESC
LIMIT #{limit}
</select>
<select id="selectTransientDetail" resultType="com.njcn.gather.event.eventlist.pojo.po.MpEventDetailPO">
SELECT
<include refid="EventDetailColumns"/>
FROM r_mp_event_detail
WHERE event_id = #{eventId}
LIMIT 1
</select>
<select id="selectTransientDetailsByIds" resultType="com.njcn.gather.event.eventlist.pojo.po.MpEventDetailPO">
SELECT
<include refid="EventDetailColumns"/>
FROM r_mp_event_detail
WHERE event_id IN
<foreach collection="eventIds" item="eventId" open="(" separator="," close=")">
#{eventId}
</foreach>
</select>
</mapper>

View File

@@ -0,0 +1,65 @@
package com.njcn.gather.event.eventlist.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 java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
/**
* 暂态事件列表查询参数。
*/
@Data
@EqualsAndHashCode(callSuper = true)
@ApiModel("暂态事件列表查询参数")
public class EventListQueryParam extends BaseParam {
@ApiModelProperty("发生时刻开始,格式 yyyy-MM-dd HH:mm:ss.SSS兼容 yyyy-MM-dd HH:mm:ss")
private String startTimeStart;
@ApiModelProperty("发生时刻结束,格式 yyyy-MM-dd HH:mm:ss.SSS兼容 yyyy-MM-dd HH:mm:ss")
private String startTimeEnd;
@ApiModelProperty("事件类型")
private String eventType;
@ApiModelProperty("相别")
private String phase;
@ApiModelProperty("持续时间下限,单位秒")
private BigDecimal durationMin;
@ApiModelProperty("持续时间上限,单位秒")
private BigDecimal durationMax;
@ApiModelProperty("暂降/暂升幅值下限")
private BigDecimal featureAmplitudeMin;
@ApiModelProperty("暂降/暂升幅值上限")
private BigDecimal featureAmplitudeMax;
@ApiModelProperty("波形文件状态0 未招1 已招")
private Integer fileFlag;
@ApiModelProperty("处理状态0 未处理1 已处理2 已处理无结果3 计算失败")
private Integer dealFlag;
@ApiModelProperty("监测点 ID 列表")
private List<String> lineIds = new ArrayList<String>();
@ApiModelProperty("工程名称关键字")
private String engineeringName;
@ApiModelProperty("项目名称关键字")
private String projectName;
@ApiModelProperty("设备名称关键字")
private String equipmentName;
@ApiModelProperty("监测点名称关键字")
private String lineName;
}

View File

@@ -0,0 +1,19 @@
package com.njcn.gather.event.eventlist.pojo.param;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.util.ArrayList;
import java.util.List;
/**
* 事件波形批量导出参数。
*/
@Data
@ApiModel("事件波形批量导出参数")
public class EventWaveExportParam {
@ApiModelProperty("页面勾选的事件 ID 列表")
private List<String> eventIds = new ArrayList<String>();
}

View File

@@ -0,0 +1,91 @@
package com.njcn.gather.event.eventlist.pojo.po;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
import lombok.Data;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
* 暂态事件明细。
*/
@Data
@TableName("r_mp_event_detail")
public class MpEventDetailPO implements Serializable {
private static final long serialVersionUID = 1L;
@TableId("event_id")
private String eventId;
private String measurementPointId;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@JsonDeserialize(using = LocalDateTimeDeserializer.class)
@JsonSerialize(using = LocalDateTimeSerializer.class)
private LocalDateTime startTime;
private String eventType;
private String advanceReason;
private String advanceType;
private BigDecimal featureAmplitude;
private BigDecimal duration;
private String eventassIndex;
private Double dqTime;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@JsonDeserialize(using = LocalDateTimeDeserializer.class)
@JsonSerialize(using = LocalDateTimeSerializer.class)
private LocalDateTime dealTime;
private Integer num;
private Integer fileFlag;
private Integer dealFlag;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@JsonDeserialize(using = LocalDateTimeDeserializer.class)
@JsonSerialize(using = LocalDateTimeSerializer.class)
private LocalDateTime firstTime;
private String firstType;
private BigDecimal firstMs;
private Double energy;
private Double severity;
private String sagsource;
private String phase;
private String eventDescribe;
private String wavePath;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@JsonDeserialize(using = LocalDateTimeDeserializer.class)
@JsonSerialize(using = LocalDateTimeSerializer.class)
private LocalDateTime createTime;
private Double transientValue;
private String verifyReason;
private String verifyReasonDetail;
}

View File

@@ -0,0 +1,78 @@
package com.njcn.gather.event.eventlist.pojo.vo;
import cn.afterturn.easypoi.excel.annotation.Excel;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
import com.njcn.gather.event.eventlist.config.EventMillisecondTimeSerializer;
import lombok.Data;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
* 暂态事件列表展示对象。
*/
@Data
public class EventListVO implements Serializable {
private static final long serialVersionUID = 1L;
private String eventId;
private String measurementPointId;
@Excel(name = "发生时刻", width = 25, exportFormat = "yyyy-MM-dd HH:mm:ss.SSS")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss.SSS")
@JsonDeserialize(using = LocalDateTimeDeserializer.class)
@JsonSerialize(using = EventMillisecondTimeSerializer.class)
private LocalDateTime startTime;
@Excel(name = "工程名称", width = 25)
private String engineeringName;
@Excel(name = "项目名称", width = 25)
private String projectName;
@Excel(name = "设备名称", width = 25)
private String equipmentName;
private String mac;
@Excel(name = "监测点名称", width = 25)
private String lineName;
@Excel(name = "暂降/暂升幅值(%)", width = 20)
private BigDecimal featureAmplitude;
@Excel(name = "持续时间(s)", width = 18)
private BigDecimal duration;
@Excel(name = "事件类型", width = 18)
private String eventType;
@Excel(name = "相别", width = 15)
private String phase;
@Excel(name = "事件描述", width = 25)
@JsonProperty("event_describe")
private String eventDescribe;
@Excel(name = "事件发生位置", width = 18)
private String sagsource;
private String wavePath;
private Integer fileFlag;
private Integer dealFlag;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@JsonDeserialize(using = LocalDateTimeDeserializer.class)
@JsonSerialize(using = LocalDateTimeSerializer.class)
private LocalDateTime createTime;
}

View File

@@ -0,0 +1,23 @@
package com.njcn.gather.event.eventlist.service;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.njcn.gather.event.eventlist.pojo.param.EventListQueryParam;
import com.njcn.gather.event.eventlist.pojo.param.EventWaveExportParam;
import com.njcn.gather.event.eventlist.pojo.vo.EventListVO;
import com.njcn.gather.tool.wave.pojo.vo.WaveComtradeResultVO;
/**
* 事件列表服务。
*/
public interface EventListService {
Page<EventListVO> pageTransientEvents(EventListQueryParam param);
EventListVO getTransientEventDetail(String eventId);
void exportTransientEvents(EventListQueryParam param);
WaveComtradeResultVO getTransientEventWave(String eventId);
void exportTransientEventWaves(EventWaveExportParam param);
}

View File

@@ -0,0 +1,743 @@
package com.njcn.gather.event.eventlist.service.impl;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.njcn.common.pojo.enums.common.DataStateEnum;
import com.njcn.common.pojo.enums.response.CommonResponseEnum;
import com.njcn.common.pojo.exception.BusinessException;
import com.njcn.gather.event.eventlist.mapper.EventListMapper;
import com.njcn.gather.event.eventlist.pojo.param.EventListQueryParam;
import com.njcn.gather.event.eventlist.pojo.param.EventWaveExportParam;
import com.njcn.gather.event.eventlist.pojo.po.MpEventDetailPO;
import com.njcn.gather.event.eventlist.pojo.vo.EventListVO;
import com.njcn.gather.event.eventlist.service.EventListService;
import com.njcn.gather.system.cfg.service.ISysConfigService;
import com.njcn.gather.system.dictionary.pojo.po.DictData;
import com.njcn.gather.system.dictionary.pojo.po.DictType;
import com.njcn.gather.system.dictionary.service.IDictDataService;
import com.njcn.gather.system.dictionary.service.IDictTypeService;
import com.njcn.gather.system.pojo.constant.DictConst;
import com.njcn.gather.system.util.ExportFileNameUtil;
import com.njcn.gather.tool.addledger.pojo.param.AddLedgerLinePathQueryParam;
import com.njcn.gather.tool.addledger.pojo.vo.AddLedgerLinePathVO;
import com.njcn.gather.tool.addledger.service.AddLedgerService;
import com.njcn.gather.tool.wave.pojo.param.WaveComtradeParseParam;
import com.njcn.gather.tool.wave.pojo.vo.WaveComtradeResultVO;
import com.njcn.gather.tool.wave.service.WaveService;
import com.njcn.web.factory.PageFactory;
import com.njcn.web.utils.ExcelUtil;
import com.njcn.web.utils.HttpServletUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.CellStyle;
import org.apache.poi.ss.usermodel.Font;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.xssf.usermodel.XSSFSheet;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.springframework.stereotype.Service;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.math.BigDecimal;
import java.net.URLEncoder;
import java.nio.file.Files;
import java.nio.file.Path;
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.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
/**
* 事件列表服务实现。
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class EventListServiceImpl implements EventListService {
private static final int LEDGER_LINE_QUERY_LIMIT = 1000;
private static final int EVENT_LINE_ID_QUERY_LIMIT = 1000;
private static final int EXPORT_LIMIT = 5000;
private static final int WAVE_EXPORT_LIMIT = 500;
private static final String EMPTY_TEXT = "-";
private static final String WAVE_EXPORT_SUCCESS = "成功";
private static final String WAVE_EXPORT_FAIL = "失败";
private static final BigDecimal PERCENT_BASE = new BigDecimal("100");
private static final DateTimeFormatter OUTPUT_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS");
private static final DateTimeFormatter[] INPUT_FORMATTERS = new DateTimeFormatter[]{
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"),
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS"),
DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss"),
DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSS")
};
private final EventListMapper eventListMapper;
private final AddLedgerService addLedgerService;
private final ISysConfigService sysConfigService;
private final IDictTypeService dictTypeService;
private final IDictDataService dictDataService;
private final WaveService waveService;
@Override
public Page<EventListVO> pageTransientEvents(EventListQueryParam param) {
EventListQueryParam queryParam = normalizeQueryParam(param);
if (!resolveLineFilter(queryParam)) {
return emptyPage(queryParam);
}
Page<MpEventDetailPO> eventPage = eventListMapper.selectTransientPage(
new Page<MpEventDetailPO>(PageFactory.getPageNum(queryParam), PageFactory.getPageSize(queryParam)),
queryParam);
List<EventListVO> records = buildEventList(eventPage.getRecords());
Page<EventListVO> resultPage = new Page<EventListVO>(eventPage.getCurrent(), eventPage.getSize(), eventPage.getTotal());
resultPage.setRecords(records);
return resultPage;
}
@Override
public EventListVO getTransientEventDetail(String eventId) {
String normalizedEventId = trimToNull(eventId);
if (normalizedEventId == null) {
throw new BusinessException(CommonResponseEnum.FAIL, "事件 ID 不能为空");
}
MpEventDetailPO eventDetail = eventListMapper.selectTransientDetail(normalizedEventId);
if (eventDetail == null) {
throw new BusinessException(CommonResponseEnum.FAIL, "暂态事件不存在");
}
List<MpEventDetailPO> eventDetails = new ArrayList<MpEventDetailPO>();
eventDetails.add(eventDetail);
return buildEventList(eventDetails).get(0);
}
@Override
public void exportTransientEvents(EventListQueryParam param) {
EventListQueryParam queryParam = normalizeQueryParam(param);
List<EventListVO> exportRecords;
if (resolveLineFilter(queryParam)) {
List<MpEventDetailPO> eventDetails = eventListMapper.selectTransientExportList(queryParam, EXPORT_LIMIT + 1);
if (eventDetails.size() > EXPORT_LIMIT) {
throw new BusinessException(CommonResponseEnum.FAIL, "导出数据超过 5000 条,请缩小查询条件");
}
exportRecords = buildEventList(eventDetails);
fillExportEventTypeNames(exportRecords);
} else {
exportRecords = Collections.emptyList();
}
ExcelUtil.exportExcel(ExportFileNameUtil.appendToday("暂态事件列表.xlsx"), "暂态事件列表", EventListVO.class, exportRecords);
}
@Override
public WaveComtradeResultVO getTransientEventWave(String eventId) {
MpEventDetailPO eventDetail = requireEventDetail(eventId);
AddLedgerLinePathVO linePath = requireLinePath(eventDetail);
EventWavePathResolver.WaveFilePath waveFilePath = resolveWaveFilePath(eventDetail, linePath);
if (!Files.exists(waveFilePath.getCfgPath())) {
throw new BusinessException(CommonResponseEnum.FAIL, "cfg 波形文件不存在");
}
if (!Files.exists(waveFilePath.getDatPath())) {
throw new BusinessException(CommonResponseEnum.FAIL, "dat 波形文件不存在");
}
WaveComtradeParseParam parseParam = new WaveComtradeParseParam();
parseParam.setMonitorName(defaultText(linePath.getLineName()));
try (InputStream cfgInputStream = new FileInputStream(waveFilePath.getCfgPath().toFile());
InputStream datInputStream = new FileInputStream(waveFilePath.getDatPath().toFile())) {
return waveService.parseComtrade(cfgInputStream, datInputStream, parseParam);
} catch (IOException ex) {
log.error("读取暂态事件波形文件失败eventId={}", eventDetail.getEventId(), ex);
throw new BusinessException(CommonResponseEnum.FAIL, "读取波形文件失败");
}
}
@Override
public void exportTransientEventWaves(EventWaveExportParam param) {
List<String> eventIds = normalizeExportEventIds(param);
List<WaveExportItem> exportItems = buildWaveExportItems(eventIds);
writeWaveExportZip(exportItems);
}
private MpEventDetailPO requireEventDetail(String eventId) {
String normalizedEventId = trimToNull(eventId);
if (normalizedEventId == null) {
throw new BusinessException(CommonResponseEnum.FAIL, "事件 ID 不能为空");
}
MpEventDetailPO eventDetail = eventListMapper.selectTransientDetail(normalizedEventId);
if (eventDetail == null) {
throw new BusinessException(CommonResponseEnum.FAIL, "暂态事件不存在");
}
return eventDetail;
}
private AddLedgerLinePathVO requireLinePath(MpEventDetailPO eventDetail) {
List<String> lineIds = new ArrayList<String>();
lineIds.add(eventDetail.getMeasurementPointId());
Map<String, AddLedgerLinePathVO> linePathMap = addLedgerService.listLinePathByLineIds(lineIds);
AddLedgerLinePathVO linePath = linePathMap.get(eventDetail.getMeasurementPointId());
if (linePath == null) {
throw new BusinessException(CommonResponseEnum.FAIL, "监测点台账不存在或已删除");
}
return linePath;
}
private List<String> normalizeExportEventIds(EventWaveExportParam param) {
List<String> eventIds = normalizeIds(param == null ? null : param.getEventIds());
if (eventIds.isEmpty()) {
throw new BusinessException(CommonResponseEnum.FAIL, "请选择需要导出的事件");
}
if (eventIds.size() > WAVE_EXPORT_LIMIT) {
throw new BusinessException(CommonResponseEnum.FAIL, "波形导出事件数量不能超过 500 条");
}
return eventIds;
}
private List<WaveExportItem> buildWaveExportItems(List<String> eventIds) {
List<MpEventDetailPO> eventDetails = eventListMapper.selectTransientDetailsByIds(eventIds);
Map<String, MpEventDetailPO> eventMap = new LinkedHashMap<String, MpEventDetailPO>();
List<String> lineIds = new ArrayList<String>();
for (MpEventDetailPO eventDetail : eventDetails) {
eventMap.put(eventDetail.getEventId(), eventDetail);
String lineId = trimToNull(eventDetail.getMeasurementPointId());
if (lineId != null && !lineIds.contains(lineId)) {
lineIds.add(lineId);
}
}
Map<String, AddLedgerLinePathVO> linePathMap = addLedgerService.listLinePathByLineIds(lineIds);
List<WaveExportItem> exportItems = new ArrayList<WaveExportItem>();
for (String eventId : eventIds) {
WaveExportItem item = new WaveExportItem();
item.setEventId(eventId);
MpEventDetailPO eventDetail = eventMap.get(eventId);
item.setEventDetail(eventDetail);
if (eventDetail == null) {
item.fail("事件不存在");
exportItems.add(item);
continue;
}
AddLedgerLinePathVO linePath = linePathMap.get(eventDetail.getMeasurementPointId());
item.setLinePath(linePath);
item.setEventVO(buildEventVO(eventDetail, linePath));
fillWaveExportFileState(item);
exportItems.add(item);
}
return exportItems;
}
private void fillWaveExportFileState(WaveExportItem item) {
MpEventDetailPO eventDetail = item.getEventDetail();
if (item.getLinePath() == null) {
item.fail("监测点台账不存在或已删除");
return;
}
if (eventDetail.getFileFlag() == null || eventDetail.getFileFlag() != 1) {
item.fail("波形文件未招");
return;
}
try {
EventWavePathResolver.WaveFilePath waveFilePath = resolveWaveFilePath(eventDetail, item.getLinePath());
item.setWaveFilePath(waveFilePath);
boolean cfgExists = Files.exists(waveFilePath.getCfgPath());
boolean datExists = Files.exists(waveFilePath.getDatPath());
if (cfgExists && datExists) {
item.success();
return;
}
if (!cfgExists && !datExists) {
item.fail("cfg/dat 文件不存在");
} else if (!cfgExists) {
item.fail("cfg 文件不存在");
} else {
item.fail("dat 文件不存在");
}
} catch (BusinessException ex) {
item.fail(ex.getMessage());
}
}
private EventWavePathResolver.WaveFilePath resolveWaveFilePath(MpEventDetailPO eventDetail, AddLedgerLinePathVO linePath) {
try {
return EventWavePathResolver.resolve(sysConfigService.getWaveStoragePath(), linePath.getEquipmentMac(), eventDetail.getWavePath());
} catch (IllegalArgumentException ex) {
throw new BusinessException(CommonResponseEnum.FAIL, ex.getMessage());
}
}
private void writeWaveExportZip(List<WaveExportItem> exportItems) {
HttpServletResponse response = HttpServletUtil.getResponse();
try {
String fileName = URLEncoder.encode(ExportFileNameUtil.appendToday("选中事件波形导出.zip"), "UTF-8");
response.reset();
response.setHeader("Content-Disposition", "attachment; filename=\"" + fileName + "\"");
response.setContentType("application/zip;charset=UTF-8");
try (ServletOutputStream outputStream = response.getOutputStream();
ZipOutputStream zipOutputStream = new ZipOutputStream(outputStream)) {
writeZipEntry(zipOutputStream, ExportFileNameUtil.appendToday("选中事件日志.xlsx"), buildWaveExportWorkbook(exportItems));
for (int i = 0; i < exportItems.size(); i++) {
WaveExportItem item = exportItems.get(i);
EventWavePathResolver.WaveFilePath waveFilePath = item.getWaveFilePath();
if (waveFilePath == null) {
continue;
}
String folder = buildZipFolderName(i + 1, item.getEventId());
writeWaveFileIfExists(zipOutputStream, folder + "/" + waveFilePath.getCfgPath().getFileName(), waveFilePath.getCfgPath());
writeWaveFileIfExists(zipOutputStream, folder + "/" + waveFilePath.getDatPath().getFileName(), waveFilePath.getDatPath());
}
zipOutputStream.flush();
}
} catch (IOException ex) {
log.error("导出暂态事件波形失败", ex);
throw new BusinessException(CommonResponseEnum.FAIL, "导出波形文件失败");
}
}
private byte[] buildWaveExportWorkbook(List<WaveExportItem> exportItems) throws IOException {
XSSFWorkbook workbook = new XSSFWorkbook();
try {
writeEventLogSheet(workbook, exportItems);
writeWaveFileSheet(workbook, exportItems);
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
workbook.write(outputStream);
return outputStream.toByteArray();
} finally {
workbook.close();
}
}
private void writeEventLogSheet(XSSFWorkbook workbook, List<WaveExportItem> exportItems) {
XSSFSheet sheet = workbook.createSheet("选中事件列表");
String[] headers = new String[]{"发生时刻", "工程名称", "项目名称", "设备名称", "监测点名称", "暂降/暂升幅值(%)",
"持续时间(s)", "事件类型", "相别", "事件描述", "事件发生位置"};
int[] columnWidths = new int[]{25, 25, 25, 25, 25, 20, 18, 18, 15, 25, 18};
writeHeader(sheet, headers, createHeaderStyle(workbook));
Map<String, String> eventTypeNameMap = buildEventTypeNameMap();
int rowIndex = 1;
for (WaveExportItem item : exportItems) {
EventListVO vo = item.getEventVO();
if (vo == null) {
continue;
}
Row row = sheet.createRow(rowIndex++);
writeCells(row, new Object[]{formatDateTime(vo.getStartTime()), vo.getEngineeringName(), vo.getProjectName(),
vo.getEquipmentName(), vo.getLineName(), vo.getFeatureAmplitude(), vo.getDuration(),
translateEventType(vo.getEventType(), eventTypeNameMap), vo.getPhase(), vo.getEventDescribe(),
vo.getSagsource()});
}
applyColumnWidth(sheet, columnWidths);
}
private void writeWaveFileSheet(XSSFWorkbook workbook, List<WaveExportItem> exportItems) {
XSSFSheet sheet = workbook.createSheet("波形文件清单");
String[] headers = new String[]{"事件ID", "设备MAC", "wavePath", "cfg文件路径", "dat文件路径", "导出状态", "失败原因"};
writeHeader(sheet, headers, createHeaderStyle(workbook));
int rowIndex = 1;
for (WaveExportItem item : exportItems) {
EventWavePathResolver.WaveFilePath waveFilePath = item.getWaveFilePath();
Row row = sheet.createRow(rowIndex++);
writeCells(row, new Object[]{item.getEventId(),
item.getLinePath() == null ? EMPTY_TEXT : item.getLinePath().getEquipmentMac(),
item.getEventDetail() == null ? EMPTY_TEXT : item.getEventDetail().getWavePath(),
waveFilePath == null ? EMPTY_TEXT : waveFilePath.getCfgPath().toString(),
waveFilePath == null ? EMPTY_TEXT : waveFilePath.getDatPath().toString(),
item.getStatus(), defaultText(item.getFailReason())});
}
applyColumnWidth(sheet, headers.length);
}
private CellStyle createHeaderStyle(XSSFWorkbook workbook) {
CellStyle style = workbook.createCellStyle();
Font font = workbook.createFont();
font.setBold(true);
style.setFont(font);
return style;
}
private void writeHeader(XSSFSheet sheet, String[] headers, CellStyle style) {
Row row = sheet.createRow(0);
for (int i = 0; i < headers.length; i++) {
Cell cell = row.createCell(i);
cell.setCellValue(headers[i]);
cell.setCellStyle(style);
}
}
private void writeCells(Row row, Object[] values) {
for (int i = 0; i < values.length; i++) {
Cell cell = row.createCell(i);
Object value = values[i];
cell.setCellValue(value == null ? "" : String.valueOf(value));
}
}
private void applyColumnWidth(XSSFSheet sheet, int columnCount) {
for (int i = 0; i < columnCount; i++) {
sheet.setColumnWidth(i, 20 * 256);
}
}
private void applyColumnWidth(XSSFSheet sheet, int[] columnWidths) {
for (int i = 0; i < columnWidths.length; i++) {
sheet.setColumnWidth(i, columnWidths[i] * 256);
}
}
private void writeZipEntry(ZipOutputStream zipOutputStream, String entryName, byte[] content) throws IOException {
ZipEntry zipEntry = new ZipEntry(entryName);
zipOutputStream.putNextEntry(zipEntry);
zipOutputStream.write(content);
zipOutputStream.closeEntry();
}
private void writeWaveFileIfExists(ZipOutputStream zipOutputStream, String entryName, Path filePath) throws IOException {
if (!Files.exists(filePath)) {
return;
}
zipOutputStream.putNextEntry(new ZipEntry(entryName));
try (InputStream inputStream = new FileInputStream(filePath.toFile())) {
byte[] buffer = new byte[8192];
int length;
while ((length = inputStream.read(buffer)) != -1) {
zipOutputStream.write(buffer, 0, length);
}
}
zipOutputStream.closeEntry();
}
private String buildZipFolderName(int index, String eventId) {
return index + "_" + sanitizeZipSegment(eventId);
}
private String sanitizeZipSegment(String value) {
String text = trimToNull(value);
return text == null ? "unknown" : text.replaceAll("[\\\\/:*?\"<>|]", "_");
}
private String formatDateTime(LocalDateTime value) {
return value == null ? EMPTY_TEXT : OUTPUT_FORMATTER.format(value);
}
private List<EventListVO> buildEventList(List<MpEventDetailPO> eventDetails) {
if (eventDetails == null || eventDetails.isEmpty()) {
return Collections.emptyList();
}
List<String> lineIds = new ArrayList<String>();
for (MpEventDetailPO eventDetail : eventDetails) {
String lineId = trimToNull(eventDetail.getMeasurementPointId());
if (lineId != null && !lineIds.contains(lineId)) {
lineIds.add(lineId);
}
}
Map<String, AddLedgerLinePathVO> linePathMap = addLedgerService.listLinePathByLineIds(lineIds);
List<EventListVO> result = new ArrayList<EventListVO>();
for (MpEventDetailPO eventDetail : eventDetails) {
AddLedgerLinePathVO linePath = linePathMap.get(eventDetail.getMeasurementPointId());
result.add(buildEventVO(eventDetail, linePath));
}
return result;
}
private EventListVO buildEventVO(MpEventDetailPO eventDetail, AddLedgerLinePathVO linePath) {
EventListVO vo = new EventListVO();
vo.setEventId(eventDetail.getEventId());
vo.setMeasurementPointId(eventDetail.getMeasurementPointId());
vo.setEventType(eventDetail.getEventType());
vo.setEquipmentName(linePath == null ? EMPTY_TEXT : defaultText(linePath.getEquipmentName()));
vo.setMac(linePath == null ? EMPTY_TEXT : defaultText(linePath.getEquipmentMac()));
vo.setEngineeringName(linePath == null ? EMPTY_TEXT : defaultText(linePath.getEngineeringName()));
vo.setProjectName(linePath == null ? EMPTY_TEXT : defaultText(linePath.getProjectName()));
vo.setStartTime(eventDetail.getStartTime());
vo.setLineName(linePath == null ? EMPTY_TEXT : defaultText(linePath.getLineName()));
vo.setEventDescribe(trimToNull(eventDetail.getEventDescribe()));
vo.setSagsource(defaultText(eventDetail.getSagsource()));
vo.setPhase(defaultText(eventDetail.getPhase()));
vo.setDuration(eventDetail.getDuration());
vo.setFeatureAmplitude(toPercent(eventDetail.getFeatureAmplitude()));
vo.setWavePath(eventDetail.getWavePath());
vo.setFileFlag(eventDetail.getFileFlag());
vo.setDealFlag(eventDetail.getDealFlag());
vo.setCreateTime(eventDetail.getCreateTime());
return vo;
}
private EventListQueryParam normalizeQueryParam(EventListQueryParam param) {
EventListQueryParam queryParam = param == null ? new EventListQueryParam() : param;
LocalDateTime startTime = parseDateTime(queryParam.getStartTimeStart());
LocalDateTime endTime = parseDateTime(queryParam.getStartTimeEnd());
if (startTime == null) {
LocalDateTime now = LocalDateTime.now();
startTime = LocalDateTime.of(now.getYear(), now.getMonth(), 1, 0, 0, 0);
}
if (endTime == null) {
endTime = LocalDateTime.now();
}
if (startTime.isAfter(endTime)) {
throw new BusinessException(CommonResponseEnum.FAIL, "开始时间不能大于结束时间");
}
validateRange(queryParam.getDurationMin(), queryParam.getDurationMax(), "持续时间下限不能大于上限");
validateRange(queryParam.getFeatureAmplitudeMin(), queryParam.getFeatureAmplitudeMax(), "幅值下限不能大于上限");
validateFlag(queryParam.getFileFlag(), "波形文件状态只能是 0 或 1");
validateDealFlag(queryParam.getDealFlag());
queryParam.setFeatureAmplitudeMin(fromPercent(queryParam.getFeatureAmplitudeMin()));
queryParam.setFeatureAmplitudeMax(fromPercent(queryParam.getFeatureAmplitudeMax()));
queryParam.setStartTimeStart(OUTPUT_FORMATTER.format(startTime));
queryParam.setStartTimeEnd(OUTPUT_FORMATTER.format(endTime));
queryParam.setEventType(trimToNull(queryParam.getEventType()));
queryParam.setPhase(trimToNull(queryParam.getPhase()));
queryParam.setEngineeringName(trimToNull(queryParam.getEngineeringName()));
queryParam.setProjectName(trimToNull(queryParam.getProjectName()));
queryParam.setEquipmentName(trimToNull(queryParam.getEquipmentName()));
queryParam.setLineName(trimToNull(queryParam.getLineName()));
List<String> lineIds = normalizeIds(queryParam.getLineIds());
if (lineIds.size() > EVENT_LINE_ID_QUERY_LIMIT) {
throw new BusinessException(CommonResponseEnum.FAIL, "监测点 ID 查询数量不能超过 1000 个");
}
queryParam.setLineIds(lineIds);
return queryParam;
}
/**
* 台账关键字先转成测点 ID避免在事件模块直接联查台账表。
*/
private boolean resolveLineFilter(EventListQueryParam queryParam) {
if (!hasLedgerKeyword(queryParam)) {
return true;
}
AddLedgerLinePathQueryParam linePathQueryParam = new AddLedgerLinePathQueryParam();
linePathQueryParam.setEngineeringName(queryParam.getEngineeringName());
linePathQueryParam.setProjectName(queryParam.getProjectName());
linePathQueryParam.setEquipmentName(queryParam.getEquipmentName());
linePathQueryParam.setLineName(queryParam.getLineName());
linePathQueryParam.setLimit(LEDGER_LINE_QUERY_LIMIT + 1);
List<String> ledgerLineIds = addLedgerService.listLineIdsByPathQuery(linePathQueryParam);
if (ledgerLineIds.size() > LEDGER_LINE_QUERY_LIMIT) {
throw new BusinessException(CommonResponseEnum.FAIL, "台账检索匹配监测点过多,请缩小查询条件");
}
if (ledgerLineIds.isEmpty()) {
return false;
}
List<String> explicitLineIds = normalizeIds(queryParam.getLineIds());
if (explicitLineIds.isEmpty()) {
queryParam.setLineIds(ledgerLineIds);
return true;
}
List<String> intersectLineIds = new ArrayList<String>();
for (String lineId : explicitLineIds) {
if (ledgerLineIds.contains(lineId)) {
intersectLineIds.add(lineId);
}
}
queryParam.setLineIds(intersectLineIds);
return !intersectLineIds.isEmpty();
}
private Page<EventListVO> emptyPage(EventListQueryParam queryParam) {
Page<EventListVO> page = new Page<EventListVO>(PageFactory.getPageNum(queryParam), PageFactory.getPageSize(queryParam), 0);
page.setRecords(Collections.<EventListVO>emptyList());
return page;
}
private LocalDateTime parseDateTime(String value) {
String text = trimToNull(value);
if (text == null) {
return null;
}
for (DateTimeFormatter formatter : INPUT_FORMATTERS) {
try {
return LocalDateTime.parse(text, formatter);
} catch (DateTimeParseException ignored) {
// 尝试下一种前端可能传入的时间格式。
}
}
throw new BusinessException(CommonResponseEnum.FAIL, "时间格式不正确,仅支持 yyyy-MM-dd HH:mm:ss 或 yyyy-MM-dd HH:mm:ss.SSS");
}
private List<String> normalizeIds(List<String> ids) {
if (ids == null || ids.isEmpty()) {
return Collections.emptyList();
}
List<String> normalizedIds = new ArrayList<String>();
for (String id : ids) {
String normalizedId = trimToNull(id);
if (normalizedId != null && !normalizedIds.contains(normalizedId)) {
normalizedIds.add(normalizedId);
}
}
return normalizedIds;
}
private boolean hasLedgerKeyword(EventListQueryParam queryParam) {
return trimToNull(queryParam.getEngineeringName()) != null
|| trimToNull(queryParam.getProjectName()) != null
|| trimToNull(queryParam.getEquipmentName()) != null
|| trimToNull(queryParam.getLineName()) != null;
}
private void validateRange(BigDecimal min, BigDecimal max, String message) {
if (min != null && max != null && min.compareTo(max) > 0) {
throw new BusinessException(CommonResponseEnum.FAIL, message);
}
}
private BigDecimal toPercent(BigDecimal value) {
return value == null ? null : value.multiply(PERCENT_BASE);
}
private BigDecimal fromPercent(BigDecimal value) {
return value == null ? null : value.divide(PERCENT_BASE);
}
private void fillExportEventTypeNames(List<EventListVO> exportRecords) {
Map<String, String> eventTypeNameMap = buildEventTypeNameMap();
for (EventListVO record : exportRecords) {
record.setEventType(translateEventType(record.getEventType(), eventTypeNameMap));
}
}
private Map<String, String> buildEventTypeNameMap() {
DictType dictType = dictTypeService.getByCode(DictConst.EVENT_TYPE_DICT);
if (dictType == null) {
dictType = dictTypeService.lambdaQuery()
.eq(DictType::getName, DictConst.EVENT_TYPE_DICT)
.eq(DictType::getState, DataStateEnum.ENABLE.getCode())
.orderByAsc(DictType::getSort)
.orderByDesc(DictType::getId)
.last("LIMIT 1")
.one();
}
if (dictType == null) {
return Collections.emptyMap();
}
List<DictData> dictDataList = dictDataService.listDictDataByTypeId(dictType.getId());
Map<String, String> eventTypeNameMap = new LinkedHashMap<String, String>();
for (DictData dictData : dictDataList) {
String id = trimToNull(dictData.getId());
String code = trimToNull(dictData.getCode());
if (id != null) {
eventTypeNameMap.put(id, defaultText(dictData.getName(), id));
}
if (code != null) {
eventTypeNameMap.put(code, defaultText(dictData.getName(), code));
}
}
return eventTypeNameMap;
}
private String translateEventType(String eventType, Map<String, String> eventTypeNameMap) {
String normalizedEventType = trimToNull(eventType);
if (normalizedEventType == null) {
return EMPTY_TEXT;
}
String eventTypeName = eventTypeNameMap.get(normalizedEventType);
return eventTypeName == null ? normalizedEventType : eventTypeName;
}
private void validateFlag(Integer flag, String message) {
if (flag != null && flag != 0 && flag != 1) {
throw new BusinessException(CommonResponseEnum.FAIL, message);
}
}
private void validateDealFlag(Integer dealFlag) {
if (dealFlag != null && (dealFlag < 0 || dealFlag > 3)) {
throw new BusinessException(CommonResponseEnum.FAIL, "处理状态只能是 0、1、2、3");
}
}
private String defaultText(String value) {
return defaultText(value, EMPTY_TEXT);
}
private String defaultText(String value, String defaultValue) {
String text = trimToNull(value);
return text == null ? defaultValue : text;
}
private String trimToNull(String value) {
if (value == null) {
return null;
}
String trimmed = value.trim();
return trimmed.isEmpty() ? null : trimmed;
}
private static class WaveExportItem {
private String eventId;
private MpEventDetailPO eventDetail;
private AddLedgerLinePathVO linePath;
private EventListVO eventVO;
private EventWavePathResolver.WaveFilePath waveFilePath;
private String status = WAVE_EXPORT_FAIL;
private String failReason;
private String getEventId() {
return eventId;
}
private void setEventId(String eventId) {
this.eventId = eventId;
}
private MpEventDetailPO getEventDetail() {
return eventDetail;
}
private void setEventDetail(MpEventDetailPO eventDetail) {
this.eventDetail = eventDetail;
}
private AddLedgerLinePathVO getLinePath() {
return linePath;
}
private void setLinePath(AddLedgerLinePathVO linePath) {
this.linePath = linePath;
}
private EventListVO getEventVO() {
return eventVO;
}
private void setEventVO(EventListVO eventVO) {
this.eventVO = eventVO;
}
private EventWavePathResolver.WaveFilePath getWaveFilePath() {
return waveFilePath;
}
private void setWaveFilePath(EventWavePathResolver.WaveFilePath waveFilePath) {
this.waveFilePath = waveFilePath;
}
private String getStatus() {
return status;
}
private String getFailReason() {
return failReason;
}
private void success() {
this.status = WAVE_EXPORT_SUCCESS;
this.failReason = null;
}
private void fail(String failReason) {
this.status = WAVE_EXPORT_FAIL;
this.failReason = failReason;
}
}
}

View File

@@ -0,0 +1,81 @@
package com.njcn.gather.event.eventlist.service.impl;
import java.nio.file.Path;
import java.nio.file.Paths;
/**
* 暂态事件波形文件路径解析器。
*/
final class EventWavePathResolver {
private EventWavePathResolver() {
}
static WaveFilePath resolve(String storageRoot, String equipmentMac, String wavePath) {
String rootText = requireText(storageRoot, "波形存储路径未配置");
String mac = requireText(equipmentMac, "设备 MAC 为空");
String normalizedWavePath = normalizeWavePath(wavePath);
Path rootPath = Paths.get(rootText).normalize();
Path rawWavePath = Paths.get(normalizedWavePath).normalize();
Path eventBasePath;
if (rawWavePath.isAbsolute()) {
eventBasePath = rawWavePath;
} else if (startsWithSegment(rawWavePath, mac)) {
eventBasePath = rootPath.resolve(rawWavePath).normalize();
} else {
eventBasePath = rootPath.resolve(mac).resolve(rawWavePath).normalize();
}
if (!eventBasePath.startsWith(rootPath)) {
throw new IllegalArgumentException("波形路径非法");
}
String fileName = eventBasePath.getFileName().toString();
return new WaveFilePath(eventBasePath.resolveSibling(fileName + ".cfg"),
eventBasePath.resolveSibling(fileName + ".dat"));
}
private static String normalizeWavePath(String value) {
String wavePath = requireText(value, "事件 wave_path 为空").replace("\\", "/");
String lowerWavePath = wavePath.toLowerCase();
if (lowerWavePath.endsWith(".cfg") || lowerWavePath.endsWith(".dat")) {
wavePath = wavePath.substring(0, wavePath.length() - 4);
}
if (wavePath.contains("..")) {
throw new IllegalArgumentException("波形路径非法");
}
return wavePath;
}
private static boolean startsWithSegment(Path path, String segment) {
return path.getNameCount() > 0 && path.getName(0).toString().equalsIgnoreCase(segment);
}
private static String requireText(String value, String message) {
if (value == null || value.trim().isEmpty()) {
throw new IllegalArgumentException(message);
}
return value.trim();
}
static class WaveFilePath {
private final Path cfgPath;
private final Path datPath;
private WaveFilePath(Path cfgPath, Path datPath) {
this.cfgPath = cfgPath;
this.datPath = datPath;
}
Path getCfgPath() {
return cfgPath;
}
Path getDatPath() {
return datPath;
}
}
}

View File

@@ -0,0 +1,14 @@
-- 暂态事件列表查询建议索引。
-- 本脚本不自动执行,请按数据库现状审阅后单独执行。
CREATE INDEX idx_event_start_id
ON r_mp_event_detail (start_time, event_id);
CREATE INDEX idx_event_mp_time_id
ON r_mp_event_detail (measurement_point_id, start_time, event_id);
CREATE INDEX idx_event_type_time_id
ON r_mp_event_detail (event_type, start_time, event_id);
CREATE INDEX idx_event_phase_time_id
ON r_mp_event_detail (phase, start_time, event_id);

View File

@@ -0,0 +1,44 @@
package com.njcn.gather.event.eventlist.service.impl;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.njcn.gather.event.eventlist.pojo.param.EventListQueryParam;
import com.njcn.gather.event.eventlist.pojo.vo.EventListVO;
import org.junit.Test;
import java.lang.reflect.Method;
import java.time.LocalDateTime;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
public class EventListTimeFormatTest {
@Test
public void startTimeJsonKeepsMilliseconds() throws Exception {
EventListVO vo = new EventListVO();
vo.setStartTime(LocalDateTime.of(2026, 5, 10, 13, 41, 17, 944000000));
String json = new ObjectMapper().writeValueAsString(vo);
assertTrue(json.contains("\"startTime\":\"2026-05-10 13:41:17.944\""));
}
@Test
public void queryTimeNormalizationKeepsMilliseconds() throws Exception {
EventListQueryParam param = new EventListQueryParam();
param.setStartTimeStart("2026-05-10 13:41:17.944");
param.setStartTimeEnd("2026-05-10 13:41:18.123");
normalizeQueryParam(param);
assertEquals("2026-05-10 13:41:17.944", param.getStartTimeStart());
assertEquals("2026-05-10 13:41:18.123", param.getStartTimeEnd());
}
private void normalizeQueryParam(EventListQueryParam param) throws Exception {
EventListServiceImpl service = new EventListServiceImpl(null, null, null, null, null, null);
Method method = EventListServiceImpl.class.getDeclaredMethod("normalizeQueryParam", EventListQueryParam.class);
method.setAccessible(true);
method.invoke(service, param);
}
}

View File

@@ -0,0 +1,40 @@
package com.njcn.gather.event.eventlist.service.impl;
import java.nio.file.Path;
import java.nio.file.Paths;
public class EventWavePathResolverTest {
public static void main(String[] args) {
resolvesAbsoluteWavePath();
resolvesRelativeWavePathWithMacPrefix();
resolvesRelativeWavePathAlreadyContainingMac();
}
private static void resolvesAbsoluteWavePath() {
EventWavePathResolver.WaveFilePath result = EventWavePathResolver.resolve("D:/", "AA-BB", "D:/wave/event-001.cfg");
assertPathEquals(Paths.get("D:/wave/event-001.cfg"), result.getCfgPath(), "absolute cfg");
assertPathEquals(Paths.get("D:/wave/event-001.dat"), result.getDatPath(), "absolute dat");
}
private static void resolvesRelativeWavePathWithMacPrefix() {
EventWavePathResolver.WaveFilePath result = EventWavePathResolver.resolve("D:/wave-root", "AA-BB", "event-001");
assertPathEquals(Paths.get("D:/wave-root/AA-BB/event-001.cfg"), result.getCfgPath(), "relative cfg");
assertPathEquals(Paths.get("D:/wave-root/AA-BB/event-001.dat"), result.getDatPath(), "relative dat");
}
private static void resolvesRelativeWavePathAlreadyContainingMac() {
EventWavePathResolver.WaveFilePath result = EventWavePathResolver.resolve("D:/wave-root", "AA-BB", "AA-BB/event-001.dat");
assertPathEquals(Paths.get("D:/wave-root/AA-BB/event-001.cfg"), result.getCfgPath(), "relative mac cfg");
assertPathEquals(Paths.get("D:/wave-root/AA-BB/event-001.dat"), result.getDatPath(), "relative mac dat");
}
private static void assertPathEquals(Path expected, Path actual, String label) {
if (!expected.normalize().equals(actual.normalize())) {
throw new AssertionError(label + " expected " + expected + " but got " + actual);
}
}
}

25
event/pom.xml Normal file
View File

@@ -0,0 +1,25 @@
<?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>CN_Tool</artifactId>
<version>1.0.0</version>
</parent>
<artifactId>event</artifactId>
<packaging>pom</packaging>
<modules>
<module>event-list</module>
</modules>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
</project>

View File

@@ -13,9 +13,12 @@
<module>entrance</module>
<module>system</module>
<module>systemmonitor</module>
<module>system-ops</module>
<module>user</module>
<module>detection</module>
<module>tools</module>
<module>event</module>
<module>steady</module>
</modules>
<distributionManagement>

24
steady/pom.xml Normal file
View File

@@ -0,0 +1,24 @@
<?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>CN_Tool</artifactId>
<version>1.0.0</version>
</parent>
<artifactId>steady</artifactId>
<packaging>pom</packaging>
<modules>
<module>steady-DataView</module>
</modules>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
</project>

View File

@@ -0,0 +1,239 @@
# steady-DataView API 调试文档
## 1. 基础信息
- 模块:`steady/steady-DataView`
- 控制器:`SteadyDataViewController`
- 接口前缀:`/steady/data-view`
- 本地默认地址:`http://localhost:18192`
- Content-Type`application/json`
- 认证:除登录和 Swagger 资源外,请求需要携带登录后的 `Authorization` 头。
## 2. 分页查询稳态数据
- 路径:`POST /steady/data-view/page`
- 返回:`HttpResult<Page<SteadyDataViewVO>>`
- 默认表:`data_v`
- 默认时间范围:当前月 1 日 `00:00:00` 到当前时间
- 默认排序:`TIMEID DESC, LINEID ASC, PHASIC_TYPE ASC`
请求示例:
```json
{
"pageNum": 1,
"pageSize": 10,
"tableName": "data_v",
"timeStart": "2026-05-01 00:00:00",
"timeEnd": "2026-05-09 23:59:59",
"phasicType": "A",
"qualityFlag": 1,
"lineIds": [
"line-001"
],
"engineeringName": "示例工程",
"projectName": "示例项目",
"equipmentName": "示例设备",
"lineName": "示例测点"
}
```
`tableName` 只允许 `tools/add-data` 已注册的 13 张 `data_*` 表;台账关键字会先通过 `add-ledger` 转换为监测点 ID再查询稳态数据表。
## 3. 查询稳态数据详情
- 路径:`POST /steady/data-view/detail`
- 返回:`HttpResult<SteadyDataViewVO>`
请求示例:
```json
{
"tableName": "data_v",
"lineId": "line-001",
"timeId": "2026-05-09 10:20:30",
"phasicType": "A"
}
```
详情使用 `LINEID + TIMEID + PHASIC_TYPE` 定位单条数据。
## 4. 查询稳态数据模板
- 路径:`GET /steady/data-view/templates`
- 返回:`HttpResult<List<SteadyDataViewTemplateVO>>`
模板来自 `tools/add-data` 的前端展示模板,返回参数名称、表名、相别和当前表可展示值字段。
## 5. 查询趋势台账树
- 路径:`GET /steady/data-view/ledger-tree`
- 返回:`HttpResult<List<SteadyDataViewLedgerNodeVO>>`
- 查询参数:`keyword`,可选,按台账节点名称搜索并保留父级路径。
节点字段:
| 字段 | 说明 |
| --- | --- |
| `id` | 台账节点 ID |
| `parentId` | 父节点 ID |
| `name` | 节点名称 |
| `level` | 层级0 工程1 项目2 设备3 监测点 |
| `deviceCount` | 当前节点下有效设备数 |
| `lineCount` | 当前节点下有效监测点数 |
| `selectable` | 是否可直接选择 |
| `children` | 子节点 |
## 6. 查询趋势指标树
- 路径:`GET /steady/data-view/indicator-tree`
- 返回:`HttpResult<List<SteadyDataViewIndicatorNodeVO>>`
当前指标目录覆盖:
- 电压趋势:`V_RMS``V_LINE_RMS`
- 电流趋势:`I_RMS`
- 频率趋势:`FREQ`
- 谐波趋势:`V_THD``I_THD``V_HARMONIC``I_HARMONIC``V_HARMONIC_RATE``I_HARMONIC_RATE``I_INTER_HARMONIC``P_HARMONIC_POWER``Q_HARMONIC_POWER``S_HARMONIC_POWER`
- 闪变趋势:`FLUC``PST``PLT`
叶子节点会返回 `tableName``phaseCodes``seriesFields``supportStats``harmonicOrderStart``harmonicOrderEnd``unit`,前端按这些字段驱动相别、统计类型和谐波次数选择。
## 7. 查询趋势数据
- 路径:`POST /steady/data-view/trend/query`
- 返回:`HttpResult<SteadyTrendQueryVO>`
请求示例:
```json
{
"lineIds": ["line-001"],
"indicatorCodes": ["V_RMS"],
"statTypes": ["AVG", "MAX", "MIN", "CP95"],
"phases": ["A", "B", "C"],
"timeStart": "2026-05-01 00:00:00",
"timeEnd": "2026-05-01 23:59:59",
"bucket": "10m",
"qualityFlag": 1
}
```
返回示例:
```json
{
"sampled": true,
"bucket": "10m",
"sourcePointCount": 144,
"displayPointCount": 144,
"loadableDays": ["2026-05-01"],
"series": [
{
"seriesKey": "line-001|V_RMS|A|AVG|RMS",
"lineId": "line-001",
"lineName": "进线一",
"indicatorCode": "V_RMS",
"indicatorName": "相电压有效值",
"seriesName": "相电压有效值",
"phase": "A",
"statType": "AVG",
"unit": "V",
"points": [
{
"time": "2026-05-01 00:00:00",
"value": 220.1
}
]
}
]
}
```
谐波请求必须指定 `harmonicOrders`,最多 6 个:
```json
{
"lineIds": ["line-001"],
"indicatorCodes": ["V_HARMONIC"],
"statTypes": ["MAX"],
"phases": ["A"],
"harmonicOrders": [3, 5, 7],
"timeStart": "2026-05-01 00:00:00",
"timeEnd": "2026-05-01 23:59:59",
"bucket": "10m",
"qualityFlag": 1
}
```
## 8. 按天查询趋势数据
- 路径:`POST /steady/data-view/trend/day`
- 返回:`HttpResult<SteadyTrendQueryVO>`
请求体与 `/trend/query` 一致。前端切换日期或加载某一天数据时,将 `timeStart``timeEnd` 控制在当天范围即可。
## 9. 查询趋势统计摘要
- 路径:`POST /steady/data-view/trend/summary`
- 返回:`HttpResult<SteadyTrendSummaryVO>`
请求体与 `/trend/query` 一致。后端按当前查询范围返回每条曲线的 `max``avg``min``cp95`
## 10. InfluxDB 配置
配置项前缀:`steady.influxdb`
```yaml
steady:
influxdb:
url: http://192.168.1.103:18086
database: pqsbase
username: admin
password: ${STEADY_INFLUXDB_PASSWORD:}
ssl: false
connect-timeout-ms: 5000
read-timeout-ms: 30000
```
接口按 InfluxDB 1.x InfluxQL `/query` 方式访问。代码不会提交明文密码;本地密码请通过环境变量或本地覆盖配置提供。
## 11. 返回字段说明
| 字段 | 说明 |
| --- | --- |
| `tableName` | 数据表名 |
| `lineId` | 监测点 ID |
| `timeId` | 数据时间 |
| `phasicType` | 相别 |
| `qualityFlag` | 质量标识 |
| `equipmentName` | 设备名称,台账缺失时为 `-` |
| `engineeringName` | 工程名称,台账缺失时为 `-` |
| `projectName` | 项目名称,台账缺失时为 `-` |
| `lineName` | 监测点名称,台账缺失时为 `-` |
| `values` | 动态指标字段,字段名与目标 `data_*` 表保持一致 |
## 12. 常见错误场景
| 场景 | 后端提示 |
| --- | --- |
| 表名不在 add-data 注册表范围内 | `稳态数据表不支持xxx` |
| 开始时间大于结束时间 | `开始时间不能大于结束时间` |
| 时间格式无法解析 | `时间格式不正确,仅支持 yyyy-MM-dd HH:mm:ss` |
| 相别不为 `A/B/C/T` | `相别只能是 A、B、C、T` |
| 质量标识不为 `0/1` | `质量标识只能是 0 或 1` |
| `lineIds` 超过 1000 个 | `监测点 ID 查询数量不能超过 1000 个` |
| 台账关键字匹配监测点超过 1000 个 | `台账检索匹配监测点过多,请缩小查询条件` |
| 趋势监测点为空 | `监测点 ID 不能为空` |
| 趋势指标为空 | `指标不能为空` |
| 多监测点同时多指标查询 | `多监测点查询时只能选择 1 个指标` |
| 趋势曲线超过 24 条 | `趋势曲线数量不能超过 24 条,请缩小监测点、指标、相别或统计类型范围` |
| 谐波指标未传次数 | `谐波次数不能为空` |
| 谐波次数超过 6 个 | `谐波次数最多选择 6 个` |
| InfluxDB 未配置地址 | `InfluxDB 地址未配置` |
## 13. 当前限制
- 当前仅提供分页、详情和模板查询,未提供动态 Excel 导出。
- 趋势接口已提供后端结构和 InfluxQL 查询封装,未做真实 InfluxDB 联调。
- `sourcePointCount` 当前与实际返回点数一致,未额外发 InfluxDB `count` 查询。

View File

@@ -0,0 +1,51 @@
<?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>steady-DataView</artifactId>
<dependencies>
<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>com.njcn.gather</groupId>
<artifactId>add-ledger</artifactId>
<version>1.0.0</version>
</dependency>
<dependency>
<groupId>com.njcn.gather</groupId>
<artifactId>add-data</artifactId>
<version>1.0.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,62 @@
package com.njcn.gather.steady.checksquare.component;
import com.njcn.gather.steady.checksquare.pojo.vo.SteadyChecksquareSegmentVO;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
/**
* 数据校验连续性计算组件。
*/
@Component
public class SteadyChecksquareCalculator {
public static final String STATUS_NORMAL = "NORMAL";
public static final String STATUS_MISSING = "MISSING";
private static final DateTimeFormatter OUTPUT_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
public List<SteadyChecksquareSegmentVO> buildSegments(List<LocalDateTime> slots, Set<LocalDateTime> actualSlots,
int intervalMinutes) {
List<SteadyChecksquareSegmentVO> result = new ArrayList<SteadyChecksquareSegmentVO>();
if (slots == null || slots.isEmpty()) {
return result;
}
String currentStatus = resolveStatus(slots.get(0), actualSlots);
LocalDateTime segmentStart = slots.get(0);
LocalDateTime previousSlot = slots.get(0);
int pointCount = 1;
for (int i = 1; i < slots.size(); i++) {
LocalDateTime slot = slots.get(i);
String status = resolveStatus(slot, actualSlots);
if (!currentStatus.equals(status)) {
result.add(buildSegment(segmentStart, previousSlot, currentStatus, pointCount, intervalMinutes));
segmentStart = slot;
pointCount = 0;
currentStatus = status;
}
previousSlot = slot;
pointCount++;
}
result.add(buildSegment(segmentStart, previousSlot, currentStatus, pointCount, intervalMinutes));
return result;
}
private SteadyChecksquareSegmentVO buildSegment(LocalDateTime startTime, LocalDateTime endTime, String status,
int pointCount, int intervalMinutes) {
SteadyChecksquareSegmentVO segment = new SteadyChecksquareSegmentVO();
segment.setStartTime(OUTPUT_TIME_FORMATTER.format(startTime));
segment.setEndTime(OUTPUT_TIME_FORMATTER.format(endTime));
segment.setStatus(status);
segment.setMissingPointCount(STATUS_MISSING.equals(status) ? pointCount : 0);
segment.setDurationMinutes(pointCount * intervalMinutes);
return segment;
}
private String resolveStatus(LocalDateTime slot, Set<LocalDateTime> actualSlots) {
return actualSlots != null && actualSlots.contains(slot) ? STATUS_NORMAL : STATUS_MISSING;
}
}

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,373 @@
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 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 {
String body = executeQuery(query);
List<SteadyChecksquareValuePointBO> points = parseValuePoints(body, 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 = parseBatchValuePoints(executeQuery(query), 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;
}
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,95 @@
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.SteadyChecksquareCreateVO;
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/data-view/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<SteadyChecksquareCreateVO> create(@RequestBody @Validated SteadyChecksquareQueryParam param) {
String methodDescribe = getMethodDescribe("create");
LogUtil.njcnDebug(log, "{}开始新增数据校验记录param={}", methodDescribe, param);
SteadyChecksquareCreateVO 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,18 @@
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_SUCCESS = "SUCCESS";
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

@@ -0,0 +1,30 @@
package com.njcn.gather.steady.checksquare.pojo.param;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.io.Serializable;
import java.util.List;
/**
* 数据校验新增检测参数。
*/
@Data
@ApiModel("数据校验新增检测参数")
public class SteadyChecksquareQueryParam implements Serializable {
private static final long serialVersionUID = 1L;
@ApiModelProperty("监测点 ID")
private String lineId;
@ApiModelProperty("指标编码")
private List<String> indicatorCodes;
@ApiModelProperty("开始时间,格式 yyyy-MM-dd HH:mm:ss")
private String timeStart;
@ApiModelProperty("结束时间,格式 yyyy-MM-dd HH:mm:ss")
private String timeEnd;
}

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,44 @@
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;
/**
* 新增数据校验结果。
*/
@Data
@ApiModel("新增数据校验结果")
public class SteadyChecksquareCreateVO 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 Integer itemCount;
@ApiModelProperty("异常检测项数量")
private Integer abnormalItemCount;
}

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

@@ -0,0 +1,81 @@
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 SteadyChecksquareItemVO implements Serializable {
private static final long serialVersionUID = 1L;
@ApiModelProperty("检测项 ID")
private String itemId;
@ApiModelProperty("校验项唯一键")
private String itemKey;
@ApiModelProperty("指标编码")
private String indicatorCode;
@ApiModelProperty("指标名称")
private String indicatorName;
@ApiModelProperty("谐波次数")
private Integer harmonicOrder;
@ApiModelProperty("当前校验项统计间隔,单位分钟")
private Integer intervalMinutes;
@ApiModelProperty("时间范围内是否存在任意数据")
private Boolean hasData;
@ApiModelProperty("期望点数")
private Integer expectedPointCount;
@ApiModelProperty("实际点数")
private Integer actualPointCount;
@ApiModelProperty("缺失点数")
private Integer missingPointCount;
@ApiModelProperty("数据完整性")
private BigDecimal dataIntegrity;
@ApiModelProperty("数据完整性文本")
private String dataIntegrityText;
@ApiModelProperty("指标值大小关系是否异常")
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("统计类型摘要")
private List<SteadyChecksquareStatSummaryVO> statSummaries = new ArrayList<SteadyChecksquareStatSummaryVO>();
@ApiModelProperty("统计类型明细")
private List<SteadyChecksquareStatDetailVO> statDetails = new ArrayList<SteadyChecksquareStatDetailVO>();
}

View File

@@ -0,0 +1,43 @@
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 SteadyChecksquareQueryVO 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 List<SteadyChecksquareItemVO> items = new ArrayList<SteadyChecksquareItemVO>();
}

View File

@@ -0,0 +1,35 @@
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;
/**
* 数据校验连续性区间。
*/
@Data
@ApiModel("数据校验连续性区间")
public class SteadyChecksquareSegmentVO implements Serializable {
private static final long serialVersionUID = 1L;
@ApiModelProperty("开始时间")
private String startTime;
@ApiModelProperty("结束时间")
private String endTime;
@ApiModelProperty("状态NORMAL/MISSING")
private String status;
@ApiModelProperty("谐波次数")
private Integer harmonicOrder;
@ApiModelProperty("缺失点数")
private Integer missingPointCount;
@ApiModelProperty("持续时长,单位分钟")
private Integer durationMinutes;
}

View File

@@ -0,0 +1,28 @@
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 SteadyChecksquareStatDetailVO implements Serializable {
private static final long serialVersionUID = 1L;
@ApiModelProperty("统计类型")
private String statType;
@ApiModelProperty("是否支持")
private Boolean supported;
@ApiModelProperty("连续性区间")
private List<SteadyChecksquareSegmentVO> segments = new ArrayList<SteadyChecksquareSegmentVO>();
}

View File

@@ -0,0 +1,42 @@
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 SteadyChecksquareStatSummaryVO implements Serializable {
private static final long serialVersionUID = 1L;
@ApiModelProperty("统计类型")
private String statType;
@ApiModelProperty("是否支持")
private Boolean supported;
@ApiModelProperty("是否存在数据")
private Boolean hasData;
@ApiModelProperty("期望点数")
private Integer expectedPointCount;
@ApiModelProperty("实际点数")
private Integer actualPointCount;
@ApiModelProperty("缺失点数")
private Integer missingPointCount;
@ApiModelProperty("数据完整性")
private BigDecimal dataIntegrity;
@ApiModelProperty("数据完整性文本")
private String dataIntegrityText;
}

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,30 @@
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.SteadyChecksquareCreateVO;
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);
SteadyChecksquareCreateVO 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,215 @@
package com.njcn.gather.steady.datavie.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 com.njcn.gather.steady.datavie.pojo.vo.SteadyTrendPointVO;
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.List;
/**
* 稳态趋势 InfluxDB 查询组件。
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class SteadyInfluxQueryComponent {
private static final DateTimeFormatter INFLUX_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss'Z'");
private static final DateTimeFormatter OUTPUT_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
private final SteadyInfluxDbProperties properties;
public List<SteadyTrendPointVO> queryTrendPoints(SteadyTrendResolvedFieldBO field, LocalDateTime startTime,
LocalDateTime endTime, Integer qualityFlag) {
validateConfig();
String query = buildTrendQuery(field, startTime, endTime, qualityFlag);
String diagnostic = buildTrendQueryDiagnostic(field, startTime, endTime, qualityFlag);
long startMillis = System.currentTimeMillis();
log.info("稳态趋势 InfluxDB 查询开始,{}query={}", diagnostic, query);
try {
String body = executeQuery(query);
List<SteadyTrendPointVO> points = parseTrendPoints(body);
log.info("稳态趋势 InfluxDB 查询结束,{}pointCount={}costMs={}", diagnostic, points.size(), System.currentTimeMillis() - startMillis);
return points;
} catch (RuntimeException ex) {
log.warn("稳态趋势 InfluxDB 查询异常,{}costMs={}error={}", diagnostic, System.currentTimeMillis() - startMillis, ex.getMessage());
throw ex;
}
}
String buildTrendQueryDiagnostic(SteadyTrendResolvedFieldBO field, LocalDateTime startTime, LocalDateTime endTime,
Integer qualityFlag) {
StringBuilder diagnostic = new StringBuilder();
diagnostic.append("measurement=").append(field.getMeasurement());
diagnostic.append(", field=").append(field.getField());
diagnostic.append(", lineId=").append(field.getLineId());
diagnostic.append(", phase=").append(field.getPhase());
diagnostic.append(", statType=").append(resolveValueType(field.getStatType()));
diagnostic.append(", qualityFlag=").append(qualityFlag);
diagnostic.append(", timeStart=").append(startTime);
diagnostic.append(", timeEnd=").append(endTime);
return diagnostic.toString();
}
public String buildTrendQuery(SteadyTrendResolvedFieldBO field, LocalDateTime startTime, LocalDateTime endTime,
Integer qualityFlag) {
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("'");
}
if (qualityFlag != null) {
sql.append(" AND \"quality_flag\" = '").append(qualityFlag).append("'");
}
sql.append(" ORDER BY time ASC");
return sql.toString();
}
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 List<SteadyTrendPointVO> parseTrendPoints(String body) {
try {
JsonNode root = OBJECT_MAPPER.readTree(body);
JsonNode series = root.path("results").path(0).path("series").path(0);
JsonNode values = series.path("values");
if (!values.isArray()) {
return new ArrayList<SteadyTrendPointVO>();
}
List<SteadyTrendPointVO> result = new ArrayList<SteadyTrendPointVO>();
for (JsonNode value : values) {
if (value.size() < 2 || value.get(1).isNull()) {
continue;
}
String time = formatInfluxTime(value.get(0).asText());
BigDecimal pointValue = value.get(1).decimalValue();
result.add(new SteadyTrendPointVO(time, pointValue));
}
return result;
} catch (IOException ex) {
throw fail("InfluxDB 返回结果解析失败:" + ex.getMessage());
}
}
private String formatInfluxTime(String value) {
try {
return OUTPUT_TIME_FORMATTER.format(OffsetDateTime.parse(value).withOffsetSameInstant(ZoneOffset.UTC).toLocalDateTime());
} catch (RuntimeException ex) {
return value;
}
}
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,219 @@
package com.njcn.gather.steady.datavie.component;
import com.njcn.common.pojo.enums.response.CommonResponseEnum;
import com.njcn.common.pojo.exception.BusinessException;
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.steady.datavie.pojo.param.SteadyTrendQueryParam;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
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.List;
/**
* 稳态趋势字段白名单解析器。
*/
@Component
@RequiredArgsConstructor
public class SteadyTrendFieldResolver {
private static final int MAX_LINE_COUNT = 8;
private static final int MAX_INDICATOR_COUNT = 8;
private static final int MAX_SERIES_COUNT = 24;
private static final int MAX_HARMONIC_ORDER_COUNT = 3;
private static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
private final SteadyTrendIndicatorCatalog indicatorCatalog;
public List<SteadyTrendResolvedFieldBO> resolveFields(SteadyTrendQueryParam param) {
validateBasicParam(param);
List<String> lineIds = normalizeTextList(param.getLineIds());
List<String> indicatorCodes = normalizeTextList(param.getIndicatorCodes());
List<String> statTypes = normalizeUpperList(param.getStatTypes());
if (statTypes.isEmpty()) {
statTypes.add("AVG");
}
List<SteadyTrendResolvedFieldBO> result = new ArrayList<SteadyTrendResolvedFieldBO>();
for (String lineId : lineIds) {
for (String indicatorCode : indicatorCodes) {
SteadyTrendIndicatorDefinitionBO indicator = requireIndicator(indicatorCode);
List<String> phases = resolvePhases(indicator);
for (String phase : phases) {
for (String statType : statTypes) {
validateStatType(indicator, statType);
result.addAll(resolveIndicatorFields(lineId, indicator, phase, statType, param.getHarmonicOrders()));
}
}
}
}
if (result.size() > MAX_SERIES_COUNT) {
throw fail("趋势曲线数量不能超过 24 条,请缩小监测点、指标或统计类型范围");
}
return result;
}
public 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 List<SteadyTrendResolvedFieldBO> resolveIndicatorFields(String lineId, SteadyTrendIndicatorDefinitionBO indicator,
String phase, String statType, List<Integer> harmonicOrders) {
if (Boolean.TRUE.equals(indicator.getHarmonic())) {
return resolveHarmonicFields(lineId, indicator, phase, statType, harmonicOrders);
}
List<SteadyTrendResolvedFieldBO> result = new ArrayList<SteadyTrendResolvedFieldBO>();
for (SteadyTrendSeriesFieldBO seriesField : indicator.getSeriesFields()) {
result.add(buildResolvedField(lineId, indicator, phase, statType, seriesField.getField(), seriesField.getName()));
}
return result;
}
private List<SteadyTrendResolvedFieldBO> resolveHarmonicFields(String lineId, SteadyTrendIndicatorDefinitionBO indicator,
String phase, String statType, List<Integer> harmonicOrders) {
List<Integer> orders = normalizeOrders(harmonicOrders);
if (orders.isEmpty()) {
throw fail("谐波次数不能为空");
}
if (orders.size() > MAX_HARMONIC_ORDER_COUNT) {
throw fail("谐波次数不允许一次展示超过3个");
}
List<SteadyTrendResolvedFieldBO> result = new ArrayList<SteadyTrendResolvedFieldBO>();
for (Integer order : orders) {
if (order < indicator.getHarmonicOrderStart() || order > indicator.getHarmonicOrderEnd()) {
throw fail("谐波次数只能在 " + indicator.getHarmonicOrderStart() + "" + indicator.getHarmonicOrderEnd() + " 之间");
}
String field = indicator.getHarmonicFieldPrefix() + "_" + order;
result.add(buildResolvedField(lineId, indicator, phase, statType, field, order + "" + indicator.getName()));
}
return result;
}
private SteadyTrendResolvedFieldBO buildResolvedField(String lineId, SteadyTrendIndicatorDefinitionBO indicator,
String phase, String statType, String field, String seriesName) {
SteadyTrendResolvedFieldBO resolved = new SteadyTrendResolvedFieldBO();
resolved.setMeasurement(indicator.getTableName());
resolved.setField(field);
resolved.setLineId(lineId);
resolved.setIndicatorCode(indicator.getIndicatorCode());
resolved.setIndicatorName(indicator.getName());
resolved.setSeriesName(seriesName);
resolved.setPhase(phase);
resolved.setStatType(statType);
resolved.setUnit(indicator.getUnit());
resolved.setSeriesKey(lineId + "|" + indicator.getIndicatorCode() + "|" + phase + "|" + statType + "|" + field);
return resolved;
}
private void validateBasicParam(SteadyTrendQueryParam param) {
if (param == null) {
throw fail("趋势查询参数不能为空");
}
List<String> lineIds = normalizeTextList(param.getLineIds());
List<String> indicatorCodes = normalizeTextList(param.getIndicatorCodes());
if (lineIds.isEmpty()) {
throw fail("监测点 ID 不能为空");
}
if (indicatorCodes.isEmpty()) {
throw fail("指标不能为空");
}
if (lineIds.size() > MAX_LINE_COUNT) {
throw fail("监测点数量不能超过 8 个");
}
if (indicatorCodes.size() > MAX_INDICATOR_COUNT) {
throw fail("指标数量不能超过 8 个");
}
if (lineIds.size() > 1 && indicatorCodes.size() > 1) {
throw fail("多监测点查询时只能选择 1 个指标");
}
LocalDateTime startTime = parseRequiredTime(param.getTimeStart(), "开始时间不能为空");
LocalDateTime endTime = parseRequiredTime(param.getTimeEnd(), "结束时间不能为空");
if (startTime.isAfter(endTime)) {
throw fail("开始时间不能大于结束时间");
}
if (param.getQualityFlag() != null && param.getQualityFlag() != 0 && param.getQualityFlag() != 1) {
throw fail("质量标识只能是 0 或 1");
}
}
private SteadyTrendIndicatorDefinitionBO requireIndicator(String indicatorCode) {
SteadyTrendIndicatorDefinitionBO indicator = indicatorCatalog.getIndicator(indicatorCode);
if (indicator == null) {
throw fail("稳态趋势指标不支持:" + indicatorCode);
}
return indicator;
}
private List<String> resolvePhases(SteadyTrendIndicatorDefinitionBO indicator) {
return new ArrayList<String>(indicator.getPhaseCodes());
}
private void validateStatType(SteadyTrendIndicatorDefinitionBO indicator, String statType) {
if (!"AVG".equals(statType) && !"MAX".equals(statType) && !"MIN".equals(statType) && !"CP95".equals(statType)) {
throw fail("统计类型只能是 AVG、MAX、MIN、CP95");
}
if (!indicator.getSupportStats().contains(statType)) {
throw fail("指标 " + indicator.getIndicatorCode() + " 不支持统计类型:" + statType);
}
}
private List<String> normalizeTextList(List<String> values) {
if (values == null || values.isEmpty()) {
return new ArrayList<String>();
}
List<String> result = new ArrayList<String>();
for (String value : values) {
String text = trimToNull(value);
if (text != null && !result.contains(text)) {
result.add(text);
}
}
return result;
}
private List<String> normalizeUpperList(List<String> values) {
List<String> result = normalizeTextList(values);
for (int i = 0; i < result.size(); i++) {
result.set(i, result.get(i).toUpperCase());
}
return result;
}
private List<Integer> normalizeOrders(List<Integer> orders) {
if (orders == null || orders.isEmpty()) {
return Collections.emptyList();
}
List<Integer> result = new ArrayList<Integer>();
for (Integer order : orders) {
if (order != null && !result.contains(order)) {
result.add(order);
}
}
return result;
}
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,111 @@
package com.njcn.gather.steady.datavie.component;
import com.njcn.gather.steady.datavie.pojo.bo.SteadyTrendIndicatorDefinitionBO;
import com.njcn.gather.steady.datavie.pojo.bo.SteadyTrendSeriesFieldBO;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
* 稳态趋势指标目录。
*/
@Component
public class SteadyTrendIndicatorCatalog {
private static final List<String> FULL_STATS = Collections.unmodifiableList(Arrays.asList("AVG", "MAX", "MIN", "CP95"));
private static final List<String> AVG_ONLY = Collections.unmodifiableList(Collections.singletonList("AVG"));
private static final List<String> ABC_PHASES = Collections.unmodifiableList(Arrays.asList("A", "B", "C"));
private static final List<String> T_PHASE = Collections.unmodifiableList(Collections.singletonList("T"));
private final List<SteadyTrendIndicatorDefinitionBO> indicators;
private final Map<String, SteadyTrendIndicatorDefinitionBO> indicatorMap;
public SteadyTrendIndicatorCatalog() {
List<SteadyTrendIndicatorDefinitionBO> result = new ArrayList<SteadyTrendIndicatorDefinitionBO>();
result.add(indicator("V_RMS", "相电压有效值", "VOLTAGE", "电压趋势", "data_v", ABC_PHASES,
fields(field("rms", "相电压有效值")), FULL_STATS, "V"));
result.add(indicator("V_LINE_RMS", "线电压有效值", "VOLTAGE", "电压趋势", "data_v", ABC_PHASES,
fields(field("rms_lvr", "线电压有效值")),
FULL_STATS, "V"));
result.add(indicator("FREQ", "频率", "FREQUENCY", "频率趋势", "data_v", T_PHASE,
fields(field("freq", "频率")), FULL_STATS, "Hz"));
result.add(indicator("V_THD", "电压总谐波畸变率", "HARMONIC", "谐波趋势", "data_v", ABC_PHASES,
fields(field("v_thd", "电压总谐波畸变率")), FULL_STATS, "%"));
result.add(indicator("I_RMS", "电流有效值", "CURRENT", "电流趋势", "data_i", ABC_PHASES,
fields(field("rms", "电流有效值")), FULL_STATS, "A"));
result.add(indicator("I_THD", "电流总谐波畸变率", "HARMONIC", "谐波趋势", "data_i", ABC_PHASES,
fields(field("i_thd", "电流总谐波畸变率")), FULL_STATS, "%"));
result.add(harmonic("V_HARMONIC", "电压谐波幅值", "data_harmphasic_v", "v", "V"));
result.add(harmonic("I_HARMONIC", "电流谐波幅值", "data_harmphasic_i", "i", "A"));
result.add(harmonic("V_HARMONIC_RATE", "电压谐波含有率", "data_harmrate_v", "v", "%"));
result.add(harmonic("I_INTER_HARMONIC", "间谐波电流", "data_inharm_i", "i", "A"));
result.add(harmonicPower("P_HARMONIC_POWER", "有功谐波功率", "data_harmpower_p", "p", "kW"));
result.add(harmonicPower("Q_HARMONIC_POWER", "无功谐波功率", "data_harmpower_q", "q", "kvar"));
result.add(harmonicPower("S_HARMONIC_POWER", "视在谐波功率", "data_harmpower_s", "s", "kVA"));
result.add(indicator("FLUC", "电压波动", "FLICKER", "闪变趋势", "data_fluc", T_PHASE,
fields(field("fluc", "电压波动")), AVG_ONLY, "%"));
result.add(indicator("PST", "短时闪变", "FLICKER", "闪变趋势", "data_flicker", T_PHASE,
fields(field("pst", "短时闪变")), AVG_ONLY, ""));
result.add(indicator("PLT", "长时闪变", "FLICKER", "闪变趋势", "data_plt", T_PHASE,
fields(field("plt", "长时闪变")), AVG_ONLY, ""));
indicators = Collections.unmodifiableList(result);
Map<String, SteadyTrendIndicatorDefinitionBO> map = new LinkedHashMap<String, SteadyTrendIndicatorDefinitionBO>();
for (SteadyTrendIndicatorDefinitionBO indicator : indicators) {
map.put(indicator.getIndicatorCode(), indicator);
}
indicatorMap = Collections.unmodifiableMap(map);
}
public List<SteadyTrendIndicatorDefinitionBO> listIndicators() {
return indicators;
}
public SteadyTrendIndicatorDefinitionBO getIndicator(String indicatorCode) {
return indicatorMap.get(indicatorCode);
}
private SteadyTrendIndicatorDefinitionBO harmonic(String code, String name, String tableName, String prefix, String unit) {
SteadyTrendIndicatorDefinitionBO indicator = indicator(code, name, "HARMONIC", "谐波趋势", tableName, ABC_PHASES,
new ArrayList<SteadyTrendSeriesFieldBO>(), FULL_STATS, unit);
indicator.setHarmonic(true);
indicator.setHarmonicFieldPrefix(prefix);
indicator.setHarmonicOrderStart(2);
indicator.setHarmonicOrderEnd(50);
return indicator;
}
private SteadyTrendIndicatorDefinitionBO harmonicPower(String code, String name, String tableName, String prefix, String unit) {
return harmonic(code, name, tableName, prefix, unit);
}
private SteadyTrendIndicatorDefinitionBO indicator(String code, String name, String groupCode, String groupName,
String tableName, List<String> phaseCodes,
List<SteadyTrendSeriesFieldBO> seriesFields,
List<String> supportStats, String unit) {
SteadyTrendIndicatorDefinitionBO indicator = new SteadyTrendIndicatorDefinitionBO();
indicator.setIndicatorCode(code);
indicator.setName(name);
indicator.setGroupCode(groupCode);
indicator.setGroupName(groupName);
indicator.setTableName(tableName);
indicator.setPhaseCodes(new ArrayList<String>(phaseCodes));
indicator.setSeriesFields(new ArrayList<SteadyTrendSeriesFieldBO>(seriesFields));
indicator.setSupportStats(new ArrayList<String>(supportStats));
indicator.setUnit(unit);
return indicator;
}
private List<SteadyTrendSeriesFieldBO> fields(SteadyTrendSeriesFieldBO... fields) {
return new ArrayList<SteadyTrendSeriesFieldBO>(Arrays.asList(fields));
}
private SteadyTrendSeriesFieldBO field(String field, String name) {
return new SteadyTrendSeriesFieldBO(field, name);
}
}

View File

@@ -0,0 +1,28 @@
package com.njcn.gather.steady.datavie.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
/**
* 稳态趋势 InfluxDB 配置。
*/
@Data
@Component
@ConfigurationProperties(prefix = "steady.influxdb")
public class SteadyInfluxDbProperties {
private String url;
private String database;
private String username;
private String password;
private Boolean ssl = false;
private Integer connectTimeoutMs = 5000;
private Integer readTimeoutMs = 30000;
}

View File

@@ -0,0 +1,26 @@
package com.njcn.gather.steady.datavie.config;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import java.io.IOException;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
/**
* 稳态数据时间字段按秒输出,避免接口响应携带毫秒。
*/
public class SteadySecondTimeSerializer extends JsonSerializer<LocalDateTime> {
private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
@Override
public void serialize(LocalDateTime value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
if (value == null) {
gen.writeNull();
return;
}
gen.writeString(FORMATTER.format(value));
}
}

View File

@@ -0,0 +1,60 @@
package com.njcn.gather.steady.datavie.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.datavie.pojo.param.SteadyDataViewDetailParam;
import com.njcn.gather.steady.datavie.pojo.vo.SteadyDataViewTemplateVO;
import com.njcn.gather.steady.datavie.pojo.vo.SteadyDataViewVO;
import com.njcn.gather.steady.datavie.service.SteadyDataViewService;
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.RestController;
import java.util.List;
/**
* 稳态数据查看接口。
*/
@Validated
@Slf4j
@Api(tags = "稳态数据查看")
@RestController
@RequestMapping("/steady/data-view")
@RequiredArgsConstructor
public class SteadyDataViewController extends BaseController {
/** 稳态数据查看服务。 */
private final SteadyDataViewService steadyDataViewService;
@OperateInfo(info = LogEnum.BUSINESS_COMMON)
@ApiOperation("查询稳态数据详情")
@PostMapping("/detail")
public HttpResult<SteadyDataViewVO> getSteadyDataDetail(@RequestBody SteadyDataViewDetailParam param) {
String methodDescribe = getMethodDescribe("getSteadyDataDetail");
LogUtil.njcnDebug(log, "{}开始查询稳态数据详情param={}", methodDescribe, param);
SteadyDataViewVO result = steadyDataViewService.getSteadyDataDetail(param);
return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, result, methodDescribe);
}
@OperateInfo(info = LogEnum.BUSINESS_COMMON)
@ApiOperation("查询稳态数据模板")
@GetMapping("/templates")
public HttpResult<List<SteadyDataViewTemplateVO>> listTemplates() {
String methodDescribe = getMethodDescribe("listTemplates");
LogUtil.njcnDebug(log, "{},开始查询稳态数据模板", methodDescribe);
List<SteadyDataViewTemplateVO> result = steadyDataViewService.listTemplates();
return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, result, methodDescribe);
}
}

View File

@@ -0,0 +1,43 @@
package com.njcn.gather.steady.datavie.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.datavie.pojo.vo.SteadyDataViewIndicatorNodeVO;
import com.njcn.gather.steady.datavie.service.SteadyDataViewIndicatorService;
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.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
/**
* 稳态趋势指标接口。
*/
@Slf4j
@Api(tags = "稳态趋势指标")
@RestController
@RequestMapping("/steady/data-view")
@RequiredArgsConstructor
public class SteadyDataViewIndicatorController extends BaseController {
private final SteadyDataViewIndicatorService indicatorService;
@OperateInfo(info = LogEnum.BUSINESS_COMMON)
@ApiOperation("查询稳态趋势指标树")
@GetMapping("/indicator-tree")
public HttpResult<List<SteadyDataViewIndicatorNodeVO>> listIndicatorTree() {
String methodDescribe = getMethodDescribe("listIndicatorTree");
LogUtil.njcnDebug(log, "{},开始查询稳态趋势指标树", methodDescribe);
List<SteadyDataViewIndicatorNodeVO> result = indicatorService.listIndicatorTree();
return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, result, methodDescribe);
}
}

View File

@@ -0,0 +1,44 @@
package com.njcn.gather.steady.datavie.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.datavie.pojo.vo.SteadyDataViewLedgerNodeVO;
import com.njcn.gather.steady.datavie.service.SteadyDataViewLedgerService;
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.GetMapping;
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/data-view")
@RequiredArgsConstructor
public class SteadyDataViewLedgerController extends BaseController {
private final SteadyDataViewLedgerService ledgerService;
@OperateInfo(info = LogEnum.BUSINESS_COMMON)
@ApiOperation("查询稳态趋势台账树")
@GetMapping("/ledger-tree")
public HttpResult<List<SteadyDataViewLedgerNodeVO>> listLedgerTree(@RequestParam(value = "keyword", required = false) String keyword) {
String methodDescribe = getMethodDescribe("listLedgerTree");
LogUtil.njcnDebug(log, "{}开始查询稳态趋势台账树keyword={}", methodDescribe, keyword);
List<SteadyDataViewLedgerNodeVO> result = ledgerService.listLedgerTree(keyword);
return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, result, methodDescribe);
}
}

View File

@@ -0,0 +1,64 @@
package com.njcn.gather.steady.datavie.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.datavie.pojo.param.SteadyTrendQueryParam;
import com.njcn.gather.steady.datavie.pojo.vo.SteadyTrendQueryVO;
import com.njcn.gather.steady.datavie.pojo.vo.SteadyTrendSummaryVO;
import com.njcn.gather.steady.datavie.service.SteadyDataViewTrendService;
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/trend")
@RequiredArgsConstructor
public class SteadyDataViewTrendController extends BaseController {
private final SteadyDataViewTrendService trendService;
@OperateInfo(info = LogEnum.BUSINESS_COMMON)
@ApiOperation("查询稳态趋势")
@PostMapping("/query")
public HttpResult<SteadyTrendQueryVO> queryTrend(@RequestBody SteadyTrendQueryParam param) {
String methodDescribe = getMethodDescribe("queryTrend");
LogUtil.njcnDebug(log, "{}开始查询稳态趋势param={}", methodDescribe, param);
SteadyTrendQueryVO result = trendService.queryTrend(param);
return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, result, methodDescribe);
}
@OperateInfo(info = LogEnum.BUSINESS_COMMON)
@ApiOperation("按天查询稳态趋势")
@PostMapping("/day")
public HttpResult<SteadyTrendQueryVO> queryTrendDay(@RequestBody SteadyTrendQueryParam param) {
String methodDescribe = getMethodDescribe("queryTrendDay");
LogUtil.njcnDebug(log, "{}开始按天查询稳态趋势param={}", methodDescribe, param);
SteadyTrendQueryVO result = trendService.queryTrendDay(param);
return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, result, methodDescribe);
}
@OperateInfo(info = LogEnum.BUSINESS_COMMON)
@ApiOperation("查询稳态趋势统计摘要")
@PostMapping("/summary")
public HttpResult<SteadyTrendSummaryVO> summarizeTrend(@RequestBody SteadyTrendQueryParam param) {
String methodDescribe = getMethodDescribe("summarizeTrend");
LogUtil.njcnDebug(log, "{}开始查询稳态趋势统计摘要param={}", methodDescribe, param);
SteadyTrendSummaryVO result = trendService.summarizeTrend(param);
return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, result, methodDescribe);
}
}

View File

@@ -0,0 +1,14 @@
package com.njcn.gather.steady.datavie.mapper;
import com.njcn.gather.steady.datavie.pojo.bo.SteadyDataViewLedgerRowBO;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
* 稳态趋势台账树 Mapper。
*/
public interface SteadyDataViewLedgerMapper {
List<SteadyDataViewLedgerRowBO> selectLedgerTree(@Param("keyword") String keyword);
}

View File

@@ -0,0 +1,18 @@
package com.njcn.gather.steady.datavie.mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
import java.util.Map;
/**
* 稳态数据查看 Mapper。
*/
public interface SteadyDataViewMapper {
Map<String, Object> selectSteadyDetail(@Param("tableName") String tableName,
@Param("columns") List<String> columns,
@Param("lineId") String lineId,
@Param("timeId") String timeId,
@Param("phasicType") String phasicType);
}

View File

@@ -0,0 +1,48 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.njcn.gather.steady.datavie.mapper.SteadyDataViewLedgerMapper">
<select id="selectLedgerTree" resultType="com.njcn.gather.steady.datavie.pojo.bo.SteadyDataViewLedgerRowBO">
SELECT ledger.Id AS id,
ledger.Pid AS parentId,
ledger.Name AS name,
ledger.Level AS level,
ledger.Sort AS sort,
(
SELECT COUNT(DISTINCT equipment.id)
FROM cs_ledger equipment_ledger
INNER JOIN cs_equipment_delivery equipment ON equipment.id = equipment_ledger.Id
WHERE equipment_ledger.State = 1
AND equipment_ledger.Level = 2
AND equipment.run_status &lt;&gt; 0
AND (equipment_ledger.Id = ledger.Id OR FIND_IN_SET(ledger.Id, equipment_ledger.Pids) &gt; 0)
) AS deviceCount,
(
SELECT COUNT(DISTINCT line.line_id)
FROM cs_ledger line_ledger
INNER JOIN cs_line line ON line.line_id = line_ledger.Id
INNER JOIN cs_equipment_delivery equipment ON equipment.id = line.device_id
WHERE line_ledger.State = 1
AND line_ledger.Level = 3
AND line.status = 1
AND equipment.run_status &lt;&gt; 0
AND (line_ledger.Id = ledger.Id OR FIND_IN_SET(ledger.Id, line_ledger.Pids) &gt; 0)
) AS lineCount
FROM cs_ledger ledger
WHERE ledger.State = 1
<if test="keyword != null and keyword != ''">
AND (
ledger.Name LIKE CONCAT('%', #{keyword}, '%')
OR EXISTS (
SELECT 1
FROM cs_ledger child
WHERE child.State = 1
AND FIND_IN_SET(ledger.Id, child.Pids) &gt; 0
AND child.Name LIKE CONCAT('%', #{keyword}, '%')
)
)
</if>
ORDER BY ledger.Level ASC, ledger.Sort ASC, ledger.Name ASC
</select>
</mapper>

View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.njcn.gather.steady.datavie.mapper.SteadyDataViewMapper">
<select id="selectSteadyDetail" resultType="java.util.LinkedHashMap">
SELECT
<foreach collection="columns" item="column" separator=",">
`${column}`
</foreach>
FROM `${tableName}`
WHERE LINEID = #{lineId}
AND TIMEID = #{timeId}
AND PHASIC_TYPE = #{phasicType}
LIMIT 1
</select>
</mapper>

View File

@@ -0,0 +1,28 @@
package com.njcn.gather.steady.datavie.pojo.bo;
import lombok.Data;
import java.io.Serializable;
/**
* 稳态趋势台账树查询行。
*/
@Data
public class SteadyDataViewLedgerRowBO implements Serializable {
private static final long serialVersionUID = 1L;
private String id;
private String parentId;
private String name;
private Integer level;
private Integer sort;
private Integer deviceCount;
private Integer lineCount;
}

View File

@@ -0,0 +1,42 @@
package com.njcn.gather.steady.datavie.pojo.bo;
import lombok.Data;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
/**
* 稳态趋势指标定义。
*/
@Data
public class SteadyTrendIndicatorDefinitionBO implements Serializable {
private static final long serialVersionUID = 1L;
private String indicatorCode;
private String name;
private String groupCode;
private String groupName;
private String tableName;
private List<String> phaseCodes = new ArrayList<String>();
private List<SteadyTrendSeriesFieldBO> seriesFields = new ArrayList<SteadyTrendSeriesFieldBO>();
private List<String> supportStats = new ArrayList<String>();
private Boolean harmonic = false;
private String harmonicFieldPrefix;
private Integer harmonicOrderStart;
private Integer harmonicOrderEnd;
private String unit;
}

View File

@@ -0,0 +1,36 @@
package com.njcn.gather.steady.datavie.pojo.bo;
import lombok.Data;
import java.io.Serializable;
/**
* 稳态趋势查询字段解析结果。
*/
@Data
public class SteadyTrendResolvedFieldBO implements Serializable {
private static final long serialVersionUID = 1L;
private String measurement;
private String field;
private String lineId;
private String lineName;
private String indicatorCode;
private String indicatorName;
private String seriesName;
private String phase;
private String statType;
private String unit;
private String seriesKey;
}

View File

@@ -0,0 +1,24 @@
package com.njcn.gather.steady.datavie.pojo.bo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
/**
* 稳态趋势指标曲线字段。
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class SteadyTrendSeriesFieldBO implements Serializable {
private static final long serialVersionUID = 1L;
/** InfluxDB field 基础名。 */
private String field;
/** 曲线展示名称。 */
private String name;
}

View File

@@ -0,0 +1,25 @@
package com.njcn.gather.steady.datavie.pojo.param;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
/**
* 稳态数据详情查询参数。
*/
@Data
@ApiModel("稳态数据详情查询参数")
public class SteadyDataViewDetailParam {
@ApiModelProperty("表名,对应 add-data 模板表名")
private String tableName;
@ApiModelProperty("监测点 ID")
private String lineId;
@ApiModelProperty("时间,格式 yyyy-MM-dd HH:mm:ss")
private String timeId;
@ApiModelProperty("相别A/B/C/T")
private String phasicType;
}

View File

@@ -0,0 +1,37 @@
package com.njcn.gather.steady.datavie.pojo.param;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.util.ArrayList;
import java.util.List;
/**
* 稳态趋势查询参数。
*/
@Data
@ApiModel("稳态趋势查询参数")
public class SteadyTrendQueryParam {
@ApiModelProperty("监测点 ID 列表")
private List<String> lineIds = new ArrayList<String>();
@ApiModelProperty("指标编码列表")
private List<String> indicatorCodes = new ArrayList<String>();
@ApiModelProperty("统计类型AVG/MAX/MIN/CP95")
private List<String> statTypes = new ArrayList<String>();
@ApiModelProperty("开始时间,格式 yyyy-MM-dd HH:mm:ss")
private String timeStart;
@ApiModelProperty("结束时间,格式 yyyy-MM-dd HH:mm:ss")
private String timeEnd;
@ApiModelProperty("质量标识")
private Integer qualityFlag;
@ApiModelProperty("谐波次数,谐波指标必填,默认最多展示 3 个")
private List<Integer> harmonicOrders = new ArrayList<Integer>();
}

View File

@@ -0,0 +1,51 @@
package com.njcn.gather.steady.datavie.pojo.vo;
import com.njcn.gather.steady.datavie.pojo.bo.SteadyTrendSeriesFieldBO;
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 SteadyDataViewIndicatorNodeVO implements Serializable {
private static final long serialVersionUID = 1L;
private String id;
private String parentId;
private String name;
private String groupCode;
private String indicatorCode;
private String tableName;
private List<String> phaseCodes = new ArrayList<String>();
private List<SteadyTrendSeriesFieldBO> seriesFields = new ArrayList<SteadyTrendSeriesFieldBO>();
private List<String> supportStats = new ArrayList<String>();
private Boolean harmonic = false;
private Integer harmonicOrderStart;
private Integer harmonicOrderEnd;
private String unit;
@ApiModelProperty("是否可选")
private Boolean selectable = false;
private List<SteadyDataViewIndicatorNodeVO> children = new ArrayList<SteadyDataViewIndicatorNodeVO>();
}

View File

@@ -0,0 +1,36 @@
package com.njcn.gather.steady.datavie.pojo.vo;
import io.swagger.annotations.ApiModel;
import lombok.Data;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
/**
* 稳态趋势台账树节点。
*/
@Data
@ApiModel("稳态趋势台账树节点")
public class SteadyDataViewLedgerNodeVO implements Serializable {
private static final long serialVersionUID = 1L;
private String id;
private String parentId;
private String name;
private Integer level;
private Integer sort;
private Integer deviceCount;
private Integer lineCount;
private Boolean selectable;
private List<SteadyDataViewLedgerNodeVO> children = new ArrayList<SteadyDataViewLedgerNodeVO>();
}

View File

@@ -0,0 +1,26 @@
package com.njcn.gather.steady.datavie.pojo.vo;
import lombok.Data;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
/**
* 稳态数据查看模板。
*/
@Data
public class SteadyDataViewTemplateVO implements Serializable {
private static final long serialVersionUID = 1L;
private String parameterName;
private String tableName;
private String phaseDisplay;
private List<String> phaseCodes = new ArrayList<String>();
private List<String> valueColumns = new ArrayList<String>();
}

View File

@@ -0,0 +1,45 @@
package com.njcn.gather.steady.datavie.pojo.vo;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
import com.njcn.gather.steady.datavie.config.SteadySecondTimeSerializer;
import lombok.Data;
import java.io.Serializable;
import java.time.LocalDateTime;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* 稳态数据查看展示对象。
*/
@Data
public class SteadyDataViewVO implements Serializable {
private static final long serialVersionUID = 1L;
private String tableName;
private String lineId;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@JsonDeserialize(using = LocalDateTimeDeserializer.class)
@JsonSerialize(using = SteadySecondTimeSerializer.class)
private LocalDateTime timeId;
private String phasicType;
private Integer qualityFlag;
private String equipmentName;
private String engineeringName;
private String projectName;
private String lineName;
private Map<String, Object> values = new LinkedHashMap<String, Object>();
}

View File

@@ -0,0 +1,28 @@
package com.njcn.gather.steady.datavie.pojo.vo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
import java.math.BigDecimal;
/**
* 稳态趋势点位。
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@ApiModel("稳态趋势点位")
public class SteadyTrendPointVO implements Serializable {
private static final long serialVersionUID = 1L;
@ApiModelProperty("时间,格式 yyyy-MM-dd HH:mm:ss")
private String time;
@ApiModelProperty("点位值")
private BigDecimal value;
}

View File

@@ -0,0 +1,28 @@
package com.njcn.gather.steady.datavie.pojo.vo;
import io.swagger.annotations.ApiModel;
import lombok.Data;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
/**
* 稳态趋势查询结果。
*/
@Data
@ApiModel("稳态趋势查询结果")
public class SteadyTrendQueryVO implements Serializable {
private static final long serialVersionUID = 1L;
private Boolean sampled;
private Integer sourcePointCount;
private Integer displayPointCount;
private List<String> loadableDays = new ArrayList<String>();
private List<SteadyTrendSeriesVO> series = new ArrayList<SteadyTrendSeriesVO>();
}

View File

@@ -0,0 +1,38 @@
package com.njcn.gather.steady.datavie.pojo.vo;
import io.swagger.annotations.ApiModel;
import lombok.Data;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
/**
* 稳态趋势曲线。
*/
@Data
@ApiModel("稳态趋势曲线")
public class SteadyTrendSeriesVO implements Serializable {
private static final long serialVersionUID = 1L;
private String seriesKey;
private String lineId;
private String lineName;
private String indicatorCode;
private String indicatorName;
private String seriesName;
private String phase;
private String statType;
private String unit;
private List<SteadyTrendPointVO> points = new ArrayList<SteadyTrendPointVO>();
}

View File

@@ -0,0 +1,27 @@
package com.njcn.gather.steady.datavie.pojo.vo;
import io.swagger.annotations.ApiModel;
import lombok.Data;
import java.io.Serializable;
import java.math.BigDecimal;
/**
* 稳态趋势统计项。
*/
@Data
@ApiModel("稳态趋势统计项")
public class SteadyTrendSummaryItemVO implements Serializable {
private static final long serialVersionUID = 1L;
private String seriesKey;
private BigDecimal max;
private BigDecimal avg;
private BigDecimal min;
private BigDecimal cp95;
}

View File

@@ -0,0 +1,20 @@
package com.njcn.gather.steady.datavie.pojo.vo;
import io.swagger.annotations.ApiModel;
import lombok.Data;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
/**
* 稳态趋势统计结果。
*/
@Data
@ApiModel("稳态趋势统计结果")
public class SteadyTrendSummaryVO implements Serializable {
private static final long serialVersionUID = 1L;
private List<SteadyTrendSummaryItemVO> items = new ArrayList<SteadyTrendSummaryItemVO>();
}

View File

@@ -0,0 +1,13 @@
package com.njcn.gather.steady.datavie.service;
import com.njcn.gather.steady.datavie.pojo.vo.SteadyDataViewIndicatorNodeVO;
import java.util.List;
/**
* 稳态趋势指标服务。
*/
public interface SteadyDataViewIndicatorService {
List<SteadyDataViewIndicatorNodeVO> listIndicatorTree();
}

View File

@@ -0,0 +1,13 @@
package com.njcn.gather.steady.datavie.service;
import com.njcn.gather.steady.datavie.pojo.vo.SteadyDataViewLedgerNodeVO;
import java.util.List;
/**
* 稳态趋势台账树服务。
*/
public interface SteadyDataViewLedgerService {
List<SteadyDataViewLedgerNodeVO> listLedgerTree(String keyword);
}

View File

@@ -0,0 +1,17 @@
package com.njcn.gather.steady.datavie.service;
import com.njcn.gather.steady.datavie.pojo.param.SteadyDataViewDetailParam;
import com.njcn.gather.steady.datavie.pojo.vo.SteadyDataViewTemplateVO;
import com.njcn.gather.steady.datavie.pojo.vo.SteadyDataViewVO;
import java.util.List;
/**
* 稳态数据查看服务。
*/
public interface SteadyDataViewService {
SteadyDataViewVO getSteadyDataDetail(SteadyDataViewDetailParam param);
List<SteadyDataViewTemplateVO> listTemplates();
}

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