diff --git a/entrance/pom.xml b/entrance/pom.xml
index eabe987..16f8932 100644
--- a/entrance/pom.xml
+++ b/entrance/pom.xml
@@ -63,6 +63,11 @@
event-list
1.0.0
+
+ com.njcn.gather
+ steady-DataView
+ 1.0.0
+
diff --git a/event/event-list/API_DEBUG.md b/event/event-list/API_DEBUG.md
new file mode 100644
index 0000000..263bd1a
--- /dev/null
+++ b/event/event-list/API_DEBUG.md
@@ -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
+```
+
+说明:
+
+- `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>`
+- 默认排序:`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 | 否 | 监测点 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 " `
+ -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`
+
+### 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 "
+```
+
+## 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 " `
+ -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` 实际定义和前端现有调用约定为准。
diff --git a/pom.xml b/pom.xml
index 18b8fde..85cf6bb 100644
--- a/pom.xml
+++ b/pom.xml
@@ -17,6 +17,7 @@
detection
tools
event
+ steady
diff --git a/steady/pom.xml b/steady/pom.xml
new file mode 100644
index 0000000..11d39b7
--- /dev/null
+++ b/steady/pom.xml
@@ -0,0 +1,24 @@
+
+
+ 4.0.0
+
+ com.njcn.gather
+ CN_Tool
+ 1.0.0
+
+
+ steady
+ pom
+
+
+ steady-DataView
+
+
+
+ 8
+ 8
+ UTF-8
+
+
diff --git a/steady/steady-DataView/API_DEBUG.md b/steady/steady-DataView/API_DEBUG.md
new file mode 100644
index 0000000..13a59b9
--- /dev/null
+++ b/steady/steady-DataView/API_DEBUG.md
@@ -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>`
+- 默认表:`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`
+
+请求示例:
+
+```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>`
+
+模板来自 `tools/add-data` 的前端展示模板,返回参数名称、表名、相别和当前表可展示值字段。
+
+## 5. 查询趋势台账树
+
+- 路径:`GET /steady/data-view/ledger-tree`
+- 返回:`HttpResult>`
+- 查询参数:`keyword`,可选,按台账节点名称搜索并保留父级路径。
+
+节点字段:
+
+| 字段 | 说明 |
+| --- | --- |
+| `id` | 台账节点 ID |
+| `parentId` | 父节点 ID |
+| `name` | 节点名称 |
+| `level` | 层级:0 工程,1 项目,2 设备,3 监测点 |
+| `deviceCount` | 当前节点下有效设备数 |
+| `lineCount` | 当前节点下有效监测点数 |
+| `selectable` | 是否可直接选择 |
+| `children` | 子节点 |
+
+## 6. 查询趋势指标树
+
+- 路径:`GET /steady/data-view/indicator-tree`
+- 返回:`HttpResult>`
+
+当前指标目录覆盖:
+
+- 电压趋势:`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`
+
+请求示例:
+
+```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`
+
+请求体与 `/trend/query` 一致。前端切换日期或加载某一天数据时,将 `timeStart`、`timeEnd` 控制在当天范围即可。
+
+## 9. 查询趋势统计摘要
+
+- 路径:`POST /steady/data-view/trend/summary`
+- 返回:`HttpResult`
+
+请求体与 `/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` 查询。
diff --git a/steady/steady-DataView/pom.xml b/steady/steady-DataView/pom.xml
new file mode 100644
index 0000000..2d661e8
--- /dev/null
+++ b/steady/steady-DataView/pom.xml
@@ -0,0 +1,51 @@
+
+
+ 4.0.0
+
+ com.njcn.gather
+ steady
+ 1.0.0
+
+
+ steady-DataView
+
+
+
+ com.njcn
+ njcn-common
+ 0.0.1
+
+
+
+ com.njcn
+ mybatis-plus
+ 0.0.1
+
+
+
+ com.njcn
+ spingboot2.3.12
+ 2.3.12
+
+
+
+ com.njcn.gather
+ add-ledger
+ 1.0.0
+
+
+
+ com.njcn.gather
+ add-data
+ 1.0.0
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
diff --git a/steady/steady-DataView/src/main/java/com/njcn/gather/steady/datavie/component/SteadyInfluxQueryComponent.java b/steady/steady-DataView/src/main/java/com/njcn/gather/steady/datavie/component/SteadyInfluxQueryComponent.java
new file mode 100644
index 0000000..726ba6a
--- /dev/null
+++ b/steady/steady-DataView/src/main/java/com/njcn/gather/steady/datavie/component/SteadyInfluxQueryComponent.java
@@ -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 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 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();
+ }
+ List result = new ArrayList();
+ 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);
+ }
+}
diff --git a/steady/steady-DataView/src/main/java/com/njcn/gather/steady/datavie/component/SteadyTrendFieldResolver.java b/steady/steady-DataView/src/main/java/com/njcn/gather/steady/datavie/component/SteadyTrendFieldResolver.java
new file mode 100644
index 0000000..2fc1078
--- /dev/null
+++ b/steady/steady-DataView/src/main/java/com/njcn/gather/steady/datavie/component/SteadyTrendFieldResolver.java
@@ -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 resolveFields(SteadyTrendQueryParam param) {
+ validateBasicParam(param);
+ List lineIds = normalizeTextList(param.getLineIds());
+ List indicatorCodes = normalizeTextList(param.getIndicatorCodes());
+ List requestPhases = normalizeUpperList(param.getPhases());
+ List statTypes = normalizeUpperList(param.getStatTypes());
+ if (statTypes.isEmpty()) {
+ statTypes.add("AVG");
+ }
+ List result = new ArrayList();
+ for (String lineId : lineIds) {
+ for (String indicatorCode : indicatorCodes) {
+ SteadyTrendIndicatorDefinitionBO indicator = requireIndicator(indicatorCode);
+ List 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 resolveIndicatorFields(String lineId, SteadyTrendIndicatorDefinitionBO indicator,
+ String phase, String statType, List harmonicOrders) {
+ if (Boolean.TRUE.equals(indicator.getHarmonic())) {
+ return resolveHarmonicFields(lineId, indicator, phase, statType, harmonicOrders);
+ }
+ List result = new ArrayList();
+ 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 resolveHarmonicFields(String lineId, SteadyTrendIndicatorDefinitionBO indicator,
+ String phase, String statType, List harmonicOrders) {
+ List orders = normalizeOrders(harmonicOrders);
+ if (orders.isEmpty()) {
+ throw fail("谐波次数不能为空");
+ }
+ if (orders.size() > MAX_HARMONIC_ORDER_COUNT) {
+ throw fail("谐波次数最多选择 6 个");
+ }
+ List result = new ArrayList();
+ 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 lineIds = normalizeTextList(param.getLineIds());
+ List 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 resolvePhases(SteadyTrendIndicatorDefinitionBO indicator, List requestPhases) {
+ if (requestPhases.isEmpty()) {
+ return new ArrayList(indicator.getPhaseCodes());
+ }
+ List result = new ArrayList();
+ 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 normalizeTextList(List values) {
+ if (values == null || values.isEmpty()) {
+ return new ArrayList();
+ }
+ List result = new ArrayList();
+ for (String value : values) {
+ String text = trimToNull(value);
+ if (text != null && !result.contains(text)) {
+ result.add(text);
+ }
+ }
+ return result;
+ }
+
+ private List normalizeUpperList(List values) {
+ List result = normalizeTextList(values);
+ for (int i = 0; i < result.size(); i++) {
+ result.set(i, result.get(i).toUpperCase());
+ }
+ return result;
+ }
+
+ private List normalizeOrders(List orders) {
+ if (orders == null || orders.isEmpty()) {
+ return Collections.emptyList();
+ }
+ List result = new ArrayList();
+ 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);
+ }
+}
diff --git a/steady/steady-DataView/src/main/java/com/njcn/gather/steady/datavie/component/SteadyTrendIndicatorCatalog.java b/steady/steady-DataView/src/main/java/com/njcn/gather/steady/datavie/component/SteadyTrendIndicatorCatalog.java
new file mode 100644
index 0000000..68aade7
--- /dev/null
+++ b/steady/steady-DataView/src/main/java/com/njcn/gather/steady/datavie/component/SteadyTrendIndicatorCatalog.java
@@ -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 FULL_STATS = Collections.unmodifiableList(Arrays.asList("AVG", "MAX", "MIN", "CP95"));
+ private static final List AVG_ONLY = Collections.unmodifiableList(Collections.singletonList("AVG"));
+ private static final List ABC_PHASES = Collections.unmodifiableList(Arrays.asList("A", "B", "C"));
+ private static final List T_PHASE = Collections.unmodifiableList(Collections.singletonList("T"));
+
+ private final List indicators;
+ private final Map indicatorMap;
+
+ public SteadyTrendIndicatorCatalog() {
+ List result = new ArrayList();
+ 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 map = new LinkedHashMap();
+ for (SteadyTrendIndicatorDefinitionBO indicator : indicators) {
+ map.put(indicator.getIndicatorCode(), indicator);
+ }
+ indicatorMap = Collections.unmodifiableMap(map);
+ }
+
+ public List 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(), 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 phaseCodes,
+ List seriesFields,
+ List 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(phaseCodes));
+ indicator.setSeriesFields(new ArrayList(seriesFields));
+ indicator.setSupportStats(new ArrayList(supportStats));
+ indicator.setUnit(unit);
+ return indicator;
+ }
+
+ private List fields(SteadyTrendSeriesFieldBO... fields) {
+ return new ArrayList(Arrays.asList(fields));
+ }
+
+ private SteadyTrendSeriesFieldBO field(String field, String name) {
+ return new SteadyTrendSeriesFieldBO(field, name);
+ }
+}
diff --git a/steady/steady-DataView/src/main/java/com/njcn/gather/steady/datavie/config/SteadyInfluxDbProperties.java b/steady/steady-DataView/src/main/java/com/njcn/gather/steady/datavie/config/SteadyInfluxDbProperties.java
new file mode 100644
index 0000000..5db0cdf
--- /dev/null
+++ b/steady/steady-DataView/src/main/java/com/njcn/gather/steady/datavie/config/SteadyInfluxDbProperties.java
@@ -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;
+}
diff --git a/steady/steady-DataView/src/main/java/com/njcn/gather/steady/datavie/config/SteadySecondTimeSerializer.java b/steady/steady-DataView/src/main/java/com/njcn/gather/steady/datavie/config/SteadySecondTimeSerializer.java
new file mode 100644
index 0000000..80d7527
--- /dev/null
+++ b/steady/steady-DataView/src/main/java/com/njcn/gather/steady/datavie/config/SteadySecondTimeSerializer.java
@@ -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 {
+
+ 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));
+ }
+}
diff --git a/steady/steady-DataView/src/main/java/com/njcn/gather/steady/datavie/controller/SteadyDataViewController.java b/steady/steady-DataView/src/main/java/com/njcn/gather/steady/datavie/controller/SteadyDataViewController.java
new file mode 100644
index 0000000..143d12b
--- /dev/null
+++ b/steady/steady-DataView/src/main/java/com/njcn/gather/steady/datavie/controller/SteadyDataViewController.java
@@ -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> pageSteadyData(@RequestBody SteadyDataViewQueryParam param) {
+ String methodDescribe = getMethodDescribe("pageSteadyData");
+ LogUtil.njcnDebug(log, "{},开始分页查询稳态数据,param={}", methodDescribe, param);
+ Page result = steadyDataViewService.pageSteadyData(param);
+ return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, result, methodDescribe);
+ }
+
+ @OperateInfo(info = LogEnum.BUSINESS_COMMON)
+ @ApiOperation("查询稳态数据详情")
+ @PostMapping("/detail")
+ public HttpResult 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> listTemplates() {
+ String methodDescribe = getMethodDescribe("listTemplates");
+ LogUtil.njcnDebug(log, "{},开始查询稳态数据模板", methodDescribe);
+ List result = steadyDataViewService.listTemplates();
+ return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, result, methodDescribe);
+ }
+}
diff --git a/steady/steady-DataView/src/main/java/com/njcn/gather/steady/datavie/controller/SteadyDataViewIndicatorController.java b/steady/steady-DataView/src/main/java/com/njcn/gather/steady/datavie/controller/SteadyDataViewIndicatorController.java
new file mode 100644
index 0000000..b8a842c
--- /dev/null
+++ b/steady/steady-DataView/src/main/java/com/njcn/gather/steady/datavie/controller/SteadyDataViewIndicatorController.java
@@ -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> listIndicatorTree() {
+ String methodDescribe = getMethodDescribe("listIndicatorTree");
+ LogUtil.njcnDebug(log, "{},开始查询稳态趋势指标树", methodDescribe);
+ List result = indicatorService.listIndicatorTree();
+ return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, result, methodDescribe);
+ }
+}
diff --git a/steady/steady-DataView/src/main/java/com/njcn/gather/steady/datavie/controller/SteadyDataViewLedgerController.java b/steady/steady-DataView/src/main/java/com/njcn/gather/steady/datavie/controller/SteadyDataViewLedgerController.java
new file mode 100644
index 0000000..6b6e3af
--- /dev/null
+++ b/steady/steady-DataView/src/main/java/com/njcn/gather/steady/datavie/controller/SteadyDataViewLedgerController.java
@@ -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> listLedgerTree(@RequestParam(value = "keyword", required = false) String keyword) {
+ String methodDescribe = getMethodDescribe("listLedgerTree");
+ LogUtil.njcnDebug(log, "{},开始查询稳态趋势台账树,keyword={}", methodDescribe, keyword);
+ List result = ledgerService.listLedgerTree(keyword);
+ return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, result, methodDescribe);
+ }
+}
diff --git a/steady/steady-DataView/src/main/java/com/njcn/gather/steady/datavie/controller/SteadyDataViewTrendController.java b/steady/steady-DataView/src/main/java/com/njcn/gather/steady/datavie/controller/SteadyDataViewTrendController.java
new file mode 100644
index 0000000..87a9e5b
--- /dev/null
+++ b/steady/steady-DataView/src/main/java/com/njcn/gather/steady/datavie/controller/SteadyDataViewTrendController.java
@@ -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 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 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 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);
+ }
+}
diff --git a/steady/steady-DataView/src/main/java/com/njcn/gather/steady/datavie/mapper/SteadyDataViewLedgerMapper.java b/steady/steady-DataView/src/main/java/com/njcn/gather/steady/datavie/mapper/SteadyDataViewLedgerMapper.java
new file mode 100644
index 0000000..dae839d
--- /dev/null
+++ b/steady/steady-DataView/src/main/java/com/njcn/gather/steady/datavie/mapper/SteadyDataViewLedgerMapper.java
@@ -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 selectLedgerTree(@Param("keyword") String keyword);
+}
diff --git a/steady/steady-DataView/src/main/java/com/njcn/gather/steady/datavie/mapper/SteadyDataViewMapper.java b/steady/steady-DataView/src/main/java/com/njcn/gather/steady/datavie/mapper/SteadyDataViewMapper.java
new file mode 100644
index 0000000..9791876
--- /dev/null
+++ b/steady/steady-DataView/src/main/java/com/njcn/gather/steady/datavie/mapper/SteadyDataViewMapper.java
@@ -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