From a9432d45345558444949f904d4e91e116fd6a0ff Mon Sep 17 00:00:00 2001 From: yexb <553699424@qq.com> Date: Wed, 15 Apr 2026 11:48:33 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B4=AA=E5=9C=A3=E6=96=87=E4=BD=A0=E6=98=AF?= =?UTF-8?q?=E5=A4=A7=E5=82=BB=E9=80=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AGENTS.md | 11 + README.md | 8 +- entrance/pom.xml | 5 + tools/README.md | 21 +- tools/pom.xml | 5 +- tools/wave-tool/pom.xml | 28 +++ .../tool/wave/controller/WaveController.java | 42 ++++ .../tool/wave/param/WaveParseParam.java | 36 +++ .../gather/tool/wave/service/WaveService.java | 15 ++ .../wave/service/impl/WaveServiceImpl.java | 214 ++++++++++++++++++ .../tool/wave/vo/WaveParseResultVO.java | 46 ++++ .../njcn/gather/tool/wave/vo/WavePointVO.java | 22 ++ 12 files changed, 445 insertions(+), 8 deletions(-) create mode 100644 tools/wave-tool/pom.xml create mode 100644 tools/wave-tool/src/main/java/com/njcn/gather/tool/wave/controller/WaveController.java create mode 100644 tools/wave-tool/src/main/java/com/njcn/gather/tool/wave/param/WaveParseParam.java create mode 100644 tools/wave-tool/src/main/java/com/njcn/gather/tool/wave/service/WaveService.java create mode 100644 tools/wave-tool/src/main/java/com/njcn/gather/tool/wave/service/impl/WaveServiceImpl.java create mode 100644 tools/wave-tool/src/main/java/com/njcn/gather/tool/wave/vo/WaveParseResultVO.java create mode 100644 tools/wave-tool/src/main/java/com/njcn/gather/tool/wave/vo/WavePointVO.java diff --git a/AGENTS.md b/AGENTS.md index 7fda346..1488a3c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -34,6 +34,17 @@ Java 源码位于 `src/main/java`,配置文件位于 `src/main/resources`,My ## 代码风格与命名规范 保持现有 Java 风格:4 空格缩进、UTF-8 文件编码、基础包名使用 `com.njcn.gather`。命名沿用分层后缀,如 `*Controller`、`*Service`、`*ServiceImpl`、`*Mapper`、`*Param`、`*PO`、`*VO`。优先复用现有 Lombok 注解,如 `@Data`、`@RequiredArgsConstructor`、`@Slf4j`。Mapper XML 文件名应与接口名保持一致。业务代码中,关键流程、分支判断、状态流转或容易误解的节点需要补充简洁的中文注释,但不要添加无信息量的注释。 +## 数据与 SQL 约束 +- 新增业务表的 DO 优先复用当前 `BaseDO` / 审计字段风格;除非表本身明确不需要逻辑删除,不要再引入另一套审计基类。 +- 不要假设运行时存在自动数据库迁移;如果代码依赖新表、新字段或新索引,必须同步补齐对应 SQL 与文档说明。 +- SQL 脚本应放在目标模块的 `src/main/resources/sql/...` 下,并保持可审阅、可单独执行、语义清晰。 +- 变更缓存、日志、审计相关逻辑时,优先沿用现有机制,不要绕开现有登录上下文、缓存约定和审计字段填充方式。 + +## 注释与编码 +- 新增或修改代码时,关键字段、关键分支、关键约束和非直观实现应补充简洁中文注释。 +- 不要为了省事删除原有有效注释,也不要添加无信息量的注释。 +- 写入中文内容时必须保持 UTF-8 编码,并自行检查中文显示是否正常;不要用“改成英文”规避乱码问题。 + ## 提交与合并请求规范 当前 `main` 分支尚无可参考的提交历史,仓库内也没有既有提交规范。建议使用“模块前缀 + 动词短句”的提交格式,例如 `user: 优化登录会话校验`、`system: 增加字典参数校验`。提交 PR 时应说明影响模块、配置或数据结构变更、人工验证步骤;若接口行为有变化,附上请求与响应示例。 diff --git a/README.md b/README.md index a01a9bf..841e864 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,7 @@ CN_Tool 是一个基于 Spring Boot 的多模块后端聚合工程,当前仓 - 系统字典、日志、系统配置、注册资源管理 - WebSocket / Netty 通信基础设施 - 激活码与许可证能力 +- 波形文本解析与查看数据组装能力 ## 当前真实模块 @@ -17,9 +18,10 @@ CN_Tool 是一个基于 Spring Boot 的多模块后端聚合工程,当前仓 - `detection` - `tools` -其中 `tools` 当前仅保留: +其中 `tools` 当前包含: - `activate-tool` +- `wave-tool` ## 启动入口 @@ -27,7 +29,7 @@ CN_Tool 是一个基于 Spring Boot 的多模块后端聚合工程,当前仓 - `entrance/src/main/java/com/njcn/gather/EntranceApplication.java` -`entrance` 模块聚合了 `system`、`user`、`detection`、`activate-tool`,是当前运行时主入口。 +`entrance` 模块聚合了 `system`、`user`、`detection`、`activate-tool`、`wave-tool`,是当前运行时主入口。 ## 技术基线 @@ -72,6 +74,8 @@ P0 已补齐基线文档,建议按以下顺序阅读: - 当前以通信基础设施为主,包含 WebSocket / Netty 相关组件 - `tools/activate-tool` - 负责激活码生成、激活码验证、许可证读取等能力 +- `tools/wave-tool` + - 负责波形文本解析与查看数据组装能力 ## 文档使用规则 diff --git a/entrance/pom.xml b/entrance/pom.xml index 9f044dc..ad56706 100644 --- a/entrance/pom.xml +++ b/entrance/pom.xml @@ -33,6 +33,11 @@ activate-tool 1.0.0 + + com.njcn.gather + wave-tool + 1.0.0 + diff --git a/tools/README.md b/tools/README.md index 97fdf20..54bcf15 100644 --- a/tools/README.md +++ b/tools/README.md @@ -4,17 +4,19 @@ `tools` 当前是工具能力聚合模块,但在本仓库内已经完成一次收口。 -当前真实保留的子模块只有: +当前真实保留的子模块有: - `activate-tool` +- `wave-tool` -因此,`tools` 现阶段不是一个包含多个通用工具的完整工具市场,而是一个仅保留激活能力的聚合模块。 +因此,`tools` 现阶段仍然是聚合模块,但当前已实际承载激活工具和波形查看工具两个子模块。 ## 当前结构 ```text tools/ -└── activate-tool/ +├── activate-tool/ +└── wave-tool/ ``` ## activate-tool 的职责 @@ -28,6 +30,17 @@ tools/ 从接口层看,当前主要围绕 `/activate/*` 路径提供能力。 +## wave-tool 的职责 + +`wave-tool` 当前提供的能力主要围绕波形文本解析与查看数据组装: + +- 解析单列幅值波形文本 +- 解析双列时间/幅值波形文本 +- 统计点位范围、均值、点数等摘要信息 +- 按查看场景输出下采样后的点位集合 + +从接口层看,当前主要围绕 `/wave/*` 路径提供能力。 + ## 模块定位 当前 `activate-tool` 更适合作为平台级基础能力模块,而不是业务检测模块的一部分。 @@ -40,7 +53,7 @@ tools/ ## 依赖关系 -`tools/activate-tool` 当前主要依赖: +`tools/activate-tool` 与 `tools/wave-tool` 当前主要依赖: - `com.njcn:njcn-common` - `com.njcn:spingboot2.3.12` diff --git a/tools/pom.xml b/tools/pom.xml index 06a07e1..c6245c9 100644 --- a/tools/pom.xml +++ b/tools/pom.xml @@ -16,9 +16,10 @@ Retained utility aggregator for platform capabilities. - + activate-tool + wave-tool diff --git a/tools/wave-tool/pom.xml b/tools/wave-tool/pom.xml new file mode 100644 index 0000000..4a8f6fe --- /dev/null +++ b/tools/wave-tool/pom.xml @@ -0,0 +1,28 @@ + + + 4.0.0 + + + com.njcn.gather + tools + 1.0.0 + + + wave-tool + + + + com.njcn + njcn-common + 0.0.1 + + + + com.njcn + spingboot2.3.12 + 2.3.12 + + + diff --git a/tools/wave-tool/src/main/java/com/njcn/gather/tool/wave/controller/WaveController.java b/tools/wave-tool/src/main/java/com/njcn/gather/tool/wave/controller/WaveController.java new file mode 100644 index 0000000..b9d36e3 --- /dev/null +++ b/tools/wave-tool/src/main/java/com/njcn/gather/tool/wave/controller/WaveController.java @@ -0,0 +1,42 @@ +package com.njcn.gather.tool.wave.controller; + +import com.njcn.common.pojo.annotation.OperateInfo; +import com.njcn.common.pojo.enums.common.LogEnum; +import com.njcn.common.pojo.enums.response.CommonResponseEnum; +import com.njcn.common.pojo.response.HttpResult; +import com.njcn.common.utils.LogUtil; +import com.njcn.gather.tool.wave.param.WaveParseParam; +import com.njcn.gather.tool.wave.service.WaveService; +import com.njcn.gather.tool.wave.vo.WaveParseResultVO; +import com.njcn.web.controller.BaseController; +import com.njcn.web.utils.HttpResultUtil; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiImplicitParam; +import io.swagger.annotations.ApiOperation; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@Slf4j +@Api(tags = "波形查看") +@RestController +@RequestMapping("/wave") +@RequiredArgsConstructor +public class WaveController extends BaseController { + + private final WaveService waveService; + + @OperateInfo(info = LogEnum.BUSINESS_COMMON) + @ApiOperation("解析波形文本") + @ApiImplicitParam(name = "param", value = "波形解析参数", required = true, dataType = "WaveParseParam") + @PostMapping("/parse") + public HttpResult parse(@RequestBody WaveParseParam param) { + String methodDescribe = getMethodDescribe("parse"); + LogUtil.njcnDebug(log, "{},开始解析波形文本", methodDescribe); + WaveParseResultVO result = waveService.parse(param); + return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, result, methodDescribe); + } +} diff --git a/tools/wave-tool/src/main/java/com/njcn/gather/tool/wave/param/WaveParseParam.java b/tools/wave-tool/src/main/java/com/njcn/gather/tool/wave/param/WaveParseParam.java new file mode 100644 index 0000000..47b2157 --- /dev/null +++ b/tools/wave-tool/src/main/java/com/njcn/gather/tool/wave/param/WaveParseParam.java @@ -0,0 +1,36 @@ +package com.njcn.gather.tool.wave.param; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import java.math.BigDecimal; + +@Data +@ApiModel("波形解析参数") +public class WaveParseParam { + + @ApiModelProperty(value = "波形文本内容,支持单列幅值、单行多值或双列时间/幅值数据", required = true) + private String waveformText; + + @ApiModelProperty(value = "分隔符,默认 AUTO 自动识别,支持直接传入具体字符,也支持 TAB 或 SPACE") + private String separator; + + @ApiModelProperty(value = "是否包含 X 轴列,true 表示文本中显式传入时间列") + private Boolean containsXAxis; + + @ApiModelProperty(value = "X 轴列下标,默认 0") + private Integer xColumnIndex; + + @ApiModelProperty(value = "Y 轴列下标,单列波形默认 0,双列波形默认 1") + private Integer yColumnIndex; + + @ApiModelProperty(value = "跳过的表头行数,默认 0") + private Integer skipHeaderLines; + + @ApiModelProperty(value = "单列波形的采样间隔,默认 1") + private BigDecimal samplingInterval; + + @ApiModelProperty(value = "返回的最大点位数,超过时自动下采样,默认 2000") + private Integer maxPointCount; +} diff --git a/tools/wave-tool/src/main/java/com/njcn/gather/tool/wave/service/WaveService.java b/tools/wave-tool/src/main/java/com/njcn/gather/tool/wave/service/WaveService.java new file mode 100644 index 0000000..c08fbbe --- /dev/null +++ b/tools/wave-tool/src/main/java/com/njcn/gather/tool/wave/service/WaveService.java @@ -0,0 +1,15 @@ +package com.njcn.gather.tool.wave.service; + +import com.njcn.gather.tool.wave.param.WaveParseParam; +import com.njcn.gather.tool.wave.vo.WaveParseResultVO; + +public interface WaveService { + + /** + * 解析波形文本并输出适合查看的点位结果。 + * + * @param param 波形解析参数 + * @return 波形查看结果 + */ + WaveParseResultVO parse(WaveParseParam param); +} diff --git a/tools/wave-tool/src/main/java/com/njcn/gather/tool/wave/service/impl/WaveServiceImpl.java b/tools/wave-tool/src/main/java/com/njcn/gather/tool/wave/service/impl/WaveServiceImpl.java new file mode 100644 index 0000000..cdea739 --- /dev/null +++ b/tools/wave-tool/src/main/java/com/njcn/gather/tool/wave/service/impl/WaveServiceImpl.java @@ -0,0 +1,214 @@ +package com.njcn.gather.tool.wave.service.impl; + +import cn.hutool.core.util.StrUtil; +import com.njcn.common.pojo.enums.response.CommonResponseEnum; +import com.njcn.common.pojo.exception.BusinessException; +import com.njcn.gather.tool.wave.param.WaveParseParam; +import com.njcn.gather.tool.wave.service.WaveService; +import com.njcn.gather.tool.wave.vo.WaveParseResultVO; +import com.njcn.gather.tool.wave.vo.WavePointVO; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.regex.Pattern; + +@Slf4j +@Service +public class WaveServiceImpl implements WaveService { + + private static final int DEFAULT_MAX_POINT_COUNT = 2000; + private static final String AUTO_SEPARATOR = "AUTO"; + + @Override + public WaveParseResultVO parse(WaveParseParam param) { + if (param == null || StrUtil.isBlank(param.getWaveformText())) { + throw new BusinessException(CommonResponseEnum.FAIL, "波形文本不能为空"); + } + + boolean containsXAxis = Boolean.TRUE.equals(param.getContainsXAxis()); + int skipHeaderLines = sanitizeSkipHeaderLines(param.getSkipHeaderLines()); + int maxPointCount = sanitizeMaxPointCount(param.getMaxPointCount()); + BigDecimal samplingInterval = sanitizeSamplingInterval(param.getSamplingInterval()); + int xColumnIndex = sanitizeColumnIndex(param.getXColumnIndex(), 0); + int yColumnIndex = sanitizeColumnIndex(param.getYColumnIndex(), containsXAxis ? 1 : 0); + + List sourcePoints = new ArrayList<>(); + int ignoredLineCount = 0; + int nonBlankLineIndex = 0; + String[] lines = param.getWaveformText().split("\\r?\\n"); + for (String line : lines) { + if (StrUtil.isBlank(line)) { + continue; + } + if (nonBlankLineIndex++ < skipHeaderLines) { + continue; + } + String[] columns = splitColumns(line, param.getSeparator()); + if (columns.length == 0) { + ignoredLineCount++; + continue; + } + try { + if (containsXAxis) { + WavePointVO point = buildPoint(columns, xColumnIndex, yColumnIndex); + sourcePoints.add(point); + } else { + sourcePoints.addAll(buildSingleColumnPoints(columns, samplingInterval, sourcePoints.size())); + } + } catch (Exception ex) { + ignoredLineCount++; + log.debug("波形行解析失败,line={}, reason={}", line, ex.getMessage()); + } + } + + if (sourcePoints.isEmpty()) { + throw new BusinessException(CommonResponseEnum.FAIL, "未解析到有效波形点位"); + } + + List displayPoints = downSample(sourcePoints, maxPointCount); + return buildResult(sourcePoints, displayPoints, ignoredLineCount, containsXAxis); + } + + private WavePointVO buildPoint(String[] columns, int xColumnIndex, int yColumnIndex) { + BigDecimal yValue = parseNumber(readColumn(columns, yColumnIndex)); + BigDecimal xValue = parseNumber(readColumn(columns, xColumnIndex)); + return new WavePointVO(xValue, yValue); + } + + private List buildSingleColumnPoints(String[] columns, BigDecimal samplingInterval, int startIndex) { + List points = new ArrayList<>(); + for (int i = 0; i < columns.length; i++) { + BigDecimal yValue = parseNumber(columns[i]); + // 单列波形默认按采样间隔自动补齐 X 轴,便于前端直接绘制。 + BigDecimal xValue = samplingInterval.multiply(BigDecimal.valueOf(startIndex + i)); + points.add(new WavePointVO(xValue, yValue)); + } + return points; + } + + private String readColumn(String[] columns, int columnIndex) { + if (columnIndex < 0 || columnIndex >= columns.length) { + throw new IllegalArgumentException("列下标超出范围"); + } + return columns[columnIndex]; + } + + private BigDecimal parseNumber(String value) { + if (StrUtil.isBlank(value)) { + throw new IllegalArgumentException("数值为空"); + } + return new BigDecimal(value.trim()); + } + + private String[] splitColumns(String line, String separator) { + String trimmedLine = line.trim(); + if (StrUtil.isBlank(trimmedLine)) { + return new String[0]; + } + String[] parts; + if (StrUtil.isBlank(separator) || AUTO_SEPARATOR.equalsIgnoreCase(separator)) { + parts = trimmedLine.split("[,;\\s]+"); + } else if ("TAB".equalsIgnoreCase(separator)) { + parts = trimmedLine.split("\\t+"); + } else if ("SPACE".equalsIgnoreCase(separator)) { + parts = trimmedLine.split("\\s+"); + } else { + parts = trimmedLine.split(Pattern.quote(separator)); + } + return Arrays.stream(parts) + .map(String::trim) + .filter(StrUtil::isNotBlank) + .toArray(String[]::new); + } + + private List downSample(List sourcePoints, int maxPointCount) { + if (sourcePoints.size() <= maxPointCount) { + return sourcePoints; + } + + List result = new ArrayList<>(); + int step = (int) Math.ceil((double) sourcePoints.size() / maxPointCount); + for (int i = 0; i < sourcePoints.size(); i += step) { + result.add(sourcePoints.get(i)); + } + WavePointVO lastPoint = sourcePoints.get(sourcePoints.size() - 1); + if (!result.contains(lastPoint) && result.size() < maxPointCount) { + result.add(lastPoint); + } else if (!result.isEmpty()) { + result.set(result.size() - 1, lastPoint); + } + return result; + } + + private WaveParseResultVO buildResult(List sourcePoints, List displayPoints, + int ignoredLineCount, boolean containsXAxis) { + BigDecimal minX = sourcePoints.get(0).getX(); + BigDecimal maxX = sourcePoints.get(0).getX(); + BigDecimal minY = sourcePoints.get(0).getY(); + BigDecimal maxY = sourcePoints.get(0).getY(); + BigDecimal sumY = BigDecimal.ZERO; + + for (WavePointVO point : sourcePoints) { + if (point.getX().compareTo(minX) < 0) { + minX = point.getX(); + } + if (point.getX().compareTo(maxX) > 0) { + maxX = point.getX(); + } + if (point.getY().compareTo(minY) < 0) { + minY = point.getY(); + } + if (point.getY().compareTo(maxY) > 0) { + maxY = point.getY(); + } + sumY = sumY.add(point.getY()); + } + + WaveParseResultVO result = new WaveParseResultVO(); + result.setContainsXAxis(containsXAxis); + result.setSourcePointCount(sourcePoints.size()); + result.setDisplayPointCount(displayPoints.size()); + result.setIgnoredLineCount(ignoredLineCount); + result.setSampled(sourcePoints.size() != displayPoints.size()); + result.setMinX(minX); + result.setMaxX(maxX); + result.setMinY(minY); + result.setMaxY(maxY); + result.setAverageY(sumY.divide(BigDecimal.valueOf(sourcePoints.size()), 6, RoundingMode.HALF_UP)); + result.setPoints(displayPoints); + return result; + } + + private int sanitizeSkipHeaderLines(Integer skipHeaderLines) { + if (skipHeaderLines == null || skipHeaderLines < 0) { + return 0; + } + return skipHeaderLines; + } + + private int sanitizeMaxPointCount(Integer maxPointCount) { + if (maxPointCount == null || maxPointCount <= 0) { + return DEFAULT_MAX_POINT_COUNT; + } + return maxPointCount; + } + + private int sanitizeColumnIndex(Integer columnIndex, int defaultValue) { + if (columnIndex == null || columnIndex < 0) { + return defaultValue; + } + return columnIndex; + } + + private BigDecimal sanitizeSamplingInterval(BigDecimal samplingInterval) { + if (samplingInterval == null || samplingInterval.compareTo(BigDecimal.ZERO) <= 0) { + return BigDecimal.ONE; + } + return samplingInterval; + } +} diff --git a/tools/wave-tool/src/main/java/com/njcn/gather/tool/wave/vo/WaveParseResultVO.java b/tools/wave-tool/src/main/java/com/njcn/gather/tool/wave/vo/WaveParseResultVO.java new file mode 100644 index 0000000..8c81748 --- /dev/null +++ b/tools/wave-tool/src/main/java/com/njcn/gather/tool/wave/vo/WaveParseResultVO.java @@ -0,0 +1,46 @@ +package com.njcn.gather.tool.wave.vo; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import java.math.BigDecimal; +import java.util.List; + +@Data +@ApiModel("波形解析结果") +public class WaveParseResultVO { + + @ApiModelProperty("是否包含显式 X 轴") + private Boolean containsXAxis; + + @ApiModelProperty("原始有效点位数") + private Integer sourcePointCount; + + @ApiModelProperty("返回的显示点位数") + private Integer displayPointCount; + + @ApiModelProperty("被忽略的无效行数") + private Integer ignoredLineCount; + + @ApiModelProperty("是否发生下采样") + private Boolean sampled; + + @ApiModelProperty("X 轴最小值") + private BigDecimal minX; + + @ApiModelProperty("X 轴最大值") + private BigDecimal maxX; + + @ApiModelProperty("Y 轴最小值") + private BigDecimal minY; + + @ApiModelProperty("Y 轴最大值") + private BigDecimal maxY; + + @ApiModelProperty("Y 轴平均值") + private BigDecimal averageY; + + @ApiModelProperty("用于查看的波形点位") + private List points; +} diff --git a/tools/wave-tool/src/main/java/com/njcn/gather/tool/wave/vo/WavePointVO.java b/tools/wave-tool/src/main/java/com/njcn/gather/tool/wave/vo/WavePointVO.java new file mode 100644 index 0000000..74258e9 --- /dev/null +++ b/tools/wave-tool/src/main/java/com/njcn/gather/tool/wave/vo/WavePointVO.java @@ -0,0 +1,22 @@ +package com.njcn.gather.tool.wave.vo; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.math.BigDecimal; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@ApiModel("波形点位") +public class WavePointVO { + + @ApiModelProperty("X 轴值") + private BigDecimal x; + + @ApiModelProperty("Y 轴值") + private BigDecimal y; +}