89 Commits

Author SHA1 Message Date
8caaf95427 ADD: 添加项目配置文档和开发指南
- 新增 CLAUDE.md 项目架构和开发指导文档
- 添加 Gitea本地协作开发服务器配置指南
- 完善检测模块架构分析文档
- 增加报告生成和Word文档处理工具指南
- 添加动态表格和结果服务测试用例
- 更新应用配置和VS Code开发环境设置

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-24 16:49:40 +08:00
81df650d09 代码调整 2025-09-24 16:44:41 +08:00
e42121ba4c 比对模式的检测报告生成和下载 2025-09-23 16:16:31 +08:00
贾同学
35e52e0722 UPDATE: 添加数据合并时更新主计划检测状态。 2025-09-23 15:44:46 +08:00
贾同学
80c383a746 UPDATE: 修改被检设备批量导入逻辑。 2025-09-23 14:54:29 +08:00
贾同学
84f9e61e57 UPDATE: 完善修改数据合并更新指定字段值。 2025-09-22 16:14:53 +08:00
贾同学
d18e84159f UPDATE: 1、异步导出检测数据;2、优化异步数据合并。 2025-09-19 16:17:18 +08:00
贾同学
1ea09cc52c ADD: 检测数据表创建后,创建索引 2025-09-19 16:15:16 +08:00
caozehui
f0540d4c92 微调 2025-09-19 14:13:13 +08:00
caozehui
04299007d0 重新计算接口调整、监测点结果更新逻辑调整 2025-09-19 09:22:02 +08:00
caozehui
947ee4f771 微调 2025-09-18 08:58:48 +08:00
caozehui
c3c46b5257 重新计算接口 2025-09-17 13:41:32 +08:00
caozehui
213d940ff1 结果展示调整 2025-09-17 09:08:23 +08:00
caozehui
d220ee0187 微调 2025-09-16 15:47:16 +08:00
caozehui
f56c1086b9 预检测时进行录波校验、完善监测点结果修改逻辑 2025-09-16 13:43:08 +08:00
贾同学
4532f2242b ADD: 1、更新监测点的检测结果接口。 2025-09-15 10:39:09 +08:00
贾同学
9deaea9e81 Merge remote-tracking branch 'origin/master' 2025-09-15 08:33:49 +08:00
贾同学
44738f2878 ADD: 1、查询检测点结果接口。 2025-09-12 16:30:23 +08:00
caozehui
3f12758d1b 微调 2025-09-12 16:29:05 +08:00
caozehui
fd7ffbe87e 微调 2025-09-12 15:11:09 +08:00
caozehui
12edb57581 生成报告监测点结果下拉框展示 2025-09-12 10:47:25 +08:00
caozehui
a163150fec 完善监测点结果、设备结果 2025-09-12 09:11:56 +08:00
贾同学
46e8811e59 UPDATE: 1、优化导出计划检测数据;2、删除导入和合并计划检测数据接口,改成导入并合并计划检测数据异步逻辑。 2025-09-11 11:08:30 +08:00
90eb90554b 波形算法调整:增加降采样处理和多段分析处理 2025-09-10 14:53:17 +08:00
caozehui
694f12bc29 录波功能完善 2025-09-10 08:26:35 +08:00
2121a293cb 表格调整部分代码 2025-09-09 09:43:44 +08:00
贾同学
6c6bed4b46 UPDATE: 完善检修计划项目负责人和项目成员逻辑。 2025-09-08 16:24:19 +08:00
贾同学
7f2a61ba21 ADD: 检修计划添加项目负责人和项目成员 2025-09-08 10:21:34 +08:00
caozehui
ecafa996a3 发送录波指令、调用录波算法、解析录波数据 2025-09-05 15:26:46 +08:00
e11107eb8f 波形比对算法迁移成功 2025-09-04 18:42:50 +08:00
c414f1a42a 波形比对算法迁移成功 2025-09-04 18:41:53 +08:00
贾同学
7c18d0038a UPDATE: 优化可选标准设备接口 2025-09-04 15:59:03 +08:00
贾同学
b09438e29d ADD: 新增可选标准设备接口 2025-09-04 10:37:15 +08:00
caozehui
efe92e28b1 微调 2025-09-03 14:28:54 +08:00
caozehui
e3f682212e Merge remote-tracking branch 'origin/master' 2025-09-03 14:27:38 +08:00
caozehui
a7f3925a2f Merge branch 'qr_branch'
# Conflicts:
#	detection/src/main/java/com/njcn/gather/detection/handler/SocketSourceResponseService.java
#	user/src/main/java/com/njcn/gather/user/user/controller/AuthController.java
2025-09-03 14:27:11 +08:00
caozehui
4ecec5e6ef 新增PQ-COM设备类型报告模板、修改系数校准抛数据组数 2025-09-03 14:25:32 +08:00
da3373c710 波形比对算法迁移成功 2025-09-02 15:53:35 +08:00
caozehui
8963b20dd3 比对完善 2025-09-01 13:39:52 +08:00
0977a77eed 新增工具模块,并添加了子模块-报告工具生成模块 2025-09-01 11:06:00 +08:00
贾同学
3951b71fff UPDATE: 修改导出计划检测结果数据,根据被检设备过滤 2025-08-29 15:07:17 +08:00
贾同学
52b99c2669 ADD: 检测计划添加导入标识字段以及逻辑 2025-08-29 11:32:37 +08:00
贾同学
760db06120 ADD: 合并子计划检测结果数据到主计划对应表中 2025-08-29 09:14:06 +08:00
贾同学
7e66b67cde UPDATE: 导入和导出计划检测数据测试报告同步 2025-08-28 14:20:19 +08:00
贾同学
5422340dd3 UPDATE:优化导入子计划检测配置逻辑 2025-08-27 20:20:24 +08:00
贾同学
d5e550a8f4 UPDATE:修改导出导入子计划检测配置同步逻辑 2025-08-27 20:11:20 +08:00
caozehui
4128b21da8 微调 2025-08-26 18:50:55 +08:00
caozehui
6ae9037a47 动态决定是否进行相角差校验、角型接线是否使用相别的指标进行正式检测、数模脚本与映射校验时动态补充相角的测试项 2025-08-26 18:49:35 +08:00
贾同学
d0f6cc46ad UPDATE:修改子计划解绑或绑定被检设备逻辑 2025-08-26 15:37:32 +08:00
贾同学
d5f22c4147 ADD:检测计划添加检测配置相关 2025-08-26 11:12:16 +08:00
贾同学
cd41320032 ADD:ICD添加检测是否两个开关字段 2025-08-25 14:39:17 +08:00
贾同学
415fc0c129 ADD:导入检测计划检测结果 2025-08-25 09:06:04 +08:00
cdf
d6108967ce Merge remote-tracking branch 'origin/master' 2025-08-25 08:49:47 +08:00
cdf
b074ba29fc 北京暂降平台调整 2025-08-25 08:49:32 +08:00
caozehui
d020890639 调整表结构、编写查看检测结果接口、误差计算逻辑调整 2025-08-22 16:27:48 +08:00
贾同学
7b9954f1fe ADD:导出检测计划检测结果; 2025-08-22 09:03:42 +08:00
caozehui
b4bd2e6d1d 间谐波电压取基波有效值、角型接线时若不存在线电压指标则使用相电压指标、表结构调整以记录配对关系 2025-08-21 10:13:16 +08:00
贾同学
ddf6da0855 ADD:导入子计划元信息; 2025-08-20 11:27:42 +08:00
cdf
cd7ae5d06c 北京暂降平台调整 2025-08-19 08:42:45 +08:00
贾同学
e1872d030a UPDATE:完善导出子计划元信息zip包; 2025-08-18 16:26:30 +08:00
caozehui
c9bf604a33 正式检测-收取数据、原始数据组装和入库、配对关系入库、误差计算逻辑 2025-08-18 16:15:35 +08:00
贾同学
257d0b3af8 ADD:导出子计划元信息zip包 2025-08-15 16:21:10 +08:00
a25febc245 代码调整 2025-08-14 09:48:17 +08:00
db992ec790 代码调整 2025-08-14 09:40:04 +08:00
贾同学
c9fb9db0c0 UPDATE:1.兼容查询子计划对应主计划的已绑定的标准设备;2.新增修改子计划逻辑 2025-08-13 20:36:58 +08:00
cdf
a35ee521b6 北京暂降平台调整 2025-08-13 11:02:03 +08:00
cdf
c91b3bdc51 北京暂降平台调整 2025-08-13 10:07:21 +08:00
贾同学
ac31267eb9 ADD:未绑定被检设备查询返回详细信息 2025-08-12 20:41:52 +08:00
caozehui
50d626f563 比对预检测-完善 2025-08-12 15:32:57 +08:00
hzj
c1d2160335 sql优化 2025-08-12 15:21:46 +08:00
cdf
9ad02d45f4 Merge remote-tracking branch 'origin/master' 2025-08-12 10:39:32 +08:00
cdf
f26e4f0e11 北京暂降平台调整 2025-08-12 10:39:12 +08:00
541a707570 代码调整 2025-08-12 10:26:55 +08:00
cdf
13fd6f11ae 北京暂降平台调整 2025-08-12 09:47:28 +08:00
fe5040c7af 代码调整 2025-08-11 16:31:26 +08:00
hzj
09d2911c4c sql优化 2025-08-11 15:30:54 +08:00
hzj
f1777f8bf1 sql优化 2025-08-11 15:27:55 +08:00
hzj
10729c44fc 去除未发生暂降的电站 2025-08-11 10:05:24 +08:00
cdf
1a0227620f 北京暂降平台调整 2025-08-11 10:03:48 +08:00
c73e062109 netty优化 2025-08-11 09:40:01 +08:00
df83f65328 netty优化 2025-08-11 09:39:44 +08:00
caozehui
c16c1f8e1d 比对预检测 2025-08-11 08:36:19 +08:00
cdf
f520234e55 北京暂降平台调整 2025-08-07 23:16:05 +08:00
cdf
e7598137ae Merge remote-tracking branch 'origin/master' 2025-08-07 23:13:07 +08:00
7078a5efe5 websocket优化调整 2025-08-07 22:50:27 +08:00
cdf
1c741d6406 Merge remote-tracking branch 'origin/master' 2025-08-06 17:12:10 +08:00
hzj
b10700d976 查询监测点关联用户 2025-08-06 16:03:02 +08:00
cdf
71083101ae 微调 2025-08-06 15:28:41 +08:00
ca1b1661d1 websocket优化调整 2025-08-06 13:36:18 +08:00
255 changed files with 28230 additions and 5141 deletions

View File

@@ -0,0 +1,28 @@
{
"permissions": {
"allow": [
"Bash(dir:*)",
"Bash(ls:*)",
"Bash(find:*)",
"Bash(mvn clean:*)",
"Bash(rm:*)",
"Bash(grep:*)",
"Bash(mkdir:*)",
"Read(/F:\\gitea\\fusionForce\\CN_Gather\\tools\\wave-comtrade\\src\\main\\java\\com\\njcn\\gather\\tools\\comtrade\\comparewave/**)",
"Read(/F:\\gitea\\fusionForce\\CN_Gather\\tools\\wave-comtrade\\src\\main\\java\\com\\njcn\\gather\\tools\\comtrade\\comparewave/**)",
"Read(/F:\\gitea\\fusionForce\\CN_Gather\\tools\\wave-comtrade\\src\\main\\java\\com\\njcn\\gather\\tools\\comtrade\\comparewave\\service\\impl/**)",
"mcp__exa__web_search_exa",
"WebSearch",
"Bash(mvn compile:*)",
"Bash(git checkout:*)",
"Bash(mvn install:*)",
"WebFetch(domain:officeopenxml.com)",
"Bash(systeminfo)",
"Bash(findstr:*)",
"Bash(ver)",
"Bash(git add:*)"
],
"deny": []
},
"outputStyle": "engineer-professional"
}

4
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,4 @@
{
"java.compile.nullAnalysis.mode": "automatic",
"java.configuration.updateBuildConfiguration": "automatic"
}

215
CLAUDE.md Normal file
View File

@@ -0,0 +1,215 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## 项目概述
CN_Gather是灿能公司的融合工具项目体专门用于电能质量设备检测的企业级应用系统。采用Spring Boot多模块Maven架构以detection模块为核心的检测业务系统。
## 项目架构
### 核心模块结构
- **entrance**: 应用入口模块端口18092整合所有其他模块
- **detection**: 核心检测业务模块,电能质量设备检测的完整业务流程
- **storage**: 数据存储模块,处理检测数据存储和谐波数据处理
- **system**: 基础系统模块,提供字典管理、日志管理、配置管理等基础功能
- **user**: 用户管理模块,处理认证授权和权限控制
### 模块依赖关系
```
entrance (启动入口)
├── system (基础服务层)
├── user (认证授权层)
├── detection (核心业务层) → 依赖 system, storage
└── storage (数据存储层) → 依赖 system
```
## 常用开发命令
### 构建和打包
```bash
# 编译整个项目
mvn clean compile
# 打包所有模块
mvn clean package
# 跳过测试打包
mvn clean package -DskipTests
# 安装到本地仓库
mvn clean install
```
### 运行应用
```bash
# 运行主入口应用 (端口18092)
cd entrance
mvn spring-boot:run
# 运行事件智能模块 (独立应用)
cd event_smart
mvn spring-boot:run
```
### 测试
```bash
# 运行所有测试
mvn test
# 运行特定模块测试
cd detection
mvn test
```
## 技术栈
### 核心框架
- **Spring Boot**: 2.3.12.RELEASE
- **MyBatis Plus**: 数据持久层框架
- **Maven**: 项目构建管理
- **Java**: 1.8
### 数据库
- **MySQL**: 主数据库 (192.168.1.24:13306/pqs9100)
- **Oracle**: event_smart模块使用
- **Druid**: 数据库连接池
### 通信技术
- **Netty**: Socket通信 (端口61000设备, 62000源)
- **WebSocket**: 实时数据推送 (端口7777)
- **RestTemplate**: HTTP客户端通信
### 其他关键技术
- **Apache POI + docx4j**: Word文档报告生成
- **FastJSON**: JSON数据处理
- **Spring Security + JWT**: 安全认证 (event_smart模块)
- **Redis**: 缓存服务 (event_smart模块)
## 关键配置
### 数据库配置
- 数据库URL: `jdbc:mysql://192.168.1.24:13306/pqs9100`
- MyBatis映射文件位置: `classpath*:com/njcn/**/mapping/*.xml`
- 主键生成策略: `assign_uuid`
### Socket通信配置
- 源设备Socket: 127.0.0.1:62000
- 被检设备Socket: 127.0.0.1:61000
- WebSocket端口: 7777
### 文件路径配置
- 日志目录: `D:\logs`
- 报告模板目录: `D:\template`
- 报告输出目录: `D:\report`
- Word模板位置: `entrance/src/main/resources/model/`
## detection模块核心架构
### 子模块功能划分
- **device**: 设备管理 - PqDev(被检设备)、PqStandardDev(标准设备)、PqDevSub(设备子表)
- **plan**: 检测计划管理 - AdPlan(检测计划)、AdPlanSource(计划源)、AdPlanStandardDev(计划标准设备)
- **script**: 检测脚本管理 - PqScript(检测脚本)、PqScriptDtls(脚本详情)、PqScriptCheckData(检测数据)
- **source**: 程控源管理 - PqSource(程控源设备)
- **err**: 误差体系管理 - PqErrSys(误差体系)、PqErrSysDtls(误差详情)
- **report**: 报告生成管理 - PqReport(报告模板)支持Word模板处理
- **monitor**: 监测管理 - PqMonitor(监测点管理)
- **icd**: ICD路径管理 - PqIcdPath(通信配置)
- **result**: 结果管理 - 检测结果查询和数据展示
- **type**: 设备类型管理 - DevType(设备类型字典)
### 核心检测流程 (PreDetectionController)
```java
// 主要检测接口
@PostMapping("/startPreTest") // 检测通用入口
@PostMapping("/ytxCheckSimulate") // 源通讯校验
@PostMapping("/startSimulateTest") // 启动程控源检测
@PostMapping("/coefficientCheck") // 系数校验
@PostMapping("/startContrastTest") // 比对检测
@PostMapping("/devPhaseSequence") // 设备相序检测
```
### Socket通信架构
- **SocketManager**: Socket会话管理存储userId与Channel映射
- **WebServiceManager**: WebSocket服务管理实时数据推送
- **通信处理器**:
- SocketSourceResponseService: 程控源响应处理
- SocketDevResponseService: 设备响应处理
- SocketContrastResponseService: 比对检测响应处理
- **通信工具**:
- CnSocketUtil: Socket连接工具
- FormalTestManager: 正式检测管理
- XiNumberManager: 系数管理
### 暂态检测参数
- 暂态前时间: 2秒
- 写入时间: 0.001秒
- 写出时间: 0.001秒
- 暂态后时间: 3秒
### 闪变参数
- 波形类型: CPM/SQU
- 占空比: 50%
## 开发注意事项
### detection模块包结构
```
detection/
├── controller/ # 控制层 - PreDetectionController
├── handler/ # Socket响应处理器
├── service/ # 业务逻辑层
│ └── impl/ # 服务实现
├── util/ # 工具类层
│ ├── business/ # 业务工具 - DetectionCommunicateUtil
│ └── socket/ # Socket通信工具
├── pojo/ # 数据模型层
│ ├── constant/ # 常量 - DetectionCommunicateConstant
│ ├── dto/ # 数据传输对象
│ ├── enums/ # 枚举 - DetectionCodeEnum等
│ ├── param/ # 请求参数
│ ├── po/ # 持久化对象
│ └── vo/ # 视图对象 - DetectionData等
└── [子模块]/ # device、plan、script等子模块
├── controller/ # 子模块控制器
├── service/ # 子模块服务
├── mapper/ # 数据访问层
│ └── mapping/ # MyBatis映射文件
└── pojo/ # 子模块数据对象
```
### 检测数据处理机制
- **任意值**: 取第一个满足条件的数据
- **部分值**: 去除最大最小值后取值
- **所有值**: 要求所有数据都合格
- **CP95值**: 取95%分位数
- **平均值**: 取算术平均值
### 检测项目类型
- **频率**: FREQ
- **电压**: V_RELATIVE(相对值)/V_ABSOLUTELY(绝对值)
- **电流**: I_RELATIVE/I_ABSOLUTELY
- **谐波**: HV/HI (2-50次谐波)
- **间谐波**: HSV/HSI
- **不平衡度**: IMBV/IMBA (三相不平衡)
- **闪变**: F (PST)
- **暂态**: VOLTAGE_MAG/VOLTAGE_DUR
### 检测模式
- **数字式检测**: 数字接口通信
- **模拟式检测**: 模拟信号输出
- **比对式检测**: 多台设备比对
### 报告生成机制
- **模板处理**: 使用POI和docx4j处理Word文档
- **模板位置**: `entrance/src/main/resources/model/`
- **支持模板**: NPQS-580、PQV-700、njcn_882系列等
- **功能**: 书签替换、表格填充、文档合并
- **云端上传**: 支持FTP批量上传报告
### 依赖组件
项目使用灿能公司自研组件:
- `njcn-common`: 通用工具包
- `mybatis-plus`: MyBatis增强包
- `spingboot2.3.12`: Spring Boot定制包
- `RestTemplate-plugin`: HTTP客户端插件

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,238 @@
# Gitea本地协作开发服务器配置指南
## 概述
本文档说明如何将本地安装的Gitea配置为团队协作开发服务器替代原有的物理服务器环境。
## 1. 网络配置
### 1.1 确认本机IP地址
```bash
# Windows系统
ipconfig
# 查找本机局域网IP地址通常形如 192.168.x.x 或 10.x.x.x
```
### 1.2 配置Gitea服务地址
编辑Gitea配置文件 `app.ini`
```ini
[server]
# 将localhost改为本机IP地址确保同事可以访问
HTTP_ADDR = 0.0.0.0
HTTP_PORT = 3000
# 外部访问URL替换为你的实际IP
ROOT_URL = http://192.168.x.x:3000/
```
### 1.3 防火墙配置
确保Windows防火墙允许Gitea端口通信
```bash
# 打开Windows防火墙入站规则
# 添加端口3000的TCP入站规则
```
或在Windows防火墙中
- 控制面板 → 系统和安全 → Windows Defender防火墙 → 高级设置
- 入站规则 → 新建规则 → 端口 → TCP → 特定本地端口: 3000
## 2. Gitea服务配置
### 2.1 启动Gitea服务
```bash
# 进入Gitea安装目录
cd C:\gitea # 或你的安装路径
gitea.exe web
```
### 2.2 配置为Windows服务推荐
创建Windows服务确保开机自启
1. 下载NSSM (Non-Sucking Service Manager)
2. 以管理员身份运行命令提示符:
```bash
nssm install Gitea
# 在弹出界面中配置:
# Path: C:\gitea\gitea.exe
# Arguments: web
# Working directory: C:\gitea
```
3. 启动服务:
```bash
net start Gitea
```
### 2.3 数据库配置优化
如果使用SQLite默认确保数据文件路径正确
```ini
[database]
DB_TYPE = sqlite3
PATH = data/gitea.db
```
如果需要更好性能考虑配置MySQL
```ini
[database]
DB_TYPE = mysql
HOST = 127.0.0.1:3306
NAME = gitea
USER = gitea
PASSWD = your_password
```
## 3. 同事访问配置
### 3.1 提供访问地址
向同事提供访问地址:
```
http://你的IP地址:3000
例如: http://192.168.1.100:3000
```
### 3.2 用户账号管理
1. 访问管理界面创建用户账号
2. 或开启用户自注册:
```ini
[service]
DISABLE_REGISTRATION = false
REQUIRE_SIGNIN_VIEW = false
```
### 3.3 权限配置
为协作项目设置适当权限:
- 项目所有者:完全控制权限
- 协作者:推送/拉取权限
- 读者:仅读取权限
## 4. 代码仓库迁移
### 4.1 从原服务器迁移仓库
如果原服务器数据可恢复:
```bash
# 在原服务器或备份中找到Git裸仓库
# 复制到新Gitea的repositories目录
# 通常位于 gitea-repositories/用户名/仓库名.git
```
### 4.2 重新创建仓库
如果需要重新创建:
1. 在Gitea界面创建新仓库
2. 本地添加新的远程地址:
```bash
git remote remove origin
git remote add origin http://你的IP:3000/用户名/仓库名.git
git push -u origin master
```
## 5. 开发工作流配置
### 5.1 分支保护规则
为主要分支设置保护规则:
- 设置 → 分支 → 分支保护规则
- 保护master分支要求代码审查
### 5.2 Webhook配置
如果需要CI/CD集成
```
设置 → Webhooks → 添加Webhook
配置自动构建触发器
```
## 6. 备份策略
### 6.1 定期备份
```bash
# 备份Gitea数据目录
# 包括repositories/, data/, log/, custom/
robocopy "C:\gitea" "D:\backup\gitea" /MIR /Z /R:3 /W:10
```
### 6.2 自动备份脚本
创建批处理文件实现定期备份:
```batch
@echo off
set BACKUP_DIR=D:\backup\gitea_%date:~0,4%%date:~5,2%%date:~8,2%
robocopy "C:\gitea" "%BACKUP_DIR%" /MIR /Z /R:3 /W:10
echo Backup completed to %BACKUP_DIR%
```
## 7. 常见问题排查
### 7.1 访问问题
- 检查防火墙设置
- 确认IP地址和端口正确
- 验证Gitea服务是否正常运行
### 7.2 权限问题
- 检查用户账号状态
- 确认仓库权限设置
- 验证SSH密钥配置如使用SSH
### 7.3 性能优化
```ini
[server]
# 调整并发连接数
HTTP_ADDR = 0.0.0.0
HTTP_PORT = 3000
[database]
# 数据库连接池配置
MAX_IDLE_CONNS = 30
MAX_OPEN_CONNS = 300
```
## 8. 安全建议
1. **网络安全**
- 仅在受信任的局域网环境中开放
- 考虑使用VPN访问
- 定期更新Gitea版本
2. **访问控制**
- 禁用不必要的公开注册
- 使用强密码策略
- 启用双因子认证
3. **数据安全**
- 定期备份重要数据
- 监控异常访问
- 记录操作日志
## 9. 同事操作指南
### 9.1 首次设置
```bash
# 克隆仓库
git clone http://你的IP:3000/用户名/CN_Gather.git
# 配置用户信息
git config user.name "姓名"
git config user.email "邮箱"
```
### 9.2 日常协作
```bash
# 拉取最新代码
git pull origin master
# 创建功能分支
git checkout -b feature/新功能
# 提交更改
git add .
git commit -m "描述信息"
git push origin feature/新功能
# 在Gitea界面创建Pull Request
```
---
**联系信息**
- Gitea服务地址http://你的IP:3000
- 管理员:[你的联系方式]
- 紧急联系:[备用联系方式]
**注意**:请确保定期备份重要代码,避免数据丢失。

View File

@@ -86,6 +86,7 @@
<artifactId>jakarta.xml.bind-api</artifactId>
<version>2.3.3</version>
</dependency>
<dependency>
<groupId>org.glassfish.jaxb</groupId>
<artifactId>jaxb-runtime</artifactId>
@@ -124,6 +125,20 @@
<version>3.10.0</version>
</dependency>
<!--波形工具模块-->
<dependency>
<groupId>com.njcn.gather</groupId>
<artifactId>wave-comtrade</artifactId>
<version>1.0.0</version>
</dependency>
<!--报告工具模块-->
<dependency>
<groupId>com.njcn.gather</groupId>
<artifactId>report-generator</artifactId>
<version>1.0.0</version>
</dependency>
</dependencies>

View File

@@ -1,8 +1,11 @@
package com.njcn.gather.detection.controller;
import com.njcn.common.pojo.annotation.OperateInfo;
import com.njcn.common.pojo.constant.OperateType;
import com.njcn.common.pojo.enums.common.LogEnum;
import com.njcn.common.pojo.enums.response.CommonResponseEnum;
import com.njcn.common.pojo.response.HttpResult;
import com.njcn.common.utils.LogUtil;
import com.njcn.gather.detection.pojo.param.ContrastDetectionParam;
import com.njcn.gather.detection.pojo.param.PreDetectionParam;
import com.njcn.gather.detection.pojo.param.SimulateDetectionParam;
@@ -142,4 +145,14 @@ public class PreDetectionController extends BaseController {
preDetectionService.startContrastTest(param);
return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, null, methodDescribe);
}
@OperateInfo(info = LogEnum.SYSTEM_COMMON, operateType = OperateType.DOWNLOAD)
@PostMapping("/exportAlignData")
@ApiOperation("实时对齐数据导出为csv文件")
public void exportAlignData() {
String methodDescribe = getMethodDescribe("exportAlignData");
LogUtil.njcnDebug(log, "{}", methodDescribe);
preDetectionService.exportAlignData();
}
}

View File

@@ -1,6 +1,5 @@
package com.njcn.gather.detection.handler;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.NumberUtil;
import cn.hutool.core.util.ObjectUtil;
@@ -19,13 +18,16 @@ import com.njcn.gather.detection.pojo.vo.*;
import com.njcn.gather.detection.service.impl.DetectionServiceImpl;
import com.njcn.gather.detection.util.DetectionUtil;
import com.njcn.gather.detection.util.socket.*;
import com.njcn.gather.detection.util.socket.websocket.WebServiceManager;
import com.njcn.gather.device.pojo.enums.CommonEnum;
import com.njcn.gather.device.pojo.enums.PatternEnum;
import com.njcn.gather.device.pojo.po.PqDevSub;
import com.njcn.gather.device.pojo.vo.PreDetection;
import com.njcn.gather.device.service.IPqDevService;
import com.njcn.gather.device.service.IPqDevSubService;
import com.njcn.gather.plan.pojo.po.AdPlan;
import com.njcn.gather.plan.service.IAdPlanService;
import com.njcn.gather.result.pojo.enums.ResultUnitEnum;
import com.njcn.gather.script.pojo.param.PqScriptCheckDataParam;
import com.njcn.gather.script.pojo.param.PqScriptIssueParam;
import com.njcn.gather.script.pojo.po.SourceIssue;
@@ -36,43 +38,33 @@ import com.njcn.gather.storage.pojo.po.SimAndDigHarmonicResult;
import com.njcn.gather.storage.pojo.po.SimAndDigNonHarmonicResult;
import com.njcn.gather.storage.service.DetectionDataDealService;
import com.njcn.gather.storage.service.SimAndDigHarmonicService;
import com.njcn.gather.system.cfg.service.ISysTestConfigService;
import com.njcn.gather.system.dictionary.pojo.enums.DictDataEnum;
import com.njcn.gather.system.dictionary.pojo.po.DictData;
import com.njcn.gather.system.dictionary.service.IDictDataService;
import com.njcn.gather.system.pojo.enums.DicDataEnum;
import com.njcn.gather.system.reg.service.ISysRegResService;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import java.lang.reflect.Field;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import static com.njcn.gather.detection.util.socket.FormalTestManager.harmonicRelationMap;
@Service
@RequiredArgsConstructor
public class SocketDevResponseService {
// ISO 8601格式
private final DateTimeFormatter formatter = DateTimeFormatter.ISO_DATE_TIME;
private List<String> dataTypeList;
private Set<String> dataTypeList;
private List<String> icdTypeList;
private final List<String> nonHarmonicList = Stream.of(DicDataEnum.FREQ.getCode(), DicDataEnum.V.getCode(), DicDataEnum.I.getCode(), DicDataEnum.IMBV.getCode(), DicDataEnum.IMBA.getCode(), DicDataEnum.VOLTAGE.getCode(), DicDataEnum.F.getCode()).collect(Collectors.toList());
private final List<String> harmonicList = Stream.of(DicDataEnum.HV.getCode(), DicDataEnum.HI.getCode(), DicDataEnum.HP.getCode(), DicDataEnum.HSV.getCode(), DicDataEnum.HSI.getCode()).collect(Collectors.toList());
private final IPqDevService iPqDevService;
private final IPqDevSubService iPqDevSubService;
@@ -80,24 +72,16 @@ public class SocketDevResponseService {
private final IPqScriptDtlsService pqScriptDtlsService;
private final DetectionServiceImpl detectionServiceImpl;
private final DetectionDataDealService detectionDataDealService;
private final ISysRegResService iSysRegResService;
private final IPqScriptCheckDataService iPqScriptCheckDataService;
private final ISysTestConfigService sysTestConfigService;
private final SimAndDigHarmonicService adHarmonicService;
private final IAdPlanService adPlanService;
private final IPqScriptCheckDataService pqScriptCheckDataService;
private final IDictDataService dictDataService;
@Value("${phaseAngle.isEnable}")
private Boolean isPhaseAngle;
/**
* 存储的装置相序数据
*/
List<DevData> devInfo = new ArrayList<>();
//Map<String, DevData> devDataMap = new HashMap<>();
/**
* 成功结束的测点
*/
@@ -128,7 +112,7 @@ public class SocketDevResponseService {
switch (Objects.requireNonNull(sourceOperateCodeEnum)) {
//设备通讯校验
case YJC_SBTXJY:
devComm(socketDataMsg, param, msg);
devComm(socketDataMsg, param);
break;
//协议校验
case YJC_XYJY:
@@ -167,6 +151,9 @@ public class SocketDevResponseService {
break;
case YXT:
break;
default:
// todo... 要日志记录或者websocket送到前端友好提示用户
break;
}
}
@@ -307,7 +294,7 @@ public class SocketDevResponseService {
}
break;
default:
CnSocketUtil.sendUnSocket(param.getUserPageId());
WebServiceManager.sendUnknownErrorMessage(param.getUserPageId());
break;
}
@@ -409,7 +396,7 @@ public class SocketDevResponseService {
issueParam.setDevIds(param.getDevIds());
issueParam.setScriptId(param.getScriptId());
if (param.getOperateType().equals(SourceOperateCodeEnum.RE_ERROR_TEST.getValue())) {
if (param.getReCheckType().equals(SourceOperateCodeEnum.RE_ERROR_TEST.getValue())) {
//不合格项复检
Set<Integer> indexes = new HashSet<>();
StorageParam storageParam = new StorageParam();
@@ -450,13 +437,19 @@ public class SocketDevResponseService {
Map<String, Long> sourceIssueMap = sourceIssues.stream().collect(Collectors.groupingBy(SourceIssue::getType, Collectors.counting()));
SocketManager.initMap(sourceIssueMap);
socketMsg.setData(JSON.toJSONString(sourceIssues.get(0)));
socketMsg.setRequestId(SourceOperateCodeEnum.FORMAL_REAL.getValue() + CnSocketUtil.STEP_TAG + sourceIssues.get(0).getType());
SocketManager.sendMsg(param.getUserPageId() + CnSocketUtil.SOURCE_TAG, JSON.toJSONString(socketMsg));
//告诉前端当前项开始了
WebSocketVO<Object> webSocketVO = new WebSocketVO<>();
webSocketVO.setRequestId(sourceIssues.get(0).getType() + CnSocketUtil.START_TAG);
String type = sourceIssues.get(0).getType();
if (ResultUnitEnum.P.getCode().equals(type)) {
sourceIssues.get(0).setType(ResultUnitEnum.V_ABSOLUTELY.getCode());
webSocketVO.setRequestId(ResultUnitEnum.P.getCode() + CnSocketUtil.START_TAG);
} else {
webSocketVO.setRequestId(sourceIssues.get(0).getType() + CnSocketUtil.START_TAG);
}
socketMsg.setData(JSON.toJSONString(sourceIssues.get(0)));
socketMsg.setRequestId(SourceOperateCodeEnum.FORMAL_REAL.getValue() + CnSocketUtil.STEP_TAG + type);
SocketManager.sendMsg(param.getUserPageId() + CnSocketUtil.SOURCE_TAG, JSON.toJSONString(socketMsg));
webSocketVO.setDesc(null);
WebServiceManager.sendMsg(param.getUserPageId(), JSON.toJSONString(webSocketVO));
} else {
@@ -549,7 +542,7 @@ public class SocketDevResponseService {
DevXiNumData.GF gfItem = createGFItem(monitorId, F);
gf.add(gfItem);
//表格数据
CnSocketUtil.sendToWebSocket(param.getUserPageId(), SourceOperateCodeEnum.Coefficient_Check.getValue(), SourceOperateCodeEnum.DATA_CHNFACTOR$02.getValue(), coefficientVO, null);
WebServiceManager.sendDetectionMessage(param.getUserPageId(), SourceOperateCodeEnum.Coefficient_Check.getValue(), SourceOperateCodeEnum.DATA_CHNFACTOR$02.getValue(), coefficientVO, null);
});
DevXiNumData devXiNumData = createDevXiNumData(devIp, gf, xiFlag.get());
saveDevXiNumData(devIp, devXiNumData);
@@ -702,7 +695,7 @@ public class SocketDevResponseService {
/**
* 装置通讯检测
*/
private void devComm(SocketDataMsg socketDataMsg, PreDetectionParam param, String msg) {
private void devComm(SocketDataMsg socketDataMsg, PreDetectionParam param) {
SourceResponseCodeEnum dictDataEnumByCode = SourceResponseCodeEnum.getDictDataEnumByCode(socketDataMsg.getCode());
String s = param.getUserPageId() + CnSocketUtil.DEV_TAG;
SocketMsg<String> socketMsg = new SocketMsg<>();
@@ -712,12 +705,11 @@ public class SocketDevResponseService {
successComm.add(result);
//单个测点通讯成功
WebServiceManager.sendMsg(param.getUserPageId(), MsgUtil.msgToWebData(socketDataMsg, FormalTestManager.devNameMapComm, 1));
System.out.println("设备通讯校验!" + successComm.size() + "=====" + FormalTestManager.monitorIdListComm.size());
if (successComm.size() == FormalTestManager.monitorIdListComm.size()) {
// 通知前端整个装置通讯检测过程成功
SocketDataMsg temMsg = new SocketDataMsg();
temMsg.setCode(SourceResponseCodeEnum.DEV_COMM_ALL_SUCCESS.getCode());
temMsg.setCode(SourceResponseCodeEnum.ALL_SUCCESS.getCode());
temMsg.setOperateCode(SourceOperateCodeEnum.DEV_INIT_GATHER_01.getValue());
temMsg.setRequestId(SourceOperateCodeEnum.YJC_SBTXJY.getValue());
WebServiceManager.sendMsg(param.getUserPageId(), JSON.toJSONString(temMsg));
@@ -754,14 +746,12 @@ public class SocketDevResponseService {
case RE_OPERATE:
//出现已经初始化情况,发送用户用户确认是否继续检测
WebServiceManager.sendMsg(param.getUserPageId(), JSON.toJSONString(socketDataMsg));
CnSocketUtil.quitSend(param);
break;
case NO_INIT_DEV:
//发起关闭操作
CnSocketUtil.quitSend(param);
break;
default:
CnSocketUtil.sendUnSocket(param.getUserPageId());
WebServiceManager.sendUnknownErrorMessage(param.getUserPageId());
break;
}
}
@@ -793,7 +783,6 @@ public class SocketDevResponseService {
String s = param.getUserPageId() + CnSocketUtil.DEV_TAG;
switch (Objects.requireNonNull(dictDataEnumByCode)) {
case SUCCESS:
if (socketDataMsg.getOperateCode().equals(SourceOperateCodeEnum.DEV_INIT_GATHER_02.getValue())) {
successComm.add(socketDataMsg.getData());
WebServiceManager.sendMsg(param.getUserPageId(), MsgUtil.msgToWebData(socketDataMsg, FormalTestManager.devNameMapComm, 1));
@@ -830,51 +819,73 @@ public class SocketDevResponseService {
icdCheckDataMap.clear();
dataTypeList = pqScriptDtlsService.getScriptToIcdCheckInfo(param);
icdTypeList = FormalTestManager.devList.stream().map(PreDetection::getIcdType).distinct().collect(Collectors.toList());
icdTypeList = FormalTestManager.devList.stream().map(PreDetection::getIcdType).collect(Collectors.toList());
PreDetection preDetection = FormalTestManager.devList.stream().filter(obj -> obj.getIcdType().equals(icdTypeList.get(0))).findFirst().orElse(null);
boolean angleCheck = false;
if (ObjectUtil.isNotNull(preDetection)) {
angleCheck = preDetection.getAngle() == 1 ? true : false;
}
if (angleCheck) {
dataTypeList.add(DetectionCodeEnum.REAL_PREFIX.getCode() + DetectionCodeEnum.VA.getCode());
dataTypeList.add(DetectionCodeEnum.REAL_PREFIX.getCode() + DetectionCodeEnum.IA.getCode());
} else {
dataTypeList.remove(DetectionCodeEnum.REAL_PREFIX.getCode() + DetectionCodeEnum.VA.getCode());
dataTypeList.remove(DetectionCodeEnum.REAL_PREFIX.getCode() + DetectionCodeEnum.IA.getCode());
}
socketMsg.setRequestId(SourceOperateCodeEnum.YJC_XYJY.getValue());
socketMsg.setOperateCode(SourceOperateCodeEnum.VERIFY_MAPPING$01.getValue());
Map<String, Object> map = new HashMap<>(2);
map.put("dataType", dataTypeList);
map.put("dataType", new ArrayList<>(dataTypeList));
map.put("icdType", icdTypeList.get(0));
socketMsg.setData(JSON.toJSONString(map));
System.out.println("开始脚本与icd校验:++++++++++");
SocketManager.sendMsg(s, JSON.toJSONString(socketMsg));
}
completeJudgment(param);
} else if (socketDataMsg.getOperateCode().equals(SourceOperateCodeEnum.VERIFY_MAPPING$01.getValue())) {
String data = socketDataMsg.getData();
IcdCheckData icdCheckData = JSON.parseObject(data, IcdCheckData.class);
boolean isContinue = true;
for (int i = 0; i < icdCheckData.getResultData().size(); i++) {
IcdCheckData.ResultData item = icdCheckData.getResultData().get(i);
Integer errorType = getErrorType(item.getDesc(), item.getPhaseResult());
// 校验脚本与icd校验失败
if (errorType.equals(0)) {
isContinue = false;
Map<String, Object> map = new HashMap<>(2);
map.put("icdType", icdCheckData.getIcdType());
DetectionCodeEnum anEnum = DetectionCodeEnum.getDetectionCodeByCode(item.getDesc());
map.put("dataType", anEnum.getMessage());
WebSocketVO<String> webSocketVO = new WebSocketVO<>();
webSocketVO.setData(JSON.toJSONString(map));
webSocketVO.setRequestId(SourceOperateCodeEnum.YJC_XYJY.getValue());
webSocketVO.setOperateCode(SourceOperateCodeEnum.VERIFY_MAPPING$01.getValue());
webSocketVO.setCode(SourceResponseCodeEnum.SUCCESS.getCode());
WebServiceManager.sendMsg(param.getUserPageId(), JSON.toJSONString(webSocketVO));
CnSocketUtil.quitSend(param);
break;
List<String> descList = icdCheckData.getResultData().stream().map(
obj -> {
if (DetectionCodeEnum.MAG.getCode().equals(obj.getDesc()) || DetectionCodeEnum.DUR.getCode().equals(obj.getDesc())) {
return DetectionCodeEnum.AVG_PREFIX.getCode() + obj.getDesc();
} else {
return DetectionCodeEnum.REAL_PREFIX.getCode() + obj.getDesc();
}
}
).collect(Collectors.toList());
if (descList.containsAll(dataTypeList)) {
for (int i = 0; i < icdCheckData.getResultData().size(); i++) {
IcdCheckData.ResultData item = icdCheckData.getResultData().get(i);
Integer errorType = getErrorType(item.getDesc(), item.getPhaseResult());
// 校验脚本与icd校验失败
if (errorType.equals(0)) {
isContinue = false;
Map<String, Object> map = new HashMap<>(2);
map.put("icdType", icdCheckData.getIcdType());
DetectionCodeEnum anEnum = DetectionCodeEnum.getDetectionCodeByCode(item.getDesc());
map.put("dataType", anEnum.getMessage());
WebSocketVO<String> webSocketVO = new WebSocketVO<>();
webSocketVO.setData(JSON.toJSONString(map));
webSocketVO.setRequestId(SourceOperateCodeEnum.YJC_XYJY.getValue());
webSocketVO.setOperateCode(SourceOperateCodeEnum.VERIFY_MAPPING$01.getValue());
webSocketVO.setCode(SourceResponseCodeEnum.FAIL.getCode());
WebServiceManager.sendMsg(param.getUserPageId(), JSON.toJSONString(webSocketVO));
} else {
icdCheckDataMap.put(icdCheckData.getIcdType(), icdCheckData);
}
}
} else {
isContinue = false;
}
icdCheckDataMap.put(icdCheckData.getIcdType(), icdCheckData);
if (isContinue) {
//System.out.println("icdCheckDataMap.size()="+icdCheckDataMap.size()+",icdTypeList.size()="+icdTypeList.size());
if (icdCheckDataMap.size() == icdTypeList.size()) {
SocketDataMsg temMsg = new SocketDataMsg();
temMsg.setCode(SourceResponseCodeEnum.DEV_COMM_ALL_SUCCESS.getCode());
temMsg.setCode(SourceResponseCodeEnum.ALL_SUCCESS.getCode());
temMsg.setOperateCode(SourceOperateCodeEnum.VERIFY_MAPPING$01.getValue());
temMsg.setRequestId(SourceOperateCodeEnum.YJC_XYJY.getValue());
WebServiceManager.sendMsg(param.getUserPageId(), JSON.toJSONString(temMsg));
@@ -897,7 +908,7 @@ public class SocketDevResponseService {
SocketManager.sendMsg(param.getUserPageId() + CnSocketUtil.SOURCE_TAG, JSON.toJSONString(socketMsg));
} else if (param.getTestItemList().get(2)) {
// 后续做正式检测
if (param.getOperateType().equals(SourceOperateCodeEnum.RE_ERROR_TEST.getValue())) {
if (param.getReCheckType().equals(SourceOperateCodeEnum.RE_ERROR_TEST.getValue())) {
//不合格项复检
Set<Integer> indexes = new HashSet<>();
StorageParam storageParam = new StorageParam();
@@ -933,35 +944,42 @@ public class SocketDevResponseService {
Map<String, Long> sourceIssueMap = sourceIssues.stream().collect(Collectors.groupingBy(SourceIssue::getType, Collectors.counting()));
SocketManager.initMap(sourceIssueMap);
socketMsg.setData(JSON.toJSONString(sourceIssues.get(0)));
socketMsg.setRequestId(SourceOperateCodeEnum.FORMAL_REAL.getValue() + CnSocketUtil.STEP_TAG + sourceIssues.get(0).getType());
SocketManager.sendMsg(param.getUserPageId() + CnSocketUtil.SOURCE_TAG, JSON.toJSONString(socketMsg));
//告诉前端当前项开始了
WebSocketVO<Object> webSocketVO = new WebSocketVO<>();
webSocketVO.setRequestId(sourceIssues.get(0).getType() + CnSocketUtil.START_TAG);
String type = sourceIssues.get(0).getType();
if (ResultUnitEnum.P.getCode().equals(type)) {
sourceIssues.get(0).setType(ResultUnitEnum.V_ABSOLUTELY.getCode());
webSocketVO.setRequestId(ResultUnitEnum.P.getCode() + CnSocketUtil.START_TAG);
} else {
webSocketVO.setRequestId(sourceIssues.get(0).getType() + CnSocketUtil.START_TAG);
}
socketMsg.setData(JSON.toJSONString(sourceIssues.get(0)));
socketMsg.setRequestId(SourceOperateCodeEnum.FORMAL_REAL.getValue() + CnSocketUtil.STEP_TAG + type);
socketMsg.setOperateCode(SourceOperateCodeEnum.OPER_GATHER.getValue());
SocketManager.sendMsg(param.getUserPageId() + CnSocketUtil.SOURCE_TAG, JSON.toJSONString(socketMsg));
webSocketVO.setDesc(null);
WebServiceManager.sendMsg(param.getUserPageId(), JSON.toJSONString(webSocketVO));
}
// if (SourceOperateCodeEnum.TEST_TEM_START.getValue().equals(param.getOperateType())) {
// //暂停检测后的继续检测
// System.out.println("进入暂停后的继续检测》》》》》》》》》》》》》》》》》》》》》》》》》》》" + "剩余检测小项" + SocketManager.getSourceList().size());
// if (CollUtil.isNotEmpty(SocketManager.getSourceList())) {
// SourceIssue sourceIssue = SocketManager.getSourceList().get(0);
// socketMsg.setRequestId(SourceOperateCodeEnum.FORMAL_REAL.getValue() + CnSocketUtil.STEP_TAG + sourceIssue.getType());
// socketMsg.setOperateCode(SourceOperateCodeEnum.OPER_GATHER.getValue());
// socketMsg.setData(JSON.toJSONString(sourceIssue));
// SocketManager.sendMsg(param.getUserPageId() + CnSocketUtil.SOURCE_TAG, JSON.toJSONString(socketMsg));
// }
// } else {
//
// }
} else {
// 发送下一个脚本与icd校验
String icdType = icdTypeList.stream().filter(it -> !icdCheckDataMap.containsKey(it)).findFirst().orElse(null);
if (ObjectUtil.isNotNull(icdType)) {
PreDetection preDetection = FormalTestManager.devList.stream().filter(obj -> obj.getIcdType().equals(icdType)).findFirst().orElse(null);
boolean angleCheck = false;
if (ObjectUtil.isNotNull(preDetection)) {
angleCheck = preDetection.getAngle() == 1 ? true : false;
}
if (angleCheck) {
dataTypeList.add(DetectionCodeEnum.REAL_PREFIX.getCode() + DetectionCodeEnum.VA.getCode());
dataTypeList.add(DetectionCodeEnum.REAL_PREFIX.getCode() + DetectionCodeEnum.IA.getCode());
} else {
dataTypeList.remove(DetectionCodeEnum.REAL_PREFIX.getCode() + DetectionCodeEnum.VA.getCode());
dataTypeList.remove(DetectionCodeEnum.REAL_PREFIX.getCode() + DetectionCodeEnum.IA.getCode());
}
Map<String, Object> map = new HashMap<>(2);
map.put("dataType", dataTypeList);
map.put("dataType", new ArrayList<>(dataTypeList));
map.put("icdType", icdType);
socketMsg.setData(JSON.toJSONString(map));
socketMsg.setRequestId(SourceOperateCodeEnum.YJC_XYJY.getValue());
@@ -970,6 +988,23 @@ public class SocketDevResponseService {
SocketManager.sendMsg(s, JSON.toJSONString(socketMsg));
}
}
} else {
WebSocketVO<String> webSocketVO = new WebSocketVO<>();
webSocketVO.setRequestId(SourceOperateCodeEnum.YJC_XYJY.getValue());
webSocketVO.setOperateCode(SourceOperateCodeEnum.VERIFY_MAPPING$01.getValue());
webSocketVO.setCode(SourceResponseCodeEnum.FAIL.getCode());
dataTypeList.removeAll(descList);
for (String dataType : dataTypeList) {
Map<String, Object> map = new HashMap<>(2);
map.put("icdType", icdCheckData.getIcdType());
DetectionCodeEnum anEnum = DetectionCodeEnum.getDetectionCodeByCode(dataType.replace(DetectionCodeEnum.REAL_PREFIX.getCode(), ""));
map.put("dataType", anEnum.getMessage());
webSocketVO.setData(JSON.toJSONString(map));
WebServiceManager.sendMsg(param.getUserPageId(), JSON.toJSONString(webSocketVO));
}
CnSocketUtil.quitSend(param);
}
}
break;
@@ -996,7 +1031,8 @@ public class SocketDevResponseService {
CnSocketUtil.quitSend(param);
break;
default:
CnSocketUtil.sendUnSocket(param.getUserPageId());
WebServiceManager.sendUnknownErrorMessage(param.getUserPageId());
// todo... 这种情况是报文的状态码不一致,需要记录到日志表,以便问题追踪
break;
}
}
@@ -1022,11 +1058,13 @@ public class SocketDevResponseService {
}
} else {
if (ObjectUtil.isNotNull(list.getA()) && list.getA().equals(1.0) && ObjectUtil.isNotNull(list.getB()) && list.getB().equals(1.0) && ObjectUtil.isNotNull(list.getC()) && list.getC().equals(1.0) || ObjectUtil.isNotNull(list.getT()) && list.getT().equals(1.0)) {
return 1; // 装置上送错误
// 装置上送错误
return 1;
}
}
}
return 0; // icd文件与脚本不匹配
// icd文件与脚本不匹配
return 0;
}
@@ -1076,8 +1114,8 @@ public class SocketDevResponseService {
WebSocketVO<String> webSocketVO = new WebSocketVO<>();
webSocketVO.setRequestId(SourceOperateCodeEnum.YJC_XUJY.getValue());
webSocketVO.setOperateCode(SourceOperateCodeEnum.DEV_DATA_REQUEST_02.getValue());
webSocketVO.setCode(SourceResponseCodeEnum.PHASE_CHECK_FAIL.getCode());
webSocketVO.setData(SourceResponseCodeEnum.PHASE_CHECK_FAIL.getMessage());
webSocketVO.setCode(SourceResponseCodeEnum.ALL_FAIL.getCode());
webSocketVO.setData(SourceResponseCodeEnum.ALL_FAIL.getMessage());
WebServiceManager.sendMsg(param.getUserPageId(), JSON.toJSONString(webSocketVO));
CnSocketUtil.quitSend(param);
@@ -1087,7 +1125,7 @@ public class SocketDevResponseService {
//向前端推送消息
SocketDataMsg temMsg = new SocketDataMsg();
temMsg.setCode(SourceResponseCodeEnum.DEV_COMM_ALL_SUCCESS.getCode());
temMsg.setCode(SourceResponseCodeEnum.ALL_SUCCESS.getCode());
temMsg.setOperateCode(SourceOperateCodeEnum.DEV_DATA_REQUEST_02.getValue());
temMsg.setRequestId(SourceOperateCodeEnum.YJC_XUJY.getValue());
WebServiceManager.sendMsg(param.getUserPageId(), JSON.toJSONString(temMsg));
@@ -1127,7 +1165,7 @@ public class SocketDevResponseService {
issueParam.setDevIds(param.getDevIds());
issueParam.setScriptId(param.getScriptId());
if (param.getOperateType().equals(SourceOperateCodeEnum.RE_ERROR_TEST.getValue())) {
if (param.getReCheckType().equals(SourceOperateCodeEnum.RE_ERROR_TEST.getValue())) {
//不合格项复检
Set<Integer> indexes = new HashSet<>();
StorageParam storageParam = new StorageParam();
@@ -1163,14 +1201,20 @@ public class SocketDevResponseService {
Map<String, Long> sourceIssueMap = sourceIssues.stream().collect(Collectors.groupingBy(SourceIssue::getType, Collectors.counting()));
SocketManager.initMap(sourceIssueMap);
//告诉前端当前项开始了
WebSocketVO<Object> webSocketVO = new WebSocketVO<>();
String type = sourceIssues.get(0).getType();
if (ResultUnitEnum.P.getCode().equals(type)) {
sourceIssues.get(0).setType(ResultUnitEnum.V_ABSOLUTELY.getCode());
webSocketVO.setRequestId(ResultUnitEnum.P.getCode() + CnSocketUtil.START_TAG);
} else {
webSocketVO.setRequestId(sourceIssues.get(0).getType() + CnSocketUtil.START_TAG);
}
socketMsg.setData(JSON.toJSONString(sourceIssues.get(0)));
socketMsg.setRequestId(SourceOperateCodeEnum.FORMAL_REAL.getValue() + CnSocketUtil.STEP_TAG + sourceIssues.get(0).getType());
socketMsg.setRequestId(SourceOperateCodeEnum.FORMAL_REAL.getValue() + CnSocketUtil.STEP_TAG + type);
socketMsg.setOperateCode(SourceOperateCodeEnum.OPER_GATHER.getValue());
SocketManager.sendMsg(param.getUserPageId() + CnSocketUtil.SOURCE_TAG, JSON.toJSONString(socketMsg));
//告诉前端当前项开始了
WebSocketVO<Object> webSocketVO = new WebSocketVO<>();
webSocketVO.setRequestId(sourceIssues.get(0).getType() + CnSocketUtil.START_TAG);
webSocketVO.setDesc(null);
WebServiceManager.sendMsg(param.getUserPageId(), JSON.toJSONString(webSocketVO));
} else {
@@ -1206,7 +1250,7 @@ public class SocketDevResponseService {
CnSocketUtil.quitSend(param);
break;
default:
CnSocketUtil.sendUnSocket(param.getUserPageId());
WebServiceManager.sendUnknownErrorMessage(param.getUserPageId());
break;
}
}
@@ -1257,8 +1301,14 @@ public class SocketDevResponseService {
System.out.println("当前小项结束进行删除============" + sourceIssue.getType() + CnSocketUtil.SPLIT_TAG + sourceIssue.getIndex());
//小项检测完后小项数减一并更新map
long residueCount = SocketManager.getSourceTarget(sourceIssue.getType()) - 1;
SocketManager.addTargetMap(sourceIssue.getType(), residueCount);
long residueCount = 0;
if (sourceIssue.getIsP()) {
residueCount = SocketManager.getSourceTarget(ResultUnitEnum.P.getCode()) - 1;
SocketManager.addTargetMap(ResultUnitEnum.P.getCode(), residueCount);
} else {
residueCount = SocketManager.getSourceTarget(sourceIssue.getType()) - 1;
SocketManager.addTargetMap(sourceIssue.getType(), residueCount);
}
System.out.println("该大项还有" + residueCount + "个小项没有进行检测!!!!!!!!");
//当该大项中小项数量变为0则任务该大项检测结束
@@ -1282,7 +1332,7 @@ public class SocketDevResponseService {
resultList.add(devTem);
});
allDevTestList.clear();
CnSocketUtil.sendToWebSocket(param.getUserPageId(), socketDataMsg.getRequestId().split(CnSocketUtil.STEP_TAG)[1] + CnSocketUtil.END_TAG, null, resultList, null);
WebServiceManager.sendDetectionMessage(param.getUserPageId(), socketDataMsg.getRequestId().split(CnSocketUtil.STEP_TAG)[1] + CnSocketUtil.END_TAG, null, resultList, null);
}
//在这一步判断是否已经触发暂停按钮
if (FormalTestManager.stopFlag && CollUtil.isNotEmpty(SocketManager.getSourceList())) {
@@ -1300,14 +1350,19 @@ public class SocketDevResponseService {
SourceIssue sourceIssues = SocketManager.getSourceList().get(0);
// 如果上一个大项检测完成,则检测下一个大项,并向前端推送消息
if (residueCount == 0) {
CnSocketUtil.sendToWebSocket(param.getUserPageId(), sourceIssues.getType() + CnSocketUtil.START_TAG, null, new ArrayList<>(), null);
WebServiceManager.sendDetectionMessage(param.getUserPageId(), sourceIssues.getType() + CnSocketUtil.START_TAG, null, new ArrayList<>(), null);
}
String type = sourceIssues.getType();
if (sourceIssues.getIsP()) {
sourceIssues.setType(ResultUnitEnum.V_ABSOLUTELY.getCode());
type = ResultUnitEnum.P.getCode();
}
//控源下发下一个小项脚本
SocketMsg<String> xuMsg = new SocketMsg<>();
xuMsg.setOperateCode(SourceOperateCodeEnum.OPER_GATHER.getValue());
xuMsg.setData(JSON.toJSONString(sourceIssues));
xuMsg.setRequestId(SourceOperateCodeEnum.FORMAL_REAL.getValue() + CnSocketUtil.STEP_TAG + sourceIssues.getType());
xuMsg.setRequestId(SourceOperateCodeEnum.FORMAL_REAL.getValue() + CnSocketUtil.STEP_TAG + type);
SocketManager.sendMsg(param.getUserPageId() + CnSocketUtil.SOURCE_TAG, JSON.toJSONString(xuMsg));
} else {
//TODO 是否最终检测完成需要推送给用户
@@ -1316,7 +1371,7 @@ public class SocketDevResponseService {
checkDataParam.setIsValueTypeName(false);
List<String> valueType = iPqScriptCheckDataService.getValueType(checkDataParam);
iPqDevService.updateResult(param.getDevIds(), valueType, param.getCode(), param.getUserId(), param.getTemperature(), param.getHumidity());
iPqDevService.updateResult( param.getDevIds(), valueType, param.getCode(), param.getUserId(), param.getTemperature(), param.getHumidity());
CnSocketUtil.quitSend(param);
}
successComm.clear();
@@ -1351,7 +1406,7 @@ public class SocketDevResponseService {
case MESSAGE_PARSING_ERROR:
break;
default:
CnSocketUtil.sendUnSocket(param.getUserPageId());
WebServiceManager.sendUnknownErrorMessage(param.getUserPageId());
break;
}
}
@@ -1453,7 +1508,7 @@ public class SocketDevResponseService {
case QUIT_INIT_01:
//关闭所有
SocketManager.removeUser(s);
// CnSocketUtil.quitSendSource(param);
CnSocketUtil.quitSendSource(param);
break;
case QUIT_INIT_02:
socketMsg.setRequestId(SourceOperateCodeEnum.QUITE.getValue());
@@ -1490,7 +1545,7 @@ public class SocketDevResponseService {
}
break;
default:
CnSocketUtil.sendUnSocket(param.getUserPageId());
WebServiceManager.sendUnknownErrorMessage(param.getUserPageId());
break;
}
@@ -1522,9 +1577,10 @@ public class SocketDevResponseService {
private String devMessage(String type,
List<DevData.SqlDataDTO> data,
List<DevData.SqlDataDTO> dataPhase) {
List<DevData.SqlDataDTO> dataPhase,
boolean angleCheck) {
StringBuffer str = new StringBuffer();
if (isPhaseAngle) {
if (angleCheck) {
if (CollUtil.isNotEmpty(data) && CollUtil.isNotEmpty(dataPhase)) {
if (data.size() == dataPhase.size()) {
DevData.SqlDataDTO.ListDTO dto = data.get(0).getList();
@@ -1589,7 +1645,13 @@ public class SocketDevResponseService {
}
compareDev.setDevName(devName);
compareDev.setLineNum(split[1]);
getSourceCompareDev(type.get(2), dataV, dataVPhase, compareDev, sourceMessageMap.get(type.get(3)), sourceV, type.get(4));
boolean angleCheck = false;
PreDetection preDetection = FormalTestManager.devList.stream().filter(x -> x.getDevIP().equals(split[0])).findFirst().orElse(null);
if (ObjectUtil.isNotNull(preDetection)) {
angleCheck = preDetection.getAngle() == 1 ? true : false;
}
getSourceCompareDev(type.get(2), dataV, dataVPhase, compareDev, sourceMessageMap.get(type.get(3)), sourceV, type.get(4), angleCheck);
info.add(compareDev);
}
@@ -1608,20 +1670,19 @@ public class SocketDevResponseService {
SourceCompareDev compareDev,
String desc,
List<SourceIssue.ChannelListDTO> channelList,
String name
) {
String name,
boolean angleCheck) {
//源信息
Map<String, SourceIssue.ChannelListDTO> sourceMap = channelList.stream()
.collect(Collectors.toMap(x -> x.getChannelType(), Function.identity()));
Boolean a = getaBoolean(sourceMap.get(type + "a"), CollUtil.isNotEmpty(data) ? data.get(0).getList().getA() : null,
CollUtil.isNotEmpty(dataPhase) ? dataPhase.get(0).getList().getA() : null);
CollUtil.isNotEmpty(dataPhase) ? dataPhase.get(0).getList().getA() : null, angleCheck);
Boolean b = getaBoolean(sourceMap.get(type + "b"), CollUtil.isNotEmpty(data) ? data.get(0).getList().getB() : null,
CollUtil.isNotEmpty(dataPhase) ? dataPhase.get(0).getList().getB() : null);
CollUtil.isNotEmpty(dataPhase) ? dataPhase.get(0).getList().getB() : null, angleCheck);
Boolean c = getaBoolean(sourceMap.get(type + "c"), CollUtil.isNotEmpty(data) ? data.get(0).getList().getC() : null,
CollUtil.isNotEmpty(dataPhase) ? dataPhase.get(0).getList().getC() : null);
CollUtil.isNotEmpty(dataPhase) ? dataPhase.get(0).getList().getC() : null, angleCheck);
compareDev.setIsQualified(a && b && c);
compareDev.setDesc(name + (compareDev.getIsQualified() ? "合格->" : "不合格->") + CnSocketUtil.STEP_TAG + desc + CnSocketUtil.STEP_TAG + devMessage(type, data, dataPhase));
compareDev.setDesc(name + (compareDev.getIsQualified() ? "合格->" : "不合格->") + CnSocketUtil.STEP_TAG + desc + CnSocketUtil.STEP_TAG + devMessage(type, data, dataPhase, angleCheck));
return compareDev;
}
@@ -1633,7 +1694,7 @@ public class SocketDevResponseService {
* @param devPhase 装置返回相角数据
* @return
*/
private Boolean getaBoolean(SourceIssue.ChannelListDTO channelListDTO, Double devData, Double devPhase) {
private Boolean getaBoolean(SourceIssue.ChannelListDTO channelListDTO, Double devData, Double devPhase, boolean angleCheck) {
Boolean isDev = false;
Boolean isPhase = false;
@@ -1642,7 +1703,7 @@ public class SocketDevResponseService {
BigDecimal.valueOf(channelListDTO.getFAmp() * 0.95),
BigDecimal.valueOf(channelListDTO.getFAmp() * 1.05));
}
if (isPhaseAngle) {
if (angleCheck) {
if (ObjectUtil.isNotNull(devPhase)) {
isPhase = phaseBoolean(channelListDTO, devPhase);
}
@@ -1707,7 +1768,8 @@ public class SocketDevResponseService {
FormalTestManager.monitorIdListComm = pqDevList.stream().flatMap(x -> x.getMonitorList().stream()).map(PreDetection.MonitorListDTO::getLineId).collect(Collectors.toList());
FormalTestManager.devNameMapComm = pqDevList.stream().collect(Collectors.toMap(PreDetection::getDevIP, PreDetection::getDevName));
FormalTestManager.devIdMapComm = pqDevList.stream().collect(Collectors.toMap(PreDetection::getDevIP, PreDetection::getDevId));
FormalTestManager.devIdMapComm.clear();
FormalTestManager.devIdMapComm.putAll(pqDevList.stream().collect(Collectors.toMap(PreDetection::getDevIP, PreDetection::getDevId)));
//初始化有效数据数
//Map<String, SysRegResVO> sysRegResMap = iSysRegResService.listRegRes();
@@ -1720,15 +1782,15 @@ public class SocketDevResponseService {
} else {
dataRule = DictDataEnum.SECTION_VALUE;
}
String code = dictDataService.getDictDataById(plan.getPattern()).getCode();
FormalTestManager.patternEnum = PatternEnum.getEnum(code);
//字典树
SocketManager.valueTypeMap = iPqScriptCheckDataService.getValueTypeMap(param.getScriptId());
if (param.getTestItemList().get(1)) {
initXiManager(param);
}
harmonicRelationMap.put(DetectionCodeEnum.V2_50.getCode(), DetectionCodeEnum.U1.getCode());
harmonicRelationMap.put(DetectionCodeEnum.I2_50.getCode(), DetectionCodeEnum.I1.getCode());
}
//初始化系数校验参数
@@ -1793,8 +1855,11 @@ public class SocketDevResponseService {
System.out.println("原始数据插入数据库开始执行=========================================");
List<SimAndDigNonHarmonicResult> simAndDigNonHarmonicResultList = new ArrayList<>();
List<SimAndDigHarmonicResult> adHarmonicResultList = new ArrayList<>();
Map<String, String> harmonicRelationMap = new HashMap<>();
harmonicRelationMap.put(DetectionCodeEnum.V2_50.getCode(), DetectionCodeEnum.U1.getCode());
harmonicRelationMap.put(DetectionCodeEnum.I2_50.getCode(), DetectionCodeEnum.I1.getCode());
for (DevData data : devDataList) {
LocalDateTime localDateTime = DetectionUtil.timeFormat(data.getTime(), formatter);
LocalDateTime localDateTime = DetectionUtil.timeFormat(data.getTime(), DetectionUtil.FORMATTER);
if (Objects.nonNull(localDateTime)) {
String[] splitArr = data.getId().split(CnSocketUtil.SPLIT_TAG);
@@ -1802,10 +1867,13 @@ public class SocketDevResponseService {
if (nonHarmonicList.contains(sourceIssue.getType())) {
for (DevData.SqlDataDTO sqlDataDTO : data.getSqlData()) {
if (sqlDataDTO.getDesc().equals("PF")) {
continue;
}
DevData.SqlDataDTO.ListDTO listDTO = sqlDataDTO.getList();
SimAndDigNonHarmonicResult adNonHarmonicResult = new SimAndDigNonHarmonicResult();
adNonHarmonicResult.setTimeId(localDateTime);
adNonHarmonicResult.setMonitorId(temId);
adNonHarmonicResult.setDevMonitorId(temId);
adNonHarmonicResult.setScriptId(param.getScriptId());
adNonHarmonicResult.setSort(sourceIssue.getIndex());
@@ -1838,7 +1906,7 @@ public class SocketDevResponseService {
SimAndDigHarmonicResult adHarmonicResult = new SimAndDigHarmonicResult();
adHarmonicResult.setTimeId(localDateTime);
adHarmonicResult.setMonitorId(temId);
adHarmonicResult.setDevMonitorId(temId);
adHarmonicResult.setScriptId(param.getScriptId());
adHarmonicResult.setSort(sourceIssue.getIndex());
adHarmonicResult.setAdType(checkDataMap.get(sqlDataDTO.getDesc()));
@@ -1911,7 +1979,7 @@ public class SocketDevResponseService {
}
if (CollUtil.isNotEmpty(simAndDigNonHarmonicResultList)) {
Map<String, List<SimAndDigNonHarmonicResult>> map = simAndDigNonHarmonicResultList.stream().collect(Collectors.groupingBy(x -> x.getMonitorId() + x.getTimeId() + x.getScriptId() + x.getSort() + x.getAdType() + x.getDataType()));
Map<String, List<SimAndDigNonHarmonicResult>> map = simAndDigNonHarmonicResultList.stream().collect(Collectors.groupingBy(x -> x.getDevMonitorId() + x.getTimeId() + x.getScriptId() + x.getSort() + x.getAdType() + x.getDataType()));
List<SimAndDigNonHarmonicResult> info = new ArrayList<>();
map.forEach((key, value) -> {
if (value.size() > 1) {
@@ -1921,10 +1989,10 @@ public class SocketDevResponseService {
}
});
detectionDataDealService.acceptAdNon(info, param.getCode());
detectionDataDealService.acceptNonHarmonic(info, param.getCode());
}
if (CollUtil.isNotEmpty(adHarmonicResultList)) {
Map<String, List<SimAndDigHarmonicResult>> map = adHarmonicResultList.stream().collect(Collectors.groupingBy(x -> x.getMonitorId() + x.getTimeId() + x.getScriptId() + x.getSort() + x.getAdType() + x.getDataType()));
Map<String, List<SimAndDigHarmonicResult>> map = adHarmonicResultList.stream().collect(Collectors.groupingBy(x -> x.getDevMonitorId() + x.getTimeId() + x.getScriptId() + x.getSort() + x.getAdType() + x.getDataType()));
List<SimAndDigHarmonicResult> info = new ArrayList<>();
map.forEach((key, value) -> {
if (value.size() > 1) {
@@ -1933,7 +2001,7 @@ public class SocketDevResponseService {
info.addAll(value);
}
});
detectionDataDealService.acceptAd(info, param.getCode());
detectionDataDealService.acceptHarmonic(info, param.getCode());
}
System.out.println("原始数据插入数据库执行成功=========================================");
// };
@@ -1958,7 +2026,8 @@ public class SocketDevResponseService {
}
public void backCheckState(PreDetectionParam param) {
if (CollUtil.isNotEmpty(param.getDevIds()) && StrUtil.isNotBlank(param.getPlanId()) && "1".equals(param.getOperateType())) {
// if (CollUtil.isNotEmpty(param.getDevIds()) && StrUtil.isNotBlank(param.getPlanId()) && "1".equals(param.getOperateType())) {
if (CollUtil.isNotEmpty(param.getDevIds()) && StrUtil.isNotBlank(param.getPlanId())) {
adPlanService.updateBackTestState(param.getPlanId(), param.getDevIds());
}
}

View File

@@ -1,6 +1,5 @@
package com.njcn.gather.detection.handler;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.util.ObjectUtil;
import com.alibaba.fastjson.JSON;
import com.njcn.gather.detection.pojo.enums.SourceOperateCodeEnum;
@@ -12,130 +11,261 @@ import com.njcn.gather.detection.pojo.vo.SocketDataMsg;
import com.njcn.gather.detection.pojo.vo.SocketMsg;
import com.njcn.gather.detection.pojo.vo.WebSocketVO;
import com.njcn.gather.detection.util.socket.*;
import com.njcn.gather.detection.util.socket.cilent.NettyClient;
import com.njcn.gather.detection.util.socket.cilent.NettyDevClientHandler;
import com.njcn.gather.detection.util.socket.websocket.WebServiceManager;
import com.njcn.gather.device.pojo.vo.PreDetection;
import com.njcn.gather.device.service.IPqDevService;
import com.njcn.gather.script.pojo.po.SourceIssue;
import com.njcn.gather.system.pojo.enums.DicDataEnum;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import java.util.*;
import java.util.stream.Collectors;
/**
* 程控源Socket响应处理服务
* <p>
* 该服务类负责处理来自程控源设备的Socket消息响应包括
* - 源初始化响应处理
* - 检测流程控制和状态管理
* - 相序检测、系数校验等特定检测类型的响应处理
* - WebSocket消息推送给前端用户
* - 错误处理和连接管理
* </p>
* <p>
* 主要处理的操作类型:
* - YJC_YTXJY: 源通信校验/源初始化
* - YJC_XUJY: 相序检测
* - FORMAL_REAL: 正式检测
* - Coefficient_Check: 系数校验
* - QUITE_SOURCE: 退出源连接
* </p>
*
* @author CN_Gather Detection Team
* @version 1.0
* @since 2023
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class SocketSourceResponseService {
/**
* 向webSocket客户端发送消息
* 设备信息服务,提供设备基础信息查询功能
*/
private final SocketDevResponseService socketDevResponseService;
private final IPqDevService iPqDevService;
@Value("${socket.device.ip}")
private String ip;
/**
* Socket连接管理器负责管理设备和源的Socket连接
*/
private final SocketManager socketManager;
@Value("${socket.device.port}")
private Integer port;
/**
* 发送WebSocket消息到指定用户页面
* <p>
* 将数据对象转换为JSON字符串并通过WebSocket推送给前端用户
* 用于实时通知用户检测进度、状态变化或错误信息。
* </p>
*
* @param userPageId 用户页面ID用于标识消息接收方
* @param data 要发送的数据对象将被转换为JSON格式
*/
private void sendWebSocketMessage(String userPageId, Object data) {
WebServiceManager.sendMsg(userPageId, JSON.toJSONString(data));
}
/**
* 发送错误消息并退出源连接
* <p>
* 当检测过程中发生错误时,执行以下操作:
* 1. 主动断开与程控源的连接
* 2. 构造包含错误信息的Socket消息
* 3. 通过WebSocket将错误信息推送给前端用户
* </p>
*
* @param param 检测参数包含用户页面ID等信息
* @param socketDataMsg 原始Socket消息用于构造响应消息
* @param errorMessage 具体的错误描述信息
*/
private void sendErrorAndQuit(PreDetectionParam param, SocketDataMsg socketDataMsg, String errorMessage) {
CnSocketUtil.quitSendSource(param);
SocketMsg<String> socketMsg = new SocketMsg<>();
socketMsg.setRequestId(socketDataMsg.getRequestId());
socketMsg.setOperateCode(socketDataMsg.getOperateCode());
socketMsg.setData(errorMessage);
sendWebSocketMessage(param.getUserPageId(), socketMsg);
}
/**
* 发送错误消息并退出源连接(使用枚举消息)
* <p>
* 重载方法,使用预定义的错误码枚举来获取标准化的错误消息。
* 确保错误信息的一致性和规范性。
* </p>
*
* @param param 检测参数
* @param socketDataMsg 原始Socket消息
* @param errorCode 错误码枚举,包含标准化的错误描述
*/
private void sendErrorAndQuit(PreDetectionParam param, SocketDataMsg socketDataMsg, SourceResponseCodeEnum errorCode) {
sendErrorAndQuit(param, socketDataMsg, errorCode.getMessage());
}
/**
* 当前检测会话中的设备列表
* <p>
* 存储正在进行检测的设备信息,包含设备基本信息和监测点配置。
* 注意:该字段存在线程安全问题,建议后续重构为线程安全的设计。
* </p>
*/
private List<PreDetection> devList = new ArrayList<>();
/**
* 当前检测会话中的监测点ID列表
* <p>
* 从设备列表中提取的所有监测点ID集合用于向设备发送数据请求时指定监测范围。
* 与devList字段保持同步更新。
* </p>
*/
private List<String> monitorIdList = new ArrayList<>();
/**
* 程控源响应消息处理主入口
* <p>
* 根据消息中的操作码,分发到相应的处理方法:
* - 解析Socket消息提取操作码
* - 根据操作码类型调用对应的处理方法
* - 支持检测计划模式和模拟测试模式的区分处理
* </p>
* <p>
* 支持的操作类型:
* - YJC_YTXJY: 源通信校验/源初始化
* - YJC_XUJY: 相序检测
* - FORMAL_REAL: 正式检测
* - Coefficient_Check: 系数校验
* - QUITE_SOURCE: 退出源连接
* </p>
*
* @param param 检测参数包含用户ID、设备ID、计划ID等关键信息
* @param msg 从程控源接收的原始Socket消息
* @throws Exception 当消息解析失败或处理过程中发生异常时抛出
*/
public void deal(PreDetectionParam param, String msg) throws Exception {
// 解析接收到的Socket消息
SocketDataMsg socketDataMsg = MsgUtil.socketDataMsg(msg);
// 从requestId中提取操作码requestId格式为操作码_步骤标识
String[] tem = socketDataMsg.getRequestId().split(CnSocketUtil.STEP_TAG);
SourceOperateCodeEnum enumByCode = SourceOperateCodeEnum.getDictDataEnumByCode(tem[0]);
if (ObjectUtil.isNotNull(enumByCode)) {
switch (enumByCode) {
//源初始化
// 源初始化处理根据是否有计划ID判断是正式检测还是模拟检测
case YJC_YTXJY:
if (ObjectUtil.isNotNull(param.getPlanId())) {
// 有计划ID正式检测模式源初始化成功后启动设备检测
detectionDev(param, socketDataMsg);
} else {
// 程控源-源通信校验
// 无计划ID模拟检测模式仅进行源通信校验
handleYtxjySimulate(param, socketDataMsg);
}
break;
//相序检测
// 相序检测:检测设备的相序是否正确
case YJC_XUJY:
phaseSequenceDev(param, socketDataMsg);
break;
//正式检测
// 正式检测根据是否有计划ID选择不同的处理方式
case FORMAL_REAL:
if (ObjectUtil.isNotNull(param.getPlanId())) {
// 有计划ID向设备发送检测参数
senParamToDev(param, socketDataMsg);
} else {
// 无计划ID模拟测试模式
handleSimulateTest(param, socketDataMsg);
}
break;
//系数校验
// 系数校验:验证设备的计量系数是否准确
case Coefficient_Check:
coefficient(param, socketDataMsg);
break;
// 退出源连接:清理资源并关闭连接
case QUITE_SOURCE:
quitDeal(socketDataMsg, param);
break;
// YXT操作暂未实现具体功能
case YXT:
// TODO: 实现YXT操作的具体逻辑
break;
default:
// TODO: 记录未知操作码到日志,并向前端发送友好提示
break;
}
} else {
System.out.println("fggggggggggggggggggggg" + enumByCode);
// TODO: 向前端发送错误提示
log.error("程控源响应消息操作码解析失败,原始消息: {}, 解析结果: {}", msg, enumByCode);
}
}
/**
* 处理模拟检测中的源通信校验响应
* <p>
* 在模拟检测模式下(非计划检测),处理程控源的通信校验响应:
* - 成功时根据参数决定是否向前端发送WebSocket消息
* - 业务未处理:直接转发消息给前端
* - 各种错误情况:统一处理为退出源连接并通知前端
* </p>
* <p>
* 支持的错误类型包括:
* - 源连接错误、程控源错误、测试项解析错误
* - 源控制错误、目标源错误、未初始化错误
* - 未知错误、无法响应错误
* </p>
*
* @param param 检测参数包含用户信息和WebSocket消息发送控制
* @param socketDataMsg 程控源返回的响应消息
*/
private void handleYtxjySimulate(PreDetectionParam param, SocketDataMsg socketDataMsg) {
SourceResponseCodeEnum dictDataEnumByCode = SourceResponseCodeEnum.getDictDataEnumByCode(socketDataMsg.getCode());
if (ObjectUtil.isNotNull(dictDataEnumByCode)) {
switch (dictDataEnumByCode) {
case SUCCESS:
// 源初始化成功根据参数控制是否发送WebSocket消息
if (param.getSendWebMsg()) {
WebServiceManager.sendMsg(param.getUserPageId(), JSON.toJSONString(socketDataMsg));
sendWebSocketMessage(param.getUserPageId(), socketDataMsg);
}
System.out.println(param.getSendWebMsg() + "模拟检测-源初始化成功");
log.info("模拟检测源初始化成功,用户: {}, WebSocket发送: {}",
param.getUserPageId(), param.getSendWebMsg());
break;
case UNPROCESSED_BUSINESS:
WebServiceManager.sendMsg(param.getUserPageId(), JSON.toJSONString(socketDataMsg));
// 业务暂未处理:直接转发消息给前端等待处理
sendWebSocketMessage(param.getUserPageId(), socketDataMsg);
break;
// 各种错误情况:源连接错误、程控源控制错误、测试项解析错误等
case SOURCE_CONNECTION_ERROR:
CnSocketUtil.quitSendSource(param);
WebServiceManager.sendMsg(param.getUserPageId(), JSON.toJSONString(socketDataMsg));
break;
case CONTROLLED_SOURCE_ERROR:
CnSocketUtil.quitSendSource(param);
WebServiceManager.sendMsg(param.getUserPageId(), JSON.toJSONString(socketDataMsg));
break;
case TEST_ITEM_PARSING_ERROR:
CnSocketUtil.quitSendSource(param);
WebServiceManager.sendMsg(param.getUserPageId(), JSON.toJSONString(socketDataMsg));
break;
case SOURCE_CONTROL_ERROR:
CnSocketUtil.quitSendSource(param);
WebServiceManager.sendMsg(param.getUserPageId(), JSON.toJSONString(socketDataMsg));
break;
case TARGET_SOURCE_ERROR:
CnSocketUtil.quitSendSource(param);
WebServiceManager.sendMsg(param.getUserPageId(), JSON.toJSONString(socketDataMsg));
break;
case NOT_INITIALIZED:
CnSocketUtil.quitSendSource(param);
WebServiceManager.sendMsg(param.getUserPageId(), JSON.toJSONString(socketDataMsg));
break;
case UNKNOWN_ERROR:
CnSocketUtil.quitSendSource(param);
WebServiceManager.sendMsg(param.getUserPageId(), JSON.toJSONString(socketDataMsg));
break;
case UNABLE_TO_RESPOND:
// 所有错误情况统一处理:退出源连接并通知前端
CnSocketUtil.quitSendSource(param);
WebServiceManager.sendMsg(param.getUserPageId(), JSON.toJSONString(socketDataMsg));
sendWebSocketMessage(param.getUserPageId(), socketDataMsg);
break;
default:
CnSocketUtil.sendUnSocket(param.getUserPageId());
// 未识别的响应码:发送通用错误消息
WebServiceManager.sendUnknownErrorMessage(param.getUserPageId());
break;
}
}
@@ -152,19 +282,15 @@ public class SocketSourceResponseService {
if (ObjectUtil.isNotNull(dictDataEnumByCode)) {
switch (dictDataEnumByCode) {
case SUCCESS:
WebServiceManager.sendMsg(param.getUserPageId(), JSON.toJSONString(socketDataMsg));
System.out.println("模拟检测-源成功执行脚本" + JSON.toJSONString(socketDataMsg));
sendWebSocketMessage(param.getUserPageId(), socketDataMsg);
log.info("模拟检测源成功执行脚本,用户: {}, 响应消息: {}",
param.getUserPageId(), JSON.toJSONString(socketDataMsg));
break;
case UNPROCESSED_BUSINESS:
WebServiceManager.sendMsg(param.getUserPageId(), JSON.toJSONString(socketDataMsg));
sendWebSocketMessage(param.getUserPageId(), socketDataMsg);
break;
default:
CnSocketUtil.quitSendSource(param);
SocketMsg<String> socketMsg = new SocketMsg<>();
socketMsg.setRequestId(socketDataMsg.getRequestId());
socketMsg.setOperateCode(socketDataMsg.getOperateCode());
socketMsg.setData(dictDataEnumByCode.getMessage());
WebServiceManager.sendMsg(param.getUserPageId(), JSON.toJSONString(socketMsg));
sendErrorAndQuit(param, socketDataMsg, dictDataEnumByCode);
break;
}
}
@@ -173,6 +299,23 @@ public class SocketSourceResponseService {
/**
* 系数校验源数据返回处理
* <p>
* 处理系数校验阶段程控源的响应消息:
* 1. 成功时:向前端推送响应信息,然后向设备发送数据请求
* 2. 构造设备数据请求参数,包含监测点列表和数据类型
* 3. 设置固定的读取参数读取3次数据忽略前4次
* 4. 请求的数据类型:实时电压有效值(real$VRMS)和实时电流有效值(real$IRMS)
* </p>
* <p>
* 数据请求配置:
* - 监测点使用当前会话的monitorIdList
* - 数据类型:["real$VRMS", "real$IRMS"]
* - 读取次数3次
* - 忽略次数4次预热数据
* </p>
*
* @param param 检测参数
* @param socketDataMsg 程控源响应消息
*/
private void coefficient(PreDetectionParam param, SocketDataMsg socketDataMsg) {
SourceResponseCodeEnum dictDataEnumByCode = SourceResponseCodeEnum.getDictDataEnumByCode(socketDataMsg.getCode());
@@ -181,29 +324,28 @@ public class SocketSourceResponseService {
switch (dictDataEnumByCode) {
case SUCCESS:
//向前端推送信息
WebServiceManager.sendMsg(param.getUserPageId(), JSON.toJSONString(socketDataMsg));
sendWebSocketMessage(param.getUserPageId(), socketDataMsg);
String s = param.getUserPageId() + CnSocketUtil.DEV_TAG;
socketMsg.setRequestId(SourceOperateCodeEnum.Coefficient_Check.getValue());
socketMsg.setOperateCode(SourceOperateCodeEnum.DEV_DATA_REQUEST_02.getValue());
DevPhaseSequenceParam phaseSequenceParam = new DevPhaseSequenceParam();
phaseSequenceParam.setMoniterIdList(monitorIdList);
// 系数校验固定检测项:实时电压有效值和实时电流有效值
phaseSequenceParam.setDataType(Arrays.asList("real$VRMS", "real$IRMS"));
// 读取3次数据用于系数计算
phaseSequenceParam.setReadCount(3);
// 忽略前4次数据等待测量稳定
phaseSequenceParam.setIgnoreCount(4);
socketMsg.setData(JSON.toJSONString(phaseSequenceParam));
SocketManager.sendMsg(s, JSON.toJSONString(socketMsg));
break;
case UNPROCESSED_BUSINESS:
WebServiceManager.sendMsg(param.getUserPageId(), JSON.toJSONString(socketDataMsg));
sendWebSocketMessage(param.getUserPageId(), socketDataMsg);
break;
default:
CnSocketUtil.quitSendSource(param);
socketMsg.setRequestId(socketDataMsg.getRequestId());
socketMsg.setOperateCode(socketDataMsg.getOperateCode());
socketMsg.setData(dictDataEnumByCode.getMessage());
WebServiceManager.sendMsg(param.getUserPageId(), JSON.toJSONString(socketMsg));
sendErrorAndQuit(param, socketDataMsg, dictDataEnumByCode);
break;
}
}
@@ -219,14 +361,11 @@ public class SocketSourceResponseService {
SourceResponseCodeEnum dictDataEnumByCode = SourceResponseCodeEnum.getDictDataEnumByCode(socketDataMsg.getCode());
if (ObjectUtil.isNotNull(dictDataEnumByCode)) {
SocketMsg<String> socketMsg = new SocketMsg<>();
switch (dictDataEnumByCode) {
case SUCCESS:
//todo 前端推送收到的消息暂未处理好
WebServiceManager.sendMsg(param.getUserPageId(), JSON.toJSONString(socketDataMsg));
String s = param.getUserPageId() + CnSocketUtil.DEV_TAG;
sendWebSocketMessage(param.getUserPageId(), socketDataMsg);
//开始设备通讯检测(发送设备初始化)
//List<PreDetection> devList = iPqDevService.getDevInfo(param.getDevIds());
Map<String, List<PreDetection>> map = new HashMap<>(1);
map.put("deviceList", FormalTestManager.devList);
String jsonString = JSON.toJSONString(map);
@@ -234,48 +373,30 @@ public class SocketSourceResponseService {
socketMsg.setOperateCode(SourceOperateCodeEnum.DEV_INIT_GATHER_01.getValue());
socketMsg.setData(jsonString);
String json = JSON.toJSONString(socketMsg);
// SocketManager.sendMsg(s,json);
NettyClient.socketClient(ip, port, param, json, new NettyDevClientHandler(param, socketDevResponseService));
// 使用智能发送工具类,自动管理设备连接
socketManager.smartSendToDevice(param, json);
break;
case UNPROCESSED_BUSINESS:
WebServiceManager.sendMsg(param.getUserPageId(), JSON.toJSONString(socketDataMsg));
sendWebSocketMessage(param.getUserPageId(), socketDataMsg);
break;
case SOURCE_CONNECTION_ERROR:
CnSocketUtil.quitSendSource(param);
WebServiceManager.sendMsg(param.getUserPageId(), JSON.toJSONString(socketDataMsg));
break;
case CONTROLLED_SOURCE_ERROR:
CnSocketUtil.quitSendSource(param);
WebServiceManager.sendMsg(param.getUserPageId(), JSON.toJSONString(socketDataMsg));
break;
case TEST_ITEM_PARSING_ERROR:
CnSocketUtil.quitSendSource(param);
WebServiceManager.sendMsg(param.getUserPageId(), JSON.toJSONString(socketDataMsg));
break;
case SOURCE_CONTROL_ERROR:
CnSocketUtil.quitSendSource(param);
WebServiceManager.sendMsg(param.getUserPageId(), JSON.toJSONString(socketDataMsg));
break;
case TARGET_SOURCE_ERROR:
CnSocketUtil.quitSendSource(param);
WebServiceManager.sendMsg(param.getUserPageId(), JSON.toJSONString(socketDataMsg));
break;
case NOT_INITIALIZED:
CnSocketUtil.quitSendSource(param);
WebServiceManager.sendMsg(param.getUserPageId(), JSON.toJSONString(socketDataMsg));
break;
case UNKNOWN_ERROR:
CnSocketUtil.quitSendSource(param);
WebServiceManager.sendMsg(param.getUserPageId(), JSON.toJSONString(socketDataMsg));
break;
case UNABLE_TO_RESPOND:
CnSocketUtil.quitSendSource(param);
WebServiceManager.sendMsg(param.getUserPageId(), JSON.toJSONString(socketDataMsg));
sendWebSocketMessage(param.getUserPageId(), socketDataMsg);
break;
default:
CnSocketUtil.sendUnSocket(param.getUserPageId());
// todo... 这种情况是报文的状态码不一致,需要记录到日志表,以便问题追踪
WebServiceManager.sendUnknownErrorMessage(param.getUserPageId());
break;
}
} else {
// todo... 这种情况是报文的状态码不一致,需要记录到日志表,以便问题追踪
}
}
@@ -292,7 +413,7 @@ public class SocketSourceResponseService {
switch (dictDataEnumByCode) {
case SUCCESS:
//向前端推送信息
WebServiceManager.sendMsg(param.getUserPageId(), JSON.toJSONString(socketDataMsg));
sendWebSocketMessage(param.getUserPageId(), socketDataMsg);
String s = param.getUserPageId() + CnSocketUtil.DEV_TAG;
socketMsg.setRequestId(SourceOperateCodeEnum.YJC_XUJY.getValue());
@@ -304,24 +425,23 @@ public class SocketSourceResponseService {
DevPhaseSequenceParam phaseSequenceParam = new DevPhaseSequenceParam();
phaseSequenceParam.setMoniterIdList(moniterIdList);
// 相序检测项:电压有效值、电压角度、电流有效值、电流角度
phaseSequenceParam.setDataType(Arrays.asList("real$VRMS", "real$VA", "real$IRMS", "real$IA"));
// 相序检测只需要读取1次数据
phaseSequenceParam.setReadCount(1);
// 忽略前10次数据确保相序稳定
phaseSequenceParam.setIgnoreCount(10);
socketMsg.setData(JSON.toJSONString(phaseSequenceParam));
SocketManager.sendMsg(s, JSON.toJSONString(socketMsg));
break;
case UNPROCESSED_BUSINESS:
WebServiceManager.sendMsg(param.getUserPageId(), JSON.toJSONString(socketDataMsg));
sendWebSocketMessage(param.getUserPageId(), socketDataMsg);
break;
case MESSAGE_PARSING_ERROR:
CnSocketUtil.quitSendSource(param);
break;
default:
CnSocketUtil.quitSendSource(param);
socketMsg.setRequestId(socketDataMsg.getRequestId());
socketMsg.setOperateCode(socketDataMsg.getOperateCode());
socketMsg.setData(dictDataEnumByCode.getMessage());
WebServiceManager.sendMsg(param.getUserPageId(), JSON.toJSONString(socketMsg));
sendErrorAndQuit(param, socketDataMsg, dictDataEnumByCode);
break;
}
}
@@ -329,10 +449,28 @@ public class SocketSourceResponseService {
/**
* 组装和装置要数据
* 正式检测时向设备发送参数请求
* <p>
* 当程控源成功执行脚本后,根据检测项目类型向设备发送相应的数据请求:
* 1. 获取源脚本信息,确定检测类型和数据类型
* 2. 根据检测类型设置不同的读取参数:
* - 闪变(F)忽略1次读取2次使用DEV_DATA_REQUEST_01
* - 暂态(VOLTAGE)忽略5次读取1次使用DEV_DATA_REQUEST_03
* - 其他类型忽略5次读取5次根据数据类型选择操作码
* 3. 构造设备数据请求并发送
* 4. 向前端推送检测开始信息
* </p>
* <p>
* 检测参数配置:
* - 闪变检测ignoreCount=1, readCount=2
* - 暂态检测ignoreCount=5, readCount=1
* - 常规检测ignoreCount=5, readCount=5
* - 实时数据使用DEV_DATA_REQUEST_02
* - 分钟数据使用DEV_DATA_REQUEST_01
* </p>
*
* @param param
* @param socketDataMsg
* @param param 检测参数,包含用户和设备信息
* @param socketDataMsg 程控源成功响应消息
*/
private void senParamToDev(PreDetectionParam param, SocketDataMsg socketDataMsg) {
SourceResponseCodeEnum dictDataEnumByCode = SourceResponseCodeEnum.getDictDataEnumByCode(socketDataMsg.getCode());
@@ -343,42 +481,74 @@ public class SocketSourceResponseService {
//向前端推送信息
// webSocketHandler.sendMsgToUser(param.getUserPageId(), JSON.toJSONString(socketDataMsg));
// 构造设备通道标识用户ID + 设备标签
String s = param.getUserPageId() + CnSocketUtil.DEV_TAG;
// 获取当前检测的源脚本信息,包含检测类型和数据要求
SourceIssue sourceIssue = SocketManager.getSourceList().get(0);
List<String> comm = sourceIssue.getDevValueTypeList(); //形如:类型&小项code这种形式。例如real$VRMS、real$IRMS
System.out.println("向装置下发的参数>>>>>>>>" + comm);
socketMsg.setRequestId(SourceOperateCodeEnum.FORMAL_REAL.getValue() + CnSocketUtil.STEP_TAG + sourceIssue.getType());
// 数据类型列表,格式real$VRMS、real$IRMS
List<String> comm = sourceIssue.getDevValueTypeList();
log.debug("向设备下发检测参数,用户: {}, 数据类型: {}", param.getUserPageId(), comm);
// 设置请求ID正式检测操作码 + 步骤标识 + 检测类型
socketMsg.setRequestId(socketDataMsg.getRequestId());
// 根据检测类型设置不同的读取参数和操作码
int ignoreCount;
int readData;
if (DicDataEnum.F.getCode().equals(sourceIssue.getType())) {
// 闪变检测:数据变化较慢,只需少量预热和读取
// 闪变测量稳定性好预热1次即可
ignoreCount = 1;
// 读取2次数据计算闪变值
readData = 2;
socketMsg.setOperateCode(SourceOperateCodeEnum.DEV_DATA_REQUEST_01.getValue());
} else if (DicDataEnum.VOLTAGE.getCode().equals(sourceIssue.getType())) {
// 暂态电压检测:需要更多预热时间,但只读取一次快照
// 暂态事件需要5次预热确保触发稳定
ignoreCount = 5;
// 暂态检测只需要捕获一次事件
readData = 1;
socketMsg.setOperateCode(SourceOperateCodeEnum.DEV_DATA_REQUEST_03.getValue());
} else {
// 常规检测(谐波、不平衡度等):需要多次采样以提高精度
// 常规检测预热5次等待稳定
ignoreCount = 5;
// 读取5次数据进行统计分析
readData = 5;
//区分实时数据还是分钟数据
// 根据数据类型选择相应的请求操作码
if ("real".equals(sourceIssue.getDataType())) {
// 实时数据:瞬时值或有效值
socketMsg.setOperateCode(SourceOperateCodeEnum.DEV_DATA_REQUEST_02.getValue());
} else {
// 分钟数据:统计周期内的平均值或累计值
socketMsg.setOperateCode(SourceOperateCodeEnum.DEV_DATA_REQUEST_01.getValue());
}
}
System.out.println("devList is empty:" + CollectionUtils.isEmpty(devList));
log.debug("检测设备列表状态检查,用户: {}, 设备列表为空: {}",
param.getUserPageId(), CollectionUtils.isEmpty(devList));
//List<String> moniterIdList = devList.stream().flatMap(x -> x.getMonitorList().stream()).map(PreDetection.MonitorListDTO::getLineId).collect(Collectors.toList());
// 构造设备数据请求参数
DevPhaseSequenceParam phaseSequenceParam = new DevPhaseSequenceParam();
// 设置监测点ID列表
phaseSequenceParam.setMoniterIdList(monitorIdList);
if (socketDataMsg.getRequestId().equals(SourceOperateCodeEnum.FORMAL_REAL.getValue() + CnSocketUtil.STEP_TAG + "P")) {
comm.add("real$PF");
}
// 设置数据类型列表
phaseSequenceParam.setDataType(comm);
// 设置读取次数
phaseSequenceParam.setReadCount(readData);
// 设置忽略次数
phaseSequenceParam.setIgnoreCount(ignoreCount);
socketMsg.setData(JSON.toJSONString(phaseSequenceParam));
// 向设备发送数据请求
SocketManager.sendMsg(s, JSON.toJSONString(socketMsg));
// 构造前端显示的设备列表只包含设备ID和名称
List<DevLineTestResult> devListRes = new ArrayList<>();
devList.forEach(item -> {
DevLineTestResult devLineTestResult = new DevLineTestResult();
@@ -387,21 +557,21 @@ public class SocketSourceResponseService {
devListRes.add(devLineTestResult);
});
// 构造WebSocket消息并推送给前端通知检测开始
WebSocketVO<Object> webSocketVO = new WebSocketVO<>();
// 设置请求ID检测类型 + 开始标识
webSocketVO.setRequestId(socketDataMsg.getRequestId().split(CnSocketUtil.STEP_TAG)[1] + CnSocketUtil.START_TAG);
// 检测描述信息
webSocketVO.setDesc(SocketManager.getSourceList().get(0).getDesc());
// 参与检测的设备列表
webSocketVO.setData(devListRes);
WebServiceManager.sendMsg(param.getUserPageId(), JSON.toJSONString(webSocketVO));
sendWebSocketMessage(param.getUserPageId(), webSocketVO);
break;
case UNPROCESSED_BUSINESS:
WebServiceManager.sendMsg(param.getUserPageId(), JSON.toJSONString(socketDataMsg));
sendWebSocketMessage(param.getUserPageId(), socketDataMsg);
break;
default:
CnSocketUtil.quitSendSource(param);
socketMsg.setRequestId(socketDataMsg.getRequestId());
socketMsg.setOperateCode(socketDataMsg.getOperateCode());
socketMsg.setData(dictDataEnumByCode.getMessage());
WebServiceManager.sendMsg(param.getUserPageId(), JSON.toJSONString(socketMsg));
sendErrorAndQuit(param, socketDataMsg, dictDataEnumByCode);
break;
}
}
@@ -409,7 +579,23 @@ public class SocketSourceResponseService {
/**
* 退出检测返回
* 处理退出检测的响应
* <p>
* 当用户主动退出检测或系统需要终止检测时,处理程控源的退出响应:
* - 成功退出移除Socket连接管理中的用户信息向前端发送成功消息
* - 业务未处理:不做特殊处理
* - 消息解析错误/无法响应:移除用户连接信息
* - 其他错误:调用退出源连接方法
* </p>
* <p>
* 退出流程:
* 1. 解析响应状态码
* 2. 根据状态码执行相应的清理操作
* 3. 确保Socket连接资源得到正确释放
* </p>
*
* @param socketDataMsg 程控源退出响应消息
* @param param 检测参数,包含用户信息
*/
private void quitDeal(SocketDataMsg socketDataMsg, PreDetectionParam param) {
SourceResponseCodeEnum dictDataEnumByCode = SourceResponseCodeEnum.getDictDataEnumByCode(socketDataMsg.getCode());
@@ -417,7 +603,7 @@ public class SocketSourceResponseService {
case SUCCESS:
//通讯校验成功
SocketManager.removeUser(param.getUserPageId() + CnSocketUtil.SOURCE_TAG);
WebServiceManager.sendMsg(param.getUserPageId(), JSON.toJSONString(socketDataMsg));
sendWebSocketMessage(param.getUserPageId(), socketDataMsg);
break;
case UNPROCESSED_BUSINESS:
break;
@@ -435,13 +621,37 @@ public class SocketSourceResponseService {
}
/**
* 初始化检测设备和监测点列表
* <p>
* 在开始检测前,根据检测参数初始化当前会话的设备信息:
* 1. 清空之前的设备列表和监测点列表
* 2. 根据设备ID列表查询设备详细信息
* 3. 从设备信息中提取所有监测点的线路ID
* 4. 同步更新XiNumberManager中的设备列表
* </p>
* <p>
* 该方法通常在检测开始前调用,确保后续的检测流程能够获取到正确的
* 设备配置和监测点信息。
* </p>
*
* @param param 检测参数包含要检测的设备ID列表
*/
public void initList(PreDetectionParam param) {
// 清空现有列表,为新的检测会话做准备
devList.clear();
monitorIdList.clear();
// 查询设备详细信息,包含监测点配置
this.devList = iPqDevService.getDevInfo(param.getDevIds());
this.monitorIdList = devList.stream().flatMap(x -> x.getMonitorList().stream())
// 提取所有设备的监测点线路ID
this.monitorIdList = devList.stream()
.flatMap(x -> x.getMonitorList().stream())
.map(PreDetection.MonitorListDTO::getLineId)
.collect(Collectors.toList());
// 同步更新系数管理器中的设备列表
XiNumberManager.xiDevList = devList;
}

View File

@@ -0,0 +1,11 @@
package com.njcn.gather.detection.mapper;
import com.github.yulichang.base.MPJBaseMapper;
import com.njcn.gather.detection.pojo.po.AdPair;
/**
* @author caozehui
* @data 2025-08-18
*/
public interface AdPairMapper extends MPJBaseMapper<AdPair> {
}

View File

@@ -0,0 +1,29 @@
package com.njcn.gather.detection.pojo.dto;
import com.njcn.gather.err.pojo.po.PqErrSysDtls;
import lombok.Data;
/**
* @author caozehui
* @data 2025-08-12
*/
@Data
public class ConditionDataDTO {
/**
* 某一相别且某一个误差条件范围内的被检色设备数据
*/
private Double devData;
/**
* 某一相别且某一个误差条件范围内的标准设备数据
*/
private Double stdDevData;
/**
* 与上面数据所对应的误差体系详情
*/
private PqErrSysDtls pqErrSysDtls;
}

View File

@@ -0,0 +1,16 @@
package com.njcn.gather.detection.pojo.dto;
import lombok.Data;
/**
* @author caozehui
* @data 2025-08-12
*/
@Data
public class HarmonicConditionDataDTO extends ConditionDataDTO {
/**
* (间谐波)谐波次数
*/
private Double harmonicNum;
}

View File

@@ -0,0 +1,18 @@
package com.njcn.gather.detection.pojo.dto;
import lombok.Data;
/**
* @author caozehui
* @data 2025-09-01
*/
@Data
public class WaveCommandDTO {
private String ip;
private Integer port;
private String oper;
private Integer line;
}

View File

@@ -0,0 +1,14 @@
package com.njcn.gather.detection.pojo.dto;
import lombok.Data;
/**
* @author caozehui
* @data 2025-09-01
*/
@Data
public class WaveResultDTO {
private String id;
private String path;
}

View File

@@ -13,28 +13,44 @@ public enum DetectionCodeEnum {
FREQ("FREQ", "频率"),
VRMS("VRMS", "相电压有效值"),
PVRMS("PVRMS", "线电压有效值"),
DELTA_V("DELTA_V", "电压偏差"),
VA("VA", "电压相角"),
U1A("U1A", "相电压基波有效值角度值"),
PU1A("PU1A", "线电压基波有效值角度值"),
U1("U1", "基波电压"),
PU1("PU1", "线电压基波电压"),
V2_50("V2-50", "谐波电压"),
PV2_50("PV2-50", "线电压谐波电压"),
I2_50("I2-50", "谐波电流"),
P2_50("P2-50", "谐波有功功率"),
SV_1_49("SV_1-49", "间谐波电压"),
PSV_1_49("PSV_1-49", "线电压间谐波电压"),
SI_1_49("SI_1-49", "间谐波电流"),
MAG("MAG", "电压幅值"),
DUR("DUR", "持续时间"),
IRMS("IRMS", "电流有效值"),
IA("IA", "电流相角"),
I1A("I1A", "电流基波角度值"),
V_UNBAN("V_UNBAN", "三相电压负序不平衡度"),
I_UNBAN("I_UNBAN", "三相电流负序不平衡度"),
PST("PST", "短时间闪变"),
P_FUND("P_FUND", "功率"),
W("W", "有功功率"),
VARW("VARW", "无功功率"),
VAW("VAW", "视在功率"),
// PF("PF", "功率因数"),
// P_FUND("P_FUND", "基波有功功率"),
// P_HVAR("P_HVAR", "基波无功功率"),
// P_HVA("P_HVA", "基波视在功率"),
I1("I1", "基波电流"),
UNKNOWN_ERROR("-1", "未知异常"),
STAR("Star","星型接线"),
DELTA("Delta","角型接线");
STAR("Star", "星型接线"),
DELTA("Delta", "角型接线"),
REAL_PREFIX("real$", "实时数据前缀"),
AVG_PREFIX("avg$", "统计数据前缀");
private final String code;
private final String message;
@@ -43,6 +59,7 @@ public enum DetectionCodeEnum {
this.code = code;
this.message = message;
}
public static DetectionCodeEnum getDetectionCodeByCode(String code) {
for (DetectionCodeEnum detectionCodeEnum : DetectionCodeEnum.values()) {
if (ObjectUtil.equals(code, detectionCodeEnum.getCode())) {

View File

@@ -9,6 +9,7 @@ import lombok.Getter;
@Getter
public enum DetectionResponseEnum {
PLAN_PATTERN_NOT("A020001", "计划模式查询为空"),
PLAN_NOT_EXIST("A020001", "计划信息缺失"),
SCRIPT_PATTERN_NOT("A020001", "检测脚本查询为空"),
SOURCE_INFO_NOT("A020002", "源表信息不存在"),
PLAN_AND_SOURCE_NOT("A020003", "计划和源关系不存在"),
@@ -17,7 +18,8 @@ public enum DetectionResponseEnum {
SOURCE_NOT_CONNECT("A020006", "源未连接"),
SCRIPT_CHECK_DATA_NOT_EXIST("A020040","测试脚本项暂无配置" );
SCRIPT_CHECK_DATA_NOT_EXIST("A020040","测试脚本项暂无配置" ),
EXCEED_MAX_TIME("A020041","检测次数超出最大限制!" );
private final String code;
private final String message;

View File

@@ -0,0 +1,24 @@
package com.njcn.gather.detection.pojo.enums;
import lombok.Getter;
/**
* @author caozehui
* @data 2025-08-13
*/
@Getter
public enum ResultEnum {
QUALIFIED(1, "符合"),
NOT_QUALIFIED(2, "不符合"),
NETWORK_TIMEOUT(3, "网络超时"),
NO_ERROR_SYS(4, "不在误差条件范围内"),
NO_COMPARE_ERROR_SYS(5, "不参与误差比较");
private int value;
private String msg;
ResultEnum(int value, String msg) {
this.value = value;
this.msg = msg;
}
}

View File

@@ -48,9 +48,11 @@ public enum SourceOperateCodeEnum {
YJC_SBTXJY("yjc_sbtxjy", "设备通讯检测"),
YJC_XYJY("yjc_xyjy", "协议校验"),
YJC_XUJY("YJC_xujy", "相序校验"),
YJC_ALIGN("YJC_align","实时数据对齐校验"),
YJC_MXYZXJY("YJC_mxyzxjy", "模型一致性校验"),
YJC_ALIGN("yjc_align","实时数据对齐校验"),
YJC_MXYZXJY("yjc_mxyzxjy", "模型一致性校验"),
FORMAL_REAL("formal_real","正式检测"),
RECORD_WAVE_STEP1("record_wave_step1","启动录波_step1"),
RECORD_WAVE_STEP2("record_wave_step2","启动录波_step2"),
// SIMULATE_REAL("simulate_real","模拟检测"),
Coefficient_Check("Coefficient_Check","系数校验"),
QUITE("quit","关闭设备通讯初始化"),
@@ -100,7 +102,8 @@ public enum SourceOperateCodeEnum {
/**
* ftp文件传送指令
*/
FTP_SEND_01("FTP_SEND$01", "发送文件"),;
FTP_SEND_01("FTP_SEND$01", "发送文件"),
RDRE$01("RDRE$01", "启动录波");
private final String value;
private final String msg;

View File

@@ -14,6 +14,7 @@ public enum SourceResponseCodeEnum {
UNPROCESSED_BUSINESS(10201, "立即响应,业务还未处理,类似肯定应答"),
NORMAL_RESPONSE(10202, "正常响应中间状态码"),
ICD_NOT_FOUND(10500, "未找到对应ICD"),
RECORD_WAVE_FAILED(10501, "录波失败"),
MESSAGE_PARSING_ERROR(10520, "报文解析有误"),
CONTROLLED_SOURCE_ERROR(10521, "程控源参数有误"),
TEST_ITEM_PARSING_ERROR(10522, "测试项解析有误"),
@@ -37,10 +38,11 @@ public enum SourceResponseCodeEnum {
//自定义前端展示消息
SOCKET_ERROR(25000,"服务端连接失败"),
DEV_COMM_ALL_SUCCESS(25001,"校验成功"),
DEV_COMM_TEST_FAIL(25002,"设备通讯校验失败"),
PHASE_CHECK_FAIL(25003,"相序校验未通过"),
ALL_SUCCESS(25001,"校验成功"),
FAIL(25002,"失败"),
ALL_FAIL(25003,"校验失败"),
RECEIVE_DATA_TIME_OUT(25004,"接收数据超时"),
REAL_DATA_CHECK_FAIL(25005,"实时数据校验失败")

View File

@@ -21,7 +21,7 @@ public class ContrastDetectionParam {
private String planId;
@ApiModelProperty("用户ID唯一标识")
private String userId;
private String loginName;
@ApiModelProperty("被检设备ID列表")
@NotEmpty(message = DetectionValidMessage.DEV_IDS_NOT_EMPTY)
@@ -32,13 +32,15 @@ public class ContrastDetectionParam {
private List<String> standardDevIds;
/**
* key为 标准设备ID_检测点序号、value为 被检设备ID_检测点序号
* key为被检设备ID_检测点序号、value为 标准设备ID_检测点序号
*/
@ApiModelProperty("配对关系")
@NotEmpty(message = DetectionValidMessage.PAIRS_NOT_EMPTY)
private Map<String,String> pairs;
private Map<String, String> pairs;
/**
* 检测项列表。第一个元素为预检测、第二个元素为系数校准、第三个元素为正式检测
*/
private List<Boolean> testItemList;
private String userId;
}

View File

@@ -5,6 +5,7 @@ import lombok.Data;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotEmpty;
import java.util.List;
import java.util.Map;
/**
* @author wr
@@ -14,7 +15,8 @@ import java.util.List;
@Data
public class PreDetectionParam {
private String operateType;
// "1"-"全部检测" , "2"-"不合格项复检"
private String reCheckType;
/**
* 检测计划id
@@ -23,11 +25,6 @@ public class PreDetectionParam {
private String planId;
/**
* 数字、模拟、比对
*/
private String pattern;
/**
* 用户功能组成唯一标识 zhangsan_test
*/

View File

@@ -0,0 +1,24 @@
package com.njcn.gather.detection.pojo.po;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.io.Serializable;
/**
* @author caozehui
* @data 2025-08-18
*/
@Data
@TableName("ad_pair")
public class AdPair implements Serializable {
private String id;
private String planId;
private Integer num;
private String devMonitorId;
private String stdDevMonitorId;
}

View File

@@ -0,0 +1,41 @@
package com.njcn.gather.detection.pojo.vo;
import cn.afterturn.easypoi.excel.annotation.Excel;
import lombok.Data;
/**
* @author caozehui
* @data 2025-08-07
*/
@Data
public class AlignDataExcel {
@Excel(name = "时间", orderNum = "1", width = 40, groupName = "标准设备")
private String timeStdDev;
@Excel(name = "Ua", orderNum = "2", groupName = "标准设备")
private Double uaStdDev;
@Excel(name = "Ub", orderNum = "3", groupName = "标准设备")
private Double ubStdDev;
@Excel(name = "Uc", orderNum = "4", groupName = "标准设备")
private Double ucStdDev;
@Excel(name = "时间", orderNum = "5", width = 40, groupName = "被检设备")
private String timeDev;
@Excel(name = "Ua", orderNum = "6", groupName = "被检设备")
private Double uaDev;
@Excel(name = "Ub", orderNum = "7", groupName = "被检设备")
private Double ubDev;
@Excel(name = "Uc", orderNum = "8", groupName = "被检设备")
private Double ucDev;
}

View File

@@ -0,0 +1,63 @@
package com.njcn.gather.detection.pojo.vo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
/**
* @author caozehui
* @data 2025-08-06
*/
@Data
public class AlignDataVO {
private String stdDevName;
private List<ChannelData> channelDataList;
// 通道数据
@Data
@AllArgsConstructor
@NoArgsConstructor
public static class ChannelData {
// 标准设备通道号
private String stdDevNum;
// 与之对应的被检设备名称_通道号
private String devInfo;
// 数据
private List<RawData> dataList;
}
@Data
@AllArgsConstructor
@NoArgsConstructor
public static class RawData {
private String timeDev;
private Double uaDev;
private Double ubDev;
private Double ucDev;
private Double utDev;
private String timeStdDev;
private Double uaStdDev;
private Double ubStdDev;
private Double ucStdDev;
private Double utStdDev;
private String unit;
}
}

View File

@@ -48,4 +48,9 @@ public class DetectionData {
* 单位
*/
private String unit;
/**
* 误差体系详情ID(比对式使用)
*/
private String errorDtlId;
}

View File

@@ -1,10 +1,7 @@
package com.njcn.gather.detection.pojo.vo;
import io.swagger.models.auth.In;
import lombok.Data;
import java.util.List;
/**
* @Author: cdf
* @CreateTime: 2024-12-26
@@ -13,6 +10,11 @@ import java.util.List;
@Data
public class DevLineTestResult {
/**
* 检测项code
*/
private String scriptName;
private String deviceId;
private String deviceName;

View File

@@ -0,0 +1,29 @@
package com.njcn.gather.detection.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.njcn.gather.detection.pojo.po.AdPair;
import java.util.List;
/**
* @author caozehui
* @data 2025-08-18
*/
public interface IAdPariService extends IService<AdPair> {
/**
* 获取最大的检测次数
*
* @param devMonitorId
* @return
*/
Integer getMaxNum(String devMonitorId);
/**
* 根据设备id查询配对关系
*
* @param devIds
* @return
*/
List<AdPair> listByDevIds(List<String> devIds);
}

View File

@@ -4,6 +4,8 @@ import com.njcn.gather.detection.pojo.param.ContrastDetectionParam;
import com.njcn.gather.detection.pojo.param.PreDetectionParam;
import com.njcn.gather.detection.pojo.param.SimulateDetectionParam;
import java.util.Map;
/**
* @author wr
@@ -56,4 +58,9 @@ public interface PreDetectionService {
* @param param
*/
void startContrastTest(ContrastDetectionParam param);
/**
* 导出实时数据对齐过程中的数据
*/
void exportAlignData();
}

View File

@@ -0,0 +1,45 @@
package com.njcn.gather.detection.service.impl;
import cn.hutool.core.collection.CollUtil;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.njcn.gather.detection.mapper.AdPairMapper;
import com.njcn.gather.detection.pojo.po.AdPair;
import com.njcn.gather.detection.service.IAdPariService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.util.Collections;
import java.util.List;
/**
* @author caozehui
* @data 2025-08-18
*/
@Service
@RequiredArgsConstructor
public class AdPairServiceImpl extends ServiceImpl<AdPairMapper, AdPair> implements IAdPariService {
@Override
public Integer getMaxNum(String devMonitorId) {
List<AdPair> adPairList = this.lambdaQuery().select(AdPair::getNum)
.eq(AdPair::getDevMonitorId, devMonitorId)
.orderByDesc(AdPair::getNum)
.last("LIMIT 1").list();
if (CollUtil.isNotEmpty(adPairList)) {
return adPairList.get(0).getNum();
}
return 1;
}
@Override
public List<AdPair> listByDevIds(List<String> devIds) {
if (CollUtil.isNotEmpty(devIds)) {
QueryWrapper<AdPair> wrapper = new QueryWrapper<>();
devIds.forEach(devId -> wrapper.likeRight("ad_pair.Dev_Monitor_Id", devId));
return this.list(wrapper);
} else {
return Collections.emptyList();
}
}
}

View File

@@ -1,7 +1,6 @@
package com.njcn.gather.detection.service.impl;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ObjectUtil;
import com.alibaba.fastjson.JSON;
@@ -22,10 +21,7 @@ import com.njcn.gather.detection.util.business.DetectionCommunicateUtil;
import com.njcn.gather.detection.util.socket.CnSocketUtil;
import com.njcn.gather.detection.util.socket.FormalTestManager;
import com.njcn.gather.detection.util.socket.SocketManager;
import com.njcn.gather.detection.util.socket.WebServiceManager;
import com.njcn.gather.detection.util.socket.cilent.NettyClient;
import com.njcn.gather.detection.util.socket.cilent.NettyContrastClientHandler;
import com.njcn.gather.detection.util.socket.cilent.NettySourceClientHandler;
import com.njcn.gather.detection.util.socket.websocket.WebServiceManager;
import com.njcn.gather.device.pojo.po.PqDev;
import com.njcn.gather.device.pojo.vo.PreDetection;
import com.njcn.gather.device.service.IPqDevService;
@@ -33,6 +29,7 @@ import com.njcn.gather.plan.pojo.po.AdPlan;
import com.njcn.gather.plan.pojo.po.AdPlanSource;
import com.njcn.gather.plan.service.IAdPlanService;
import com.njcn.gather.plan.service.IAdPlanSourceService;
import com.njcn.gather.result.pojo.enums.ResultUnitEnum;
import com.njcn.gather.script.pojo.param.PqScriptCheckDataParam;
import com.njcn.gather.script.pojo.param.PqScriptIssueParam;
import com.njcn.gather.script.pojo.po.SourceIssue;
@@ -42,17 +39,21 @@ import com.njcn.gather.source.pojo.po.SourceInitialize;
import com.njcn.gather.source.service.IPqSourceService;
import com.njcn.gather.system.dictionary.pojo.enums.DictDataEnum;
import com.njcn.gather.system.dictionary.service.IDictDataService;
import com.njcn.web.utils.HttpServletUtil;
import com.njcn.web.utils.RequestUtil;
import io.netty.channel.Channel;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.math.BigDecimal;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.*;
import java.util.stream.Collectors;
@@ -61,9 +62,6 @@ import java.util.stream.Collectors;
@Slf4j
public class PreDetectionServiceImpl implements PreDetectionService {
private final String stepTag = "&&";
private final String handlerSourceStr = "_Source";
private final IPqDevService iPqDevService;
private final IDictDataService dictDataService;
private final IAdPlanService iAdPlanService;
@@ -75,14 +73,10 @@ public class PreDetectionServiceImpl implements PreDetectionService {
private final SocketSourceResponseService socketSourceResponseService;
private final SocketContrastResponseService socketContrastResponseService;
private final IPqScriptCheckDataService iPqScriptCheckDataService;
private final SocketManager socketManager;
@Value("${socket.source.ip:192.168.1.138}")
private String ip;
@Value("${socket.source.port:61000}")
private Integer port;
//private final SocketSourceResponseService sourceResponseService;
@Value("${log.homeDir}")
private String alignDataFilePath;
@Override
@@ -92,9 +86,9 @@ public class PreDetectionServiceImpl implements PreDetectionService {
//用于处理异常导致的socket通道未关闭socket交互异常
DetectionCommunicateUtil.checkCommunicateChannel(param);
/*
先组装源通讯协议
查询计划什么模式的(除了对比式,其他都是一个计划对应一个源)
*/
* 先组装源通讯协议
* 查询计划什么模式的(除了对比式,其他都是一个计划对应一个源)
*/
AdPlan plan = iAdPlanService.getById(param.getPlanId());
param.setScriptId(plan.getScriptId());
param.setErrorSysId(plan.getErrorSysId());
@@ -102,21 +96,30 @@ public class PreDetectionServiceImpl implements PreDetectionService {
if (ObjectUtil.isNotNull(plan)) {
String code = dictDataService.getDictDataById(plan.getPattern()).getCode();
DictDataEnum dictDataEnumByCode = DictDataEnum.getDictDataEnumByCode(code);
switch (dictDataEnumByCode) {
case DIGITAL:
case SIMULATE:
sendYtxSocket(param);
break;
case CONTRAST:
break;
default:
throw new BusinessException(DetectionResponseEnum.PLAN_PATTERN_NOT);
if (Objects.nonNull(dictDataEnumByCode)) {
switch (dictDataEnumByCode) {
case DIGITAL:
case SIMULATE:
sendYtxSocket(param);
break;
case CONTRAST:
break;
default:
throw new BusinessException(DetectionResponseEnum.PLAN_PATTERN_NOT);
}
} else {
throw new BusinessException(DetectionResponseEnum.PLAN_PATTERN_NOT);
}
} else {
throw new BusinessException(DetectionResponseEnum.PLAN_NOT_EXIST);
}
}
/**
* 原本系数校准单独写的,现在合并了,该方法过期了,没有调用了
*/
@Deprecated
@Override
public void coefficientCheck(PreDetectionParam param) {
// 检测是否存在连接的通道,后期需要做成动态,如果组合中不是第一位,则不需要关闭,也不用初始化 todo....
@@ -135,8 +138,8 @@ public class PreDetectionServiceImpl implements PreDetectionService {
msg.setOperateCode(SourceOperateCodeEnum.INIT_GATHER.getValue());
msg.setData(JSON.toJSONString(sourceParam));
param.setSourceId(sourceParam.getSourceId());
// NettyClient.socketClient(ip, port, param, JSON.toJSONString(msg), new NettySourceClientHandler(param, sourceResponseService));
NettyClient.socketClient(ip, port, param, JSON.toJSONString(msg), new NettySourceClientHandler(param, socketSourceResponseService));
// 使用智能发送工具类,自动管理连接
socketManager.smartSendToSource(param, JSON.toJSONString(msg));
} else {
throw new BusinessException(DetectionResponseEnum.SOURCE_INFO_NOT);
}
@@ -146,10 +149,29 @@ public class PreDetectionServiceImpl implements PreDetectionService {
}
/**
* 发送源通信校验Socket连接数字式和模拟式检测模式
*
* <p>该方法用于建立与程控源设备的Socket连接进行源通信校验。主要流程</p>
* <ul>
* <li>1. 存储检测参数到全局管理器</li>
* <li>2. 根据计划ID获取计划源信息</li>
* <li>3. 获取源设备初始化参数</li>
* <li>4. 初始化设备和源响应服务列表</li>
* <li>5. 组装Socket请求报文</li>
* <li>6. 建立Netty客户端连接</li>
* </ul>
*
* @param param 预检测参数包含计划ID、用户ID等信息
* @throws BusinessException 当计划源信息不存在或源初始化参数为空时抛出
* @see SourceOperateCodeEnum#YJC_YTXJY 源通信校验操作码
* @see SourceOperateCodeEnum#INIT_GATHER 初始化采集操作码
*/
private void sendYtxSocket(PreDetectionParam param) {
WebServiceManager.addPreDetectionParam(param);
AdPlanSource planSource = adPlanSourceService.getOne(new LambdaQueryWrapper<AdPlanSource>().eq(AdPlanSource::getPlanId, param.getPlanId()));
param.setSourceId(planSource.getSourceId());
String loginName = RequestUtil.getLoginNameByToken();
WebServiceManager.addPreDetectionParam(loginName, param);
if (ObjectUtil.isNotNull(planSource)) {
//获取源初始化参数
SourceInitialize sourceParam = pqSourceService.getSourceInitializeParam(planSource.getSourceId());
@@ -161,9 +183,8 @@ public class PreDetectionServiceImpl implements PreDetectionService {
socketMsg.setRequestId(SourceOperateCodeEnum.YJC_YTXJY.getValue());
socketMsg.setOperateCode(SourceOperateCodeEnum.INIT_GATHER.getValue());
socketMsg.setData(JSON.toJSONString(sourceParam));
//建立与源控程序的socket连接
// NettyClient.socketClient(ip, port, param, JSON.toJSONString(socketMsg), new NettySourceClientHandler(param, sourceResponseService));
NettyClient.socketClient(ip, port, param, JSON.toJSONString(socketMsg), new NettySourceClientHandler(param, socketSourceResponseService));
//使用智能发送工具类,自动管理与源控程序的socket连接
socketManager.smartSendToSource(param, JSON.toJSONString(socketMsg));
} else {
throw new BusinessException(DetectionResponseEnum.SOURCE_INFO_NOT);
}
@@ -172,16 +193,34 @@ public class PreDetectionServiceImpl implements PreDetectionService {
}
}
/**
* 发送源通信校验Socket连接仿真模式
*
* <p>该方法专门用于仿真检测模式下的源通信校验。与普通模式的区别:</p>
* <ul>
* <li>直接使用传入的sourceId获取源初始化参数</li>
* <li>不需要通过计划ID查询计划源信息</li>
* <li>适用于独立的源设备通信测试</li>
* </ul>
*
* @param param 预检测参数必须包含sourceId和userPageId
* @throws BusinessException 当源初始化参数为空时抛出
* @see #sendYtxSocket(PreDetectionParam) 普通检测模式的源通信校验
* @see SourceOperateCodeEnum#YJC_YTXJY 源通信校验操作码
* @see SourceOperateCodeEnum#INIT_GATHER 初始化采集操作码
*/
private void sendYtxSocketSimulate(PreDetectionParam param) {
SourceInitialize sourceParam = pqSourceService.getSourceInitializeParam(param.getSourceId());
param.setSourceId(sourceParam.getSourceId());
WebServiceManager.addPreDetectionParam(param);
String loginName = RequestUtil.getLoginNameByToken();
WebServiceManager.addPreDetectionParam(loginName, param);
if (ObjectUtil.isNotNull(sourceParam)) {
SocketMsg<String> socketMsg = new SocketMsg<>();
socketMsg.setRequestId(SourceOperateCodeEnum.YJC_YTXJY.getValue());
socketMsg.setOperateCode(SourceOperateCodeEnum.INIT_GATHER.getValue());
socketMsg.setData(JSON.toJSONString(sourceParam));
NettyClient.socketClient(ip, port, param, JSON.toJSONString(socketMsg), new NettySourceClientHandler(param, socketSourceResponseService));
// 使用智能发送工具类,自动管理连接
socketManager.smartSendToSource(param, JSON.toJSONString(socketMsg));
} else {
throw new BusinessException(DetectionResponseEnum.SOURCE_INFO_NOT);
}
@@ -205,14 +244,15 @@ public class PreDetectionServiceImpl implements PreDetectionService {
xuMsg.setData(JSON.toJSONString(sourceIssues));
xuMsg.setRequestId(SourceOperateCodeEnum.FORMAL_REAL.getValue() + "&&" + sourceIssues.getType());
SocketManager.sendMsg(param.getUserPageId() + DetectionCommunicateConstant.SOURCE, JSON.toJSONString(xuMsg));
// Resume_Success
} else {
//TODO 是否最终检测完成需要推送给用户
//TODO 是否最终检测完成需要推送给用户 检测完成
PqScriptCheckDataParam checkDataParam = new PqScriptCheckDataParam();
checkDataParam.setScriptId(param.getScriptId());
checkDataParam.setIsValueTypeName(false);
List<String> valueType = iPqScriptCheckDataService.getValueType(checkDataParam);
List<String> adType = iPqScriptCheckDataService.getValueType(checkDataParam);
iPqDevService.updateResult(param.getDevIds(), valueType, param.getCode(), param.getUserId(), param.getTemperature(), param.getHumidity());
iPqDevService.updateResult(param.getDevIds(), adType, param.getCode(), param.getUserId(), param.getTemperature(), param.getHumidity());
CnSocketUtil.quitSend(param);
}
@@ -254,14 +294,20 @@ public class PreDetectionServiceImpl implements PreDetectionService {
.collect(Collectors.toList());
SourceIssue sourceIssue = sourceIssues.get(0);
String type = sourceIssue.getType();
if (sourceIssue.getIsP()) {
sourceIssue.setType(ResultUnitEnum.V_ABSOLUTELY.getCode());
type = ResultUnitEnum.P.getCode();
}
List<String> comm = sourceIssue.getDevValueTypeList();
System.out.println("向装置下发的参数ddd>>>>>>>>" + comm);
SocketMsg<String> socketMsg = new SocketMsg<>();
socketMsg.setOperateCode(SourceOperateCodeEnum.OPER_GATHER.getValue());
socketMsg.setRequestId(SourceOperateCodeEnum.FORMAL_REAL.getValue() + stepTag + sourceIssue.getType());
socketMsg.setRequestId(SourceOperateCodeEnum.FORMAL_REAL.getValue() + CnSocketUtil.STEP_TAG + type);
socketMsg.setData(JSON.toJSONString(sourceIssues.get(0)));
SocketManager.sendMsg(param.getUserPageId() + handlerSourceStr, JSON.toJSONString(socketMsg));
SocketManager.sendMsg(param.getUserPageId() + CnSocketUtil.SOURCE_TAG, JSON.toJSONString(socketMsg));
}
@Override
@@ -282,51 +328,72 @@ public class PreDetectionServiceImpl implements PreDetectionService {
public void startContrastTest(ContrastDetectionParam param) {
// 参数校验目前仅检查IP是否重复后续可在里面扩展
checkDevIp(param.getDevIds());
//用于处理异常导致的socket通道未关闭socket交互异常
DetectionCommunicateUtil.checkContrastCommunicateChannel(param.getUserId());
socketContrastResponseService.init(param);
// 和通信模块进行连接
this.sendContrastSocket(param);
}
@Override
public void exportAlignData() {
String fileName = "实时数据.xlsx";
HttpServletResponse response = HttpServletUtil.getResponse();
response.reset();
response.setHeader("Content-Disposition", "attachment; filename=\"" + fileName + "\"");
response.setContentType("application/octet-stream;charset=UTF-8");
try {
InputStream inputStream = new FileInputStream(alignDataFilePath + "\\" + fileName);
byte[] buffer = new byte[1024];
int len = 0;
ServletOutputStream outputStream = response.getOutputStream();
while ((len = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, len);
}
outputStream.flush();
outputStream.close();
inputStream.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
/**
* 比对式-与通信模块进行连接
*
* @param param
*/
private void sendContrastSocket(ContrastDetectionParam param) {
String s = param.getUserId() + CnSocketUtil.DEV_TAG;
Map<String, List<PreDetection>> map = new HashMap<>(1);
List<PreDetection> preDetections = BeanUtil.copyToList(FormalTestManager.devList, PreDetection.class);
preDetections.addAll(BeanUtil.copyToList(FormalTestManager.standardDevList, PreDetection.class));
List<PreDetection> preDetections = new ArrayList<>();
preDetections.addAll(FormalTestManager.devList);
preDetections.addAll(FormalTestManager.standardDevList);
preDetections.forEach(x -> {
x.setDevType(x.getIcdType());
x.getMonitorList().forEach(y -> {
String pt = y.getPt();
int i = pt.indexOf(":");
y.setPt(BigDecimal.valueOf(Double.parseDouble(pt.substring(0, i))).divide(BigDecimal.valueOf(Double.parseDouble(pt.substring(i + 1))), 5, BigDecimal.ROUND_HALF_UP) + "");
String ptStr = y.getPtStr();
int i = ptStr.indexOf(":");
y.setPt(BigDecimal.valueOf(Double.parseDouble(ptStr.substring(0, i))).divide(BigDecimal.valueOf(Double.parseDouble(ptStr.substring(i + 1))), 5, BigDecimal.ROUND_HALF_UP).doubleValue());
String ct = y.getCt();
i = ct.indexOf(":");
y.setCt(BigDecimal.valueOf(Double.parseDouble(ct.substring(0, i))).divide(BigDecimal.valueOf(Double.parseDouble(ct.substring(i + 1))), 5, BigDecimal.ROUND_HALF_UP) + "");
String ctStr = y.getCtStr();
i = ctStr.indexOf(":");
y.setCt(BigDecimal.valueOf(Double.parseDouble(ctStr.substring(0, i))).divide(BigDecimal.valueOf(Double.parseDouble(ctStr.substring(i + 1))), 5, BigDecimal.ROUND_HALF_UP).doubleValue());
});
});
map.put("deviceList", preDetections);
String jsonString = JSON.toJSONString(map);
SocketMsg<String> socketMsg = new SocketMsg<>();
socketMsg.setRequestId(SourceOperateCodeEnum.YJC_SBTXJY.getValue());
socketMsg.setOperateCode(SourceOperateCodeEnum.DEV_INIT_GATHER_01.getValue());
socketMsg.setOperateCode(SourceOperateCodeEnum.DEV_INIT_GATHER_02.getValue());
socketMsg.setData(jsonString);
String json = JSON.toJSONString(socketMsg);
SocketManager.sendMsg(s, json);
PreDetectionParam preDetectionParam = new PreDetectionParam();
preDetectionParam.setUserPageId(param.getUserId());
NettyClient.socketClient(ip, port, preDetectionParam, json, new NettyContrastClientHandler(preDetectionParam, socketContrastResponseService));
preDetectionParam.setUserPageId(param.getLoginName());
preDetectionParam.setTestItemList(param.getTestItemList());
preDetectionParam.setDevIds(param.getDevIds());
preDetectionParam.setUserId(param.getUserId());
WebServiceManager.addPreDetectionParam(param.getLoginName(), preDetectionParam);
socketManager.smartSendToContrast(param, JSON.toJSONString(socketMsg));
}
/**

View File

@@ -3,56 +3,141 @@ package com.njcn.gather.detection.util;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ObjectUtil;
import com.njcn.gather.detection.pojo.po.DevData;
import com.njcn.gather.detection.service.impl.DetectionServiceImpl;
import lombok.extern.slf4j.Slf4j;
import java.io.File;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.Arrays;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
* 检测工具类
* <p>
* 提供电能质量检测相关的数据处理、时间转换、统计计算等工具方法。
* 主要功能包括:
* <ul>
* <li>相角矫正和标准化</li>
* <li>设备数据时间对齐判断</li>
* <li>时间格式转换和毫秒数计算</li>
* <li>数值零值判断</li>
* <li>统计数据处理CP95分位数、部分值、平均值等</li>
* <li>数据排序和索引计算</li>
* </ul>
*
* @author caozehui
* @data 2025-07-28
* @version 1.0
* @since 2025-07-28
*/
@Slf4j
public class DetectionUtil {
/**
* 相角矫正到统一个区间
* ISO 8601日期时间格式化器
* 用于解析和格式化符合ISO 8601标准的日期时间字符串
*/
public static final DateTimeFormatter FORMATTER = DateTimeFormatter.ISO_DATE_TIME;
/**
* 毫秒转秒的转换因子
*/
private static final long MILLIS_TO_SECONDS = 1000L;
/**
* 时间对齐判断的容差毫秒数
* 当两个时间戳差值小于此值时,认为时间是对齐的
*/
private static final long TIME_ALIGNMENT_TOLERANCE_MS = 100L;
/**
* 角度相关常量
*/
private static final double ANGLE_180 = 180.0;
private static final double ANGLE_360 = 360.0;
private static final double ANGLE_MINUS_180 = -180.0;
/**
* CP95算法相关常量
*/
private static final int CP95_DATA_SIZE_THRESHOLD = 21;
private static final double CP95_PERCENTILE = 0.05;
private static final int CP95_SMALL_DATA_INDEX = 1;
/**
* 数据处理相关常量
*/
private static final int MIN_DATA_SIZE_FOR_SECTION_VALUE = 2;
/**
* 相角矫正到统一区间[-180°, 180°]
* <p>
* 将任意角度值标准化到[-180°, 180°]范围内,便于相角比较和计算。
* 使用模运算处理任意大小的角度值能正确处理超出多个360°范围的情况。
* <p>
* 示例:
* <ul>
* <li>-1080° → 0°</li>
* <li>-900° → 180°</li>
* <li>720° → 0°</li>
* <li>450° → 90°</li>
* </ul>
*
* @param phase
* @return
* @param phase 待矫正的相角值(单位:度)
* @return 矫正后的相角值,范围在[-180°, 180°]内
*/
public static Double adjustPhase(Double phase) {
if (phase < -180) {
return phase + 360;
if (phase == null) {
return null;
}
if (phase > 180) {
return phase - 360;
// 使用模运算将角度标准化到[-180, 180]范围
double normalizedPhase = phase % ANGLE_360;
// 处理超出[-180, 180]范围的情况
if (normalizedPhase > ANGLE_180) {
normalizedPhase -= ANGLE_360;
} else if (normalizedPhase < ANGLE_MINUS_180) {
normalizedPhase += ANGLE_360;
}
return phase;
return normalizedPhase;
}
/**
* 判断数据是否对齐
* 判断被检设备数据与标准设备数据的时间是否对齐
* <p>
* 数据对齐的判断标准(满足任一条件即可):
* 1. 将时间戳四舍五入到秒级精度后完全相等(处理跨秒边界情况)
* 2. 两个时间戳的毫秒差值小于容差值(处理同秒内的精确对齐)
* <p>
* 示例499ms vs 509ms → 四舍五入后不同秒但差值仅10ms < 100ms → 对齐
*
* @param devData 被检设备数据
* @param standardDevData 标准设备数据
* @return
* @param devData 被检设备数据,包含时间戳信息
* @param standardDevData 标准设备数据,包含时间戳信息
* @return true表示数据时间对齐false表示不对齐
*/
public static boolean isAlignData(DevData devData, DevData standardDevData) {
if (ObjectUtil.isNotNull(devData) && ObjectUtil.isNotNull(standardDevData)) {
// 获取两个设备数据的时间戳(毫秒)
long devMillis = getMillis(devData.getTime());
long standardMillis = getMillis(standardDevData.getTime());
if (BigDecimal.valueOf(devMillis).divide(BigDecimal.valueOf(10), 0, BigDecimal.ROUND_HALF_UP).compareTo(BigDecimal.valueOf(standardMillis).divide(BigDecimal.valueOf(10), 0, BigDecimal.ROUND_HALF_UP)) == 0) {
// 方式1将时间戳转换为秒级精度进行比较处理跨秒边界情况
BigDecimal devSeconds = BigDecimal.valueOf(devMillis).divide(BigDecimal.valueOf(MILLIS_TO_SECONDS), 0, RoundingMode.HALF_UP);
BigDecimal standardSeconds = BigDecimal.valueOf(standardMillis).divide(BigDecimal.valueOf(MILLIS_TO_SECONDS), 0, RoundingMode.HALF_UP);
if (devSeconds.compareTo(standardSeconds) == 0) {
return true;
} else if (Math.abs(devMillis - standardMillis) < 100) {
}
// 方式2毫秒级时间差小于容差值也认为是对齐的处理精确对齐
if (Math.abs(devMillis - standardMillis) < TIME_ALIGNMENT_TOLERANCE_MS) {
return true;
}
}
@@ -60,160 +145,282 @@ public class DetectionUtil {
}
/**
* 将字符串日期时间转换为指定格式的LocalDateTime
* 将字符串日期时间转换为LocalDateTime对象
* <p>
* 使用指定的格式解析时间字符串支持带时区的ISO 8601格式。
* 解析时会将时间统一转换为UTC时区的LocalDateTime。
*
* @param dateTimeStr
* @param formatter
* @return
* @param dateTimeStr 日期时间字符串,应符合指定格式
* @param formatter 时间格式化器,用于解析字符串
* @return 解析成功返回LocalDateTime对象解析失败返回null
*/
public static LocalDateTime timeFormat(String dateTimeStr, DateTimeFormatter formatter) {
try {
// 使用UTC时区解析时间字符串
ZonedDateTime zonedDateTime = ZonedDateTime.parse(dateTimeStr, formatter.withZone(ZoneId.of("UTC")));
LocalDateTime localDateTime = zonedDateTime.toLocalDateTime();
return localDateTime;
// 转换为LocalDateTime对象
return zonedDateTime.toLocalDateTime();
} catch (DateTimeParseException e) {
System.err.println("日期时间字符串格式错误: " + e.getMessage());
log.error("日期时间字符串格式错误: {}", e.getMessage());
return null;
}
}
/**
* 获取字符串日期时间对应的毫秒数
* 获取字符串日期时间对应的UTC毫秒时间戳
* <p>
* 使用默认的ISO_DATE_TIME格式解析时间字符串
* 并转换为UTC时区的毫秒时间戳。
*
* @param dateTimeStr
* @return
* @param dateTimeStr ISO 8601格式的日期时间字符串
* @return UTC时区的毫秒时间戳
*/
public static long getMillis(String dateTimeStr) {
LocalDateTime localDateTime = timeFormat(dateTimeStr, DateTimeFormatter.ISO_DATE_TIME);
if (dateTimeStr == null || dateTimeStr.trim().isEmpty()) {
throw new IllegalArgumentException("日期时间字符串不能为空");
}
LocalDateTime localDateTime = timeFormat(dateTimeStr, FORMATTER);
if (localDateTime == null) {
throw new IllegalArgumentException("无法解析日期时间字符串: " + dateTimeStr);
}
return getMillis(localDateTime);
}
/**
* 获取LocalDateTime的所对应的毫秒数
* 获取LocalDateTime对应的UTC毫秒时间戳
* <p>
* 将LocalDateTime对象转换为UTC时区的毫秒时间戳。
*
* @param localDateTime
* @return
* @param localDateTime 本地日期时间对象
* @return UTC时区的毫秒时间戳
*/
public static long getMillis(LocalDateTime localDateTime) {
if (localDateTime == null) {
throw new IllegalArgumentException("LocalDateTime参数不能为null");
}
return localDateTime.atZone(ZoneId.of("UTC")).toInstant().toEpochMilli();
}
/**
* 判断value是否为0
* @param value
* @return
* 判断数值是否为零(在容差范围内)
* <p>
* 使用BigDecimal进行精确计算避免浮点数精度问题。
* 当数值的绝对值小于预设阈值0.01)时,认为该数值为零。
* 主要用于电流等物理量的零值判断。
*
* @param value 待判断的数值null值被认为是零
* @param ratedCurrent 额定电流,用于计算阈值
* @return true表示数值为零在容差范围内false表示非零
*/
public static boolean isZero(Double value) {
BigDecimal bd = BigDecimal.valueOf(value);
if(bd.subtract(BigDecimal.ZERO).abs().compareTo(BigDecimal.valueOf(0.001)) < 0){
public static boolean isZero(Double value, Double ratedCurrent) {
if (value == null) {
return true;
}
return false;
double threshold = 0.01 * ratedCurrent;
BigDecimal bd = BigDecimal.valueOf(value);
return bd.subtract(BigDecimal.ZERO).abs().compareTo(BigDecimal.valueOf(threshold)) < 0;
}
/**
* 获取CP95值
* 获取CP95分位数
* <p>
* CP95表示95%分位数即有95%的数据小于等于此值。
* 算法逻辑:
* <ul>
* <li>数据量=1时返回该数据</li>
* <li>数据量<21时返回第2个数据索引1</li>
* <li>数据量≥21时计算5%位置的数据(适用于从大到小排序的数据)</li>
* </ul>
*
* @param t
* @return
* @param t 已排序的数据列表(从大到小排序)
* @return CP95分位数值列表包含单个元素
*/
public static List<Double> getCP95Doubles(List<Double> t) {
if (CollUtil.isNotEmpty(t)) {
if (t.size() < 21) {
if (t.size() == 1) {
return t;
}
if (t.size() > 1) {
return t.subList(1, 2);
}
} else {
int v = (int) (t.size() * 0.5);
return t.subList(v, v + 1);
}
if (CollUtil.isEmpty(t)) {
return new ArrayList<>();
}
return t;
// 单个数据直接返回
if (t.size() == 1) {
return new ArrayList<>(t);
}
// 数据量较少时取第2个数据作为CP95值
if (t.size() < CP95_DATA_SIZE_THRESHOLD) {
return t.subList(CP95_SMALL_DATA_INDEX, CP95_SMALL_DATA_INDEX + 1);
}
// 数据量充足时计算真正的95%分位数
// 由于数据已从大到小排序95%分位数位于5%位置
int cp95Index = (int) Math.ceil(t.size() * CP95_PERCENTILE) - 1;
return t.subList(cp95Index, cp95Index + 1);
}
/**
* 获取CP95值所在索引
* 获取CP95分位数值在列表中的索引位置
* <p>
* 计算CP95分位数在已排序列表中的索引位置。
* 索引计算规则与getCP95Doubles方法保持一致。
*
* @param t
* @return
* @param t 已排序的数据列表(从大到小排序)
* @return CP95分位数的索引位置列表为空时返回-1
*/
public static int getCP95Idx(List<Double> t) {
if (CollUtil.isNotEmpty(t)) {
if (t.size() < 21) {
if (t.size() == 1) {
return 0;
}
if (t.size() > 1) {
return 1;
}
} else {
int v = (int) (t.size() * 0.5);
return v;
}
if (CollUtil.isEmpty(t)) {
return -1;
}
return -1;
// 单个数据返回索引0
if (t.size() == 1) {
return 0;
}
// 数据量较少时返回索引1
if (t.size() < CP95_DATA_SIZE_THRESHOLD) {
return CP95_SMALL_DATA_INDEX;
}
// 数据量充足时计算95%分位数的索引位置
// 由于数据已从大到小排序95%分位数索引为5%位置
return (int) Math.ceil(t.size() * CP95_PERCENTILE) - 1;
}
/**
* 获取部分值
* 获取部分值(去除最大最小值后的数据)
* <p>
* 用于数据预处理,去除可能的异常值。
* 算法逻辑:
* <ul>
* <li>数据量≤2时返回原数据副本</li>
* <li>数据量>2时移除一个最大值和一个最小值后返回剩余数据</li>
* </ul>
* 注意:该方法不会修改原始列表,而是返回新的列表。
*
* @param t
* @return
* @param t 原始数据列表
* @return 去除最大最小值后的数据列表副本
*/
public static List<Double> getSectionValueDoubles(List<Double> t) {
if (CollUtil.isNotEmpty(t)) {
if (t.size() > 2) {
Double max = Collections.max(t);
Double min = Collections.min(t);
t.remove(max);
t.remove(min);
}
if (CollUtil.isEmpty(t) || t.size() <= MIN_DATA_SIZE_FOR_SECTION_VALUE) {
return new ArrayList<>(t);
}
return t;
// 创建副本避免修改原始列表
List<Double> result = new ArrayList<>(t);
Double max = Collections.max(result);
Double min = Collections.min(result);
result.remove(max);
result.remove(min);
return result;
}
/**
* 获取平均值
* 计算数据列表的算术平均值
* <p>
* 对输入的数值列表计算算术平均值,并以单元素列表形式返回。
* 空列表会返回空列表。
*
* @param t
* @return
* @param t 数值列表
* @return 包含平均值的单元素列表,输入为空时返回空列表
*/
public static List<Double> getAvgDoubles(List<Double> t) {
if (CollUtil.isNotEmpty(t)) {
t = Arrays.asList(t.stream().mapToDouble(Double::doubleValue).average().orElse(0.0));
if (CollUtil.isEmpty(t)) {
return new ArrayList<>();
}
return t;
// 计算列表中所有数值的算术平均值
double average = t.stream().mapToDouble(Double::doubleValue).average().orElse(0.0);
// 将平均值包装为单元素列表返回
return Collections.singletonList(average);
}
/**
* 对list进行从大到小排序并返回排序后的索引序列
* 对数据列表进行排序并返回原始索引序列
* <p>
* 使用选择排序算法对列表进行排序,同时跟踪每个元素的原始索引位置。
* 这样可以在数据排序后仍然知道每个数据在原始列表中的位置。
*
* @param list
* @return
* <b>注意:</b>该方法会直接修改输入的列表。
*
* @param list 待排序的数据列表(会被直接修改)
* @param isAsc 排序方式true为升序false为降序
* @return 排序后各元素在原始列表中的索引位置
*/
public static List<Integer> sort(List<Double> list) {
public static List<Integer> sort(List<Double> list, Boolean isAsc) {
if (CollUtil.isEmpty(list)) {
return new ArrayList<>();
}
if (isAsc == null) {
throw new IllegalArgumentException("排序方式参数不能为null");
}
// 创建索引列表,记录每个元素的原始位置
List<Integer> indexList = Stream.iterate(0, i -> i + 1).limit(list.size()).collect(Collectors.toList());
// 使用选择排序算法,同时维护索引映射
for (int i = 0; i < list.size(); i++) {
int maxIdx = i;
// 当前轮次要放置的目标位置
int targetIdx = i;
// 在未排序部分寻找最值
for (int j = i + 1; j < list.size(); j++) {
if (list.get(j) > list.get(maxIdx)) {
maxIdx = j;
if (isAsc) {
// 升序:寻找最小值
if (list.get(j) < list.get(targetIdx)) {
targetIdx = j;
}
} else {
// 降序:寻找最大值
if (list.get(j) > list.get(targetIdx)) {
targetIdx = j;
}
}
}
if (maxIdx != i) {
// 交换数据值和对应的索引
if (targetIdx != i) {
// 交换数据值
double temp = list.get(i);
list.set(i, list.get(maxIdx));
list.set(maxIdx, temp);
list.set(i, list.get(targetIdx));
list.set(targetIdx, temp);
// 交换对应的原始索引
int tempIdx = indexList.get(i);
indexList.set(i, indexList.get(maxIdx));
indexList.set(maxIdx, tempIdx);
indexList.set(i, indexList.get(targetIdx));
indexList.set(targetIdx, tempIdx);
}
}
return indexList;
}
/**
* 根据idxList索引列表从oldList中获取新数组
*
* @param oldList
* @param idxList
* @return
*/
public static List<Double> getNewArray(List<Double> oldList, List<Integer> idxList) {
if (CollUtil.isNotEmpty(oldList) && CollUtil.isNotEmpty(idxList)) {
if (CollUtil.max(idxList) > oldList.size() - 1 || CollUtil.min(idxList) < 0) {
return null;
}
List<Double> newList = new ArrayList<>();
for (int i = 0; i < idxList.size(); i++) {
newList.add(oldList.get(idxList.get(i)));
}
return newList;
}
return null;
}
/**
* 检查文件是否存在
*/
public static void checkFileExists(String filePath, String description) {
File file = new File(filePath);
if (!file.exists()) {
System.err.println("警告: " + description + " 不存在: " + filePath);
System.err.println("请确保文件路径正确,或修改测试中的文件路径");
} else {
System.out.println(description + " 存在: " + filePath);
System.out.println(" 文件大小: " + file.length() + " bytes");
}
}
}

View File

@@ -28,56 +28,52 @@ public class DetectionCommunicateUtil {
Channel channelSource = SocketManager.getChannelByUserId(param.getUserPageId() + DetectionCommunicateConstant.SOURCE);
Channel channelDev = SocketManager.getChannelByUserId(param.getUserPageId() + DetectionCommunicateConstant.DEV);
if (Objects.nonNull(channelSource) && channelSource.isActive()) {
System.out.println("发送关闭源指令。。。。。。。。");
CnSocketUtil.quitSendSource(param);
}
if (Objects.nonNull(channelDev) && channelDev.isActive()) {
System.out.println("发送关闭设备通讯指令。。。。。。。。");
CnSocketUtil.quitSend(param);
boolean channelSourceActive = channelSource != null && channelSource.isActive();
boolean channelDevActive = channelDev != null && channelDev.isActive();
if(channelSourceActive || channelDevActive){
if(channelSourceActive){
System.out.println("发送关闭源指令。。。。。。。。");
CnSocketUtil.quitSendSource(param);
}
if(channelDevActive){
System.out.println("发送关闭设备通讯指令。。。。。。。。");
CnSocketUtil.quitSend(param);
}
// 休眠4秒
try {
Thread.sleep(4000);
} catch (InterruptedException e) {
log.error(e.getMessage());
}
}
try {
Thread.sleep(4000);
} catch (InterruptedException e) {
log.error(e.getMessage());
}
SocketManager.removeUser(param.getUserPageId() + DetectionCommunicateConstant.SOURCE);
SocketManager.removeUser(param.getUserPageId() + DetectionCommunicateConstant.DEV);
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
log.error(e.getMessage());
}
// SocketManager.removeUser(param.getUserPageId() + DetectionCommunicateConstant.SOURCE);
// SocketManager.removeUser(param.getUserPageId() + DetectionCommunicateConstant.DEV);
}
/**
* 比对式-检测是否存在已有的Socket通道有则强行关闭
*
* @param userId
* @param loginName
*/
public static void checkContrastCommunicateChannel(String userId) {
Channel channel = SocketManager.getChannelByUserId(userId);
if (Objects.nonNull(channel) && channel.isActive()) {
System.out.println("存在已有的Socket通道强行关闭。。。。。。。。");
CnSocketUtil.contrastSendquit(userId);
}
try {
Thread.sleep(4000);
} catch (InterruptedException e) {
log.error(e.getMessage());
}
SocketManager.removeUser(userId);
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
log.error(e.getMessage());
}
}
// public static void checkContrastCommunicateChannel(String loginName) {
// Channel channel = SocketManager.getChannelByUserId(loginName + CnSocketUtil.CONTRAST_DEV_TAG);
//
// if (Objects.nonNull(channel) && channel.isActive()) {
// System.out.println("存在已有的Socket通道强行关闭。。。。。。。。");
// CnSocketUtil.contrastSendquit(loginName);
// SocketManager.removeUser(loginName + CnSocketUtil.CONTRAST_DEV_TAG);
// try {
// Thread.sleep(4000);
// } catch (InterruptedException e) {
// log.error(e.getMessage());
// }
// }
// }
}

View File

@@ -5,7 +5,7 @@ import com.alibaba.fastjson.JSONObject;
import com.njcn.gather.detection.pojo.enums.SourceOperateCodeEnum;
import com.njcn.gather.detection.pojo.param.PreDetectionParam;
import com.njcn.gather.detection.pojo.vo.SocketMsg;
import com.njcn.gather.detection.pojo.vo.WebSocketVO;
import com.njcn.gather.detection.util.socket.websocket.WebServiceManager;
/**
* @Author: cdf
@@ -16,6 +16,8 @@ public class CnSocketUtil {
public final static String DEV_TAG = "_Dev";
public final static String CONTRAST_DEV_TAG = "_Contrast_Dev";
public final static String SOURCE_TAG = "_Source";
public final static String START_TAG = "_Start";
@@ -34,7 +36,7 @@ public class CnSocketUtil {
socketMsg.setRequestId(SourceOperateCodeEnum.QUITE.getValue());
socketMsg.setOperateCode(SourceOperateCodeEnum.QUIT_INIT_03.getValue());
SocketManager.sendMsg(param.getUserPageId() + DEV_TAG, JSON.toJSONString(socketMsg));
WebServiceManager.removePreDetectionParam();
WebServiceManager.removePreDetectionParam(param.getUserPageId());
}
/**
@@ -48,43 +50,21 @@ public class CnSocketUtil {
jsonObject.put("sourceId", param.getSourceId());
socketMsg.setData(jsonObject.toJSONString());
SocketManager.sendMsg(param.getUserPageId() + SOURCE_TAG, JSON.toJSONString(socketMsg));
WebServiceManager.removePreDetectionParam();
}
/**
* 推送webSocket数据
*/
public static void sendToWebSocket(String userId, String requestId, String operatorType, Object data, String desc) {
WebSocketVO<Object> webSocketVO = new WebSocketVO<>();
webSocketVO.setRequestId(requestId);
webSocketVO.setOperateCode(operatorType);
webSocketVO.setData(data);
webSocketVO.setDesc(desc);
WebServiceManager.sendMessage(userId, webSocketVO);
}
/**
* 推送未知异常的webSocket数据
*/
public static void sendUnSocket(String userId) {
WebSocketVO<Object> webSocketVO = new WebSocketVO<>();
webSocketVO.setRequestId(SourceOperateCodeEnum.UNKNOWN_OPERATE.getValue());
webSocketVO.setData(SourceOperateCodeEnum.UNKNOWN_OPERATE.getMsg());
webSocketVO.setOperateCode(SourceOperateCodeEnum.UNKNOWN_OPERATE.getMsg());
WebServiceManager.sendMessage(userId, webSocketVO);
WebServiceManager.removePreDetectionParam(param.getUserPageId());
}
/**
* 比对式-退出检测
*/
public static void contrastSendquit(String userId) {
public static void contrastSendquit(String loginName, SourceOperateCodeEnum operateCode, boolean isRemoveSocket) {
System.out.println("比对式-发送关闭备通讯模块指令。。。。。。。。");
SocketMsg<String> socketMsg = new SocketMsg<>();
socketMsg.setRequestId(SourceOperateCodeEnum.QUITE.getValue());
socketMsg.setOperateCode(SourceOperateCodeEnum.QUIT_INIT_03.getValue());
SocketManager.sendMsg(userId + DEV_TAG, JSON.toJSONString(socketMsg));
WebServiceManager.removePreDetectionParam();
socketMsg.setOperateCode(operateCode.getValue());
SocketManager.sendMsg(loginName + CONTRAST_DEV_TAG, JSON.toJSONString(socketMsg));
// WebServiceManager.removePreDetectionParam();
FormalTestManager.isRemoveSocket = isRemoveSocket;
FormalTestManager.currentStep = SourceOperateCodeEnum.QUITE;
}
}

View File

@@ -1,10 +1,14 @@
package com.njcn.gather.detection.util.socket;
import com.google.common.collect.HashBiMap;
import com.njcn.gather.detection.pojo.dto.WaveResultDTO;
import com.njcn.gather.detection.pojo.enums.SourceOperateCodeEnum;
import com.njcn.gather.detection.pojo.po.DevData;
import com.njcn.gather.device.pojo.po.PqStandardDev;
import com.njcn.gather.detection.pojo.vo.DevLineTestResult;
import com.njcn.gather.device.pojo.enums.PatternEnum;
import com.njcn.gather.device.pojo.vo.PreDetection;
import com.njcn.gather.plan.pojo.po.AdPlan;
import com.njcn.gather.plan.pojo.po.AdPlanTestConfig;
import com.njcn.gather.system.dictionary.pojo.enums.DictDataEnum;
import java.util.ArrayList;
@@ -20,9 +24,11 @@ import java.util.concurrent.ConcurrentHashMap;
*/
public class FormalTestManager {
// 当前步骤
public static SourceOperateCodeEnum currentStep;
/**
* key:设备ip,value:当前设备下面的通道号
* key:设备ip,value:当前设备下面的监测点ID(ip_通道号)
*/
public static Map<String, List<String>> devMapMonitorNum = new ConcurrentHashMap<>();
@@ -42,7 +48,7 @@ public class FormalTestManager {
public static List<String> monitorIdListComm = new ArrayList<>();
/**
* 所有参与检测的监测点。key:监测点idip_通道号,value:检测点实体
* 所有参与检测的监测点。key:监测点ip_通道号,value:检测点实体
*/
public static Map<String, PreDetection.MonitorListDTO> monitorMap = new HashMap<>();
@@ -59,7 +65,7 @@ public class FormalTestManager {
/**
* key:设备ip,value:装置id
*/
public static Map<String, String> devIdMapComm = new HashMap<>();
public static HashBiMap<String, String> devIdMapComm = HashBiMap.create();
/**
* 停止触发标识
@@ -76,20 +82,22 @@ public class FormalTestManager {
*/
public static Integer stopTime = 0;
/**
* 强行赋值关系
*/
public static Map<String, String> harmonicRelationMap = new HashMap<>();
/**
* 当前正在检测的计划
*/
public static AdPlan currentTestPlan;
public static AdPlanTestConfig curretntTestPlanConfig;
/**
* 比对式检测-检测项。
* 当前正在检测的模式
*/
public static List<String> testItemCodeList = new ArrayList<>();
public static PatternEnum patternEnum;
/**
* 比对式检测-检测项。key为检测项code,value为检测项id
*/
public static Map<String, String> testItemMap = new HashMap<>();
/**
* 数据处理原则
@@ -97,9 +105,14 @@ public class FormalTestManager {
public static DictDataEnum dataRule;
/**
* 所有参与比对式检测的被检设备、标准设备配对关系。key:标准设备id_通道号,value:被检设备id_通道号
* 所有参与比对式检测的被检设备、标准设备配对关系。key:被检设备ip_通道号,value:标准设备ip_通道号
*/
public static Map<String, String> pairsMap = HashBiMap.create();
public static HashBiMap<String, String> pairsIpMap = HashBiMap.create();
/**
* 所有参与比对式检测的被检设备、标准设备配对关系。key:被检设备id_通道号,value:标准设备id_通道号
*/
public static HashBiMap<String, String> pairsIdMap = HashBiMap.create();
/**
* 被检设备的数据。key:设备ip_通道号,value:DevData数据集合
@@ -110,4 +123,35 @@ public class FormalTestManager {
* 标准设备的数据。key:设备ip_通道号,value:DevData数据集合
*/
public static Map<String, List<DevData>> standardDevDataMap = new HashMap<>();
/**
* 是否要移除和通信模块的socket连接
*/
public static Boolean isRemoveSocket;
/**
* 录波功能校验
*/
public static Boolean waveCheckFlag;
/**
* 第几次监测 key为设备监测点id,value为第几次监测
*/
public static Map<String, Integer> numMap = new HashMap<>();
/**
* 存放录波相关数据。key:设备ip_通道号,value:WaveResultDTO数据
*/
public static Map<String, WaveResultDTO> waveResultDTOMap = new HashMap<>();
/**
* 录波组数
*/
public static Integer waveNum;
/**
* 每次录波检测结果
*/
public static List<DevLineTestResult> preNumTestResultList = new ArrayList<>();
}

View File

@@ -65,4 +65,35 @@ public class MsgUtil {
}
return JSON.toJSONString(socketDataMsg);
}
/**
* 获取一组监测点配对的字符串
*
* @param devMonitorId
* @param standardDevId
* @param devMap key为设备ipvalue为设备名称
* @return
*/
public static String getPairStr(String devMonitorId, String standardDevId, Map<String, String> devMap) {
if (StrUtil.isBlank(devMonitorId) || StrUtil.isBlank(standardDevId)) {
return "";
} else {
String[] split1 = devMonitorId.split("_");
String[] split2 = standardDevId.split("_");
return "被检设备\"" + devMap.get(split1[0]) + CnSocketUtil.SPLIT_TAG + split1[1] + "\"" + " -> 标准设备\"" + devMap.get(split2[0]) + CnSocketUtil.SPLIT_TAG + split2[1] + "\"";
}
}
/**
* 获取消息
*
* @param monitorId 监测点id
* @param devMap key为设备ipvalue为设备名称
* @param appendMsg 附加的消息
* @return
*/
public static String getMsg(String monitorId, Map<String, String> devMap, String appendMsg) {
String[] split1 = monitorId.split("_");
return "\"" + devMap.get(split1[0]) + CnSocketUtil.SPLIT_TAG + "" + split1[1] + "\"" + appendMsg;
}
}

View File

@@ -1,24 +1,50 @@
package com.njcn.gather.detection.util.socket;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.njcn.gather.detection.pojo.param.ContrastDetectionParam;
import com.njcn.gather.detection.pojo.param.PreDetectionParam;
import com.njcn.gather.detection.util.socket.cilent.NettyClient;
import com.njcn.gather.detection.util.socket.cilent.NettyContrastClientHandler;
import com.njcn.gather.detection.util.socket.config.SocketConnectionConfig;
import com.njcn.gather.plan.pojo.enums.DataSourceEnum;
import com.njcn.gather.script.pojo.po.SourceIssue;
import io.netty.channel.Channel;
import io.netty.channel.nio.NioEventLoopGroup;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
/**
* Socket连接管理器
* 提供Socket连接的生命周期管理、消息发送、检测任务管理等功能
* <p>
* 包含以下主要功能:
* 1. 基础连接管理addUser, removeUser, sendMsg等
* 2. 智能消息发送smartSendToSource, smartSendToDevice等新增
* 3. 检测任务管理targetMap, sourceIssueList等管理
* 4. 连接状态监控getConnectionStatus等新增
*
* @Description: webSocket存储的通道
* @Author: wr
* @Author: wr, hongawen
* @Date: 2024/12/11 13:04
*/
@Slf4j
@Component
public class SocketManager {
@Resource
private SocketConnectionConfig socketConnectionConfig;
/**
* key为userIdxxx_Source、xxx_Devvalue为channel
*/
@@ -34,24 +60,25 @@ public class SocketManager {
}
public static void addGroup(String userId, NioEventLoopGroup group) {
socketGroup.put(userId, group);
socketGroup.put(userId, group);
}
public static void removeUser(String userId) {
Channel channel = socketSessions.get(userId);
if(ObjectUtil.isNotNull(channel)){
if (ObjectUtil.isNotNull(channel)) {
try {
channel.close().sync();
} catch (InterruptedException e) {
e.printStackTrace();
}
NioEventLoopGroup eventExecutors = socketGroup.get(userId);
if(ObjectUtil.isNotNull(channel)){
if (ObjectUtil.isNotNull(eventExecutors)) {
eventExecutors.shutdownGracefully();
System.out.println(userId+"__"+channel.id()+"关闭了客户端");
System.out.println(userId + "__" + channel.id() + "关闭了客户端");
}
}
socketSessions.remove(userId);
socketGroup.remove(userId);
}
public static Channel getChannelByUserId(String userId) {
@@ -62,15 +89,170 @@ public class SocketManager {
return socketGroup.get(userId);
}
public static void sendMsg(String userId,String msg) {
public static void sendMsg(String userId, String msg) {
Channel channel = socketSessions.get(userId);
if(ObjectUtil.isNotNull(channel)){
channel.writeAndFlush(msg+'\n');
System.out.println(userId+"__"+channel.id()+""+channel.remoteAddress()+"发送数据:"+msg);
}else{
System.out.println(userId+"__发送数据失败通道不存在"+msg);
if (ObjectUtil.isNotNull(channel)) {
channel.writeAndFlush(msg + '\n');
log.info("{}__{}往{}发送数据:{}", userId, channel.id(), channel.remoteAddress(), msg);
} else {
log.warn("{}__发送数据失败通道不存在{}", userId, msg);
}
}
// =================== 智能发送功能 ===================
/**
* 智能发送消息到程控源设备
* 自动从配置文件读取IP和PORT开发者无需关心网络配置
* 如果连接不存在且requestId需要建立连接会自动建立连接后发送
*
* @param param 检测参数包含用户页面ID等信息
* @param msg 要发送的消息内容JSON格式包含requestId字段
*/
public void smartSendToSource(PreDetectionParam param, String msg) {
String requestId = extractRequestId(msg);
String userId = param.getUserPageId() + CnSocketUtil.SOURCE_TAG;
// 检查是否需要建立连接
if (SocketConnectionConfig.needsSourceConnection(requestId)) {
String ip = socketConnectionConfig.getSource().getIp();
Integer port = socketConnectionConfig.getSource().getPort();
// 检查连接是否存在且活跃
if (!isChannelActive(userId)) {
log.info("程控源连接不存在,自动建立连接: userId={}, requestId={}", userId, requestId);
// 异步建立程控源连接并发送消息
CompletableFuture.runAsync(() -> {
NettyClient.connectToSourceStatic(ip, port, param, msg);
});
return;
}
}
// 连接已存在或不需要建立连接,直接发送消息
log.info("直接发送消息到程控源: userId={}, requestId={}", userId, requestId);
sendMsg(userId, msg);
}
/**
* 智能发送消息到被检设备
* 自动从配置文件读取IP和PORT开发者无需关心网络配置
* 如果连接不存在且requestId需要建立连接会自动建立连接后发送
*
* @param param 检测参数包含用户页面ID等信息
* @param msg 要发送的消息内容JSON格式包含requestId字段
*/
public void smartSendToDevice(PreDetectionParam param, String msg) {
String requestId = extractRequestId(msg);
String userId = param.getUserPageId() + CnSocketUtil.DEV_TAG;
// 检查是否需要建立连接
if (SocketConnectionConfig.needsDeviceConnection(requestId)) {
String ip = socketConnectionConfig.getDevice().getIp();
Integer port = socketConnectionConfig.getDevice().getPort();
// 检查连接是否存在且活跃
if (!isChannelActive(userId)) {
log.info("被检设备连接不存在,自动建立连接: userId={}, requestId={}", userId, requestId);
// 异步建立被检设备连接并发送消息
CompletableFuture.runAsync(() -> {
NettyClient.connectToDeviceStatic(ip, port, param, msg);
});
return;
}
}
// 连接已存在或不需要建立连接,直接发送消息
log.info("直接发送消息到被检设备: userId={}, requestId={}", userId, requestId);
sendMsg(userId, msg);
}
/**
* 比对智能发送消息到被检设备
* 自动从配置文件读取IP和PORT开发者无需关心网络配置
* 如果连接不存在且requestId需要建立连接会自动建立连接后发送
*
* @param param 检测参数包含用户页面ID等信息
* @param msg 要发送的消息内容JSON格式包含requestId字段
*/
public void smartSendToContrast(ContrastDetectionParam param, String msg) {
String requestId = extractRequestId(msg);
String userId = param.getLoginName() + CnSocketUtil.CONTRAST_DEV_TAG;
// 检查是否需要建立连接
if (SocketConnectionConfig.needsDeviceConnection(requestId)) {
String ip = socketConnectionConfig.getDevice().getIp();
Integer port = socketConnectionConfig.getDevice().getPort();
// 检查连接是否存在且活跃
if (!isChannelActive(userId)) {
log.info("比对被检设备连接不存在,自动建立连接: userId={}, requestId={}", userId, requestId);
// 异步建立比对被检设备连接并发送消息
CompletableFuture.runAsync(() -> {
NettyClient.connectToContrastDeviceStatic(ip, port, param, msg);
});
return;
} else {
PreDetectionParam preDetectionParam = new PreDetectionParam();
preDetectionParam.setUserPageId(param.getLoginName());
preDetectionParam.setTestItemList(param.getTestItemList());
preDetectionParam.setDevIds(param.getDevIds());
preDetectionParam.setUserId(param.getUserId());
NettyContrastClientHandler.param = preDetectionParam;
}
}
// 连接已存在或不需要建立连接,直接发送消息
log.info("直接发送消息到比对被检设备: userId={}, requestId={}", userId, requestId);
sendMsg(userId, msg);
}
// =================== 私有工具方法 ===================
/**
* 从消息中提取requestId
* 支持JSON格式的消息解析
*
* @param msg 消息内容
* @return String requestId如果解析失败返回"unknown"
*/
private static String extractRequestId(String msg) {
try {
if (StrUtil.isNotBlank(msg)) {
// 尝试解析JSON格式消息
JSONObject jsonObject = JSON.parseObject(msg);
String requestId = jsonObject.getString("requestId");
if (StrUtil.isNotBlank(requestId)) {
return requestId;
}
// 如果没有requestId字段尝试解析request_id字段
requestId = jsonObject.getString("request_id");
if (StrUtil.isNotBlank(requestId)) {
return requestId;
}
// 如果没有JSON字段尝试从普通字符串中匹配
if (msg.contains("requestId=")) {
String[] parts = msg.split("requestId=");
if (parts.length > 1) {
String idPart = parts[1].split("[,\\s&]")[0];
return idPart.trim();
}
}
}
} catch (Exception e) {
log.warn("解析消息中的requestId失败: msg={}, error={}", msg, e.getMessage());
}
return "unknown";
}
/**
* 检查指定用户的Channel是否活跃
*
* @param userId 用户ID
* @return boolean true:连接活跃, false:连接不存在或不活跃
*/
public static boolean isChannelActive(String userId) {
Channel channel = getChannelByUserId(userId);
return ObjectUtil.isNotNull(channel) && channel.isActive();
}
@@ -93,7 +275,7 @@ public class SocketManager {
/**
* 用于存储每个测试小项超时时长key key:检测项 value:时间秒
*/
public static volatile Map<Integer,Long> clockMap = new ConcurrentHashMap<>();
public static volatile Map<Integer, Long> clockMap = new ConcurrentHashMap<>();
/**
* 用于存储比对式测试时间。
@@ -101,7 +283,6 @@ public class SocketManager {
public static volatile Map<DataSourceEnum, Long> contrastClockMap = new ConcurrentHashMap<>();
public static void addSourceList(List<SourceIssue> sList) {
sourceIssueList = sList;
}
@@ -124,8 +305,8 @@ public class SocketManager {
targetMap = map;
}
public static void addTargetMap(String scriptType,Long count) {
targetMap.put(scriptType,count);
public static void addTargetMap(String scriptType, Long count) {
targetMap.put(scriptType, count);
}
public static Long getSourceTarget(String scriptType) {
@@ -133,10 +314,5 @@ public class SocketManager {
}
}

View File

@@ -1,63 +0,0 @@
package com.njcn.gather.detection.util.socket;
import com.njcn.gather.detection.pojo.enums.DetectionCodeEnum;
import java.util.Arrays;
/**
* @author wr
* @description
* @date 2025/3/27 14:58
*/
public class UnitUtil {
// public static String unit(String code, Integer fly) {
// String unit = "";
// if (Arrays.asList(0, 1).contains(fly)) {
// if (DetectionCodeEnum.FREQ.getCode().equals(code)) {
// unit = "Hz";
// }
// if (DetectionCodeEnum.VRMS.getCode().equals(code)) {
// unit = "V";
// }
// if (DetectionCodeEnum.IRMS.getCode().equals(code)) {
// unit = "A";
// }
// if (DetectionCodeEnum.V2_50.getCode().equals(code) ||
// DetectionCodeEnum.SV_1_49.getCode().equals(code)||
// DetectionCodeEnum.V_UNBAN.getCode().equals(code) ||
// DetectionCodeEnum.I_UNBAN.getCode().equals(code)
// ) {
// unit = "%";
// }
// if (DetectionCodeEnum.I2_50.getCode().equals(code) ||
// DetectionCodeEnum.SI_1_49.getCode().equals(code)
// ) {
// unit = "A";
// }
// if (DetectionCodeEnum.P2_50.getCode().equals(code)) {
// unit = "W";
// }
// if (DetectionCodeEnum.P.getCode().equals(code)) {
// unit = "P";
// }
// if (DetectionCodeEnum.MAG.getCode().equals(code)) {
// unit = "V";
// }
// if (DetectionCodeEnum.DUR.getCode().equals(code)) {
// unit = "s";
// }
// if (DetectionCodeEnum.VA.getCode().equals(code) ||
// DetectionCodeEnum.IA.getCode().equals(code)
// ) {
// unit = "°";
// }
// if (DetectionCodeEnum.DELTA_V.getCode().equals(code)
// ) {
// unit = "%";
// }
// }else{
// unit = "%";
// }
// return unit;
// }
}

View File

@@ -1,86 +0,0 @@
package com.njcn.gather.detection.util.socket;
import com.alibaba.fastjson.JSON;
import com.njcn.gather.detection.pojo.param.PreDetectionParam;
import com.njcn.gather.detection.pojo.vo.WebSocketVO;
import io.netty.channel.Channel;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import lombok.extern.slf4j.Slf4j;
import org.apache.poi.ss.formula.functions.T;
import java.time.LocalDateTime;
import java.util.Iterator;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
/**
* @Description: webSocket存储的通道
* @Author: wr
* @Date: 2024/12/11 13:04
*/
@Slf4j
public class WebServiceManager {
//key:页面 value:channel
private static final Map<String, Channel> userSessions = new ConcurrentHashMap<>();
// 检测参数。key固定为preDetectionParam, value:检测参数
private static final Map<String, PreDetectionParam> preDetectionParamMap = new ConcurrentHashMap<>();
public static void addUser(String userId, Channel channel) {
userSessions.put(userId, channel);
}
public static void removeChannel(String channelId) {
// 遍历并删除
Iterator<Map.Entry<String, Channel>> iterator = userSessions.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry<String, Channel> entry = iterator.next();
if (entry.getValue().id().toString().equals(channelId)) {
iterator.remove();
}
}
}
public static Channel getChannelByUserId(String userId) {
return userSessions.get(userId);
}
public static void sendMsg(String userId,String msg) {
Channel channel = userSessions.get(userId);
if(Objects.nonNull(channel) && channel.isActive()){
TextWebSocketFrame wd = new TextWebSocketFrame(msg);
channel.writeAndFlush(wd);
}else {
log.error("{}-websocket推送消息失败;当前用户-{}-客户端已经断开连接", LocalDateTime.now(),userId);
// PreDetectionParam param = preDetectionParamMap.get("preDetectionParam");
// CnSocketUtil.quitSend(param);
// CnSocketUtil.quitSendSource(param);
}
}
public static void sendMessage(String userId, WebSocketVO<Object> webSocketVO) {
Channel channel = userSessions.get(userId);
if(Objects.nonNull(channel) && channel.isActive()){
TextWebSocketFrame wd = new TextWebSocketFrame(JSON.toJSONString(webSocketVO));
channel.writeAndFlush(wd);
}else {
log.error("{}-websocket推送消息失败;当前用户-{}-客户端已经断开连接", LocalDateTime.now(),userId);
}
}
public static void addPreDetectionParam(PreDetectionParam preDetectionParam) {
preDetectionParamMap.put("preDetectionParam", preDetectionParam);
}
public static PreDetectionParam getPreDetectionParam() {
return preDetectionParamMap.get("preDetectionParam");
}
public static void removePreDetectionParam() {
preDetectionParamMap.clear();
}
}

View File

@@ -6,56 +6,152 @@ import com.njcn.gather.detection.pojo.param.PreDetectionParam;
import com.njcn.gather.detection.pojo.vo.SocketDataMsg;
import com.njcn.gather.detection.pojo.vo.SocketMsg;
import com.njcn.gather.detection.util.socket.CnSocketUtil;
import com.njcn.gather.detection.util.socket.FormalTestManager;
import com.njcn.gather.detection.util.socket.MsgUtil;
import com.njcn.gather.detection.util.socket.SocketManager;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import lombok.extern.slf4j.Slf4j;
import java.time.LocalDateTime;
import java.util.Objects;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.*;
/**
* @Author: cdf
* @CreateTime: 2025-02-11
* @Description: 心跳处理类
* Netty心跳处理器
* <p>
* 负责维护Socket长连接的心跳检测机制通过定期发送心跳包来检测连接状态
* 当连续多次未收到心跳响应时自动断开连接并清理相关资源。
* </p>
*
* <h3>核心功能:</h3>
* <ul>
* <li>定时发送心跳包 (默认10秒间隔3秒后开始)</li>
* <li>监听心跳响应,重置失败计数器</li>
* <li>连续失败超过阈值时触发断开逻辑 (默认3次)</li>
* <li>异步处理断开操作,避免阻塞心跳线程</li>
* <li>优雅关闭资源,防止内存泄漏</li>
* </ul>
*
* <h3>心跳机制流程:</h3>
* <pre>
* 连接建立 → 启动心跳定时任务(3秒后开始每10秒执行)
* ↓
* 发送心跳包 → 等待响应 → 收到响应(重置计数器) / 未收到响应(递增计数器)
* ↓
* 连续3次失败 → 异步执行断开逻辑 → 发送退出指令 → 延迟清理连接
* ↓
* 连接断开 → 优雅关闭定时任务和线程池
* </pre>
*
* <h3>线程安全设计:</h3>
* <p>
* 使用单线程的ScheduledExecutorService处理心跳发送避免并发问题。
* 超时处理使用CompletableFuture异步执行不阻塞心跳发送线程。
* Future引用使用volatile修饰确保多线程环境下的可见性。
* </p>
*
* <h3>设备类型支持:</h3>
* <ul>
* <li>程控源设备 (CnSocketUtil.SOURCE_TAG): "_Source"</li>
* <li>被检设备 (CnSocketUtil.DEV_TAG): "_Dev"</li>
* </ul>
*
* <h3>使用示例:</h3>
* <pre>{@code
* // 创建心跳处理器
* HeartbeatHandler handler = new HeartbeatHandler(param, CnSocketUtil.SOURCE_TAG);
*
* // 添加到Netty管道中
* pipeline.addLast(handler);
* }</pre>
*
* @author cdf
* @version 1.2
* @since 2025-02-11
*/
@Slf4j
public class HeartbeatHandler extends SimpleChannelInboundHandler<String> {
/**
* 心跳定时任务执行器,使用单线程池避免并发问题
*/
private final ScheduledExecutorService heartbeatExecutor = Executors.newScheduledThreadPool(1);
/**
* 检测参数包含用户页面ID等信息
*/
private final PreDetectionParam param;
/**
* 处理器类型标识("_Source" 或 "_Dev"
*/
private final String handlerType;
// 允许连续未收到心跳响应的最大次数
/**
* 保存定时任务的Future引用便于取消和管理
*/
private ScheduledFuture<?> heartbeatFuture;
/**
* 允许连续未收到心跳响应的最大次数
*/
private static final int MAX_HEARTBEAT_MISSES = 3;
// 连续未收到心跳响应的次数
/**
* 连续未收到心跳响应的次数
*/
private int consecutiveHeartbeatMisses = 0;
/**
* 构造函数
*
* @param param 检测参数包含用户页面ID等信息
* @param type 处理器类型CnSocketUtil.SOURCE_TAG 或 CnSocketUtil.DEV_TAG
*/
public HeartbeatHandler(PreDetectionParam param, String type) {
this.param = param;
this.handlerType = type;
}
/**
* 通道激活时的回调方法
* 在Socket连接建立成功后被调用启动心跳机制
*
* @param ctx Netty的通道上下文对象
*/
@Override
public void channelActive(ChannelHandlerContext ctx) {
log.info("心跳处理器启动 - 设备类型: {}", handlerType);
// 启动心跳定时任务
scheduleHeartbeat(ctx);
// 传播事件给管道中的后续处理器
ctx.fireChannelActive();
}
/**
* 通道断开时的回调方法
* 在Socket连接断开时被调用负责清理相关资源
*
* @param ctx Netty的通道上下文对象
* @throws Exception 异常情况
*/
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
heartbeatExecutor.shutdown();
log.info("心跳处理器开始清理资源 - 设备类型: {}", handlerType);
shutdownExecutorGracefully();
super.channelInactive(ctx);
}
// 每10秒发送一次心跳
/**
* 启动心跳定时任务
* 每10秒发送一次心跳包3秒后开始执行
*
* @param ctx Netty的通道上下文对象
*/
private void scheduleHeartbeat(ChannelHandlerContext ctx) {
heartbeatExecutor.scheduleAtFixedRate(() -> {
heartbeatFuture = heartbeatExecutor.scheduleAtFixedRate(() -> {
if (ctx.channel().isActive()) {
// 发送心跳包
SocketMsg<String> msg = new SocketMsg<>();
@@ -64,48 +160,154 @@ public class HeartbeatHandler extends SimpleChannelInboundHandler<String> {
msg.setData("");
ctx.channel().writeAndFlush(JSON.toJSONString(msg) + "\n");
System.out.println(handlerType + "♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥send" + LocalDateTime.now());
log.debug("心跳发送 - 设备类型: {}, 时间: {}", handlerType, LocalDateTime.now());
consecutiveHeartbeatMisses++;
if (consecutiveHeartbeatMisses >= MAX_HEARTBEAT_MISSES) {
// 连续三次未收到心跳响应,断开连接
System.out.println(handlerType + "连续三次未收到心跳响应,断开连接");
if (CnSocketUtil.DEV_TAG.equals(handlerType)) {
//CnSocketUtil.sendToWebSocket(param.getUserPageId(),);
CnSocketUtil.quitSend(param);
} else {
CnSocketUtil.quitSendSource(param);
}
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
System.err.println("线程中断异常: " + e.getMessage());
}
String key = CnSocketUtil.DEV_TAG.equals(handlerType) ? param.getUserPageId() + CnSocketUtil.DEV_TAG : param.getUserPageId() + CnSocketUtil.SOURCE_TAG;
SocketManager.removeUser(key);
// 连续三次未收到心跳响应,异步处理断开逻辑,避免阻塞心跳线程
log.warn("心跳响应超时 - 设备类型: {}, 连续失败次数: {}/{}, 执行断开连接",
handlerType, consecutiveHeartbeatMisses, MAX_HEARTBEAT_MISSES);
handleHeartbeatTimeoutAsync();
consecutiveHeartbeatMisses = 0; // 重置连续心跳丢失次数
}
}
}, 3, 10, TimeUnit.SECONDS);
}
/**
* 异步处理心跳超时断开逻辑
* <p>
* 使用CompletableFuture避免阻塞心跳发送线程确保心跳机制不受影响。
* 处理流程:
* 1. 异步发送退出指令
* 2. 延迟3秒后清理Socket连接
* 3. 记录处理过程和异常
* </p>
*/
private void handleHeartbeatTimeoutAsync() {
CompletableFuture.runAsync(() -> {
try {
log.info("开始执行心跳超时断开处理 - 设备类型: {}", handlerType);
// 根据设备类型发送对应的退出指令
if (CnSocketUtil.DEV_TAG.equals(handlerType)) {
CnSocketUtil.quitSend(param);
} else if (CnSocketUtil.SOURCE_TAG.equals(handlerType)) {
CnSocketUtil.quitSendSource(param);
} else {
if (FormalTestManager.currentStep == SourceOperateCodeEnum.RECORD_WAVE_STEP1 || FormalTestManager.currentStep == SourceOperateCodeEnum.RECORD_WAVE_STEP2) {
CnSocketUtil.contrastSendquit(param.getUserPageId(), SourceOperateCodeEnum.QUIT_INIT_03, true);
} else {
CnSocketUtil.contrastSendquit(param.getUserPageId(), SourceOperateCodeEnum.QUIT_INIT_02, true);
}
}
log.debug("退出指令已发送等待3秒后清理连接 - 设备类型: {}", handlerType);
} catch (Exception e) {
log.error("心跳超时处理发送退出指令异常 - 设备类型: {}", handlerType, e);
}
}).thenRunAsync(() -> {
try {
// 延迟3秒后清理连接给退出指令留出处理时间
Thread.sleep(3000);
// 构建连接Key并从SocketManager中移除
String key = CnSocketUtil.DEV_TAG.equals(handlerType) ?
param.getUserPageId() + CnSocketUtil.DEV_TAG :
param.getUserPageId() + CnSocketUtil.SOURCE_TAG;
SocketManager.removeUser(key);
log.info("心跳超时断开处理完成 - 设备类型: {}, 连接已清理", handlerType);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
log.error("心跳超时处理等待过程中被中断 - 设备类型: {}", handlerType);
} catch (Exception e) {
log.error("心跳超时处理清理连接异常 - 设备类型: {}", handlerType, e);
}
});
}
/**
* 优雅关闭线程池执行器
* <p>
* 确保资源的完全清理,避免内存泄漏:
* 1. 取消当前的心跳定时任务
* 2. 关闭线程池并等待正在执行的任务完成
* 3. 如果等待超时则强制关闭
* 4. 处理中断异常并恢复中断状态
* </p>
*/
private void shutdownExecutorGracefully() {
try {
// 1. 取消心跳定时任务
if (heartbeatFuture != null && !heartbeatFuture.isCancelled()) {
boolean cancelled = heartbeatFuture.cancel(false);
log.debug("心跳定时任务取消结果: {} - 设备类型: {}", cancelled, handlerType);
}
// 2. 关闭线程池,不再接收新任务
heartbeatExecutor.shutdown();
// 3. 等待已提交的任务完成最多等待5秒
if (!heartbeatExecutor.awaitTermination(5, TimeUnit.SECONDS)) {
log.warn("心跳线程池未能在5秒内正常关闭执行强制关闭 - 设备类型: {}", handlerType);
heartbeatExecutor.shutdownNow();
// 再次等待强制关闭完成最多等待2秒
if (!heartbeatExecutor.awaitTermination(2, TimeUnit.SECONDS)) {
log.error("心跳线程池强制关闭失败 - 设备类型: {}", handlerType);
}
} else {
log.debug("心跳线程池已优雅关闭 - 设备类型: {}", handlerType);
}
} catch (InterruptedException e) {
// 如果等待过程中被中断,立即强制关闭
log.warn("心跳线程池关闭过程中被中断,执行强制关闭 - 设备类型: {}", handlerType);
heartbeatExecutor.shutdownNow();
// 恢复中断状态遵循Java并发编程最佳实践
Thread.currentThread().interrupt();
}
}
/**
* 消息接收处理方法
* <p>
* 负责处理从服务端接收到的消息:
* 1. 过滤心跳响应包,重置失败计数器
* 2. 业务消息传递给后续处理器
* </p>
*
* @param ctx Netty的通道上下文对象
* @param msg 接收到的消息内容
* @throws Exception 异常情况
*/
@Override
protected void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception {
// 过滤心跳包,避免进入业务逻辑
if (isHeartbeatPacket(msg)) {
System.out.println(handlerType + "♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥response" + LocalDateTime.now());
log.debug("心跳响应 - 设备类型: {}, 时间: {}", handlerType, LocalDateTime.now());
// 重置连续失败计数器,表示连接正常
consecutiveHeartbeatMisses = 0;
return;
}
// 处理业务数据
// 业务消息传递给管道中的后续处理器
ctx.fireChannelRead(msg);
}
/**
* 判断是否为心跳数据包
* <p>
* 通过解析消息的operateCode字段来判断是否为心跳响应。
* 心跳包的操作码为SourceOperateCodeEnum.HEARTBEAT。
* </p>
*
* @param msg 需要判断的消息内容
* @return true:心跳包, false:业务消息
*/
private boolean isHeartbeatPacket(String msg) {
// 判断是否为心跳包
SocketDataMsg socketDataMsg = MsgUtil.socketDataMsg(msg);
return !Objects.isNull(socketDataMsg.getOperateCode()) && socketDataMsg.getOperateCode().equals(SourceOperateCodeEnum.HEARTBEAT.getValue());
try {
// 解析消息为SocketDataMsg对象
SocketDataMsg socketDataMsg = MsgUtil.socketDataMsg(msg);
// 检查操作码是否为心跳类型
return socketDataMsg != null &&
socketDataMsg.getOperateCode() != null &&
socketDataMsg.getOperateCode().equals(SourceOperateCodeEnum.HEARTBEAT.getValue());
} catch (Exception e) {
// 消息解析失败,可能不是标准格式的心跳包
log.debug("消息解析失败,可能不是心跳包: {}", msg, e);
return false;
}
}
}

View File

@@ -1,14 +1,17 @@
package com.njcn.gather.detection.util.socket.cilent;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson.JSON;
import com.njcn.gather.detection.handler.SocketContrastResponseService;
import com.njcn.gather.detection.handler.SocketDevResponseService;
import com.njcn.gather.detection.handler.SocketSourceResponseService;
import com.njcn.gather.detection.pojo.enums.SourceResponseCodeEnum;
import com.njcn.gather.detection.pojo.param.ContrastDetectionParam;
import com.njcn.gather.detection.pojo.param.PreDetectionParam;
import com.njcn.gather.detection.pojo.vo.SocketDataMsg;
import com.njcn.gather.detection.util.socket.CnSocketUtil;
import com.njcn.gather.detection.util.socket.SocketManager;
import com.njcn.gather.detection.util.socket.WebServiceManager;
import com.njcn.gather.detection.util.socket.websocket.WebServiceManager;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
@@ -18,140 +21,486 @@ import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;
import io.netty.handler.timeout.IdleStateHandler;
import io.netty.util.CharsetUtil;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.util.concurrent.TimeUnit;
/**
* Netty客户端工具类
* 用于建立与检测设备和程控源设备的Socket通信连接
* 支持心跳检测、断线重连和异常处理
*
* @Description: 心跳检测服务端 对应的服务端在netty-server 包下的NettyClient
* @Author: wr
* @Date: 2024/12/10 14:16
*/
@Getter
@Slf4j
@Component
public class NettyClient {
public static void socketClient(String ip, Integer port, PreDetectionParam param, String msg, SimpleChannelInboundHandler<String> handler) {
NioEventLoopGroup group = new NioEventLoopGroup();
Bootstrap bootstrap = new Bootstrap();
try {
bootstrap.group(group)
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000)
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) {
if (handler instanceof NettySourceClientHandler) {
ch.pipeline()
//空闲状态的handler
// 添加LineBasedFrameDecoder来按行分割数据
.addLast(new LineBasedFrameDecoder(10240))
// .addLast(new IdleStateHandler(0, 10, 0, TimeUnit.SECONDS))
.addLast(new StringDecoder(CharsetUtil.UTF_8))
.addLast(new StringEncoder(CharsetUtil.UTF_8))
.addLast(new HeartbeatHandler(param, CnSocketUtil.SOURCE_TAG))
.addLast(handler);
} else {
ch.pipeline()
@Resource
private SocketSourceResponseService socketSourceResponseService;
// 添加LineBasedFrameDecoder来按行分割数据
.addLast(new LineBasedFrameDecoder(10240))
.addLast(new StringDecoder(CharsetUtil.UTF_8))
.addLast(new StringEncoder(CharsetUtil.UTF_8))
.addLast(new HeartbeatHandler(param, CnSocketUtil.DEV_TAG))
//空闲状态的handler
//readerIdleTimeSeconds在指定的秒数内如果没有读取到任何数据则触发IdleState.READER_IDLE事件。
//writerIdleTimeSeconds在指定的秒数内如果没有写入任何数据则触发IdleState.WRITER_IDLE事件。
//allIdleTimeSeconds在指定的秒数内如果没有发生任何读取或写入操作则触发IdleState.ALL_IDLE事件。
.addLast(new IdleStateHandler(60, 0, 0, TimeUnit.SECONDS))
.addLast(handler);
}
@Resource
private SocketDevResponseService socketDevResponseService;
}
});
ChannelFuture channelFuture = bootstrap.connect(ip, port).sync();
channelFuture.addListener((ChannelFutureListener) ch -> {
if (!ch.isSuccess()) {
System.out.println("链接服务端失败...");
// 连接失败时关闭 group
group.shutdownGracefully();
} else {
System.out.println("链接服务端成功...");
if (handler instanceof NettySourceClientHandler) {
NioEventLoopGroup groupByUserId = SocketManager.getGroupByUserId(param.getUserPageId() + CnSocketUtil.SOURCE_TAG);
if (ObjectUtil.isNotNull(groupByUserId)) {
groupByUserId.shutdownGracefully().sync();
}
SocketManager.addGroup(param.getUserPageId() + CnSocketUtil.SOURCE_TAG, group);
} else {
NioEventLoopGroup groupByUserId = SocketManager.getGroupByUserId(param.getUserPageId() + CnSocketUtil.DEV_TAG);
if (ObjectUtil.isNotNull(groupByUserId)) {
groupByUserId.shutdownGracefully().sync();
}
SocketManager.addGroup(param.getUserPageId() + CnSocketUtil.DEV_TAG, group);
}
@Resource
private SocketContrastResponseService socketContrastResponseService;
System.out.println("客户端向服务端发送消息:" + port + msg);
channelFuture.channel().writeAndFlush(msg + "\n");
}
});
} catch (Exception e) {
System.out.println("连接socket服务端发送异常............" + e.getMessage());
group.shutdownGracefully();
//TODO 通知页面
SocketDataMsg socketDataMsg = new SocketDataMsg();
socketDataMsg.setType("aaa");
socketDataMsg.setCode(SourceResponseCodeEnum.SOCKET_ERROR.getCode());
socketDataMsg.setData(SourceResponseCodeEnum.SOCKET_ERROR.getMessage());
socketDataMsg.setRequestId("connect");
if (handler instanceof NettySourceClientHandler) {
socketDataMsg.setOperateCode("Source");
} else if (handler instanceof NettyDevClientHandler) {
CnSocketUtil.quitSendSource(param);
socketDataMsg.setOperateCode("Dev");
} else {
socketDataMsg.setOperateCode("Dev");
CnSocketUtil.quitSend(param);
}
WebServiceManager.sendMsg(param.getUserPageId(), JSON.toJSONString(socketDataMsg));
} finally {
// System.out.println("进入clientSocket最后步骤---------------------");
/**
* 静态实例,用于保持向后兼容
*/
private static NettyClient instance;
@PostConstruct
public void init() {
instance = this;
}
/**
* 创建程控源Handler实例Spring管理方式
* 自动注入SocketSourceResponseService统一使用CnSocketUtil.SOURCE_TAG
*
* @param param 检测参数
* @return NettySourceClientHandler 程控源处理器实例
*/
public NettySourceClientHandler createSourceHandler(PreDetectionParam param) {
return new NettySourceClientHandler(param, socketSourceResponseService);
}
/**
* 创建被检设备Handler实例Spring管理方式
* 自动注入SocketDevResponseService
*
* @param param 检测参数
* @return NettyDevClientHandler 被检设备处理器实例
*/
public NettyDevClientHandler createDeviceHandler(PreDetectionParam param) {
return new NettyDevClientHandler(param, socketDevResponseService);
}
/**
* 智能连接程控源设备(新增方法)
* 自动创建Handler并建立连接
*
* @param ip 程控源IP地址
* @param port 程控源端口
* @param param 检测参数
* @param msg 初始消息
*/
public void connectToSource(String ip, Integer port, PreDetectionParam param, String msg) {
NettySourceClientHandler handler = createSourceHandler(param);
executeSocketConnection(ip, port, param, msg, handler);
}
/**
* 智能连接被检设备(新增方法)
* 自动创建Handler并建立连接
*
* @param ip 被检设备IP地址
* @param port 被检设备端口
* @param param 检测参数
* @param msg 初始消息
*/
public void connectToDevice(String ip, Integer port, PreDetectionParam param, String msg) {
NettyDevClientHandler handler = createDeviceHandler(param);
executeSocketConnection(ip, port, param, msg, handler);
}
/**
* 智能连接比对被检设备(新增方法)
* 自动创建Handler并建立连接
*
* @param ip 被检设备IP地址
* @param port 被检设备端口
* @param param 检测参数
* @param msg 初始消息
* 静态方法:智能连接程控源设备(兼容性包装)
*/
private void connectToContrast(String ip, Integer port, ContrastDetectionParam param, String msg) {
PreDetectionParam preDetectionParam = new PreDetectionParam();
preDetectionParam.setUserPageId(param.getLoginName());
preDetectionParam.setTestItemList(param.getTestItemList());
preDetectionParam.setDevIds(param.getDevIds());
preDetectionParam.setUserId(param.getUserId());
NettyContrastClientHandler handler = new NettyContrastClientHandler();
handler.param = preDetectionParam;
handler.socketContrastResponseService = socketContrastResponseService;
executeSocketConnection(ip, port, preDetectionParam, msg, handler);
}
/**
* 静态方法:智能连接程控源设备(兼容性包装)
*/
public static void connectToContrastDeviceStatic(String ip, Integer port, ContrastDetectionParam param, String msg) {
if (instance != null) {
instance.connectToContrast(ip, port, param, msg);
} else {
log.error("NettyClient未初始化无法创建比对设备通讯连接");
}
}
/**
* 静态方法:智能连接程控源设备(兼容性包装)
*/
public static void connectToSourceStatic(String ip, Integer port, PreDetectionParam param, String msg) {
if (instance != null) {
instance.connectToSource(ip, port, param, msg);
} else {
log.error("NettyClient未初始化无法创建程控源连接");
}
}
/**
* 重连方法
* 静态方法:智能连接被检设备(兼容性包装)
*/
public static void connect(Bootstrap bootstrap, String msg) {
try {
bootstrap.connect("127.0.0.1", 8787).sync()
.addListener((ChannelFutureListener) ch -> {
if (!ch.isSuccess()) {
ch.channel().close();
final EventLoop loop = ch.channel().eventLoop();
loop.schedule(() -> {
System.err.println("服务端链接不上,开始重连操作...");
//重连
connect(bootstrap, msg);
}, 3L, TimeUnit.SECONDS);
} else {
if (StrUtil.isNotBlank(msg)) {
ch.channel().writeAndFlush(msg);
}
System.out.println("服务端链接成功...");
}
});
} catch (Exception e) {
System.out.println(e.getMessage());
try {
Thread.sleep(3000L);
} catch (InterruptedException ex) {
ex.printStackTrace();
}
//再重连
connect(bootstrap, msg);
public static void connectToDeviceStatic(String ip, Integer port, PreDetectionParam param, String msg) {
if (instance != null) {
instance.connectToDevice(ip, port, param, msg);
} else {
log.error("NettyClient未初始化无法创建被检设备连接");
}
}
/**
* 内部重构后的实现 - 拆分职责但不暴露给外部
* 执行完整的Socket连接建立流程
* 1. 创建事件循环组
* 2. 配置Bootstrap启动器
* 3. 设置管道处理链
* 4. 建立连接并处理结果
*
* @param ip 目标服务器IP地址
* @param port 目标服务器端口号
* @param param 检测参数对象
* @param msg 连接成功后发送的初始消息
* @param handler 业务处理器(区分程控源和被检设备)
*/
private static void executeSocketConnection(String ip, Integer port,
PreDetectionParam param, String msg, SimpleChannelInboundHandler<String> handler) {
// 创建NIO事件循环组用于处理网络I/O事件
NioEventLoopGroup group = createEventLoopGroup();
try {
// 配置客户端启动器
Bootstrap bootstrap = configureBootstrap(group);
// 创建管道初始化器,配置编解码和业务处理链
ChannelInitializer<NioSocketChannel> initializer = createChannelInitializer(param, handler);
bootstrap.handler(initializer);
// 同步连接到目标服务器
ChannelFuture channelFuture = bootstrap.connect(ip, port).sync();
// 处理连接结果(成功或失败)
handleConnectionResult(channelFuture, param, handler, group, msg);
} catch (Exception e) {
// 处理连接过程中的异常
handleConnectionException(e, param, handler, group);
}
}
/**
* 创建NIO事件循环组
* 用于管理网络I/O操作的线程池处理连接、读写等异步事件
*
* @return NioEventLoopGroup 事件循环组实例
*/
private static NioEventLoopGroup createEventLoopGroup() {
return new NioEventLoopGroup();
}
/**
* 配置Bootstrap客户端启动器
* 设置连接超时、通道类型等基础参数
*
* @param group 事件循环组
* @return Bootstrap 配置好的启动器
*/
private static Bootstrap configureBootstrap(NioEventLoopGroup group) {
return new Bootstrap()
// 绑定事件循环组
.group(group)
// 连接超时5秒
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000)
// 使用NIO Socket通道
.channel(NioSocketChannel.class);
}
/**
* 创建通道初始化器
* 当新连接建立时,初始化该连接的处理管道
*
* @param param 检测参数,用于配置心跳处理器
* @param handler 业务处理器
* @return ChannelInitializer 通道初始化器
*/
private static ChannelInitializer<NioSocketChannel> createChannelInitializer(
PreDetectionParam param, SimpleChannelInboundHandler<String> handler) {
return new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) {
setupPipeline(ch.pipeline(), param, handler);
}
};
}
/**
* 配置管道处理链
* 按顺序添加各种处理器,构成完整的数据处理流水线:
* 1. LineBasedFrameDecoder按行分割数据解决TCP粘包拆包问题
* 2. StringDecoder/StringEncoder字符串编解码器
* 3. HeartbeatHandler心跳处理器维持连接活跃
* 4. IdleStateHandler空闲检测器仅被检设备需要
* 5. 业务处理器:具体的业务逻辑处理
*
* @param pipeline 管道对象
* @param param 检测参数
* @param handler 业务处理器
*/
private static void setupPipeline(ChannelPipeline pipeline,
PreDetectionParam param, SimpleChannelInboundHandler<String> handler) {
// 基础编解码器:处理数据格式转换和粘包拆包
// 按行分割最大20KB
pipeline.addLast(new LineBasedFrameDecoder(10240 * 2))
// 字节转字符串
.addLast(new StringDecoder(CharsetUtil.UTF_8))
// 字符串转字节
.addLast(new StringEncoder(CharsetUtil.UTF_8));
// 心跳处理器:根据设备类型选择不同的标签
String tag = getDeviceTag(handler);
pipeline.addLast(new HeartbeatHandler(param, tag));
// 空闲检测器仅被检设备和比对被检设备需要60秒无读操作触发空闲事件
if (!isSourceHandler(handler)) {
pipeline.addLast(new IdleStateHandler(60, 0, 0, TimeUnit.SECONDS));
}
// 业务处理器:处理具体的检测业务逻辑
pipeline.addLast(handler);
}
/**
* 判断是否为程控源处理器
* 程控源设备和被检设备使用不同的处理器和配置
*
* @param handler 业务处理器
* @return boolean true:程控源处理器, false:被检设备处理器
*/
private static boolean isSourceHandler(SimpleChannelInboundHandler<String> handler) {
return handler instanceof NettySourceClientHandler;
}
/**
* 获取设备标签
* 用于在SocketManager中区分不同类型的设备连接
*
* @param handler 业务处理器
* @return String 设备标签("_Source" 或 "_Dev"
*/
private static String getDeviceTag(SimpleChannelInboundHandler<String> handler) {
String tag;
if (handler instanceof NettySourceClientHandler) {
tag = CnSocketUtil.SOURCE_TAG;
} else if (handler instanceof NettyDevClientHandler) {
tag = CnSocketUtil.DEV_TAG;
} else {
tag = CnSocketUtil.CONTRAST_DEV_TAG;
}
return tag;
}
/**
* 获取名称
* 用于在SocketManager中区分不同类型的设备连接
*
* @param handler 业务处理器
* @return String 设备标签("程控源设备" 或 "被检设备"
*/
private static String getDeviceType(SimpleChannelInboundHandler<String> handler) {
String deviceType;
if (handler instanceof NettySourceClientHandler) {
deviceType = "程控源设备";
} else if (handler instanceof NettyDevClientHandler) {
deviceType = "被检设备";
} else {
deviceType = "比对被检设备";
}
return deviceType;
}
/**
* 处理连接结果
* 为连接Future添加监听器异步处理连接成功或失败的情况
*
* @param channelFuture 连接Future对象
* @param param 检测参数
* @param handler 业务处理器
* @param group 事件循环组
* @param msg 初始消息
*/
private static void handleConnectionResult(ChannelFuture channelFuture,
PreDetectionParam param, SimpleChannelInboundHandler<String> handler,
NioEventLoopGroup group, String msg) {
channelFuture.addListener((ChannelFutureListener) ch -> {
if (!ch.isSuccess()) {
onConnectionFailure(handler, group);
} else {
onConnectionSuccess(channelFuture, param, handler, group, msg);
}
});
}
/**
* 连接失败处理
* 输出失败信息并优雅关闭事件循环组
*
* @param handler 业务处理器,用于区分设备类型
* @param group 事件循环组
*/
private static void onConnectionFailure(SimpleChannelInboundHandler<String> handler, NioEventLoopGroup group) {
String deviceType = getDeviceType(handler);
log.info("连接{}服务端失败...", deviceType);
group.shutdownGracefully();
}
/**
* 连接成功处理
* 执行连接成功后的初始化操作:
* 1. 管理Socket连接会话注册EventLoopGroup到SocketManager
* 2. 注册Channel到SocketManager实现统一的连接管理
* 3. 通过SocketManager发送初始消息统一消息发送入口
*
* @param channelFuture 连接Future对象
* @param param 检测参数
* @param handler 业务处理器
* @param group 事件循环组
* @param msg 初始消息
*/
private static void onConnectionSuccess(ChannelFuture channelFuture,
PreDetectionParam param, SimpleChannelInboundHandler<String> handler,
NioEventLoopGroup group, String msg) {
String deviceType = getDeviceType(handler);
log.info("连接{}服务端成功...", deviceType);
// 管理连接会话将EventLoopGroup注册到SocketManager
manageSocketConnection(param, handler, group);
// 将Channel也注册到SocketManager便于统一消息发送
String userId = param.getUserPageId() + getDeviceTag(handler);
SocketManager.addUser(userId, channelFuture.channel());
// 通过SocketManager发送初始消息统一消息发送入口
SocketManager.sendMsg(userId, msg);
}
/**
* 管理Socket连接会话
* 将新建立的连接注册到SocketManager中进行统一管理
* 1. 检查并关闭同用户同设备类型的旧连接,避免资源泄露
* 2. 将新连接注册到SocketManager便于后续管理和查找
* <p>
* 连接Key格式{userPageId}_{deviceTag}
* 例如zhangsan_test_Source程控源 / zhangsan_test_Dev被检设备
*
* @param param 检测参数包含用户页面ID
* @param handler 业务处理器,用于区分设备类型
* @param group 事件循环组,表示具体的连接资源
*/
private static void manageSocketConnection(PreDetectionParam param,
SimpleChannelInboundHandler<String> handler, NioEventLoopGroup group) {
// 构建连接标识用户ID + 设备标签
String key = param.getUserPageId() + getDeviceTag(handler);
// 关闭旧连接:同一用户同一设备类型只能有一个活跃连接
NioEventLoopGroup existingGroup = SocketManager.getGroupByUserId(key);
if (ObjectUtil.isNotNull(existingGroup)) {
try {
existingGroup.shutdownGracefully().sync();
} catch (InterruptedException e) {
// 恢复中断状态
Thread.currentThread().interrupt();
}
}
// 注册新连接到SocketManager
SocketManager.addGroup(key, group);
}
/**
* 处理连接异常
* 当连接建立过程中发生异常时的统一处理流程:
* 1. 关闭相关资源,防止资源泄露
* 2. 执行设备相关的退出操作
* 3. 通过WebSocket向前端通知错误信息
*
* @param e 异常对象
* @param param 检测参数
* @param handler 业务处理器
* @param group 事件循环组
*/
private static void handleConnectionException(Exception e, PreDetectionParam param,
SimpleChannelInboundHandler<String> handler, NioEventLoopGroup group) {
log.info("连接socket服务端发送异常: {}", e.getMessage());
// 关闭事件循环组资源
group.shutdownGracefully();
// 执行设备相关的退出操作
executeQuitOperations(param, handler);
// 通过WebSocket通知前端页面
notifyFrontendError(param, handler);
}
/**
* 执行退出操作
* 根据不同的设备处理器类型执行相应的退出指令:
* - NettyDevClientHandler被检设备处理器需要发送程控源退出指令
* - 其他非程控源处理器:发送通用退出指令
* - NettySourceClientHandler程控源处理器无需额外退出操作
*
* @param param 检测参数
* @param handler 业务处理器
*/
private static void executeQuitOperations(PreDetectionParam param,
SimpleChannelInboundHandler<String> handler) {
if (handler instanceof NettyDevClientHandler) {
// 被检设备异常时,发送程控源退出指令
CnSocketUtil.quitSendSource(param);
}
// 程控源处理器异常时无需额外操作
}
/**
* 通知前端错误信息
* 构建错误消息对象并通过WebSocket发送给前端页面
* 前端可以根据操作码Source/Dev显示相应的错误提示
*
* @param param 检测参数包含用户页面ID
* @param handler 业务处理器,用于确定操作码
*/
private static void notifyFrontendError(PreDetectionParam param,
SimpleChannelInboundHandler<String> handler) {
// 构建错误消息对象
SocketDataMsg socketDataMsg = new SocketDataMsg();
// 消息类型
socketDataMsg.setType("aaa");
// 错误码
socketDataMsg.setCode(SourceResponseCodeEnum.SOCKET_ERROR.getCode());
// 错误消息
socketDataMsg.setData(SourceResponseCodeEnum.SOCKET_ERROR.getMessage());
// 请求ID标识
socketDataMsg.setRequestId("connect");
// 设置操作码:程控源为"Source",被检设备为"Dev"
String devTag = getDeviceTag(handler).substring(1);
socketDataMsg.setOperateCode(devTag);
// 通过WebSocket发送错误信息到前端页面
WebServiceManager.sendMsg(param.getUserPageId(), JSON.toJSONString(socketDataMsg));
}
}

View File

@@ -1,10 +1,16 @@
package com.njcn.gather.detection.util.socket.cilent;
import com.alibaba.fastjson.JSON;
import com.njcn.gather.detection.handler.SocketContrastResponseService;
import com.njcn.gather.detection.pojo.enums.SourceOperateCodeEnum;
import com.njcn.gather.detection.pojo.enums.SourceResponseCodeEnum;
import com.njcn.gather.detection.pojo.param.PreDetectionParam;
import com.njcn.gather.detection.pojo.vo.SocketDataMsg;
import com.njcn.gather.detection.util.socket.CnSocketUtil;
import com.njcn.gather.detection.util.socket.FormalTestManager;
import com.njcn.gather.detection.util.socket.MsgUtil;
import com.njcn.gather.detection.util.socket.SocketManager;
import com.njcn.gather.detection.util.socket.websocket.WebServiceManager;
import com.njcn.gather.plan.pojo.enums.DataSourceEnum;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
@@ -13,6 +19,7 @@ import io.netty.handler.timeout.IdleState;
import io.netty.handler.timeout.IdleStateEvent;
import io.netty.handler.timeout.TimeoutException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import java.io.IOException;
import java.net.ConnectException;
@@ -24,17 +31,18 @@ import java.util.Objects;
* @author caozehui
* @data 2025-07-25
*/
@Slf4j
@RequiredArgsConstructor
public class NettyContrastClientHandler extends SimpleChannelInboundHandler<String> {
private final PreDetectionParam param;
private final SocketContrastResponseService socketContrastResponseService;
public static PreDetectionParam param;
public static SocketContrastResponseService socketContrastResponseService;
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
System.out.println("客户端通道已建立" + ctx.channel().id());
Channel channel = SocketManager.getChannelByUserId(param.getUserPageId() + CnSocketUtil.DEV_TAG);
Channel channel = SocketManager.getChannelByUserId(param.getUserPageId() + CnSocketUtil.CONTRAST_DEV_TAG);
if (Objects.nonNull(channel)) {
try {
channel.close().sync();
@@ -42,17 +50,21 @@ public class NettyContrastClientHandler extends SimpleChannelInboundHandler<Stri
e.printStackTrace();
}
}
SocketManager.addUser(param.getUserPageId() + CnSocketUtil.DEV_TAG, ctx.channel());
SocketManager.addUser(param.getUserPageId() + CnSocketUtil.CONTRAST_DEV_TAG, ctx.channel());
}
@Override
protected void channelRead0(ChannelHandlerContext ctx, String msg) throws InterruptedException {
System.out.println("contrastClientHandler接收server端数据>>>>>>" + msg);
log.info("contrastdevhandler接收server端数据: {}", msg);
try {
socketContrastResponseService.deal(param, msg);
} catch (Exception e) {
e.printStackTrace();
CnSocketUtil.quitSend(param);
log.error("处理服务端消息异常", e);
if (FormalTestManager.currentStep == SourceOperateCodeEnum.RECORD_WAVE_STEP1 || FormalTestManager.currentStep == SourceOperateCodeEnum.RECORD_WAVE_STEP2) {
CnSocketUtil.contrastSendquit(param.getUserPageId(), SourceOperateCodeEnum.QUIT_INIT_03, true);
} else {
CnSocketUtil.contrastSendquit(param.getUserPageId(), SourceOperateCodeEnum.QUIT_INIT_02, true);
}
}
}
@@ -60,7 +72,7 @@ public class NettyContrastClientHandler extends SimpleChannelInboundHandler<Stri
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
System.out.println("与通信模块端断线");
ctx.close();
SocketManager.removeUser(param.getUserPageId() + CnSocketUtil.DEV_TAG);
SocketManager.removeUser(param.getUserPageId() + CnSocketUtil.CONTRAST_DEV_TAG);
}
/**
@@ -76,68 +88,17 @@ public class NettyContrastClientHandler extends SimpleChannelInboundHandler<Stri
System.out.println(LocalDateTime.now() + "contrastClientHandler触发读超时函数**************************************");
SocketManager.contrastClockMap.put(DataSourceEnum.REAL_DATA, SocketManager.contrastClockMap.get(DataSourceEnum.REAL_DATA) + 60L);
//实时数据
if (SocketManager.contrastClockMap.get(DataSourceEnum.REAL_DATA) >= 60) {
CnSocketUtil.quitSend(param);
System.out.println("超时处理-----》" + "实时数据已超时----------------关闭");
timeoutSend();
if (FormalTestManager.isRemoveSocket) {
//实时数据
if (SocketManager.contrastClockMap.get(DataSourceEnum.REAL_DATA) >= 60) {
CnSocketUtil.contrastSendquit(param.getUserPageId(), SourceOperateCodeEnum.QUIT_INIT_02, false);
System.out.println("超时处理-----》" + "实时数据已超时----------------关闭");
timeoutSend();
}
}
// if (!FormalTestManager.hasStopFlag) {
// if (CollUtil.isNotEmpty(SocketManager.getSourceList())) {
// SourceIssue sourceIssue = SocketManager.getSourceList().get(0);
// if (SocketManager.clockMap.containsKey(sourceIssue.getIndex())) {
// SocketManager.clockMap.put(sourceIssue.getIndex(), SocketManager.clockMap.get(sourceIssue.getIndex()) + 60L);
// } else {
// SocketManager.clockMap.put(sourceIssue.getIndex(), 60L);
// }
//
// if (sourceIssue.getType().equals(DicDataEnum.F.getCode())) {
// //闪变,正常抛一轮最大等待20分钟超时
// if (SocketManager.clockMap.get(sourceIssue.getIndex()) > 1300) {
// fly = true;
// System.out.println("超时处理-----》" + sourceIssue.getType() + "已超时----------------关闭");
// CnSocketUtil.quitSend(param);
// timeoutSend(sourceIssue);
// }
// } else if (sourceIssue.getType().equals(DicDataEnum.VOLTAGE.getCode()) || sourceIssue.getType().equals(DicDataEnum.HP.getCode())) {
// //统计数据项,正常抛一轮数据,超时
// if (SocketManager.clockMap.get(sourceIssue.getIndex()) > 180) {
// fly = true;
// CnSocketUtil.quitSend(param);
// System.out.println("超时处理-----》" + sourceIssue.getType() + "已超时----------------关闭");
// timeoutSend(sourceIssue);
// }
// } else {
// //实时数据
// if (SocketManager.clockMap.get(sourceIssue.getIndex()) > 60) {
// fly = true;
// CnSocketUtil.quitSend(param);
// System.out.println("超时处理-----》" + sourceIssue.getType() + "已超时----------------关闭");
// timeoutSend(sourceIssue);
// }
// }
// } else {
// fly = true;
// //为空则认为是常规步骤,设定一分钟超时
// CnSocketUtil.quitSend(param);
// CnSocketUtil.sendToWebSocket(param.getUserPageId(), SourceOperateCodeEnum.SOCKET_TIMEOUT.getValue(), SourceOperateCodeEnum.SOCKET_TIMEOUT.getValue(), SourceOperateCodeEnum.SOCKET_TIMEOUT.getMsg(), null);
// }
// if (fly) {
// socketContrastResponseService.backCheckState(param);
// }
// } else {
// //如果是暂停操作后
// FormalTestManager.stopTime += 60;
// System.out.println("当前进入暂停操作超时函数-----------------" + FormalTestManager.stopTime);
// if (FormalTestManager.stopTime > 600) {
// CnSocketUtil.quitSend(param);
// CnSocketUtil.sendToWebSocket(param.getUserPageId(), SourceOperateCodeEnum.FORMAL_REAL.getValue(), SourceOperateCodeEnum.STOP_TIMEOUT.getValue(), SourceOperateCodeEnum.STOP_TIMEOUT.getMsg(), null);
// }
// }
}
}
}
@Override
@@ -155,7 +116,7 @@ public class NettyContrastClientHandler extends SimpleChannelInboundHandler<Stri
System.out.println("连接socket服务端异常");
} else if (cause instanceof IOException) {
System.out.println("IOException caught: There was an I/O error.");
CnSocketUtil.sendToWebSocket(param.getUserPageId(), SourceOperateCodeEnum.DEVICE_ERROR.getValue(), SourceOperateCodeEnum.DEVICE_ERROR.getValue(), SourceOperateCodeEnum.DEVICE_ERROR.getMsg(), null);
WebServiceManager.sendDetectionErrorMessage(param.getUserPageId(), SourceOperateCodeEnum.DEVICE_ERROR);
} else if (cause instanceof TimeoutException) {
System.out.println("TimeoutException caught: Operation timed out.");
} else if (cause instanceof ProtocolException) {
@@ -163,9 +124,9 @@ public class NettyContrastClientHandler extends SimpleChannelInboundHandler<Stri
} else {
// 处理其他类型的异常
System.out.println("Unknown exception caught: " + cause.getMessage());
CnSocketUtil.sendToWebSocket(param.getUserPageId(), SourceOperateCodeEnum.DEVICE_ERROR.getValue(), SourceOperateCodeEnum.DEVICE_ERROR.getValue(), SourceOperateCodeEnum.DEVICE_ERROR.getMsg(), null);
WebServiceManager.sendDetectionErrorMessage(param.getUserPageId(), SourceOperateCodeEnum.DEVICE_ERROR);
}
CnSocketUtil.quitSend(param);
CnSocketUtil.contrastSendquit(param.getUserPageId(), SourceOperateCodeEnum.QUIT_INIT_02, true);
// socketContrastResponseService.backCheckState(param);
ctx.close();
}
@@ -175,7 +136,12 @@ public class NettyContrastClientHandler extends SimpleChannelInboundHandler<Stri
* 接收数据超时处理
*/
private void timeoutSend() {
// 向前端推送超时消息
SocketDataMsg webSend = new SocketDataMsg();
webSend.setCode(SourceResponseCodeEnum.RECEIVE_DATA_TIME_OUT.getCode());
WebServiceManager.sendMsg(param.getUserPageId(), MsgUtil.msgToWebData(webSend, FormalTestManager.devNameMapComm, 0));
WebServiceManager.sendMsg(param.getUserPageId(), JSON.toJSONString(webSend));
}
}

View File

@@ -3,6 +3,7 @@ package com.njcn.gather.detection.util.socket.cilent;
import cn.hutool.core.collection.CollUtil;
import com.alibaba.fastjson.JSON;
import com.njcn.gather.detection.handler.SocketDevResponseService;
import com.njcn.gather.detection.pojo.enums.ResultEnum;
import com.njcn.gather.detection.pojo.enums.SourceOperateCodeEnum;
import com.njcn.gather.detection.pojo.param.PreDetectionParam;
import com.njcn.gather.detection.pojo.vo.DevLineTestResult;
@@ -10,7 +11,7 @@ import com.njcn.gather.detection.pojo.vo.WebSocketVO;
import com.njcn.gather.detection.util.socket.CnSocketUtil;
import com.njcn.gather.detection.util.socket.FormalTestManager;
import com.njcn.gather.detection.util.socket.SocketManager;
import com.njcn.gather.detection.util.socket.WebServiceManager;
import com.njcn.gather.detection.util.socket.websocket.WebServiceManager;
import com.njcn.gather.device.pojo.vo.PreDetection;
import com.njcn.gather.script.pojo.po.SourceIssue;
import com.njcn.gather.system.pojo.enums.DicDataEnum;
@@ -21,227 +22,325 @@ import io.netty.handler.timeout.IdleState;
import io.netty.handler.timeout.IdleStateEvent;
import io.netty.handler.timeout.TimeoutException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import java.io.IOException;
import java.net.ConnectException;
import java.net.ProtocolException;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
/**
* @Description: 源客户端业务处理(示例)
* Netty设备客户端处理器
* <p>负责处理与被检测设备的Socket通信包括</p>
* <ul>
* <li>通道生命周期管理(建立、断开)</li>
* <li>消息接收和处理</li>
* <li>心跳超时处理</li>
* <li>异常处理和恢复</li>
* </ul>
*
* @Description: 设备客户端业务处理器
* @Author: wr
* @Date: 2024/12/10 14:16
*/
@Slf4j
@RequiredArgsConstructor
public class NettyDevClientHandler extends SimpleChannelInboundHandler<String> {
/**
* 闪变检测超时时间20分钟1300秒
*/
private static final long FLICKER_TIMEOUT = 1300L;
/**
* 统计数据检测超时时间3分钟180秒
*/
private static final long STATISTICS_TIMEOUT = 180L;
/**
* 实时数据检测超时时间1分钟60秒
*/
private static final long REALTIME_TIMEOUT = 60L;
/**
* 暂停操作超时时间10分钟600秒
*/
private static final long STOP_TIMEOUT = 600L;
private final PreDetectionParam param;
private final SocketDevResponseService socketResponseService;
/**
* 当通道进行连接时推送消息
* 当通道连接建立时的处理逻辑
* <p>将关闭原有连接并将新连接注册到SocketManager中</p>
*
* @param ctx
* @param ctx 通道上下文
* @throws Exception 连接异常
*/
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
System.out.println("客户端通道已建立" + ctx.channel().id());
log.info("客户端通道已建立: {}", ctx.channel().id());
// 检查是否存在同一用户的老连接
Channel channel = SocketManager.getChannelByUserId(param.getUserPageId() + CnSocketUtil.DEV_TAG);
if (Objects.nonNull(channel)) {
try {
// 关闭老连接避免连接泄漏
channel.close().sync();
} catch (InterruptedException e) {
e.printStackTrace();
log.error("关闭通道异常", e);
}
}
// 注册新的连接到用户管理器
SocketManager.addUser(param.getUserPageId() + CnSocketUtil.DEV_TAG, ctx.channel());
}
/**
* 处理服务端消息消息信息
*/
@Override
protected void channelRead0(ChannelHandlerContext ctx, String msg) throws InterruptedException {
System.out.println("devhandler接收server端数据>>>>>>" + msg);
try {
socketResponseService.deal(param, msg);
} catch (Exception e) {
e.printStackTrace();
CnSocketUtil.quitSend(param);
}
}
/**
* 当通道断线时,支持重连
* 当通道断开时的清理工作
* <p>关闭连接,清理用户映射,退出源设备发送</p>
*
* @param ctx
* @param ctx 通道上下文
* @throws Exception 关闭异常
*/
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
System.out.println("设备通讯客户端断线");
log.warn("设备通讯客户端断线");
ctx.close();
SocketManager.removeUser(param.getUserPageId() + CnSocketUtil.DEV_TAG);
CnSocketUtil.quitSendSource(param);
}
/**
* 用户事件的回调方法(自定义事件用于心跳机制)
* 处理从服务端接收到的消息
* <p>将消息交给SocketDevResponseService进行具体处理</p>
*
* @param ctx
* @param evt
* @param ctx 通道上下文
* @param msg 接收到的消息
*/
@Override
protected void channelRead0(ChannelHandlerContext ctx, String msg) {
log.info("devhandler接收server端数据: {}", msg);
try {
socketResponseService.deal(param, msg);
} catch (Exception e) {
log.error("处理服务端消息异常", e);
CnSocketUtil.quitSend(param);
}
}
/**
* 用户事件回调方法,主要用于处理心跳超时
* <p>当触发READER_IDLE事件时根据当前状态进行超时处理</p>
*
* @param ctx 通道上下文
* @param evt 用户事件对象
*/
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) {
Boolean fly = false;
if (evt instanceof IdleStateEvent) { //IdleState.在一段时间内没有收到任何消息时,会触发该事件
if (((IdleStateEvent) evt).state() == IdleState.READER_IDLE) {
System.out.println(LocalDateTime.now() + "devHandler触发读超时函数**************************************");
if (!FormalTestManager.hasStopFlag) {
if (CollUtil.isNotEmpty(SocketManager.getSourceList())) {
SourceIssue sourceIssue = SocketManager.getSourceList().get(0);
if (SocketManager.clockMap.containsKey(sourceIssue.getIndex())) {
SocketManager.clockMap.put(sourceIssue.getIndex(), SocketManager.clockMap.get(sourceIssue.getIndex()) + 60L);
} else {
SocketManager.clockMap.put(sourceIssue.getIndex(), 60L);
}
if (sourceIssue.getType().equals(DicDataEnum.F.getCode())) {
//闪变,正常抛一轮最大等待20分钟超时
if (SocketManager.clockMap.get(sourceIssue.getIndex()) > 1300) {
fly = true;
System.out.println("超时处理-----》" + sourceIssue.getType() + "已超时----------------关闭");
CnSocketUtil.quitSend(param);
timeoutSend(sourceIssue);
}
} else if (sourceIssue.getType().equals(DicDataEnum.VOLTAGE.getCode()) || sourceIssue.getType().equals(DicDataEnum.HP.getCode())) {
//统计数据项,正常抛一轮数据,超时
if (SocketManager.clockMap.get(sourceIssue.getIndex()) > 180) {
fly = true;
CnSocketUtil.quitSend(param);
System.out.println("超时处理-----》" + sourceIssue.getType() + "已超时----------------关闭");
timeoutSend(sourceIssue);
}
} else {
//实时数据
if (SocketManager.clockMap.get(sourceIssue.getIndex()) > 60) {
fly = true;
CnSocketUtil.quitSend(param);
System.out.println("超时处理-----》" + sourceIssue.getType() + "已超时----------------关闭");
timeoutSend(sourceIssue);
}
}
} else {
fly = true;
//为空则认为是常规步骤,设定一分钟超时
CnSocketUtil.quitSend(param);
CnSocketUtil.sendToWebSocket(param.getUserPageId(), SourceOperateCodeEnum.SOCKET_TIMEOUT.getValue(), SourceOperateCodeEnum.SOCKET_TIMEOUT.getValue(), SourceOperateCodeEnum.SOCKET_TIMEOUT.getMsg(), null);
}
if (fly) {
socketResponseService.backCheckState(param);
}
} else {
//如果是暂停操作后
FormalTestManager.stopTime += 60;
System.out.println("当前进入暂停操作超时函数-----------------" + FormalTestManager.stopTime);
if (FormalTestManager.stopTime > 600) {
CnSocketUtil.quitSend(param);
CnSocketUtil.sendToWebSocket(param.getUserPageId(), SourceOperateCodeEnum.FORMAL_REAL.getValue(), SourceOperateCodeEnum.STOP_TIMEOUT.getValue(), SourceOperateCodeEnum.STOP_TIMEOUT.getMsg(), null);
}
}
// 检查是否为读取空闲事件(心跳超时)
if (evt instanceof IdleStateEvent && ((IdleStateEvent) evt).state() == IdleState.READER_IDLE) {
log.warn("devHandler触发读超时函数: {}", LocalDateTime.now());
// 根据是否有停止标志采取不同的超时处理策略
if (!FormalTestManager.hasStopFlag) {
// 正常检测中的超时处理
handleReadTimeout();
} else {
// 暂停状态下的超时处理
handleStopTimeout();
}
}
}
/**
* 处理器被添加到管道时的回调
*
* @param ctx 通道上下文
*/
@Override
public void handlerAdded(ChannelHandlerContext ctx) {
System.out.println("有通道准备接入" + ctx.channel());
log.info("有通道准备接入: {}", ctx.channel());
}
/**
* 异常捕获处理
* <p>捕获并处理各种类型的异常,执行清理工作</p>
*
* @param ctx 通道上下文
* @param cause 异常原因
*/
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
System.out.println("捕获到设备服务异常。。。。。。。");
// 处理异常,例如记录日志、关闭连接等
cause.printStackTrace();
// 根据异常类型进行不同的处理
if (cause instanceof ConnectException) {
// 处理连接异常,例如重试连接或记录特定的连接错误信息
System.out.println("连接socket服务端异常");
} else if (cause instanceof IOException) {
// 处理I/O异常例如读写错误
System.out.println("IOException caught: There was an I/O error.");
CnSocketUtil.sendToWebSocket(param.getUserPageId(), SourceOperateCodeEnum.DEVICE_ERROR.getValue(), SourceOperateCodeEnum.DEVICE_ERROR.getValue(), SourceOperateCodeEnum.DEVICE_ERROR.getMsg(), null);
// 例如可以记录更详细的I/O错误信息
} else if (cause instanceof TimeoutException) {
// 处理超时异常
System.out.println("TimeoutException caught: Operation timed out.");
// 可以根据业务逻辑决定是否重试或记录超时信息
} else if (cause instanceof ProtocolException) {
// 处理协议异常,例如消息格式不正确
System.out.println("ProtocolException caught: Invalid protocol message.");
// 可以记录协议错误信息或向客户端发送错误响应
} else {
// 处理其他类型的异常
System.out.println("Unknown exception caught: " + cause.getMessage());
CnSocketUtil.sendToWebSocket(param.getUserPageId(), SourceOperateCodeEnum.DEVICE_ERROR.getValue(), SourceOperateCodeEnum.DEVICE_ERROR.getValue(), SourceOperateCodeEnum.DEVICE_ERROR.getMsg(), null);
// 可以记录未知异常信息
}
log.error("捕获到设备服务异常", cause);
handleSpecificException(cause);
// 统一清理工作
CnSocketUtil.quitSend(param);
CnSocketUtil.quitSendSource(param);
socketResponseService.backCheckState(param);
ctx.close();
}
/**
* 发送业务消息时候开启计时器,
* @param requestId
* 处理特定类型的异常
* <p>根据异常类型进行相应的日志记录和错误通知</p>
*
* @param cause 异常对象
*/
/* private void scheduleTimeoutTask(String requestId) {
ScheduledFuture<?> future = scheduler.schedule(() -> {
if (requestTimeoutTasks.containsKey(requestId)) {
// 处理超时逻辑
System.out.println("Business request with ID " + requestId + " timed out.");
requestTimeoutTasks.remove(requestId);
ctx.close();
}
}, BUSINESS_REQUEST_TIMEOUT, TimeUnit.MILLISECONDS);
requestTimeoutTasks.put(requestId, future);
}*/
private void handleSpecificException(Throwable cause) {
if (cause instanceof ConnectException) {
log.error("连接socket服务端异常");
} else if (cause instanceof IOException) {
log.error("IO异常: {}", cause.getMessage());
WebServiceManager.sendDetectionErrorMessage(param.getUserPageId(), SourceOperateCodeEnum.DEVICE_ERROR);
} else if (cause instanceof TimeoutException) {
log.error("操作超时: {}", cause.getMessage());
} else if (cause instanceof ProtocolException) {
log.error("协议异常: {}", cause.getMessage());
} else {
log.error("未知异常: {}", cause.getMessage());
WebServiceManager.sendDetectionErrorMessage(param.getUserPageId(), SourceOperateCodeEnum.DEVICE_ERROR);
}
}
/**
* 超时后的处理
* 处理读取超时事件
* <p>检查源列表,更新超时计数器,判断是否超时并处理</p>
*/
private void handleReadTimeout() {
if (CollUtil.isNotEmpty(SocketManager.getSourceList())) {
// 获取当前正在检测的源问题(取第一个)
SourceIssue sourceIssue = SocketManager.getSourceList().get(0);
// 更新该源问题的超时计数器
updateTimeoutCounter(sourceIssue);
// 根据检测类型判断是否已超时
if (isTimeout(sourceIssue)) {
handleTimeout(sourceIssue);
}
} else {
// 源列表为空,认为是常规步骤的超时,默认一分钟超时
CnSocketUtil.quitSend(param);
WebServiceManager.sendDetectionErrorMessage(param.getUserPageId(), SourceOperateCodeEnum.SOCKET_TIMEOUT);
socketResponseService.backCheckState(param);
}
}
/**
* 处理暂停操作的超时事件
* <p>当检测被暂停时,统计暂停时间,超过限制后发送超时通知</p>
*/
private void handleStopTimeout() {
FormalTestManager.stopTime += 60;
log.warn("当前进入暂停操作超时函数,停止时间: {}", FormalTestManager.stopTime);
if (FormalTestManager.stopTime > STOP_TIMEOUT) {
CnSocketUtil.quitSend(param);
WebServiceManager.sendDetectionErrorMessage(param.getUserPageId(), SourceOperateCodeEnum.FORMAL_REAL.getValue(), SourceOperateCodeEnum.STOP_TIMEOUT);
}
}
/**
* 更新指定源问题的超时计数器
* <p>每次调用时增加60秒用于统计累计超时时间</p>
*
* @param sourceIssue 源问题对象
*/
private void updateTimeoutCounter(SourceIssue sourceIssue) {
Integer index = sourceIssue.getIndex();
if (SocketManager.clockMap.containsKey(index)) {
SocketManager.clockMap.put(index, SocketManager.clockMap.get(index) + 60L);
} else {
SocketManager.clockMap.put(index, 60L);
}
}
/**
* 根据检测类型判断是否已超时
* <p>不同检测类型有不同的超时阈值:</p>
* <ul>
* <li>闪变检测20分钟</li>
* <li>统计数据3分钟</li>
* <li>实时数据1分钟</li>
* </ul>
*
* @param sourceIssue 源问题对象
* @return true 如果已超时false 否则
*/
private boolean isTimeout(SourceIssue sourceIssue) {
long currentTime = SocketManager.clockMap.get(sourceIssue.getIndex());
String type = sourceIssue.getType();
// 根据不同检测类型使用不同的超时阈值
if (DicDataEnum.F.getCode().equals(type)) {
// 闪变检测需要更长时间20分钟超时
return currentTime >= FLICKER_TIMEOUT;
} else if (DicDataEnum.VOLTAGE.getCode().equals(type) || DicDataEnum.HP.getCode().equals(type)) {
// 统计数据类型电压、谐波中等时间3分钟超时
return currentTime >= STATISTICS_TIMEOUT;
} else {
// 实时数据类型短时间1分钟超时
return currentTime >= REALTIME_TIMEOUT;
}
}
/**
* 执行超时处理操作
* <p>记录超时日志,退出发送,发送超时结果,恢复检测状态</p>
*
* @param sourceIssue 源问题对象
*/
private void handleTimeout(SourceIssue sourceIssue) {
log.warn("超时处理 - {} 已超时,关闭连接", sourceIssue.getType());
CnSocketUtil.quitSend(param);
timeoutSend(sourceIssue);
socketResponseService.backCheckState(param);
}
/**
* 发送超时结果
* <p>为所有设备创建超时的检测结果并通过WebSocket发送给客户端</p>
*
* @param sourceIssue 源问题对象
*/
private void timeoutSend(SourceIssue sourceIssue) {
List<DevLineTestResult> devListRes = new ArrayList<>();
FormalTestManager.devList.forEach(dev -> {
DevLineTestResult devLineTestResult = new DevLineTestResult();
devLineTestResult.setDeviceId(dev.getDevId());
devLineTestResult.setDeviceName(dev.getDevName());
List<Integer> resultFlagList = new ArrayList<>();
List<PreDetection.MonitorListDTO> monitorListDTOList = dev.getMonitorList();
monitorListDTOList.forEach(i -> resultFlagList.add(3));
devLineTestResult.setChnResult(resultFlagList.toArray(new Integer[monitorListDTOList.size()]));
devListRes.add(devLineTestResult);
});
List<DevLineTestResult> devListRes = FormalTestManager.devList.stream()
.map(this::createTimeoutResult)
.collect(Collectors.toList());
WebSocketVO<List<DevLineTestResult>> socketVO = new WebSocketVO<>();
socketVO.setRequestId(sourceIssue.getType() + CnSocketUtil.END_TAG);
socketVO.setOperateCode(sourceIssue.getType());
socketVO.setData(devListRes);
WebServiceManager.sendMsg(param.getUserPageId(), JSON.toJSONString(socketVO));
}
/**
* 为指定设备创建超时的检测结果
* <p>将所有监测点的结果设置为超时标志值</p>
*
* @param dev 设备对象
* @return 设备检测结果
*/
private DevLineTestResult createTimeoutResult(PreDetection dev) {
DevLineTestResult devLineTestResult = new DevLineTestResult();
devLineTestResult.setDeviceId(dev.getDevId());
devLineTestResult.setDeviceName(dev.getDevName());
Integer[] resultFlags = dev.getMonitorList().stream()
.map(monitor -> ResultEnum.NETWORK_TIMEOUT)
.toArray(Integer[]::new);
devLineTestResult.setChnResult(resultFlags);
return devLineTestResult;
}
}

View File

@@ -5,135 +5,136 @@ import com.njcn.gather.detection.pojo.enums.SourceOperateCodeEnum;
import com.njcn.gather.detection.pojo.param.PreDetectionParam;
import com.njcn.gather.detection.util.socket.CnSocketUtil;
import com.njcn.gather.detection.util.socket.SocketManager;
import com.njcn.gather.detection.util.socket.websocket.WebServiceManager;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.timeout.IdleState;
import io.netty.handler.timeout.IdleStateEvent;
import io.netty.handler.timeout.TimeoutException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import java.io.IOException;
import java.net.ConnectException;
import java.net.ProtocolException;
import cn.hutool.core.util.StrUtil;
/**
* @Description: 源客户端业务处理(示例)
* @Author: wr
* @Date: 2024/12/10 14:16
* 源设备Netty客户端通道处理器
* 负责处理程控源设备的Socket通信
*
* @author wr
* @since 2024/12/10
*/
@Slf4j
@RequiredArgsConstructor
public class NettySourceClientHandler extends SimpleChannelInboundHandler<String> {
/** 检测参数对象包含用户页面ID等信息 */
private final PreDetectionParam webUser;
private final String sourceTag = "_Source";
/** 源设备响应处理服务 */
private final SocketSourceResponseService sourceResponseService;
/**
* 通道进行连接时推送消息
*
* @param ctx
* 通道激活回调将通道注册到SocketManager
*/
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
System.out.println("客户端通道已建立" + ctx.channel().id());
SocketManager.addUser(webUser.getUserPageId() + sourceTag, ctx.channel());
}
/**
* 处理服务端消息信息
*/
@Override
protected void channelRead0(ChannelHandlerContext ctx, String msg) throws InterruptedException {
System.out.println("source接收server端数据>>>>>>" + msg);
try {
sourceResponseService.deal(webUser, msg);
} catch (Exception e) {
e.printStackTrace();
CnSocketUtil.quitSend(webUser);
// 验证webUser参数有效性
if (webUser == null) {
log.warn("源设备客户端通道已建立但webUser为空, channelId: {}", ctx.channel().id());
return;
}
String userId = webUser.getUserPageId();
log.info("源设备客户端通道已建立, channelId: {}, userId: {}", ctx.channel().id(), userId);
// 将通道注册到Socket管理器便于后续消息推送
if (StrUtil.isNotBlank(userId)) {
SocketManager.addUser(userId + CnSocketUtil.SOURCE_TAG, ctx.channel());
} else {
log.warn("源设备userId为空或空白跳过通道注册, channelId: {}", ctx.channel().id());
}
}
/**
* 通道断线时,支持重连
*
* @param ctx
* 通道断开回调,清理资源
*/
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
System.out.println("源通讯客户端断线");
String userId = webUser != null ? webUser.getUserPageId() : "unknown";
log.warn("源通讯客户端断线, channelId: {}, userId: {}", ctx.channel().id(), userId);
// 关闭通道连接
ctx.close();
SocketManager.removeUser(webUser.getUserPageId() + sourceTag);
// System.out.println("断线了......" + ctx.channel());
// ctx.channel().eventLoop().schedule(() -> {
// System.out.println("断线重连......");
// //重连
// NettyClient.connect();
// }, 3L, TimeUnit.SECONDS);
}
/**
* 用户事件的回调方法(自定义事件用于心跳机制)
*
* @param ctx
* @param evt
*/
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) {
//如果是空闲状态事件
if (evt instanceof IdleStateEvent) {
if (((IdleStateEvent) evt).state() == IdleState.WRITER_IDLE) {
//发送ping 保持心跳链接
/* SocketMsg<String> msg = new SocketMsg<>();
msg.setRequestId("yxt");
msg.setOperateCode(SourceOperateCodeEnum.HEARTBEAT.getValue());
msg.setData("");
ctx.writeAndFlush(JSON.toJSONString(msg)+"\n");*/
}
} else {
//防止堆栈溢出
//userEventTriggered(ctx, evt);
// 从Socket管理器中移除用户通道映射
if (webUser != null && StrUtil.isNotBlank(userId)) {
SocketManager.removeUser(userId + CnSocketUtil.SOURCE_TAG);
}
}
/**
* 处理源设备响应消息
*/
@Override
protected void channelRead0(ChannelHandlerContext ctx, String msg) throws InterruptedException {
// 验证用户参数
if (webUser == null) {
log.warn("源设备消息处理失败: webUser为空, message: {}", msg);
return;
}
String userId = webUser.getUserPageId();
log.debug("源设备接收服务端数据, userId: {}, message: {}", userId, msg);
try {
// 委托给专门的响应处理服务处理业务逻辑
sourceResponseService.deal(webUser, msg);
} catch (Exception e) {
log.error("源设备消息处理异常, userId: {}, message: {}", userId, msg, e);
// 发生异常时退出发送,避免后续问题
CnSocketUtil.quitSend(webUser);
}
}
@Override
public void handlerAdded(ChannelHandlerContext ctx) {
System.out.println("有通道准备接入" + ctx.channel().id());
// 记录处理器添加事件,用于调试
String userId = webUser != null ? webUser.getUserPageId() : "unknown";
log.debug("源设备通道准备接入, channelId: {}, userId: {}", ctx.channel().id(), userId);
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
// 处理异常,例如记录日志、关闭连接等
System.out.println("捕获到源异常。。。。。。。");
cause.printStackTrace();
// 根据异常类型进行不同的处理
String userId = webUser != null ? webUser.getUserPageId() : "unknown";
String channelId = ctx.channel().id().toString();
// 根据异常类型进行分类处理和日志记录
if (cause instanceof ConnectException) {
// 处理连接异常,例如重试连接或记录特定的连接错误信息
System.out.println("连接socket服务端异常");
// 连接异常:网络连接失败
log.error("连接源设备Socket服务端异常, channelId: {}, userId: {}", channelId, userId, cause);
} else if (cause instanceof IOException) {
// 处理I/O异常例如读写错误
CnSocketUtil.sendToWebSocket(webUser.getUserPageId(), SourceOperateCodeEnum.SERVER_ERROR.getValue(), SourceOperateCodeEnum.SERVER_ERROR.getValue(), SourceOperateCodeEnum.SERVER_ERROR.getMsg(), null);
// 例如可以记录更详细的I/O错误信息
// IO异常数据传输错误需通知前端
log.error("源设备IO异常, channelId: {}, userId: {}", channelId, userId, cause);
// 向前端发送服务器错误消息
if (StrUtil.isNotBlank(userId) && !"unknown".equals(userId)) {
WebServiceManager.sendDetectionErrorMessage(userId, SourceOperateCodeEnum.SERVER_ERROR);
}
} else if (cause instanceof TimeoutException) {
// 处理超时异常
System.out.println("TimeoutException caught: Operation timed out.");
// 可以根据业务逻辑决定是否重试或记录超时信息
// 超时异常:通信响应超时
log.warn("源设备通信超时, channelId: {}, userId: {}", channelId, userId, cause);
} else if (cause instanceof ProtocolException) {
// 处理协议异常,例如消息格式不正确
System.out.println("ProtocolException caught: Invalid protocol message.");
// 可以记录协议错误信息或向客户端发送错误响应
// 协议异常:数据格式不符合协议规范
log.error("源设备协议异常, channelId: {}, userId: {}", channelId, userId, cause);
} else {
// 处理其他类型的异常
System.out.println("Unknown exception caught: " + cause.getMessage());
// 可以记录未知异常信息
// 其他未知异常
log.error("源设备未知异常, channelId: {}, userId: {}, message: {}", channelId, userId, cause.getMessage(), cause);
}
// 发生异常时关闭通道
ctx.close();
}

View File

@@ -0,0 +1,169 @@
package com.njcn.gather.detection.util.socket.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
/**
* Socket连接配置管理类
* 定义哪些requestId需要建立通道连接以及IP/PORT配置
*
* @Author: hongawen
* @Date: 2024/12/10
*/
@Component
@ConfigurationProperties(prefix = "socket")
public class SocketConnectionConfig {
/**
* 程控源设备配置
*/
private SourceConfig source = new SourceConfig();
/**
* 被检设备配置
*/
private DeviceConfig device = new DeviceConfig();
@Data
public static class SourceConfig {
/**
* 程控源IP地址
*/
private String ip;
/**
* 程控源端口号
*/
private Integer port;
}
@Data
public static class DeviceConfig {
/**
* 被检设备IP地址
*/
private String ip;
/**
* 被检设备端口号
*/
private Integer port;
}
/**
* 获取程控源配置
*/
public SourceConfig getSource() {
return source;
}
/**
* 获取被检设备配置
*/
public DeviceConfig getDevice() {
return device;
}
/**
* 需要建立程控源通道的requestId集合
* 这些requestId在发送消息时如果程控源通道不存在会自动建立连接
*/
private static final Set<String> SOURCE_CONNECTION_REQUEST_IDS = new HashSet<>(Arrays.asList(
// 源通讯检测
"yjc_ytxjy"
// 可以根据实际业务需求添加更多requestId
));
/**
* 需要建立被检设备通道的requestId集合
* 这些requestId在发送消息时如果被检设备通道不存在会自动建立连接
*/
private static final Set<String> DEVICE_CONNECTION_REQUEST_IDS = new HashSet<>(Arrays.asList(
// 连接建立
"yjc_sbtxjy",
// ftp文件传送指令
"FTP_SEND$01"
// 可以根据实际业务需求添加更多requestId
));
/**
* 检查指定的requestId是否需要建立程控源连接
*
* @param requestId 请求ID
* @return boolean true:需要建立连接, false:不需要建立连接
*/
public static boolean needsSourceConnection(String requestId) {
return SOURCE_CONNECTION_REQUEST_IDS.contains(requestId);
}
/**
* 检查指定的requestId是否需要建立被检设备连接
*
* @param requestId 请求ID
* @return boolean true:需要建立连接, false:不需要建立连接
*/
public static boolean needsDeviceConnection(String requestId) {
return DEVICE_CONNECTION_REQUEST_IDS.contains(requestId);
}
/**
* 添加需要建立程控源连接的requestId
* 支持运行时动态添加
*
* @param requestId 请求ID
*/
public static void addSourceConnectionRequestId(String requestId) {
SOURCE_CONNECTION_REQUEST_IDS.add(requestId);
}
/**
* 添加需要建立被检设备连接的requestId
* 支持运行时动态添加
*
* @param requestId 请求ID
*/
public static void addDeviceConnectionRequestId(String requestId) {
DEVICE_CONNECTION_REQUEST_IDS.add(requestId);
}
/**
* 移除程控源连接requestId
*
* @param requestId 请求ID
*/
public static void removeSourceConnectionRequestId(String requestId) {
SOURCE_CONNECTION_REQUEST_IDS.remove(requestId);
}
/**
* 移除被检设备连接requestId
*
* @param requestId 请求ID
*/
public static void removeDeviceConnectionRequestId(String requestId) {
DEVICE_CONNECTION_REQUEST_IDS.remove(requestId);
}
/**
* 获取所有需要建立程控源连接的requestId集合只读
*
* @return Set<String> requestId集合
*/
public static Set<String> getSourceConnectionRequestIds() {
return new HashSet<>(SOURCE_CONNECTION_REQUEST_IDS);
}
/**
* 获取所有需要建立被检设备连接的requestId集合只读
*
* @return Set<String> requestId集合
*/
public static Set<String> getDeviceConnectionRequestIds() {
return new HashSet<>(DEVICE_CONNECTION_REQUEST_IDS);
}
}

View File

@@ -60,7 +60,7 @@ public class NettyServer {
ch.pipeline()
//空闲状态的handler
// 添加LineBasedFrameDecoder来按行分割数据
.addLast(new LineBasedFrameDecoder(10240))
.addLast(new LineBasedFrameDecoder(10240*2))
.addLast(new StringDecoder(CharsetUtil.UTF_8))
.addLast(new StringEncoder(CharsetUtil.UTF_8))
.addLast(new DevNettyServerHandler());
@@ -107,7 +107,7 @@ public class NettyServer {
ch.pipeline()
//空闲状态的handler
// 添加LineBasedFrameDecoder来按行分割数据
.addLast(new LineBasedFrameDecoder(10240))
.addLast(new LineBasedFrameDecoder(10240*2))
.addLast(new StringDecoder(CharsetUtil.UTF_8))
.addLast(new StringEncoder(CharsetUtil.UTF_8))
.addLast(new SourceNettyServerHandler());

View File

@@ -1,169 +0,0 @@
package com.njcn.gather.detection.util.socket.web;
import cn.hutool.core.util.ObjectUtil;
import com.njcn.gather.detection.pojo.param.PreDetectionParam;
import com.njcn.gather.detection.util.socket.CnSocketUtil;
import com.njcn.gather.detection.util.socket.WebServiceManager;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http.FullHttpRequest;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import io.netty.handler.timeout.IdleStateEvent;
import lombok.extern.slf4j.Slf4j;
/**
* @Description: 泛型 代表的是处理数据的单位
* TextWebSocketFrame : 文本信息帧
* @Author: wr
* @Date: 2024/12/10 13:56
*/
@Slf4j
public class WebSocketHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {
private int times;
private final static String QUESTION_MARK = "?";
private final static String EQUAL_TO = "=";
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
System.out.println("webSocket服务端通道已建立" + ctx.channel().id());
super.channelActive(ctx);
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
//首次连接是FullHttpRequest把用户id和对应的channel对象存储起来
if (null != msg && msg instanceof FullHttpRequest) {
FullHttpRequest request = (FullHttpRequest) msg;
String uri = request.uri();
String userId = getUrlParams(uri);
WebServiceManager.addUser(userId, ctx.channel());
log.info("登录的用户id是{}", userId);
//如果url包含参数需要处理
if (uri.contains(QUESTION_MARK)) {
String newUri = uri.substring(0, uri.indexOf(QUESTION_MARK));
request.setUri(newUri);
}
} else if (msg instanceof TextWebSocketFrame) {
//正常的TEXT消息类型
TextWebSocketFrame frame = (TextWebSocketFrame) msg;
//log.info("webSocket服务器收到客户端心跳信息{}", frame.text());
if ("alive".equals(frame.text())) {
//System.out.println("webSocket心跳收到时间………………………………………………………………"+LocalDateTime.now());
times = 0;
TextWebSocketFrame wd = new TextWebSocketFrame("over");
ctx.channel().writeAndFlush(wd);
}
}
super.channelRead(ctx, msg);
}
/**
* 根据用户地址获取用户名 ws://127.0.0.1:7777/hello?name=aa
*
* @param url
* @return
*/
private static String getUrlParams(String url) {
if (!url.contains(EQUAL_TO)) {
return null;
}
String userId = url.substring(url.indexOf(EQUAL_TO) + 1);
return userId;
}
@Override
protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) {
/* System.out.println("服务端消息 == " + msg.text());
if(msg.text().equals("下发指令")){
*//**
* 处理对应消息
* 1.先下发所要操作的流程信息
* 2.组装对应的入参信息
* 3.再用socket信息返回结束
*//*
//NettyClient.socketClient(msg.text(),new NettySourceClientHandler());
}*/
//可以直接调用text 拿到文本信息帧中的信息
/* Channel channel = ctx.channel();
TextWebSocketFrame resp = new TextWebSocketFrame(msg.text());
channel.writeAndFlush(resp);*/
}
@Override
public void handlerAdded(ChannelHandlerContext ctx) {
//WebServiceManager.addUser(userId, ctx.channel());
System.out.println("webSocket有新的连接接入:" + ctx);
}
@Override
public void handlerRemoved(ChannelHandlerContext ctx) {
// 假设用户 ID 是从某个地方获取的,这里简单示例为 "userId"
System.out.println("weoSocket客户端退出: " + ctx.channel().id());
WebServiceManager.removeChannel(ctx.channel().id().toString());
}
@Override
public void channelInactive(ChannelHandlerContext ctx) {
System.out.println("weoSocket断线" + ctx.channel().id());
ctx.close();
PreDetectionParam preDetectionParam = WebServiceManager.getPreDetectionParam();
if (ObjectUtil.isNotNull(preDetectionParam)) {
CnSocketUtil.quitSendSource(preDetectionParam); // 能否在这里关闭源socket连接
CnSocketUtil.quitSend(preDetectionParam);
} else {
preDetectionParam = new PreDetectionParam();
preDetectionParam.setUserPageId("cdf");
CnSocketUtil.quitSend(preDetectionParam);
}
}
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) {
IdleStateEvent event = (IdleStateEvent) evt;
String eventDesc = null;
switch (event.state()) {
case READER_IDLE:
eventDesc = "读空闲";
System.out.println("c端心跳检测发生超时事件--" + eventDesc);
times++;
if (times > 3) {
System.out.println("c端心跳检测空闲次数超过三次 关闭连接");
ctx.channel().close();
WebServiceManager.removeChannel(ctx.channel().id().toString());
}
break;
case WRITER_IDLE:
eventDesc = "写空闲";
break;
case ALL_IDLE:
eventDesc = "读写空闲";
break;
}
//super.userEventTriggered(ctx, evt);
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
cause.printStackTrace();
ctx.close();
}
}

View File

@@ -1,63 +0,0 @@
package com.njcn.gather.detection.util.socket.web;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.socket.SocketChannel;
import io.netty.handler.codec.http.HttpObjectAggregator;
import io.netty.handler.codec.http.HttpResponseDecoder;
import io.netty.handler.codec.http.HttpServerCodec;
import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler;
import io.netty.handler.stream.ChunkedWriteHandler;
import io.netty.handler.timeout.IdleStateHandler;
import java.util.concurrent.TimeUnit;
/**
* @Description: webSocket服务端自定义配置
* @Author: wr
* @Date: 2024/12/10 14:20
*/
public class WebSocketInitializer extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
//设置心跳机制
// ch.pipeline().addLast(new IdleStateHandler(5, 0, 0));
//增加编解码器 的另一种方式
pipeline.addLast(new HttpServerCodec());
pipeline.addLast(new HttpResponseDecoder());
//块方式写的处理器 适合处理大数据
pipeline.addLast(new ChunkedWriteHandler());
//聚合
pipeline.addLast(new HttpObjectAggregator(512 * 1024));
/*
* 这个时候 我们需要声明我们使用的是 websocket 协议
* netty为websocket也准备了对应处理器 设置的是访问路径
* 这个时候我们只需要访问 ws://127.0.0.1:7777/hello 就可以了
* 这个handler是将http协议升级为websocket 并且使用 101 作为响应码
* */
pipeline.addLast(new IdleStateHandler(13, 0, 0, TimeUnit.SECONDS));
pipeline.addLast(new WebSocketHandler());
pipeline.addLast(new WebSocketServerProtocolHandler("/hello"));
pipeline.addLast(new ChannelInboundHandlerAdapter() {
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
// 处理异常,例如记录日志、关闭连接等
System.out.println("进入异常++++++++++++++++++");
cause.printStackTrace();
ctx.close();
}
});
}
}

View File

@@ -1,94 +0,0 @@
package com.njcn.gather.detection.util.socket.web;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.logging.LoggingHandler;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
/**
* @Description: websocket服务端
* @Author: wr
* @Date: 2024/12/10 13:59
*/
@Component
@RequiredArgsConstructor
public class WebSocketService {
/**
* 端口号
*/
@Value("${webSocket.port:7777}")
int port;
EventLoopGroup bossGroup;
EventLoopGroup workerGroup;
@PostConstruct
public void start() {
new Thread(() -> {
//可以自定义线程的数量
bossGroup = new NioEventLoopGroup();
// 默认创建的线程数量 = CPU 处理器数量 *2
workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.handler(new LoggingHandler())
//当前连接被阻塞的时候BACKLOG代表的事 阻塞队列的长度
.option(ChannelOption.SO_BACKLOG, 128)
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000)
//设置连接保持为活动状态
.childOption(ChannelOption.SO_KEEPALIVE, true)
.childHandler(new WebSocketInitializer());
ChannelFuture future = serverBootstrap.bind(port).sync();
future.addListener(f -> {
if (future.isSuccess()) {
System.out.println("webSocket服务启动成功");
} else {
System.out.println("webSocket服务启动失败");
}
});
future.channel().closeFuture().sync();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}).start();
}
/**
* 释放资源
*/
@PreDestroy
public void destroy() throws InterruptedException {
if (bossGroup != null) {
bossGroup.shutdownGracefully().sync();
}
if (workerGroup != null) {
workerGroup.shutdownGracefully().sync();
}
System.out.println("webSocket销毁---------------");
}
}

View File

@@ -0,0 +1,316 @@
package com.njcn.gather.detection.util.socket.websocket;
import com.alibaba.fastjson.JSON;
import com.njcn.gather.detection.pojo.param.PreDetectionParam;
import com.njcn.gather.detection.pojo.vo.WebSocketVO;
import com.njcn.gather.detection.pojo.enums.SourceOperateCodeEnum;
import io.netty.channel.Channel;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import lombok.extern.slf4j.Slf4j;
import java.time.LocalDateTime;
import java.util.Iterator;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
/**
* WebSocket会话管理器
*
* <p>负责管理电能质量检测系统中的WebSocket连接会话和检测参数主要功能包括</p>
* <ul>
* <li>WebSocket连接会话的添加、删除和管理</li>
* <li>向指定用户推送实时消息(文本消息和结构化消息)</li>
* <li>全局检测参数的存储和管理</li>
* </ul>
*
* <p><b>线程安全性:</b></p>
* 使用ConcurrentHashMap确保在高并发环境下的线程安全。
*
* <p><b>使用场景:</b></p>
* <ul>
* <li>检测进度实时推送</li>
* <li>检测结果数据推送</li>
* <li>设备状态变更通知</li>
* <li>异常信息推送</li>
* </ul>
*
* <p><b>消息推送方式:</b></p>
* <ul>
* <li>{@link #sendMsg(String, String)} - 发送纯文本消息</li>
* <li>{@link #sendMessage(String, WebSocketVO)} - 发送结构化JSON消息</li>
* </ul>
*
* @author wr
* @version 1.0
* @date 2024/12/11 13:04
* @see com.njcn.gather.detection.util.socket.websocket.WebSocketHandler WebSocket处理器
* @see com.njcn.gather.detection.pojo.vo.WebSocketVO WebSocket消息对象
* @since 检测系统 v2.3.12
*/
@Slf4j
public class WebServiceManager {
/**
* WebSocket用户会话存储
* key: 用户ID, value: WebSocket连接通道
*/
private static final Map<String, Channel> userSessions = new ConcurrentHashMap<>();
/**
* 检测参数存储
* key: 用户ID(userPageId), value: 检测参数对象
* 支持多用户并发检测,每个用户的检测参数独立存储
*/
private static final Map<String, PreDetectionParam> preDetectionParamMap = new ConcurrentHashMap<>();
/**
* 添加用户WebSocket会话
*
* @param userId 用户ID不能为null
* @param channel WebSocket连接通道不能为null
*/
public static void addUser(String userId, Channel channel) {
userSessions.put(userId, channel);
}
/**
* 根据用户ID移除会话推荐使用
* 时间复杂度O(1)
*
* @param userId 用户ID
* @return 被移除的Channel如果不存在则返回null
*/
public static Channel removeByUserId(String userId) {
return userSessions.remove(userId);
}
/**
* 根据channelId移除会话兼容老版本
* 时间复杂度O(n)建议使用removeByUserId替代
*
* @param channelId 通道ID
* @deprecated 建议使用 {@link #removeByUserId(String)} 替代
*/
@Deprecated
public static void removeChannel(String channelId) {
// 遍历并删除
Iterator<Map.Entry<String, Channel>> iterator = userSessions.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry<String, Channel> entry = iterator.next();
if (entry.getValue().id().toString().equals(channelId)) {
iterator.remove();
break; // 找到后立即退出,避免继续遍历
}
}
}
/**
* 发送纯文本消息给指定用户
*
* @param userId 目标用户ID
* @param msg 要发送的文本消息
*/
public static void sendMsg(String userId, String msg) {
Channel channel = userSessions.get(userId);
if (Objects.nonNull(channel) && channel.isActive()) {
TextWebSocketFrame frame = new TextWebSocketFrame(msg);
channel.writeAndFlush(frame);
} else {
log.error("WebSocket推送消息失败用户连接已断开时间: {}, userId: {}", LocalDateTime.now(), userId);
}
}
/**
* 发送结构化消息给指定用户
* 消息会被序列化为JSON格式后发送
*
* @param userId 目标用户ID
* @param webSocketVO 要发送的结构化消息对象
*/
public static void sendMessage(String userId, WebSocketVO<Object> webSocketVO) {
Channel channel = userSessions.get(userId);
if (Objects.nonNull(channel) && channel.isActive()) {
TextWebSocketFrame frame = new TextWebSocketFrame(JSON.toJSONString(webSocketVO));
channel.writeAndFlush(frame);
} else {
log.error("WebSocket推送结构化消息失败用户连接已断开时间: {}, userId: {}", LocalDateTime.now(), userId);
}
}
/**
* 存储检测参数基于用户ID
* 支持多用户并发检测,每个用户的检测参数独立存储
*
* @param userId 用户ID登录名
* @param preDetectionParam 检测参数对象
* @throws IllegalArgumentException 当userId或检测参数为空时抛出
*/
public static void addPreDetectionParam(String userId, PreDetectionParam preDetectionParam) {
if (userId == null || userId.trim().isEmpty()) {
throw new IllegalArgumentException("用户ID不能为空");
}
if (preDetectionParam == null) {
throw new IllegalArgumentException("检测参数不能为空");
}
preDetectionParamMap.put(userId, preDetectionParam);
}
/**
* 获取指定用户的检测参数
*
* @param userId 用户ID
* @return 检测参数对象如果不存在则返回null
*/
public static PreDetectionParam getPreDetectionParam(String userId) {
return preDetectionParamMap.get(userId);
}
/**
* 获取当前检测参数(兼容老版本)
* 注意:该方法已废弃,建议使用 {@link #getPreDetectionParam(String)}
*
* @return 检测参数对象如果不存在则返回null
* @deprecated 多用户并发场景下该方法不安全,请使用 {@link #getPreDetectionParam(String)}
*/
@Deprecated
public static PreDetectionParam getPreDetectionParam() {
if (preDetectionParamMap.size() == 1) {
return preDetectionParamMap.values().iterator().next();
}
log.warn("存在多个检测参数,无法确定返回哪个,当前参数数量: {}", preDetectionParamMap.size());
return null;
}
/**
* 移除指定用户的检测参数
*
* @param userId 用户ID
* @return 被移除的检测参数如果不存在则返回null
*/
public static PreDetectionParam removePreDetectionParam(String userId) {
return preDetectionParamMap.remove(userId);
}
/**
* 清空所有检测参数
*/
public static void removeAllPreDetectionParam() {
preDetectionParamMap.clear();
}
/**
* 清空所有检测参数(兼容老版本)
*
* @deprecated 建议使用 {@link #removeAllPreDetectionParam()} 或 {@link #removePreDetectionParam(String)}
*/
@Deprecated
public static void removePreDetectionParam() {
removeAllPreDetectionParam();
}
// ================================ 实用功能方法 ================================
/**
* 获取当前在线用户数量
*
* @return 在线用户数量
*/
public static int getOnlineUserCount() {
return userSessions.size();
}
/**
* 检查指定用户是否在线
*
* @param userId 用户ID
* @return true如果用户在线且连接活跃否则返回false
*/
public static boolean isUserOnline(String userId) {
Channel channel = userSessions.get(userId);
return Objects.nonNull(channel) && channel.isActive();
}
/**
* 获取所有在线用户ID集合
*
* @return 在线用户ID集合的快照
*/
public static java.util.Set<String> getOnlineUserIds() {
return new java.util.HashSet<>(userSessions.keySet());
}
// ================================ 检测消息推送方法 ================================
/**
* 发送检测相关消息给指定用户
* <p>用于推送检测状态、进度、结果等结构化消息</p>
*
* @param userId 目标用户ID
* @param requestId 请求ID用于标识消息类型和流程
* @param operateCode 操作代码,标识具体的操作类型
* @param data 数据载荷,可以是任意类型的数据
* @param desc 描述信息
* @since v2.3.12 重构版本
*/
public static void sendDetectionMessage(String userId, String requestId, String operateCode, Object data, String desc) {
WebSocketVO<Object> webSocketVO = new WebSocketVO<>();
webSocketVO.setRequestId(requestId);
webSocketVO.setOperateCode(operateCode);
webSocketVO.setData(data);
webSocketVO.setDesc(desc);
sendMessage(userId, webSocketVO);
}
/**
* 发送未知错误消息给指定用户
* <p>用于处理系统无法识别的操作或未知异常情况</p>
*
* @param userId 目标用户ID
* @since v2.3.12 重构版本
*/
public static void sendUnknownErrorMessage(String userId) {
WebSocketVO<Object> webSocketVO = new WebSocketVO<>();
webSocketVO.setRequestId(SourceOperateCodeEnum.UNKNOWN_OPERATE.getValue());
webSocketVO.setData(SourceOperateCodeEnum.UNKNOWN_OPERATE.getMsg());
webSocketVO.setOperateCode(SourceOperateCodeEnum.UNKNOWN_OPERATE.getMsg());
sendMessage(userId, webSocketVO);
}
/**
* 发送检测错误消息给指定用户
* <p>用于推送特定类型的检测错误信息</p>
*
* @param userId 目标用户ID
* @param errorType 错误类型枚举
* @since v2.3.12 重构版本
*/
public static void sendDetectionErrorMessage(String userId, SourceOperateCodeEnum errorType) {
WebSocketVO<Object> webSocketVO = new WebSocketVO<>();
webSocketVO.setRequestId(errorType.getValue());
webSocketVO.setData(errorType.getMsg());
webSocketVO.setOperateCode(errorType.getValue());
sendMessage(userId, webSocketVO);
}
/**
* 发送检测错误消息给指定用户
* <p>用于推送特定类型的检测错误信息</p>
*
* @param userId 目标用户ID
* @param requestId 请求ID
* @param errorType 错误类型枚举
* @since v2.3.12 重构版本
*/
public static void sendDetectionErrorMessage(String userId, String requestId, SourceOperateCodeEnum errorType) {
WebSocketVO<Object> webSocketVO = new WebSocketVO<>();
webSocketVO.setRequestId(requestId);
webSocketVO.setData(errorType.getMsg());
webSocketVO.setOperateCode(errorType.getValue());
sendMessage(userId, webSocketVO);
}
}

View File

@@ -0,0 +1,49 @@
package com.njcn.gather.detection.util.socket.websocket;
/**
* WebSocket常量管理类
*
* @author wr
* @date 2024/12/10
*/
public final class WebSocketConstants {
/**
* URL参数分隔符
*/
public static final String QUESTION_MARK = "?";
/**
* URL参数等号分隔符
*/
public static final String EQUAL_TO = "=";
/**
* 客户端心跳消息
*/
public static final String HEARTBEAT_PING = "alive";
/**
* 服务端心跳响应
*/
public static final String HEARTBEAT_PONG = "over";
/**
* 心跳超时最大次数
*/
public static final int MAX_HEARTBEAT_MISS_COUNT = 3;
/**
* WebSocket握手失败状态码
*/
public static final int HANDSHAKE_FAILED_STATUS = 4000;
/**
* WebSocket握手失败原因
*/
public static final String HANDSHAKE_FAILED_REASON = "Missing required userId parameter";
private WebSocketConstants() {
// 私有构造函数,防止实例化
}
}

View File

@@ -0,0 +1,402 @@
package com.njcn.gather.detection.util.socket.websocket;
import cn.hutool.core.util.ObjectUtil;
import com.njcn.gather.detection.pojo.enums.SourceOperateCodeEnum;
import com.njcn.gather.detection.pojo.param.PreDetectionParam;
import com.njcn.gather.detection.util.socket.CnSocketUtil;
import com.njcn.gather.detection.util.socket.FormalTestManager;
import com.njcn.gather.detection.util.socket.SocketManager;
import com.njcn.gather.device.pojo.enums.PatternEnum;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.CorruptedFrameException;
import io.netty.handler.codec.DecoderException;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import io.netty.handler.codec.http.websocketx.WebSocketHandshakeException;
import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler;
import io.netty.handler.timeout.IdleStateEvent;
import io.netty.util.AttributeKey;
import lombok.extern.slf4j.Slf4j;
import java.io.IOException;
import static com.njcn.gather.detection.util.socket.websocket.WebSocketConstants.*;
/**
* WebSocket消息处理器
*
* <p>负责处理电能质量检测系统中的WebSocket连接和消息通信主要功能包括</p>
* <ul>
* <li>WebSocket连接的建立、维护和断开</li>
* <li>用户身份验证和会话管理</li>
* <li>心跳检测和连接保活</li>
* <li>检测状态和结果的实时推送</li>
* <li>异常处理和资源清理</li>
* </ul>
*
* <p><b>通信协议:</b></p>
* <pre>
* 连接URL: ws://host:port/path?name=userId
* 心跳消息: "alive" -> "over"
* 业务消息: JSON格式的检测数据和状态信息
* </pre>
*
* <p><b>安全策略:</b></p>
* <ul>
* <li>连接时必须提供有效的userId参数否则拒绝连接</li>
* <li>支持心跳超时检测超时3次自动断开连接</li>
* <li>连接断开时自动清理相关Socket资源</li>
* </ul>
*
* <p><b>使用场景:</b></p>
* 主要用于前端实时接收检测进度、检测结果、设备状态等信息的推送,
* 配合detection模块的ResponseService类实现完整的实时通信链路。
*
* @author wr
* @version 1.0
* @date 2024/12/10 13:56
* @see WebServiceManager 会话管理器
* @see WebSocketConstants 常量定义
* @see com.njcn.gather.detection.handler.SocketDevResponseService 设备响应处理
* @see com.njcn.gather.detection.handler.SocketSourceResponseService 源响应处理
* @since 检测系统 v2.3.12
*/
@Slf4j
public class WebSocketHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {
// ================================ 字段定义 ================================
/**
* 心跳超时计数器
*/
private int times;
/**
* 当前WebSocket连接对应的用户ID
* 在首次HTTP握手时从URL参数中提取并存储
* 用于后续的Socket连接管理和资源清理
*/
private String userId;
/**
* 心跳响应内容常量
* 注意不能预创建TextWebSocketFrame对象因为ByteBuf状态会改变
*/
private static final String HEARTBEAT_RESPONSE_TEXT = HEARTBEAT_PONG;
// ================================ Netty生命周期方法 ================================
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
log.info("webSocket服务端通道已建立channelId: {}", ctx.channel().id());
super.channelActive(ctx);
}
// HTTP握手处理已移至WebSocketPreprocessor这里只处理WebSocket帧
@Override
protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) {
handleWebSocketMessage(ctx, msg);
}
@Override
public void handlerAdded(ChannelHandlerContext ctx) {
log.info("webSocket有新的连接接入channelId: {}", ctx.channel().id());
}
@Override
public void handlerRemoved(ChannelHandlerContext ctx) {
log.info("webSocket客户端退出channelId: {}, userId: {}", ctx.channel().id(), this.userId);
if (this.userId != null) {
WebServiceManager.removeByUserId(this.userId);
} else {
// 备用方案如果userId为空使用传统方法
WebServiceManager.removeChannel(ctx.channel().id().toString());
}
}
@Override
public void channelInactive(ChannelHandlerContext ctx) {
log.info("webSocket连接断线channelId: {}, userId: {}", ctx.channel().id(), this.userId);
// 确保通道关闭
if (ctx.channel() != null && ctx.channel().isActive()) {
ctx.close();
}
// 使用Handler实例中保存的userId进行资源清理
cleanupSocketResources(this.userId);
}
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
// 处理WebSocket握手完成事件
if (evt instanceof WebSocketServerProtocolHandler.HandshakeComplete) {
WebSocketServerProtocolHandler.HandshakeComplete handshakeComplete =
(WebSocketServerProtocolHandler.HandshakeComplete) evt;
// 从Channel属性获取userId由WebSocketPreprocessor设置
this.userId = ctx.channel().attr(AttributeKey.<String>valueOf("userId")).get();
log.info("WebSocket协议升级完成userId: {}, channelId: {}, requestUri: {}",
this.userId, ctx.channel().id(), handshakeComplete.requestUri());
// 握手完成后建立用户会话
if (this.userId != null) {
WebServiceManager.addUser(this.userId, ctx.channel());
log.info("WebSocket用户会话已建立userId: {}, channelId: {}", this.userId, ctx.channel().id());
}
// 发送连接成功消息给前端
sendConnectionSuccessMessage(ctx);
return;
}
// 处理心跳超时事件
if (evt instanceof IdleStateEvent) {
IdleStateEvent event = (IdleStateEvent) evt;
String eventDesc;
switch (event.state()) {
case READER_IDLE:
eventDesc = "读空闲";
log.warn("客户端心跳检测发生超时事件: {}channelId: {}", eventDesc, ctx.channel().id());
times++;
if (times > MAX_HEARTBEAT_MISS_COUNT) {
log.error("客户端心跳检测空闲次数超过{}次关闭连接channelId: {}, userId: {}", MAX_HEARTBEAT_MISS_COUNT, ctx.channel().id(), this.userId);
ctx.channel().close();
if (this.userId != null) {
WebServiceManager.removeByUserId(this.userId);
} else {
WebServiceManager.removeChannel(ctx.channel().id().toString());
}
}
break;
case WRITER_IDLE:
log.debug("webSocket写空闲事件channelId: {}", ctx.channel().id());
break;
case ALL_IDLE:
log.debug("webSocket读写空闲事件channelId: {}", ctx.channel().id());
break;
default:
break;
}
return;
}
// 其他事件传递给父类处理
super.userEventTriggered(ctx, evt);
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
String channelId = ctx.channel().id().toString();
try {
// 1. 异常分类记录
logExceptionByType(channelId, cause);
// 2. 业务清理
cleanupOnException(ctx, cause);
// 3. 连接处理决策
handleConnectionByExceptionType(ctx, cause);
} catch (Exception e) {
// 防止异常处理本身出错
log.error("异常处理过程中发生错误强制关闭连接channelId: {}", channelId, e);
ctx.close();
}
}
// ================================ HTTP握手处理已移至WebSocketPreprocessor ================================
// ================================ WebSocket消息处理 ================================
/**
* 发送连接成功消息给前端
* WebSocket握手完成后立即调用通知前端连接建立成功
*
* @param ctx Netty通道上下文
*/
private void sendConnectionSuccessMessage(ChannelHandlerContext ctx) {
if (ctx == null || ctx.channel() == null || !ctx.channel().isActive()) {
log.warn("无法发送连接成功消息:通道不可用, userId: {}", this.userId);
return;
}
try {
// 构建连接成功消息
String welcomeMessage = String.format("{\"type\":\"connection\",\"status\":\"success\",\"message\":\"WebSocket连接建立成功\",\"userId\":\"%s\",\"timestamp\":%d}",
this.userId, System.currentTimeMillis());
TextWebSocketFrame frame = new TextWebSocketFrame(welcomeMessage);
ctx.channel().writeAndFlush(frame);
log.info("已发送连接成功消息给前端, userId: {}, channelId: {}", this.userId, ctx.channel().id());
} catch (Exception e) {
log.error("发送连接成功消息失败, userId: {}, channelId: {}", this.userId, ctx.channel().id(), e);
}
}
/**
* 处理WebSocket文本消息
* 这里是所有WebSocket文本消息的统一处理入口
*
* @param ctx Netty通道上下文
* @param frame WebSocket文本帧
*/
private void handleWebSocketMessage(ChannelHandlerContext ctx, TextWebSocketFrame frame) {
String messageText = frame.text();
// 处理心跳消息
if (HEARTBEAT_PING.equals(messageText)) {
handleHeartbeat(ctx);
} else {
// 处理业务消息
handleBusinessMessage(ctx, frame, messageText);
}
}
/**
* 处理心跳消息
* 重置超时计数器并回复心跳响应
*
* @param ctx Netty通道上下文
*/
private void handleHeartbeat(ChannelHandlerContext ctx) {
if (ctx == null || ctx.channel() == null || !ctx.channel().isActive()) {
log.warn("心跳处理失败通道不可用userId: {}", this.userId);
return;
}
log.debug("收到心跳消息userId: {}, channelId: {}", this.userId, ctx.channel().id());
// 重置心跳超时计数器
times = 0;
// 每次创建新的心跳响应帧,确保内容正确
TextWebSocketFrame heartbeatFrame = new TextWebSocketFrame(HEARTBEAT_RESPONSE_TEXT);
ctx.channel().writeAndFlush(heartbeatFrame);
log.debug("发送心跳响应userId: {}, channelId: {}", this.userId, ctx.channel().id());
}
/**
* 处理业务消息
* 可以在这里扩展具体的业务逻辑处理
*
* @param ctx Netty通道上下文
* @param frame WebSocket文本帧
* @param messageText 消息文本内容
*/
private void handleBusinessMessage(ChannelHandlerContext ctx, TextWebSocketFrame frame, String messageText) {
log.debug("收到WebSocket业务消息userId: {}, channelId: {}, message: {}",
this.userId, ctx.channel().id(), messageText);
// TODO: 根据业务需要扩展消息处理逻辑
// 例如:
// - 解析JSON消息
// - 根据消息类型分发到不同的处理器
// - 调用业务服务处理具体逻辑
}
// ================================ 异常处理 ================================
/**
* 根据异常类型记录不同级别的日志
*/
private void logExceptionByType(String channelId, Throwable cause) {
if (cause instanceof IOException) {
log.info("webSocket网络异常客户端可能异常断开channelId: {}, 异常: {}", channelId, cause.getMessage());
} else if (cause instanceof WebSocketHandshakeException) {
log.warn("webSocket握手异常channelId: {}, 异常: {}", channelId, cause.getMessage());
} else if (cause instanceof DecoderException || cause instanceof CorruptedFrameException) {
log.error("webSocket协议解码异常可能是恶意请求channelId: {}, 异常: {}", channelId, cause.getMessage(), cause);
} else if (cause instanceof IllegalArgumentException) {
log.warn("webSocket参数异常channelId: {}, 异常: {}", channelId, cause.getMessage());
} else {
log.error("webSocket未分类异常channelId: {}, 类型: {}, 异常: {}",
channelId, cause.getClass().getSimpleName(), cause.getMessage(), cause);
}
}
/**
* 异常发生时的业务清理工作
*/
private void cleanupOnException(ChannelHandlerContext ctx, Throwable cause) {
if (ctx == null || ctx.channel() == null) {
log.warn("异常处理:通道上下文为空,无法进行清理");
return;
}
String channelId = ctx.channel().id().toString();
// 清理会话
if (this.userId != null) {
WebServiceManager.removeByUserId(this.userId);
log.debug("已清理WebSocket会话userId: {}, channelId: {}", this.userId, channelId);
} else {
WebServiceManager.removeChannel(channelId);
log.debug("已清理WebSocket会话使用channelIdchannelId: {}", channelId);
}
// 清理检测相关资源
cleanupSocketResources(this.userId);
}
/**
* 根据异常类型决定连接处理策略
*/
private void handleConnectionByExceptionType(ChannelHandlerContext ctx, Throwable cause) {
String channelId = ctx.channel().id().toString();
// URL参数异常但连接本身可能正常尝试保持连接
if (cause instanceof IllegalArgumentException &&
cause.getMessage() != null && cause.getMessage().contains("URL")) {
log.info("URL参数异常尝试保持连接channelId: {}", channelId);
return;
}
// 其他情况都关闭连接
log.debug("关闭WebSocket连接channelId: {}", channelId);
ctx.close();
}
// ================================ 资源清理 ================================
/**
* 清理Socket相关资源
*
* @param userId 用户ID
*/
private void cleanupSocketResources(String userId) {
if (userId == null || userId.trim().isEmpty()) {
log.warn("userId为空无法进行Socket连接清理");
return;
}
try {
PreDetectionParam preDetectionParam = WebServiceManager.getPreDetectionParam(userId);
if (ObjectUtil.isNotNull(preDetectionParam)) {
// 使用该用户的检测参数关闭Socket连接
log.info("使用用户检测参数关闭Socket连接userId: {}", userId);
if (FormalTestManager.patternEnum.equals(PatternEnum.CONTRAST)) {
if (!FormalTestManager.isRemoveSocket) {
if (FormalTestManager.currentStep == SourceOperateCodeEnum.RECORD_WAVE_STEP1 || FormalTestManager.currentStep == SourceOperateCodeEnum.RECORD_WAVE_STEP2) {
CnSocketUtil.contrastSendquit(preDetectionParam.getUserPageId(), SourceOperateCodeEnum.QUIT_INIT_03, true);
} else if (FormalTestManager.currentStep != SourceOperateCodeEnum.QUITE) {
CnSocketUtil.contrastSendquit(preDetectionParam.getUserPageId(), SourceOperateCodeEnum.QUIT_INIT_02, true);
} else {
SocketManager.removeUser(preDetectionParam.getUserPageId() + CnSocketUtil.CONTRAST_DEV_TAG);
}
} else {
boolean channelActive = SocketManager.isChannelActive(preDetectionParam.getUserPageId() + CnSocketUtil.CONTRAST_DEV_TAG);
if (channelActive) {
SocketManager.removeUser(preDetectionParam.getUserPageId() + CnSocketUtil.CONTRAST_DEV_TAG);
}
}
} else {
CnSocketUtil.quitSendSource(preDetectionParam);
CnSocketUtil.quitSend(preDetectionParam);
}
// 清理完成后移除该用户的检测参数
WebServiceManager.removePreDetectionParam(userId);
}
} catch (Exception e) {
log.error("清理Socket连接时发生异常userId: {}", userId, e);
}
}
// ================================ URL解析工具已移至WebSocketPreprocessor ================================
}

View File

@@ -0,0 +1,184 @@
package com.njcn.gather.detection.util.socket.websocket;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.socket.SocketChannel;
import io.netty.handler.codec.http.*;
import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler;
import io.netty.handler.stream.ChunkedWriteHandler;
import io.netty.handler.timeout.IdleStateHandler;
import io.netty.util.AttributeKey;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.TimeUnit;
/**
* WebSocket服务端管道初始化器
*
* 职责:
* 1. 为每个新的WebSocket连接配置处理器链Pipeline
* 2. 按正确顺序添加各种Handler确保数据流正确处理
* 3. 配置HTTP到WebSocket的协议升级
* 4. 设置心跳检测和异常处理机制
*
* 处理流程:
* HTTP请求 → HTTP编解码 → 分块处理 → 消息聚合 → 协议升级 → 心跳检测 → 业务处理 → 异常处理
*
* @Description: webSocket服务端自定义配置
* @Author: wr
* @Date: 2024/12/10 14:20
*/
@Slf4j
public class WebSocketInitializer extends ChannelInitializer<SocketChannel> {
/**
* WebSocket访问路径
*/
private static final String WEBSOCKET_PATH = "/hello";
/**
* HTTP消息最大聚合大小512KB
* 用于WebSocket握手和消息传输
*/
private static final int MAX_CONTENT_LENGTH = 512 * 1024;
/**
* 心跳检测间隔13秒
* 13秒内没有收到客户端消息则触发空闲事件
*/
private static final int READER_IDLE_TIME_SECONDS = 13;
/**
* 为每个新连接初始化处理器管道
* 注意Handler的添加顺序非常重要决定了数据的处理流向
*
* @param ch 新建立的Socket通道
* @throws Exception 初始化过程中的异常
*/
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
// 1. HTTP协议处理器
// HttpServerCodec = HttpRequestDecoder + HttpResponseEncoder
// 负责HTTP请求解码和HTTP响应编码
pipeline.addLast("http-codec", new HttpServerCodec());
// 2. 分块写入处理器
// 用于处理大文件的分块传输,防止内存溢出
// 支持ChunkedInput如ChunkedFile、ChunkedNioFile等
pipeline.addLast("chunked-write", new ChunkedWriteHandler());
// 3. HTTP消息聚合器
// 将分片的HTTP消息重新组装成完整的FullHttpRequest或FullHttpResponse
// WebSocket握手需要完整的HTTP请求所以这个Handler必须添加
pipeline.addLast("http-aggregator", new HttpObjectAggregator(MAX_CONTENT_LENGTH));
// 4. WebSocket URL预处理器
// 在WebSocket握手之前处理URL参数验证用户ID
pipeline.addLast("websocket-preprocessor", new WebSocketPreprocessor());
// 5. WebSocket协议升级处理器
// 处理WebSocket握手将HTTP协议升级为WebSocket协议
// 只有访问指定路径(WEBSOCKET_PATH)的请求才会被升级
// 升级后会移除HTTP相关的Handler添加WebSocket相关的Handler
pipeline.addLast("websocket-protocol", new WebSocketServerProtocolHandler(WEBSOCKET_PATH));
// 6. 空闲状态检测器
// 检测连接的空闲状态,用于心跳机制
// readerIdleTime: 读空闲时间writerIdleTime: 写空闲时间allIdleTime: 读写空闲时间
pipeline.addLast("idle-state", new IdleStateHandler(READER_IDLE_TIME_SECONDS, 0, 0, TimeUnit.SECONDS));
// 7. 自定义WebSocket业务处理器
// 处理WebSocket帧实现具体的业务逻辑
// 包括心跳处理、消息路由、连接管理等
pipeline.addLast("websocket-handler", new WebSocketHandler());
// 7. 全局异常处理器
// 处理整个管道中未被捕获的异常,作为最后的异常处理兜底
pipeline.addLast("exception-handler", new GlobalExceptionHandler());
}
/**
* WebSocket预处理器
* 在WebSocket握手之前验证URL参数并清理URL
*/
private static class WebSocketPreprocessor extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
if (msg instanceof FullHttpRequest) {
FullHttpRequest request = (FullHttpRequest) msg;
String uri = request.uri();
log.debug("WebSocket预处理器收到HTTP请求{}", uri);
// 验证并提取userId
String userId = extractUserId(uri);
if (userId == null || userId.trim().isEmpty()) {
log.warn("WebSocket连接被拒绝缺少userId参数, uri: {}", uri);
FullHttpResponse response = new DefaultFullHttpResponse(
HttpVersion.HTTP_1_1,
HttpResponseStatus.BAD_REQUEST
);
ctx.writeAndFlush(response).addListener(f -> ctx.close());
return;
}
// 将userId存储到Channel属性中
ctx.channel().attr(AttributeKey.<String>valueOf("userId")).set(userId);
// 清理URL参数
if (uri.contains("?")) {
String cleanUri = uri.substring(0, uri.indexOf("?"));
request.setUri(cleanUri);
log.debug("URL已清理原始: {}, 清理后: {}, userId: {}", uri, cleanUri, userId);
}
}
// 继续传递给下一个Handler
super.channelRead(ctx, msg);
}
private String extractUserId(String uri) {
if (!uri.contains("name=")) {
return null;
}
int start = uri.indexOf("name=") + 5;
int end = uri.indexOf("&", start);
if (end == -1) {
return uri.substring(start);
} else {
return uri.substring(start, end);
}
}
}
/**
* 全局异常处理器
* 作为管道中的最后一个Handler捕获所有未处理的异常
*/
private static class GlobalExceptionHandler extends ChannelInboundHandlerAdapter {
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
// 记录异常详情,便于问题排查
log.error("WebSocket连接发生未处理异常远程地址{},异常信息:{}",
ctx.channel().remoteAddress(), cause.getMessage(), cause);
// 优雅关闭连接
if (ctx.channel().isActive()) {
ctx.close();
}
}
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
log.debug("WebSocket连接断开远程地址{}", ctx.channel().remoteAddress());
super.channelInactive(ctx);
}
}
}

View File

@@ -0,0 +1,237 @@
package com.njcn.gather.detection.util.socket.websocket;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.logging.LoggingHandler;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.stereotype.Component;
import javax.annotation.PreDestroy;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
/**
* WebSocket服务端核心类
*
* 职责:
* 1. 启动基于Netty的WebSocket服务器
* 2. 管理服务器生命周期(启动/关闭)
* 3. 提供高性能的WebSocket通信支持
*
* 特性:
* - 使用ApplicationRunner确保在Spring容器完全启动后再启动WebSocket服务
* - 使用CompletableFuture异步启动避免阻塞Spring Boot主线程
* - 支持优雅关闭,确保资源正确释放
* - 完善的异常处理和日志记录
*
* @Description: websocket服务端
* @Author: wr
* @Date: 2024/12/10 13:59
*/
@Component
@RequiredArgsConstructor
@Slf4j
public class WebSocketService implements ApplicationRunner {
/**
* WebSocket服务器监听端口
* 默认7777端口可通过配置文件webSocket.port自定义
* 客户端连接地址ws://host:port/hello?name=userId
*/
@Value("${webSocket.port:7777}")
int port;
/**
* Netty Boss线程组
* 专门负责接受新的客户端连接请求
* 通常配置1个线程即可因为接受连接的操作相对简单
*/
EventLoopGroup bossGroup;
/**
* Netty Worker线程组
* 专门负责处理已建立连接的I/O操作和业务逻辑
* 默认线程数 = CPU核心数 * 2用于并发处理多个客户端
*/
EventLoopGroup workerGroup;
/**
* 服务器通道引用
* 保存绑定端口后的Channel用于服务器关闭时释放资源
*/
private Channel serverChannel;
/**
* 异步启动任务的Future对象
* 用于管理WebSocket服务器的异步启动过程
* 可以用来取消启动任务或检查启动状态
*/
private CompletableFuture<Void> serverFuture;
/**
* Spring Boot应用启动完成后自动调用此方法
* 使用ApplicationRunner确保在所有Bean初始化完成后再启动WebSocket服务
*/
@Override
public void run(ApplicationArguments args){
// 使用CompletableFuture异步启动WebSocket服务避免阻塞Spring Boot主线程
// 这样可以让应用快速启动完成WebSocket服务在后台异步启动
serverFuture = CompletableFuture.runAsync(this::startWebSocketServer)
.exceptionally(throwable -> {
// 如果启动过程中发生异常,记录日志但不影响应用启动
log.error("WebSocket服务启动异常", throwable);
return null;
});
}
/**
* 启动WebSocket服务器的核心方法
* 此方法会一直阻塞直到服务器关闭,所以需要在异步线程中执行
*/
private void startWebSocketServer() {
try {
// 1. 创建线程组
// bossGroup: 专门负责接受新的客户端连接请求
// 可以自定义线程的数量这里使用默认值通常为1个线程
bossGroup = new NioEventLoopGroup(1);
// workerGroup: 专门负责处理已建立连接的I/O操作
// 默认创建的线程数量 = CPU 处理器数量 * 2用于处理业务逻辑
workerGroup = new NioEventLoopGroup();
// 2. 配置服务器启动参数
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.handler(new LoggingHandler())
// 网络配置参数
.option(ChannelOption.SO_BACKLOG, 128)
// TCP连接建立超时时间5秒
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000)
// 子通道配置(针对每个客户端连接)
// 启用TCP keepalive机制检测死连接
.childOption(ChannelOption.SO_KEEPALIVE, true)
.childHandler(new WebSocketInitializer());
// 3. 绑定端口并启动服务器
ChannelFuture future = serverBootstrap.bind(port).sync();
// 保存服务器通道引用,用于后续关闭操作
serverChannel = future.channel();
// 4. 监听绑定结果并记录日志
future.addListener(f -> {
if (future.isSuccess()) {
log.info("webSocket服务启动成功端口{}", port);
} else {
log.error("webSocket服务启动失败端口{}", port);
}
});
// 5. 等待服务器关闭
// 这里会一直阻塞直到serverChannel被外部关闭
// 这就是为什么需要在异步线程中执行此方法的原因
future.channel().closeFuture().sync();
} catch (InterruptedException e) {
// 如果线程被中断(比如应用关闭),记录日志并恢复中断状态
log.error("WebSocket服务启动过程中被中断", e);
Thread.currentThread().interrupt(); // 恢复中断状态
} catch (Exception e) {
// 捕获其他所有异常,记录日志并抛出运行时异常
log.error("WebSocket服务启动失败", e);
throw new RuntimeException("WebSocket服务启动失败", e);
} finally {
// 无论成功还是失败,都要清理资源
shutdownGracefully();
}
}
/**
* 优雅关闭Netty线程组资源
* 私有方法,用于在服务器启动异常时清理资源
*/
private void shutdownGracefully() {
// 优雅关闭接收连接的线程组
if (bossGroup != null) {
bossGroup.shutdownGracefully();
}
// 优雅关闭处理I/O的线程组
if (workerGroup != null) {
workerGroup.shutdownGracefully();
}
}
/**
* Spring容器销毁时自动调用此方法释放资源
* 使用@PreDestroy确保在应用关闭时优雅地关闭WebSocket服务
*/
@PreDestroy
public void destroy() throws InterruptedException {
log.info("正在关闭WebSocket服务...");
// 步骤1: 首先关闭服务器通道,停止接受新的连接请求
// 这样可以确保不会有新的客户端连接进来
if (serverChannel != null) {
try {
// 等待最多5秒让服务器通道关闭
serverChannel.close().awaitUninterruptibly(5, TimeUnit.SECONDS);
log.debug("服务器通道已关闭");
} catch (Exception e) {
log.warn("关闭服务器通道时发生异常", e);
}
}
// 步骤2: 关闭bossGroup线程组
// bossGroup负责接受连接现在可以安全关闭了
if (bossGroup != null) {
try {
// 优雅关闭静默期0秒超时时间5秒
// 静默期0秒意味着立即开始关闭超时5秒后强制关闭
bossGroup.shutdownGracefully(0, 5, TimeUnit.SECONDS).sync();
log.debug("bossGroup线程组已关闭");
} catch (InterruptedException e) {
log.warn("关闭bossGroup时被中断", e);
Thread.currentThread().interrupt(); // 恢复中断状态
}
}
// 步骤3: 关闭workerGroup线程组
// workerGroup负责处理I/O需要等待现有连接处理完成
if (workerGroup != null) {
try {
// 等待现有任务完成但最多等待5秒
workerGroup.shutdownGracefully(0, 5, TimeUnit.SECONDS).sync();
log.debug("workerGroup线程组已关闭");
} catch (InterruptedException e) {
log.warn("关闭workerGroup时被中断", e);
Thread.currentThread().interrupt(); // 恢复中断状态
}
}
// 步骤4: 取消异步启动任务(如果还在运行)
// 这可以避免在应用关闭后还有线程在后台运行
if (serverFuture != null && !serverFuture.isDone()) {
// true表示允许中断正在执行的任务
boolean cancelled = serverFuture.cancel(true);
if (cancelled) {
log.debug("异步启动任务已取消");
}
}
log.info("webSocket服务已销毁");
}
}

View File

@@ -1,6 +1,5 @@
package com.njcn.gather.device.controller;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.njcn.common.pojo.annotation.OperateInfo;
@@ -10,12 +9,10 @@ import com.njcn.common.pojo.enums.response.CommonResponseEnum;
import com.njcn.common.pojo.exception.BusinessException;
import com.njcn.common.pojo.response.HttpResult;
import com.njcn.common.utils.LogUtil;
import com.njcn.gather.device.pojo.enums.CommonEnum;
import com.njcn.gather.device.pojo.param.PqDevParam;
import com.njcn.gather.device.pojo.vo.PqDevVO;
import com.njcn.gather.device.service.IPqDevService;
import com.njcn.gather.plan.pojo.po.AdPlan;
import com.njcn.gather.type.pojo.po.DevType;
import com.njcn.gather.monitor.pojo.po.PqMonitor;
import com.njcn.gather.type.service.IDevTypeService;
import com.njcn.web.controller.BaseController;
import com.njcn.web.utils.FileUtil;
@@ -147,7 +144,7 @@ public class PqDevController extends BaseController {
if (!fileType) {
throw new BusinessException(CommonResponseEnum.FILE_XLSX_ERROR);
}
if("null".equals(planId)){
if ("null".equals(planId)) {
planId = null;
}
Boolean result = pqDevService.importDev(file, patternId, planId, response);

View File

@@ -160,5 +160,17 @@ public class PqStandardDevController extends BaseController {
return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, pqDevVOList, methodDescribe);
}
@OperateInfo(info = LogEnum.BUSINESS_COMMON)
@GetMapping("/canBindingList")
@ApiOperation("查询可绑定的标准设备")
public HttpResult<List<PqStandardDev>> canBindingList() {
String methodDescribe = getMethodDescribe("canBindingList");
LogUtil.njcnDebug(log, "{},查询可绑定的标准设备", methodDescribe);
List<PqStandardDev> result = pqStandardDevService.canBindingList();
return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, result, methodDescribe);
}
}

View File

@@ -13,6 +13,6 @@ import java.util.List;
*/
public interface PqStandardDevMapper extends MPJBaseMapper<PqStandardDev> {
List<PreDetection> listStandardDevPreDetection(@Param("devIds") List<String> ids);
List<PreDetection> listStandardDevPreDetection(@Param("ids") List<String> ids);
}

View File

@@ -15,6 +15,9 @@
<result column="Dev_Chns" property="devChns"/>
<result column="Dev_Volt" property="devVolt"/>
<result column="Dev_Curr" property="devCurr"/>
<result column="Angle" property="angle"/>
<result column="Use_Phase_Index" property="usePhaseIndex"/>
<result column="Wave_Cmd" property="waveCmd"/>
<collection
property="monitorList"
@@ -35,7 +38,10 @@
p.name as icdType,
t.Dev_Chns,
t.Dev_Volt,
t.Dev_Curr
t.Dev_Curr,
p.Angle,
p.Use_Phase_Index,
t.Wave_Cmd
FROM
pq_dev d
inner join pq_dev_type t on d.Dev_Type = t.id

View File

@@ -15,13 +15,9 @@
<result column="Dev_Chns" property="devChns"/>
<result column="Dev_Volt" property="devVolt"/>
<result column="Dev_Curr" property="devCurr"/>
<!-- <collection-->
<!-- property="monitorList"-->
<!-- column="{ devId = Id}"-->
<!-- select="com.njcn.gather.monitor.mapper.PqMonitorMapper.selectMonitorInfo"-->
<!-- >-->
<!-- </collection>-->
<result column="Angle" property="angle"/>
<result column="Use_Phase_Index" property="usePhaseIndex"/>
<result column="Wave_Cmd" property="waveCmd"/>
</resultMap>
<select id="listStandardDevPreDetection" resultMap="standardDevResultMap">
@@ -30,15 +26,19 @@
standard_dev.Name,
standard_dev.IP,
standard_dev.Port,
standard_dev.Dev_Type,
dev_type.name as Dev_Type,
standard_dev.Series,
standard_dev.Dev_Key,
dev_type.icdType,
icd_path.Name as icdType,
dev_type.Dev_Chns,
dev_type.Dev_Volt,
dev_type.Dev_Curr
dev_type.Dev_Curr,
icd_path.Angle,
icd_path.Use_Phase_Index,
dev_type.Wave_Cmd
from pq_standard_dev standard_dev
inner join pq_dev_type dev_type on standard_dev.Dev_Type = dev_type.id
inner join pq_icd_path icd_path on dev_type.icd = icd_path.id
where standard_dev.Id in
<foreach collection="ids" item="id" open="(" separator="," close=")">
#{id}

View File

@@ -19,4 +19,13 @@ public enum PatternEnum {
this.value = value;
this.msg = msg;
}
public static PatternEnum getEnum(String value) {
for (PatternEnum patternEnum : PatternEnum.values()) {
if (patternEnum.getValue().equals(value)) {
return patternEnum;
}
}
return null;
}
}

View File

@@ -9,11 +9,13 @@ import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer;
import com.njcn.db.mybatisplus.bo.BaseEntity;
import com.njcn.gather.monitor.pojo.po.PqMonitor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.io.Serializable;
import java.time.LocalDate;
import java.util.List;
/**
* @author caozehui
@@ -186,5 +188,8 @@ public class PqDev extends BaseEntity implements Serializable {
* 状态0-删除 1-正常
*/
private Integer state;
@TableField(exist = false)
private List<PqMonitor> monitorList;
}

View File

@@ -50,21 +50,6 @@ public class PqDevSub {
*/
private Integer factorCheckResult;
/**
* 实时数据结果 0:不合格1:合格2未检
*/
private Integer realtimeResult;
/**
* 统计数据结果 0:不合格1:合格2未检
*/
private Integer statisticsResult;
/**
* 录波数据结果 0:不合格1:合格2未检
*/
private Integer recordedResult;
/**
* 检测人
*/

View File

@@ -54,6 +54,18 @@ public class PreDetection {
@JSONField(name = "icdType")
private String icdType;
/**
* 是否支持相角。0不支持1支持
*/
@JSONField(serialize = false)
private Integer angle;
/**
* 角型接线时是否使用相别的指标来进行检测0表示否1表示是
*/
@JSONField(serialize = false)
private Integer usePhaseIndex;
/**
* 装置识别码3ds加密
*/
@@ -72,6 +84,9 @@ public class PreDetection {
private Double devVolt;
private Double devCurr;
@JSONField(serialize = false)
private String waveCmd;
/**
* 监测点信息
*/
@@ -97,25 +112,37 @@ public class PreDetection {
/**
* pt
*/
@JSONField(name = "pt")
private String pt;
@JSONField(serialize = false)
private String ptStr;
/**
* ct
*/
@JSONField(name = "ct") //todo 是否改为ct
private String ct;
@JSONField(serialize = false)
private String ctStr;
/**
* pt
*/
@JSONField(name = "pt")
private Double pt;
/**
* ct
*/
@JSONField(name = "ct")
private Double ct;
/**
* 统计间隔
*/
@JSONField(name = "statInterval")
@JSONField(serialize = false)
private Integer statInterval;
/**
* 接线方式
*/
@JSONField(name = "connection")
@JSONField(serialize = false)
private String connection;
}

View File

@@ -109,14 +109,22 @@ public interface IPqDevService extends IService<PqDev> {
* 正式监测完成,修改中断状态
*
* @param ids
* @param valueType
* @param adType
* @param code
* @param userId
* @param temperature
* @param humidity
* @return
*/
boolean updateResult(List<String> ids, List<String> valueType, String code, String userId, Float temperature, Float humidity);
boolean updateResult(List<String> ids, List<String> adType, String code, String userId, Float temperature, Float humidity);
/**
* 比对式-修改设备状态
*
* @param devId
* @param userId
*/
void updateResult(String devId,String userId);
void updatePqDevReportState(String devId, int i);

View File

@@ -91,4 +91,12 @@ public interface IPqStandardDevService extends IService<PqStandardDev> {
* @return
*/
List<PreDetection> listStandardDevPreDetection(List<String> ids);
/**
* 查询可绑定的标准设备列表
*
* @return
*/
List<PqStandardDev> canBindingList();
}

View File

@@ -44,6 +44,7 @@ import com.njcn.gather.system.dictionary.service.IDictDataService;
import com.njcn.gather.system.dictionary.service.IDictTypeService;
import com.njcn.gather.type.pojo.po.DevType;
import com.njcn.gather.type.service.IDevTypeService;
import com.njcn.gather.user.user.pojo.po.SysUser;
import com.njcn.gather.user.user.service.ISysUserService;
import com.njcn.web.factory.PageFactory;
import com.njcn.web.utils.ExcelUtil;
@@ -57,7 +58,6 @@ import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.http.HttpServletResponse;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.*;
import java.util.regex.Pattern;
@@ -210,7 +210,7 @@ public class PqDevServiceImpl extends ServiceImpl<PqDevMapper, PqDev> implements
public boolean deletePqDev(PqDevParam.DeleteParam param) {
if (PatternEnum.CONTRAST.getValue().equals(dictDataService.getDictDataById(param.getPattern()).getCode())) {
for (String id : param.getIds()) {
if (ObjectUtils.isNotEmpty(pqMonitorService.listPqMonitorByDevId(id))) {
if (ObjectUtils.isNotEmpty(pqMonitorService.listPqMonitorByDevIds(Collections.singletonList(id)))) {
throw new BusinessException(DetectionResponseEnum.PQ_DEV_HAS_MONITOR);
}
}
@@ -269,6 +269,11 @@ public class PqDevServiceImpl extends ServiceImpl<PqDevMapper, PqDev> implements
Map<String, Object> map = new HashMap<>();
map.put("id", pqDev.getId());
map.put("name", pqDev.getName());
map.put("devType", pqDev.getDevType());
map.put("manufacturer", pqDev.getManufacturer());
map.put("cityName", pqDev.getCityName());
map.put("gdName", pqDev.getGdName());
map.put("subName", pqDev.getSubName());
return map;
}).collect(Collectors.toList());
return result;
@@ -349,9 +354,13 @@ public class PqDevServiceImpl extends ServiceImpl<PqDevMapper, PqDev> implements
pqDevVO.setDevKey(EncryptionUtil.decoderString(1, pqDevVO.getDevKey()));
}
if (StrUtil.isNotBlank(pqDevVO.getCheckBy())) {
pqDevVO.setCheckBy(userService.getById(pqDevVO.getCheckBy()).getName());
SysUser sysUser = userService.getById(pqDevVO.getCheckBy());
if (ObjectUtil.isNotNull(sysUser)) {
pqDevVO.setCheckBy(sysUser.getName());
} else {
pqDevVO.setCheckBy(pqDevVO.getCheckBy());
}
}
DevType devType = devTypeService.getById(pqDevVO.getDevType());
if (ObjectUtil.isNotNull(devType)) {
pqDevVO.setDevChns(devType.getDevChns());
@@ -359,7 +368,7 @@ public class PqDevServiceImpl extends ServiceImpl<PqDevMapper, PqDev> implements
pqDevVO.setDevVolt(devType.getDevVolt());
}
List<PqMonitor> monitorList = pqMonitorService.listPqMonitorByDevId(id);
List<PqMonitor> monitorList = pqMonitorService.listPqMonitorByDevIds(Collections.singletonList(id));
if (ObjectUtil.isNotEmpty(monitorList)) {
pqDevVO.setMonitorList(monitorList);
}
@@ -418,8 +427,8 @@ public class PqDevServiceImpl extends ServiceImpl<PqDevMapper, PqDev> implements
monitorListDTO = new PreDetection.MonitorListDTO();
monitorListDTO.setLineId(preDetection.getDevIP() + "_" + i);
monitorListDTO.setLine(i);
monitorListDTO.setPt("1");
monitorListDTO.setCt("1");
monitorListDTO.setPt(1.0);
monitorListDTO.setCt(1.0);
monitorList.add(monitorListDTO);
}
preDetection.setMonitorList(monitorList);
@@ -430,10 +439,11 @@ public class PqDevServiceImpl extends ServiceImpl<PqDevMapper, PqDev> implements
@Override
public boolean updateResult(List<String> ids, List<String> valueType, String code, String userId, Float temperature, Float humidity) {
public boolean updateResult(List<String> ids, List<String> adType, String code, String userId, Float temperature, Float humidity) {
if (CollUtil.isNotEmpty(ids)) {
SysTestConfig config = sysTestConfigService.getOneConfig();
Map<String, Integer> result = detectionDataDealService.devResult(ids, valueType, code);
Map<String, Integer> result = detectionDataDealService.devResult(false, ids, adType, code);
List<PqDevVO> list = new ArrayList<>();
if (CollUtil.isNotEmpty(ids)) {
list.addAll(this.baseMapper.listByDevIds(ids));
@@ -503,6 +513,41 @@ public class PqDevServiceImpl extends ServiceImpl<PqDevMapper, PqDev> implements
return true;
}
@Override
@Transactional
public void updateResult(String devId, String userId) {
PqDev dev = this.getById(devId);
Integer checkState = pqMonitorService.getDevCheckState(devId);
Integer checkResult = pqMonitorService.getDevCheckResult(devId);
pqDevSubService.lambdaUpdate()
.set(PqDevSub::getCheckState, checkState)
.set(PqDevSub::getCheckResult, checkResult)
.set(StrUtil.isNotBlank(userId), PqDevSub::getCheckBy, userId)
.eq(PqDevSub::getDevId, devId).update();
PqDevParam.QueryParam param = new PqDevParam.QueryParam();
String planId = dev.getPlanId();
param.setPlanIdList(Arrays.asList(planId));
List<PqDevVO> pqDevVOList = this.baseMapper.selectByQueryParam(param);
if (CollUtil.isNotEmpty(pqDevVOList)) {
Set<Integer> set = pqDevVOList.stream().map(PqDevVO::getCheckResult).collect(Collectors.toSet());
if (set.contains(CheckResultEnum.NOT_ACCORD.getValue())) {
this.baseMapper.updatePlanCheckResult(planId, CheckResultEnum.NOT_ACCORD.getValue());
} else if (set.contains(CheckResultEnum.UNCHECKED.getValue())) {
this.baseMapper.updatePlanCheckResult(planId, CheckResultEnum.UNCHECKED.getValue());
} else {
this.baseMapper.updatePlanCheckResult(planId, CheckResultEnum.ACCORD.getValue());
}
set = pqDevVOList.stream().map(PqDevVO::getCheckState).collect(Collectors.toSet());
if (set.contains(CheckStateEnum.UNCHECKED.getValue())) {
this.baseMapper.updatePlanTestState(planId, CheckStateEnum.CHECKING.getValue());
}
}
}
@Override
public void updatePqDevReportState(String devId, int reportState) {
LambdaUpdateWrapper<PqDevSub> updateWrapper = new LambdaUpdateWrapper<>();
@@ -738,7 +783,7 @@ public class PqDevServiceImpl extends ServiceImpl<PqDevMapper, PqDev> implements
}
int count = this.count(queryWrapper);
if (count > 0) {
throw new BusinessException(DetectionResponseEnum.PQ_DEV_REPEAT);
throw new BusinessException(DetectionResponseEnum.PQ_DEV_REPEAT, "" + param.getName() + "】被检设备已存在");
}
}
@@ -1139,10 +1184,23 @@ public class PqDevServiceImpl extends ServiceImpl<PqDevMapper, PqDev> implements
@Transactional
public boolean importContrastDev(List<ContrastDevExcel> contrastDevExcelList, String patternId, String planId) {
if (CollUtil.isNotEmpty(contrastDevExcelList)) {
List<PqMonitor> monitorList = new ArrayList<>();
List<PqDev> oldDevList = contrastDevExcelList.stream().map(devExcel -> {
// 根据设备名称分组
Map<String, List<ContrastDevExcel>> listMap = contrastDevExcelList.stream()
.collect(Collectors.groupingBy(ContrastDevExcel::getName, LinkedHashMap::new, Collectors.toList()));
List<PqDev> oldDevList = new ArrayList<>(listMap.size());
List<PqMonitor> finalMonitorList = new ArrayList<>();
for (Map.Entry<String, List<ContrastDevExcel>> entry : listMap.entrySet()) {
String name = entry.getKey();
List<ContrastDevExcel> devExcelList = entry.getValue();
// 监测点数据
List<PqMonitorExcel> pqMonitorExcelList = devExcelList.stream()
.map(ContrastDevExcel::getPqMonitorExcelList)
.filter(Objects::nonNull)
.flatMap(List::stream)
.collect(Collectors.toList());
// 取第一条为设备基本信息
ContrastDevExcel devExcel = devExcelList.get(0);
PqDev pqDev = BeanUtil.copyProperties(devExcel, PqDev.class);
if (pqDev.getEncryptionFlag() == 1) {
if (StrUtil.isNotBlank(pqDev.getSeries()) && StrUtil.isNotBlank(pqDev.getDevKey())) {
pqDev.setSeries(EncryptionUtil.encodeString(1, pqDev.getSeries()));
@@ -1154,43 +1212,47 @@ public class PqDevServiceImpl extends ServiceImpl<PqDevMapper, PqDev> implements
DevType devType = devTypeService.getByName(pqDev.getDevType());
if (ObjectUtil.isNull(devType)) {
throw new BusinessException(DetectionResponseEnum.DEV_TYPE_NOT_EXIST);
} else {
pqDev.setDevType(devType.getId());
Integer devChns = devType.getDevChns();
List<Integer> numList = devExcel.getPqMonitorExcelList().stream().map(monitorExcel -> monitorExcel.getNum()).collect(Collectors.toList());
if (CollUtil.isNotEmpty(numList)) {
Integer max = CollectionUtil.max(numList);
Integer min = CollectionUtil.min(numList);
if (min < 1 || max > devChns) {
throw new BusinessException(DetectionResponseEnum.MONITOR_NUM_OUT_OF_RANGE);
}
if (min == max && numList.size() > 1) {
throw new BusinessException(DetectionResponseEnum.MONITOR_NUM_REPEAT);
}
}
}
// 校验监测点数量
int devChns = devType.getDevChns();
if (pqMonitorExcelList.size() != devChns) {
throw new BusinessException(DetectionResponseEnum.IMPORT_DATA_FAIL, "" + name + "】的设备类型必须具备" + devChns + "个监测点信息!");
}
List<Integer> numList = pqMonitorExcelList.stream().map(PqMonitorExcel::getNum).collect(Collectors.toList());
// 判断是否有重复的num
Set<Integer> uniqueNumSet = new HashSet<>(numList);
if (uniqueNumSet.size() != numList.size()) {
throw new BusinessException(DetectionResponseEnum.MONITOR_NUM_REPEAT);
}
Integer max = CollectionUtil.max(numList);
Integer min = CollectionUtil.min(numList);
if (min < 1 || max > devChns) {
throw new BusinessException(DetectionResponseEnum.MONITOR_NUM_OUT_OF_RANGE);
}
pqDev.setDevType(devType.getId());
pqDev.setImportFlag(1);
pqDev.setId(UUID.randomUUID().toString().replaceAll("-", ""));
pqDev.setCreateId(pqDev.getName()); //导入时设备序列号默认与设备名称相同
StringBuilder sb = new StringBuilder();
for (int i = 0; i < devExcel.getPqMonitorExcelList().size(); i++) {
PqMonitor monitor = BeanUtil.copyProperties(devExcel.getPqMonitorExcelList().get(i), PqMonitor.class);
if (StrUtil.isBlank(monitor.getName())) {
continue;
}
sb.append(monitor.getNum() + StrUtil.COMMA);
List<PqMonitor> monitorList = new ArrayList<>();
// 根据num排序
pqMonitorExcelList.sort(Comparator.comparingInt(PqMonitorExcel::getNum));
for (PqMonitorExcel pqMonitorExcel : pqMonitorExcelList) {
PqMonitor monitor = BeanUtil.copyProperties(pqMonitorExcel, PqMonitor.class);
monitor.setDevId(pqDev.getId());
monitorList.add(monitor);
}
if (sb.length() > 0) {
pqDev.setInspectChannel(sb.replace(sb.length() - 1, sb.length(), "").toString());
StringBuilder inspectChannelBuilder = new StringBuilder();
for (int i = 1; i <= devChns; i++) {
inspectChannelBuilder.append(i);
if (i < devChns) {
inspectChannelBuilder.append(",");
}
}
return pqDev;
}).collect(Collectors.toList());
pqDev.setInspectChannel(inspectChannelBuilder.toString());
oldDevList.add(pqDev);
finalMonitorList.addAll(monitorList);
}
//逆向可视化
this.reverseVisualizeProvinceDev(oldDevList, patternId);
@@ -1219,7 +1281,7 @@ public class PqDevServiceImpl extends ServiceImpl<PqDevMapper, PqDev> implements
PqDev newDev = newDevList.stream().filter(dev -> dev.getHarmSysId().equals(oldDev.getHarmSysId())).findFirst().orElse(null);
if (ObjectUtil.isNotNull(newDev)) {
newDevList.remove(newDev);
monitorList.stream()
finalMonitorList.stream()
.filter(monitor -> monitor.getDevId().equals(newDev.getId()))
.forEach(monitor -> monitor.setDevId(oldDev.getId()));
BeanUtil.copyProperties(newDev, oldDev, "id");
@@ -1247,8 +1309,8 @@ public class PqDevServiceImpl extends ServiceImpl<PqDevMapper, PqDev> implements
.in("pq_monitor.Dev_Id", devIdList);
pqMonitorService.remove(wrapper);
}
pqMonitorService.reverseVisualizeMonitor(monitorList);
pqMonitorService.saveBatch(monitorList);
pqMonitorService.reverseVisualizeMonitor(finalMonitorList);
pqMonitorService.saveBatch(finalMonitorList);
return true;
}
return false;
@@ -1364,11 +1426,6 @@ public class PqDevServiceImpl extends ServiceImpl<PqDevMapper, PqDev> implements
pqDev.setDelegate(delegateDictData.getId());
}
}
// pqDev.setTimeCheckResult(TimeCheckResultEnum.UNKNOWN.getValue());
// pqDev.setFactorCheckResult(FactorCheckResultEnum.UNKNOWN.getValue());
// pqDev.setCheckState(CheckStateEnum.UNCHECKED.getValue());
// pqDev.setReportState(DevReportStateEnum.UNCHECKED.getValue());
// pqDev.setCheckResult(CheckResultEnum.UNCHECKED.getValue());
pqDev.setState(DataStateEnum.ENABLE.getCode());
});
}
@@ -1380,7 +1437,7 @@ public class PqDevServiceImpl extends ServiceImpl<PqDevMapper, PqDev> implements
this.visualizeProvinceDev(pqDevVOList);
contrastDevExcels.addAll(BeanUtil.copyToList(pqDevVOList, ContrastDevExcel.class));
contrastDevExcels.forEach(contrastDevExcel -> {
List<PqMonitor> monitorList = pqMonitorService.listPqMonitorByDevId(contrastDevExcel.getId());
List<PqMonitor> monitorList = pqMonitorService.listPqMonitorByDevIds(Collections.singletonList(contrastDevExcel.getId()));
pqMonitorService.visualizeMonitor(monitorList);
List<PqMonitorExcel> pqMonitorExcelList = BeanUtil.copyToList(monitorList, PqMonitorExcel.class);
contrastDevExcel.setPqMonitorExcelList(pqMonitorExcelList);

View File

@@ -8,6 +8,7 @@ import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.spring.SpringUtil;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
@@ -23,6 +24,10 @@ import com.njcn.gather.device.pojo.vo.PqStandardDevExcel;
import com.njcn.gather.device.pojo.vo.PreDetection;
import com.njcn.gather.device.service.IPqStandardDevService;
import com.njcn.gather.plan.mapper.AdPlanStandardDevMapper;
import com.njcn.gather.plan.pojo.po.AdPlan;
import com.njcn.gather.plan.pojo.po.AdPlanStandardDev;
import com.njcn.gather.plan.service.IAdPlanService;
import com.njcn.gather.plan.service.IAdPlanStandardDevService;
import com.njcn.gather.pojo.enums.DetectionResponseEnum;
import com.njcn.gather.system.dictionary.pojo.po.DictData;
import com.njcn.gather.system.dictionary.pojo.po.DictType;
@@ -58,6 +63,7 @@ public class PqStandardDevServiceImpl extends ServiceImpl<PqStandardDevMapper, P
private final IDictDataService dictDataService;
private final IDictTypeService dictTypeService;
private final AdPlanStandardDevMapper adPlanStandardDevMapper;
private final IAdPlanStandardDevService adPlanStandardDevService;
@Override
public Page<PqStandardDev> listPqStandardDevs(PqStandardDevParam.QueryParam queryParam) {
@@ -300,4 +306,50 @@ public class PqStandardDevServiceImpl extends ServiceImpl<PqStandardDevMapper, P
}
}
}
@Override
public List<PqStandardDev> canBindingList() {
List<String> excludeStandardDevIds = new ArrayList<>();
// 获取所有已绑定的标准设备
List<AdPlanStandardDev> boundList = adPlanStandardDevService.list();
if (CollectionUtil.isNotEmpty(boundList)) {
// 获取对应检测计划
List<String> planIds = boundList.stream().map(AdPlanStandardDev::getPlanId).collect(Collectors.toList());
IAdPlanService adPlanService = SpringUtil.getBean(IAdPlanService.class);
List<AdPlan> planList = adPlanService.listByIds(planIds);
// 区分主计划和子计划
List<AdPlan> mainPlanList = planList.stream().filter(plan -> plan.getFatherPlanId() == null).collect(Collectors.toList());
List<AdPlan> subPlanList = planList.stream().filter(plan -> plan.getFatherPlanId() != null).collect(Collectors.toList());
List<String> excludePlanIds = new ArrayList<>();
// 主计划直接排除
if (CollectionUtil.isNotEmpty(mainPlanList)) {
List<String> excludeMainPlanIds = mainPlanList.stream().filter(plan -> plan.getTestState() != 2).map(plan -> plan.getId()).collect(Collectors.toList());
excludePlanIds.addAll(excludeMainPlanIds);
}
// 子计划需要判断其主计划, 如果主计划未完成则排除
if (CollectionUtil.isNotEmpty(subPlanList)) {
List<String> fatherPlanIds = subPlanList.stream().map(plan -> plan.getFatherPlanId()).collect(Collectors.toList());
List<AdPlan> fatherPlanList = adPlanService.listByIds(fatherPlanIds);
List<String> excludeFatherPlanIds = fatherPlanList.stream()
.filter(plan -> plan.getTestState() != 2)
.map(plan -> plan.getId()).collect(Collectors.toList());
List<String> excludeSubPlanIds = subPlanList.stream()
.filter(plan -> excludeFatherPlanIds.contains(plan.getFatherPlanId()))
.map(plan -> plan.getId()).collect(Collectors.toList());
excludePlanIds.addAll(excludeSubPlanIds);
}
if (CollectionUtil.isNotEmpty(excludePlanIds)) {
List<AdPlanStandardDev> excludeBoundList = boundList.stream()
.filter(bound -> excludePlanIds.contains(bound.getPlanId()))
.collect(Collectors.toList());
excludeStandardDevIds = excludeBoundList.stream().map(AdPlanStandardDev::getStandardDevId).collect(Collectors.toList());
}
}
return this.lambdaQuery()
.eq(PqStandardDev::getState, DataStateEnum.ENABLE.getCode())
.notIn(CollectionUtil.isNotEmpty(excludeStandardDevIds), PqStandardDev::getId, excludeStandardDevIds)
.list();
}
}

View File

@@ -144,10 +144,10 @@ public class PqErrSysController extends BaseController {
@GetMapping("/getTestItems")
@ApiOperation("根据误差体系id获取测试项")
@ApiImplicitParam(name = "id", value = "误差体系id", required = true)
public HttpResult<List<Map<String, String>>> getTestItems(@RequestParam("id") String id) {
public HttpResult<Map<String, String>> getTestItems(@RequestParam("id") String id) {
String methodDescribe = getMethodDescribe("getTestItems");
LogUtil.njcnDebug(log, "{}获取测试项ID为{}", methodDescribe, id);
List<Map<String, String>> result = pqErrSysService.getTestItems(id);
Map<String, String> result = pqErrSysService.getTestItems(id);
return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, result, methodDescribe);
}
}

View File

@@ -13,6 +13,7 @@
</foreach>
</where>
</if>
and State = 1
</select>
</mapper>

View File

@@ -86,5 +86,5 @@ public interface IPqErrSysService extends IService<PqErrSys> {
* @param id 误差体系id
* @return
*/
List<Map<String, String>> getTestItems(String id);
Map<String, String> getTestItems(String id);
}

View File

@@ -162,7 +162,7 @@ public class PqErrSysDtlsServiceImpl extends ServiceImpl<PqErrSysDtlsMapper, PqE
wrapper.selectAll(PqErrSysDtls.class)
.leftJoin(DictTree.class, DictTree::getId, PqErrSysDtls::getScriptType)
.eq(PqErrSysDtls::getErrorSysId, errSysId)
.eq(DictTree::getId, scriptType);
.eq(DictTree::getCode, scriptType);
return this.list(wrapper);
}
}

View File

@@ -1,6 +1,7 @@
package com.njcn.gather.err.service.impl;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.text.StrPool;
import cn.hutool.core.util.ObjectUtil;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
@@ -128,7 +129,7 @@ public class PqErrSysServiceImpl extends ServiceImpl<PqErrSysMapper, PqErrSys> i
@Override
public List<Map<String, Object>> listAllPqErrSys() {
List<PqErrSys> pqErrSysList = this.lambdaQuery()
.eq(PqErrSys::getEnable,DataStateEnum.ENABLE.getCode())
.eq(PqErrSys::getEnable, DataStateEnum.ENABLE.getCode())
.eq(PqErrSys::getState, DataStateEnum.ENABLE.getCode()).list();
List<Map<String, Object>> result = pqErrSysList.stream().map(pqErrSys -> {
Map<String, Object> map = new HashMap<>();
@@ -209,24 +210,36 @@ public class PqErrSysServiceImpl extends ServiceImpl<PqErrSysMapper, PqErrSys> i
}
@Override
public List<Map<String, String>> getTestItems(String id) {
public Map<String, String> getTestItems(String id) {
List<PqErrSysDtls> pqErrSysDtls = pqErrSysDtlsService.listPqErrSysDtlsByPqErrSysId(id);
List<String> scriptTypeList = pqErrSysDtls.stream().map(PqErrSysDtls::getScriptType).distinct().collect(Collectors.toList());
List<DictTree> dictTreeList = dictTreeService.lambdaQuery().in(CollUtil.isNotEmpty(scriptTypeList), DictTree::getId, scriptTypeList).list();
if (CollUtil.isNotEmpty(dictTreeList)) {
List<String> pids = dictTreeList.stream().map(DictTree::getPid).collect(Collectors.toList());
List<DictTree> parentDictTreeList = dictTreeService.listByIds(pids);
Map<String, String> map = new HashMap<>();
parentDictTreeList.forEach(dictTree -> {
map.put(dictTree.getId(), dictTree.getName());
});
return map;
}
return null;
}
/**
* 检查重复
*
* @param param 检测参数
* @param param 检测参数
* @param isExcludeSelf 是否排除自己
*/
private void checkRepeat(PqErrSysParam param, boolean isExcludeSelf) {
QueryWrapper<PqErrSys> wrapper = new QueryWrapper<>();
wrapper.eq("Standard_Name", param.getStandardName())
.eq("Standard_Time",param.getStandardTime()+"-01-01")
.eq("Dev_Level",param.getDevLevel())
.eq("Standard_Time", param.getStandardTime() + "-01-01")
.eq("Dev_Level", param.getDevLevel())
.eq("state", DataStateEnum.ENABLE.getCode());
if (isExcludeSelf) {
if(param instanceof PqErrSysParam.UpdateParam){
if (param instanceof PqErrSysParam.UpdateParam) {
wrapper.ne("Id", ((PqErrSysParam.UpdateParam) param).getId());
}
}

View File

@@ -26,6 +26,12 @@ public class PqIcdPathParam {
@Pattern(regexp = PatternRegex.ICD_PATH_REGEX, message = DetectionValidMessage.ICD_PATH_FORMAT_ERROR)
private String path;
@ApiModelProperty(value = "是否支持电压相角、电流相角指标0表示否1表示是", required = true)
private Integer angle;
@ApiModelProperty(value = "角型接线时是否使用相别的指标来进行检测0表示否1表示是", required = true)
private Integer usePhaseIndex;
/**
* 分页查询实体
*/

View File

@@ -36,5 +36,15 @@ public class PqIcdPath extends BaseEntity implements Serializable {
* 状态0-删除 1-正常
*/
private Integer state;
/**
* 是否支持电压相角、电流相角指标0表示否1表示是
*/
private Integer angle;
/**
* 角型接线时是否使用相别的指标来进行检测0表示否1表示是
*/
private Integer usePhaseIndex;
}

View File

@@ -19,6 +19,7 @@ import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Collections;
import java.util.List;
@@ -41,7 +42,7 @@ public class PqMonitorController extends BaseController {
public HttpResult<List<PqMonitor>> list(@RequestBody PqMonitorParam.QueryParam queryParam) {
String methodDescribe = getMethodDescribe("list");
LogUtil.njcnDebug(log, "{},查询数据为:{}", methodDescribe, queryParam);
List<PqMonitor> result = pqMonitorService.listPqMonitorByDevId(queryParam.getDevId());
List<PqMonitor> result = pqMonitorService.listPqMonitorByDevIds(queryParam.getDevIds());
return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, result, methodDescribe);
}
}

View File

@@ -15,11 +15,23 @@ public interface PqMonitorMapper extends MPJBaseMapper<PqMonitor> {
/**
* 根据终端id获取监测点集合
*
* @param devId
* @return: java.util.List<com.njcn.gather.device.pojo.vo.PreDetection.MonitorListDTO>
* @Author: wr
* @Date: 2024/12/12 13:15
*/
List<PreDetection.MonitorListDTO> selectMonitorInfo(@Param("devId") String devId);
/**
* 根据被检设备id和线路号获取监测点信息
*
* @param devId 被检设备id
* @param num 线路号
* @return
*/
PqMonitor getByDevIdAndNum(@Param("devId") String devId, @Param("num") Integer num);
List<PqMonitor> listByDevIds(@Param("devIds") List<String> devIds);
}

View File

@@ -6,8 +6,8 @@
resultType="com.njcn.gather.device.pojo.vo.PreDetection$MonitorListDTO">
SELECT CONCAT(pq_dev.IP, '_', Num) as lineId,
Num as line,
pt as pt,
ct as ct,
pt as ptStr,
ct as ctStr,
Stat_Interval,
sys_dict_data.Code as `Connection`
FROM pq_monitor
@@ -16,5 +16,30 @@
WHERE Dev_Id = #{devId}
</select>
<select id="getByDevIdAndNum" resultType="com.njcn.gather.monitor.pojo.po.PqMonitor">
select pq_monitor.Id,
pq_monitor.Dev_Id,
pq_monitor.Name,
pq_monitor.Busbar,
pq_monitor.Num,
pq_monitor.Pt,
pq_monitor.Ct,
pq_monitor.Stat_Interval,
pq_monitor.Harm_Sys_Id,
sys_dict_data.Code as `Connection`
from pq_monitor
inner join sys_dict_data on pq_monitor.Connection = sys_dict_data.id
where Dev_Id = #{devId}
and Num = #{num}
</select>
<select id="listByDevIds" resultType="com.njcn.gather.monitor.pojo.po.PqMonitor">
select *
from pq_monitor
where Dev_Id in
<foreach collection="devIds" item="devId" open="(" separator="," close=")">
#{devId}
</foreach>
order by Num
</select>
</mapper>

View File

@@ -9,6 +9,7 @@ import lombok.EqualsAndHashCode;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Pattern;
import java.util.List;
/**
* @author caozehui
@@ -54,6 +55,9 @@ public class PqMonitorParam {
@NotBlank(message = DetectionValidMessage.MONITOR_ID_NOT_BLANK)
private String harmSysId;
@ApiModelProperty(value = "是否做检测")
private Integer checkFlag;
/**
* 分页查询实体
@@ -62,18 +66,6 @@ public class PqMonitorParam {
public static class QueryParam {
@ApiModelProperty(value = "所属设备id")
@NotBlank(message = DetectionValidMessage.DEVICE_ID_NOT_BLANK)
private String devId;
private List<String> devIds;
}
/**
* 修改实体
*/
// @Data
// @EqualsAndHashCode(callSuper = true)
// public static class UpdateParam extends PqMonitorParam {
// @ApiModelProperty(value = "监测点id", required = true)
// @NotBlank(message = DetectionValidMessage.ID_NOT_BLANK)
// @Pattern(regexp = PatternRegex.SYSTEM_ID, message = DetectionValidMessage.ID_FORMAT_ERROR)
// private String id;
// }
}

View File

@@ -1,5 +1,7 @@
package com.njcn.gather.monitor.pojo.po;
import com.baomidou.mybatisplus.annotation.FieldStrategy;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
@@ -62,5 +64,51 @@ public class PqMonitor implements Serializable {
* 谐波系统监测点id
*/
private String harmSysId;
/**
* 实时数据结果 0:不合格1:合格2未检
*/
@TableField(updateStrategy = FieldStrategy.IGNORED)
private Integer realtimeResult;
/**
* 统计数据结果 0:不合格1:合格2未检
*/
@TableField(updateStrategy = FieldStrategy.IGNORED)
private Integer statisticsResult;
/**
* 录波数据结果 0:不合格1:合格2未检
*/
@TableField(updateStrategy = FieldStrategy.IGNORED)
private Integer recordedResult;
/**
* 实时数据使用哪一次的检测数据来表示结论
*/
@TableField(updateStrategy = FieldStrategy.IGNORED)
private String realtimeNum;
/**
* 统计数据使用哪一次的检测数据来表示结论
*/
@TableField(updateStrategy = FieldStrategy.IGNORED)
private String statisticsNum;
/**
* 录波数据使用哪一次、哪一组的检测数据来表示结论 例如 3_2第三次检测第二组
*/
@TableField(updateStrategy = FieldStrategy.IGNORED)
private String recordedNum;
/**
* 整个通道的结果使用那种数据。real、wave_data、statistics
*/
@TableField(updateStrategy = FieldStrategy.IGNORED)
private String resultType;
private Integer qualifiedNum; // 合格次数
private Integer checkFlag; // 是否做检测
}

View File

@@ -1,11 +1,12 @@
package com.njcn.gather.monitor.service;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.IService;
import com.njcn.gather.monitor.pojo.param.PqMonitorParam;
import com.njcn.gather.monitor.pojo.po.PqMonitor;
import com.njcn.gather.plan.pojo.enums.DataSourceEnum;
import java.util.List;
import java.util.Map;
/**
* @author caozehui
@@ -14,14 +15,13 @@ import java.util.List;
public interface IPqMonitorService extends IService<PqMonitor> {
/**
* 根据设备id获取所有监测点信息
*
* @param devId 被检设备id
* @param devIds 被检设备id列表
* @return 监测点信息列表
*/
List<PqMonitor> listPqMonitorByDevId(String devId);
List<PqMonitor> listPqMonitorByDevIds(List<String> devIds);
/**
* 根据设备id批量新增监测点信息
@@ -62,4 +62,54 @@ public interface IPqMonitorService extends IService<PqMonitor> {
* @param monitorList
*/
void reverseVisualizeMonitor(List<PqMonitor> monitorList);
/**
* 根据被检设备监测点id获取监测点信息
*
* @param devMonitorId 被检设备监测点id
* @return
*/
PqMonitor getByDevMonitorId(String devMonitorId);
/**
* 根据被检设备id获取额定电流
*
* @param devMonitorId 被检设备监测点id
* @return 额定电流
*/
Double getRatedCurrent(String devMonitorId);
/**
* 根据被检设备id获取额定电压
*
* @param devMonitorId 被检设备监测点id
* @return 额定电压
*/
Double getRatedVoltage(String devMonitorId);
/**
* @param monitorId 监测点id
* @param adTypes 检测项指标id列表
* @param dataSourceEnum 那种数据源
* @param code 结果表后缀
* @return
*/
boolean updateMonitorResult(String monitorId, List<String> adTypes, DataSourceEnum dataSourceEnum, Integer num, Integer waveNum, String code);
/**
* 查询设备的检测状态
*
* @param devId
* @return
*/
Integer getDevCheckState(String devId);
/**
* 查询设备的检测结果
*
* @param devId
* @return
*/
Integer getDevCheckResult(String devId);
}

View File

@@ -1,15 +1,23 @@
package com.njcn.gather.monitor.service.impl;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.njcn.common.pojo.exception.BusinessException;
import com.njcn.gather.detection.util.socket.CnSocketUtil;
import com.njcn.gather.device.pojo.enums.CheckResultEnum;
import com.njcn.gather.device.pojo.enums.CheckStateEnum;
import com.njcn.gather.monitor.mapper.PqMonitorMapper;
import com.njcn.gather.monitor.pojo.param.PqMonitorParam;
import com.njcn.gather.monitor.pojo.po.PqMonitor;
import com.njcn.gather.monitor.service.IPqMonitorService;
import com.njcn.gather.plan.pojo.enums.DataSourceEnum;
import com.njcn.gather.pojo.enums.DetectionResponseEnum;
import com.njcn.gather.storage.service.DetectionDataDealService;
import com.njcn.gather.storage.service.impl.DetectionDataServiceImpl;
import com.njcn.gather.system.dictionary.pojo.po.DictData;
import com.njcn.gather.system.dictionary.service.IDictDataService;
import lombok.RequiredArgsConstructor;
@@ -17,7 +25,11 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Collectors;
/**
* @author caozehui
@@ -29,10 +41,14 @@ import java.util.List;
public class PqMonitorServiceImpl extends ServiceImpl<PqMonitorMapper, PqMonitor> implements IPqMonitorService {
private final IDictDataService dictDataService;
private final DetectionDataDealService detectionDataDealService;
@Override
public List<PqMonitor> listPqMonitorByDevId(String devId) {
return this.lambdaQuery().eq(PqMonitor::getDevId, devId).list();
public List<PqMonitor> listPqMonitorByDevIds(List<String> devIds) {
if (CollUtil.isNotEmpty(devIds)) {
return this.baseMapper.listByDevIds(devIds);
}
return CollUtil.empty(PqMonitor.class);
}
@Override
@@ -84,4 +100,180 @@ public class PqMonitorServiceImpl extends ServiceImpl<PqMonitorMapper, PqMonitor
}
});
}
@Override
public PqMonitor getByDevMonitorId(String devMonitorId) {
String[] split = devMonitorId.split("_");
return this.baseMapper.getByDevIdAndNum(split[0], Integer.valueOf(split[1]));
}
@Override
public Double getRatedCurrent(String devMonitorId) {
PqMonitor pqMonitor = this.getByDevMonitorId(devMonitorId);
if (ObjectUtil.isNotNull(pqMonitor)) {
String ct = pqMonitor.getCt();
if (StrUtil.isNotBlank(ct) && ct.contains(StrUtil.COLON)) {
String[] ctArray = ct.split(StrUtil.COLON);
return Double.parseDouble(ctArray[1]);
}
}
throw new BusinessException(DetectionResponseEnum.RATED_CT_ERROR);
}
@Override
public Double getRatedVoltage(String devMonitorId) {
PqMonitor pqMonitor = this.getByDevMonitorId(devMonitorId);
if (ObjectUtil.isNotNull(pqMonitor)) {
String pt = pqMonitor.getPt();
if (StrUtil.isNotBlank(pt) && pt.contains(StrUtil.COLON)) {
String[] ptArray = pt.split(StrUtil.COLON);
return Double.parseDouble(ptArray[1]);
}
}
throw new BusinessException(DetectionResponseEnum.RATED_PT_ERROR);
}
@Override
@Transactional
public boolean updateMonitorResult(String monitorId, List<String> adTypes, DataSourceEnum dataSourceEnum, Integer num, Integer waveNum, String code) {
String[] split = monitorId.split(CnSocketUtil.SPLIT_TAG);
QueryWrapper<PqMonitor> wrapper = new QueryWrapper<>();
wrapper.eq("pq_monitor.Dev_Id", split[0])
.eq("pq_monitor.Num", split[1])
.last("LIMIT 1");
PqMonitor monitor = this.getOne(wrapper);
String resultType = monitor.getResultType();
Integer qualifiedNum = monitor.getQualifiedNum();
Integer newMonitorResult = CheckResultEnum.UNCHECKED.getValue();
AtomicReference<Integer> newWaveNum = new AtomicReference<>(-1);
switch (dataSourceEnum) {
case REAL_DATA:
newMonitorResult = detectionDataDealService.getMonitorResult(monitorId, adTypes, dataSourceEnum.getValue(), num, null, code);
break;
case WAVE_DATA:
Map<Integer, Integer> waveNumResultMap = detectionDataDealService.getWaveNumResultMap(monitorId, adTypes, num, code);
if(CollUtil.isEmpty(waveNumResultMap)){
return true;
}
waveNumResultMap.forEach((key, value) -> {
if (CheckResultEnum.ACCORD.getValue().equals(value)) {
newWaveNum.set(key);
}
});
if (!newWaveNum.get().equals(-1)) {
newMonitorResult = CheckResultEnum.ACCORD.getValue();
} else {
newWaveNum.set(waveNum);
newMonitorResult = waveNumResultMap.get(waveNum);
}
break;
default:
break;
}
boolean updateFlag = false;
if (StrUtil.isBlank(resultType)) {
updateFlag = true;
if (newMonitorResult.equals(CheckResultEnum.ACCORD.getValue())) {
qualifiedNum += 1;
}
} else {
Integer oldMonitorResult = null;
if (DataSourceEnum.REAL_DATA.getValue().equals(resultType)) {
oldMonitorResult = monitor.getRealtimeResult();
} else if (DataSourceEnum.WAVE_DATA.getValue().equals(resultType)) {
oldMonitorResult = monitor.getRecordedResult();
} else {
oldMonitorResult = monitor.getStatisticsResult();
}
if (CheckResultEnum.ACCORD.getValue().equals(oldMonitorResult)) {
if (CheckResultEnum.ACCORD.getValue().equals(newMonitorResult)) {
qualifiedNum += 1;
updateFlag = true;
}
} else {
updateFlag = true;
if (CheckResultEnum.ACCORD.getValue().equals(newMonitorResult)) {
qualifiedNum += 1;
}
}
}
if (updateFlag) {
monitor.setResultType(dataSourceEnum.getValue());
switch (dataSourceEnum) {
case REAL_DATA:
monitor.setRealtimeResult(newMonitorResult);
monitor.setRealtimeNum(String.valueOf(num));
monitor.setRecordedResult(null);
monitor.setRecordedNum(null);
monitor.setStatisticsResult(null);
monitor.setStatisticsNum(null);
break;
case WAVE_DATA:
monitor.setRealtimeResult(null);
monitor.setRealtimeNum(null);
monitor.setRecordedResult(newMonitorResult);
monitor.setRecordedNum(num + CnSocketUtil.SPLIT_TAG + newWaveNum.get());
monitor.setStatisticsResult(null);
monitor.setStatisticsNum(null);
break;
default:
break;
}
monitor.setQualifiedNum(qualifiedNum);
return this.updateById(monitor);
} else {
return true;
}
}
@Override
public Integer getDevCheckState(String devId) {
QueryWrapper<PqMonitor> wrapper = new QueryWrapper<>();
wrapper.eq("pq_monitor.Dev_Id", devId)
.eq("pq_monitor.Check_Flag", 1);
List<PqMonitor> monitorList = this.list(wrapper);
if (CollUtil.isNotEmpty(monitorList)) {
List<Integer> allNumList = monitorList.stream().map(PqMonitor::getNum).collect(Collectors.toList());
List<Integer> checkedNumList = monitorList.stream().filter(obj -> ObjectUtil.isNotNull(obj.getResultType())).map(PqMonitor::getNum).collect(Collectors.toList());
if (checkedNumList.containsAll(allNumList)) {
return CheckStateEnum.CHECKED.getValue();
} else {
if (checkedNumList.size() > 0) {
return CheckStateEnum.CHECKING.getValue();
}
}
}
return CheckStateEnum.UNCHECKED.getValue();
}
@Override
public Integer getDevCheckResult(String devId) {
QueryWrapper<PqMonitor> wrapper = new QueryWrapper<>();
wrapper.eq("pq_monitor.Dev_Id", devId)
.eq("pq_monitor.Check_Flag", 1);
List<PqMonitor> monitorList = this.list(wrapper);
List<Integer> allResultFlags = new ArrayList<>();
for (PqMonitor monitor : monitorList) {
String resultType = monitor.getResultType();
if (StrUtil.isNotBlank(resultType)) {
if (DataSourceEnum.REAL_DATA.getValue().equals(resultType)) {
allResultFlags.add(monitor.getRealtimeResult());
} else if (DataSourceEnum.WAVE_DATA.getValue().equals(resultType)) {
allResultFlags.add(monitor.getRecordedResult());
} else {
allResultFlags.add(monitor.getStatisticsResult());
}
}
}
return DetectionDataServiceImpl.isResultFlag(allResultFlags);
}
}

View File

@@ -1,7 +1,9 @@
package com.njcn.gather.plan.controller;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.njcn.common.pojo.annotation.OperateInfo;
import com.njcn.common.pojo.constant.OperateType;
@@ -13,15 +15,19 @@ import com.njcn.common.pojo.response.HttpResult;
import com.njcn.common.utils.LogUtil;
import com.njcn.gather.device.pojo.enums.CommonEnum;
import com.njcn.gather.device.pojo.param.PqDevParam;
import com.njcn.gather.device.pojo.po.PqDev;
import com.njcn.gather.device.pojo.po.PqStandardDev;
import com.njcn.gather.device.pojo.vo.PqDevVO;
import com.njcn.gather.device.service.IPqDevService;
import com.njcn.gather.plan.pojo.param.AdPlanParam;
import com.njcn.gather.plan.pojo.po.AdPlan;
import com.njcn.gather.plan.pojo.vo.AdPlanVO;
import com.njcn.gather.plan.service.AsyncPlanHandler;
import com.njcn.gather.plan.service.IAdPlanService;
import com.njcn.gather.type.pojo.po.DevType;
import com.njcn.gather.type.service.IDevTypeService;
import com.njcn.gather.user.user.pojo.po.SysUser;
import com.njcn.gather.user.user.service.ISysUserService;
import com.njcn.web.controller.BaseController;
import com.njcn.web.utils.FileUtil;
import com.njcn.web.utils.HttpResultUtil;
@@ -36,10 +42,13 @@ import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.http.HttpServletResponse;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import static com.njcn.web.utils.RequestUtil.getUserId;
/**
* @author caozehui
* @date 2024-12-09
@@ -54,6 +63,8 @@ public class AdPlanController extends BaseController {
private final IAdPlanService adPlanService;
private final IPqDevService pqDevService;
private final IDevTypeService devTypeService;
private final ISysUserService sysUserService;
private final AsyncPlanHandler asyncPlanHandler;
@OperateInfo(info = LogEnum.BUSINESS_COMMON)
@PostMapping("/list")
@@ -171,7 +182,7 @@ public class AdPlanController extends BaseController {
public HttpResult<List<Map<String, String>>> getBigTestItem(@RequestBody AdPlanParam.CheckParam checkParam) {
String methodDescribe = getMethodDescribe("getBigTestItem");
LogUtil.njcnDebug(log, "{},查询数据为:{}", methodDescribe, checkParam);
List<Map<String, String>> result = adPlanService.getBigTestItem(checkParam.getReCheckType(), checkParam.getPlanId(), checkParam.getDevIds());
List<Map<String, String>> result = adPlanService.getBigTestItem(checkParam.getReCheckType(), checkParam.getPlanId(), checkParam.getDevIds(), checkParam.getPatternId(), checkParam.getScriptType());
return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, result, methodDescribe);
}
@@ -291,11 +302,14 @@ public class AdPlanController extends BaseController {
@OperateInfo(info = LogEnum.BUSINESS_COMMON)
@GetMapping("/getBoundStandardDev")
@ApiOperation("根据计划ID获取已绑定的标准设备")
@ApiImplicitParam(name = "planId", value = "计划ID", required = true)
public HttpResult<List<PqStandardDev>> getBoundStandardDev(@RequestParam("planId") String planId) {
@ApiImplicitParams({
@ApiImplicitParam(name = "planId", value = "计划ID", required = true),
@ApiImplicitParam(name = "all", value = "是否获取所有")
})
public HttpResult<List<PqStandardDev>> getBoundStandardDev(@RequestParam("planId") String planId, @RequestParam(name = "all", required = false) Integer all) {
String methodDescribe = getMethodDescribe("getUnBoundStandardDev");
LogUtil.njcnDebug(log, "{}计划ID为{}", methodDescribe, planId);
List<PqStandardDev> result = adPlanService.getBoundStandardDev(planId);
LogUtil.njcnDebug(log, "{}计划ID为{},获取所有:{}", methodDescribe, planId, all);
List<PqStandardDev> result = adPlanService.getBoundStandardDev(planId, all);
return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, result, methodDescribe);
}
@@ -348,12 +362,98 @@ public class AdPlanController extends BaseController {
@OperateInfo(operateType = OperateType.DOWNLOAD)
@PostMapping("/exportSubPlan")
@ApiOperation("导出子计划")
@ApiOperation("导出子计划元信息")
@ApiImplicitParam(name = "planId", value = "子计划id", required = true)
public void exportSubPlan(@RequestParam("planId") String planId, HttpServletResponse response) {
String methodDescribe = getMethodDescribe("exportSubPlan");
LogUtil.njcnDebug(log, "{}导出ID数据为{}", methodDescribe, planId);
adPlanService.exportSubPlan(planId, response);
adPlanService.exportSubPlanDataZip(planId, response);
}
@OperateInfo(info = LogEnum.BUSINESS_COMMON, operateType = OperateType.UPLOAD)
@PostMapping(value = "/importSubPlan")
@ApiOperation("导入子计划元信息")
@ApiImplicitParams({
@ApiImplicitParam(name = "file", value = "检测计划数据文件", required = true),
@ApiImplicitParam(name = "patternId", value = "模式Id", required = true)
})
public HttpResult<Boolean> importSubPlan(@RequestParam("file") MultipartFile file, @RequestParam("patternId") String patternId, HttpServletResponse response) {
String methodDescribe = getMethodDescribe("importSubPlan");
LogUtil.njcnDebug(log, "{},上传文件为:{}", methodDescribe, file.getOriginalFilename());
boolean fileType = FileUtil.judgeFileIsZip(file.getOriginalFilename());
if (!fileType) {
CommonResponseEnum fileTypeError = CommonResponseEnum.FILE_XLSX_ERROR;
fileTypeError.setMessage("请上传zip文件");
throw new BusinessException(fileTypeError);
}
adPlanService.importSubPlanDataZip(file, patternId, response);
return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, true, methodDescribe);
}
@OperateInfo(operateType = OperateType.DOWNLOAD)
@PostMapping("/exportPlanCheckData")
@ApiOperation("导出计划检测结果数据")
@ApiImplicitParams({
@ApiImplicitParam(name = "planId", value = "计划id", required = true),
@ApiImplicitParam(name = "devIds", value = "被检设备ids", required = true),
@ApiImplicitParam(name = "report", value = "是否导出报告, 0 否1 是", required = true)
})
public HttpResult<Boolean> exportPlanCheckData(@RequestParam("planId") String planId, @RequestParam("devIds") String devIds, @RequestParam("report") Integer report) {
String methodDescribe = getMethodDescribe("exportPlanCheckData");
LogUtil.njcnDebug(log, "{}导出计划ID数据为{} {} {}", methodDescribe, planId, devIds, report);
// 获取检测计划绑定的被检设备数据
List<PqDev> devList = pqDevService.list(new LambdaQueryWrapper<PqDev>().eq(PqDev::getPlanId, planId).in(PqDev::getId, StrUtil.split(devIds, StrUtil.COMMA)));
if (CollUtil.isEmpty(devList)) {
throw new BusinessException(CommonResponseEnum.FAIL, "选择的被检设备不存在");
}
asyncPlanHandler.exportPlanCheckDataZip(getUserId(), planId, devList, report);
return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, true, methodDescribe);
}
@OperateInfo(info = LogEnum.BUSINESS_COMMON)
@GetMapping("/getMemberList")
@ApiOperation("根据计划ID获取项目成员")
@ApiImplicitParam(name = "planId", value = "计划ID", required = true)
public HttpResult<List<SysUser>> getMemberList(@RequestParam("planId") String planId) {
String methodDescribe = getMethodDescribe("getMemberList");
LogUtil.njcnDebug(log, "{}计划ID为{},获取项目成员", methodDescribe, planId);
AdPlan plan = adPlanService.getById(planId);
List<SysUser> result = new ArrayList<>();
if (plan != null) {
String members = plan.getMembers();
if (StrUtil.isNotBlank(members)) {
List<String> memberIds = StrUtil.split(members, StrUtil.COMMA);
result = sysUserService.listByIds(memberIds);
result.forEach(member -> member.setPassword(null));
}
}
return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, result, methodDescribe);
}
@OperateInfo(info = LogEnum.BUSINESS_COMMON, operateType = OperateType.UPLOAD)
@PostMapping(value = "/importAndMergePlanCheckData")
@ApiOperation("合并计划检测结果数据")
@ApiImplicitParams({
@ApiImplicitParam(name = "file", value = "检测计划检测结果数据文件", required = true),
@ApiImplicitParam(name = "planId", value = "计划Id", required = true)
})
public HttpResult<Boolean> importAndMergePlanCheckData(@RequestParam("file") MultipartFile file, @RequestParam("planId") String planId) {
String methodDescribe = getMethodDescribe("importAndMergePlanCheckData");
LogUtil.njcnDebug(log, "{}合并计划ID检测数据为{}", methodDescribe, planId);
boolean fileType = FileUtil.judgeFileIsZip(file.getOriginalFilename());
if (!fileType) {
CommonResponseEnum fileTypeError = CommonResponseEnum.FILE_XLSX_ERROR;
fileTypeError.setMessage("请上传zip文件");
throw new BusinessException(fileTypeError);
}
asyncPlanHandler.importAndMergePlanCheckData(file, getUserId(), planId);
return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, true, methodDescribe);
}
}

View File

@@ -0,0 +1,38 @@
package com.njcn.gather.plan.controller;
import com.njcn.gather.plan.service.SseClient;
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 org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import static com.njcn.web.utils.RequestUtil.getUserId;
/**
* SSE控制器用于处理SSE相关的请求。
*/
@Slf4j
@RequiredArgsConstructor
@RequestMapping("/sse")
@RestController
public class SseController {
private final SseClient sseClient;
/**
* 创建SSE连接。
*
* @return SseEmitter对象用于与客户端建立SSE连接。
*/
@GetMapping("/createSse")
public SseEmitter createConnect() {
String uid = getUserId();
SseEmitter sse = sseClient.createSse(uid);
log.info("为用户UID: {} 创建SSE连接", uid);
return sse;
}
}

View File

@@ -0,0 +1,16 @@
package com.njcn.gather.plan.mapper;
import com.github.yulichang.base.MPJBaseMapper;
import com.njcn.gather.plan.pojo.po.AdPlanTestConfig;
/**
* @author stary
* @date 2025-08-25
*/
public interface AdPlanTestConfigMapper extends MPJBaseMapper<AdPlanTestConfig> {
}

View File

@@ -2,19 +2,21 @@ package com.njcn.gather.plan.pojo.enums;
import lombok.Getter;
import java.util.Objects;
/**
* @author caozehui
* @data 2024-12-12
*/
@Getter
public enum DataSourceEnum {
REAL_DATA("real", "3s实时数据"),
REAL_DATA("real", "3s数据150周波数据"),
MINUTE_STATISTICS_MAX("max", "分钟统计数据-最大"),
MINUTE_STATISTICS_MIN("min", "分钟统计数据-最小"),
MINUTE_STATISTICS_AVG("avg", "分钟统计数据-平均"),
MINUTE_STATISTICS_CP95("cp95", "分钟统计数据-CP95"),
RECORDED_DATA("Recorded_data", "数据");
MINUTE_STATISTICS_MAX("max", "分钟统计数据-最大"),
MINUTE_STATISTICS_MIN("min", "分钟统计数据-最小"),
MINUTE_STATISTICS_AVG("avg", "分钟统计数据-平均"),
MINUTE_STATISTICS_CP95("cp95", "分钟统计数据-CP95"),
WAVE_DATA("wave_data", "数据");
private String value;
private String msg;
@@ -24,21 +26,29 @@ public enum DataSourceEnum {
this.msg = msg;
}
public static String getMsgByValue(String value) {
public static DataSourceEnum ofByValue(String value) {
for (DataSourceEnum dataSourceEnum : DataSourceEnum.values()) {
if (dataSourceEnum.getValue().equals(value)) {
return dataSourceEnum.getMsg();
return dataSourceEnum;
}
}
return null;
}
public static String getValueByMsg(String msg) {
public static DataSourceEnum ofByMsg(String msg) {
for (DataSourceEnum dataSourceEnum : DataSourceEnum.values()) {
if (dataSourceEnum.getMsg().equals(msg)) {
return dataSourceEnum.getValue();
return dataSourceEnum;
}
}
return null;
}
public static String getMsgByValue(String value) {
return Objects.requireNonNull(ofByValue(value)).getMsg();
}
public static String getValueByMsg(String msg) {
return Objects.requireNonNull(ofByMsg(msg)).getMsg();
}
}

View File

@@ -1,6 +1,7 @@
package com.njcn.gather.plan.pojo.param;
import com.njcn.common.pojo.constant.PatternRegex;
import com.njcn.gather.plan.pojo.po.AdPlanTestConfig;
import com.njcn.gather.pojo.constant.DetectionValidMessage;
import com.njcn.web.pojo.param.BaseParam;
import io.swagger.annotations.ApiModelProperty;
@@ -69,8 +70,16 @@ public class AdPlanParam {
private List<String> standardDevIds;
@ApiModelProperty(value = "测试项ID列表")
private List<@Pattern(regexp = PatternRegex.SYSTEM_ID, message = DetectionValidMessage.SOURCE_ID_FORMAT_ERROR)String> testItems;
private List<@Pattern(regexp = PatternRegex.SYSTEM_ID, message = DetectionValidMessage.SOURCE_ID_FORMAT_ERROR) String> testItems;
@ApiModelProperty(value = "检测配置")
private AdPlanTestConfig testConfig;
@ApiModelProperty(value = "检测负责人")
private String leader;
@ApiModelProperty(value = "检测成员")
private List<String> memberIds;
/**
* 分页查询实体
@@ -118,5 +127,7 @@ public class AdPlanParam {
private Integer reCheckType;
private String planId;
private List<String> devIds;
private String patternId;
private String scriptType;
}
}

View File

@@ -81,6 +81,9 @@ public class AdPlan extends BaseEntity implements Serializable {
/**
* 是否关联报告0-不关联 1-关联
* 目前这个阶段
* 0-不关联的是采用替换占位符的方式生成测试报告
* 1-关联的是采用模板生成测试报告
*/
private Integer associateReport;
@@ -110,5 +113,22 @@ public class AdPlan extends BaseEntity implements Serializable {
* 来源
*/
private String origin;
/**
* 是否为导入(比对式使用) 0-否 1-是
*/
private Integer importFlag;
/**
* 检测负责人
*/
private String leader;
/**
* 检测成员
*/
private String members;
}

View File

@@ -0,0 +1,51 @@
package com.njcn.gather.plan.pojo.po;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
/**
* @author stary
* @date 2025-08-25
*/
@Data
@TableName("ad_plan_test_config")
@AllArgsConstructor
@NoArgsConstructor
public class AdPlanTestConfig implements Serializable {
private static final long serialVersionUID = -796292730578249530L;
/**
* 检测计划表Id
*/
private String planId;
/**
* 录波数据有效组数
*/
private Integer waveRecord;
/**
* 实时数据有效组数
*/
private Integer realTime;
/**
* 统计数据有效组数
*/
private Integer statistics;
/**
* 短闪数据有效组数
*/
private Integer flicker;
/**
* 最大检测次数默认3次
*/
private Integer maxTime;
}

View File

@@ -0,0 +1,24 @@
package com.njcn.gather.plan.pojo.vo;
import com.njcn.gather.detection.pojo.po.AdPair;
import com.njcn.gather.device.pojo.po.PqDev;
import com.njcn.gather.device.pojo.po.PqDevSub;
import com.njcn.gather.monitor.pojo.po.PqMonitor;
import com.njcn.gather.plan.pojo.po.AdPlan;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.util.List;
@Data
@EqualsAndHashCode(callSuper = false)
public class AdPlanCheckDataVO {
private AdPlan plan;
private List<PqDev> devList;
private List<PqDevSub> devSubList;
private List<AdPair> pairList;
private List<PqMonitor> monitorList;
private List<String> devMonitorIds;
private int dataBatch;
}

View File

@@ -1,6 +1,7 @@
package com.njcn.gather.plan.pojo.vo;
import com.njcn.gather.device.pojo.vo.PqDevVO;
import com.njcn.gather.plan.pojo.po.AdPlanTestConfig;
import lombok.Data;
import lombok.EqualsAndHashCode;
@@ -149,4 +150,22 @@ public class AdPlanVO {
* 来源
*/
private String origin;
private AdPlanTestConfig testConfig;
/**
* 是否导入0-否 1-是
*/
private Integer importFlag;
/**
* 检测负责人
*/
private String leader;
private String leaderName;
/**
* 检测成员
*/
private String members;
private String membersName;
}

View File

@@ -0,0 +1,45 @@
package com.njcn.gather.plan.pojo.vo;
import com.njcn.gather.device.pojo.po.PqDev;
import com.njcn.gather.device.pojo.po.PqStandardDev;
import com.njcn.gather.err.pojo.po.PqErrSys;
import com.njcn.gather.err.pojo.po.PqErrSysDtls;
import com.njcn.gather.icd.pojo.po.PqIcdPath;
import com.njcn.gather.plan.pojo.po.AdPlan;
import com.njcn.gather.plan.pojo.po.AdPlanTestConfig;
import com.njcn.gather.report.pojo.po.PqReport;
import com.njcn.gather.system.dictionary.pojo.po.DictData;
import com.njcn.gather.system.dictionary.pojo.po.DictTree;
import com.njcn.gather.system.dictionary.pojo.po.DictType;
import com.njcn.gather.type.pojo.po.DevType;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.util.List;
@Data
@EqualsAndHashCode(callSuper = false)
public class AdSubPlanMetaDataVO {
private Dict dict;
private AdPlan plan;
private AdPlanTestConfig testConfig;
private List<PqDev> devList;
private List<PqStandardDev> standardDevList;
private List<DevType> devTypeList;
private List<PqIcdPath> icdPathList;
private List<PqErrSys> errSysList;
private List<PqErrSysDtls> errSysDtlsList;
private PqReport reportTemplate;
@Data
@EqualsAndHashCode(callSuper = false)
public static class Dict{
private List<DictType> typeList;
private List<DictData> dataList;
private List<DictTree> treeList;
}
}

View File

@@ -0,0 +1,520 @@
package com.njcn.gather.plan.service;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.core.util.ZipUtil;
import cn.hutool.json.JSONConfig;
import cn.hutool.json.JSONUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.njcn.common.pojo.enums.response.CommonResponseEnum;
import com.njcn.gather.detection.pojo.po.AdPair;
import com.njcn.gather.detection.service.IAdPariService;
import com.njcn.gather.device.pojo.enums.CheckStateEnum;
import com.njcn.gather.device.pojo.po.PqDev;
import com.njcn.gather.device.pojo.po.PqDevSub;
import com.njcn.gather.device.service.IPqDevService;
import com.njcn.gather.device.service.IPqDevSubService;
import com.njcn.gather.monitor.pojo.po.PqMonitor;
import com.njcn.gather.monitor.service.IPqMonitorService;
import com.njcn.gather.plan.pojo.po.AdPlan;
import com.njcn.gather.plan.pojo.vo.AdPlanCheckDataVO;
import com.njcn.gather.plan.service.util.BatchFileReader;
import com.njcn.gather.system.config.handler.NonWebAutoFillValueHandler;
import com.njcn.gather.tools.report.model.constant.ReportConstant;
import com.njcn.gather.type.pojo.po.DevType;
import com.njcn.gather.type.service.IDevTypeService;
import com.njcn.web.utils.HttpResultUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.jdbc.core.BatchPreparedStatementSetter;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.time.Duration;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
@Slf4j
@EnableAsync
@RequiredArgsConstructor
@Component
public class AsyncPlanHandler {
private final SseClient sseClient;
private final IAdPlanService adPlanService;
private final IPqDevService pqDevService;
private final IDevTypeService devTypeService;
private final IPqDevSubService pqDevSubService;
private final IPqMonitorService pqMonitorService;
private final IAdPariService adPairService;
private final JdbcTemplate jdbcTemplate;
@Value("${report.reportDir}")
private String reportPath;
@Value("${data.homeDir}")
private String dataPath;
private static final int BATCH_SIZE = 10000;
private static final int FINAL_STEP = 85;
private static final String TEST_DATA_DIR = "plan_test_data";
@Async
public void exportPlanCheckDataZip(String uid, String planId, List<PqDev> devList, Integer report) {
NonWebAutoFillValueHandler.setCurrentUserId(uid);
LocalDateTime startTime = LocalDateTime.now();
AdPlanCheckDataVO planCheckDataVO = new AdPlanCheckDataVO();
AtomicInteger progress = new AtomicInteger(0);
AtomicInteger currentProgress = new AtomicInteger(0);
sseClient.sendMessage(uid, planId, HttpResultUtil.assembleResult(CommonResponseEnum.SUCCESS.getCode(), progress, "开始导出文件"));
// 获取检测计划基本数据
AdPlan plan = adPlanService.getById(planId);
planCheckDataVO.setPlan(plan);
planCheckDataVO.setDevList(devList);
List<String> devIdList = devList.stream().map(PqDev::getId).collect(Collectors.toList());
// 被检设备状态统计
List<PqDevSub> devSubList = pqDevSubService.list(new LambdaQueryWrapper<PqDevSub>().in(PqDevSub::getDevId, devIdList));
planCheckDataVO.setDevSubList(devSubList);
// 被检设备监测点信息
List<PqMonitor> monitorList = pqMonitorService.list(new LambdaQueryWrapper<PqMonitor>().in(PqMonitor::getDevId, devIdList));
planCheckDataVO.setMonitorList(monitorList);
// devMonitorId = 被检设备ID+通道号
List<String> devMonitorIds = new ArrayList<>();
for (PqDev dev : devList) {
List<String> channelNoList = StrUtil.split(dev.getInspectChannel(), StrUtil.COMMA);
for (String channelNo : channelNoList) {
devMonitorIds.add(dev.getId() + StrUtil.UNDERLINE + channelNo);
}
}
planCheckDataVO.setDevMonitorIds(devMonitorIds);
// 设备通道匹对关系
List<AdPair> pairList = adPairService.list(new LambdaQueryWrapper<AdPair>().eq(AdPair::getPlanId, planId).in(AdPair::getDevMonitorId, devMonitorIds));
planCheckDataVO.setPairList(pairList);
progress.addAndGet(1);
sseClient.sendMessage(uid, planId, HttpResultUtil.assembleResult(CommonResponseEnum.SUCCESS.getCode(), progress, "生成检测计划基本信息数据文件中,请耐心等待..."));
// 获取计划检测结果数据表以及数据
Integer code = plan.getCode();
List<String> dataTableNames = CollUtil.newArrayList("ad_harmonic_" + code, "ad_non_harmonic_" + code, "ad_harmonic_result_" + code, "ad_non_harmonic_result_" + code);
// 创建临时目录用于存储文件
File tempDataDir = FileUtil.mkdir(FileUtil.getTmpDirPath() + "plan_test_data_" + System.currentTimeMillis() + "/");
int dataBatch = 0;
int current = 0;
if (CollUtil.isNotEmpty(pairList)) {
for (String dataTableName : dataTableNames) {
// 创建数据文件
String fileName = dataTableName.replace("_" + code, "") + ".txt";
File dataFile = FileUtil.file(tempDataDir, fileName);
// 确保文件存在
FileUtil.touch(dataFile);
// 初始化写入标志,用于判断是否已写入字段名
boolean isFirstWrite = true;
// 分页查询,避免一次性加载大量数据
int pageSize = BATCH_SIZE; // 每页查询10000条记录
int offset = 0;
List<Map<String, Object>> pageData;
do {
dataBatch += 1;
if (current < FINAL_STEP + 5) {
current = Math.min(current + 1, FINAL_STEP + 5);
}
currentProgress.set(current);
sseClient.sendMessage(uid, planId, HttpResultUtil.assembleResult(CommonResponseEnum.SUCCESS.getCode(), progress.get() + currentProgress.get(), "生成检测结果数据文件中,请耐心等待..."));
String paginatedSql = buildPaginatedQuery(dataTableName, devMonitorIds, pageSize, offset);
pageData = jdbcTemplate.queryForList(paginatedSql);
// 将当前页数据追加到文件中
if (CollUtil.isNotEmpty(pageData)) {
StringBuilder content = new StringBuilder();
// 如果是第一次写入,先写入字段名
if (isFirstWrite) {
// 获取字段名
Map<String, Object> firstRow = pageData.get(0);
List<String> fieldNames = new ArrayList<>(firstRow.keySet());
// 写入字段名作为第一行
content.append(StrUtil.join("\t", fieldNames)).append(System.lineSeparator());
isFirstWrite = false;
}
// 写入数据行
for (Map<String, Object> data : pageData) {
List<Object> values = new ArrayList<>(data.values());
content.append(StrUtil.join("\t", values)).append(System.lineSeparator());
}
// 追加内容到文件
FileUtil.appendUtf8String(content.toString(), dataFile);
}
offset += pageSize;
} while (pageData.size() == pageSize); // 如果查询结果少于pageSize说明已经查询完所有数据
}
}
planCheckDataVO.setDataBatch(dataBatch);
int currentVal = progress.get() + currentProgress.get();
if (currentVal < FINAL_STEP + 5) {
progress.addAndGet(FINAL_STEP + 5);
} else {
progress.set(currentVal);
}
sseClient.sendMessage(uid, planId, HttpResultUtil.assembleResult(CommonResponseEnum.SUCCESS.getCode(), progress, "压缩检测结果数据文件中,请耐心等待..."));
// 导出数据.zip文件
String jsonStr = JSONUtil.toJsonStr(planCheckDataVO, new JSONConfig().setIgnoreNullValue(false));
try {
// 创建 JSON 文件
String jsonFileName = plan.getName() + ".json";
File jsonFile = FileUtil.file(tempDataDir, jsonFileName);
FileUtil.writeUtf8String(jsonStr, jsonFile);
// 创建 ZIP 文件
String zipFileName = plan.getName() + "_检测数据包.zip";
File zipFile = FileUtil.file(dataPath + File.separator + TEST_DATA_DIR + File.separator, zipFileName);
// 添加检测报告文件
if (ObjectUtil.isNotNull(report) && report.equals(1)) {
for (PqDev dev : devList) {
DevType devType = devTypeService.getById(dev.getDevType());
String dirPath = reportPath.concat(File.separator).concat(devType.getName());
File reportFile = new File(dirPath.concat(File.separator).concat(dev.getCreateId()).concat(ReportConstant.DOCX));
// 如果reportFile存在则将reportFile中的文件添加到已有的zip文件中
if (FileUtil.exist(reportFile)) {
// 复制reportFile到临时目录
FileUtil.copy(reportFile, tempDataDir, true);
}
}
}
// 创建zip文件包含所有文件
ZipUtil.zip(tempDataDir.getAbsolutePath(), zipFile.getAbsolutePath());
// 删除临时目录
FileUtil.del(tempDataDir);
LocalDateTime endTime = LocalDateTime.now();
log.info("生成数据包完成,耗时: {}s", Duration.between(startTime, endTime).getSeconds());
progress.set(100);
sseClient.sendMessage(uid, planId, HttpResultUtil.assembleResult(CommonResponseEnum.SUCCESS.getCode(), progress, zipFile.getAbsolutePath()));
} catch (Exception e) {
log.error("生成数据包失败", e);
sseClient.sendMessage(uid, planId, HttpResultUtil.assembleResult(CommonResponseEnum.FAIL.getCode(), progress.get() + currentProgress.get(), "生成数据包失败"));
} finally {
NonWebAutoFillValueHandler.clearCurrentUserId();
}
sseClient.closeSse(uid);
}
@Transactional
@Async
public void importAndMergePlanCheckData(MultipartFile file, String uid, String planId) {
NonWebAutoFillValueHandler.setCurrentUserId(uid);
LocalDateTime startTime = LocalDateTime.now();
AtomicInteger progress = new AtomicInteger(0);
AtomicInteger currentProgress = new AtomicInteger(0);
AtomicInteger dataCount = new AtomicInteger(0);
try {
sseClient.sendMessage(uid, planId, HttpResultUtil.assembleResult(CommonResponseEnum.SUCCESS.getCode(), progress, "开始保存文件,请耐心等待..."));
// 创建临时目录用于解压文件
File tempDir = FileUtil.mkdir(FileUtil.getTmpDirPath() + "import_plan_check_data_" + System.currentTimeMillis() + "/");
// 将上传的zip文件保存到临时目录
File zipFile = FileUtil.file(tempDir, file.getOriginalFilename());
file.transferTo(zipFile);
progress.addAndGet(1);
sseClient.sendMessage(uid, planId, HttpResultUtil.assembleResult(CommonResponseEnum.SUCCESS.getCode(), progress, "开始解压文件,请耐心等待..."));
// 解压zip文件
File unzipDir = FileUtil.mkdir(FileUtil.file(tempDir, "unzip"));
ZipUtil.unzip(zipFile.getAbsolutePath(), unzipDir.getAbsolutePath());
// 查找解压目录中的json文件
File[] files = unzipDir.listFiles();
AdPlanCheckDataVO planCheckDataVO = null;
List<File> dataFiles = new ArrayList<>();
List<File> docxFiles = new ArrayList<>();
if (files != null) {
for (File f : files) {
if (f.isFile()) {
// 读取json文件内容
if (f.getName().endsWith(".json")) {
String jsonStr = FileUtil.readUtf8String(f);
planCheckDataVO = JSONUtil.toBean(jsonStr, AdPlanCheckDataVO.class);
} else if (f.getName().endsWith(".docx")) {
docxFiles.add(f);
} else if (f.getName().endsWith(".txt")) {
dataFiles.add(f);
}
}
}
}
if (planCheckDataVO == null) {
FileUtil.del(tempDir);
sseClient.sendMessage(uid, planId, HttpResultUtil.assembleResult(CommonResponseEnum.FAIL.getCode(), progress, "ZIP文件中未找到JSON数据文件"));
return;
}
AdPlan checkPlan = planCheckDataVO.getPlan();
if (checkPlan == null) {
FileUtil.del(tempDir);
sseClient.sendMessage(uid, planId, HttpResultUtil.assembleResult(CommonResponseEnum.FAIL.getCode(), progress, "ZIP文件中未找到检测计划信息"));
return;
}
List<PqDev> devList = planCheckDataVO.getDevList();
if (CollUtil.isEmpty(devList)) {
FileUtil.del(tempDir);
sseClient.sendMessage(uid, planId, HttpResultUtil.assembleResult(CommonResponseEnum.FAIL.getCode(), progress, "ZIP文件中未找到被检设备信息"));
return;
}
AdPlan subPlan = adPlanService.getById(checkPlan.getId());
if (subPlan == null) {
FileUtil.del(tempDir);
sseClient.sendMessage(uid, planId, HttpResultUtil.assembleResult(CommonResponseEnum.FAIL.getCode(), progress, "该子计划不存在"));
return;
}
if (!StrUtil.equals(planId, subPlan.getFatherPlanId())) {
FileUtil.del(tempDir);
sseClient.sendMessage(uid, planId, HttpResultUtil.assembleResult(CommonResponseEnum.FAIL.getCode(), progress, "非当前检修计划的子计划"));
return;
}
progress.addAndGet(1);
sseClient.sendMessage(uid, planId, HttpResultUtil.assembleResult(CommonResponseEnum.SUCCESS.getCode(), progress, "开始同步计划基本信息,请耐心等待..."));
// 更新检测计划几个状态字段
subPlan.setTestState(checkPlan.getTestState());
subPlan.setReportState(checkPlan.getReportState());
subPlan.setResult(checkPlan.getResult());
adPlanService.updateById(subPlan);
progress.addAndGet(1);
sseClient.sendMessage(uid, planId, HttpResultUtil.assembleResult(CommonResponseEnum.SUCCESS.getCode(), progress, "开始同步计划设备信息,请耐心等待..."));
// 批量更新被检设备信息
// 不更新导入标志
devList.forEach(dev -> dev.setImportFlag(null));
pqDevService.updateBatchById(devList);
List<PqDevSub> devSubList = planCheckDataVO.getDevSubList();
for (PqDevSub devSub : devSubList) {
pqDevSubService.update(devSub, new LambdaUpdateWrapper<PqDevSub>().eq(PqDevSub::getDevId, devSub.getDevId()));
}
progress.addAndGet(1);
sseClient.sendMessage(uid, planId, HttpResultUtil.assembleResult(CommonResponseEnum.SUCCESS.getCode(), progress, "开始同步通道配对信息,请耐心等待..."));
// 同步检测数据
List<AdPair> pairList = planCheckDataVO.getPairList();
adPairService.updateBatchById(pairList);
// 主计划
AdPlan plan = adPlanService.getById(planId);
if (CollUtil.isNotEmpty(docxFiles)) {
progress.addAndGet(1);
sseClient.sendMessage(uid, planId, HttpResultUtil.assembleResult(CommonResponseEnum.SUCCESS.getCode(), progress, "开始同步检测报告文件"));
for (File docx : docxFiles) {
for (PqDev dev : devList) {
DevType devType = devTypeService.getById(dev.getDevType());
String dirPath = reportPath.concat(File.separator).concat(devType.getName());
File reportFile = new File(dirPath.concat(File.separator).concat(dev.getCreateId()).concat(ReportConstant.DOCX));
// 文件名匹配,复制到对应目录下
if (docx.getName().equals(reportFile.getName())) {
File parentDir = FileUtil.mkParentDirs(reportFile);
FileUtil.copy(docx, parentDir, true);
}
}
}
}
if (CollUtil.isNotEmpty(dataFiles)) {
Integer planCode = plan.getCode();
progress.addAndGet(1);
sseClient.sendMessage(uid, planId, HttpResultUtil.assembleResult(CommonResponseEnum.SUCCESS.getCode(), progress, "开始同步检测数据信息,请耐心等待..."));
// 合并前清除相关表数据
String mainHarmonicTableName = "ad_harmonic_" + planCode;
String mainNonHarmonicTableName = "ad_non_harmonic_" + planCode;
String mainHarmonicResultTableName = "ad_harmonic_result_" + planCode;
String mainNonHarmonicResultTableName = "ad_non_harmonic_result_" + planCode;
List<String> devMonitorIds = planCheckDataVO.getDevMonitorIds();
if (CollUtil.isNotEmpty(devMonitorIds)) {
// 使用 StringBuilder 构建带引号的ID列表防止SQL注入
StringBuilder sb = new StringBuilder();
for (int i = 0; i < devMonitorIds.size(); i++) {
if (i > 0) sb.append(",");
sb.append("'").append(devMonitorIds.get(i)).append("'");
}
String devMonitorIdsStr = sb.toString();
jdbcTemplate.update("DELETE FROM " + mainHarmonicTableName + " WHERE dev_monitor_id IN (" + devMonitorIdsStr + ")");
jdbcTemplate.update("DELETE FROM " + mainNonHarmonicTableName + " WHERE dev_monitor_id IN (" + devMonitorIdsStr + ")");
jdbcTemplate.update("DELETE FROM " + mainHarmonicResultTableName + " WHERE dev_monitor_id IN (" + devMonitorIdsStr + ")");
jdbcTemplate.update("DELETE FROM " + mainNonHarmonicResultTableName + " WHERE dev_monitor_id IN (" + devMonitorIdsStr + ")");
}
int dataBatch = planCheckDataVO.getDataBatch();
int stepCount = dataBatch * BATCH_SIZE / FINAL_STEP;
for (File dataFile : dataFiles) {
// 直接插入主计划表中
String fileName = FileUtil.mainName(dataFile);
String tableName = fileName + StrUtil.UNDERLINE + planCode;
// 使用BatchFileReader分批处理文件
final boolean[] isFirstBatch = {true};
final String[] headers = {null};
BatchFileReader.readLinesInBatches(dataFile, BATCH_SIZE, lines -> {
dataCount.addAndGet(lines.size());
// 计算当前进度
int current = dataCount.get() / stepCount;
// 确保进度不超过finalStep
if (current > FINAL_STEP) {
current = FINAL_STEP;
}
currentProgress.set(current);
sseClient.sendMessage(uid, planId, HttpResultUtil.assembleResult(CommonResponseEnum.SUCCESS.getCode(), progress.get() + currentProgress.get(), "同步检测数据信息中,请耐心等待..."));
if (CollUtil.isNotEmpty(lines)) {
if (isFirstBatch[0]) {
// 第一批次的第一行为字段名
headers[0] = lines.get(0);
// 处理剩余行作为数据
if (lines.size() > 1) {
List<String> dataLines = lines.subList(1, lines.size());
processBatchData(tableName, headers[0], dataLines);
}
isFirstBatch[0] = false;
} else {
// 后续批次全部为数据行
processBatchData(tableName, headers[0], lines);
}
}
});
progress.addAndGet(1);
sseClient.sendMessage(uid, planId, HttpResultUtil.assembleResult(CommonResponseEnum.SUCCESS.getCode(), progress.get() + currentProgress.get(), "" + tableName + " 数据同步完成"));
}
}
// 删除临时目录
FileUtil.del(tempDir);
// 更新主计划状态
List<String> planIds = adPlanService.lambdaQuery().eq(AdPlan::getFatherPlanId, planId).list().stream().map(AdPlan::getId).collect(Collectors.toList());
planIds.add(planId);
List<String> devIds = pqDevService.lambdaQuery().in(PqDev::getPlanId, planIds).list().stream().map(PqDev::getId).collect(Collectors.toList());
List<PqDevSub> devSubs = pqDevSubService.lambdaQuery().in(PqDevSub::getDevId, devIds).list();
long checkedCount = devSubs.stream().filter(sub -> sub.getCheckState().equals(CheckStateEnum.CHECKED.getValue())).count();
if (checkedCount > 0) {
plan.setTestState(CheckStateEnum.CHECKING.getValue());
// 都已检测完成
if (checkedCount == devSubs.size()) {
plan.setTestState(CheckStateEnum.CHECKED.getValue());
}
} else {
plan.setTestState(CheckStateEnum.UNCHECKED.getValue());
// 是否有检测中
long checkingCount = devSubs.stream().filter(sub -> sub.getCheckState().equals(CheckStateEnum.CHECKING.getValue())).count();
if (checkingCount > 0) {
plan.setTestState(CheckStateEnum.CHECKING.getValue());
}
}
adPlanService.updateById(plan);
LocalDateTime endTime = LocalDateTime.now();
log.info("数据合并完成,耗时:{}s", Duration.between(startTime, endTime).getSeconds());
progress.set(100);
sseClient.sendMessage(uid, planId, HttpResultUtil.assembleResult(CommonResponseEnum.SUCCESS.getCode(), progress, "数据合并完成"));
} catch (Exception e) {
log.error("导入数据失败", e);
sseClient.sendMessage(uid, planId, HttpResultUtil.assembleResult(CommonResponseEnum.FAIL.getCode(), progress.get() + currentProgress.get(), "导入失败"));
} finally {
NonWebAutoFillValueHandler.clearCurrentUserId();
}
sseClient.closeSse(uid);
}
// 构建分页查询SQL
private String buildPaginatedQuery(String tableName, List<String> devMonitorIds, int limit, int offset) {
StringBuilder sql = new StringBuilder("SELECT * FROM " + tableName);
sql.append(" WHERE Dev_Monitor_Id IN (");
for (int i = 0; i < devMonitorIds.size(); i++) {
sql.append("'").append(devMonitorIds.get(i)).append("'");
if (i < devMonitorIds.size() - 1) {
sql.append(",");
}
}
sql.append(")");
sql.append(" ORDER BY Id");
// 添加分页限制
sql.append(" LIMIT ").append(limit).append(" OFFSET ").append(offset);
return sql.toString();
}
/**
* 处理数据行
*
* @param tableName 表名
* @param headers 表头
* @param lines 数据行
*/
private void processBatchData(String tableName, String headers, List<String> lines) {
if (lines == null || lines.isEmpty()) {
return;
}
// 构建INSERT语句
String[] headerArray = headers.split("\t");
String columnNames = String.join(",", headerArray);
String placeholders = String.join(",", java.util.Collections.nCopies(headerArray.length, "?"));
String sql = "INSERT INTO " + tableName + " (" + columnNames + ") VALUES (" + placeholders + ")";
// 批量插入数据
jdbcTemplate.batchUpdate(sql, new BatchPreparedStatementSetter() {
@Override
public void setValues(java.sql.PreparedStatement ps, int i) throws java.sql.SQLException {
String line = lines.get(i);
String[] fields = line.split("\t", -1); // 使用-1限制以保留空值
for (int j = 0; j < fields.length && j < headerArray.length; j++) {
String value = fields[j];
if (StrUtil.isEmpty(value) || StrUtil.equals(value, StrUtil.NULL)) {
ps.setNull(j + 1, java.sql.Types.VARCHAR);
} else {
ps.setString(j + 1, value);
}
}
}
@Override
public int getBatchSize() {
return lines.size();
}
});
}
}

View File

@@ -1,12 +1,10 @@
package com.njcn.gather.plan.service;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.IService;
import com.njcn.gather.device.pojo.param.PqDevParam;
import com.njcn.gather.device.pojo.po.PqStandardDev;
import com.njcn.gather.plan.pojo.param.AdPlanParam;
import com.njcn.gather.plan.pojo.po.AdPlan;
import com.njcn.gather.plan.pojo.vo.AdPlanExcel;
import com.njcn.gather.plan.pojo.vo.AdPlanVO;
import org.springframework.web.multipart.MultipartFile;
@@ -47,12 +45,11 @@ public interface IAdPlanService extends IService<AdPlan> {
/**
* 删除检测计划
*
* @param ids 检测计划id列表
* @param ids 检测计划id列表
* @param pattern 模式Id
*
* @return 删除成功则返回true否则返回false
*/
boolean deleteAdPlan(List<String> ids,String pattern);
boolean deleteAdPlan(List<String> ids, String pattern);
/**
* 根据模式查询检测计划
@@ -72,12 +69,14 @@ public interface IAdPlanService extends IService<AdPlan> {
/**
* 获取检测大项
*
* @param reCheckType 0:不合格项复检 1:全部复检
* @param planId 检测计划Id
* @param devIds 设备Id列表
* @param reCheckType 0:不合格项复检 1:全部复检
* @param planId 检测计划Id
* @param devIds 设备Id列表
* @param patternId 模式Id
* @param scriptType 脚本类型
* @return
*/
List<Map<String, String>> getBigTestItem(Integer reCheckType, String planId, List<String> devIds);
List<Map<String, String>> getBigTestItem(Integer reCheckType, String planId, List<String> devIds, String patternId, String scriptType);
/**
* 修改计划状态
@@ -142,16 +141,16 @@ public interface IAdPlanService extends IService<AdPlan> {
* 根据计划Id获取已绑定标准设备
*
* @param planId
* @param all
* @return
*/
List<PqStandardDev> getBoundStandardDev(String planId);
List<PqStandardDev> getBoundStandardDev(String planId, Integer all);
/**
* 修改子计划名称
*
* @param planId
* @param name
*
* @return
*/
boolean updateSubPlanName(String planId, String name);
@@ -159,8 +158,7 @@ public interface IAdPlanService extends IService<AdPlan> {
/**
* 子计划绑定/解绑被检设备
*
* @param planId
* @param pqDevIds
* @param param
* @return
*/
boolean updateBindDev(PqDevParam.BindPlanParam param);
@@ -175,10 +173,36 @@ public interface IAdPlanService extends IService<AdPlan> {
boolean updateBindStandardDev(String planId, List<String> standardDevIds);
/**
* 根据计划Id导出子计划数据
* 项目负责人导出子计划元信息
*
* @param planId
* @param response
*/
void exportSubPlan(String planId, HttpServletResponse response);
void exportSubPlanDataZip(String planId, HttpServletResponse response);
/**
* 项目成员导入子计划元信息
*
* @param file
* @param patternId
* @param response
*/
boolean importSubPlanDataZip(MultipartFile file, String patternId, HttpServletResponse response);
/**
* 导出计划检测结果数据
*
* @param planId
* @param devIds
* @param report
* @param response
*/
void exportPlanCheckDataZip(String planId, List<String> devIds, Integer report, HttpServletResponse response);
/**
* 比对模式下计划的检测大项获取
* @param planId 计划ID
* @return 检测项集合
*/
List<String> getScriptListContrast(String planId);
}

View File

@@ -0,0 +1,19 @@
package com.njcn.gather.plan.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.njcn.gather.plan.pojo.po.AdPlanTestConfig;
/**
* @author stary
* @date 2025-08-25
*/
public interface IAdPlanTestConfigService extends IService<AdPlanTestConfig> {
/**
* 根据计划id获取测试配置
*
* @param planId 计划id
* @return
*/
AdPlanTestConfig getByPlanId(String planId);
}

View File

@@ -0,0 +1,136 @@
package com.njcn.gather.plan.service;
import cn.hutool.core.util.ObjectUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import java.io.IOException;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@Slf4j
@Component
public class SseClient {
private static final Map<String, SseEmitter> sseEmitterMap = new ConcurrentHashMap<>();
private static final int RECONNECT_DELAY = 5000; // 重连延迟时间5秒
/**
* 创建SSE连接
*
* @param uid 用户唯一标识
* @return SseEmitter 实例
*/
public SseEmitter createSse(String uid) {
// 创建一个永不超时的SseEmitter
SseEmitter sseEmitter = new SseEmitter(0L);
sseEmitterMap.put(uid, sseEmitter);
// 完成后的回调记录日志并从map中移除
sseEmitter.onCompletion(() -> {
log.info("[{}]结束连接...................", uid);
sseEmitterMap.remove(uid);
});
// 超时回调,记录日志
sseEmitter.onTimeout(() -> {
log.info("[{}]连接超时,准备重连...................", uid);
scheduleReconnect(uid);
});
// 异常回调,记录日志并发送错误事件,然后重试
sseEmitter.onError(throwable -> {
log.error("[{}]连接异常,{}", uid, throwable.toString(), throwable);
try {
sseEmitter.send(SseEmitter.event()
.id(uid)
.name("发生异常!")
.data("发生异常请重试!")
.reconnectTime(RECONNECT_DELAY));
} catch (IOException e) {
log.error("[{}]发送错误事件失败", uid, e);
}
scheduleReconnect(uid);
});
try {
// 发送初始化事件,设置重连时间
sseEmitter.send(SseEmitter.event().reconnectTime(RECONNECT_DELAY));
} catch (IOException e) {
log.error("[{}]发送初始化事件失败", uid, e);
}
log.info("[{}]创建sse连接成功", uid);
return sseEmitter;
}
/**
* 给指定用户发送消息可以定时或在事件发生时调用sseEmitter.send()方法来发送事件。)
*
* @param uid 用户唯一标识
* @param messageId 消息ID
* @param message 消息内容
* @return 是否发送成功
*/
public boolean sendMessage(String uid, String messageId, Object message) {
if (ObjectUtil.isEmpty(message)) {
log.warn("参数异常message 为null");
return false;
}
SseEmitter sseEmitter = sseEmitterMap.get(uid);
if (sseEmitter == null) {
log.info("消息推送失败uid:[{}],没有创建连接,请重试。", uid);
return false;
}
try {
sseEmitter.send(SseEmitter.event().id(messageId).reconnectTime(1 * 60 * 1000L).data(message));
// log.info("用户{},消息id:{},推送成功:{}", uid, messageId, message);
return true;
} catch (IOException e) {
log.error("用户{},消息id:{},推送IO异常:{}", uid, messageId, e.getMessage());
// 客户端断开连接属于正常情况,不需要重连,直接移除连接
sseEmitterMap.remove(uid);
sseEmitter.complete();
return false;
} catch (Exception e) {
log.error("用户{},消息id:{},推送其他异常:{}", uid, messageId, e.getMessage(), e);
sseEmitter.complete();
scheduleReconnect(uid);
return false;
}
}
/**
* 断开SSE连接
*
* @param uid 用户唯一标识
*/
public void closeSse(String uid) {
SseEmitter sseEmitter = sseEmitterMap.remove(uid);
if (sseEmitter != null) {
sseEmitter.complete();
log.info("用户{} 连接已关闭", uid);
} else {
log.info("用户{} 连接已关闭,或连接不存在", uid);
}
}
/**
* 计划重连
*
* @param uid 用户唯一标识
*/
private void scheduleReconnect(String uid) {
// 这里可以添加一个定时任务来尝试重连
// 例如使用Spring的@Scheduled注解或者ScheduledExecutorService
log.info("[{}]计划在{}毫秒后重连", uid, RECONNECT_DELAY);
// 模拟重连操作,实际应用中应根据业务需求实现
try {
Thread.sleep(RECONNECT_DELAY);
createSse(uid);
} catch (InterruptedException e) {
log.error("[{}]重连操作被中断", uid, e);
}
}
}

View File

@@ -0,0 +1,25 @@
package com.njcn.gather.plan.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.njcn.gather.plan.mapper.AdPlanTestConfigMapper;
import com.njcn.gather.plan.pojo.po.AdPlanTestConfig;
import com.njcn.gather.plan.service.IAdPlanTestConfigService;
import org.springframework.stereotype.Service;
/**
* @author stary
* @date 2025-08-25
*/
@Service
public class AdPlanTestConfigServiceImpl extends ServiceImpl<AdPlanTestConfigMapper, AdPlanTestConfig>
implements IAdPlanTestConfigService {
@Override
public AdPlanTestConfig getByPlanId(String planId) {
return this.lambdaQuery().eq(AdPlanTestConfig::getPlanId, planId).last("LIMIT 1").one();
}
}

View File

@@ -0,0 +1,117 @@
package com.njcn.gather.plan.service.util;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.util.StrUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.*;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
/**
* 分批读取文件行的工具类
* 用于处理大文件,避免一次性加载整个文件到内存中导致内存溢出
*/
public class BatchFileReader {
private static final Logger log = LoggerFactory.getLogger(BatchFileReader.class);
/**
* 默认批次大小
*/
private static final int DEFAULT_BATCH_SIZE = 10000;
/**
* 分批读取文件行
*
* @param file 要读取的文件
* @param batchSize 批次大小
* @param consumer 处理每批数据的消费者函数
*/
public static void readLinesInBatches(File file, int batchSize, Consumer<List<String>> consumer) {
readLinesInBatches(file, batchSize, Charset.defaultCharset(), consumer);
}
/**
* 分批读取文件行
*
* @param file 要读取的文件
* @param batchSize 批次大小
* @param charset 文件编码
* @param consumer 处理每批数据的消费者函数
*/
public static void readLinesInBatches(File file, int batchSize, Charset charset, Consumer<List<String>> consumer) {
if (batchSize <= 0) {
batchSize = DEFAULT_BATCH_SIZE;
}
if (!FileUtil.exist(file)) {
log.warn("文件不存在: {}", file.getAbsolutePath());
return;
}
try (BufferedReader reader = IoUtil.getReader(new InputStreamReader(FileUtil.getInputStream(file), charset))) {
List<String> batchLines = new ArrayList<>(batchSize);
String line;
int count = 0;
while ((line = reader.readLine()) != null) {
batchLines.add(line);
count++;
// 当达到批次大小时,处理这一批数据
if (count % batchSize == 0) {
if (CollUtil.isNotEmpty(batchLines)) {
consumer.accept(batchLines);
batchLines = new ArrayList<>(batchSize);
}
}
}
// 处理最后一批数据(不足一批的数据)
if (CollUtil.isNotEmpty(batchLines)) {
consumer.accept(batchLines);
}
} catch (IOException e) {
log.error("读取文件失败: {}", file.getAbsolutePath(), e);
throw new RuntimeException("读取文件失败: " + file.getAbsolutePath(), e);
}
}
/**
* 分批读取文件行(使用默认批次大小)
*
* @param file 要读取的文件
* @param consumer 处理每批数据的消费者函数
*/
public static void readLinesInBatches(File file, Consumer<List<String>> consumer) {
readLinesInBatches(file, DEFAULT_BATCH_SIZE, consumer);
}
/**
* 分批读取文件行(使用默认编码)
*
* @param filePath 文件路径
* @param batchSize 批次大小
* @param consumer 处理每批数据的消费者函数
*/
public static void readLinesInBatches(String filePath, int batchSize, Consumer<List<String>> consumer) {
readLinesInBatches(FileUtil.file(filePath), batchSize, consumer);
}
/**
* 分批读取文件行(使用默认编码和批次大小)
*
* @param filePath 文件路径
* @param consumer 处理每批数据的消费者函数
*/
public static void readLinesInBatches(String filePath, Consumer<List<String>> consumer) {
readLinesInBatches(FileUtil.file(filePath), DEFAULT_BATCH_SIZE, consumer);
}
}

View File

@@ -199,4 +199,8 @@ public interface DetectionValidMessage {
String DEV_IDS_NOT_EMPTY = "被检设备不能为空";
String STANDARD_DEV_IDS_NOT_EMPTY = "标准设备不能为空";
String PAIRS_NOT_EMPTY = "配对关系不能为空";
String DEV_MONITOR_ID_NOT_BLANK = "设备监测点ID不能为空";
String DEV_MONITOR_RESULT_NOT_NULL = "设备监测点结果不能为空";
String DEV_MONITOR_RESULT_TYPE_NOT_BLANK = "设备监测点结果来源类型不能为空";
String DEV_MONITOR_RESULT_NUM_NOT_BLANK = "设备监测点结果使用批次不能为空";
}

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