我叫圣文

This commit is contained in:
2026-05-15 16:37:22 +08:00
parent 41c915d1df
commit 90219a3daf
52 changed files with 3315 additions and 5 deletions

View File

@@ -63,6 +63,11 @@
<artifactId>event-list</artifactId>
<version>1.0.0</version>
</dependency>
<dependency>
<groupId>com.njcn.gather</groupId>
<artifactId>steady-DataView</artifactId>
<version>1.0.0</version>
</dependency>
</dependencies>
<build>

View File

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

View File

@@ -17,6 +17,7 @@
<module>detection</module>
<module>tools</module>
<module>event</module>
<module>steady</module>
</modules>
<distributionManagement>

24
steady/pom.xml Normal file
View File

@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.njcn.gather</groupId>
<artifactId>CN_Tool</artifactId>
<version>1.0.0</version>
</parent>
<artifactId>steady</artifactId>
<packaging>pom</packaging>
<modules>
<module>steady-DataView</module>
</modules>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
</project>

View File

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

View File

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

View File

@@ -0,0 +1,183 @@
package com.njcn.gather.steady.datavie.component;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.njcn.common.pojo.enums.response.CommonResponseEnum;
import com.njcn.common.pojo.exception.BusinessException;
import com.njcn.gather.steady.datavie.config.SteadyInfluxDbProperties;
import com.njcn.gather.steady.datavie.pojo.bo.SteadyTrendResolvedFieldBO;
import com.njcn.gather.steady.datavie.pojo.vo.SteadyTrendPointVO;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.math.BigDecimal;
import java.net.HttpURLConnection;
import java.net.URLEncoder;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
/**
* 稳态趋势 InfluxDB 查询组件。
*/
@Component
@RequiredArgsConstructor
public class SteadyInfluxQueryComponent {
private static final DateTimeFormatter INFLUX_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss'Z'");
private static final DateTimeFormatter OUTPUT_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
private final SteadyInfluxDbProperties properties;
public List<SteadyTrendPointVO> queryTrendPoints(SteadyTrendResolvedFieldBO field, LocalDateTime startTime,
LocalDateTime endTime, String bucket, Integer qualityFlag) {
validateConfig();
String query = buildTrendQuery(field, startTime, endTime, bucket, qualityFlag);
String body = executeQuery(query);
return parseTrendPoints(body);
}
public String buildTrendQuery(SteadyTrendResolvedFieldBO field, LocalDateTime startTime, LocalDateTime endTime,
String bucket, Integer qualityFlag) {
StringBuilder sql = new StringBuilder();
if (bucket == null || bucket.trim().isEmpty()) {
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(" WHERE time >= '").append(INFLUX_TIME_FORMATTER.format(startTime)).append("'");
sql.append(" AND time <= '").append(INFLUX_TIME_FORMATTER.format(endTime)).append("'");
sql.append(" AND \"LINEID\" = '").append(escapeTagValue(field.getLineId())).append("'");
sql.append(" AND \"PHASIC_TYPE\" = '").append(escapeTagValue(field.getPhase())).append("'");
if (qualityFlag != null) {
sql.append(" AND \"QUALITYFLAG\" = '").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");
}
return sql.toString();
}
private String executeQuery(String query) {
HttpURLConnection connection = null;
try {
URL url = new URL(buildQueryUrl(query));
connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("GET");
connection.setConnectTimeout(properties.getConnectTimeoutMs());
connection.setReadTimeout(properties.getReadTimeoutMs());
int status = connection.getResponseCode();
InputStream stream = status >= 200 && status < 300 ? connection.getInputStream() : connection.getErrorStream();
String body = readBody(stream);
if (status < 200 || status >= 300) {
throw fail("InfluxDB 查询失败:" + body);
}
return body;
} catch (IOException ex) {
throw fail("InfluxDB 查询异常:" + ex.getMessage());
} finally {
if (connection != null) {
connection.disconnect();
}
}
}
private String buildQueryUrl(String query) throws IOException {
StringBuilder url = new StringBuilder(trimRightSlash(properties.getUrl())).append("/query?");
url.append("db=").append(encode(properties.getDatabase()));
if (properties.getUsername() != null && !properties.getUsername().trim().isEmpty()) {
url.append("&u=").append(encode(properties.getUsername().trim()));
}
if (properties.getPassword() != null && !properties.getPassword().trim().isEmpty()) {
url.append("&p=").append(encode(properties.getPassword()));
}
url.append("&q=").append(encode(query));
return url.toString();
}
private List<SteadyTrendPointVO> parseTrendPoints(String body) {
try {
JsonNode root = OBJECT_MAPPER.readTree(body);
JsonNode series = root.path("results").path(0).path("series").path(0);
JsonNode values = series.path("values");
if (!values.isArray()) {
return new ArrayList<SteadyTrendPointVO>();
}
List<SteadyTrendPointVO> result = new ArrayList<SteadyTrendPointVO>();
for (JsonNode value : values) {
if (value.size() < 2 || value.get(1).isNull()) {
continue;
}
String time = formatInfluxTime(value.get(0).asText());
BigDecimal pointValue = value.get(1).decimalValue();
result.add(new SteadyTrendPointVO(time, pointValue));
}
return result;
} catch (IOException ex) {
throw fail("InfluxDB 返回结果解析失败:" + ex.getMessage());
}
}
private String formatInfluxTime(String value) {
try {
return OUTPUT_TIME_FORMATTER.format(OffsetDateTime.parse(value).withOffsetSameInstant(ZoneOffset.UTC).toLocalDateTime());
} catch (RuntimeException ex) {
return value;
}
}
private void validateConfig() {
if (properties.getUrl() == null || properties.getUrl().trim().isEmpty()) {
throw fail("InfluxDB 地址未配置");
}
if (properties.getDatabase() == null || properties.getDatabase().trim().isEmpty()) {
throw fail("InfluxDB database 未配置");
}
}
private String readBody(InputStream stream) throws IOException {
if (stream == null) {
return "";
}
BufferedReader reader = new BufferedReader(new InputStreamReader(stream, StandardCharsets.UTF_8));
StringBuilder body = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
body.append(line);
}
return body.toString();
}
private String escapeTagValue(String value) {
return value == null ? "" : value.replace("\\", "\\\\").replace("'", "\\'");
}
private String trimRightSlash(String value) {
String text = value.trim();
while (text.endsWith("/")) {
text = text.substring(0, text.length() - 1);
}
return text;
}
private String encode(String value) throws IOException {
return URLEncoder.encode(value, StandardCharsets.UTF_8.name());
}
private BusinessException fail(String message) {
return new BusinessException(CommonResponseEnum.FAIL, message);
}
}

View File

@@ -0,0 +1,261 @@
package com.njcn.gather.steady.datavie.component;
import com.njcn.common.pojo.enums.response.CommonResponseEnum;
import com.njcn.common.pojo.exception.BusinessException;
import com.njcn.gather.steady.datavie.pojo.bo.SteadyTrendIndicatorDefinitionBO;
import com.njcn.gather.steady.datavie.pojo.bo.SteadyTrendResolvedFieldBO;
import com.njcn.gather.steady.datavie.pojo.bo.SteadyTrendSeriesFieldBO;
import com.njcn.gather.steady.datavie.pojo.param.SteadyTrendQueryParam;
import com.njcn.gather.tool.adddata.component.AddDataTableRegistry;
import com.njcn.gather.tool.adddata.pojo.bo.AddDataTableDefinition;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* 稳态趋势字段白名单解析器。
*/
@Component
@RequiredArgsConstructor
public class SteadyTrendFieldResolver {
private static final int MAX_LINE_COUNT = 8;
private static final int MAX_INDICATOR_COUNT = 8;
private static final int MAX_SERIES_COUNT = 24;
private static final int MAX_HARMONIC_ORDER_COUNT = 6;
private static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
private final SteadyTrendIndicatorCatalog indicatorCatalog;
private final AddDataTableRegistry addDataTableRegistry;
public List<SteadyTrendResolvedFieldBO> resolveFields(SteadyTrendQueryParam param) {
validateBasicParam(param);
List<String> lineIds = normalizeTextList(param.getLineIds());
List<String> indicatorCodes = normalizeTextList(param.getIndicatorCodes());
List<String> requestPhases = normalizeUpperList(param.getPhases());
List<String> statTypes = normalizeUpperList(param.getStatTypes());
if (statTypes.isEmpty()) {
statTypes.add("AVG");
}
List<SteadyTrendResolvedFieldBO> result = new ArrayList<SteadyTrendResolvedFieldBO>();
for (String lineId : lineIds) {
for (String indicatorCode : indicatorCodes) {
SteadyTrendIndicatorDefinitionBO indicator = requireIndicator(indicatorCode);
List<String> phases = resolvePhases(indicator, requestPhases);
for (String phase : phases) {
for (String statType : statTypes) {
validateStatType(indicator, statType);
result.addAll(resolveIndicatorFields(lineId, indicator, phase, statType, param.getHarmonicOrders()));
}
}
}
}
if (result.size() > MAX_SERIES_COUNT) {
throw fail("趋势曲线数量不能超过 24 条,请缩小监测点、指标、相别或统计类型范围");
}
return result;
}
public LocalDateTime parseRequiredTime(String time, String emptyMessage) {
String text = trimToNull(time);
if (text == null) {
throw fail(emptyMessage);
}
try {
return LocalDateTime.parse(text, TIME_FORMATTER);
} catch (DateTimeParseException ex) {
throw fail("时间格式不正确,仅支持 yyyy-MM-dd HH:mm:ss");
}
}
private List<SteadyTrendResolvedFieldBO> resolveIndicatorFields(String lineId, SteadyTrendIndicatorDefinitionBO indicator,
String phase, String statType, List<Integer> harmonicOrders) {
if (Boolean.TRUE.equals(indicator.getHarmonic())) {
return resolveHarmonicFields(lineId, indicator, phase, statType, harmonicOrders);
}
List<SteadyTrendResolvedFieldBO> result = new ArrayList<SteadyTrendResolvedFieldBO>();
for (SteadyTrendSeriesFieldBO seriesField : indicator.getSeriesFields()) {
String field = resolveStatField(seriesField.getField(), statType);
validateColumn(indicator.getTableName(), field);
result.add(buildResolvedField(lineId, indicator, phase, statType, field, seriesField.getName()));
}
return result;
}
private List<SteadyTrendResolvedFieldBO> resolveHarmonicFields(String lineId, SteadyTrendIndicatorDefinitionBO indicator,
String phase, String statType, List<Integer> harmonicOrders) {
List<Integer> orders = normalizeOrders(harmonicOrders);
if (orders.isEmpty()) {
throw fail("谐波次数不能为空");
}
if (orders.size() > MAX_HARMONIC_ORDER_COUNT) {
throw fail("谐波次数最多选择 6 个");
}
List<SteadyTrendResolvedFieldBO> result = new ArrayList<SteadyTrendResolvedFieldBO>();
for (Integer order : orders) {
if (order < indicator.getHarmonicOrderStart() || order > indicator.getHarmonicOrderEnd()) {
throw fail("谐波次数只能在 " + indicator.getHarmonicOrderStart() + "" + indicator.getHarmonicOrderEnd() + " 之间");
}
String baseField = indicator.getHarmonicFieldPrefix() + "_" + order;
String field = resolveStatField(baseField, statType);
validateColumn(indicator.getTableName(), field);
result.add(buildResolvedField(lineId, indicator, phase, statType, field, order + "" + indicator.getName()));
}
return result;
}
private SteadyTrendResolvedFieldBO buildResolvedField(String lineId, SteadyTrendIndicatorDefinitionBO indicator,
String phase, String statType, String field, String seriesName) {
SteadyTrendResolvedFieldBO resolved = new SteadyTrendResolvedFieldBO();
resolved.setMeasurement(indicator.getTableName());
resolved.setField(field);
resolved.setLineId(lineId);
resolved.setIndicatorCode(indicator.getIndicatorCode());
resolved.setIndicatorName(indicator.getName());
resolved.setSeriesName(seriesName);
resolved.setPhase(phase);
resolved.setStatType(statType);
resolved.setUnit(indicator.getUnit());
resolved.setSeriesKey(lineId + "|" + indicator.getIndicatorCode() + "|" + phase + "|" + statType + "|" + field);
return resolved;
}
private void validateBasicParam(SteadyTrendQueryParam param) {
if (param == null) {
throw fail("趋势查询参数不能为空");
}
List<String> lineIds = normalizeTextList(param.getLineIds());
List<String> indicatorCodes = normalizeTextList(param.getIndicatorCodes());
if (lineIds.isEmpty()) {
throw fail("监测点 ID 不能为空");
}
if (indicatorCodes.isEmpty()) {
throw fail("指标不能为空");
}
if (lineIds.size() > MAX_LINE_COUNT) {
throw fail("监测点数量不能超过 8 个");
}
if (indicatorCodes.size() > MAX_INDICATOR_COUNT) {
throw fail("指标数量不能超过 8 个");
}
if (lineIds.size() > 1 && indicatorCodes.size() > 1) {
throw fail("多监测点查询时只能选择 1 个指标");
}
LocalDateTime startTime = parseRequiredTime(param.getTimeStart(), "开始时间不能为空");
LocalDateTime endTime = parseRequiredTime(param.getTimeEnd(), "结束时间不能为空");
if (startTime.isAfter(endTime)) {
throw fail("开始时间不能大于结束时间");
}
if (param.getQualityFlag() != null && param.getQualityFlag() != 0 && param.getQualityFlag() != 1) {
throw fail("质量标识只能是 0 或 1");
}
}
private SteadyTrendIndicatorDefinitionBO requireIndicator(String indicatorCode) {
SteadyTrendIndicatorDefinitionBO indicator = indicatorCatalog.getIndicator(indicatorCode);
if (indicator == null) {
throw fail("稳态趋势指标不支持:" + indicatorCode);
}
return indicator;
}
private List<String> resolvePhases(SteadyTrendIndicatorDefinitionBO indicator, List<String> requestPhases) {
if (requestPhases.isEmpty()) {
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) {
if (!"AVG".equals(statType) && !"MAX".equals(statType) && !"MIN".equals(statType) && !"CP95".equals(statType)) {
throw fail("统计类型只能是 AVG、MAX、MIN、CP95");
}
if (!indicator.getSupportStats().contains(statType)) {
throw fail("指标 " + indicator.getIndicatorCode() + " 不支持统计类型:" + statType);
}
}
private 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) {
if (values == null || values.isEmpty()) {
return new ArrayList<String>();
}
List<String> result = new ArrayList<String>();
for (String value : values) {
String text = trimToNull(value);
if (text != null && !result.contains(text)) {
result.add(text);
}
}
return result;
}
private List<String> normalizeUpperList(List<String> values) {
List<String> result = normalizeTextList(values);
for (int i = 0; i < result.size(); i++) {
result.set(i, result.get(i).toUpperCase());
}
return result;
}
private List<Integer> normalizeOrders(List<Integer> orders) {
if (orders == null || orders.isEmpty()) {
return Collections.emptyList();
}
List<Integer> result = new ArrayList<Integer>();
for (Integer order : orders) {
if (order != null && !result.contains(order)) {
result.add(order);
}
}
return result;
}
private String trimToNull(String value) {
if (value == null) {
return null;
}
String trimmed = value.trim();
return trimmed.isEmpty() ? null : trimmed;
}
private BusinessException fail(String message) {
return new BusinessException(CommonResponseEnum.FAIL, message);
}
}

View File

@@ -0,0 +1,112 @@
package com.njcn.gather.steady.datavie.component;
import com.njcn.gather.steady.datavie.pojo.bo.SteadyTrendIndicatorDefinitionBO;
import com.njcn.gather.steady.datavie.pojo.bo.SteadyTrendSeriesFieldBO;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
* 稳态趋势指标目录。
*/
@Component
public class SteadyTrendIndicatorCatalog {
private static final List<String> FULL_STATS = Collections.unmodifiableList(Arrays.asList("AVG", "MAX", "MIN", "CP95"));
private static final List<String> AVG_ONLY = Collections.unmodifiableList(Collections.singletonList("AVG"));
private static final List<String> ABC_PHASES = Collections.unmodifiableList(Arrays.asList("A", "B", "C"));
private static final List<String> T_PHASE = Collections.unmodifiableList(Collections.singletonList("T"));
private final List<SteadyTrendIndicatorDefinitionBO> indicators;
private final Map<String, SteadyTrendIndicatorDefinitionBO> indicatorMap;
public SteadyTrendIndicatorCatalog() {
List<SteadyTrendIndicatorDefinitionBO> result = new ArrayList<SteadyTrendIndicatorDefinitionBO>();
result.add(indicator("V_RMS", "相电压有效值", "VOLTAGE", "电压趋势", "data_v", ABC_PHASES,
fields(field("RMS", "相电压有效值")), FULL_STATS, "V"));
result.add(indicator("V_LINE_RMS", "线电压有效值", "VOLTAGE", "电压趋势", "data_v", T_PHASE,
fields(field("RMSAB", "AB线电压"), field("RMSBC", "BC线电压"), field("RMSCA", "CA线电压")),
FULL_STATS, "V"));
result.add(indicator("FREQ", "频率", "FREQUENCY", "频率趋势", "data_v", T_PHASE,
fields(field("FREQ", "频率")), FULL_STATS, "Hz"));
result.add(indicator("V_THD", "电压总谐波畸变率", "HARMONIC", "谐波趋势", "data_v", ABC_PHASES,
fields(field("V_THD", "电压总谐波畸变率")), FULL_STATS, "%"));
result.add(indicator("I_RMS", "电流有效值", "CURRENT", "电流趋势", "data_i", ABC_PHASES,
fields(field("RMS", "电流有效值")), FULL_STATS, "A"));
result.add(indicator("I_THD", "电流总谐波畸变率", "HARMONIC", "谐波趋势", "data_i", ABC_PHASES,
fields(field("I_THD", "电流总谐波畸变率")), FULL_STATS, "%"));
result.add(harmonic("V_HARMONIC", "电压谐波幅值", "data_harmphasic_v", "V", "V"));
result.add(harmonic("I_HARMONIC", "电流谐波幅值", "data_harmphasic_i", "I", "A"));
result.add(harmonic("V_HARMONIC_RATE", "电压谐波含有率", "data_harmrate_v", "V", "%"));
result.add(harmonic("I_HARMONIC_RATE", "电流谐波含有率", "data_harmrate_i", "I", "%"));
result.add(harmonic("I_INTER_HARMONIC", "间谐波电流", "data_inharm_i", "I", "A"));
result.add(harmonicPower("P_HARMONIC_POWER", "有功谐波功率", "data_harmpower_p", "P", "kW"));
result.add(harmonicPower("Q_HARMONIC_POWER", "无功谐波功率", "data_harmpower_q", "Q", "kvar"));
result.add(harmonicPower("S_HARMONIC_POWER", "视在谐波功率", "data_harmpower_s", "S", "kVA"));
result.add(indicator("FLUC", "电压波动", "FLICKER", "闪变趋势", "data_fluc", T_PHASE,
fields(field("FLUC", "电压波动")), AVG_ONLY, "%"));
result.add(indicator("PST", "短时闪变", "FLICKER", "闪变趋势", "data_flicker", T_PHASE,
fields(field("PST", "短时闪变")), AVG_ONLY, ""));
result.add(indicator("PLT", "长时闪变", "FLICKER", "闪变趋势", "data_plt", T_PHASE,
fields(field("PLT", "长时闪变")), AVG_ONLY, ""));
indicators = Collections.unmodifiableList(result);
Map<String, SteadyTrendIndicatorDefinitionBO> map = new LinkedHashMap<String, SteadyTrendIndicatorDefinitionBO>();
for (SteadyTrendIndicatorDefinitionBO indicator : indicators) {
map.put(indicator.getIndicatorCode(), indicator);
}
indicatorMap = Collections.unmodifiableMap(map);
}
public List<SteadyTrendIndicatorDefinitionBO> listIndicators() {
return indicators;
}
public SteadyTrendIndicatorDefinitionBO getIndicator(String indicatorCode) {
return indicatorMap.get(indicatorCode);
}
private SteadyTrendIndicatorDefinitionBO harmonic(String code, String name, String tableName, String prefix, String unit) {
SteadyTrendIndicatorDefinitionBO indicator = indicator(code, name, "HARMONIC", "谐波趋势", tableName, ABC_PHASES,
new ArrayList<SteadyTrendSeriesFieldBO>(), FULL_STATS, unit);
indicator.setHarmonic(true);
indicator.setHarmonicFieldPrefix(prefix);
indicator.setHarmonicOrderStart(2);
indicator.setHarmonicOrderEnd(50);
return indicator;
}
private SteadyTrendIndicatorDefinitionBO harmonicPower(String code, String name, String tableName, String prefix, String unit) {
return harmonic(code, name, tableName, prefix, unit);
}
private SteadyTrendIndicatorDefinitionBO indicator(String code, String name, String groupCode, String groupName,
String tableName, List<String> phaseCodes,
List<SteadyTrendSeriesFieldBO> seriesFields,
List<String> supportStats, String unit) {
SteadyTrendIndicatorDefinitionBO indicator = new SteadyTrendIndicatorDefinitionBO();
indicator.setIndicatorCode(code);
indicator.setName(name);
indicator.setGroupCode(groupCode);
indicator.setGroupName(groupName);
indicator.setTableName(tableName);
indicator.setPhaseCodes(new ArrayList<String>(phaseCodes));
indicator.setSeriesFields(new ArrayList<SteadyTrendSeriesFieldBO>(seriesFields));
indicator.setSupportStats(new ArrayList<String>(supportStats));
indicator.setUnit(unit);
return indicator;
}
private List<SteadyTrendSeriesFieldBO> fields(SteadyTrendSeriesFieldBO... fields) {
return new ArrayList<SteadyTrendSeriesFieldBO>(Arrays.asList(fields));
}
private SteadyTrendSeriesFieldBO field(String field, String name) {
return new SteadyTrendSeriesFieldBO(field, name);
}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,44 @@
package com.njcn.gather.steady.datavie.controller;
import com.njcn.common.pojo.annotation.OperateInfo;
import com.njcn.common.pojo.enums.common.LogEnum;
import com.njcn.common.pojo.enums.response.CommonResponseEnum;
import com.njcn.common.pojo.response.HttpResult;
import com.njcn.common.utils.LogUtil;
import com.njcn.gather.steady.datavie.pojo.vo.SteadyDataViewLedgerNodeVO;
import com.njcn.gather.steady.datavie.service.SteadyDataViewLedgerService;
import com.njcn.web.controller.BaseController;
import com.njcn.web.utils.HttpResultUtil;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
/**
* 稳态趋势台账树接口。
*/
@Slf4j
@Api(tags = "稳态趋势台账树")
@RestController
@RequestMapping("/steady/data-view")
@RequiredArgsConstructor
public class SteadyDataViewLedgerController extends BaseController {
private final SteadyDataViewLedgerService ledgerService;
@OperateInfo(info = LogEnum.BUSINESS_COMMON)
@ApiOperation("查询稳态趋势台账树")
@GetMapping("/ledger-tree")
public HttpResult<List<SteadyDataViewLedgerNodeVO>> listLedgerTree(@RequestParam(value = "keyword", required = false) String keyword) {
String methodDescribe = getMethodDescribe("listLedgerTree");
LogUtil.njcnDebug(log, "{}开始查询稳态趋势台账树keyword={}", methodDescribe, keyword);
List<SteadyDataViewLedgerNodeVO> result = ledgerService.listLedgerTree(keyword);
return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, result, methodDescribe);
}
}

View File

@@ -0,0 +1,64 @@
package com.njcn.gather.steady.datavie.controller;
import com.njcn.common.pojo.annotation.OperateInfo;
import com.njcn.common.pojo.enums.common.LogEnum;
import com.njcn.common.pojo.enums.response.CommonResponseEnum;
import com.njcn.common.pojo.response.HttpResult;
import com.njcn.common.utils.LogUtil;
import com.njcn.gather.steady.datavie.pojo.param.SteadyTrendQueryParam;
import com.njcn.gather.steady.datavie.pojo.vo.SteadyTrendQueryVO;
import com.njcn.gather.steady.datavie.pojo.vo.SteadyTrendSummaryVO;
import com.njcn.gather.steady.datavie.service.SteadyDataViewTrendService;
import com.njcn.web.controller.BaseController;
import com.njcn.web.utils.HttpResultUtil;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 稳态趋势查询接口。
*/
@Slf4j
@Api(tags = "稳态趋势查询")
@RestController
@RequestMapping("/steady/data-view/trend")
@RequiredArgsConstructor
public class SteadyDataViewTrendController extends BaseController {
private final SteadyDataViewTrendService trendService;
@OperateInfo(info = LogEnum.BUSINESS_COMMON)
@ApiOperation("查询稳态趋势")
@PostMapping("/query")
public HttpResult<SteadyTrendQueryVO> queryTrend(@RequestBody SteadyTrendQueryParam param) {
String methodDescribe = getMethodDescribe("queryTrend");
LogUtil.njcnDebug(log, "{}开始查询稳态趋势param={}", methodDescribe, param);
SteadyTrendQueryVO result = trendService.queryTrend(param);
return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, result, methodDescribe);
}
@OperateInfo(info = LogEnum.BUSINESS_COMMON)
@ApiOperation("按天查询稳态趋势")
@PostMapping("/day")
public HttpResult<SteadyTrendQueryVO> queryTrendDay(@RequestBody SteadyTrendQueryParam param) {
String methodDescribe = getMethodDescribe("queryTrendDay");
LogUtil.njcnDebug(log, "{}开始按天查询稳态趋势param={}", methodDescribe, param);
SteadyTrendQueryVO result = trendService.queryTrendDay(param);
return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, result, methodDescribe);
}
@OperateInfo(info = LogEnum.BUSINESS_COMMON)
@ApiOperation("查询稳态趋势统计摘要")
@PostMapping("/summary")
public HttpResult<SteadyTrendSummaryVO> summarizeTrend(@RequestBody SteadyTrendQueryParam param) {
String methodDescribe = getMethodDescribe("summarizeTrend");
LogUtil.njcnDebug(log, "{}开始查询稳态趋势统计摘要param={}", methodDescribe, param);
SteadyTrendSummaryVO result = trendService.summarizeTrend(param);
return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, result, methodDescribe);
}
}

View File

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

View File

@@ -0,0 +1,25 @@
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 java.util.List;
import java.util.Map;
/**
* 稳态数据查看 Mapper。
*/
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,
@Param("columns") List<String> columns,
@Param("lineId") String lineId,
@Param("timeId") String timeId,
@Param("phasicType") String phasicType);
}

View File

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

View File

@@ -0,0 +1,50 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.njcn.gather.steady.datavie.mapper.SteadyDataViewMapper">
<sql id="SteadyDataWhere">
<where>
<if test="param.timeStart != null and param.timeStart != ''">
AND TIMEID &gt;= #{param.timeStart}
</if>
<if test="param.timeEnd != null and param.timeEnd != ''">
AND TIMEID &lt;= #{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() &gt; 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
<foreach collection="columns" item="column" separator=",">
`${column}`
</foreach>
FROM `${tableName}`
WHERE LINEID = #{lineId}
AND TIMEID = #{timeId}
AND PHASIC_TYPE = #{phasicType}
LIMIT 1
</select>
</mapper>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,49 @@
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;
}

View File

@@ -0,0 +1,43 @@
package com.njcn.gather.steady.datavie.pojo.param;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.util.ArrayList;
import java.util.List;
/**
* 稳态趋势查询参数。
*/
@Data
@ApiModel("稳态趋势查询参数")
public class SteadyTrendQueryParam {
@ApiModelProperty("监测点 ID 列表")
private List<String> lineIds = new ArrayList<String>();
@ApiModelProperty("指标编码列表")
private List<String> indicatorCodes = new ArrayList<String>();
@ApiModelProperty("统计类型AVG/MAX/MIN/CP95")
private List<String> statTypes = new ArrayList<String>();
@ApiModelProperty("相别A/B/C/T")
private List<String> phases = new ArrayList<String>();
@ApiModelProperty("开始时间,格式 yyyy-MM-dd HH:mm:ss")
private String timeStart;
@ApiModelProperty("结束时间,格式 yyyy-MM-dd HH:mm:ss")
private String timeEnd;
@ApiModelProperty("分桶粒度,如 1m、5m、10m、30m、1h")
private String bucket;
@ApiModelProperty("质量标识")
private Integer qualityFlag;
@ApiModelProperty("谐波次数,谐波指标必填,最多 6 个")
private List<Integer> harmonicOrders = new ArrayList<Integer>();
}

View File

@@ -0,0 +1,51 @@
package com.njcn.gather.steady.datavie.pojo.vo;
import com.njcn.gather.steady.datavie.pojo.bo.SteadyTrendSeriesFieldBO;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
/**
* 稳态趋势指标树节点。
*/
@Data
@ApiModel("稳态趋势指标树节点")
public class SteadyDataViewIndicatorNodeVO implements Serializable {
private static final long serialVersionUID = 1L;
private String id;
private String parentId;
private String name;
private String groupCode;
private String indicatorCode;
private String tableName;
private List<String> phaseCodes = new ArrayList<String>();
private List<SteadyTrendSeriesFieldBO> seriesFields = new ArrayList<SteadyTrendSeriesFieldBO>();
private List<String> supportStats = new ArrayList<String>();
private Boolean harmonic = false;
private Integer harmonicOrderStart;
private Integer harmonicOrderEnd;
private String unit;
@ApiModelProperty("是否可选")
private Boolean selectable = false;
private List<SteadyDataViewIndicatorNodeVO> children = new ArrayList<SteadyDataViewIndicatorNodeVO>();
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,21 @@
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.SteadyDataViewQueryParam;
import com.njcn.gather.steady.datavie.pojo.vo.SteadyDataViewTemplateVO;
import com.njcn.gather.steady.datavie.pojo.vo.SteadyDataViewVO;
import java.util.List;
/**
* 稳态数据查看服务。
*/
public interface SteadyDataViewService {
Page<SteadyDataViewVO> pageSteadyData(SteadyDataViewQueryParam param);
SteadyDataViewVO getSteadyDataDetail(SteadyDataViewDetailParam param);
List<SteadyDataViewTemplateVO> listTemplates();
}

View File

@@ -0,0 +1,17 @@
package com.njcn.gather.steady.datavie.service;
import com.njcn.gather.steady.datavie.pojo.param.SteadyTrendQueryParam;
import com.njcn.gather.steady.datavie.pojo.vo.SteadyTrendQueryVO;
import com.njcn.gather.steady.datavie.pojo.vo.SteadyTrendSummaryVO;
/**
* 稳态趋势查询服务。
*/
public interface SteadyDataViewTrendService {
SteadyTrendQueryVO queryTrend(SteadyTrendQueryParam param);
SteadyTrendQueryVO queryTrendDay(SteadyTrendQueryParam param);
SteadyTrendSummaryVO summarizeTrend(SteadyTrendQueryParam param);
}

View File

@@ -0,0 +1,60 @@
package com.njcn.gather.steady.datavie.service.impl;
import com.njcn.gather.steady.datavie.component.SteadyTrendIndicatorCatalog;
import com.njcn.gather.steady.datavie.pojo.bo.SteadyTrendIndicatorDefinitionBO;
import com.njcn.gather.steady.datavie.pojo.vo.SteadyDataViewIndicatorNodeVO;
import com.njcn.gather.steady.datavie.service.SteadyDataViewIndicatorService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
* 稳态趋势指标服务实现。
*/
@Service
@RequiredArgsConstructor
public class SteadyDataViewIndicatorServiceImpl implements SteadyDataViewIndicatorService {
private final SteadyTrendIndicatorCatalog indicatorCatalog;
@Override
public List<SteadyDataViewIndicatorNodeVO> listIndicatorTree() {
Map<String, SteadyDataViewIndicatorNodeVO> groupMap = new LinkedHashMap<String, SteadyDataViewIndicatorNodeVO>();
for (SteadyTrendIndicatorDefinitionBO indicator : indicatorCatalog.listIndicators()) {
SteadyDataViewIndicatorNodeVO groupNode = groupMap.get(indicator.getGroupCode());
if (groupNode == null) {
groupNode = new SteadyDataViewIndicatorNodeVO();
groupNode.setId(indicator.getGroupCode());
groupNode.setName(indicator.getGroupName());
groupNode.setGroupCode(indicator.getGroupCode());
groupNode.setSelectable(false);
groupMap.put(indicator.getGroupCode(), groupNode);
}
groupNode.getChildren().add(buildIndicatorNode(indicator, groupNode.getId()));
}
return new ArrayList<SteadyDataViewIndicatorNodeVO>(groupMap.values());
}
private SteadyDataViewIndicatorNodeVO buildIndicatorNode(SteadyTrendIndicatorDefinitionBO indicator, String parentId) {
SteadyDataViewIndicatorNodeVO node = new SteadyDataViewIndicatorNodeVO();
node.setId(indicator.getIndicatorCode());
node.setParentId(parentId);
node.setName(indicator.getName());
node.setGroupCode(indicator.getGroupCode());
node.setIndicatorCode(indicator.getIndicatorCode());
node.setTableName(indicator.getTableName());
node.setPhaseCodes(new ArrayList<String>(indicator.getPhaseCodes()));
node.setSeriesFields(new ArrayList<com.njcn.gather.steady.datavie.pojo.bo.SteadyTrendSeriesFieldBO>(indicator.getSeriesFields()));
node.setSupportStats(new ArrayList<String>(indicator.getSupportStats()));
node.setHarmonic(indicator.getHarmonic());
node.setHarmonicOrderStart(indicator.getHarmonicOrderStart());
node.setHarmonicOrderEnd(indicator.getHarmonicOrderEnd());
node.setUnit(indicator.getUnit());
node.setSelectable(true);
return node;
}
}

View File

@@ -0,0 +1,70 @@
package com.njcn.gather.steady.datavie.service.impl;
import com.njcn.gather.steady.datavie.mapper.SteadyDataViewLedgerMapper;
import com.njcn.gather.steady.datavie.pojo.bo.SteadyDataViewLedgerRowBO;
import com.njcn.gather.steady.datavie.pojo.vo.SteadyDataViewLedgerNodeVO;
import com.njcn.gather.steady.datavie.service.SteadyDataViewLedgerService;
import com.njcn.gather.tool.addledger.pojo.constant.AddLedgerConst;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
* 稳态趋势台账树服务实现。
*/
@Service
@RequiredArgsConstructor
public class SteadyDataViewLedgerServiceImpl implements SteadyDataViewLedgerService {
private final SteadyDataViewLedgerMapper ledgerMapper;
@Override
public List<SteadyDataViewLedgerNodeVO> listLedgerTree(String keyword) {
List<SteadyDataViewLedgerRowBO> rows = ledgerMapper.selectLedgerTree(trimToNull(keyword));
Map<String, SteadyDataViewLedgerNodeVO> nodeMap = new LinkedHashMap<String, SteadyDataViewLedgerNodeVO>();
for (SteadyDataViewLedgerRowBO row : rows) {
SteadyDataViewLedgerNodeVO node = new SteadyDataViewLedgerNodeVO();
node.setId(row.getId());
node.setParentId(row.getParentId());
node.setName(row.getName());
node.setLevel(row.getLevel());
node.setSort(row.getSort());
node.setDeviceCount(row.getDeviceCount() == null ? 0 : row.getDeviceCount());
node.setLineCount(row.getLineCount() == null ? 0 : row.getLineCount());
node.setSelectable(AddLedgerConst.LEVEL_EQUIPMENT == row.getLevel() || AddLedgerConst.LEVEL_LINE == row.getLevel());
nodeMap.put(node.getId(), node);
}
List<SteadyDataViewLedgerNodeVO> roots = new ArrayList<SteadyDataViewLedgerNodeVO>();
for (SteadyDataViewLedgerNodeVO node : nodeMap.values()) {
SteadyDataViewLedgerNodeVO parent = nodeMap.get(node.getParentId());
if (parent == null || AddLedgerConst.ROOT_PARENT_ID.equals(node.getParentId())) {
roots.add(node);
} else {
parent.getChildren().add(node);
}
}
sortTree(roots);
return roots;
}
private void sortTree(List<SteadyDataViewLedgerNodeVO> nodes) {
nodes.sort(Comparator.comparing(SteadyDataViewLedgerNodeVO::getSort, Comparator.nullsLast(Integer::compareTo))
.thenComparing(SteadyDataViewLedgerNodeVO::getName, Comparator.nullsLast(String::compareTo)));
for (SteadyDataViewLedgerNodeVO node : nodes) {
sortTree(node.getChildren());
}
}
private String trimToNull(String value) {
if (value == null) {
return null;
}
String trimmed = value.trim();
return trimmed.isEmpty() ? null : trimmed;
}
}

View File

@@ -0,0 +1,352 @@
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.exception.BusinessException;
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.SteadyDataViewQueryParam;
import com.njcn.gather.steady.datavie.pojo.vo.SteadyDataViewTemplateVO;
import com.njcn.gather.steady.datavie.pojo.vo.SteadyDataViewVO;
import com.njcn.gather.steady.datavie.service.SteadyDataViewService;
import com.njcn.gather.tool.adddata.component.AddDataTableRegistry;
import com.njcn.gather.tool.adddata.component.AddDataTemplateRegistry;
import com.njcn.gather.tool.adddata.pojo.bo.AddDataTableDefinition;
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.service.AddLedgerService;
import com.njcn.web.factory.PageFactory;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.sql.Timestamp;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
* 稳态数据查看服务实现。
*/
@Slf4j
@Service
@RequiredArgsConstructor
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 EMPTY_TEXT = "-";
private static final DateTimeFormatter OUTPUT_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
private static final DateTimeFormatter[] INPUT_FORMATTERS = new DateTimeFormatter[]{
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"),
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS"),
DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss"),
DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSS")
};
private final SteadyDataViewMapper steadyDataViewMapper;
private final AddLedgerService addLedgerService;
private final AddDataTableRegistry addDataTableRegistry;
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
public SteadyDataViewVO getSteadyDataDetail(SteadyDataViewDetailParam param) {
if (param == null) {
throw new BusinessException(CommonResponseEnum.FAIL, "详情查询参数不能为空");
}
String tableName = normalizeTableName(param.getTableName());
String lineId = trimToNull(param.getLineId());
String timeId = normalizeRequiredTime(param.getTimeId(), "时间不能为空");
String phasicType = normalizePhasicType(param.getPhasicType());
if (lineId == null) {
throw new BusinessException(CommonResponseEnum.FAIL, "监测点 ID 不能为空");
}
if (phasicType == null) {
throw new BusinessException(CommonResponseEnum.FAIL, "相别不能为空");
}
AddDataTableDefinition definition = resolveTableDefinition(tableName);
Map<String, Object> row = steadyDataViewMapper.selectSteadyDetail(definition.getTableName(), definition.getColumns(),
lineId, timeId, phasicType);
if (row == null || row.isEmpty()) {
throw new BusinessException(CommonResponseEnum.FAIL, "稳态数据不存在");
}
List<Map<String, Object>> rows = new ArrayList<Map<String, Object>>();
rows.add(row);
return buildSteadyDataList(definition.getTableName(), definition.getColumns(), rows).get(0);
}
@Override
public List<SteadyDataViewTemplateVO> listTemplates() {
List<SteadyDataViewTemplateVO> result = new ArrayList<SteadyDataViewTemplateVO>();
for (AddDataTemplateVO template : addDataTemplateRegistry.getTemplates()) {
AddDataTableDefinition definition = resolveTableDefinition(template.getTableName());
SteadyDataViewTemplateVO vo = new SteadyDataViewTemplateVO();
vo.setParameterName(template.getParameterName());
vo.setTableName(template.getTableName());
vo.setPhaseDisplay(template.getPhaseDisplay());
vo.setPhaseCodes(template.getPhaseCodes());
vo.setValueColumns(resolveValueColumns(definition.getColumns()));
result.add(vo);
}
return result;
}
private List<SteadyDataViewVO> buildSteadyDataList(String tableName, List<String> columns, List<Map<String, Object>> rows) {
if (rows == null || rows.isEmpty()) {
return Collections.emptyList();
}
List<String> lineIds = new ArrayList<String>();
for (Map<String, Object> row : rows) {
String lineId = trimToNull(toStringValue(row.get("LINEID")));
if (lineId != null && !lineIds.contains(lineId)) {
lineIds.add(lineId);
}
}
Map<String, AddLedgerLinePathVO> linePathMap = addLedgerService.listLinePathByLineIds(lineIds);
List<SteadyDataViewVO> result = new ArrayList<SteadyDataViewVO>();
for (Map<String, Object> row : rows) {
String lineId = trimToNull(toStringValue(row.get("LINEID")));
AddLedgerLinePathVO linePath = linePathMap.get(lineId);
result.add(buildSteadyDataVO(tableName, columns, row, linePath));
}
return result;
}
private SteadyDataViewVO buildSteadyDataVO(String tableName, List<String> columns, Map<String, Object> row, AddLedgerLinePathVO linePath) {
SteadyDataViewVO vo = new SteadyDataViewVO();
vo.setTableName(tableName);
vo.setLineId(toStringValue(row.get("LINEID")));
vo.setTimeId(toLocalDateTime(row.get("TIMEID")));
vo.setPhasicType(toStringValue(row.get("PHASIC_TYPE")));
vo.setQualityFlag(toInteger(row.get("QUALITYFLAG")));
vo.setEquipmentName(linePath == null ? EMPTY_TEXT : defaultText(linePath.getEquipmentName()));
vo.setEngineeringName(linePath == null ? EMPTY_TEXT : defaultText(linePath.getEngineeringName()));
vo.setProjectName(linePath == null ? EMPTY_TEXT : defaultText(linePath.getProjectName()));
vo.setLineName(linePath == null ? EMPTY_TEXT : defaultText(linePath.getLineName()));
Map<String, Object> values = new LinkedHashMap<String, Object>();
for (String column : columns) {
if (!isInfrastructureColumn(column)) {
values.put(column, row.get(column));
}
}
vo.setValues(values);
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) {
try {
return addDataTableRegistry.getDefinition(tableName);
} catch (IllegalArgumentException ex) {
throw new BusinessException(CommonResponseEnum.FAIL, "稳态数据表不支持:" + tableName);
}
}
private String normalizeTableName(String tableName) {
String normalizedTableName = trimToNull(tableName);
return normalizedTableName == null ? DEFAULT_TABLE_NAME : normalizedTableName;
}
private String normalizeRequiredTime(String value, String emptyMessage) {
LocalDateTime dateTime = parseDateTime(value);
if (dateTime == null) {
throw new BusinessException(CommonResponseEnum.FAIL, emptyMessage);
}
return OUTPUT_FORMATTER.format(dateTime);
}
private LocalDateTime parseDateTime(String value) {
String text = trimToNull(value);
if (text == null) {
return null;
}
for (DateTimeFormatter formatter : INPUT_FORMATTERS) {
try {
return LocalDateTime.parse(text, formatter);
} catch (DateTimeParseException ignored) {
// 尝试下一个前端可能传入的时间格式。
}
}
throw new BusinessException(CommonResponseEnum.FAIL, "时间格式不正确,仅支持 yyyy-MM-dd HH:mm:ss");
}
private String normalizePhasicType(String phasicType) {
String text = trimToNull(phasicType);
if (text == null) {
return null;
}
String normalized = text.toUpperCase();
if (!"A".equals(normalized) && !"B".equals(normalized) && !"C".equals(normalized) && !"T".equals(normalized)) {
throw new BusinessException(CommonResponseEnum.FAIL, "相别只能是 A、B、C、T");
}
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) {
List<String> result = new ArrayList<String>();
for (String column : columns) {
if (!isInfrastructureColumn(column)) {
result.add(column);
}
}
return result;
}
private boolean isInfrastructureColumn(String column) {
return "TIMEID".equals(column) || "LINEID".equals(column) || "PHASIC_TYPE".equals(column) || "QUALITYFLAG".equals(column);
}
private LocalDateTime toLocalDateTime(Object value) {
if (value instanceof LocalDateTime) {
return (LocalDateTime) value;
}
if (value instanceof Timestamp) {
return ((Timestamp) value).toLocalDateTime();
}
String text = trimToNull(toStringValue(value));
return text == null ? null : parseDateTime(text);
}
private Integer toInteger(Object value) {
if (value instanceof Number) {
return ((Number) value).intValue();
}
String text = trimToNull(toStringValue(value));
return text == null ? null : Integer.valueOf(text);
}
private String toStringValue(Object value) {
return value == null ? null : String.valueOf(value);
}
private String defaultText(String value) {
String text = trimToNull(value);
return text == null ? EMPTY_TEXT : text;
}
private String trimToNull(String value) {
if (value == null) {
return null;
}
String trimmed = value.trim();
return trimmed.isEmpty() ? null : trimmed;
}
}

View File

@@ -0,0 +1,210 @@
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.SteadyTrendFieldResolver;
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.vo.SteadyTrendPointVO;
import com.njcn.gather.steady.datavie.pojo.vo.SteadyTrendQueryVO;
import com.njcn.gather.steady.datavie.pojo.vo.SteadyTrendSeriesVO;
import com.njcn.gather.steady.datavie.pojo.vo.SteadyTrendSummaryItemVO;
import com.njcn.gather.steady.datavie.pojo.vo.SteadyTrendSummaryVO;
import com.njcn.gather.steady.datavie.service.SteadyDataViewTrendService;
import com.njcn.gather.tool.addledger.pojo.vo.AddLedgerLinePathVO;
import com.njcn.gather.tool.addledger.service.AddLedgerService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
/**
* 稳态趋势查询服务实现。
*/
@Service
@RequiredArgsConstructor
public class SteadyDataViewTrendServiceImpl implements SteadyDataViewTrendService {
private static final String EMPTY_TEXT = "-";
private final SteadyTrendFieldResolver fieldResolver;
private final SteadyInfluxQueryComponent influxQueryComponent;
private final AddLedgerService addLedgerService;
@Override
public SteadyTrendQueryVO queryTrend(SteadyTrendQueryParam param) {
return queryTrendInternal(param, true);
}
@Override
public SteadyTrendQueryVO queryTrendDay(SteadyTrendQueryParam param) {
return queryTrendInternal(param, true);
}
@Override
public SteadyTrendSummaryVO summarizeTrend(SteadyTrendQueryParam param) {
SteadyTrendQueryParam summaryParam = copyParam(param);
summaryParam.setBucket(null);
SteadyTrendQueryVO trend = queryTrendInternal(summaryParam, false);
SteadyTrendSummaryVO result = new SteadyTrendSummaryVO();
for (SteadyTrendSeriesVO series : trend.getSeries()) {
result.getItems().add(buildSummaryItem(series));
}
return result;
}
private SteadyTrendQueryVO queryTrendInternal(SteadyTrendQueryParam param, boolean autoBucket) {
LocalDateTime startTime = fieldResolver.parseRequiredTime(param == null ? null : param.getTimeStart(), "开始时间不能为空");
LocalDateTime endTime = fieldResolver.parseRequiredTime(param == null ? null : param.getTimeEnd(), "结束时间不能为空");
List<SteadyTrendResolvedFieldBO> fields = fieldResolver.resolveFields(param);
enrichLineNames(fields);
String bucket = autoBucket ? resolveBucket(param.getBucket(), startTime, endTime) : null;
SteadyTrendQueryVO result = new SteadyTrendQueryVO();
result.setBucket(bucket);
result.setSampled(bucket != null);
result.setLoadableDays(resolveLoadableDays(startTime, endTime));
int displayPointCount = 0;
for (SteadyTrendResolvedFieldBO field : fields) {
List<SteadyTrendPointVO> points = influxQueryComponent.queryTrendPoints(field, startTime, endTime, bucket, param.getQualityFlag());
displayPointCount += points.size();
result.getSeries().add(buildSeries(field, points));
}
/*
* 当前 Influx 查询按曲线独立执行,未额外发 count 查询sourcePointCount 保持与实际返回点数一致。
* 后续如需要精确原始点数,可单独增加 count(field) 查询。
*/
result.setDisplayPointCount(displayPointCount);
result.setSourcePointCount(displayPointCount);
return result;
}
private SteadyTrendSeriesVO buildSeries(SteadyTrendResolvedFieldBO field, List<SteadyTrendPointVO> points) {
SteadyTrendSeriesVO series = new SteadyTrendSeriesVO();
series.setSeriesKey(field.getSeriesKey());
series.setLineId(field.getLineId());
series.setLineName(field.getLineName());
series.setIndicatorCode(field.getIndicatorCode());
series.setIndicatorName(field.getIndicatorName());
series.setSeriesName(field.getSeriesName());
series.setPhase(field.getPhase());
series.setStatType(field.getStatType());
series.setUnit(field.getUnit());
series.setPoints(points);
return series;
}
private void enrichLineNames(List<SteadyTrendResolvedFieldBO> fields) {
List<String> lineIds = new ArrayList<String>();
for (SteadyTrendResolvedFieldBO field : fields) {
if (!lineIds.contains(field.getLineId())) {
lineIds.add(field.getLineId());
}
}
Map<String, AddLedgerLinePathVO> linePathMap = addLedgerService.listLinePathByLineIds(lineIds);
for (SteadyTrendResolvedFieldBO field : fields) {
AddLedgerLinePathVO linePath = linePathMap.get(field.getLineId());
field.setLineName(linePath == null || trimToNull(linePath.getLineName()) == null ? EMPTY_TEXT : linePath.getLineName());
}
}
private SteadyTrendSummaryItemVO buildSummaryItem(SteadyTrendSeriesVO series) {
SteadyTrendSummaryItemVO item = new SteadyTrendSummaryItemVO();
item.setSeriesKey(series.getSeriesKey());
List<BigDecimal> values = new ArrayList<BigDecimal>();
for (SteadyTrendPointVO point : series.getPoints()) {
if (point.getValue() != null) {
values.add(point.getValue());
}
}
if (values.isEmpty()) {
return item;
}
values.sort(Comparator.naturalOrder());
BigDecimal sum = BigDecimal.ZERO;
for (BigDecimal value : values) {
sum = sum.add(value);
}
item.setMin(values.get(0));
item.setMax(values.get(values.size() - 1));
item.setAvg(sum.divide(new BigDecimal(values.size()), 6, RoundingMode.HALF_UP));
int cp95Index = Math.max(0, (int) Math.ceil(values.size() * 0.95D) - 1);
item.setCp95(values.get(cp95Index));
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) {
List<String> result = new ArrayList<String>();
LocalDate date = startTime.toLocalDate();
LocalDate endDate = endTime.toLocalDate();
while (!date.isAfter(endDate)) {
result.add(date.toString());
date = date.plusDays(1);
}
return result;
}
private SteadyTrendQueryParam copyParam(SteadyTrendQueryParam source) {
SteadyTrendQueryParam target = new SteadyTrendQueryParam();
if (source == null) {
return target;
}
target.setLineIds(copyList(source.getLineIds()));
target.setIndicatorCodes(copyList(source.getIndicatorCodes()));
target.setStatTypes(copyList(source.getStatTypes()));
target.setPhases(copyList(source.getPhases()));
target.setTimeStart(source.getTimeStart());
target.setTimeEnd(source.getTimeEnd());
target.setBucket(source.getBucket());
target.setQualityFlag(source.getQualityFlag());
target.setHarmonicOrders(source.getHarmonicOrders() == null ? Collections.<Integer>emptyList() : new ArrayList<Integer>(source.getHarmonicOrders()));
return target;
}
private List<String> copyList(List<String> source) {
return source == null ? new ArrayList<String>() : new ArrayList<String>(source);
}
private String trimToNull(String value) {
if (value == null) {
return null;
}
String trimmed = value.trim();
return trimmed.isEmpty() ? null : trimmed;
}
private BusinessException fail(String message) {
return new BusinessException(CommonResponseEnum.FAIL, message);
}
}

View File

@@ -0,0 +1,51 @@
package com.njcn.gather.steady.datavie.component;
import com.njcn.gather.steady.datavie.config.SteadyInfluxDbProperties;
import com.njcn.gather.steady.datavie.pojo.bo.SteadyTrendResolvedFieldBO;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import java.time.LocalDateTime;
/**
* InfluxQL 查询语句生成测试。
*/
class SteadyInfluxQueryComponentTest {
@Test
void shouldBuildBucketedTrendQueryWithRequiredTags() {
SteadyInfluxQueryComponent component = new SteadyInfluxQueryComponent(new SteadyInfluxDbProperties());
SteadyTrendResolvedFieldBO field = new SteadyTrendResolvedFieldBO();
field.setMeasurement("data_v");
field.setField("RMS_CP95");
field.setLineId("line-001");
field.setPhase("A");
String query = component.buildTrendQuery(field,
LocalDateTime.of(2026, 5, 1, 0, 0, 0),
LocalDateTime.of(2026, 5, 1, 1, 0, 0),
"10m",
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);
}
@Test
void shouldEscapeTagValuesInTrendQuery() {
SteadyInfluxQueryComponent component = new SteadyInfluxQueryComponent(new SteadyInfluxDbProperties());
SteadyTrendResolvedFieldBO field = new SteadyTrendResolvedFieldBO();
field.setMeasurement("data_v");
field.setField("RMS");
field.setLineId("line'001");
field.setPhase("A");
String query = component.buildTrendQuery(field,
LocalDateTime.of(2026, 5, 1, 0, 0, 0),
LocalDateTime.of(2026, 5, 1, 1, 0, 0),
null,
null);
Assertions.assertTrue(query.contains("\"LINEID\" = 'line\\'001'"));
Assertions.assertFalse(query.contains("GROUP BY time"));
}
}

View File

@@ -0,0 +1,99 @@
package com.njcn.gather.steady.datavie.component;
import com.njcn.common.pojo.exception.BusinessException;
import com.njcn.gather.steady.datavie.pojo.bo.SteadyTrendResolvedFieldBO;
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.BeforeEach;
import org.junit.jupiter.api.Test;
import java.util.Arrays;
import java.util.List;
/**
* 稳态趋势字段解析测试。
*/
class SteadyTrendFieldResolverTest {
private SteadyTrendFieldResolver resolver;
@BeforeEach
void setUp() throws Exception {
AddDataTableRegistry tableRegistry = new AddDataTableRegistry();
tableRegistry.afterPropertiesSet();
resolver = new SteadyTrendFieldResolver(new SteadyTrendIndicatorCatalog(), tableRegistry);
}
@Test
void shouldResolveVoltageRmsAverageAndCp95Fields() {
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", "CP95"));
param.setTimeStart("2026-05-01 00:00:00");
param.setTimeEnd("2026-05-01 01:00:00");
List<SteadyTrendResolvedFieldBO> fields = resolver.resolveFields(param);
Assertions.assertEquals(2, fields.size());
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
void shouldExpandLineVoltageTotalPhaseToThreeSeries() {
SteadyTrendQueryParam param = new SteadyTrendQueryParam();
param.setLineIds(Arrays.asList("line-001"));
param.setIndicatorCodes(Arrays.asList("V_LINE_RMS"));
param.setPhases(Arrays.asList("T"));
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);
Assertions.assertEquals(3, fields.size());
Assertions.assertEquals("RMSAB", fields.get(0).getField());
Assertions.assertEquals("RMSBC", fields.get(1).getField());
Assertions.assertEquals("RMSCA", fields.get(2).getField());
Assertions.assertEquals("T", fields.get(0).getPhase());
}
@Test
void shouldRejectHarmonicTrendWithoutOrders() {
SteadyTrendQueryParam param = new SteadyTrendQueryParam();
param.setLineIds(Arrays.asList("line-001"));
param.setIndicatorCodes(Arrays.asList("V_HARMONIC"));
param.setPhases(Arrays.asList("A"));
param.setStatTypes(Arrays.asList("AVG"));
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("谐波次数不能为空"));
}
@Test
void shouldResolveSelectedHarmonicOrdersOnly() {
SteadyTrendQueryParam param = new SteadyTrendQueryParam();
param.setLineIds(Arrays.asList("line-001"));
param.setIndicatorCodes(Arrays.asList("V_HARMONIC"));
param.setPhases(Arrays.asList("A"));
param.setStatTypes(Arrays.asList("MAX"));
param.setHarmonicOrders(Arrays.asList(3, 5));
param.setTimeStart("2026-05-01 00:00:00");
param.setTimeEnd("2026-05-01 01:00:00");
List<SteadyTrendResolvedFieldBO> fields = resolver.resolveFields(param);
Assertions.assertEquals(2, fields.size());
Assertions.assertEquals("V_3_MAX", fields.get(0).getField());
Assertions.assertEquals("V_5_MAX", fields.get(1).getField());
}
}

View File

@@ -0,0 +1,27 @@
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());
}
}

View File

@@ -0,0 +1,123 @@
package com.njcn.gather.steady.datavie.service.impl;
import com.njcn.gather.steady.datavie.component.SteadyInfluxQueryComponent;
import com.njcn.gather.steady.datavie.component.SteadyTrendFieldResolver;
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.vo.SteadyTrendPointVO;
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 org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.Collections;
/**
* 稳态趋势服务编排测试。
*/
class SteadyDataViewTrendServiceImplTest {
@Test
void shouldBuildTrendQueryWithDefaultBucketAndLineName() {
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("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);
Assertions.assertEquals("1m", result.getBucket());
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;
}
}

View File

@@ -3,6 +3,10 @@ package com.njcn.gather.system.dictionary.pojo.enums;
import lombok.Getter;
import org.apache.commons.lang3.StringUtils;
import static com.njcn.gather.system.pojo.constant.DictConst.REG_RES_CONTRAST_CODE;
import static com.njcn.gather.system.pojo.constant.DictConst.REG_RES_DIGITAL_CODE;
import static com.njcn.gather.system.pojo.constant.DictConst.REG_RES_SIMULATE_CODE;
/**
* @author caozehui
* @data 2024-12-12
@@ -14,9 +18,9 @@ public enum DictDataEnum {
* Key cleanup point: only keep registration-related platform capability
* types that are still referenced by the retained activation flow.
*/
DIGITAL("数字式", "Digital"),
SIMULATE("模拟式", "Simulate"),
CONTRAST("比对式", "Contrast");
DIGITAL("数字式", REG_RES_DIGITAL_CODE),
SIMULATE("模拟式", REG_RES_SIMULATE_CODE),
CONTRAST("比对式", REG_RES_CONTRAST_CODE);
private final String name;
private final String code;

View File

@@ -18,4 +18,24 @@ public interface DictConst {
* 顶层父类的pid
*/
String FATHER_ID = "0";
/**
* 注册资源字典数据编码:数字式
*/
String REG_RES_DIGITAL_CODE = "Digital";
/**
* 注册资源字典数据编码:模拟式
*/
String REG_RES_SIMULATE_CODE = "Simulate";
/**
* 注册资源字典数据编码:比对式
*/
String REG_RES_CONTRAST_CODE = "Contrast";
/**
* 注册资源字典数据 ID比对式
*/
String REG_RES_CONTRAST_ID = "7cd65363a6bf675ae408f28a281b77d4";
}

View File

@@ -12,6 +12,7 @@ import com.njcn.gather.system.cfg.service.ISysTestConfigService;
import com.njcn.gather.system.dictionary.pojo.enums.DictDataEnum;
import com.njcn.gather.system.dictionary.pojo.po.DictData;
import com.njcn.gather.system.dictionary.service.IDictDataService;
import com.njcn.gather.system.pojo.constant.DictConst;
import com.njcn.gather.system.pojo.enums.SystemResponseEnum;
import com.njcn.gather.system.reg.mapper.SysRegResMapper;
import com.njcn.gather.system.reg.pojo.dto.RegInfoData;
@@ -214,7 +215,7 @@ public class SysRegResServiceImpl extends ServiceImpl<SysRegResMapper, SysRegRes
@Override
public SysRegRes getContrastRegRes() {
return this.getRegResByType("7cd65363a6bf675ae408f28a281b77d4");
return this.getRegResByType(DictConst.REG_RES_CONTRAST_ID);
}
/**

View File

@@ -297,7 +297,8 @@ public class WaveVectorComponent {
private float resolveVectorRatio(WaveDataDTO waveDataDTO, int titleIndex) {
if (waveDataDTO.getWaveTitle() != null && waveDataDTO.getWaveTitle().size() > titleIndex
&& StrUtil.startWithIgnoreCase(waveDataDTO.getWaveTitle().get(titleIndex), "U")) {
return waveDataDTO.getPt() == null ? 1F : waveDataDTO.getPt().floatValue() ;
// 电压向量按 kV 展示,需与普通波形查看保持一致,避免将 V 级数值直接标记为 kV。
return waveDataDTO.getPt() == null ? 0.001F : waveDataDTO.getPt().floatValue() / 1000F;
}
return waveDataDTO.getCt() == null ? 1F : waveDataDTO.getCt().floatValue();
}

View File

@@ -0,0 +1,73 @@
package com.njcn.gather.tool.wave.component;
import com.njcn.gather.tool.wave.pojo.dto.ComtradeCfgDTO;
import com.njcn.gather.tool.wave.pojo.dto.WaveDataDTO;
import com.njcn.gather.tool.wave.pojo.dto.WaveVectorGroupDTO;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* 波形向量计算组件测试。
*/
class WaveVectorComponentTest {
@Test
void shouldScaleVoltageVectorValuesToKv() {
WaveVectorComponent component = new WaveVectorComponent();
WaveDataDTO waveDataDTO = buildThreePhaseWaveData("U", 8153.209D, 1D);
List<WaveVectorGroupDTO> groups = component.calculateVectors(waveDataDTO);
Assertions.assertEquals("kV", groups.get(0).getUnit());
Assertions.assertEquals(5.7652F, groups.get(0).getVectorSeries().get(0).getPhaseVectors().get(0).getTotalRms());
Assertions.assertEquals(8.1532F, groups.get(0).getVectorSeries().get(0).getPhaseVectors().get(0).getFundamentalAmplitude());
Assertions.assertEquals(5.7652F, groups.get(0).getVectorSeries().get(0).getPhaseVectors().get(0).getFundamentalRms());
Assertions.assertEquals(5.7652F, groups.get(0).getVectorSeries().get(0).getPositiveSequence().getRms());
}
@Test
void shouldKeepCurrentVectorValuesInAmpere() {
WaveVectorComponent component = new WaveVectorComponent();
WaveDataDTO waveDataDTO = buildThreePhaseWaveData("I", 35D, 1D);
List<WaveVectorGroupDTO> groups = component.calculateVectors(waveDataDTO);
Assertions.assertEquals("A", groups.get(0).getUnit());
Assertions.assertEquals(24.7487F, groups.get(0).getVectorSeries().get(0).getPhaseVectors().get(0).getTotalRms());
Assertions.assertEquals(35F, groups.get(0).getVectorSeries().get(0).getPhaseVectors().get(0).getFundamentalAmplitude());
Assertions.assertEquals(24.7487F, groups.get(0).getVectorSeries().get(0).getPhaseVectors().get(0).getFundamentalRms());
}
private WaveDataDTO buildThreePhaseWaveData(String titlePrefix, double amplitude, double ratio) {
int samplePerCycle = 32;
WaveDataDTO waveDataDTO = new WaveDataDTO();
ComtradeCfgDTO cfgDTO = new ComtradeCfgDTO();
cfgDTO.setFinalSampleRate(samplePerCycle);
waveDataDTO.setComtradeCfgDTO(cfgDTO);
waveDataDTO.setIPhasic(3);
waveDataDTO.setPt(ratio);
waveDataDTO.setCt(ratio);
waveDataDTO.setWaveTitle(Arrays.asList("x", titlePrefix + "A", titlePrefix + "B", titlePrefix + "C"));
waveDataDTO.setChannelNames(Arrays.asList("x", titlePrefix));
waveDataDTO.setListWaveData(buildRows(samplePerCycle, amplitude));
return waveDataDTO;
}
private List<List<Float>> buildRows(int samplePerCycle, double amplitude) {
List<List<Float>> rows = new ArrayList<>();
for (int i = 0; i < samplePerCycle; i++) {
double angle = 2D * Math.PI * i / samplePerCycle;
List<Float> row = new ArrayList<>();
row.add((float) i);
row.add((float) (amplitude * Math.sin(angle)));
row.add((float) (amplitude * Math.sin(angle - 2D * Math.PI / 3D)));
row.add((float) (amplitude * Math.sin(angle + 2D * Math.PI / 3D)));
rows.add(row);
}
return rows;
}
}