Compare commits
11 Commits
6c4a4e05c1
...
2026-04
| Author | SHA1 | Date | |
|---|---|---|---|
| 2c1d926261 | |||
| 34b1a81c70 | |||
| 81e4ff4009 | |||
| f57fd45b47 | |||
| f69eed857f | |||
| 76a254fef4 | |||
| 6258c2dda5 | |||
|
|
b3e679249b | ||
| 98b3f0dbe8 | |||
| 04e1dc2659 | |||
| 35471714c8 |
@@ -33,6 +33,7 @@ Java 源码位于 `src/main/java`,配置文件位于 `src/main/resources`,My
|
||||
|
||||
## 代码风格与命名规范
|
||||
保持现有 Java 风格:4 空格缩进、UTF-8 文件编码、基础包名使用 `com.njcn.gather`。命名沿用分层后缀,如 `*Controller`、`*Service`、`*ServiceImpl`、`*Mapper`、`*Param`、`*PO`、`*VO`。优先复用现有 Lombok 注解,如 `@Data`、`@RequiredArgsConstructor`、`@Slf4j`。Mapper XML 文件名应与接口名保持一致。业务代码中,关键流程、分支判断、状态流转或容易误解的节点需要补充简洁的中文注释,但不要添加无信息量的注释。
|
||||
- 参照 `user` 模块的组织方式,Controller 与 Service 都按职责拆分;不同职责的方法放到不同 `*Controller`、`*Service`、`*ServiceImpl` 中,同一模块后续新增方法也要沿既有职责边界归类,不再回退为单一大类承载全部接口。
|
||||
|
||||
## 数据与 SQL 约束
|
||||
- 新增业务表的 DO 优先复用当前 `BaseDO` / 审计字段风格;除非表本身明确不需要逻辑删除,不要再引入另一套审计基类。
|
||||
|
||||
@@ -26,6 +26,7 @@ CN_Tool 是一个基于 Spring Boot 的多模块后端聚合工程,当前仓
|
||||
其中 `tools` 当前包含:
|
||||
|
||||
- `activate-tool`
|
||||
- `add-data`
|
||||
- `mms-mapping`
|
||||
- `wave-tool`
|
||||
|
||||
@@ -35,7 +36,7 @@ CN_Tool 是一个基于 Spring Boot 的多模块后端聚合工程,当前仓
|
||||
|
||||
- `entrance/src/main/java/com/njcn/gather/EntranceApplication.java`
|
||||
|
||||
`entrance` 模块聚合了 `system`、`disk-monitor`、`user`、`detection`、`activate-tool`、`wave-tool`、`mms-mapping`,是当前运行时主入口。
|
||||
`entrance` 模块聚合了 `system`、`disk-monitor`、`user`、`detection`、`activate-tool`、`add-data`、`wave-tool`、`mms-mapping`,是当前运行时主入口。
|
||||
|
||||
## 技术基线
|
||||
|
||||
@@ -82,6 +83,8 @@ P0 已补齐基线文档,建议按以下顺序阅读:
|
||||
- 当前以通信基础设施为主,包含 WebSocket / Netty 相关组件
|
||||
- `tools/activate-tool`
|
||||
- 负责激活码生成、激活码验证、许可证读取等能力
|
||||
- `tools/add-data`
|
||||
- 当前提供电能质量 13 张表批量补数、任务状态查询和模板规则查询能力
|
||||
- `tools/mms-mapping`
|
||||
- 负责 ICD 文件解析与 MMS 映射数据生成能力
|
||||
- `tools/wave-tool`
|
||||
|
||||
@@ -0,0 +1,789 @@
|
||||
# 电能质量 13 张表批量补数设计说明
|
||||
|
||||
## 1. 背景
|
||||
|
||||
`tools/add-data` 当前只有模块骨架和历史 SQL 文件 [DATA_FLICKER.sql](D:/Work/SourceCode/CN_Tool/tools/add-data/DATA_FLICKER.sql),尚未提供真实业务接口。
|
||||
|
||||
本次需求是在 `add-data` 模块中补齐一套“按时间区间批量生成电能质量数据并写入数据库”的能力,满足以下业务目标:
|
||||
|
||||
- 支持按时间区间补数,例如一天、一月。
|
||||
- 支持时间戳步长选择:`1`、`3`、`5`、`10` 分钟。
|
||||
- 支持单监测点和多监测点,前端可选择监测点 ID,默认单监测点。
|
||||
- 支持批量写入,优先保证大区间补数时的效率。
|
||||
- 同一主键数据已存在时跳过,不覆盖、不删除。
|
||||
- 同步提供前端页面交互方案与参数展示规则。
|
||||
|
||||
## 2. 范围确认
|
||||
|
||||
根据当前 [DATA_FLICKER.sql](D:/Work/SourceCode/CN_Tool/tools/add-data/DATA_FLICKER.sql) 内容,本次补数范围按 13 张表处理,而不是 14 张表:
|
||||
|
||||
1. `data_flicker`
|
||||
2. `data_fluc`
|
||||
3. `data_harmphasic_i`
|
||||
4. `data_harmphasic_v`
|
||||
5. `data_harmpower_p`
|
||||
6. `data_harmpower_q`
|
||||
7. `data_harmpower_s`
|
||||
8. `data_harmrate_i`
|
||||
9. `data_harmrate_v`
|
||||
10. `data_i`
|
||||
11. `data_inharm_i`
|
||||
12. `data_plt`
|
||||
13. `data_v`
|
||||
|
||||
## 3. 已确认需求
|
||||
|
||||
本次设计基于以下已确认结论:
|
||||
|
||||
- 写入方式是“按时间戳步长生成整段数据”,不是定时常驻写库。
|
||||
- 重复补数时采用“跳过已存在数据”策略。
|
||||
- 前端方案需要和后端接口一起设计。
|
||||
- 分表时间轴规则已确认:
|
||||
- `data_flicker` 固定每 `10` 分钟写一组
|
||||
- `data_fluc` 固定每 `10` 分钟写一组
|
||||
- `data_plt` 固定每 `2` 小时写一组
|
||||
- 其余 `10` 张表按前端选择的 `1`、`3`、`5`、`10` 分钟步长写入
|
||||
- 所有时间点都按自然时间槽对齐,采用整点整分方式生成,不按传入开始时间顺延
|
||||
- 展示字段需要覆盖:
|
||||
- `电能质量参数名称`
|
||||
- `相别`
|
||||
- `是否合格`
|
||||
- `最大值`
|
||||
- `最小值`
|
||||
- `平均值`
|
||||
- `95%概率大值`
|
||||
- `数据展示(保留小数)`
|
||||
- 数值展示默认保留 `2` 位小数。
|
||||
- 相别参照用户提供图片中的“相别”列。
|
||||
|
||||
## 4. 关键约束
|
||||
|
||||
### 4.1 主键约束
|
||||
|
||||
13 张表均以 `LINEID + TIMEID + PHASIC_TYPE` 作为主键,重复写入时必须避免主键冲突导致整批失败。
|
||||
|
||||
### 4.2 相别字段长度约束
|
||||
|
||||
`PHASIC_TYPE` 字段长度为 `varchar(2)`。因此前端展示中的相别值和数据库实际落库值不能简单等同:
|
||||
|
||||
- 页面展示可使用 `T`、`ABC`、`AB/BC/CA`
|
||||
- 落库时必须拆分为真实可存储的 `PHASIC_TYPE`
|
||||
|
||||
### 4.3 数据量约束
|
||||
|
||||
当时间区间为“一月”、步长为“1 分钟”、监测点为“多选”时,补数规模会非常大,不能采用单条插入或一次性全量加载入内存的方式。
|
||||
|
||||
### 4.4 现有仓库约束
|
||||
|
||||
- 当前仓库是后端工程,没有现成前端代码,本次只输出前端交互方案和接口契约。
|
||||
- 默认不执行 `mvn` 编译、打包、测试。
|
||||
- 修改范围应尽量收敛在 `tools/add-data` 模块内。
|
||||
|
||||
## 5. 方案选择
|
||||
|
||||
本次比较三种实现路径:
|
||||
|
||||
### 方案 A:同步接口直接写库
|
||||
|
||||
特点:
|
||||
|
||||
- 控制器接收参数后直接在请求线程中完成全部写库。
|
||||
- 实现最简单。
|
||||
|
||||
问题:
|
||||
|
||||
- 大区间、多监测点时容易超时。
|
||||
- 任务执行中断后难以定位进度。
|
||||
- 前端体验差,无法明确看到当前执行状态。
|
||||
|
||||
结论:
|
||||
|
||||
不采用。
|
||||
|
||||
### 方案 B:异步任务 + JDBC 批量写入
|
||||
|
||||
特点:
|
||||
|
||||
- 请求只负责创建任务,后台异步执行。
|
||||
- 任务状态可轮询。
|
||||
- 批量写入时可按表、按批次提交。
|
||||
|
||||
优势:
|
||||
|
||||
- 更适合月级、多监测点补数。
|
||||
- 可以精细控制批次大小。
|
||||
- 当前数据源连接串已开启 `rewriteBatchedStatements=true`,适合批处理。
|
||||
|
||||
结论:
|
||||
|
||||
采用该方案。
|
||||
|
||||
### 方案 C:只生成 SQL 文件,人工执行
|
||||
|
||||
特点:
|
||||
|
||||
- 后端只生成 SQL 脚本,不直接写库。
|
||||
|
||||
问题:
|
||||
|
||||
- 需要人工执行,流程割裂。
|
||||
- 不符合“前端选择后直接补数”的目标。
|
||||
|
||||
结论:
|
||||
|
||||
不采用。
|
||||
|
||||
## 6. 总体设计
|
||||
|
||||
### 6.1 设计目标
|
||||
|
||||
在 `tools/add-data` 中新增一套“批量补数任务”能力,支持:
|
||||
|
||||
- 前端提交补数参数
|
||||
- 后端预估本次写入规模
|
||||
- 后端异步生成数据并按表批量写入
|
||||
- 前端轮询任务状态
|
||||
- 前端获取参数展示规则
|
||||
|
||||
### 6.2 模块边界
|
||||
|
||||
本次能力只落在 `tools/add-data`,不向 `system`、`user`、`detection` 扩散业务逻辑。
|
||||
|
||||
如需监测点列表,首版不假设存在基础监测点表,也不新增点位管理能力,直接由前端传入选中的 `lineIds`。
|
||||
|
||||
### 6.3 接口拆分
|
||||
|
||||
建议把“任务管理”和“参数模板展示”分成两类接口:
|
||||
|
||||
- 补数任务接口:负责预估、创建、状态查询
|
||||
- 模板展示接口:负责返回图片对应的参数展示规则
|
||||
|
||||
## 7. 数据模型与相别规则
|
||||
|
||||
### 7.1 展示相别与落库相别分离
|
||||
|
||||
前端展示相别来自用户图片中的“相别”列,但落库时需要展开成真正的 `PHASIC_TYPE` 值。
|
||||
|
||||
建议在模板中同时维护:
|
||||
|
||||
- `phaseDisplay`:前端展示值
|
||||
- `phaseCodes`:落库使用的真实相别集合
|
||||
|
||||
### 7.2 相别展开规则
|
||||
|
||||
建议默认规则如下:
|
||||
|
||||
- `T` -> `["T"]`
|
||||
- `ABC` -> `["A", "B", "C"]`
|
||||
- `AB/BC/CA` -> `["AB", "BC", "CA"]`
|
||||
|
||||
这套规则同时用于:
|
||||
|
||||
- 决定每个参数需要展开多少条写库数据
|
||||
- 决定前端参数表如何显示相别列
|
||||
|
||||
### 7.3 参数模板职责
|
||||
|
||||
参数模板不直接等于数据库表结构,而是承担以下职责:
|
||||
|
||||
- 描述前端展示行
|
||||
- 绑定该参数对应的数据表
|
||||
- 描述展示相别和落库相别
|
||||
- 约定是否展示“是否合格”
|
||||
- 约定最大值、最小值、平均值、95%概率大值的展示逻辑
|
||||
- 约定保留小数位数,默认 `2`
|
||||
|
||||
## 8. 后端接口设计
|
||||
|
||||
### 8.1 预估接口
|
||||
|
||||
接口:
|
||||
|
||||
- `POST /addData/task/preview`
|
||||
|
||||
作用:
|
||||
|
||||
- 根据 `lineIds`、时间区间、时间戳步长,返回本次补数的预估规模
|
||||
|
||||
入参:
|
||||
|
||||
- `lineIds`:监测点 ID 列表
|
||||
- `startTime`:开始时间
|
||||
- `endTime`:结束时间
|
||||
- `intervalMinutes`:`1`、`3`、`5`、`10`
|
||||
|
||||
返回内容:
|
||||
|
||||
- 时间点数量
|
||||
- 监测点数量
|
||||
- 每张表预估写入条数
|
||||
- 总预估条数
|
||||
|
||||
说明:
|
||||
|
||||
- 预估逻辑必须按分表时间轴分别计算。
|
||||
- `intervalMinutes` 只影响 `data_harmphasic_i`、`data_harmphasic_v`、`data_harmpower_p`、`data_harmpower_q`、`data_harmpower_s`、`data_harmrate_i`、`data_harmrate_v`、`data_i`、`data_inharm_i`、`data_v` 这 `10` 张表。
|
||||
- `data_flicker`、`data_fluc` 固定按 `10` 分钟时间槽计算。
|
||||
- `data_plt` 固定按 `2` 小时时间槽计算。
|
||||
|
||||
### 8.2 创建任务接口
|
||||
|
||||
接口:
|
||||
|
||||
- `POST /addData/task/create`
|
||||
|
||||
作用:
|
||||
|
||||
- 创建一个批量补数任务,并立即进入后台执行
|
||||
|
||||
入参:
|
||||
|
||||
- `lineIds`
|
||||
- `startTime`
|
||||
- `endTime`
|
||||
- `intervalMinutes`
|
||||
|
||||
返回内容:
|
||||
|
||||
- `taskId`
|
||||
- 初始状态
|
||||
|
||||
说明:
|
||||
|
||||
- 前端默认提交单个 `lineId`
|
||||
- 多监测点时前端直接提交多个 `lineIds`
|
||||
- 不单独拆分“单点接口”和“多点接口”
|
||||
- `intervalMinutes` 仅作为 `10` 张基础实时类表的生成步长
|
||||
- 固定频率表仍按各自时间轴生成,不跟随 `intervalMinutes`
|
||||
|
||||
### 8.3 任务状态接口
|
||||
|
||||
接口:
|
||||
|
||||
- `GET /addData/task/status/{taskId}`
|
||||
|
||||
作用:
|
||||
|
||||
- 供前端轮询当前任务执行状态
|
||||
|
||||
返回内容:
|
||||
|
||||
- 任务状态:`WAITING`、`RUNNING`、`SUCCESS`、`FAILED`
|
||||
- 当前执行表名
|
||||
- 当前批次信息
|
||||
- 已写入条数
|
||||
- 已跳过条数
|
||||
- 失败条数
|
||||
- 失败原因
|
||||
- 开始时间、结束时间
|
||||
|
||||
### 8.4 参数模板接口
|
||||
|
||||
接口:
|
||||
|
||||
- `GET /addData/template/list`
|
||||
|
||||
作用:
|
||||
|
||||
- 返回前端页面参数规则表所需的静态配置
|
||||
|
||||
返回内容:
|
||||
|
||||
- 电能质量参数名称
|
||||
- 相别展示值
|
||||
- 是否展示“是否合格”
|
||||
- 最大值规则
|
||||
- 最小值规则
|
||||
- 平均值规则
|
||||
- 95%概率大值规则
|
||||
- 保留小数位数
|
||||
|
||||
## 9. 后端分层设计
|
||||
|
||||
### 9.1 Controller 层
|
||||
|
||||
建议新增两个控制器:
|
||||
|
||||
- `AddDataTaskController`
|
||||
- 补数任务相关接口
|
||||
- `AddDataTemplateController`
|
||||
- 参数模板查询接口
|
||||
|
||||
### 9.2 Service 层
|
||||
|
||||
建议拆分为:
|
||||
|
||||
- `AddDataTaskService`
|
||||
- 创建任务
|
||||
- 状态查询
|
||||
- 预估规模
|
||||
- `AddDataTemplateService`
|
||||
- 返回前端参数模板
|
||||
|
||||
### 9.3 Component 层
|
||||
|
||||
建议新增以下组件:
|
||||
|
||||
- `AddDataTaskExecutor`
|
||||
- 负责异步执行任务
|
||||
- `AddDataBatchWriter`
|
||||
- 负责按表批量写入
|
||||
- `AddDataValueGenerator`
|
||||
- 负责根据模板生成每条记录的值
|
||||
- `AddDataTaskStatusHolder`
|
||||
- 首版用内存保存任务状态
|
||||
- `AddDataTemplateRegistry`
|
||||
- 持有参数模板与相别映射
|
||||
|
||||
### 9.4 数据访问层
|
||||
|
||||
本次不建议为 13 张表分别创建超长 MyBatis XML 批量插入语句。
|
||||
|
||||
原因如下:
|
||||
|
||||
- 宽表字段极多,XML 可维护性差。
|
||||
- 13 张表结构差异大,重复代码很多。
|
||||
- 本次更偏工具型批处理写数,不适合以业务实体方式逐表建模。
|
||||
|
||||
建议采用:
|
||||
|
||||
- `JdbcTemplate`
|
||||
- `PreparedStatement`
|
||||
- `batchUpdate`
|
||||
|
||||
在写入 SQL 上统一使用:
|
||||
|
||||
- `INSERT IGNORE`
|
||||
|
||||
以满足“跳过已存在数据”的业务要求。
|
||||
|
||||
## 10. 批量写入策略
|
||||
|
||||
### 10.1 总体原则
|
||||
|
||||
批量写入必须满足以下原则:
|
||||
|
||||
- 不先查存在再插入
|
||||
- 不整段数据一次性全部放入内存
|
||||
- 按表、按批次逐步提交
|
||||
- 主键冲突自动跳过
|
||||
|
||||
### 10.2 跳过已存在数据
|
||||
|
||||
采用 `INSERT IGNORE` 而不是以下策略:
|
||||
|
||||
- 不采用“先查后写”,避免额外查询放大数据库压力
|
||||
- 不采用“覆盖更新”,与已确认需求不符
|
||||
- 不采用“先删后写”,避免误删
|
||||
|
||||
### 10.3 流式生成
|
||||
|
||||
建议按如下顺序流式生成:
|
||||
|
||||
1. 先遍历时间点
|
||||
2. 再遍历监测点
|
||||
3. 再按模板展开相别
|
||||
4. 最后路由到对应表的批缓存
|
||||
|
||||
达到批次阈值后立即落库并清空当前缓存。
|
||||
|
||||
### 10.4 分批阈值
|
||||
|
||||
建议根据表宽度分层设置批次大小:
|
||||
|
||||
- 窄表:`500 ~ 1000` 行/批
|
||||
- 宽表:`100 ~ 200` 行/批
|
||||
|
||||
其中以下表建议视为宽表:
|
||||
|
||||
- `data_harmphasic_i`
|
||||
- `data_harmphasic_v`
|
||||
- `data_harmpower_p`
|
||||
- `data_harmpower_q`
|
||||
- `data_harmpower_s`
|
||||
- `data_harmrate_i`
|
||||
- `data_harmrate_v`
|
||||
- `data_i`
|
||||
- `data_inharm_i`
|
||||
- `data_v`
|
||||
|
||||
### 10.5 任务统计
|
||||
|
||||
任务执行过程中需累计以下统计值:
|
||||
|
||||
- `insertedCount`
|
||||
- `skippedCount`
|
||||
- `failedCount`
|
||||
|
||||
其中:
|
||||
|
||||
- `insertedCount` 表示本次真正插入成功的行数
|
||||
- `skippedCount` 表示因主键重复被忽略的行数
|
||||
- `failedCount` 表示非主键冲突导致的失败数量
|
||||
|
||||
## 11. 数据生成策略
|
||||
|
||||
### 11.1 首版原则
|
||||
|
||||
首版不引入复杂公式配置,不让前端传计算表达式。数据生成规则由后端内置。
|
||||
|
||||
### 11.2 分表时间轴规则
|
||||
|
||||
本次补数不采用“全部表共用同一时间轴”的方式,而是拆成三类时间轴:
|
||||
|
||||
1. 固定 `10` 分钟时间轴
|
||||
2. 固定 `2` 小时时间轴
|
||||
3. 用户步长时间轴
|
||||
|
||||
具体规则如下:
|
||||
|
||||
- `data_flicker`
|
||||
- 固定每 `10` 分钟一组
|
||||
- 按自然时间槽对齐,例如 `10:00`、`10:10`、`10:20`
|
||||
- `data_fluc`
|
||||
- 固定每 `10` 分钟一组
|
||||
- 按自然时间槽对齐
|
||||
- `data_plt`
|
||||
- 固定每 `2` 小时一组
|
||||
- 按自然时间槽对齐,例如 `00:00`、`02:00`、`04:00`
|
||||
- 其余 `10` 张表
|
||||
- 按前端传入的 `intervalMinutes` 生成
|
||||
- 允许值仅为 `1`、`3`、`5`、`10`
|
||||
- 同样按自然时间槽对齐
|
||||
|
||||
如果前端传入 `startTime=10:07`:
|
||||
|
||||
- `10` 分钟表应从 `10:10` 开始
|
||||
- `2` 小时表应从 `12:00` 或下一个符合槽位的时刻开始
|
||||
- 用户步长表应从不小于 `startTime` 的下一个对应槽位开始
|
||||
|
||||
### 11.3 统一生成方式
|
||||
|
||||
建议采用“基础状态生成 + 派生字段回填 + 分表时间轴裁切”的方式:
|
||||
|
||||
- 先按 `(lineId, timeId)` 生成基础电气状态
|
||||
- 再从基础状态派生电压、电流、谐波、功率、闪变
|
||||
- 最后按各表自己的时间轴决定是否落库
|
||||
|
||||
统一原则如下:
|
||||
|
||||
- 同一时间点的 13 张表数据必须同源,不允许各表独立随机
|
||||
- 主值字段按基准值和受控扰动生成
|
||||
- 最大值字段不小于主值
|
||||
- 最小值字段不大于主值
|
||||
- `95%概率大值` 对应 `CP95` 字段
|
||||
- 宽表中的谐波分量和派生列按统一规则回填
|
||||
- 同一输入参数应尽量生成可复现的结果,不建议使用完全无约束随机数
|
||||
|
||||
### 11.4 逐表生成规律
|
||||
|
||||
#### 11.4.1 基础源表
|
||||
|
||||
`data_v` 作为电压源表,优先生成:
|
||||
|
||||
- 先生成 `FREQ`
|
||||
- 再生成 `RMS`
|
||||
- 再派生 `RMSAB`、`RMSBC`、`RMSCA`
|
||||
- 再派生 `VU_DEV`、`VL_DEV`、`FREQ_DEV`
|
||||
- 再派生 `V_POS`、`V_NEG`、`V_ZERO`、`V_UNBALANCE`
|
||||
- 最后生成 `V_1 ~ V_50`
|
||||
- `V_THD` 由 `V_2 ~ V_50` 与 `V_1` 计算
|
||||
|
||||
`data_i` 作为电流源表,与 `data_v` 保持同源:
|
||||
|
||||
- 先生成 `RMS`
|
||||
- 再生成 `I_POS`、`I_NEG`、`I_ZERO`、`I_UNBALANCE`
|
||||
- 再生成 `I_1 ~ I_50`
|
||||
- `I_THD` 由 `I_2 ~ I_50` 与 `I_1` 计算
|
||||
|
||||
对这两张源表:
|
||||
|
||||
- `RMS_MAX`、`RMS_MIN`、`RMS_CP95`
|
||||
- `FREQ_MAX`、`FREQ_MIN`、`FREQ_CP95`
|
||||
- `V_THD_MAX`、`V_THD_MIN`、`V_THD_CP95`
|
||||
- `I_THD_MAX`、`I_THD_MIN`、`I_THD_CP95`
|
||||
|
||||
均必须由主值派生,不单独随机。
|
||||
|
||||
#### 11.4.2 谐波幅值表
|
||||
|
||||
`data_harmphasic_v`:
|
||||
|
||||
- 直接复用 `data_v` 中的 `V_1 ~ V_50`
|
||||
- 各次谐波的 `MAX`、`MIN`、`CP95` 与 `data_v` 同源派生
|
||||
|
||||
`data_harmphasic_i`:
|
||||
|
||||
- 直接复用 `data_i` 中的 `I_1 ~ I_50`
|
||||
- 各次谐波的 `MAX`、`MIN`、`CP95` 与 `data_i` 同源派生
|
||||
|
||||
#### 11.4.3 谐波占比表
|
||||
|
||||
`data_harmrate_v`:
|
||||
|
||||
- 从 `data_harmphasic_v` 派生
|
||||
- 建议按 `V_n / V_1 * 100` 计算 `V_1 ~ V_50` 对应占比值
|
||||
- `MAX`、`MIN`、`CP95` 也按同样比例换算
|
||||
|
||||
`data_harmrate_i`:
|
||||
|
||||
- 从 `data_harmphasic_i` 派生
|
||||
- 建议按 `I_n / I_1 * 100` 计算 `I_1 ~ I_50` 对应占比值
|
||||
- `MAX`、`MIN`、`CP95` 也按同样比例换算
|
||||
|
||||
#### 11.4.4 间谐波电流表
|
||||
|
||||
`data_inharm_i`:
|
||||
|
||||
- 作为电流间谐波表生成
|
||||
- 各阶值应明显小于对应整数次谐波值
|
||||
- 建议按“整数次谐波值乘以小比例系数”生成
|
||||
- `MAX`、`MIN`、`CP95` 继续由主值派生
|
||||
|
||||
#### 11.4.5 谐波功率表
|
||||
|
||||
`data_harmpower_p`:
|
||||
|
||||
- 从电压谐波和电流谐波联合派生
|
||||
- `P_1 ~ P_50` 建议按电压谐波、电流谐波和相位关系计算
|
||||
- `P` 为各次分量汇总
|
||||
- `PF`、`DF` 由功率关系派生
|
||||
|
||||
`data_harmpower_q`:
|
||||
|
||||
- 与 `data_harmpower_p` 同源
|
||||
- `Q_1 ~ Q_50` 按无功功率关系派生
|
||||
- `Q` 为各次分量汇总
|
||||
|
||||
`data_harmpower_s`:
|
||||
|
||||
- 与 `data_harmpower_p`、`data_harmpower_q` 同源
|
||||
- `S_1 ~ S_50` 建议由 `P_n` 与 `Q_n` 派生
|
||||
- `S` 为各次分量汇总
|
||||
|
||||
这三张表中的 `MAX`、`MIN`、`CP95` 也必须基于对应主值派生。
|
||||
|
||||
#### 11.4.6 闪变与波动表
|
||||
|
||||
`data_flicker`:
|
||||
|
||||
- 固定 `10` 分钟时间轴
|
||||
- 生成 `FLUC`、`PST`、`PLT`
|
||||
- `FLUC` 作为波动主值
|
||||
- `PST` 从 `FLUC` 派生
|
||||
- `PLT` 作为长时闪变指标生成,但落库节奏仍为 `10` 分钟
|
||||
|
||||
`data_fluc`:
|
||||
|
||||
- 固定 `10` 分钟时间轴
|
||||
- 生成 `FLUC`、`FLUCCF`
|
||||
- 与 `data_flicker` 同源
|
||||
- `FLUCCF` 作为 `FLUC` 的修正或归一化结果生成
|
||||
|
||||
`data_plt`:
|
||||
|
||||
- 固定 `2` 小时时间轴
|
||||
- 只写 `PLT`
|
||||
- `PLT` 来源应与 `data_flicker` 中的 `PLT` 同源
|
||||
- 但只在 `2` 小时槽位落表
|
||||
|
||||
### 11.5 前端展示字段映射
|
||||
|
||||
由于表结构中没有独立 `AVG` 字段,前端“平均值”首版建议直接取各参数主值字段。
|
||||
|
||||
例如:
|
||||
|
||||
- 电压类参数取 `RMS`、`V_THD`、`FREQ`
|
||||
- 电流类参数取 `RMS`、`I_THD`
|
||||
- 闪变类参数取 `FLUC`、`PST`、`PLT`
|
||||
- 功率类参数取 `P`、`Q`、`S`
|
||||
|
||||
前端展示中的:
|
||||
|
||||
- `最大值` -> 对应 `*_MAX`
|
||||
- `最小值` -> 对应 `*_MIN`
|
||||
- `95%概率大值` -> 对应 `*_CP95`
|
||||
- `平均值` -> 对应主值字段
|
||||
|
||||
### 11.6 质量标识
|
||||
|
||||
`QUALITYFLAG` 首版建议统一使用固定有效值,例如 `1`。
|
||||
|
||||
前端“是否合格”不直接读取数据库 `QUALITYFLAG` 的原始含义,而是走模板展示规则,避免把数据生成逻辑和展示文案硬耦合。
|
||||
|
||||
## 12. 前端页面交互方案
|
||||
|
||||
### 12.1 页面结构
|
||||
|
||||
建议页面分为两个区域:
|
||||
|
||||
1. 补数任务区
|
||||
2. 参数规则展示区
|
||||
|
||||
### 12.2 补数任务区
|
||||
|
||||
表单项建议如下:
|
||||
|
||||
- 监测点选择模式:`单点` / `多点`
|
||||
- 监测点 ID 选择框
|
||||
- 开始时间
|
||||
- 结束时间
|
||||
- 时间戳步长:`1`、`3`、`5`、`10` 分钟
|
||||
- 预计写入量按钮
|
||||
- 开始补数按钮
|
||||
|
||||
交互说明:
|
||||
|
||||
- 默认单监测点
|
||||
- 多监测点时切换为多选
|
||||
- 时间戳步长只影响 `10` 张基础实时类表
|
||||
- `data_flicker`、`data_fluc` 固定按 `10` 分钟槽位写入
|
||||
- `data_plt` 固定按 `2` 小时槽位写入
|
||||
- 前端先调用预估接口,展示预计写入规模
|
||||
- 用户确认后再调用创建任务接口
|
||||
|
||||
### 12.3 任务状态展示
|
||||
|
||||
前端创建任务后轮询状态接口,建议展示:
|
||||
|
||||
- 当前状态
|
||||
- 当前表名
|
||||
- 已写入数量
|
||||
- 已跳过数量
|
||||
- 失败数量
|
||||
- 失败原因
|
||||
- 开始时间
|
||||
- 结束时间
|
||||
|
||||
### 12.4 参数规则展示区
|
||||
|
||||
参数规则表按用户提供图片组织,至少展示以下列:
|
||||
|
||||
- `电能质量参数名称`
|
||||
- `相别`
|
||||
- `显示`
|
||||
- `最大值`
|
||||
- `最小值`
|
||||
- `平均值`
|
||||
- `95%概率大值`
|
||||
- `是否合格`
|
||||
- `数据展示(保留小数)`
|
||||
|
||||
其中:
|
||||
|
||||
- 相别列使用 `phaseDisplay`
|
||||
- 所有数值默认保留 `2` 位小数
|
||||
- 该表展示的是模板规则,不是实时数据库统计结果
|
||||
|
||||
## 13. 文件结构建议
|
||||
|
||||
建议在 `tools/add-data` 下按现有仓库风格扩展:
|
||||
|
||||
```text
|
||||
tools/add-data/src/main/java/com/njcn/gather/tool/adddata/
|
||||
├── controller
|
||||
│ ├── AddDataTaskController.java
|
||||
│ └── AddDataTemplateController.java
|
||||
├── service
|
||||
│ ├── AddDataTaskService.java
|
||||
│ ├── AddDataTemplateService.java
|
||||
│ └── impl
|
||||
│ ├── AddDataTaskServiceImpl.java
|
||||
│ └── AddDataTemplateServiceImpl.java
|
||||
├── component
|
||||
│ ├── AddDataTaskExecutor.java
|
||||
│ ├── AddDataBatchWriter.java
|
||||
│ ├── AddDataValueGenerator.java
|
||||
│ ├── AddDataTaskStatusHolder.java
|
||||
│ └── AddDataTemplateRegistry.java
|
||||
└── pojo
|
||||
├── param
|
||||
└── vo
|
||||
```
|
||||
|
||||
如参数模板需落成资源文件,建议放在:
|
||||
|
||||
- `tools/add-data/src/main/resources/template/add-data-parameter-template.json`
|
||||
|
||||
## 14. 非目标
|
||||
|
||||
本次不包含以下内容:
|
||||
|
||||
- 不新增监测点基础信息管理能力
|
||||
- 不新增监测点字典表
|
||||
- 不新增数据库迁移框架
|
||||
- 不在 `system` 或 `user` 模块中扩展通用任务中心
|
||||
- 不实现真实前端页面代码
|
||||
- 不做覆盖更新或先删后写逻辑
|
||||
|
||||
## 15. 风险与取舍
|
||||
|
||||
### 15.1 内存态任务状态的风险
|
||||
|
||||
首版如果使用内存保存任务状态:
|
||||
|
||||
- 服务重启后任务状态会丢失
|
||||
- 适合当前工具型场景
|
||||
- 不适合后续演进为长期审计能力
|
||||
|
||||
当前取舍:
|
||||
|
||||
- 首版接受该限制
|
||||
- 如果后续需要审计与追踪,再补任务表
|
||||
|
||||
### 15.2 模板与真实表字段映射的风险
|
||||
|
||||
用户图片中的参数项比“13 张表名”更偏业务视角,实际落库需要一层参数模板到表结构的映射。
|
||||
|
||||
当前取舍:
|
||||
|
||||
- 首版把模板规则集中维护在 `AddDataTemplateRegistry`
|
||||
- 不在前端直接拼接数据库字段名
|
||||
|
||||
### 15.3 大批量写入的数据库压力
|
||||
|
||||
多监测点、长区间、1 分钟步长时数据库压力会很大。
|
||||
|
||||
当前取舍:
|
||||
|
||||
- 采用预估接口提前提示规模
|
||||
- 控制批次大小
|
||||
- 使用异步执行与 `INSERT IGNORE`
|
||||
|
||||
## 16. 验证方式
|
||||
|
||||
本次设计和后续实现默认不执行 `mvn`,验证方式以静态检查和链路闭合检查为主。
|
||||
|
||||
### 16.1 设计验证
|
||||
|
||||
确认以下设计点闭合:
|
||||
|
||||
- 13 张表范围明确
|
||||
- 相别展示与落库规则明确
|
||||
- 重复数据处理策略明确
|
||||
- 前后端接口契约明确
|
||||
- 批量写入策略明确
|
||||
|
||||
### 16.2 实现后验证
|
||||
|
||||
后续实现时重点检查:
|
||||
|
||||
- 入参校验是否覆盖时间区间、步长、监测点 ID 列表
|
||||
- 预估条数计算是否正确
|
||||
- SQL 是否统一使用 `INSERT IGNORE`
|
||||
- 批次是否按宽表和窄表区分
|
||||
- 重复补数时是否正确累加 `skippedCount`
|
||||
- 参数模板返回是否与图片字段一致
|
||||
|
||||
## 17. 结论
|
||||
|
||||
本次采用“异步任务 + JDBC 批量写入 + 参数模板展示接口”的实现方案,在 `tools/add-data` 模块内完成电能质量 13 张表的批量补数能力。
|
||||
|
||||
该方案兼顾以下目标:
|
||||
|
||||
- 支持单监测点和多监测点
|
||||
- 支持日级、月级区间
|
||||
- 支持 1/3/5/10 分钟时间戳步长
|
||||
- 支持重复补数时跳过已存在数据
|
||||
- 支持前端按图片规则展示参数和相别
|
||||
- 在当前仓库约束下把改动范围控制在最小必要集合
|
||||
@@ -48,6 +48,11 @@
|
||||
<artifactId>mms-mapping</artifactId>
|
||||
<version>1.0.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.njcn.gather</groupId>
|
||||
<artifactId>add-data</artifactId>
|
||||
<version>1.0.0</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
|
||||
@@ -7,22 +7,36 @@
|
||||
当前真实保留的子模块有:
|
||||
|
||||
- `activate-tool`
|
||||
- `add-data`
|
||||
- `mms-mapping`
|
||||
- `wave-tool`
|
||||
|
||||
因此,`tools` 现阶段仍然是聚合模块,但当前已实际承载激活工具、ICD/MMS 映射工具和波形查看工具三个子模块。
|
||||
因此,`tools` 现阶段仍然是聚合模块,但当前已实际承载激活工具、电能质量数据补录工具、ICD/MMS 映射工具和波形查看工具四个子模块。
|
||||
|
||||
## 当前结构
|
||||
|
||||
```text
|
||||
tools/
|
||||
├── activate-tool/
|
||||
├── add-data/
|
||||
├── mms-mapping/
|
||||
└── wave-tool/
|
||||
```
|
||||
|
||||
其中 `tools/mms-mapping` 当前 Maven `artifactId` 为 `mms-mapping`。
|
||||
|
||||
## add-data 的职责
|
||||
|
||||
`add-data` 当前已提供电能质量数据批量补录能力,包含:
|
||||
|
||||
- 任务预估接口
|
||||
- 任务创建接口
|
||||
- 任务状态查询接口
|
||||
- 模板规则查询接口
|
||||
- 异步执行、批量写入和内存态任务状态管理
|
||||
|
||||
模块内部已按职责拆分 `controller`、`service`、`service/impl`、`component`、`pojo`、`config` 和 `util`,并通过 `JdbcTemplate + INSERT IGNORE` 执行批量补数。
|
||||
|
||||
## activate-tool 的职责
|
||||
|
||||
`activate-tool` 当前提供的能力主要围绕设备授权与许可证:
|
||||
@@ -76,7 +90,7 @@ tools/
|
||||
|
||||
## 依赖关系
|
||||
|
||||
`tools/activate-tool`、`tools/mms-mapping` 与 `tools/wave-tool` 当前主要依赖:
|
||||
`tools/activate-tool`、`tools/add-data`、`tools/mms-mapping` 与 `tools/wave-tool` 当前主要依赖:
|
||||
|
||||
- `com.njcn:njcn-common`
|
||||
- `com.njcn:spingboot2.3.12`
|
||||
|
||||
55
tools/add-data/README.md
Normal file
55
tools/add-data/README.md
Normal file
@@ -0,0 +1,55 @@
|
||||
# add-data 模块说明
|
||||
|
||||
## 模块定位
|
||||
|
||||
`add-data` 当前提供电能质量 13 张表批量补数能力,支持补数规模预估、后台异步执行、任务状态查询和前端模板规则查询。
|
||||
|
||||
## 当前范围
|
||||
|
||||
当前模块本次实现范围聚焦在工具型批处理,不接入以下内容:
|
||||
|
||||
- 覆盖更新或先删后写
|
||||
- 监测点基础信息管理
|
||||
- 独立任务持久化表
|
||||
- 前端页面代码
|
||||
|
||||
目录中保留历史 SQL 脚本 `DATA_FLICKER.sql`,并同步复制到 `src/main/resources/sql/add-data` 供运行时解析表字段元数据。
|
||||
|
||||
## 当前结构
|
||||
|
||||
```text
|
||||
add-data/
|
||||
├── pom.xml
|
||||
├── README.md
|
||||
├── DATA_FLICKER.sql
|
||||
└── src/main/java/com/njcn/gather/tool/adddata/
|
||||
├── component/
|
||||
├── config/
|
||||
├── controller/
|
||||
├── pojo/
|
||||
├── service/
|
||||
└── util/
|
||||
```
|
||||
|
||||
## 基础骨架说明
|
||||
|
||||
- `controller/AddDataTaskController`
|
||||
- 提供预估、创建任务、查询任务状态三个接口
|
||||
- `controller/AddDataTemplateController`
|
||||
- 提供前端参数模板规则查询接口
|
||||
- `component/AddDataTaskExecutor`
|
||||
- 负责后台异步补数任务执行
|
||||
- `component/AddDataBatchWriter`
|
||||
- 负责 `INSERT IGNORE` 批量写入与失败降级
|
||||
- `component/AddDataValueGenerator`
|
||||
- 负责按同源规则生成 13 张表数据
|
||||
- `component/AddDataTableRegistry`
|
||||
- 负责从 SQL 资源解析字段元数据并注册表定义
|
||||
- `component/AddDataTaskStatusHolder`
|
||||
- 首版以内存方式保存任务状态
|
||||
|
||||
## 扩展约束
|
||||
|
||||
当前实现按 `A/B/C/T` 四类数据类型生成和预估补数。
|
||||
|
||||
后续如果补齐逐表真实相别映射、任务持久化或更细粒度模板规则,应优先沿现有职责边界扩展,不回退为单一大类承载全部逻辑。
|
||||
41
tools/add-data/pom.xml
Normal file
41
tools/add-data/pom.xml
Normal file
@@ -0,0 +1,41 @@
|
||||
<?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>tools</artifactId>
|
||||
<version>1.0.0</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>add-data</artifactId>
|
||||
|
||||
<properties>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.njcn</groupId>
|
||||
<artifactId>njcn-common</artifactId>
|
||||
<version>0.0.1</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.njcn</groupId>
|
||||
<artifactId>spingboot2.3.12</artifactId>
|
||||
<version>2.3.12</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework</groupId>
|
||||
<artifactId>spring-jdbc</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
@@ -0,0 +1,135 @@
|
||||
package com.njcn.gather.tool.adddata.component;
|
||||
|
||||
import com.njcn.gather.tool.adddata.pojo.bo.AddDataBatchWriteResult;
|
||||
import com.njcn.gather.tool.adddata.pojo.bo.AddDataTableDefinition;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.dao.DataAccessException;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 批量写入组件。
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class AddDataBatchWriter {
|
||||
|
||||
/** JDBC 模板。 */
|
||||
private final JdbcTemplate jdbcTemplate;
|
||||
|
||||
/**
|
||||
* 写入一个批次。
|
||||
*
|
||||
* @param definition 表定义
|
||||
* @param rows 行数据
|
||||
* @return 写入结果
|
||||
*/
|
||||
public AddDataBatchWriteResult writeBatch(AddDataTableDefinition definition, List<List<Object>> rows) {
|
||||
if (rows == null || rows.isEmpty()) {
|
||||
return new AddDataBatchWriteResult(0L, 0L, 0L, null);
|
||||
}
|
||||
try {
|
||||
return executeInsertIgnore(definition, rows);
|
||||
} catch (DataAccessException ex) {
|
||||
log.warn("批量写入失败,开始降级为逐行写入,table={}, batchSize={}, message={}",
|
||||
definition.getTableName(), rows.size(), resolveErrorMessage(ex));
|
||||
return fallbackWriteOneByOne(definition, rows, ex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行批量 INSERT IGNORE。
|
||||
*
|
||||
* @param definition 表定义
|
||||
* @param rows 行数据
|
||||
* @return 写入结果
|
||||
*/
|
||||
private AddDataBatchWriteResult executeInsertIgnore(AddDataTableDefinition definition, List<List<Object>> rows) {
|
||||
String sql = buildInsertIgnoreSql(definition, rows.size());
|
||||
List<Object> args = flattenRows(rows);
|
||||
int affectedRows = jdbcTemplate.update(sql, args.toArray(new Object[0]));
|
||||
long insertedCount = Math.max(affectedRows, 0);
|
||||
long skippedCount = rows.size() - insertedCount;
|
||||
return new AddDataBatchWriteResult(insertedCount, skippedCount, 0L, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量写入失败后逐行降级。
|
||||
*
|
||||
* @param definition 表定义
|
||||
* @param rows 行数据
|
||||
* @param batchException 原始批量异常
|
||||
* @return 写入结果
|
||||
*/
|
||||
private AddDataBatchWriteResult fallbackWriteOneByOne(AddDataTableDefinition definition, List<List<Object>> rows, DataAccessException batchException) {
|
||||
long insertedCount = 0L;
|
||||
long skippedCount = 0L;
|
||||
long failedCount = 0L;
|
||||
String firstFailureMessage = null;
|
||||
String sql = buildInsertIgnoreSql(definition, 1);
|
||||
for (List<Object> row : rows) {
|
||||
try {
|
||||
int affectedRows = jdbcTemplate.update(sql, row.toArray(new Object[0]));
|
||||
if (affectedRows > 0) {
|
||||
insertedCount += affectedRows;
|
||||
} else {
|
||||
skippedCount++;
|
||||
}
|
||||
} catch (DataAccessException ex) {
|
||||
failedCount++;
|
||||
if (firstFailureMessage == null) {
|
||||
firstFailureMessage = resolveErrorMessage(ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (firstFailureMessage == null) {
|
||||
firstFailureMessage = resolveErrorMessage(batchException);
|
||||
}
|
||||
return new AddDataBatchWriteResult(insertedCount, skippedCount, failedCount, firstFailureMessage);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建 INSERT IGNORE SQL。
|
||||
*
|
||||
* @param definition 表定义
|
||||
* @param rowCount 行数
|
||||
* @return SQL 文本
|
||||
*/
|
||||
private String buildInsertIgnoreSql(AddDataTableDefinition definition, int rowCount) {
|
||||
String columnSegment = "`" + String.join("`,`", definition.getColumns()) + "`";
|
||||
String placeholderSegment = "(" + String.join(",", Collections.nCopies(definition.getColumns().size(), "?")) + ")";
|
||||
String valuesSegment = String.join(",", Collections.nCopies(rowCount, placeholderSegment));
|
||||
return "INSERT IGNORE INTO `" + definition.getTableName() + "` (" + columnSegment + ") VALUES " + valuesSegment;
|
||||
}
|
||||
|
||||
/**
|
||||
* 展平批量参数。
|
||||
*
|
||||
* @param rows 行数据
|
||||
* @return 扁平参数列表
|
||||
*/
|
||||
private List<Object> flattenRows(List<List<Object>> rows) {
|
||||
List<Object> args = new ArrayList<Object>();
|
||||
for (List<Object> row : rows) {
|
||||
args.addAll(row);
|
||||
}
|
||||
return args;
|
||||
}
|
||||
|
||||
/**
|
||||
* 提取错误信息。
|
||||
*
|
||||
* @param ex 异常
|
||||
* @return 错误信息
|
||||
*/
|
||||
private String resolveErrorMessage(DataAccessException ex) {
|
||||
Throwable root = ex.getMostSpecificCause();
|
||||
return root == null ? ex.getMessage() : root.getMessage();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
package com.njcn.gather.tool.adddata.component;
|
||||
|
||||
import com.njcn.gather.tool.adddata.pojo.bo.AddDataTableDefinition;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.InitializingBean;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.StreamUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* 数据补录表定义注册器。
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class AddDataTableRegistry implements InitializingBean {
|
||||
|
||||
/** 元数据 SQL 资源。 */
|
||||
private static final String SCHEMA_RESOURCE_PATH = "sql/add-data/DATA_FLICKER.sql";
|
||||
|
||||
/** 建表语句解析表达式。 */
|
||||
private static final Pattern TABLE_PATTERN = Pattern.compile("CREATE TABLE\\s+`([^`]+)`\\s*\\((.*?)\\)\\s*ENGINE",
|
||||
Pattern.CASE_INSENSITIVE | Pattern.DOTALL);
|
||||
|
||||
/** 列定义解析表达式。 */
|
||||
private static final Pattern COLUMN_PATTERN = Pattern.compile("^\\s*`([^`]+)`", Pattern.MULTILINE);
|
||||
|
||||
/** 首版统一按 A/B/C/T 四类相别落库。 */
|
||||
private static final List<String> WRITE_PHASE_CODES = Collections.unmodifiableList(Arrays.asList("A", "B", "C", "T"));
|
||||
|
||||
/** 宽表批次大小。 */
|
||||
private static final int WIDE_TABLE_BATCH_SIZE = 100;
|
||||
|
||||
/** 窄表批次大小。 */
|
||||
private static final int NARROW_TABLE_BATCH_SIZE = 500;
|
||||
|
||||
/** 表定义列表。 */
|
||||
private List<AddDataTableDefinition> tableDefinitions = Collections.emptyList();
|
||||
|
||||
@Override
|
||||
public void afterPropertiesSet() throws Exception {
|
||||
Map<String, List<String>> columnMap = parseSchemaColumns();
|
||||
List<AddDataTableDefinition> definitions = new ArrayList<AddDataTableDefinition>();
|
||||
definitions.add(buildDefinition(columnMap, "data_flicker", AddDataTableDefinition.TimeAxisType.FIXED_TEN_MINUTES, NARROW_TABLE_BATCH_SIZE));
|
||||
definitions.add(buildDefinition(columnMap, "data_fluc", AddDataTableDefinition.TimeAxisType.FIXED_TEN_MINUTES, NARROW_TABLE_BATCH_SIZE));
|
||||
definitions.add(buildDefinition(columnMap, "data_harmphasic_i", AddDataTableDefinition.TimeAxisType.REQUEST_INTERVAL, WIDE_TABLE_BATCH_SIZE));
|
||||
definitions.add(buildDefinition(columnMap, "data_harmphasic_v", AddDataTableDefinition.TimeAxisType.REQUEST_INTERVAL, WIDE_TABLE_BATCH_SIZE));
|
||||
definitions.add(buildDefinition(columnMap, "data_harmpower_p", AddDataTableDefinition.TimeAxisType.REQUEST_INTERVAL, WIDE_TABLE_BATCH_SIZE));
|
||||
definitions.add(buildDefinition(columnMap, "data_harmpower_q", AddDataTableDefinition.TimeAxisType.REQUEST_INTERVAL, WIDE_TABLE_BATCH_SIZE));
|
||||
definitions.add(buildDefinition(columnMap, "data_harmpower_s", AddDataTableDefinition.TimeAxisType.REQUEST_INTERVAL, WIDE_TABLE_BATCH_SIZE));
|
||||
definitions.add(buildDefinition(columnMap, "data_harmrate_i", AddDataTableDefinition.TimeAxisType.REQUEST_INTERVAL, WIDE_TABLE_BATCH_SIZE));
|
||||
definitions.add(buildDefinition(columnMap, "data_harmrate_v", AddDataTableDefinition.TimeAxisType.REQUEST_INTERVAL, WIDE_TABLE_BATCH_SIZE));
|
||||
definitions.add(buildDefinition(columnMap, "data_i", AddDataTableDefinition.TimeAxisType.REQUEST_INTERVAL, WIDE_TABLE_BATCH_SIZE));
|
||||
definitions.add(buildDefinition(columnMap, "data_inharm_i", AddDataTableDefinition.TimeAxisType.REQUEST_INTERVAL, WIDE_TABLE_BATCH_SIZE));
|
||||
definitions.add(buildDefinition(columnMap, "data_plt", AddDataTableDefinition.TimeAxisType.FIXED_TWO_HOURS, NARROW_TABLE_BATCH_SIZE));
|
||||
definitions.add(buildDefinition(columnMap, "data_v", AddDataTableDefinition.TimeAxisType.REQUEST_INTERVAL, WIDE_TABLE_BATCH_SIZE));
|
||||
tableDefinitions = Collections.unmodifiableList(definitions);
|
||||
log.info("加载 add-data 表定义完成,tableCount={}", tableDefinitions.size());
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回全部表定义。
|
||||
*
|
||||
* @return 表定义列表
|
||||
*/
|
||||
public List<AddDataTableDefinition> getTableDefinitions() {
|
||||
return tableDefinitions;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据表名查找表定义。
|
||||
*
|
||||
* @param tableName 表名
|
||||
* @return 表定义
|
||||
*/
|
||||
public AddDataTableDefinition getDefinition(String tableName) {
|
||||
for (AddDataTableDefinition definition : tableDefinitions) {
|
||||
if (definition.getTableName().equals(tableName)) {
|
||||
return definition;
|
||||
}
|
||||
}
|
||||
throw new IllegalArgumentException("未找到 add-data 表定义:" + tableName);
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析 schema SQL 中的列定义。
|
||||
*
|
||||
* @return 表名与字段列表映射
|
||||
*/
|
||||
private Map<String, List<String>> parseSchemaColumns() {
|
||||
String schemaText = loadSchemaText();
|
||||
Matcher tableMatcher = TABLE_PATTERN.matcher(schemaText);
|
||||
Map<String, List<String>> columnMap = new LinkedHashMap<String, List<String>>();
|
||||
while (tableMatcher.find()) {
|
||||
String tableName = tableMatcher.group(1);
|
||||
String tableBody = tableMatcher.group(2);
|
||||
Matcher columnMatcher = COLUMN_PATTERN.matcher(tableBody);
|
||||
List<String> columns = new ArrayList<String>();
|
||||
while (columnMatcher.find()) {
|
||||
columns.add(columnMatcher.group(1));
|
||||
}
|
||||
columnMap.put(tableName, columns);
|
||||
}
|
||||
return columnMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建单表定义。
|
||||
*
|
||||
* @param columnMap 字段映射
|
||||
* @param tableName 表名
|
||||
* @param timeAxisType 时间轴类型
|
||||
* @param batchSize 批次大小
|
||||
* @return 表定义
|
||||
*/
|
||||
private AddDataTableDefinition buildDefinition(Map<String, List<String>> columnMap, String tableName,
|
||||
AddDataTableDefinition.TimeAxisType timeAxisType, int batchSize) {
|
||||
List<String> columns = columnMap.get(tableName);
|
||||
if (columns == null || columns.isEmpty()) {
|
||||
throw new IllegalStateException("未从 SQL 元数据中解析到表字段:" + tableName);
|
||||
}
|
||||
/*
|
||||
* 当前按用户最新确认,13 张表统一支持 A/B/C/T 四类数据类型。
|
||||
* 如果后续补齐逐表更细的相别映射,只需要在这里调整落库相别集合即可。
|
||||
*/
|
||||
return new AddDataTableDefinition(tableName, columns, WRITE_PHASE_CODES, batchSize, timeAxisType);
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载 schema SQL 文本。
|
||||
*
|
||||
* @return SQL 文本
|
||||
*/
|
||||
private String loadSchemaText() {
|
||||
ClassPathResource resource = new ClassPathResource(SCHEMA_RESOURCE_PATH);
|
||||
try {
|
||||
return StreamUtils.copyToString(resource.getInputStream(), StandardCharsets.UTF_8);
|
||||
} catch (IOException e) {
|
||||
throw new IllegalStateException("读取 add-data SQL 元数据失败:" + SCHEMA_RESOURCE_PATH, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,310 @@
|
||||
package com.njcn.gather.tool.adddata.component;
|
||||
|
||||
import com.njcn.gather.tool.adddata.pojo.bo.AddDataBatchWriteResult;
|
||||
import com.njcn.gather.tool.adddata.pojo.bo.AddDataTableDefinition;
|
||||
import com.njcn.gather.tool.adddata.pojo.bo.AddDataTaskCommand;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.TreeSet;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
|
||||
/**
|
||||
* 异步任务执行器。
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class AddDataTaskExecutor {
|
||||
|
||||
/** 多个时间点组成一个处理窗口,兼顾批量效率和内存占用。 */
|
||||
private static final int TIME_WINDOW_SIZE = 30;
|
||||
|
||||
/** 任务线程池。 */
|
||||
@Qualifier("addDataTaskExecutorService")
|
||||
private final ExecutorService addDataTaskExecutorService;
|
||||
|
||||
/** 表定义注册器。 */
|
||||
private final AddDataTableRegistry addDataTableRegistry;
|
||||
|
||||
/** 时间槽计算器。 */
|
||||
private final AddDataTimeSlotCalculator addDataTimeSlotCalculator;
|
||||
|
||||
/** 数据生成器。 */
|
||||
private final AddDataValueGenerator addDataValueGenerator;
|
||||
|
||||
/** 批量写入组件。 */
|
||||
private final AddDataBatchWriter addDataBatchWriter;
|
||||
|
||||
/** 状态持有器。 */
|
||||
private final AddDataTaskStatusHolder addDataTaskStatusHolder;
|
||||
|
||||
public AddDataTaskExecutor(@Qualifier("addDataTaskExecutorService") ExecutorService addDataTaskExecutorService,
|
||||
AddDataTableRegistry addDataTableRegistry,
|
||||
AddDataTimeSlotCalculator addDataTimeSlotCalculator,
|
||||
AddDataValueGenerator addDataValueGenerator,
|
||||
AddDataBatchWriter addDataBatchWriter,
|
||||
AddDataTaskStatusHolder addDataTaskStatusHolder) {
|
||||
this.addDataTaskExecutorService = addDataTaskExecutorService;
|
||||
this.addDataTableRegistry = addDataTableRegistry;
|
||||
this.addDataTimeSlotCalculator = addDataTimeSlotCalculator;
|
||||
this.addDataValueGenerator = addDataValueGenerator;
|
||||
this.addDataBatchWriter = addDataBatchWriter;
|
||||
this.addDataTaskStatusHolder = addDataTaskStatusHolder;
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交后台任务。
|
||||
*
|
||||
* @param taskId 任务编号
|
||||
* @param command 任务命令
|
||||
*/
|
||||
public void submit(String taskId, AddDataTaskCommand command) {
|
||||
addDataTaskExecutorService.submit(() -> execute(taskId, command));
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行补数任务。
|
||||
*
|
||||
* @param taskId 任务编号
|
||||
* @param command 任务命令
|
||||
*/
|
||||
private void execute(String taskId, AddDataTaskCommand command) {
|
||||
try {
|
||||
addDataTaskStatusHolder.markRunning(taskId);
|
||||
Map<String, List<LocalDateTime>> timeSlotsByTable = buildTimeSlotsByTable(command);
|
||||
Map<String, Set<LocalDateTime>> timeSlotLookupByTable = buildTimeSlotLookupByTable(timeSlotsByTable);
|
||||
List<LocalDateTime> mergedTimeSlots = mergeTimeSlots(timeSlotsByTable);
|
||||
Map<String, Integer> batchNoMap = new HashMap<String, Integer>();
|
||||
Map<String, List<List<Object>>> pendingRowsByTable = buildPendingRowsByTable();
|
||||
for (int startIndex = 0; startIndex < mergedTimeSlots.size(); startIndex += TIME_WINDOW_SIZE) {
|
||||
int endIndex = Math.min(startIndex + TIME_WINDOW_SIZE, mergedTimeSlots.size());
|
||||
List<LocalDateTime> timeWindow = mergedTimeSlots.subList(startIndex, endIndex);
|
||||
GeneratedBatchData generatedBatchData = generateBatchData(command, timeWindow, timeSlotLookupByTable);
|
||||
if (!writeBatchData(taskId, generatedBatchData, pendingRowsByTable, batchNoMap)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (!flushRemainingBatchData(taskId, pendingRowsByTable, batchNoMap)) {
|
||||
return;
|
||||
}
|
||||
addDataTaskStatusHolder.markSuccess(taskId);
|
||||
} catch (Exception ex) {
|
||||
log.error("执行补数任务失败,taskId={}", taskId, ex);
|
||||
addDataTaskStatusHolder.markFailed(taskId, ex.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 为每张表构建各自命中的时间槽。
|
||||
*
|
||||
* @param command 任务命令
|
||||
* @return 按表名分组的时间槽
|
||||
*/
|
||||
private Map<String, List<LocalDateTime>> buildTimeSlotsByTable(AddDataTaskCommand command) {
|
||||
Map<String, List<LocalDateTime>> result = new LinkedHashMap<String, List<LocalDateTime>>();
|
||||
for (AddDataTableDefinition definition : addDataTableRegistry.getTableDefinitions()) {
|
||||
int intervalMinutes = definition.resolveIntervalMinutes(command.getIntervalMinutes());
|
||||
List<LocalDateTime> timeSlots = addDataTimeSlotCalculator.buildTimeSlots(
|
||||
command.getStartTime(), command.getEndTime(), intervalMinutes);
|
||||
result.put(definition.getTableName(), timeSlots);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 为每张表补充时间槽查找索引,避免窗口生成时重复线性扫描。
|
||||
*
|
||||
* @param timeSlotsByTable 按表名分组的时间槽
|
||||
* @return 按表名分组的时间槽查找集合
|
||||
*/
|
||||
private Map<String, Set<LocalDateTime>> buildTimeSlotLookupByTable(Map<String, List<LocalDateTime>> timeSlotsByTable) {
|
||||
Map<String, Set<LocalDateTime>> result = new LinkedHashMap<String, Set<LocalDateTime>>();
|
||||
for (Map.Entry<String, List<LocalDateTime>> entry : timeSlotsByTable.entrySet()) {
|
||||
result.put(entry.getKey(), new HashSet<LocalDateTime>(entry.getValue()));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 为每张表初始化跨窗口复用的待写入缓存。
|
||||
*
|
||||
* @return 按表名分组的待写入缓存
|
||||
*/
|
||||
private Map<String, List<List<Object>>> buildPendingRowsByTable() {
|
||||
Map<String, List<List<Object>>> result = new LinkedHashMap<String, List<List<Object>>>();
|
||||
for (AddDataTableDefinition definition : addDataTableRegistry.getTableDefinitions()) {
|
||||
result.put(definition.getTableName(), new ArrayList<List<Object>>());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 合并所有表的时间槽,得到统一的自然时间轴。
|
||||
*
|
||||
* @param timeSlotsByTable 按表名分组的时间槽
|
||||
* @return 去重后的统一时间轴
|
||||
*/
|
||||
private List<LocalDateTime> mergeTimeSlots(Map<String, List<LocalDateTime>> timeSlotsByTable) {
|
||||
TreeSet<LocalDateTime> merged = new TreeSet<LocalDateTime>();
|
||||
for (List<LocalDateTime> timeSlots : timeSlotsByTable.values()) {
|
||||
merged.addAll(timeSlots);
|
||||
}
|
||||
return new ArrayList<LocalDateTime>(merged);
|
||||
}
|
||||
|
||||
/**
|
||||
* 按多个时间点窗口生成待写入数据,不直接写库。
|
||||
*
|
||||
* @param command 任务命令
|
||||
* @param timeWindow 当前时间窗口
|
||||
* @param timeSlotLookupByTable 各表命中的时间槽索引
|
||||
* @return 当前窗口生成结果
|
||||
*/
|
||||
private GeneratedBatchData generateBatchData(AddDataTaskCommand command, List<LocalDateTime> timeWindow,
|
||||
Map<String, Set<LocalDateTime>> timeSlotLookupByTable) {
|
||||
Map<String, List<List<Object>>> rowsByTable = new LinkedHashMap<String, List<List<Object>>>();
|
||||
for (AddDataTableDefinition definition : addDataTableRegistry.getTableDefinitions()) {
|
||||
rowsByTable.put(definition.getTableName(), new ArrayList<List<Object>>());
|
||||
}
|
||||
for (LocalDateTime timeSlot : timeWindow) {
|
||||
for (AddDataTableDefinition definition : addDataTableRegistry.getTableDefinitions()) {
|
||||
Set<LocalDateTime> tableTimeSlots = timeSlotLookupByTable.get(definition.getTableName());
|
||||
if (!tableTimeSlots.contains(timeSlot)) {
|
||||
continue;
|
||||
}
|
||||
List<List<Object>> rows = rowsByTable.get(definition.getTableName());
|
||||
for (String lineId : command.getLineIds()) {
|
||||
for (String phaseCode : definition.getPhaseCodes()) {
|
||||
rows.add(addDataValueGenerator.generateRow(definition, lineId, timeSlot, phaseCode));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return new GeneratedBatchData(rowsByTable);
|
||||
}
|
||||
|
||||
/**
|
||||
* 写入当前窗口已经生成的数据。
|
||||
*
|
||||
* @param taskId 任务编号
|
||||
* @param generatedBatchData 当前窗口生成结果
|
||||
* @param pendingRowsByTable 跨窗口复用的待写入缓存
|
||||
* @param batchNoMap 每张表的批次号缓存
|
||||
* @return true 表示继续执行
|
||||
*/
|
||||
private boolean writeBatchData(String taskId, GeneratedBatchData generatedBatchData,
|
||||
Map<String, List<List<Object>>> pendingRowsByTable, Map<String, Integer> batchNoMap) {
|
||||
for (AddDataTableDefinition definition : addDataTableRegistry.getTableDefinitions()) {
|
||||
List<List<Object>> rows = generatedBatchData.getRows(definition.getTableName());
|
||||
if (rows.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
List<List<Object>> pendingRows = pendingRowsByTable.get(definition.getTableName());
|
||||
pendingRows.addAll(rows);
|
||||
int batchSize = definition.getBatchSize();
|
||||
while (pendingRows.size() >= batchSize) {
|
||||
List<List<Object>> batchRows = new ArrayList<List<Object>>(pendingRows.subList(0, batchSize));
|
||||
int batchNo = nextBatchNo(batchNoMap, definition.getTableName());
|
||||
if (!flushBatch(taskId, definition, batchRows, batchNo)) {
|
||||
return false;
|
||||
}
|
||||
pendingRows.subList(0, batchSize).clear();
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 任务结束后刷新所有未满批次的尾批数据。
|
||||
*
|
||||
* @param taskId 任务编号
|
||||
* @param pendingRowsByTable 跨窗口复用的待写入缓存
|
||||
* @param batchNoMap 每张表的批次号缓存
|
||||
* @return true 表示继续执行
|
||||
*/
|
||||
private boolean flushRemainingBatchData(String taskId, Map<String, List<List<Object>>> pendingRowsByTable,
|
||||
Map<String, Integer> batchNoMap) {
|
||||
for (AddDataTableDefinition definition : addDataTableRegistry.getTableDefinitions()) {
|
||||
List<List<Object>> pendingRows = pendingRowsByTable.get(definition.getTableName());
|
||||
if (pendingRows == null || pendingRows.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
int batchNo = nextBatchNo(batchNoMap, definition.getTableName());
|
||||
List<List<Object>> batchRows = new ArrayList<List<Object>>(pendingRows);
|
||||
if (!flushBatch(taskId, definition, batchRows, batchNo)) {
|
||||
return false;
|
||||
}
|
||||
pendingRows.clear();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前表的下一个批次号。
|
||||
*
|
||||
* @param batchNoMap 批次号缓存
|
||||
* @param tableName 表名
|
||||
* @return 下一个批次号
|
||||
*/
|
||||
private int nextBatchNo(Map<String, Integer> batchNoMap, String tableName) {
|
||||
Integer currentBatchNo = batchNoMap.get(tableName);
|
||||
int nextBatchNo = currentBatchNo == null ? 1 : currentBatchNo + 1;
|
||||
batchNoMap.put(tableName, nextBatchNo);
|
||||
return nextBatchNo;
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新当前批次。
|
||||
*
|
||||
* @param taskId 任务编号
|
||||
* @param definition 表定义
|
||||
* @param batchRows 批次行
|
||||
* @param batchNo 批次号
|
||||
* @return true 表示继续执行
|
||||
*/
|
||||
private boolean flushBatch(String taskId, AddDataTableDefinition definition, List<List<Object>> batchRows, int batchNo) {
|
||||
addDataTaskStatusHolder.updateCurrentBatch(taskId, definition.getTableName(), batchNo, batchRows.size());
|
||||
AddDataBatchWriteResult writeResult = addDataBatchWriter.writeBatch(definition, batchRows);
|
||||
addDataTaskStatusHolder.addProgress(taskId, writeResult.getInsertedCount(), writeResult.getSkippedCount(), writeResult.getFailedCount());
|
||||
if (writeResult.hasFailure()) {
|
||||
addDataTaskStatusHolder.markFailed(taskId,
|
||||
"表 " + definition.getTableName() + " 第 " + batchNo + " 批执行失败:" + writeResult.getFirstFailureMessage());
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 当前窗口生成结果。
|
||||
*/
|
||||
private static final class GeneratedBatchData {
|
||||
|
||||
/** 按表名分组的待写入行数据。 */
|
||||
private final Map<String, List<List<Object>>> rowsByTable;
|
||||
|
||||
private GeneratedBatchData(Map<String, List<List<Object>>> rowsByTable) {
|
||||
this.rowsByTable = rowsByTable;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定表的待写入行。
|
||||
*
|
||||
* @param tableName 表名
|
||||
* @return 待写入行列表
|
||||
*/
|
||||
private List<List<Object>> getRows(String tableName) {
|
||||
List<List<Object>> rows = rowsByTable.get(tableName);
|
||||
return rows == null ? Collections.emptyList() : rows;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,240 @@
|
||||
package com.njcn.gather.tool.adddata.component;
|
||||
|
||||
import com.njcn.gather.tool.adddata.pojo.bo.AddDataTaskCommand;
|
||||
import com.njcn.gather.tool.adddata.pojo.enums.AddDataTaskStatusEnum;
|
||||
import com.njcn.gather.tool.adddata.pojo.vo.AddDataTaskStatusVO;
|
||||
import com.njcn.gather.tool.adddata.util.AddDataDateTimeUtil;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ConcurrentMap;
|
||||
|
||||
/**
|
||||
* 任务状态持有器。
|
||||
*/
|
||||
@Component
|
||||
public class AddDataTaskStatusHolder {
|
||||
|
||||
/** 任务记录缓存。 */
|
||||
private final ConcurrentMap<String, TaskRecord> taskRecordMap = new ConcurrentHashMap<String, TaskRecord>();
|
||||
|
||||
/** 时间槽计算组件。 */
|
||||
private final AddDataTimeSlotCalculator addDataTimeSlotCalculator;
|
||||
|
||||
public AddDataTaskStatusHolder(AddDataTimeSlotCalculator addDataTimeSlotCalculator) {
|
||||
this.addDataTimeSlotCalculator = addDataTimeSlotCalculator;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建等待中的任务。
|
||||
*
|
||||
* @param command 任务命令
|
||||
* @return 任务状态快照
|
||||
*/
|
||||
public AddDataTaskStatusVO createWaitingTask(AddDataTaskCommand command) {
|
||||
String taskId = UUID.randomUUID().toString().replace("-", "");
|
||||
TaskRecord record = new TaskRecord(taskId, command, buildHourlyTimeResults(command));
|
||||
taskRecordMap.put(taskId, record);
|
||||
return copySnapshot(record);
|
||||
}
|
||||
|
||||
/**
|
||||
* 标记为运行中。
|
||||
*
|
||||
* @param taskId 任务编号
|
||||
*/
|
||||
public void markRunning(String taskId) {
|
||||
TaskRecord record = requireRecord(taskId);
|
||||
synchronized (record) {
|
||||
record.status = AddDataTaskStatusEnum.RUNNING;
|
||||
record.startTime = LocalDateTime.now();
|
||||
record.currentBatchInfo = "任务已启动";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新当前批次信息。
|
||||
*
|
||||
* @param taskId 任务编号
|
||||
* @param tableName 当前表名
|
||||
* @param batchNo 批次号
|
||||
* @param batchSize 批次大小
|
||||
*/
|
||||
public void updateCurrentBatch(String taskId, String tableName, int batchNo, int batchSize) {
|
||||
TaskRecord record = requireRecord(taskId);
|
||||
synchronized (record) {
|
||||
record.currentTableName = tableName;
|
||||
record.currentBatchInfo = "第 " + batchNo + " 批," + batchSize + " 行";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 累加执行统计。
|
||||
*
|
||||
* @param taskId 任务编号
|
||||
* @param insertedCount 新增成功数
|
||||
* @param skippedCount 跳过数
|
||||
* @param failedCount 失败数
|
||||
*/
|
||||
public void addProgress(String taskId, long insertedCount, long skippedCount, long failedCount) {
|
||||
TaskRecord record = requireRecord(taskId);
|
||||
synchronized (record) {
|
||||
record.insertedCount += insertedCount;
|
||||
record.skippedCount += skippedCount;
|
||||
record.failedCount += failedCount;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 标记任务成功。
|
||||
*
|
||||
* @param taskId 任务编号
|
||||
*/
|
||||
public void markSuccess(String taskId) {
|
||||
TaskRecord record = requireRecord(taskId);
|
||||
synchronized (record) {
|
||||
record.status = AddDataTaskStatusEnum.SUCCESS;
|
||||
record.endTime = LocalDateTime.now();
|
||||
record.currentBatchInfo = "执行完成";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 标记任务失败。
|
||||
*
|
||||
* @param taskId 任务编号
|
||||
* @param failureReason 失败原因
|
||||
*/
|
||||
public void markFailed(String taskId, String failureReason) {
|
||||
TaskRecord record = requireRecord(taskId);
|
||||
synchronized (record) {
|
||||
record.status = AddDataTaskStatusEnum.FAILED;
|
||||
record.endTime = LocalDateTime.now();
|
||||
record.failureReason = failureReason;
|
||||
record.currentBatchInfo = "执行失败";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询任务状态。
|
||||
*
|
||||
* @param taskId 任务编号
|
||||
* @return 状态快照
|
||||
*/
|
||||
public AddDataTaskStatusVO getStatus(String taskId) {
|
||||
return copySnapshot(requireRecord(taskId));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取任务命令。
|
||||
*
|
||||
* @param taskId 任务编号
|
||||
* @return 任务命令
|
||||
*/
|
||||
public AddDataTaskCommand getCommand(String taskId) {
|
||||
return requireRecord(taskId).command;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取全部任务编号。
|
||||
*
|
||||
* @return 任务编号列表
|
||||
*/
|
||||
public List<String> getTaskIds() {
|
||||
return new ArrayList<String>(taskRecordMap.keySet());
|
||||
}
|
||||
|
||||
/**
|
||||
* 复制状态快照。
|
||||
*
|
||||
* @param record 任务记录
|
||||
* @return 状态快照
|
||||
*/
|
||||
private AddDataTaskStatusVO copySnapshot(TaskRecord record) {
|
||||
synchronized (record) {
|
||||
AddDataTaskStatusVO status = new AddDataTaskStatusVO();
|
||||
status.setTaskId(record.taskId);
|
||||
status.setStatus(record.status.name());
|
||||
status.setCurrentTableName(record.currentTableName);
|
||||
status.setCurrentBatchInfo(record.currentBatchInfo);
|
||||
status.setInsertedCount(record.insertedCount);
|
||||
status.setSkippedCount(record.skippedCount);
|
||||
status.setFailedCount(record.failedCount);
|
||||
status.setFailureReason(record.failureReason);
|
||||
status.setHourlyTimeResults(new ArrayList<String>(record.hourlyTimeResults));
|
||||
status.setStartTime(AddDataDateTimeUtil.format(record.startTime));
|
||||
status.setEndTime(AddDataDateTimeUtil.format(record.endTime));
|
||||
return status;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据任务时间范围生成前端展示的每小时时刻。
|
||||
*
|
||||
* @param command 任务命令
|
||||
* @return 每小时时刻文本
|
||||
*/
|
||||
private List<String> buildHourlyTimeResults(AddDataTaskCommand command) {
|
||||
List<LocalDateTime> timeSlots = addDataTimeSlotCalculator.buildHourlyTimeSlots(command.getStartTime(), command.getEndTime());
|
||||
List<String> result = new ArrayList<String>(timeSlots.size());
|
||||
for (LocalDateTime timeSlot : timeSlots) {
|
||||
result.add(AddDataDateTimeUtil.format(timeSlot));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取任务记录。
|
||||
*
|
||||
* @param taskId 任务编号
|
||||
* @return 任务记录
|
||||
*/
|
||||
private TaskRecord requireRecord(String taskId) {
|
||||
TaskRecord record = taskRecordMap.get(taskId);
|
||||
if (record == null) {
|
||||
throw new IllegalArgumentException("未找到补数任务:" + taskId);
|
||||
}
|
||||
return record;
|
||||
}
|
||||
|
||||
/**
|
||||
* 内存态任务记录。
|
||||
*/
|
||||
private static final class TaskRecord {
|
||||
|
||||
/** 任务编号。 */
|
||||
private final String taskId;
|
||||
/** 任务命令。 */
|
||||
private final AddDataTaskCommand command;
|
||||
/** 每小时返回给前端展示的业务时刻。 */
|
||||
private final List<String> hourlyTimeResults;
|
||||
/** 当前状态。 */
|
||||
private AddDataTaskStatusEnum status = AddDataTaskStatusEnum.WAITING;
|
||||
/** 当前表名。 */
|
||||
private String currentTableName;
|
||||
/** 当前批次。 */
|
||||
private String currentBatchInfo = "等待执行";
|
||||
/** 成功数。 */
|
||||
private long insertedCount;
|
||||
/** 跳过数。 */
|
||||
private long skippedCount;
|
||||
/** 失败数。 */
|
||||
private long failedCount;
|
||||
/** 失败原因。 */
|
||||
private String failureReason;
|
||||
/** 开始时间。 */
|
||||
private LocalDateTime startTime;
|
||||
/** 结束时间。 */
|
||||
private LocalDateTime endTime;
|
||||
|
||||
private TaskRecord(String taskId, AddDataTaskCommand command, List<String> hourlyTimeResults) {
|
||||
this.taskId = taskId;
|
||||
this.command = command;
|
||||
this.hourlyTimeResults = hourlyTimeResults;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
package com.njcn.gather.tool.adddata.component;
|
||||
|
||||
import com.njcn.gather.tool.adddata.pojo.vo.AddDataTemplateVO;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 前端展示模板注册器。
|
||||
*/
|
||||
@Component
|
||||
public class AddDataTemplateRegistry {
|
||||
|
||||
/** 模板列表。 */
|
||||
private final List<AddDataTemplateVO> templates;
|
||||
|
||||
public AddDataTemplateRegistry() {
|
||||
List<AddDataTemplateVO> result = new ArrayList<AddDataTemplateVO>();
|
||||
result.add(createTemplate("电压有效值", "data_v", "ABC", Arrays.asList("A", "B", "C"),
|
||||
"RMS_MAX", "RMS_MIN", "主值字段 RMS", "RMS_CP95"));
|
||||
result.add(createTemplate("线电压有效值", "data_v", "T", Arrays.asList("T"),
|
||||
"RMSAB/RMSBC/RMSCA 对应 MAX 字段", "RMSAB/RMSBC/RMSCA 对应 MIN 字段", "主值字段 RMSAB/RMSBC/RMSCA", "RMSAB/RMSBC/RMSCA 对应 CP95 字段"));
|
||||
result.add(createTemplate("频率", "data_v", "T", Arrays.asList("T"),
|
||||
"FREQ_MAX", "FREQ_MIN", "主值字段 FREQ", "FREQ_CP95"));
|
||||
result.add(createTemplate("电压总谐波畸变率", "data_v", "ABC", Arrays.asList("A", "B", "C"),
|
||||
"V_THD_MAX", "V_THD_MIN", "主值字段 V_THD", "V_THD_CP95"));
|
||||
result.add(createTemplate("电流有效值", "data_i", "ABC", Arrays.asList("A", "B", "C"),
|
||||
"RMS_MAX", "RMS_MIN", "主值字段 RMS", "RMS_CP95"));
|
||||
result.add(createTemplate("电流总谐波畸变率", "data_i", "ABC", Arrays.asList("A", "B", "C"),
|
||||
"I_THD_MAX", "I_THD_MIN", "主值字段 I_THD", "I_THD_CP95"));
|
||||
result.add(createTemplate("电压谐波幅值", "data_harmphasic_v", "ABC", Arrays.asList("A", "B", "C"),
|
||||
"各次 V_n_MAX", "各次 V_n_MIN", "主值字段 V_n", "各次 V_n_CP95"));
|
||||
result.add(createTemplate("电流谐波幅值", "data_harmphasic_i", "ABC", Arrays.asList("A", "B", "C"),
|
||||
"各次 I_n_MAX", "各次 I_n_MIN", "主值字段 I_n", "各次 I_n_CP95"));
|
||||
result.add(createTemplate("电压谐波含有率", "data_harmrate_v", "ABC", Arrays.asList("A", "B", "C"),
|
||||
"各次 V_n_MAX", "各次 V_n_MIN", "主值字段 V_n", "各次 V_n_CP95"));
|
||||
result.add(createTemplate("电流谐波含有率", "data_harmrate_i", "ABC", Arrays.asList("A", "B", "C"),
|
||||
"各次 I_n_MAX", "各次 I_n_MIN", "主值字段 I_n", "各次 I_n_CP95"));
|
||||
result.add(createTemplate("间谐波电流", "data_inharm_i", "ABC", Arrays.asList("A", "B", "C"),
|
||||
"各次 I_n_MAX", "各次 I_n_MIN", "主值字段 I_n", "各次 I_n_CP95"));
|
||||
result.add(createTemplate("有功谐波功率", "data_harmpower_p", "ABC", Arrays.asList("A", "B", "C"),
|
||||
"P_MAX 与各次 P_n_MAX", "P_MIN 与各次 P_n_MIN", "主值字段 P / P_n", "P_CP95 与各次 P_n_CP95"));
|
||||
result.add(createTemplate("无功谐波功率", "data_harmpower_q", "ABC", Arrays.asList("A", "B", "C"),
|
||||
"Q_MAX 与各次 Q_n_MAX", "Q_MIN 与各次 Q_n_MIN", "主值字段 Q / Q_n", "Q_CP95 与各次 Q_n_CP95"));
|
||||
result.add(createTemplate("视在谐波功率", "data_harmpower_s", "ABC", Arrays.asList("A", "B", "C"),
|
||||
"S_MAX 与各次 S_n_MAX", "S_MIN 与各次 S_n_MIN", "主值字段 S / S_n", "S_CP95 与各次 S_n_CP95"));
|
||||
result.add(createTemplate("电压波动", "data_fluc", "T", Arrays.asList("T"),
|
||||
"当前表无独立最大值字段", "当前表无独立最小值字段", "主值字段 FLUC", "当前表无独立 CP95 字段"));
|
||||
result.add(createTemplate("短时闪变", "data_flicker", "T", Arrays.asList("T"),
|
||||
"当前表无独立最大值字段", "当前表无独立最小值字段", "主值字段 PST", "当前表无独立 CP95 字段"));
|
||||
result.add(createTemplate("长时闪变", "data_plt", "T", Arrays.asList("T"),
|
||||
"当前表无独立最大值字段", "当前表无独立最小值字段", "主值字段 PLT", "当前表无独立 CP95 字段"));
|
||||
templates = Collections.unmodifiableList(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回模板列表。
|
||||
*
|
||||
* @return 模板列表
|
||||
*/
|
||||
public List<AddDataTemplateVO> getTemplates() {
|
||||
return templates;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建模板对象。
|
||||
*
|
||||
* @param parameterName 参数名称
|
||||
* @param tableName 表名
|
||||
* @param phaseDisplay 展示相别
|
||||
* @param phaseCodes 落库相别
|
||||
* @param maxRule 最大值规则
|
||||
* @param minRule 最小值规则
|
||||
* @param averageRule 平均值规则
|
||||
* @param cp95Rule cp95 规则
|
||||
* @return 模板对象
|
||||
*/
|
||||
private AddDataTemplateVO createTemplate(String parameterName, String tableName, String phaseDisplay, List<String> phaseCodes,
|
||||
String maxRule, String minRule, String averageRule, String cp95Rule) {
|
||||
AddDataTemplateVO template = new AddDataTemplateVO();
|
||||
template.setParameterName(parameterName);
|
||||
template.setTableName(tableName);
|
||||
template.setPhaseDisplay(phaseDisplay);
|
||||
template.setPhaseCodes(new ArrayList<String>(phaseCodes));
|
||||
template.setDisplay(true);
|
||||
template.setShowQualified(true);
|
||||
template.setMaxValueRule(maxRule);
|
||||
template.setMinValueRule(minRule);
|
||||
template.setAverageValueRule(averageRule);
|
||||
template.setCp95ValueRule(cp95Rule);
|
||||
template.setDecimalScale(2);
|
||||
return template;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
package com.njcn.gather.tool.adddata.component;
|
||||
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 时间槽计算组件。
|
||||
*/
|
||||
@Component
|
||||
public class AddDataTimeSlotCalculator {
|
||||
|
||||
/**
|
||||
* 构建自然时间槽列表。
|
||||
*
|
||||
* @param startTime 开始时间
|
||||
* @param endTime 结束时间
|
||||
* @param intervalMinutes 步长,单位分钟
|
||||
* @return 时间槽列表
|
||||
*/
|
||||
public List<LocalDateTime> buildTimeSlots(LocalDateTime startTime, LocalDateTime endTime, int intervalMinutes) {
|
||||
if (intervalMinutes <= 0) {
|
||||
throw new IllegalArgumentException("时间步长必须大于 0");
|
||||
}
|
||||
List<LocalDateTime> result = new ArrayList<LocalDateTime>();
|
||||
LocalDateTime cursor = alignToNextSlot(startTime, intervalMinutes);
|
||||
while (!cursor.isAfter(endTime)) {
|
||||
result.add(cursor);
|
||||
cursor = cursor.plusMinutes(intervalMinutes);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建返回给前端展示的每小时自然时刻列表。
|
||||
*
|
||||
* @param startTime 开始时间
|
||||
* @param endTime 结束时间
|
||||
* @return 每小时自然时刻列表
|
||||
*/
|
||||
public List<LocalDateTime> buildHourlyTimeSlots(LocalDateTime startTime, LocalDateTime endTime) {
|
||||
return buildTimeSlots(startTime, endTime, 60);
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算时间槽数量。
|
||||
*
|
||||
* @param startTime 开始时间
|
||||
* @param endTime 结束时间
|
||||
* @param intervalMinutes 步长
|
||||
* @return 时间槽数量
|
||||
*/
|
||||
public long countTimeSlots(LocalDateTime startTime, LocalDateTime endTime, int intervalMinutes) {
|
||||
return buildTimeSlots(startTime, endTime, intervalMinutes).size();
|
||||
}
|
||||
|
||||
/**
|
||||
* 向上对齐到最近的自然时间槽。
|
||||
*
|
||||
* @param time 原始时间
|
||||
* @param intervalMinutes 步长
|
||||
* @return 对齐后的时间
|
||||
*/
|
||||
public LocalDateTime alignToNextSlot(LocalDateTime time, int intervalMinutes) {
|
||||
LocalDateTime minuteFloor = time.truncatedTo(ChronoUnit.MINUTES);
|
||||
int minuteOfDay = minuteFloor.getHour() * 60 + minuteFloor.getMinute();
|
||||
int remainder = minuteOfDay % intervalMinutes;
|
||||
LocalDateTime aligned = minuteFloor.minusMinutes(remainder);
|
||||
if (remainder != 0 || !minuteFloor.equals(time)) {
|
||||
aligned = aligned.plusMinutes(intervalMinutes);
|
||||
}
|
||||
return aligned;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,579 @@
|
||||
package com.njcn.gather.tool.adddata.component;
|
||||
|
||||
import com.njcn.gather.tool.adddata.pojo.bo.AddDataTableDefinition;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.sql.Timestamp;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.SplittableRandom;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* 电能质量数据生成器。
|
||||
*/
|
||||
@Component
|
||||
public class AddDataValueGenerator {
|
||||
|
||||
/** 谐波列匹配。 */
|
||||
private static final Pattern HARMONIC_PATTERN = Pattern.compile("^([VIQPS])_(\\d+)$");
|
||||
|
||||
/** 基础列后缀。 */
|
||||
private static final String SUFFIX_MAX = "_MAX";
|
||||
private static final String SUFFIX_MIN = "_MIN";
|
||||
private static final String SUFFIX_CP95 = "_CP95";
|
||||
|
||||
/** 两倍 PI。 */
|
||||
private static final double TWO_PI = Math.PI * 2D;
|
||||
|
||||
/**
|
||||
* 生成一行落库值。
|
||||
*
|
||||
* @param definition 表定义
|
||||
* @param lineId 监测点 ID
|
||||
* @param timeId 时间点
|
||||
* @param phaseCode 相别
|
||||
* @return 与字段顺序一致的值列表
|
||||
*/
|
||||
public List<Object> generateRow(AddDataTableDefinition definition, String lineId, LocalDateTime timeId, String phaseCode) {
|
||||
MeasurementState state = buildState(lineId, timeId, phaseCode);
|
||||
Map<String, Double> baseValues = buildBaseValues(definition, state);
|
||||
List<Object> row = new ArrayList<Object>(definition.getColumns().size());
|
||||
for (String column : definition.getColumns()) {
|
||||
if ("TIMEID".equals(column)) {
|
||||
row.add(Timestamp.valueOf(timeId));
|
||||
continue;
|
||||
}
|
||||
if ("LINEID".equals(column)) {
|
||||
row.add(lineId);
|
||||
continue;
|
||||
}
|
||||
if ("PHASIC_TYPE".equals(column)) {
|
||||
row.add(phaseCode);
|
||||
continue;
|
||||
}
|
||||
if ("QUALITYFLAG".equals(column)) {
|
||||
row.add(1);
|
||||
continue;
|
||||
}
|
||||
row.add(resolveColumnValue(definition.getTableName(), column, baseValues, state));
|
||||
}
|
||||
return row;
|
||||
}
|
||||
|
||||
/**
|
||||
* 按列名解析字段值。
|
||||
*
|
||||
* @param tableName 表名
|
||||
* @param column 列名
|
||||
* @param baseValues 主值集合
|
||||
* @param state 基础状态
|
||||
* @return 对应值
|
||||
*/
|
||||
private Double resolveColumnValue(String tableName, String column, Map<String, Double> baseValues, MeasurementState state) {
|
||||
if (baseValues.containsKey(column)) {
|
||||
return baseValues.get(column);
|
||||
}
|
||||
if (column.endsWith(SUFFIX_MAX)) {
|
||||
return deriveMetric(baseValues.get(removeSuffix(column, SUFFIX_MAX)), column, state, MetricType.MAX);
|
||||
}
|
||||
if (column.endsWith(SUFFIX_MIN)) {
|
||||
return deriveMetric(baseValues.get(removeSuffix(column, SUFFIX_MIN)), column, state, MetricType.MIN);
|
||||
}
|
||||
if (column.endsWith(SUFFIX_CP95)) {
|
||||
return deriveMetric(baseValues.get(removeSuffix(column, SUFFIX_CP95)), column, state, MetricType.CP95);
|
||||
}
|
||||
throw new IllegalStateException("未支持的 add-data 列:" + tableName + "." + column);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成主值列集合。
|
||||
*
|
||||
* @param definition 表定义
|
||||
* @param state 基础状态
|
||||
* @return 主值集合
|
||||
*/
|
||||
private Map<String, Double> buildBaseValues(AddDataTableDefinition definition, MeasurementState state) {
|
||||
Map<String, Double> values = new HashMap<String, Double>();
|
||||
for (String column : definition.getColumns()) {
|
||||
if (isInfrastructureColumn(column) || isDerivedColumn(column)) {
|
||||
continue;
|
||||
}
|
||||
values.put(column, resolveBaseMetric(definition.getTableName(), column, state));
|
||||
}
|
||||
return values;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析基础主值。
|
||||
*
|
||||
* @param tableName 表名
|
||||
* @param column 列名
|
||||
* @param state 基础状态
|
||||
* @return 主值
|
||||
*/
|
||||
private Double resolveBaseMetric(String tableName, String column, MeasurementState state) {
|
||||
Matcher matcher = HARMONIC_PATTERN.matcher(column);
|
||||
if (matcher.matches()) {
|
||||
String prefix = matcher.group(1);
|
||||
int order = Integer.parseInt(matcher.group(2));
|
||||
if ("V".equals(prefix)) {
|
||||
if ("data_harmrate_v".equals(tableName)) {
|
||||
return resolveRatioPercent(state.vHarmonics[order], state.vHarmonics[1]);
|
||||
}
|
||||
return state.vHarmonics[order];
|
||||
}
|
||||
if ("I".equals(prefix)) {
|
||||
if ("data_harmrate_i".equals(tableName)) {
|
||||
return resolveRatioPercent(state.iHarmonics[order], state.iHarmonics[1]);
|
||||
}
|
||||
if ("data_inharm_i".equals(tableName)) {
|
||||
return state.iInharmonics[order];
|
||||
}
|
||||
return state.iHarmonics[order];
|
||||
}
|
||||
if ("P".equals(prefix)) {
|
||||
return state.pHarmonics[order];
|
||||
}
|
||||
if ("Q".equals(prefix)) {
|
||||
return state.qHarmonics[order];
|
||||
}
|
||||
if ("S".equals(prefix)) {
|
||||
return state.sHarmonics[order];
|
||||
}
|
||||
}
|
||||
|
||||
switch (column) {
|
||||
case "RMS":
|
||||
return "data_v".equals(tableName) ? state.phaseVoltage : state.currentRms;
|
||||
case "RMSAB":
|
||||
return state.lineVoltageAB;
|
||||
case "RMSBC":
|
||||
return state.lineVoltageBC;
|
||||
case "RMSCA":
|
||||
return state.lineVoltageCA;
|
||||
case "VU_DEV":
|
||||
return state.vUpperDeviation;
|
||||
case "VL_DEV":
|
||||
return state.vLowerDeviation;
|
||||
case "FREQ":
|
||||
return state.frequency;
|
||||
case "FREQ_DEV":
|
||||
return state.frequencyDeviation;
|
||||
case "V_UNBALANCE":
|
||||
return state.vUnbalance;
|
||||
case "V_POS":
|
||||
return state.vPositive;
|
||||
case "V_NEG":
|
||||
return state.vNegative;
|
||||
case "V_ZERO":
|
||||
return state.vZero;
|
||||
case "V_THD":
|
||||
return state.vThd;
|
||||
case "I_UNBALANCE":
|
||||
return state.iUnbalance;
|
||||
case "I_POS":
|
||||
return state.iPositive;
|
||||
case "I_NEG":
|
||||
return state.iNegative;
|
||||
case "I_ZERO":
|
||||
return state.iZero;
|
||||
case "I_THD":
|
||||
return state.iThd;
|
||||
case "PF":
|
||||
return state.powerFactor;
|
||||
case "DF":
|
||||
return state.distortionFactor;
|
||||
case "P":
|
||||
return state.activePower;
|
||||
case "Q":
|
||||
return state.reactivePower;
|
||||
case "S":
|
||||
return state.apparentPower;
|
||||
case "FLUC":
|
||||
return state.fluc;
|
||||
case "PST":
|
||||
return state.pst;
|
||||
case "PLT":
|
||||
return state.plt;
|
||||
case "FLUCCF":
|
||||
return state.flucCf;
|
||||
default:
|
||||
throw new IllegalStateException("未支持的基础指标列:" + tableName + "." + column);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成主值派生指标。
|
||||
*
|
||||
* @param baseValue 主值
|
||||
* @param column 派生列名
|
||||
* @param state 基础状态
|
||||
* @param metricType 派生类型
|
||||
* @return 派生值
|
||||
*/
|
||||
private Double deriveMetric(Double baseValue, String column, MeasurementState state, MetricType metricType) {
|
||||
if (baseValue == null) {
|
||||
throw new IllegalStateException("派生字段缺少主值:" + column);
|
||||
}
|
||||
double factor = noise(state.sharedSeed + column.hashCode(), 0.01D, 0.05D);
|
||||
double delta = Math.max(Math.abs(baseValue) * factor, 0.005D);
|
||||
double value;
|
||||
if (MetricType.MAX.equals(metricType)) {
|
||||
value = baseValue + delta;
|
||||
} else if (MetricType.MIN.equals(metricType)) {
|
||||
value = baseValue - delta;
|
||||
if (baseValue >= 0D) {
|
||||
value = Math.max(0D, value);
|
||||
}
|
||||
} else {
|
||||
value = baseValue + delta * 0.6D;
|
||||
}
|
||||
return round(value, 4);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建同源基础状态。
|
||||
*
|
||||
* @param lineId 监测点 ID
|
||||
* @param timeId 时间
|
||||
* @param phaseCode 相别
|
||||
* @return 基础状态
|
||||
*/
|
||||
private MeasurementState buildState(String lineId, LocalDateTime timeId, String phaseCode) {
|
||||
long sharedSeed = Objects.hash(lineId, timeId.getYear(), timeId.getDayOfYear(), timeId.getHour(), timeId.getMinute());
|
||||
long phaseSeed = Objects.hash(sharedSeed, phaseCode);
|
||||
double minuteOfDay = timeId.getHour() * 60D + timeId.getMinute();
|
||||
double dayWave = Math.sin(minuteOfDay / 1440D * TWO_PI);
|
||||
double fastWave = Math.cos(minuteOfDay / 240D * TWO_PI);
|
||||
double phaseWave = Math.sin(minuteOfDay / 1440D * TWO_PI + resolvePhaseOffset(phaseCode));
|
||||
|
||||
double phaseVoltage = round(220D + dayWave * 3.6D + phaseWave * 1.5D + noise(phaseSeed + 11L, -0.9D, 0.9D), 4);
|
||||
double lineVoltageAB = round(380D + dayWave * 4.8D + noise(sharedSeed + 21L, -1.2D, 1.2D), 4);
|
||||
double lineVoltageBC = round(381D + fastWave * 3.6D + noise(sharedSeed + 22L, -1.2D, 1.2D), 4);
|
||||
double lineVoltageCA = round(379D + Math.sin(minuteOfDay / 720D * TWO_PI) * 4.2D + noise(sharedSeed + 23L, -1.2D, 1.2D), 4);
|
||||
double frequency = round(50D + Math.sin(minuteOfDay / 180D * TWO_PI) * 0.03D + noise(sharedSeed + 31L, -0.01D, 0.01D), 4);
|
||||
double frequencyDeviation = round(frequency - 50D, 4);
|
||||
|
||||
double currentRms = round(85D + fastWave * 12D + phaseWave * 6D + noise(phaseSeed + 41L, -2D, 2D), 4);
|
||||
double vPositive = round(phaseVoltage * (0.992D + noise(sharedSeed + 51L, -0.003D, 0.003D)), 4);
|
||||
double vNegative = round(Math.max(0D, phaseVoltage * (0.008D + noise(phaseSeed + 52L, 0D, 0.006D))), 4);
|
||||
double vZero = round(Math.max(0D, phaseVoltage * (0.006D + noise(phaseSeed + 53L, 0D, 0.004D))), 4);
|
||||
double vUnbalance = resolveRatioPercent(vNegative, vPositive);
|
||||
double vUpperDeviation = round(Math.max((phaseVoltage - 220D) / 220D * 100D, 0D), 4);
|
||||
double vLowerDeviation = round(Math.max((220D - phaseVoltage) / 220D * 100D, 0D), 4);
|
||||
|
||||
double iPositive = round(currentRms * (0.986D + noise(sharedSeed + 61L, -0.003D, 0.003D)), 4);
|
||||
double iNegative = round(Math.max(0D, currentRms * (0.016D + noise(phaseSeed + 62L, 0D, 0.01D))), 4);
|
||||
double iZero = round(Math.max(0D, currentRms * (0.011D + noise(phaseSeed + 63L, 0D, 0.008D))), 4);
|
||||
double iUnbalance = resolveRatioPercent(iNegative, iPositive);
|
||||
|
||||
double[] vHarmonics = new double[51];
|
||||
double[] iHarmonics = new double[51];
|
||||
double[] iInharmonics = new double[51];
|
||||
vHarmonics[1] = phaseVoltage;
|
||||
iHarmonics[1] = currentRms;
|
||||
iInharmonics[1] = round(currentRms * 0.06D, 4);
|
||||
for (int order = 2; order <= 50; order++) {
|
||||
double voltageRatio = (0.015D / order) * (1D + noise(sharedSeed + order, -0.35D, 0.35D));
|
||||
double currentRatio = (0.08D / Math.sqrt(order)) * (1D + noise(phaseSeed + order, -0.4D, 0.4D));
|
||||
vHarmonics[order] = round(Math.max(0D, phaseVoltage * voltageRatio), 4);
|
||||
iHarmonics[order] = round(Math.max(0D, currentRms * currentRatio), 4);
|
||||
iInharmonics[order] = round(Math.max(0D, iHarmonics[order] * noise(phaseSeed + order * 13L, 0.08D, 0.18D)), 4);
|
||||
}
|
||||
|
||||
double vThd = round(resolveThd(vHarmonics), 4);
|
||||
double iThd = round(resolveThd(iHarmonics), 4);
|
||||
|
||||
double[] pHarmonics = new double[51];
|
||||
double[] qHarmonics = new double[51];
|
||||
double[] sHarmonics = new double[51];
|
||||
double activePower = 0D;
|
||||
double reactivePower = 0D;
|
||||
double apparentPower = 0D;
|
||||
for (int order = 1; order <= 50; order++) {
|
||||
double apparent = Math.max(0D, round(vHarmonics[order] * iHarmonics[order] / 1000D, 4));
|
||||
double angle = 0.16D + order * 0.008D + noise(sharedSeed + order * 17L, -0.03D, 0.03D);
|
||||
double active = round(apparent * Math.cos(angle), 4);
|
||||
double reactive = round(apparent * Math.sin(angle), 4);
|
||||
pHarmonics[order] = active;
|
||||
qHarmonics[order] = reactive;
|
||||
sHarmonics[order] = round(Math.sqrt(active * active + reactive * reactive), 4);
|
||||
activePower += active;
|
||||
reactivePower += reactive;
|
||||
apparentPower += sHarmonics[order];
|
||||
}
|
||||
activePower = round(activePower, 4);
|
||||
reactivePower = round(reactivePower, 4);
|
||||
apparentPower = round(apparentPower, 4);
|
||||
double powerFactor = apparentPower == 0D ? 1D : round(clamp(activePower / apparentPower, -1D, 1D), 4);
|
||||
double distortionFactor = round(1D / Math.sqrt(1D + Math.pow(vThd / 100D, 2D) + Math.pow(iThd / 100D, 2D)), 4);
|
||||
|
||||
double fluc = round(0.35D + Math.abs(dayWave) * 0.18D + noise(sharedSeed + 71L, -0.03D, 0.03D), 4);
|
||||
double pst = round(fluc * 1.12D + 0.08D + noise(sharedSeed + 72L, -0.02D, 0.02D), 4);
|
||||
double plt = round(Math.max(0.01D, pst * 0.92D + 0.05D + noise(sharedSeed + 73L, -0.02D, 0.02D)), 4);
|
||||
double flucCf = round(fluc * (0.96D + noise(sharedSeed + 74L, -0.03D, 0.03D)), 4);
|
||||
|
||||
return new MeasurementState(sharedSeed, phaseVoltage, lineVoltageAB, lineVoltageBC, lineVoltageCA,
|
||||
frequency, frequencyDeviation, currentRms, vPositive, vNegative, vZero, vUnbalance,
|
||||
vUpperDeviation, vLowerDeviation, iPositive, iNegative, iZero, iUnbalance,
|
||||
vHarmonics, iHarmonics, iInharmonics, vThd, iThd, pHarmonics, qHarmonics, sHarmonics,
|
||||
activePower, reactivePower, apparentPower, powerFactor, distortionFactor, fluc, pst, plt, flucCf);
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析百分比。
|
||||
*
|
||||
* @param part 分子
|
||||
* @param total 分母
|
||||
* @return 百分比
|
||||
*/
|
||||
private double resolveRatioPercent(double part, double total) {
|
||||
if (total == 0D) {
|
||||
return 0D;
|
||||
}
|
||||
return round(part / total * 100D, 4);
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析 THD。
|
||||
*
|
||||
* @param harmonics 谐波数组
|
||||
* @return thd 百分比
|
||||
*/
|
||||
private double resolveThd(double[] harmonics) {
|
||||
if (harmonics[1] == 0D) {
|
||||
return 0D;
|
||||
}
|
||||
double sum = 0D;
|
||||
for (int order = 2; order < harmonics.length; order++) {
|
||||
sum += harmonics[order] * harmonics[order];
|
||||
}
|
||||
return Math.sqrt(sum) / harmonics[1] * 100D;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析相别偏移角。
|
||||
*
|
||||
* @param phaseCode 相别
|
||||
* @return 偏移角
|
||||
*/
|
||||
private double resolvePhaseOffset(String phaseCode) {
|
||||
if ("B".equals(phaseCode)) {
|
||||
return TWO_PI / 3D;
|
||||
}
|
||||
if ("C".equals(phaseCode)) {
|
||||
return TWO_PI / 3D * 2D;
|
||||
}
|
||||
return 0D;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成稳定随机扰动。
|
||||
*
|
||||
* @param seed 种子
|
||||
* @param min 最小值
|
||||
* @param max 最大值
|
||||
* @return 扰动值
|
||||
*/
|
||||
private double noise(long seed, double min, double max) {
|
||||
return min + (max - min) * new SplittableRandom(seed).nextDouble();
|
||||
}
|
||||
|
||||
/**
|
||||
* 截断小数位。
|
||||
*
|
||||
* @param value 原始值
|
||||
* @param scale 保留位数
|
||||
* @return 处理后的值
|
||||
*/
|
||||
private double round(double value, int scale) {
|
||||
if (Math.abs(value) < 0.0000001D) {
|
||||
return 0D;
|
||||
}
|
||||
return BigDecimal.valueOf(value).setScale(scale, RoundingMode.HALF_UP).doubleValue();
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除列后缀。
|
||||
*
|
||||
* @param column 列名
|
||||
* @param suffix 后缀
|
||||
* @return 主值列名
|
||||
*/
|
||||
private String removeSuffix(String column, String suffix) {
|
||||
return column.substring(0, column.length() - suffix.length());
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为系统字段。
|
||||
*
|
||||
* @param column 列名
|
||||
* @return true 表示系统字段
|
||||
*/
|
||||
private boolean isInfrastructureColumn(String column) {
|
||||
return "TIMEID".equals(column) || "LINEID".equals(column) || "PHASIC_TYPE".equals(column) || "QUALITYFLAG".equals(column);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为派生列。
|
||||
*
|
||||
* @param column 列名
|
||||
* @return true 表示派生列
|
||||
*/
|
||||
private boolean isDerivedColumn(String column) {
|
||||
return column.endsWith(SUFFIX_MAX) || column.endsWith(SUFFIX_MIN) || column.endsWith(SUFFIX_CP95);
|
||||
}
|
||||
|
||||
/**
|
||||
* 数值类型。
|
||||
*/
|
||||
private enum MetricType {
|
||||
/** 最大值。 */
|
||||
MAX,
|
||||
/** 最小值。 */
|
||||
MIN,
|
||||
/** cp95。 */
|
||||
CP95
|
||||
}
|
||||
|
||||
/**
|
||||
* 数值约束。
|
||||
*
|
||||
* @param value 值
|
||||
* @param min 最小值
|
||||
* @param max 最大值
|
||||
* @return 限幅结果
|
||||
*/
|
||||
private double clamp(double value, double min, double max) {
|
||||
return Math.max(min, Math.min(max, value));
|
||||
}
|
||||
|
||||
/**
|
||||
* 同源基础状态。
|
||||
*/
|
||||
private static final class MeasurementState {
|
||||
|
||||
/** 共享种子。 */
|
||||
private final long sharedSeed;
|
||||
/** 电压有效值。 */
|
||||
private final double phaseVoltage;
|
||||
/** AB 线电压。 */
|
||||
private final double lineVoltageAB;
|
||||
/** BC 线电压。 */
|
||||
private final double lineVoltageBC;
|
||||
/** CA 线电压。 */
|
||||
private final double lineVoltageCA;
|
||||
/** 频率。 */
|
||||
private final double frequency;
|
||||
/** 频偏。 */
|
||||
private final double frequencyDeviation;
|
||||
/** 电流有效值。 */
|
||||
private final double currentRms;
|
||||
/** 电压正序。 */
|
||||
private final double vPositive;
|
||||
/** 电压负序。 */
|
||||
private final double vNegative;
|
||||
/** 电压零序。 */
|
||||
private final double vZero;
|
||||
/** 电压不平衡度。 */
|
||||
private final double vUnbalance;
|
||||
/** 电压上偏差。 */
|
||||
private final double vUpperDeviation;
|
||||
/** 电压下偏差。 */
|
||||
private final double vLowerDeviation;
|
||||
/** 电流正序。 */
|
||||
private final double iPositive;
|
||||
/** 电流负序。 */
|
||||
private final double iNegative;
|
||||
/** 电流零序。 */
|
||||
private final double iZero;
|
||||
/** 电流不平衡度。 */
|
||||
private final double iUnbalance;
|
||||
/** 电压谐波。 */
|
||||
private final double[] vHarmonics;
|
||||
/** 电流谐波。 */
|
||||
private final double[] iHarmonics;
|
||||
/** 间谐波电流。 */
|
||||
private final double[] iInharmonics;
|
||||
/** 电压 thd。 */
|
||||
private final double vThd;
|
||||
/** 电流 thd。 */
|
||||
private final double iThd;
|
||||
/** 有功谐波功率。 */
|
||||
private final double[] pHarmonics;
|
||||
/** 无功谐波功率。 */
|
||||
private final double[] qHarmonics;
|
||||
/** 视在谐波功率。 */
|
||||
private final double[] sHarmonics;
|
||||
/** 总有功。 */
|
||||
private final double activePower;
|
||||
/** 总无功。 */
|
||||
private final double reactivePower;
|
||||
/** 总视在。 */
|
||||
private final double apparentPower;
|
||||
/** 功率因数。 */
|
||||
private final double powerFactor;
|
||||
/** 畸变因数。 */
|
||||
private final double distortionFactor;
|
||||
/** 波动值。 */
|
||||
private final double fluc;
|
||||
/** 短时闪变。 */
|
||||
private final double pst;
|
||||
/** 长时闪变。 */
|
||||
private final double plt;
|
||||
/** 波动修正值。 */
|
||||
private final double flucCf;
|
||||
|
||||
private MeasurementState(long sharedSeed, double phaseVoltage, double lineVoltageAB, double lineVoltageBC, double lineVoltageCA,
|
||||
double frequency, double frequencyDeviation, double currentRms, double vPositive, double vNegative,
|
||||
double vZero, double vUnbalance, double vUpperDeviation, double vLowerDeviation, double iPositive,
|
||||
double iNegative, double iZero, double iUnbalance, double[] vHarmonics, double[] iHarmonics,
|
||||
double[] iInharmonics, double vThd, double iThd, double[] pHarmonics, double[] qHarmonics,
|
||||
double[] sHarmonics, double activePower, double reactivePower, double apparentPower,
|
||||
double powerFactor, double distortionFactor, double fluc, double pst, double plt, double flucCf) {
|
||||
this.sharedSeed = sharedSeed;
|
||||
this.phaseVoltage = phaseVoltage;
|
||||
this.lineVoltageAB = lineVoltageAB;
|
||||
this.lineVoltageBC = lineVoltageBC;
|
||||
this.lineVoltageCA = lineVoltageCA;
|
||||
this.frequency = frequency;
|
||||
this.frequencyDeviation = frequencyDeviation;
|
||||
this.currentRms = currentRms;
|
||||
this.vPositive = vPositive;
|
||||
this.vNegative = vNegative;
|
||||
this.vZero = vZero;
|
||||
this.vUnbalance = vUnbalance;
|
||||
this.vUpperDeviation = vUpperDeviation;
|
||||
this.vLowerDeviation = vLowerDeviation;
|
||||
this.iPositive = iPositive;
|
||||
this.iNegative = iNegative;
|
||||
this.iZero = iZero;
|
||||
this.iUnbalance = iUnbalance;
|
||||
this.vHarmonics = vHarmonics;
|
||||
this.iHarmonics = iHarmonics;
|
||||
this.iInharmonics = iInharmonics;
|
||||
this.vThd = vThd;
|
||||
this.iThd = iThd;
|
||||
this.pHarmonics = pHarmonics;
|
||||
this.qHarmonics = qHarmonics;
|
||||
this.sHarmonics = sHarmonics;
|
||||
this.activePower = activePower;
|
||||
this.reactivePower = reactivePower;
|
||||
this.apparentPower = apparentPower;
|
||||
this.powerFactor = powerFactor;
|
||||
this.distortionFactor = distortionFactor;
|
||||
this.fluc = fluc;
|
||||
this.pst = pst;
|
||||
this.plt = plt;
|
||||
this.flucCf = flucCf;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package com.njcn.gather.tool.adddata.config;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.LinkedBlockingQueue;
|
||||
import java.util.concurrent.ThreadPoolExecutor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
/**
|
||||
* add-data 模块任务线程池配置。
|
||||
*/
|
||||
@Slf4j
|
||||
@Configuration
|
||||
public class AddDataExecutorConfig {
|
||||
|
||||
@Bean(name = "addDataTaskExecutorService", destroyMethod = "shutdown")
|
||||
public ExecutorService addDataTaskExecutorService() {
|
||||
AtomicInteger threadIndex = new AtomicInteger(1);
|
||||
return new ThreadPoolExecutor(
|
||||
1,
|
||||
1,
|
||||
30,
|
||||
TimeUnit.SECONDS,
|
||||
new LinkedBlockingQueue<Runnable>(8),
|
||||
runnable -> {
|
||||
Thread thread = new Thread(runnable);
|
||||
thread.setName("add-data-task-" + threadIndex.getAndIncrement());
|
||||
return thread;
|
||||
},
|
||||
(runnable, executor) -> log.warn("数据补录任务线程池已满,拒绝新的补数任务")
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
package com.njcn.gather.tool.adddata.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.tool.adddata.pojo.param.AddDataTaskRequestParam;
|
||||
import com.njcn.gather.tool.adddata.pojo.vo.AddDataPreviewVO;
|
||||
import com.njcn.gather.tool.adddata.pojo.vo.AddDataTaskCreateVO;
|
||||
import com.njcn.gather.tool.adddata.pojo.vo.AddDataTaskStatusVO;
|
||||
import com.njcn.gather.tool.adddata.service.AddDataTaskService;
|
||||
import com.njcn.web.controller.BaseController;
|
||||
import com.njcn.web.utils.HttpResultUtil;
|
||||
import io.swagger.annotations.Api;
|
||||
import io.swagger.annotations.ApiOperation;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
/**
|
||||
* 数据补录任务接口。
|
||||
*/
|
||||
@Validated
|
||||
@Slf4j
|
||||
@Api(tags = "数据补录任务")
|
||||
@RestController
|
||||
@RequestMapping("/addData/task")
|
||||
@RequiredArgsConstructor
|
||||
public class AddDataTaskController extends BaseController {
|
||||
|
||||
/** 数据补录任务服务。 */
|
||||
private final AddDataTaskService addDataTaskService;
|
||||
|
||||
/**
|
||||
* 预估本次补数规模。
|
||||
*
|
||||
* @param param 补数参数
|
||||
* @return 预估结果
|
||||
*/
|
||||
@OperateInfo(info = LogEnum.BUSINESS_COMMON)
|
||||
@ApiOperation("预估电能质量批量补数规模")
|
||||
@PostMapping("/preview")
|
||||
public HttpResult<AddDataPreviewVO> preview(@RequestBody @Validated AddDataTaskRequestParam param) {
|
||||
String methodDescribe = getMethodDescribe("preview");
|
||||
LogUtil.njcnDebug(log, "{},开始预估补数规模,lineCount={}, intervalMinutes={}",
|
||||
methodDescribe, param.getLineIds() == null ? 0 : param.getLineIds().size(), param.getIntervalMinutes());
|
||||
AddDataPreviewVO result = addDataTaskService.preview(param);
|
||||
return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, result, methodDescribe);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建后台补数任务。
|
||||
*
|
||||
* @param param 补数参数
|
||||
* @return 任务编号
|
||||
*/
|
||||
@OperateInfo(info = LogEnum.BUSINESS_COMMON)
|
||||
@ApiOperation("创建电能质量批量补数任务")
|
||||
@PostMapping("/create")
|
||||
public HttpResult<AddDataTaskCreateVO> create(@RequestBody @Validated AddDataTaskRequestParam param) {
|
||||
String methodDescribe = getMethodDescribe("create");
|
||||
LogUtil.njcnDebug(log, "{},开始创建补数任务,lineCount={}, intervalMinutes={}",
|
||||
methodDescribe, param.getLineIds() == null ? 0 : param.getLineIds().size(), param.getIntervalMinutes());
|
||||
AddDataTaskCreateVO result = addDataTaskService.create(param);
|
||||
return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, result, methodDescribe);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询任务状态。
|
||||
*
|
||||
* @param taskId 任务编号
|
||||
* @return 当前任务状态
|
||||
*/
|
||||
@OperateInfo(info = LogEnum.BUSINESS_COMMON)
|
||||
@ApiOperation("查询电能质量批量补数任务状态")
|
||||
@GetMapping("/status/{taskId}")
|
||||
public HttpResult<AddDataTaskStatusVO> status(@PathVariable("taskId") String taskId) {
|
||||
String methodDescribe = getMethodDescribe("status");
|
||||
LogUtil.njcnDebug(log, "{},开始查询补数任务状态,taskId={}", methodDescribe, taskId);
|
||||
AddDataTaskStatusVO result = addDataTaskService.getStatus(taskId);
|
||||
return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, result, methodDescribe);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package com.njcn.gather.tool.adddata.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.tool.adddata.pojo.vo.AddDataTemplateVO;
|
||||
import com.njcn.gather.tool.adddata.service.AddDataTemplateService;
|
||||
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("/addData/template")
|
||||
@RequiredArgsConstructor
|
||||
public class AddDataTemplateController extends BaseController {
|
||||
|
||||
/** 数据补录模板服务。 */
|
||||
private final AddDataTemplateService addDataTemplateService;
|
||||
|
||||
/**
|
||||
* 返回前端模板展示规则。
|
||||
*
|
||||
* @return 模板列表
|
||||
*/
|
||||
@OperateInfo(info = LogEnum.BUSINESS_COMMON)
|
||||
@ApiOperation("查询数据补录模板规则")
|
||||
@GetMapping("/list")
|
||||
public HttpResult<List<AddDataTemplateVO>> list() {
|
||||
String methodDescribe = getMethodDescribe("list");
|
||||
LogUtil.njcnDebug(log, "{},开始查询数据补录模板规则", methodDescribe);
|
||||
List<AddDataTemplateVO> result = addDataTemplateService.listTemplates();
|
||||
return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, result, methodDescribe);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package com.njcn.gather.tool.adddata.pojo.bo;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* 批量写入结果。
|
||||
*/
|
||||
@Getter
|
||||
public class AddDataBatchWriteResult {
|
||||
|
||||
/** 成功插入条数。 */
|
||||
private final long insertedCount;
|
||||
|
||||
/** 因主键重复被跳过的条数。 */
|
||||
private final long skippedCount;
|
||||
|
||||
/** 非主键冲突失败条数。 */
|
||||
private final long failedCount;
|
||||
|
||||
/** 第一条失败原因。 */
|
||||
private final String firstFailureMessage;
|
||||
|
||||
public AddDataBatchWriteResult(long insertedCount, long skippedCount, long failedCount, String firstFailureMessage) {
|
||||
this.insertedCount = insertedCount;
|
||||
this.skippedCount = skippedCount;
|
||||
this.failedCount = failedCount;
|
||||
this.firstFailureMessage = firstFailureMessage;
|
||||
}
|
||||
|
||||
/**
|
||||
* 当前批次是否存在失败。
|
||||
*
|
||||
* @return true 表示存在失败
|
||||
*/
|
||||
public boolean hasFailure() {
|
||||
return failedCount > 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package com.njcn.gather.tool.adddata.pojo.bo;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 数据补录表定义。
|
||||
*/
|
||||
@Getter
|
||||
public class AddDataTableDefinition {
|
||||
|
||||
/** 分表时间轴类型。 */
|
||||
public enum TimeAxisType {
|
||||
/** 使用前端传入步长。 */
|
||||
REQUEST_INTERVAL,
|
||||
/** 固定 10 分钟。 */
|
||||
FIXED_TEN_MINUTES,
|
||||
/** 固定 2 小时。 */
|
||||
FIXED_TWO_HOURS
|
||||
}
|
||||
|
||||
/** 表名。 */
|
||||
private final String tableName;
|
||||
|
||||
/** 字段列表,顺序与落库 SQL 保持一致。 */
|
||||
private final List<String> columns;
|
||||
|
||||
/** 首版实际落库相别集合。 */
|
||||
private final List<String> phaseCodes;
|
||||
|
||||
/** 单批写入大小。 */
|
||||
private final int batchSize;
|
||||
|
||||
/** 时间轴类型。 */
|
||||
private final TimeAxisType timeAxisType;
|
||||
|
||||
public AddDataTableDefinition(String tableName, List<String> columns, List<String> phaseCodes,
|
||||
int batchSize, TimeAxisType timeAxisType) {
|
||||
this.tableName = tableName;
|
||||
this.columns = Collections.unmodifiableList(new ArrayList<String>(columns));
|
||||
this.phaseCodes = Collections.unmodifiableList(new ArrayList<String>(phaseCodes));
|
||||
this.batchSize = batchSize;
|
||||
this.timeAxisType = timeAxisType;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析当前表真实使用的时间步长。
|
||||
*
|
||||
* @param requestIntervalMinutes 前端传入步长
|
||||
* @return 当前表步长
|
||||
*/
|
||||
public int resolveIntervalMinutes(int requestIntervalMinutes) {
|
||||
if (TimeAxisType.FIXED_TEN_MINUTES.equals(timeAxisType)) {
|
||||
return 10;
|
||||
}
|
||||
if (TimeAxisType.FIXED_TWO_HOURS.equals(timeAxisType)) {
|
||||
return 120;
|
||||
}
|
||||
return requestIntervalMinutes;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package com.njcn.gather.tool.adddata.pojo.bo;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 归一化后的补数任务命令。
|
||||
*/
|
||||
@Getter
|
||||
public class AddDataTaskCommand {
|
||||
|
||||
/** 监测点 ID 列表。 */
|
||||
private final List<String> lineIds;
|
||||
|
||||
/** 开始时间。 */
|
||||
private final LocalDateTime startTime;
|
||||
|
||||
/** 结束时间。 */
|
||||
private final LocalDateTime endTime;
|
||||
|
||||
/** 用户步长。 */
|
||||
private final int intervalMinutes;
|
||||
|
||||
public AddDataTaskCommand(List<String> lineIds, LocalDateTime startTime, LocalDateTime endTime, int intervalMinutes) {
|
||||
this.lineIds = Collections.unmodifiableList(new ArrayList<String>(lineIds));
|
||||
this.startTime = startTime;
|
||||
this.endTime = endTime;
|
||||
this.intervalMinutes = intervalMinutes;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.njcn.gather.tool.adddata.pojo.enums;
|
||||
|
||||
/**
|
||||
* 数据补录任务状态。
|
||||
*/
|
||||
public enum AddDataTaskStatusEnum {
|
||||
/** 等待执行。 */
|
||||
WAITING,
|
||||
/** 执行中。 */
|
||||
RUNNING,
|
||||
/** 执行成功。 */
|
||||
SUCCESS,
|
||||
/** 执行失败。 */
|
||||
FAILED
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package com.njcn.gather.tool.adddata.pojo.param;
|
||||
|
||||
import io.swagger.annotations.ApiModel;
|
||||
import io.swagger.annotations.ApiModelProperty;
|
||||
import lombok.Data;
|
||||
|
||||
import javax.validation.constraints.NotBlank;
|
||||
import javax.validation.constraints.NotEmpty;
|
||||
import javax.validation.constraints.NotNull;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 数据补录任务请求参数。
|
||||
*/
|
||||
@Data
|
||||
@ApiModel("数据补录任务请求参数")
|
||||
public class AddDataTaskRequestParam {
|
||||
|
||||
@ApiModelProperty(value = "监测点 ID 列表", required = true)
|
||||
@NotEmpty(message = "监测点 ID 列表不能为空")
|
||||
private List<String> lineIds = new ArrayList<String>();
|
||||
|
||||
@ApiModelProperty(value = "开始时间,格式 yyyy-MM-dd HH:mm:ss", required = true)
|
||||
@NotBlank(message = "开始时间不能为空")
|
||||
private String startTime;
|
||||
|
||||
@ApiModelProperty(value = "结束时间,格式 yyyy-MM-dd HH:mm:ss", required = true)
|
||||
@NotBlank(message = "结束时间不能为空")
|
||||
private String endTime;
|
||||
|
||||
@ApiModelProperty(value = "用户步长,仅支持 1/3/5/10 分钟", required = true)
|
||||
@NotNull(message = "时间步长不能为空")
|
||||
private Integer intervalMinutes;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.njcn.gather.tool.adddata.pojo.vo;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 单表预估结果。
|
||||
*/
|
||||
@Data
|
||||
public class AddDataPreviewTableVO {
|
||||
|
||||
/** 表名。 */
|
||||
private String tableName;
|
||||
|
||||
/** 当前表命中的时间点数量。 */
|
||||
private long timePointCount;
|
||||
|
||||
/** 当前表实际展开的落库相别数量。 */
|
||||
private int phaseCount;
|
||||
|
||||
/** 当前表预估写入条数。 */
|
||||
private long rowCount;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.njcn.gather.tool.adddata.pojo.vo;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 补数规模预估结果。
|
||||
*/
|
||||
@Data
|
||||
public class AddDataPreviewVO {
|
||||
|
||||
/** 监测点数量。 */
|
||||
private int lineCount;
|
||||
|
||||
/** 用户步长。 */
|
||||
private int intervalMinutes;
|
||||
|
||||
/** 总预估条数。 */
|
||||
private long totalRowCount;
|
||||
|
||||
/** 单表预估详情。 */
|
||||
private List<AddDataPreviewTableVO> tableStats = new ArrayList<AddDataPreviewTableVO>();
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.njcn.gather.tool.adddata.pojo.vo;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 创建任务返回值。
|
||||
*/
|
||||
@Data
|
||||
public class AddDataTaskCreateVO {
|
||||
|
||||
/** 任务编号。 */
|
||||
private String taskId;
|
||||
|
||||
/** 初始状态。 */
|
||||
private String status;
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package com.njcn.gather.tool.adddata.pojo.vo;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 数据补录任务状态。
|
||||
*/
|
||||
@Data
|
||||
public class AddDataTaskStatusVO {
|
||||
|
||||
/** 任务编号。 */
|
||||
private String taskId;
|
||||
|
||||
/** 当前状态。 */
|
||||
private String status;
|
||||
|
||||
/** 当前执行表名。 */
|
||||
private String currentTableName;
|
||||
|
||||
/** 当前批次描述。 */
|
||||
private String currentBatchInfo;
|
||||
|
||||
/** 成功写入数量。 */
|
||||
private long insertedCount;
|
||||
|
||||
/** 主键重复跳过数量。 */
|
||||
private long skippedCount;
|
||||
|
||||
/** 非主键冲突失败数量。 */
|
||||
private long failedCount;
|
||||
|
||||
/** 失败原因。 */
|
||||
private String failureReason;
|
||||
|
||||
/** 每小时返回给前端展示的业务时刻。 */
|
||||
private List<String> hourlyTimeResults = new ArrayList<String>();
|
||||
|
||||
/** 任务开始时间。 */
|
||||
private String startTime;
|
||||
|
||||
/** 任务结束时间。 */
|
||||
private String endTime;
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package com.njcn.gather.tool.adddata.pojo.vo;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 前端展示模板规则。
|
||||
*/
|
||||
@Data
|
||||
public class AddDataTemplateVO {
|
||||
|
||||
/** 电能质量参数名称。 */
|
||||
private String parameterName;
|
||||
|
||||
/** 关联表名。 */
|
||||
private String tableName;
|
||||
|
||||
/** 前端展示相别。 */
|
||||
private String phaseDisplay;
|
||||
|
||||
/** 实际落库相别集合。 */
|
||||
private List<String> phaseCodes = new ArrayList<String>();
|
||||
|
||||
/** 当前模板是否展示。 */
|
||||
private boolean display;
|
||||
|
||||
/** 是否展示是否合格列。 */
|
||||
private boolean showQualified;
|
||||
|
||||
/** 最大值规则。 */
|
||||
private String maxValueRule;
|
||||
|
||||
/** 最小值规则。 */
|
||||
private String minValueRule;
|
||||
|
||||
/** 平均值规则。 */
|
||||
private String averageValueRule;
|
||||
|
||||
/** 95% 概率大值规则。 */
|
||||
private String cp95ValueRule;
|
||||
|
||||
/** 默认保留小数位数。 */
|
||||
private int decimalScale;
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package com.njcn.gather.tool.adddata.service;
|
||||
|
||||
import com.njcn.gather.tool.adddata.pojo.param.AddDataTaskRequestParam;
|
||||
import com.njcn.gather.tool.adddata.pojo.vo.AddDataPreviewVO;
|
||||
import com.njcn.gather.tool.adddata.pojo.vo.AddDataTaskCreateVO;
|
||||
import com.njcn.gather.tool.adddata.pojo.vo.AddDataTaskStatusVO;
|
||||
|
||||
/**
|
||||
* 数据补录任务服务。
|
||||
*/
|
||||
public interface AddDataTaskService {
|
||||
|
||||
/**
|
||||
* 预估补数规模。
|
||||
*
|
||||
* @param param 补数参数
|
||||
* @return 预估结果
|
||||
*/
|
||||
AddDataPreviewVO preview(AddDataTaskRequestParam param);
|
||||
|
||||
/**
|
||||
* 创建补数任务。
|
||||
*
|
||||
* @param param 补数参数
|
||||
* @return 任务创建结果
|
||||
*/
|
||||
AddDataTaskCreateVO create(AddDataTaskRequestParam param);
|
||||
|
||||
/**
|
||||
* 查询补数任务状态。
|
||||
*
|
||||
* @param taskId 任务编号
|
||||
* @return 当前任务状态
|
||||
*/
|
||||
AddDataTaskStatusVO getStatus(String taskId);
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.njcn.gather.tool.adddata.service;
|
||||
|
||||
import com.njcn.gather.tool.adddata.pojo.vo.AddDataTemplateVO;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 数据补录模板服务。
|
||||
*/
|
||||
public interface AddDataTemplateService {
|
||||
|
||||
/**
|
||||
* 返回前端参数模板。
|
||||
*
|
||||
* @return 模板列表
|
||||
*/
|
||||
List<AddDataTemplateVO> listTemplates();
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
package com.njcn.gather.tool.adddata.service.impl;
|
||||
|
||||
import com.njcn.gather.tool.adddata.component.AddDataTableRegistry;
|
||||
import com.njcn.gather.tool.adddata.component.AddDataTaskExecutor;
|
||||
import com.njcn.gather.tool.adddata.component.AddDataTaskStatusHolder;
|
||||
import com.njcn.gather.tool.adddata.component.AddDataTimeSlotCalculator;
|
||||
import com.njcn.gather.tool.adddata.pojo.bo.AddDataTableDefinition;
|
||||
import com.njcn.gather.tool.adddata.pojo.bo.AddDataTaskCommand;
|
||||
import com.njcn.gather.tool.adddata.pojo.param.AddDataTaskRequestParam;
|
||||
import com.njcn.gather.tool.adddata.pojo.vo.AddDataPreviewTableVO;
|
||||
import com.njcn.gather.tool.adddata.pojo.vo.AddDataPreviewVO;
|
||||
import com.njcn.gather.tool.adddata.pojo.vo.AddDataTaskCreateVO;
|
||||
import com.njcn.gather.tool.adddata.pojo.vo.AddDataTaskStatusVO;
|
||||
import com.njcn.gather.tool.adddata.service.AddDataTaskService;
|
||||
import com.njcn.gather.tool.adddata.util.AddDataDateTimeUtil;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* 数据补录任务服务实现。
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class AddDataTaskServiceImpl implements AddDataTaskService {
|
||||
|
||||
/** 支持的用户步长。 */
|
||||
private static final Set<Integer> SUPPORTED_INTERVALS = new LinkedHashSet<Integer>(Arrays.asList(1, 3, 5, 10));
|
||||
|
||||
/** 表定义注册器。 */
|
||||
private final AddDataTableRegistry addDataTableRegistry;
|
||||
|
||||
/** 时间槽计算器。 */
|
||||
private final AddDataTimeSlotCalculator addDataTimeSlotCalculator;
|
||||
|
||||
/** 任务状态持有器。 */
|
||||
private final AddDataTaskStatusHolder addDataTaskStatusHolder;
|
||||
|
||||
/** 后台执行器。 */
|
||||
private final AddDataTaskExecutor addDataTaskExecutor;
|
||||
|
||||
@Override
|
||||
public AddDataPreviewVO preview(AddDataTaskRequestParam param) {
|
||||
AddDataTaskCommand command = buildCommand(param);
|
||||
AddDataPreviewVO result = new AddDataPreviewVO();
|
||||
result.setLineCount(command.getLineIds().size());
|
||||
result.setIntervalMinutes(command.getIntervalMinutes());
|
||||
long totalRowCount = 0L;
|
||||
List<AddDataPreviewTableVO> tableStats = new ArrayList<AddDataPreviewTableVO>();
|
||||
for (AddDataTableDefinition definition : addDataTableRegistry.getTableDefinitions()) {
|
||||
long timePointCount = addDataTimeSlotCalculator.countTimeSlots(
|
||||
command.getStartTime(), command.getEndTime(), definition.resolveIntervalMinutes(command.getIntervalMinutes()));
|
||||
long rowCount = timePointCount * command.getLineIds().size() * definition.getPhaseCodes().size();
|
||||
AddDataPreviewTableVO tableVO = new AddDataPreviewTableVO();
|
||||
tableVO.setTableName(definition.getTableName());
|
||||
tableVO.setTimePointCount(timePointCount);
|
||||
tableVO.setPhaseCount(definition.getPhaseCodes().size());
|
||||
tableVO.setRowCount(rowCount);
|
||||
tableStats.add(tableVO);
|
||||
totalRowCount += rowCount;
|
||||
}
|
||||
result.setTableStats(tableStats);
|
||||
result.setTotalRowCount(totalRowCount);
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AddDataTaskCreateVO create(AddDataTaskRequestParam param) {
|
||||
AddDataTaskCommand command = buildCommand(param);
|
||||
AddDataTaskStatusVO snapshot = addDataTaskStatusHolder.createWaitingTask(command);
|
||||
addDataTaskExecutor.submit(snapshot.getTaskId(), command);
|
||||
AddDataTaskCreateVO result = new AddDataTaskCreateVO();
|
||||
result.setTaskId(snapshot.getTaskId());
|
||||
result.setStatus(snapshot.getStatus());
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AddDataTaskStatusVO getStatus(String taskId) {
|
||||
if (taskId == null || taskId.trim().isEmpty()) {
|
||||
throw new IllegalArgumentException("任务编号不能为空");
|
||||
}
|
||||
return addDataTaskStatusHolder.getStatus(taskId.trim());
|
||||
}
|
||||
|
||||
/**
|
||||
* 归一化任务命令。
|
||||
*
|
||||
* @param param 请求参数
|
||||
* @return 任务命令
|
||||
*/
|
||||
private AddDataTaskCommand buildCommand(AddDataTaskRequestParam param) {
|
||||
if (param == null) {
|
||||
throw new IllegalArgumentException("补数参数不能为空");
|
||||
}
|
||||
Integer intervalMinutes = param.getIntervalMinutes();
|
||||
if (intervalMinutes == null || !SUPPORTED_INTERVALS.contains(intervalMinutes)) {
|
||||
throw new IllegalArgumentException("时间步长仅支持 1、3、5、10 分钟");
|
||||
}
|
||||
List<String> lineIds = normalizeLineIds(param.getLineIds());
|
||||
LocalDateTime startTime = AddDataDateTimeUtil.parse(param.getStartTime());
|
||||
LocalDateTime endTime = AddDataDateTimeUtil.parse(param.getEndTime());
|
||||
if (startTime.isAfter(endTime)) {
|
||||
throw new IllegalArgumentException("开始时间不能大于结束时间");
|
||||
}
|
||||
return new AddDataTaskCommand(lineIds, startTime, endTime, intervalMinutes);
|
||||
}
|
||||
|
||||
/**
|
||||
* 标准化监测点列表。
|
||||
*
|
||||
* @param lineIds 原始监测点列表
|
||||
* @return 去重后的监测点列表
|
||||
*/
|
||||
private List<String> normalizeLineIds(List<String> lineIds) {
|
||||
if (lineIds == null || lineIds.isEmpty()) {
|
||||
throw new IllegalArgumentException("监测点 ID 列表不能为空");
|
||||
}
|
||||
LinkedHashSet<String> normalized = new LinkedHashSet<String>();
|
||||
for (String lineId : lineIds) {
|
||||
if (lineId == null) {
|
||||
throw new IllegalArgumentException("监测点 ID 不能为空");
|
||||
}
|
||||
String normalizedLineId = lineId.trim();
|
||||
// LINEID 已改为 char(32),这里同步限制字符串长度,避免运行时写入越界。
|
||||
if (normalizedLineId.isEmpty()) {
|
||||
throw new IllegalArgumentException("监测点 ID 不能为空");
|
||||
}
|
||||
if (normalizedLineId.length() > 32) {
|
||||
throw new IllegalArgumentException("监测点 ID 长度不能超过 32 位");
|
||||
}
|
||||
normalized.add(normalizedLineId);
|
||||
}
|
||||
return new ArrayList<String>(normalized);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.njcn.gather.tool.adddata.service.impl;
|
||||
|
||||
import com.njcn.gather.tool.adddata.component.AddDataTemplateRegistry;
|
||||
import com.njcn.gather.tool.adddata.pojo.vo.AddDataTemplateVO;
|
||||
import com.njcn.gather.tool.adddata.service.AddDataTemplateService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 数据补录模板服务实现。
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class AddDataTemplateServiceImpl implements AddDataTemplateService {
|
||||
|
||||
/** 模板注册器。 */
|
||||
private final AddDataTemplateRegistry addDataTemplateRegistry;
|
||||
|
||||
@Override
|
||||
public List<AddDataTemplateVO> listTemplates() {
|
||||
return addDataTemplateRegistry.getTemplates();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package com.njcn.gather.tool.adddata.util;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.time.format.DateTimeParseException;
|
||||
|
||||
/**
|
||||
* add-data 模块时间工具。
|
||||
*/
|
||||
public final class AddDataDateTimeUtil {
|
||||
|
||||
/** 统一返回格式。 */
|
||||
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"),
|
||||
DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss"),
|
||||
DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm")
|
||||
};
|
||||
|
||||
private AddDataDateTimeUtil() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析请求时间。
|
||||
*
|
||||
* @param timeText 时间文本
|
||||
* @return 解析后的时间
|
||||
*/
|
||||
public static LocalDateTime parse(String timeText) {
|
||||
for (DateTimeFormatter formatter : INPUT_FORMATTERS) {
|
||||
try {
|
||||
return LocalDateTime.parse(timeText, formatter);
|
||||
} catch (DateTimeParseException ignored) {
|
||||
}
|
||||
}
|
||||
throw new IllegalArgumentException("时间格式不正确,仅支持 yyyy-MM-dd HH:mm:ss 或 yyyy-MM-dd'T'HH:mm:ss");
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化时间。
|
||||
*
|
||||
* @param time 时间对象
|
||||
* @return 格式化结果
|
||||
*/
|
||||
public static String format(LocalDateTime time) {
|
||||
return time == null ? null : time.format(OUTPUT_FORMATTER);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.njcn.gather.tool.adddata.component;
|
||||
|
||||
import com.njcn.gather.tool.adddata.pojo.bo.AddDataTableDefinition;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 表定义注册测试。
|
||||
*/
|
||||
class AddDataTableRegistryTest {
|
||||
|
||||
@Test
|
||||
void shouldLoadAllThirteenTablesFromSchema() throws Exception {
|
||||
AddDataTableRegistry registry = new AddDataTableRegistry();
|
||||
registry.afterPropertiesSet();
|
||||
|
||||
List<AddDataTableDefinition> definitions = registry.getTableDefinitions();
|
||||
|
||||
Assertions.assertEquals(13, definitions.size());
|
||||
Assertions.assertEquals("data_flicker", definitions.get(0).getTableName());
|
||||
Assertions.assertEquals("data_v", definitions.get(definitions.size() - 1).getTableName());
|
||||
Assertions.assertTrue(registry.getDefinition("data_v").getColumns().contains("V_THD"));
|
||||
Assertions.assertEquals(4, registry.getDefinition("data_v").getPhaseCodes().size());
|
||||
Assertions.assertTrue(registry.getDefinition("data_v").getPhaseCodes().contains("T"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package com.njcn.gather.tool.adddata.component;
|
||||
|
||||
import com.njcn.gather.tool.adddata.pojo.bo.AddDataTaskCommand;
|
||||
import com.njcn.gather.tool.adddata.pojo.vo.AddDataTaskStatusVO;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* 补数任务状态持有器测试。
|
||||
*/
|
||||
class AddDataTaskStatusHolderTest {
|
||||
|
||||
private final AddDataTaskStatusHolder holder = new AddDataTaskStatusHolder(new AddDataTimeSlotCalculator());
|
||||
|
||||
@Test
|
||||
void shouldReturnHourlyTimeResultsWhenCreateTask() {
|
||||
AddDataTaskCommand command = new AddDataTaskCommand(
|
||||
Arrays.asList("1"),
|
||||
LocalDateTime.of(2026, 4, 28, 10, 7, 0),
|
||||
LocalDateTime.of(2026, 4, 28, 13, 0, 0),
|
||||
5);
|
||||
|
||||
AddDataTaskStatusVO status = holder.createWaitingTask(command);
|
||||
|
||||
Assertions.assertEquals(Arrays.asList(
|
||||
"2026-04-28 11:00:00",
|
||||
"2026-04-28 12:00:00",
|
||||
"2026-04-28 13:00:00"), status.getHourlyTimeResults());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package com.njcn.gather.tool.adddata.component;
|
||||
|
||||
import com.njcn.gather.tool.adddata.pojo.vo.AddDataTemplateVO;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* 模板注册测试。
|
||||
*/
|
||||
class AddDataTemplateRegistryTest {
|
||||
|
||||
@Test
|
||||
void shouldOnlyExposeABCTPhaseCodes() {
|
||||
AddDataTemplateRegistry registry = new AddDataTemplateRegistry();
|
||||
|
||||
List<AddDataTemplateVO> templates = registry.getTemplates();
|
||||
Set<String> allowedPhaseCodes = new HashSet<String>();
|
||||
allowedPhaseCodes.add("A");
|
||||
allowedPhaseCodes.add("B");
|
||||
allowedPhaseCodes.add("C");
|
||||
allowedPhaseCodes.add("T");
|
||||
|
||||
for (AddDataTemplateVO template : templates) {
|
||||
for (String phaseCode : template.getPhaseCodes()) {
|
||||
Assertions.assertTrue(allowedPhaseCodes.contains(phaseCode));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
package com.njcn.gather.tool.adddata.component;
|
||||
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 时间槽计算测试。
|
||||
*/
|
||||
class AddDataTimeSlotCalculatorTest {
|
||||
|
||||
private final AddDataTimeSlotCalculator calculator = new AddDataTimeSlotCalculator();
|
||||
|
||||
@Test
|
||||
void shouldAlignToNextNaturalSlot() {
|
||||
LocalDateTime start = LocalDateTime.of(2026, 4, 28, 10, 7, 12);
|
||||
LocalDateTime end = LocalDateTime.of(2026, 4, 28, 10, 22, 0);
|
||||
|
||||
List<LocalDateTime> slots = calculator.buildTimeSlots(start, end, 5);
|
||||
|
||||
Assertions.assertEquals(3, slots.size());
|
||||
Assertions.assertEquals(LocalDateTime.of(2026, 4, 28, 10, 10, 0), slots.get(0));
|
||||
Assertions.assertEquals(LocalDateTime.of(2026, 4, 28, 10, 20, 0), slots.get(2));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnEmptyWhenRangeDoesNotContainAnySlot() {
|
||||
LocalDateTime start = LocalDateTime.of(2026, 4, 28, 10, 7, 0);
|
||||
LocalDateTime end = LocalDateTime.of(2026, 4, 28, 10, 9, 59);
|
||||
|
||||
List<LocalDateTime> slots = calculator.buildTimeSlots(start, end, 10);
|
||||
|
||||
Assertions.assertTrue(slots.isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldBuildHourlySlotsFromNextNaturalHour() {
|
||||
LocalDateTime start = LocalDateTime.of(2026, 4, 28, 10, 7, 0);
|
||||
LocalDateTime end = LocalDateTime.of(2026, 4, 28, 13, 0, 0);
|
||||
|
||||
List<LocalDateTime> slots = calculator.buildHourlyTimeSlots(start, end);
|
||||
|
||||
Assertions.assertEquals(3, slots.size());
|
||||
Assertions.assertEquals(LocalDateTime.of(2026, 4, 28, 11, 0, 0), slots.get(0));
|
||||
Assertions.assertEquals(LocalDateTime.of(2026, 4, 28, 13, 0, 0), slots.get(2));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldIncludeStartWhenAlreadyAtNaturalHour() {
|
||||
LocalDateTime start = LocalDateTime.of(2026, 4, 28, 10, 0, 0);
|
||||
LocalDateTime end = LocalDateTime.of(2026, 4, 28, 12, 30, 0);
|
||||
|
||||
List<LocalDateTime> slots = calculator.buildHourlyTimeSlots(start, end);
|
||||
|
||||
Assertions.assertEquals(3, slots.size());
|
||||
Assertions.assertEquals(LocalDateTime.of(2026, 4, 28, 10, 0, 0), slots.get(0));
|
||||
Assertions.assertEquals(LocalDateTime.of(2026, 4, 28, 12, 0, 0), slots.get(2));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package com.njcn.gather.tool.adddata.component;
|
||||
|
||||
import com.njcn.gather.tool.adddata.pojo.bo.AddDataTableDefinition;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 数值生成器测试。
|
||||
*/
|
||||
class AddDataValueGeneratorTest {
|
||||
|
||||
@Test
|
||||
void shouldGenerateStableDataVRowWithExpectedColumnCount() throws Exception {
|
||||
AddDataTableRegistry registry = new AddDataTableRegistry();
|
||||
registry.afterPropertiesSet();
|
||||
AddDataTableDefinition definition = registry.getDefinition("data_v");
|
||||
AddDataValueGenerator generator = new AddDataValueGenerator();
|
||||
|
||||
List<Object> row = generator.generateRow(definition, "f04a9d62e3d24e6580e4f32b40967505", LocalDateTime.of(2026, 4, 28, 10, 10, 0), "A");
|
||||
|
||||
Assertions.assertEquals(definition.getColumns().size(), row.size());
|
||||
Assertions.assertEquals("A", row.get(definition.getColumns().indexOf("PHASIC_TYPE")));
|
||||
Double rms = (Double) row.get(definition.getColumns().indexOf("RMS"));
|
||||
Double rmsMax = (Double) row.get(definition.getColumns().indexOf("RMS_MAX"));
|
||||
Double rmsMin = (Double) row.get(definition.getColumns().indexOf("RMS_MIN"));
|
||||
Assertions.assertTrue(rmsMax >= rms);
|
||||
Assertions.assertTrue(rmsMin <= rms);
|
||||
}
|
||||
}
|
||||
@@ -1,371 +0,0 @@
|
||||
# getIcdMmsJson 标准 API 调试文档
|
||||
|
||||
## 1. 文档范围
|
||||
|
||||
本文档用于说明 `mms-mapping` 模块统一调试接口 `getIcdMmsJson` 的标准调用方式、请求结构、响应规则和联调注意事项。
|
||||
|
||||
本文档内容以当前源码为准,主要对照以下实现:
|
||||
|
||||
- `tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/controller/MappingController.java`
|
||||
- `tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/component/MappingRequestConverter.java`
|
||||
- `tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/component/MappingResponseConverter.java`
|
||||
- `tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/service/impl/MappingTaskServiceImpl.java`
|
||||
- `tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/component/MappingGenerationService.java`
|
||||
- `tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/pojo/param/GenerateMappingFromIcdRequest.java`
|
||||
- `tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/pojo/vo/MappingTaskResponse.java`
|
||||
|
||||
说明:
|
||||
|
||||
- 本文档仅描述接口契约和调试方式,不改动业务代码。
|
||||
- 本次未执行 `mvn` 编译、打包或真实接口联调。
|
||||
- 如文档与运行结果冲突,以源码和实际部署配置为准。
|
||||
|
||||
## 2. 接口基本信息
|
||||
|
||||
| 项 | 说明 |
|
||||
| --- | --- |
|
||||
| 接口名称 | `getIcdMmsJson` |
|
||||
| 请求方法 | `POST` |
|
||||
| 请求路径 | `/api/mms-mapping/get-icd-mms-json` |
|
||||
| Content-Type | `multipart/form-data` |
|
||||
| 控制器入口 | `MappingController#getIcdMmsJson` |
|
||||
| 请求组成 | `icdFile` 文件 Part + `request` JSON Part |
|
||||
| 正常业务响应体 | `MappingTaskResponse` |
|
||||
|
||||
## 3. 接口职责
|
||||
|
||||
该接口是 `mms-mapping` 模块的统一调试入口,串联以下两个阶段:
|
||||
|
||||
1. 上传 ICD 文件并完成解析,生成 `icdDocument` 和 `indexCandidates`
|
||||
2. 根据 `request.indexSelection` 判断是否继续生成正式 `mappingJson`
|
||||
|
||||
接口行为分为三种典型结果:
|
||||
|
||||
1. `request.indexSelection` 未传或为空
|
||||
返回 `NEED_INDEX_SELECTION`,用于引导前端或调试人员先确认标签与 `lnInst` 的绑定关系。
|
||||
2. `request.indexSelection` 已传但校验不通过
|
||||
返回 `NEED_INDEX_SELECTION`,同时通过 `problems` 给出不合法原因,要求重新选择。
|
||||
3. `request.indexSelection` 校验通过
|
||||
返回 `SUCCESS`,输出正式 `mappingJson`,必要时同时落盘并返回 `savedPath`。
|
||||
|
||||
补充说明:
|
||||
|
||||
- 该接口每次都会重新解析上传的 ICD 文件,因此第二次调试仍然必须重新上传 ICD 文件。
|
||||
- 该接口正常进入业务编排后,返回体类型为 `MappingTaskResponse`。
|
||||
- 如果异常发生在控制器参数绑定或请求转换阶段,例如文件为空、Part 缺失、JSON Part 解析失败,则由全局异常处理器统一包装为 `HttpResult<String>`,而不是 `MappingTaskResponse`。
|
||||
|
||||
## 4. 请求规范
|
||||
|
||||
### 4.1 multipart/form-data Part 说明
|
||||
|
||||
| Part 名称 | 类型 | 必填 | 说明 |
|
||||
| --- | --- | --- | --- |
|
||||
| `icdFile` | File | 是 | ICD 文件,不能为空 |
|
||||
| `request` | JSON Part | 是 | 生成参数,必须按 `application/json` 发送 |
|
||||
|
||||
说明:
|
||||
|
||||
- `request` Part 不能省略。即使第一次只想拿候选结果,也必须传一个最小 JSON。
|
||||
- `request.indexSelection` 可以省略或传空数组,此时接口只返回候选结果,不生成正式映射。
|
||||
|
||||
### 4.2 request JSON 结构
|
||||
|
||||
```json
|
||||
{
|
||||
"version": "2026-04-22",
|
||||
"author": "debug-user",
|
||||
"saveToDisk": false,
|
||||
"prettyJson": true,
|
||||
"outputDir": "D:/temp/mms-output",
|
||||
"indexSelection": [
|
||||
{
|
||||
"groupKey": "harm",
|
||||
"groupDesc": "谐波数据",
|
||||
"bindings": [
|
||||
{
|
||||
"reportName": "brcbStHarm",
|
||||
"dataSetName": "dsStHarm",
|
||||
"label": "A相",
|
||||
"lnInst": "1"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 4.3 request 字段说明
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
| --- | --- | --- | --- |
|
||||
| `version` | String | 否 | 输出版本号。未传或空白时,后端按当天日期补齐,格式为 `yyyy-MM-dd` |
|
||||
| `author` | String | 否 | 作者。未传或空白时,回退到配置项 `icd.mapping.default-author`,默认值为 `system` |
|
||||
| `saveToDisk` | boolean | 否 | 是否将生成结果写入磁盘 |
|
||||
| `prettyJson` | boolean | 否 | 是否输出格式化 JSON。`true` 为美化 JSON,`false` 为紧凑 JSON |
|
||||
| `outputDir` | String | 否 | 输出目录。未传或空白时,先回退到配置项 `icd.mapping.default-output-dir`;如果配置也为空,最终落到当前工作目录 |
|
||||
| `indexSelection` | Array | 否 | 标签与 `lnInst` 的最终绑定关系。未传或为空时,只返回候选结果 |
|
||||
|
||||
### 4.4 indexSelection 字段说明
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
| --- | --- | --- | --- |
|
||||
| `groupKey` | String | 是 | 分组唯一键,必须使用第一次响应里返回的原值 |
|
||||
| `groupDesc` | String | 否 | 分组中文描述,便于调试查看 |
|
||||
| `bindings` | Array | 是 | 当前业务分组下最终确认的绑定关系列表 |
|
||||
|
||||
### 4.5 bindings 字段说明
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
| --- | --- | --- | --- |
|
||||
| `reportName` | String | 是 | 绑定发生在哪个报告上,例如 `brcbStHarm` |
|
||||
| `dataSetName` | String | 是 | 绑定发生在哪个数据集上,例如 `dsStHarm` |
|
||||
| `label` | String | 是 | 业务标签,例如 `A相`、`最大值`、`实时数据` |
|
||||
| `lnInst` | String | 是 | 标签最终绑定到的逻辑节点实例值,例如 `1`、`2`、`3` |
|
||||
|
||||
## 5. 标准调试流程
|
||||
|
||||
### 5.1 第一次调试:只获取候选结果
|
||||
|
||||
用途:
|
||||
|
||||
- 上传 ICD 文件
|
||||
- 获取 `icdDocument`
|
||||
- 获取 `indexCandidates`
|
||||
- 确认每个业务分组下可选的 `reportName`、`dataSetName` 和 `availableLnInstValues`
|
||||
|
||||
调用要求:
|
||||
|
||||
- `request` Part 仍然必须传
|
||||
- `request.indexSelection` 可以不传,或传空数组
|
||||
|
||||
预期结果:
|
||||
|
||||
- `status = NEED_INDEX_SELECTION`
|
||||
- 响应中返回 `icdDocument`
|
||||
- 响应中返回 `indexCandidates`
|
||||
|
||||
### 5.2 第二次调试:带索引绑定生成正式结果
|
||||
|
||||
用途:
|
||||
|
||||
- 根据第一次返回的 `indexCandidates` 组装 `request.indexSelection`
|
||||
- 再次上传同一个 ICD 文件
|
||||
- 生成正式 `mappingJson`
|
||||
|
||||
调用要求:
|
||||
|
||||
- 必须继续上传 `icdFile`
|
||||
- `groupKey` 必须沿用第一次返回值
|
||||
- `reportName`、`dataSetName`、`lnInst` 必须与第一次返回的候选结果匹配
|
||||
|
||||
预期结果:
|
||||
|
||||
- `status = SUCCESS`
|
||||
- 响应中返回 `mappingJson`
|
||||
- 当 `saveToDisk = true` 时,响应中额外返回 `savedPath`
|
||||
|
||||
### 5.3 第二次调试但绑定不合法
|
||||
|
||||
适用场景:
|
||||
|
||||
- `groupKey` 与候选结果不匹配
|
||||
- `reportName` 或 `dataSetName` 不在候选集中
|
||||
- `lnInst` 不在 `availableLnInstValues` 内
|
||||
- 绑定关系缺失、不完整或结构错误
|
||||
|
||||
预期结果:
|
||||
|
||||
- `status = NEED_INDEX_SELECTION`
|
||||
- 响应中仍然返回 `icdDocument` 和 `indexCandidates`
|
||||
- `problems` 返回具体问题列表,要求重新确认绑定关系
|
||||
|
||||
## 6. 响应规范
|
||||
|
||||
### 6.1 正常业务响应体
|
||||
|
||||
接口正常进入业务编排后,统一返回 `MappingTaskResponse`。该对象使用了 `@JsonInclude(JsonInclude.Include.NON_EMPTY)`,空字段和空集合不会参与序列化。
|
||||
|
||||
基础字段说明:
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `status` | Enum | 本次处理状态,可能为 `SUCCESS`、`NEED_INDEX_SELECTION`、`FAILED` |
|
||||
| `message` | String | 状态说明或错误提示 |
|
||||
| `icdDocument` | Object | 需要重新选择索引时返回的 ICD 解析结果 |
|
||||
| `mappingJson` | String | 正式生成成功后的映射 JSON 文本 |
|
||||
| `savedPath` | String | 结果已落盘时返回的绝对路径 |
|
||||
| `indexCandidates` | Array | 待绑定状态下返回的索引候选分组 |
|
||||
| `problems` | Array | 模板校验、候选分析或绑定校验问题 |
|
||||
|
||||
字段出现规则:
|
||||
|
||||
| 状态 | 必有字段 | 可能出现字段 |
|
||||
| --- | --- | --- |
|
||||
| `SUCCESS` | `status`、`message`、`mappingJson` | `savedPath`、`problems` |
|
||||
| `NEED_INDEX_SELECTION` | `status`、`message`、`icdDocument`、`indexCandidates` | `problems` |
|
||||
| `FAILED` | `status`、`message` | `problems` |
|
||||
|
||||
### 6.2 NEED_INDEX_SELECTION 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "NEED_INDEX_SELECTION",
|
||||
"message": "索引配置缺失,请根据候选信息完成标签与数字索引的绑定后重新提交",
|
||||
"icdDocument": {
|
||||
"fileName": "demo.icd",
|
||||
"iedName": "IED1",
|
||||
"ldInst": "LD0",
|
||||
"ldPrefix": "LD",
|
||||
"logicalNodes": [
|
||||
{
|
||||
"lnInst": "1"
|
||||
}
|
||||
]
|
||||
},
|
||||
"indexCandidates": [
|
||||
{
|
||||
"groupKey": "harm",
|
||||
"groupDesc": "谐波数据",
|
||||
"reportCount": 1,
|
||||
"templateLabels": [
|
||||
"A相",
|
||||
"B相",
|
||||
"C相"
|
||||
],
|
||||
"reports": [
|
||||
{
|
||||
"reportName": "brcbStHarm",
|
||||
"dataSetName": "dsStHarm",
|
||||
"reportDesc": "谐波报告",
|
||||
"availableLnInstValues": [
|
||||
"1",
|
||||
"2",
|
||||
"3"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
说明:
|
||||
|
||||
- `icdDocument` 实际字段可能比示例更多。
|
||||
- 如果本次是“索引配置不合法”而不是“索引配置缺失”,通常还会返回 `problems`。
|
||||
|
||||
### 6.3 SUCCESS 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "SUCCESS",
|
||||
"message": "映射生成成功",
|
||||
"mappingJson": "{\n \"version\": \"2026-04-22\",\n \"author\": \"debug-user\",\n \"ied\": \"IED1\",\n \"ld\": \"LD\",\n \"instList\": []\n}"
|
||||
}
|
||||
```
|
||||
|
||||
说明:
|
||||
|
||||
- `mappingJson` 是字符串字段,字段值本身是一段 JSON 文本。
|
||||
- 当 `saveToDisk = true` 时,响应中还会额外返回 `savedPath`。
|
||||
|
||||
### 6.4 FAILED 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "FAILED",
|
||||
"message": "映射生成失败:加载 DefaultCfg.txt 失败:默认模板文件不存在:template/DefaultCfg.txt",
|
||||
"problems": [
|
||||
"加载 DefaultCfg.txt 失败:默认模板文件不存在:template/DefaultCfg.txt"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
说明:
|
||||
|
||||
- `FAILED` 主要对应服务编排阶段捕获到的运行异常,例如 ICD 解析、模板加载、映射生成、序列化或落盘失败。
|
||||
- 并非所有错误都会进入 `FAILED`。如果异常发生在控制器参数绑定或请求转换阶段,会走全局异常处理器,而不是这里的业务响应结构。
|
||||
|
||||
## 7. 全局异常响应说明
|
||||
|
||||
以下场景通常不会返回 `MappingTaskResponse`,而是由 `GlobalBusinessExceptionHandler` 统一包装:
|
||||
|
||||
- `icdFile` 缺失或为空
|
||||
- `request` Part 缺失
|
||||
- `request` Part 的 `Content-Type` 不是 `application/json`
|
||||
- `multipart/form-data` 结构不合法
|
||||
- JSON 反序列化失败或框架参数绑定失败
|
||||
|
||||
这类异常最终会包装为统一的 `HttpResult<String>` 响应,具体字段结构以全局公共响应定义为准,本文不展开其完整协议,只强调:
|
||||
|
||||
- 不能把这类错误等同理解为 `MappingTaskResponse.status = FAILED`
|
||||
- 联调时应先区分“业务响应体”与“全局异常包装”
|
||||
|
||||
## 8. 调试示例
|
||||
|
||||
### 8.1 curl 示例:第一次调用,只获取候选结果
|
||||
|
||||
```powershell
|
||||
curl.exe -X POST "http://localhost:8080/api/mms-mapping/get-icd-mms-json" `
|
||||
-H "Accept: application/json" `
|
||||
-F 'icdFile=@D:/data/demo.icd' `
|
||||
-F 'request={"prettyJson":true,"saveToDisk":false};type=application/json'
|
||||
```
|
||||
|
||||
### 8.2 curl 示例:第二次调用,带索引绑定直接生成 MMS JSON
|
||||
|
||||
```powershell
|
||||
curl.exe -X POST "http://localhost:8080/api/mms-mapping/get-icd-mms-json" `
|
||||
-H "Accept: application/json" `
|
||||
-F 'icdFile=@D:/data/demo.icd' `
|
||||
-F 'request={"version":"2026-04-22","author":"debug-user","prettyJson":true,"saveToDisk":false,"indexSelection":[{"groupKey":"harm","groupDesc":"谐波数据","bindings":[{"reportName":"brcbStHarm","dataSetName":"dsStHarm","label":"A相","lnInst":"1"},{"reportName":"brcbStHarm","dataSetName":"dsStHarm","label":"B相","lnInst":"2"},{"reportName":"brcbStHarm","dataSetName":"dsStHarm","label":"C相","lnInst":"3"}]}]};type=application/json'
|
||||
```
|
||||
|
||||
## 9. Postman 调试要点
|
||||
|
||||
1. `Body` 选择 `form-data`
|
||||
2. `icdFile` 类型选择 `File`
|
||||
3. `request` 保持文本输入,但该 Part 的 `Content-Type` 必须显式设置为 `application/json`
|
||||
4. 第一次调试不要省略 `request` Part,只是不传 `indexSelection`
|
||||
5. 第二次调试时必须继续上传 ICD 文件,并严格按第一次返回的候选结果组装绑定关系
|
||||
|
||||
## 10. 常见问题
|
||||
|
||||
### 10.1 为什么第一次调试也必须传 `request`
|
||||
|
||||
因为控制器方法签名使用的是 `@RequestPart("request") GenerateMappingFromIcdRequest request`,该 Part 本身就是必填参数。第一次调试可以只传最小 JSON,但不能完全省略。
|
||||
|
||||
### 10.2 为什么没有传 `indexSelection`,却没有返回 `FAILED`
|
||||
|
||||
这是接口设计的正常行为。`indexSelection` 缺失或为空时,业务语义不是“接口执行失败”,而是“还需要前端继续确认索引绑定”,因此返回的是 `NEED_INDEX_SELECTION`。
|
||||
|
||||
### 10.3 `saveToDisk=true` 但没有传 `outputDir`,结果会保存到哪里
|
||||
|
||||
处理顺序如下:
|
||||
|
||||
1. 先读取请求中的 `outputDir`
|
||||
2. 如果请求空白,则回退到配置项 `icd.mapping.default-output-dir`
|
||||
3. 如果配置项也为空,则最终落到当前工作目录
|
||||
|
||||
### 10.4 `version` 不传时会变成什么
|
||||
|
||||
后端在正式生成映射文档时,会把空白 `version` 自动补成当天日期,格式为 `yyyy-MM-dd`。
|
||||
|
||||
### 10.5 `mappingJson` 为什么是字符串,不是嵌套对象
|
||||
|
||||
因为当前响应结构中 `mappingJson` 定义为 `String`,接口返回的是一段已经序列化好的 JSON 文本,而不是再次展开后的对象结构。
|
||||
|
||||
### 10.6 什么情况下会返回 `problems`
|
||||
|
||||
`problems` 主要用于承载以下问题:
|
||||
|
||||
- 默认模板校验问题
|
||||
- 索引候选分析问题
|
||||
- `indexSelection` 绑定校验问题
|
||||
- 服务编排阶段捕获到的异常原因
|
||||
|
||||
## 11. 当前边界
|
||||
|
||||
- 当前文档仅覆盖 `getIcdMmsJson` 接口,不覆盖 `get-icd` 与 `get-mms-json` 的独立接口文档
|
||||
- 当前文档重点描述业务返回体与调试方式,不展开全局 `HttpResult` 的完整协议
|
||||
- 示例中的 `icdDocument`、`indexCandidates` 和 `mappingJson` 为结构化示意,实际字段数量与内容以运行结果为准
|
||||
@@ -2,6 +2,12 @@
|
||||
|
||||
`mms-mapping` 模块负责解析 ICD 文件并生成 MMS 映射数据。当前统一调试入口为 `getIcdMmsJson`,该接口同时覆盖“先解析 ICD 获取索引候选”和“确认索引后生成正式 MMS JSON”两类场景。
|
||||
|
||||
## 0. 调试文档
|
||||
|
||||
- `getIcdMmsJson`:`API-getIcdMmsJson.md`
|
||||
- `getXmlFromJson`:`API-getXmlFromJson.md`
|
||||
- `buildIndexConfirmData` / `buildIndexSelection`:`API-buildIndexDebug.md`
|
||||
|
||||
## 1. 接口信息
|
||||
|
||||
- 路径:`POST /api/mms-mapping/get-icd-mms-json`
|
||||
|
||||
@@ -0,0 +1,231 @@
|
||||
package com.njcn.gather.icd.mapping.component;
|
||||
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import com.njcn.gather.icd.mapping.pojo.dto.*;
|
||||
import com.njcn.gather.icd.mapping.pojo.bo.*;
|
||||
import com.njcn.gather.icd.mapping.pojo.param.*;
|
||||
import com.njcn.gather.icd.mapping.pojo.vo.*;
|
||||
import com.njcn.gather.icd.mapping.pojo.enums.GenerateStatus;
|
||||
@Setter
|
||||
@Getter
|
||||
@Component
|
||||
public class IcdToXmlMappingService {
|
||||
|
||||
private IndexMappingConfig indexMapping;
|
||||
|
||||
@Data
|
||||
public static class IndexMappingConfig {
|
||||
private StatIndex mmxu;
|
||||
private StatIndex mhai;
|
||||
private StatIndex interHarmonic;
|
||||
private StatIndex msqi;
|
||||
private Integer mflkShort;
|
||||
private Integer mflkLong;
|
||||
private Integer qvvr;
|
||||
|
||||
@Data
|
||||
public static class StatIndex {
|
||||
private Integer average;
|
||||
private Integer maximum;
|
||||
private Integer minimum;
|
||||
private Integer percentile95;
|
||||
}
|
||||
}
|
||||
|
||||
public IndexMappingConfig toIndexMapping(){
|
||||
IndexMappingConfig indexMapping = new IndexMappingConfig();
|
||||
return indexMapping;
|
||||
}
|
||||
|
||||
public void setIndexMapping(IndexMappingConfig config) {
|
||||
this.indexMapping = config;
|
||||
}
|
||||
|
||||
public IndexMappingConfig buildIndexMappingFromSelection(List<IndexSelectionGroupCommand> indexSelectionGroups) {
|
||||
IndexMappingConfig config = new IndexMappingConfig();
|
||||
|
||||
if (indexSelectionGroups == null || indexSelectionGroups.isEmpty()) {
|
||||
return config;
|
||||
}
|
||||
|
||||
for (IndexSelectionGroupCommand group : indexSelectionGroups) {
|
||||
String groupKey = group.getGroupKey();
|
||||
List<IndexBindingCommand> bindings = group.getBindings();
|
||||
|
||||
if (bindings == null || bindings.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ("统计数据__DSSTATISTICDATA".equals(groupKey)) {
|
||||
processStatisticDataGroup(config, bindings);
|
||||
} else if ("波动闪变__DSFLICKERDATA".equals(groupKey)) {
|
||||
processFlickerDataGroup(config, bindings);
|
||||
} else if ("暂态事件__DSEVEQVVR".equals(groupKey)) {
|
||||
processQvvrGroup(config, bindings);
|
||||
}
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
private void processStatisticDataGroup(IndexMappingConfig config, List<IndexBindingCommand> bindings) {
|
||||
for (IndexBindingCommand binding : bindings) {
|
||||
String dataSetName = binding.getDataSetName();
|
||||
String label = binding.getLabel();
|
||||
Integer lnInst = parseLnInst(binding.getLnInst());
|
||||
|
||||
if (lnInst == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ("dsStHarm".equals(dataSetName) ) {
|
||||
if (config.getMhai() == null) {
|
||||
config.setMhai(new IndexMappingConfig.StatIndex());
|
||||
}
|
||||
mapStatIndex(config.getMhai(), label, lnInst);
|
||||
} else if ("dsStIHarm".equals(dataSetName)) {
|
||||
if (config.getInterHarmonic() == null) {
|
||||
config.setInterHarmonic(new IndexMappingConfig.StatIndex());
|
||||
}
|
||||
mapStatIndex(config.getInterHarmonic(), label, lnInst);
|
||||
} else if ("dsStMMXU".equals(dataSetName)) {
|
||||
if (config.getMmxu() == null) {
|
||||
config.setMmxu(new IndexMappingConfig.StatIndex());
|
||||
}
|
||||
mapStatIndex(config.getMmxu(), label, lnInst);
|
||||
}else if ("dsStMSQI".equals(dataSetName)) {
|
||||
if (config.getMsqi() == null) {
|
||||
config.setMsqi(new IndexMappingConfig.StatIndex());
|
||||
}
|
||||
mapStatIndex(config.getMsqi(), label, lnInst);
|
||||
} else if ("dsStatisticData".equals(dataSetName)) {
|
||||
if (label != null && label.contains("间谐波")) {
|
||||
if (config.getInterHarmonic() == null) {
|
||||
config.setInterHarmonic(new IndexMappingConfig.StatIndex());
|
||||
}
|
||||
mapStatIndex(config.getInterHarmonic(), label, lnInst);
|
||||
} else {
|
||||
if (config.getMmxu() == null) {
|
||||
config.setMmxu(new IndexMappingConfig.StatIndex());
|
||||
}
|
||||
mapStatIndex(config.getMmxu(), label, lnInst);
|
||||
if (config.getMhai() == null) {
|
||||
config.setMhai(new IndexMappingConfig.StatIndex());
|
||||
}
|
||||
mapStatIndex(config.getMhai(), label, lnInst);
|
||||
if (config.getMsqi() == null) {
|
||||
config.setMsqi(new IndexMappingConfig.StatIndex());
|
||||
}
|
||||
mapStatIndex(config.getMsqi(), label, lnInst);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void processFlickerDataGroup(IndexMappingConfig config, List<IndexBindingCommand> bindings) {
|
||||
for (IndexBindingCommand binding : bindings) {
|
||||
String dataSetName = binding.getDataSetName();
|
||||
String label = binding.getLabel();
|
||||
Integer lnInst = parseLnInst(binding.getLnInst());
|
||||
|
||||
if (lnInst == null) {
|
||||
continue;
|
||||
}
|
||||
if ("dsPST".equals(dataSetName)) {
|
||||
if (config.getMflkShort() == null) {
|
||||
config.setMflkShort(0);
|
||||
}
|
||||
config.setMflkShort(lnInst);
|
||||
}
|
||||
else if ("dsPLT".equals(dataSetName)) {
|
||||
if (config.getMflkLong() == null) {
|
||||
config.setMflkLong(0);
|
||||
}
|
||||
config.setMflkLong(lnInst);
|
||||
}else {
|
||||
if (config.getMflkShort() == null) {
|
||||
config.setMflkShort(0);
|
||||
}
|
||||
config.setMflkShort(lnInst);
|
||||
if (config.getMflkLong() == null) {
|
||||
config.setMflkLong(0);
|
||||
}
|
||||
config.setMflkLong(lnInst);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void processQvvrGroup(IndexMappingConfig config, List<IndexBindingCommand> bindings) {
|
||||
for (IndexBindingCommand binding : bindings) {
|
||||
String label = binding.getLabel();
|
||||
Integer lnInst = parseLnInst(binding.getLnInst());
|
||||
|
||||
if (lnInst == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (label != null && label.startsWith("电压变动")) {
|
||||
config.setQvvr(lnInst);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void mapStatIndex(IndexMappingConfig.StatIndex statIndex, String label, Integer lnInst) {
|
||||
if (label == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (label) {
|
||||
case "最大值":
|
||||
statIndex.setMaximum(lnInst);
|
||||
break;
|
||||
case "最小值":
|
||||
statIndex.setMinimum(lnInst);
|
||||
break;
|
||||
case "平均值":
|
||||
statIndex.setAverage(lnInst);
|
||||
break;
|
||||
case "95值":
|
||||
statIndex.setPercentile95(lnInst);
|
||||
break;
|
||||
case "间谐波最大值":
|
||||
statIndex.setMaximum(lnInst);
|
||||
break;
|
||||
case "间谐波最小值":
|
||||
statIndex.setMinimum(lnInst);
|
||||
break;
|
||||
case "间谐波平均值":
|
||||
statIndex.setAverage(lnInst);
|
||||
break;
|
||||
case "间谐波95值":
|
||||
statIndex.setPercentile95(lnInst);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private Integer parseLnInst(String lnInstStr) {
|
||||
if (lnInstStr == null || lnInstStr.trim().isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return Integer.parseInt(lnInstStr.trim());
|
||||
} catch (NumberFormatException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package com.njcn.gather.icd.mapping.component;
|
||||
|
||||
import com.njcn.gather.icd.mapping.pojo.dto.IcdToXmlGenerateCommand;
|
||||
import com.njcn.gather.icd.mapping.pojo.dto.IndexBindingCommand;
|
||||
import com.njcn.gather.icd.mapping.pojo.dto.IndexSelectionGroupCommand;
|
||||
import com.njcn.gather.icd.mapping.pojo.param.IcdToXmlGenerateRequest;
|
||||
import com.njcn.gather.icd.mapping.pojo.param.IndexBindingRequest;
|
||||
import com.njcn.gather.icd.mapping.pojo.param.IndexSelectionGroupRequest;
|
||||
import com.njcn.gather.icd.mapping.pojo.param.JsonToXmlRequest;import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@Component
|
||||
public class IcdToXmlRequestConverter {
|
||||
|
||||
public IcdToXmlGenerateCommand toCommand(MultipartFile icdFile, IcdToXmlGenerateRequest request) {
|
||||
IcdToXmlGenerateCommand command = new IcdToXmlGenerateCommand();
|
||||
try {
|
||||
command.setFileBytes(icdFile.getBytes());
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException("读取 ICD 文件失败", e);
|
||||
}
|
||||
command.setFileName(icdFile.getOriginalFilename());
|
||||
command.setVersion(request.getVersion());
|
||||
command.setAuthor(request.getAuthor());
|
||||
command.setSaveToDisk(request.isSaveToDisk());
|
||||
command.setOutputDir(request.getOutputDir());
|
||||
command.setIndexSelection(toIndexSelectionGroupCommands(request.getIndexSelection()));
|
||||
return command;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 JsonToXmlRequest 转换为 IcdToXmlGenerateCommand。
|
||||
* 注意:此方法不处理文件,仅转换索引选择结果。
|
||||
*/
|
||||
public IcdToXmlGenerateCommand toCommand(JsonToXmlRequest request) {
|
||||
IcdToXmlGenerateCommand command = new IcdToXmlGenerateCommand();
|
||||
command.setVersion("1.0");
|
||||
command.setAuthor("");
|
||||
command.setSaveToDisk(false);
|
||||
command.setOutputDir(null);
|
||||
|
||||
|
||||
return command;
|
||||
}
|
||||
|
||||
private List<IndexSelectionGroupCommand> toIndexSelectionGroupCommands(List<IndexSelectionGroupRequest> indexSelection) {
|
||||
List<IndexSelectionGroupCommand> indexSelectionGroupCommands = new ArrayList<>();
|
||||
if (indexSelection != null) {
|
||||
for (IndexSelectionGroupRequest groupRequest : indexSelection) {
|
||||
IndexSelectionGroupCommand groupCommand = new IndexSelectionGroupCommand();
|
||||
groupCommand.setGroupKey(groupRequest.getGroupKey());
|
||||
groupCommand.setGroupDesc(groupRequest.getGroupDesc());
|
||||
groupCommand.setBindings(toIndexBindingCommands(groupRequest.getBindings()));
|
||||
indexSelectionGroupCommands.add(groupCommand);
|
||||
}
|
||||
}
|
||||
return indexSelectionGroupCommands;
|
||||
}
|
||||
|
||||
private List<IndexBindingCommand> toIndexBindingCommands(List<IndexBindingRequest> bindings) {
|
||||
List<IndexBindingCommand> indexBindingCommands = new ArrayList<>();
|
||||
if (bindings != null) {
|
||||
for (IndexBindingRequest bindingRequest : bindings) {
|
||||
IndexBindingCommand bindingCommand = new IndexBindingCommand();
|
||||
bindingCommand.setReportName(bindingRequest.getReportName());
|
||||
bindingCommand.setDataSetName(bindingRequest.getDataSetName());
|
||||
bindingCommand.setLabel(bindingRequest.getLabel());
|
||||
bindingCommand.setLnInst(bindingRequest.getLnInst());
|
||||
indexBindingCommands.add(bindingCommand);
|
||||
}
|
||||
}
|
||||
return indexBindingCommands;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
package com.njcn.gather.icd.mapping.component;
|
||||
|
||||
import com.njcn.gather.icd.mapping.pojo.bo.IcdToXmlGenerateResult;
|
||||
import com.njcn.gather.icd.mapping.pojo.bo.analysis.IndexCandidate;
|
||||
import com.njcn.gather.icd.mapping.pojo.bo.analysis.IndexCandidateReportItem;
|
||||
import com.njcn.gather.icd.mapping.pojo.vo.IcdToXmlResponse;
|
||||
import com.njcn.gather.icd.mapping.pojo.vo.IndexCandidateReportItemResponse;
|
||||
import com.njcn.gather.icd.mapping.pojo.vo.IndexCandidateResponse;
|
||||
import com.njcn.gather.icd.mapping.pojo.vo.MappingDocumentResponse;
|
||||
import com.njcn.gather.icd.mapping.pojo.vo.XmlFileResponse;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class IcdToXmlResponseConverter {
|
||||
|
||||
/** 默认 XML 展示文件名。 */
|
||||
private static final String DEFAULT_XML_FILE_NAME = "mapping.xml";
|
||||
/** XML 内容类型。 */
|
||||
private static final String XML_CONTENT_TYPE = "application/xml";
|
||||
/** XML 文件编码。 */
|
||||
private static final String XML_ENCODING = "UTF-8";
|
||||
|
||||
public IcdToXmlResponse fromResult(IcdToXmlGenerateResult result) {
|
||||
IcdToXmlResponse response = new IcdToXmlResponse();
|
||||
response.setStatus(result.getStatus());
|
||||
response.setMessage(result.getMessage());
|
||||
response.setIedName(result.getIedName());
|
||||
response.setLdInst(result.getLdInst());
|
||||
response.setXmlFile(buildXmlFile(result));
|
||||
response.getProblems().addAll(result.getProblems());
|
||||
|
||||
if (result.getIndexAnalysis() != null && result.getIndexAnalysis().getCandidates() != null) {
|
||||
for (IndexCandidate candidate : result.getIndexAnalysis().getCandidates()) {
|
||||
IndexCandidateResponse candidateResponse = new IndexCandidateResponse();
|
||||
candidateResponse.setGroupKey(candidate.getGroupKey());
|
||||
candidateResponse.setGroupDesc(candidate.getGroupDesc());
|
||||
candidateResponse.setReportCount(candidate.getReportCount());
|
||||
candidateResponse.getTemplateLabels().addAll(candidate.getTemplateLabels());
|
||||
|
||||
if (candidate.getReports() != null) {
|
||||
for (IndexCandidateReportItem item : candidate.getReports()) {
|
||||
IndexCandidateReportItemResponse itemResponse = new IndexCandidateReportItemResponse();
|
||||
itemResponse.setReportName(item.getReportName());
|
||||
itemResponse.setDataSetName(item.getDataSetName());
|
||||
itemResponse.setReportDesc(item.getReportDesc());
|
||||
itemResponse.getAvailableLnInstValues().addAll(item.getAvailableLnInstValues());
|
||||
candidateResponse.getReports().add(itemResponse);
|
||||
}
|
||||
}
|
||||
|
||||
response.getIndexCandidates().add(candidateResponse);
|
||||
}
|
||||
}
|
||||
|
||||
if (result.getMappingDocument() != null) {
|
||||
MappingDocumentResponse doc = new MappingDocumentResponse();
|
||||
doc.setVersion(result.getMappingDocument().getVersion());
|
||||
doc.setAuthor(result.getMappingDocument().getAuthor());
|
||||
doc.setIed(result.getMappingDocument().getIed());
|
||||
doc.setLd(result.getMappingDocument().getLd());
|
||||
doc.setReportCount(result.getMappingDocument().getReportMap() == null
|
||||
? 0 : result.getMappingDocument().getReportMap().size());
|
||||
doc.setDataSetCount(result.getMappingDocument().getDataSetList() == null
|
||||
? 0 : result.getMappingDocument().getDataSetList().size());
|
||||
response.setMappingDocument(doc);
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 XML 内容包装为前端展示用的标准文件容器。
|
||||
*/
|
||||
private XmlFileResponse buildXmlFile(IcdToXmlGenerateResult result) {
|
||||
if (result.getXmlContent() == null || result.getXmlContent().isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
XmlFileResponse xmlFile = new XmlFileResponse();
|
||||
xmlFile.setFileName(resolveXmlFileName(result));
|
||||
xmlFile.setContentType(XML_CONTENT_TYPE);
|
||||
xmlFile.setEncoding(XML_ENCODING);
|
||||
xmlFile.setContent(result.getXmlContent());
|
||||
return xmlFile;
|
||||
}
|
||||
|
||||
/**
|
||||
* 优先使用 IED 名称构造文件名,缺失时回退默认名。
|
||||
*/
|
||||
private String resolveXmlFileName(IcdToXmlGenerateResult result) {
|
||||
String iedName = result.getIedName();
|
||||
if (iedName == null || iedName.trim().isEmpty()) {
|
||||
return DEFAULT_XML_FILE_NAME;
|
||||
}
|
||||
|
||||
String safeName = iedName.replaceAll("[\\\\/:*?\"<>|]+", "_").trim();
|
||||
if (safeName.isEmpty()) {
|
||||
return DEFAULT_XML_FILE_NAME;
|
||||
}
|
||||
return safeName + ".xml";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,464 @@
|
||||
package com.njcn.gather.icd.mapping.component;
|
||||
|
||||
import com.njcn.gather.icd.mapping.pojo.param.BuildIndexSelectionRequest;
|
||||
import com.njcn.gather.icd.mapping.pojo.param.ConfirmedIndexGroupRequest;
|
||||
import com.njcn.gather.icd.mapping.pojo.param.ConfirmedLabelItemRequest;
|
||||
import com.njcn.gather.icd.mapping.pojo.param.IndexCandidateReportItemRequest;
|
||||
import com.njcn.gather.icd.mapping.pojo.param.IndexCandidateRequest;
|
||||
import com.njcn.gather.icd.mapping.pojo.param.IndexConfirmGroupRequest;
|
||||
import com.njcn.gather.icd.mapping.pojo.param.IndexConfirmLabelItemRequest;
|
||||
import com.njcn.gather.icd.mapping.pojo.param.IndexConfirmTargetRequest;
|
||||
import com.njcn.gather.icd.mapping.pojo.vo.IndexBindingResponse;
|
||||
import com.njcn.gather.icd.mapping.pojo.vo.IndexConfirmGroupResponse;
|
||||
import com.njcn.gather.icd.mapping.pojo.vo.IndexConfirmLabelItemResponse;
|
||||
import com.njcn.gather.icd.mapping.pojo.vo.IndexConfirmTargetResponse;
|
||||
import com.njcn.gather.icd.mapping.pojo.vo.IndexSelectionGroupResponse;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* ICD 结构确认弹窗编排服务。
|
||||
* 第一阶段:把 indexCandidates 整理成前端可确认的 label 级模型。
|
||||
* 第二阶段:根据前端确认结果展开为最终 indexSelection。
|
||||
*/
|
||||
@Service
|
||||
public class IndexSelectionBuildService {
|
||||
|
||||
private static final String GROUP_KEY_STATISTIC = "统计数据__DSSTATISTICDATA";
|
||||
private static final String GROUP_KEY_REALTIME = "实时数据__DSREALTIMEDATA";
|
||||
private static final String DATA_SET_STATISTIC_UNGROUPED = "dsStatisticData";
|
||||
private static final String DATA_SET_STATISTIC_INTER = "dsStIHarm";
|
||||
private static final String DATA_SET_REALTIME_UNGROUPED = "dsRealTimeData";
|
||||
private static final String DATA_SET_REALTIME_INTER = "dsRtIHarm";
|
||||
private static final String LABEL_REALTIME_INTER = "间谐波实时数据";
|
||||
|
||||
private static final Comparator<IndexBindingResponse> INDEX_BINDING_COMPARATOR = (left, right) -> {
|
||||
int result = normalizeSortValue(left == null ? null : left.getReportName())
|
||||
.compareTo(normalizeSortValue(right == null ? null : right.getReportName()));
|
||||
if (result != 0) {
|
||||
return result;
|
||||
}
|
||||
result = normalizeSortValue(left == null ? null : left.getDataSetName())
|
||||
.compareTo(normalizeSortValue(right == null ? null : right.getDataSetName()));
|
||||
if (result != 0) {
|
||||
return result;
|
||||
}
|
||||
return compareLnInstValues(left == null ? null : left.getLnInst(), right == null ? null : right.getLnInst());
|
||||
};
|
||||
|
||||
/**
|
||||
* 根据页面回传的 indexCandidates 生成弹窗确认模型。
|
||||
*/
|
||||
public List<IndexConfirmGroupResponse> buildConfirmData(List<IndexCandidateRequest> indexCandidates) {
|
||||
if (indexCandidates == null || indexCandidates.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
List<IndexConfirmGroupResponse> result = new ArrayList<IndexConfirmGroupResponse>();
|
||||
for (IndexCandidateRequest candidate : indexCandidates) {
|
||||
if (candidate == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
IndexConfirmGroupResponse groupResponse = new IndexConfirmGroupResponse();
|
||||
groupResponse.setGroupKey(candidate.getGroupKey());
|
||||
groupResponse.setGroupDesc(candidate.getGroupDesc());
|
||||
|
||||
if (candidate.getTemplateLabels() != null) {
|
||||
Set<String> uniqueLabels = new LinkedHashSet<String>(candidate.getTemplateLabels());
|
||||
for (String label : uniqueLabels) {
|
||||
if (isBlank(label)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
List<IndexCandidateReportItemRequest> targets = resolveTargets(candidate, label);
|
||||
if (targets.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
IndexConfirmLabelItemResponse labelItem = new IndexConfirmLabelItemResponse();
|
||||
labelItem.setLabel(label);
|
||||
labelItem.setRequired(false);
|
||||
labelItem.setConfigurableOnce(true);
|
||||
|
||||
List<List<String>> targetLnInstLists = new ArrayList<List<String>>();
|
||||
for (IndexCandidateReportItemRequest target : targets) {
|
||||
IndexConfirmTargetResponse targetResponse = new IndexConfirmTargetResponse();
|
||||
targetResponse.setReportName(target.getReportName());
|
||||
targetResponse.setDataSetName(target.getDataSetName());
|
||||
targetResponse.setReportDesc(target.getReportDesc());
|
||||
List<String> labelSpecificValues = resolveAvailableLnInstValues(candidate, target, label);
|
||||
targetResponse.getAvailableLnInstValues().addAll(labelSpecificValues);
|
||||
targetLnInstLists.add(labelSpecificValues);
|
||||
labelItem.getTargets().add(targetResponse);
|
||||
}
|
||||
|
||||
List<String> commonLnInstValues = intersectLnInstValues(targetLnInstLists);
|
||||
labelItem.getCommonLnInstValues().addAll(commonLnInstValues);
|
||||
if (commonLnInstValues.size() == 1) {
|
||||
labelItem.setDefaultLnInst(commonLnInstValues.get(0));
|
||||
}
|
||||
if (commonLnInstValues.isEmpty()) {
|
||||
labelItem.setConfigurableOnce(false);
|
||||
}
|
||||
|
||||
groupResponse.getLabelItems().add(labelItem);
|
||||
}
|
||||
}
|
||||
|
||||
result.add(groupResponse);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据确认模型和前端最终确认结果展开生成 indexSelection。
|
||||
*/
|
||||
public List<IndexSelectionGroupResponse> buildIndexSelection(BuildIndexSelectionRequest request) {
|
||||
if (request == null) {
|
||||
throw new IllegalArgumentException("请求体不能为空");
|
||||
}
|
||||
if (request.getConfirmData() == null || request.getConfirmData().isEmpty()) {
|
||||
throw new IllegalArgumentException("confirmData 不能为空");
|
||||
}
|
||||
|
||||
Map<String, IndexConfirmGroupRequest> confirmGroupMap = toConfirmGroupMap(request.getConfirmData());
|
||||
Map<String, ConfirmedIndexGroupRequest> confirmedGroupMap = toConfirmedGroupMap(request.getConfirmedData());
|
||||
validateConfirmedGroups(confirmGroupMap, confirmedGroupMap);
|
||||
|
||||
List<IndexSelectionGroupResponse> result = new ArrayList<IndexSelectionGroupResponse>();
|
||||
for (IndexConfirmGroupRequest confirmGroup : request.getConfirmData()) {
|
||||
if (confirmGroup == null || isBlank(confirmGroup.getGroupKey())) {
|
||||
continue;
|
||||
}
|
||||
|
||||
ConfirmedIndexGroupRequest confirmedGroup = confirmedGroupMap.get(confirmGroup.getGroupKey());
|
||||
if (confirmedGroup == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Map<String, ConfirmedLabelItemRequest> confirmedLabelMap = toConfirmedLabelMap(confirmedGroup.getLabelItems());
|
||||
validateConfirmedLabels(confirmGroup, confirmedLabelMap);
|
||||
IndexSelectionGroupResponse selectionGroup = new IndexSelectionGroupResponse();
|
||||
selectionGroup.setGroupKey(confirmGroup.getGroupKey());
|
||||
selectionGroup.setGroupDesc(confirmGroup.getGroupDesc());
|
||||
|
||||
if (confirmGroup.getLabelItems() != null) {
|
||||
for (IndexConfirmLabelItemRequest confirmLabel : confirmGroup.getLabelItems()) {
|
||||
if (confirmLabel == null || isBlank(confirmLabel.getLabel())) {
|
||||
continue;
|
||||
}
|
||||
|
||||
ConfirmedLabelItemRequest confirmedLabel = confirmedLabelMap.get(confirmLabel.getLabel());
|
||||
if (confirmedLabel == null || !confirmedLabel.isEnabled()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
validateConfirmedLabel(confirmGroupMap, confirmGroup, confirmLabel, confirmedLabel);
|
||||
expandBindings(selectionGroup, confirmLabel, confirmedLabel.getLnInst());
|
||||
}
|
||||
}
|
||||
|
||||
if (!selectionGroup.getBindings().isEmpty()) {
|
||||
// 返回前统一排序,避免前端展示顺序受展开过程影响。
|
||||
selectionGroup.getBindings().sort(INDEX_BINDING_COMPARATOR);
|
||||
result.add(selectionGroup);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private void validateConfirmedGroups(Map<String, IndexConfirmGroupRequest> confirmGroupMap,
|
||||
Map<String, ConfirmedIndexGroupRequest> confirmedGroupMap) {
|
||||
for (String groupKey : confirmedGroupMap.keySet()) {
|
||||
if (!confirmGroupMap.containsKey(groupKey)) {
|
||||
throw new IllegalArgumentException("confirmedData 中存在未知分组:" + groupKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void validateConfirmedLabels(IndexConfirmGroupRequest confirmGroup,
|
||||
Map<String, ConfirmedLabelItemRequest> confirmedLabelMap) {
|
||||
if (confirmedLabelMap.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
Set<String> allowedLabels = new LinkedHashSet<String>();
|
||||
if (confirmGroup.getLabelItems() != null) {
|
||||
for (IndexConfirmLabelItemRequest labelItem : confirmGroup.getLabelItems()) {
|
||||
if (labelItem != null && !isBlank(labelItem.getLabel())) {
|
||||
allowedLabels.add(labelItem.getLabel());
|
||||
}
|
||||
}
|
||||
}
|
||||
for (String label : confirmedLabelMap.keySet()) {
|
||||
if (!allowedLabels.contains(label)) {
|
||||
throw new IllegalArgumentException("分组【" + confirmGroup.getGroupDesc() + "】中存在未知标签:" + label);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Map<String, IndexConfirmGroupRequest> toConfirmGroupMap(List<IndexConfirmGroupRequest> confirmData) {
|
||||
Map<String, IndexConfirmGroupRequest> result = new LinkedHashMap<String, IndexConfirmGroupRequest>();
|
||||
if (confirmData == null) {
|
||||
return result;
|
||||
}
|
||||
for (IndexConfirmGroupRequest group : confirmData) {
|
||||
if (group != null && !isBlank(group.getGroupKey())) {
|
||||
result.put(group.getGroupKey(), group);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private Map<String, ConfirmedIndexGroupRequest> toConfirmedGroupMap(List<ConfirmedIndexGroupRequest> confirmedData) {
|
||||
Map<String, ConfirmedIndexGroupRequest> result = new LinkedHashMap<String, ConfirmedIndexGroupRequest>();
|
||||
if (confirmedData == null) {
|
||||
return result;
|
||||
}
|
||||
for (ConfirmedIndexGroupRequest group : confirmedData) {
|
||||
if (group != null && !isBlank(group.getGroupKey())) {
|
||||
result.put(group.getGroupKey(), group);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private Map<String, ConfirmedLabelItemRequest> toConfirmedLabelMap(List<ConfirmedLabelItemRequest> labelItems) {
|
||||
Map<String, ConfirmedLabelItemRequest> result = new LinkedHashMap<String, ConfirmedLabelItemRequest>();
|
||||
if (labelItems == null) {
|
||||
return result;
|
||||
}
|
||||
for (ConfirmedLabelItemRequest item : labelItems) {
|
||||
if (item != null && !isBlank(item.getLabel())) {
|
||||
result.put(item.getLabel(), item);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 统计数据、实时数据存在“同一 label 只配一次”的特殊归并规则,
|
||||
* 其他分组默认把同名 label 展开到当前分组下全部报告。
|
||||
*/
|
||||
private List<IndexCandidateReportItemRequest> resolveTargets(IndexCandidateRequest candidate, String label) {
|
||||
List<IndexCandidateReportItemRequest> reports = candidate.getReports();
|
||||
if (reports == null || reports.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
boolean hasRealtimeInterReport = hasDataSet(reports, DATA_SET_REALTIME_INTER);
|
||||
List<IndexCandidateReportItemRequest> result = new ArrayList<IndexCandidateReportItemRequest>();
|
||||
for (IndexCandidateReportItemRequest report : reports) {
|
||||
if (report == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (GROUP_KEY_STATISTIC.equals(candidate.getGroupKey())) {
|
||||
if (isInterHarmonicLabel(label)) {
|
||||
if (DATA_SET_STATISTIC_INTER.equals(report.getDataSetName()) || reports.size() == 1) {
|
||||
result.add(report);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (!DATA_SET_STATISTIC_INTER.equals(report.getDataSetName())) {
|
||||
result.add(report);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (GROUP_KEY_REALTIME.equals(candidate.getGroupKey())) {
|
||||
if (LABEL_REALTIME_INTER.equals(label)) {
|
||||
if (DATA_SET_REALTIME_INTER.equals(report.getDataSetName())) {
|
||||
result.add(report);
|
||||
} else if (!hasRealtimeInterReport && DATA_SET_REALTIME_UNGROUPED.equals(report.getDataSetName())) {
|
||||
result.add(report);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (!DATA_SET_REALTIME_INTER.equals(report.getDataSetName())) {
|
||||
result.add(report);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
result.add(report);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private boolean hasDataSet(List<IndexCandidateReportItemRequest> reports, String dataSetName) {
|
||||
if (reports == null || reports.isEmpty() || isBlank(dataSetName)) {
|
||||
return false;
|
||||
}
|
||||
for (IndexCandidateReportItemRequest report : reports) {
|
||||
if (report != null && dataSetName.equals(report.getDataSetName())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算多个目标报告可共同接受的 lnInst 候选,保持首个目标报告的顺序。
|
||||
*/
|
||||
private List<String> intersectLnInstValues(List<List<String>> targetLnInstLists) {
|
||||
if (targetLnInstLists == null || targetLnInstLists.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
List<String> firstValues = targetLnInstLists.get(0);
|
||||
if (firstValues == null || firstValues.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
List<String> result = new ArrayList<String>();
|
||||
for (String value : firstValues) {
|
||||
if (isBlank(value)) {
|
||||
continue;
|
||||
}
|
||||
boolean existsInAllTargets = true;
|
||||
for (int i = 1; i < targetLnInstLists.size(); i++) {
|
||||
List<String> currentValues = targetLnInstLists.get(i);
|
||||
if (currentValues == null || !currentValues.contains(value)) {
|
||||
existsInAllTargets = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (existsInAllTargets) {
|
||||
result.add(value);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 某些未分组报告会把多种 label 的候选值拼在同一个 availableLnInstValues 里,
|
||||
* 这里按当前业务规则先拆开后再返回给前端。
|
||||
*/
|
||||
private List<String> resolveAvailableLnInstValues(IndexCandidateRequest candidate,
|
||||
IndexCandidateReportItemRequest target,
|
||||
String label) {
|
||||
if (target == null || target.getAvailableLnInstValues() == null) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
List<String> values = target.getAvailableLnInstValues();
|
||||
if (GROUP_KEY_STATISTIC.equals(candidate.getGroupKey())
|
||||
&& DATA_SET_STATISTIC_UNGROUPED.equals(target.getDataSetName())
|
||||
&& values.size() > 1) {
|
||||
int splitIndex = values.size() / 2;
|
||||
return isInterHarmonicLabel(label)
|
||||
? new ArrayList<String>(values.subList(splitIndex, values.size()))
|
||||
: new ArrayList<String>(values.subList(0, splitIndex));
|
||||
}
|
||||
|
||||
if (GROUP_KEY_REALTIME.equals(candidate.getGroupKey())
|
||||
&& DATA_SET_REALTIME_UNGROUPED.equals(target.getDataSetName())
|
||||
&& values.size() > 1) {
|
||||
return LABEL_REALTIME_INTER.equals(label)
|
||||
? new ArrayList<String>(values.subList(1, values.size()))
|
||||
: new ArrayList<String>(values.subList(0, 1));
|
||||
}
|
||||
|
||||
return new ArrayList<String>(values);
|
||||
}
|
||||
|
||||
private void validateConfirmedLabel(Map<String, IndexConfirmGroupRequest> confirmGroupMap,
|
||||
IndexConfirmGroupRequest confirmGroup,
|
||||
IndexConfirmLabelItemRequest confirmLabel,
|
||||
ConfirmedLabelItemRequest confirmedLabel) {
|
||||
if (!confirmGroupMap.containsKey(confirmGroup.getGroupKey())) {
|
||||
throw new IllegalArgumentException("未找到确认分组:" + confirmGroup.getGroupKey());
|
||||
}
|
||||
if (isBlank(confirmedLabel.getLnInst())) {
|
||||
throw new IllegalArgumentException("分组【" + confirmGroup.getGroupDesc() + "】的标签【"
|
||||
+ confirmLabel.getLabel() + "】未选择 lnInst");
|
||||
}
|
||||
if (confirmLabel.getCommonLnInstValues() == null || !confirmLabel.getCommonLnInstValues().contains(confirmedLabel.getLnInst())) {
|
||||
throw new IllegalArgumentException(
|
||||
"分组【" + confirmGroup.getGroupDesc() + "】的标签【" + confirmLabel.getLabel()
|
||||
+ "】选择的 lnInst【" + confirmedLabel.getLnInst() + "】不在共同候选范围内:"
|
||||
+ confirmLabel.getCommonLnInstValues()
|
||||
);
|
||||
}
|
||||
if (confirmLabel.getTargets() != null) {
|
||||
for (IndexConfirmTargetRequest target : confirmLabel.getTargets()) {
|
||||
if (target == null || target.getAvailableLnInstValues() == null) {
|
||||
continue;
|
||||
}
|
||||
if (!target.getAvailableLnInstValues().contains(confirmedLabel.getLnInst())) {
|
||||
throw new IllegalArgumentException(
|
||||
"标签【" + confirmLabel.getLabel() + "】在目标报告【" + target.getReportName()
|
||||
+ "】下不支持 lnInst【" + confirmedLabel.getLnInst() + "】"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void expandBindings(IndexSelectionGroupResponse selectionGroup,
|
||||
IndexConfirmLabelItemRequest confirmLabel,
|
||||
String lnInst) {
|
||||
if (confirmLabel.getTargets() == null) {
|
||||
return;
|
||||
}
|
||||
for (IndexConfirmTargetRequest target : confirmLabel.getTargets()) {
|
||||
if (target == null) {
|
||||
continue;
|
||||
}
|
||||
IndexBindingResponse binding = new IndexBindingResponse();
|
||||
binding.setReportName(target.getReportName());
|
||||
binding.setDataSetName(target.getDataSetName());
|
||||
binding.setLabel(confirmLabel.getLabel());
|
||||
binding.setLnInst(lnInst);
|
||||
selectionGroup.getBindings().add(binding);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isInterHarmonicLabel(String label) {
|
||||
return label != null && label.startsWith("间谐波");
|
||||
}
|
||||
|
||||
private static String normalizeSortValue(String value) {
|
||||
return value == null ? "" : value;
|
||||
}
|
||||
|
||||
private static int compareLnInstValues(String left, String right) {
|
||||
Long leftNumeric = parseNumericLnInst(left);
|
||||
Long rightNumeric = parseNumericLnInst(right);
|
||||
if (leftNumeric != null && rightNumeric != null) {
|
||||
int result = leftNumeric.compareTo(rightNumeric);
|
||||
if (result != 0) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
return normalizeSortValue(left).compareTo(normalizeSortValue(right));
|
||||
}
|
||||
|
||||
private static Long parseNumericLnInst(String value) {
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
String trimmedValue = value.trim();
|
||||
if (trimmedValue.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return Long.valueOf(trimmedValue);
|
||||
} catch (NumberFormatException ex) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isBlank(String value) {
|
||||
return value == null || value.trim().isEmpty();
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -38,6 +38,7 @@ public class MappingResponseConverter {
|
||||
MappingTaskResponse response = initBaseResponse(result);
|
||||
if (result.getStatus() == GenerateStatus.SUCCESS) {
|
||||
response.setMappingJson(result.getMappingJson());
|
||||
response.setMappingDocument(result.getMappingDocument());
|
||||
response.setSavedPath(result.getSavedPath());
|
||||
return response;
|
||||
}
|
||||
@@ -82,4 +83,4 @@ public class MappingResponseConverter {
|
||||
response.getIndexCandidates().add(candidateResponse);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,852 @@
|
||||
package com.njcn.gather.icd.mapping.component;
|
||||
|
||||
|
||||
import com.njcn.gather.icd.mapping.pojo.bo.icd.*;
|
||||
import com.njcn.gather.icd.mapping.utils.XmlTemplateParser;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.io.*;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.*;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import com.njcn.gather.icd.mapping.pojo.dto.*;
|
||||
import com.njcn.gather.icd.mapping.pojo.bo.*;
|
||||
import com.njcn.gather.icd.mapping.pojo.param.*;
|
||||
import com.njcn.gather.icd.mapping.pojo.vo.*;
|
||||
import com.njcn.gather.icd.mapping.pojo.enums.GenerateStatus;
|
||||
@Component
|
||||
public class RuleBasedXmlMappingService {
|
||||
|
||||
@Autowired
|
||||
private IcdParserService icdParserService;
|
||||
|
||||
private static final Pattern VALUE_RULE_PATTERN = Pattern.compile(
|
||||
"<Value\\s+" +
|
||||
"name=\"([^\"]+)\"\\s+" +
|
||||
"desc=\"([^\"]+)\"\\s+" +
|
||||
"type=\"([^\"]+)\"\\s+" +
|
||||
"(?:DO=\"([^\"]*)\")?\\s*" +
|
||||
"(?:DA=\"([^\"]*)\")?\\s*" +
|
||||
"(?:BaseFlag=\"([^\"]*)\")?\\s*" +
|
||||
"(?:LimitUp=\"([^\"]*)\")?\\s*" +
|
||||
"(?:LimitDown=\"([^\"]*)\")?\\s*" +
|
||||
"(?:Coefficient=\"([^\"]*)\")?\\s*" +
|
||||
"/>", Pattern.CASE_INSENSITIVE
|
||||
);
|
||||
|
||||
private static final Pattern XML_VALUE_PATTERN = Pattern.compile(
|
||||
"<Value\\s+" +
|
||||
"name=\"([^\"]+)\"\\s+" +
|
||||
"desc=\"([^\"]+)\"\\s+" +
|
||||
"type=\"([^\"]+)\"\\s+" +
|
||||
"(?:DO=\"([^\"]*)\")?\\s*" +
|
||||
"(?:DA=\"([^\"]*)\")?\\s*" +
|
||||
"(?:BaseFlag=\"([^\"]*)\")?\\s*" +
|
||||
"(?:LimitUp=\"([^\"]*)\")?\\s*" +
|
||||
"(?:LimitDown=\"([^\"]*)\")?\\s*" +
|
||||
"(?:Coefficient=\"([^\"]*)\")?\\s*" +
|
||||
"/>", Pattern.CASE_INSENSITIVE
|
||||
);
|
||||
|
||||
private static final Pattern DO_INDEX_PATTERN = Pattern.compile("(MMXU|MHAI|MSQI|MFLK|QVVR)(\\d+)");
|
||||
private static final Pattern DO_NO_INDEX_PATTERN = Pattern.compile("(MMXU|MHAI|MSQI|MFLK|QVVR)\\$");
|
||||
|
||||
public InputStream loadDefaultXmlFile( ) throws Exception {
|
||||
ClassPathResource templateResource = new ClassPathResource("template/JiangSu_Config2.xml");
|
||||
|
||||
if (!templateResource.exists()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
return templateResource.getInputStream();
|
||||
}
|
||||
|
||||
public List<InputStream> loadDefaultRuleFile( ) throws Exception {
|
||||
|
||||
ClassPathResource ruleResource = new ClassPathResource("template/默认规则.txt");
|
||||
if ( !ruleResource.exists()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
InputStream ruleStream = ruleResource.getInputStream();
|
||||
List<InputStream> ruleStreams = new ArrayList<>();
|
||||
ruleStreams.add(ruleStream);
|
||||
return ruleStreams;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
public String generateWithRuleFiles(IcdDocument icdDocument,
|
||||
InputStream templateStream,
|
||||
List<InputStream> ruleStreams,
|
||||
IcdToXmlMappingService.IndexMappingConfig indexMapping) throws Exception {
|
||||
|
||||
Map<String, List<ValueRule>> mergedRules = mergeAllRules(ruleStreams, indexMapping);
|
||||
|
||||
Map<String, IcdMetricInfo> icdMetrics = extractIcdMetrics(icdDocument);
|
||||
|
||||
Map<String, ValueRule> applicableRules = findApplicableRules(mergedRules, icdMetrics);
|
||||
|
||||
XmlTemplateParser.ParseResult parseResult = new XmlTemplateParser().parse(templateStream);
|
||||
String xmlContent = parseResult.getTemplateContent();
|
||||
|
||||
xmlContent = fillIedInfo(xmlContent, icdDocument);
|
||||
|
||||
xmlContent = fillReportControls(xmlContent, icdDocument);
|
||||
|
||||
xmlContent = applyRulesToXml(xmlContent, applicableRules);
|
||||
|
||||
Path tempPath = Files.createTempFile("rule_mapped_", ".xml");
|
||||
Files.write(tempPath, xmlContent.getBytes(StandardCharsets.UTF_8));
|
||||
return tempPath.toString();
|
||||
}
|
||||
|
||||
|
||||
private Map<String, List<ValueRule>> mergeAllRules(List<InputStream> ruleStreams,
|
||||
IcdToXmlMappingService.IndexMappingConfig indexMapping) throws Exception {
|
||||
Map<String, List<ValueRule>> mergedRules = new LinkedHashMap<>();
|
||||
|
||||
for (InputStream ruleStream : ruleStreams) {
|
||||
Map<String, List<ValueRule>> rules = parseRuleFile(ruleStream, indexMapping);
|
||||
for (Map.Entry<String, List<ValueRule>> entry : rules.entrySet()) {
|
||||
String key = entry.getKey();
|
||||
List<ValueRule> ruleList = entry.getValue();
|
||||
mergedRules.computeIfAbsent(key, k -> new ArrayList<>()).addAll(ruleList);
|
||||
}
|
||||
}
|
||||
|
||||
return mergedRules;
|
||||
}
|
||||
|
||||
|
||||
private Map<String, List<ValueRule>> parseRuleFile(InputStream ruleStream,
|
||||
IcdToXmlMappingService.IndexMappingConfig indexMapping) throws Exception {
|
||||
Map<String, List<ValueRule>> rules = new HashMap<>();
|
||||
|
||||
BufferedReader reader = new BufferedReader(new InputStreamReader(ruleStream, StandardCharsets.UTF_8));
|
||||
String line;
|
||||
String currentGroup = "";
|
||||
|
||||
while ((line = reader.readLine()) != null) {
|
||||
line = line.trim();
|
||||
|
||||
if (line.startsWith("<!--")) {
|
||||
if (line.contains("MMXU") || line.contains("基本数据")) {
|
||||
currentGroup = "MMXU";
|
||||
} else if (line.contains("间谐波")) {
|
||||
currentGroup = "INTER_HARMONIC";
|
||||
} else if (line.contains("MHAI") && line.contains("谐波数据")) {
|
||||
currentGroup = "MHAI";
|
||||
} else if (line.contains("MSQI") || line.contains("序分量")) {
|
||||
currentGroup = "MSQI";
|
||||
} else if (line.contains("短闪") || line.contains("短时闪变")) {
|
||||
currentGroup = "MFLK_SHORT";
|
||||
} else if (line.contains("长闪") || line.contains("长时闪变")) {
|
||||
currentGroup = "MFLK_LONG";
|
||||
} else if (line.contains("QVVR") || line.contains("电压变动")) {
|
||||
currentGroup = "QVVR";
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (line.isEmpty() || !line.startsWith("<Value")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Matcher matcher = VALUE_RULE_PATTERN.matcher(line);
|
||||
if (matcher.find()) {
|
||||
ValueRule rule = new ValueRule();
|
||||
rule.setName(matcher.group(1));
|
||||
rule.setDesc(matcher.group(2));
|
||||
rule.setType(matcher.group(3));
|
||||
String doPath = matcher.group(4) != null ? matcher.group(4) : "";
|
||||
String daPath = matcher.group(5) != null ? matcher.group(5) : "";
|
||||
|
||||
if (indexMapping != null && !doPath.isEmpty()) {
|
||||
doPath = applyIndexMapping(doPath, currentGroup, indexMapping, rule.getName(), rule.getDesc());
|
||||
}
|
||||
|
||||
rule.setDoPath(doPath);
|
||||
rule.setDaPath(daPath);
|
||||
rule.setBaseFlag(matcher.group(6));
|
||||
rule.setLimitUp(matcher.group(7));
|
||||
rule.setLimitDown(matcher.group(8));
|
||||
rule.setCoefficient(matcher.group(9));
|
||||
|
||||
String key = buildRuleKey(rule.getName(), rule.getDesc());
|
||||
if (!key.isEmpty() && hasValidDoOrDa(rule)) {
|
||||
rules.computeIfAbsent(key.toLowerCase(), k -> new ArrayList<>()).add(rule);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
reader.close();
|
||||
return rules;
|
||||
}
|
||||
|
||||
private String applyIndexMapping(String doPath, String currentGroup,
|
||||
IcdToXmlMappingService.IndexMappingConfig indexMapping,
|
||||
String name, String desc) {
|
||||
if (indexMapping == null) {
|
||||
return doPath;
|
||||
}
|
||||
|
||||
Matcher matcherWithIndex = DO_INDEX_PATTERN.matcher(doPath);
|
||||
if (matcherWithIndex.find()) {
|
||||
String lnClass = matcherWithIndex.group(1);
|
||||
int originalIndex = Integer.parseInt(matcherWithIndex.group(2));
|
||||
|
||||
int newIndex = determineNewIndex(currentGroup, lnClass, originalIndex, indexMapping, name, desc);
|
||||
if(name.contains("fluc")){
|
||||
System.out.println("fluc");
|
||||
}
|
||||
if (newIndex >= 0) {
|
||||
return matcherWithIndex.replaceAll(lnClass + newIndex);
|
||||
}
|
||||
|
||||
return doPath;
|
||||
}
|
||||
|
||||
Matcher matcherNoIndex = DO_NO_INDEX_PATTERN.matcher(doPath);
|
||||
if (matcherNoIndex.find()) {
|
||||
String lnClass = matcherNoIndex.group(1);
|
||||
|
||||
int newIndex = determineNewIndex(currentGroup, lnClass,0, indexMapping, name, desc);
|
||||
|
||||
if (newIndex >= 0) {
|
||||
return matcherNoIndex.replaceAll(lnClass + newIndex + "\\$");
|
||||
}
|
||||
}
|
||||
|
||||
return doPath;
|
||||
}
|
||||
|
||||
private int determineNewIndexByName(String currentGroup, String lnClass,
|
||||
IcdToXmlMappingService.IndexMappingConfig indexMapping,
|
||||
String name, String desc) {
|
||||
if (indexMapping == null || name == null) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
String upperName = name.toUpperCase();
|
||||
String lowerDesc = (desc != null ? desc : "").toLowerCase();
|
||||
|
||||
boolean isMaximum = upperName.startsWith("MAX_");
|
||||
boolean isMinimum = upperName.startsWith("MIN_");
|
||||
boolean is95Percentile = upperName.startsWith("G_");
|
||||
boolean isAverage = !isMaximum && !isMinimum && !is95Percentile;
|
||||
|
||||
// 判断是否为间谐波指标:通过desc是否包含"间谐波"
|
||||
boolean isInterHarmonic = lowerDesc.contains("间谐波");
|
||||
|
||||
if ("MMXU".equals(currentGroup) || "MMXU".equals(lnClass)) {
|
||||
IcdToXmlMappingService.IndexMappingConfig.StatIndex mmxu = indexMapping.getMmxu();
|
||||
if (mmxu != null) {
|
||||
if (isAverage && mmxu.getAverage() != null) return mmxu.getAverage();
|
||||
if (isMaximum && mmxu.getMaximum() != null) return mmxu.getMaximum();
|
||||
if (isMinimum && mmxu.getMinimum() != null) return mmxu.getMinimum();
|
||||
if (is95Percentile && mmxu.getPercentile95() != null) return mmxu.getPercentile95();
|
||||
}
|
||||
}
|
||||
|
||||
if ("INTER_HARMONIC".equals(currentGroup) || isInterHarmonic) {
|
||||
IcdToXmlMappingService.IndexMappingConfig.StatIndex interHarmonic = indexMapping.getInterHarmonic();
|
||||
if (interHarmonic != null) {
|
||||
if (isAverage && interHarmonic.getAverage() != null) return interHarmonic.getAverage();
|
||||
if (isMaximum && interHarmonic.getMaximum() != null) return interHarmonic.getMaximum();
|
||||
if (isMinimum && interHarmonic.getMinimum() != null) return interHarmonic.getMinimum();
|
||||
if (is95Percentile && interHarmonic.getPercentile95() != null) return interHarmonic.getPercentile95();
|
||||
}
|
||||
}
|
||||
|
||||
if ("MHAI".equals(currentGroup) && !isInterHarmonic) {
|
||||
IcdToXmlMappingService.IndexMappingConfig.StatIndex mhai = indexMapping.getMhai();
|
||||
if (mhai != null) {
|
||||
if (isAverage && mhai.getAverage() != null) return mhai.getAverage();
|
||||
if (isMaximum && mhai.getMaximum() != null) return mhai.getMaximum();
|
||||
if (isMinimum && mhai.getMinimum() != null) return mhai.getMinimum();
|
||||
if (is95Percentile && mhai.getPercentile95() != null) return mhai.getPercentile95();
|
||||
}
|
||||
}
|
||||
|
||||
if ("MSQI".equals(currentGroup) || "MSQI".equals(lnClass)) {
|
||||
IcdToXmlMappingService.IndexMappingConfig.StatIndex msqi = indexMapping.getMsqi();
|
||||
if (msqi != null) {
|
||||
if (isAverage && msqi.getAverage() != null) return msqi.getAverage();
|
||||
if (isMaximum && msqi.getMaximum() != null) return msqi.getMaximum();
|
||||
if (isMinimum && msqi.getMinimum() != null) return msqi.getMinimum();
|
||||
if (is95Percentile && msqi.getPercentile95() != null) return msqi.getPercentile95();
|
||||
}
|
||||
}
|
||||
|
||||
if ("MFLK_SHORT".equals(currentGroup) || "MFLK".equals(lnClass)) {
|
||||
if (indexMapping.getMflkShort() != null) {
|
||||
return indexMapping.getMflkShort();
|
||||
}
|
||||
}
|
||||
|
||||
if ("MFLK_LONG".equals(currentGroup)) {
|
||||
if (indexMapping.getMflkLong() != null) {
|
||||
return indexMapping.getMflkLong();
|
||||
}
|
||||
}
|
||||
|
||||
if ("QVVR".equals(currentGroup) || "QVVR".equals(lnClass)) {
|
||||
if (indexMapping.getQvvr() != null) {
|
||||
return indexMapping.getQvvr();
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
private int determineNewIndex(String currentGroup, String lnClass, int originalIndex,
|
||||
IcdToXmlMappingService.IndexMappingConfig indexMapping,
|
||||
String name, String desc) {
|
||||
if (indexMapping == null) {
|
||||
return originalIndex;
|
||||
}
|
||||
|
||||
String lowerName = (name != null ? name : "").toLowerCase();
|
||||
String lowerDesc = (desc != null ? desc : "").toLowerCase();
|
||||
|
||||
boolean isAverage = lowerName.contains("avg") || lowerDesc.contains("平均值");
|
||||
boolean isMaximum = lowerName.startsWith("max_") || lowerDesc.contains("最大值");
|
||||
boolean isMinimum = lowerName.startsWith("min_") || lowerDesc.contains("最小值");
|
||||
boolean is95Percentile = lowerName.startsWith("g_") || lowerDesc.contains("95值");
|
||||
|
||||
// 判断是否为间谐波指标
|
||||
boolean isInterHarmonic = lowerDesc.contains("间谐波");
|
||||
|
||||
if ("MMXU".equals(currentGroup) || "MMXU".equals(lnClass)) {
|
||||
IcdToXmlMappingService.IndexMappingConfig.StatIndex mmxu = indexMapping.getMmxu();
|
||||
if (mmxu != null) {
|
||||
if (isAverage && mmxu.getAverage() != null) return mmxu.getAverage();
|
||||
if (isMaximum && mmxu.getMaximum() != null) return mmxu.getMaximum();
|
||||
if (isMinimum && mmxu.getMinimum() != null) return mmxu.getMinimum();
|
||||
if (is95Percentile && mmxu.getPercentile95() != null) return mmxu.getPercentile95();
|
||||
}
|
||||
}
|
||||
|
||||
if ("INTER_HARMONIC".equals(currentGroup) || isInterHarmonic) {
|
||||
IcdToXmlMappingService.IndexMappingConfig.StatIndex interHarmonic = indexMapping.getInterHarmonic();
|
||||
if (interHarmonic != null) {
|
||||
if (isAverage && interHarmonic.getAverage() != null) return interHarmonic.getAverage();
|
||||
if (isMaximum && interHarmonic.getMaximum() != null) return interHarmonic.getMaximum();
|
||||
if (isMinimum && interHarmonic.getMinimum() != null) return interHarmonic.getMinimum();
|
||||
if (is95Percentile && interHarmonic.getPercentile95() != null) return interHarmonic.getPercentile95();
|
||||
}
|
||||
}
|
||||
|
||||
if ("MHAI".equals(currentGroup) && !isInterHarmonic) {
|
||||
IcdToXmlMappingService.IndexMappingConfig.StatIndex mhai = indexMapping.getMhai();
|
||||
if (mhai != null) {
|
||||
if (isAverage && mhai.getAverage() != null) return mhai.getAverage();
|
||||
if (isMaximum && mhai.getMaximum() != null) return mhai.getMaximum();
|
||||
if (isMinimum && mhai.getMinimum() != null) return mhai.getMinimum();
|
||||
if (is95Percentile && mhai.getPercentile95() != null) return mhai.getPercentile95();
|
||||
}
|
||||
}
|
||||
|
||||
if ("MSQI".equals(currentGroup) || "MSQI".equals(lnClass)) {
|
||||
IcdToXmlMappingService.IndexMappingConfig.StatIndex msqi = indexMapping.getMsqi();
|
||||
if (msqi != null) {
|
||||
if (isAverage && msqi.getAverage() != null) return msqi.getAverage();
|
||||
if (isMaximum && msqi.getMaximum() != null) return msqi.getMaximum();
|
||||
if (isMinimum && msqi.getMinimum() != null) return msqi.getMinimum();
|
||||
if (is95Percentile && msqi.getPercentile95() != null) return msqi.getPercentile95();
|
||||
}
|
||||
}
|
||||
|
||||
if ("MFLK_SHORT".equals(currentGroup)) {
|
||||
if (indexMapping.getMflkShort() != null) {
|
||||
return indexMapping.getMflkShort();
|
||||
}
|
||||
}
|
||||
|
||||
if ("MFLK_LONG".equals(currentGroup)) {
|
||||
if (indexMapping.getMflkLong() != null) {
|
||||
return indexMapping.getMflkLong();
|
||||
}
|
||||
}
|
||||
|
||||
if ("QVVR".equals(currentGroup) || "QVVR".equals(lnClass)) {
|
||||
if (indexMapping.getQvvr() != null) {
|
||||
return indexMapping.getQvvr();
|
||||
}
|
||||
}
|
||||
|
||||
return originalIndex;
|
||||
}
|
||||
|
||||
private Map<String, IcdMetricInfo> extractIcdMetrics(IcdDocument icdDocument) {
|
||||
Map<String, IcdMetricInfo> metrics = new HashMap<>();
|
||||
|
||||
if (icdDocument == null || icdDocument.getLogicalNodes() == null) {
|
||||
return metrics;
|
||||
}
|
||||
|
||||
for (LnNode lnNode : icdDocument.getLogicalNodes()) {
|
||||
if (lnNode.getDoiList() == null) continue;
|
||||
|
||||
for (DoiNode doiNode : lnNode.getDoiList()) {
|
||||
String doPath = buildDoPath(lnNode, doiNode);
|
||||
|
||||
List<String> daPaths = collectAllDaPaths(doiNode.getChildren(), new ArrayList<>());
|
||||
for (String daPath : daPaths) {
|
||||
String fullKey = buildMetricKey(doPath, daPath);
|
||||
|
||||
IcdMetricInfo info = new IcdMetricInfo();
|
||||
info.setLnClass(lnNode.getLnClass());
|
||||
info.setLnInst(lnNode.getLnInst());
|
||||
info.setDoName(doiNode.getName());
|
||||
info.setDoPath(doPath);
|
||||
info.setDaPath(daPath);
|
||||
info.setPhase(extractPhase(daPath));
|
||||
info.setType(extractType(daPath));
|
||||
|
||||
metrics.put(fullKey.toLowerCase(), info);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return metrics;
|
||||
}
|
||||
|
||||
private List<String> collectAllDaPaths(List<DoiElementNode> nodes, List<String> currentPath) {
|
||||
List<String> results = new ArrayList<>();
|
||||
if (nodes == null) return results;
|
||||
|
||||
for (DoiElementNode node : nodes) {
|
||||
List<String> newPath = new ArrayList<>(currentPath);
|
||||
|
||||
if ("SDI".equalsIgnoreCase(node.getKind())) {
|
||||
if (node.getName() != null && !node.getName().isEmpty()) {
|
||||
String name = node.getName();
|
||||
if (name.startsWith("phs")) {
|
||||
if (name.endsWith("Har")) {
|
||||
newPath.add("phs*Har");
|
||||
} else {
|
||||
newPath.add("phs*");
|
||||
}
|
||||
}
|
||||
else {
|
||||
newPath.add(name);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (node.getChildren() != null && !node.getChildren().isEmpty()) {
|
||||
results.addAll(collectAllDaPaths(node.getChildren(), newPath));
|
||||
}
|
||||
} else if ("DAI".equalsIgnoreCase(node.getKind())) {
|
||||
if (node.getName() != null && !node.getName().isEmpty()) {
|
||||
newPath.add(node.getName());
|
||||
}
|
||||
|
||||
if (node.getValues() != null && !node.getValues().isEmpty()) {
|
||||
for (String val : node.getValues()) {
|
||||
if (val != null && !val.isEmpty()) {
|
||||
newPath.add(val);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
results.add(String.join("$", newPath));
|
||||
|
||||
if (node.getChildren() != null && !node.getChildren().isEmpty()) {
|
||||
results.addAll(collectAllDaPaths(node.getChildren(), newPath));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
private String buildDoPath(LnNode lnNode, DoiNode doiNode) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
if (lnNode.getLnClass() != null) {
|
||||
sb.append(lnNode.getLnClass());
|
||||
}
|
||||
if (lnNode.getLnInst() != null) {
|
||||
sb.append(lnNode.getLnInst());
|
||||
}
|
||||
if (doiNode.getName() != null) {
|
||||
if (sb.length() > 0) {
|
||||
sb.append("$");
|
||||
sb.append("MX");
|
||||
sb.append("$");
|
||||
}
|
||||
sb.append(doiNode.getName());
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
private String buildMetricKey(String doPath, String daPath) {
|
||||
return doPath + "|" + daPath;
|
||||
}
|
||||
|
||||
private Map<String, ValueRule> findApplicableRules(Map<String, List<ValueRule>> allRules,
|
||||
Map<String, IcdMetricInfo> icdMetrics) {
|
||||
Map<String, ValueRule> applicable = new HashMap<>();
|
||||
|
||||
System.out.println("========== 开始匹配规则 ==========");
|
||||
System.out.println("规则总数: " + allRules.size());
|
||||
System.out.println("ICD指标总数: " + icdMetrics.size());
|
||||
|
||||
int matchedCount = 0;
|
||||
int unmatchedCount = 0;
|
||||
|
||||
for (Map.Entry<String, List<ValueRule>> entry : allRules.entrySet()) {
|
||||
String ruleKey = entry.getKey();
|
||||
List<ValueRule> ruleVariants = entry.getValue();
|
||||
|
||||
boolean foundInIcd = false;
|
||||
ValueRule matchedRule = null;
|
||||
|
||||
System.out.println("\n正在匹配规则: " + ruleKey + " (共" + ruleVariants.size() + "个候选)");
|
||||
|
||||
for (int i = 0; i < ruleVariants.size(); i++) {
|
||||
ValueRule rule = ruleVariants.get(i);
|
||||
if (rule.getDoPath() == null || rule.getDoPath().isEmpty()) {
|
||||
System.out.println(" 候选[" + (i+1) + "] 跳过: DO为空");
|
||||
continue;
|
||||
}
|
||||
|
||||
String ruleDo = normalizePath(rule.getDoPath());
|
||||
String ruleDa = normalizePath(rule.getDaPath());
|
||||
|
||||
System.out.println(" 尝试候选[" + (i+1) + "]: DO=" + rule.getDoPath() + " DA=" + rule.getDaPath());
|
||||
|
||||
for (Map.Entry<String, IcdMetricInfo> icdEntry : icdMetrics.entrySet()) {
|
||||
String icdKey = icdEntry.getKey();
|
||||
IcdMetricInfo icdMetric = icdEntry.getValue();
|
||||
|
||||
String icdDo = normalizePath(icdMetric.getDoPath());
|
||||
String icdDa = normalizePath(icdMetric.getDaPath());
|
||||
|
||||
boolean doMatch = matchesPattern(ruleDo, icdDo);
|
||||
boolean daMatch = matchesPattern(ruleDa, icdDa);
|
||||
|
||||
if (doMatch && daMatch) {
|
||||
System.out.println(" ✓ 匹配成功! ICD Key: " + icdKey);
|
||||
System.out.println(" ICD DO=" + icdMetric.getDoPath() + " DA=" + icdMetric.getDaPath());
|
||||
foundInIcd = true;
|
||||
matchedRule = rule;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (foundInIcd) {
|
||||
System.out.println(" >> 规则 " + ruleKey + " 匹配成功,使用候选[" + (i+1) + "]");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (foundInIcd && matchedRule != null) {
|
||||
applicable.put(ruleKey, matchedRule);
|
||||
matchedCount++;
|
||||
} else {
|
||||
unmatchedCount++;
|
||||
if (unmatchedCount <= 15) {
|
||||
System.out.println("✗ 未匹配: " + ruleKey + " (共" + ruleVariants.size() + "个候选)");
|
||||
for (ValueRule rule : ruleVariants) {
|
||||
System.out.println(" 候选: DO=" + rule.getDoPath() + " DA=" + rule.getDaPath());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
System.out.println("\n========== 规则匹配结束 ==========");
|
||||
System.out.println("匹配成功: " + matchedCount + " 条规则");
|
||||
System.out.println("未匹配: " + unmatchedCount + " 条规则");
|
||||
|
||||
return applicable;
|
||||
}
|
||||
|
||||
private String normalizePath(String path) {
|
||||
if (path == null) return "";
|
||||
return path.trim().toLowerCase();
|
||||
}
|
||||
|
||||
private String fillIedInfo(String xmlContent, IcdDocument icdDocument) {
|
||||
if (icdDocument == null) {
|
||||
return xmlContent;
|
||||
}
|
||||
|
||||
String iedName = icdDocument.getIedName();
|
||||
String ldPrefix = icdDocument.getLdPrefix();
|
||||
|
||||
if (iedName != null && !iedName.isEmpty()) {
|
||||
// 替换 <IED name="..." .../>
|
||||
xmlContent = xmlContent.replaceAll(
|
||||
"<IED\\s+name=\"[^\"]*\"",
|
||||
"<IED name=\"" + escapeXml(iedName) + "\""
|
||||
);
|
||||
}
|
||||
|
||||
if (ldPrefix != null && !ldPrefix.isEmpty()) {
|
||||
// 替换 <LDevice Prefix="..." .../>
|
||||
xmlContent = xmlContent.replaceAll(
|
||||
"<LDevice\\s+Prefix=\"[^\"]*\"",
|
||||
"<LDevice Prefix=\"" + escapeXml(ldPrefix) + "\""
|
||||
);
|
||||
}
|
||||
|
||||
return xmlContent;
|
||||
}
|
||||
|
||||
private String fillReportControls(String xmlContent, IcdDocument icdDocument) {
|
||||
if (icdDocument == null || icdDocument.getReportControls() == null || icdDocument.getReportControls().isEmpty()) {
|
||||
return xmlContent;
|
||||
}
|
||||
|
||||
// 构建 ReportStat 内容
|
||||
StringBuilder reportStatBuilder = new StringBuilder();
|
||||
reportStatBuilder.append("\t\t<ReportStat>\n");
|
||||
|
||||
for (ReportControlNode rc : icdDocument.getReportControls()) {
|
||||
if (rc.getName() != null && !rc.getName().isEmpty()) {
|
||||
String name = rc.getName();
|
||||
// 只处理特定的 ReportControl 名称
|
||||
if (name.contains("brcbFluc") ||
|
||||
name.contains("brcbStHarm") ||
|
||||
name.contains("brcbStIHarm") ||
|
||||
name.contains("brcbStMMXU") ||
|
||||
name.contains("brcbStMSQI") ||
|
||||
name.contains("brcbPLT")||
|
||||
name.contains("brcbPST")||
|
||||
name.contains("brcbFlicker")||
|
||||
name.contains("brcbStatistic") ){
|
||||
// 构建 ReportControl 字符串格式:
|
||||
// LLN0$BR$brcbFlickerData,600,0,0,1,0,0,yes,1,1,1,1,1,0,1,1,1,3,1
|
||||
String reportControlStr = buildReportControlString(rc);
|
||||
reportStatBuilder.append("\t\t\t<Report ReportControl=\"")
|
||||
.append(escapeXml(reportControlStr))
|
||||
.append("\" />\n");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
reportStatBuilder.append("\t\t</ReportStat>");
|
||||
|
||||
// 替换原有的 <ReportStat>...</ReportStat> 部分
|
||||
xmlContent = xmlContent.replaceAll(
|
||||
"<ReportStat>[\\s\\S]*?</ReportStat>",
|
||||
reportStatBuilder.toString().replace("$", "\\$").replace("[", "\\[").replace("]", "\\]")
|
||||
);
|
||||
|
||||
return xmlContent;
|
||||
}
|
||||
|
||||
private String buildReportControlString(ReportControlNode rc) {
|
||||
// 格式:LLN0$BR$brcbName,intgPd,dchg,qchg,dupd,period,gi,issuffixed,seqNum,timeStamp,reasonCode,dataSet,dataRef,bufOvfl,entryID,configRef,segmentation,FlickerFlag
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
// ReportControl 名称(需要根据实际情况调整前缀)
|
||||
sb.append("LLN0$").append(rc.getBuffered() != null && rc.getBuffered() ? "BR$" : "RP$").append(rc.getName());
|
||||
// 这里需要根据实际 ICD 中的配置来填充其他字段
|
||||
// 目前使用默认值,后续可以根据 ReportControlNode 的字段扩展
|
||||
if(rc.getName().contains("PLT") || rc.getName().contains("Flicker") || rc.getName().contains("PST") || rc.getName().contains("Fluc")){
|
||||
sb.append(",600"); // intgPd - 上送周期
|
||||
}else{
|
||||
sb.append(",60"); // intgPd - 上送周期
|
||||
}
|
||||
sb.append(",1"); // dchg - 数据变化触发
|
||||
sb.append(",0"); // qchg - 品质变化触发
|
||||
sb.append(",0"); // dupd - 数据更新触发
|
||||
sb.append(",0"); // period - 周期触发
|
||||
sb.append(",0"); // gi - 总召唤
|
||||
sb.append(",yes"); // issuffixed
|
||||
sb.append(",1"); // seqNum
|
||||
sb.append(",1"); // timeStamp
|
||||
sb.append(",1"); // reasonCode
|
||||
sb.append(",1"); // dataSet
|
||||
sb.append(",1"); // dataRef
|
||||
sb.append(",0"); // bufOvfl
|
||||
sb.append(",1"); // entryID
|
||||
sb.append(",1"); // configRef
|
||||
sb.append(",1"); // segmentation
|
||||
sb.append(",3"); // 保留字段
|
||||
if(rc.getName().contains("PLT") || rc.getName().contains("Flicker")){
|
||||
sb.append(",1"); // FlickerFlag
|
||||
}else if(rc.getName().contains("PST") || rc.getName().contains("Fluc")){
|
||||
sb.append(",2"); // FlickerFlag
|
||||
} else {
|
||||
sb.append(",0"); // FlickerFlag
|
||||
}
|
||||
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
private boolean matchesPattern(String pattern, String actual) {
|
||||
if (pattern == null || actual == null) {
|
||||
return pattern == null && actual == null;
|
||||
}
|
||||
|
||||
String normalizedPattern = pattern.replaceAll("\\[[^\\]]*\\]", "");
|
||||
|
||||
|
||||
if (normalizedPattern.isEmpty() || actual.isEmpty()) {
|
||||
return normalizedPattern.isEmpty() && actual.isEmpty();
|
||||
}
|
||||
|
||||
return actual.equals(normalizedPattern);
|
||||
}
|
||||
|
||||
private String applyRulesToXml(String xmlContent, Map<String, ValueRule> applicableRules) {
|
||||
Matcher matcher = XML_VALUE_PATTERN.matcher(xmlContent);
|
||||
StringBuffer result = new StringBuffer();
|
||||
|
||||
while (matcher.find()) {
|
||||
String name = matcher.group(1);
|
||||
String desc = matcher.group(2);
|
||||
String type = matcher.group(3);
|
||||
|
||||
String key = buildRuleKey(name, desc);
|
||||
ValueRule matchedRule = applicableRules.get(key.toLowerCase());
|
||||
|
||||
if (matchedRule != null) {
|
||||
StringBuilder valueNode = new StringBuilder("<Value ");
|
||||
valueNode.append("name=\"").append(escapeXml(name)).append("\" ");
|
||||
valueNode.append("desc=\"").append(escapeXml(desc)).append("\" ");
|
||||
valueNode.append("type=\"").append(type).append("\" ");
|
||||
|
||||
if ((matchedRule.getDoPath() != null) && !matchedRule.getDoPath().isEmpty()) {
|
||||
valueNode.append("DO=\"").append(escapeXml(matchedRule.getDoPath())).append("\" ");
|
||||
}
|
||||
if (matchedRule.getDaPath() != null && !matchedRule.getDaPath().isEmpty()) {
|
||||
valueNode.append("DA=\"").append(escapeXml(matchedRule.getDaPath())).append("\" ");
|
||||
}
|
||||
if (matchedRule.getBaseFlag() != null && !matchedRule.getBaseFlag().isEmpty()) {
|
||||
valueNode.append("BaseFlag=\"").append(escapeXml(matchedRule.getBaseFlag())).append("\" ");
|
||||
}
|
||||
if (matchedRule.getLimitUp() != null && !matchedRule.getLimitUp().isEmpty()) {
|
||||
valueNode.append("LimitUp=\"").append(escapeXml(matchedRule.getLimitUp())).append("\" ");
|
||||
}
|
||||
if (matchedRule.getLimitDown() != null && !matchedRule.getLimitDown().isEmpty()) {
|
||||
valueNode.append("LimitDown=\"").append(escapeXml(matchedRule.getLimitDown())).append("\" ");
|
||||
}
|
||||
if (matchedRule.getCoefficient() != null && !matchedRule.getCoefficient().isEmpty()) {
|
||||
valueNode.append("Coefficient=\"").append(escapeXml(matchedRule.getCoefficient())).append("\" ");
|
||||
}
|
||||
|
||||
valueNode.append("/>");
|
||||
matcher.appendReplacement(result, Matcher.quoteReplacement(valueNode.toString()));
|
||||
}
|
||||
}
|
||||
matcher.appendTail(result);
|
||||
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
private String buildRuleKey(String name, String desc) {
|
||||
if (name != null && !name.isEmpty()) {
|
||||
return name;
|
||||
}
|
||||
return desc != null ? desc : "";
|
||||
}
|
||||
|
||||
private boolean hasValidDoOrDa(ValueRule rule) {
|
||||
return (rule.getDoPath() != null && !rule.getDoPath().isEmpty()) ||
|
||||
(rule.getDaPath() != null && !rule.getDaPath().isEmpty());
|
||||
}
|
||||
|
||||
private String extractPhase(String daPath) {
|
||||
if (daPath == null) return "";
|
||||
if (daPath.contains("phsA")) return "A";
|
||||
if (daPath.contains("phsB")) return "B";
|
||||
if (daPath.contains("phsC")) return "C";
|
||||
if (daPath.contains("c1")) return "正序";
|
||||
if (daPath.contains("c2")) return "负序";
|
||||
if (daPath.contains("c3")) return "零序";
|
||||
return "无相别";
|
||||
}
|
||||
|
||||
private String extractType(String daPath) {
|
||||
if (daPath == null) return "";
|
||||
if (daPath.contains("mag")) return "mag";
|
||||
if (daPath.contains("ang")) return "ang";
|
||||
return "";
|
||||
}
|
||||
|
||||
private String escapeXml(String text) {
|
||||
if (text == null) return "";
|
||||
return text.replace("&", "&")
|
||||
.replace("<", "<")
|
||||
.replace(">", ">")
|
||||
.replace("\"", """)
|
||||
.replace("'", "'");
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
static class ValueRule {
|
||||
private String name;
|
||||
private String desc;
|
||||
private String type;
|
||||
private String doPath;
|
||||
private String daPath;
|
||||
private String baseFlag;
|
||||
private String limitUp;
|
||||
private String limitDown;
|
||||
private String coefficient;
|
||||
|
||||
public String getName() { return name; }
|
||||
public void setName(String name) { this.name = name; }
|
||||
public String getDesc() { return desc; }
|
||||
public void setDesc(String desc) { this.desc = desc; }
|
||||
public String getType() { return type; }
|
||||
public void setType(String type) { this.type = type; }
|
||||
public String getDoPath() { return doPath; }
|
||||
public void setDoPath(String doPath) { this.doPath = doPath; }
|
||||
public String getDaPath() { return daPath; }
|
||||
public void setDaPath(String daPath) { this.daPath = daPath; }
|
||||
public String getBaseFlag() { return baseFlag; }
|
||||
public void setBaseFlag(String baseFlag) { this.baseFlag = baseFlag; }
|
||||
public String getLimitUp() { return limitUp; }
|
||||
public void setLimitUp(String limitUp) { this.limitUp = limitUp; }
|
||||
public String getLimitDown() { return limitDown; }
|
||||
public void setLimitDown(String limitDown) { this.limitDown = limitDown; }
|
||||
public String getCoefficient() { return coefficient; }
|
||||
public void setCoefficient(String coefficient) { this.coefficient = coefficient; }
|
||||
}
|
||||
|
||||
static class IcdMetricInfo {
|
||||
private String lnClass;
|
||||
private String lnInst;
|
||||
private String doName;
|
||||
private String doPath;
|
||||
private String daPath;
|
||||
private String phase;
|
||||
private String type;
|
||||
|
||||
public String getLnClass() { return lnClass; }
|
||||
public void setLnClass(String lnClass) { this.lnClass = lnClass; }
|
||||
public String getLnInst() { return lnInst; }
|
||||
public void setLnInst(String lnInst) { this.lnInst = lnInst; }
|
||||
public String getDoName() { return doName; }
|
||||
public void setDoName(String doName) { this.doName = doName; }
|
||||
public String getDoPath() { return doPath; }
|
||||
public void setDoPath(String doPath) { this.doPath = doPath; }
|
||||
public String getDaPath() { return daPath; }
|
||||
public void setDaPath(String daPath) { this.daPath = daPath; }
|
||||
public String getPhase() { return phase; }
|
||||
public void setPhase(String phase) { this.phase = phase; }
|
||||
public String getType() { return type; }
|
||||
public void setType(String type) { this.type = type; }
|
||||
}
|
||||
}
|
||||
@@ -2,18 +2,33 @@ package com.njcn.gather.icd.mapping.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.icd.mapping.component.IcdToXmlResponseConverter;
|
||||
import com.njcn.gather.icd.mapping.component.IndexSelectionBuildService;
|
||||
import com.njcn.gather.icd.mapping.component.MappingRequestConverter;
|
||||
import com.njcn.gather.icd.mapping.component.MappingResponseConverter;
|
||||
import com.njcn.gather.icd.mapping.pojo.bo.GenerateMappingResult;
|
||||
import com.njcn.gather.icd.mapping.pojo.bo.IcdToXmlGenerateResult;
|
||||
import com.njcn.gather.icd.mapping.pojo.dto.GenerateFromIcdCommand;
|
||||
import com.njcn.gather.icd.mapping.pojo.param.BuildIndexSelectionRequest;
|
||||
import com.njcn.gather.icd.mapping.pojo.param.GenerateMappingFromIcdRequest;
|
||||
import com.njcn.gather.icd.mapping.pojo.param.IndexCandidateRequest;
|
||||
import com.njcn.gather.icd.mapping.pojo.param.JsonToXmlRequest;
|
||||
import com.njcn.gather.icd.mapping.pojo.param.SubmitIndexSelectionRequest;
|
||||
import com.njcn.gather.icd.mapping.pojo.vo.IcdToXmlResponse;
|
||||
import com.njcn.gather.icd.mapping.pojo.vo.IndexConfirmGroupResponse;
|
||||
import com.njcn.gather.icd.mapping.pojo.vo.IndexSelectionGroupResponse;
|
||||
import com.njcn.gather.icd.mapping.pojo.vo.MappingTaskResponse;
|
||||
import com.njcn.gather.icd.mapping.service.MappingTaskService;
|
||||
import com.njcn.gather.icd.mapping.service.impl.IcdToXmlTaskAppService;
|
||||
import com.njcn.gather.icd.mapping.utils.DateUtils;
|
||||
import com.njcn.web.controller.BaseController;
|
||||
import com.njcn.web.utils.HttpResultUtil;
|
||||
import io.swagger.annotations.Api;
|
||||
import io.swagger.annotations.ApiImplicitParam;
|
||||
import io.swagger.annotations.ApiImplicitParams;
|
||||
import io.swagger.annotations.ApiOperation;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
@@ -25,6 +40,8 @@ import org.springframework.web.bind.annotation.RequestPart;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* ICD 映射接口入口。
|
||||
*/
|
||||
@@ -44,11 +61,21 @@ public class MappingController extends BaseController {
|
||||
/** 响应转换器,按接口阶段裁剪最小返回字段。 */
|
||||
private final MappingResponseConverter responseConverter;
|
||||
|
||||
/** 请求参数转换器,将接口入参转换为应用层命令。 */
|
||||
private final IcdToXmlResponseConverter icdResponseConverter;
|
||||
|
||||
/** 响应转换器,按接口阶段裁剪最小返回字段。 */
|
||||
private final IcdToXmlTaskAppService icdToXmlTaskAppService;
|
||||
|
||||
/** ICD 结构确认弹窗结果组装服务。 */
|
||||
private final IndexSelectionBuildService indexSelectionBuildService;
|
||||
|
||||
/**
|
||||
* 上传 ICD 文件,返回候选结果和可编辑的 ICD 解析结果。
|
||||
*/
|
||||
@OperateInfo(info = LogEnum.BUSINESS_COMMON)
|
||||
@ApiOperation("上传 ICD 文件并生成索引候选")
|
||||
@ApiImplicitParam(name = "icdFile", value = "ICD 文件", required = true, dataType = "__file", paramType = "form")
|
||||
@PostMapping(value = "/get-icd", consumes = {"multipart/form-data"})
|
||||
public MappingTaskResponse getICD(@RequestPart("icdFile") MultipartFile icdFile) {
|
||||
String methodDescribe = getMethodDescribe("getICD");
|
||||
@@ -63,6 +90,7 @@ public class MappingController extends BaseController {
|
||||
*/
|
||||
@OperateInfo(info = LogEnum.BUSINESS_COMMON)
|
||||
@ApiOperation("获取 MMS JSON")
|
||||
@ApiImplicitParam(name = "request", value = "索引确认后生成 MMS JSON 参数", required = true, dataType = "SubmitIndexSelectionRequest")
|
||||
@PostMapping("/get-mms-json")
|
||||
public MappingTaskResponse getMmsJson(@Validated @RequestBody SubmitIndexSelectionRequest request) {
|
||||
String methodDescribe = getMethodDescribe("getMmsJson");
|
||||
@@ -78,6 +106,10 @@ public class MappingController extends BaseController {
|
||||
*/
|
||||
@OperateInfo(info = LogEnum.BUSINESS_COMMON)
|
||||
@ApiOperation("上传 ICD 后直接获取 MMS JSON")
|
||||
@ApiImplicitParams({
|
||||
@ApiImplicitParam(name = "icdFile", value = "ICD 文件", required = true, dataType = "__file", paramType = "form"),
|
||||
@ApiImplicitParam(name = "request", value = "上传 ICD 后直接生成映射参数", required = true, dataType = "GenerateMappingFromIcdRequest", paramType = "form")
|
||||
})
|
||||
@PostMapping(value = "/get-icd-mms-json", consumes = {"multipart/form-data"})
|
||||
public MappingTaskResponse getIcdMmsJson(@RequestPart("icdFile") MultipartFile icdFile,
|
||||
@Validated @RequestPart("request") GenerateMappingFromIcdRequest request) {
|
||||
@@ -90,5 +122,59 @@ public class MappingController extends BaseController {
|
||||
return responseConverter.fromSubmitResult(result);
|
||||
}
|
||||
|
||||
//测试提交添加的注释内容
|
||||
/**
|
||||
* 根据页面回传的 indexCandidates 生成弹窗确认模型。
|
||||
*/
|
||||
@OperateInfo(info = LogEnum.BUSINESS_COMMON)
|
||||
@ApiOperation("根据 indexCandidates 生成确认数据")
|
||||
@ApiImplicitParam(name = "indexCandidates", value = "索引候选分组列表", required = true, dataType = "List")
|
||||
@PostMapping("/build-index-confirm-data")
|
||||
public HttpResult<List<IndexConfirmGroupResponse>> buildIndexConfirmData(@RequestBody List<IndexCandidateRequest> indexCandidates) {
|
||||
String methodDescribe = getMethodDescribe("buildIndexConfirmData");
|
||||
LogUtil.njcnDebug(log, "{},开始根据 indexCandidates 生成确认数据,candidateCount={}",
|
||||
methodDescribe, indexCandidates == null ? 0 : indexCandidates.size());
|
||||
List<IndexConfirmGroupResponse> result = indexSelectionBuildService.buildConfirmData(indexCandidates);
|
||||
return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, result, methodDescribe);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据前端确认后的结果生成最终 indexSelection。
|
||||
*/
|
||||
@OperateInfo(info = LogEnum.BUSINESS_COMMON)
|
||||
@ApiOperation("根据确认结果生成 indexSelection")
|
||||
@ApiImplicitParam(name = "request", value = "根据确认结果生成 indexSelection 参数", required = true, dataType = "BuildIndexSelectionRequest")
|
||||
@PostMapping("/build-index-selection")
|
||||
public HttpResult<List<IndexSelectionGroupResponse>> buildIndexSelection(@RequestBody BuildIndexSelectionRequest request) {
|
||||
String methodDescribe = getMethodDescribe("buildIndexSelection");
|
||||
LogUtil.njcnDebug(log, "{},开始根据确认结果生成 indexSelection,confirmGroupCount={}, confirmedGroupCount={}",
|
||||
methodDescribe,
|
||||
request == null || request.getConfirmData() == null ? 0 : request.getConfirmData().size(),
|
||||
request == null || request.getConfirmedData() == null ? 0 : request.getConfirmedData().size());
|
||||
List<IndexSelectionGroupResponse> result = indexSelectionBuildService.buildIndexSelection(request);
|
||||
return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, result, methodDescribe);
|
||||
}
|
||||
|
||||
/**
|
||||
* 直接将 MMS JSON 转换为 XML 内容。
|
||||
* 适用于已经通过 getIcdMmsJson 获得 JSON 后,单独进行 XML 转换的场景。
|
||||
*/
|
||||
@OperateInfo(info = LogEnum.BUSINESS_COMMON)
|
||||
@ApiOperation("MMS JSON 转 XML")
|
||||
@ApiImplicitParam(name = "request", value = "MMS JSON 转 XML 参数", required = true, dataType = "JsonToXmlRequest")
|
||||
@PostMapping("/get-xml-from-json")
|
||||
public IcdToXmlResponse getXmlFromJson(@Validated @RequestPart("request") JsonToXmlRequest request) {
|
||||
String methodDescribe = getMethodDescribe("getXmlFromJson") + ",开始将 MMS JSON 转换为 XML";
|
||||
|
||||
// 直接返回 XML 内容给前端展示,不再输出临时文件。
|
||||
IcdToXmlGenerateResult result =
|
||||
icdToXmlTaskAppService.generateXmlFromJson(request == null ? null : request.getMappingJson());
|
||||
if (result.getMethodDescribe() != null && !result.getMethodDescribe().trim().isEmpty()) {
|
||||
methodDescribe = methodDescribe + "\n" + result.getMethodDescribe();
|
||||
}
|
||||
|
||||
IcdToXmlResponse response = icdResponseConverter.fromResult(result);
|
||||
response.setMethodDescribe(methodDescribe);
|
||||
return response;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -96,7 +96,7 @@ public class SclGeneratedModelReader {
|
||||
* 从 LN0 或 LN 中读取 DataSet 与 ReportControl 信息。
|
||||
*/
|
||||
private void readReportAndDataSetFromAnyLn(TAnyLN anyLn, IcdDocument document) {
|
||||
List<FcdaNode> allFcdas = new ArrayList<FcdaNode>();
|
||||
//List<FcdaNode> allFcdas = new ArrayList<FcdaNode>();
|
||||
if (anyLn.getDataSet() != null) {
|
||||
for (TDataSet dataSet : anyLn.getDataSet()) {
|
||||
DataSetNode dataSetNode = new DataSetNode();
|
||||
@@ -105,7 +105,7 @@ public class SclGeneratedModelReader {
|
||||
for (TFCDA fcda : dataSet.getFCDA()) {
|
||||
FcdaNode node = toFcdaNode(fcda);
|
||||
dataSetNode.getFcdas().add(node);
|
||||
allFcdas.add(node);
|
||||
//allFcdas.add(node);
|
||||
}
|
||||
}
|
||||
document.getDataSets().put(dataSetNode.getName(), dataSetNode);
|
||||
@@ -113,7 +113,7 @@ public class SclGeneratedModelReader {
|
||||
}
|
||||
for (DataSetNode dataSet : document.getDataSets().values()) {
|
||||
for (FcdaNode fcda : dataSet.getFcdas()) {
|
||||
fcda.setSequenceCount(SclTraversalSupport.calculateSequenceCount(allFcdas, fcda));
|
||||
fcda.setSequenceCount(SclTraversalSupport.calculateSequenceCount(dataSet.getFcdas(), fcda));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
package com.njcn.gather.icd.mapping.pojo.bo;
|
||||
|
||||
import com.njcn.gather.icd.mapping.pojo.bo.analysis.IndexAnalysis;
|
||||
import com.njcn.gather.icd.mapping.pojo.bo.mapping.MappingDocument;
|
||||
import com.njcn.gather.icd.mapping.pojo.enums.GenerateStatus;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* XML 生成应用层返回对象。
|
||||
*
|
||||
* 统一封装成功、需选择索引、失败三类结果。
|
||||
*/
|
||||
@Data
|
||||
public class IcdToXmlGenerateResult {
|
||||
/** 本次生成流程状态。 */
|
||||
private GenerateStatus status;
|
||||
|
||||
/** 给前端或调用方展示的处理结果说明。 */
|
||||
private String message;
|
||||
|
||||
/** 生成过程中需要返回的方法描述信息。 */
|
||||
private String methodDescribe;
|
||||
|
||||
/** ICD 中解析到的 IED 名称。 */
|
||||
private String iedName;
|
||||
|
||||
/** ICD 中解析到的 LD 实例名。 */
|
||||
private String ldInst;
|
||||
|
||||
/** 需要人工绑定索引时返回的候选分析结果。 */
|
||||
private IndexAnalysis indexAnalysis;
|
||||
|
||||
/** 生成成功后的结构化映射文档。 */
|
||||
private MappingDocument mappingDocument;
|
||||
|
||||
/** 生成成功后的 XML 内容,用于前端直接展示。 */
|
||||
private String xmlContent;
|
||||
|
||||
/** 解析、校验或生成过程中收集到的问题。 */
|
||||
private List<String> problems = new ArrayList<String>();
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
package com.njcn.gather.icd.mapping.pojo.dto;
|
||||
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class IcdToXmlGenerateCommand {
|
||||
/** 原始文件名。 */
|
||||
private String fileName;
|
||||
|
||||
/** ICD 文件字节数组。 */
|
||||
private byte[] fileBytes;
|
||||
|
||||
/** 输出版本号。 */
|
||||
private String version;
|
||||
|
||||
/** 作者。 */
|
||||
private String author;
|
||||
|
||||
/** 是否保存到磁盘。 */
|
||||
private boolean saveToDisk;
|
||||
|
||||
|
||||
|
||||
/** 输出目录。 */
|
||||
private String outputDir;
|
||||
|
||||
/** 用户上送的索引选择结果。 */
|
||||
private List<IndexSelectionGroupCommand> indexSelection = new ArrayList<IndexSelectionGroupCommand>();
|
||||
|
||||
public String getFileName() {
|
||||
return fileName;
|
||||
}
|
||||
|
||||
public void setFileName(String fileName) {
|
||||
this.fileName = fileName;
|
||||
}
|
||||
|
||||
public byte[] getFileBytes() {
|
||||
return fileBytes;
|
||||
}
|
||||
|
||||
public void setFileBytes(byte[] fileBytes) {
|
||||
this.fileBytes = fileBytes;
|
||||
}
|
||||
|
||||
public String getVersion() {
|
||||
return version;
|
||||
}
|
||||
|
||||
public void setVersion(String version) {
|
||||
this.version = version;
|
||||
}
|
||||
|
||||
public String getAuthor() {
|
||||
return author;
|
||||
}
|
||||
|
||||
public void setAuthor(String author) {
|
||||
this.author = author;
|
||||
}
|
||||
|
||||
public boolean isSaveToDisk() {
|
||||
return saveToDisk;
|
||||
}
|
||||
|
||||
public void setSaveToDisk(boolean saveToDisk) {
|
||||
this.saveToDisk = saveToDisk;
|
||||
}
|
||||
|
||||
|
||||
|
||||
public String getOutputDir() {
|
||||
return outputDir;
|
||||
}
|
||||
|
||||
public void setOutputDir(String outputDir) {
|
||||
this.outputDir = outputDir;
|
||||
}
|
||||
|
||||
public List<IndexSelectionGroupCommand> getIndexSelection() {
|
||||
return indexSelection;
|
||||
}
|
||||
|
||||
public void setIndexSelection(List<IndexSelectionGroupCommand> indexSelection) {
|
||||
this.indexSelection = indexSelection;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.njcn.gather.icd.mapping.pojo.param;
|
||||
|
||||
import io.swagger.annotations.ApiModel;
|
||||
import io.swagger.annotations.ApiModelProperty;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 根据确认结果生成 indexSelection 的请求参数。
|
||||
*/
|
||||
@Data
|
||||
@ApiModel("根据确认结果生成 indexSelection 的请求参数")
|
||||
public class BuildIndexSelectionRequest {
|
||||
|
||||
/** 接口返回的确认模型数据。 */
|
||||
@ApiModelProperty("接口返回的确认模型数据")
|
||||
private List<IndexConfirmGroupRequest> confirmData = new ArrayList<IndexConfirmGroupRequest>();
|
||||
|
||||
/** 前端最终确认后的选择结果。 */
|
||||
@ApiModelProperty("前端最终确认后的选择结果")
|
||||
private List<ConfirmedIndexGroupRequest> confirmedData = new ArrayList<ConfirmedIndexGroupRequest>();
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.njcn.gather.icd.mapping.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 ConfirmedIndexGroupRequest {
|
||||
|
||||
/** 分组唯一键。 */
|
||||
@ApiModelProperty("分组唯一键")
|
||||
private String groupKey;
|
||||
|
||||
/** 当前分组下按 label 汇总后的确认项。 */
|
||||
@ApiModelProperty("当前分组下按 label 汇总后的确认项")
|
||||
private List<ConfirmedLabelItemRequest> labelItems = new ArrayList<ConfirmedLabelItemRequest>();
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.njcn.gather.icd.mapping.pojo.param;
|
||||
|
||||
import io.swagger.annotations.ApiModel;
|
||||
import io.swagger.annotations.ApiModelProperty;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 前端提交的单个 label 确认结果。
|
||||
*/
|
||||
@Data
|
||||
@ApiModel("前端提交的单个 label 确认结果")
|
||||
public class ConfirmedLabelItemRequest {
|
||||
|
||||
/** 模板标签。 */
|
||||
@ApiModelProperty("模板标签")
|
||||
private String label;
|
||||
|
||||
/** 是否启用当前配置。 */
|
||||
@ApiModelProperty("是否启用当前配置")
|
||||
private boolean enabled;
|
||||
|
||||
/** 当前 label 选中的 lnInst。 */
|
||||
@ApiModelProperty("当前 label 选中的 lnInst")
|
||||
private String lnInst;
|
||||
}
|
||||
@@ -1,38 +1,43 @@
|
||||
package com.njcn.gather.icd.mapping.pojo.param;
|
||||
|
||||
import io.swagger.annotations.ApiModel;
|
||||
import io.swagger.annotations.ApiModelProperty;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 候选接口请求体。
|
||||
*
|
||||
* 当前主要承载输出版本、作者、落盘选项等基础参数。
|
||||
* 为兼容旧调用,仍保留 indexSelection 字段,但候选接口本身不依赖该字段。
|
||||
* 上传 ICD 后直接生成映射的请求参数。
|
||||
*/
|
||||
@Data
|
||||
@ApiModel("上传 ICD 后直接生成映射的请求参数")
|
||||
public class GenerateMappingFromIcdRequest {
|
||||
|
||||
/** 输出版本号。为空时后端默认补当天日期。 */
|
||||
/** 映射版本号。 */
|
||||
@ApiModelProperty("映射版本号,空时默认使用当天日期")
|
||||
private String version;
|
||||
|
||||
/** 作者。为空时使用模块默认作者。 */
|
||||
/** 映射作者。 */
|
||||
@ApiModelProperty("映射作者,空时使用模块默认作者")
|
||||
private String author;
|
||||
|
||||
/** 是否保存到磁盘。 */
|
||||
/** 是否落盘。 */
|
||||
@ApiModelProperty("是否将生成结果落盘保存")
|
||||
private boolean saveToDisk;
|
||||
|
||||
/** 是否返回格式化 JSON。 */
|
||||
/** 是否格式化 JSON。 */
|
||||
@ApiModelProperty("是否返回格式化后的 JSON")
|
||||
private boolean prettyJson;
|
||||
|
||||
/** 输出目录。saveToDisk=true 时才会用到。 */
|
||||
/** 输出目录。 */
|
||||
@ApiModelProperty("输出目录,仅在 saveToDisk=true 时生效")
|
||||
private String outputDir;
|
||||
|
||||
/**
|
||||
* 兼容保留的索引选择结果。
|
||||
*
|
||||
* 当前候选接口会直接返回 indexCandidates 和 icdDocument;
|
||||
* 正式提交时请改用 get-mms-json 接口。
|
||||
* 兼容保留的索引绑定结果。
|
||||
* 正式提交流程建议改用 get-mms-json 接口。
|
||||
*/
|
||||
@ApiModelProperty("兼容保留的索引绑定结果,正式提交建议改用 get-mms-json 接口")
|
||||
private List<IndexSelectionGroupRequest> indexSelection = new ArrayList<IndexSelectionGroupRequest>();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
package com.njcn.gather.icd.mapping.pojo.param;
|
||||
|
||||
import lombok.Data;
|
||||
import javax.validation.constraints.NotBlank;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
|
||||
public class IcdToXmlGenerateRequest {
|
||||
/** 输出版本号。为空时后端默认补 1.0。 */
|
||||
private String version;
|
||||
|
||||
/** 作者。为空时默认空字符串。 */
|
||||
private String author;
|
||||
|
||||
/** 是否保存到磁盘。 */
|
||||
private boolean saveToDisk;
|
||||
|
||||
/** 输出目录。saveToDisk=true 时才会用到。 */
|
||||
private String outputDir;
|
||||
|
||||
|
||||
/**
|
||||
* 索引选择结果。
|
||||
*
|
||||
* 说明:
|
||||
* 1. 每一个元素代表一个“业务分组”,例如:实时数据、统计数据、波动闪变。
|
||||
* 2. 每个业务分组下面又包含多条绑定关系。
|
||||
* 3. 允许为空;为空时后端返回 NEED_INDEX_SELECTION,给前端候选参考项。
|
||||
*/
|
||||
private List<IndexSelectionGroupRequest> indexSelection = new ArrayList<IndexSelectionGroupRequest>();
|
||||
|
||||
public String getVersion() {
|
||||
return version;
|
||||
}
|
||||
|
||||
public void setVersion(String version) {
|
||||
this.version = version;
|
||||
}
|
||||
|
||||
public String getAuthor() {
|
||||
return author;
|
||||
}
|
||||
|
||||
public void setAuthor(String author) {
|
||||
this.author = author;
|
||||
}
|
||||
|
||||
public boolean isSaveToDisk() {
|
||||
return saveToDisk;
|
||||
}
|
||||
|
||||
public void setSaveToDisk(boolean saveToDisk) {
|
||||
this.saveToDisk = saveToDisk;
|
||||
}
|
||||
|
||||
public String getOutputDir() {
|
||||
return outputDir;
|
||||
}
|
||||
|
||||
public void setOutputDir(String outputDir) {
|
||||
this.outputDir = outputDir;
|
||||
}
|
||||
|
||||
public List<IndexSelectionGroupRequest> getIndexSelection() {
|
||||
return indexSelection;
|
||||
}
|
||||
|
||||
public void setIndexSelection(List<IndexSelectionGroupRequest> indexSelection) {
|
||||
this.indexSelection = indexSelection;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -1,24 +1,29 @@
|
||||
package com.njcn.gather.icd.mapping.pojo.param;
|
||||
|
||||
import io.swagger.annotations.ApiModel;
|
||||
import io.swagger.annotations.ApiModelProperty;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 单条索引绑定请求。
|
||||
*
|
||||
* 一条绑定只表达一个最小关系:
|
||||
* 某个报告 reportName 下,使用某个标签 label 与某个 lnInst 数字做绑定。
|
||||
* 单条索引绑定请求项。
|
||||
*/
|
||||
@Data
|
||||
@ApiModel("单条索引绑定请求项")
|
||||
public class IndexBindingRequest {
|
||||
|
||||
/** 绑定发生在哪个报告上,例如 brcbStHarm。 */
|
||||
/** 报告名称。 */
|
||||
@ApiModelProperty("报告名称")
|
||||
private String reportName;
|
||||
|
||||
/** 绑定发生在哪个数据集上,例如 dsStHarm。 */
|
||||
/** 数据集名称。 */
|
||||
@ApiModelProperty("数据集名称")
|
||||
private String dataSetName;
|
||||
|
||||
/** 业务标签,例如最大值、最小值、实时数据。 */
|
||||
/** 模板标签。 */
|
||||
@ApiModelProperty("模板标签")
|
||||
private String label;
|
||||
|
||||
/** 当前标签最终绑定到的 lnInst 数字,例如 1、2、3。 */
|
||||
/** 最终绑定的 lnInst 值。 */
|
||||
@ApiModelProperty("最终绑定的 lnInst 值")
|
||||
private String lnInst;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
package com.njcn.gather.icd.mapping.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 IndexCandidateReportItemRequest {
|
||||
|
||||
/** 报告名称。 */
|
||||
@ApiModelProperty("报告名称")
|
||||
private String reportName;
|
||||
|
||||
/** 数据集名称。 */
|
||||
@ApiModelProperty("数据集名称")
|
||||
private String dataSetName;
|
||||
|
||||
/** 报告描述。 */
|
||||
@ApiModelProperty("报告描述")
|
||||
private String reportDesc;
|
||||
|
||||
/** 当前报告允许选择的 lnInst 值列表。 */
|
||||
@ApiModelProperty("当前报告允许选择的 lnInst 值列表")
|
||||
private List<String> availableLnInstValues = new ArrayList<String>();
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package com.njcn.gather.icd.mapping.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 IndexCandidateRequest {
|
||||
|
||||
/** 分组唯一键。 */
|
||||
@ApiModelProperty("分组唯一键")
|
||||
private String groupKey;
|
||||
|
||||
/** 分组中文描述。 */
|
||||
@ApiModelProperty("分组中文描述")
|
||||
private String groupDesc;
|
||||
|
||||
/** 当前分组包含的报告数量。 */
|
||||
@ApiModelProperty("当前分组包含的报告数量")
|
||||
private int reportCount;
|
||||
|
||||
/** 当前分组展示的模板标签列表。 */
|
||||
@ApiModelProperty("当前分组展示的模板标签列表")
|
||||
private List<String> templateLabels = new ArrayList<String>();
|
||||
|
||||
/** 当前分组下的报告候选列表。 */
|
||||
@ApiModelProperty("当前分组下的报告候选列表")
|
||||
private List<IndexCandidateReportItemRequest> reports = new ArrayList<IndexCandidateReportItemRequest>();
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.njcn.gather.icd.mapping.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 IndexConfirmGroupRequest {
|
||||
|
||||
/** 分组唯一键。 */
|
||||
@ApiModelProperty("分组唯一键")
|
||||
private String groupKey;
|
||||
|
||||
/** 分组中文描述。 */
|
||||
@ApiModelProperty("分组中文描述")
|
||||
private String groupDesc;
|
||||
|
||||
/** 当前分组下按 label 聚合后的确认项。 */
|
||||
@ApiModelProperty("当前分组下按 label 聚合后的确认项")
|
||||
private List<IndexConfirmLabelItemRequest> labelItems = new ArrayList<IndexConfirmLabelItemRequest>();
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package com.njcn.gather.icd.mapping.pojo.param;
|
||||
|
||||
import io.swagger.annotations.ApiModel;
|
||||
import io.swagger.annotations.ApiModelProperty;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 索引确认模型中的单个 label 请求项。
|
||||
*/
|
||||
@Data
|
||||
@ApiModel("索引确认模型中的单个 label 请求项")
|
||||
public class IndexConfirmLabelItemRequest {
|
||||
|
||||
/** 模板标签。 */
|
||||
@ApiModelProperty("模板标签")
|
||||
private String label;
|
||||
|
||||
/** 是否必配。 */
|
||||
@ApiModelProperty("是否必配")
|
||||
private boolean required;
|
||||
|
||||
/** 是否支持一次配置后展开到多个报告。 */
|
||||
@ApiModelProperty("是否支持一次配置后展开到多个报告")
|
||||
private boolean configurableOnce;
|
||||
|
||||
/** 所有目标报告共同支持的 lnInst 候选值。 */
|
||||
@ApiModelProperty("所有目标报告共同支持的 lnInst 候选值")
|
||||
private List<String> commonLnInstValues = new ArrayList<String>();
|
||||
|
||||
/** 候选值唯一时的默认 lnInst。 */
|
||||
@ApiModelProperty("候选值唯一时的默认 lnInst")
|
||||
private String defaultLnInst;
|
||||
|
||||
/** 当前 label 影响的目标报告列表。 */
|
||||
@ApiModelProperty("当前 label 影响的目标报告列表")
|
||||
private List<IndexConfirmTargetRequest> targets = new ArrayList<IndexConfirmTargetRequest>();
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package com.njcn.gather.icd.mapping.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 IndexConfirmTargetRequest {
|
||||
|
||||
/** 报告名称。 */
|
||||
@ApiModelProperty("报告名称")
|
||||
private String reportName;
|
||||
|
||||
/** 数据集名称。 */
|
||||
@ApiModelProperty("数据集名称")
|
||||
private String dataSetName;
|
||||
|
||||
/** 报告描述。 */
|
||||
@ApiModelProperty("报告描述")
|
||||
private String reportDesc;
|
||||
|
||||
/** 当前目标报告可接受的 lnInst 列表。 */
|
||||
@ApiModelProperty("当前目标报告可接受的 lnInst 列表")
|
||||
private List<String> availableLnInstValues = new ArrayList<String>();
|
||||
}
|
||||
@@ -1,28 +1,28 @@
|
||||
package com.njcn.gather.icd.mapping.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 IndexSelectionGroupRequest {
|
||||
|
||||
/**
|
||||
* 分组唯一键。
|
||||
*
|
||||
* 该值由后端在 NEED_INDEX_SELECTION 场景返回,前端应原样回传,
|
||||
* 避免仅依赖中文描述做匹配。
|
||||
*/
|
||||
/** 分组唯一键。 */
|
||||
@ApiModelProperty("分组唯一键,需原样回传后端返回值")
|
||||
private String groupKey;
|
||||
|
||||
/** 分组中文描述,例如“实时数据”“统计数据”。 */
|
||||
/** 分组中文描述。 */
|
||||
@ApiModelProperty("分组中文描述")
|
||||
private String groupDesc;
|
||||
|
||||
/** 当前业务分组下,用户最终确认的绑定关系。 */
|
||||
/** 当前分组最终确认的绑定关系。 */
|
||||
@ApiModelProperty("当前分组最终确认的绑定关系")
|
||||
private List<IndexBindingRequest> bindings = new ArrayList<IndexBindingRequest>();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.njcn.gather.icd.mapping.pojo.param;
|
||||
|
||||
import io.swagger.annotations.ApiModel;
|
||||
import io.swagger.annotations.ApiModelProperty;
|
||||
|
||||
/**
|
||||
* MMS JSON 转 XML 的请求参数。
|
||||
*/
|
||||
@ApiModel("MMS JSON 转 XML 的请求参数")
|
||||
public class JsonToXmlRequest {
|
||||
|
||||
/** MMS 映射 JSON 字符串。 */
|
||||
@ApiModelProperty(value = "MMS 映射 JSON 字符串", required = true)
|
||||
private String mappingJson;
|
||||
|
||||
public String getMappingJson() {
|
||||
return mappingJson;
|
||||
}
|
||||
|
||||
public void setMappingJson(String mappingJson) {
|
||||
this.mappingJson = mappingJson;
|
||||
}
|
||||
}
|
||||
@@ -1,37 +1,45 @@
|
||||
package com.njcn.gather.icd.mapping.pojo.param;
|
||||
|
||||
import lombok.Data;
|
||||
import com.njcn.gather.icd.mapping.pojo.bo.icd.IcdDocument;
|
||||
import io.swagger.annotations.ApiModel;
|
||||
import io.swagger.annotations.ApiModelProperty;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 提交索引绑定并生成映射的请求体。
|
||||
*
|
||||
* 第二个接口不再重复上传 ICD 文件,而是直接接收前端确认或修改后的 ICD 解析结果。
|
||||
* 提交索引绑定并生成 MMS JSON 的请求参数。
|
||||
*/
|
||||
@Data
|
||||
@ApiModel("提交索引绑定并生成 MMS JSON 的请求参数")
|
||||
public class SubmitIndexSelectionRequest {
|
||||
|
||||
/** 前端基于候选接口返回值确认或修改后的 ICD 解析结果。 */
|
||||
/** ICD 解析结果。 */
|
||||
@ApiModelProperty("ICD 解析结果,通常来自候选接口返回")
|
||||
private IcdDocument icdDocument;
|
||||
|
||||
/** 输出版本号。为空时后端默认补当天日期。 */
|
||||
/** 映射版本号。 */
|
||||
@ApiModelProperty("映射版本号,空时默认使用当天日期")
|
||||
private String version;
|
||||
|
||||
/** 作者。为空时使用模块默认作者。 */
|
||||
/** 映射作者。 */
|
||||
@ApiModelProperty("映射作者,空时使用模块默认作者")
|
||||
private String author;
|
||||
|
||||
/** 是否保存到磁盘。 */
|
||||
/** 是否落盘。 */
|
||||
@ApiModelProperty("是否将生成结果落盘保存")
|
||||
private boolean saveToDisk;
|
||||
|
||||
/** 是否返回格式化 JSON。 */
|
||||
/** 是否格式化 JSON。 */
|
||||
@ApiModelProperty("是否返回格式化后的 JSON")
|
||||
private boolean prettyJson;
|
||||
|
||||
/** 输出目录。saveToDisk=true 时才会用到。 */
|
||||
/** 输出目录。 */
|
||||
@ApiModelProperty("输出目录,仅在 saveToDisk=true 时生效")
|
||||
private String outputDir;
|
||||
|
||||
/** 用户最终确认的索引绑定关系。 */
|
||||
@ApiModelProperty("用户最终确认的索引绑定关系")
|
||||
private List<IndexSelectionGroupRequest> indexSelection = new ArrayList<IndexSelectionGroupRequest>();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
package com.njcn.gather.icd.mapping.pojo.vo;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import com.njcn.gather.icd.mapping.pojo.enums.GenerateStatus;
|
||||
import io.swagger.annotations.ApiModel;
|
||||
import io.swagger.annotations.ApiModelProperty;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* MMS JSON 转 XML 的响应。
|
||||
*/
|
||||
@Data
|
||||
@JsonInclude(JsonInclude.Include.NON_EMPTY)
|
||||
@ApiModel("MMS JSON 转 XML 的响应")
|
||||
public class IcdToXmlResponse {
|
||||
|
||||
@ApiModelProperty("本次接口处理状态")
|
||||
private GenerateStatus status;
|
||||
|
||||
@ApiModelProperty("状态说明或错误提示")
|
||||
private String message;
|
||||
|
||||
@ApiModelProperty("接口方法描述")
|
||||
private String methodDescribe;
|
||||
|
||||
@ApiModelProperty("IED 名称")
|
||||
private String iedName;
|
||||
|
||||
@ApiModelProperty("逻辑设备实例名")
|
||||
private String ldInst;
|
||||
|
||||
@ApiModelProperty("生成后的 XML 文件展示容器")
|
||||
private XmlFileResponse xmlFile;
|
||||
|
||||
@ApiModelProperty("生成后的映射文档摘要")
|
||||
private MappingDocumentResponse mappingDocument;
|
||||
|
||||
@ApiModelProperty("索引候选分组,存在待确认场景时返回")
|
||||
private List<IndexCandidateResponse> indexCandidates = new ArrayList<IndexCandidateResponse>();
|
||||
|
||||
@ApiModelProperty("处理过程中发现的问题列表")
|
||||
private List<String> problems = new ArrayList<String>();
|
||||
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package com.njcn.gather.icd.mapping.pojo.vo;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import io.swagger.annotations.ApiModel;
|
||||
import io.swagger.annotations.ApiModelProperty;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 单条索引绑定响应项。
|
||||
*/
|
||||
@Data
|
||||
@JsonInclude(JsonInclude.Include.NON_EMPTY)
|
||||
@ApiModel("单条索引绑定响应项")
|
||||
public class IndexBindingResponse {
|
||||
|
||||
/** 报告名称。 */
|
||||
@ApiModelProperty("报告名称")
|
||||
private String reportName;
|
||||
|
||||
/** 数据集名称。 */
|
||||
@ApiModelProperty("数据集名称")
|
||||
private String dataSetName;
|
||||
|
||||
/** 模板标签。 */
|
||||
@ApiModelProperty("模板标签")
|
||||
private String label;
|
||||
|
||||
/** 当前标签对应的 lnInst。 */
|
||||
@ApiModelProperty("当前标签对应的 lnInst")
|
||||
private String lnInst;
|
||||
}
|
||||
@@ -1,27 +1,34 @@
|
||||
package com.njcn.gather.icd.mapping.pojo.vo;
|
||||
|
||||
import lombok.Data;
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import io.swagger.annotations.ApiModel;
|
||||
import io.swagger.annotations.ApiModelProperty;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 业务分组下的单个报告候选响应。
|
||||
* 索引候选中的单个报告响应项。
|
||||
*/
|
||||
@JsonInclude(JsonInclude.Include.NON_EMPTY)
|
||||
@Data
|
||||
@JsonInclude(JsonInclude.Include.NON_EMPTY)
|
||||
@ApiModel("索引候选中的单个报告响应项")
|
||||
public class IndexCandidateReportItemResponse {
|
||||
|
||||
/** 报告名称。 */
|
||||
@ApiModelProperty("报告名称")
|
||||
private String reportName;
|
||||
|
||||
/** 数据集名称。 */
|
||||
@ApiModelProperty("数据集名称")
|
||||
private String dataSetName;
|
||||
|
||||
/** 报告描述。 */
|
||||
@ApiModelProperty("报告描述")
|
||||
private String reportDesc;
|
||||
|
||||
/** 当前报告可选的 `lnInst` 数值。 */
|
||||
/** 当前报告可选的 lnInst 值列表。 */
|
||||
@ApiModelProperty("当前报告可选的 lnInst 值列表")
|
||||
private List<String> availableLnInstValues = new ArrayList<String>();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,33 +1,38 @@
|
||||
package com.njcn.gather.icd.mapping.pojo.vo;
|
||||
|
||||
import lombok.Data;
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import io.swagger.annotations.ApiModel;
|
||||
import io.swagger.annotations.ApiModelProperty;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 索引候选响应对象。
|
||||
*
|
||||
* 一个候选对应一个业务分组,分组下可包含多个报告,
|
||||
* 前端据此完成模板标签与 `lnInst` 的人工绑定。
|
||||
* 索引候选分组响应。
|
||||
*/
|
||||
@JsonInclude(JsonInclude.Include.NON_EMPTY)
|
||||
@Data
|
||||
@JsonInclude(JsonInclude.Include.NON_EMPTY)
|
||||
@ApiModel("索引候选分组响应")
|
||||
public class IndexCandidateResponse {
|
||||
|
||||
/** 分组唯一键。 */
|
||||
@ApiModelProperty("分组唯一键")
|
||||
private String groupKey;
|
||||
|
||||
/** 分组中文描述。 */
|
||||
@ApiModelProperty("分组中文描述")
|
||||
private String groupDesc;
|
||||
|
||||
/** 当前分组包含的报告数。 */
|
||||
/** 当前分组包含的报告数量。 */
|
||||
@ApiModelProperty("当前分组包含的报告数量")
|
||||
private int reportCount;
|
||||
|
||||
/** 模板里配置的可选标签。 */
|
||||
/** 模板中配置的可选标签。 */
|
||||
@ApiModelProperty("模板中配置的可选标签")
|
||||
private List<String> templateLabels = new ArrayList<String>();
|
||||
|
||||
/** 当前分组下的报告候选列表。 */
|
||||
@ApiModelProperty("当前分组下的报告候选列表")
|
||||
private List<IndexCandidateReportItemResponse> reports = new ArrayList<IndexCandidateReportItemResponse>();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.njcn.gather.icd.mapping.pojo.vo;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import io.swagger.annotations.ApiModel;
|
||||
import io.swagger.annotations.ApiModelProperty;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 索引确认分组响应。
|
||||
*/
|
||||
@Data
|
||||
@JsonInclude(JsonInclude.Include.NON_EMPTY)
|
||||
@ApiModel("索引确认分组响应")
|
||||
public class IndexConfirmGroupResponse {
|
||||
|
||||
/** 分组唯一键。 */
|
||||
@ApiModelProperty("分组唯一键")
|
||||
private String groupKey;
|
||||
|
||||
/** 分组中文描述。 */
|
||||
@ApiModelProperty("分组中文描述")
|
||||
private String groupDesc;
|
||||
|
||||
/** 当前分组下按 label 聚合后的确认项。 */
|
||||
@ApiModelProperty("当前分组下按 label 聚合后的确认项")
|
||||
private List<IndexConfirmLabelItemResponse> labelItems = new ArrayList<IndexConfirmLabelItemResponse>();
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package com.njcn.gather.icd.mapping.pojo.vo;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import io.swagger.annotations.ApiModel;
|
||||
import io.swagger.annotations.ApiModelProperty;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 索引确认模型中的单个 label 响应项。
|
||||
*/
|
||||
@Data
|
||||
@JsonInclude(JsonInclude.Include.NON_EMPTY)
|
||||
@ApiModel("索引确认模型中的单个 label 响应项")
|
||||
public class IndexConfirmLabelItemResponse {
|
||||
|
||||
/** 模板标签。 */
|
||||
@ApiModelProperty("模板标签")
|
||||
private String label;
|
||||
|
||||
/** 是否必配。 */
|
||||
@ApiModelProperty("是否必配")
|
||||
private boolean required;
|
||||
|
||||
/** 是否支持一次配置后展开到多个报告。 */
|
||||
@ApiModelProperty("是否支持一次配置后展开到多个报告")
|
||||
private boolean configurableOnce;
|
||||
|
||||
/** 所有目标报告共同支持的 lnInst 候选值。 */
|
||||
@ApiModelProperty("所有目标报告共同支持的 lnInst 候选值")
|
||||
private List<String> commonLnInstValues = new ArrayList<String>();
|
||||
|
||||
/** 候选值唯一时的默认 lnInst。 */
|
||||
@ApiModelProperty("候选值唯一时的默认 lnInst")
|
||||
private String defaultLnInst;
|
||||
|
||||
/** 当前 label 影响的全部目标报告。 */
|
||||
@ApiModelProperty("当前 label 影响的全部目标报告")
|
||||
private List<IndexConfirmTargetResponse> targets = new ArrayList<IndexConfirmTargetResponse>();
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package com.njcn.gather.icd.mapping.pojo.vo;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import io.swagger.annotations.ApiModel;
|
||||
import io.swagger.annotations.ApiModelProperty;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 索引确认模型中的目标报告响应项。
|
||||
*/
|
||||
@Data
|
||||
@JsonInclude(JsonInclude.Include.NON_EMPTY)
|
||||
@ApiModel("索引确认模型中的目标报告响应项")
|
||||
public class IndexConfirmTargetResponse {
|
||||
|
||||
/** 报告名称。 */
|
||||
@ApiModelProperty("报告名称")
|
||||
private String reportName;
|
||||
|
||||
/** 数据集名称。 */
|
||||
@ApiModelProperty("数据集名称")
|
||||
private String dataSetName;
|
||||
|
||||
/** 报告描述。 */
|
||||
@ApiModelProperty("报告描述")
|
||||
private String reportDesc;
|
||||
|
||||
/** 当前目标报告可接受的 lnInst 列表。 */
|
||||
@ApiModelProperty("当前目标报告可接受的 lnInst 列表")
|
||||
private List<String> availableLnInstValues = new ArrayList<String>();
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.njcn.gather.icd.mapping.pojo.vo;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import io.swagger.annotations.ApiModel;
|
||||
import io.swagger.annotations.ApiModelProperty;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 生成后的索引选择分组响应。
|
||||
*/
|
||||
@Data
|
||||
@JsonInclude(JsonInclude.Include.NON_EMPTY)
|
||||
@ApiModel("生成后的索引选择分组响应")
|
||||
public class IndexSelectionGroupResponse {
|
||||
|
||||
/** 分组唯一键。 */
|
||||
@ApiModelProperty("分组唯一键")
|
||||
private String groupKey;
|
||||
|
||||
/** 分组中文描述。 */
|
||||
@ApiModelProperty("分组中文描述")
|
||||
private String groupDesc;
|
||||
|
||||
/** 当前分组最终生成的绑定关系。 */
|
||||
@ApiModelProperty("当前分组最终生成的绑定关系")
|
||||
private List<IndexBindingResponse> bindings = new ArrayList<IndexBindingResponse>();
|
||||
}
|
||||
@@ -1,28 +1,37 @@
|
||||
package com.njcn.gather.icd.mapping.pojo.vo;
|
||||
|
||||
import io.swagger.annotations.ApiModel;
|
||||
import io.swagger.annotations.ApiModelProperty;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 映射文档摘要响应。
|
||||
*
|
||||
* 用于返回最终映射结果中的关键信息摘要。
|
||||
*/
|
||||
@Data
|
||||
@ApiModel("映射文档摘要响应")
|
||||
public class MappingDocumentResponse {
|
||||
|
||||
/** 映射文档版本。 */
|
||||
@ApiModelProperty("映射文档版本")
|
||||
private String version;
|
||||
|
||||
/** 映射文档作者。 */
|
||||
@ApiModelProperty("映射文档作者")
|
||||
private String author;
|
||||
|
||||
/** 输出 JSON 中的 IED。 */
|
||||
@ApiModelProperty("输出 JSON 中的 IED")
|
||||
private String ied;
|
||||
|
||||
/** 输出 JSON 中的 LD。 */
|
||||
@ApiModelProperty("输出 JSON 中的 LD")
|
||||
private String ld;
|
||||
|
||||
/** ReportMap 条目数量。 */
|
||||
@ApiModelProperty("ReportMap 条目数量")
|
||||
private int reportCount;
|
||||
|
||||
/** DataSetList 分组数量。 */
|
||||
@ApiModelProperty("DataSetList 分组数量")
|
||||
private int dataSetCount;
|
||||
}
|
||||
|
||||
@@ -1,39 +1,53 @@
|
||||
package com.njcn.gather.icd.mapping.pojo.vo;
|
||||
|
||||
import lombok.Data;
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import com.njcn.gather.icd.mapping.pojo.bo.icd.IcdDocument;
|
||||
import com.njcn.gather.icd.mapping.pojo.bo.mapping.MappingDocument;
|
||||
import com.njcn.gather.icd.mapping.pojo.enums.GenerateStatus;
|
||||
import io.swagger.annotations.ApiModel;
|
||||
import io.swagger.annotations.ApiModelProperty;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* ICD 映射接口统一响应。
|
||||
*
|
||||
* 按接口阶段仅返回当前场景必需字段;空字段和空集合不参与序列化。
|
||||
* ICD 映射统一响应。
|
||||
*/
|
||||
@JsonInclude(JsonInclude.Include.NON_EMPTY)
|
||||
@Data
|
||||
@JsonInclude(JsonInclude.Include.NON_EMPTY)
|
||||
@ApiModel("ICD 映射统一响应")
|
||||
public class MappingTaskResponse {
|
||||
|
||||
/** 本次接口处理状态。 */
|
||||
@ApiModelProperty("本次接口处理状态")
|
||||
private GenerateStatus status;
|
||||
|
||||
/** 状态说明或错误提示。 */
|
||||
@ApiModelProperty("状态说明或错误提示")
|
||||
private String message;
|
||||
|
||||
/** 候选接口或需要重新选择索引时返回的 ICD 解析结果。 */
|
||||
/** ICD 解析结果。 */
|
||||
@ApiModelProperty("ICD 解析结果,在需要确认索引时返回")
|
||||
private IcdDocument icdDocument;
|
||||
|
||||
/** 正式生成成功后的完整映射 JSON。 */
|
||||
@ApiModelProperty("正式生成成功后的完整映射 JSON")
|
||||
private String mappingJson;
|
||||
|
||||
/** 生成文件落盘后的绝对路径。 */
|
||||
/** 正式生成成功后的结构化映射文档。 */
|
||||
@ApiModelProperty("正式生成成功后的结构化映射文档")
|
||||
private MappingDocument mappingDocument;
|
||||
|
||||
/** 落盘后的绝对路径。 */
|
||||
@ApiModelProperty("落盘后的绝对路径")
|
||||
private String savedPath;
|
||||
|
||||
/** 待绑定状态下返回的索引候选分组。 */
|
||||
@ApiModelProperty("待绑定状态下返回的索引候选分组")
|
||||
private List<IndexCandidateResponse> indexCandidates = new ArrayList<IndexCandidateResponse>();
|
||||
|
||||
/** 模板校验、候选分析或绑定校验问题。 */
|
||||
@ApiModelProperty("模板校验、候选分析或绑定校验问题")
|
||||
private List<String> problems = new ArrayList<String>();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.njcn.gather.icd.mapping.pojo.vo;
|
||||
|
||||
import io.swagger.annotations.ApiModel;
|
||||
import io.swagger.annotations.ApiModelProperty;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* XML 文件展示容器。
|
||||
*/
|
||||
@Data
|
||||
@ApiModel("XML 文件展示容器")
|
||||
public class XmlFileResponse {
|
||||
|
||||
/** XML 文件名。 */
|
||||
@ApiModelProperty("XML 文件名")
|
||||
private String fileName;
|
||||
|
||||
/** 文件内容类型。 */
|
||||
@ApiModelProperty("文件内容类型")
|
||||
private String contentType;
|
||||
|
||||
/** 文件编码。 */
|
||||
@ApiModelProperty("文件编码")
|
||||
private String encoding;
|
||||
|
||||
/** XML 文件内容。 */
|
||||
@ApiModelProperty("XML 文件内容")
|
||||
private String content;
|
||||
}
|
||||
@@ -0,0 +1,308 @@
|
||||
package com.njcn.gather.icd.mapping.service.impl;
|
||||
|
||||
import com.njcn.gather.icd.mapping.component.DefaultTemplateLoader;
|
||||
import com.njcn.gather.icd.mapping.component.IcdParserService;
|
||||
import com.njcn.gather.icd.mapping.component.IcdToXmlMappingService;
|
||||
import com.njcn.gather.icd.mapping.component.IndexAnalysisService;
|
||||
import com.njcn.gather.icd.mapping.component.IndexValidationService;
|
||||
import com.njcn.gather.icd.mapping.component.JsonToXmlConversionService;
|
||||
import com.njcn.gather.icd.mapping.component.MappingDocumentSerializer;
|
||||
import com.njcn.gather.icd.mapping.component.MappingGenerationService;
|
||||
import com.njcn.gather.icd.mapping.component.RuleBasedXmlMappingService;
|
||||
import com.njcn.gather.icd.mapping.pojo.bo.IcdToXmlGenerateResult;
|
||||
import com.njcn.gather.icd.mapping.pojo.bo.analysis.IndexAnalysis;
|
||||
import com.njcn.gather.icd.mapping.pojo.bo.analysis.ValidationResult;
|
||||
import com.njcn.gather.icd.mapping.pojo.bo.icd.IcdDocument;
|
||||
import com.njcn.gather.icd.mapping.pojo.bo.mapping.MappingDocument;
|
||||
import com.njcn.gather.icd.mapping.pojo.bo.template.DefaultTemplate;
|
||||
import com.njcn.gather.icd.mapping.pojo.dto.IcdToXmlGenerateCommand;
|
||||
import com.njcn.gather.icd.mapping.pojo.dto.IndexSelectionGroupCommand;
|
||||
import com.njcn.gather.icd.mapping.pojo.enums.GenerateStatus;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* ICD/XML 生成任务编排服务。
|
||||
*
|
||||
* 负责组织 ICD 到 XML、MMS JSON 到 XML 两类转换链路,避免 Controller 关注模板、规则和异常处理细节。
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class IcdToXmlTaskAppService {
|
||||
|
||||
/** ICD 转 XML 阶段名称。 */
|
||||
private static final String ICD_TO_XML_TASK_NAME = "ICD 转 XML";
|
||||
/** JSON 转 XML 阶段名称。 */
|
||||
private static final String JSON_TO_XML_TASK_NAME = "XML 生成";
|
||||
/** 缺少索引绑定提示。 */
|
||||
private static final String INDEX_SELECTION_MISSING_MESSAGE = "索引配置缺失或不合法,请根据候选信息完成标签与数字索引的绑定后重新提交";
|
||||
/** 映射生成成功提示。 */
|
||||
private static final String MAPPING_GENERATE_SUCCESS_MESSAGE = "映射生成成功";
|
||||
/** XML 生成成功提示。 */
|
||||
private static final String XML_GENERATE_SUCCESS_MESSAGE = "XML 生成成功";
|
||||
/** 空 JSON 提示。 */
|
||||
private static final String MAPPING_JSON_EMPTY_MESSAGE = "MMS映射JSON不能为空";
|
||||
/** 缺少默认 XML 模板提示。 */
|
||||
private static final String DEFAULT_XML_MISSING_MESSAGE = "缺少默认xml配置文件";
|
||||
/** 缺少默认规则文件提示。 */
|
||||
private static final String DEFAULT_RULE_MISSING_MESSAGE = "缺少默认规则配置文件";
|
||||
|
||||
/** ICD/SCL 文件解析服务。 */
|
||||
private final IcdParserService icdParserService;
|
||||
/** DefaultCfg.txt 默认模板加载器。 */
|
||||
private final DefaultTemplateLoader defaultTemplateLoader;
|
||||
/** 根据 ICD 和模板生成前端可选索引候选。 */
|
||||
private final IndexAnalysisService indexAnalysisService;
|
||||
/** 校验前端回传的标签与 lnInst 绑定关系。 */
|
||||
private final IndexValidationService indexValidationService;
|
||||
/** 根据有效绑定关系生成最终 MappingDocument。 */
|
||||
private final MappingGenerationService mappingGenerationService;
|
||||
/** XML 模板和规则文件加载服务。 */
|
||||
private final RuleBasedXmlMappingService ruleBasedXmlMappingService;
|
||||
/** 将 MappingDocument 序列化为 JSON。 */
|
||||
private final MappingDocumentSerializer mappingDocumentSerializer;
|
||||
/** 根据索引绑定关系重建 XML 规则中的实例索引。 */
|
||||
private final IcdToXmlMappingService icdToXmlMappingService;
|
||||
/** 将 MMS JSON 中间态转换为 XML 内容。 */
|
||||
private final JsonToXmlConversionService jsonToXmlConversionService;
|
||||
|
||||
/**
|
||||
* 解析 ICD 并按索引绑定关系直接生成 XML 内容。
|
||||
*/
|
||||
public IcdToXmlGenerateResult generateFromIcd(IcdToXmlGenerateCommand command) {
|
||||
return executeTask(ICD_TO_XML_TASK_NAME, result -> {
|
||||
IcdToXmlTaskContext context = buildTaskContext(command, result);
|
||||
|
||||
if (isIndexSelectionEmpty(command.getIndexSelection())) {
|
||||
markNeedIndexSelection(result);
|
||||
return;
|
||||
}
|
||||
|
||||
ValidationResult validationResult = indexValidationService.validate(context.indexAnalysis, command.getIndexSelection());
|
||||
if (!validationResult.isValid()) {
|
||||
markNeedIndexSelection(result);
|
||||
result.getProblems().addAll(validationResult.getProblems());
|
||||
return;
|
||||
}
|
||||
|
||||
MappingDocument mappingDocument = mappingGenerationService.generate(
|
||||
context.icdDocument,
|
||||
context.template,
|
||||
context.indexAnalysis,
|
||||
command.getIndexSelection(),
|
||||
command.getVersion(),
|
||||
command.getAuthor()
|
||||
);
|
||||
result.setMappingDocument(mappingDocument);
|
||||
|
||||
String mappingJson = mappingDocumentSerializer.toPrettyJson(mappingDocument);
|
||||
bindIndexMapping(command.getIndexSelection());
|
||||
fillXmlContent(result, mappingJson, loadXmlResources());
|
||||
|
||||
result.setStatus(GenerateStatus.SUCCESS);
|
||||
result.setMessage(MAPPING_GENERATE_SUCCESS_MESSAGE);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 直接从 JSON 字符串生成 XML 内容。
|
||||
*
|
||||
* @param mappingJson MMS 映射 JSON 字符串(由 getIcdMmsJson 接口返回)
|
||||
* @return XML 生成结果
|
||||
*/
|
||||
public IcdToXmlGenerateResult generateXmlFromJson(String mappingJson) {
|
||||
return executeTask(JSON_TO_XML_TASK_NAME, result -> {
|
||||
if (isBlank(mappingJson)) {
|
||||
markFailed(result, JSON_TO_XML_TASK_NAME, MAPPING_JSON_EMPTY_MESSAGE);
|
||||
return;
|
||||
}
|
||||
|
||||
fillXmlContent(result, mappingJson, loadXmlResources());
|
||||
result.setStatus(GenerateStatus.SUCCESS);
|
||||
result.setMessage(XML_GENERATE_SUCCESS_MESSAGE);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 统一包装任务执行结果,避免每个入口重复编写异常兜底逻辑。
|
||||
*/
|
||||
private IcdToXmlGenerateResult executeTask(String taskName, IcdToXmlTaskAction action) {
|
||||
IcdToXmlGenerateResult result = new IcdToXmlGenerateResult();
|
||||
try {
|
||||
action.execute(result);
|
||||
} catch (Exception ex) {
|
||||
handleTaskFailure(result, taskName, ex);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析 ICD 并补齐模板、索引候选上下文。
|
||||
*/
|
||||
private IcdToXmlTaskContext buildTaskContext(IcdToXmlGenerateCommand command, IcdToXmlGenerateResult result) {
|
||||
IcdDocument icdDocument = icdParserService.parse(command.getFileBytes(), command.getFileName());
|
||||
fillIcdSummary(result, icdDocument);
|
||||
|
||||
DefaultTemplate template = loadTemplate(result);
|
||||
IndexAnalysis indexAnalysis = analyzeIndexCandidates(icdDocument, template, result);
|
||||
result.setIndexAnalysis(indexAnalysis);
|
||||
return new IcdToXmlTaskContext(icdDocument, template, indexAnalysis);
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载并校验默认模板。
|
||||
*/
|
||||
private DefaultTemplate loadTemplate(IcdToXmlGenerateResult result) {
|
||||
DefaultTemplate template = defaultTemplateLoader.load();
|
||||
result.getProblems().addAll(template.verify());
|
||||
return template;
|
||||
}
|
||||
|
||||
/**
|
||||
* 分析 ICD 对应的索引候选。
|
||||
*/
|
||||
private IndexAnalysis analyzeIndexCandidates(IcdDocument icdDocument,
|
||||
DefaultTemplate template,
|
||||
IcdToXmlGenerateResult result) {
|
||||
IndexAnalysis indexAnalysis = indexAnalysisService.analyze(icdDocument, template);
|
||||
result.getProblems().addAll(indexAnalysis.getProblems());
|
||||
return indexAnalysis;
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载 XML 模板和规则文件。
|
||||
*/
|
||||
private XmlResourceContext loadXmlResources() throws Exception {
|
||||
InputStream templateStream = ruleBasedXmlMappingService.loadDefaultXmlFile();
|
||||
if (templateStream == null) {
|
||||
throw new IllegalArgumentException(DEFAULT_XML_MISSING_MESSAGE);
|
||||
}
|
||||
|
||||
List<InputStream> ruleStreams = ruleBasedXmlMappingService.loadDefaultRuleFile();
|
||||
if (ruleStreams == null) {
|
||||
throw new IllegalArgumentException(DEFAULT_RULE_MISSING_MESSAGE);
|
||||
}
|
||||
|
||||
return new XmlResourceContext(templateStream, ruleStreams);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据索引绑定关系刷新 XML 转换所需的实例索引。
|
||||
*/
|
||||
private void bindIndexMapping(List<IndexSelectionGroupCommand> indexSelection) {
|
||||
IcdToXmlMappingService.IndexMappingConfig mappingConfig = icdToXmlMappingService.buildIndexMappingFromSelection(indexSelection);
|
||||
icdToXmlMappingService.setIndexMapping(mappingConfig);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 MMS JSON 中间态转换为 XML 内容。
|
||||
*/
|
||||
private void fillXmlContent(IcdToXmlGenerateResult result,
|
||||
String mappingJson,
|
||||
XmlResourceContext xmlResourceContext) throws Exception {
|
||||
List<String> methodDescribeList = new ArrayList<>();
|
||||
String xmlContent = jsonToXmlConversionService.buildXmlContentFromJson(
|
||||
mappingJson,
|
||||
xmlResourceContext.templateStream,
|
||||
xmlResourceContext.ruleStreams,
|
||||
icdToXmlMappingService.getIndexMapping(),
|
||||
methodDescribeList
|
||||
);
|
||||
result.setXmlContent(xmlContent);
|
||||
result.setMethodDescribe(String.join("\n", methodDescribeList));
|
||||
}
|
||||
|
||||
/**
|
||||
* 回填响应公共信息。
|
||||
*/
|
||||
private void fillIcdSummary(IcdToXmlGenerateResult result, IcdDocument icdDocument) {
|
||||
result.setIedName(icdDocument.getIedName());
|
||||
result.setLdInst(icdDocument.getLdInst());
|
||||
}
|
||||
|
||||
/**
|
||||
* 标记当前仍需用户重新确认索引绑定。
|
||||
*/
|
||||
private void markNeedIndexSelection(IcdToXmlGenerateResult result) {
|
||||
result.setStatus(GenerateStatus.NEED_INDEX_SELECTION);
|
||||
result.setMessage(INDEX_SELECTION_MISSING_MESSAGE);
|
||||
}
|
||||
|
||||
/**
|
||||
* 标记可预期的业务失败。
|
||||
*/
|
||||
private void markFailed(IcdToXmlGenerateResult result, String taskName, String errorMessage) {
|
||||
result.setStatus(GenerateStatus.FAILED);
|
||||
result.setMessage(taskName + "失败:" + errorMessage);
|
||||
result.getProblems().add(errorMessage);
|
||||
}
|
||||
|
||||
/**
|
||||
* 统一处理任务异常,避免控制层再补失败分支。
|
||||
*/
|
||||
private void handleTaskFailure(IcdToXmlGenerateResult result, String taskName, Exception ex) {
|
||||
log.error("{}失败", taskName, ex);
|
||||
String errorMessage = resolveErrorMessage(ex);
|
||||
markFailed(result, taskName, errorMessage);
|
||||
}
|
||||
|
||||
private boolean isIndexSelectionEmpty(List<IndexSelectionGroupCommand> indexSelection) {
|
||||
return indexSelection == null || indexSelection.isEmpty();
|
||||
}
|
||||
|
||||
private boolean isBlank(String value) {
|
||||
return value == null || value.trim().isEmpty();
|
||||
}
|
||||
|
||||
private String resolveErrorMessage(Exception ex) {
|
||||
if (ex.getMessage() == null || ex.getMessage().trim().isEmpty()) {
|
||||
return ex.getClass().getSimpleName();
|
||||
}
|
||||
return ex.getMessage();
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
private interface IcdToXmlTaskAction {
|
||||
void execute(IcdToXmlGenerateResult result) throws Exception;
|
||||
}
|
||||
|
||||
/**
|
||||
* 供 ICD 转 XML 流程复用的编排上下文。
|
||||
*/
|
||||
private static class IcdToXmlTaskContext {
|
||||
|
||||
private final IcdDocument icdDocument;
|
||||
|
||||
private final DefaultTemplate template;
|
||||
|
||||
private final IndexAnalysis indexAnalysis;
|
||||
|
||||
private IcdToXmlTaskContext(IcdDocument icdDocument, DefaultTemplate template, IndexAnalysis indexAnalysis) {
|
||||
this.icdDocument = icdDocument;
|
||||
this.template = template;
|
||||
this.indexAnalysis = indexAnalysis;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* XML 转换所需的模板和规则资源。
|
||||
*/
|
||||
private static class XmlResourceContext {
|
||||
|
||||
private final InputStream templateStream;
|
||||
|
||||
private final List<InputStream> ruleStreams;
|
||||
|
||||
private XmlResourceContext(InputStream templateStream, List<InputStream> ruleStreams) {
|
||||
this.templateStream = templateStream;
|
||||
this.ruleStreams = ruleStreams;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package com.njcn.gather.icd.mapping.utils;
|
||||
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@Component
|
||||
public class XmlTemplateParser {
|
||||
|
||||
/**
|
||||
* 读取XML模板为原始字符串(100%保留所有换行、空格、注释、空属性)
|
||||
*/
|
||||
public ParseResult parse(InputStream inputStream) throws Exception {
|
||||
StringBuilder template = new StringBuilder();
|
||||
try (BufferedReader br = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8))) {
|
||||
String line;
|
||||
while ((line = br.readLine()) != null) {
|
||||
template.append(line).append("\n"); // 保留原始换行
|
||||
}
|
||||
}
|
||||
Map<String, String> baseParams = new HashMap<>();
|
||||
return new ParseResult(template.toString(), baseParams);
|
||||
}
|
||||
|
||||
public static class ParseResult {
|
||||
private final String templateContent;
|
||||
private final Map<String, String> baseParams;
|
||||
|
||||
public ParseResult(String templateContent, Map<String, String> baseParams) {
|
||||
this.templateContent = templateContent;
|
||||
this.baseParams = baseParams;
|
||||
}
|
||||
|
||||
public String getTemplateContent() { return templateContent; }
|
||||
public Map<String, String> getBaseParams() { return baseParams; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,723 @@
|
||||
{
|
||||
"ReportList": [
|
||||
{
|
||||
"desc": "统计数据",
|
||||
"inst": "01",
|
||||
"TrgOps": "96",
|
||||
"Select": "DataStatFileMap",
|
||||
"DataSetList": [
|
||||
"dsStatisticData",
|
||||
"dsStHarm",
|
||||
"dsStIHarm",
|
||||
"dsStMMXU",
|
||||
"dsStMSQI",
|
||||
],
|
||||
"LnInstList": [
|
||||
"最大值",
|
||||
"最小值",
|
||||
"平均值",
|
||||
"95值",
|
||||
"间谐波最大值",
|
||||
"间谐波最小值",
|
||||
"间谐波平均值",
|
||||
"间谐波95值",
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "波动闪变",
|
||||
"inst": "01",
|
||||
"TrgOps": "96",
|
||||
"Select": "FlickerFileMap",
|
||||
"DataSetList": [
|
||||
"dsFlickerData",
|
||||
"dsPST"
|
||||
],
|
||||
"LnInstList": [
|
||||
"波动闪变值"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "暂态事件",
|
||||
"inst": "01",
|
||||
"TrgOps": "96",
|
||||
"Select": "QVVR",
|
||||
"DataSetList": [
|
||||
"dsEveQVVR"
|
||||
],
|
||||
"LnInstList": [
|
||||
"电压变动A",
|
||||
"电压变动B",
|
||||
"电压变动C"
|
||||
]
|
||||
}
|
||||
],
|
||||
"LnClassList": [
|
||||
{
|
||||
"desc": "基本数据",
|
||||
"nameList": [
|
||||
"MMXU"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "序分量值",
|
||||
"nameList": [
|
||||
"MSQI"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "谐波/间谐波数据",
|
||||
"nameList": [
|
||||
"MHAI"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "波动闪变",
|
||||
"nameList": [
|
||||
"MFLK"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "电压变动",
|
||||
"nameList": [
|
||||
"QVVR"
|
||||
]
|
||||
}
|
||||
],
|
||||
"PhaseList": [
|
||||
{
|
||||
"desc": "无相别",
|
||||
"nameList": [
|
||||
"null"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "正序",
|
||||
"nameList": [
|
||||
"c1"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "负序",
|
||||
"nameList": [
|
||||
"c2"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "零序",
|
||||
"nameList": [
|
||||
"c3"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "A相",
|
||||
"nameList": [
|
||||
"phsA",
|
||||
"phsAHar"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "B相",
|
||||
"nameList": [
|
||||
"phsB",
|
||||
"phsBHar"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "C相",
|
||||
"nameList": [
|
||||
"phsC",
|
||||
"phsCHar"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "AB线",
|
||||
"nameList": [
|
||||
"phsAB",
|
||||
"phsABHar"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "BC线",
|
||||
"nameList": [
|
||||
"phsBC",
|
||||
"phsBCHar"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "CA线",
|
||||
"nameList": [
|
||||
"phsCA",
|
||||
"phsCAHar"
|
||||
]
|
||||
}
|
||||
],
|
||||
"MultiplierList": [
|
||||
{
|
||||
"multiplier": 1,
|
||||
"nameList": [
|
||||
"null"
|
||||
]
|
||||
},
|
||||
{
|
||||
"multiplier": 1000,
|
||||
"nameList": [
|
||||
"k"
|
||||
]
|
||||
}
|
||||
],
|
||||
"UnitList": [
|
||||
{
|
||||
"desc": "other",
|
||||
"nameList": [
|
||||
"null"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "v",
|
||||
"nameList": [
|
||||
"V"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "a",
|
||||
"nameList": [
|
||||
"A"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "w",
|
||||
"nameList": [
|
||||
"W",
|
||||
"VAr",
|
||||
"VA"
|
||||
]
|
||||
}
|
||||
],
|
||||
"TypeList": [
|
||||
{
|
||||
"desc": "值",
|
||||
"nameList": [
|
||||
"mag"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "角度",
|
||||
"nameList": [
|
||||
"ang"
|
||||
]
|
||||
}
|
||||
],
|
||||
"DataObjectsList": [
|
||||
{
|
||||
"desc": "非间谐波数据",
|
||||
"LnInstList": [
|
||||
"最大值",
|
||||
"最小值",
|
||||
"平均值",
|
||||
"95值"
|
||||
],
|
||||
"ObjectList": [
|
||||
{
|
||||
"desc": "频率",
|
||||
"nameList": [
|
||||
"Hz"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "线电压总有效值",
|
||||
"nameList": [
|
||||
"PPV"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "相电压总有效值",
|
||||
"nameList": [
|
||||
"PhV"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "电流总有效值",
|
||||
"nameList": [
|
||||
"A"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "有功功率",
|
||||
"nameList": [
|
||||
"W"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "无功功率",
|
||||
"nameList": [
|
||||
"VAr"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "视在功率",
|
||||
"nameList": [
|
||||
"VA"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "功率因数",
|
||||
"nameList": [
|
||||
"PF"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "位移功率因数",
|
||||
"nameList": [
|
||||
"DF"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "三相总有功功率",
|
||||
"nameList": [
|
||||
"TotW"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "三相总无功功率",
|
||||
"nameList": [
|
||||
"TotVAr"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "三相总视在功率",
|
||||
"nameList": [
|
||||
"TotVA"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "三相功率因数",
|
||||
"nameList": [
|
||||
"TotPF"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "三相位移功率因数",
|
||||
"nameList": [
|
||||
"TotDF"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "频率偏差",
|
||||
"nameList": [
|
||||
"HzDev"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "相电压偏差",
|
||||
"nameList": [
|
||||
"PhVDev"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "线电压偏差",
|
||||
"nameList": [
|
||||
"PPVDev"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "正序负序和零序电压",
|
||||
"nameList": [
|
||||
"SeqV"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "正序负序和零序电流",
|
||||
"nameList": [
|
||||
"SeqA"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "电压负序不平衡度",
|
||||
"nameList": [
|
||||
"ImbNgV"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "电流负序不平衡度",
|
||||
"nameList": [
|
||||
"ImbNgA"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "电压零序不平衡度",
|
||||
"nameList": [
|
||||
"ImbZroV"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "电流零序不平衡度",
|
||||
"nameList": [
|
||||
"ImbZroA"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "相电压谐波总畸变率",
|
||||
"nameList": [
|
||||
"ThdPhV"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "相电压总偶次谐波畸变率",
|
||||
"nameList": [
|
||||
"ThdEvnPhV"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "相电压总奇次谐波畸变率",
|
||||
"nameList": [
|
||||
"ThdOddPhV"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "线电压谐波总畸变率",
|
||||
"nameList": [
|
||||
"ThdPPV"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "线电压总偶次谐波畸变率",
|
||||
"nameList": [
|
||||
"ThdEvnPPV"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "线电压总奇次谐波畸变率",
|
||||
"nameList": [
|
||||
"ThdOddPPV"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "相电压谐波含有率序列",
|
||||
"baseflag": 1,
|
||||
"basecount": 49,
|
||||
"nameList": [
|
||||
"HRPhV",
|
||||
"HPhVMag"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "线电压谐波含有率序列",
|
||||
"baseflag": 1,
|
||||
"basecount": 49,
|
||||
"nameList": [
|
||||
"HRPPV"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "电流总谐波畸变率",
|
||||
"nameList": [
|
||||
"ThdA"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "电流总偶次谐波畸变率",
|
||||
"nameList": [
|
||||
"ThdEvnA"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "电流总奇次谐波畸变率",
|
||||
"nameList": [
|
||||
"ThdOddA"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "谐波电流有效值序列",
|
||||
"baseflag": 1,
|
||||
"basecount": 49,
|
||||
"nameList": [
|
||||
"HA",
|
||||
"HAMag"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "谐波电压有效值序列",
|
||||
"baseflag": 1,
|
||||
"basecount": 49,
|
||||
"nameList": [
|
||||
"HPhV"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "2~50次谐波有功功率序列",
|
||||
"baseflag": 1,
|
||||
"basecount": 49,
|
||||
"nameList": [
|
||||
"HW"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "2~50次谐波无功功率序列",
|
||||
"baseflag": 1,
|
||||
"basecount": 49,
|
||||
"nameList": [
|
||||
"HVAr"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "2~50次谐波视在功率序列",
|
||||
"baseflag": 1,
|
||||
"basecount": 49,
|
||||
"nameList": [
|
||||
"HVA"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "三相总谐波视在功率",
|
||||
"nameList": [
|
||||
"TotHVA"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "三相总谐波无功功率",
|
||||
"nameList": [
|
||||
"TotHVAr"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "三相总谐波有功功率",
|
||||
"nameList": [
|
||||
"TotHW"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "相电压基波有效值",
|
||||
"baseflag": 2,
|
||||
"queuecount": 49,
|
||||
"nameList": [
|
||||
"HFundPhV",
|
||||
"FundPhV"
|
||||
],
|
||||
"queueList":[
|
||||
"HPhV"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "线电压基波有效值",
|
||||
"baseflag": 2,
|
||||
"queuecount": 49,
|
||||
"nameList": [
|
||||
"HFundPPV"
|
||||
],
|
||||
"queueList":[
|
||||
"HPPV"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "电流基波有效值",
|
||||
"baseflag": 2,
|
||||
"queuecount": 49,
|
||||
"nameList": [
|
||||
|
||||
],
|
||||
"queueList":[
|
||||
"HA"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "基波有功功率",
|
||||
"baseflag": 2,
|
||||
"queuecount": 49,
|
||||
"nameList": [
|
||||
|
||||
],
|
||||
"queueList":[
|
||||
"HW"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "基波无功功率",
|
||||
"baseflag": 2,
|
||||
"queuecount": 49,
|
||||
"nameList": [
|
||||
|
||||
],
|
||||
"queueList":[
|
||||
"HVAr"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "基波视在功率",
|
||||
"baseflag": 2,
|
||||
"queuecount": 49,
|
||||
"nameList": [
|
||||
|
||||
],
|
||||
"queueList":[
|
||||
"HVA"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "间谐波数据",
|
||||
"LnInstList": [
|
||||
"间谐波最大值",
|
||||
"间谐波最小值",
|
||||
"间谐波平均值",
|
||||
"间谐波95值",
|
||||
"间谐波实时数据"
|
||||
],
|
||||
"ObjectList": [
|
||||
{
|
||||
"desc": "相电压间谐波含有率序列",
|
||||
"baseflag": 1,
|
||||
"basecount": 50,
|
||||
"nameList": [
|
||||
"HPhV"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "线电压间谐波含有率序列",
|
||||
"baseflag": 1,
|
||||
"basecount": 50,
|
||||
"nameList": [
|
||||
"HRPPV"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "间谐波电流有效值序列",
|
||||
"baseflag": 1,
|
||||
"basecount": 50,
|
||||
"nameList": [
|
||||
"HA"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "间谐波电压有效值序列",
|
||||
"baseflag": 1,
|
||||
"basecount": 50,
|
||||
"nameList": [
|
||||
"HRPhV"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "电压变动",
|
||||
"LnInstList": [
|
||||
"电压变动A",
|
||||
"电压变动B",
|
||||
"电压变动C",
|
||||
],
|
||||
"ObjectList": [
|
||||
{
|
||||
"desc": "电压扰动事件启动",
|
||||
"nameList": [
|
||||
"VarStr"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "电压暂降事件启动",
|
||||
"nameList": [
|
||||
"DipStr"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "电压暂升事件启动",
|
||||
"nameList": [
|
||||
"SwlStr"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "电压中断事件启动",
|
||||
"nameList": [
|
||||
"IntrStr"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "电压扰动事件特征幅值",
|
||||
"nameList": [
|
||||
"VVa"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "电压扰动事件持续时间",
|
||||
"nameList": [
|
||||
"VVaTm"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "电压暂降启动定值",
|
||||
"nameList": [
|
||||
"DipStrVal"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "电压暂升启动定值",
|
||||
"nameList": [
|
||||
"SwlStrVal"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "电压中断启动定值",
|
||||
"nameList": [
|
||||
"IntrStrVal"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "其余数据",
|
||||
"LnInstList": [
|
||||
"波动闪变值",
|
||||
"录波文件"
|
||||
],
|
||||
"ObjectList": [
|
||||
{
|
||||
"desc": "线电压短时闪变值",
|
||||
"nameList": [
|
||||
"PPPst"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "相电压短时闪变值",
|
||||
"nameList": [
|
||||
"PhPst"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "线电压长时闪变值",
|
||||
"nameList": [
|
||||
"PPPlt"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "相电压长时闪变值",
|
||||
"nameList": [
|
||||
"PhPlt"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "线电压电压变动幅值",
|
||||
"nameList": [
|
||||
"PPFluc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "相电压电压变动幅值",
|
||||
"nameList": [
|
||||
"PhFluc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "线电压电压变动频度",
|
||||
"nameList": [
|
||||
"PPFlucf"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "相电压电压变动频度",
|
||||
"nameList": [
|
||||
"PhFlucf"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,248 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--注:#7#代表通配符,用于通配数据类型 采用8421码,8-CP95 4-最小值 2-最大值 1-平均值-->
|
||||
<!-- %2,50%代表通配符,用于通配谐波数据,采用范围编码,第一个数字表示谐波起始号,第二个数字谐波结束号-->
|
||||
<!-- SEQ=后面的值采用8421码 8-T 4-C 2-B 1-A-->
|
||||
<!--注:type类型:0-DataType 1-监测点 2-剔除标记 3-发生时刻,毫秒 4-数据链表 5-相位 6-值索引 9-实时SOE事件-->
|
||||
<JSConfigTemplate version="2023-10-31" author="ww" SelectStat="JiangSu" SelectReal="Kafka Producer" desc="默认">
|
||||
<!--注:暂态事件解析规则配置 Flag:0-不分相 1-分相 如果Flag=0 A,B,C配置成一样,如果Flag=1,A,B,C根据实际配置-->
|
||||
<WavePhasic Flag="1" A="QVVR0" B="QVVR1" C="QVVR2" />
|
||||
<!--注:暂态事件持续事件单位:0-毫秒 1-秒-->
|
||||
<UnitOfTime Unit="0" />
|
||||
<!--注:上送值的时间:UTC-UTC时间 beijing-北京时间-->
|
||||
<ValueOfTime Unit="UTC" />
|
||||
<!--注:录波文件的时间:UTC-UTC时间 beijing-北京时间-->
|
||||
<ComtradeFile WaveTimeFlag="beijing" />
|
||||
<IED name="PQMonitor" desc="电能质量监测装置" />
|
||||
<LDevice Prefix="PQM" desc="监测点" />
|
||||
<ReportMap>
|
||||
<!--用于映射需要触发那些报告-->
|
||||
<!--ReportControl:ID,RCBName,intgPd,dchg,qchg,dupd,period,gi,issuffixed,seqNum,timeStamp,reasonCode,dataSet,dataRef,bufOvfl,entryID,configRef,segmentation,FlickerFlag-->
|
||||
<!--600/60秒,即10分钟/1分钟-->
|
||||
<!--0,0,0,1,0为周期触发报告-->
|
||||
<!--1,0,0,0,0为变位触发报告-->
|
||||
<!--0,0,1,0,0为更新触发报告-->
|
||||
<ReportStat>
|
||||
<Report ReportControl="LLN0$BR$brcbFlickerData,600,0,0,1,0,0,yes,1,1,1,1,1,0,1,1,1,3,1" />
|
||||
<Report ReportControl="LLN0$BR$brcbStatisticData,60,0,0,1,0,0,yes,1,1,1,1,1,0,1,1,1,3,0" />
|
||||
</ReportStat>
|
||||
<ReportReal>
|
||||
<Report ReportControl="LLN0$RP$brcbFlickerData,600,0,0,0,1,0,yes,1,1,1,1,1,0,1,1,1,3,1" />
|
||||
<Report ReportControl="LLN0$RP$urcbRealData,3,0,0,0,1,0,yes,1,1,1,1,1,0,1,1,1,3,0" />
|
||||
</ReportReal>
|
||||
<ReportEvent>
|
||||
<Report ReportControl="LLN0$BR$brcbQVVR,60,1,0,0,0,0,yes,1,1,1,1,1,0,1,1,1,8,1,0"/>
|
||||
<Report ReportControl="LLN0$BR$brcbRDRE,60,1,0,0,0,0,yes,1,1,1,1,1,0,1,1,1,8,1,0"/>
|
||||
<Report ReportControl="LLN0$BR$brcbGGIO,60,1,0,0,0,0,yes,1,1,1,1,1,0,1,1,1,8,1,0"/>
|
||||
</ReportEvent>
|
||||
</ReportMap>
|
||||
<Topic name="HISDATA" desc="历史稳态数据">
|
||||
<DataType name="DATA_TYPE" value="02" desc="历史短时闪变数据" type="0">
|
||||
<Monitor name="MONITOR" desc="监测点" type="1">
|
||||
<Item name="TIME" desc="发生时刻" type="3" />
|
||||
<Item name="F_S" desc="短时闪变和波动" type="4" >
|
||||
<Sequence name="SEQ" value="7" desc="相别" type="5" >
|
||||
<Value name="PST" desc="短时闪变" type="6" DO="" DA="" />
|
||||
<Value name="FLUC" desc="电压波动幅度" type="6" DO="" DA="" />
|
||||
<Value name="FLUCF" desc="电压波动频度" type="6" DO="" DA="" />
|
||||
</Sequence>
|
||||
</Item>
|
||||
</Monitor>
|
||||
</DataType>
|
||||
<DataType name="DATA_TYPE" value="04" desc="历史长时闪变数据" type="0">
|
||||
<Monitor name="MONITOR" desc="监测点" type="1">
|
||||
<Item name="TIME" desc="发生时刻" type="3" />
|
||||
<Item name="F_L" desc="闪变" type="4" >
|
||||
<Sequence name="SEQ" value="7" desc="相别" type="5" >
|
||||
<Value name="PLT" desc="长时闪变" type="6" DO="" DA="" />
|
||||
</Sequence>
|
||||
</Item>
|
||||
</Monitor>
|
||||
</DataType>
|
||||
<DataType name="DATA_TYPE" value="03" desc="历史暂态数据" type="0" >
|
||||
<Monitor name="MONITOR" desc="监测点" type="1" >
|
||||
<Item name="VOLTAGE" desc="暂态指标" type="4" >
|
||||
<Sequence name="SEQ" value="7" desc="相别" type="5">
|
||||
<Value name="MAG" desc="残余电压" type="6" DO="" DA="" />
|
||||
<Value name="DUR" desc="持续时间" type="6" DO="" DA="" />
|
||||
<Value name="SEQ" desc="相别" type="6" DO="" DA="" />
|
||||
<Value name="STARTTIME" desc="开始时间" type="6" DO="" DA="" />
|
||||
<Value name="ENDTIME" desc="结束时间" type="6" DO="" DA="" />
|
||||
<Value name="DISKIND" desc="暂降类型" type="6" DO="" DA="" />
|
||||
<Value name="WAVEFILE" desc="波形文件名称" type="6" DO="" DA="" />
|
||||
</Sequence>
|
||||
</Item>
|
||||
</Monitor>
|
||||
</DataType>
|
||||
<DataType name="DATA_TYPE" value="01" desc="历史稳态数据" type="0">
|
||||
<Monitor name="MONITOR" desc="监测点" type="1">
|
||||
<Item name="FLAG" value="0" desc="剔除标记" type="2" />
|
||||
<Item name="TIME" desc="发生时刻" type="3" />
|
||||
<Item name="V" desc="电压" type="4" >
|
||||
<!--电压V部分(A-C相)-->
|
||||
<Sequence name="SEQ" value="7" desc="相别(A-C相)" type="5">
|
||||
<!--注:Coefficient 值系数,转成kafka的时候乘以系数;BaseFlag 基础数据标志 0或没有-非基础数据 1-基础数据;LimitUp 数据合理上限,如果包含*%UN字符表示需要乘以电压等级电压,如果包含*%U字符表示需要乘以电压等级/1.732电压例如:220kV则相电压上限为1.5*220;LimitDown 数据合理下限 如果包含*%U字符表示需要乘以电压等级电压-->
|
||||
<Value name="G_DELTA_V" desc="电压偏差95值" type="6" DO="" DA="" BaseFlag="1" LimitUp="20" LimitDown="-20" Coefficient="1"/>
|
||||
<Value name="DELTA_V" desc="电压偏差平均值" type="6" DO="" DA="" BaseFlag="1" LimitUp="20" LimitDown="-20" Coefficient="1"/>
|
||||
<Value name="MAX_DELTA_V" desc="电压偏差最大值" type="6" DO="" DA="" BaseFlag="1" LimitUp="20" LimitDown="-20" Coefficient="1"/>
|
||||
<Value name="MIN_DELTA_V" desc="电压偏差最小值" type="6" DO="" DA="" BaseFlag="1" LimitUp="20" LimitDown="-20" Coefficient="1"/>
|
||||
<Value name="G_VRMS" desc="电压有效值95值" type="6" DO="" DA="" BaseFlag="1" LimitUp="0*%U" LimitDown="150*%U" />
|
||||
<Value name="VRMS" desc="电压有效值平均值" type="6" DO="" DA="" BaseFlag="1" LimitUp="0*%U" LimitDown="150*%U" />
|
||||
<Value name="MAX_VRMS" desc="电压有效值最大值" type="6" DO="" DA="" BaseFlag="1" LimitUp="0*%U" LimitDown="150*%U" />
|
||||
<Value name="MIN_VRMS" desc="电压有效值最小值" type="6" DO="" DA="" BaseFlag="1" LimitUp="0*%U" LimitDown="150*%U" />
|
||||
<Value name="VTHD" desc="电压总谐波畸变率平均值" type="6" DO="" DA="" />
|
||||
<Value name="MAX_VTHD" desc="电压总谐波畸变率最大值" type="6" DO="" DA="" />
|
||||
<Value name="MIN_VTHD" desc="电压总谐波畸变率最小值" type="6" DO="" DA="" />
|
||||
<Value name="MIN_VFUND_ANGLE" desc="基波电压相角最小值" type="6" DO="" DA="" />
|
||||
<Value name="V1" desc="基波电压有效值平均值" type="6" DO="" DA="" />
|
||||
<Value name="MAX_V1" desc="基波电压有效值最大值" type="6" DO="" DA="" />
|
||||
<Value name="MIN_V1" desc="基波电压有效值最小值" type="6" DO="" DA="" />
|
||||
<Value name="SV_%0,49%" desc="间谐波电压含有率(f25-2475)平均值" type="6" DO="" DA="" />
|
||||
<Value name="MAX_SV_%0,49%" desc="间谐波电压含有率(f25-2475)最大值" type="6" DO="" DA="" />
|
||||
<Value name="MIN_SV_%0,49%" desc="间谐波电压含有率(f25-2475)最小值" type="6" DO="" DA="" />
|
||||
<Value name="V%2,50%" desc="谐波电压含有率(2-50)平均值" type="6" DO="" DA="" />
|
||||
<Value name="MAX_V%2,50%" desc="谐波电压含有率(2-50)最大值" type="6" DO="" DA="" />
|
||||
<Value name="MIN_V%2,50%" desc="谐波电压含有率(2-50)最小值" type="6" DO="" DA="" />
|
||||
<Value name="VA%2,50%" desc="谐波电压相角(2-50)平均值" type="6" DO="" DA="" />
|
||||
</Sequence>
|
||||
<Sequence name="SEQ" value="8" desc="相别(T相)" type="5">
|
||||
<!--电压V部分(T相)-->
|
||||
<Value name="VNSEQ" desc="负序电压平均值" type="6" DO="" DA="" />
|
||||
<Value name="MAX_VNSEQ" desc="负序电压最大值" type="6" DO="" DA="" />
|
||||
<Value name="MIN_VNSEQ" desc="负序电压最小值" type="6" DO="" DA="" />
|
||||
<Value name="V_UNBAN" desc="负序电压不平衡平均值" type="6" DO="" DA="" />
|
||||
<Value name="MAX_V_UNBAN" desc="负序电压不平衡最大值" type="6" DO="" DA="" />
|
||||
<Value name="MIN_V_UNBAN" desc="负序电压不平衡最小值" type="6" DO="" DA="" />
|
||||
<Value name="VZSEQ" desc="零序电压平均值" type="6" DO="" DA="" />
|
||||
<Value name="MAX_VZSEQ" desc="零序电压最大值" type="6" DO="" DA="" />
|
||||
<Value name="MIN_VZSEQ" desc="零序电压最小值" type="6" DO="" DA="" />
|
||||
<Value name="VZSEQ_UNBAN" desc="零序电压不平衡平均值" type="6" DO="" DA="" />
|
||||
<Value name="MAX_VZSEQ_UNBAN" desc="零序电压不平衡最大值" type="6" DO="" DA="" />
|
||||
<Value name="MIN_VZSEQ_UNBAN" desc="零序电压不平衡最小值" type="6" DO="" DA="" />
|
||||
<Value name="G_FREQ" desc="频率95值" type="6" DO="" DA="" />
|
||||
<Value name="FREQ" desc="频率平均值" type="6" DO="" DA="" />
|
||||
<Value name="MAX_FREQ" desc="频率最大值" type="6" DO="" DA="" />
|
||||
<Value name="MIN_FREQ" desc="频率最小值" type="6" DO="" DA="" />
|
||||
<Value name="G_DELTA_FREQ" desc="频率偏差95值" type="6" DO="" DA="" />
|
||||
<Value name="DELTA_FREQ" desc="频率偏差平均值" type="6" DO="" DA="" />
|
||||
<Value name="MAX_DELTA_FREQ" desc="频率偏差最大值" type="6" DO="" DA="" />
|
||||
<Value name="MIN_DELTA_FREQ" desc="频率偏差最小值" type="6" DO="" DA="" />
|
||||
<Value name="VPSEQ" desc="正序电压平均值" type="6" DO="" DA="" />
|
||||
<Value name="MAX_VPSEQ" desc="正序电压最大值" type="6" DO="" DA="" />
|
||||
<Value name="MIN_VPSEQ" desc="正序电压最小值" type="6" DO="" DA="" />
|
||||
</Sequence>
|
||||
</Item>
|
||||
<Item name="I" desc="电流" type="4" >
|
||||
<!--电流I部分(A-C相)-->
|
||||
<Sequence name="SEQ" value="7" desc="相别(A-C相)" type="5">
|
||||
<Value name="G_IRMS" desc="电流有效值95值" type="6" DO="" DA="" />
|
||||
<Value name="IRMS" desc="电流有效值平均值" type="6" DO="" DA="" />
|
||||
<Value name="MAX_IRMS" desc="电流有效值最大值" type="6" DO="" DA="" />
|
||||
<Value name="MIN_IRMS" desc="电流有效值最小值" type="6" DO="" DA="" />
|
||||
<Value name="I1" desc="基波电流有效值平均值" type="6" DO="" DA="" />
|
||||
<Value name="MAX_I1" desc="基波电流有效值最大值" type="6" DO="" DA="" />
|
||||
<Value name="MIN_I1" desc="基波电流有效值最小值" type="6" DO="" DA="" />
|
||||
<Value name="SI_%0,49%" desc="间谐波电流幅值(f25-2475)平均值" type="6" DO="" DA="" />
|
||||
<Value name="MAX_SI_%0,49%" desc="间谐波电流幅值(f25-2475)最大值" type="6" DO="" DA="" />
|
||||
<Value name="MIN_SI_%0,49%" desc="间谐波电流幅值(f25-2475)最小值" type="6" DO="" DA="" />
|
||||
<Value name="I%2,50%" desc="谐波电流幅值(2-50)平均值" type="6" DO="" DA="" />
|
||||
<Value name="MAX_I%2,50%" desc="谐波电流幅值(2-50)最大值" type="6" DO="" DA="" />
|
||||
<Value name="MIN_I%2,50%" desc="谐波电流幅值(2-50)最小值" type="6" DO="" DA="" />
|
||||
</Sequence>
|
||||
<!--电流I部分(T相)-->
|
||||
<Sequence name="SEQ" value="8" desc="相别(T相)" type="5">
|
||||
<Value name="INSEQ" desc="负序电流平均值" type="6" DO="" DA="" />
|
||||
<Value name="MAX_INSEQ" desc="负序电流最大值" type="6" DO="" DA="" />
|
||||
<Value name="MIN_INSEQ" desc="负序电流最小值" type="6" DO="" DA="" />
|
||||
<Value name="I_UNBAN" desc="负序电流不平衡平均值" type="6" DO="" DA="" />
|
||||
<Value name="MAX_I_UNBAN" desc="负序电流不平衡最大值" type="6" DO="" DA="" />
|
||||
<Value name="MIN_I_UNBAN" desc="负序电流不平衡最小值" type="6" DO="" DA="" />
|
||||
<Value name="IZSEQ" desc="零序电流平均值" type="6" DO="" DA="" />
|
||||
<Value name="MAX_IZSEQ" desc="零序电流最大值" type="6" DO="" DA="" />
|
||||
<Value name="MIN_IZSEQ" desc="零序电流最小值" type="6" DO="" DA="" />
|
||||
<Value name="IZSEQ_UNBAN" desc="零序电流不平衡平均值" type="6" DO="" DA="" />
|
||||
<Value name="MAX_IZSEQ_UNBAN" desc="零序电流不平衡最大值" type="6" DO="" DA="" />
|
||||
<Value name="MIN_IZSEQ_UNBAN" desc="零序电流不平衡最小值" type="6" DO="" DA="" />
|
||||
<Value name="IPSEQ" desc="正序电流平均值" type="6" DO="" DA="" />
|
||||
<Value name="MAX_IPSEQ" desc="正序电流最大值" type="6" DO="" DA="" />
|
||||
<Value name="MIN_IPSEQ" desc="正序电流最小值" type="6" DO="" DA="" />
|
||||
</Sequence>
|
||||
</Item>
|
||||
<Item name="PQ" desc="功率" type="4" >
|
||||
<!--功率PQ部分(A-C相)-->
|
||||
<Sequence name="SEQ" value="7" desc="相别(A-C相)" type="5">
|
||||
<Value name="PF" desc="功率因数平均值" type="6" DO="" DA="" />
|
||||
<Value name="MAX_PF" desc="功率因数最大值" type="6" DO="" DA="" />
|
||||
<Value name="MIN_PF" desc="功率因数最小值" type="6" DO="" DA="" />
|
||||
<Value name="DF" desc="基波功率因数平均值" type="6" DO="" DA="" />
|
||||
<Value name="MAX_DF" desc="基波功率因数最大值" type="6" DO="" DA="" />
|
||||
<Value name="P_FUND" desc="基波有功功率平均值" type="6" DO="" DA="" />
|
||||
<Value name="MAX_P_FUND" desc="基波有功功率最大值" type="6" DO="" DA="" />
|
||||
<Value name="MIN_P_FUND" desc="基波有功功率最小值" type="6" DO="" DA="" />
|
||||
<Value name="G_S" desc="视在功率95值" type="6" DO="" DA="" />
|
||||
<Value name="S" desc="视在功率平均值" type="6" DO="" DA="" />
|
||||
<Value name="MAX_S" desc="视在功率最大值" type="6" DO="" DA="" />
|
||||
<Value name="MIN_S" desc="视在功率最小值" type="6" DO="" DA="" />
|
||||
<Value name="G_Q" desc="无功功率95值" type="6" DO="" DA="" />
|
||||
<Value name="Q" desc="无功功率平均值" type="6" DO="" DA="" />
|
||||
<Value name="MAX_Q" desc="无功功率最大值" type="6" DO="" DA="" />
|
||||
<Value name="MIN_Q" desc="无功功率最小值" type="6" DO="" DA="" />
|
||||
<Value name="P%2,50%" desc="谐波有功功率(2-50)平均值" type="6" DO="" DA="" />
|
||||
<Value name="MAX_P%2,50%" desc="谐波有功功率(2-50)最大值" type="6" DO="" DA="" />
|
||||
<Value name="MIN_P%2,50%" desc="谐波有功功率(2-50)最小值" type="6" DO="" DA="" />
|
||||
<Value name="G_P" desc="有功功率95值" type="6" DO="" DA="" />
|
||||
<Value name="P" desc="有功功率平均值" type="6" DO="" DA="" />
|
||||
<Value name="MAX_P" desc="有功功率最大值" type="6" DO="" DA="" />
|
||||
<Value name="MIN_P" desc="有功功率最小值" type="6" DO="" DA="" />
|
||||
</Sequence>
|
||||
<!--功率PQ部分(T相)-->
|
||||
<Sequence name="SEQ" value="8" desc="相别(T相)" type="5">
|
||||
<Value name="PF" desc="总功率因数平均值" type="6" DO="" DA="" />
|
||||
<Value name="MAX_PF" desc="总功率因数最大值" type="6" DO="" DA="" />
|
||||
<Value name="MIN_PF" desc="总功率因数最小值" type="6" DO="" DA="" />
|
||||
<Value name="DF" desc="总基波功率因数平均值" type="6" DO="" DA="" />
|
||||
<Value name="MAX_DF" desc="总基波功率因数最大值" type="6" DO="" DA="" />
|
||||
<Value name="MIN_DF" desc="总基波功率因数最小值" type="6" DO="" DA="" />
|
||||
<Value name="S" desc="总视在功率平均值" type="6" DO="" DA="" />
|
||||
<Value name="MAX_S" desc="总视在功率最大值" type="6" DO="" DA="" />
|
||||
<Value name="MIN_S" desc="总视在功率最小值" type="6" DO="" DA="" />
|
||||
<Value name="Q" desc="总无功功率平均值" type="6" DO="" DA="" />
|
||||
<Value name="MAX_Q" desc="总无功功率最大值" type="6" DO="" DA="" />
|
||||
<Value name="MIN_Q" desc="总无功功率最小值" type="6" DO="" DA="" />
|
||||
<Value name="P" desc="总有功功率平均值" type="6" DO="" DA="" />
|
||||
<Value name="MAX_P" desc="总有功功率最大值" type="6" DO="" DA="" />
|
||||
<Value name="MIN_P" desc="总有功功率最小值" type="6" DO="" DA="" />
|
||||
</Sequence>
|
||||
</Item>
|
||||
</Monitor>
|
||||
</DataType>
|
||||
</Topic>
|
||||
<Topic name="SOEDATA" desc="告警SOE">
|
||||
<SOE name="ThdVVal" desc="电压总畸变率越限告警" type="9" DO="" DA="" />
|
||||
<SOE name="ThdAVal" desc="电流总畸变率越限告警" type="9" DO="" DA="" />
|
||||
<SOE name="HToddVVal" desc="奇次谐波电压含有率越限告警" type="9" DO="" DA="" />
|
||||
<SOE name="HTeddVVal" desc="偶次谐波电压含有率越限告警" type="9" DO="" DA="" />
|
||||
<SOE name="H2AVal" desc="2次谐波电流越限告警" type="9" DO="" DA="" />
|
||||
<SOE name="H3AVal" desc="3次谐波电流越限告警" type="9" DO="" DA="" />
|
||||
<SOE name="H4AVal" desc="4次谐波电流越限告警" type="9" DO="" DA="" />
|
||||
<SOE name="H5AVal" desc="5次谐波电流越限告警" type="9" DO="" DA="" />
|
||||
<SOE name="H7AVal" desc="7次谐波电流越限告警" type="9" DO="" DA="" />
|
||||
<SOE name="H9AVal" desc="9次谐波电流越限告警" type="9" DO="" DA="" />
|
||||
<SOE name="H11AVal" desc="11次谐波电流越限告警" type="9" DO="" DA="" />
|
||||
<SOE name="H13AVal" desc="13次谐波电流越限告警" type="9" DO="" DA="" />
|
||||
<SOE name="ImbNgVFVal" desc="电压负序不平衡度越限告警" type="9" DO="" DA="" />
|
||||
<SOE name="ImbNgAFVal" desc="电流负序不平衡度越限告警" type="9" DO="" DA="" />
|
||||
<SOE name="OvHzStrVal" desc="频率高越限告警" type="9" DO="" DA="" />
|
||||
<SOE name="UnHzStrVal" desc="频率低越限告警" type="9" DO="" DA="" />
|
||||
<SOE name="LRVInterrup" desc="长期电压中断告警" type="9" DO="" DA="" />
|
||||
<SOE name="LRVSwell" desc="电压上偏差越限告警" type="9" DO="" DA="" />
|
||||
<SOE name="LRVSag" desc="电压下偏差越限告警" type="9" DO="" DA="" />
|
||||
<SOE name="PhPstVal" desc="短时闪变越限告警" type="9" DO="" DA="" />
|
||||
<SOE name="PhPltVal" desc="长时闪变越限告警" type="9" DO="" DA="" />
|
||||
<SOE name="PhyStateFault" desc="终端运行状态:故障" type="9" DO="" DA="" />
|
||||
<SOE name="PhyStateRun" desc="终端运行状态:运行" type="9" DO="" DA="" />
|
||||
<SOE name="PwrUp" desc="终端上电" type="9" DO="" DA="" />
|
||||
<SOE name="PwrDn" desc="终端掉电" type="9" DO="" DA="" />
|
||||
<SOE name="CommInterrupt" desc="终端通信中断" type="9" DO="" DA="" />
|
||||
<SOE name="CommResume" desc="终端通信恢复" type="9" DO="" DA="" />
|
||||
</Topic>
|
||||
</JSConfigTemplate>
|
||||
127
tools/mms-mapping/src/main/resources/template/默认规则.txt
Normal file
127
tools/mms-mapping/src/main/resources/template/默认规则.txt
Normal file
@@ -0,0 +1,127 @@
|
||||
<Value name="相电压短时闪变值波动闪变值值" desc="短时闪变" type="6" DO="MFLK1$MX$PhPst" DA="phs*$cVal$mag$f" />
|
||||
<Value name="相电压电压变动幅值波动闪变值值" desc="电压波动幅值" type="6" DO="MMXU2$MX$FLUC" DA="phs*$cVal$mag$f" />
|
||||
<Value name="相电压电压变动频度波动闪变值值" desc="电压波动频度" type="6" DO="MMXU2$MX$FLUCCF" DA="phs*$cVal$mag$f" />
|
||||
<Value name="相电压长时闪变值波动闪变值值" desc="长时闪变" type="6" DO="MFLK2$MX$PhPlt" DA="phs*$cVal$mag$f" />
|
||||
<Value name="电压扰动事件特征幅值值" desc="残余电压" type="6" DO="QVVR0$MX$VVa" DA="mag$f" />
|
||||
<Value name="电压扰动事件持续时间" desc="持续时间" type="6" DO="QVVR0$MX$VVaTm" DA="mag$f" />
|
||||
<Value name="SEQ" desc="相别" type="6" DO="" DA="" />
|
||||
<Value name="STARTTIME" desc="开始时间" type="6" DO="" DA="" />
|
||||
<Value name="ENDTIME" desc="结束时间" type="6" DO="" DA="" />
|
||||
<Value name="DISKIND" desc="暂降类型" type="6" DO="" DA="" />
|
||||
<Value name="WAVEFILE" desc="波形文件名称" type="6" DO="" DA="" />
|
||||
<Value name="相电压偏差95值值" desc="电压偏差95值" type="6" DO="MMXU5$MX$VolDev" DA="phs*$cVal$mag$f" BaseFlag="1" LimitUp="20" LimitDown="-20" Coefficient="1"/>
|
||||
<Value name="相电压偏差平均值值" desc="电压偏差平均值" type="6" DO="MMXU2$MX$VolDev" DA="phs*$cVal$mag$f" BaseFlag="1" LimitUp="20" LimitDown="-20" Coefficient="1"/>
|
||||
<Value name="相电压偏差最大值值" desc="电压偏差最大值" type="6" DO="MMXU3$MX$VolDev" DA="phs*$cVal$mag$f" BaseFlag="1" LimitUp="20" LimitDown="-20" Coefficient="1"/>
|
||||
<Value name="相电压偏差最小值值" desc="电压偏差最小值" type="6" DO="MMXU4$MX$VolDev" DA="phs*$cVal$mag$f" BaseFlag="1" LimitUp="20" LimitDown="-20" Coefficient="1"/>
|
||||
<Value name="相电压总有效值95值值" desc="电压有效值95值" type="6" DO="MMXU5$MX$PhV" DA="phs*$cVal$mag$f" BaseFlag="1" LimitUp="0*%U" LimitDown="150*%U" />
|
||||
<Value name="相电压总有效值平均值值" desc="电压有效值平均值" type="6" DO="MMXU2$MX$PhV" DA="phs*$cVal$mag$f" BaseFlag="1" LimitUp="0*%U" LimitDown="150*%U" />
|
||||
<Value name="相电压总有效值最大值值" desc="电压有效值最大值" type="6" DO="MMXU3$MX$PhV" DA="phs*$cVal$mag$f" BaseFlag="1" LimitUp="0*%U" LimitDown="150*%U" />
|
||||
<Value name="相电压总有效值最小值值" desc="电压有效值最小值" type="6" DO="MMXU4$MX$PhV" DA="phs*$cVal$mag$f" BaseFlag="1" LimitUp="0*%U" LimitDown="150*%U" />
|
||||
<Value name="相电压谐波总畸变率平均值值" desc="电压总谐波畸变率平均值" type="6" DO="MHAI2$MX$ThdPhV" DA="phs*$cVal$mag$f" />
|
||||
<Value name="相电压谐波总畸变率最大值值" desc="电压总谐波畸变率最大值" type="6" DO="MHAI3$MX$ThdPhV" DA="phs*$cVal$mag$f" />
|
||||
<Value name="相电压谐波总畸变率最小值值" desc="电压总谐波畸变率最小值" type="6" DO="MHAI4$MX$ThdPhV" DA="phs*$cVal$mag$f" />
|
||||
<Value name="MIN_VFUND_ANGLE" desc="基波电压相角最小值" type="6" DO="MHAI4$MX$FundPhVAng" DA="phs*$cVal$ang$f" />
|
||||
<Value name="相电压基波有效值平均值值" desc="基波电压有效值平均值" type="6" DO="MHAI2$MX$FundPhV" DA="phs*$cVal$mag$f" />
|
||||
<Value name="相电压基波有效值最大值值" desc="基波电压有效值最大值" type="6" DO="MHAI3$MX$FundPhV" DA="phs*$cVal$mag$f" />
|
||||
<Value name="相电压基波有效值最小值值" desc="基波电压有效值最小值" type="6" DO="MHAI4$MX$FundPhV" DA="phs*$cVal$mag$f" />
|
||||
<Value name="相电压间谐波含有率序列间谐波平均值值" desc="间谐波电压含有率(f25-2475)平均值" type="6" DO="MHAI8$MX$HPhV" DA="phs*Har[%-0]$mag$f" />
|
||||
<Value name="相电压间谐波含有率序列间谐波最大值值" desc="间谐波电压含有率(f25-2475)最大值" type="6" DO="MHAI9$MX$HPhV" DA="phs*Har[%-0]$mag$f" />
|
||||
<Value name="相电压间谐波含有率序列间谐波最小值值" desc="间谐波电压含有率(f25-2475)最小值" type="6" DO="MHAI10$MX$HPhV" DA="phs*Har[%-0]$mag$f" />
|
||||
|
||||
<Value name="间谐波电压有效值序列间谐波平均值值" desc="间谐波电压含有率(f25-2475)平均值" type="6" DO="MHAI8$MX$HPhV" DA="phs*Har[%-0]$mag$f" />
|
||||
<Value name="间谐波电压有效值序列间谐波最大值值" desc="间谐波电压含有率(f25-2475)最大值" type="6" DO="MHAI9$MX$HPhV" DA="phs*Har[%-0]$mag$f" />
|
||||
<Value name="间谐波电压有效值序列间谐波最小值值" desc="间谐波电压含有率(f25-2475)最小值" type="6" DO="MHAI10$MX$HPhV" DA="phs*Har[%-0]$mag$f" />
|
||||
|
||||
<Value name="相电压谐波含有率序列平均值值" desc="谐波电压含有率(2-50)平均值" type="6" DO="MHAI2$MX$HPhV" DA="phs*Har[%-2]$mag$f" />
|
||||
<Value name="相电压谐波含有率序列最大值值" desc="谐波电压含有率(2-50)最大值" type="6" DO="MHAI3$MX$HPhV" DA="phs*Har[%-2]$mag$f" />
|
||||
<Value name="相电压谐波含有率序列最小值值" desc="谐波电压含有率(2-50)最小值" type="6" DO="MHAI4$MX$HPhV" DA="phs*Har[%-2]$mag$f" />
|
||||
<Value name="谐波电压有效值序列平均值角度" desc="谐波电压相角(2-50)平均值" type="6" DO="MHAI2$MX$HPhV" DA="phs*Har[%-2]$ang$f" />
|
||||
<Value name="正序负序和零序电压平均值负序值" desc="负序电压平均值" type="6" DO="MSQI2$MX$SeqV" DA="c2$cVal$mag$f" />
|
||||
<Value name="正序负序和零序电压最大值负序值" desc="负序电压最大值" type="6" DO="MSQI3$MX$SeqV" DA="c2$cVal$mag$f" />
|
||||
<Value name="正序负序和零序电压最小值负序值" desc="负序电压最小值" type="6" DO="MSQI4$MX$SeqV" DA="c2$cVal$mag$f" />
|
||||
<Value name="电压负序不平衡度平均值值" desc="负序电压不平衡平均值" type="6" DO="MSQI2$MX$ImbNgV" DA="mag$f" />
|
||||
<Value name="电压负序不平衡度最大值值" desc="负序电压不平衡最大值" type="6" DO="MSQI3$MX$ImbNgV" DA="mag$f" />
|
||||
<Value name="电压负序不平衡度最小值值" desc="负序电压不平衡最小值" type="6" DO="MSQI4$MX$ImbNgV" DA="mag$f" />
|
||||
<Value name="正序负序和零序电压平均值零序值" desc="零序电压平均值" type="6" DO="MSQI2$MX$SeqV" DA="c3$cVal$mag$f" />
|
||||
<Value name="正序负序和零序电压最大值零序值" desc="零序电压最大值" type="6" DO="MSQI3$MX$SeqV" DA="c3$cVal$mag$f" />
|
||||
<Value name="正序负序和零序电压最小值零序值" desc="零序电压最小值" type="6" DO="MSQI4$MX$SeqV" DA="c3$cVal$mag$f" />
|
||||
<Value name="电压零序不平衡度平均值值" desc="零序电压不平衡平均值" type="6" DO="MSQI2$MX$ImbZroV" DA="mag$f" />
|
||||
<Value name="电压零序不平衡度最大值值" desc="零序电压不平衡最大值" type="6" DO="MSQI3$MX$ImbZroV" DA="mag$f" />
|
||||
<Value name="电压零序不平衡度最小值值" desc="零序电压不平衡最小值" type="6" DO="MSQI4$MX$ImbZroV" DA="mag$f" />
|
||||
<Value name="频率95值值" desc="频率95值" type="6" DO="MMXU5$MX$Hz" DA="mag$f" />
|
||||
<Value name="频率平均值值" desc="频率平均值" type="6" DO="MMXU2$MX$Hz" DA="mag$f" />
|
||||
<Value name="频率最大值值" desc="频率最大值" type="6" DO="MMXU3$MX$Hz" DA="mag$f" />
|
||||
<Value name="频率最小值值" desc="频率最小值" type="6" DO="MMXU4$MX$Hz" DA="mag$f" />
|
||||
<Value name="频率偏差95值值" desc="频率偏差95值" type="6" DO="MMXU5$MX$FreqDev" DA="mag$f" />
|
||||
<Value name="频率偏差平均值值" desc="频率偏差平均值" type="6" DO="MMXU2$MX$FreqDev" DA="mag$f" />
|
||||
<Value name="频率偏差最大值值" desc="频率偏差最大值" type="6" DO="MMXU3$MX$FreqDev" DA="mag$f" />
|
||||
<Value name="频率偏差最小值值" desc="频率偏差最小值" type="6" DO="MMXU4$MX$FreqDev" DA="mag$f" />
|
||||
<Value name="正序负序和零序电压平均值正序值" desc="正序电压平均值" type="6" DO="MSQI2$MX$SeqV" DA="c1$cVal$mag$f" />
|
||||
<Value name="正序负序和零序电压最大值正序值" desc="正序电压最大值" type="6" DO="MSQI3$MX$SeqV" DA="c1$cVal$mag$f" />
|
||||
<Value name="正序负序和零序电压最小值正序值" desc="正序电压最小值" type="6" DO="MSQI4$MX$SeqV" DA="c1$cVal$mag$f" />
|
||||
<Value name="电流总有效值95值值" desc="电流有效值95值" type="6" DO="MMXU5$MX$A" DA="phs*$cVal$mag$f" />
|
||||
<Value name="电流总有效值平均值值" desc="电流有效值平均值" type="6" DO="MMXU2$MX$A" DA="phs*$cVal$mag$f" />
|
||||
<Value name="电流总有效值最大值值" desc="电流有效值最大值" type="6" DO="MMXU3$MX$A" DA="phs*$cVal$mag$f" />
|
||||
<Value name="电流总有效值最小值值" desc="电流有效值最小值" type="6" DO="MMXU4$MX$A" DA="phs*$cVal$mag$f" />
|
||||
<Value name="电流基波有效值平均值值" desc="基波电流有效值平均值" type="6" DO="MHAI2$MX$HA" DA="phs*Har[1]$mag$f" />
|
||||
<Value name="电流基波有效值最大值值" desc="基波电流有效值最大值" type="6" DO="MHAI3$MX$HA" DA="phs*Har[1]$mag$f" />
|
||||
<Value name="电流基波有效值最小值值" desc="基波电流有效值最小值" type="6" DO="MHAI4$MX$HA" DA="phs*Har[1]$mag$f" />
|
||||
<Value name="间谐波电流有效值序列间谐波平均值值" desc="间谐波电流幅值(f25-2475)平均值" type="6" DO="MHAI8$MX$HA" DA="phs*Har[%-0]$mag$f" />
|
||||
<Value name="间谐波电流有效值序列间谐波最大值值" desc="间谐波电流幅值(f25-2475)最大值" type="6" DO="MHAI9$MX$HA" DA="phs*Har[%-0]$mag$f" />
|
||||
<Value name="间谐波电流有效值序列间谐波最小值值" desc="间谐波电流幅值(f25-2475)最小值" type="6" DO="MHAI10$MX$HA" DA="phs*Har[%-0]$mag$f" />
|
||||
<Value name="谐波电流有效值序列平均值值" desc="谐波电流幅值(2-50)平均值" type="6" DO="MHAI2$MX$HA" DA="phs*Har[%-0]$mag$f" />
|
||||
<Value name="谐波电流有效值序列最大值值" desc="谐波电流幅值(2-50)最大值" type="6" DO="MHAI3$MX$HA" DA="phs*Har[%-0]$mag$f" />
|
||||
<Value name="谐波电流有效值序列最小值值" desc="谐波电流幅值(2-50)最小值" type="6" DO="MHAI4$MX$HA" DA="phs*Har[%-0]$mag$f" />
|
||||
<Value name="正序负序和零序电流平均值负序值" desc="负序电流平均值" type="6" DO="MSQI2$MX$SeqA" DA="c2$cVal$mag$f" />
|
||||
<Value name="正序负序和零序电流最大值负序值" desc="负序电流最大值" type="6" DO="MSQI3$MX$SeqA" DA="c2$cVal$mag$f" />
|
||||
<Value name="正序负序和零序电流最小值负序值" desc="负序电流最小值" type="6" DO="MSQI4$MX$SeqA" DA="c2$cVal$mag$f" />
|
||||
<Value name="电流负序不平衡度平均值值" desc="负序电流不平衡平均值" type="6" DO="MSQI2$MX$ImbNgA" DA="mag$f" />
|
||||
<Value name="电流负序不平衡度最大值值" desc="负序电流不平衡最大值" type="6" DO="MSQI3$MX$ImbNgA" DA="mag$f" />
|
||||
<Value name="电流负序不平衡度最小值值" desc="负序电流不平衡最小值" type="6" DO="MSQI4$MX$ImbNgA" DA="mag$f" />
|
||||
<Value name="正序负序和零序电流平均值零序值" desc="零序电流平均值" type="6" DO="MSQI2$MX$SeqA" DA="c3$cVal$mag$f" />
|
||||
<Value name="正序负序和零序电流最大值零序值" desc="零序电流最大值" type="6" DO="MSQI3$MX$SeqA" DA="c3$cVal$mag$f" />
|
||||
<Value name="正序负序和零序电流最小值零序值" desc="零序电流最小值" type="6" DO="MSQI4$MX$SeqA" DA="c3$cVal$mag$f" />
|
||||
<Value name="电流零序不平衡度平均值值" desc="零序电流不平衡平均值" type="6" DO="MSQI2$MX$ImbZroA" DA="mag$f" />
|
||||
<Value name="电流零序不平衡度最大值值" desc="零序电流不平衡最大值" type="6" DO="MSQI3$MX$ImbZroA" DA="mag$f" />
|
||||
<Value name="电流零序不平衡度最小值值" desc="零序电流不平衡最小值" type="6" DO="MSQI4$MX$ImbZroA" DA="mag$f" />
|
||||
<Value name="正序负序和零序电流平均值正序值" desc="正序电流平均值" type="6" DO="MSQI2$MX$SeqA" DA="c1$cVal$mag$f" />
|
||||
<Value name="正序负序和零序电流最大值正序值" desc="正序电流最大值" type="6" DO="MSQI3$MX$SeqA" DA="c1$cVal$mag$f" />
|
||||
<Value name="正序负序和零序电流最小值正序值" desc="正序电流最小值" type="6" DO="MSQI4$MX$SeqA" DA="c1$cVal$mag$f" />
|
||||
<Value name="功率因数平均值值" desc="功率因数平均值" type="6" DO="MMXU2$MX$PF" DA="phs*$cVal$mag$f" />
|
||||
<Value name="功率因数最大值值" desc="功率因数最大值" type="6" DO="MMXU3$MX$PF" DA="phs*$cVal$mag$f" />
|
||||
<Value name="功率因数最小值值" desc="功率因数最小值" type="6" DO="MMXU4$MX$PF" DA="phs*$cVal$mag$f" />
|
||||
<Value name="位移功率因数平均值值" desc="基波功率因数平均值" type="6" DO="MHAI2$MX$HSigPF" DA="phs*Har[1]$mag$f" />
|
||||
<Value name="位移功率因数最大值值" desc="基波功率因数最大值" type="6" DO="MHAI3$MX$HSigPF" DA="phs*Har[1]$mag$f" />
|
||||
<Value name="基波有功功率平均值值" desc="基波有功功率平均值" type="6" DO="MHAI2$MX$HW" DA="phs*Har[1]$mag$f" />
|
||||
<Value name="基波有功功率最大值值" desc="基波有功功率最大值" type="6" DO="MHAI3$MX$HW" DA="phs*Har[1]$mag$f" />
|
||||
<Value name="基波有功功率最小值值" desc="基波有功功率最小值" type="6" DO="MHAI4$MX$HW" DA="phs*Har[1]$mag$f" />
|
||||
<Value name="视在功率95值值" desc="视在功率95值" type="6" DO="MMXU5$MX$VA" DA="phs*$cVal$mag$f" />
|
||||
<Value name="视在功率平均值值" desc="视在功率平均值" type="6" DO="MMXU2$MX$VA" DA="phs*$cVal$mag$f" />
|
||||
<Value name="视在功率最大值值" desc="视在功率最大值" type="6" DO="MMXU3$MX$VA" DA="phs*$cVal$mag$f" />
|
||||
<Value name="视在功率最小值值" desc="视在功率最小值" type="6" DO="MMXU4$MX$VA" DA="phs*$cVal$mag$f" />
|
||||
<Value name="无功功率95值值" desc="无功功率95值" type="6" DO="MMXU5$MX$VAr" DA="phs*$cVal$mag$f" />
|
||||
<Value name="无功功率平均值值" desc="无功功率平均值" type="6" DO="MMXU2$MX$VAr" DA="phs*$cVal$mag$f" />
|
||||
<Value name="无功功率最大值值" desc="无功功率最大值" type="6" DO="MMXU3$MX$VAr" DA="phs*$cVal$mag$f" />
|
||||
<Value name="无功功率最小值值" desc="无功功率最小值" type="6" DO="MMXU4$MX$VAr" DA="phs*$cVal$mag$f" />
|
||||
<Value name="2~50次谐波有功功率序列平均值值" desc="谐波有功功率(2-50)平均值" type="6" DO="MHAI2$MX$HW" DA="phs*Har[%-0]$mag$f" />
|
||||
<Value name="2~50次谐波有功功率序列最大值值" desc="谐波有功功率(2-50)最大值" type="6" DO="MHAI3$MX$HW" DA="phs*Har[%-0]$mag$f" />
|
||||
<Value name="2~50次谐波有功功率序列最小值值" desc="谐波有功功率(2-50)最小值" type="6" DO="MHAI4$MX$HW" DA="phs*Har[%-0]$mag$f" />
|
||||
<Value name="有功功率95值值" desc="有功功率95值" type="6" DO="MMXU5$MX$W" DA="phs*$cVal$mag$f" />
|
||||
<Value name="有功功率平均值值" desc="有功功率平均值" type="6" DO="MMXU2$MX$W" DA="phs*$cVal$mag$f" />
|
||||
<Value name="有功功率最大值值" desc="有功功率最大值" type="6" DO="MMXU3$MX$W" DA="phs*$cVal$mag$f" />
|
||||
<Value name="有功功率最小值值" desc="有功功率最小值" type="6" DO="MMXU4$MX$W" DA="phs*$cVal$mag$f" />
|
||||
<Value name="三相功率因数平均值值" desc="总功率因数平均值" type="6" DO="MMXU2$MX$TotPF" DA="mag$f" />
|
||||
<Value name="三相功率因数最大值值" desc="总功率因数最大值" type="6" DO="MMXU3$MX$TotPF" DA="mag$f" />
|
||||
<Value name="三相功率因数最小值值" desc="总功率因数最小值" type="6" DO="MMXU4$MX$TotPF" DA="mag$f" />
|
||||
<Value name="三相位移功率因数平均值值" desc="总基波功率因数平均值" type="6" DO="MHAI2$MX$HTotPF" DA="har[1]$mag$f" />
|
||||
<Value name="三相位移功率因数最大值值" desc="总基波功率因数最大值" type="6" DO="MHAI3$MX$HTotPF" DA="har[1]$mag$f" />
|
||||
<Value name="三相位移功率因数最小值值" desc="总基波功率因数最小值" type="6" DO="MHAI4$MX$HTotPF" DA="har[1]$mag$f" />
|
||||
<Value name="三相总视在功率平均值值" desc="总视在功率平均值" type="6" DO="MMXU2$MX$TotVA" DA="mag$f" />
|
||||
<Value name="三相总视在功率最大值值" desc="总视在功率最大值" type="6" DO="MMXU3$MX$TotVA" DA="mag$f" />
|
||||
<Value name="三相总视在功率最小值值" desc="总视在功率最小值" type="6" DO="MMXU4$MX$TotVA" DA="mag$f" />
|
||||
<Value name="三相总无功功率平均值值" desc="总无功功率平均值" type="6" DO="MMXU2$MX$TotVAr" DA="mag$f" />
|
||||
<Value name="三相总无功功率最大值值" desc="总无功功率最大值" type="6" DO="MMXU3$MX$TotVAr" DA="mag$f" />
|
||||
<Value name="三相总无功功率最小值值" desc="总无功功率最小值" type="6" DO="MMXU4$MX$TotVAr" DA="mag$f" />
|
||||
<Value name="三相总有功功率平均值值" desc="总有功功率平均值" type="6" DO="MMXU2$MX$TotW" DA="mag$f" />
|
||||
<Value name="三相总有功功率最大值值" desc="总有功功率最大值" type="6" DO="MMXU3$MX$TotW" DA="mag$f" />
|
||||
<Value name="三相总有功功率最小值值" desc="总有功功率最小值" type="6" DO="MMXU4$MX$TotW" DA="mag$f" />
|
||||
@@ -34,7 +34,7 @@ import java.util.List;
|
||||
public class GetIcdMmsJsonDebugRunner {
|
||||
|
||||
/** 本地 ICD/SCD 文件路径,运行前请改成真实文件。 */
|
||||
private static final String ICD_FILE_PATH = "D:/temp/demo.icd";
|
||||
private static final String ICD_FILE_PATH = "D:\\Work\\工作资料\\1灿能项目资料\\01自研\\01灿能\\09灿能C端功能\\01需求文档\\灿能工具箱开发\\icd\\PQS882_VX_BJ_1(V111).icd";
|
||||
|
||||
/** 调试时可固定版本号,便于对比输出。 */
|
||||
private static final String VERSION = "20260421";
|
||||
@@ -69,6 +69,7 @@ public class GetIcdMmsJsonDebugRunner {
|
||||
|
||||
MappingTaskResponse firstResponse = debugNeedIndexSelection(mappingTaskService, responseConverter);
|
||||
printResponse("第一次调试:获取索引候选", firstResponse, objectMapper);
|
||||
printIndexCandidatesJson(firstResponse, objectMapper);
|
||||
printIndexCandidateSummary(firstResponse);
|
||||
|
||||
if (!RUN_SECOND_STEP) {
|
||||
@@ -192,6 +193,23 @@ public class GetIcdMmsJsonDebugRunner {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 单独输出 indexCandidates 的 JSON,便于直接复制做第二次调试绑定。
|
||||
*/
|
||||
private static void printIndexCandidatesJson(MappingTaskResponse response, ObjectMapper objectMapper) {
|
||||
try {
|
||||
System.out.println();
|
||||
System.out.println("===== indexCandidates JSON =====");
|
||||
if (response == null) {
|
||||
System.out.println("null");
|
||||
return;
|
||||
}
|
||||
System.out.println(objectMapper.writeValueAsString(response.getIndexCandidates()));
|
||||
} catch (Exception ex) {
|
||||
throw new IllegalArgumentException("print indexCandidates JSON failed: " + ex.getMessage(), ex);
|
||||
}
|
||||
}
|
||||
|
||||
private static void printResponse(String title, MappingTaskResponse response, ObjectMapper objectMapper) {
|
||||
try {
|
||||
System.out.println();
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
<module>activate-tool</module>
|
||||
<module>mms-mapping</module>
|
||||
<module>wave-tool</module>
|
||||
<module>add-data</module>
|
||||
</modules>
|
||||
|
||||
</project>
|
||||
|
||||
Reference in New Issue
Block a user