Compare commits
28 Commits
90219a3daf
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1edee2bf12 | ||
| b7b18dc325 | |||
| a5a09022f2 | |||
| 4aca8ca2c4 | |||
| 362bbf536f | |||
|
|
89ec9e1fa3 | ||
|
|
1343d235c8 | ||
|
|
0cecf2d7a2 | ||
|
|
c0cf4de315 | ||
| 212b69060c | |||
|
|
f7154db93d | ||
|
|
557022d346 | ||
|
|
e43ab264e0 | ||
| 1c979e248a | |||
| 36962221f5 | |||
|
|
24bdaa1ae9 | ||
| 5f6c10b9cb | |||
|
|
58ca8b0c23 | ||
| 66d351afe4 | |||
| e5369fef5a | |||
| ba8bc43377 | |||
| 9a9614a9e5 | |||
| 89efc55119 | |||
| bff89bede0 | |||
| 1ee94208ae | |||
| 38f910fccd | |||
|
|
8a92ff3be0 | ||
|
|
ddadf26837 |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -5,6 +5,8 @@
|
|||||||
target/
|
target/
|
||||||
logs/
|
logs/
|
||||||
docs/
|
docs/
|
||||||
|
.codex-tmp/
|
||||||
|
.docs/
|
||||||
|
|
||||||
# Log file
|
# Log file
|
||||||
*.log
|
*.log
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
- 只清理自己造成的问题:可以删除因本次修改而产生的未使用 `import`、变量或方法;不要删除仓库中原本就存在的死代码,除非用户明确要求。
|
- 只清理自己造成的问题:可以删除因本次修改而产生的未使用 `import`、变量或方法;不要删除仓库中原本就存在的死代码,除非用户明确要求。
|
||||||
- 先定义验证方式:执行方案里要写清楚“改哪里、怎么判断改对了”;默认通过代码路径、配置一致性和受影响范围检查进行验证。
|
- 先定义验证方式:执行方案里要写清楚“改哪里、怎么判断改对了”;默认通过代码路径、配置一致性和受影响范围检查进行验证。
|
||||||
- 除非用户明确要求,否则不执行任何 `mvn` 编译、打包、测试或其他构建命令。
|
- 除非用户明确要求,否则不执行任何 `mvn` 编译、打包、测试或其他构建命令。
|
||||||
|
- 所有导出或生成文件名统一追加日期,格式为“原文件名 + `_` + `yyyyMMdd` + 原扩展名”,例如 `暂态事件列表_20260516.xlsx`;新增下载导出、临时生成或落盘保存文件时,优先复用目标模块已有文件名工具,不要在业务代码中散落手写日期拼接。
|
||||||
|
|
||||||
## 项目结构与模块划分
|
## 项目结构与模块划分
|
||||||
`CN_Tool` 是一个 Maven 多模块后端项目,根目录的 [`pom.xml`](C:/code/gitea/cn_tool/CN_Tool/pom.xml) 聚合了 `entrance`、`system`、`user`、`detection` 和 `tools`。
|
`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 与文档说明。
|
||||||
- SQL 脚本应放在目标模块的 `src/main/resources/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` 初始化脚本。
|
||||||
|
|
||||||
## 注释与编码
|
## 注释与编码
|
||||||
- 新增或修改代码时,关键字段、关键分支、关键约束和非直观实现应补充简洁中文注释。
|
- 新增或修改代码时,关键字段、关键分支、关键约束和非直观实现应补充简洁中文注释。
|
||||||
|
|||||||
15
README.md
15
README.md
@@ -15,6 +15,7 @@ CN_Tool 是一个基于 Spring Boot 的多模块后端聚合工程,当前仓
|
|||||||
- `entrance`
|
- `entrance`
|
||||||
- `system`
|
- `system`
|
||||||
- `systemmonitor`
|
- `systemmonitor`
|
||||||
|
- `system-ops`
|
||||||
- `user`
|
- `user`
|
||||||
- `detection`
|
- `detection`
|
||||||
- `tools`
|
- `tools`
|
||||||
@@ -23,12 +24,18 @@ CN_Tool 是一个基于 Spring Boot 的多模块后端聚合工程,当前仓
|
|||||||
|
|
||||||
- `disk-monitor`
|
- `disk-monitor`
|
||||||
|
|
||||||
|
其中 `system-ops` 当前包含:
|
||||||
|
|
||||||
|
- `dbms`
|
||||||
|
- `deploy`
|
||||||
|
|
||||||
其中 `tools` 当前包含:
|
其中 `tools` 当前包含:
|
||||||
|
|
||||||
- `activate-tool`
|
- `activate-tool`
|
||||||
- `add-data`
|
- `add-data`
|
||||||
- `add-ledger`
|
- `add-ledger`
|
||||||
- `mms-mapping`
|
- `mms-mapping`
|
||||||
|
- `parse-pqdif`
|
||||||
- `wave-tool`
|
- `wave-tool`
|
||||||
|
|
||||||
## 启动入口
|
## 启动入口
|
||||||
@@ -37,7 +44,7 @@ CN_Tool 是一个基于 Spring Boot 的多模块后端聚合工程,当前仓
|
|||||||
|
|
||||||
- `entrance/src/main/java/com/njcn/gather/EntranceApplication.java`
|
- `entrance/src/main/java/com/njcn/gather/EntranceApplication.java`
|
||||||
|
|
||||||
`entrance` 模块聚合了 `system`、`disk-monitor`、`user`、`detection`、`activate-tool`、`add-data`、`add-ledger`、`wave-tool`、`mms-mapping`,是当前运行时主入口。
|
`entrance` 模块聚合了 `system`、`disk-monitor`、`dbms`、`deploy`、`user`、`detection`、`activate-tool`、`add-data`、`add-ledger`、`wave-tool`、`mms-mapping`、`parse-pqdif`,是当前运行时主入口。
|
||||||
|
|
||||||
## 技术基线
|
## 技术基线
|
||||||
|
|
||||||
@@ -80,6 +87,10 @@ P0 已补齐基线文档,建议按以下顺序阅读:
|
|||||||
- 负责字典、日志、系统配置、注册资源相关能力
|
- 负责字典、日志、系统配置、注册资源相关能力
|
||||||
- `systemmonitor/disk-monitor`
|
- `systemmonitor/disk-monitor`
|
||||||
- 负责磁盘监控相关能力的独立扩展实现
|
- 负责磁盘监控相关能力的独立扩展实现
|
||||||
|
- `system-ops/dbms`
|
||||||
|
- 负责系统运维下数据库监控基础入口
|
||||||
|
- `system-ops/deploy`
|
||||||
|
- 负责系统运维下系统部署基础入口
|
||||||
- `detection`
|
- `detection`
|
||||||
- 当前以通信基础设施为主,包含 WebSocket / Netty 相关组件
|
- 当前以通信基础设施为主,包含 WebSocket / Netty 相关组件
|
||||||
- `tools/activate-tool`
|
- `tools/activate-tool`
|
||||||
@@ -90,6 +101,8 @@ P0 已补齐基线文档,建议按以下顺序阅读:
|
|||||||
- 当前为数据台账工具预留空模块
|
- 当前为数据台账工具预留空模块
|
||||||
- `tools/mms-mapping`
|
- `tools/mms-mapping`
|
||||||
- 负责 ICD 文件解析与 MMS 映射数据生成能力
|
- 负责 ICD 文件解析与 MMS 映射数据生成能力
|
||||||
|
- `tools/parse-pqdif`
|
||||||
|
- 当前为 PQDIF 文件解析能力预留空方法骨架
|
||||||
- `tools/wave-tool`
|
- `tools/wave-tool`
|
||||||
- 负责波形文本解析与查看数据组装能力
|
- 负责波形文本解析与查看数据组装能力
|
||||||
|
|
||||||
|
|||||||
1922
docs/superpowers/plans/2026-05-21-deploy-linux-ssh-sftp.md
Normal file
1922
docs/superpowers/plans/2026-05-21-deploy-linux-ssh-sftp.md
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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-生产 │ 上传 下载 新建目录 删除 刷新 │ 用户名 │
|
||||||
|
│ │ │ 端口 │
|
||||||
|
│ │ 文件表格 │ 测试连接 │
|
||||||
|
│ │ │ 打开终端 │
|
||||||
|
├──────────────┴──────────────────────────────┴────────────────┤
|
||||||
|
│ 终端 Tabs:Linux-测试 │
|
||||||
|
│ $ 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` 模块从基础入口一次扩张为完整运维平台。
|
||||||
@@ -21,6 +21,16 @@
|
|||||||
<artifactId>disk-monitor</artifactId>
|
<artifactId>disk-monitor</artifactId>
|
||||||
<version>1.0.0</version>
|
<version>1.0.0</version>
|
||||||
</dependency>
|
</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>
|
<dependency>
|
||||||
<groupId>com.njcn.gather</groupId>
|
<groupId>com.njcn.gather</groupId>
|
||||||
<artifactId>detection</artifactId>
|
<artifactId>detection</artifactId>
|
||||||
@@ -48,6 +58,11 @@
|
|||||||
<artifactId>mms-mapping</artifactId>
|
<artifactId>mms-mapping</artifactId>
|
||||||
<version>1.0.0</version>
|
<version>1.0.0</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.njcn.gather</groupId>
|
||||||
|
<artifactId>parse-pqdif</artifactId>
|
||||||
|
<version>1.0.0</version>
|
||||||
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.njcn.gather</groupId>
|
<groupId>com.njcn.gather</groupId>
|
||||||
<artifactId>add-data</artifactId>
|
<artifactId>add-data</artifactId>
|
||||||
|
|||||||
@@ -48,6 +48,16 @@ socket:
|
|||||||
webSocket:
|
webSocket:
|
||||||
port: 7777
|
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:
|
log:
|
||||||
homeDir: D:\logs
|
homeDir: D:\logs
|
||||||
commonLevel: info
|
commonLevel: info
|
||||||
|
|||||||
@@ -35,5 +35,26 @@
|
|||||||
<artifactId>add-ledger</artifactId>
|
<artifactId>add-ledger</artifactId>
|
||||||
<version>1.0.0</version>
|
<version>1.0.0</version>
|
||||||
</dependency>
|
</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>
|
</dependencies>
|
||||||
</project>
|
</project>
|
||||||
|
|||||||
@@ -9,11 +9,11 @@ import java.time.LocalDateTime;
|
|||||||
import java.time.format.DateTimeFormatter;
|
import java.time.format.DateTimeFormatter;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 暂态事件时间字段按秒输出,避免接口响应携带毫秒。
|
* 暂态事件时间字段按毫秒输出,保持与数据库 datetime(3) 精度一致。
|
||||||
*/
|
*/
|
||||||
public class EventSecondTimeSerializer extends JsonSerializer<LocalDateTime> {
|
public class EventMillisecondTimeSerializer extends JsonSerializer<LocalDateTime> {
|
||||||
|
|
||||||
private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS");
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void serialize(LocalDateTime value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
|
public void serialize(LocalDateTime value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
|
||||||
@@ -8,8 +8,10 @@ import com.njcn.common.pojo.enums.response.CommonResponseEnum;
|
|||||||
import com.njcn.common.pojo.response.HttpResult;
|
import com.njcn.common.pojo.response.HttpResult;
|
||||||
import com.njcn.common.utils.LogUtil;
|
import com.njcn.common.utils.LogUtil;
|
||||||
import com.njcn.gather.event.eventlist.pojo.param.EventListQueryParam;
|
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.pojo.vo.EventListVO;
|
||||||
import com.njcn.gather.event.eventlist.service.EventListService;
|
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.controller.BaseController;
|
||||||
import com.njcn.web.utils.HttpResultUtil;
|
import com.njcn.web.utils.HttpResultUtil;
|
||||||
import io.swagger.annotations.Api;
|
import io.swagger.annotations.Api;
|
||||||
@@ -64,4 +66,21 @@ public class EventListController extends BaseController {
|
|||||||
public void exportTransientEvents(@RequestBody EventListQueryParam param) {
|
public void exportTransientEvents(@RequestBody EventListQueryParam param) {
|
||||||
eventListService.exportTransientEvents(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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,4 +18,6 @@ public interface EventListMapper extends BaseMapper<MpEventDetailPO> {
|
|||||||
List<MpEventDetailPO> selectTransientExportList(@Param("param") EventListQueryParam param, @Param("limit") Integer limit);
|
List<MpEventDetailPO> selectTransientExportList(@Param("param") EventListQueryParam param, @Param("limit") Integer limit);
|
||||||
|
|
||||||
MpEventDetailPO selectTransientDetail(@Param("eventId") String eventId);
|
MpEventDetailPO selectTransientDetail(@Param("eventId") String eventId);
|
||||||
|
|
||||||
|
List<MpEventDetailPO> selectTransientDetailsByIds(@Param("eventIds") List<String> eventIds);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,9 +47,6 @@
|
|||||||
<if test="param.phase != null and param.phase != ''">
|
<if test="param.phase != null and param.phase != ''">
|
||||||
AND phase = #{param.phase}
|
AND phase = #{param.phase}
|
||||||
</if>
|
</if>
|
||||||
<if test="param.eventDescribe != null and param.eventDescribe != ''">
|
|
||||||
AND event_describe LIKE CONCAT('%', #{param.eventDescribe}, '%')
|
|
||||||
</if>
|
|
||||||
<if test="param.durationMin != null">
|
<if test="param.durationMin != null">
|
||||||
AND duration >= #{param.durationMin}
|
AND duration >= #{param.durationMin}
|
||||||
</if>
|
</if>
|
||||||
@@ -101,4 +98,14 @@
|
|||||||
WHERE event_id = #{eventId}
|
WHERE event_id = #{eventId}
|
||||||
LIMIT 1
|
LIMIT 1
|
||||||
</select>
|
</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>
|
</mapper>
|
||||||
|
|||||||
@@ -18,10 +18,10 @@ import java.util.List;
|
|||||||
@ApiModel("暂态事件列表查询参数")
|
@ApiModel("暂态事件列表查询参数")
|
||||||
public class EventListQueryParam extends BaseParam {
|
public class EventListQueryParam extends BaseParam {
|
||||||
|
|
||||||
@ApiModelProperty("发生时刻开始,格式 yyyy-MM-dd HH:mm:ss")
|
@ApiModelProperty("发生时刻开始,格式 yyyy-MM-dd HH:mm:ss.SSS,兼容 yyyy-MM-dd HH:mm:ss")
|
||||||
private String startTimeStart;
|
private String startTimeStart;
|
||||||
|
|
||||||
@ApiModelProperty("发生时刻结束,格式 yyyy-MM-dd HH:mm:ss")
|
@ApiModelProperty("发生时刻结束,格式 yyyy-MM-dd HH:mm:ss.SSS,兼容 yyyy-MM-dd HH:mm:ss")
|
||||||
private String startTimeEnd;
|
private String startTimeEnd;
|
||||||
|
|
||||||
@ApiModelProperty("事件类型")
|
@ApiModelProperty("事件类型")
|
||||||
@@ -30,9 +30,6 @@ public class EventListQueryParam extends BaseParam {
|
|||||||
@ApiModelProperty("相别")
|
@ApiModelProperty("相别")
|
||||||
private String phase;
|
private String phase;
|
||||||
|
|
||||||
@ApiModelProperty("事件描述关键字")
|
|
||||||
private String eventDescribe;
|
|
||||||
|
|
||||||
@ApiModelProperty("持续时间下限,单位秒")
|
@ApiModelProperty("持续时间下限,单位秒")
|
||||||
private BigDecimal durationMin;
|
private BigDecimal durationMin;
|
||||||
|
|
||||||
|
|||||||
@@ -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>();
|
||||||
|
}
|
||||||
@@ -2,11 +2,12 @@ package com.njcn.gather.event.eventlist.pojo.vo;
|
|||||||
|
|
||||||
import cn.afterturn.easypoi.excel.annotation.Excel;
|
import cn.afterturn.easypoi.excel.annotation.Excel;
|
||||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
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.JsonDeserialize;
|
||||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||||
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
|
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
|
||||||
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
|
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
|
||||||
import com.njcn.gather.event.eventlist.config.EventSecondTimeSerializer;
|
import com.njcn.gather.event.eventlist.config.EventMillisecondTimeSerializer;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
@@ -25,10 +26,11 @@ public class EventListVO implements Serializable {
|
|||||||
|
|
||||||
private String measurementPointId;
|
private String measurementPointId;
|
||||||
|
|
||||||
private String eventType;
|
@Excel(name = "发生时刻", width = 25, exportFormat = "yyyy-MM-dd HH:mm:ss.SSS")
|
||||||
|
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss.SSS")
|
||||||
@Excel(name = "设备名称", width = 25)
|
@JsonDeserialize(using = LocalDateTimeDeserializer.class)
|
||||||
private String equipmentName;
|
@JsonSerialize(using = EventMillisecondTimeSerializer.class)
|
||||||
|
private LocalDateTime startTime;
|
||||||
|
|
||||||
@Excel(name = "工程名称", width = 25)
|
@Excel(name = "工程名称", width = 25)
|
||||||
private String engineeringName;
|
private String engineeringName;
|
||||||
@@ -36,29 +38,32 @@ public class EventListVO implements Serializable {
|
|||||||
@Excel(name = "项目名称", width = 25)
|
@Excel(name = "项目名称", width = 25)
|
||||||
private String projectName;
|
private String projectName;
|
||||||
|
|
||||||
@Excel(name = "发生时刻", width = 25, exportFormat = "yyyy-MM-dd HH:mm:ss")
|
@Excel(name = "设备名称", width = 25)
|
||||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
private String equipmentName;
|
||||||
@JsonDeserialize(using = LocalDateTimeDeserializer.class)
|
|
||||||
@JsonSerialize(using = EventSecondTimeSerializer.class)
|
private String mac;
|
||||||
private LocalDateTime startTime;
|
|
||||||
|
|
||||||
@Excel(name = "监测点名称", width = 25)
|
@Excel(name = "监测点名称", width = 25)
|
||||||
private String lineName;
|
private String lineName;
|
||||||
|
|
||||||
@Excel(name = "事件描述", width = 25)
|
@Excel(name = "暂降/暂升幅值(%)", width = 20)
|
||||||
private String eventDescribe;
|
private BigDecimal featureAmplitude;
|
||||||
|
|
||||||
@Excel(name = "事件发生位置", width = 18)
|
|
||||||
private String sagsource;
|
|
||||||
|
|
||||||
@Excel(name = "相别", width = 15)
|
|
||||||
private String phase;
|
|
||||||
|
|
||||||
@Excel(name = "持续时间(s)", width = 18)
|
@Excel(name = "持续时间(s)", width = 18)
|
||||||
private BigDecimal duration;
|
private BigDecimal duration;
|
||||||
|
|
||||||
@Excel(name = "暂降/暂升幅值(%)", width = 20)
|
@Excel(name = "事件类型", width = 18)
|
||||||
private BigDecimal featureAmplitude;
|
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 String wavePath;
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,9 @@ package com.njcn.gather.event.eventlist.service;
|
|||||||
|
|
||||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
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.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.pojo.vo.EventListVO;
|
||||||
|
import com.njcn.gather.tool.wave.pojo.vo.WaveComtradeResultVO;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 事件列表服务。
|
* 事件列表服务。
|
||||||
@@ -14,4 +16,8 @@ public interface EventListService {
|
|||||||
EventListVO getTransientEventDetail(String eventId);
|
EventListVO getTransientEventDetail(String eventId);
|
||||||
|
|
||||||
void exportTransientEvents(EventListQueryParam param);
|
void exportTransientEvents(EventListQueryParam param);
|
||||||
|
|
||||||
|
WaveComtradeResultVO getTransientEventWave(String eventId);
|
||||||
|
|
||||||
|
void exportTransientEventWaves(EventWaveExportParam param);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,30 +1,61 @@
|
|||||||
package com.njcn.gather.event.eventlist.service.impl;
|
package com.njcn.gather.event.eventlist.service.impl;
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
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.enums.response.CommonResponseEnum;
|
||||||
import com.njcn.common.pojo.exception.BusinessException;
|
import com.njcn.common.pojo.exception.BusinessException;
|
||||||
import com.njcn.gather.event.eventlist.mapper.EventListMapper;
|
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.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.po.MpEventDetailPO;
|
||||||
import com.njcn.gather.event.eventlist.pojo.vo.EventListVO;
|
import com.njcn.gather.event.eventlist.pojo.vo.EventListVO;
|
||||||
import com.njcn.gather.event.eventlist.service.EventListService;
|
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.param.AddLedgerLinePathQueryParam;
|
||||||
import com.njcn.gather.tool.addledger.pojo.vo.AddLedgerLinePathVO;
|
import com.njcn.gather.tool.addledger.pojo.vo.AddLedgerLinePathVO;
|
||||||
import com.njcn.gather.tool.addledger.service.AddLedgerService;
|
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.factory.PageFactory;
|
||||||
import com.njcn.web.utils.ExcelUtil;
|
import com.njcn.web.utils.ExcelUtil;
|
||||||
|
import com.njcn.web.utils.HttpServletUtil;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
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 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.math.BigDecimal;
|
||||||
|
import java.net.URLEncoder;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.time.format.DateTimeFormatter;
|
import java.time.format.DateTimeFormatter;
|
||||||
import java.time.format.DateTimeParseException;
|
import java.time.format.DateTimeParseException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.zip.ZipEntry;
|
||||||
|
import java.util.zip.ZipOutputStream;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 事件列表服务实现。
|
* 事件列表服务实现。
|
||||||
@@ -37,8 +68,12 @@ public class EventListServiceImpl implements EventListService {
|
|||||||
private static final int LEDGER_LINE_QUERY_LIMIT = 1000;
|
private static final int LEDGER_LINE_QUERY_LIMIT = 1000;
|
||||||
private static final int EVENT_LINE_ID_QUERY_LIMIT = 1000;
|
private static final int EVENT_LINE_ID_QUERY_LIMIT = 1000;
|
||||||
private static final int EXPORT_LIMIT = 5000;
|
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 EMPTY_TEXT = "-";
|
||||||
private static final DateTimeFormatter OUTPUT_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
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[]{
|
private static final DateTimeFormatter[] INPUT_FORMATTERS = new DateTimeFormatter[]{
|
||||||
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"),
|
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"),
|
||||||
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS"),
|
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS"),
|
||||||
@@ -48,6 +83,10 @@ public class EventListServiceImpl implements EventListService {
|
|||||||
|
|
||||||
private final EventListMapper eventListMapper;
|
private final EventListMapper eventListMapper;
|
||||||
private final AddLedgerService addLedgerService;
|
private final AddLedgerService addLedgerService;
|
||||||
|
private final ISysConfigService sysConfigService;
|
||||||
|
private final IDictTypeService dictTypeService;
|
||||||
|
private final IDictDataService dictDataService;
|
||||||
|
private final WaveService waveService;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Page<EventListVO> pageTransientEvents(EventListQueryParam param) {
|
public Page<EventListVO> pageTransientEvents(EventListQueryParam param) {
|
||||||
@@ -89,10 +128,298 @@ public class EventListServiceImpl implements EventListService {
|
|||||||
throw new BusinessException(CommonResponseEnum.FAIL, "导出数据超过 5000 条,请缩小查询条件");
|
throw new BusinessException(CommonResponseEnum.FAIL, "导出数据超过 5000 条,请缩小查询条件");
|
||||||
}
|
}
|
||||||
exportRecords = buildEventList(eventDetails);
|
exportRecords = buildEventList(eventDetails);
|
||||||
|
fillExportEventTypeNames(exportRecords);
|
||||||
} else {
|
} else {
|
||||||
exportRecords = Collections.emptyList();
|
exportRecords = Collections.emptyList();
|
||||||
}
|
}
|
||||||
ExcelUtil.exportExcel("暂态事件列表.xlsx", "暂态事件列表", EventListVO.class, exportRecords);
|
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) {
|
private List<EventListVO> buildEventList(List<MpEventDetailPO> eventDetails) {
|
||||||
@@ -121,15 +448,16 @@ public class EventListServiceImpl implements EventListService {
|
|||||||
vo.setMeasurementPointId(eventDetail.getMeasurementPointId());
|
vo.setMeasurementPointId(eventDetail.getMeasurementPointId());
|
||||||
vo.setEventType(eventDetail.getEventType());
|
vo.setEventType(eventDetail.getEventType());
|
||||||
vo.setEquipmentName(linePath == null ? EMPTY_TEXT : defaultText(linePath.getEquipmentName()));
|
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.setEngineeringName(linePath == null ? EMPTY_TEXT : defaultText(linePath.getEngineeringName()));
|
||||||
vo.setProjectName(linePath == null ? EMPTY_TEXT : defaultText(linePath.getProjectName()));
|
vo.setProjectName(linePath == null ? EMPTY_TEXT : defaultText(linePath.getProjectName()));
|
||||||
vo.setStartTime(eventDetail.getStartTime());
|
vo.setStartTime(eventDetail.getStartTime());
|
||||||
vo.setLineName(linePath == null ? EMPTY_TEXT : defaultText(linePath.getLineName()));
|
vo.setLineName(linePath == null ? EMPTY_TEXT : defaultText(linePath.getLineName()));
|
||||||
vo.setEventDescribe(defaultText(eventDetail.getEventDescribe(), eventDetail.getEventType()));
|
vo.setEventDescribe(trimToNull(eventDetail.getEventDescribe()));
|
||||||
vo.setSagsource(defaultText(eventDetail.getSagsource()));
|
vo.setSagsource(defaultText(eventDetail.getSagsource()));
|
||||||
vo.setPhase(defaultText(eventDetail.getPhase()));
|
vo.setPhase(defaultText(eventDetail.getPhase()));
|
||||||
vo.setDuration(eventDetail.getDuration());
|
vo.setDuration(eventDetail.getDuration());
|
||||||
vo.setFeatureAmplitude(eventDetail.getFeatureAmplitude());
|
vo.setFeatureAmplitude(toPercent(eventDetail.getFeatureAmplitude()));
|
||||||
vo.setWavePath(eventDetail.getWavePath());
|
vo.setWavePath(eventDetail.getWavePath());
|
||||||
vo.setFileFlag(eventDetail.getFileFlag());
|
vo.setFileFlag(eventDetail.getFileFlag());
|
||||||
vo.setDealFlag(eventDetail.getDealFlag());
|
vo.setDealFlag(eventDetail.getDealFlag());
|
||||||
@@ -155,11 +483,12 @@ public class EventListServiceImpl implements EventListService {
|
|||||||
validateRange(queryParam.getFeatureAmplitudeMin(), queryParam.getFeatureAmplitudeMax(), "幅值下限不能大于上限");
|
validateRange(queryParam.getFeatureAmplitudeMin(), queryParam.getFeatureAmplitudeMax(), "幅值下限不能大于上限");
|
||||||
validateFlag(queryParam.getFileFlag(), "波形文件状态只能是 0 或 1");
|
validateFlag(queryParam.getFileFlag(), "波形文件状态只能是 0 或 1");
|
||||||
validateDealFlag(queryParam.getDealFlag());
|
validateDealFlag(queryParam.getDealFlag());
|
||||||
|
queryParam.setFeatureAmplitudeMin(fromPercent(queryParam.getFeatureAmplitudeMin()));
|
||||||
|
queryParam.setFeatureAmplitudeMax(fromPercent(queryParam.getFeatureAmplitudeMax()));
|
||||||
queryParam.setStartTimeStart(OUTPUT_FORMATTER.format(startTime));
|
queryParam.setStartTimeStart(OUTPUT_FORMATTER.format(startTime));
|
||||||
queryParam.setStartTimeEnd(OUTPUT_FORMATTER.format(endTime));
|
queryParam.setStartTimeEnd(OUTPUT_FORMATTER.format(endTime));
|
||||||
queryParam.setEventType(trimToNull(queryParam.getEventType()));
|
queryParam.setEventType(trimToNull(queryParam.getEventType()));
|
||||||
queryParam.setPhase(trimToNull(queryParam.getPhase()));
|
queryParam.setPhase(trimToNull(queryParam.getPhase()));
|
||||||
queryParam.setEventDescribe(trimToNull(queryParam.getEventDescribe()));
|
|
||||||
queryParam.setEngineeringName(trimToNull(queryParam.getEngineeringName()));
|
queryParam.setEngineeringName(trimToNull(queryParam.getEngineeringName()));
|
||||||
queryParam.setProjectName(trimToNull(queryParam.getProjectName()));
|
queryParam.setProjectName(trimToNull(queryParam.getProjectName()));
|
||||||
queryParam.setEquipmentName(trimToNull(queryParam.getEquipmentName()));
|
queryParam.setEquipmentName(trimToNull(queryParam.getEquipmentName()));
|
||||||
@@ -222,10 +551,10 @@ public class EventListServiceImpl implements EventListService {
|
|||||||
try {
|
try {
|
||||||
return LocalDateTime.parse(text, formatter);
|
return LocalDateTime.parse(text, formatter);
|
||||||
} catch (DateTimeParseException ignored) {
|
} catch (DateTimeParseException ignored) {
|
||||||
// 尝试下一个前端可能传入的时间格式。
|
// 尝试下一种前端可能传入的时间格式。
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
throw new BusinessException(CommonResponseEnum.FAIL, "时间格式不正确,仅支持 yyyy-MM-dd HH:mm:ss");
|
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) {
|
private List<String> normalizeIds(List<String> ids) {
|
||||||
@@ -255,6 +584,59 @@ public class EventListServiceImpl implements EventListService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
private void validateFlag(Integer flag, String message) {
|
||||||
if (flag != null && flag != 0 && flag != 1) {
|
if (flag != null && flag != 0 && flag != 1) {
|
||||||
throw new BusinessException(CommonResponseEnum.FAIL, message);
|
throw new BusinessException(CommonResponseEnum.FAIL, message);
|
||||||
@@ -283,4 +665,79 @@ public class EventListServiceImpl implements EventListService {
|
|||||||
String trimmed = value.trim();
|
String trimmed = value.trim();
|
||||||
return trimmed.isEmpty() ? null : trimmed;
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
1
pom.xml
1
pom.xml
@@ -13,6 +13,7 @@
|
|||||||
<module>entrance</module>
|
<module>entrance</module>
|
||||||
<module>system</module>
|
<module>system</module>
|
||||||
<module>systemmonitor</module>
|
<module>systemmonitor</module>
|
||||||
|
<module>system-ops</module>
|
||||||
<module>user</module>
|
<module>user</module>
|
||||||
<module>detection</module>
|
<module>detection</module>
|
||||||
<module>tools</module>
|
<module>tools</module>
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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> {
|
||||||
|
}
|
||||||
@@ -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> {
|
||||||
|
}
|
||||||
@@ -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> {
|
||||||
|
}
|
||||||
@@ -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> {
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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() {
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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>();
|
||||||
|
}
|
||||||
@@ -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>();
|
||||||
|
}
|
||||||
@@ -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>();
|
||||||
|
}
|
||||||
@@ -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>();
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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>();
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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>();
|
||||||
|
}
|
||||||
@@ -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> {
|
||||||
|
}
|
||||||
@@ -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> {
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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> {
|
||||||
|
}
|
||||||
@@ -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> {
|
||||||
|
}
|
||||||
@@ -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 {
|
||||||
|
}
|
||||||
@@ -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 {
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -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 {
|
||||||
|
}
|
||||||
@@ -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 {
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@ import com.njcn.gather.steady.datavie.config.SteadyInfluxDbProperties;
|
|||||||
import com.njcn.gather.steady.datavie.pojo.bo.SteadyTrendResolvedFieldBO;
|
import com.njcn.gather.steady.datavie.pojo.bo.SteadyTrendResolvedFieldBO;
|
||||||
import com.njcn.gather.steady.datavie.pojo.vo.SteadyTrendPointVO;
|
import com.njcn.gather.steady.datavie.pojo.vo.SteadyTrendPointVO;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
import java.io.BufferedReader;
|
import java.io.BufferedReader;
|
||||||
@@ -29,6 +30,7 @@ import java.util.List;
|
|||||||
/**
|
/**
|
||||||
* 稳态趋势 InfluxDB 查询组件。
|
* 稳态趋势 InfluxDB 查询组件。
|
||||||
*/
|
*/
|
||||||
|
@Slf4j
|
||||||
@Component
|
@Component
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class SteadyInfluxQueryComponent {
|
public class SteadyInfluxQueryComponent {
|
||||||
@@ -40,34 +42,53 @@ public class SteadyInfluxQueryComponent {
|
|||||||
private final SteadyInfluxDbProperties properties;
|
private final SteadyInfluxDbProperties properties;
|
||||||
|
|
||||||
public List<SteadyTrendPointVO> queryTrendPoints(SteadyTrendResolvedFieldBO field, LocalDateTime startTime,
|
public List<SteadyTrendPointVO> queryTrendPoints(SteadyTrendResolvedFieldBO field, LocalDateTime startTime,
|
||||||
LocalDateTime endTime, String bucket, Integer qualityFlag) {
|
LocalDateTime endTime, Integer qualityFlag) {
|
||||||
validateConfig();
|
validateConfig();
|
||||||
String query = buildTrendQuery(field, startTime, endTime, bucket, qualityFlag);
|
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);
|
String body = executeQuery(query);
|
||||||
return parseTrendPoints(body);
|
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,
|
public String buildTrendQuery(SteadyTrendResolvedFieldBO field, LocalDateTime startTime, LocalDateTime endTime,
|
||||||
String bucket, Integer qualityFlag) {
|
Integer qualityFlag) {
|
||||||
StringBuilder sql = new StringBuilder();
|
StringBuilder sql = new StringBuilder();
|
||||||
if (bucket == null || bucket.trim().isEmpty()) {
|
|
||||||
sql.append("SELECT \"").append(field.getField()).append("\" AS \"value\"");
|
sql.append("SELECT \"").append(field.getField()).append("\" AS \"value\"");
|
||||||
} else {
|
|
||||||
sql.append("SELECT mean(\"").append(field.getField()).append("\") AS \"value\"");
|
|
||||||
}
|
|
||||||
sql.append(" FROM \"").append(field.getMeasurement()).append("\"");
|
sql.append(" FROM \"").append(field.getMeasurement()).append("\"");
|
||||||
sql.append(" WHERE time >= '").append(INFLUX_TIME_FORMATTER.format(startTime)).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 time <= '").append(INFLUX_TIME_FORMATTER.format(endTime)).append("'");
|
||||||
sql.append(" AND \"LINEID\" = '").append(escapeTagValue(field.getLineId())).append("'");
|
sql.append(" AND \"line_id\" = '").append(escapeTagValue(field.getLineId())).append("'");
|
||||||
sql.append(" AND \"PHASIC_TYPE\" = '").append(escapeTagValue(field.getPhase())).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) {
|
if (qualityFlag != null) {
|
||||||
sql.append(" AND \"QUALITYFLAG\" = '").append(qualityFlag).append("'");
|
sql.append(" AND \"quality_flag\" = '").append(qualityFlag).append("'");
|
||||||
}
|
}
|
||||||
if (bucket != null && !bucket.trim().isEmpty()) {
|
|
||||||
sql.append(" GROUP BY time(").append(bucket.trim()).append(") fill(none)");
|
|
||||||
} else {
|
|
||||||
sql.append(" ORDER BY time ASC");
|
sql.append(" ORDER BY time ASC");
|
||||||
}
|
|
||||||
return sql.toString();
|
return sql.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -165,6 +186,17 @@ public class SteadyInfluxQueryComponent {
|
|||||||
return value == null ? "" : value.replace("\\", "\\\\").replace("'", "\\'");
|
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) {
|
private String trimRightSlash(String value) {
|
||||||
String text = value.trim();
|
String text = value.trim();
|
||||||
while (text.endsWith("/")) {
|
while (text.endsWith("/")) {
|
||||||
|
|||||||
@@ -6,8 +6,6 @@ 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.SteadyTrendResolvedFieldBO;
|
||||||
import com.njcn.gather.steady.datavie.pojo.bo.SteadyTrendSeriesFieldBO;
|
import com.njcn.gather.steady.datavie.pojo.bo.SteadyTrendSeriesFieldBO;
|
||||||
import com.njcn.gather.steady.datavie.pojo.param.SteadyTrendQueryParam;
|
import com.njcn.gather.steady.datavie.pojo.param.SteadyTrendQueryParam;
|
||||||
import com.njcn.gather.tool.adddata.component.AddDataTableRegistry;
|
|
||||||
import com.njcn.gather.tool.adddata.pojo.bo.AddDataTableDefinition;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
@@ -28,17 +26,15 @@ public class SteadyTrendFieldResolver {
|
|||||||
private static final int MAX_LINE_COUNT = 8;
|
private static final int MAX_LINE_COUNT = 8;
|
||||||
private static final int MAX_INDICATOR_COUNT = 8;
|
private static final int MAX_INDICATOR_COUNT = 8;
|
||||||
private static final int MAX_SERIES_COUNT = 24;
|
private static final int MAX_SERIES_COUNT = 24;
|
||||||
private static final int MAX_HARMONIC_ORDER_COUNT = 6;
|
private static final int MAX_HARMONIC_ORDER_COUNT = 3;
|
||||||
private static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
private static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
||||||
|
|
||||||
private final SteadyTrendIndicatorCatalog indicatorCatalog;
|
private final SteadyTrendIndicatorCatalog indicatorCatalog;
|
||||||
private final AddDataTableRegistry addDataTableRegistry;
|
|
||||||
|
|
||||||
public List<SteadyTrendResolvedFieldBO> resolveFields(SteadyTrendQueryParam param) {
|
public List<SteadyTrendResolvedFieldBO> resolveFields(SteadyTrendQueryParam param) {
|
||||||
validateBasicParam(param);
|
validateBasicParam(param);
|
||||||
List<String> lineIds = normalizeTextList(param.getLineIds());
|
List<String> lineIds = normalizeTextList(param.getLineIds());
|
||||||
List<String> indicatorCodes = normalizeTextList(param.getIndicatorCodes());
|
List<String> indicatorCodes = normalizeTextList(param.getIndicatorCodes());
|
||||||
List<String> requestPhases = normalizeUpperList(param.getPhases());
|
|
||||||
List<String> statTypes = normalizeUpperList(param.getStatTypes());
|
List<String> statTypes = normalizeUpperList(param.getStatTypes());
|
||||||
if (statTypes.isEmpty()) {
|
if (statTypes.isEmpty()) {
|
||||||
statTypes.add("AVG");
|
statTypes.add("AVG");
|
||||||
@@ -47,7 +43,7 @@ public class SteadyTrendFieldResolver {
|
|||||||
for (String lineId : lineIds) {
|
for (String lineId : lineIds) {
|
||||||
for (String indicatorCode : indicatorCodes) {
|
for (String indicatorCode : indicatorCodes) {
|
||||||
SteadyTrendIndicatorDefinitionBO indicator = requireIndicator(indicatorCode);
|
SteadyTrendIndicatorDefinitionBO indicator = requireIndicator(indicatorCode);
|
||||||
List<String> phases = resolvePhases(indicator, requestPhases);
|
List<String> phases = resolvePhases(indicator);
|
||||||
for (String phase : phases) {
|
for (String phase : phases) {
|
||||||
for (String statType : statTypes) {
|
for (String statType : statTypes) {
|
||||||
validateStatType(indicator, statType);
|
validateStatType(indicator, statType);
|
||||||
@@ -57,7 +53,7 @@ public class SteadyTrendFieldResolver {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (result.size() > MAX_SERIES_COUNT) {
|
if (result.size() > MAX_SERIES_COUNT) {
|
||||||
throw fail("趋势曲线数量不能超过 24 条,请缩小监测点、指标、相别或统计类型范围");
|
throw fail("趋势曲线数量不能超过 24 条,请缩小监测点、指标或统计类型范围");
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
@@ -81,9 +77,7 @@ public class SteadyTrendFieldResolver {
|
|||||||
}
|
}
|
||||||
List<SteadyTrendResolvedFieldBO> result = new ArrayList<SteadyTrendResolvedFieldBO>();
|
List<SteadyTrendResolvedFieldBO> result = new ArrayList<SteadyTrendResolvedFieldBO>();
|
||||||
for (SteadyTrendSeriesFieldBO seriesField : indicator.getSeriesFields()) {
|
for (SteadyTrendSeriesFieldBO seriesField : indicator.getSeriesFields()) {
|
||||||
String field = resolveStatField(seriesField.getField(), statType);
|
result.add(buildResolvedField(lineId, indicator, phase, statType, seriesField.getField(), seriesField.getName()));
|
||||||
validateColumn(indicator.getTableName(), field);
|
|
||||||
result.add(buildResolvedField(lineId, indicator, phase, statType, field, seriesField.getName()));
|
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
@@ -95,16 +89,14 @@ public class SteadyTrendFieldResolver {
|
|||||||
throw fail("谐波次数不能为空");
|
throw fail("谐波次数不能为空");
|
||||||
}
|
}
|
||||||
if (orders.size() > MAX_HARMONIC_ORDER_COUNT) {
|
if (orders.size() > MAX_HARMONIC_ORDER_COUNT) {
|
||||||
throw fail("谐波次数最多选择 6 个");
|
throw fail("谐波次数不允许一次展示超过3个");
|
||||||
}
|
}
|
||||||
List<SteadyTrendResolvedFieldBO> result = new ArrayList<SteadyTrendResolvedFieldBO>();
|
List<SteadyTrendResolvedFieldBO> result = new ArrayList<SteadyTrendResolvedFieldBO>();
|
||||||
for (Integer order : orders) {
|
for (Integer order : orders) {
|
||||||
if (order < indicator.getHarmonicOrderStart() || order > indicator.getHarmonicOrderEnd()) {
|
if (order < indicator.getHarmonicOrderStart() || order > indicator.getHarmonicOrderEnd()) {
|
||||||
throw fail("谐波次数只能在 " + indicator.getHarmonicOrderStart() + " 到 " + indicator.getHarmonicOrderEnd() + " 之间");
|
throw fail("谐波次数只能在 " + indicator.getHarmonicOrderStart() + " 到 " + indicator.getHarmonicOrderEnd() + " 之间");
|
||||||
}
|
}
|
||||||
String baseField = indicator.getHarmonicFieldPrefix() + "_" + order;
|
String field = indicator.getHarmonicFieldPrefix() + "_" + order;
|
||||||
String field = resolveStatField(baseField, statType);
|
|
||||||
validateColumn(indicator.getTableName(), field);
|
|
||||||
result.add(buildResolvedField(lineId, indicator, phase, statType, field, order + "次" + indicator.getName()));
|
result.add(buildResolvedField(lineId, indicator, phase, statType, field, order + "次" + indicator.getName()));
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
@@ -165,24 +157,9 @@ public class SteadyTrendFieldResolver {
|
|||||||
return indicator;
|
return indicator;
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<String> resolvePhases(SteadyTrendIndicatorDefinitionBO indicator, List<String> requestPhases) {
|
private List<String> resolvePhases(SteadyTrendIndicatorDefinitionBO indicator) {
|
||||||
if (requestPhases.isEmpty()) {
|
|
||||||
return new ArrayList<String>(indicator.getPhaseCodes());
|
return new ArrayList<String>(indicator.getPhaseCodes());
|
||||||
}
|
}
|
||||||
List<String> result = new ArrayList<String>();
|
|
||||||
for (String phase : requestPhases) {
|
|
||||||
if (!"A".equals(phase) && !"B".equals(phase) && !"C".equals(phase) && !"T".equals(phase)) {
|
|
||||||
throw fail("相别只能是 A、B、C、T");
|
|
||||||
}
|
|
||||||
if (indicator.getPhaseCodes().contains(phase) && !result.contains(phase)) {
|
|
||||||
result.add(phase);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (result.isEmpty()) {
|
|
||||||
throw fail("指标 " + indicator.getIndicatorCode() + " 不支持当前相别");
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void validateStatType(SteadyTrendIndicatorDefinitionBO indicator, String statType) {
|
private void validateStatType(SteadyTrendIndicatorDefinitionBO indicator, String statType) {
|
||||||
if (!"AVG".equals(statType) && !"MAX".equals(statType) && !"MIN".equals(statType) && !"CP95".equals(statType)) {
|
if (!"AVG".equals(statType) && !"MAX".equals(statType) && !"MIN".equals(statType) && !"CP95".equals(statType)) {
|
||||||
@@ -193,25 +170,6 @@ public class SteadyTrendFieldResolver {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private String resolveStatField(String baseField, String statType) {
|
|
||||||
if ("AVG".equals(statType)) {
|
|
||||||
return baseField;
|
|
||||||
}
|
|
||||||
return baseField + "_" + statType;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void validateColumn(String tableName, String field) {
|
|
||||||
AddDataTableDefinition definition;
|
|
||||||
try {
|
|
||||||
definition = addDataTableRegistry.getDefinition(tableName);
|
|
||||||
} catch (IllegalArgumentException ex) {
|
|
||||||
throw fail("稳态数据表不支持:" + tableName);
|
|
||||||
}
|
|
||||||
if (!definition.getColumns().contains(field)) {
|
|
||||||
throw fail("稳态趋势字段不支持:" + tableName + "." + field);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<String> normalizeTextList(List<String> values) {
|
private List<String> normalizeTextList(List<String> values) {
|
||||||
if (values == null || values.isEmpty()) {
|
if (values == null || values.isEmpty()) {
|
||||||
return new ArrayList<String>();
|
return new ArrayList<String>();
|
||||||
|
|||||||
@@ -28,32 +28,31 @@ public class SteadyTrendIndicatorCatalog {
|
|||||||
public SteadyTrendIndicatorCatalog() {
|
public SteadyTrendIndicatorCatalog() {
|
||||||
List<SteadyTrendIndicatorDefinitionBO> result = new ArrayList<SteadyTrendIndicatorDefinitionBO>();
|
List<SteadyTrendIndicatorDefinitionBO> result = new ArrayList<SteadyTrendIndicatorDefinitionBO>();
|
||||||
result.add(indicator("V_RMS", "相电压有效值", "VOLTAGE", "电压趋势", "data_v", ABC_PHASES,
|
result.add(indicator("V_RMS", "相电压有效值", "VOLTAGE", "电压趋势", "data_v", ABC_PHASES,
|
||||||
fields(field("RMS", "相电压有效值")), FULL_STATS, "V"));
|
fields(field("rms", "相电压有效值")), FULL_STATS, "V"));
|
||||||
result.add(indicator("V_LINE_RMS", "线电压有效值", "VOLTAGE", "电压趋势", "data_v", T_PHASE,
|
result.add(indicator("V_LINE_RMS", "线电压有效值", "VOLTAGE", "电压趋势", "data_v", ABC_PHASES,
|
||||||
fields(field("RMSAB", "AB线电压"), field("RMSBC", "BC线电压"), field("RMSCA", "CA线电压")),
|
fields(field("rms_lvr", "线电压有效值")),
|
||||||
FULL_STATS, "V"));
|
FULL_STATS, "V"));
|
||||||
result.add(indicator("FREQ", "频率", "FREQUENCY", "频率趋势", "data_v", T_PHASE,
|
result.add(indicator("FREQ", "频率", "FREQUENCY", "频率趋势", "data_v", T_PHASE,
|
||||||
fields(field("FREQ", "频率")), FULL_STATS, "Hz"));
|
fields(field("freq", "频率")), FULL_STATS, "Hz"));
|
||||||
result.add(indicator("V_THD", "电压总谐波畸变率", "HARMONIC", "谐波趋势", "data_v", ABC_PHASES,
|
result.add(indicator("V_THD", "电压总谐波畸变率", "HARMONIC", "谐波趋势", "data_v", ABC_PHASES,
|
||||||
fields(field("V_THD", "电压总谐波畸变率")), FULL_STATS, "%"));
|
fields(field("v_thd", "电压总谐波畸变率")), FULL_STATS, "%"));
|
||||||
result.add(indicator("I_RMS", "电流有效值", "CURRENT", "电流趋势", "data_i", ABC_PHASES,
|
result.add(indicator("I_RMS", "电流有效值", "CURRENT", "电流趋势", "data_i", ABC_PHASES,
|
||||||
fields(field("RMS", "电流有效值")), FULL_STATS, "A"));
|
fields(field("rms", "电流有效值")), FULL_STATS, "A"));
|
||||||
result.add(indicator("I_THD", "电流总谐波畸变率", "HARMONIC", "谐波趋势", "data_i", ABC_PHASES,
|
result.add(indicator("I_THD", "电流总谐波畸变率", "HARMONIC", "谐波趋势", "data_i", ABC_PHASES,
|
||||||
fields(field("I_THD", "电流总谐波畸变率")), FULL_STATS, "%"));
|
fields(field("i_thd", "电流总谐波畸变率")), FULL_STATS, "%"));
|
||||||
result.add(harmonic("V_HARMONIC", "电压谐波幅值", "data_harmphasic_v", "V", "V"));
|
result.add(harmonic("V_HARMONIC", "电压谐波幅值", "data_harmphasic_v", "v", "V"));
|
||||||
result.add(harmonic("I_HARMONIC", "电流谐波幅值", "data_harmphasic_i", "I", "A"));
|
result.add(harmonic("I_HARMONIC", "电流谐波幅值", "data_harmphasic_i", "i", "A"));
|
||||||
result.add(harmonic("V_HARMONIC_RATE", "电压谐波含有率", "data_harmrate_v", "V", "%"));
|
result.add(harmonic("V_HARMONIC_RATE", "电压谐波含有率", "data_harmrate_v", "v", "%"));
|
||||||
result.add(harmonic("I_HARMONIC_RATE", "电流谐波含有率", "data_harmrate_i", "I", "%"));
|
result.add(harmonic("I_INTER_HARMONIC", "间谐波电流", "data_inharm_i", "i", "A"));
|
||||||
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("P_HARMONIC_POWER", "有功谐波功率", "data_harmpower_p", "P", "kW"));
|
result.add(harmonicPower("Q_HARMONIC_POWER", "无功谐波功率", "data_harmpower_q", "q", "kvar"));
|
||||||
result.add(harmonicPower("Q_HARMONIC_POWER", "无功谐波功率", "data_harmpower_q", "Q", "kvar"));
|
result.add(harmonicPower("S_HARMONIC_POWER", "视在谐波功率", "data_harmpower_s", "s", "kVA"));
|
||||||
result.add(harmonicPower("S_HARMONIC_POWER", "视在谐波功率", "data_harmpower_s", "S", "kVA"));
|
|
||||||
result.add(indicator("FLUC", "电压波动", "FLICKER", "闪变趋势", "data_fluc", T_PHASE,
|
result.add(indicator("FLUC", "电压波动", "FLICKER", "闪变趋势", "data_fluc", T_PHASE,
|
||||||
fields(field("FLUC", "电压波动")), AVG_ONLY, "%"));
|
fields(field("fluc", "电压波动")), AVG_ONLY, "%"));
|
||||||
result.add(indicator("PST", "短时闪变", "FLICKER", "闪变趋势", "data_flicker", T_PHASE,
|
result.add(indicator("PST", "短时闪变", "FLICKER", "闪变趋势", "data_flicker", T_PHASE,
|
||||||
fields(field("PST", "短时闪变")), AVG_ONLY, ""));
|
fields(field("pst", "短时闪变")), AVG_ONLY, ""));
|
||||||
result.add(indicator("PLT", "长时闪变", "FLICKER", "闪变趋势", "data_plt", T_PHASE,
|
result.add(indicator("PLT", "长时闪变", "FLICKER", "闪变趋势", "data_plt", T_PHASE,
|
||||||
fields(field("PLT", "长时闪变")), AVG_ONLY, ""));
|
fields(field("plt", "长时闪变")), AVG_ONLY, ""));
|
||||||
indicators = Collections.unmodifiableList(result);
|
indicators = Collections.unmodifiableList(result);
|
||||||
|
|
||||||
Map<String, SteadyTrendIndicatorDefinitionBO> map = new LinkedHashMap<String, SteadyTrendIndicatorDefinitionBO>();
|
Map<String, SteadyTrendIndicatorDefinitionBO> map = new LinkedHashMap<String, SteadyTrendIndicatorDefinitionBO>();
|
||||||
|
|||||||
@@ -1,13 +1,11 @@
|
|||||||
package com.njcn.gather.steady.datavie.controller;
|
package com.njcn.gather.steady.datavie.controller;
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
|
||||||
import com.njcn.common.pojo.annotation.OperateInfo;
|
import com.njcn.common.pojo.annotation.OperateInfo;
|
||||||
import com.njcn.common.pojo.enums.common.LogEnum;
|
import com.njcn.common.pojo.enums.common.LogEnum;
|
||||||
import com.njcn.common.pojo.enums.response.CommonResponseEnum;
|
import com.njcn.common.pojo.enums.response.CommonResponseEnum;
|
||||||
import com.njcn.common.pojo.response.HttpResult;
|
import com.njcn.common.pojo.response.HttpResult;
|
||||||
import com.njcn.common.utils.LogUtil;
|
import com.njcn.common.utils.LogUtil;
|
||||||
import com.njcn.gather.steady.datavie.pojo.param.SteadyDataViewDetailParam;
|
import com.njcn.gather.steady.datavie.pojo.param.SteadyDataViewDetailParam;
|
||||||
import com.njcn.gather.steady.datavie.pojo.param.SteadyDataViewQueryParam;
|
|
||||||
import com.njcn.gather.steady.datavie.pojo.vo.SteadyDataViewTemplateVO;
|
import com.njcn.gather.steady.datavie.pojo.vo.SteadyDataViewTemplateVO;
|
||||||
import com.njcn.gather.steady.datavie.pojo.vo.SteadyDataViewVO;
|
import com.njcn.gather.steady.datavie.pojo.vo.SteadyDataViewVO;
|
||||||
import com.njcn.gather.steady.datavie.service.SteadyDataViewService;
|
import com.njcn.gather.steady.datavie.service.SteadyDataViewService;
|
||||||
@@ -40,16 +38,6 @@ public class SteadyDataViewController extends BaseController {
|
|||||||
/** 稳态数据查看服务。 */
|
/** 稳态数据查看服务。 */
|
||||||
private final SteadyDataViewService steadyDataViewService;
|
private final SteadyDataViewService steadyDataViewService;
|
||||||
|
|
||||||
@OperateInfo(info = LogEnum.BUSINESS_COMMON)
|
|
||||||
@ApiOperation("分页查询稳态数据")
|
|
||||||
@PostMapping("/page")
|
|
||||||
public HttpResult<Page<SteadyDataViewVO>> pageSteadyData(@RequestBody SteadyDataViewQueryParam param) {
|
|
||||||
String methodDescribe = getMethodDescribe("pageSteadyData");
|
|
||||||
LogUtil.njcnDebug(log, "{},开始分页查询稳态数据,param={}", methodDescribe, param);
|
|
||||||
Page<SteadyDataViewVO> result = steadyDataViewService.pageSteadyData(param);
|
|
||||||
return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, result, methodDescribe);
|
|
||||||
}
|
|
||||||
|
|
||||||
@OperateInfo(info = LogEnum.BUSINESS_COMMON)
|
@OperateInfo(info = LogEnum.BUSINESS_COMMON)
|
||||||
@ApiOperation("查询稳态数据详情")
|
@ApiOperation("查询稳态数据详情")
|
||||||
@PostMapping("/detail")
|
@PostMapping("/detail")
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
package com.njcn.gather.steady.datavie.mapper;
|
package com.njcn.gather.steady.datavie.mapper;
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
|
||||||
import com.njcn.gather.steady.datavie.pojo.param.SteadyDataViewQueryParam;
|
|
||||||
import org.apache.ibatis.annotations.Param;
|
import org.apache.ibatis.annotations.Param;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -12,11 +10,6 @@ import java.util.Map;
|
|||||||
*/
|
*/
|
||||||
public interface SteadyDataViewMapper {
|
public interface SteadyDataViewMapper {
|
||||||
|
|
||||||
Page<Map<String, Object>> selectSteadyPage(Page<Map<String, Object>> page,
|
|
||||||
@Param("tableName") String tableName,
|
|
||||||
@Param("columns") List<String> columns,
|
|
||||||
@Param("param") SteadyDataViewQueryParam param);
|
|
||||||
|
|
||||||
Map<String, Object> selectSteadyDetail(@Param("tableName") String tableName,
|
Map<String, Object> selectSteadyDetail(@Param("tableName") String tableName,
|
||||||
@Param("columns") List<String> columns,
|
@Param("columns") List<String> columns,
|
||||||
@Param("lineId") String lineId,
|
@Param("lineId") String lineId,
|
||||||
|
|||||||
@@ -3,39 +3,6 @@
|
|||||||
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||||
<mapper namespace="com.njcn.gather.steady.datavie.mapper.SteadyDataViewMapper">
|
<mapper namespace="com.njcn.gather.steady.datavie.mapper.SteadyDataViewMapper">
|
||||||
|
|
||||||
<sql id="SteadyDataWhere">
|
|
||||||
<where>
|
|
||||||
<if test="param.timeStart != null and param.timeStart != ''">
|
|
||||||
AND TIMEID >= #{param.timeStart}
|
|
||||||
</if>
|
|
||||||
<if test="param.timeEnd != null and param.timeEnd != ''">
|
|
||||||
AND TIMEID <= #{param.timeEnd}
|
|
||||||
</if>
|
|
||||||
<if test="param.phasicType != null and param.phasicType != ''">
|
|
||||||
AND PHASIC_TYPE = #{param.phasicType}
|
|
||||||
</if>
|
|
||||||
<if test="param.qualityFlag != null">
|
|
||||||
AND QUALITYFLAG = #{param.qualityFlag}
|
|
||||||
</if>
|
|
||||||
<if test="param.lineIds != null and param.lineIds.size() > 0">
|
|
||||||
AND LINEID IN
|
|
||||||
<foreach collection="param.lineIds" item="lineId" open="(" separator="," close=")">
|
|
||||||
#{lineId}
|
|
||||||
</foreach>
|
|
||||||
</if>
|
|
||||||
</where>
|
|
||||||
</sql>
|
|
||||||
|
|
||||||
<select id="selectSteadyPage" resultType="java.util.LinkedHashMap">
|
|
||||||
SELECT
|
|
||||||
<foreach collection="columns" item="column" separator=",">
|
|
||||||
`${column}`
|
|
||||||
</foreach>
|
|
||||||
FROM `${tableName}`
|
|
||||||
<include refid="SteadyDataWhere"/>
|
|
||||||
ORDER BY TIMEID DESC, LINEID ASC, PHASIC_TYPE ASC
|
|
||||||
</select>
|
|
||||||
|
|
||||||
<select id="selectSteadyDetail" resultType="java.util.LinkedHashMap">
|
<select id="selectSteadyDetail" resultType="java.util.LinkedHashMap">
|
||||||
SELECT
|
SELECT
|
||||||
<foreach collection="columns" item="column" separator=",">
|
<foreach collection="columns" item="column" separator=",">
|
||||||
|
|||||||
@@ -1,49 +0,0 @@
|
|||||||
package com.njcn.gather.steady.datavie.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.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 稳态数据查看查询参数。
|
|
||||||
*/
|
|
||||||
@Data
|
|
||||||
@EqualsAndHashCode(callSuper = true)
|
|
||||||
@ApiModel("稳态数据查看查询参数")
|
|
||||||
public class SteadyDataViewQueryParam extends BaseParam {
|
|
||||||
|
|
||||||
@ApiModelProperty("表名,对应 add-data 模板表名")
|
|
||||||
private String tableName;
|
|
||||||
|
|
||||||
@ApiModelProperty("时间开始,格式 yyyy-MM-dd HH:mm:ss")
|
|
||||||
private String timeStart;
|
|
||||||
|
|
||||||
@ApiModelProperty("时间结束,格式 yyyy-MM-dd HH:mm:ss")
|
|
||||||
private String timeEnd;
|
|
||||||
|
|
||||||
@ApiModelProperty("相别:A/B/C/T")
|
|
||||||
private String phasicType;
|
|
||||||
|
|
||||||
@ApiModelProperty("质量标识")
|
|
||||||
private Integer qualityFlag;
|
|
||||||
|
|
||||||
@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;
|
|
||||||
}
|
|
||||||
@@ -23,21 +23,15 @@ public class SteadyTrendQueryParam {
|
|||||||
@ApiModelProperty("统计类型:AVG/MAX/MIN/CP95")
|
@ApiModelProperty("统计类型:AVG/MAX/MIN/CP95")
|
||||||
private List<String> statTypes = new ArrayList<String>();
|
private List<String> statTypes = new ArrayList<String>();
|
||||||
|
|
||||||
@ApiModelProperty("相别:A/B/C/T")
|
|
||||||
private List<String> phases = new ArrayList<String>();
|
|
||||||
|
|
||||||
@ApiModelProperty("开始时间,格式 yyyy-MM-dd HH:mm:ss")
|
@ApiModelProperty("开始时间,格式 yyyy-MM-dd HH:mm:ss")
|
||||||
private String timeStart;
|
private String timeStart;
|
||||||
|
|
||||||
@ApiModelProperty("结束时间,格式 yyyy-MM-dd HH:mm:ss")
|
@ApiModelProperty("结束时间,格式 yyyy-MM-dd HH:mm:ss")
|
||||||
private String timeEnd;
|
private String timeEnd;
|
||||||
|
|
||||||
@ApiModelProperty("分桶粒度,如 1m、5m、10m、30m、1h")
|
|
||||||
private String bucket;
|
|
||||||
|
|
||||||
@ApiModelProperty("质量标识")
|
@ApiModelProperty("质量标识")
|
||||||
private Integer qualityFlag;
|
private Integer qualityFlag;
|
||||||
|
|
||||||
@ApiModelProperty("谐波次数,谐波指标必填,最多 6 个")
|
@ApiModelProperty("谐波次数,谐波指标必填,默认最多展示 3 个")
|
||||||
private List<Integer> harmonicOrders = new ArrayList<Integer>();
|
private List<Integer> harmonicOrders = new ArrayList<Integer>();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,8 +18,6 @@ public class SteadyTrendQueryVO implements Serializable {
|
|||||||
|
|
||||||
private Boolean sampled;
|
private Boolean sampled;
|
||||||
|
|
||||||
private String bucket;
|
|
||||||
|
|
||||||
private Integer sourcePointCount;
|
private Integer sourcePointCount;
|
||||||
|
|
||||||
private Integer displayPointCount;
|
private Integer displayPointCount;
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
package com.njcn.gather.steady.datavie.service;
|
package com.njcn.gather.steady.datavie.service;
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
|
||||||
import com.njcn.gather.steady.datavie.pojo.param.SteadyDataViewDetailParam;
|
import com.njcn.gather.steady.datavie.pojo.param.SteadyDataViewDetailParam;
|
||||||
import com.njcn.gather.steady.datavie.pojo.param.SteadyDataViewQueryParam;
|
|
||||||
import com.njcn.gather.steady.datavie.pojo.vo.SteadyDataViewTemplateVO;
|
import com.njcn.gather.steady.datavie.pojo.vo.SteadyDataViewTemplateVO;
|
||||||
import com.njcn.gather.steady.datavie.pojo.vo.SteadyDataViewVO;
|
import com.njcn.gather.steady.datavie.pojo.vo.SteadyDataViewVO;
|
||||||
|
|
||||||
@@ -13,8 +11,6 @@ import java.util.List;
|
|||||||
*/
|
*/
|
||||||
public interface SteadyDataViewService {
|
public interface SteadyDataViewService {
|
||||||
|
|
||||||
Page<SteadyDataViewVO> pageSteadyData(SteadyDataViewQueryParam param);
|
|
||||||
|
|
||||||
SteadyDataViewVO getSteadyDataDetail(SteadyDataViewDetailParam param);
|
SteadyDataViewVO getSteadyDataDetail(SteadyDataViewDetailParam param);
|
||||||
|
|
||||||
List<SteadyDataViewTemplateVO> listTemplates();
|
List<SteadyDataViewTemplateVO> listTemplates();
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
package com.njcn.gather.steady.datavie.service.impl;
|
package com.njcn.gather.steady.datavie.service.impl;
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
|
||||||
import com.njcn.common.pojo.enums.response.CommonResponseEnum;
|
import com.njcn.common.pojo.enums.response.CommonResponseEnum;
|
||||||
import com.njcn.common.pojo.exception.BusinessException;
|
import com.njcn.common.pojo.exception.BusinessException;
|
||||||
import com.njcn.gather.steady.datavie.mapper.SteadyDataViewMapper;
|
import com.njcn.gather.steady.datavie.mapper.SteadyDataViewMapper;
|
||||||
import com.njcn.gather.steady.datavie.pojo.param.SteadyDataViewDetailParam;
|
import com.njcn.gather.steady.datavie.pojo.param.SteadyDataViewDetailParam;
|
||||||
import com.njcn.gather.steady.datavie.pojo.param.SteadyDataViewQueryParam;
|
|
||||||
import com.njcn.gather.steady.datavie.pojo.vo.SteadyDataViewTemplateVO;
|
import com.njcn.gather.steady.datavie.pojo.vo.SteadyDataViewTemplateVO;
|
||||||
import com.njcn.gather.steady.datavie.pojo.vo.SteadyDataViewVO;
|
import com.njcn.gather.steady.datavie.pojo.vo.SteadyDataViewVO;
|
||||||
import com.njcn.gather.steady.datavie.service.SteadyDataViewService;
|
import com.njcn.gather.steady.datavie.service.SteadyDataViewService;
|
||||||
@@ -13,12 +11,9 @@ import com.njcn.gather.tool.adddata.component.AddDataTableRegistry;
|
|||||||
import com.njcn.gather.tool.adddata.component.AddDataTemplateRegistry;
|
import com.njcn.gather.tool.adddata.component.AddDataTemplateRegistry;
|
||||||
import com.njcn.gather.tool.adddata.pojo.bo.AddDataTableDefinition;
|
import com.njcn.gather.tool.adddata.pojo.bo.AddDataTableDefinition;
|
||||||
import com.njcn.gather.tool.adddata.pojo.vo.AddDataTemplateVO;
|
import com.njcn.gather.tool.adddata.pojo.vo.AddDataTemplateVO;
|
||||||
import com.njcn.gather.tool.addledger.pojo.param.AddLedgerLinePathQueryParam;
|
|
||||||
import com.njcn.gather.tool.addledger.pojo.vo.AddLedgerLinePathVO;
|
import com.njcn.gather.tool.addledger.pojo.vo.AddLedgerLinePathVO;
|
||||||
import com.njcn.gather.tool.addledger.service.AddLedgerService;
|
import com.njcn.gather.tool.addledger.service.AddLedgerService;
|
||||||
import com.njcn.web.factory.PageFactory;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
import java.sql.Timestamp;
|
import java.sql.Timestamp;
|
||||||
@@ -34,13 +29,10 @@ import java.util.Map;
|
|||||||
/**
|
/**
|
||||||
* 稳态数据查看服务实现。
|
* 稳态数据查看服务实现。
|
||||||
*/
|
*/
|
||||||
@Slf4j
|
|
||||||
@Service
|
@Service
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class SteadyDataViewServiceImpl implements SteadyDataViewService {
|
public class SteadyDataViewServiceImpl implements SteadyDataViewService {
|
||||||
|
|
||||||
private static final int LEDGER_LINE_QUERY_LIMIT = 1000;
|
|
||||||
private static final int STEADY_LINE_ID_QUERY_LIMIT = 1000;
|
|
||||||
private static final String DEFAULT_TABLE_NAME = "data_v";
|
private static final String DEFAULT_TABLE_NAME = "data_v";
|
||||||
private static final String EMPTY_TEXT = "-";
|
private static final String EMPTY_TEXT = "-";
|
||||||
private static final DateTimeFormatter OUTPUT_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
private static final DateTimeFormatter OUTPUT_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
||||||
@@ -56,22 +48,6 @@ public class SteadyDataViewServiceImpl implements SteadyDataViewService {
|
|||||||
private final AddDataTableRegistry addDataTableRegistry;
|
private final AddDataTableRegistry addDataTableRegistry;
|
||||||
private final AddDataTemplateRegistry addDataTemplateRegistry;
|
private final AddDataTemplateRegistry addDataTemplateRegistry;
|
||||||
|
|
||||||
@Override
|
|
||||||
public Page<SteadyDataViewVO> pageSteadyData(SteadyDataViewQueryParam param) {
|
|
||||||
SteadyDataViewQueryParam queryParam = normalizeQueryParam(param);
|
|
||||||
AddDataTableDefinition definition = resolveTableDefinition(queryParam.getTableName());
|
|
||||||
if (!resolveLineFilter(queryParam)) {
|
|
||||||
return emptyPage(queryParam);
|
|
||||||
}
|
|
||||||
Page<Map<String, Object>> steadyPage = steadyDataViewMapper.selectSteadyPage(
|
|
||||||
new Page<Map<String, Object>>(PageFactory.getPageNum(queryParam), PageFactory.getPageSize(queryParam)),
|
|
||||||
definition.getTableName(), definition.getColumns(), queryParam);
|
|
||||||
List<SteadyDataViewVO> records = buildSteadyDataList(definition.getTableName(), definition.getColumns(), steadyPage.getRecords());
|
|
||||||
Page<SteadyDataViewVO> resultPage = new Page<SteadyDataViewVO>(steadyPage.getCurrent(), steadyPage.getSize(), steadyPage.getTotal());
|
|
||||||
resultPage.setRecords(records);
|
|
||||||
return resultPage;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public SteadyDataViewVO getSteadyDataDetail(SteadyDataViewDetailParam param) {
|
public SteadyDataViewVO getSteadyDataDetail(SteadyDataViewDetailParam param) {
|
||||||
if (param == null) {
|
if (param == null) {
|
||||||
@@ -156,69 +132,6 @@ public class SteadyDataViewServiceImpl implements SteadyDataViewService {
|
|||||||
return vo;
|
return vo;
|
||||||
}
|
}
|
||||||
|
|
||||||
private SteadyDataViewQueryParam normalizeQueryParam(SteadyDataViewQueryParam param) {
|
|
||||||
SteadyDataViewQueryParam queryParam = param == null ? new SteadyDataViewQueryParam() : param;
|
|
||||||
queryParam.setTableName(normalizeTableName(queryParam.getTableName()));
|
|
||||||
LocalDateTime startTime = parseDateTime(queryParam.getTimeStart());
|
|
||||||
LocalDateTime endTime = parseDateTime(queryParam.getTimeEnd());
|
|
||||||
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, "开始时间不能大于结束时间");
|
|
||||||
}
|
|
||||||
queryParam.setTimeStart(OUTPUT_FORMATTER.format(startTime));
|
|
||||||
queryParam.setTimeEnd(OUTPUT_FORMATTER.format(endTime));
|
|
||||||
queryParam.setPhasicType(normalizePhasicType(queryParam.getPhasicType()));
|
|
||||||
validateQualityFlag(queryParam.getQualityFlag());
|
|
||||||
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() > STEADY_LINE_ID_QUERY_LIMIT) {
|
|
||||||
throw new BusinessException(CommonResponseEnum.FAIL, "监测点 ID 查询数量不能超过 1000 个");
|
|
||||||
}
|
|
||||||
queryParam.setLineIds(lineIds);
|
|
||||||
return queryParam;
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean resolveLineFilter(SteadyDataViewQueryParam 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 AddDataTableDefinition resolveTableDefinition(String tableName) {
|
private AddDataTableDefinition resolveTableDefinition(String tableName) {
|
||||||
try {
|
try {
|
||||||
return addDataTableRegistry.getDefinition(tableName);
|
return addDataTableRegistry.getDefinition(tableName);
|
||||||
@@ -267,39 +180,6 @@ public class SteadyDataViewServiceImpl implements SteadyDataViewService {
|
|||||||
return normalized;
|
return normalized;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void validateQualityFlag(Integer qualityFlag) {
|
|
||||||
if (qualityFlag != null && qualityFlag != 0 && qualityFlag != 1) {
|
|
||||||
throw new BusinessException(CommonResponseEnum.FAIL, "质量标识只能是 0 或 1");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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(SteadyDataViewQueryParam queryParam) {
|
|
||||||
return trimToNull(queryParam.getEngineeringName()) != null
|
|
||||||
|| trimToNull(queryParam.getProjectName()) != null
|
|
||||||
|| trimToNull(queryParam.getEquipmentName()) != null
|
|
||||||
|| trimToNull(queryParam.getLineName()) != null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Page<SteadyDataViewVO> emptyPage(SteadyDataViewQueryParam queryParam) {
|
|
||||||
Page<SteadyDataViewVO> page = new Page<SteadyDataViewVO>(PageFactory.getPageNum(queryParam), PageFactory.getPageSize(queryParam), 0);
|
|
||||||
page.setRecords(Collections.<SteadyDataViewVO>emptyList());
|
|
||||||
return page;
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<String> resolveValueColumns(List<String> columns) {
|
private List<String> resolveValueColumns(List<String> columns) {
|
||||||
List<String> result = new ArrayList<String>();
|
List<String> result = new ArrayList<String>();
|
||||||
for (String column : columns) {
|
for (String column : columns) {
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
package com.njcn.gather.steady.datavie.service.impl;
|
package com.njcn.gather.steady.datavie.service.impl;
|
||||||
|
|
||||||
import com.njcn.common.pojo.enums.response.CommonResponseEnum;
|
|
||||||
import com.njcn.common.pojo.exception.BusinessException;
|
|
||||||
import com.njcn.gather.steady.datavie.component.SteadyInfluxQueryComponent;
|
import com.njcn.gather.steady.datavie.component.SteadyInfluxQueryComponent;
|
||||||
import com.njcn.gather.steady.datavie.component.SteadyTrendFieldResolver;
|
import com.njcn.gather.steady.datavie.component.SteadyTrendFieldResolver;
|
||||||
import com.njcn.gather.steady.datavie.pojo.bo.SteadyTrendResolvedFieldBO;
|
import com.njcn.gather.steady.datavie.pojo.bo.SteadyTrendResolvedFieldBO;
|
||||||
@@ -15,13 +13,13 @@ import com.njcn.gather.steady.datavie.service.SteadyDataViewTrendService;
|
|||||||
import com.njcn.gather.tool.addledger.pojo.vo.AddLedgerLinePathVO;
|
import com.njcn.gather.tool.addledger.pojo.vo.AddLedgerLinePathVO;
|
||||||
import com.njcn.gather.tool.addledger.service.AddLedgerService;
|
import com.njcn.gather.tool.addledger.service.AddLedgerService;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.math.RoundingMode;
|
import java.math.RoundingMode;
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.time.temporal.ChronoUnit;
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
@@ -31,11 +29,13 @@ import java.util.Map;
|
|||||||
/**
|
/**
|
||||||
* 稳态趋势查询服务实现。
|
* 稳态趋势查询服务实现。
|
||||||
*/
|
*/
|
||||||
|
@Slf4j
|
||||||
@Service
|
@Service
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class SteadyDataViewTrendServiceImpl implements SteadyDataViewTrendService {
|
public class SteadyDataViewTrendServiceImpl implements SteadyDataViewTrendService {
|
||||||
|
|
||||||
private static final String EMPTY_TEXT = "-";
|
private static final String EMPTY_TEXT = "-";
|
||||||
|
private static final String LINE_VOLTAGE_RMS_INDICATOR = "V_LINE_RMS";
|
||||||
|
|
||||||
private final SteadyTrendFieldResolver fieldResolver;
|
private final SteadyTrendFieldResolver fieldResolver;
|
||||||
private final SteadyInfluxQueryComponent influxQueryComponent;
|
private final SteadyInfluxQueryComponent influxQueryComponent;
|
||||||
@@ -43,19 +43,18 @@ public class SteadyDataViewTrendServiceImpl implements SteadyDataViewTrendServic
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public SteadyTrendQueryVO queryTrend(SteadyTrendQueryParam param) {
|
public SteadyTrendQueryVO queryTrend(SteadyTrendQueryParam param) {
|
||||||
return queryTrendInternal(param, true);
|
return queryTrendInternal(param);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public SteadyTrendQueryVO queryTrendDay(SteadyTrendQueryParam param) {
|
public SteadyTrendQueryVO queryTrendDay(SteadyTrendQueryParam param) {
|
||||||
return queryTrendInternal(param, true);
|
return queryTrendInternal(param);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public SteadyTrendSummaryVO summarizeTrend(SteadyTrendQueryParam param) {
|
public SteadyTrendSummaryVO summarizeTrend(SteadyTrendQueryParam param) {
|
||||||
SteadyTrendQueryParam summaryParam = copyParam(param);
|
SteadyTrendQueryParam summaryParam = copyParam(param);
|
||||||
summaryParam.setBucket(null);
|
SteadyTrendQueryVO trend = queryTrendInternal(summaryParam);
|
||||||
SteadyTrendQueryVO trend = queryTrendInternal(summaryParam, false);
|
|
||||||
SteadyTrendSummaryVO result = new SteadyTrendSummaryVO();
|
SteadyTrendSummaryVO result = new SteadyTrendSummaryVO();
|
||||||
for (SteadyTrendSeriesVO series : trend.getSeries()) {
|
for (SteadyTrendSeriesVO series : trend.getSeries()) {
|
||||||
result.getItems().add(buildSummaryItem(series));
|
result.getItems().add(buildSummaryItem(series));
|
||||||
@@ -63,23 +62,24 @@ public class SteadyDataViewTrendServiceImpl implements SteadyDataViewTrendServic
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
private SteadyTrendQueryVO queryTrendInternal(SteadyTrendQueryParam param, boolean autoBucket) {
|
private SteadyTrendQueryVO queryTrendInternal(SteadyTrendQueryParam param) {
|
||||||
LocalDateTime startTime = fieldResolver.parseRequiredTime(param == null ? null : param.getTimeStart(), "开始时间不能为空");
|
LocalDateTime startTime = fieldResolver.parseRequiredTime(param == null ? null : param.getTimeStart(), "开始时间不能为空");
|
||||||
LocalDateTime endTime = fieldResolver.parseRequiredTime(param == null ? null : param.getTimeEnd(), "结束时间不能为空");
|
LocalDateTime endTime = fieldResolver.parseRequiredTime(param == null ? null : param.getTimeEnd(), "结束时间不能为空");
|
||||||
List<SteadyTrendResolvedFieldBO> fields = fieldResolver.resolveFields(param);
|
List<SteadyTrendResolvedFieldBO> fields = fieldResolver.resolveFields(param);
|
||||||
enrichLineNames(fields);
|
enrichLineNames(fields);
|
||||||
String bucket = autoBucket ? resolveBucket(param.getBucket(), startTime, endTime) : null;
|
|
||||||
|
|
||||||
SteadyTrendQueryVO result = new SteadyTrendQueryVO();
|
SteadyTrendQueryVO result = new SteadyTrendQueryVO();
|
||||||
result.setBucket(bucket);
|
result.setSampled(false);
|
||||||
result.setSampled(bucket != null);
|
|
||||||
result.setLoadableDays(resolveLoadableDays(startTime, endTime));
|
result.setLoadableDays(resolveLoadableDays(startTime, endTime));
|
||||||
int displayPointCount = 0;
|
int displayPointCount = 0;
|
||||||
|
long startMillis = System.currentTimeMillis();
|
||||||
|
log.info("稳态趋势查询开始,seriesCount={},timeStart={},timeEnd={},qualityFlag={}", fields.size(), startTime, endTime, param.getQualityFlag());
|
||||||
for (SteadyTrendResolvedFieldBO field : fields) {
|
for (SteadyTrendResolvedFieldBO field : fields) {
|
||||||
List<SteadyTrendPointVO> points = influxQueryComponent.queryTrendPoints(field, startTime, endTime, bucket, param.getQualityFlag());
|
List<SteadyTrendPointVO> points = influxQueryComponent.queryTrendPoints(field, startTime, endTime, param.getQualityFlag());
|
||||||
displayPointCount += points.size();
|
displayPointCount += points.size();
|
||||||
result.getSeries().add(buildSeries(field, points));
|
result.getSeries().add(buildSeries(field, points));
|
||||||
}
|
}
|
||||||
|
log.info("稳态趋势查询结束,seriesCount={},displayPointCount={},costMs={}", fields.size(), displayPointCount, System.currentTimeMillis() - startMillis);
|
||||||
/*
|
/*
|
||||||
* 当前 Influx 查询按曲线独立执行,未额外发 count 查询;sourcePointCount 保持与实际返回点数一致。
|
* 当前 Influx 查询按曲线独立执行,未额外发 count 查询;sourcePointCount 保持与实际返回点数一致。
|
||||||
* 后续如需要精确原始点数,可单独增加 count(field) 查询。
|
* 后续如需要精确原始点数,可单独增加 count(field) 查询。
|
||||||
@@ -97,13 +97,29 @@ public class SteadyDataViewTrendServiceImpl implements SteadyDataViewTrendServic
|
|||||||
series.setIndicatorCode(field.getIndicatorCode());
|
series.setIndicatorCode(field.getIndicatorCode());
|
||||||
series.setIndicatorName(field.getIndicatorName());
|
series.setIndicatorName(field.getIndicatorName());
|
||||||
series.setSeriesName(field.getSeriesName());
|
series.setSeriesName(field.getSeriesName());
|
||||||
series.setPhase(field.getPhase());
|
series.setPhase(resolveDisplayPhase(field));
|
||||||
series.setStatType(field.getStatType());
|
series.setStatType(field.getStatType());
|
||||||
series.setUnit(field.getUnit());
|
series.setUnit(field.getUnit());
|
||||||
series.setPoints(points);
|
series.setPoints(points);
|
||||||
return series;
|
return series;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String resolveDisplayPhase(SteadyTrendResolvedFieldBO field) {
|
||||||
|
if (!LINE_VOLTAGE_RMS_INDICATOR.equals(field.getIndicatorCode())) {
|
||||||
|
return field.getPhase();
|
||||||
|
}
|
||||||
|
if ("A".equals(field.getPhase())) {
|
||||||
|
return "AB";
|
||||||
|
}
|
||||||
|
if ("B".equals(field.getPhase())) {
|
||||||
|
return "BC";
|
||||||
|
}
|
||||||
|
if ("C".equals(field.getPhase())) {
|
||||||
|
return "CA";
|
||||||
|
}
|
||||||
|
return field.getPhase();
|
||||||
|
}
|
||||||
|
|
||||||
private void enrichLineNames(List<SteadyTrendResolvedFieldBO> fields) {
|
private void enrichLineNames(List<SteadyTrendResolvedFieldBO> fields) {
|
||||||
List<String> lineIds = new ArrayList<String>();
|
List<String> lineIds = new ArrayList<String>();
|
||||||
for (SteadyTrendResolvedFieldBO field : fields) {
|
for (SteadyTrendResolvedFieldBO field : fields) {
|
||||||
@@ -143,27 +159,6 @@ public class SteadyDataViewTrendServiceImpl implements SteadyDataViewTrendServic
|
|||||||
return item;
|
return item;
|
||||||
}
|
}
|
||||||
|
|
||||||
private String resolveBucket(String requestBucket, LocalDateTime startTime, LocalDateTime endTime) {
|
|
||||||
String bucket = trimToNull(requestBucket);
|
|
||||||
if (bucket != null) {
|
|
||||||
if (!bucket.matches("^[1-9][0-9]*(m|h|d)$")) {
|
|
||||||
throw fail("分桶粒度仅支持 m、h、d,例如 10m、1h");
|
|
||||||
}
|
|
||||||
return bucket;
|
|
||||||
}
|
|
||||||
long hours = ChronoUnit.HOURS.between(startTime, endTime);
|
|
||||||
if (hours < 6) {
|
|
||||||
return "1m";
|
|
||||||
}
|
|
||||||
if (hours <= 24) {
|
|
||||||
return "10m";
|
|
||||||
}
|
|
||||||
if (hours <= 24 * 7) {
|
|
||||||
return "30m";
|
|
||||||
}
|
|
||||||
return "1h";
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<String> resolveLoadableDays(LocalDateTime startTime, LocalDateTime endTime) {
|
private List<String> resolveLoadableDays(LocalDateTime startTime, LocalDateTime endTime) {
|
||||||
List<String> result = new ArrayList<String>();
|
List<String> result = new ArrayList<String>();
|
||||||
LocalDate date = startTime.toLocalDate();
|
LocalDate date = startTime.toLocalDate();
|
||||||
@@ -183,10 +178,8 @@ public class SteadyDataViewTrendServiceImpl implements SteadyDataViewTrendServic
|
|||||||
target.setLineIds(copyList(source.getLineIds()));
|
target.setLineIds(copyList(source.getLineIds()));
|
||||||
target.setIndicatorCodes(copyList(source.getIndicatorCodes()));
|
target.setIndicatorCodes(copyList(source.getIndicatorCodes()));
|
||||||
target.setStatTypes(copyList(source.getStatTypes()));
|
target.setStatTypes(copyList(source.getStatTypes()));
|
||||||
target.setPhases(copyList(source.getPhases()));
|
|
||||||
target.setTimeStart(source.getTimeStart());
|
target.setTimeStart(source.getTimeStart());
|
||||||
target.setTimeEnd(source.getTimeEnd());
|
target.setTimeEnd(source.getTimeEnd());
|
||||||
target.setBucket(source.getBucket());
|
|
||||||
target.setQualityFlag(source.getQualityFlag());
|
target.setQualityFlag(source.getQualityFlag());
|
||||||
target.setHarmonicOrders(source.getHarmonicOrders() == null ? Collections.<Integer>emptyList() : new ArrayList<Integer>(source.getHarmonicOrders()));
|
target.setHarmonicOrders(source.getHarmonicOrders() == null ? Collections.<Integer>emptyList() : new ArrayList<Integer>(source.getHarmonicOrders()));
|
||||||
return target;
|
return target;
|
||||||
@@ -204,7 +197,4 @@ public class SteadyDataViewTrendServiceImpl implements SteadyDataViewTrendServic
|
|||||||
return trimmed.isEmpty() ? null : trimmed;
|
return trimmed.isEmpty() ? null : trimmed;
|
||||||
}
|
}
|
||||||
|
|
||||||
private BusinessException fail(String message) {
|
|
||||||
return new BusinessException(CommonResponseEnum.FAIL, message);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,102 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS `steady_checksquare_task` (
|
||||||
|
`id` VARCHAR(64) NOT NULL COMMENT '主键',
|
||||||
|
`task_no` VARCHAR(64) NOT NULL COMMENT '检测任务编号',
|
||||||
|
`line_id` VARCHAR(64) NOT NULL COMMENT '监测点ID',
|
||||||
|
`line_name` VARCHAR(255) NULL COMMENT '监测点名称',
|
||||||
|
`time_start` DATETIME NOT NULL COMMENT '检测开始时间',
|
||||||
|
`time_end` DATETIME NOT NULL COMMENT '检测结束时间',
|
||||||
|
`interval_minutes` INT NULL COMMENT '默认统计间隔,单位分钟',
|
||||||
|
`indicator_codes_json` JSON NULL COMMENT '请求指标编码列表',
|
||||||
|
`indicator_codes_text` VARCHAR(2000) NULL COMMENT '请求指标编码检索文本,格式 |code1|code2|',
|
||||||
|
`task_status` VARCHAR(32) NOT NULL DEFAULT 'SUCCESS' COMMENT '任务状态:SUCCESS/FAIL',
|
||||||
|
`item_count` INT NOT NULL DEFAULT 0 COMMENT '检测项数量',
|
||||||
|
`abnormal_item_count` INT NOT NULL DEFAULT 0 COMMENT '异常检测项数量',
|
||||||
|
`min_data_integrity` DECIMAL(12,6) NOT NULL DEFAULT 0.000000 COMMENT '最低数据完整性',
|
||||||
|
`result_message` VARCHAR(2000) NULL COMMENT '执行结果说明',
|
||||||
|
`state` TINYINT NOT NULL DEFAULT 1 COMMENT '状态:0-删除,1-正常',
|
||||||
|
`create_by` VARCHAR(64) NULL COMMENT '创建人',
|
||||||
|
`create_time` DATETIME NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||||
|
`update_by` VARCHAR(64) NULL COMMENT '更新人',
|
||||||
|
`update_time` DATETIME NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
UNIQUE KEY `uk_steady_checksquare_task_no` (`task_no`),
|
||||||
|
KEY `idx_steady_checksquare_task_line_time` (`line_id`, `time_start`, `time_end`),
|
||||||
|
KEY `idx_steady_checksquare_task_status` (`task_status`),
|
||||||
|
KEY `idx_steady_checksquare_task_indicator_text` (`indicator_codes_text`(255)),
|
||||||
|
KEY `idx_steady_checksquare_task_create_time` (`create_time`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='稳态数据校验任务表';
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `steady_checksquare_item` (
|
||||||
|
`id` VARCHAR(64) NOT NULL COMMENT '主键',
|
||||||
|
`task_id` VARCHAR(64) NOT NULL COMMENT '检测任务ID',
|
||||||
|
`item_key` VARCHAR(255) NOT NULL COMMENT '检测项唯一键',
|
||||||
|
`indicator_code` VARCHAR(64) NOT NULL COMMENT '指标编码',
|
||||||
|
`indicator_name` VARCHAR(255) NULL COMMENT '指标名称',
|
||||||
|
`harmonic_order` INT NULL COMMENT '谐波次数;聚合项为空',
|
||||||
|
`interval_minutes` INT NULL COMMENT '当前检测项统计间隔,单位分钟',
|
||||||
|
`has_data` TINYINT NOT NULL DEFAULT 0 COMMENT '是否存在任意数据:0-否,1-是',
|
||||||
|
`expected_point_count` INT NOT NULL DEFAULT 0 COMMENT '期望点数',
|
||||||
|
`actual_point_count` INT NOT NULL DEFAULT 0 COMMENT '实际点数',
|
||||||
|
`missing_point_count` INT NOT NULL DEFAULT 0 COMMENT '缺失点数',
|
||||||
|
`data_integrity` DECIMAL(12,6) NOT NULL DEFAULT 0.000000 COMMENT '数据完整性',
|
||||||
|
`data_integrity_text` VARCHAR(32) NULL COMMENT '数据完整性文本',
|
||||||
|
`abnormal` TINYINT NOT NULL DEFAULT 0 COMMENT '指标值大小关系是否异常',
|
||||||
|
`abnormal_point_count` INT NOT NULL DEFAULT 0 COMMENT '大小关系异常点数',
|
||||||
|
`harmonic_parity_abnormal` TINYINT NOT NULL DEFAULT 0 COMMENT '谐波奇偶关系是否异常',
|
||||||
|
`harmonic_parity_abnormal_point_count` INT NOT NULL DEFAULT 0 COMMENT '谐波奇偶关系异常点数',
|
||||||
|
`state` TINYINT NOT NULL DEFAULT 1 COMMENT '状态:0-删除,1-正常',
|
||||||
|
`create_time` DATETIME NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||||
|
`update_time` DATETIME NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
UNIQUE KEY `uk_steady_checksquare_item` (`task_id`, `item_key`),
|
||||||
|
KEY `idx_steady_checksquare_item_indicator` (`indicator_code`),
|
||||||
|
KEY `idx_steady_checksquare_item_abnormal` (`abnormal`, `harmonic_parity_abnormal`),
|
||||||
|
KEY `idx_steady_checksquare_item_data_integrity` (`data_integrity`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='稳态数据校验检测项表';
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `steady_checksquare_stat_summary` (
|
||||||
|
`id` VARCHAR(64) NOT NULL COMMENT '主键',
|
||||||
|
`item_id` VARCHAR(64) NOT NULL COMMENT '检测项ID',
|
||||||
|
`stat_type` VARCHAR(16) NOT NULL COMMENT '统计类型:AVG/MAX/MIN/CP95',
|
||||||
|
`supported` TINYINT NOT NULL DEFAULT 1 COMMENT '是否支持',
|
||||||
|
`has_data` TINYINT NOT NULL DEFAULT 0 COMMENT '是否存在数据',
|
||||||
|
`expected_point_count` INT NOT NULL DEFAULT 0 COMMENT '期望点数',
|
||||||
|
`actual_point_count` INT NOT NULL DEFAULT 0 COMMENT '实际点数',
|
||||||
|
`missing_point_count` INT NOT NULL DEFAULT 0 COMMENT '缺失点数',
|
||||||
|
`data_integrity` DECIMAL(12,6) NOT NULL DEFAULT 0.000000 COMMENT '数据完整性',
|
||||||
|
`data_integrity_text` VARCHAR(32) NULL COMMENT '数据完整性文本',
|
||||||
|
`create_time` DATETIME NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
UNIQUE KEY `uk_steady_checksquare_stat` (`item_id`, `stat_type`),
|
||||||
|
KEY `idx_steady_checksquare_stat_type` (`stat_type`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='稳态数据校验统计摘要表';
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `steady_checksquare_detail` (
|
||||||
|
`id` VARCHAR(64) NOT NULL COMMENT '主键',
|
||||||
|
`item_id` VARCHAR(64) NOT NULL COMMENT '检测项ID',
|
||||||
|
`detail_type` VARCHAR(32) NOT NULL COMMENT '明细类型:SEGMENT/VALUE_ORDER/HARMONIC_PARITY',
|
||||||
|
`stat_type` VARCHAR(16) NULL COMMENT '统计类型',
|
||||||
|
`start_time` DATETIME NULL COMMENT '区间开始时间',
|
||||||
|
`end_time` DATETIME NULL COMMENT '区间结束时间',
|
||||||
|
`point_time` DATETIME NULL COMMENT '异常点时间',
|
||||||
|
`segment_status` VARCHAR(16) NULL COMMENT '区间状态:NORMAL/MISSING',
|
||||||
|
`missing_point_count` INT NULL COMMENT '缺失点数',
|
||||||
|
`duration_minutes` INT NULL COMMENT '持续时长,单位分钟',
|
||||||
|
`phase` VARCHAR(16) NULL COMMENT '相别',
|
||||||
|
`harmonic_order` INT NULL COMMENT '谐波次数',
|
||||||
|
`max_value` DECIMAL(24,8) NULL COMMENT '最大值',
|
||||||
|
`min_value` DECIMAL(24,8) NULL COMMENT '最小值',
|
||||||
|
`avg_value` DECIMAL(24,8) NULL COMMENT '平均值',
|
||||||
|
`cp95_value` DECIMAL(24,8) NULL COMMENT 'CP95值',
|
||||||
|
`even_harmonic_order` INT NULL COMMENT '偶次谐波次数',
|
||||||
|
`even_value` DECIMAL(24,8) NULL COMMENT '偶次谐波值',
|
||||||
|
`odd_harmonic_orders_json` JSON NULL COMMENT '参与比较的奇次谐波次数',
|
||||||
|
`odd_values_json` JSON NULL COMMENT '参与比较的奇次谐波值',
|
||||||
|
`odd_median_value` DECIMAL(24,8) NULL COMMENT '奇次谐波中位数',
|
||||||
|
`threshold_multiplier` DECIMAL(12,6) NULL COMMENT '异常阈值倍数',
|
||||||
|
`create_time` DATETIME NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
KEY `idx_steady_checksquare_detail_item_type` (`item_id`, `detail_type`),
|
||||||
|
KEY `idx_steady_checksquare_detail_point_time` (`point_time`),
|
||||||
|
KEY `idx_steady_checksquare_detail_segment` (`start_time`, `end_time`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='稳态数据校验明细表';
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
ALTER TABLE `steady_checksquare_task`
|
||||||
|
CHANGE COLUMN `max_missing_rate` `min_data_integrity` DECIMAL(12,6) NOT NULL DEFAULT 0.000000 COMMENT '最低数据完整性';
|
||||||
|
|
||||||
|
ALTER TABLE `steady_checksquare_item`
|
||||||
|
DROP INDEX `idx_steady_checksquare_item_missing_rate`,
|
||||||
|
CHANGE COLUMN `missing_rate` `data_integrity` DECIMAL(12,6) NOT NULL DEFAULT 0.000000 COMMENT '数据完整性',
|
||||||
|
CHANGE COLUMN `missing_rate_text` `data_integrity_text` VARCHAR(32) NULL COMMENT '数据完整性文本',
|
||||||
|
DROP COLUMN `max_continuous_missing_minutes`,
|
||||||
|
ADD KEY `idx_steady_checksquare_item_data_integrity` (`data_integrity`);
|
||||||
|
|
||||||
|
ALTER TABLE `steady_checksquare_stat_summary`
|
||||||
|
CHANGE COLUMN `missing_rate` `data_integrity` DECIMAL(12,6) NOT NULL DEFAULT 0.000000 COMMENT '数据完整性',
|
||||||
|
CHANGE COLUMN `missing_rate_text` `data_integrity_text` VARCHAR(32) NULL COMMENT '数据完整性文本',
|
||||||
|
DROP COLUMN `max_continuous_missing_minutes`;
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
-- 稳态数据查看建议索引。
|
||||||
|
-- 本脚本不自动执行,请按数据库现状审阅后单独执行。
|
||||||
|
|
||||||
|
CREATE INDEX idx_data_v_time_line_phase
|
||||||
|
ON data_v (TIMEID, LINEID, PHASIC_TYPE);
|
||||||
|
|
||||||
|
CREATE INDEX idx_data_i_time_line_phase
|
||||||
|
ON data_i (TIMEID, LINEID, PHASIC_TYPE);
|
||||||
|
|
||||||
|
CREATE INDEX idx_data_flicker_time_line_phase
|
||||||
|
ON data_flicker (TIMEID, LINEID, PHASIC_TYPE);
|
||||||
|
|
||||||
|
CREATE INDEX idx_data_fluc_time_line_phase
|
||||||
|
ON data_fluc (TIMEID, LINEID, PHASIC_TYPE);
|
||||||
|
|
||||||
|
CREATE INDEX idx_data_harmphasic_i_time_line_phase
|
||||||
|
ON data_harmphasic_i (TIMEID, LINEID, PHASIC_TYPE);
|
||||||
|
|
||||||
|
CREATE INDEX idx_data_harmphasic_v_time_line_phase
|
||||||
|
ON data_harmphasic_v (TIMEID, LINEID, PHASIC_TYPE);
|
||||||
|
|
||||||
|
CREATE INDEX idx_data_harmpower_p_time_line_phase
|
||||||
|
ON data_harmpower_p (TIMEID, LINEID, PHASIC_TYPE);
|
||||||
|
|
||||||
|
CREATE INDEX idx_data_harmpower_q_time_line_phase
|
||||||
|
ON data_harmpower_q (TIMEID, LINEID, PHASIC_TYPE);
|
||||||
|
|
||||||
|
CREATE INDEX idx_data_harmpower_s_time_line_phase
|
||||||
|
ON data_harmpower_s (TIMEID, LINEID, PHASIC_TYPE);
|
||||||
|
|
||||||
|
CREATE INDEX idx_data_harmrate_i_time_line_phase
|
||||||
|
ON data_harmrate_i (TIMEID, LINEID, PHASIC_TYPE);
|
||||||
|
|
||||||
|
CREATE INDEX idx_data_harmrate_v_time_line_phase
|
||||||
|
ON data_harmrate_v (TIMEID, LINEID, PHASIC_TYPE);
|
||||||
|
|
||||||
|
CREATE INDEX idx_data_inharm_i_time_line_phase
|
||||||
|
ON data_inharm_i (TIMEID, LINEID, PHASIC_TYPE);
|
||||||
|
|
||||||
|
CREATE INDEX idx_data_plt_time_line_phase
|
||||||
|
ON data_plt (TIMEID, LINEID, PHASIC_TYPE);
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
-- 稳态模块菜单图标修正脚本。
|
||||||
|
-- 本脚本不自动执行,请按数据库现状审阅后单独执行。
|
||||||
|
|
||||||
|
UPDATE sys_function
|
||||||
|
SET Icon = 'DataAnalysis'
|
||||||
|
WHERE State = 1
|
||||||
|
AND Type = 0
|
||||||
|
AND (
|
||||||
|
Name = '稳态模块'
|
||||||
|
OR Code IN ('steady', 'steadyModule', 'steadyDataView')
|
||||||
|
OR Path IN ('/steady', '/steadyDataView', '/steady/data-view')
|
||||||
|
);
|
||||||
|
|
||||||
|
UPDATE sys_function
|
||||||
|
SET Icon = 'DataBoard'
|
||||||
|
WHERE State = 1
|
||||||
|
AND Type = 0
|
||||||
|
AND (
|
||||||
|
Name = '稳态数据'
|
||||||
|
OR Code IN ('steadyData', 'steadyDataDetail')
|
||||||
|
OR Path IN ('/steady/data', '/steady/data-view/detail', '/steadyDataView/index')
|
||||||
|
);
|
||||||
|
|
||||||
|
UPDATE sys_function
|
||||||
|
SET Icon = 'TrendCharts'
|
||||||
|
WHERE State = 1
|
||||||
|
AND Type = 0
|
||||||
|
AND (
|
||||||
|
Name = '稳态趋势'
|
||||||
|
OR Code IN ('steadyTrend', 'steadyDataTrend')
|
||||||
|
OR Path IN ('/steady/trend', '/steady/data-view/trend', '/steadyTrend/index')
|
||||||
|
);
|
||||||
|
|
||||||
|
UPDATE sys_function
|
||||||
|
SET Icon = 'CircleCheck'
|
||||||
|
WHERE State = 1
|
||||||
|
AND Type = 0
|
||||||
|
AND (
|
||||||
|
Name = '数据验证'
|
||||||
|
OR Code IN ('dataValidation', 'steadyDataValidation')
|
||||||
|
OR Path IN ('/steady/data-validation', '/steady/data-view/validation', '/dataValidation/index')
|
||||||
|
);
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
package com.njcn.gather.steady.checksquare.component;
|
||||||
|
|
||||||
|
import com.njcn.gather.steady.checksquare.pojo.vo.SteadyChecksquareSegmentVO;
|
||||||
|
import org.junit.jupiter.api.Assertions;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 数据校验缺失区间计算测试。
|
||||||
|
*/
|
||||||
|
class SteadyChecksquareCalculatorTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldMergeContinuousMissingSlots() {
|
||||||
|
SteadyChecksquareCalculator calculator = new SteadyChecksquareCalculator();
|
||||||
|
List<LocalDateTime> slots = Arrays.asList(
|
||||||
|
LocalDateTime.of(2026, 5, 1, 0, 0),
|
||||||
|
LocalDateTime.of(2026, 5, 1, 0, 1),
|
||||||
|
LocalDateTime.of(2026, 5, 1, 0, 2),
|
||||||
|
LocalDateTime.of(2026, 5, 1, 0, 3),
|
||||||
|
LocalDateTime.of(2026, 5, 1, 0, 4)
|
||||||
|
);
|
||||||
|
|
||||||
|
List<SteadyChecksquareSegmentVO> segments = calculator.buildSegments(slots,
|
||||||
|
new HashSet<LocalDateTime>(Arrays.asList(slots.get(0), slots.get(3))), 1);
|
||||||
|
|
||||||
|
Assertions.assertEquals(4, segments.size());
|
||||||
|
Assertions.assertEquals("MISSING", segments.get(1).getStatus());
|
||||||
|
Assertions.assertEquals("2026-05-01 00:01:00", segments.get(1).getStartTime());
|
||||||
|
Assertions.assertEquals("2026-05-01 00:02:00", segments.get(1).getEndTime());
|
||||||
|
Assertions.assertEquals(Integer.valueOf(2), segments.get(1).getMissingPointCount());
|
||||||
|
Assertions.assertEquals(Integer.valueOf(2), segments.get(1).getDurationMinutes());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,147 @@
|
|||||||
|
package com.njcn.gather.steady.checksquare.component;
|
||||||
|
|
||||||
|
import com.njcn.gather.steady.checksquare.pojo.bo.SteadyChecksquareValuePointBO;
|
||||||
|
import com.njcn.gather.steady.checksquare.pojo.vo.SteadyChecksquareHarmonicParityDetailVO;
|
||||||
|
import com.njcn.gather.steady.checksquare.pojo.vo.SteadyChecksquareHarmonicParityRuleVO;
|
||||||
|
import com.njcn.gather.steady.datavie.component.SteadyTrendIndicatorCatalog;
|
||||||
|
import com.njcn.gather.steady.datavie.pojo.bo.SteadyTrendIndicatorDefinitionBO;
|
||||||
|
import com.njcn.gather.steady.datavie.pojo.bo.SteadyTrendResolvedFieldBO;
|
||||||
|
import org.junit.jupiter.api.Assertions;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.ArgumentMatchers.anyInt;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 谐波奇偶关系规则测试。
|
||||||
|
*/
|
||||||
|
class SteadyChecksquareHarmonicParityRuleComponentTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldRecordAbnormalWhenEvenHarmonicExceedsOddMedianThreshold() {
|
||||||
|
SteadyChecksquareInfluxQueryComponent influxQueryComponent = mock(SteadyChecksquareInfluxQueryComponent.class);
|
||||||
|
SteadyChecksquareHarmonicParityRuleComponent component = new SteadyChecksquareHarmonicParityRuleComponent(influxQueryComponent);
|
||||||
|
LocalDateTime time = LocalDateTime.of(2026, 5, 1, 0, 0);
|
||||||
|
when(influxQueryComponent.queryValuePointMap(any(),
|
||||||
|
any(LocalDateTime.class), any(LocalDateTime.class), anyInt()))
|
||||||
|
.thenAnswer(invocation -> {
|
||||||
|
Map<String, List<SteadyChecksquareValuePointBO>> values = emptyBatchResult(invocation.getArgument(0));
|
||||||
|
putPoint(values, "v_3", time, "10");
|
||||||
|
putPoint(values, "v_4", time, "31");
|
||||||
|
putPoint(values, "v_5", time, "12");
|
||||||
|
putPoint(values, "v_7", time, "14");
|
||||||
|
return values;
|
||||||
|
});
|
||||||
|
SteadyTrendIndicatorDefinitionBO indicator = new SteadyTrendIndicatorCatalog().getIndicator("V_HARMONIC");
|
||||||
|
|
||||||
|
SteadyChecksquareHarmonicParityRuleVO result = component.check("line-001", indicator,
|
||||||
|
time, time, 1);
|
||||||
|
|
||||||
|
Assertions.assertEquals(Boolean.TRUE, result.getAbnormal());
|
||||||
|
Assertions.assertEquals(Integer.valueOf(1), result.getAbnormalPointCount());
|
||||||
|
SteadyChecksquareHarmonicParityDetailVO detail = result.getAbnormalDetails().get(0);
|
||||||
|
Assertions.assertEquals("2026-05-01 00:00:00", detail.getTime());
|
||||||
|
Assertions.assertEquals("A", detail.getPhase());
|
||||||
|
Assertions.assertEquals("AVG", detail.getStatType());
|
||||||
|
Assertions.assertEquals(Integer.valueOf(4), detail.getEvenHarmonicOrder());
|
||||||
|
Assertions.assertEquals(new BigDecimal("31"), detail.getEvenValue());
|
||||||
|
Assertions.assertEquals(Arrays.asList(3, 5, 7), detail.getOddHarmonicOrders());
|
||||||
|
Assertions.assertEquals(new BigDecimal("12"), detail.getOddMedianValue());
|
||||||
|
Assertions.assertEquals(new BigDecimal("2"), detail.getThresholdMultiplier());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldSkipWhenOddReferenceCountLessThanTwo() {
|
||||||
|
SteadyChecksquareInfluxQueryComponent influxQueryComponent = mock(SteadyChecksquareInfluxQueryComponent.class);
|
||||||
|
SteadyChecksquareHarmonicParityRuleComponent component = new SteadyChecksquareHarmonicParityRuleComponent(influxQueryComponent);
|
||||||
|
LocalDateTime time = LocalDateTime.of(2026, 5, 1, 0, 0);
|
||||||
|
when(influxQueryComponent.queryValuePointMap(any(),
|
||||||
|
any(LocalDateTime.class), any(LocalDateTime.class), anyInt()))
|
||||||
|
.thenAnswer(invocation -> {
|
||||||
|
Map<String, List<SteadyChecksquareValuePointBO>> values = emptyBatchResult(invocation.getArgument(0));
|
||||||
|
putPoint(values, "v_2", time, "50");
|
||||||
|
putPoint(values, "v_3", time, "10");
|
||||||
|
return values;
|
||||||
|
});
|
||||||
|
SteadyTrendIndicatorDefinitionBO indicator = new SteadyTrendIndicatorCatalog().getIndicator("V_HARMONIC");
|
||||||
|
|
||||||
|
SteadyChecksquareHarmonicParityRuleVO result = component.check("line-001", indicator,
|
||||||
|
time, time, 1);
|
||||||
|
|
||||||
|
Assertions.assertEquals(Boolean.FALSE, result.getAbnormal());
|
||||||
|
Assertions.assertEquals(Integer.valueOf(0), result.getAbnormalPointCount());
|
||||||
|
Assertions.assertTrue(result.getAbnormalDetails().isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldSkipEvenHarmonicWhenValueNotGreaterThanDeadband() {
|
||||||
|
SteadyChecksquareInfluxQueryComponent influxQueryComponent = mock(SteadyChecksquareInfluxQueryComponent.class);
|
||||||
|
SteadyChecksquareHarmonicParityRuleComponent component = new SteadyChecksquareHarmonicParityRuleComponent(influxQueryComponent);
|
||||||
|
LocalDateTime time = LocalDateTime.of(2026, 5, 1, 0, 0);
|
||||||
|
when(influxQueryComponent.queryValuePointMap(any(),
|
||||||
|
any(LocalDateTime.class), any(LocalDateTime.class), anyInt()))
|
||||||
|
.thenAnswer(invocation -> {
|
||||||
|
Map<String, List<SteadyChecksquareValuePointBO>> values = emptyBatchResult(invocation.getArgument(0));
|
||||||
|
putPoint(values, "v_3", time, "0.01");
|
||||||
|
putPoint(values, "v_4", time, "0.10");
|
||||||
|
putPoint(values, "v_5", time, "0.02");
|
||||||
|
return values;
|
||||||
|
});
|
||||||
|
SteadyTrendIndicatorDefinitionBO indicator = new SteadyTrendIndicatorCatalog().getIndicator("V_HARMONIC");
|
||||||
|
|
||||||
|
SteadyChecksquareHarmonicParityRuleVO result = component.check("line-001", indicator,
|
||||||
|
time, time, 1);
|
||||||
|
|
||||||
|
Assertions.assertEquals(Boolean.FALSE, result.getAbnormal());
|
||||||
|
Assertions.assertEquals(Integer.valueOf(0), result.getAbnormalPointCount());
|
||||||
|
Assertions.assertTrue(result.getAbnormalDetails().isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldSkipNonHarmonicIndicator() {
|
||||||
|
SteadyChecksquareInfluxQueryComponent influxQueryComponent = mock(SteadyChecksquareInfluxQueryComponent.class);
|
||||||
|
SteadyChecksquareHarmonicParityRuleComponent component = new SteadyChecksquareHarmonicParityRuleComponent(influxQueryComponent);
|
||||||
|
SteadyTrendIndicatorDefinitionBO indicator = new SteadyTrendIndicatorCatalog().getIndicator("V_RMS");
|
||||||
|
|
||||||
|
SteadyChecksquareHarmonicParityRuleVO result = component.check("line-001", indicator,
|
||||||
|
LocalDateTime.of(2026, 5, 1, 0, 0),
|
||||||
|
LocalDateTime.of(2026, 5, 1, 0, 1), 1);
|
||||||
|
|
||||||
|
Assertions.assertEquals(Boolean.FALSE, result.getAbnormal());
|
||||||
|
Assertions.assertEquals(Integer.valueOf(0), result.getAbnormalPointCount());
|
||||||
|
Assertions.assertTrue(result.getAbnormalDetails().isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
private SteadyChecksquareValuePointBO point(LocalDateTime time, String value) {
|
||||||
|
SteadyChecksquareValuePointBO point = new SteadyChecksquareValuePointBO();
|
||||||
|
point.setTime(time);
|
||||||
|
point.setValue(new BigDecimal(value));
|
||||||
|
return point;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<String, List<SteadyChecksquareValuePointBO>> emptyBatchResult(List<SteadyTrendResolvedFieldBO> fields) {
|
||||||
|
Map<String, List<SteadyChecksquareValuePointBO>> result =
|
||||||
|
new LinkedHashMap<String, List<SteadyChecksquareValuePointBO>>();
|
||||||
|
for (SteadyTrendResolvedFieldBO field : fields) {
|
||||||
|
result.put(field.getField(), Collections.<SteadyChecksquareValuePointBO>emptyList());
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void putPoint(Map<String, List<SteadyChecksquareValuePointBO>> values, String field,
|
||||||
|
LocalDateTime time, String value) {
|
||||||
|
if (values.containsKey(field)) {
|
||||||
|
values.put(field, Collections.singletonList(point(time, value)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,153 @@
|
|||||||
|
package com.njcn.gather.steady.checksquare.component;
|
||||||
|
|
||||||
|
import com.njcn.gather.steady.datavie.config.SteadyInfluxDbProperties;
|
||||||
|
import com.njcn.gather.steady.datavie.pojo.bo.SteadyTrendResolvedFieldBO;
|
||||||
|
import com.sun.net.httpserver.HttpServer;
|
||||||
|
import org.junit.jupiter.api.Assertions;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.net.InetSocketAddress;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 数据校验 InfluxQL 构造契约测试。
|
||||||
|
*/
|
||||||
|
class SteadyChecksquareInfluxQueryComponentTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldBuildChecksquareQueryWithoutQualityFlag() {
|
||||||
|
SteadyChecksquareInfluxQueryComponent component = new SteadyChecksquareInfluxQueryComponent(new SteadyInfluxDbProperties());
|
||||||
|
SteadyTrendResolvedFieldBO field = new SteadyTrendResolvedFieldBO();
|
||||||
|
field.setMeasurement("data_v");
|
||||||
|
field.setField("rms");
|
||||||
|
field.setLineId("line-001");
|
||||||
|
field.setPhase("A");
|
||||||
|
field.setStatType("AVG");
|
||||||
|
|
||||||
|
String query = component.buildChecksquareQuery(field,
|
||||||
|
LocalDateTime.of(2026, 5, 1, 0, 0, 0),
|
||||||
|
LocalDateTime.of(2026, 5, 1, 23, 59, 59));
|
||||||
|
|
||||||
|
Assertions.assertTrue(query.contains("SELECT \"rms\" AS \"value\""));
|
||||||
|
Assertions.assertTrue(query.contains("\"line_id\" = 'line-001'"));
|
||||||
|
Assertions.assertTrue(query.contains("\"phasic_type\" = 'A'"));
|
||||||
|
Assertions.assertTrue(query.contains("\"value_type\" = 'AVG'"));
|
||||||
|
Assertions.assertFalse(query.contains("quality_flag"));
|
||||||
|
Assertions.assertFalse(query.contains("GROUP BY time"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldBuildValuePointQueryWithStatTypeFilter() {
|
||||||
|
SteadyChecksquareInfluxQueryComponent component = new SteadyChecksquareInfluxQueryComponent(new SteadyInfluxDbProperties());
|
||||||
|
SteadyTrendResolvedFieldBO field = new SteadyTrendResolvedFieldBO();
|
||||||
|
field.setMeasurement("data_v");
|
||||||
|
field.setField("rms");
|
||||||
|
field.setLineId("line-001");
|
||||||
|
field.setPhase("A");
|
||||||
|
field.setStatType("CP95");
|
||||||
|
|
||||||
|
String query = component.buildValuePointQuery(field,
|
||||||
|
LocalDateTime.of(2026, 5, 1, 0, 0, 0),
|
||||||
|
LocalDateTime.of(2026, 5, 1, 23, 59, 59));
|
||||||
|
|
||||||
|
Assertions.assertTrue(query.contains("SELECT \"rms\" AS \"value\""));
|
||||||
|
Assertions.assertTrue(query.contains("\"line_id\" = 'line-001'"));
|
||||||
|
Assertions.assertTrue(query.contains("\"phasic_type\" = 'A'"));
|
||||||
|
Assertions.assertTrue(query.contains("\"value_type\" = 'CP95'"));
|
||||||
|
Assertions.assertTrue(query.endsWith("ORDER BY time ASC"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldReuseValuePointQueryWithinRequestCache() throws Exception {
|
||||||
|
AtomicInteger requestCount = new AtomicInteger();
|
||||||
|
HttpServer server = HttpServer.create(new InetSocketAddress(0), 0);
|
||||||
|
server.createContext("/query", exchange -> {
|
||||||
|
requestCount.incrementAndGet();
|
||||||
|
byte[] body = ("{\"results\":[{\"series\":[{\"values\":["
|
||||||
|
+ "[\"2026-05-01T00:00:00Z\",1.23],"
|
||||||
|
+ "[\"2026-05-01T00:01:00Z\",2.34]"
|
||||||
|
+ "]}]}]}").getBytes(StandardCharsets.UTF_8);
|
||||||
|
exchange.sendResponseHeaders(200, body.length);
|
||||||
|
exchange.getResponseBody().write(body);
|
||||||
|
exchange.close();
|
||||||
|
});
|
||||||
|
server.start();
|
||||||
|
try {
|
||||||
|
SteadyInfluxDbProperties properties = new SteadyInfluxDbProperties();
|
||||||
|
properties.setUrl("http://127.0.0.1:" + server.getAddress().getPort());
|
||||||
|
properties.setDatabase("steady");
|
||||||
|
SteadyChecksquareInfluxQueryComponent component = new SteadyChecksquareInfluxQueryComponent(properties);
|
||||||
|
SteadyTrendResolvedFieldBO field = new SteadyTrendResolvedFieldBO();
|
||||||
|
field.setMeasurement("data_v");
|
||||||
|
field.setField("rms");
|
||||||
|
field.setLineId("line-001");
|
||||||
|
field.setPhase("A");
|
||||||
|
field.setStatType("AVG");
|
||||||
|
|
||||||
|
component.enableRequestCache();
|
||||||
|
component.queryExistingSlots(field,
|
||||||
|
LocalDateTime.of(2026, 5, 1, 0, 0, 0),
|
||||||
|
LocalDateTime.of(2026, 5, 1, 0, 1, 0), 1);
|
||||||
|
component.queryValuePoints(field,
|
||||||
|
LocalDateTime.of(2026, 5, 1, 0, 0, 0),
|
||||||
|
LocalDateTime.of(2026, 5, 1, 0, 1, 0), 1);
|
||||||
|
component.clearRequestCache();
|
||||||
|
|
||||||
|
Assertions.assertEquals(1, requestCount.get());
|
||||||
|
} finally {
|
||||||
|
server.stop(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldQueryMultipleValueFieldsOnce() throws Exception {
|
||||||
|
AtomicInteger requestCount = new AtomicInteger();
|
||||||
|
HttpServer server = HttpServer.create(new InetSocketAddress(0), 0);
|
||||||
|
server.createContext("/query", exchange -> {
|
||||||
|
requestCount.incrementAndGet();
|
||||||
|
byte[] body = ("{\"results\":[{\"series\":[{\"columns\":[\"time\",\"h_2\",\"h_3\"],\"values\":["
|
||||||
|
+ "[\"2026-05-01T00:00:00Z\",1.23,2.34],"
|
||||||
|
+ "[\"2026-05-01T00:01:00Z\",3.45,null]"
|
||||||
|
+ "]}]}]}").getBytes(StandardCharsets.UTF_8);
|
||||||
|
exchange.sendResponseHeaders(200, body.length);
|
||||||
|
exchange.getResponseBody().write(body);
|
||||||
|
exchange.close();
|
||||||
|
});
|
||||||
|
server.start();
|
||||||
|
try {
|
||||||
|
SteadyInfluxDbProperties properties = new SteadyInfluxDbProperties();
|
||||||
|
properties.setUrl("http://127.0.0.1:" + server.getAddress().getPort());
|
||||||
|
properties.setDatabase("steady");
|
||||||
|
SteadyChecksquareInfluxQueryComponent component = new SteadyChecksquareInfluxQueryComponent(properties);
|
||||||
|
SteadyTrendResolvedFieldBO h2 = buildField("h_2");
|
||||||
|
SteadyTrendResolvedFieldBO h3 = buildField("h_3");
|
||||||
|
|
||||||
|
component.enableRequestCache();
|
||||||
|
Map<String, java.util.List<com.njcn.gather.steady.checksquare.pojo.bo.SteadyChecksquareValuePointBO>> result =
|
||||||
|
component.queryValuePointMap(Arrays.asList(h2, h3),
|
||||||
|
LocalDateTime.of(2026, 5, 1, 0, 0, 0),
|
||||||
|
LocalDateTime.of(2026, 5, 1, 0, 1, 0), 1);
|
||||||
|
component.clearRequestCache();
|
||||||
|
|
||||||
|
Assertions.assertEquals(1, requestCount.get());
|
||||||
|
Assertions.assertEquals(2, result.get("h_2").size());
|
||||||
|
Assertions.assertEquals(1, result.get("h_3").size());
|
||||||
|
} finally {
|
||||||
|
server.stop(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private SteadyTrendResolvedFieldBO buildField(String fieldName) {
|
||||||
|
SteadyTrendResolvedFieldBO field = new SteadyTrendResolvedFieldBO();
|
||||||
|
field.setMeasurement("data_harmonic");
|
||||||
|
field.setField(fieldName);
|
||||||
|
field.setLineId("line-001");
|
||||||
|
field.setPhase("A");
|
||||||
|
field.setStatType("AVG");
|
||||||
|
return field;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,219 @@
|
|||||||
|
package com.njcn.gather.steady.checksquare.component;
|
||||||
|
|
||||||
|
import com.njcn.gather.steady.checksquare.pojo.bo.SteadyChecksquareValuePointBO;
|
||||||
|
import com.njcn.gather.steady.checksquare.pojo.vo.SteadyChecksquareValueOrderRuleVO;
|
||||||
|
import com.njcn.gather.steady.datavie.pojo.bo.SteadyTrendIndicatorDefinitionBO;
|
||||||
|
import com.njcn.gather.steady.datavie.pojo.bo.SteadyTrendSeriesFieldBO;
|
||||||
|
import org.junit.jupiter.api.Assertions;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
|
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 数据校验指标值大小关系规则测试。
|
||||||
|
*/
|
||||||
|
class SteadyChecksquareValueOrderRuleComponentTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldMarkIndicatorAbnormalWhenInvalidPointCountGreaterThanOne() {
|
||||||
|
SteadyChecksquareInfluxQueryComponent influxQueryComponent = mock(SteadyChecksquareInfluxQueryComponent.class);
|
||||||
|
SteadyChecksquareValueOrderRuleComponent component = new SteadyChecksquareValueOrderRuleComponent(influxQueryComponent);
|
||||||
|
LocalDateTime firstTime = LocalDateTime.of(2026, 5, 1, 0, 0);
|
||||||
|
LocalDateTime secondTime = LocalDateTime.of(2026, 5, 1, 0, 1);
|
||||||
|
when(influxQueryComponent.queryValuePoints(any(), any(LocalDateTime.class), any(LocalDateTime.class), eq(1)))
|
||||||
|
.thenAnswer(invocation -> {
|
||||||
|
String statType = invocation.getArgument(0, com.njcn.gather.steady.datavie.pojo.bo.SteadyTrendResolvedFieldBO.class).getStatType();
|
||||||
|
if ("MAX".equals(statType)) {
|
||||||
|
return Arrays.asList(point(firstTime, "8"), point(secondTime, "9"));
|
||||||
|
}
|
||||||
|
if ("CP95".equals(statType)) {
|
||||||
|
return Arrays.asList(point(firstTime, "9"), point(secondTime, "10"));
|
||||||
|
}
|
||||||
|
if ("AVG".equals(statType)) {
|
||||||
|
return Arrays.asList(point(firstTime, "7"), point(secondTime, "8"));
|
||||||
|
}
|
||||||
|
if ("MIN".equals(statType)) {
|
||||||
|
return Arrays.asList(point(firstTime, "1"), point(secondTime, "9"));
|
||||||
|
}
|
||||||
|
return Collections.emptyList();
|
||||||
|
});
|
||||||
|
|
||||||
|
SteadyChecksquareValueOrderRuleVO result = component.check("line-001", indicator(), null,
|
||||||
|
LocalDateTime.of(2026, 5, 1, 0, 0), LocalDateTime.of(2026, 5, 1, 0, 2), 1);
|
||||||
|
|
||||||
|
Assertions.assertEquals(Integer.valueOf(2), result.getAbnormalPointCount());
|
||||||
|
Assertions.assertEquals(Boolean.TRUE, result.getAbnormal());
|
||||||
|
Assertions.assertEquals(2, result.getAbnormalDetails().size());
|
||||||
|
Assertions.assertEquals("2026-05-01 00:00:00", result.getAbnormalDetails().get(0).getTime());
|
||||||
|
Assertions.assertEquals("A", result.getAbnormalDetails().get(0).getPhase());
|
||||||
|
Assertions.assertEquals(new BigDecimal("8"), result.getAbnormalDetails().get(0).getMaxValue());
|
||||||
|
Assertions.assertEquals(new BigDecimal("1"), result.getAbnormalDetails().get(0).getMinValue());
|
||||||
|
Assertions.assertEquals(new BigDecimal("7"), result.getAbnormalDetails().get(0).getAvgValue());
|
||||||
|
Assertions.assertEquals(new BigDecimal("9"), result.getAbnormalDetails().get(0).getCp95Value());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldTreatEqualAdjacentStatValuesAsNormal() {
|
||||||
|
SteadyChecksquareInfluxQueryComponent influxQueryComponent = mock(SteadyChecksquareInfluxQueryComponent.class);
|
||||||
|
SteadyChecksquareValueOrderRuleComponent component = new SteadyChecksquareValueOrderRuleComponent(influxQueryComponent);
|
||||||
|
LocalDateTime time = LocalDateTime.of(2026, 5, 1, 0, 0);
|
||||||
|
when(influxQueryComponent.queryValuePoints(any(), any(LocalDateTime.class), any(LocalDateTime.class), eq(1)))
|
||||||
|
.thenAnswer(invocation -> {
|
||||||
|
String statType = invocation.getArgument(0, com.njcn.gather.steady.datavie.pojo.bo.SteadyTrendResolvedFieldBO.class).getStatType();
|
||||||
|
if ("MAX".equals(statType)) {
|
||||||
|
return Collections.singletonList(point(time, "10"));
|
||||||
|
}
|
||||||
|
if ("CP95".equals(statType)) {
|
||||||
|
return Collections.singletonList(point(time, "10"));
|
||||||
|
}
|
||||||
|
if ("AVG".equals(statType)) {
|
||||||
|
return Collections.singletonList(point(time, "8"));
|
||||||
|
}
|
||||||
|
if ("MIN".equals(statType)) {
|
||||||
|
return Collections.singletonList(point(time, "8"));
|
||||||
|
}
|
||||||
|
return Collections.emptyList();
|
||||||
|
});
|
||||||
|
|
||||||
|
SteadyChecksquareValueOrderRuleVO result = component.check("line-001", indicator(), null,
|
||||||
|
LocalDateTime.of(2026, 5, 1, 0, 0), LocalDateTime.of(2026, 5, 1, 0, 1), 1);
|
||||||
|
|
||||||
|
Assertions.assertEquals(Integer.valueOf(0), result.getAbnormalPointCount());
|
||||||
|
Assertions.assertEquals(Boolean.FALSE, result.getAbnormal());
|
||||||
|
Assertions.assertTrue(result.getAbnormalDetails().isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldNotMarkIndicatorAbnormalWhenOnlyOneInvalidPointExists() {
|
||||||
|
SteadyChecksquareInfluxQueryComponent influxQueryComponent = mock(SteadyChecksquareInfluxQueryComponent.class);
|
||||||
|
SteadyChecksquareValueOrderRuleComponent component = new SteadyChecksquareValueOrderRuleComponent(influxQueryComponent);
|
||||||
|
LocalDateTime time = LocalDateTime.of(2026, 5, 1, 0, 0);
|
||||||
|
when(influxQueryComponent.queryValuePoints(any(), any(LocalDateTime.class), any(LocalDateTime.class), eq(1)))
|
||||||
|
.thenAnswer(invocation -> {
|
||||||
|
String statType = invocation.getArgument(0, com.njcn.gather.steady.datavie.pojo.bo.SteadyTrendResolvedFieldBO.class).getStatType();
|
||||||
|
if ("MAX".equals(statType)) {
|
||||||
|
return Collections.singletonList(point(time, "8"));
|
||||||
|
}
|
||||||
|
if ("CP95".equals(statType)) {
|
||||||
|
return Collections.singletonList(point(time, "10"));
|
||||||
|
}
|
||||||
|
if ("AVG".equals(statType)) {
|
||||||
|
return Collections.singletonList(point(time, "8"));
|
||||||
|
}
|
||||||
|
if ("MIN".equals(statType)) {
|
||||||
|
return Collections.singletonList(point(time, "1"));
|
||||||
|
}
|
||||||
|
return Collections.emptyList();
|
||||||
|
});
|
||||||
|
|
||||||
|
SteadyChecksquareValueOrderRuleVO result = component.check("line-001", indicator(), null,
|
||||||
|
LocalDateTime.of(2026, 5, 1, 0, 0), LocalDateTime.of(2026, 5, 1, 0, 1), 1);
|
||||||
|
|
||||||
|
Assertions.assertEquals(Integer.valueOf(1), result.getAbnormalPointCount());
|
||||||
|
Assertions.assertEquals(Boolean.FALSE, result.getAbnormal());
|
||||||
|
Assertions.assertEquals(1, result.getAbnormalDetails().size());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldFillHarmonicOrderInAbnormalDetailForHarmonicIndicator() {
|
||||||
|
SteadyChecksquareInfluxQueryComponent influxQueryComponent = mock(SteadyChecksquareInfluxQueryComponent.class);
|
||||||
|
SteadyChecksquareValueOrderRuleComponent component = new SteadyChecksquareValueOrderRuleComponent(influxQueryComponent);
|
||||||
|
LocalDateTime time = LocalDateTime.of(2026, 5, 1, 0, 0);
|
||||||
|
when(influxQueryComponent.queryValuePoints(any(), any(LocalDateTime.class), any(LocalDateTime.class), eq(1)))
|
||||||
|
.thenAnswer(invocation -> {
|
||||||
|
String statType = invocation.getArgument(0, com.njcn.gather.steady.datavie.pojo.bo.SteadyTrendResolvedFieldBO.class).getStatType();
|
||||||
|
if ("MAX".equals(statType)) {
|
||||||
|
return Collections.singletonList(point(time, "8"));
|
||||||
|
}
|
||||||
|
if ("CP95".equals(statType)) {
|
||||||
|
return Collections.singletonList(point(time, "10"));
|
||||||
|
}
|
||||||
|
if ("AVG".equals(statType)) {
|
||||||
|
return Collections.singletonList(point(time, "8"));
|
||||||
|
}
|
||||||
|
if ("MIN".equals(statType)) {
|
||||||
|
return Collections.singletonList(point(time, "1"));
|
||||||
|
}
|
||||||
|
return Collections.emptyList();
|
||||||
|
});
|
||||||
|
SteadyTrendIndicatorDefinitionBO indicator = indicator();
|
||||||
|
indicator.setHarmonic(true);
|
||||||
|
indicator.setHarmonicFieldPrefix("v");
|
||||||
|
|
||||||
|
SteadyChecksquareValueOrderRuleVO result = component.check("line-001", indicator, 5,
|
||||||
|
LocalDateTime.of(2026, 5, 1, 0, 0), LocalDateTime.of(2026, 5, 1, 0, 1), 1);
|
||||||
|
|
||||||
|
Assertions.assertEquals(1, result.getAbnormalDetails().size());
|
||||||
|
Assertions.assertEquals(Integer.valueOf(5), result.getAbnormalDetails().get(0).getHarmonicOrder());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldSkipPointWhenAnyRequiredStatValueMissing() {
|
||||||
|
SteadyChecksquareInfluxQueryComponent influxQueryComponent = mock(SteadyChecksquareInfluxQueryComponent.class);
|
||||||
|
SteadyChecksquareValueOrderRuleComponent component = new SteadyChecksquareValueOrderRuleComponent(influxQueryComponent);
|
||||||
|
LocalDateTime time = LocalDateTime.of(2026, 5, 1, 0, 0);
|
||||||
|
when(influxQueryComponent.queryValuePoints(any(), any(LocalDateTime.class), any(LocalDateTime.class), eq(1)))
|
||||||
|
.thenAnswer(invocation -> {
|
||||||
|
String statType = invocation.getArgument(0, com.njcn.gather.steady.datavie.pojo.bo.SteadyTrendResolvedFieldBO.class).getStatType();
|
||||||
|
if ("MAX".equals(statType)) {
|
||||||
|
return Collections.singletonList(point(time, "10"));
|
||||||
|
}
|
||||||
|
if ("CP95".equals(statType)) {
|
||||||
|
return Collections.singletonList(point(time, "11"));
|
||||||
|
}
|
||||||
|
if ("MIN".equals(statType)) {
|
||||||
|
return Collections.singletonList(point(time, "1"));
|
||||||
|
}
|
||||||
|
return Collections.emptyList();
|
||||||
|
});
|
||||||
|
|
||||||
|
SteadyChecksquareValueOrderRuleVO result = component.check("line-001", indicator(), null,
|
||||||
|
LocalDateTime.of(2026, 5, 1, 0, 0), LocalDateTime.of(2026, 5, 1, 0, 1), 1);
|
||||||
|
|
||||||
|
Assertions.assertEquals(Integer.valueOf(0), result.getAbnormalPointCount());
|
||||||
|
Assertions.assertEquals(Boolean.FALSE, result.getAbnormal());
|
||||||
|
Assertions.assertTrue(result.getAbnormalDetails().isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldSkipIndicatorWhenNotAllFourStatsSupported() {
|
||||||
|
SteadyChecksquareInfluxQueryComponent influxQueryComponent = mock(SteadyChecksquareInfluxQueryComponent.class);
|
||||||
|
SteadyChecksquareValueOrderRuleComponent component = new SteadyChecksquareValueOrderRuleComponent(influxQueryComponent);
|
||||||
|
SteadyTrendIndicatorDefinitionBO indicator = indicator();
|
||||||
|
indicator.setSupportStats(Collections.singletonList("AVG"));
|
||||||
|
|
||||||
|
SteadyChecksquareValueOrderRuleVO result = component.check("line-001", indicator, null,
|
||||||
|
LocalDateTime.of(2026, 5, 1, 0, 0), LocalDateTime.of(2026, 5, 1, 0, 1), 1);
|
||||||
|
|
||||||
|
Assertions.assertEquals(Integer.valueOf(0), result.getAbnormalPointCount());
|
||||||
|
Assertions.assertEquals(Boolean.FALSE, result.getAbnormal());
|
||||||
|
Assertions.assertTrue(result.getAbnormalDetails().isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
private SteadyTrendIndicatorDefinitionBO indicator() {
|
||||||
|
SteadyTrendIndicatorDefinitionBO indicator = new SteadyTrendIndicatorDefinitionBO();
|
||||||
|
indicator.setIndicatorCode("V_RMS");
|
||||||
|
indicator.setName("相电压有效值");
|
||||||
|
indicator.setTableName("data_v");
|
||||||
|
indicator.setPhaseCodes(Collections.singletonList("A"));
|
||||||
|
indicator.setSeriesFields(Collections.singletonList(new SteadyTrendSeriesFieldBO("rms", "相电压有效值")));
|
||||||
|
indicator.setSupportStats(Arrays.asList("AVG", "MAX", "MIN", "CP95"));
|
||||||
|
indicator.setUnit("V");
|
||||||
|
return indicator;
|
||||||
|
}
|
||||||
|
|
||||||
|
private SteadyChecksquareValuePointBO point(LocalDateTime time, String value) {
|
||||||
|
SteadyChecksquareValuePointBO point = new SteadyChecksquareValuePointBO();
|
||||||
|
point.setTime(time);
|
||||||
|
point.setValue(new BigDecimal(value));
|
||||||
|
return point;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
package com.njcn.gather.steady.checksquare.controller;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Assertions;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 数据校验接口契约测试。
|
||||||
|
*/
|
||||||
|
class SteadyChecksquareControllerTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldExposeChecksquareQueryEndpointInSeparateController() throws Exception {
|
||||||
|
RequestMapping requestMapping = SteadyChecksquareController.class.getAnnotation(RequestMapping.class);
|
||||||
|
Assertions.assertArrayEquals(new String[]{"/steady/data-view/checksquare"}, requestMapping.value());
|
||||||
|
|
||||||
|
Method queryMethod = SteadyChecksquareController.class.getDeclaredMethod("query", com.njcn.gather.steady.checksquare.pojo.param.SteadyChecksquareHistoryQueryParam.class);
|
||||||
|
PostMapping queryMapping = queryMethod.getAnnotation(PostMapping.class);
|
||||||
|
Assertions.assertArrayEquals(new String[]{"/query"}, queryMapping.value());
|
||||||
|
|
||||||
|
Method createMethod = SteadyChecksquareController.class.getDeclaredMethod("create", com.njcn.gather.steady.checksquare.pojo.param.SteadyChecksquareQueryParam.class);
|
||||||
|
PostMapping createMapping = createMethod.getAnnotation(PostMapping.class);
|
||||||
|
Assertions.assertArrayEquals(new String[]{"/create"}, createMapping.value());
|
||||||
|
|
||||||
|
Method detailMethod = SteadyChecksquareController.class.getDeclaredMethod("detail", String.class);
|
||||||
|
GetMapping detailMapping = detailMethod.getAnnotation(GetMapping.class);
|
||||||
|
Assertions.assertArrayEquals(new String[]{"/detail"}, detailMapping.value());
|
||||||
|
|
||||||
|
Method itemDetailMethod = SteadyChecksquareController.class.getDeclaredMethod("itemDetail",
|
||||||
|
String.class, String.class, String.class, Integer.class, Integer.class);
|
||||||
|
GetMapping itemDetailMapping = itemDetailMethod.getAnnotation(GetMapping.class);
|
||||||
|
Assertions.assertArrayEquals(new String[]{"/item-detail"}, itemDetailMapping.value());
|
||||||
|
|
||||||
|
Method deleteMethod = SteadyChecksquareController.class.getDeclaredMethod("delete", List.class);
|
||||||
|
PostMapping deleteMapping = deleteMethod.getAnnotation(PostMapping.class);
|
||||||
|
Assertions.assertArrayEquals(new String[]{"/delete"}, deleteMapping.value());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
package com.njcn.gather.steady.checksquare.pojo.param;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Assertions;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.lang.reflect.Field;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 数据校验查询参数契约测试。
|
||||||
|
*/
|
||||||
|
class SteadyChecksquareQueryParamTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldOnlyExposeChecksquareQueryFields() {
|
||||||
|
Assertions.assertNotNull(field("lineId"));
|
||||||
|
Assertions.assertNotNull(field("indicatorCodes"));
|
||||||
|
Assertions.assertNotNull(field("timeStart"));
|
||||||
|
Assertions.assertNotNull(field("timeEnd"));
|
||||||
|
Assertions.assertNull(field("qualityFlag"));
|
||||||
|
Assertions.assertNull(field("statTypes"));
|
||||||
|
Assertions.assertNull(field("phases"));
|
||||||
|
Assertions.assertNull(field("harmonicOrders"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private Field field(String name) {
|
||||||
|
try {
|
||||||
|
return SteadyChecksquareQueryParam.class.getDeclaredField(name);
|
||||||
|
} catch (NoSuchFieldException ex) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,714 @@
|
|||||||
|
package com.njcn.gather.steady.checksquare.service.impl;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper;
|
||||||
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||||
|
import com.njcn.gather.steady.checksquare.component.SteadyChecksquareCalculator;
|
||||||
|
import com.njcn.gather.steady.checksquare.component.SteadyChecksquareHarmonicParityRuleComponent;
|
||||||
|
import com.njcn.gather.steady.checksquare.component.SteadyChecksquareInfluxQueryComponent;
|
||||||
|
import com.njcn.gather.steady.checksquare.component.SteadyChecksquareValueOrderRuleComponent;
|
||||||
|
import com.njcn.gather.steady.checksquare.pojo.param.SteadyChecksquareQueryParam;
|
||||||
|
import com.njcn.gather.steady.checksquare.pojo.po.SteadyChecksquareDetailPO;
|
||||||
|
import com.njcn.gather.steady.checksquare.pojo.po.SteadyChecksquareItemPO;
|
||||||
|
import com.njcn.gather.steady.checksquare.pojo.po.SteadyChecksquareStatSummaryPO;
|
||||||
|
import com.njcn.gather.steady.checksquare.pojo.po.SteadyChecksquareTaskPO;
|
||||||
|
import com.njcn.gather.steady.checksquare.pojo.vo.SteadyChecksquareHarmonicParityDetailVO;
|
||||||
|
import com.njcn.gather.steady.checksquare.pojo.vo.SteadyChecksquareHarmonicParityRuleVO;
|
||||||
|
import com.njcn.gather.steady.checksquare.pojo.vo.SteadyChecksquareItemDetailVO;
|
||||||
|
import com.njcn.gather.steady.checksquare.pojo.vo.SteadyChecksquareItemVO;
|
||||||
|
import com.njcn.gather.steady.checksquare.pojo.vo.SteadyChecksquareQueryVO;
|
||||||
|
import com.njcn.gather.steady.checksquare.pojo.vo.SteadyChecksquareStatSummaryVO;
|
||||||
|
import com.njcn.gather.steady.checksquare.pojo.vo.SteadyChecksquareValueOrderDetailVO;
|
||||||
|
import com.njcn.gather.steady.checksquare.pojo.vo.SteadyChecksquareValueOrderRuleVO;
|
||||||
|
import com.njcn.gather.steady.checksquare.service.SteadyChecksquareDetailService;
|
||||||
|
import com.njcn.gather.steady.checksquare.service.SteadyChecksquareItemService;
|
||||||
|
import com.njcn.gather.steady.checksquare.service.SteadyChecksquareStatSummaryService;
|
||||||
|
import com.njcn.gather.steady.checksquare.service.SteadyChecksquareTaskService;
|
||||||
|
import com.njcn.gather.steady.datavie.component.SteadyTrendIndicatorCatalog;
|
||||||
|
import com.njcn.gather.steady.datavie.pojo.bo.SteadyTrendIndicatorDefinitionBO;
|
||||||
|
import com.njcn.gather.steady.datavie.pojo.bo.SteadyTrendResolvedFieldBO;
|
||||||
|
import com.njcn.gather.tool.adddata.component.AddDataTimeSlotCalculator;
|
||||||
|
import com.njcn.gather.tool.addledger.pojo.vo.AddLedgerLinePathVO;
|
||||||
|
import com.njcn.gather.tool.addledger.service.AddLedgerService;
|
||||||
|
import org.junit.jupiter.api.Assertions;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.mockito.ArgumentCaptor;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.ArgumentMatchers.anyInt;
|
||||||
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.times;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 数据校验服务测试。
|
||||||
|
*/
|
||||||
|
class SteadyChecksquareServiceImplTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldNotOpenTransactionAroundCreateCalculation() throws Exception {
|
||||||
|
Method createMethod = SteadyChecksquareServiceImpl.class.getMethod("create", SteadyChecksquareQueryParam.class);
|
||||||
|
|
||||||
|
Assertions.assertNull(createMethod.getAnnotation(Transactional.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldUseFixedFlickerIntervalsPerIndicator() {
|
||||||
|
SteadyChecksquareInfluxQueryComponent influxQueryComponent = mock(SteadyChecksquareInfluxQueryComponent.class);
|
||||||
|
SteadyChecksquareValueOrderRuleComponent valueOrderRuleComponent = mock(SteadyChecksquareValueOrderRuleComponent.class);
|
||||||
|
SteadyChecksquareHarmonicParityRuleComponent harmonicParityRuleComponent = mock(SteadyChecksquareHarmonicParityRuleComponent.class);
|
||||||
|
AddLedgerService addLedgerService = mock(AddLedgerService.class);
|
||||||
|
SteadyChecksquareServiceImpl service = new SteadyChecksquareServiceImpl(new SteadyTrendIndicatorCatalog(),
|
||||||
|
influxQueryComponent, new SteadyChecksquareCalculator(), valueOrderRuleComponent, harmonicParityRuleComponent,
|
||||||
|
new AddDataTimeSlotCalculator(), addLedgerService, mock(SteadyChecksquareTaskService.class),
|
||||||
|
mock(SteadyChecksquareItemService.class), mock(SteadyChecksquareStatSummaryService.class),
|
||||||
|
mock(SteadyChecksquareDetailService.class), new ObjectMapper());
|
||||||
|
when(valueOrderRuleComponent.check(any(), any(), any(), any(LocalDateTime.class), any(LocalDateTime.class), anyInt()))
|
||||||
|
.thenReturn(emptyRuleResult());
|
||||||
|
when(harmonicParityRuleComponent.check(any(), any(), any(LocalDateTime.class), any(LocalDateTime.class), anyInt()))
|
||||||
|
.thenReturn(emptyHarmonicParityRuleResult());
|
||||||
|
AddLedgerLinePathVO linePath = new AddLedgerLinePathVO();
|
||||||
|
linePath.setLineId("line-001");
|
||||||
|
linePath.setLineName("进线一");
|
||||||
|
linePath.setLineInterval(1);
|
||||||
|
when(addLedgerService.listLinePathByLineIds(eq(Collections.singletonList("line-001"))))
|
||||||
|
.thenReturn(Collections.singletonMap("line-001", linePath));
|
||||||
|
when(influxQueryComponent.queryExistingSlots(any(), any(LocalDateTime.class), any(LocalDateTime.class), eq(10)))
|
||||||
|
.thenReturn(new HashSet<LocalDateTime>(Arrays.asList(
|
||||||
|
LocalDateTime.of(2026, 5, 1, 0, 0),
|
||||||
|
LocalDateTime.of(2026, 5, 1, 0, 10))));
|
||||||
|
when(influxQueryComponent.queryExistingSlots(any(), any(LocalDateTime.class), any(LocalDateTime.class), eq(120)))
|
||||||
|
.thenReturn(new HashSet<LocalDateTime>(Collections.singletonList(
|
||||||
|
LocalDateTime.of(2026, 5, 1, 0, 0))));
|
||||||
|
|
||||||
|
SteadyChecksquareQueryParam param = new SteadyChecksquareQueryParam();
|
||||||
|
param.setLineId("line-001");
|
||||||
|
param.setIndicatorCodes(Arrays.asList("FLUC", "PST", "PLT"));
|
||||||
|
param.setTimeStart("2026-05-01 00:00:00");
|
||||||
|
param.setTimeEnd("2026-05-01 02:00:00");
|
||||||
|
|
||||||
|
SteadyChecksquareQueryVO result = calculate(service, param);
|
||||||
|
|
||||||
|
Assertions.assertEquals(Integer.valueOf(1), result.getIntervalMinutes());
|
||||||
|
Assertions.assertEquals(3, result.getItems().size());
|
||||||
|
assertItemInterval(result.getItems().get(0), "FLUC", 10, 13);
|
||||||
|
assertItemInterval(result.getItems().get(1), "PST", 10, 13);
|
||||||
|
assertItemInterval(result.getItems().get(2), "PLT", 120, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldAggregateAllHarmonicOrdersIntoIndicatorItem() {
|
||||||
|
SteadyChecksquareInfluxQueryComponent influxQueryComponent = mock(SteadyChecksquareInfluxQueryComponent.class);
|
||||||
|
SteadyChecksquareValueOrderRuleComponent valueOrderRuleComponent = mock(SteadyChecksquareValueOrderRuleComponent.class);
|
||||||
|
SteadyChecksquareHarmonicParityRuleComponent harmonicParityRuleComponent = mock(SteadyChecksquareHarmonicParityRuleComponent.class);
|
||||||
|
AddLedgerService addLedgerService = mock(AddLedgerService.class);
|
||||||
|
SteadyChecksquareServiceImpl service = new SteadyChecksquareServiceImpl(new SteadyTrendIndicatorCatalog(),
|
||||||
|
influxQueryComponent, new SteadyChecksquareCalculator(), valueOrderRuleComponent, harmonicParityRuleComponent,
|
||||||
|
new AddDataTimeSlotCalculator(), addLedgerService, mock(SteadyChecksquareTaskService.class),
|
||||||
|
mock(SteadyChecksquareItemService.class), mock(SteadyChecksquareStatSummaryService.class),
|
||||||
|
mock(SteadyChecksquareDetailService.class), new ObjectMapper());
|
||||||
|
when(valueOrderRuleComponent.check(any(), any(), any(), any(LocalDateTime.class), any(LocalDateTime.class), anyInt()))
|
||||||
|
.thenReturn(emptyRuleResult());
|
||||||
|
when(harmonicParityRuleComponent.check(any(), any(), any(LocalDateTime.class), any(LocalDateTime.class), anyInt()))
|
||||||
|
.thenReturn(emptyHarmonicParityRuleResult());
|
||||||
|
AddLedgerLinePathVO linePath = new AddLedgerLinePathVO();
|
||||||
|
linePath.setLineId("line-001");
|
||||||
|
linePath.setLineName("进线一");
|
||||||
|
linePath.setLineInterval(1);
|
||||||
|
when(addLedgerService.listLinePathByLineIds(eq(Collections.singletonList("line-001"))))
|
||||||
|
.thenReturn(Collections.singletonMap("line-001", linePath));
|
||||||
|
when(influxQueryComponent.queryExistingSlots(any(), any(LocalDateTime.class), any(LocalDateTime.class), eq(1)))
|
||||||
|
.thenReturn(new HashSet<LocalDateTime>(Collections.singletonList(
|
||||||
|
LocalDateTime.of(2026, 5, 1, 0, 0))));
|
||||||
|
|
||||||
|
SteadyChecksquareQueryParam param = new SteadyChecksquareQueryParam();
|
||||||
|
param.setLineId("line-001");
|
||||||
|
param.setIndicatorCodes(Collections.singletonList("V_HARMONIC"));
|
||||||
|
param.setTimeStart("2026-05-01 00:00:00");
|
||||||
|
param.setTimeEnd("2026-05-01 00:01:00");
|
||||||
|
|
||||||
|
SteadyChecksquareQueryVO result = calculate(service, param);
|
||||||
|
|
||||||
|
Assertions.assertEquals(1, result.getItems().size());
|
||||||
|
Assertions.assertEquals("line-001|V_HARMONIC", result.getItems().get(0).getItemKey());
|
||||||
|
Assertions.assertNull(result.getItems().get(0).getHarmonicOrder());
|
||||||
|
Assertions.assertEquals(Integer.valueOf(2), result.getItems().get(0).getStatDetails().get(0).getSegments().get(0).getHarmonicOrder());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldAverageHarmonicOrderResultsAndMarkAbnormalWhenAnyOrderAbnormal() {
|
||||||
|
SteadyChecksquareInfluxQueryComponent influxQueryComponent = mock(SteadyChecksquareInfluxQueryComponent.class);
|
||||||
|
SteadyChecksquareValueOrderRuleComponent valueOrderRuleComponent = mock(SteadyChecksquareValueOrderRuleComponent.class);
|
||||||
|
SteadyChecksquareHarmonicParityRuleComponent harmonicParityRuleComponent = mock(SteadyChecksquareHarmonicParityRuleComponent.class);
|
||||||
|
AddLedgerService addLedgerService = mock(AddLedgerService.class);
|
||||||
|
SteadyChecksquareServiceImpl service = new SteadyChecksquareServiceImpl(new SteadyTrendIndicatorCatalog(),
|
||||||
|
influxQueryComponent, new SteadyChecksquareCalculator(), valueOrderRuleComponent, harmonicParityRuleComponent,
|
||||||
|
new AddDataTimeSlotCalculator(), addLedgerService, mock(SteadyChecksquareTaskService.class),
|
||||||
|
mock(SteadyChecksquareItemService.class), mock(SteadyChecksquareStatSummaryService.class),
|
||||||
|
mock(SteadyChecksquareDetailService.class), new ObjectMapper());
|
||||||
|
when(valueOrderRuleComponent.check(any(), any(), any(), any(LocalDateTime.class), any(LocalDateTime.class), anyInt()))
|
||||||
|
.thenReturn(emptyRuleResult());
|
||||||
|
when(harmonicParityRuleComponent.check(any(), any(), any(LocalDateTime.class), any(LocalDateTime.class), anyInt()))
|
||||||
|
.thenReturn(emptyHarmonicParityRuleResult());
|
||||||
|
AddLedgerLinePathVO linePath = new AddLedgerLinePathVO();
|
||||||
|
linePath.setLineId("line-001");
|
||||||
|
linePath.setLineName("进线一");
|
||||||
|
linePath.setLineInterval(1);
|
||||||
|
when(addLedgerService.listLinePathByLineIds(eq(Collections.singletonList("line-001"))))
|
||||||
|
.thenReturn(Collections.singletonMap("line-001", linePath));
|
||||||
|
when(influxQueryComponent.queryExistingSlots(any(), any(LocalDateTime.class), any(LocalDateTime.class), eq(1)))
|
||||||
|
.thenReturn(new HashSet<LocalDateTime>(Collections.singletonList(
|
||||||
|
LocalDateTime.of(2026, 5, 1, 0, 0))));
|
||||||
|
SteadyChecksquareValueOrderRuleVO normalRuleResult = new SteadyChecksquareValueOrderRuleVO();
|
||||||
|
SteadyChecksquareValueOrderRuleVO abnormalRuleResult = new SteadyChecksquareValueOrderRuleVO();
|
||||||
|
SteadyChecksquareValueOrderDetailVO abnormalDetail = new SteadyChecksquareValueOrderDetailVO();
|
||||||
|
abnormalDetail.setTime("2026-05-01 00:00:00");
|
||||||
|
abnormalDetail.setPhase("A");
|
||||||
|
abnormalDetail.setHarmonicOrder(2);
|
||||||
|
abnormalRuleResult.setAbnormal(true);
|
||||||
|
abnormalRuleResult.setAbnormalPointCount(4);
|
||||||
|
abnormalRuleResult.setAbnormalDetails(Collections.singletonList(abnormalDetail));
|
||||||
|
when(valueOrderRuleComponent.check(any(), any(), any(), any(LocalDateTime.class), any(LocalDateTime.class), anyInt()))
|
||||||
|
.thenReturn(normalRuleResult);
|
||||||
|
when(valueOrderRuleComponent.check(any(), any(), eq(2), any(LocalDateTime.class), any(LocalDateTime.class), anyInt()))
|
||||||
|
.thenReturn(abnormalRuleResult);
|
||||||
|
when(valueOrderRuleComponent.check(any(), any(), eq(3), any(LocalDateTime.class), any(LocalDateTime.class), anyInt()))
|
||||||
|
.thenReturn(normalRuleResult);
|
||||||
|
|
||||||
|
SteadyChecksquareQueryParam param = new SteadyChecksquareQueryParam();
|
||||||
|
param.setLineId("line-001");
|
||||||
|
param.setIndicatorCodes(Collections.singletonList("V_HARMONIC"));
|
||||||
|
param.setTimeStart("2026-05-01 00:00:00");
|
||||||
|
param.setTimeEnd("2026-05-01 00:01:00");
|
||||||
|
|
||||||
|
SteadyChecksquareQueryVO result = calculate(service, param);
|
||||||
|
|
||||||
|
List<SteadyChecksquareItemVO> items = result.getItems();
|
||||||
|
Assertions.assertEquals(1, items.size());
|
||||||
|
Assertions.assertEquals(Boolean.TRUE, items.get(0).getAbnormal());
|
||||||
|
Assertions.assertEquals(Integer.valueOf(1), items.get(0).getAbnormalPointCount());
|
||||||
|
Assertions.assertEquals(1, items.get(0).getAbnormalDetails().size());
|
||||||
|
Assertions.assertEquals(Integer.valueOf(2), items.get(0).getAbnormalDetails().get(0).getHarmonicOrder());
|
||||||
|
Assertions.assertEquals(Integer.valueOf(8), items.get(0).getExpectedPointCount());
|
||||||
|
Assertions.assertEquals(Integer.valueOf(4), items.get(0).getActualPointCount());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldAssembleValueOrderRuleResultIntoItem() {
|
||||||
|
SteadyChecksquareInfluxQueryComponent influxQueryComponent = mock(SteadyChecksquareInfluxQueryComponent.class);
|
||||||
|
SteadyChecksquareValueOrderRuleComponent valueOrderRuleComponent = mock(SteadyChecksquareValueOrderRuleComponent.class);
|
||||||
|
SteadyChecksquareHarmonicParityRuleComponent harmonicParityRuleComponent = mock(SteadyChecksquareHarmonicParityRuleComponent.class);
|
||||||
|
AddLedgerService addLedgerService = mock(AddLedgerService.class);
|
||||||
|
SteadyChecksquareServiceImpl service = new SteadyChecksquareServiceImpl(new SteadyTrendIndicatorCatalog(),
|
||||||
|
influxQueryComponent, new SteadyChecksquareCalculator(), valueOrderRuleComponent, harmonicParityRuleComponent,
|
||||||
|
new AddDataTimeSlotCalculator(), addLedgerService, mock(SteadyChecksquareTaskService.class),
|
||||||
|
mock(SteadyChecksquareItemService.class), mock(SteadyChecksquareStatSummaryService.class),
|
||||||
|
mock(SteadyChecksquareDetailService.class), new ObjectMapper());
|
||||||
|
AddLedgerLinePathVO linePath = new AddLedgerLinePathVO();
|
||||||
|
linePath.setLineId("line-001");
|
||||||
|
linePath.setLineName("进线一");
|
||||||
|
linePath.setLineInterval(1);
|
||||||
|
when(addLedgerService.listLinePathByLineIds(eq(Collections.singletonList("line-001"))))
|
||||||
|
.thenReturn(Collections.singletonMap("line-001", linePath));
|
||||||
|
when(influxQueryComponent.queryExistingSlots(any(), any(LocalDateTime.class), any(LocalDateTime.class), eq(1)))
|
||||||
|
.thenReturn(new HashSet<LocalDateTime>(Collections.singletonList(
|
||||||
|
LocalDateTime.of(2026, 5, 1, 0, 0))));
|
||||||
|
SteadyChecksquareValueOrderRuleVO ruleResult = new SteadyChecksquareValueOrderRuleVO();
|
||||||
|
SteadyChecksquareValueOrderDetailVO detail = new SteadyChecksquareValueOrderDetailVO();
|
||||||
|
detail.setTime("2026-05-01 00:00:00");
|
||||||
|
detail.setPhase("A");
|
||||||
|
ruleResult.setAbnormalPointCount(2);
|
||||||
|
ruleResult.setAbnormal(true);
|
||||||
|
ruleResult.setAbnormalDetails(Collections.singletonList(detail));
|
||||||
|
when(valueOrderRuleComponent.check(any(), any(), any(), any(LocalDateTime.class), any(LocalDateTime.class), eq(1)))
|
||||||
|
.thenReturn(ruleResult);
|
||||||
|
|
||||||
|
SteadyChecksquareQueryParam param = new SteadyChecksquareQueryParam();
|
||||||
|
param.setLineId("line-001");
|
||||||
|
param.setIndicatorCodes(Collections.singletonList("V_RMS"));
|
||||||
|
param.setTimeStart("2026-05-01 00:00:00");
|
||||||
|
param.setTimeEnd("2026-05-01 00:01:00");
|
||||||
|
|
||||||
|
SteadyChecksquareQueryVO result = calculate(service, param);
|
||||||
|
|
||||||
|
SteadyChecksquareItemVO item = result.getItems().get(0);
|
||||||
|
Assertions.assertEquals(Boolean.TRUE, item.getAbnormal());
|
||||||
|
Assertions.assertEquals(Integer.valueOf(2), item.getAbnormalPointCount());
|
||||||
|
Assertions.assertEquals(1, item.getAbnormalDetails().size());
|
||||||
|
Assertions.assertEquals("A", item.getAbnormalDetails().get(0).getPhase());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldPrefetchNormalIndicatorFieldsByMeasurementPhaseAndStat() {
|
||||||
|
SteadyChecksquareInfluxQueryComponent influxQueryComponent = mock(SteadyChecksquareInfluxQueryComponent.class);
|
||||||
|
SteadyChecksquareValueOrderRuleComponent valueOrderRuleComponent = mock(SteadyChecksquareValueOrderRuleComponent.class);
|
||||||
|
SteadyChecksquareHarmonicParityRuleComponent harmonicParityRuleComponent = mock(SteadyChecksquareHarmonicParityRuleComponent.class);
|
||||||
|
AddLedgerService addLedgerService = mock(AddLedgerService.class);
|
||||||
|
SteadyChecksquareServiceImpl service = new SteadyChecksquareServiceImpl(new SteadyTrendIndicatorCatalog(),
|
||||||
|
influxQueryComponent, new SteadyChecksquareCalculator(), valueOrderRuleComponent, harmonicParityRuleComponent,
|
||||||
|
new AddDataTimeSlotCalculator(), addLedgerService, mock(SteadyChecksquareTaskService.class),
|
||||||
|
mock(SteadyChecksquareItemService.class), mock(SteadyChecksquareStatSummaryService.class),
|
||||||
|
mock(SteadyChecksquareDetailService.class), new ObjectMapper());
|
||||||
|
when(valueOrderRuleComponent.check(any(), any(), any(), any(LocalDateTime.class), any(LocalDateTime.class), anyInt()))
|
||||||
|
.thenReturn(emptyRuleResult());
|
||||||
|
when(harmonicParityRuleComponent.check(any(), any(), any(LocalDateTime.class), any(LocalDateTime.class), anyInt()))
|
||||||
|
.thenReturn(emptyHarmonicParityRuleResult());
|
||||||
|
AddLedgerLinePathVO linePath = new AddLedgerLinePathVO();
|
||||||
|
linePath.setLineId("line-001");
|
||||||
|
linePath.setLineName("进线一");
|
||||||
|
linePath.setLineInterval(1);
|
||||||
|
when(addLedgerService.listLinePathByLineIds(eq(Collections.singletonList("line-001"))))
|
||||||
|
.thenReturn(Collections.singletonMap("line-001", linePath));
|
||||||
|
when(influxQueryComponent.queryExistingSlots(any(), any(LocalDateTime.class), any(LocalDateTime.class), eq(1)))
|
||||||
|
.thenReturn(new HashSet<LocalDateTime>(Collections.singletonList(
|
||||||
|
LocalDateTime.of(2026, 5, 1, 0, 0))));
|
||||||
|
|
||||||
|
SteadyChecksquareQueryParam param = new SteadyChecksquareQueryParam();
|
||||||
|
param.setLineId("line-001");
|
||||||
|
param.setIndicatorCodes(Arrays.asList("V_RMS", "V_LINE_RMS"));
|
||||||
|
param.setTimeStart("2026-05-01 00:00:00");
|
||||||
|
param.setTimeEnd("2026-05-01 00:01:00");
|
||||||
|
|
||||||
|
calculate(service, param);
|
||||||
|
|
||||||
|
ArgumentCaptor<List> captor = ArgumentCaptor.forClass(List.class);
|
||||||
|
verify(influxQueryComponent, times(12)).queryValuePointMap(captor.capture(),
|
||||||
|
any(LocalDateTime.class), any(LocalDateTime.class), eq(1));
|
||||||
|
boolean foundBatch = false;
|
||||||
|
for (List fields : captor.getAllValues()) {
|
||||||
|
if (fields.size() == 2) {
|
||||||
|
List<String> fieldNames = new ArrayList<String>();
|
||||||
|
for (Object field : fields) {
|
||||||
|
fieldNames.add(((SteadyTrendResolvedFieldBO) field).getField());
|
||||||
|
}
|
||||||
|
foundBatch = fieldNames.contains("rms") && fieldNames.contains("rms_lvr");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Assertions.assertTrue(foundBatch);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldAssembleHarmonicParityRuleResultIntoAggregateItem() {
|
||||||
|
SteadyChecksquareInfluxQueryComponent influxQueryComponent = mock(SteadyChecksquareInfluxQueryComponent.class);
|
||||||
|
SteadyChecksquareValueOrderRuleComponent valueOrderRuleComponent = mock(SteadyChecksquareValueOrderRuleComponent.class);
|
||||||
|
SteadyChecksquareHarmonicParityRuleComponent harmonicParityRuleComponent = mock(SteadyChecksquareHarmonicParityRuleComponent.class);
|
||||||
|
AddLedgerService addLedgerService = mock(AddLedgerService.class);
|
||||||
|
SteadyChecksquareServiceImpl service = new SteadyChecksquareServiceImpl(new SteadyTrendIndicatorCatalog(),
|
||||||
|
influxQueryComponent, new SteadyChecksquareCalculator(), valueOrderRuleComponent, harmonicParityRuleComponent,
|
||||||
|
new AddDataTimeSlotCalculator(), addLedgerService, mock(SteadyChecksquareTaskService.class),
|
||||||
|
mock(SteadyChecksquareItemService.class), mock(SteadyChecksquareStatSummaryService.class),
|
||||||
|
mock(SteadyChecksquareDetailService.class), new ObjectMapper());
|
||||||
|
when(valueOrderRuleComponent.check(any(), any(), any(), any(LocalDateTime.class), any(LocalDateTime.class), anyInt()))
|
||||||
|
.thenReturn(emptyRuleResult());
|
||||||
|
AddLedgerLinePathVO linePath = new AddLedgerLinePathVO();
|
||||||
|
linePath.setLineId("line-001");
|
||||||
|
linePath.setLineName("进线一");
|
||||||
|
linePath.setLineInterval(1);
|
||||||
|
when(addLedgerService.listLinePathByLineIds(eq(Collections.singletonList("line-001"))))
|
||||||
|
.thenReturn(Collections.singletonMap("line-001", linePath));
|
||||||
|
when(influxQueryComponent.queryExistingSlots(any(), any(LocalDateTime.class), any(LocalDateTime.class), eq(1)))
|
||||||
|
.thenReturn(new HashSet<LocalDateTime>(Collections.singletonList(
|
||||||
|
LocalDateTime.of(2026, 5, 1, 0, 0))));
|
||||||
|
SteadyChecksquareHarmonicParityRuleVO ruleResult = new SteadyChecksquareHarmonicParityRuleVO();
|
||||||
|
SteadyChecksquareHarmonicParityDetailVO detail = new SteadyChecksquareHarmonicParityDetailVO();
|
||||||
|
detail.setTime("2026-05-01 00:00:00");
|
||||||
|
detail.setPhase("A");
|
||||||
|
detail.setStatType("AVG");
|
||||||
|
detail.setEvenHarmonicOrder(4);
|
||||||
|
ruleResult.setAbnormal(true);
|
||||||
|
ruleResult.setAbnormalPointCount(1);
|
||||||
|
ruleResult.setAbnormalDetails(Collections.singletonList(detail));
|
||||||
|
when(harmonicParityRuleComponent.check(any(), any(), any(LocalDateTime.class), any(LocalDateTime.class), eq(1)))
|
||||||
|
.thenReturn(ruleResult);
|
||||||
|
|
||||||
|
SteadyChecksquareQueryParam param = new SteadyChecksquareQueryParam();
|
||||||
|
param.setLineId("line-001");
|
||||||
|
param.setIndicatorCodes(Collections.singletonList("V_HARMONIC"));
|
||||||
|
param.setTimeStart("2026-05-01 00:00:00");
|
||||||
|
param.setTimeEnd("2026-05-01 00:01:00");
|
||||||
|
|
||||||
|
SteadyChecksquareQueryVO result = calculate(service, param);
|
||||||
|
|
||||||
|
SteadyChecksquareItemVO item = result.getItems().get(0);
|
||||||
|
Assertions.assertNull(item.getHarmonicOrder());
|
||||||
|
Assertions.assertEquals(Boolean.TRUE, item.getHarmonicParityAbnormal());
|
||||||
|
Assertions.assertEquals(Integer.valueOf(1), item.getHarmonicParityAbnormalPointCount());
|
||||||
|
Assertions.assertEquals("AVG", item.getHarmonicParityAbnormalDetails().get(0).getStatType());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldRejectUnsupportedItemDetailType() {
|
||||||
|
SteadyChecksquareItemService itemService = mock(SteadyChecksquareItemService.class);
|
||||||
|
SteadyChecksquareItemPO item = new SteadyChecksquareItemPO();
|
||||||
|
item.setId("item-001");
|
||||||
|
item.setState(1);
|
||||||
|
when(itemService.getById("item-001")).thenReturn(item);
|
||||||
|
SteadyChecksquareServiceImpl service = new SteadyChecksquareServiceImpl(new SteadyTrendIndicatorCatalog(),
|
||||||
|
mock(SteadyChecksquareInfluxQueryComponent.class), new SteadyChecksquareCalculator(),
|
||||||
|
mock(SteadyChecksquareValueOrderRuleComponent.class), mock(SteadyChecksquareHarmonicParityRuleComponent.class),
|
||||||
|
new AddDataTimeSlotCalculator(), mock(AddLedgerService.class), mock(SteadyChecksquareTaskService.class),
|
||||||
|
itemService, mock(SteadyChecksquareStatSummaryService.class),
|
||||||
|
mock(SteadyChecksquareDetailService.class), new ObjectMapper());
|
||||||
|
|
||||||
|
Assertions.assertThrows(RuntimeException.class, () -> service.itemDetail("item-001", "UNKNOWN", null));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldLoadDetailSummariesInSingleBatch() {
|
||||||
|
SteadyChecksquareTaskService taskService = mock(SteadyChecksquareTaskService.class);
|
||||||
|
SteadyChecksquareItemService itemService = mock(SteadyChecksquareItemService.class);
|
||||||
|
SteadyChecksquareStatSummaryService statSummaryService = mock(SteadyChecksquareStatSummaryService.class);
|
||||||
|
LambdaQueryChainWrapper<SteadyChecksquareItemPO> itemQuery = mock(LambdaQueryChainWrapper.class);
|
||||||
|
LambdaQueryChainWrapper<SteadyChecksquareStatSummaryPO> summaryQuery = mock(LambdaQueryChainWrapper.class);
|
||||||
|
SteadyChecksquareTaskPO task = new SteadyChecksquareTaskPO();
|
||||||
|
task.setId("task-001");
|
||||||
|
task.setState(1);
|
||||||
|
task.setLineId("line-001");
|
||||||
|
task.setLineName("进线一");
|
||||||
|
task.setTimeStart(LocalDateTime.of(2026, 5, 1, 0, 0));
|
||||||
|
task.setTimeEnd(LocalDateTime.of(2026, 5, 1, 0, 1));
|
||||||
|
task.setIntervalMinutes(1);
|
||||||
|
SteadyChecksquareItemPO item1 = buildItemPO("item-001", "V_RMS");
|
||||||
|
SteadyChecksquareItemPO item2 = buildItemPO("item-002", "FREQ");
|
||||||
|
SteadyChecksquareStatSummaryPO summary1 = buildSummaryPO("item-001", "AVG");
|
||||||
|
SteadyChecksquareStatSummaryPO summary2 = buildSummaryPO("item-002", "AVG");
|
||||||
|
when(taskService.getById("task-001")).thenReturn(task);
|
||||||
|
when(itemService.lambdaQuery()).thenReturn(itemQuery);
|
||||||
|
when(itemQuery.eq(any(), any())).thenReturn(itemQuery);
|
||||||
|
when(itemQuery.list()).thenReturn(Arrays.asList(item1, item2));
|
||||||
|
when(statSummaryService.lambdaQuery()).thenReturn(summaryQuery);
|
||||||
|
when(summaryQuery.in(any(), any(List.class))).thenReturn(summaryQuery);
|
||||||
|
when(summaryQuery.list()).thenReturn(Arrays.asList(summary1, summary2));
|
||||||
|
SteadyChecksquareServiceImpl service = new SteadyChecksquareServiceImpl(new SteadyTrendIndicatorCatalog(),
|
||||||
|
mock(SteadyChecksquareInfluxQueryComponent.class), new SteadyChecksquareCalculator(),
|
||||||
|
mock(SteadyChecksquareValueOrderRuleComponent.class), mock(SteadyChecksquareHarmonicParityRuleComponent.class),
|
||||||
|
new AddDataTimeSlotCalculator(), mock(AddLedgerService.class), taskService,
|
||||||
|
itemService, statSummaryService, mock(SteadyChecksquareDetailService.class), new ObjectMapper());
|
||||||
|
|
||||||
|
SteadyChecksquareQueryVO result = service.detail("task-001");
|
||||||
|
|
||||||
|
Assertions.assertEquals(2, result.getItems().size());
|
||||||
|
Assertions.assertEquals(1, result.getItems().get(0).getStatSummaries().size());
|
||||||
|
Assertions.assertEquals(1, result.getItems().get(1).getStatSummaries().size());
|
||||||
|
verify(statSummaryService, times(1)).lambdaQuery();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldPageItemDetailWhenPageArgumentsPresent() {
|
||||||
|
SteadyChecksquareItemService itemService = mock(SteadyChecksquareItemService.class);
|
||||||
|
SteadyChecksquareDetailService detailService = mock(SteadyChecksquareDetailService.class);
|
||||||
|
SteadyChecksquareItemPO item = new SteadyChecksquareItemPO();
|
||||||
|
item.setId("item-001");
|
||||||
|
item.setState(1);
|
||||||
|
SteadyChecksquareDetailPO detail = new SteadyChecksquareDetailPO();
|
||||||
|
detail.setItemId("item-001");
|
||||||
|
detail.setDetailType("VALUE_ORDER");
|
||||||
|
detail.setPointTime(LocalDateTime.of(2026, 5, 1, 0, 0));
|
||||||
|
detail.setPhase("A");
|
||||||
|
Page<SteadyChecksquareDetailPO> page = new Page<SteadyChecksquareDetailPO>(2, 1);
|
||||||
|
page.setTotal(1);
|
||||||
|
page.setRecords(Collections.singletonList(detail));
|
||||||
|
when(itemService.getById("item-001")).thenReturn(item);
|
||||||
|
when(detailService.page(any(Page.class), any())).thenReturn(page);
|
||||||
|
SteadyChecksquareServiceImpl service = new SteadyChecksquareServiceImpl(new SteadyTrendIndicatorCatalog(),
|
||||||
|
mock(SteadyChecksquareInfluxQueryComponent.class), new SteadyChecksquareCalculator(),
|
||||||
|
mock(SteadyChecksquareValueOrderRuleComponent.class), mock(SteadyChecksquareHarmonicParityRuleComponent.class),
|
||||||
|
new AddDataTimeSlotCalculator(), mock(AddLedgerService.class), mock(SteadyChecksquareTaskService.class),
|
||||||
|
itemService, mock(SteadyChecksquareStatSummaryService.class),
|
||||||
|
detailService, new ObjectMapper());
|
||||||
|
|
||||||
|
SteadyChecksquareItemDetailVO result = service.itemDetail("item-001", "VALUE_ORDER", null, 2, 1);
|
||||||
|
|
||||||
|
Assertions.assertEquals(Integer.valueOf(2), result.getPageNum());
|
||||||
|
Assertions.assertEquals(Integer.valueOf(1), result.getPageSize());
|
||||||
|
Assertions.assertEquals(Long.valueOf(1L), result.getTotal());
|
||||||
|
Assertions.assertEquals(1, result.getValueOrderDetails().size());
|
||||||
|
verify(detailService).page(any(Page.class), any());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldDeleteTasksAndItemsLogically() {
|
||||||
|
SteadyChecksquareTaskService taskService = mock(SteadyChecksquareTaskService.class);
|
||||||
|
SteadyChecksquareItemService itemService = mock(SteadyChecksquareItemService.class);
|
||||||
|
LambdaQueryChainWrapper<SteadyChecksquareTaskPO> taskQuery = mock(LambdaQueryChainWrapper.class);
|
||||||
|
SteadyChecksquareTaskPO task = new SteadyChecksquareTaskPO();
|
||||||
|
task.setId("task-001");
|
||||||
|
task.setState(1);
|
||||||
|
when(taskService.lambdaQuery()).thenReturn(taskQuery);
|
||||||
|
when(taskQuery.in(any(), any(List.class))).thenReturn(taskQuery);
|
||||||
|
when(taskQuery.eq(any(), any())).thenReturn(taskQuery);
|
||||||
|
when(taskQuery.list()).thenReturn(Collections.singletonList(task));
|
||||||
|
when(taskService.update(any())).thenReturn(true);
|
||||||
|
when(itemService.update(any())).thenReturn(true);
|
||||||
|
SteadyChecksquareServiceImpl service = new SteadyChecksquareServiceImpl(new SteadyTrendIndicatorCatalog(),
|
||||||
|
mock(SteadyChecksquareInfluxQueryComponent.class), new SteadyChecksquareCalculator(),
|
||||||
|
mock(SteadyChecksquareValueOrderRuleComponent.class), mock(SteadyChecksquareHarmonicParityRuleComponent.class),
|
||||||
|
new AddDataTimeSlotCalculator(), mock(AddLedgerService.class), taskService,
|
||||||
|
itemService, mock(SteadyChecksquareStatSummaryService.class),
|
||||||
|
mock(SteadyChecksquareDetailService.class), new ObjectMapper());
|
||||||
|
|
||||||
|
boolean result = service.delete(Collections.singletonList("task-001"));
|
||||||
|
|
||||||
|
Assertions.assertTrue(result);
|
||||||
|
verify(taskService).update(any());
|
||||||
|
verify(itemService).update(any());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldSaveChecksquareResultsInBatch() {
|
||||||
|
SteadyChecksquareTaskService taskService = mock(SteadyChecksquareTaskService.class);
|
||||||
|
SteadyChecksquareItemService itemService = mock(SteadyChecksquareItemService.class);
|
||||||
|
SteadyChecksquareStatSummaryService statSummaryService = mock(SteadyChecksquareStatSummaryService.class);
|
||||||
|
SteadyChecksquareDetailService detailService = mock(SteadyChecksquareDetailService.class);
|
||||||
|
SteadyChecksquareServiceImpl service = new SteadyChecksquareServiceImpl(new SteadyTrendIndicatorCatalog(),
|
||||||
|
mock(SteadyChecksquareInfluxQueryComponent.class), new SteadyChecksquareCalculator(),
|
||||||
|
mock(SteadyChecksquareValueOrderRuleComponent.class), mock(SteadyChecksquareHarmonicParityRuleComponent.class),
|
||||||
|
new AddDataTimeSlotCalculator(), mock(AddLedgerService.class), taskService,
|
||||||
|
itemService, statSummaryService, detailService, new ObjectMapper());
|
||||||
|
SteadyChecksquareQueryParam param = new SteadyChecksquareQueryParam();
|
||||||
|
param.setIndicatorCodes(Collections.singletonList("V_RMS"));
|
||||||
|
SteadyChecksquareQueryVO result = new SteadyChecksquareQueryVO();
|
||||||
|
result.setLineId("line-001");
|
||||||
|
result.setLineName("进线一");
|
||||||
|
result.setTimeStart("2026-05-01 00:00:00");
|
||||||
|
result.setTimeEnd("2026-05-01 00:01:00");
|
||||||
|
result.setIntervalMinutes(1);
|
||||||
|
SteadyChecksquareItemVO item = new SteadyChecksquareItemVO();
|
||||||
|
item.setItemKey("line-001|V_RMS");
|
||||||
|
item.setIndicatorCode("V_RMS");
|
||||||
|
item.setIndicatorName("相电压有效值");
|
||||||
|
item.setIntervalMinutes(1);
|
||||||
|
item.setHasData(true);
|
||||||
|
item.setExpectedPointCount(2);
|
||||||
|
item.setActualPointCount(2);
|
||||||
|
item.setMissingPointCount(0);
|
||||||
|
item.setDataIntegrity(BigDecimal.ONE.setScale(6));
|
||||||
|
item.setDataIntegrityText("100.00%");
|
||||||
|
item.setAbnormal(false);
|
||||||
|
item.setAbnormalPointCount(0);
|
||||||
|
item.setHarmonicParityAbnormal(false);
|
||||||
|
item.setHarmonicParityAbnormalPointCount(0);
|
||||||
|
SteadyChecksquareStatSummaryVO summary = new SteadyChecksquareStatSummaryVO();
|
||||||
|
summary.setStatType("AVG");
|
||||||
|
summary.setSupported(true);
|
||||||
|
summary.setHasData(true);
|
||||||
|
summary.setExpectedPointCount(2);
|
||||||
|
summary.setActualPointCount(2);
|
||||||
|
summary.setMissingPointCount(0);
|
||||||
|
summary.setDataIntegrity(BigDecimal.ONE.setScale(6));
|
||||||
|
summary.setDataIntegrityText("100.00%");
|
||||||
|
item.getStatSummaries().add(summary);
|
||||||
|
result.getItems().add(item);
|
||||||
|
|
||||||
|
saveResult(service, param, result);
|
||||||
|
|
||||||
|
verify(taskService).save(any());
|
||||||
|
verify(itemService).saveBatch(any());
|
||||||
|
verify(statSummaryService).saveBatch(any());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldCountNoDataItemAsAbnormalWhenSavingTask() {
|
||||||
|
SteadyChecksquareTaskService taskService = mock(SteadyChecksquareTaskService.class);
|
||||||
|
SteadyChecksquareServiceImpl service = new SteadyChecksquareServiceImpl(new SteadyTrendIndicatorCatalog(),
|
||||||
|
mock(SteadyChecksquareInfluxQueryComponent.class), new SteadyChecksquareCalculator(),
|
||||||
|
mock(SteadyChecksquareValueOrderRuleComponent.class), mock(SteadyChecksquareHarmonicParityRuleComponent.class),
|
||||||
|
new AddDataTimeSlotCalculator(), mock(AddLedgerService.class), taskService,
|
||||||
|
mock(SteadyChecksquareItemService.class), mock(SteadyChecksquareStatSummaryService.class),
|
||||||
|
mock(SteadyChecksquareDetailService.class), new ObjectMapper());
|
||||||
|
SteadyChecksquareQueryParam param = new SteadyChecksquareQueryParam();
|
||||||
|
param.setIndicatorCodes(Collections.singletonList("V_RMS"));
|
||||||
|
SteadyChecksquareQueryVO result = new SteadyChecksquareQueryVO();
|
||||||
|
result.setLineId("line-001");
|
||||||
|
result.setLineName("进线一");
|
||||||
|
result.setTimeStart("2026-05-01 00:00:00");
|
||||||
|
result.setTimeEnd("2026-05-01 00:01:00");
|
||||||
|
result.setIntervalMinutes(1);
|
||||||
|
SteadyChecksquareItemVO item = new SteadyChecksquareItemVO();
|
||||||
|
item.setItemKey("line-001|V_RMS");
|
||||||
|
item.setIndicatorCode("V_RMS");
|
||||||
|
item.setHasData(false);
|
||||||
|
item.setExpectedPointCount(2);
|
||||||
|
item.setActualPointCount(0);
|
||||||
|
item.setMissingPointCount(2);
|
||||||
|
item.setDataIntegrity(BigDecimal.ZERO.setScale(6));
|
||||||
|
item.setDataIntegrityText("0.00%");
|
||||||
|
item.setAbnormal(false);
|
||||||
|
item.setHarmonicParityAbnormal(false);
|
||||||
|
result.getItems().add(item);
|
||||||
|
|
||||||
|
saveResult(service, param, result);
|
||||||
|
|
||||||
|
ArgumentCaptor<SteadyChecksquareTaskPO> captor = ArgumentCaptor.forClass(SteadyChecksquareTaskPO.class);
|
||||||
|
verify(taskService).save(captor.capture());
|
||||||
|
Assertions.assertEquals(Integer.valueOf(1), captor.getValue().getAbnormalItemCount());
|
||||||
|
Assertions.assertEquals(BigDecimal.ZERO.setScale(6), captor.getValue().getMinDataIntegrity());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldMarkAggregateHarmonicItemNoDataWhenDataIntegrityIsZero() {
|
||||||
|
SteadyChecksquareServiceImpl service = newService();
|
||||||
|
SteadyTrendIndicatorDefinitionBO indicator = buildHarmonicIndicator();
|
||||||
|
SteadyChecksquareItemVO orderItem = buildOrderItem(true, BigDecimal.ZERO.setScale(6));
|
||||||
|
orderItem.getStatSummaries().add(buildSummaryVO(true, BigDecimal.ZERO.setScale(6)));
|
||||||
|
|
||||||
|
SteadyChecksquareItemVO result = aggregateHarmonicItems(service, indicator, Collections.singletonList(orderItem));
|
||||||
|
|
||||||
|
Assertions.assertEquals(BigDecimal.ZERO.setScale(6), result.getDataIntegrity());
|
||||||
|
Assertions.assertEquals(Boolean.FALSE, result.getHasData());
|
||||||
|
Assertions.assertEquals(Boolean.FALSE, result.getStatSummaries().get(0).getHasData());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldMarkAggregateHarmonicItemHasDataWhenDataIntegrityIsGreaterThanZero() {
|
||||||
|
SteadyChecksquareServiceImpl service = newService();
|
||||||
|
SteadyTrendIndicatorDefinitionBO indicator = buildHarmonicIndicator();
|
||||||
|
SteadyChecksquareItemVO orderItem = buildOrderItem(false, new BigDecimal("0.500000"));
|
||||||
|
orderItem.getStatSummaries().add(buildSummaryVO(false, new BigDecimal("0.500000")));
|
||||||
|
|
||||||
|
SteadyChecksquareItemVO result = aggregateHarmonicItems(service, indicator, Collections.singletonList(orderItem));
|
||||||
|
|
||||||
|
Assertions.assertEquals(new BigDecimal("0.500000"), result.getDataIntegrity());
|
||||||
|
Assertions.assertEquals(Boolean.TRUE, result.getHasData());
|
||||||
|
Assertions.assertEquals(Boolean.TRUE, result.getStatSummaries().get(0).getHasData());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void assertItemInterval(SteadyChecksquareItemVO item, String indicatorCode, int intervalMinutes, int expectedPointCount) {
|
||||||
|
Assertions.assertEquals(indicatorCode, item.getIndicatorCode());
|
||||||
|
Assertions.assertEquals(Integer.valueOf(intervalMinutes), item.getIntervalMinutes());
|
||||||
|
Assertions.assertEquals(Integer.valueOf(expectedPointCount), item.getExpectedPointCount());
|
||||||
|
}
|
||||||
|
|
||||||
|
private SteadyChecksquareServiceImpl newService() {
|
||||||
|
return new SteadyChecksquareServiceImpl(new SteadyTrendIndicatorCatalog(),
|
||||||
|
mock(SteadyChecksquareInfluxQueryComponent.class), new SteadyChecksquareCalculator(),
|
||||||
|
mock(SteadyChecksquareValueOrderRuleComponent.class), mock(SteadyChecksquareHarmonicParityRuleComponent.class),
|
||||||
|
new AddDataTimeSlotCalculator(), mock(AddLedgerService.class), mock(SteadyChecksquareTaskService.class),
|
||||||
|
mock(SteadyChecksquareItemService.class), mock(SteadyChecksquareStatSummaryService.class),
|
||||||
|
mock(SteadyChecksquareDetailService.class), new ObjectMapper());
|
||||||
|
}
|
||||||
|
|
||||||
|
private SteadyChecksquareQueryVO calculate(SteadyChecksquareServiceImpl service, SteadyChecksquareQueryParam param) {
|
||||||
|
try {
|
||||||
|
Method method = SteadyChecksquareServiceImpl.class.getDeclaredMethod("calculate", SteadyChecksquareQueryParam.class);
|
||||||
|
method.setAccessible(true);
|
||||||
|
return (SteadyChecksquareQueryVO) method.invoke(service, param);
|
||||||
|
} catch (Exception exception) {
|
||||||
|
throw new RuntimeException(exception);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void saveResult(SteadyChecksquareServiceImpl service, SteadyChecksquareQueryParam param, SteadyChecksquareQueryVO result) {
|
||||||
|
try {
|
||||||
|
Method method = SteadyChecksquareServiceImpl.class.getDeclaredMethod("saveResult",
|
||||||
|
SteadyChecksquareQueryParam.class, SteadyChecksquareQueryVO.class);
|
||||||
|
method.setAccessible(true);
|
||||||
|
method.invoke(service, param, result);
|
||||||
|
} catch (Exception exception) {
|
||||||
|
throw new RuntimeException(exception);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private SteadyChecksquareItemVO aggregateHarmonicItems(SteadyChecksquareServiceImpl service,
|
||||||
|
SteadyTrendIndicatorDefinitionBO indicator,
|
||||||
|
List<SteadyChecksquareItemVO> orderItems) {
|
||||||
|
try {
|
||||||
|
Method method = SteadyChecksquareServiceImpl.class.getDeclaredMethod("aggregateHarmonicItems",
|
||||||
|
String.class, SteadyTrendIndicatorDefinitionBO.class, List.class, int.class);
|
||||||
|
method.setAccessible(true);
|
||||||
|
return (SteadyChecksquareItemVO) method.invoke(service, "line-001", indicator, orderItems, 1);
|
||||||
|
} catch (Exception exception) {
|
||||||
|
throw new RuntimeException(exception);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private SteadyTrendIndicatorDefinitionBO buildHarmonicIndicator() {
|
||||||
|
SteadyTrendIndicatorDefinitionBO indicator = new SteadyTrendIndicatorDefinitionBO();
|
||||||
|
indicator.setIndicatorCode("V_HARMONIC");
|
||||||
|
indicator.setName("V_HARMONIC");
|
||||||
|
indicator.setHarmonic(true);
|
||||||
|
return indicator;
|
||||||
|
}
|
||||||
|
|
||||||
|
private SteadyChecksquareItemVO buildOrderItem(boolean hasData, BigDecimal dataIntegrity) {
|
||||||
|
SteadyChecksquareItemVO item = new SteadyChecksquareItemVO();
|
||||||
|
item.setItemKey("line-001|V_HARMONIC|2");
|
||||||
|
item.setIndicatorCode("V_HARMONIC");
|
||||||
|
item.setIndicatorName("V_HARMONIC");
|
||||||
|
item.setHarmonicOrder(2);
|
||||||
|
item.setIntervalMinutes(1);
|
||||||
|
item.setHasData(hasData);
|
||||||
|
item.setExpectedPointCount(2);
|
||||||
|
item.setActualPointCount(dataIntegrity.compareTo(BigDecimal.ZERO) > 0 ? 1 : 0);
|
||||||
|
item.setMissingPointCount(dataIntegrity.compareTo(BigDecimal.ZERO) > 0 ? 1 : 2);
|
||||||
|
item.setDataIntegrity(dataIntegrity);
|
||||||
|
item.setAbnormal(false);
|
||||||
|
item.setAbnormalPointCount(0);
|
||||||
|
item.setHarmonicParityAbnormal(false);
|
||||||
|
item.setHarmonicParityAbnormalPointCount(0);
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
|
||||||
|
private SteadyChecksquareStatSummaryVO buildSummaryVO(boolean hasData, BigDecimal dataIntegrity) {
|
||||||
|
SteadyChecksquareStatSummaryVO summary = new SteadyChecksquareStatSummaryVO();
|
||||||
|
summary.setStatType("AVG");
|
||||||
|
summary.setSupported(true);
|
||||||
|
summary.setHasData(hasData);
|
||||||
|
summary.setExpectedPointCount(2);
|
||||||
|
summary.setActualPointCount(dataIntegrity.compareTo(BigDecimal.ZERO) > 0 ? 1 : 0);
|
||||||
|
summary.setMissingPointCount(dataIntegrity.compareTo(BigDecimal.ZERO) > 0 ? 1 : 2);
|
||||||
|
summary.setDataIntegrity(dataIntegrity);
|
||||||
|
return summary;
|
||||||
|
}
|
||||||
|
|
||||||
|
private SteadyChecksquareItemPO buildItemPO(String itemId, String indicatorCode) {
|
||||||
|
SteadyChecksquareItemPO item = new SteadyChecksquareItemPO();
|
||||||
|
item.setId(itemId);
|
||||||
|
item.setIndicatorCode(indicatorCode);
|
||||||
|
item.setIndicatorName(indicatorCode);
|
||||||
|
item.setState(1);
|
||||||
|
item.setHasData(1);
|
||||||
|
item.setExpectedPointCount(1);
|
||||||
|
item.setActualPointCount(1);
|
||||||
|
item.setMissingPointCount(0);
|
||||||
|
item.setDataIntegrity(BigDecimal.ONE.setScale(6));
|
||||||
|
item.setAbnormal(0);
|
||||||
|
item.setAbnormalPointCount(0);
|
||||||
|
item.setHarmonicParityAbnormal(0);
|
||||||
|
item.setHarmonicParityAbnormalPointCount(0);
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
|
||||||
|
private SteadyChecksquareStatSummaryPO buildSummaryPO(String itemId, String statType) {
|
||||||
|
SteadyChecksquareStatSummaryPO summary = new SteadyChecksquareStatSummaryPO();
|
||||||
|
summary.setItemId(itemId);
|
||||||
|
summary.setStatType(statType);
|
||||||
|
summary.setSupported(1);
|
||||||
|
summary.setHasData(1);
|
||||||
|
summary.setExpectedPointCount(1);
|
||||||
|
summary.setActualPointCount(1);
|
||||||
|
summary.setMissingPointCount(0);
|
||||||
|
summary.setDataIntegrity(BigDecimal.ONE.setScale(6));
|
||||||
|
return summary;
|
||||||
|
}
|
||||||
|
|
||||||
|
private SteadyChecksquareValueOrderRuleVO emptyRuleResult() {
|
||||||
|
return new SteadyChecksquareValueOrderRuleVO();
|
||||||
|
}
|
||||||
|
|
||||||
|
private SteadyChecksquareHarmonicParityRuleVO emptyHarmonicParityRuleResult() {
|
||||||
|
return new SteadyChecksquareHarmonicParityRuleVO();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,44 +8,112 @@ import org.junit.jupiter.api.Test;
|
|||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* InfluxQL 查询语句生成测试。
|
* 稳态趋势 InfluxQL 构造契约测试。
|
||||||
*/
|
*/
|
||||||
class SteadyInfluxQueryComponentTest {
|
class SteadyInfluxQueryComponentTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldBuildBucketedTrendQueryWithRequiredTags() {
|
void shouldBuildRawPointQueryWithoutTimeBucketAggregation() {
|
||||||
SteadyInfluxQueryComponent component = new SteadyInfluxQueryComponent(new SteadyInfluxDbProperties());
|
SteadyInfluxQueryComponent component = new SteadyInfluxQueryComponent(new SteadyInfluxDbProperties());
|
||||||
SteadyTrendResolvedFieldBO field = new SteadyTrendResolvedFieldBO();
|
SteadyTrendResolvedFieldBO field = new SteadyTrendResolvedFieldBO();
|
||||||
field.setMeasurement("data_v");
|
field.setMeasurement("data_v");
|
||||||
field.setField("RMS_CP95");
|
field.setField("RMS");
|
||||||
field.setLineId("line-001");
|
field.setLineId("line-001");
|
||||||
field.setPhase("A");
|
field.setPhase("A");
|
||||||
|
|
||||||
String query = component.buildTrendQuery(field,
|
String query = component.buildTrendQuery(field,
|
||||||
LocalDateTime.of(2026, 5, 1, 0, 0, 0),
|
LocalDateTime.of(2026, 5, 1, 0, 0, 0),
|
||||||
LocalDateTime.of(2026, 5, 1, 1, 0, 0),
|
LocalDateTime.of(2026, 5, 1, 23, 59, 59),
|
||||||
"10m",
|
|
||||||
1);
|
1);
|
||||||
|
|
||||||
Assertions.assertEquals("SELECT mean(\"RMS_CP95\") AS \"value\" FROM \"data_v\" WHERE time >= '2026-05-01T00:00:00Z' AND time <= '2026-05-01T01:00:00Z' AND \"LINEID\" = 'line-001' AND \"PHASIC_TYPE\" = 'A' AND \"QUALITYFLAG\" = '1' GROUP BY time(10m) fill(none)", query);
|
Assertions.assertTrue(query.startsWith("SELECT \"RMS\" AS \"value\""), "趋势查询应读取原始字段值");
|
||||||
|
Assertions.assertTrue(query.endsWith(" ORDER BY time ASC"), "趋势查询应按原始时间升序返回");
|
||||||
|
Assertions.assertFalse(query.contains("mean("), "趋势查询不应按颗粒度聚合");
|
||||||
|
Assertions.assertFalse(query.contains("GROUP BY time"), "趋势查询不应生成时间分桶");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldEscapeTagValuesInTrendQuery() {
|
void shouldBuildQueryWithActualInfluxTagsAndValueType() {
|
||||||
SteadyInfluxQueryComponent component = new SteadyInfluxQueryComponent(new SteadyInfluxDbProperties());
|
SteadyInfluxQueryComponent component = new SteadyInfluxQueryComponent(new SteadyInfluxDbProperties());
|
||||||
SteadyTrendResolvedFieldBO field = new SteadyTrendResolvedFieldBO();
|
SteadyTrendResolvedFieldBO field = new SteadyTrendResolvedFieldBO();
|
||||||
field.setMeasurement("data_v");
|
field.setMeasurement("data_v");
|
||||||
field.setField("RMS");
|
field.setField("rms");
|
||||||
field.setLineId("line'001");
|
field.setLineId("00B78D00A8791");
|
||||||
|
field.setPhase("A");
|
||||||
|
field.setStatType("AVG");
|
||||||
|
|
||||||
|
String query = component.buildTrendQuery(field,
|
||||||
|
LocalDateTime.of(2026, 5, 1, 0, 0, 0),
|
||||||
|
LocalDateTime.of(2026, 5, 1, 23, 59, 59),
|
||||||
|
0);
|
||||||
|
|
||||||
|
Assertions.assertTrue(query.contains("\"line_id\" = '00B78D00A8791'"));
|
||||||
|
Assertions.assertTrue(query.contains("\"phasic_type\" = 'A'"));
|
||||||
|
Assertions.assertTrue(query.contains("\"quality_flag\" = '0'"));
|
||||||
|
Assertions.assertTrue(query.contains("\"value_type\" = 'AVG'"));
|
||||||
|
Assertions.assertFalse(query.contains("\"LINEID\""));
|
||||||
|
Assertions.assertFalse(query.contains("\"PHASIC_TYPE\""));
|
||||||
|
Assertions.assertFalse(query.contains("\"QUALITYFLAG\""));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldDefaultValueTypeToUpperAvg() {
|
||||||
|
SteadyInfluxQueryComponent component = new SteadyInfluxQueryComponent(new SteadyInfluxDbProperties());
|
||||||
|
SteadyTrendResolvedFieldBO field = new SteadyTrendResolvedFieldBO();
|
||||||
|
field.setMeasurement("data_v");
|
||||||
|
field.setField("rms");
|
||||||
|
field.setLineId("0798e5a9a72ebbc4daaa4a631701c813");
|
||||||
field.setPhase("A");
|
field.setPhase("A");
|
||||||
|
|
||||||
String query = component.buildTrendQuery(field,
|
String query = component.buildTrendQuery(field,
|
||||||
LocalDateTime.of(2026, 5, 1, 0, 0, 0),
|
LocalDateTime.of(2026, 5, 1, 0, 0, 0),
|
||||||
LocalDateTime.of(2026, 5, 1, 1, 0, 0),
|
LocalDateTime.of(2026, 5, 1, 23, 59, 59),
|
||||||
null,
|
0);
|
||||||
null);
|
|
||||||
|
|
||||||
Assertions.assertTrue(query.contains("\"LINEID\" = 'line\\'001'"));
|
Assertions.assertTrue(query.contains("\"value_type\" = 'AVG'"));
|
||||||
Assertions.assertFalse(query.contains("GROUP BY time"));
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldBuildDiagnosticTextForTrendQuery() {
|
||||||
|
SteadyInfluxQueryComponent component = new SteadyInfluxQueryComponent(new SteadyInfluxDbProperties());
|
||||||
|
SteadyTrendResolvedFieldBO field = new SteadyTrendResolvedFieldBO();
|
||||||
|
field.setMeasurement("data_harmpower_p");
|
||||||
|
field.setField("p_3");
|
||||||
|
field.setLineId("f828bc42132841c2aeebc6859f5a9b7c");
|
||||||
|
field.setPhase("A");
|
||||||
|
field.setStatType("AVG");
|
||||||
|
|
||||||
|
String diagnostic = component.buildTrendQueryDiagnostic(field,
|
||||||
|
LocalDateTime.of(2026, 5, 1, 0, 0, 0),
|
||||||
|
LocalDateTime.of(2026, 5, 31, 23, 59, 59),
|
||||||
|
0);
|
||||||
|
|
||||||
|
Assertions.assertTrue(diagnostic.contains("measurement=data_harmpower_p"));
|
||||||
|
Assertions.assertTrue(diagnostic.contains("field=p_3"));
|
||||||
|
Assertions.assertTrue(diagnostic.contains("lineId=f828bc42132841c2aeebc6859f5a9b7c"));
|
||||||
|
Assertions.assertTrue(diagnostic.contains("phase=A"));
|
||||||
|
Assertions.assertTrue(diagnostic.contains("statType=AVG"));
|
||||||
|
Assertions.assertTrue(diagnostic.contains("qualityFlag=0"));
|
||||||
|
Assertions.assertTrue(diagnostic.contains("timeStart=2026-05-01T00:00"));
|
||||||
|
Assertions.assertTrue(diagnostic.contains("timeEnd=2026-05-31T23:59:59"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldSkipValueTypeWhenMeasurementHasNoValueTypeTag() {
|
||||||
|
SteadyInfluxQueryComponent component = new SteadyInfluxQueryComponent(new SteadyInfluxDbProperties());
|
||||||
|
SteadyTrendResolvedFieldBO field = new SteadyTrendResolvedFieldBO();
|
||||||
|
field.setMeasurement("data_fluc");
|
||||||
|
field.setField("fluc");
|
||||||
|
field.setLineId("0798e5a9a72ebbc4daaa4a631701c813");
|
||||||
|
field.setPhase("T");
|
||||||
|
field.setStatType("AVG");
|
||||||
|
|
||||||
|
String query = component.buildTrendQuery(field,
|
||||||
|
LocalDateTime.of(2026, 5, 1, 0, 0, 0),
|
||||||
|
LocalDateTime.of(2026, 5, 1, 23, 59, 59),
|
||||||
|
0);
|
||||||
|
|
||||||
|
Assertions.assertFalse(query.contains("\"value_type\""));
|
||||||
|
Assertions.assertTrue(query.contains("\"quality_flag\" = '0'"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,13 +3,13 @@ package com.njcn.gather.steady.datavie.component;
|
|||||||
import com.njcn.common.pojo.exception.BusinessException;
|
import com.njcn.common.pojo.exception.BusinessException;
|
||||||
import com.njcn.gather.steady.datavie.pojo.bo.SteadyTrendResolvedFieldBO;
|
import com.njcn.gather.steady.datavie.pojo.bo.SteadyTrendResolvedFieldBO;
|
||||||
import com.njcn.gather.steady.datavie.pojo.param.SteadyTrendQueryParam;
|
import com.njcn.gather.steady.datavie.pojo.param.SteadyTrendQueryParam;
|
||||||
import com.njcn.gather.tool.adddata.component.AddDataTableRegistry;
|
|
||||||
import org.junit.jupiter.api.Assertions;
|
import org.junit.jupiter.api.Assertions;
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 稳态趋势字段解析测试。
|
* 稳态趋势字段解析测试。
|
||||||
@@ -19,49 +19,80 @@ class SteadyTrendFieldResolverTest {
|
|||||||
private SteadyTrendFieldResolver resolver;
|
private SteadyTrendFieldResolver resolver;
|
||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
void setUp() throws Exception {
|
void setUp() {
|
||||||
AddDataTableRegistry tableRegistry = new AddDataTableRegistry();
|
resolver = new SteadyTrendFieldResolver(new SteadyTrendIndicatorCatalog());
|
||||||
tableRegistry.afterPropertiesSet();
|
|
||||||
resolver = new SteadyTrendFieldResolver(new SteadyTrendIndicatorCatalog(), tableRegistry);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldResolveVoltageRmsAverageAndCp95Fields() {
|
void shouldExpandAllCatalogPhasesWithoutRequestPhaseFilter() {
|
||||||
SteadyTrendQueryParam param = new SteadyTrendQueryParam();
|
SteadyTrendQueryParam param = new SteadyTrendQueryParam();
|
||||||
param.setLineIds(Arrays.asList("line-001"));
|
param.setLineIds(Arrays.asList("line-001"));
|
||||||
param.setIndicatorCodes(Arrays.asList("V_RMS"));
|
param.setIndicatorCodes(Arrays.asList("V_RMS"));
|
||||||
param.setPhases(Arrays.asList("A"));
|
param.setStatTypes(Arrays.asList("AVG"));
|
||||||
param.setStatTypes(Arrays.asList("AVG", "CP95"));
|
|
||||||
param.setTimeStart("2026-05-01 00:00:00");
|
param.setTimeStart("2026-05-01 00:00:00");
|
||||||
param.setTimeEnd("2026-05-01 01:00:00");
|
param.setTimeEnd("2026-05-01 01:00:00");
|
||||||
|
|
||||||
List<SteadyTrendResolvedFieldBO> fields = resolver.resolveFields(param);
|
List<SteadyTrendResolvedFieldBO> fields = resolver.resolveFields(param);
|
||||||
|
List<String> phases = fields.stream().map(SteadyTrendResolvedFieldBO::getPhase).collect(Collectors.toList());
|
||||||
|
|
||||||
Assertions.assertEquals(2, fields.size());
|
Assertions.assertEquals(Arrays.asList("A", "B", "C"), phases);
|
||||||
Assertions.assertEquals("data_v", fields.get(0).getMeasurement());
|
|
||||||
Assertions.assertEquals("RMS", fields.get(0).getField());
|
|
||||||
Assertions.assertEquals("AVG", fields.get(0).getStatType());
|
|
||||||
Assertions.assertEquals("RMS_CP95", fields.get(1).getField());
|
|
||||||
Assertions.assertEquals("V", fields.get(1).getUnit());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldExpandLineVoltageTotalPhaseToThreeSeries() {
|
void shouldResolveVoltageRmsToActualInfluxField() {
|
||||||
SteadyTrendQueryParam param = new SteadyTrendQueryParam();
|
SteadyTrendQueryParam param = new SteadyTrendQueryParam();
|
||||||
param.setLineIds(Arrays.asList("line-001"));
|
param.setLineIds(Arrays.asList("00B78D00A8791"));
|
||||||
param.setIndicatorCodes(Arrays.asList("V_LINE_RMS"));
|
param.setIndicatorCodes(Arrays.asList("V_RMS"));
|
||||||
param.setPhases(Arrays.asList("T"));
|
|
||||||
param.setStatTypes(Arrays.asList("AVG"));
|
param.setStatTypes(Arrays.asList("AVG"));
|
||||||
param.setTimeStart("2026-05-01 00:00:00");
|
param.setTimeStart("2026-05-01 00:00:00");
|
||||||
param.setTimeEnd("2026-05-01 01:00:00");
|
param.setTimeEnd("2026-05-01 01:00:00");
|
||||||
|
|
||||||
List<SteadyTrendResolvedFieldBO> fields = resolver.resolveFields(param);
|
List<SteadyTrendResolvedFieldBO> fields = resolver.resolveFields(param);
|
||||||
|
|
||||||
Assertions.assertEquals(3, fields.size());
|
Assertions.assertEquals("rms", fields.get(0).getField());
|
||||||
Assertions.assertEquals("RMSAB", fields.get(0).getField());
|
Assertions.assertEquals("00B78D00A8791|V_RMS|A|AVG|rms", fields.get(0).getSeriesKey());
|
||||||
Assertions.assertEquals("RMSBC", fields.get(1).getField());
|
}
|
||||||
Assertions.assertEquals("RMSCA", fields.get(2).getField());
|
|
||||||
Assertions.assertEquals("T", fields.get(0).getPhase());
|
@Test
|
||||||
|
void shouldExpandLineVoltageIndicatorWithQueryPhases() {
|
||||||
|
SteadyTrendQueryParam param = new SteadyTrendQueryParam();
|
||||||
|
param.setLineIds(Arrays.asList("line-001"));
|
||||||
|
param.setIndicatorCodes(Arrays.asList("V_LINE_RMS"));
|
||||||
|
param.setStatTypes(Arrays.asList("AVG"));
|
||||||
|
param.setTimeStart("2026-05-01 00:00:00");
|
||||||
|
param.setTimeEnd("2026-05-01 01:00:00");
|
||||||
|
|
||||||
|
List<SteadyTrendResolvedFieldBO> fields = resolver.resolveFields(param);
|
||||||
|
List<String> phases = fields.stream().map(SteadyTrendResolvedFieldBO::getPhase).collect(Collectors.toList());
|
||||||
|
|
||||||
|
Assertions.assertEquals(Arrays.asList("A", "B", "C"), phases);
|
||||||
|
Assertions.assertEquals("rms_lvr", fields.get(0).getField());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldResolveCommonIndicatorsToActualInfluxFields() {
|
||||||
|
Assertions.assertEquals("freq", resolveSingleField("FREQ"));
|
||||||
|
Assertions.assertEquals("v_thd", resolveSingleField("V_THD"));
|
||||||
|
Assertions.assertEquals("rms", resolveSingleField("I_RMS"));
|
||||||
|
Assertions.assertEquals("i_thd", resolveSingleField("I_THD"));
|
||||||
|
Assertions.assertEquals("fluc", resolveSingleField("FLUC"));
|
||||||
|
Assertions.assertEquals("pst", resolveSingleField("PST"));
|
||||||
|
Assertions.assertEquals("plt", resolveSingleField("PLT"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldExposeActualInfluxFieldsInIndicatorCatalog() {
|
||||||
|
SteadyTrendIndicatorCatalog catalog = new SteadyTrendIndicatorCatalog();
|
||||||
|
|
||||||
|
Assertions.assertEquals("rms", catalog.getIndicator("V_RMS").getSeriesFields().get(0).getField());
|
||||||
|
Assertions.assertEquals("rms_lvr", catalog.getIndicator("V_LINE_RMS").getSeriesFields().get(0).getField());
|
||||||
|
Assertions.assertEquals("freq", catalog.getIndicator("FREQ").getSeriesFields().get(0).getField());
|
||||||
|
Assertions.assertEquals("v_thd", catalog.getIndicator("V_THD").getSeriesFields().get(0).getField());
|
||||||
|
Assertions.assertEquals("rms", catalog.getIndicator("I_RMS").getSeriesFields().get(0).getField());
|
||||||
|
Assertions.assertEquals("i_thd", catalog.getIndicator("I_THD").getSeriesFields().get(0).getField());
|
||||||
|
Assertions.assertEquals("fluc", catalog.getIndicator("FLUC").getSeriesFields().get(0).getField());
|
||||||
|
Assertions.assertEquals("pst", catalog.getIndicator("PST").getSeriesFields().get(0).getField());
|
||||||
|
Assertions.assertEquals("plt", catalog.getIndicator("PLT").getSeriesFields().get(0).getField());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -69,7 +100,6 @@ class SteadyTrendFieldResolverTest {
|
|||||||
SteadyTrendQueryParam param = new SteadyTrendQueryParam();
|
SteadyTrendQueryParam param = new SteadyTrendQueryParam();
|
||||||
param.setLineIds(Arrays.asList("line-001"));
|
param.setLineIds(Arrays.asList("line-001"));
|
||||||
param.setIndicatorCodes(Arrays.asList("V_HARMONIC"));
|
param.setIndicatorCodes(Arrays.asList("V_HARMONIC"));
|
||||||
param.setPhases(Arrays.asList("A"));
|
|
||||||
param.setStatTypes(Arrays.asList("AVG"));
|
param.setStatTypes(Arrays.asList("AVG"));
|
||||||
param.setTimeStart("2026-05-01 00:00:00");
|
param.setTimeStart("2026-05-01 00:00:00");
|
||||||
param.setTimeEnd("2026-05-01 01:00:00");
|
param.setTimeEnd("2026-05-01 01:00:00");
|
||||||
@@ -80,11 +110,25 @@ class SteadyTrendFieldResolverTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldResolveSelectedHarmonicOrdersOnly() {
|
void shouldRejectHarmonicTrendWithMoreThanThreeOrders() {
|
||||||
|
SteadyTrendQueryParam param = new SteadyTrendQueryParam();
|
||||||
|
param.setLineIds(Arrays.asList("line-001"));
|
||||||
|
param.setIndicatorCodes(Arrays.asList("V_HARMONIC"));
|
||||||
|
param.setStatTypes(Arrays.asList("AVG"));
|
||||||
|
param.setHarmonicOrders(Arrays.asList(3, 5, 7, 11));
|
||||||
|
param.setTimeStart("2026-05-01 00:00:00");
|
||||||
|
param.setTimeEnd("2026-05-01 01:00:00");
|
||||||
|
|
||||||
|
BusinessException exception = Assertions.assertThrows(BusinessException.class, () -> resolver.resolveFields(param));
|
||||||
|
|
||||||
|
Assertions.assertTrue(exception.getMessage().contains("谐波次数不允许一次展示超过3个"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldResolveSelectedHarmonicOrdersForAllCatalogPhases() {
|
||||||
SteadyTrendQueryParam param = new SteadyTrendQueryParam();
|
SteadyTrendQueryParam param = new SteadyTrendQueryParam();
|
||||||
param.setLineIds(Arrays.asList("line-001"));
|
param.setLineIds(Arrays.asList("line-001"));
|
||||||
param.setIndicatorCodes(Arrays.asList("V_HARMONIC"));
|
param.setIndicatorCodes(Arrays.asList("V_HARMONIC"));
|
||||||
param.setPhases(Arrays.asList("A"));
|
|
||||||
param.setStatTypes(Arrays.asList("MAX"));
|
param.setStatTypes(Arrays.asList("MAX"));
|
||||||
param.setHarmonicOrders(Arrays.asList(3, 5));
|
param.setHarmonicOrders(Arrays.asList(3, 5));
|
||||||
param.setTimeStart("2026-05-01 00:00:00");
|
param.setTimeStart("2026-05-01 00:00:00");
|
||||||
@@ -92,8 +136,36 @@ class SteadyTrendFieldResolverTest {
|
|||||||
|
|
||||||
List<SteadyTrendResolvedFieldBO> fields = resolver.resolveFields(param);
|
List<SteadyTrendResolvedFieldBO> fields = resolver.resolveFields(param);
|
||||||
|
|
||||||
Assertions.assertEquals(2, fields.size());
|
Assertions.assertEquals(6, fields.size());
|
||||||
Assertions.assertEquals("V_3_MAX", fields.get(0).getField());
|
Assertions.assertEquals("A", fields.get(0).getPhase());
|
||||||
Assertions.assertEquals("V_5_MAX", fields.get(1).getField());
|
Assertions.assertEquals("v_3", fields.get(0).getField());
|
||||||
|
Assertions.assertEquals("v_5", fields.get(1).getField());
|
||||||
|
Assertions.assertEquals("B", fields.get(2).getPhase());
|
||||||
|
Assertions.assertEquals("C", fields.get(4).getPhase());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldResolveHarmonicPowerToActualInfluxField() {
|
||||||
|
SteadyTrendQueryParam param = new SteadyTrendQueryParam();
|
||||||
|
param.setLineIds(Arrays.asList("line-001"));
|
||||||
|
param.setIndicatorCodes(Arrays.asList("P_HARMONIC_POWER"));
|
||||||
|
param.setStatTypes(Arrays.asList("CP95"));
|
||||||
|
param.setHarmonicOrders(Arrays.asList(3));
|
||||||
|
param.setTimeStart("2026-05-01 00:00:00");
|
||||||
|
param.setTimeEnd("2026-05-01 01:00:00");
|
||||||
|
|
||||||
|
List<SteadyTrendResolvedFieldBO> fields = resolver.resolveFields(param);
|
||||||
|
|
||||||
|
Assertions.assertEquals("p_3", fields.get(0).getField());
|
||||||
|
}
|
||||||
|
|
||||||
|
private String resolveSingleField(String indicatorCode) {
|
||||||
|
SteadyTrendQueryParam param = new SteadyTrendQueryParam();
|
||||||
|
param.setLineIds(Arrays.asList("line-001"));
|
||||||
|
param.setIndicatorCodes(Arrays.asList(indicatorCode));
|
||||||
|
param.setStatTypes(Arrays.asList("AVG"));
|
||||||
|
param.setTimeStart("2026-05-01 00:00:00");
|
||||||
|
param.setTimeEnd("2026-05-01 01:00:00");
|
||||||
|
return resolver.resolveFields(param).get(0).getField();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package com.njcn.gather.steady.datavie.controller;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Assertions;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 稳态数据查看接口契约测试。
|
||||||
|
*/
|
||||||
|
class SteadyDataViewControllerTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldNotExposePageQueryEndpointMethod() {
|
||||||
|
for (Method method : SteadyDataViewController.class.getDeclaredMethods()) {
|
||||||
|
Assertions.assertNotEquals("pageSteadyData", method.getName(), "本功能不提供分页检索接口");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package com.njcn.gather.steady.datavie.pojo.param;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Assertions;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.lang.reflect.Field;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 稳态趋势查询参数契约测试。
|
||||||
|
*/
|
||||||
|
class SteadyTrendQueryParamTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldNotExposePhaseFilterInTrendQueryParam() {
|
||||||
|
for (Field field : SteadyTrendQueryParam.class.getDeclaredFields()) {
|
||||||
|
Assertions.assertNotEquals("phases", field.getName(), "趋势检索请求不再携带相别条件");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
package com.njcn.gather.steady.datavie.service.impl;
|
|
||||||
|
|
||||||
import com.njcn.gather.steady.datavie.component.SteadyTrendIndicatorCatalog;
|
|
||||||
import com.njcn.gather.steady.datavie.pojo.vo.SteadyDataViewIndicatorNodeVO;
|
|
||||||
import org.junit.jupiter.api.Assertions;
|
|
||||||
import org.junit.jupiter.api.Test;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 稳态趋势指标服务测试。
|
|
||||||
*/
|
|
||||||
class SteadyDataViewIndicatorServiceImplTest {
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldGroupIndicatorsByCategory() {
|
|
||||||
SteadyDataViewIndicatorServiceImpl service = new SteadyDataViewIndicatorServiceImpl(new SteadyTrendIndicatorCatalog());
|
|
||||||
|
|
||||||
List<SteadyDataViewIndicatorNodeVO> tree = service.listIndicatorTree();
|
|
||||||
|
|
||||||
Assertions.assertEquals(5, tree.size());
|
|
||||||
Assertions.assertEquals("VOLTAGE", tree.get(0).getGroupCode());
|
|
||||||
Assertions.assertTrue(tree.get(0).getChildren().size() >= 2);
|
|
||||||
Assertions.assertEquals("V_RMS", tree.get(0).getChildren().get(0).getIndicatorCode());
|
|
||||||
Assertions.assertTrue(tree.get(0).getChildren().get(0).getSelectable());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -2,122 +2,53 @@ package com.njcn.gather.steady.datavie.service.impl;
|
|||||||
|
|
||||||
import com.njcn.gather.steady.datavie.component.SteadyInfluxQueryComponent;
|
import com.njcn.gather.steady.datavie.component.SteadyInfluxQueryComponent;
|
||||||
import com.njcn.gather.steady.datavie.component.SteadyTrendFieldResolver;
|
import com.njcn.gather.steady.datavie.component.SteadyTrendFieldResolver;
|
||||||
|
import com.njcn.gather.steady.datavie.component.SteadyTrendIndicatorCatalog;
|
||||||
import com.njcn.gather.steady.datavie.pojo.bo.SteadyTrendResolvedFieldBO;
|
import com.njcn.gather.steady.datavie.pojo.bo.SteadyTrendResolvedFieldBO;
|
||||||
import com.njcn.gather.steady.datavie.pojo.param.SteadyTrendQueryParam;
|
import com.njcn.gather.steady.datavie.pojo.param.SteadyTrendQueryParam;
|
||||||
import com.njcn.gather.steady.datavie.pojo.vo.SteadyTrendPointVO;
|
|
||||||
import com.njcn.gather.steady.datavie.pojo.vo.SteadyTrendQueryVO;
|
import com.njcn.gather.steady.datavie.pojo.vo.SteadyTrendQueryVO;
|
||||||
import com.njcn.gather.steady.datavie.pojo.vo.SteadyTrendSummaryVO;
|
|
||||||
import com.njcn.gather.tool.addledger.pojo.vo.AddLedgerLinePathVO;
|
|
||||||
import com.njcn.gather.tool.addledger.service.AddLedgerService;
|
import com.njcn.gather.tool.addledger.service.AddLedgerService;
|
||||||
import org.junit.jupiter.api.Assertions;
|
import org.junit.jupiter.api.Assertions;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.mockito.Mockito;
|
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 稳态趋势服务编排测试。
|
* 稳态趋势查询服务测试。
|
||||||
*/
|
*/
|
||||||
class SteadyDataViewTrendServiceImplTest {
|
class SteadyDataViewTrendServiceImplTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldBuildTrendQueryWithDefaultBucketAndLineName() {
|
void shouldDisplayLineVoltagePhasesAsLinePairs() {
|
||||||
SteadyTrendFieldResolver fieldResolver = Mockito.mock(SteadyTrendFieldResolver.class);
|
SteadyInfluxQueryComponent influxQueryComponent = mock(SteadyInfluxQueryComponent.class);
|
||||||
SteadyInfluxQueryComponent influxQueryComponent = Mockito.mock(SteadyInfluxQueryComponent.class);
|
AddLedgerService addLedgerService = mock(AddLedgerService.class);
|
||||||
AddLedgerService addLedgerService = Mockito.mock(AddLedgerService.class);
|
SteadyDataViewTrendServiceImpl service = new SteadyDataViewTrendServiceImpl(
|
||||||
|
new SteadyTrendFieldResolver(new SteadyTrendIndicatorCatalog()), influxQueryComponent, addLedgerService);
|
||||||
|
when(addLedgerService.listLinePathByLineIds(eq(Collections.singletonList("line-001"))))
|
||||||
|
.thenReturn(Collections.emptyMap());
|
||||||
|
when(influxQueryComponent.queryTrendPoints(any(SteadyTrendResolvedFieldBO.class),
|
||||||
|
any(LocalDateTime.class), any(LocalDateTime.class), eq(0))).thenReturn(Collections.emptyList());
|
||||||
SteadyTrendQueryParam param = new SteadyTrendQueryParam();
|
SteadyTrendQueryParam param = new SteadyTrendQueryParam();
|
||||||
param.setLineIds(Arrays.asList("line-001"));
|
param.setLineIds(Collections.singletonList("line-001"));
|
||||||
param.setIndicatorCodes(Arrays.asList("V_RMS"));
|
param.setIndicatorCodes(Collections.singletonList("V_LINE_RMS"));
|
||||||
param.setPhases(Arrays.asList("A"));
|
param.setStatTypes(Collections.singletonList("AVG"));
|
||||||
param.setStatTypes(Arrays.asList("AVG"));
|
|
||||||
param.setTimeStart("2026-05-01 00:00:00");
|
param.setTimeStart("2026-05-01 00:00:00");
|
||||||
param.setTimeEnd("2026-05-01 05:59:59");
|
param.setTimeEnd("2026-05-01 01:00:00");
|
||||||
|
param.setQualityFlag(0);
|
||||||
|
|
||||||
SteadyTrendResolvedFieldBO field = new SteadyTrendResolvedFieldBO();
|
|
||||||
field.setMeasurement("data_v");
|
|
||||||
field.setField("RMS");
|
|
||||||
field.setLineId("line-001");
|
|
||||||
field.setIndicatorCode("V_RMS");
|
|
||||||
field.setIndicatorName("相电压有效值");
|
|
||||||
field.setSeriesName("相电压有效值");
|
|
||||||
field.setPhase("A");
|
|
||||||
field.setStatType("AVG");
|
|
||||||
field.setUnit("V");
|
|
||||||
field.setSeriesKey("line-001|V_RMS|A|AVG|RMS");
|
|
||||||
|
|
||||||
Mockito.when(fieldResolver.parseRequiredTime("2026-05-01 00:00:00", "开始时间不能为空"))
|
|
||||||
.thenReturn(LocalDateTime.of(2026, 5, 1, 0, 0, 0));
|
|
||||||
Mockito.when(fieldResolver.parseRequiredTime("2026-05-01 05:59:59", "结束时间不能为空"))
|
|
||||||
.thenReturn(LocalDateTime.of(2026, 5, 1, 5, 59, 59));
|
|
||||||
Mockito.when(fieldResolver.resolveFields(Mockito.any())).thenReturn(Collections.singletonList(field));
|
|
||||||
Mockito.when(addLedgerService.listLinePathByLineIds(Collections.singletonList("line-001")))
|
|
||||||
.thenReturn(Collections.singletonMap("line-001", buildLinePath("进线一")));
|
|
||||||
Mockito.when(influxQueryComponent.queryTrendPoints(Mockito.any(), Mockito.any(), Mockito.any(), Mockito.eq("1m"), Mockito.isNull()))
|
|
||||||
.thenReturn(Collections.singletonList(new SteadyTrendPointVO("2026-05-01 00:00:00", new BigDecimal("1.2"))));
|
|
||||||
|
|
||||||
SteadyDataViewTrendServiceImpl service = new SteadyDataViewTrendServiceImpl(fieldResolver, influxQueryComponent, addLedgerService);
|
|
||||||
SteadyTrendQueryVO result = service.queryTrend(param);
|
SteadyTrendQueryVO result = service.queryTrend(param);
|
||||||
|
List<String> phases = result.getSeries().stream()
|
||||||
|
.map(series -> series.getPhase())
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
Assertions.assertEquals("1m", result.getBucket());
|
Assertions.assertEquals(Arrays.asList("AB", "BC", "CA"), phases);
|
||||||
Assertions.assertTrue(result.getSampled());
|
|
||||||
Assertions.assertEquals("进线一", result.getSeries().get(0).getLineName());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldCalculateTrendSummaryFromSeriesPoints() {
|
|
||||||
SteadyTrendFieldResolver fieldResolver = Mockito.mock(SteadyTrendFieldResolver.class);
|
|
||||||
SteadyInfluxQueryComponent influxQueryComponent = Mockito.mock(SteadyInfluxQueryComponent.class);
|
|
||||||
AddLedgerService addLedgerService = Mockito.mock(AddLedgerService.class);
|
|
||||||
|
|
||||||
SteadyTrendQueryParam param = new SteadyTrendQueryParam();
|
|
||||||
param.setLineIds(Arrays.asList("line-001"));
|
|
||||||
param.setIndicatorCodes(Arrays.asList("V_RMS"));
|
|
||||||
param.setPhases(Arrays.asList("A"));
|
|
||||||
param.setStatTypes(Arrays.asList("AVG"));
|
|
||||||
param.setTimeStart("2026-05-01 00:00:00");
|
|
||||||
param.setTimeEnd("2026-05-01 05:59:59");
|
|
||||||
|
|
||||||
SteadyTrendResolvedFieldBO field = new SteadyTrendResolvedFieldBO();
|
|
||||||
field.setMeasurement("data_v");
|
|
||||||
field.setField("RMS");
|
|
||||||
field.setLineId("line-001");
|
|
||||||
field.setIndicatorCode("V_RMS");
|
|
||||||
field.setIndicatorName("相电压有效值");
|
|
||||||
field.setSeriesName("相电压有效值");
|
|
||||||
field.setPhase("A");
|
|
||||||
field.setStatType("AVG");
|
|
||||||
field.setUnit("V");
|
|
||||||
field.setSeriesKey("line-001|V_RMS|A|AVG|RMS");
|
|
||||||
|
|
||||||
Mockito.when(fieldResolver.parseRequiredTime(Mockito.anyString(), Mockito.anyString()))
|
|
||||||
.thenReturn(LocalDateTime.of(2026, 5, 1, 0, 0, 0))
|
|
||||||
.thenReturn(LocalDateTime.of(2026, 5, 1, 5, 59, 59));
|
|
||||||
Mockito.when(fieldResolver.resolveFields(param)).thenReturn(Collections.singletonList(field));
|
|
||||||
Mockito.when(addLedgerService.listLinePathByLineIds(Collections.singletonList("line-001")))
|
|
||||||
.thenReturn(Collections.<String, AddLedgerLinePathVO>emptyMap());
|
|
||||||
Mockito.when(influxQueryComponent.queryTrendPoints(Mockito.any(), Mockito.any(), Mockito.any(), Mockito.isNull(), Mockito.isNull()))
|
|
||||||
.thenReturn(Arrays.asList(
|
|
||||||
new SteadyTrendPointVO("2026-05-01 00:00:00", new BigDecimal("1")),
|
|
||||||
new SteadyTrendPointVO("2026-05-01 01:00:00", new BigDecimal("3")),
|
|
||||||
new SteadyTrendPointVO("2026-05-01 02:00:00", new BigDecimal("2"))
|
|
||||||
));
|
|
||||||
|
|
||||||
SteadyDataViewTrendServiceImpl service = new SteadyDataViewTrendServiceImpl(fieldResolver, influxQueryComponent, addLedgerService);
|
|
||||||
SteadyTrendSummaryVO summary = service.summarizeTrend(param);
|
|
||||||
|
|
||||||
Assertions.assertEquals(new BigDecimal("3"), summary.getItems().get(0).getMax());
|
|
||||||
Assertions.assertEquals(new BigDecimal("1"), summary.getItems().get(0).getMin());
|
|
||||||
Assertions.assertEquals(new BigDecimal("2.000000"), summary.getItems().get(0).getAvg());
|
|
||||||
Assertions.assertEquals(new BigDecimal("3"), summary.getItems().get(0).getCp95());
|
|
||||||
}
|
|
||||||
|
|
||||||
private AddLedgerLinePathVO buildLinePath(String lineName) {
|
|
||||||
AddLedgerLinePathVO linePathVO = new AddLedgerLinePathVO();
|
|
||||||
linePathVO.setLineName(lineName);
|
|
||||||
return linePathVO;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
522
steady/steady-DataView/steady-checksquare-api-debug_20260610.md
Normal file
522
steady/steady-DataView/steady-checksquare-api-debug_20260610.md
Normal file
@@ -0,0 +1,522 @@
|
|||||||
|
# 数据校验 API 调试文档
|
||||||
|
|
||||||
|
## 1. 基础信息
|
||||||
|
|
||||||
|
- 模块:`steady/steady-DataView`
|
||||||
|
- 控制器:`com.njcn.gather.steady.checksquare.controller.SteadyChecksquareController`
|
||||||
|
- 接口前缀:`/steady/data-view/checksquare`
|
||||||
|
- 本地默认地址:`http://localhost:18192`
|
||||||
|
- Content-Type:`application/json`
|
||||||
|
- 认证:除登录和 Swagger 资源外,请求需要携带登录后的 `Authorization` 请求头。
|
||||||
|
- 数据库结果表:`steady_checksquare_task`、`steady_checksquare_item`、`steady_checksquare_stat_summary`、`steady_checksquare_detail`
|
||||||
|
|
||||||
|
通用请求头:
|
||||||
|
|
||||||
|
```http
|
||||||
|
Authorization: Bearer <token>
|
||||||
|
Content-Type: application/json
|
||||||
|
```
|
||||||
|
|
||||||
|
通用返回结构:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 200,
|
||||||
|
"message": "success",
|
||||||
|
"data": {}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
> 实际 `code`、`message` 字段以项目公共 `HttpResult` 封装为准,下面示例重点展示 `data` 内容。
|
||||||
|
|
||||||
|
## 2. 查询数据校验历史记录
|
||||||
|
|
||||||
|
- 方法:`POST`
|
||||||
|
- 路径:`/steady/data-view/checksquare/query`
|
||||||
|
- 返回:`HttpResult<Page<SteadyChecksquareTaskVO>>`
|
||||||
|
- 说明:分页查询已落库的数据校验任务,按创建时间倒序返回。
|
||||||
|
|
||||||
|
请求示例:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"pageNum": 1,
|
||||||
|
"pageSize": 10,
|
||||||
|
"lineId": "line-001",
|
||||||
|
"indicatorCode": "V_RMS",
|
||||||
|
"timeStart": "2026-05-01 00:00:00",
|
||||||
|
"timeEnd": "2026-05-01 23:59:59",
|
||||||
|
"hasAbnormal": true
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
请求字段:
|
||||||
|
|
||||||
|
| 字段 | 必填 | 说明 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `pageNum` | 否 | 当前页码,继承自 `BaseParam` |
|
||||||
|
| `pageSize` | 否 | 每页数量,继承自 `BaseParam` |
|
||||||
|
| `lineId` | 否 | 监测点 ID,精确匹配 |
|
||||||
|
| `indicatorCode` | 否 | 指标编码,按任务指标集合匹配 |
|
||||||
|
| `timeStart` | 否 | 检测开始时间下限,格式 `yyyy-MM-dd HH:mm:ss` |
|
||||||
|
| `timeEnd` | 否 | 检测结束时间上限,格式 `yyyy-MM-dd HH:mm:ss` |
|
||||||
|
| `hasAbnormal` | 否 | 是否只查询存在异常项的任务;`true` 表示 `abnormalItemCount > 0` |
|
||||||
|
|
||||||
|
返回示例:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"records": [
|
||||||
|
{
|
||||||
|
"taskId": "8f7a4d6d1f3145a88b6f9d7a8e6c1001",
|
||||||
|
"taskNo": "CS202606101630001",
|
||||||
|
"lineId": "line-001",
|
||||||
|
"lineName": "进线一",
|
||||||
|
"timeStart": "2026-05-01 00:00:00",
|
||||||
|
"timeEnd": "2026-05-01 23:59:59",
|
||||||
|
"intervalMinutes": 1,
|
||||||
|
"taskStatus": "SUCCESS",
|
||||||
|
"itemCount": 2,
|
||||||
|
"abnormalItemCount": 1,
|
||||||
|
"minDataIntegrity": 0.999306,
|
||||||
|
"createTime": "2026-06-10 16:30:00"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"total": 1,
|
||||||
|
"size": 10,
|
||||||
|
"current": 1,
|
||||||
|
"pages": 1
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
cURL 示例:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST "http://localhost:18192/steady/data-view/checksquare/query" \
|
||||||
|
-H "Authorization: Bearer <token>" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"pageNum":1,"pageSize":10,"lineId":"line-001","indicatorCode":"V_RMS","hasAbnormal":true}'
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. 新增数据校验记录
|
||||||
|
|
||||||
|
- 方法:`POST`
|
||||||
|
- 路径:`/steady/data-view/checksquare/create`
|
||||||
|
- 返回:`HttpResult<SteadyChecksquareCreateVO>`
|
||||||
|
- 说明:按监测点、指标和时间范围实时查询 InfluxDB,执行缺数校验、指标值大小关系校验、谐波奇偶关系校验,并将任务、检测项、统计摘要和明细写入 MySQL。
|
||||||
|
|
||||||
|
请求示例:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"lineId": "line-001",
|
||||||
|
"indicatorCodes": ["V_RMS", "FREQ", "V_HARMONIC"],
|
||||||
|
"timeStart": "2026-05-01 00:00:00",
|
||||||
|
"timeEnd": "2026-05-01 23:59:59"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
请求字段:
|
||||||
|
|
||||||
|
| 字段 | 必填 | 说明 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `lineId` | 是 | 监测点 ID,需要能在台账中找到且处于可用状态 |
|
||||||
|
| `indicatorCodes` | 是 | 指标编码列表,来自 `/steady/data-view/indicator-tree` |
|
||||||
|
| `timeStart` | 是 | 检测开始时间,格式 `yyyy-MM-dd HH:mm:ss` |
|
||||||
|
| `timeEnd` | 是 | 检测结束时间,格式 `yyyy-MM-dd HH:mm:ss` |
|
||||||
|
|
||||||
|
返回示例:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"taskId": "8f7a4d6d1f3145a88b6f9d7a8e6c1001",
|
||||||
|
"taskNo": "CS202606101630001",
|
||||||
|
"lineId": "line-001",
|
||||||
|
"lineName": "进线一",
|
||||||
|
"timeStart": "2026-05-01 00:00:00",
|
||||||
|
"timeEnd": "2026-05-01 23:59:59",
|
||||||
|
"intervalMinutes": 1,
|
||||||
|
"itemCount": 3,
|
||||||
|
"abnormalItemCount": 1
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
cURL 示例:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST "http://localhost:18192/steady/data-view/checksquare/create" \
|
||||||
|
-H "Authorization: Bearer <token>" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"lineId":"line-001","indicatorCodes":["V_RMS","FREQ","V_HARMONIC"],"timeStart":"2026-05-01 00:00:00","timeEnd":"2026-05-01 23:59:59"}'
|
||||||
|
```
|
||||||
|
|
||||||
|
调试建议:
|
||||||
|
|
||||||
|
- 新增接口会实际写入 MySQL,重复调用会生成新的任务记录。
|
||||||
|
- 时间范围越大、指标越多,InfluxDB 查询和明细落库耗时越高。
|
||||||
|
- 谐波类指标固定按 2-50 次聚合检测,不需要在请求体传 `harmonicOrders`。
|
||||||
|
|
||||||
|
## 4. 查询数据校验任务详情
|
||||||
|
|
||||||
|
- 方法:`GET`
|
||||||
|
- 路径:`/steady/data-view/checksquare/detail`
|
||||||
|
- 返回:`HttpResult<SteadyChecksquareQueryVO>`
|
||||||
|
- 说明:按任务 ID 查询任务基础信息、检测项列表和各检测项统计摘要。检测项的明细列表通过 `/item-detail` 按需查询。
|
||||||
|
|
||||||
|
请求参数:
|
||||||
|
|
||||||
|
| 参数 | 必填 | 说明 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `taskId` | 是 | 数据校验任务 ID,即 `/query` 或 `/create` 返回的 `taskId` |
|
||||||
|
|
||||||
|
请求示例:
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET /steady/data-view/checksquare/detail?taskId=8f7a4d6d1f3145a88b6f9d7a8e6c1001
|
||||||
|
```
|
||||||
|
|
||||||
|
返回示例:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"taskId": "8f7a4d6d1f3145a88b6f9d7a8e6c1001",
|
||||||
|
"taskNo": "CS202606101630001",
|
||||||
|
"lineId": "line-001",
|
||||||
|
"lineName": "进线一",
|
||||||
|
"timeStart": "2026-05-01 00:00:00",
|
||||||
|
"timeEnd": "2026-05-01 23:59:59",
|
||||||
|
"intervalMinutes": 1,
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"itemId": "0f4b6d6c3e9d400a902a2df101d10001",
|
||||||
|
"itemKey": "line-001|V_RMS",
|
||||||
|
"indicatorCode": "V_RMS",
|
||||||
|
"indicatorName": "相电压有效值",
|
||||||
|
"harmonicOrder": null,
|
||||||
|
"intervalMinutes": 1,
|
||||||
|
"hasData": true,
|
||||||
|
"expectedPointCount": 5760,
|
||||||
|
"actualPointCount": 5756,
|
||||||
|
"missingPointCount": 4,
|
||||||
|
"dataIntegrity": 0.999306,
|
||||||
|
"dataIntegrityText": "99.93%",
|
||||||
|
"abnormal": true,
|
||||||
|
"abnormalPointCount": 2,
|
||||||
|
"harmonicParityAbnormal": false,
|
||||||
|
"harmonicParityAbnormalPointCount": 0,
|
||||||
|
"statSummaries": [
|
||||||
|
{
|
||||||
|
"statType": "AVG",
|
||||||
|
"supported": true,
|
||||||
|
"hasData": true,
|
||||||
|
"expectedPointCount": 1440,
|
||||||
|
"actualPointCount": 1439,
|
||||||
|
"missingPointCount": 1,
|
||||||
|
"dataIntegrity": 0.999306,
|
||||||
|
"dataIntegrityText": "99.93%",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"statDetails": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
cURL 示例:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X GET "http://localhost:18192/steady/data-view/checksquare/detail?taskId=8f7a4d6d1f3145a88b6f9d7a8e6c1001" \
|
||||||
|
-H "Authorization: Bearer <token>"
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5. 查询检测项明细
|
||||||
|
|
||||||
|
- 方法:`GET`
|
||||||
|
- 路径:`/steady/data-view/checksquare/item-detail`
|
||||||
|
- 返回:`HttpResult<SteadyChecksquareItemDetailVO>`
|
||||||
|
- 说明:按检测项 ID、明细类型查询缺数连续区间、指标值大小关系异常点或谐波奇偶关系异常点。`pageNum` 和 `pageSize` 未同时传入时保持全量返回;两者同时传入且均大于 0 时返回当前页明细,并在结果中带回分页元数据。
|
||||||
|
|
||||||
|
请求参数:
|
||||||
|
|
||||||
|
| 参数 | 必填 | 说明 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `itemId` | 是 | 检测项 ID,即任务详情中每个 `items[].itemId` |
|
||||||
|
| `detailType` | 是 | 明细类型:`SEGMENT`、`VALUE_ORDER`、`HARMONIC_PARITY` |
|
||||||
|
| `statType` | 否 | 统计类型:`AVG`、`MAX`、`MIN`、`CP95`;查询缺数区间时建议传入 |
|
||||||
|
| `pageNum` | 否 | 明细分页页码;与 `pageSize` 同时传入且大于 0 时启用分页 |
|
||||||
|
| `pageSize` | 否 | 明细分页条数;与 `pageNum` 同时传入且大于 0 时启用分页 |
|
||||||
|
|
||||||
|
### 5.1 查询缺数连续区间
|
||||||
|
|
||||||
|
请求示例:
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET /steady/data-view/checksquare/item-detail?itemId=0f4b6d6c3e9d400a902a2df101d10001&detailType=SEGMENT&statType=AVG
|
||||||
|
```
|
||||||
|
|
||||||
|
返回示例:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"itemId": "0f4b6d6c3e9d400a902a2df101d10001",
|
||||||
|
"detailType": "SEGMENT",
|
||||||
|
"statType": "AVG",
|
||||||
|
"pageNum": null,
|
||||||
|
"pageSize": null,
|
||||||
|
"total": null,
|
||||||
|
"segments": [
|
||||||
|
{
|
||||||
|
"startTime": "2026-05-01 00:00:00",
|
||||||
|
"endTime": "2026-05-01 00:09:00",
|
||||||
|
"status": "NORMAL",
|
||||||
|
"harmonicOrder": null,
|
||||||
|
"missingPointCount": 0,
|
||||||
|
"durationMinutes": 10
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"startTime": "2026-05-01 00:10:00",
|
||||||
|
"endTime": "2026-05-01 00:10:00",
|
||||||
|
"status": "MISSING",
|
||||||
|
"harmonicOrder": null,
|
||||||
|
"missingPointCount": 1,
|
||||||
|
"durationMinutes": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"valueOrderDetails": [],
|
||||||
|
"harmonicParityDetails": []
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.2 查询指标值大小关系异常明细
|
||||||
|
|
||||||
|
请求示例:
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET /steady/data-view/checksquare/item-detail?itemId=0f4b6d6c3e9d400a902a2df101d10001&detailType=VALUE_ORDER&pageNum=1&pageSize=20
|
||||||
|
```
|
||||||
|
|
||||||
|
返回示例:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"itemId": "0f4b6d6c3e9d400a902a2df101d10001",
|
||||||
|
"detailType": "VALUE_ORDER",
|
||||||
|
"statType": null,
|
||||||
|
"pageNum": 1,
|
||||||
|
"pageSize": 20,
|
||||||
|
"total": 1,
|
||||||
|
"segments": [],
|
||||||
|
"valueOrderDetails": [
|
||||||
|
{
|
||||||
|
"time": "2026-05-01 00:10:00",
|
||||||
|
"phase": "A",
|
||||||
|
"harmonicOrder": null,
|
||||||
|
"maxValue": 219.8,
|
||||||
|
"minValue": 218.1,
|
||||||
|
"avgValue": 219.0,
|
||||||
|
"cp95Value": 219.8
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"harmonicParityDetails": []
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.3 查询谐波奇偶关系异常明细
|
||||||
|
|
||||||
|
请求示例:
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET /steady/data-view/checksquare/item-detail?itemId=0f4b6d6c3e9d400a902a2df101d10002&detailType=HARMONIC_PARITY&pageNum=1&pageSize=20
|
||||||
|
```
|
||||||
|
|
||||||
|
返回示例:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"itemId": "0f4b6d6c3e9d400a902a2df101d10002",
|
||||||
|
"detailType": "HARMONIC_PARITY",
|
||||||
|
"statType": null,
|
||||||
|
"pageNum": 1,
|
||||||
|
"pageSize": 20,
|
||||||
|
"total": 1,
|
||||||
|
"segments": [],
|
||||||
|
"valueOrderDetails": [],
|
||||||
|
"harmonicParityDetails": [
|
||||||
|
{
|
||||||
|
"time": "2026-05-01 00:10:00",
|
||||||
|
"phase": "A",
|
||||||
|
"statType": "AVG",
|
||||||
|
"evenHarmonicOrder": 4,
|
||||||
|
"evenValue": 0.32,
|
||||||
|
"oddHarmonicOrders": [3, 5],
|
||||||
|
"oddValues": [0.08, 0.09],
|
||||||
|
"oddMedianValue": 0.085,
|
||||||
|
"thresholdMultiplier": 3.0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
cURL 示例:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X GET "http://localhost:18192/steady/data-view/checksquare/item-detail?itemId=0f4b6d6c3e9d400a902a2df101d10001&detailType=VALUE_ORDER" \
|
||||||
|
-H "Authorization: Bearer <token>"
|
||||||
|
```
|
||||||
|
|
||||||
|
分页 cURL 示例:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X GET "http://localhost:18192/steady/data-view/checksquare/item-detail?itemId=0f4b6d6c3e9d400a902a2df101d10001&detailType=VALUE_ORDER&pageNum=1&pageSize=20" \
|
||||||
|
-H "Authorization: Bearer <token>"
|
||||||
|
```
|
||||||
|
|
||||||
|
## 6. 删除数据校验任务
|
||||||
|
|
||||||
|
- 方法:`POST`
|
||||||
|
- 路径:`/steady/data-view/checksquare/delete`
|
||||||
|
- 返回:`HttpResult<Boolean>`
|
||||||
|
- 说明:按任务 ID 批量逻辑删除数据校验任务,并同步将任务下检测项置为删除态。删除后历史查询不再返回该任务,详情和检测项明细会按不存在处理。
|
||||||
|
|
||||||
|
请求示例:
|
||||||
|
|
||||||
|
```json
|
||||||
|
[
|
||||||
|
"8f7a4d6d1f3145a88b6f9d7a8e6c1001"
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
请求字段:
|
||||||
|
|
||||||
|
| 字段 | 必填 | 说明 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 请求体 | 是 | 数据校验任务 ID 数组 |
|
||||||
|
|
||||||
|
返回示例:
|
||||||
|
|
||||||
|
```json
|
||||||
|
true
|
||||||
|
```
|
||||||
|
|
||||||
|
cURL 示例:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST "http://localhost:18192/steady/data-view/checksquare/delete" \
|
||||||
|
-H "Authorization: Bearer <token>" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '["8f7a4d6d1f3145a88b6f9d7a8e6c1001"]'
|
||||||
|
```
|
||||||
|
|
||||||
|
## 7. 校验规则说明
|
||||||
|
|
||||||
|
### 7.1 缺数校验
|
||||||
|
|
||||||
|
- 按检测项支持的统计类型分别查询实际存在的时间点。
|
||||||
|
- 返回期望点数、实际点数、缺失点数、数据完整性和最大连续缺失时长。
|
||||||
|
- `SEGMENT` 明细按连续区间返回,`status` 可取 `NORMAL`、`MISSING`。
|
||||||
|
- `FLUC`、`PST` 固定按 10 分钟间隔校验,`PLT` 固定按 120 分钟间隔校验,其余指标按监测点统计间隔校验。
|
||||||
|
|
||||||
|
### 7.2 指标值大小关系校验
|
||||||
|
|
||||||
|
- 同一时间点、同一指标、同一相别需要满足 `MAX >= CP95 >= AVG >= MIN`。
|
||||||
|
- 缺少 `MAX`、`CP95`、`AVG`、`MIN` 任一值的时间点不计入大小关系异常,由缺数校验体现。
|
||||||
|
- 异常点写入 `VALUE_ORDER` 明细,前端可通过 `item-detail` 拉取后弹窗展示。
|
||||||
|
|
||||||
|
### 7.3 谐波奇偶关系校验
|
||||||
|
|
||||||
|
- 谐波指标固定聚合 2-50 次检测。
|
||||||
|
- 偶次谐波值需要大于 `0.1` 才继续参与奇偶关系检测。
|
||||||
|
- 异常点写入 `HARMONIC_PARITY` 明细,包含偶次谐波值、参与比较的奇次谐波值、中位数和阈值倍数。
|
||||||
|
|
||||||
|
## 8. 字段说明
|
||||||
|
|
||||||
|
### 8.1 任务字段
|
||||||
|
|
||||||
|
| 字段 | 说明 |
|
||||||
|
| --- | --- |
|
||||||
|
| `taskId` | 数据校验任务 ID |
|
||||||
|
| `taskNo` | 数据校验任务编号 |
|
||||||
|
| `lineId` | 监测点 ID |
|
||||||
|
| `lineName` | 监测点名称 |
|
||||||
|
| `timeStart` | 检测开始时间 |
|
||||||
|
| `timeEnd` | 检测结束时间 |
|
||||||
|
| `intervalMinutes` | 监测点统计间隔,单位分钟 |
|
||||||
|
| `taskStatus` | 任务状态,当前成功任务为 `SUCCESS` |
|
||||||
|
| `itemCount` | 检测项数量 |
|
||||||
|
| `abnormalItemCount` | 存在异常的检测项数量 |
|
||||||
|
| `minDataIntegrity` | 检测项中的最低数据完整性 |
|
||||||
|
| `createTime` | 任务创建时间 |
|
||||||
|
|
||||||
|
### 8.2 检测项字段
|
||||||
|
|
||||||
|
| 字段 | 说明 |
|
||||||
|
| --- | --- |
|
||||||
|
| `itemId` | 检测项 ID |
|
||||||
|
| `itemKey` | 检测项唯一键 |
|
||||||
|
| `indicatorCode` | 指标编码 |
|
||||||
|
| `indicatorName` | 指标名称 |
|
||||||
|
| `harmonicOrder` | 谐波次数;聚合检测项为空 |
|
||||||
|
| `hasData` | 当前检测项是否存在任意数据 |
|
||||||
|
| `expectedPointCount` | 期望点数 |
|
||||||
|
| `actualPointCount` | 实际点数 |
|
||||||
|
| `missingPointCount` | 缺失点数 |
|
||||||
|
| `dataIntegrity` | 数据完整性数值 |
|
||||||
|
| `dataIntegrityText` | 数据完整性文本 |
|
||||||
|
| `abnormal` | 指标值大小关系是否异常 |
|
||||||
|
| `abnormalPointCount` | 指标值大小关系异常点数 |
|
||||||
|
| `harmonicParityAbnormal` | 谐波奇偶关系是否异常 |
|
||||||
|
| `harmonicParityAbnormalPointCount` | 谐波奇偶关系异常点数 |
|
||||||
|
| `statSummaries` | 各统计类型缺数摘要 |
|
||||||
|
| `statDetails` | 兼容字段;明细建议通过 `/item-detail` 按需查询 |
|
||||||
|
|
||||||
|
### 8.3 检测项明细字段
|
||||||
|
|
||||||
|
| 字段 | 说明 |
|
||||||
|
| --- | --- |
|
||||||
|
| `itemId` | 检测项 ID |
|
||||||
|
| `detailType` | 明细类型:`SEGMENT`、`VALUE_ORDER`、`HARMONIC_PARITY` |
|
||||||
|
| `statType` | 统计类型;未传入或当前明细类型不按统计类型过滤时为空 |
|
||||||
|
| `pageNum` | 当前页码;未启用分页时为空 |
|
||||||
|
| `pageSize` | 每页条数;未启用分页时为空 |
|
||||||
|
| `total` | 当前查询条件下的总明细数;未启用分页时为空 |
|
||||||
|
| `segments` | 缺数连续区间,仅 `SEGMENT` 查询返回数据 |
|
||||||
|
| `valueOrderDetails` | 指标值大小关系异常点,仅 `VALUE_ORDER` 查询返回数据 |
|
||||||
|
| `harmonicParityDetails` | 谐波奇偶关系异常点,仅 `HARMONIC_PARITY` 查询返回数据 |
|
||||||
|
|
||||||
|
## 9. 常见错误场景
|
||||||
|
|
||||||
|
| 场景 | 后端提示 |
|
||||||
|
| --- | --- |
|
||||||
|
| 新增时 `lineId` 为空 | `监测点 ID 不能为空` |
|
||||||
|
| 新增时 `indicatorCodes` 为空 | `指标不能为空` |
|
||||||
|
| 新增时开始时间为空 | `开始时间不能为空` |
|
||||||
|
| 新增时结束时间为空 | `结束时间不能为空` |
|
||||||
|
| 开始时间大于结束时间 | `开始时间不能大于结束时间` |
|
||||||
|
| 时间格式不正确 | `时间格式不正确,仅支持 yyyy-MM-dd HH:mm:ss` |
|
||||||
|
| 监测点不存在或不可用 | `监测点不存在或不可用` |
|
||||||
|
| 指标编码不支持 | `稳态指标不支持:xxx` |
|
||||||
|
| 任务不存在或已删除 | `数据校验任务不存在` |
|
||||||
|
| 检测项不存在或已删除 | `数据校验检测项不存在` |
|
||||||
|
| 删除时任务 ID 为空 | `数据校验任务 ID 不能为空` |
|
||||||
|
| 删除时任务不存在或已删除 | `数据校验任务不存在或已删除` |
|
||||||
|
| 明细类型为空 | `明细类型不能为空` |
|
||||||
|
| 明细类型不支持 | `明细类型不支持:xxx` |
|
||||||
|
|
||||||
|
## 10. 调试流程建议
|
||||||
|
|
||||||
|
1. 调用 `/steady/data-view/ledger-tree` 获取可用监测点,取叶子节点 `lineId`。
|
||||||
|
2. 调用 `/steady/data-view/indicator-tree` 获取可用指标编码。
|
||||||
|
3. 调用 `/steady/data-view/checksquare/create` 新增校验任务。
|
||||||
|
4. 使用返回的 `taskId` 调用 `/steady/data-view/checksquare/detail` 查看任务详情和检测项列表。
|
||||||
|
5. 使用 `items[].itemId` 调用 `/steady/data-view/checksquare/item-detail` 查看缺数区间或异常点明细。
|
||||||
|
6. 调用 `/steady/data-view/checksquare/query` 验证任务已落库,并按监测点、指标、时间和异常状态筛选历史记录。
|
||||||
|
7. 调用 `/steady/data-view/checksquare/delete` 删除任务后,再调用 `/query`、`/detail` 验证任务已不可见。
|
||||||
|
|
||||||
|
## 11. 环境依赖
|
||||||
|
|
||||||
|
- MySQL:需要已初始化 4 张 `steady_checksquare_*` 结果表。
|
||||||
|
- InfluxDB:新增校验任务时需要可访问 `steady.influxdb` 配置的时序库。
|
||||||
|
- 台账:新增校验任务时需要 `lineId` 能在台账中解析到监测点路径和统计间隔。
|
||||||
|
- 指标目录:`indicatorCodes` 必须来自后端趋势指标目录。
|
||||||
|
|
||||||
|
|
||||||
34
system-ops/README.md
Normal file
34
system-ops/README.md
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
# system-ops 模块说明
|
||||||
|
|
||||||
|
## 模块定位
|
||||||
|
|
||||||
|
`system-ops` 是根工程下的系统运维聚合模块,当前按菜单职责拆分为数据库监控和系统部署两个子模块。
|
||||||
|
|
||||||
|
当前包含:
|
||||||
|
|
||||||
|
- `dbms`:数据库监控基础入口
|
||||||
|
- `deploy`:系统部署基础入口
|
||||||
|
- 系统运维菜单初始化 SQL
|
||||||
|
|
||||||
|
## 当前结构
|
||||||
|
|
||||||
|
```text
|
||||||
|
system-ops/
|
||||||
|
├── pom.xml
|
||||||
|
├── README.md
|
||||||
|
├── dbms/
|
||||||
|
└── deploy/
|
||||||
|
```
|
||||||
|
|
||||||
|
## 当前接口
|
||||||
|
|
||||||
|
- `GET /database/overview`
|
||||||
|
- 查询数据库监控基础信息。
|
||||||
|
- `GET /deploy/overview`
|
||||||
|
- 查询系统部署基础信息。
|
||||||
|
|
||||||
|
## 当前限制
|
||||||
|
|
||||||
|
- 当前只完成后端基础入口,不包含真实数据库探测逻辑。
|
||||||
|
- 当前只完成后端基础入口,不包含真实系统部署执行逻辑。
|
||||||
|
- SQL 脚本不自动执行,需要按部署流程手动执行或纳入目标环境初始化流程。
|
||||||
101
system-ops/dbms/README.md
Normal file
101
system-ops/dbms/README.md
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
# dbms 模块说明
|
||||||
|
|
||||||
|
## 模块定位
|
||||||
|
|
||||||
|
`dbms` 是 `system-ops` 下的数据库运维模块,当前支持 Oracle、MySQL 两类数据库运维能力,其中 Oracle 支持 `DATA_PUMP`、`JDBC_EXPORT`,MySQL 当前支持 `JDBC_EXPORT`。
|
||||||
|
|
||||||
|
## 当前接口
|
||||||
|
|
||||||
|
- `GET /database/overview`
|
||||||
|
- 查询数据库运维概览信息。
|
||||||
|
- `POST /database/connections/list`
|
||||||
|
- 查询数据库连接配置。
|
||||||
|
- `POST /database/connections/add`
|
||||||
|
- 新增 Oracle 数据库连接配置。
|
||||||
|
- `POST /database/connections/update`
|
||||||
|
- 修改 Oracle 数据库连接配置。
|
||||||
|
- `POST /database/connections/delete`
|
||||||
|
- 删除数据库连接配置。
|
||||||
|
- `POST /database/connections/test`
|
||||||
|
- 测试 Oracle 数据库连接。
|
||||||
|
- `POST /database/connections/tables`
|
||||||
|
- 查询 Oracle 表列表。
|
||||||
|
- `POST /database/backups/create`
|
||||||
|
- 创建备份任务,默认使用 `DATA_PUMP`,可选 `JDBC_EXPORT`。
|
||||||
|
- `POST /database/backups/tasks/list`
|
||||||
|
- 查询备份任务列表。
|
||||||
|
- `GET /database/backups/tasks/status`
|
||||||
|
- 查询任务状态。
|
||||||
|
- `POST /database/backups/files/list`
|
||||||
|
- 查询备份文件记录。
|
||||||
|
- `POST /database/restores/create`
|
||||||
|
- 创建恢复任务。
|
||||||
|
- `GET /database/restores/tasks/status`
|
||||||
|
- 查询恢复任务状态。
|
||||||
|
- `POST /database/delete/backup-file`
|
||||||
|
- 删除备份文件,要求 `confirmText=确认删除`。
|
||||||
|
- `POST /database/delete/task`
|
||||||
|
- 删除任务记录,要求 `confirmText=确认删除`。
|
||||||
|
|
||||||
|
## 数据脚本
|
||||||
|
|
||||||
|
- `src/main/resources/sql/system-ops/system-ops-init.sql`
|
||||||
|
- 系统运维菜单初始化脚本。
|
||||||
|
- `src/main/resources/sql/system-ops/dbms-database-ops-init.sql`
|
||||||
|
- 数据库运维连接、任务、备份文件和恢复记录表结构。
|
||||||
|
|
||||||
|
## 配置项
|
||||||
|
|
||||||
|
建议通过环境配置覆盖:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
dbms:
|
||||||
|
backup:
|
||||||
|
storage-path: D:/dbms-backup
|
||||||
|
default-max-file-size-mb: 512
|
||||||
|
mysql-fetch-size: 1000
|
||||||
|
tools:
|
||||||
|
expdp-path:
|
||||||
|
impdp-path:
|
||||||
|
```
|
||||||
|
|
||||||
|
说明:
|
||||||
|
|
||||||
|
- `backup.storage-path`
|
||||||
|
- `JDBC_EXPORT` 生成的 CSV 和元数据 JSON 的受管根目录。
|
||||||
|
- `backup.default-max-file-size-mb`
|
||||||
|
- MySQL `JDBC_EXPORT` 默认分片大小,前端可通过 `maxFileSizeMb` 覆盖,默认 512MB。
|
||||||
|
- `backup.mysql-fetch-size`
|
||||||
|
- MySQL `JDBC_EXPORT` 流式读取批量大小,默认 1000。
|
||||||
|
- `tools.expdp-path`、`tools.impdp-path`
|
||||||
|
- Oracle Data Pump 工具路径;为空时尝试走系统 `PATH`。
|
||||||
|
|
||||||
|
## 当前行为
|
||||||
|
|
||||||
|
- 当前能力矩阵如下:
|
||||||
|
|
||||||
|
| 数据库类型 | 连接测试 | 表列表 | JDBC_EXPORT | DATA_PUMP |
|
||||||
|
| --- | --- | --- | --- | --- |
|
||||||
|
| ORACLE | 支持 | 支持 | 支持 | 支持 |
|
||||||
|
| MYSQL | 支持 | 支持 | 支持 | 不支持 |
|
||||||
|
- 备份和恢复只允许基于已保存且连接可用的连接配置发起。
|
||||||
|
- 新增连接前的测试接口仍可传 `temporaryPassword` 做临时连通性测试。
|
||||||
|
- 备份任务异步执行,只有实际文件生成成功后才会写入 `dbms_backup_file` 记录。
|
||||||
|
- `JDBC_EXPORT` 当前会生成两类文件:
|
||||||
|
- MySQL `JDBC_EXPORT` 会按任务号创建独立备份目录,每张表独立 CSV,默认按 512MB 分片:
|
||||||
|
- 数据分片文件:`<table>_part001_yyyyMMdd_<taskNo>.csv`
|
||||||
|
- 元数据文件:`mysql_jdbc_export_metadata_yyyyMMdd_<taskNo>.json`
|
||||||
|
- 备份任务支持停止和重新开始:
|
||||||
|
- `POST /database/backups/tasks/stop`
|
||||||
|
- `POST /database/backups/tasks/restart`
|
||||||
|
- `JDBC_EXPORT` 恢复依赖元数据文件,不再允许缺少元数据直接发起恢复。
|
||||||
|
- 删除备份文件时,会校验目标路径必须位于受管备份目录下,避免误删非备份文件。
|
||||||
|
|
||||||
|
## 当前限制
|
||||||
|
|
||||||
|
- `DATA_PUMP` 仍依赖部署机器可执行 `expdp`、`impdp`,并且 Oracle 侧已准备好 `directory` 对象和权限。
|
||||||
|
- 当前代码要求 `DATA_PUMP` 连接配置里补齐可管理的 `directoryPath`,否则虽然 Oracle 端可能已导出成功,后端无法安全管理文件记录与删除。
|
||||||
|
- `JDBC_EXPORT` 恢复当前仅覆盖表数据,不承诺恢复索引、约束、触发器、序列、存储过程、权限等数据库对象。
|
||||||
|
- `TIME_RANGE` 模式当前只在 `JDBC_EXPORT` 场景真正参与查询过滤;`DATA_PUMP` 尚未接入 Oracle `QUERY` 参数。
|
||||||
|
- MySQL `JDBC_EXPORT` 已实现按大小分片;Oracle `JDBC_EXPORT` 仍沿用原单文件导出路径。
|
||||||
|
- 本轮仅完成代码路径和文档收口,未执行 `mvn` 编译、测试或真实库联调。
|
||||||
44
system-ops/dbms/pom.xml
Normal file
44
system-ops/dbms/pom.xml
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
<?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>system-ops</artifactId>
|
||||||
|
<version>1.0.0</version>
|
||||||
|
</parent>
|
||||||
|
|
||||||
|
<artifactId>dbms</artifactId>
|
||||||
|
<packaging>jar</packaging>
|
||||||
|
<name>dbms</name>
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.njcn</groupId>
|
||||||
|
<artifactId>njcn-common</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</groupId>
|
||||||
|
<artifactId>mybatis-plus</artifactId>
|
||||||
|
<version>0.0.1</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework</groupId>
|
||||||
|
<artifactId>spring-tx</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.oracle</groupId>
|
||||||
|
<artifactId>ojdbc6</artifactId>
|
||||||
|
<version>11.2.0.3</version>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
</project>
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
package com.njcn.gather.systemops.database.component;
|
||||||
|
|
||||||
|
import cn.hutool.core.util.StrUtil;
|
||||||
|
import com.njcn.gather.systemops.database.config.DbmsProperties;
|
||||||
|
import com.njcn.gather.systemops.database.pojo.po.DatabaseConnection;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.io.BufferedReader;
|
||||||
|
import java.io.InputStreamReader;
|
||||||
|
import java.nio.charset.Charset;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Oracle Data Pump 命令执行组件。
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class DataPumpCommandExecutor {
|
||||||
|
|
||||||
|
private final DbmsProperties dbmsProperties;
|
||||||
|
|
||||||
|
public CommandResult expdp(DatabaseConnection connection, String password, String directoryName,
|
||||||
|
String dumpFileName, String logFileName, List<String> tableNames) {
|
||||||
|
List<String> command = new ArrayList<>();
|
||||||
|
command.add(resolveTool(dbmsProperties.getTools().getExpdpPath(), "expdp"));
|
||||||
|
fillCommonArgs(command, connection, password, directoryName, dumpFileName, logFileName);
|
||||||
|
if (tableNames != null && !tableNames.isEmpty()) {
|
||||||
|
command.add("tables=" + connection.getSchemaName() + "." + String.join("," + connection.getSchemaName() + ".", tableNames));
|
||||||
|
}
|
||||||
|
return execute(command);
|
||||||
|
}
|
||||||
|
|
||||||
|
public CommandResult impdp(DatabaseConnection connection, String password, String directoryName,
|
||||||
|
String dumpFileName, String logFileName, String tableExistsAction) {
|
||||||
|
List<String> command = new ArrayList<>();
|
||||||
|
command.add(resolveTool(dbmsProperties.getTools().getImpdpPath(), "impdp"));
|
||||||
|
fillCommonArgs(command, connection, password, directoryName, dumpFileName, logFileName);
|
||||||
|
if (StrUtil.isNotBlank(tableExistsAction)) {
|
||||||
|
command.add("table_exists_action=" + tableExistsAction);
|
||||||
|
}
|
||||||
|
return execute(command);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void fillCommonArgs(List<String> command, DatabaseConnection connection, String password,
|
||||||
|
String directoryName, String dumpFileName, String logFileName) {
|
||||||
|
command.add(connection.getUsername() + "/" + password + "@" + buildConnectIdentifier(connection));
|
||||||
|
command.add("directory=" + directoryName);
|
||||||
|
command.add("dumpfile=" + dumpFileName);
|
||||||
|
command.add("logfile=" + logFileName);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String buildConnectIdentifier(DatabaseConnection connection) {
|
||||||
|
if (StrUtil.isNotBlank(connection.getServiceName())) {
|
||||||
|
return "//" + connection.getHost() + ":" + connection.getPort() + "/" + connection.getServiceName();
|
||||||
|
}
|
||||||
|
return connection.getHost() + ":" + connection.getPort() + ":" + connection.getSid();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String resolveTool(String configuredPath, String defaultName) {
|
||||||
|
return StrUtil.blankToDefault(configuredPath, defaultName);
|
||||||
|
}
|
||||||
|
|
||||||
|
private CommandResult execute(List<String> command) {
|
||||||
|
CommandResult result = new CommandResult();
|
||||||
|
result.setCommand(maskPassword(command));
|
||||||
|
try {
|
||||||
|
Process process = new ProcessBuilder(command).redirectErrorStream(true).start();
|
||||||
|
StringBuilder output = new StringBuilder();
|
||||||
|
try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream(), Charset.defaultCharset()))) {
|
||||||
|
String line;
|
||||||
|
while ((line = reader.readLine()) != null) {
|
||||||
|
output.append(line).append(System.lineSeparator());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
int exitCode = process.waitFor();
|
||||||
|
result.setExitCode(exitCode);
|
||||||
|
result.setOutput(output.toString());
|
||||||
|
result.setSuccess(exitCode == 0);
|
||||||
|
} catch (Exception exception) {
|
||||||
|
result.setExitCode(-1);
|
||||||
|
result.setOutput(exception.getMessage());
|
||||||
|
result.setSuccess(false);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String maskPassword(List<String> command) {
|
||||||
|
if (command.size() < 2) {
|
||||||
|
return String.join(" ", command);
|
||||||
|
}
|
||||||
|
List<String> masked = new ArrayList<>(command);
|
||||||
|
String credential = masked.get(1);
|
||||||
|
int slashIndex = credential.indexOf('/');
|
||||||
|
int atIndex = credential.indexOf('@');
|
||||||
|
if (slashIndex > 0 && atIndex > slashIndex) {
|
||||||
|
masked.set(1, credential.substring(0, slashIndex + 1) + "******" + credential.substring(atIndex));
|
||||||
|
}
|
||||||
|
return String.join(" ", masked);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public static class CommandResult {
|
||||||
|
private Boolean success;
|
||||||
|
private Integer exitCode;
|
||||||
|
private String command;
|
||||||
|
private String output;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package com.njcn.gather.systemops.database.component;
|
||||||
|
|
||||||
|
import cn.hutool.core.util.StrUtil;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 数据库连接密码处理组件。
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
public class DatabasePasswordComponent {
|
||||||
|
|
||||||
|
public String encrypt(String plainText) {
|
||||||
|
if (StrUtil.isBlank(plainText)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return plainText;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 优先使用本次请求传入的临时密码;否则复用已保存的数据库密码。
|
||||||
|
*/
|
||||||
|
public String resolveRuntimePassword(String passwordCipher, String temporaryPassword) {
|
||||||
|
if (StrUtil.isNotBlank(temporaryPassword)) {
|
||||||
|
return temporaryPassword;
|
||||||
|
}
|
||||||
|
return StrUtil.isBlank(passwordCipher) ? null : passwordCipher;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,541 @@
|
|||||||
|
package com.njcn.gather.systemops.database.component;
|
||||||
|
|
||||||
|
import cn.hutool.core.util.StrUtil;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.njcn.gather.systemops.database.constant.DatabaseOpsConst;
|
||||||
|
import com.njcn.gather.systemops.database.pojo.param.DatabaseBackupParam;
|
||||||
|
import com.njcn.gather.systemops.database.util.DatabaseFileNameUtil;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.io.BufferedReader;
|
||||||
|
import java.io.BufferedWriter;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
import java.sql.Connection;
|
||||||
|
import java.sql.PreparedStatement;
|
||||||
|
import java.sql.ResultSet;
|
||||||
|
import java.sql.ResultSetMetaData;
|
||||||
|
import java.sql.Statement;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
import java.util.function.BooleanSupplier;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JDBC 表数据导出与恢复组件。
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class JdbcExportComponent {
|
||||||
|
|
||||||
|
private static final Pattern IDENTIFIER_PATTERN = Pattern.compile("^[A-Za-z][A-Za-z0-9_#$]*$");
|
||||||
|
private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
||||||
|
|
||||||
|
private final ObjectMapper objectMapper;
|
||||||
|
|
||||||
|
public void exportCsv(Connection jdbcConnection, String ownerName, DatabaseBackupParam.CreateParam param,
|
||||||
|
Path dataFilePath, Path metadataFilePath) throws Exception {
|
||||||
|
Files.createDirectories(dataFilePath.getParent());
|
||||||
|
if (metadataFilePath.getParent() != null) {
|
||||||
|
Files.createDirectories(metadataFilePath.getParent());
|
||||||
|
}
|
||||||
|
List<TableMetadata> metadataList = new ArrayList<>();
|
||||||
|
try (BufferedWriter writer = Files.newBufferedWriter(dataFilePath, StandardCharsets.UTF_8)) {
|
||||||
|
for (String tableName : param.getTargetNames()) {
|
||||||
|
metadataList.add(exportTable(jdbcConnection, ownerName, tableName, param, writer));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try (BufferedWriter metadataWriter = Files.newBufferedWriter(metadataFilePath, StandardCharsets.UTF_8)) {
|
||||||
|
objectMapper.writeValue(metadataWriter, metadataList);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void importCsv(Connection jdbcConnection, Path dataFilePath, Path metadataFilePath, String dbType,
|
||||||
|
String restoreMode, String targetOwnerName) throws Exception {
|
||||||
|
String metadataText = new String(Files.readAllBytes(metadataFilePath), StandardCharsets.UTF_8);
|
||||||
|
if (metadataText.trim().startsWith("{")) {
|
||||||
|
importCsvV2(jdbcConnection, metadataFilePath, dbType, restoreMode, targetOwnerName);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
List<TableMetadata> metadataList = Arrays.asList(objectMapper.readValue(metadataFilePath.toFile(), TableMetadata[].class));
|
||||||
|
Map<String, TableMetadata> metadataMap = new LinkedHashMap<>();
|
||||||
|
for (TableMetadata metadata : metadataList) {
|
||||||
|
metadataMap.put(metadata.getFullTableName(), metadata);
|
||||||
|
}
|
||||||
|
jdbcConnection.setAutoCommit(false);
|
||||||
|
try (BufferedReader reader = Files.newBufferedReader(dataFilePath, StandardCharsets.UTF_8)) {
|
||||||
|
try {
|
||||||
|
String line;
|
||||||
|
TableMetadata currentMetadata = null;
|
||||||
|
List<String> currentColumns = null;
|
||||||
|
while ((line = reader.readLine()) != null) {
|
||||||
|
if (line.startsWith("-- TABLE ")) {
|
||||||
|
currentMetadata = metadataMap.get(line.substring("-- TABLE ".length()).trim());
|
||||||
|
if (currentMetadata == null) {
|
||||||
|
throw new IllegalArgumentException("未找到表元数据:" + line);
|
||||||
|
}
|
||||||
|
currentColumns = null;
|
||||||
|
prepareTargetTable(jdbcConnection, currentMetadata, dbType, restoreMode, targetOwnerName);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (currentMetadata == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (currentColumns == null) {
|
||||||
|
currentColumns = parseCsvLine(line);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
List<String> values = parseCsvLine(line);
|
||||||
|
insertRow(jdbcConnection, currentMetadata, currentColumns, values, dbType, restoreMode, targetOwnerName);
|
||||||
|
}
|
||||||
|
jdbcConnection.commit();
|
||||||
|
} catch (Exception exception) {
|
||||||
|
jdbcConnection.rollback();
|
||||||
|
throw exception;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public ExportManifest exportMysqlCsvV2(Connection jdbcConnection, String databaseName, String taskNo,
|
||||||
|
DatabaseBackupParam.CreateParam param, Path backupDirectory,
|
||||||
|
Path metadataFilePath, int fetchSize, long maxPartBytes,
|
||||||
|
BooleanSupplier cancelled) throws Exception {
|
||||||
|
Files.createDirectories(backupDirectory);
|
||||||
|
ExportManifest manifest = new ExportManifest();
|
||||||
|
manifest.setVersion(2);
|
||||||
|
manifest.setDbType("MYSQL");
|
||||||
|
manifest.setBackupStrategy("JDBC_EXPORT");
|
||||||
|
manifest.setTaskNo(taskNo);
|
||||||
|
manifest.setDatabaseName(databaseName);
|
||||||
|
List<TableExportMetadata> tableMetadataList = new ArrayList<>();
|
||||||
|
manifest.setTables(tableMetadataList);
|
||||||
|
for (String tableName : param.getTargetNames()) {
|
||||||
|
checkCancelled(cancelled, backupDirectory);
|
||||||
|
tableMetadataList.add(exportMysqlTableV2(jdbcConnection, tableName, param, backupDirectory, taskNo,
|
||||||
|
fetchSize, maxPartBytes, cancelled));
|
||||||
|
}
|
||||||
|
try (BufferedWriter metadataWriter = Files.newBufferedWriter(metadataFilePath, StandardCharsets.UTF_8)) {
|
||||||
|
objectMapper.writeValue(metadataWriter, manifest);
|
||||||
|
}
|
||||||
|
return manifest;
|
||||||
|
}
|
||||||
|
|
||||||
|
private TableMetadata exportTable(Connection connection, String ownerName, String tableName,
|
||||||
|
DatabaseBackupParam.CreateParam param, BufferedWriter writer) throws Exception {
|
||||||
|
String normalizedOwner = normalizeOwner(ownerName);
|
||||||
|
String normalizedTable = normalizeMysqlIdentifier(tableName);
|
||||||
|
String fullTableName = buildFullTableName(normalizedOwner, normalizedTable);
|
||||||
|
String querySql = buildQuerySql(fullTableName, param);
|
||||||
|
TableMetadata metadata = new TableMetadata();
|
||||||
|
metadata.setOwnerName(normalizedOwner);
|
||||||
|
metadata.setTableName(normalizedTable);
|
||||||
|
metadata.setFullTableName(fullTableName);
|
||||||
|
metadata.setTimeColumn(StrUtil.isBlank(param.getTimeColumn()) ? null : normalizeMysqlIdentifier(param.getTimeColumn()));
|
||||||
|
metadata.setStartTime(param.getStartTime() == null ? null : param.getStartTime().format(DATE_TIME_FORMATTER));
|
||||||
|
metadata.setEndTime(param.getEndTime() == null ? null : param.getEndTime().format(DATE_TIME_FORMATTER));
|
||||||
|
writer.write("-- TABLE " + fullTableName);
|
||||||
|
writer.newLine();
|
||||||
|
try (PreparedStatement statement = connection.prepareStatement(querySql)) {
|
||||||
|
fillQueryParams(statement, param);
|
||||||
|
try (ResultSet resultSet = statement.executeQuery()) {
|
||||||
|
ResultSetMetaData resultSetMetaData = resultSet.getMetaData();
|
||||||
|
int columnCount = resultSetMetaData.getColumnCount();
|
||||||
|
List<String> columnNames = new ArrayList<>();
|
||||||
|
List<String> columnTypes = new ArrayList<>();
|
||||||
|
for (int i = 1; i <= columnCount; i++) {
|
||||||
|
String columnName = resultSetMetaData.getColumnName(i);
|
||||||
|
columnNames.add(normalizeMysqlIdentifier(columnName));
|
||||||
|
columnTypes.add(resultSetMetaData.getColumnTypeName(i));
|
||||||
|
if (i > 1) {
|
||||||
|
writer.write(",");
|
||||||
|
}
|
||||||
|
writer.write(escape(columnName));
|
||||||
|
}
|
||||||
|
writer.newLine();
|
||||||
|
long rowCount = 0L;
|
||||||
|
while (resultSet.next()) {
|
||||||
|
for (int i = 1; i <= columnCount; i++) {
|
||||||
|
if (i > 1) {
|
||||||
|
writer.write(",");
|
||||||
|
}
|
||||||
|
writer.write(escape(resultSet.getString(i)));
|
||||||
|
}
|
||||||
|
writer.newLine();
|
||||||
|
rowCount++;
|
||||||
|
}
|
||||||
|
metadata.setColumnNames(columnNames);
|
||||||
|
metadata.setColumnTypes(columnTypes);
|
||||||
|
metadata.setRowCount(rowCount);
|
||||||
|
return metadata;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private TableExportMetadata exportMysqlTableV2(Connection connection, String tableName,
|
||||||
|
DatabaseBackupParam.CreateParam param, Path backupDirectory,
|
||||||
|
String taskNo, int fetchSize, long maxPartBytes,
|
||||||
|
BooleanSupplier cancelled) throws Exception {
|
||||||
|
String normalizedTable = normalizeIdentifier(tableName);
|
||||||
|
String querySql = buildQuerySql(normalizedTable, param);
|
||||||
|
TableExportMetadata metadata = new TableExportMetadata();
|
||||||
|
metadata.setTableName(normalizedTable);
|
||||||
|
metadata.setFullTableName(normalizedTable);
|
||||||
|
metadata.setTimeColumn(StrUtil.isBlank(param.getTimeColumn()) ? null : normalizeIdentifier(param.getTimeColumn()));
|
||||||
|
metadata.setStartTime(param.getStartTime() == null ? null : param.getStartTime().format(DATE_TIME_FORMATTER));
|
||||||
|
metadata.setEndTime(param.getEndTime() == null ? null : param.getEndTime().format(DATE_TIME_FORMATTER));
|
||||||
|
metadata.setColumns(new ArrayList<>());
|
||||||
|
metadata.setParts(new ArrayList<>());
|
||||||
|
try (PreparedStatement statement = connection.prepareStatement(querySql, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY)) {
|
||||||
|
statement.setFetchSize(fetchSize);
|
||||||
|
fillQueryParams(statement, param);
|
||||||
|
try (ResultSet resultSet = statement.executeQuery()) {
|
||||||
|
ResultSetMetaData resultSetMetaData = resultSet.getMetaData();
|
||||||
|
int columnCount = resultSetMetaData.getColumnCount();
|
||||||
|
List<String> columnNames = new ArrayList<>();
|
||||||
|
for (int i = 1; i <= columnCount; i++) {
|
||||||
|
String columnName = resultSetMetaData.getColumnName(i);
|
||||||
|
columnNames.add(columnName);
|
||||||
|
ColumnMetadata columnMetadata = new ColumnMetadata();
|
||||||
|
columnMetadata.setName(columnName);
|
||||||
|
columnMetadata.setType(resultSetMetaData.getColumnTypeName(i));
|
||||||
|
metadata.getColumns().add(columnMetadata);
|
||||||
|
}
|
||||||
|
PartWriter partWriter = openPartWriter(backupDirectory, normalizedTable, taskNo,
|
||||||
|
metadata.getParts().size() + 1, columnNames);
|
||||||
|
metadata.getParts().add(partWriter.getPart());
|
||||||
|
long totalRows = 0L;
|
||||||
|
try {
|
||||||
|
while (resultSet.next()) {
|
||||||
|
checkCancelled(cancelled, backupDirectory);
|
||||||
|
if (partWriter.shouldRotate(maxPartBytes)) {
|
||||||
|
partWriter.close();
|
||||||
|
partWriter = openPartWriter(backupDirectory, normalizedTable, taskNo,
|
||||||
|
metadata.getParts().size() + 1, columnNames);
|
||||||
|
metadata.getParts().add(partWriter.getPart());
|
||||||
|
}
|
||||||
|
partWriter.writeRow(resultSet, columnCount);
|
||||||
|
totalRows++;
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
partWriter.close();
|
||||||
|
}
|
||||||
|
metadata.setRowCount(totalRows);
|
||||||
|
return metadata;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String buildQuerySql(String fullTableName, DatabaseBackupParam.CreateParam param) {
|
||||||
|
StringBuilder sql = new StringBuilder("SELECT * FROM ").append(fullTableName);
|
||||||
|
if (param.getStartTime() != null && param.getEndTime() != null && StrUtil.isNotBlank(param.getTimeColumn())) {
|
||||||
|
sql.append(" WHERE ").append(normalizeIdentifier(param.getTimeColumn())).append(" BETWEEN ? AND ?");
|
||||||
|
}
|
||||||
|
return sql.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void fillQueryParams(PreparedStatement statement, DatabaseBackupParam.CreateParam param) throws Exception {
|
||||||
|
if (param.getStartTime() != null && param.getEndTime() != null && StrUtil.isNotBlank(param.getTimeColumn())) {
|
||||||
|
statement.setString(1, param.getStartTime().format(DATE_TIME_FORMATTER));
|
||||||
|
statement.setString(2, param.getEndTime().format(DATE_TIME_FORMATTER));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void prepareTargetTable(Connection connection, TableMetadata metadata, String dbType, String restoreMode,
|
||||||
|
String targetOwnerName) throws Exception {
|
||||||
|
if (!"TRUNCATE".equalsIgnoreCase(restoreMode)
|
||||||
|
&& !("REPLACE".equalsIgnoreCase(restoreMode) && !isMysql(dbType))) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
String fullTargetName = buildTargetTableName(metadata, targetOwnerName);
|
||||||
|
try (Statement statement = connection.createStatement()) {
|
||||||
|
statement.execute("TRUNCATE TABLE " + fullTargetName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void insertRow(Connection connection, TableMetadata metadata, List<String> columns,
|
||||||
|
List<String> values, String dbType, String restoreMode, String targetOwnerName) throws Exception {
|
||||||
|
String fullTargetName = buildTargetTableName(metadata, targetOwnerName);
|
||||||
|
StringBuilder placeholders = new StringBuilder();
|
||||||
|
for (int i = 0; i < columns.size(); i++) {
|
||||||
|
if (i > 0) {
|
||||||
|
placeholders.append(",");
|
||||||
|
}
|
||||||
|
placeholders.append("?");
|
||||||
|
}
|
||||||
|
String sql = buildInsertSql(dbType, restoreMode, fullTargetName, columns, placeholders.toString());
|
||||||
|
try (PreparedStatement statement = connection.prepareStatement(sql)) {
|
||||||
|
for (int i = 0; i < columns.size(); i++) {
|
||||||
|
statement.setString(i + 1, i < values.size() ? values.get(i) : null);
|
||||||
|
}
|
||||||
|
statement.executeUpdate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String buildInsertSql(String dbType, String restoreMode, String fullTargetName, List<String> columns,
|
||||||
|
String placeholders) {
|
||||||
|
String command = "INSERT INTO";
|
||||||
|
if (isMysql(dbType) && "SKIP".equalsIgnoreCase(restoreMode)) {
|
||||||
|
// MySQL 跳过重复主键行,避免普通恢复因历史数据重复而整体失败。
|
||||||
|
command = "INSERT IGNORE INTO";
|
||||||
|
} else if (isMysql(dbType) && "REPLACE".equalsIgnoreCase(restoreMode)) {
|
||||||
|
command = "REPLACE INTO";
|
||||||
|
}
|
||||||
|
return command + " " + fullTargetName + " (" + String.join(",", columns) + ") VALUES (" + placeholders + ")";
|
||||||
|
}
|
||||||
|
|
||||||
|
private void importCsvV2(Connection jdbcConnection, Path metadataFilePath, String dbType, String restoreMode,
|
||||||
|
String targetOwnerName) throws Exception {
|
||||||
|
ExportManifest manifest = objectMapper.readValue(metadataFilePath.toFile(), ExportManifest.class);
|
||||||
|
jdbcConnection.setAutoCommit(false);
|
||||||
|
try {
|
||||||
|
for (TableExportMetadata tableMetadata : manifest.getTables()) {
|
||||||
|
prepareTargetTable(jdbcConnection, toLegacyMetadata(tableMetadata), dbType, restoreMode, targetOwnerName);
|
||||||
|
for (FilePartMetadata part : tableMetadata.getParts()) {
|
||||||
|
importPart(jdbcConnection, metadataFilePath.getParent(), tableMetadata, part, dbType, restoreMode, targetOwnerName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
jdbcConnection.commit();
|
||||||
|
} catch (Exception exception) {
|
||||||
|
jdbcConnection.rollback();
|
||||||
|
throw exception;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void importPart(Connection jdbcConnection, Path backupDirectory, TableExportMetadata tableMetadata,
|
||||||
|
FilePartMetadata part, String dbType, String restoreMode, String targetOwnerName) throws Exception {
|
||||||
|
Path partPath = backupDirectory.resolve(part.getFileName()).normalize();
|
||||||
|
if (!partPath.startsWith(backupDirectory.normalize())) {
|
||||||
|
throw new IllegalArgumentException("备份分片路径不在元数据目录内:" + part.getFileName());
|
||||||
|
}
|
||||||
|
try (BufferedReader reader = Files.newBufferedReader(partPath, StandardCharsets.UTF_8)) {
|
||||||
|
List<String> columns = null;
|
||||||
|
String line;
|
||||||
|
while ((line = reader.readLine()) != null) {
|
||||||
|
if (line.startsWith("-- TABLE ")) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (columns == null) {
|
||||||
|
columns = parseCsvLine(line);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
List<String> values = parseCsvLine(line);
|
||||||
|
insertRow(jdbcConnection, toLegacyMetadata(tableMetadata), columns, values, dbType, restoreMode, targetOwnerName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isMysql(String dbType) {
|
||||||
|
return DatabaseOpsConst.DB_TYPE_MYSQL.equalsIgnoreCase(dbType);
|
||||||
|
}
|
||||||
|
|
||||||
|
private PartWriter openPartWriter(Path backupDirectory, String tableName, String taskNo, int partIndex,
|
||||||
|
List<String> columnNames) throws IOException {
|
||||||
|
String rawName = tableName.toLowerCase(Locale.ROOT) + "_part" + String.format("%03d", partIndex) + ".csv";
|
||||||
|
String fileName = DatabaseFileNameUtil.appendTodayWithTask(rawName, taskNo);
|
||||||
|
Path filePath = backupDirectory.resolve(fileName).normalize();
|
||||||
|
BufferedWriter writer = Files.newBufferedWriter(filePath, StandardCharsets.UTF_8);
|
||||||
|
writer.write("-- TABLE " + tableName);
|
||||||
|
writer.newLine();
|
||||||
|
for (int i = 0; i < columnNames.size(); i++) {
|
||||||
|
if (i > 0) {
|
||||||
|
writer.write(",");
|
||||||
|
}
|
||||||
|
writer.write(escape(columnNames.get(i)));
|
||||||
|
}
|
||||||
|
writer.newLine();
|
||||||
|
FilePartMetadata part = new FilePartMetadata();
|
||||||
|
part.setFileName(fileName);
|
||||||
|
part.setFilePath(filePath.toString());
|
||||||
|
part.setRowCount(0L);
|
||||||
|
part.setFileSize(0L);
|
||||||
|
return new PartWriter(writer, filePath, part);
|
||||||
|
}
|
||||||
|
|
||||||
|
private TableMetadata toLegacyMetadata(TableExportMetadata metadata) {
|
||||||
|
TableMetadata legacy = new TableMetadata();
|
||||||
|
legacy.setOwnerName(null);
|
||||||
|
legacy.setTableName(metadata.getTableName());
|
||||||
|
legacy.setFullTableName(metadata.getFullTableName());
|
||||||
|
legacy.setTimeColumn(metadata.getTimeColumn());
|
||||||
|
legacy.setStartTime(metadata.getStartTime());
|
||||||
|
legacy.setEndTime(metadata.getEndTime());
|
||||||
|
legacy.setRowCount(metadata.getRowCount());
|
||||||
|
List<String> columnNames = new ArrayList<>();
|
||||||
|
List<String> columnTypes = new ArrayList<>();
|
||||||
|
for (ColumnMetadata column : metadata.getColumns()) {
|
||||||
|
columnNames.add(column.getName());
|
||||||
|
columnTypes.add(column.getType());
|
||||||
|
}
|
||||||
|
legacy.setColumnNames(columnNames);
|
||||||
|
legacy.setColumnTypes(columnTypes);
|
||||||
|
return legacy;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void checkCancelled(BooleanSupplier cancelled, Path backupDirectory) {
|
||||||
|
if (cancelled != null && cancelled.getAsBoolean()) {
|
||||||
|
throw new IllegalStateException("备份任务已停止,已生成文件保留在:" + backupDirectory);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String buildTargetTableName(TableMetadata metadata, String targetOwnerName) {
|
||||||
|
String owner = normalizeOwner(StrUtil.blankToDefault(targetOwnerName, metadata.getOwnerName()));
|
||||||
|
return buildFullTableName(owner, metadata.getTableName());
|
||||||
|
}
|
||||||
|
|
||||||
|
private String buildFullTableName(String ownerName, String tableName) {
|
||||||
|
if (StrUtil.isBlank(ownerName)) {
|
||||||
|
return tableName;
|
||||||
|
}
|
||||||
|
return ownerName + "." + tableName;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String normalizeOwner(String ownerName) {
|
||||||
|
if (StrUtil.isBlank(ownerName)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return normalizeIdentifier(ownerName);
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<String> parseCsvLine(String line) {
|
||||||
|
List<String> result = new ArrayList<>();
|
||||||
|
StringBuilder current = new StringBuilder();
|
||||||
|
boolean quoted = false;
|
||||||
|
for (int i = 0; i < line.length(); i++) {
|
||||||
|
char currentChar = line.charAt(i);
|
||||||
|
if (currentChar == '"') {
|
||||||
|
if (quoted && i + 1 < line.length() && line.charAt(i + 1) == '"') {
|
||||||
|
current.append('"');
|
||||||
|
i++;
|
||||||
|
} else {
|
||||||
|
quoted = !quoted;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (currentChar == ',' && !quoted) {
|
||||||
|
result.add(current.toString());
|
||||||
|
current.setLength(0);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
current.append(currentChar);
|
||||||
|
}
|
||||||
|
result.add(current.toString());
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String escape(String value) {
|
||||||
|
if (value == null) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return "\"" + value.replace("\"", "\"\"") + "\"";
|
||||||
|
}
|
||||||
|
|
||||||
|
private String normalizeIdentifier(String value) {
|
||||||
|
if (value == null || !IDENTIFIER_PATTERN.matcher(value).matches()) {
|
||||||
|
throw new IllegalArgumentException("数据库对象名称格式不正确:" + value);
|
||||||
|
}
|
||||||
|
return value.trim().toUpperCase(Locale.ROOT);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String normalizeMysqlIdentifier(String value) {
|
||||||
|
if (value == null || !IDENTIFIER_PATTERN.matcher(value).matches()) {
|
||||||
|
throw new IllegalArgumentException("数据库对象名称格式不正确:" + value);
|
||||||
|
}
|
||||||
|
return value.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public static class TableMetadata {
|
||||||
|
private String ownerName;
|
||||||
|
private String tableName;
|
||||||
|
private String fullTableName;
|
||||||
|
private List<String> columnNames;
|
||||||
|
private List<String> columnTypes;
|
||||||
|
private String timeColumn;
|
||||||
|
private String startTime;
|
||||||
|
private String endTime;
|
||||||
|
private Long rowCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public static class ExportManifest {
|
||||||
|
private Integer version;
|
||||||
|
private String dbType;
|
||||||
|
private String backupStrategy;
|
||||||
|
private String taskNo;
|
||||||
|
private String databaseName;
|
||||||
|
private List<TableExportMetadata> tables;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public static class TableExportMetadata {
|
||||||
|
private String tableName;
|
||||||
|
private String fullTableName;
|
||||||
|
private String timeColumn;
|
||||||
|
private String startTime;
|
||||||
|
private String endTime;
|
||||||
|
private List<ColumnMetadata> columns;
|
||||||
|
private Long rowCount;
|
||||||
|
private List<FilePartMetadata> parts;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public static class ColumnMetadata {
|
||||||
|
private String name;
|
||||||
|
private String type;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public static class FilePartMetadata {
|
||||||
|
private String fileName;
|
||||||
|
private String filePath;
|
||||||
|
private Long rowCount;
|
||||||
|
private Long fileSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
private class PartWriter {
|
||||||
|
private final BufferedWriter writer;
|
||||||
|
private final Path filePath;
|
||||||
|
private final FilePartMetadata part;
|
||||||
|
|
||||||
|
private PartWriter(BufferedWriter writer, Path filePath, FilePartMetadata part) {
|
||||||
|
this.writer = writer;
|
||||||
|
this.filePath = filePath;
|
||||||
|
this.part = part;
|
||||||
|
}
|
||||||
|
|
||||||
|
private FilePartMetadata getPart() {
|
||||||
|
return part;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean shouldRotate(long maxPartBytes) throws IOException {
|
||||||
|
writer.flush();
|
||||||
|
return part.getRowCount() > 0 && Files.size(filePath) >= maxPartBytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void writeRow(ResultSet resultSet, int columnCount) throws Exception {
|
||||||
|
for (int i = 1; i <= columnCount; i++) {
|
||||||
|
if (i > 1) {
|
||||||
|
writer.write(",");
|
||||||
|
}
|
||||||
|
writer.write(escape(resultSet.getString(i)));
|
||||||
|
}
|
||||||
|
writer.newLine();
|
||||||
|
part.setRowCount(part.getRowCount() + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void close() throws IOException {
|
||||||
|
writer.close();
|
||||||
|
part.setFileSize(Files.exists(filePath) ? Files.size(filePath) : 0L);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
package com.njcn.gather.systemops.database.component;
|
||||||
|
|
||||||
|
import cn.hutool.core.util.StrUtil;
|
||||||
|
import com.njcn.gather.systemops.database.constant.DatabaseOpsConst;
|
||||||
|
import com.njcn.gather.systemops.database.pojo.po.DatabaseConnection;
|
||||||
|
import com.njcn.gather.systemops.database.pojo.vo.DatabaseTableVO;
|
||||||
|
import com.njcn.gather.systemops.database.pojo.vo.DatabaseTestResultVO;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.sql.Connection;
|
||||||
|
import java.sql.DriverManager;
|
||||||
|
import java.sql.PreparedStatement;
|
||||||
|
import java.sql.ResultSet;
|
||||||
|
import java.sql.Timestamp;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Oracle JDBC 连接与元数据探测组件。
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
public class OracleJdbcComponent {
|
||||||
|
|
||||||
|
public DatabaseTestResultVO test(DatabaseConnection connection, String password) {
|
||||||
|
DatabaseTestResultVO result = new DatabaseTestResultVO();
|
||||||
|
try (Connection ignored = openConnection(connection, password)) {
|
||||||
|
result.setSuccess(true);
|
||||||
|
result.setMessage("连接成功");
|
||||||
|
} catch (Exception exception) {
|
||||||
|
result.setSuccess(false);
|
||||||
|
result.setMessage(exception.getMessage());
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<DatabaseTableVO> listTables(DatabaseConnection connection, String password, String schemaName) throws Exception {
|
||||||
|
String owner = StrUtil.blankToDefault(schemaName, connection.getSchemaName());
|
||||||
|
if (StrUtil.isBlank(owner)) {
|
||||||
|
owner = connection.getUsername();
|
||||||
|
}
|
||||||
|
owner = owner.trim().toUpperCase(Locale.ROOT);
|
||||||
|
String sql = "SELECT t.owner, t.table_name, t.num_rows, o.last_ddl_time, c.comments "
|
||||||
|
+ "FROM all_tables t "
|
||||||
|
+ "LEFT JOIN all_tab_comments c "
|
||||||
|
+ "ON t.owner = c.owner AND t.table_name = c.table_name "
|
||||||
|
+ "LEFT JOIN all_objects o "
|
||||||
|
+ "ON t.owner = o.owner AND t.table_name = o.object_name AND o.object_type = 'TABLE' "
|
||||||
|
+ "WHERE t.owner = ? ORDER BY t.table_name";
|
||||||
|
try (Connection jdbcConnection = openConnection(connection, password);
|
||||||
|
PreparedStatement statement = jdbcConnection.prepareStatement(sql)) {
|
||||||
|
statement.setString(1, owner);
|
||||||
|
try (ResultSet resultSet = statement.executeQuery()) {
|
||||||
|
List<DatabaseTableVO> result = new ArrayList<>();
|
||||||
|
while (resultSet.next()) {
|
||||||
|
DatabaseTableVO table = new DatabaseTableVO();
|
||||||
|
table.setOwner(resultSet.getString("owner"));
|
||||||
|
table.setTableName(resultSet.getString("table_name"));
|
||||||
|
table.setEngine(DatabaseOpsConst.DB_TYPE_ORACLE);
|
||||||
|
table.setTableRows(getLongValue(resultSet, "num_rows"));
|
||||||
|
Timestamp updateTime = resultSet.getTimestamp("last_ddl_time");
|
||||||
|
table.setUpdateTime(updateTime == null ? null : updateTime.toLocalDateTime());
|
||||||
|
table.setComments(resultSet.getString("comments"));
|
||||||
|
result.add(table);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Long getLongValue(ResultSet resultSet, String columnName) throws Exception {
|
||||||
|
long value = resultSet.getLong(columnName);
|
||||||
|
return resultSet.wasNull() ? null : value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Connection openConnection(DatabaseConnection connection, String password) throws Exception {
|
||||||
|
if (StrUtil.isBlank(password)) {
|
||||||
|
throw new IllegalArgumentException("数据库密码不能为空");
|
||||||
|
}
|
||||||
|
return DriverManager.getConnection(buildJdbcUrl(connection), connection.getUsername(), password);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String buildJdbcUrl(DatabaseConnection connection) {
|
||||||
|
if (DatabaseOpsConst.CONNECT_TYPE_SID.equalsIgnoreCase(connection.getConnectType())) {
|
||||||
|
return "jdbc:oracle:thin:@" + connection.getHost() + ":" + connection.getPort() + ":" + connection.getSid();
|
||||||
|
}
|
||||||
|
return "jdbc:oracle:thin:@//" + connection.getHost() + ":" + connection.getPort() + "/" + connection.getServiceName();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
package com.njcn.gather.systemops.database.config;
|
||||||
|
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
|
||||||
|
import java.util.concurrent.ExecutorService;
|
||||||
|
import java.util.concurrent.LinkedBlockingQueue;
|
||||||
|
import java.util.concurrent.ThreadPoolExecutor;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 数据库运维后台任务线程池。
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Configuration
|
||||||
|
public class DbmsExecutorConfig {
|
||||||
|
|
||||||
|
@Bean(name = "dbmsTaskExecutorService", destroyMethod = "shutdown")
|
||||||
|
public ExecutorService dbmsTaskExecutorService() {
|
||||||
|
AtomicInteger threadIndex = new AtomicInteger(1);
|
||||||
|
return new ThreadPoolExecutor(
|
||||||
|
1,
|
||||||
|
1,
|
||||||
|
30,
|
||||||
|
TimeUnit.SECONDS,
|
||||||
|
new LinkedBlockingQueue<Runnable>(8),
|
||||||
|
runnable -> {
|
||||||
|
Thread thread = new Thread(runnable);
|
||||||
|
thread.setName("dbms-task-" + threadIndex.getAndIncrement());
|
||||||
|
return thread;
|
||||||
|
},
|
||||||
|
(runnable, executor) -> log.warn("数据库运维任务线程池已满,拒绝新的任务")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
package com.njcn.gather.systemops.database.config;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 数据库运维配置。
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Component
|
||||||
|
@ConfigurationProperties(prefix = "dbms")
|
||||||
|
public class DbmsProperties {
|
||||||
|
|
||||||
|
private Backup backup = new Backup();
|
||||||
|
private Tools tools = new Tools();
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public static class Backup {
|
||||||
|
private String storagePath = "D:/dbms-backup";
|
||||||
|
private Integer defaultMaxFileSizeMb = 512;
|
||||||
|
private Integer mysqlFetchSize = 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public static class Tools {
|
||||||
|
private String expdpPath;
|
||||||
|
private String impdpPath;
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user