feat(parse-pqdif): 重构PQDIF文件存储及解析功能

- 引入PqdifFileStorageService组件实现PQDIF原始文件本地存储
- 修改application.yml添加parse-pqdif.storage.path配置项
- 更新CsPqdifPath相关实体类将pqdifContent改为filePath存储路径
- 重构CsPqdifPathController中的文件上传处理逻辑
- 新增PqdifRecognizeResultVO支持PQDIF二次解析结果结构
- 实现ParsePqdifService接口支持可识别数据解析功能
- 添加PqdifSecondParseComponent进行底层解析结果转换
- 新增桌面PQDIF文件测试程序便于调试验证
- 为CsIcdPath增加referenceIcdId字段及相应测试用例
This commit is contained in:
2026-06-23 08:23:15 +08:00
parent 97b1334714
commit 2b56da2134
21 changed files with 713 additions and 51 deletions

View File

@@ -30,6 +30,9 @@ public class CsIcdPathParam {
@ApiModelProperty("ICD类型1-手动录入的标准ICD2-手动录入的非标准ICD3-上游解析传递的标准ICD4-上游解析传递的非标准ICD")
private Integer type;
@ApiModelProperty("标准ICD引用ID")
private String referenceIcdId;
/**
* ICD 存储记录编辑参数。
*/

View File

@@ -189,6 +189,7 @@ public class CsIcdPathServiceImpl implements CsIcdPathService {
icdPath.setAngle(param.getAngle());
icdPath.setUsePhaseIndex(param.getUsePhaseIndex());
icdPath.setType(useDefaultType ? resolveIcdType(param.getType()) : param.getType());
icdPath.setReferenceIcdId(trimToNull(param.getReferenceIcdId()));
return icdPath;
}

View File

@@ -100,6 +100,20 @@ class CsIcdPathServiceImplTest {
Assertions.assertArrayEquals(fileContent, captor.getValue().getIcdContent());
}
@Test
void addIcdPathShouldSaveReferenceIcdId() {
CsIcdPathParam param = buildParam("非标准ICD");
param.setReferenceIcdId(" reference-icd ");
when(csIcdPathMapper.insert(any(CsIcdPathPO.class))).thenReturn(1);
boolean result = service.addIcdPath(param);
ArgumentCaptor<CsIcdPathPO> captor = ArgumentCaptor.forClass(CsIcdPathPO.class);
verify(csIcdPathMapper).insert(captor.capture());
Assertions.assertTrue(result);
Assertions.assertEquals("reference-icd", captor.getValue().getReferenceIcdId());
}
@Test
void addIcdPathShouldDefaultTypeToManualNonStandard() {
CsIcdPathParam param = buildParam("手动录入非标准ICD");

View File

@@ -55,6 +55,12 @@
<scope>system</scope>
<systemPath>${project.basedir}/lib/pqdif-native-basic-bridge-1.0.0-jar-with-dependencies.jar</systemPath>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>

View File

@@ -0,0 +1,71 @@
package com.njcn.gather.tool.parsepqdif.component;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.util.UUID;
/**
* PQDIF 原始文件本地存储服务。
*/
@Component
public class PqdifFileStorageService {
private static final String DEFAULT_STORAGE_DIR = "data/parse-pqdif";
@Value("${parse-pqdif.storage.path:}")
private String storagePath;
public String save(MultipartFile pqdifFile) {
if (pqdifFile == null || pqdifFile.isEmpty()) {
throw new IllegalArgumentException("PQDIF文件不能为空");
}
try {
Path storageDir = resolveStorageDir();
Files.createDirectories(storageDir);
if (!Files.isDirectory(storageDir)) {
throw new IllegalStateException("PQDIF文件存储路径不是目录" + storageDir);
}
String fileName = UUID.randomUUID().toString().replace("-", "") + "_" + sanitizeFileName(pqdifFile.getOriginalFilename());
Path target = storageDir.resolve(fileName).normalize();
if (!target.startsWith(storageDir)) {
throw new IllegalArgumentException("PQDIF文件名不合法");
}
try (InputStream inputStream = pqdifFile.getInputStream()) {
Files.copy(inputStream, target, StandardCopyOption.REPLACE_EXISTING);
}
return target.toString();
} catch (Exception ex) {
throw new IllegalArgumentException("保存PQDIF文件失败" + ex.getMessage(), ex);
}
}
private Path resolveStorageDir() {
String configuredPath = trimToNull(storagePath);
Path path = configuredPath == null ? Paths.get(DEFAULT_STORAGE_DIR) : Paths.get(configuredPath);
return path.toAbsolutePath().normalize();
}
private String sanitizeFileName(String originalFilename) {
String fileName = trimToNull(originalFilename);
if (fileName == null) {
return "unnamed.pqd";
}
String normalizedName = Paths.get(fileName).getFileName().toString();
return normalizedName.replaceAll("[\\\\/:*?\"<>|]", "_");
}
private String trimToNull(String value) {
if (value == null) {
return null;
}
String text = value.trim();
return text.isEmpty() ? null : text;
}
}

View File

@@ -0,0 +1,281 @@
package com.njcn.gather.tool.parsepqdif.component;
import com.njcn.gather.tool.parsepqdif.pojo.vo.PqdifParseResponse;
import com.njcn.gather.tool.parsepqdif.pojo.vo.PqdifRecognizeResultVO;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 将 parse 方法返回的底层 PQDIF 解析结果转换为业务可识别结构。
*/
@Component
public class PqdifSecondParseComponent {
private static final String STATUS_SUCCESS = "SUCCESS";
private static final String STATUS_FAILED = "FAILED";
private static final String MESSAGE_SUCCESS = "PQDIF二次解析完成";
private static final DateTimeFormatter STANDARD_DATE_TIME_FORMATTER =
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
private static final Map<Long, PhaseInfo> PHASE_MAP = buildPhaseMap();
private static final Map<Long, String> MEASUREMENT_MAP = buildMeasurementMap();
private static final Map<Long, String> UNIT_MAP = buildUnitMap();
private static final Map<Long, String> SERIES_BASE_TYPE_MAP = buildSeriesBaseTypeMap();
public PqdifRecognizeResultVO parseRecognizableData(PqdifParseResponse parseResponse) {
if (parseResponse == null) {
return failed("parse解析结果不能为空");
}
if (!STATUS_SUCCESS.equals(parseResponse.getStatus())) {
String message = parseResponse.getMessage() == null ? "" : parseResponse.getMessage();
return failed("parse解析未成功不能进行二次解析" + message);
}
PqdifRecognizeResultVO result = emptyResult(STATUS_SUCCESS, MESSAGE_SUCCESS);
List<PqdifParseResponse.ObservationVO> sourceObservations = parseResponse.getObservations();
if (sourceObservations == null || sourceObservations.isEmpty()) {
return result;
}
for (int observationIndex = 0; observationIndex < sourceObservations.size(); observationIndex++) {
PqdifParseResponse.ObservationVO sourceObservation = sourceObservations.get(observationIndex);
result.getObservations().add(parseObservation(sourceObservation, observationIndex, result));
}
result.setObservationCount((long) result.getObservations().size());
return result;
}
private PqdifRecognizeResultVO.RecognizedObservationVO parseObservation(
PqdifParseResponse.ObservationVO source,
int observationIndex,
PqdifRecognizeResultVO result) {
PqdifRecognizeResultVO.RecognizedObservationVO target = new PqdifRecognizeResultVO.RecognizedObservationVO();
target.setRecordIndex(source == null ? null : source.getRecordIndex());
target.setName(source == null ? null : source.getName());
target.setStartTime(source == null ? null : formatStartTime(source.getTimeStartText()));
target.setChannels(new ArrayList<PqdifRecognizeResultVO.RecognizedChannelVO>());
if (source == null || source.getChannels() == null || source.getChannels().isEmpty()) {
return target;
}
for (int channelIndex = 0; channelIndex < source.getChannels().size(); channelIndex++) {
PqdifParseResponse.ChannelInfoVO sourceChannel = source.getChannels().get(channelIndex);
target.getChannels().add(parseChannel(sourceChannel, observationIndex, channelIndex, result));
result.setChannelCount(result.getChannelCount() + 1);
}
return target;
}
private PqdifRecognizeResultVO.RecognizedChannelVO parseChannel(
PqdifParseResponse.ChannelInfoVO source,
int observationIndex,
int channelIndex,
PqdifRecognizeResultVO result) {
PqdifRecognizeResultVO.RecognizedChannelVO target = new PqdifRecognizeResultVO.RecognizedChannelVO();
target.setChannelIndex(source == null ? null : source.getChannelIndex());
target.setName(source == null ? null : source.getName());
target.setPhaseId(source == null ? null : source.getPhaseId());
target.setQuantityMeasuredId(source == null ? null : source.getQuantityMeasuredId());
target.setQuantityTypeGuid(source == null ? null : source.getQuantityTypeGuid());
target.setSeries(new ArrayList<PqdifRecognizeResultVO.RecognizedSeriesVO>());
if (source == null) {
return target;
}
PhaseInfo phaseInfo = PHASE_MAP.get(source.getPhaseId());
if (phaseInfo == null) {
addUnknown(result, channelPath(observationIndex, channelIndex), "phaseId", source.getPhaseId(), "未识别的相别ID");
} else {
target.setPhaseCode(phaseInfo.getCode());
target.setPhaseName(phaseInfo.getName());
}
String measurementName = MEASUREMENT_MAP.get(source.getQuantityMeasuredId());
if (measurementName == null) {
addUnknown(result, channelPath(observationIndex, channelIndex),
"quantityMeasuredId", source.getQuantityMeasuredId(), "未识别的测量量ID");
} else {
target.setMeasurementName(measurementName);
}
if (source.getSeries() == null || source.getSeries().isEmpty()) {
return target;
}
for (int seriesIndex = 0; seriesIndex < source.getSeries().size(); seriesIndex++) {
PqdifParseResponse.SeriesInfoVO sourceSeries = source.getSeries().get(seriesIndex);
target.getSeries().add(parseSeries(sourceSeries, observationIndex, channelIndex, seriesIndex, result));
result.setSeriesCount(result.getSeriesCount() + 1);
}
return target;
}
private PqdifRecognizeResultVO.RecognizedSeriesVO parseSeries(
PqdifParseResponse.SeriesInfoVO source,
int observationIndex,
int channelIndex,
int seriesIndex,
PqdifRecognizeResultVO result) {
PqdifRecognizeResultVO.RecognizedSeriesVO target = new PqdifRecognizeResultVO.RecognizedSeriesVO();
if (source == null) {
return target;
}
target.setSeriesIndex(source.getSeriesIndex());
target.setQuantityUnitsId(source.getQuantityUnitsId());
target.setQuantityCharacteristicGuid(source.getQuantityCharacteristicGuid());
target.setValueTypeGuid(source.getValueTypeGuid());
target.setSeriesBaseType(source.getSeriesBaseType());
target.setScale(source.getScale());
target.setOffset(source.getOffset());
target.setDataStatus(source.getDataStatus());
target.setDataMessage(source.getDataMessage());
target.setValueCount(source.getValueCount());
target.setFirstValues(source.getFirstValues());
String unitName = UNIT_MAP.get(source.getQuantityUnitsId());
if (unitName == null) {
addUnknown(result, seriesPath(observationIndex, channelIndex, seriesIndex),
"quantityUnitsId", source.getQuantityUnitsId(), "未识别的单位ID");
} else {
target.setUnitName(unitName);
}
String dataTypeName = SERIES_BASE_TYPE_MAP.get(source.getSeriesBaseType());
if (dataTypeName == null) {
addUnknown(result, seriesPath(observationIndex, channelIndex, seriesIndex),
"seriesBaseType", source.getSeriesBaseType(), "未识别的Series基础类型");
} else {
target.setDataTypeName(dataTypeName);
}
return target;
}
private PqdifRecognizeResultVO failed(String message) {
return emptyResult(STATUS_FAILED, message);
}
private String formatStartTime(String timeStartText) {
if (timeStartText == null || timeStartText.trim().isEmpty()) {
return timeStartText;
}
try {
return LocalDateTime.parse(timeStartText.trim()).format(STANDARD_DATE_TIME_FORMATTER);
} catch (DateTimeParseException e) {
return timeStartText;
}
}
private PqdifRecognizeResultVO emptyResult(String status, String message) {
PqdifRecognizeResultVO result = new PqdifRecognizeResultVO();
result.setStatus(status);
result.setMessage(message);
result.setObservationCount(0L);
result.setChannelCount(0L);
result.setSeriesCount(0L);
result.setObservations(new ArrayList<PqdifRecognizeResultVO.RecognizedObservationVO>());
result.setUnknownItems(new ArrayList<PqdifRecognizeResultVO.UnknownItemVO>());
return result;
}
private void addUnknown(PqdifRecognizeResultVO result, String path, String fieldName, Object fieldValue, String message) {
if (fieldValue == null) {
return;
}
PqdifRecognizeResultVO.UnknownItemVO item = new PqdifRecognizeResultVO.UnknownItemVO();
item.setPath(path);
item.setFieldName(fieldName);
item.setFieldValue(String.valueOf(fieldValue));
item.setMessage(message);
result.getUnknownItems().add(item);
}
private String channelPath(int observationIndex, int channelIndex) {
return "observations[" + observationIndex + "].channels[" + channelIndex + "]";
}
private String seriesPath(int observationIndex, int channelIndex, int seriesIndex) {
return channelPath(observationIndex, channelIndex) + ".series[" + seriesIndex + "]";
}
private static Map<Long, PhaseInfo> buildPhaseMap() {
Map<Long, PhaseInfo> map = new HashMap<Long, PhaseInfo>();
map.put(0L, new PhaseInfo("UNKNOWN", "未知相别"));
map.put(1L, new PhaseInfo("A", "A相"));
map.put(2L, new PhaseInfo("B", "B相"));
map.put(3L, new PhaseInfo("C", "C相"));
map.put(4L, new PhaseInfo("N", "中性线"));
map.put(5L, new PhaseInfo("AB", "AB线电压"));
map.put(6L, new PhaseInfo("BC", "BC线电压"));
map.put(7L, new PhaseInfo("CA", "CA线电压"));
return map;
}
private static Map<Long, String> buildMeasurementMap() {
Map<Long, String> map = new HashMap<Long, String>();
map.put(0L, "电压");
map.put(1L, "电流");
map.put(2L, "频率");
map.put(3L, "有功功率");
map.put(4L, "无功功率");
map.put(5L, "视在功率");
map.put(6L, "功率因数");
return map;
}
private static Map<Long, String> buildUnitMap() {
Map<Long, String> map = new HashMap<Long, String>();
map.put(27L, "A");
map.put(29L, "V");
map.put(32L, "Hz");
map.put(38L, "W");
map.put(39L, "var");
map.put(40L, "VA");
map.put(41L, "%");
return map;
}
private static Map<Long, String> buildSeriesBaseTypeMap() {
Map<Long, String> map = new HashMap<Long, String>();
map.put(1L, "DOUBLE");
map.put(2L, "INTEGER");
map.put(3L, "BOOLEAN");
map.put(4L, "TEXT");
return map;
}
private static class PhaseInfo {
private final String code;
private final String name;
private PhaseInfo(String code, String name) {
this.code = code;
this.name = name;
}
private String getCode() {
return code;
}
private String getName() {
return name;
}
}
}

View File

@@ -6,6 +6,7 @@ import com.njcn.common.pojo.enums.common.LogEnum;
import com.njcn.common.pojo.enums.response.CommonResponseEnum;
import com.njcn.common.pojo.response.HttpResult;
import com.njcn.common.utils.LogUtil;
import com.njcn.gather.tool.parsepqdif.component.PqdifFileStorageService;
import com.njcn.gather.tool.parsepqdif.pojo.param.CsPqdifPathParam;
import com.njcn.gather.tool.parsepqdif.pojo.param.PqdifParseResultSaveParam;
import com.njcn.gather.tool.parsepqdif.pojo.vo.CsPqdifPathDetailVO;
@@ -27,7 +28,6 @@ import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.util.List;
/**
@@ -42,6 +42,8 @@ public class CsPqdifPathController extends BaseController {
private final CsPqdifPathService csPqdifPathService;
private final PqdifFileStorageService pqdifFileStorageService;
@OperateInfo(info = LogEnum.BUSINESS_COMMON)
@ApiOperation("查询PQDIF存储记录列表")
@PostMapping("/list")
@@ -69,7 +71,7 @@ public class CsPqdifPathController extends BaseController {
@RequestPart("request") @Validated CsPqdifPathParam param) {
String methodDescribe = getMethodDescribe("addWithFile");
LogUtil.njcnDebug(log, "{}开始上传并新增PQDIF存储记录fileName={}", methodDescribe, resolveFileName(pqdifFile));
fillPqdifFile(param, pqdifFile);
fillPqdifFilePath(param, pqdifFile);
boolean result = csPqdifPathService.addPqdifPath(param);
return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, result, methodDescribe);
}
@@ -92,7 +94,7 @@ public class CsPqdifPathController extends BaseController {
String methodDescribe = getMethodDescribe("updateWithFile");
LogUtil.njcnDebug(log, "{}开始上传并编辑PQDIF存储记录pqdifId={}fileName={}",
methodDescribe, param.getId(), resolveFileName(pqdifFile));
fillPqdifFile(param, pqdifFile);
fillPqdifFilePath(param, pqdifFile);
boolean result = csPqdifPathService.updatePqdifPath(param);
return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, result, methodDescribe);
}
@@ -141,15 +143,8 @@ public class CsPqdifPathController extends BaseController {
return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, result, methodDescribe);
}
private void fillPqdifFile(CsPqdifPathParam param, MultipartFile pqdifFile) {
if (pqdifFile == null || pqdifFile.isEmpty()) {
throw new IllegalArgumentException("PQDIF文件不能为空");
}
try {
param.setPqdifContent(pqdifFile.getBytes());
} catch (IOException ex) {
throw new IllegalArgumentException("读取PQDIF文件失败" + ex.getMessage(), ex);
}
private void fillPqdifFilePath(CsPqdifPathParam param, MultipartFile pqdifFile) {
param.setFilePath(pqdifFileStorageService.save(pqdifFile));
}
private String resolveFileName(MultipartFile pqdifFile) {

View File

@@ -6,6 +6,7 @@ import com.njcn.common.pojo.enums.response.CommonResponseEnum;
import com.njcn.common.pojo.response.HttpResult;
import com.njcn.common.utils.LogUtil;
import com.njcn.gather.tool.parsepqdif.pojo.vo.PqdifParseResponse;
import com.njcn.gather.tool.parsepqdif.pojo.vo.PqdifRecognizeResultVO;
import com.njcn.gather.tool.parsepqdif.service.ParsePqdifService;
import com.njcn.web.controller.BaseController;
import com.njcn.web.utils.HttpResultUtil;
@@ -43,4 +44,17 @@ public class ParsePqdifController extends BaseController {
PqdifParseResponse result = parsePqdifService.parse(pqdifFile);
return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, result, methodDescribe);
}
@OperateInfo(info = LogEnum.BUSINESS_COMMON)
@ApiOperation("上传 PQDIF 文件并解析为可识别数据")
@ApiImplicitParam(name = "pqdifFile", value = "PQDIF文件", required = true, dataType = "__file", paramType = "form")
@PostMapping(value = "/parse-recognizable", consumes = {"multipart/form-data"})
public HttpResult<PqdifRecognizeResultVO> parseRecognizable(@RequestPart("pqdifFile") MultipartFile pqdifFile) {
String methodDescribe = getMethodDescribe("parseRecognizable");
LogUtil.njcnDebug(log, "{}PQDIF二次解析入口fileName={}",
methodDescribe, pqdifFile == null ? null : pqdifFile.getOriginalFilename());
PqdifParseResponse parseResponse = parsePqdifService.parse(pqdifFile);
PqdifRecognizeResultVO result = parsePqdifService.parseRecognizable(parseResponse);
return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, result, methodDescribe);
}
}

View File

@@ -7,7 +7,7 @@
type="com.njcn.gather.tool.parsepqdif.pojo.vo.CsPqdifPathVO">
<id column="id" property="id"/>
<result column="name" property="name"/>
<result column="nativeVersion" property="nativeVersion"/>
<result column="filePath" property="filePath"/>
<result column="recordCount" property="recordCount"/>
<result column="observationCount" property="observationCount"/>
<result column="sampleValueCount" property="sampleValueCount"/>
@@ -26,13 +26,12 @@
SELECT
ID AS id,
Name AS name,
Native_Version AS nativeVersion,
File_Path AS filePath,
Record_Count AS recordCount,
Observation_Count AS observationCount,
Sample_Value_Count AS sampleValueCount,
State AS state,
Result AS result,
Msg AS msg,
Create_By AS createBy,
Create_Time AS createTime,
Update_By AS updateBy,
@@ -61,8 +60,7 @@
SELECT
ID AS id,
Name AS name,
Json_Str AS jsonStr,
Pqdif AS pqdifContent
File_Path AS filePath
FROM cs_pqdif_path
WHERE ID = #{id}
AND State = 1

View File

@@ -1,5 +1,6 @@
package com.njcn.gather.tool.parsepqdif.pojo.param;
import com.fasterxml.jackson.annotation.JsonIgnore;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
@@ -18,8 +19,9 @@ public class CsPqdifPathParam {
@NotBlank(message = "PQDIF名称不能为空")
private String name;
@ApiModelProperty("PQDIF文件二进制内容")
private byte[] pqdifContent;
@ApiModelProperty("PQDIF原始文件存储路径")
@JsonIgnore
private String filePath;
/**
* PQDIF 存储记录编辑参数。

View File

@@ -12,9 +12,6 @@ import lombok.Data;
@ApiModel("PQDIF解析结果保存参数")
public class PqdifParseResultSaveParam {
@ApiModelProperty("native解析库版本")
private String nativeVersion;
@ApiModelProperty("Record总数")
private Long recordCount;
@@ -30,6 +27,4 @@ public class PqdifParseResultSaveParam {
@ApiModelProperty("解析提示、失败原因或解析结论JSON")
private JsonNode msg;
@ApiModelProperty("完整PQDIF解析结果JSON")
private String jsonStr;
}

View File

@@ -25,11 +25,8 @@ public class CsPqdifPathPO implements Serializable {
@TableField("Name")
private String name;
@TableField("Pqdif")
private byte[] pqdifContent;
@TableField("Native_Version")
private String nativeVersion;
@TableField("File_Path")
private String filePath;
@TableField("Record_Count")
private Long recordCount;
@@ -46,9 +43,6 @@ public class CsPqdifPathPO implements Serializable {
@TableField(value = "Msg", typeHandler = JsonNodeTypeHandler.class)
private JsonNode msg;
@TableField("Json_Str")
private String jsonStr;
@TableField("State")
private Integer state;

View File

@@ -1,6 +1,5 @@
package com.njcn.gather.tool.parsepqdif.pojo.vo;
import com.fasterxml.jackson.annotation.JsonIgnore;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
@@ -18,9 +17,6 @@ public class CsPqdifPathDetailVO {
@ApiModelProperty("PQDIF名称")
private String name;
@ApiModelProperty("完整PQDIF解析结果JSON")
private String jsonStr;
@JsonIgnore
private byte[] pqdifContent;
@ApiModelProperty("PQDIF原始文件存储路径")
private String filePath;
}

View File

@@ -20,8 +20,8 @@ public class CsPqdifPathVO {
@ApiModelProperty("PQDIF名称")
private String name;
@ApiModelProperty("native解析库版本")
private String nativeVersion;
@ApiModelProperty("PQDIF原始文件存储路径")
private String filePath;
@ApiModelProperty("Record总数")
private Long recordCount;

View File

@@ -0,0 +1,81 @@
package com.njcn.gather.tool.parsepqdif.pojo.vo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.util.List;
@Data
@ApiModel("PQDIF二次解析结果")
public class PqdifRecognizeResultVO {
@ApiModelProperty("状态SUCCESS / FAILED")
private String status;
@ApiModelProperty("提示信息")
private String message;
@ApiModelProperty("Observation数量")
private Long observationCount;
@ApiModelProperty("Channel数量")
private Long channelCount;
@ApiModelProperty("Series数量")
private Long seriesCount;
@ApiModelProperty("可识别Observation列表")
private List<RecognizedObservationVO> observations;
@ApiModelProperty("未识别字段列表")
private List<UnknownItemVO> unknownItems;
@Data
public static class RecognizedObservationVO {
private Long recordIndex;
private String name;
private String startTime;
private List<RecognizedChannelVO> channels;
}
@Data
public static class RecognizedChannelVO {
private Long channelIndex;
private String name;
private String phaseCode;
private String phaseName;
private String measurementName;
private Long phaseId;
private Long quantityMeasuredId;
private String quantityTypeGuid;
private List<RecognizedSeriesVO> series;
}
@Data
public static class RecognizedSeriesVO {
private Long seriesIndex;
private String unitName;
private String characteristicName;
private String valueTypeName;
private String dataTypeName;
private Long quantityUnitsId;
private String quantityCharacteristicGuid;
private String valueTypeGuid;
private Long seriesBaseType;
private Double scale;
private Double offset;
private String dataStatus;
private String dataMessage;
private Integer valueCount;
private List<Double> firstValues;
}
@Data
public static class UnknownItemVO {
private String path;
private String fieldName;
private String fieldValue;
private String message;
}
}

View File

@@ -1,6 +1,7 @@
package com.njcn.gather.tool.parsepqdif.service;
import com.njcn.gather.tool.parsepqdif.pojo.vo.PqdifParseResponse;
import com.njcn.gather.tool.parsepqdif.pojo.vo.PqdifRecognizeResultVO;
import org.springframework.web.multipart.MultipartFile;
/**
@@ -9,4 +10,6 @@ import org.springframework.web.multipart.MultipartFile;
public interface ParsePqdifService {
PqdifParseResponse parse(MultipartFile pqdifFile);
PqdifRecognizeResultVO parseRecognizable(PqdifParseResponse parseResponse);
}

View File

@@ -1,7 +1,9 @@
package com.njcn.gather.tool.parsepqdif.service.impl;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.njcn.gather.tool.parsepqdif.mapper.CsPqdifPathMapper;
import com.njcn.gather.tool.parsepqdif.pojo.param.CsPqdifPathParam;
import com.njcn.gather.tool.parsepqdif.pojo.param.PqdifParseResultSaveParam;
@@ -29,6 +31,8 @@ public class CsPqdifPathServiceImpl implements CsPqdifPathService {
private static final int STATE_DELETED = 0;
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
private final CsPqdifPathMapper csPqdifPathMapper;
@Override
@@ -71,7 +75,7 @@ public class CsPqdifPathServiceImpl implements CsPqdifPathService {
@Transactional
public boolean updatePqdifPath(CsPqdifPathParam.UpdateParam param) {
CsPqdifPathParam.UpdateParam checkedParam = requireUpdateParam(param);
requirePqdifPath(checkedParam.getId());
requireNormalPqdifPath(checkedParam.getId());
CsPqdifPathPO pqdifPath = buildPqdifPath(checkedParam);
pqdifPath.setId(checkedParam.getId());
pqdifPath.setUpdateBy(currentUserId());
@@ -100,14 +104,15 @@ public class CsPqdifPathServiceImpl implements CsPqdifPathService {
if (param == null) {
throw new IllegalArgumentException("PQDIF解析结果不能为空");
}
CsPqdifPathPO pqdifPath = requirePqdifPath(pqdifId);
pqdifPath.setNativeVersion(trimToNull(param.getNativeVersion()));
String id = requireNormalPqdifPath(pqdifId);
CsPqdifPathPO pqdifPath = new CsPqdifPathPO();
pqdifPath.setId(id);
pqdifPath.setRecordCount(param.getRecordCount());
pqdifPath.setObservationCount(param.getObservationCount());
pqdifPath.setSampleValueCount(param.getSampleValueCount());
pqdifPath.setResult(normalizeResult(param.getResult()));
pqdifPath.setMsg(param.getMsg());
pqdifPath.setJsonStr(trimToNull(param.getJsonStr()));
Integer result = normalizeResult(param.getResult());
pqdifPath.setResult(result);
pqdifPath.setMsg(buildStorageResultMsg(result));
pqdifPath.setUpdateBy(currentUserId());
pqdifPath.setUpdateTime(LocalDateTime.now());
return csPqdifPathMapper.updateById(pqdifPath) > 0;
@@ -116,7 +121,7 @@ public class CsPqdifPathServiceImpl implements CsPqdifPathService {
private CsPqdifPathPO buildPqdifPath(CsPqdifPathParam param) {
CsPqdifPathPO pqdifPath = new CsPqdifPathPO();
pqdifPath.setName(requireText(param.getName(), "PQDIF名称不能为空"));
pqdifPath.setPqdifContent(param.getPqdifContent());
pqdifPath.setFilePath(trimToNull(param.getFilePath()));
return pqdifPath;
}
@@ -135,13 +140,13 @@ public class CsPqdifPathServiceImpl implements CsPqdifPathService {
return param;
}
private CsPqdifPathPO requirePqdifPath(String pqdifId) {
private String requireNormalPqdifPath(String pqdifId) {
String id = requireText(pqdifId, "PQDIF记录ID不能为空");
CsPqdifPathPO pqdifPath = csPqdifPathMapper.selectById(id);
if (pqdifPath == null || !Integer.valueOf(STATE_NORMAL).equals(pqdifPath.getState())) {
CsPqdifPathDetailVO pqdifPath = csPqdifPathMapper.selectPqdifPathDetailById(id);
if (pqdifPath == null) {
throw new IllegalArgumentException("PQDIF记录不存在或已删除");
}
return pqdifPath;
return id;
}
private Integer normalizeResult(Integer result) {
@@ -154,6 +159,18 @@ public class CsPqdifPathServiceImpl implements CsPqdifPathService {
return result;
}
private JsonNode buildStorageResultMsg(Integer result) {
ObjectNode msg = OBJECT_MAPPER.createObjectNode();
if (Integer.valueOf(1).equals(result)) {
msg.put("storageResult", "SUCCESS");
msg.put("message", "存储成功");
} else {
msg.put("storageResult", "FAILED");
msg.put("message", "存储失败");
}
return msg;
}
private String requireText(String value, String message) {
String text = trimToNull(value);
if (text == null) {

View File

@@ -1,6 +1,8 @@
package com.njcn.gather.tool.parsepqdif.service.impl;
import com.njcn.gather.tool.parsepqdif.component.PqdifSecondParseComponent;
import com.njcn.gather.tool.parsepqdif.pojo.vo.PqdifParseResponse;
import com.njcn.gather.tool.parsepqdif.pojo.vo.PqdifRecognizeResultVO;
import com.njcn.gather.tool.parsepqdif.reader.PqdifNativeReader;
import com.njcn.gather.tool.parsepqdif.service.ParsePqdifService;
import lombok.RequiredArgsConstructor;
@@ -31,6 +33,7 @@ public class ParsePqdifServiceImpl implements ParsePqdifService {
private static final String UNKNOWN_FAILED_REASON = "请检查文件内容或原生解析库状态";
private final PqdifNativeReader pqdifNativeReader;
private final PqdifSecondParseComponent pqdifSecondParseComponent;
@Override
public PqdifParseResponse parse(MultipartFile pqdifFile) {
@@ -56,6 +59,11 @@ public class ParsePqdifServiceImpl implements ParsePqdifService {
}
}
@Override
public PqdifRecognizeResultVO parseRecognizable(PqdifParseResponse parseResponse) {
return pqdifSecondParseComponent.parseRecognizableData(parseResponse);
}
private Path createTempPqdifFile(MultipartFile pqdifFile, String suffix) throws Exception {
// 原生解析库只接收文件路径,因此上传内容需先落到系统临时目录。
Path uploadDir = Paths.get(System.getProperty("java.io.tmpdir"), TEMP_DIR_NAME);

View File

@@ -0,0 +1,151 @@
package com.njcn.gather.tool.parsepqdif;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.njcn.gather.tool.parsepqdif.component.PqdifSecondParseComponent;
import com.njcn.gather.tool.parsepqdif.pojo.vo.PqdifParseResponse;
import com.njcn.gather.tool.parsepqdif.pojo.vo.PqdifRecognizeResultVO;
import com.njcn.gather.tool.parsepqdif.reader.PqdifNativeReader;
import com.njcn.gather.tool.parsepqdif.service.ParsePqdifService;
import com.njcn.gather.tool.parsepqdif.service.impl.ParsePqdifServiceImpl;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.util.Arrays;
import java.util.List;
public class PqdifDesktopFileTestProgram {
private static final String PQDIF_FILE_NAME = "PQMonitor_PQM1_20210512_1000_01.pqd";
private static final String MULTIPART_PARAM_NAME = "pqdifFile";
private static final String SAMPLE_RESOURCE_PATH = "pqdif-samples/" + PQDIF_FILE_NAME;
public static void main(String[] args) throws Exception {
Path pqdifPath = resolvePqdifPath(args);
if (!Files.isRegularFile(pqdifPath)) {
System.out.println("PQDIF file not found: " + pqdifPath.toAbsolutePath());
System.out.println("Usage: java " + PqdifDesktopFileTestProgram.class.getName() + " <pqdif-file-path>");
return;
}
System.out.println("Parsing PQDIF file: " + pqdifPath.toAbsolutePath());
ParsePqdifService parsePqdifService = new ParsePqdifServiceImpl(new PqdifNativeReader(), new PqdifSecondParseComponent());
PqdifParseResponse response = parsePqdifService.parse(new PathMultipartFile(MULTIPART_PARAM_NAME, pqdifPath));
System.out.println("Original parse response:");
printResponse(response);
PqdifRecognizeResultVO recognizableResult = parsePqdifService.parseRecognizable(response);
System.out.println("Recognizable parse response:");
printResponse(recognizableResult);
}
private static Path resolvePqdifPath(String[] args) {
if (args != null && args.length > 0 && args[0] != null && !args[0].trim().isEmpty()) {
return Paths.get(args[0].trim()).toAbsolutePath().normalize();
}
Path bundledSample = resolveBundledSamplePath();
if (bundledSample != null && Files.isRegularFile(bundledSample)) {
return bundledSample;
}
return defaultCandidatePaths().stream()
.filter(Files::isRegularFile)
.findFirst()
.orElse(defaultDesktopPath());
}
private static Path resolveBundledSamplePath() {
URL resource = PqdifDesktopFileTestProgram.class.getClassLoader().getResource(SAMPLE_RESOURCE_PATH);
if (resource == null || !"file".equalsIgnoreCase(resource.getProtocol())) {
return null;
}
try {
return Paths.get(resource.toURI()).toAbsolutePath().normalize();
} catch (Exception e) {
return null;
}
}
private static List<Path> defaultCandidatePaths() {
return Arrays.asList(
Paths.get("tools", "parse-pqdif", "src", "main", "resources", "pqdif-samples", PQDIF_FILE_NAME),
Paths.get("src", "main", "resources", "pqdif-samples", PQDIF_FILE_NAME),
defaultDesktopPath());
}
private static Path defaultDesktopPath() {
return Paths.get(System.getProperty("user.home"), "Desktop", PQDIF_FILE_NAME);
}
private static void printResponse(Object response) throws IOException {
ObjectMapper objectMapper = new ObjectMapper();
System.out.println(objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(response));
}
private static class PathMultipartFile implements MultipartFile {
private final String name;
private final Path path;
private PathMultipartFile(String name, Path path) {
this.name = name;
this.path = path;
}
@Override
public String getName() {
return name;
}
@Override
public String getOriginalFilename() {
return path.getFileName().toString();
}
@Override
public String getContentType() {
return "application/octet-stream";
}
@Override
public boolean isEmpty() {
try {
return Files.size(path) == 0;
} catch (IOException e) {
return true;
}
}
@Override
public long getSize() {
try {
return Files.size(path);
} catch (IOException e) {
return 0L;
}
}
@Override
public byte[] getBytes() throws IOException {
return Files.readAllBytes(path);
}
@Override
public InputStream getInputStream() throws IOException {
return Files.newInputStream(path);
}
@Override
public void transferTo(File dest) throws IOException {
Files.copy(path, dest.toPath(), StandardCopyOption.REPLACE_EXISTING);
}
}
}

View File

@@ -0,0 +1,28 @@
package com.njcn.gather.tool.parsepqdif.component;
import com.njcn.gather.tool.parsepqdif.pojo.vo.PqdifParseResponse;
import com.njcn.gather.tool.parsepqdif.pojo.vo.PqdifRecognizeResultVO;
import org.junit.jupiter.api.Test;
import java.util.ArrayList;
import static org.junit.jupiter.api.Assertions.assertEquals;
class PqdifSecondParseComponentTest {
@Test
void parseRecognizableDataFormatsStartTimeAsStandard24HourText() {
PqdifParseResponse parseResponse = new PqdifParseResponse();
parseResponse.setStatus("SUCCESS");
parseResponse.setObservations(new ArrayList<PqdifParseResponse.ObservationVO>());
PqdifParseResponse.ObservationVO observation = new PqdifParseResponse.ObservationVO();
observation.setTimeStartText("2026-06-22T15:04:05.123");
observation.setChannels(new ArrayList<PqdifParseResponse.ChannelInfoVO>());
parseResponse.getObservations().add(observation);
PqdifRecognizeResultVO result = new PqdifSecondParseComponent().parseRecognizableData(parseResponse);
assertEquals("2026-06-22 15:04:05", result.getObservations().get(0).getStartTime());
}
}