feat(event): 添加暂态事件波形查看与导出功能

- 新增 getTransientEventWave 接口用于查看暂态事件波形
- 新增 exportTransientEventWaves 接口用于批量导出暂态事件波形
- 添加 EventWaveExportParam 参数类支持波形导出
- 在 EventListMapper 中增加 selectTransientDetailsByIds 查询方法
- 更新事件列表查询参数支持毫秒级时间格式
- 移除事件描述模糊查询条件优化查询性能
- 添加波形导出相关的常量和工具类集成
This commit is contained in:
2026-05-18 08:45:05 +08:00
parent 90219a3daf
commit 38f910fccd
67 changed files with 1203 additions and 1760 deletions

View File

@@ -18,6 +18,7 @@
- 只清理自己造成的问题:可以删除因本次修改而产生的未使用 `import`、变量或方法;不要删除仓库中原本就存在的死代码,除非用户明确要求。
- 先定义验证方式:执行方案里要写清楚“改哪里、怎么判断改对了”;默认通过代码路径、配置一致性和受影响范围检查进行验证。
- 除非用户明确要求,否则不执行任何 `mvn` 编译、打包、测试或其他构建命令。
- 所有导出或生成文件名统一追加日期,格式为“原文件名 + `_` + `yyyyMMdd` + 原扩展名”,例如 `暂态事件列表_20260516.xlsx`;新增下载导出、临时生成或落盘保存文件时,优先复用目标模块已有文件名工具,不要在业务代码中散落手写日期拼接。
## 项目结构与模块划分
`CN_Tool` 是一个 Maven 多模块后端项目,根目录的 [`pom.xml`](C:/code/gitea/cn_tool/CN_Tool/pom.xml) 聚合了 `entrance``system``user``detection``tools`
@@ -40,6 +41,8 @@ Java 源码位于 `src/main/java`,配置文件位于 `src/main/resources`My
- 不要假设运行时存在自动数据库迁移;如果代码依赖新表、新字段或新索引,必须同步补齐对应 SQL 与文档说明。
- SQL 脚本应放在目标模块的 `src/main/resources/sql/...` 下,并保持可审阅、可单独执行、语义清晰。
- 变更缓存、日志、审计相关逻辑时,优先沿用现有机制,不要绕开现有登录上下文、缓存约定和审计字段填充方式。
- 涉及 `sys_dict_type``sys_dict_data` 的字典类型编码、字典数据编码或固定字典数据 ID 时,必须统一维护在 `system/src/main/java/com/njcn/gather/system/pojo/constant/DictConst.java`后续新增使用点也要先补常量再引用不要在业务代码、SQL 或文档中散落硬编码。
- 新增或保留字典初始化 SQL 前,必须先确认对应字典类型在后端代码、配置或明确页面契约中确实被使用;未使用的字典类型和字典数据不要写入 `sys_dict_type``sys_dict_data` 初始化脚本。
## 注释与编码
- 新增或修改代码时,关键字段、关键分支、关键约束和非直观实现应补充简洁中文注释。

View File

@@ -48,6 +48,16 @@ socket:
webSocket:
port: 7777
steady:
influxdb:
url: http://192.168.1.103:18086
database: pqsbase
username: admin
password: ${STEADY_INFLUXDB_PASSWORD:}
ssl: false
connect-timeout-ms: 5000
read-timeout-ms: 30000
log:
homeDir: D:\logs
commonLevel: info

View File

@@ -35,5 +35,26 @@
<artifactId>add-ledger</artifactId>
<version>1.0.0</version>
</dependency>
<dependency>
<groupId>com.njcn.gather</groupId>
<artifactId>system</artifactId>
<version>1.0.0</version>
</dependency>
<dependency>
<groupId>com.njcn.gather</groupId>
<artifactId>wave-tool</artifactId>
<version>1.0.0</version>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>4.1.2</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@@ -9,11 +9,11 @@ import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
/**
* 暂态事件时间字段按秒输出避免接口响应携带毫秒
* 暂态事件时间字段按秒输出保持与数据库 datetime(3) 精度一致
*/
public class EventSecondTimeSerializer extends JsonSerializer<LocalDateTime> {
public class EventMillisecondTimeSerializer extends JsonSerializer<LocalDateTime> {
private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS");
@Override
public void serialize(LocalDateTime value, JsonGenerator gen, SerializerProvider serializers) throws IOException {

View File

@@ -8,8 +8,10 @@ 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.event.eventlist.pojo.param.EventListQueryParam;
import com.njcn.gather.event.eventlist.pojo.param.EventWaveExportParam;
import com.njcn.gather.event.eventlist.pojo.vo.EventListVO;
import com.njcn.gather.event.eventlist.service.EventListService;
import com.njcn.gather.tool.wave.pojo.vo.WaveComtradeResultVO;
import com.njcn.web.controller.BaseController;
import com.njcn.web.utils.HttpResultUtil;
import io.swagger.annotations.Api;
@@ -64,4 +66,21 @@ public class EventListController extends BaseController {
public void exportTransientEvents(@RequestBody EventListQueryParam param) {
eventListService.exportTransientEvents(param);
}
@OperateInfo(info = LogEnum.BUSINESS_COMMON)
@ApiOperation("查看暂态事件波形")
@GetMapping("/transient/{eventId}/wave")
public HttpResult<WaveComtradeResultVO> getTransientEventWave(@PathVariable("eventId") String eventId) {
String methodDescribe = getMethodDescribe("getTransientEventWave");
LogUtil.njcnDebug(log, "{}开始查看暂态事件波形eventId={}", methodDescribe, eventId);
WaveComtradeResultVO result = eventListService.getTransientEventWave(eventId);
return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, result, methodDescribe);
}
@OperateInfo(info = LogEnum.BUSINESS_COMMON, operateType = OperateType.DOWNLOAD)
@ApiOperation("导出选中暂态事件波形")
@PostMapping("/transient/wave/export")
public void exportTransientEventWaves(@RequestBody EventWaveExportParam param) {
eventListService.exportTransientEventWaves(param);
}
}

View File

@@ -18,4 +18,6 @@ public interface EventListMapper extends BaseMapper<MpEventDetailPO> {
List<MpEventDetailPO> selectTransientExportList(@Param("param") EventListQueryParam param, @Param("limit") Integer limit);
MpEventDetailPO selectTransientDetail(@Param("eventId") String eventId);
List<MpEventDetailPO> selectTransientDetailsByIds(@Param("eventIds") List<String> eventIds);
}

View File

@@ -47,9 +47,6 @@
<if test="param.phase != null and param.phase != ''">
AND phase = #{param.phase}
</if>
<if test="param.eventDescribe != null and param.eventDescribe != ''">
AND event_describe LIKE CONCAT('%', #{param.eventDescribe}, '%')
</if>
<if test="param.durationMin != null">
AND duration &gt;= #{param.durationMin}
</if>
@@ -101,4 +98,14 @@
WHERE event_id = #{eventId}
LIMIT 1
</select>
<select id="selectTransientDetailsByIds" resultType="com.njcn.gather.event.eventlist.pojo.po.MpEventDetailPO">
SELECT
<include refid="EventDetailColumns"/>
FROM r_mp_event_detail
WHERE event_id IN
<foreach collection="eventIds" item="eventId" open="(" separator="," close=")">
#{eventId}
</foreach>
</select>
</mapper>

View File

@@ -18,10 +18,10 @@ import java.util.List;
@ApiModel("暂态事件列表查询参数")
public class EventListQueryParam extends BaseParam {
@ApiModelProperty("发生时刻开始,格式 yyyy-MM-dd HH:mm:ss")
@ApiModelProperty("发生时刻开始,格式 yyyy-MM-dd HH:mm:ss.SSS兼容 yyyy-MM-dd HH:mm:ss")
private String startTimeStart;
@ApiModelProperty("发生时刻结束,格式 yyyy-MM-dd HH:mm:ss")
@ApiModelProperty("发生时刻结束,格式 yyyy-MM-dd HH:mm:ss.SSS兼容 yyyy-MM-dd HH:mm:ss")
private String startTimeEnd;
@ApiModelProperty("事件类型")
@@ -30,9 +30,6 @@ public class EventListQueryParam extends BaseParam {
@ApiModelProperty("相别")
private String phase;
@ApiModelProperty("事件描述关键字")
private String eventDescribe;
@ApiModelProperty("持续时间下限,单位秒")
private BigDecimal durationMin;

View File

@@ -0,0 +1,19 @@
package com.njcn.gather.event.eventlist.pojo.param;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.util.ArrayList;
import java.util.List;
/**
* 事件波形批量导出参数。
*/
@Data
@ApiModel("事件波形批量导出参数")
public class EventWaveExportParam {
@ApiModelProperty("页面勾选的事件 ID 列表")
private List<String> eventIds = new ArrayList<String>();
}

View File

@@ -2,11 +2,12 @@ package com.njcn.gather.event.eventlist.pojo.vo;
import cn.afterturn.easypoi.excel.annotation.Excel;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
import com.njcn.gather.event.eventlist.config.EventSecondTimeSerializer;
import com.njcn.gather.event.eventlist.config.EventMillisecondTimeSerializer;
import lombok.Data;
import java.io.Serializable;
@@ -25,10 +26,11 @@ public class EventListVO implements Serializable {
private String measurementPointId;
private String eventType;
@Excel(name = "设备名称", width = 25)
private String equipmentName;
@Excel(name = "发生时刻", width = 25, exportFormat = "yyyy-MM-dd HH:mm:ss.SSS")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss.SSS")
@JsonDeserialize(using = LocalDateTimeDeserializer.class)
@JsonSerialize(using = EventMillisecondTimeSerializer.class)
private LocalDateTime startTime;
@Excel(name = "工程名称", width = 25)
private String engineeringName;
@@ -36,29 +38,32 @@ public class EventListVO implements Serializable {
@Excel(name = "项目名称", width = 25)
private String projectName;
@Excel(name = "发生时刻", width = 25, exportFormat = "yyyy-MM-dd HH:mm:ss")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@JsonDeserialize(using = LocalDateTimeDeserializer.class)
@JsonSerialize(using = EventSecondTimeSerializer.class)
private LocalDateTime startTime;
@Excel(name = "设备名称", width = 25)
private String equipmentName;
private String mac;
@Excel(name = "监测点名称", width = 25)
private String lineName;
@Excel(name = "事件描述", width = 25)
private String eventDescribe;
@Excel(name = "事件发生位置", width = 18)
private String sagsource;
@Excel(name = "相别", width = 15)
private String phase;
@Excel(name = "暂降/暂升幅值(%)", width = 20)
private BigDecimal featureAmplitude;
@Excel(name = "持续时间(s)", width = 18)
private BigDecimal duration;
@Excel(name = "暂降/暂升幅值(%)", width = 20)
private BigDecimal featureAmplitude;
@Excel(name = "事件类型", width = 18)
private String eventType;
@Excel(name = "相别", width = 15)
private String phase;
@Excel(name = "事件描述", width = 25)
@JsonProperty("event_describe")
private String eventDescribe;
@Excel(name = "事件发生位置", width = 18)
private String sagsource;
private String wavePath;

View File

@@ -2,7 +2,9 @@ package com.njcn.gather.event.eventlist.service;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.njcn.gather.event.eventlist.pojo.param.EventListQueryParam;
import com.njcn.gather.event.eventlist.pojo.param.EventWaveExportParam;
import com.njcn.gather.event.eventlist.pojo.vo.EventListVO;
import com.njcn.gather.tool.wave.pojo.vo.WaveComtradeResultVO;
/**
* 事件列表服务。
@@ -14,4 +16,8 @@ public interface EventListService {
EventListVO getTransientEventDetail(String eventId);
void exportTransientEvents(EventListQueryParam param);
WaveComtradeResultVO getTransientEventWave(String eventId);
void exportTransientEventWaves(EventWaveExportParam param);
}

View File

@@ -1,30 +1,61 @@
package com.njcn.gather.event.eventlist.service.impl;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.njcn.common.pojo.enums.common.DataStateEnum;
import com.njcn.common.pojo.enums.response.CommonResponseEnum;
import com.njcn.common.pojo.exception.BusinessException;
import com.njcn.gather.event.eventlist.mapper.EventListMapper;
import com.njcn.gather.event.eventlist.pojo.param.EventListQueryParam;
import com.njcn.gather.event.eventlist.pojo.param.EventWaveExportParam;
import com.njcn.gather.event.eventlist.pojo.po.MpEventDetailPO;
import com.njcn.gather.event.eventlist.pojo.vo.EventListVO;
import com.njcn.gather.event.eventlist.service.EventListService;
import com.njcn.gather.system.cfg.service.ISysConfigService;
import com.njcn.gather.system.dictionary.pojo.po.DictData;
import com.njcn.gather.system.dictionary.pojo.po.DictType;
import com.njcn.gather.system.dictionary.service.IDictDataService;
import com.njcn.gather.system.dictionary.service.IDictTypeService;
import com.njcn.gather.system.pojo.constant.DictConst;
import com.njcn.gather.system.util.ExportFileNameUtil;
import com.njcn.gather.tool.addledger.pojo.param.AddLedgerLinePathQueryParam;
import com.njcn.gather.tool.addledger.pojo.vo.AddLedgerLinePathVO;
import com.njcn.gather.tool.addledger.service.AddLedgerService;
import com.njcn.gather.tool.wave.pojo.param.WaveComtradeParseParam;
import com.njcn.gather.tool.wave.pojo.vo.WaveComtradeResultVO;
import com.njcn.gather.tool.wave.service.WaveService;
import com.njcn.web.factory.PageFactory;
import com.njcn.web.utils.ExcelUtil;
import com.njcn.web.utils.HttpServletUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.CellStyle;
import org.apache.poi.ss.usermodel.Font;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.xssf.usermodel.XSSFSheet;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.springframework.stereotype.Service;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.math.BigDecimal;
import java.net.URLEncoder;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
/**
* 事件列表服务实现。
@@ -37,8 +68,12 @@ public class EventListServiceImpl implements EventListService {
private static final int LEDGER_LINE_QUERY_LIMIT = 1000;
private static final int EVENT_LINE_ID_QUERY_LIMIT = 1000;
private static final int EXPORT_LIMIT = 5000;
private static final int WAVE_EXPORT_LIMIT = 500;
private static final String EMPTY_TEXT = "-";
private static final DateTimeFormatter OUTPUT_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
private static final String WAVE_EXPORT_SUCCESS = "成功";
private static final String WAVE_EXPORT_FAIL = "失败";
private static final BigDecimal PERCENT_BASE = new BigDecimal("100");
private static final DateTimeFormatter OUTPUT_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS");
private static final DateTimeFormatter[] INPUT_FORMATTERS = new DateTimeFormatter[]{
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"),
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS"),
@@ -48,6 +83,10 @@ public class EventListServiceImpl implements EventListService {
private final EventListMapper eventListMapper;
private final AddLedgerService addLedgerService;
private final ISysConfigService sysConfigService;
private final IDictTypeService dictTypeService;
private final IDictDataService dictDataService;
private final WaveService waveService;
@Override
public Page<EventListVO> pageTransientEvents(EventListQueryParam param) {
@@ -89,10 +128,298 @@ public class EventListServiceImpl implements EventListService {
throw new BusinessException(CommonResponseEnum.FAIL, "导出数据超过 5000 条,请缩小查询条件");
}
exportRecords = buildEventList(eventDetails);
fillExportEventTypeNames(exportRecords);
} else {
exportRecords = Collections.emptyList();
}
ExcelUtil.exportExcel("暂态事件列表.xlsx", "暂态事件列表", EventListVO.class, exportRecords);
ExcelUtil.exportExcel(ExportFileNameUtil.appendToday("暂态事件列表.xlsx"), "暂态事件列表", EventListVO.class, exportRecords);
}
@Override
public WaveComtradeResultVO getTransientEventWave(String eventId) {
MpEventDetailPO eventDetail = requireEventDetail(eventId);
AddLedgerLinePathVO linePath = requireLinePath(eventDetail);
EventWavePathResolver.WaveFilePath waveFilePath = resolveWaveFilePath(eventDetail, linePath);
if (!Files.exists(waveFilePath.getCfgPath())) {
throw new BusinessException(CommonResponseEnum.FAIL, "cfg 波形文件不存在");
}
if (!Files.exists(waveFilePath.getDatPath())) {
throw new BusinessException(CommonResponseEnum.FAIL, "dat 波形文件不存在");
}
WaveComtradeParseParam parseParam = new WaveComtradeParseParam();
parseParam.setMonitorName(defaultText(linePath.getLineName()));
try (InputStream cfgInputStream = new FileInputStream(waveFilePath.getCfgPath().toFile());
InputStream datInputStream = new FileInputStream(waveFilePath.getDatPath().toFile())) {
return waveService.parseComtrade(cfgInputStream, datInputStream, parseParam);
} catch (IOException ex) {
log.error("读取暂态事件波形文件失败eventId={}", eventDetail.getEventId(), ex);
throw new BusinessException(CommonResponseEnum.FAIL, "读取波形文件失败");
}
}
@Override
public void exportTransientEventWaves(EventWaveExportParam param) {
List<String> eventIds = normalizeExportEventIds(param);
List<WaveExportItem> exportItems = buildWaveExportItems(eventIds);
writeWaveExportZip(exportItems);
}
private MpEventDetailPO requireEventDetail(String eventId) {
String normalizedEventId = trimToNull(eventId);
if (normalizedEventId == null) {
throw new BusinessException(CommonResponseEnum.FAIL, "事件 ID 不能为空");
}
MpEventDetailPO eventDetail = eventListMapper.selectTransientDetail(normalizedEventId);
if (eventDetail == null) {
throw new BusinessException(CommonResponseEnum.FAIL, "暂态事件不存在");
}
return eventDetail;
}
private AddLedgerLinePathVO requireLinePath(MpEventDetailPO eventDetail) {
List<String> lineIds = new ArrayList<String>();
lineIds.add(eventDetail.getMeasurementPointId());
Map<String, AddLedgerLinePathVO> linePathMap = addLedgerService.listLinePathByLineIds(lineIds);
AddLedgerLinePathVO linePath = linePathMap.get(eventDetail.getMeasurementPointId());
if (linePath == null) {
throw new BusinessException(CommonResponseEnum.FAIL, "监测点台账不存在或已删除");
}
return linePath;
}
private List<String> normalizeExportEventIds(EventWaveExportParam param) {
List<String> eventIds = normalizeIds(param == null ? null : param.getEventIds());
if (eventIds.isEmpty()) {
throw new BusinessException(CommonResponseEnum.FAIL, "请选择需要导出的事件");
}
if (eventIds.size() > WAVE_EXPORT_LIMIT) {
throw new BusinessException(CommonResponseEnum.FAIL, "波形导出事件数量不能超过 500 条");
}
return eventIds;
}
private List<WaveExportItem> buildWaveExportItems(List<String> eventIds) {
List<MpEventDetailPO> eventDetails = eventListMapper.selectTransientDetailsByIds(eventIds);
Map<String, MpEventDetailPO> eventMap = new LinkedHashMap<String, MpEventDetailPO>();
List<String> lineIds = new ArrayList<String>();
for (MpEventDetailPO eventDetail : eventDetails) {
eventMap.put(eventDetail.getEventId(), eventDetail);
String lineId = trimToNull(eventDetail.getMeasurementPointId());
if (lineId != null && !lineIds.contains(lineId)) {
lineIds.add(lineId);
}
}
Map<String, AddLedgerLinePathVO> linePathMap = addLedgerService.listLinePathByLineIds(lineIds);
List<WaveExportItem> exportItems = new ArrayList<WaveExportItem>();
for (String eventId : eventIds) {
WaveExportItem item = new WaveExportItem();
item.setEventId(eventId);
MpEventDetailPO eventDetail = eventMap.get(eventId);
item.setEventDetail(eventDetail);
if (eventDetail == null) {
item.fail("事件不存在");
exportItems.add(item);
continue;
}
AddLedgerLinePathVO linePath = linePathMap.get(eventDetail.getMeasurementPointId());
item.setLinePath(linePath);
item.setEventVO(buildEventVO(eventDetail, linePath));
fillWaveExportFileState(item);
exportItems.add(item);
}
return exportItems;
}
private void fillWaveExportFileState(WaveExportItem item) {
MpEventDetailPO eventDetail = item.getEventDetail();
if (item.getLinePath() == null) {
item.fail("监测点台账不存在或已删除");
return;
}
if (eventDetail.getFileFlag() == null || eventDetail.getFileFlag() != 1) {
item.fail("波形文件未招");
return;
}
try {
EventWavePathResolver.WaveFilePath waveFilePath = resolveWaveFilePath(eventDetail, item.getLinePath());
item.setWaveFilePath(waveFilePath);
boolean cfgExists = Files.exists(waveFilePath.getCfgPath());
boolean datExists = Files.exists(waveFilePath.getDatPath());
if (cfgExists && datExists) {
item.success();
return;
}
if (!cfgExists && !datExists) {
item.fail("cfg/dat 文件不存在");
} else if (!cfgExists) {
item.fail("cfg 文件不存在");
} else {
item.fail("dat 文件不存在");
}
} catch (BusinessException ex) {
item.fail(ex.getMessage());
}
}
private EventWavePathResolver.WaveFilePath resolveWaveFilePath(MpEventDetailPO eventDetail, AddLedgerLinePathVO linePath) {
try {
return EventWavePathResolver.resolve(sysConfigService.getWaveStoragePath(), linePath.getEquipmentMac(), eventDetail.getWavePath());
} catch (IllegalArgumentException ex) {
throw new BusinessException(CommonResponseEnum.FAIL, ex.getMessage());
}
}
private void writeWaveExportZip(List<WaveExportItem> exportItems) {
HttpServletResponse response = HttpServletUtil.getResponse();
try {
String fileName = URLEncoder.encode(ExportFileNameUtil.appendToday("选中事件波形导出.zip"), "UTF-8");
response.reset();
response.setHeader("Content-Disposition", "attachment; filename=\"" + fileName + "\"");
response.setContentType("application/zip;charset=UTF-8");
try (ServletOutputStream outputStream = response.getOutputStream();
ZipOutputStream zipOutputStream = new ZipOutputStream(outputStream)) {
writeZipEntry(zipOutputStream, ExportFileNameUtil.appendToday("选中事件日志.xlsx"), buildWaveExportWorkbook(exportItems));
for (int i = 0; i < exportItems.size(); i++) {
WaveExportItem item = exportItems.get(i);
EventWavePathResolver.WaveFilePath waveFilePath = item.getWaveFilePath();
if (waveFilePath == null) {
continue;
}
String folder = buildZipFolderName(i + 1, item.getEventId());
writeWaveFileIfExists(zipOutputStream, folder + "/" + waveFilePath.getCfgPath().getFileName(), waveFilePath.getCfgPath());
writeWaveFileIfExists(zipOutputStream, folder + "/" + waveFilePath.getDatPath().getFileName(), waveFilePath.getDatPath());
}
zipOutputStream.flush();
}
} catch (IOException ex) {
log.error("导出暂态事件波形失败", ex);
throw new BusinessException(CommonResponseEnum.FAIL, "导出波形文件失败");
}
}
private byte[] buildWaveExportWorkbook(List<WaveExportItem> exportItems) throws IOException {
XSSFWorkbook workbook = new XSSFWorkbook();
try {
writeEventLogSheet(workbook, exportItems);
writeWaveFileSheet(workbook, exportItems);
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
workbook.write(outputStream);
return outputStream.toByteArray();
} finally {
workbook.close();
}
}
private void writeEventLogSheet(XSSFWorkbook workbook, List<WaveExportItem> exportItems) {
XSSFSheet sheet = workbook.createSheet("选中事件列表");
String[] headers = new String[]{"发生时刻", "工程名称", "项目名称", "设备名称", "监测点名称", "暂降/暂升幅值(%)",
"持续时间(s)", "事件类型", "相别", "事件描述", "事件发生位置"};
int[] columnWidths = new int[]{25, 25, 25, 25, 25, 20, 18, 18, 15, 25, 18};
writeHeader(sheet, headers, createHeaderStyle(workbook));
Map<String, String> eventTypeNameMap = buildEventTypeNameMap();
int rowIndex = 1;
for (WaveExportItem item : exportItems) {
EventListVO vo = item.getEventVO();
if (vo == null) {
continue;
}
Row row = sheet.createRow(rowIndex++);
writeCells(row, new Object[]{formatDateTime(vo.getStartTime()), vo.getEngineeringName(), vo.getProjectName(),
vo.getEquipmentName(), vo.getLineName(), vo.getFeatureAmplitude(), vo.getDuration(),
translateEventType(vo.getEventType(), eventTypeNameMap), vo.getPhase(), vo.getEventDescribe(),
vo.getSagsource()});
}
applyColumnWidth(sheet, columnWidths);
}
private void writeWaveFileSheet(XSSFWorkbook workbook, List<WaveExportItem> exportItems) {
XSSFSheet sheet = workbook.createSheet("波形文件清单");
String[] headers = new String[]{"事件ID", "设备MAC", "wavePath", "cfg文件路径", "dat文件路径", "导出状态", "失败原因"};
writeHeader(sheet, headers, createHeaderStyle(workbook));
int rowIndex = 1;
for (WaveExportItem item : exportItems) {
EventWavePathResolver.WaveFilePath waveFilePath = item.getWaveFilePath();
Row row = sheet.createRow(rowIndex++);
writeCells(row, new Object[]{item.getEventId(),
item.getLinePath() == null ? EMPTY_TEXT : item.getLinePath().getEquipmentMac(),
item.getEventDetail() == null ? EMPTY_TEXT : item.getEventDetail().getWavePath(),
waveFilePath == null ? EMPTY_TEXT : waveFilePath.getCfgPath().toString(),
waveFilePath == null ? EMPTY_TEXT : waveFilePath.getDatPath().toString(),
item.getStatus(), defaultText(item.getFailReason())});
}
applyColumnWidth(sheet, headers.length);
}
private CellStyle createHeaderStyle(XSSFWorkbook workbook) {
CellStyle style = workbook.createCellStyle();
Font font = workbook.createFont();
font.setBold(true);
style.setFont(font);
return style;
}
private void writeHeader(XSSFSheet sheet, String[] headers, CellStyle style) {
Row row = sheet.createRow(0);
for (int i = 0; i < headers.length; i++) {
Cell cell = row.createCell(i);
cell.setCellValue(headers[i]);
cell.setCellStyle(style);
}
}
private void writeCells(Row row, Object[] values) {
for (int i = 0; i < values.length; i++) {
Cell cell = row.createCell(i);
Object value = values[i];
cell.setCellValue(value == null ? "" : String.valueOf(value));
}
}
private void applyColumnWidth(XSSFSheet sheet, int columnCount) {
for (int i = 0; i < columnCount; i++) {
sheet.setColumnWidth(i, 20 * 256);
}
}
private void applyColumnWidth(XSSFSheet sheet, int[] columnWidths) {
for (int i = 0; i < columnWidths.length; i++) {
sheet.setColumnWidth(i, columnWidths[i] * 256);
}
}
private void writeZipEntry(ZipOutputStream zipOutputStream, String entryName, byte[] content) throws IOException {
ZipEntry zipEntry = new ZipEntry(entryName);
zipOutputStream.putNextEntry(zipEntry);
zipOutputStream.write(content);
zipOutputStream.closeEntry();
}
private void writeWaveFileIfExists(ZipOutputStream zipOutputStream, String entryName, Path filePath) throws IOException {
if (!Files.exists(filePath)) {
return;
}
zipOutputStream.putNextEntry(new ZipEntry(entryName));
try (InputStream inputStream = new FileInputStream(filePath.toFile())) {
byte[] buffer = new byte[8192];
int length;
while ((length = inputStream.read(buffer)) != -1) {
zipOutputStream.write(buffer, 0, length);
}
}
zipOutputStream.closeEntry();
}
private String buildZipFolderName(int index, String eventId) {
return index + "_" + sanitizeZipSegment(eventId);
}
private String sanitizeZipSegment(String value) {
String text = trimToNull(value);
return text == null ? "unknown" : text.replaceAll("[\\\\/:*?\"<>|]", "_");
}
private String formatDateTime(LocalDateTime value) {
return value == null ? EMPTY_TEXT : OUTPUT_FORMATTER.format(value);
}
private List<EventListVO> buildEventList(List<MpEventDetailPO> eventDetails) {
@@ -121,15 +448,16 @@ public class EventListServiceImpl implements EventListService {
vo.setMeasurementPointId(eventDetail.getMeasurementPointId());
vo.setEventType(eventDetail.getEventType());
vo.setEquipmentName(linePath == null ? EMPTY_TEXT : defaultText(linePath.getEquipmentName()));
vo.setMac(linePath == null ? EMPTY_TEXT : defaultText(linePath.getEquipmentMac()));
vo.setEngineeringName(linePath == null ? EMPTY_TEXT : defaultText(linePath.getEngineeringName()));
vo.setProjectName(linePath == null ? EMPTY_TEXT : defaultText(linePath.getProjectName()));
vo.setStartTime(eventDetail.getStartTime());
vo.setLineName(linePath == null ? EMPTY_TEXT : defaultText(linePath.getLineName()));
vo.setEventDescribe(defaultText(eventDetail.getEventDescribe(), eventDetail.getEventType()));
vo.setEventDescribe(trimToNull(eventDetail.getEventDescribe()));
vo.setSagsource(defaultText(eventDetail.getSagsource()));
vo.setPhase(defaultText(eventDetail.getPhase()));
vo.setDuration(eventDetail.getDuration());
vo.setFeatureAmplitude(eventDetail.getFeatureAmplitude());
vo.setFeatureAmplitude(toPercent(eventDetail.getFeatureAmplitude()));
vo.setWavePath(eventDetail.getWavePath());
vo.setFileFlag(eventDetail.getFileFlag());
vo.setDealFlag(eventDetail.getDealFlag());
@@ -155,11 +483,12 @@ public class EventListServiceImpl implements EventListService {
validateRange(queryParam.getFeatureAmplitudeMin(), queryParam.getFeatureAmplitudeMax(), "幅值下限不能大于上限");
validateFlag(queryParam.getFileFlag(), "波形文件状态只能是 0 或 1");
validateDealFlag(queryParam.getDealFlag());
queryParam.setFeatureAmplitudeMin(fromPercent(queryParam.getFeatureAmplitudeMin()));
queryParam.setFeatureAmplitudeMax(fromPercent(queryParam.getFeatureAmplitudeMax()));
queryParam.setStartTimeStart(OUTPUT_FORMATTER.format(startTime));
queryParam.setStartTimeEnd(OUTPUT_FORMATTER.format(endTime));
queryParam.setEventType(trimToNull(queryParam.getEventType()));
queryParam.setPhase(trimToNull(queryParam.getPhase()));
queryParam.setEventDescribe(trimToNull(queryParam.getEventDescribe()));
queryParam.setEngineeringName(trimToNull(queryParam.getEngineeringName()));
queryParam.setProjectName(trimToNull(queryParam.getProjectName()));
queryParam.setEquipmentName(trimToNull(queryParam.getEquipmentName()));
@@ -222,10 +551,10 @@ public class EventListServiceImpl implements EventListService {
try {
return LocalDateTime.parse(text, formatter);
} catch (DateTimeParseException ignored) {
// 尝试下一前端可能传入的时间格式。
// 尝试下一前端可能传入的时间格式。
}
}
throw new BusinessException(CommonResponseEnum.FAIL, "时间格式不正确,仅支持 yyyy-MM-dd HH:mm:ss");
throw new BusinessException(CommonResponseEnum.FAIL, "时间格式不正确,仅支持 yyyy-MM-dd HH:mm:ss 或 yyyy-MM-dd HH:mm:ss.SSS");
}
private List<String> normalizeIds(List<String> ids) {
@@ -255,6 +584,59 @@ public class EventListServiceImpl implements EventListService {
}
}
private BigDecimal toPercent(BigDecimal value) {
return value == null ? null : value.multiply(PERCENT_BASE);
}
private BigDecimal fromPercent(BigDecimal value) {
return value == null ? null : value.divide(PERCENT_BASE);
}
private void fillExportEventTypeNames(List<EventListVO> exportRecords) {
Map<String, String> eventTypeNameMap = buildEventTypeNameMap();
for (EventListVO record : exportRecords) {
record.setEventType(translateEventType(record.getEventType(), eventTypeNameMap));
}
}
private Map<String, String> buildEventTypeNameMap() {
DictType dictType = dictTypeService.getByCode(DictConst.EVENT_TYPE_DICT);
if (dictType == null) {
dictType = dictTypeService.lambdaQuery()
.eq(DictType::getName, DictConst.EVENT_TYPE_DICT)
.eq(DictType::getState, DataStateEnum.ENABLE.getCode())
.orderByAsc(DictType::getSort)
.orderByDesc(DictType::getId)
.last("LIMIT 1")
.one();
}
if (dictType == null) {
return Collections.emptyMap();
}
List<DictData> dictDataList = dictDataService.listDictDataByTypeId(dictType.getId());
Map<String, String> eventTypeNameMap = new LinkedHashMap<String, String>();
for (DictData dictData : dictDataList) {
String id = trimToNull(dictData.getId());
String code = trimToNull(dictData.getCode());
if (id != null) {
eventTypeNameMap.put(id, defaultText(dictData.getName(), id));
}
if (code != null) {
eventTypeNameMap.put(code, defaultText(dictData.getName(), code));
}
}
return eventTypeNameMap;
}
private String translateEventType(String eventType, Map<String, String> eventTypeNameMap) {
String normalizedEventType = trimToNull(eventType);
if (normalizedEventType == null) {
return EMPTY_TEXT;
}
String eventTypeName = eventTypeNameMap.get(normalizedEventType);
return eventTypeName == null ? normalizedEventType : eventTypeName;
}
private void validateFlag(Integer flag, String message) {
if (flag != null && flag != 0 && flag != 1) {
throw new BusinessException(CommonResponseEnum.FAIL, message);
@@ -283,4 +665,79 @@ public class EventListServiceImpl implements EventListService {
String trimmed = value.trim();
return trimmed.isEmpty() ? null : trimmed;
}
private static class WaveExportItem {
private String eventId;
private MpEventDetailPO eventDetail;
private AddLedgerLinePathVO linePath;
private EventListVO eventVO;
private EventWavePathResolver.WaveFilePath waveFilePath;
private String status = WAVE_EXPORT_FAIL;
private String failReason;
private String getEventId() {
return eventId;
}
private void setEventId(String eventId) {
this.eventId = eventId;
}
private MpEventDetailPO getEventDetail() {
return eventDetail;
}
private void setEventDetail(MpEventDetailPO eventDetail) {
this.eventDetail = eventDetail;
}
private AddLedgerLinePathVO getLinePath() {
return linePath;
}
private void setLinePath(AddLedgerLinePathVO linePath) {
this.linePath = linePath;
}
private EventListVO getEventVO() {
return eventVO;
}
private void setEventVO(EventListVO eventVO) {
this.eventVO = eventVO;
}
private EventWavePathResolver.WaveFilePath getWaveFilePath() {
return waveFilePath;
}
private void setWaveFilePath(EventWavePathResolver.WaveFilePath waveFilePath) {
this.waveFilePath = waveFilePath;
}
private String getStatus() {
return status;
}
private String getFailReason() {
return failReason;
}
private void success() {
this.status = WAVE_EXPORT_SUCCESS;
this.failReason = null;
}
private void fail(String failReason) {
this.status = WAVE_EXPORT_FAIL;
this.failReason = failReason;
}
}
}

View File

@@ -0,0 +1,81 @@
package com.njcn.gather.event.eventlist.service.impl;
import java.nio.file.Path;
import java.nio.file.Paths;
/**
* 暂态事件波形文件路径解析器。
*/
final class EventWavePathResolver {
private EventWavePathResolver() {
}
static WaveFilePath resolve(String storageRoot, String equipmentMac, String wavePath) {
String rootText = requireText(storageRoot, "波形存储路径未配置");
String mac = requireText(equipmentMac, "设备 MAC 为空");
String normalizedWavePath = normalizeWavePath(wavePath);
Path rootPath = Paths.get(rootText).normalize();
Path rawWavePath = Paths.get(normalizedWavePath).normalize();
Path eventBasePath;
if (rawWavePath.isAbsolute()) {
eventBasePath = rawWavePath;
} else if (startsWithSegment(rawWavePath, mac)) {
eventBasePath = rootPath.resolve(rawWavePath).normalize();
} else {
eventBasePath = rootPath.resolve(mac).resolve(rawWavePath).normalize();
}
if (!eventBasePath.startsWith(rootPath)) {
throw new IllegalArgumentException("波形路径非法");
}
String fileName = eventBasePath.getFileName().toString();
return new WaveFilePath(eventBasePath.resolveSibling(fileName + ".cfg"),
eventBasePath.resolveSibling(fileName + ".dat"));
}
private static String normalizeWavePath(String value) {
String wavePath = requireText(value, "事件 wave_path 为空").replace("\\", "/");
String lowerWavePath = wavePath.toLowerCase();
if (lowerWavePath.endsWith(".cfg") || lowerWavePath.endsWith(".dat")) {
wavePath = wavePath.substring(0, wavePath.length() - 4);
}
if (wavePath.contains("..")) {
throw new IllegalArgumentException("波形路径非法");
}
return wavePath;
}
private static boolean startsWithSegment(Path path, String segment) {
return path.getNameCount() > 0 && path.getName(0).toString().equalsIgnoreCase(segment);
}
private static String requireText(String value, String message) {
if (value == null || value.trim().isEmpty()) {
throw new IllegalArgumentException(message);
}
return value.trim();
}
static class WaveFilePath {
private final Path cfgPath;
private final Path datPath;
private WaveFilePath(Path cfgPath, Path datPath) {
this.cfgPath = cfgPath;
this.datPath = datPath;
}
Path getCfgPath() {
return cfgPath;
}
Path getDatPath() {
return datPath;
}
}
}

View File

@@ -0,0 +1,44 @@
package com.njcn.gather.event.eventlist.service.impl;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.njcn.gather.event.eventlist.pojo.param.EventListQueryParam;
import com.njcn.gather.event.eventlist.pojo.vo.EventListVO;
import org.junit.Test;
import java.lang.reflect.Method;
import java.time.LocalDateTime;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
public class EventListTimeFormatTest {
@Test
public void startTimeJsonKeepsMilliseconds() throws Exception {
EventListVO vo = new EventListVO();
vo.setStartTime(LocalDateTime.of(2026, 5, 10, 13, 41, 17, 944000000));
String json = new ObjectMapper().writeValueAsString(vo);
assertTrue(json.contains("\"startTime\":\"2026-05-10 13:41:17.944\""));
}
@Test
public void queryTimeNormalizationKeepsMilliseconds() throws Exception {
EventListQueryParam param = new EventListQueryParam();
param.setStartTimeStart("2026-05-10 13:41:17.944");
param.setStartTimeEnd("2026-05-10 13:41:18.123");
normalizeQueryParam(param);
assertEquals("2026-05-10 13:41:17.944", param.getStartTimeStart());
assertEquals("2026-05-10 13:41:18.123", param.getStartTimeEnd());
}
private void normalizeQueryParam(EventListQueryParam param) throws Exception {
EventListServiceImpl service = new EventListServiceImpl(null, null, null, null, null, null);
Method method = EventListServiceImpl.class.getDeclaredMethod("normalizeQueryParam", EventListQueryParam.class);
method.setAccessible(true);
method.invoke(service, param);
}
}

View File

@@ -0,0 +1,40 @@
package com.njcn.gather.event.eventlist.service.impl;
import java.nio.file.Path;
import java.nio.file.Paths;
public class EventWavePathResolverTest {
public static void main(String[] args) {
resolvesAbsoluteWavePath();
resolvesRelativeWavePathWithMacPrefix();
resolvesRelativeWavePathAlreadyContainingMac();
}
private static void resolvesAbsoluteWavePath() {
EventWavePathResolver.WaveFilePath result = EventWavePathResolver.resolve("D:/", "AA-BB", "D:/wave/event-001.cfg");
assertPathEquals(Paths.get("D:/wave/event-001.cfg"), result.getCfgPath(), "absolute cfg");
assertPathEquals(Paths.get("D:/wave/event-001.dat"), result.getDatPath(), "absolute dat");
}
private static void resolvesRelativeWavePathWithMacPrefix() {
EventWavePathResolver.WaveFilePath result = EventWavePathResolver.resolve("D:/wave-root", "AA-BB", "event-001");
assertPathEquals(Paths.get("D:/wave-root/AA-BB/event-001.cfg"), result.getCfgPath(), "relative cfg");
assertPathEquals(Paths.get("D:/wave-root/AA-BB/event-001.dat"), result.getDatPath(), "relative dat");
}
private static void resolvesRelativeWavePathAlreadyContainingMac() {
EventWavePathResolver.WaveFilePath result = EventWavePathResolver.resolve("D:/wave-root", "AA-BB", "AA-BB/event-001.dat");
assertPathEquals(Paths.get("D:/wave-root/AA-BB/event-001.cfg"), result.getCfgPath(), "relative mac cfg");
assertPathEquals(Paths.get("D:/wave-root/AA-BB/event-001.dat"), result.getDatPath(), "relative mac dat");
}
private static void assertPathEquals(Path expected, Path actual, String label) {
if (!expected.normalize().equals(actual.normalize())) {
throw new AssertionError(label + " expected " + expected + " but got " + actual);
}
}
}

View File

@@ -38,7 +38,6 @@ public class SteadyTrendFieldResolver {
validateBasicParam(param);
List<String> lineIds = normalizeTextList(param.getLineIds());
List<String> indicatorCodes = normalizeTextList(param.getIndicatorCodes());
List<String> requestPhases = normalizeUpperList(param.getPhases());
List<String> statTypes = normalizeUpperList(param.getStatTypes());
if (statTypes.isEmpty()) {
statTypes.add("AVG");
@@ -47,7 +46,7 @@ public class SteadyTrendFieldResolver {
for (String lineId : lineIds) {
for (String indicatorCode : indicatorCodes) {
SteadyTrendIndicatorDefinitionBO indicator = requireIndicator(indicatorCode);
List<String> phases = resolvePhases(indicator, requestPhases);
List<String> phases = resolvePhases(indicator);
for (String phase : phases) {
for (String statType : statTypes) {
validateStatType(indicator, statType);
@@ -57,7 +56,7 @@ public class SteadyTrendFieldResolver {
}
}
if (result.size() > MAX_SERIES_COUNT) {
throw fail("趋势曲线数量不能超过 24 条,请缩小监测点、指标、相别或统计类型范围");
throw fail("趋势曲线数量不能超过 24 条,请缩小监测点、指标或统计类型范围");
}
return result;
}
@@ -165,23 +164,8 @@ public class SteadyTrendFieldResolver {
return indicator;
}
private List<String> resolvePhases(SteadyTrendIndicatorDefinitionBO indicator, List<String> requestPhases) {
if (requestPhases.isEmpty()) {
return new ArrayList<String>(indicator.getPhaseCodes());
}
List<String> result = new ArrayList<String>();
for (String phase : requestPhases) {
if (!"A".equals(phase) && !"B".equals(phase) && !"C".equals(phase) && !"T".equals(phase)) {
throw fail("相别只能是 A、B、C、T");
}
if (indicator.getPhaseCodes().contains(phase) && !result.contains(phase)) {
result.add(phase);
}
}
if (result.isEmpty()) {
throw fail("指标 " + indicator.getIndicatorCode() + " 不支持当前相别");
}
return result;
private List<String> resolvePhases(SteadyTrendIndicatorDefinitionBO indicator) {
return new ArrayList<String>(indicator.getPhaseCodes());
}
private void validateStatType(SteadyTrendIndicatorDefinitionBO indicator, String statType) {

View File

@@ -1,13 +1,11 @@
package com.njcn.gather.steady.datavie.controller;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.njcn.common.pojo.annotation.OperateInfo;
import com.njcn.common.pojo.enums.common.LogEnum;
import com.njcn.common.pojo.enums.response.CommonResponseEnum;
import com.njcn.common.pojo.response.HttpResult;
import com.njcn.common.utils.LogUtil;
import com.njcn.gather.steady.datavie.pojo.param.SteadyDataViewDetailParam;
import com.njcn.gather.steady.datavie.pojo.param.SteadyDataViewQueryParam;
import com.njcn.gather.steady.datavie.pojo.vo.SteadyDataViewTemplateVO;
import com.njcn.gather.steady.datavie.pojo.vo.SteadyDataViewVO;
import com.njcn.gather.steady.datavie.service.SteadyDataViewService;
@@ -40,16 +38,6 @@ public class SteadyDataViewController extends BaseController {
/** 稳态数据查看服务。 */
private final SteadyDataViewService steadyDataViewService;
@OperateInfo(info = LogEnum.BUSINESS_COMMON)
@ApiOperation("分页查询稳态数据")
@PostMapping("/page")
public HttpResult<Page<SteadyDataViewVO>> pageSteadyData(@RequestBody SteadyDataViewQueryParam param) {
String methodDescribe = getMethodDescribe("pageSteadyData");
LogUtil.njcnDebug(log, "{}开始分页查询稳态数据param={}", methodDescribe, param);
Page<SteadyDataViewVO> result = steadyDataViewService.pageSteadyData(param);
return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, result, methodDescribe);
}
@OperateInfo(info = LogEnum.BUSINESS_COMMON)
@ApiOperation("查询稳态数据详情")
@PostMapping("/detail")

View File

@@ -1,7 +1,5 @@
package com.njcn.gather.steady.datavie.mapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.njcn.gather.steady.datavie.pojo.param.SteadyDataViewQueryParam;
import org.apache.ibatis.annotations.Param;
import java.util.List;
@@ -12,11 +10,6 @@ import java.util.Map;
*/
public interface SteadyDataViewMapper {
Page<Map<String, Object>> selectSteadyPage(Page<Map<String, Object>> page,
@Param("tableName") String tableName,
@Param("columns") List<String> columns,
@Param("param") SteadyDataViewQueryParam param);
Map<String, Object> selectSteadyDetail(@Param("tableName") String tableName,
@Param("columns") List<String> columns,
@Param("lineId") String lineId,

View File

@@ -3,39 +3,6 @@
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.njcn.gather.steady.datavie.mapper.SteadyDataViewMapper">
<sql id="SteadyDataWhere">
<where>
<if test="param.timeStart != null and param.timeStart != ''">
AND TIMEID &gt;= #{param.timeStart}
</if>
<if test="param.timeEnd != null and param.timeEnd != ''">
AND TIMEID &lt;= #{param.timeEnd}
</if>
<if test="param.phasicType != null and param.phasicType != ''">
AND PHASIC_TYPE = #{param.phasicType}
</if>
<if test="param.qualityFlag != null">
AND QUALITYFLAG = #{param.qualityFlag}
</if>
<if test="param.lineIds != null and param.lineIds.size() &gt; 0">
AND LINEID IN
<foreach collection="param.lineIds" item="lineId" open="(" separator="," close=")">
#{lineId}
</foreach>
</if>
</where>
</sql>
<select id="selectSteadyPage" resultType="java.util.LinkedHashMap">
SELECT
<foreach collection="columns" item="column" separator=",">
`${column}`
</foreach>
FROM `${tableName}`
<include refid="SteadyDataWhere"/>
ORDER BY TIMEID DESC, LINEID ASC, PHASIC_TYPE ASC
</select>
<select id="selectSteadyDetail" resultType="java.util.LinkedHashMap">
SELECT
<foreach collection="columns" item="column" separator=",">

View File

@@ -1,49 +0,0 @@
package com.njcn.gather.steady.datavie.pojo.param;
import com.njcn.web.pojo.param.BaseParam;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.util.ArrayList;
import java.util.List;
/**
* 稳态数据查看查询参数。
*/
@Data
@EqualsAndHashCode(callSuper = true)
@ApiModel("稳态数据查看查询参数")
public class SteadyDataViewQueryParam extends BaseParam {
@ApiModelProperty("表名,对应 add-data 模板表名")
private String tableName;
@ApiModelProperty("时间开始,格式 yyyy-MM-dd HH:mm:ss")
private String timeStart;
@ApiModelProperty("时间结束,格式 yyyy-MM-dd HH:mm:ss")
private String timeEnd;
@ApiModelProperty("相别A/B/C/T")
private String phasicType;
@ApiModelProperty("质量标识")
private Integer qualityFlag;
@ApiModelProperty("监测点 ID 列表")
private List<String> lineIds = new ArrayList<String>();
@ApiModelProperty("工程名称关键字")
private String engineeringName;
@ApiModelProperty("项目名称关键字")
private String projectName;
@ApiModelProperty("设备名称关键字")
private String equipmentName;
@ApiModelProperty("监测点名称关键字")
private String lineName;
}

View File

@@ -23,9 +23,6 @@ public class SteadyTrendQueryParam {
@ApiModelProperty("统计类型AVG/MAX/MIN/CP95")
private List<String> statTypes = new ArrayList<String>();
@ApiModelProperty("相别A/B/C/T")
private List<String> phases = new ArrayList<String>();
@ApiModelProperty("开始时间,格式 yyyy-MM-dd HH:mm:ss")
private String timeStart;

View File

@@ -1,8 +1,6 @@
package com.njcn.gather.steady.datavie.service;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.njcn.gather.steady.datavie.pojo.param.SteadyDataViewDetailParam;
import com.njcn.gather.steady.datavie.pojo.param.SteadyDataViewQueryParam;
import com.njcn.gather.steady.datavie.pojo.vo.SteadyDataViewTemplateVO;
import com.njcn.gather.steady.datavie.pojo.vo.SteadyDataViewVO;
@@ -13,8 +11,6 @@ import java.util.List;
*/
public interface SteadyDataViewService {
Page<SteadyDataViewVO> pageSteadyData(SteadyDataViewQueryParam param);
SteadyDataViewVO getSteadyDataDetail(SteadyDataViewDetailParam param);
List<SteadyDataViewTemplateVO> listTemplates();

View File

@@ -1,11 +1,9 @@
package com.njcn.gather.steady.datavie.service.impl;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.njcn.common.pojo.enums.response.CommonResponseEnum;
import com.njcn.common.pojo.exception.BusinessException;
import com.njcn.gather.steady.datavie.mapper.SteadyDataViewMapper;
import com.njcn.gather.steady.datavie.pojo.param.SteadyDataViewDetailParam;
import com.njcn.gather.steady.datavie.pojo.param.SteadyDataViewQueryParam;
import com.njcn.gather.steady.datavie.pojo.vo.SteadyDataViewTemplateVO;
import com.njcn.gather.steady.datavie.pojo.vo.SteadyDataViewVO;
import com.njcn.gather.steady.datavie.service.SteadyDataViewService;
@@ -13,12 +11,9 @@ import com.njcn.gather.tool.adddata.component.AddDataTableRegistry;
import com.njcn.gather.tool.adddata.component.AddDataTemplateRegistry;
import com.njcn.gather.tool.adddata.pojo.bo.AddDataTableDefinition;
import com.njcn.gather.tool.adddata.pojo.vo.AddDataTemplateVO;
import com.njcn.gather.tool.addledger.pojo.param.AddLedgerLinePathQueryParam;
import com.njcn.gather.tool.addledger.pojo.vo.AddLedgerLinePathVO;
import com.njcn.gather.tool.addledger.service.AddLedgerService;
import com.njcn.web.factory.PageFactory;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.sql.Timestamp;
@@ -34,13 +29,10 @@ import java.util.Map;
/**
* 稳态数据查看服务实现。
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class SteadyDataViewServiceImpl implements SteadyDataViewService {
private static final int LEDGER_LINE_QUERY_LIMIT = 1000;
private static final int STEADY_LINE_ID_QUERY_LIMIT = 1000;
private static final String DEFAULT_TABLE_NAME = "data_v";
private static final String EMPTY_TEXT = "-";
private static final DateTimeFormatter OUTPUT_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
@@ -56,22 +48,6 @@ public class SteadyDataViewServiceImpl implements SteadyDataViewService {
private final AddDataTableRegistry addDataTableRegistry;
private final AddDataTemplateRegistry addDataTemplateRegistry;
@Override
public Page<SteadyDataViewVO> pageSteadyData(SteadyDataViewQueryParam param) {
SteadyDataViewQueryParam queryParam = normalizeQueryParam(param);
AddDataTableDefinition definition = resolveTableDefinition(queryParam.getTableName());
if (!resolveLineFilter(queryParam)) {
return emptyPage(queryParam);
}
Page<Map<String, Object>> steadyPage = steadyDataViewMapper.selectSteadyPage(
new Page<Map<String, Object>>(PageFactory.getPageNum(queryParam), PageFactory.getPageSize(queryParam)),
definition.getTableName(), definition.getColumns(), queryParam);
List<SteadyDataViewVO> records = buildSteadyDataList(definition.getTableName(), definition.getColumns(), steadyPage.getRecords());
Page<SteadyDataViewVO> resultPage = new Page<SteadyDataViewVO>(steadyPage.getCurrent(), steadyPage.getSize(), steadyPage.getTotal());
resultPage.setRecords(records);
return resultPage;
}
@Override
public SteadyDataViewVO getSteadyDataDetail(SteadyDataViewDetailParam param) {
if (param == null) {
@@ -156,69 +132,6 @@ public class SteadyDataViewServiceImpl implements SteadyDataViewService {
return vo;
}
private SteadyDataViewQueryParam normalizeQueryParam(SteadyDataViewQueryParam param) {
SteadyDataViewQueryParam queryParam = param == null ? new SteadyDataViewQueryParam() : param;
queryParam.setTableName(normalizeTableName(queryParam.getTableName()));
LocalDateTime startTime = parseDateTime(queryParam.getTimeStart());
LocalDateTime endTime = parseDateTime(queryParam.getTimeEnd());
if (startTime == null) {
LocalDateTime now = LocalDateTime.now();
startTime = LocalDateTime.of(now.getYear(), now.getMonth(), 1, 0, 0, 0);
}
if (endTime == null) {
endTime = LocalDateTime.now();
}
if (startTime.isAfter(endTime)) {
throw new BusinessException(CommonResponseEnum.FAIL, "开始时间不能大于结束时间");
}
queryParam.setTimeStart(OUTPUT_FORMATTER.format(startTime));
queryParam.setTimeEnd(OUTPUT_FORMATTER.format(endTime));
queryParam.setPhasicType(normalizePhasicType(queryParam.getPhasicType()));
validateQualityFlag(queryParam.getQualityFlag());
queryParam.setEngineeringName(trimToNull(queryParam.getEngineeringName()));
queryParam.setProjectName(trimToNull(queryParam.getProjectName()));
queryParam.setEquipmentName(trimToNull(queryParam.getEquipmentName()));
queryParam.setLineName(trimToNull(queryParam.getLineName()));
List<String> lineIds = normalizeIds(queryParam.getLineIds());
if (lineIds.size() > STEADY_LINE_ID_QUERY_LIMIT) {
throw new BusinessException(CommonResponseEnum.FAIL, "监测点 ID 查询数量不能超过 1000 个");
}
queryParam.setLineIds(lineIds);
return queryParam;
}
private boolean resolveLineFilter(SteadyDataViewQueryParam queryParam) {
if (!hasLedgerKeyword(queryParam)) {
return true;
}
AddLedgerLinePathQueryParam linePathQueryParam = new AddLedgerLinePathQueryParam();
linePathQueryParam.setEngineeringName(queryParam.getEngineeringName());
linePathQueryParam.setProjectName(queryParam.getProjectName());
linePathQueryParam.setEquipmentName(queryParam.getEquipmentName());
linePathQueryParam.setLineName(queryParam.getLineName());
linePathQueryParam.setLimit(LEDGER_LINE_QUERY_LIMIT + 1);
List<String> ledgerLineIds = addLedgerService.listLineIdsByPathQuery(linePathQueryParam);
if (ledgerLineIds.size() > LEDGER_LINE_QUERY_LIMIT) {
throw new BusinessException(CommonResponseEnum.FAIL, "台账检索匹配监测点过多,请缩小查询条件");
}
if (ledgerLineIds.isEmpty()) {
return false;
}
List<String> explicitLineIds = normalizeIds(queryParam.getLineIds());
if (explicitLineIds.isEmpty()) {
queryParam.setLineIds(ledgerLineIds);
return true;
}
List<String> intersectLineIds = new ArrayList<String>();
for (String lineId : explicitLineIds) {
if (ledgerLineIds.contains(lineId)) {
intersectLineIds.add(lineId);
}
}
queryParam.setLineIds(intersectLineIds);
return !intersectLineIds.isEmpty();
}
private AddDataTableDefinition resolveTableDefinition(String tableName) {
try {
return addDataTableRegistry.getDefinition(tableName);
@@ -267,39 +180,6 @@ public class SteadyDataViewServiceImpl implements SteadyDataViewService {
return normalized;
}
private void validateQualityFlag(Integer qualityFlag) {
if (qualityFlag != null && qualityFlag != 0 && qualityFlag != 1) {
throw new BusinessException(CommonResponseEnum.FAIL, "质量标识只能是 0 或 1");
}
}
private List<String> normalizeIds(List<String> ids) {
if (ids == null || ids.isEmpty()) {
return Collections.emptyList();
}
List<String> normalizedIds = new ArrayList<String>();
for (String id : ids) {
String normalizedId = trimToNull(id);
if (normalizedId != null && !normalizedIds.contains(normalizedId)) {
normalizedIds.add(normalizedId);
}
}
return normalizedIds;
}
private boolean hasLedgerKeyword(SteadyDataViewQueryParam queryParam) {
return trimToNull(queryParam.getEngineeringName()) != null
|| trimToNull(queryParam.getProjectName()) != null
|| trimToNull(queryParam.getEquipmentName()) != null
|| trimToNull(queryParam.getLineName()) != null;
}
private Page<SteadyDataViewVO> emptyPage(SteadyDataViewQueryParam queryParam) {
Page<SteadyDataViewVO> page = new Page<SteadyDataViewVO>(PageFactory.getPageNum(queryParam), PageFactory.getPageSize(queryParam), 0);
page.setRecords(Collections.<SteadyDataViewVO>emptyList());
return page;
}
private List<String> resolveValueColumns(List<String> columns) {
List<String> result = new ArrayList<String>();
for (String column : columns) {

View File

@@ -183,7 +183,6 @@ public class SteadyDataViewTrendServiceImpl implements SteadyDataViewTrendServic
target.setLineIds(copyList(source.getLineIds()));
target.setIndicatorCodes(copyList(source.getIndicatorCodes()));
target.setStatTypes(copyList(source.getStatTypes()));
target.setPhases(copyList(source.getPhases()));
target.setTimeStart(source.getTimeStart());
target.setTimeEnd(source.getTimeEnd());
target.setBucket(source.getBucket());

View File

@@ -1,51 +0,0 @@
package com.njcn.gather.steady.datavie.component;
import com.njcn.gather.steady.datavie.config.SteadyInfluxDbProperties;
import com.njcn.gather.steady.datavie.pojo.bo.SteadyTrendResolvedFieldBO;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import java.time.LocalDateTime;
/**
* InfluxQL 查询语句生成测试。
*/
class SteadyInfluxQueryComponentTest {
@Test
void shouldBuildBucketedTrendQueryWithRequiredTags() {
SteadyInfluxQueryComponent component = new SteadyInfluxQueryComponent(new SteadyInfluxDbProperties());
SteadyTrendResolvedFieldBO field = new SteadyTrendResolvedFieldBO();
field.setMeasurement("data_v");
field.setField("RMS_CP95");
field.setLineId("line-001");
field.setPhase("A");
String query = component.buildTrendQuery(field,
LocalDateTime.of(2026, 5, 1, 0, 0, 0),
LocalDateTime.of(2026, 5, 1, 1, 0, 0),
"10m",
1);
Assertions.assertEquals("SELECT mean(\"RMS_CP95\") AS \"value\" FROM \"data_v\" WHERE time >= '2026-05-01T00:00:00Z' AND time <= '2026-05-01T01:00:00Z' AND \"LINEID\" = 'line-001' AND \"PHASIC_TYPE\" = 'A' AND \"QUALITYFLAG\" = '1' GROUP BY time(10m) fill(none)", query);
}
@Test
void shouldEscapeTagValuesInTrendQuery() {
SteadyInfluxQueryComponent component = new SteadyInfluxQueryComponent(new SteadyInfluxDbProperties());
SteadyTrendResolvedFieldBO field = new SteadyTrendResolvedFieldBO();
field.setMeasurement("data_v");
field.setField("RMS");
field.setLineId("line'001");
field.setPhase("A");
String query = component.buildTrendQuery(field,
LocalDateTime.of(2026, 5, 1, 0, 0, 0),
LocalDateTime.of(2026, 5, 1, 1, 0, 0),
null,
null);
Assertions.assertTrue(query.contains("\"LINEID\" = 'line\\'001'"));
Assertions.assertFalse(query.contains("GROUP BY time"));
}
}

View File

@@ -10,6 +10,7 @@ import org.junit.jupiter.api.Test;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
/**
* 稳态趋势字段解析测试。
@@ -26,31 +27,25 @@ class SteadyTrendFieldResolverTest {
}
@Test
void shouldResolveVoltageRmsAverageAndCp95Fields() {
void shouldExpandAllCatalogPhasesWithoutRequestPhaseFilter() {
SteadyTrendQueryParam param = new SteadyTrendQueryParam();
param.setLineIds(Arrays.asList("line-001"));
param.setIndicatorCodes(Arrays.asList("V_RMS"));
param.setPhases(Arrays.asList("A"));
param.setStatTypes(Arrays.asList("AVG", "CP95"));
param.setStatTypes(Arrays.asList("AVG"));
param.setTimeStart("2026-05-01 00:00:00");
param.setTimeEnd("2026-05-01 01:00:00");
List<SteadyTrendResolvedFieldBO> fields = resolver.resolveFields(param);
List<String> phases = fields.stream().map(SteadyTrendResolvedFieldBO::getPhase).collect(Collectors.toList());
Assertions.assertEquals(2, fields.size());
Assertions.assertEquals("data_v", fields.get(0).getMeasurement());
Assertions.assertEquals("RMS", fields.get(0).getField());
Assertions.assertEquals("AVG", fields.get(0).getStatType());
Assertions.assertEquals("RMS_CP95", fields.get(1).getField());
Assertions.assertEquals("V", fields.get(1).getUnit());
Assertions.assertEquals(Arrays.asList("A", "B", "C"), phases);
}
@Test
void shouldExpandLineVoltageTotalPhaseToThreeSeries() {
void shouldExpandTotalPhaseIndicatorWithoutRequestPhaseFilter() {
SteadyTrendQueryParam param = new SteadyTrendQueryParam();
param.setLineIds(Arrays.asList("line-001"));
param.setIndicatorCodes(Arrays.asList("V_LINE_RMS"));
param.setPhases(Arrays.asList("T"));
param.setStatTypes(Arrays.asList("AVG"));
param.setTimeStart("2026-05-01 00:00:00");
param.setTimeEnd("2026-05-01 01:00:00");
@@ -58,10 +53,10 @@ class SteadyTrendFieldResolverTest {
List<SteadyTrendResolvedFieldBO> fields = resolver.resolveFields(param);
Assertions.assertEquals(3, fields.size());
Assertions.assertEquals("T", fields.get(0).getPhase());
Assertions.assertEquals("RMSAB", fields.get(0).getField());
Assertions.assertEquals("RMSBC", fields.get(1).getField());
Assertions.assertEquals("RMSCA", fields.get(2).getField());
Assertions.assertEquals("T", fields.get(0).getPhase());
}
@Test
@@ -69,7 +64,6 @@ class SteadyTrendFieldResolverTest {
SteadyTrendQueryParam param = new SteadyTrendQueryParam();
param.setLineIds(Arrays.asList("line-001"));
param.setIndicatorCodes(Arrays.asList("V_HARMONIC"));
param.setPhases(Arrays.asList("A"));
param.setStatTypes(Arrays.asList("AVG"));
param.setTimeStart("2026-05-01 00:00:00");
param.setTimeEnd("2026-05-01 01:00:00");
@@ -80,11 +74,10 @@ class SteadyTrendFieldResolverTest {
}
@Test
void shouldResolveSelectedHarmonicOrdersOnly() {
void shouldResolveSelectedHarmonicOrdersForAllCatalogPhases() {
SteadyTrendQueryParam param = new SteadyTrendQueryParam();
param.setLineIds(Arrays.asList("line-001"));
param.setIndicatorCodes(Arrays.asList("V_HARMONIC"));
param.setPhases(Arrays.asList("A"));
param.setStatTypes(Arrays.asList("MAX"));
param.setHarmonicOrders(Arrays.asList(3, 5));
param.setTimeStart("2026-05-01 00:00:00");
@@ -92,8 +85,11 @@ class SteadyTrendFieldResolverTest {
List<SteadyTrendResolvedFieldBO> fields = resolver.resolveFields(param);
Assertions.assertEquals(2, fields.size());
Assertions.assertEquals(6, fields.size());
Assertions.assertEquals("A", fields.get(0).getPhase());
Assertions.assertEquals("V_3_MAX", fields.get(0).getField());
Assertions.assertEquals("V_5_MAX", fields.get(1).getField());
Assertions.assertEquals("B", fields.get(2).getPhase());
Assertions.assertEquals("C", fields.get(4).getPhase());
}
}

View File

@@ -0,0 +1,19 @@
package com.njcn.gather.steady.datavie.controller;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import java.lang.reflect.Method;
/**
* 稳态数据查看接口契约测试。
*/
class SteadyDataViewControllerTest {
@Test
void shouldNotExposePageQueryEndpointMethod() {
for (Method method : SteadyDataViewController.class.getDeclaredMethods()) {
Assertions.assertNotEquals("pageSteadyData", method.getName(), "本功能不提供分页检索接口");
}
}
}

View File

@@ -0,0 +1,19 @@
package com.njcn.gather.steady.datavie.pojo.param;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import java.lang.reflect.Field;
/**
* 稳态趋势查询参数契约测试。
*/
class SteadyTrendQueryParamTest {
@Test
void shouldNotExposePhaseFilterInTrendQueryParam() {
for (Field field : SteadyTrendQueryParam.class.getDeclaredFields()) {
Assertions.assertNotEquals("phases", field.getName(), "趋势检索请求不再携带相别条件");
}
}
}

View File

@@ -1,27 +0,0 @@
package com.njcn.gather.steady.datavie.service.impl;
import com.njcn.gather.steady.datavie.component.SteadyTrendIndicatorCatalog;
import com.njcn.gather.steady.datavie.pojo.vo.SteadyDataViewIndicatorNodeVO;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import java.util.List;
/**
* 稳态趋势指标服务测试。
*/
class SteadyDataViewIndicatorServiceImplTest {
@Test
void shouldGroupIndicatorsByCategory() {
SteadyDataViewIndicatorServiceImpl service = new SteadyDataViewIndicatorServiceImpl(new SteadyTrendIndicatorCatalog());
List<SteadyDataViewIndicatorNodeVO> tree = service.listIndicatorTree();
Assertions.assertEquals(5, tree.size());
Assertions.assertEquals("VOLTAGE", tree.get(0).getGroupCode());
Assertions.assertTrue(tree.get(0).getChildren().size() >= 2);
Assertions.assertEquals("V_RMS", tree.get(0).getChildren().get(0).getIndicatorCode());
Assertions.assertTrue(tree.get(0).getChildren().get(0).getSelectable());
}
}

View File

@@ -1,123 +0,0 @@
package com.njcn.gather.steady.datavie.service.impl;
import com.njcn.gather.steady.datavie.component.SteadyInfluxQueryComponent;
import com.njcn.gather.steady.datavie.component.SteadyTrendFieldResolver;
import com.njcn.gather.steady.datavie.pojo.bo.SteadyTrendResolvedFieldBO;
import com.njcn.gather.steady.datavie.pojo.param.SteadyTrendQueryParam;
import com.njcn.gather.steady.datavie.pojo.vo.SteadyTrendPointVO;
import com.njcn.gather.steady.datavie.pojo.vo.SteadyTrendQueryVO;
import com.njcn.gather.steady.datavie.pojo.vo.SteadyTrendSummaryVO;
import com.njcn.gather.tool.addledger.pojo.vo.AddLedgerLinePathVO;
import com.njcn.gather.tool.addledger.service.AddLedgerService;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.Collections;
/**
* 稳态趋势服务编排测试。
*/
class SteadyDataViewTrendServiceImplTest {
@Test
void shouldBuildTrendQueryWithDefaultBucketAndLineName() {
SteadyTrendFieldResolver fieldResolver = Mockito.mock(SteadyTrendFieldResolver.class);
SteadyInfluxQueryComponent influxQueryComponent = Mockito.mock(SteadyInfluxQueryComponent.class);
AddLedgerService addLedgerService = Mockito.mock(AddLedgerService.class);
SteadyTrendQueryParam param = new SteadyTrendQueryParam();
param.setLineIds(Arrays.asList("line-001"));
param.setIndicatorCodes(Arrays.asList("V_RMS"));
param.setPhases(Arrays.asList("A"));
param.setStatTypes(Arrays.asList("AVG"));
param.setTimeStart("2026-05-01 00:00:00");
param.setTimeEnd("2026-05-01 05:59:59");
SteadyTrendResolvedFieldBO field = new SteadyTrendResolvedFieldBO();
field.setMeasurement("data_v");
field.setField("RMS");
field.setLineId("line-001");
field.setIndicatorCode("V_RMS");
field.setIndicatorName("相电压有效值");
field.setSeriesName("相电压有效值");
field.setPhase("A");
field.setStatType("AVG");
field.setUnit("V");
field.setSeriesKey("line-001|V_RMS|A|AVG|RMS");
Mockito.when(fieldResolver.parseRequiredTime("2026-05-01 00:00:00", "开始时间不能为空"))
.thenReturn(LocalDateTime.of(2026, 5, 1, 0, 0, 0));
Mockito.when(fieldResolver.parseRequiredTime("2026-05-01 05:59:59", "结束时间不能为空"))
.thenReturn(LocalDateTime.of(2026, 5, 1, 5, 59, 59));
Mockito.when(fieldResolver.resolveFields(Mockito.any())).thenReturn(Collections.singletonList(field));
Mockito.when(addLedgerService.listLinePathByLineIds(Collections.singletonList("line-001")))
.thenReturn(Collections.singletonMap("line-001", buildLinePath("进线一")));
Mockito.when(influxQueryComponent.queryTrendPoints(Mockito.any(), Mockito.any(), Mockito.any(), Mockito.eq("1m"), Mockito.isNull()))
.thenReturn(Collections.singletonList(new SteadyTrendPointVO("2026-05-01 00:00:00", new BigDecimal("1.2"))));
SteadyDataViewTrendServiceImpl service = new SteadyDataViewTrendServiceImpl(fieldResolver, influxQueryComponent, addLedgerService);
SteadyTrendQueryVO result = service.queryTrend(param);
Assertions.assertEquals("1m", result.getBucket());
Assertions.assertTrue(result.getSampled());
Assertions.assertEquals("进线一", result.getSeries().get(0).getLineName());
}
@Test
void shouldCalculateTrendSummaryFromSeriesPoints() {
SteadyTrendFieldResolver fieldResolver = Mockito.mock(SteadyTrendFieldResolver.class);
SteadyInfluxQueryComponent influxQueryComponent = Mockito.mock(SteadyInfluxQueryComponent.class);
AddLedgerService addLedgerService = Mockito.mock(AddLedgerService.class);
SteadyTrendQueryParam param = new SteadyTrendQueryParam();
param.setLineIds(Arrays.asList("line-001"));
param.setIndicatorCodes(Arrays.asList("V_RMS"));
param.setPhases(Arrays.asList("A"));
param.setStatTypes(Arrays.asList("AVG"));
param.setTimeStart("2026-05-01 00:00:00");
param.setTimeEnd("2026-05-01 05:59:59");
SteadyTrendResolvedFieldBO field = new SteadyTrendResolvedFieldBO();
field.setMeasurement("data_v");
field.setField("RMS");
field.setLineId("line-001");
field.setIndicatorCode("V_RMS");
field.setIndicatorName("相电压有效值");
field.setSeriesName("相电压有效值");
field.setPhase("A");
field.setStatType("AVG");
field.setUnit("V");
field.setSeriesKey("line-001|V_RMS|A|AVG|RMS");
Mockito.when(fieldResolver.parseRequiredTime(Mockito.anyString(), Mockito.anyString()))
.thenReturn(LocalDateTime.of(2026, 5, 1, 0, 0, 0))
.thenReturn(LocalDateTime.of(2026, 5, 1, 5, 59, 59));
Mockito.when(fieldResolver.resolveFields(param)).thenReturn(Collections.singletonList(field));
Mockito.when(addLedgerService.listLinePathByLineIds(Collections.singletonList("line-001")))
.thenReturn(Collections.<String, AddLedgerLinePathVO>emptyMap());
Mockito.when(influxQueryComponent.queryTrendPoints(Mockito.any(), Mockito.any(), Mockito.any(), Mockito.isNull(), Mockito.isNull()))
.thenReturn(Arrays.asList(
new SteadyTrendPointVO("2026-05-01 00:00:00", new BigDecimal("1")),
new SteadyTrendPointVO("2026-05-01 01:00:00", new BigDecimal("3")),
new SteadyTrendPointVO("2026-05-01 02:00:00", new BigDecimal("2"))
));
SteadyDataViewTrendServiceImpl service = new SteadyDataViewTrendServiceImpl(fieldResolver, influxQueryComponent, addLedgerService);
SteadyTrendSummaryVO summary = service.summarizeTrend(param);
Assertions.assertEquals(new BigDecimal("3"), summary.getItems().get(0).getMax());
Assertions.assertEquals(new BigDecimal("1"), summary.getItems().get(0).getMin());
Assertions.assertEquals(new BigDecimal("2.000000"), summary.getItems().get(0).getAvg());
Assertions.assertEquals(new BigDecimal("3"), summary.getItems().get(0).getCp95());
}
private AddLedgerLinePathVO buildLinePath(String lineName) {
AddLedgerLinePathVO linePathVO = new AddLedgerLinePathVO();
linePathVO.setLineName(lineName);
return linePathVO;
}
}

View File

@@ -0,0 +1,61 @@
package com.njcn.gather.system.cfg.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.system.cfg.pojo.param.SysConfigParam;
import com.njcn.gather.system.cfg.pojo.po.SysConfig;
import com.njcn.gather.system.cfg.service.ISysConfigService;
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.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 系统配置接口。
*/
@Slf4j
@Api(tags = "系统配置")
@RestController
@RequestMapping("/sysConfig")
@RequiredArgsConstructor
public class SysConfigController extends BaseController {
private final ISysConfigService sysConfigService;
@OperateInfo(info = LogEnum.SYSTEM_COMMON)
@GetMapping("/getConfig")
@ApiOperation("获取系统配置")
public HttpResult<SysConfig> getConfig() {
String methodDescribe = getMethodDescribe("getConfig");
LogUtil.njcnDebug(log, "{}", methodDescribe);
SysConfig sysConfig = sysConfigService.getOneConfig();
return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, sysConfig, methodDescribe);
}
@OperateInfo(info = LogEnum.SYSTEM_COMMON, operateType = OperateType.UPDATE)
@PostMapping("/update")
@ApiOperation("修改系统配置")
@ApiImplicitParam(name = "sysConfig", value = "系统配置", required = true)
public HttpResult<Boolean> update(@RequestBody @Validated SysConfigParam.UpdateParam sysConfig) {
String methodDescribe = getMethodDescribe("update");
LogUtil.njcnDebug(log, "{}", methodDescribe);
boolean result = sysConfigService.updateConfig(sysConfig);
if (result) {
return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, true, methodDescribe);
}
return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.FAIL, false, methodDescribe);
}
}

View File

@@ -0,0 +1,10 @@
package com.njcn.gather.system.cfg.mapper;
import com.github.yulichang.base.MPJBaseMapper;
import com.njcn.gather.system.cfg.pojo.po.SysConfig;
/**
* 系统配置 Mapper。
*/
public interface SysConfigMapper extends MPJBaseMapper<SysConfig> {
}

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.njcn.gather.system.cfg.mapper.SysConfigMapper">
</mapper>

View File

@@ -0,0 +1,26 @@
package com.njcn.gather.system.cfg.pojo.param;
import com.njcn.common.pojo.constant.PatternRegex;
import com.njcn.gather.system.pojo.constant.SystemValidMessage;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import javax.validation.constraints.Pattern;
/**
* 系统配置参数。
*/
@Data
public class SysConfigParam {
@ApiModelProperty("波形文件存储根路径")
private String waveStoragePath;
@Data
public static class UpdateParam extends SysConfigParam {
@ApiModelProperty("id")
@Pattern(regexp = PatternRegex.SYSTEM_ID, message = SystemValidMessage.ID_FORMAT_ERROR)
private String id;
}
}

View File

@@ -0,0 +1,36 @@
package com.njcn.gather.system.cfg.pojo.po;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import com.njcn.db.mybatisplus.bo.BaseEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.io.Serializable;
/**
* 系统配置。
*/
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("sys_config")
public class SysConfig extends BaseEntity implements Serializable {
private static final long serialVersionUID = -3293972902209033082L;
/**
* 系统配置表Id。
*/
private String id;
/**
* 波形文件存储根路径,默认 D:/。
*/
@TableField("wave_storage_path")
private String waveStoragePath;
/**
* 状态0-删除 1-正常。
*/
private Integer state;
}

View File

@@ -0,0 +1,33 @@
package com.njcn.gather.system.cfg.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.njcn.gather.system.cfg.pojo.param.SysConfigParam;
import com.njcn.gather.system.cfg.pojo.po.SysConfig;
/**
* 系统配置服务。
*/
public interface ISysConfigService extends IService<SysConfig> {
/**
* 更新系统配置。
*
* @param param 系统配置
* @return 是否更新成功
*/
boolean updateConfig(SysConfigParam.UpdateParam param);
/**
* 获取系统配置。
*
* @return 系统配置
*/
SysConfig getOneConfig();
/**
* 获取波形文件存储根路径。
*
* @return 波形文件存储根路径,未配置时返回默认 D:/
*/
String getWaveStoragePath();
}

View File

@@ -0,0 +1,59 @@
package com.njcn.gather.system.cfg.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.njcn.common.pojo.enums.common.DataStateEnum;
import com.njcn.gather.system.cfg.mapper.SysConfigMapper;
import com.njcn.gather.system.cfg.pojo.param.SysConfigParam;
import com.njcn.gather.system.cfg.pojo.po.SysConfig;
import com.njcn.gather.system.cfg.service.ISysConfigService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
/**
* 系统配置服务实现。
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class SysConfigServiceImpl extends ServiceImpl<SysConfigMapper, SysConfig> implements ISysConfigService {
private static final String DEFAULT_CONFIG_ID = "00000000000000000000000000000001";
private static final String DEFAULT_WAVE_STORAGE_PATH = "D:/";
@Override
@Transactional
public boolean updateConfig(SysConfigParam.UpdateParam param) {
SysConfig config = getOneConfig();
if (config == null) {
config = new SysConfig();
config.setId(param != null && StringUtils.isNotBlank(param.getId()) ? param.getId().trim() : DEFAULT_CONFIG_ID);
config.setState(DataStateEnum.ENABLE.getCode());
config.setWaveStoragePath(DEFAULT_WAVE_STORAGE_PATH);
}
if (param != null && StringUtils.isNotBlank(param.getWaveStoragePath())) {
config.setWaveStoragePath(param.getWaveStoragePath().trim());
}
return this.saveOrUpdate(config);
}
@Override
public SysConfig getOneConfig() {
QueryWrapper<SysConfig> queryWrapper = new QueryWrapper<SysConfig>();
queryWrapper.eq("state", DataStateEnum.ENABLE.getCode());
queryWrapper.last("LIMIT 1");
return this.getOne(queryWrapper);
}
@Override
public String getWaveStoragePath() {
SysConfig config = getOneConfig();
if (config == null || StringUtils.isBlank(config.getWaveStoragePath())) {
return DEFAULT_WAVE_STORAGE_PATH;
}
return config.getWaveStoragePath().trim();
}
}

View File

@@ -19,6 +19,7 @@ import com.njcn.gather.system.dictionary.pojo.po.DictType;
import com.njcn.gather.system.dictionary.pojo.vo.DictDataExcel;
import com.njcn.gather.system.dictionary.service.IDictDataService;
import com.njcn.gather.system.pojo.enums.SystemResponseEnum;
import com.njcn.gather.system.util.ExportFileNameUtil;
import com.njcn.web.factory.PageFactory;
import com.njcn.web.pojo.dto.SimpleDTO;
import com.njcn.web.pojo.dto.SimpleTreeDTO;
@@ -179,7 +180,7 @@ public class DictDataServiceImpl extends ServiceImpl<DictDataMapper, DictData> i
.eq("sys_dict_data.type_id", queryParam.getTypeId());
List<DictData> dictDatas = this.list(queryWrapper);
List<DictDataExcel> dictDataExcels = BeanUtil.copyToList(dictDatas, DictDataExcel.class);
ExcelUtil.exportExcel("字典数据导出数据.xlsx", "字典数据", DictDataExcel.class, dictDataExcels);
ExcelUtil.exportExcel(ExportFileNameUtil.appendToday("字典数据导出数据.xlsx"), "字典数据", DictDataExcel.class, dictDataExcels);
}
@Override

View File

@@ -19,6 +19,7 @@ import com.njcn.gather.system.dictionary.pojo.vo.DictTypeExcel;
import com.njcn.gather.system.dictionary.service.IDictDataService;
import com.njcn.gather.system.dictionary.service.IDictTypeService;
import com.njcn.gather.system.pojo.enums.SystemResponseEnum;
import com.njcn.gather.system.util.ExportFileNameUtil;
import com.njcn.web.factory.PageFactory;
import com.njcn.web.utils.ExcelUtil;
import lombok.RequiredArgsConstructor;
@@ -115,7 +116,7 @@ public class DictTypeServiceImpl extends ServiceImpl<DictTypeMapper, DictType> i
dictTypeVOS.add(dictTypeExcel);
});
ExcelUtil.exportExcel("字典类型导出数据.xlsx", "字典类型", DictTypeExcel.class, dictTypeVOS);
ExcelUtil.exportExcel(ExportFileNameUtil.appendToday("字典类型导出数据.xlsx"), "字典类型", DictTypeExcel.class, dictTypeVOS);
}
@Override

View File

@@ -17,6 +17,7 @@ import com.njcn.gather.system.log.pojo.param.SysLogParam;
import com.njcn.gather.system.log.pojo.po.SysLogAudit;
import com.njcn.gather.system.log.service.ISysLogAuditService;
import com.njcn.gather.system.log.util.CSVUtil;
import com.njcn.gather.system.util.ExportFileNameUtil;
import com.njcn.gather.user.user.pojo.po.SysUser;
import com.njcn.gather.user.user.service.ISysUserService;
import com.njcn.web.factory.PageFactory;
@@ -149,7 +150,7 @@ public class SysLogAuditServiceImpl extends ServiceImpl<SysLogAuditMapper, SysLo
XSSFWorkbook wb = new XSSFWorkbook();
try (ServletOutputStream outputStream = response.getOutputStream()) {
fileName = URLEncoder.encode(fileName, CharsetUtil.UTF_8);
fileName = URLEncoder.encode(ExportFileNameUtil.appendToday(fileName), CharsetUtil.UTF_8);
response.reset();
response.setHeader("Content-Disposition", "attachment; filename=\"" + fileName + "\"");
response.setContentType("application/octet-stream;charset=UTF-8");

View File

@@ -5,6 +5,7 @@ import cn.afterturn.easypoi.csv.CsvExportUtil;
import cn.afterturn.easypoi.csv.entity.CsvExportParams;
import cn.hutool.core.util.ObjectUtil;
import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
import com.njcn.gather.system.util.ExportFileNameUtil;
import com.njcn.web.utils.HttpServletUtil;
import lombok.extern.slf4j.Slf4j;
@@ -73,7 +74,7 @@ public class CSVUtil {
Throwable var1 = null;
try {
fileName = URLEncoder.encode(fileName, "UTF-8");
fileName = URLEncoder.encode(ExportFileNameUtil.appendToday(fileName), "UTF-8");
response.reset();
response.setHeader("Content-Disposition", "attachment; filename=\"" + fileName + "\"");
response.setContentType("application/octet-stream;charset=UTF-8");
@@ -108,7 +109,7 @@ public class CSVUtil {
ServletOutputStream os = null;
try {
os = response.getOutputStream();
fileName = URLEncoder.encode(fileName, "UTF-8");
fileName = URLEncoder.encode(ExportFileNameUtil.appendToday(fileName), "UTF-8");
response.reset();
response.setHeader("Content-Disposition", "attachment; filename=\"" + fileName + "\"");
response.setContentType("application/octet-stream;charset=UTF-8");

View File

@@ -38,4 +38,9 @@ public interface DictConst {
* 注册资源字典数据 ID比对式
*/
String REG_RES_CONTRAST_ID = "7cd65363a6bf675ae408f28a281b77d4";
/**
* 事件类型字典类型。
*/
String EVENT_TYPE_DICT = "事件类型";
}

View File

@@ -0,0 +1,32 @@
package com.njcn.gather.system.util;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
/**
* 导出文件名处理工具。
*/
public final class ExportFileNameUtil {
private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMdd");
private ExportFileNameUtil() {
}
public static String appendToday(String fileName) {
return appendDate(fileName, LocalDate.now());
}
public static String appendDate(String fileName, LocalDate date) {
if (fileName == null || date == null) {
return fileName;
}
String dateText = DATE_FORMATTER.format(date);
int separatorIndex = Math.max(fileName.lastIndexOf('/'), fileName.lastIndexOf('\\'));
int dotIndex = fileName.lastIndexOf('.');
if (dotIndex > separatorIndex) {
return fileName.substring(0, dotIndex) + "_" + dateText + fileName.substring(dotIndex);
}
return fileName + "_" + dateText;
}
}

View File

@@ -0,0 +1,34 @@
-- 系统配置表。
-- wave_storage_path 为波形文件存储根路径,默认值为 D:/。
CREATE TABLE IF NOT EXISTS `sys_config` (
`id` VARCHAR(64) NOT NULL COMMENT '系统配置表Id',
`wave_storage_path` VARCHAR(255) NULL DEFAULT 'D:/' COMMENT '波形文件存储根路径',
`state` TINYINT NULL DEFAULT 1 COMMENT '状态0-删除1-正常',
`create_by` VARCHAR(64) NULL COMMENT '创建人',
`create_time` DATETIME NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_by` VARCHAR(64) NULL COMMENT '更新人',
`update_time` DATETIME NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
KEY `idx_sys_config_state` (`state`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='系统配置表';
INSERT INTO `sys_config` (
`id`,
`wave_storage_path`,
`state`,
`create_time`,
`update_time`
)
SELECT
'00000000000000000000000000000001',
'D:/',
1,
NOW(),
NOW()
WHERE NOT EXISTS (
SELECT 1
FROM `sys_config`
WHERE `state` = 1
LIMIT 1
);

View File

@@ -11,6 +11,7 @@ import com.njcn.gather.systemmonitor.disk.pojo.po.DiskMonitorJob;
import com.njcn.gather.systemmonitor.disk.pojo.po.DiskMonitorNotifyLog;
import com.njcn.gather.systemmonitor.disk.pojo.po.DiskMonitorResult;
import com.njcn.gather.systemmonitor.disk.pojo.po.DiskMonitorTarget;
import com.njcn.gather.systemmonitor.disk.util.GeneratedFileNameUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
@@ -102,7 +103,7 @@ public class DiskMonitorNotificationComponent {
String fileName = String.format("disk-monitor-%s-%s-%s.json",
job.getJobNo(), target.getDriveLetter().replace(":", ""),
LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss")));
Path filePath = directoryPath.resolve(fileName);
Path filePath = directoryPath.resolve(GeneratedFileNameUtil.appendToday(fileName));
Map<String, Object> payload = buildNotifyPayload(job, target, usedPercent, currentStatus, notifyReason, notifyLevel, scanTime, message);
Files.write(filePath, objectMapper.writerWithDefaultPrettyPrinter().writeValueAsBytes(payload));
notifyLog.setSendStatus(DiskMonitorConstant.SEND_STATUS_SUCCESS);

View File

@@ -0,0 +1,32 @@
package com.njcn.gather.systemmonitor.disk.util;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
/**
* 生成文件名处理工具。
*/
public final class GeneratedFileNameUtil {
private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMdd");
private GeneratedFileNameUtil() {
}
public static String appendToday(String fileName) {
return appendDate(fileName, LocalDate.now());
}
public static String appendDate(String fileName, LocalDate date) {
if (fileName == null || date == null) {
return fileName;
}
String dateText = DATE_FORMATTER.format(date);
int separatorIndex = Math.max(fileName.lastIndexOf('/'), fileName.lastIndexOf('\\'));
int dotIndex = fileName.lastIndexOf('.');
if (dotIndex > separatorIndex) {
return fileName.substring(0, dotIndex) + "_" + dateText + fileName.substring(dotIndex);
}
return fileName + "_" + dateText;
}
}

View File

@@ -1,28 +0,0 @@
package com.njcn.gather.tool.adddata.component;
import com.njcn.gather.tool.adddata.pojo.bo.AddDataTableDefinition;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import java.util.List;
/**
* 表定义注册测试。
*/
class AddDataTableRegistryTest {
@Test
void shouldLoadAllThirteenTablesFromSchema() throws Exception {
AddDataTableRegistry registry = new AddDataTableRegistry();
registry.afterPropertiesSet();
List<AddDataTableDefinition> definitions = registry.getTableDefinitions();
Assertions.assertEquals(13, definitions.size());
Assertions.assertEquals("data_flicker", definitions.get(0).getTableName());
Assertions.assertEquals("data_v", definitions.get(definitions.size() - 1).getTableName());
Assertions.assertTrue(registry.getDefinition("data_v").getColumns().contains("V_THD"));
Assertions.assertEquals(4, registry.getDefinition("data_v").getPhaseCodes().size());
Assertions.assertTrue(registry.getDefinition("data_v").getPhaseCodes().contains("T"));
}
}

View File

@@ -1,33 +0,0 @@
package com.njcn.gather.tool.adddata.component;
import com.njcn.gather.tool.adddata.pojo.bo.AddDataTaskCommand;
import com.njcn.gather.tool.adddata.pojo.vo.AddDataTaskStatusVO;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import java.time.LocalDateTime;
import java.util.Arrays;
/**
* 补数任务状态持有器测试。
*/
class AddDataTaskStatusHolderTest {
private final AddDataTaskStatusHolder holder = new AddDataTaskStatusHolder(new AddDataTimeSlotCalculator());
@Test
void shouldReturnHourlyTimeResultsWhenCreateTask() {
AddDataTaskCommand command = new AddDataTaskCommand(
Arrays.asList("1"),
LocalDateTime.of(2026, 4, 28, 10, 7, 0),
LocalDateTime.of(2026, 4, 28, 13, 0, 0),
5);
AddDataTaskStatusVO status = holder.createWaitingTask(command);
Assertions.assertEquals(Arrays.asList(
"2026-04-28 11:00:00",
"2026-04-28 12:00:00",
"2026-04-28 13:00:00"), status.getHourlyTimeResults());
}
}

View File

@@ -1,33 +0,0 @@
package com.njcn.gather.tool.adddata.component;
import com.njcn.gather.tool.adddata.pojo.vo.AddDataTemplateVO;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* 模板注册测试。
*/
class AddDataTemplateRegistryTest {
@Test
void shouldOnlyExposeABCTPhaseCodes() {
AddDataTemplateRegistry registry = new AddDataTemplateRegistry();
List<AddDataTemplateVO> templates = registry.getTemplates();
Set<String> allowedPhaseCodes = new HashSet<String>();
allowedPhaseCodes.add("A");
allowedPhaseCodes.add("B");
allowedPhaseCodes.add("C");
allowedPhaseCodes.add("T");
for (AddDataTemplateVO template : templates) {
for (String phaseCode : template.getPhaseCodes()) {
Assertions.assertTrue(allowedPhaseCodes.contains(phaseCode));
}
}
}
}

View File

@@ -1,61 +0,0 @@
package com.njcn.gather.tool.adddata.component;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import java.time.LocalDateTime;
import java.util.List;
/**
* 时间槽计算测试。
*/
class AddDataTimeSlotCalculatorTest {
private final AddDataTimeSlotCalculator calculator = new AddDataTimeSlotCalculator();
@Test
void shouldAlignToNextNaturalSlot() {
LocalDateTime start = LocalDateTime.of(2026, 4, 28, 10, 7, 12);
LocalDateTime end = LocalDateTime.of(2026, 4, 28, 10, 22, 0);
List<LocalDateTime> slots = calculator.buildTimeSlots(start, end, 5);
Assertions.assertEquals(3, slots.size());
Assertions.assertEquals(LocalDateTime.of(2026, 4, 28, 10, 10, 0), slots.get(0));
Assertions.assertEquals(LocalDateTime.of(2026, 4, 28, 10, 20, 0), slots.get(2));
}
@Test
void shouldReturnEmptyWhenRangeDoesNotContainAnySlot() {
LocalDateTime start = LocalDateTime.of(2026, 4, 28, 10, 7, 0);
LocalDateTime end = LocalDateTime.of(2026, 4, 28, 10, 9, 59);
List<LocalDateTime> slots = calculator.buildTimeSlots(start, end, 10);
Assertions.assertTrue(slots.isEmpty());
}
@Test
void shouldBuildHourlySlotsFromNextNaturalHour() {
LocalDateTime start = LocalDateTime.of(2026, 4, 28, 10, 7, 0);
LocalDateTime end = LocalDateTime.of(2026, 4, 28, 13, 0, 0);
List<LocalDateTime> slots = calculator.buildHourlyTimeSlots(start, end);
Assertions.assertEquals(3, slots.size());
Assertions.assertEquals(LocalDateTime.of(2026, 4, 28, 11, 0, 0), slots.get(0));
Assertions.assertEquals(LocalDateTime.of(2026, 4, 28, 13, 0, 0), slots.get(2));
}
@Test
void shouldIncludeStartWhenAlreadyAtNaturalHour() {
LocalDateTime start = LocalDateTime.of(2026, 4, 28, 10, 0, 0);
LocalDateTime end = LocalDateTime.of(2026, 4, 28, 12, 30, 0);
List<LocalDateTime> slots = calculator.buildHourlyTimeSlots(start, end);
Assertions.assertEquals(3, slots.size());
Assertions.assertEquals(LocalDateTime.of(2026, 4, 28, 10, 0, 0), slots.get(0));
Assertions.assertEquals(LocalDateTime.of(2026, 4, 28, 12, 0, 0), slots.get(2));
}
}

View File

@@ -1,32 +0,0 @@
package com.njcn.gather.tool.adddata.component;
import com.njcn.gather.tool.adddata.pojo.bo.AddDataTableDefinition;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import java.time.LocalDateTime;
import java.util.List;
/**
* 数值生成器测试。
*/
class AddDataValueGeneratorTest {
@Test
void shouldGenerateStableDataVRowWithExpectedColumnCount() throws Exception {
AddDataTableRegistry registry = new AddDataTableRegistry();
registry.afterPropertiesSet();
AddDataTableDefinition definition = registry.getDefinition("data_v");
AddDataValueGenerator generator = new AddDataValueGenerator();
List<Object> row = generator.generateRow(definition, "f04a9d62e3d24e6580e4f32b40967505", LocalDateTime.of(2026, 4, 28, 10, 10, 0), "A");
Assertions.assertEquals(definition.getColumns().size(), row.size());
Assertions.assertEquals("A", row.get(definition.getColumns().indexOf("PHASIC_TYPE")));
Double rms = (Double) row.get(definition.getColumns().indexOf("RMS"));
Double rmsMax = (Double) row.get(definition.getColumns().indexOf("RMS_MAX"));
Double rmsMin = (Double) row.get(definition.getColumns().indexOf("RMS_MIN"));
Assertions.assertTrue(rmsMax >= rms);
Assertions.assertTrue(rmsMin <= rms);
}
}

View File

@@ -32,6 +32,7 @@
project.name AS projectName,
equipment.id AS equipmentId,
equipment.name AS equipmentName,
equipment.mac AS equipmentMac,
line.line_id AS lineId,
line.name AS lineName
FROM cs_line line
@@ -55,6 +56,7 @@
project.name AS projectName,
equipment.id AS equipmentId,
equipment.name AS equipmentName,
equipment.mac AS equipmentMac,
line.line_id AS lineId,
line.name AS lineName
FROM cs_line line

View File

@@ -24,6 +24,8 @@ public class AddLedgerLinePathVO implements Serializable {
private String equipmentName;
private String equipmentMac;
private String lineId;
private String lineName;

View File

@@ -1,224 +0,0 @@
/*
Navicat/MySQL 初始化脚本
用途:
1. 初始化 add-ledger 台账设备相关字典。
2. 脚本可重复执行,按字典编码避免重复插入。
说明:
- dev_type 对应 cs_equipment_delivery.dev_type保存 sys_dict_data.id。
- dev_model 对应 cs_equipment_delivery.dev_model保存 sys_dict_data.id。
*/
SET NAMES utf8mb4;
-- ----------------------------
-- 字典类型:装置类型
-- ----------------------------
SET @ledger_device_type_type_id := (
SELECT `id`
FROM `sys_dict_type`
WHERE `code` = 'ledger_device_type'
AND `state` = 1
LIMIT 1
);
INSERT INTO `sys_dict_type` (
`id`, `name`, `code`, `sort`, `open_level`, `open_describe`, `remark`, `state`,
`create_by`, `create_time`, `update_by`, `update_time`
)
SELECT
'7f91c2a1e9f44b47a6e7c8b227d00101',
'装置类型',
'ledger_device_type',
1,
0,
0,
'数据台账设备装置类型',
1,
'system',
NOW(),
'system',
NOW()
WHERE @ledger_device_type_type_id IS NULL;
SET @ledger_device_type_type_id := (
SELECT `id`
FROM `sys_dict_type`
WHERE `code` = 'ledger_device_type'
AND `state` = 1
LIMIT 1
);
INSERT INTO `sys_dict_data` (
`id`, `type_id`, `name`, `code`, `sort`, `level`, `algo_describe`, `value`, `open_value`, `state`,
`create_by`, `create_time`, `update_by`, `update_time`
)
SELECT
'7f91c2a1e9f44b47a6e7c8b227d10101',
@ledger_device_type_type_id,
'直连设备',
'direct_device',
1,
0,
NULL,
NULL,
0,
1,
'system',
NOW(),
'system',
NOW()
WHERE NOT EXISTS (
SELECT 1
FROM `sys_dict_data`
WHERE `type_id` = @ledger_device_type_type_id
AND `code` = 'direct_device'
AND `state` = 1
);
INSERT INTO `sys_dict_data` (
`id`, `type_id`, `name`, `code`, `sort`, `level`, `algo_describe`, `value`, `open_value`, `state`,
`create_by`, `create_time`, `update_by`, `update_time`
)
SELECT
'7f91c2a1e9f44b47a6e7c8b227d10102',
@ledger_device_type_type_id,
'网关',
'gateway',
2,
0,
NULL,
NULL,
0,
1,
'system',
NOW(),
'system',
NOW()
WHERE NOT EXISTS (
SELECT 1
FROM `sys_dict_data`
WHERE `type_id` = @ledger_device_type_type_id
AND `code` = 'gateway'
AND `state` = 1
);
INSERT INTO `sys_dict_data` (
`id`, `type_id`, `name`, `code`, `sort`, `level`, `algo_describe`, `value`, `open_value`, `state`,
`create_by`, `create_time`, `update_by`, `update_time`
)
SELECT
'7f91c2a1e9f44b47a6e7c8b227d10103',
@ledger_device_type_type_id,
'装置',
'device',
3,
0,
NULL,
NULL,
0,
1,
'system',
NOW(),
'system',
NOW()
WHERE NOT EXISTS (
SELECT 1
FROM `sys_dict_data`
WHERE `type_id` = @ledger_device_type_type_id
AND `code` = 'device'
AND `state` = 1
);
-- ----------------------------
-- 字典类型:装置型号
-- ----------------------------
SET @ledger_device_model_type_id := (
SELECT `id`
FROM `sys_dict_type`
WHERE `code` = 'ledger_device_model'
AND `state` = 1
LIMIT 1
);
INSERT INTO `sys_dict_type` (
`id`, `name`, `code`, `sort`, `open_level`, `open_describe`, `remark`, `state`,
`create_by`, `create_time`, `update_by`, `update_time`
)
SELECT
'7f91c2a1e9f44b47a6e7c8b227d00201',
'装置型号',
'ledger_device_model',
2,
0,
0,
'数据台账设备装置型号',
1,
'system',
NOW(),
'system',
NOW()
WHERE @ledger_device_model_type_id IS NULL;
SET @ledger_device_model_type_id := (
SELECT `id`
FROM `sys_dict_type`
WHERE `code` = 'ledger_device_model'
AND `state` = 1
LIMIT 1
);
INSERT INTO `sys_dict_data` (
`id`, `type_id`, `name`, `code`, `sort`, `level`, `algo_describe`, `value`, `open_value`, `state`,
`create_by`, `create_time`, `update_by`, `update_time`
)
SELECT
'7f91c2a1e9f44b47a6e7c8b227d20101',
@ledger_device_model_type_id,
'PQS588',
'pqs588',
1,
0,
NULL,
NULL,
0,
1,
'system',
NOW(),
'system',
NOW()
WHERE NOT EXISTS (
SELECT 1
FROM `sys_dict_data`
WHERE `type_id` = @ledger_device_model_type_id
AND `code` = 'pqs588'
AND `state` = 1
);
INSERT INTO `sys_dict_data` (
`id`, `type_id`, `name`, `code`, `sort`, `level`, `algo_describe`, `value`, `open_value`, `state`,
`create_by`, `create_time`, `update_by`, `update_time`
)
SELECT
'7f91c2a1e9f44b47a6e7c8b227d20102',
@ledger_device_model_type_id,
'PQS680',
'pqs680',
2,
0,
NULL,
NULL,
0,
1,
'system',
NOW(),
'system',
NOW()
WHERE NOT EXISTS (
SELECT 1
FROM `sys_dict_data`
WHERE `type_id` = @ledger_device_model_type_id
AND `code` = 'pqs680'
AND `state` = 1
);

View File

@@ -1,43 +0,0 @@
package com.njcn.gather.tool.addledger.component;
import com.njcn.gather.tool.addledger.pojo.po.AddLedgerLedgerPO;
import com.njcn.gather.tool.addledger.pojo.vo.AddLedgerTreeNodeVO;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import java.util.Arrays;
import java.util.List;
/**
* 台账树组装测试。
*/
class AddLedgerTreeBuilderTest {
private final AddLedgerTreeBuilder treeBuilder = new AddLedgerTreeBuilder();
@Test
void shouldBuildFourLevelTreeFromFlatLedgerNodes() {
AddLedgerLedgerPO engineering = buildLedger("engineering-1", "0", 0, "工程");
AddLedgerLedgerPO project = buildLedger("project-1", "engineering-1", 1, "项目");
AddLedgerLedgerPO equipment = buildLedger("equipment-1", "project-1", 2, "设备");
AddLedgerLedgerPO line = buildLedger("line-1", "equipment-1", 3, "测点");
List<AddLedgerTreeNodeVO> result = treeBuilder.buildTree(Arrays.asList(line, equipment, project, engineering));
Assertions.assertEquals(1, result.size());
Assertions.assertEquals("engineering-1", result.get(0).getId());
Assertions.assertEquals("project-1", result.get(0).getChildren().get(0).getId());
Assertions.assertEquals("equipment-1", result.get(0).getChildren().get(0).getChildren().get(0).getId());
Assertions.assertEquals("line-1", result.get(0).getChildren().get(0).getChildren().get(0).getChildren().get(0).getId());
}
private AddLedgerLedgerPO buildLedger(String id, String parentId, Integer level, String name) {
AddLedgerLedgerPO ledger = new AddLedgerLedgerPO();
ledger.setId(id);
ledger.setPid(parentId);
ledger.setLevel(level);
ledger.setName(name);
ledger.setSort(0);
return ledger;
}
}

View File

@@ -1,33 +0,0 @@
package com.njcn.gather.tool.addledger.util;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import java.util.Arrays;
import java.util.List;
/**
* 台账测点线路号工具测试。
*/
class AddLedgerLineNoUtilTest {
@Test
void shouldReturnOnlyUnusedLineNosWhenCreateLine() {
List<Integer> result = AddLedgerLineNoUtil.resolveAvailableLineNos(Arrays.asList(1, 2, 20), null);
Assertions.assertEquals(17, result.size());
Assertions.assertFalse(result.contains(1));
Assertions.assertFalse(result.contains(2));
Assertions.assertFalse(result.contains(20));
Assertions.assertTrue(result.contains(3));
}
@Test
void shouldKeepCurrentLineNoAvailableWhenEditLine() {
List<Integer> result = AddLedgerLineNoUtil.resolveAvailableLineNos(Arrays.asList(1, 2, 20), 2);
Assertions.assertTrue(result.contains(2));
Assertions.assertFalse(result.contains(1));
Assertions.assertFalse(result.contains(20));
}
}

View File

@@ -196,7 +196,7 @@ curl.exe -X POST "http://localhost:8080/api/mms-mapping/get-icd-mms-json" `
说明:
- `mappingJson` 是字符串字段,字段值本身也是一段 JSON 文本。
-`saveToDisk=true` 时,响应中还会返回 `savedPath`
-`saveToDisk=true` 时,响应中还会返回 `savedPath`,落盘文件名按统一规则追加 `_yyyyMMdd`
### 5.3 FAILED

View File

@@ -1,5 +1,6 @@
package com.njcn.gather.icd.mapping.component;
import com.njcn.gather.icd.mapping.utils.GeneratedFileNameUtil;
import org.springframework.stereotype.Component;
import java.io.File;
@@ -26,7 +27,7 @@ public class FileStorageService {
if (!dir.isDirectory()) {
throw new IllegalStateException("输出路径不是目录:" + dir.getAbsolutePath());
}
File target = new File(dir, fileName);
File target = new File(dir, GeneratedFileNameUtil.appendToday(fileName));
try (FileOutputStream fos = new FileOutputStream(target)) {
fos.write(content.getBytes(StandardCharsets.UTF_8));
}

View File

@@ -8,6 +8,7 @@ import com.njcn.gather.icd.mapping.pojo.vo.IndexCandidateReportItemResponse;
import com.njcn.gather.icd.mapping.pojo.vo.IndexCandidateResponse;
import com.njcn.gather.icd.mapping.pojo.vo.MappingDocumentResponse;
import com.njcn.gather.icd.mapping.pojo.vo.XmlFileResponse;
import com.njcn.gather.icd.mapping.utils.GeneratedFileNameUtil;
import org.springframework.stereotype.Component;
@Component
@@ -90,13 +91,13 @@ public class IcdToXmlResponseConverter {
private String resolveXmlFileName(IcdToXmlGenerateResult result) {
String iedName = result.getIedName();
if (iedName == null || iedName.trim().isEmpty()) {
return DEFAULT_XML_FILE_NAME;
return GeneratedFileNameUtil.appendToday(DEFAULT_XML_FILE_NAME);
}
String safeName = iedName.replaceAll("[\\\\/:*?\"<>|]+", "_").trim();
if (safeName.isEmpty()) {
return DEFAULT_XML_FILE_NAME;
return GeneratedFileNameUtil.appendToday(DEFAULT_XML_FILE_NAME);
}
return safeName + ".xml";
return GeneratedFileNameUtil.appendToday(safeName + ".xml");
}
}

View File

@@ -9,6 +9,7 @@ import com.njcn.gather.icd.mapping.pojo.param.*;
import com.njcn.gather.icd.mapping.pojo.vo.*;
import com.njcn.gather.icd.mapping.pojo.enums.GenerateStatus;
import com.njcn.gather.icd.mapping.pojo.bo.mapping.MappingDocument;
import com.njcn.gather.icd.mapping.utils.GeneratedFileNameUtil;
import lombok.var;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;
@@ -17,6 +18,8 @@ import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.util.ArrayList;
import java.util.List;
@@ -57,8 +60,9 @@ public class JsonToXmlConversionService {
String xmlContent = buildXmlContentFromJson(mappingJson, templateStream, ruleStreams, indexMapping);
// 3. 保存为临时文件
Path tempPath = Files.createTempFile("converted_", ".xml");
Files.write(tempPath, xmlContent.getBytes(StandardCharsets.UTF_8));
Path tempPath = Paths.get(System.getProperty("java.io.tmpdir"),
GeneratedFileNameUtil.appendToday("converted_" + java.util.UUID.randomUUID().toString().replace("-", "") + ".xml"));
Files.write(tempPath, xmlContent.getBytes(StandardCharsets.UTF_8), StandardOpenOption.CREATE_NEW);
return tempPath.toString();
}

View File

@@ -12,6 +12,8 @@ import java.io.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -20,6 +22,7 @@ import com.njcn.gather.icd.mapping.pojo.bo.*;
import com.njcn.gather.icd.mapping.pojo.param.*;
import com.njcn.gather.icd.mapping.pojo.vo.*;
import com.njcn.gather.icd.mapping.pojo.enums.GenerateStatus;
import com.njcn.gather.icd.mapping.utils.GeneratedFileNameUtil;
@Component
public class RuleBasedXmlMappingService {
@@ -104,8 +107,9 @@ public class RuleBasedXmlMappingService {
xmlContent = applyRulesToXml(xmlContent, applicableRules);
Path tempPath = Files.createTempFile("rule_mapped_", ".xml");
Files.write(tempPath, xmlContent.getBytes(StandardCharsets.UTF_8));
Path tempPath = Paths.get(System.getProperty("java.io.tmpdir"),
GeneratedFileNameUtil.appendToday("rule_mapped_" + UUID.randomUUID().toString().replace("-", "") + ".xml"));
Files.write(tempPath, xmlContent.getBytes(StandardCharsets.UTF_8), StandardOpenOption.CREATE_NEW);
return tempPath.toString();
}

View File

@@ -0,0 +1,33 @@
package com.njcn.gather.icd.mapping.utils;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
/**
* 生成文件名处理工具。
*/
public final class GeneratedFileNameUtil {
private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMdd");
private GeneratedFileNameUtil() {
}
public static String appendToday(String fileName) {
return appendDate(fileName, LocalDate.now());
}
public static String appendDate(String fileName, LocalDate date) {
if (fileName == null || date == null) {
return fileName;
}
String dateText = DATE_FORMATTER.format(date);
int separatorIndex = Math.max(fileName.lastIndexOf('/'), fileName.lastIndexOf('\\'));
int dotIndex = fileName.lastIndexOf('.');
if (dotIndex > separatorIndex) {
return fileName.substring(0, dotIndex) + "_" + dateText + fileName.substring(dotIndex);
}
return fileName + "_" + dateText;
}
}

View File

@@ -1,232 +0,0 @@
package com.njcn.gather.icd.mapping.debug;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.njcn.gather.icd.mapping.component.MappingResponseConverter;
import com.njcn.gather.icd.mapping.pojo.dto.GenerateFromIcdCommand;
import com.njcn.gather.icd.mapping.pojo.dto.IndexBindingCommand;
import com.njcn.gather.icd.mapping.pojo.dto.IndexSelectionGroupCommand;
import com.njcn.gather.icd.mapping.pojo.vo.IndexCandidateReportItemResponse;
import com.njcn.gather.icd.mapping.pojo.vo.IndexCandidateResponse;
import com.njcn.gather.icd.mapping.pojo.vo.MappingTaskResponse;
import com.njcn.gather.icd.mapping.service.MappingTaskService;
import org.springframework.boot.Banner;
import org.springframework.boot.WebApplicationType;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.context.ConfigurableApplicationContext;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
/**
* `getIcdMmsJson` 本地调试入口。
*
* 使用方式:
* 1. 先修改 ICD_FILE_PATH 指向本地真实 ICD/SCD 文件。
* 2. 直接运行 main先观察第一次调试返回的 indexCandidates。
* 3. 按第一次返回结果补齐 buildSecondStepSelection() 中的绑定关系。
* 4. 把 RUN_SECOND_STEP 改成 true再次运行 main 获取最终 mappingJson。
*/
public class GetIcdMmsJsonDebugRunner {
/** 本地 ICD/SCD 文件路径,运行前请改成真实文件。 */
private static final String ICD_FILE_PATH = "D:\\Work\\工作资料\\1灿能项目资料\\01自研\\01灿能\\09灿能C端功能\\01需求文档\\灿能工具箱开发\\icd\\PQS882_VX_BJ_1(V111).icd";
/** 调试时可固定版本号,便于对比输出。 */
private static final String VERSION = "20260421";
/** 调试作者标识。 */
private static final String AUTHOR = "debug-user";
/** 是否输出格式化 JSON便于人工查看。 */
private static final boolean PRETTY_JSON = true;
/** 是否把生成结果写入磁盘。 */
private static final boolean SAVE_TO_DISK = false;
/** saveToDisk=true 时使用的输出目录。 */
private static final String OUTPUT_DIR = "D:/temp/mms-output";
/**
* 第二次正式生成开关。
* 默认先只跑第一次,确认 groupKey/reportName/dataSetName/lnInst 候选值后再打开。
*/
private static final boolean RUN_SECOND_STEP = false;
public static void main(String[] args) {
try (ConfigurableApplicationContext context = new SpringApplicationBuilder(DebugApplication.class)
.web(WebApplicationType.NONE)
.bannerMode(Banner.Mode.OFF)
.logStartupInfo(false)
.run(args)) {
MappingTaskService mappingTaskService = context.getBean(MappingTaskService.class);
MappingResponseConverter responseConverter = context.getBean(MappingResponseConverter.class);
ObjectMapper objectMapper = createObjectMapper();
MappingTaskResponse firstResponse = debugNeedIndexSelection(mappingTaskService, responseConverter);
printResponse("第一次调试:获取索引候选", firstResponse, objectMapper);
printIndexCandidatesJson(firstResponse, objectMapper);
printIndexCandidateSummary(firstResponse);
if (!RUN_SECOND_STEP) {
System.out.println("第二次调试未开启。请先根据第一次返回结果补齐 buildSecondStepSelection(),再把 RUN_SECOND_STEP 改成 true。");
return;
}
MappingTaskResponse secondResponse = debugGenerateMapping(mappingTaskService, responseConverter);
printResponse("第二次调试:生成 MMS JSON", secondResponse, objectMapper);
} catch (Exception ex) {
throw new IllegalStateException("getIcdMmsJson 调试失败:" + ex.getMessage(), ex);
}
}
/**
* 第一次调试:不传 indexSelection只获取 icdDocument 和 indexCandidates。
*/
private static MappingTaskResponse debugNeedIndexSelection(MappingTaskService mappingTaskService,
MappingResponseConverter responseConverter) {
GenerateFromIcdCommand command = buildBaseCommand();
return responseConverter.fromSubmitResult(mappingTaskService.getIcdMmsJson(command));
}
/**
* 第二次调试:补齐 indexSelection 后直接生成最终 mappingJson。
*/
private static MappingTaskResponse debugGenerateMapping(MappingTaskService mappingTaskService,
MappingResponseConverter responseConverter) {
List<IndexSelectionGroupCommand> selectionGroups = buildSecondStepSelection();
if (selectionGroups.isEmpty()) {
throw new IllegalArgumentException("第二次调试缺少 indexSelection请先补齐 buildSecondStepSelection()");
}
GenerateFromIcdCommand command = buildBaseCommand();
command.getIndexSelection().addAll(selectionGroups);
return responseConverter.fromSubmitResult(mappingTaskService.getIcdMmsJson(command));
}
/**
* 组装调试用基础命令,等价于接口中的 request 基础参数。
*/
private static GenerateFromIcdCommand buildBaseCommand() {
Path icdPath = Paths.get(ICD_FILE_PATH);
if (!Files.exists(icdPath)) {
throw new IllegalArgumentException("ICD 文件不存在:" + icdPath.toAbsolutePath());
}
try {
GenerateFromIcdCommand command = new GenerateFromIcdCommand();
command.setFileName(icdPath.getFileName().toString());
command.setFileBytes(Files.readAllBytes(icdPath));
command.setVersion(VERSION);
command.setAuthor(AUTHOR);
command.setPrettyJson(PRETTY_JSON);
command.setSaveToDisk(SAVE_TO_DISK);
command.setOutputDir(OUTPUT_DIR);
return command;
} catch (Exception ex) {
throw new IllegalArgumentException("读取 ICD 文件失败:" + ex.getMessage(), ex);
}
}
/**
* 第二次调试的索引绑定示例。
*
* 注意:
* 1. groupKey/groupDesc/reportName/dataSetName/lnInst 必须使用第一次返回的真实值。
* 2. 下面示例仅作占位,默认不参与运行。
*/
private static List<IndexSelectionGroupCommand> buildSecondStepSelection() {
List<IndexSelectionGroupCommand> groups = new ArrayList<IndexSelectionGroupCommand>();
// 示例:
// IndexSelectionGroupCommand group = createSelectionGroup("HARM__DSSTHARM", "谐波数据");
// group.getBindings().add(createBinding("brcbStHarm", "dsStHarm", "A相", "1"));
// group.getBindings().add(createBinding("brcbStHarm", "dsStHarm", "B相", "2"));
// group.getBindings().add(createBinding("brcbStHarm", "dsStHarm", "C相", "3"));
// groups.add(group);
return groups;
}
private static IndexSelectionGroupCommand createSelectionGroup(String groupKey, String groupDesc) {
IndexSelectionGroupCommand group = new IndexSelectionGroupCommand();
group.setGroupKey(groupKey);
group.setGroupDesc(groupDesc);
return group;
}
private static IndexBindingCommand createBinding(String reportName,
String dataSetName,
String label,
String lnInst) {
IndexBindingCommand binding = new IndexBindingCommand();
binding.setReportName(reportName);
binding.setDataSetName(dataSetName);
binding.setLabel(label);
binding.setLnInst(lnInst);
return binding;
}
/**
* 控制台输出候选摘要,方便把第一次返回值填回第二次调试配置。
*/
private static void printIndexCandidateSummary(MappingTaskResponse response) {
if (response == null || response.getIndexCandidates() == null || response.getIndexCandidates().isEmpty()) {
return;
}
System.out.println("===== 索引候选摘要 =====");
for (IndexCandidateResponse candidate : response.getIndexCandidates()) {
System.out.println("groupKey=" + candidate.getGroupKey() + ", groupDesc=" + candidate.getGroupDesc());
if (candidate.getReports() == null) {
continue;
}
for (IndexCandidateReportItemResponse report : candidate.getReports()) {
System.out.println(" reportName=" + report.getReportName()
+ ", dataSetName=" + report.getDataSetName()
+ ", availableLnInstValues=" + report.getAvailableLnInstValues());
}
}
}
/**
* 单独输出 indexCandidates 的 JSON便于直接复制做第二次调试绑定。
*/
private static void printIndexCandidatesJson(MappingTaskResponse response, ObjectMapper objectMapper) {
try {
System.out.println();
System.out.println("===== indexCandidates JSON =====");
if (response == null) {
System.out.println("null");
return;
}
System.out.println(objectMapper.writeValueAsString(response.getIndexCandidates()));
} catch (Exception ex) {
throw new IllegalArgumentException("print indexCandidates JSON failed: " + ex.getMessage(), ex);
}
}
private static void printResponse(String title, MappingTaskResponse response, ObjectMapper objectMapper) {
try {
System.out.println();
System.out.println("===== " + title + " =====");
System.out.println(objectMapper.writeValueAsString(response));
} catch (Exception ex) {
throw new IllegalArgumentException("打印调试响应失败:" + ex.getMessage(), ex);
}
}
private static ObjectMapper createObjectMapper() {
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.enable(SerializationFeature.INDENT_OUTPUT);
return objectMapper;
}
@SpringBootApplication(scanBasePackages = "com.njcn.gather.icd.mapping")
public static class DebugApplication {
}
}

View File

@@ -1,265 +0,0 @@
package com.njcn.gather.tool.wave.component;
import com.njcn.gather.tool.wave.pojo.dto.EigenvalueDTO;
import com.njcn.gather.tool.wave.pojo.dto.WaveCycleVectorDTO;
import com.njcn.gather.tool.wave.pojo.dto.WaveHarmonicDTO;
import com.njcn.gather.tool.wave.pojo.dto.WavePhaseVectorDTO;
import com.njcn.gather.tool.wave.pojo.dto.WaveVectorGroupDTO;
import com.njcn.gather.tool.wave.pojo.dto.WaveDataDTO;
import com.njcn.gather.tool.wave.pojo.param.WaveComtradeParseParam;
import com.njcn.gather.tool.wave.pojo.vo.WaveComtradeVectorResultVO;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
/**
* 本地调试 getComtrade 的命令行入口。
*/
public final class WaveFileComponentTestMain {
/** 默认测试 CFG 文件路径。 */
private static final String DEFAULT_CFG_PATH = "D:\\00-B7-8D-00-E4-09\\PQMonitor_PQM2_006970_20260320_175033_734.CFG";
/** 默认测试 DAT 文件路径。 */
private static final String DEFAULT_DAT_PATH = "D:\\00-B7-8D-00-E4-09\\PQMonitor_PQM2_006970_20260320_175033_734.DAT";
/** 默认解析类型1 表示普通展示。 */
private static final int DEFAULT_PARSE_TYPE = 1;
/** 向量调试固定使用原始波形。 */
private static final int VECTOR_PARSE_TYPE = 3;
/** 默认使用浮动门槛计算特征值。 */
private static final boolean DEFAULT_DYNAMIC_THRESHOLD = true;
private WaveFileComponentTestMain() {
}
/**
* 用法:
* java com.njcn.gather.tool.wave.component.WaveFileComponentTestMain <cfgPath> <datPath> [parseType]
*/
public static void main(String[] args) throws Exception {
if (args.length < 2) {
System.out.println("未传入参数,使用默认测试文件路径");
System.out.println("cfgPath: " + DEFAULT_CFG_PATH);
System.out.println("datPath: " + DEFAULT_DAT_PATH);
}
Path cfgPath = Paths.get(args.length > 0 ? args[0] : DEFAULT_CFG_PATH);
Path datPath = Paths.get(args.length > 1 ? args[1] : DEFAULT_DAT_PATH);
int parseType = args.length > 2 ? Integer.parseInt(args[2]) : DEFAULT_PARSE_TYPE;
WaveFileComponent waveFileComponent = new WaveFileComponent();
try (InputStream cfgStream = Files.newInputStream(cfgPath);
InputStream datStream = Files.newInputStream(datPath)) {
WaveDataDTO waveDataDTO = waveFileComponent.getComtrade(cfgStream, datStream, parseType);
printComtradeResult(waveDataDTO);
WaveDataDTO validWaveDataDTO = waveFileComponent.getValidData(waveDataDTO);
printValidDataResult(validWaveDataDTO);
List<EigenvalueDTO> eigenvalues = waveFileComponent.getEigenvalue(validWaveDataDTO, DEFAULT_DYNAMIC_THRESHOLD);
printEigenvalueResult(eigenvalues);
}
runParseComtradeVectorTest(cfgPath, datPath);
}
/**
* 调试 parseComtradeVector 逻辑。
*/
private static void runParseComtradeVectorTest(Path cfgPath, Path datPath) throws Exception {
System.out.println("=== parseComtradeVector 结果 ===");
WaveFileComponent waveFileComponent = new WaveFileComponent();
WaveVectorComponent waveVectorComponent = new WaveVectorComponent();
WaveComtradeParseParam param = new WaveComtradeParseParam();
param.setParseType(VECTOR_PARSE_TYPE);
param.setPtType(0);
param.setPt(1D);
param.setCt(1D);
param.setMonitorName("main方法调试测点");
try (InputStream cfgStream = Files.newInputStream(cfgPath);
InputStream datStream = Files.newInputStream(datPath)) {
WaveDataDTO waveDataDTO = waveFileComponent.getComtrade(cfgStream, datStream, VECTOR_PARSE_TYPE);
applyWaveMetadata(waveDataDTO, param);
List<WaveVectorGroupDTO> vectorGroups = waveVectorComponent.calculateVectors(waveDataDTO);
WaveComtradeVectorResultVO result = new WaveComtradeVectorResultVO();
result.setMonitorName(waveDataDTO.getMonitorName());
result.setTime(waveDataDTO.getTime());
result.setSamplePerCycle(waveDataDTO.getComtradeCfgDTO() == null ? null : waveDataDTO.getComtradeCfgDTO().getFinalSampleRate());
result.setCycleCount(resolveCycleCount(vectorGroups));
result.setVectorGroups(vectorGroups);
printParseComtradeVectorResult(result);
}
}
/**
* 给向量调试结果补齐 PT/CT/测点信息。
*/
private static void applyWaveMetadata(WaveDataDTO waveDataDTO, WaveComtradeParseParam param) {
waveDataDTO.setPt(param.getPt() == null || param.getPt() <= 0 ? 1D : param.getPt());
waveDataDTO.setCt(param.getCt() == null || param.getCt() <= 0 ? 1D : param.getCt());
waveDataDTO.setPtType(param.getPtType() == null ? 0 : param.getPtType());
waveDataDTO.setMonitorName(param.getMonitorName() == null || param.getMonitorName().trim().isEmpty()
? "未命名测点"
: param.getMonitorName().trim());
}
/**
* 打印 parseComtradeVector 的关键摘要。
*/
private static void printParseComtradeVectorResult(WaveComtradeVectorResultVO result) {
System.out.println("测点名称: " + result.getMonitorName());
System.out.println("事件时间: " + result.getTime());
System.out.println("每周波采样点数: " + result.getSamplePerCycle());
System.out.println("可计算周波数: " + result.getCycleCount());
System.out.println("分组数量: " + sizeOf(result.getVectorGroups()));
if (result.getVectorGroups() == null || result.getVectorGroups().isEmpty()) {
return;
}
for (WaveVectorGroupDTO group : result.getVectorGroups()) {
System.out.println("-- 分组: channelName=" + group.getChannelName()
+ ", unit=" + group.getUnit()
+ ", phaseCount=" + group.getPhaseCount()
+ ", phaseNames=" + group.getPhaseNames());
if (group.getVectorSeries() == null || group.getVectorSeries().isEmpty()) {
continue;
}
WaveCycleVectorDTO firstCycle = group.getVectorSeries().get(0);
System.out.println(" 首周波: cycleIndex=" + firstCycle.getCycleIndex() + ", time=" + firstCycle.getTime());
printPhaseVectorSummary(firstCycle.getPhaseVectors());
if (firstCycle.getPositiveSequence() != null) {
System.out.println(" 正序: amplitude=" + firstCycle.getPositiveSequence().getAmplitude()
+ ", rms=" + firstCycle.getPositiveSequence().getRms()
+ ", phaseAngle=" + firstCycle.getPositiveSequence().getPhaseAngle());
}
if (firstCycle.getNegativeSequence() != null) {
System.out.println(" 负序: amplitude=" + firstCycle.getNegativeSequence().getAmplitude()
+ ", rms=" + firstCycle.getNegativeSequence().getRms()
+ ", phaseAngle=" + firstCycle.getNegativeSequence().getPhaseAngle());
}
if (firstCycle.getZeroSequence() != null) {
System.out.println(" 零序: amplitude=" + firstCycle.getZeroSequence().getAmplitude()
+ ", rms=" + firstCycle.getZeroSequence().getRms()
+ ", phaseAngle=" + firstCycle.getZeroSequence().getPhaseAngle());
}
if (firstCycle.getUnbalance() != null) {
System.out.println(" 不平衡度: negative=" + firstCycle.getUnbalance().getNegativeUnbalanceRate()
+ ", zero=" + firstCycle.getUnbalance().getZeroUnbalanceRate());
}
}
}
/**
* 打印单相电能质量结果摘要。
*/
private static void printPhaseVectorSummary(List<WavePhaseVectorDTO> phaseVectors) {
if (phaseVectors == null || phaseVectors.isEmpty()) {
return;
}
for (WavePhaseVectorDTO phaseVector : phaseVectors) {
System.out.println(" 相别=" + phaseVector.getPhaseName()
+ ", totalRms=" + phaseVector.getTotalRms()
+ ", fundamentalAmplitude=" + phaseVector.getFundamentalAmplitude()
+ ", fundamentalRms=" + phaseVector.getFundamentalRms()
+ ", fundamentalPhaseAngle=" + phaseVector.getFundamentalPhaseAngle()
+ ", harmonicDistortionRate=" + phaseVector.getHarmonicDistortionRate());
printHarmonicSummary("电压谐波", phaseVector.getHarmonicVoltageContentRates());
printHarmonicSummary("电流谐波", phaseVector.getHarmonicCurrentAmplitudes());
}
}
/**
* 打印前几个谐波结果,避免控制台过长。
*/
private static void printHarmonicSummary(String label, List<WaveHarmonicDTO> harmonics) {
if (harmonics == null || harmonics.isEmpty()) {
return;
}
int limit = Math.min(3, harmonics.size());
for (int i = 0; i < limit; i++) {
WaveHarmonicDTO harmonic = harmonics.get(i);
System.out.println(" " + label + "-第" + harmonic.getHarmonicOrder() + "次: amplitude=" + harmonic.getAmplitude()
+ ", rms=" + harmonic.getRms()
+ ", rate=" + harmonic.getRate());
}
}
/**
* 解析向量结果中的可计算周波数。
*/
private static Integer resolveCycleCount(List<WaveVectorGroupDTO> vectorGroups) {
if (vectorGroups == null || vectorGroups.isEmpty() || vectorGroups.get(0).getVectorSeries() == null) {
return 0;
}
return vectorGroups.get(0).getVectorSeries().size();
}
/**
* 打印 getComtrade 的关键摘要,便于快速人工核对。
*/
private static void printComtradeResult(WaveDataDTO waveDataDTO) {
System.out.println("=== getComtrade 结果 ===");
System.out.println("事件时间: " + waveDataDTO.getTime());
System.out.println("相别数: " + waveDataDTO.getIPhasic());
System.out.println("标题数: " + sizeOf(waveDataDTO.getWaveTitle()));
System.out.println("通道数: " + sizeOf(waveDataDTO.getChannelNames()));
System.out.println("波形点数: " + sizeOf(waveDataDTO.getListWaveData()));
if (waveDataDTO.getComtradeCfgDTO() != null) {
System.out.println("模拟量通道数: " + waveDataDTO.getComtradeCfgDTO().getNAnalogNum());
System.out.println("开关量通道数: " + waveDataDTO.getComtradeCfgDTO().getNDigitalNum());
System.out.println("最终采样率: " + waveDataDTO.getComtradeCfgDTO().getFinalSampleRate());
}
if (waveDataDTO.getWaveTitle() != null && !waveDataDTO.getWaveTitle().isEmpty()) {
System.out.println("波形标题: " + waveDataDTO.getWaveTitle());
}
if (waveDataDTO.getListWaveData() != null && !waveDataDTO.getListWaveData().isEmpty()) {
System.out.println("首行数据: " + waveDataDTO.getListWaveData().get(0));
System.out.println("末行数据: " + waveDataDTO.getListWaveData().get(waveDataDTO.getListWaveData().size() - 1));
}
}
/**
* 打印 getValidData 的关键摘要。
*/
private static void printValidDataResult(WaveDataDTO waveDataDTO) {
System.out.println("=== getValidData 结果 ===");
System.out.println("RMS 点数: " + sizeOf(waveDataDTO.getListRmsData()));
System.out.println("RMS 最小值点数: " + sizeOf(waveDataDTO.getListRmsMinData()));
if (waveDataDTO.getListRmsData() != null && !waveDataDTO.getListRmsData().isEmpty()) {
System.out.println("RMS 首行数据: " + waveDataDTO.getListRmsData().get(0));
System.out.println("RMS 末行数据: " + waveDataDTO.getListRmsData().get(waveDataDTO.getListRmsData().size() - 1));
}
if (waveDataDTO.getListRmsMinData() != null && !waveDataDTO.getListRmsMinData().isEmpty()) {
System.out.println("RMS 最小值数据: " + waveDataDTO.getListRmsMinData());
}
}
/**
* 打印特征值结果摘要。
*/
private static void printEigenvalueResult(List<EigenvalueDTO> eigenvalues) {
System.out.println("=== getEigenvalue 结果 ===");
System.out.println("特征值数量: " + sizeOf(eigenvalues));
if (eigenvalues == null || eigenvalues.isEmpty()) {
return;
}
for (int i = 0; i < eigenvalues.size(); i++) {
EigenvalueDTO eigenvalueDTO = eigenvalues.get(i);
System.out.println("" + (i + 1) + "相: amplitude=" + eigenvalueDTO.getAmplitude()
+ ", residualVoltage=" + eigenvalueDTO.getResidualVoltage()
+ ", ratedVoltage=" + eigenvalueDTO.getRatedVoltage()
+ ", durationTime=" + eigenvalueDTO.getDurationTime());
}
}
/**
* 安全返回列表大小,避免空指针。
*/
private static int sizeOf(List<?> list) {
return list == null ? 0 : list.size();
}
}

View File

@@ -1,22 +0,0 @@
package com.njcn.gather.tool.wave.component;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
/**
* 波形文件解析组件测试。
*/
class WaveFileComponentTest {
@Test
void shouldRoundWaveTimeToThreeDecimals() {
Assertions.assertEquals(1.235F, WaveFileComponent.roundWaveTime(1.2345F));
Assertions.assertEquals(-99.988F, WaveFileComponent.roundWaveTime(-99.9876F));
}
@Test
void shouldRoundWaveAmplitudeToThreeDecimals() {
Assertions.assertEquals(220.123F, WaveFileComponent.roundWaveAmplitude(220.1234F));
Assertions.assertEquals(-12.988F, WaveFileComponent.roundWaveAmplitude(-12.9876F));
}
}

View File

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

View File

@@ -1,162 +0,0 @@
package com.njcn.gather.tool.wave.service.impl;
import com.njcn.gather.tool.wave.component.WaveFileComponent;
import com.njcn.gather.tool.wave.component.WaveVectorComponent;
import com.njcn.gather.tool.wave.pojo.dto.AnalogDTO;
import com.njcn.gather.tool.wave.pojo.dto.ComtradeCfgDTO;
import com.njcn.gather.tool.wave.pojo.dto.WaveDataDTO;
import com.njcn.gather.tool.wave.pojo.param.WaveComtradeParseParam;
import com.njcn.gather.tool.wave.pojo.vo.WaveComtradeResultVO;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* 波形服务测试。
*/
class WaveServiceImplTest {
@Test
void shouldUseCfgRatioWhenCfgContainsValidPtAndCt() throws Exception {
WaveServiceImpl service = new WaveServiceImpl(new WaveFileComponent(), new WaveVectorComponent());
WaveDataDTO waveDataDTO = new WaveDataDTO();
ComtradeCfgDTO cfgDTO = new ComtradeCfgDTO();
cfgDTO.setLstAnalogDTO(Arrays.asList(
buildAnalog("V", 10000F, 100F),
buildAnalog("A", 200F, 1F)
));
waveDataDTO.setComtradeCfgDTO(cfgDTO);
WaveComtradeParseParam param = new WaveComtradeParseParam();
param.setPt(1D);
param.setCt(1D);
Method method = WaveServiceImpl.class.getDeclaredMethod("applyWaveMetadata", WaveDataDTO.class, WaveComtradeParseParam.class);
method.setAccessible(true);
method.invoke(service, waveDataDTO, param);
Assertions.assertEquals(100D, waveDataDTO.getPt());
Assertions.assertEquals(200D, waveDataDTO.getCt());
}
@Test
void shouldFillComtradeSummaryFields() throws Exception {
WaveServiceImpl service = new WaveServiceImpl(new WaveFileComponent(), new WaveVectorComponent());
WaveDataDTO waveDataDTO = new WaveDataDTO();
ComtradeCfgDTO cfgDTO = new ComtradeCfgDTO();
cfgDTO.setNChannelNum(8);
cfgDTO.setNPhasic(3);
cfgDTO.setLstAnalogDTO(Arrays.asList(
buildAnalog("V", 10000F, 100F),
buildAnalog("A", 200F, 1F)
));
waveDataDTO.setComtradeCfgDTO(cfgDTO);
waveDataDTO.setIPhasic(3);
WaveComtradeResultVO result = new WaveComtradeResultVO();
Method method = WaveServiceImpl.class.getDeclaredMethod("fillComtradeSummary", WaveComtradeResultVO.class, WaveDataDTO.class);
method.setAccessible(true);
method.invoke(service, result, waveDataDTO);
Assertions.assertEquals(8, result.getTotalChannels());
Assertions.assertEquals(3, result.getPhaseCount());
Assertions.assertEquals("kV/A", result.getUnit());
}
@Test
void shouldMaskValuesInsideBottomPlatformMiddle() throws Exception {
WaveServiceImpl service = new WaveServiceImpl(new WaveFileComponent(), new WaveVectorComponent());
WaveDataDTO waveDataDTO = new WaveDataDTO();
ComtradeCfgDTO cfgDTO = new ComtradeCfgDTO();
cfgDTO.setFinalSampleRate(1);
waveDataDTO.setComtradeCfgDTO(cfgDTO);
waveDataDTO.setListWaveData(buildRows(new float[][]{
{0F, 100F, 100F, 100F},
{1F, 100F, 100F, 100F},
{2F, 100F, 100F, 100F},
{3F, 100F, 100F, 100F},
{4F, 80F, 100F, 100F},
{5F, 60F, 100F, 100F},
{6F, 60F, 100F, 100F},
{7F, 60F, 100F, 100F},
{8F, 95F, 100F, 100F},
{9F, 100F, 100F, 100F}
}));
waveDataDTO.setListRmsData(buildRows(new float[][]{
{0F, 100F, 100F, 100F},
{1F, 100F, 100F, 100F},
{2F, 100F, 100F, 100F},
{3F, 100F, 100F, 100F},
{4F, 80F, 100F, 100F},
{5F, 60F, 100F, 100F},
{6F, 60F, 100F, 100F},
{7F, 60F, 100F, 100F},
{8F, 95F, 100F, 100F},
{9F, 100F, 100F, 100F}
}));
Method method = WaveServiceImpl.class.getDeclaredMethod("applySimplifiedDisplay", WaveDataDTO.class);
method.setAccessible(true);
method.invoke(service, waveDataDTO);
Assertions.assertEquals(0F, waveDataDTO.getListWaveData().get(0).get(0));
Assertions.assertEquals(100F, waveDataDTO.getListWaveData().get(0).get(1));
Assertions.assertEquals(100F, waveDataDTO.getListWaveData().get(2).get(1));
Assertions.assertEquals(80F, waveDataDTO.getListWaveData().get(4).get(1));
Assertions.assertEquals(60F, waveDataDTO.getListWaveData().get(5).get(1));
Assertions.assertNull(waveDataDTO.getListWaveData().get(6).get(1));
Assertions.assertEquals(60F, waveDataDTO.getListWaveData().get(7).get(1));
Assertions.assertEquals(95F, waveDataDTO.getListWaveData().get(8).get(1));
Assertions.assertEquals(100F, waveDataDTO.getListWaveData().get(9).get(1));
}
@Test
void shouldKeepDataWhenSimplifiedRangeCannotBeDetected() throws Exception {
WaveServiceImpl service = new WaveServiceImpl(new WaveFileComponent(), new WaveVectorComponent());
WaveDataDTO waveDataDTO = new WaveDataDTO();
ComtradeCfgDTO cfgDTO = new ComtradeCfgDTO();
cfgDTO.setFinalSampleRate(1);
waveDataDTO.setComtradeCfgDTO(cfgDTO);
waveDataDTO.setListWaveData(buildRows(new float[][]{
{0F, 100F, 100F, 100F},
{1F, 99F, 100F, 100F},
{2F, 98F, 100F, 100F}
}));
waveDataDTO.setListRmsData(buildRows(new float[][]{
{0F, 100F, 100F, 100F},
{1F, 99F, 100F, 100F},
{2F, 98F, 100F, 100F}
}));
Method method = WaveServiceImpl.class.getDeclaredMethod("applySimplifiedDisplay", WaveDataDTO.class);
method.setAccessible(true);
method.invoke(service, waveDataDTO);
Assertions.assertEquals(99F, waveDataDTO.getListWaveData().get(1).get(1));
Assertions.assertEquals(98F, waveDataDTO.getListRmsData().get(2).get(1));
}
private AnalogDTO buildAnalog(String unit, Float primary, Float secondary) {
AnalogDTO analogDTO = new AnalogDTO();
analogDTO.setSzUnitName(unit);
analogDTO.setFPrimary(primary);
analogDTO.setFSecondary(secondary);
return analogDTO;
}
private List<List<Float>> buildRows(float[][] values) {
List<List<Float>> rows = new ArrayList<>();
for (float[] value : values) {
List<Float> row = new ArrayList<>();
for (float item : value) {
row.add(item);
}
rows.add(row);
}
return rows;
}
}