diff --git a/tools/parse-pqdif/pom.xml b/tools/parse-pqdif/pom.xml
index 9a6d660..22e177e 100644
--- a/tools/parse-pqdif/pom.xml
+++ b/tools/parse-pqdif/pom.xml
@@ -35,10 +35,41 @@
org.springframework.boot
spring-boot-starter-validation
+
+
+ com.njcn
+ pqdif-native-basic-bridge
+ 1.0.0
+ system
+ ${project.basedir}/lib/pqdif-native-basic-bridge-1.0.0-jar-with-dependencies.jar
+
parse-pqdif
+
+
+
+
+ src/main/resources
+ false
+
+ pqdif-native/**
+ pqdif-samples/**
+
+
+
+
+
+ src/main/resources
+ false
+
+ pqdif-native/**
+ pqdif-samples/**
+
+
+
+
org.apache.maven.plugins
diff --git a/tools/parse-pqdif/src/main/java/com/njcn/gather/tool/parsepqdif/controller/ParsePqdifController.java b/tools/parse-pqdif/src/main/java/com/njcn/gather/tool/parsepqdif/controller/ParsePqdifController.java
index 58f7320..997b494 100644
--- a/tools/parse-pqdif/src/main/java/com/njcn/gather/tool/parsepqdif/controller/ParsePqdifController.java
+++ b/tools/parse-pqdif/src/main/java/com/njcn/gather/tool/parsepqdif/controller/ParsePqdifController.java
@@ -28,7 +28,7 @@ import org.springframework.web.multipart.MultipartFile;
@RestController
@RequestMapping("/api/parse-pqdif")
@RequiredArgsConstructor
-public class ParsePqdifController extends BaseController {
+public class ParsePqdifController extends BaseController {
private final ParsePqdifService parsePqdifService;
diff --git a/tools/parse-pqdif/src/main/java/com/njcn/gather/tool/parsepqdif/nativebridge/PqdifNativeLibraryLoader.java b/tools/parse-pqdif/src/main/java/com/njcn/gather/tool/parsepqdif/nativebridge/PqdifNativeLibraryLoader.java
new file mode 100644
index 0000000..a40bf95
--- /dev/null
+++ b/tools/parse-pqdif/src/main/java/com/njcn/gather/tool/parsepqdif/nativebridge/PqdifNativeLibraryLoader.java
@@ -0,0 +1,84 @@
+package com.njcn.gather.tool.parsepqdif.nativebridge;
+
+import com.sun.jna.NativeLibrary;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.*;
+import java.util.Locale;
+
+public final class PqdifNativeLibraryLoader {
+
+ private static final String RESOURCE_DLL = "/pqdif-native/win-x64/pqdifbasic.dll";
+ private static final String DLL_NAME = "pqdifbasic.dll";
+ private static final String TEMP_DIR_NAME = "cn-tool-pqdif-native";
+
+ private static volatile boolean prepared;
+ private static Path preparedNativeDir;
+
+ private PqdifNativeLibraryLoader() {
+ }
+
+ public static synchronized Path ensurePrepared() {
+ if (prepared) {
+ return preparedNativeDir;
+ }
+
+ if (!isWindows()) {
+ throw new IllegalStateException("当前接入的是 Windows x64 版 pqdifbasic.dll,非 Windows 环境无法加载");
+ }
+
+ try {
+ Path nativeDir = Paths.get(System.getProperty("java.io.tmpdir"), TEMP_DIR_NAME, "win-x64");
+ Files.createDirectories(nativeDir);
+
+ Path dllPath = nativeDir.resolve(DLL_NAME);
+ copyResourceDll(dllPath);
+
+ appendLibraryPath("jna.library.path", nativeDir);
+ appendLibraryPath("java.library.path", nativeDir);
+
+ NativeLibrary.addSearchPath("pqdifbasic", nativeDir.toAbsolutePath().toString());
+ NativeLibrary.addSearchPath("pqdifbasic.dll", nativeDir.toAbsolutePath().toString());
+
+ System.out.println("PQDIF native dir = " + nativeDir.toAbsolutePath());
+ System.out.println("PQDIF native dll = " + dllPath.toAbsolutePath());
+
+ preparedNativeDir = nativeDir;
+ prepared = true;
+ return nativeDir;
+ } catch (IOException e) {
+ throw new IllegalStateException("准备 pqdifbasic.dll 失败:" + e.getMessage(), e);
+ }
+ }
+
+ private static boolean isWindows() {
+ String osName = System.getProperty("os.name", "").toLowerCase(Locale.ROOT);
+ return osName.contains("win");
+ }
+
+ private static void copyResourceDll(Path dllPath) throws IOException {
+ try (InputStream inputStream = PqdifNativeLibraryLoader.class.getResourceAsStream(RESOURCE_DLL)) {
+ if (inputStream == null) {
+ throw new IOException("classpath 中找不到 " + RESOURCE_DLL);
+ }
+
+ Files.copy(inputStream, dllPath, StandardCopyOption.REPLACE_EXISTING);
+ }
+ }
+
+ private static void appendLibraryPath(String propertyName, Path nativeDir) {
+ String nativePath = nativeDir.toAbsolutePath().toString();
+ String oldValue = System.getProperty(propertyName);
+ String separator = System.getProperty("path.separator");
+
+ if (oldValue == null || oldValue.trim().isEmpty()) {
+ System.setProperty(propertyName, nativePath);
+ return;
+ }
+
+ if (!oldValue.toLowerCase(Locale.ROOT).contains(nativePath.toLowerCase(Locale.ROOT))) {
+ System.setProperty(propertyName, nativePath + separator + oldValue);
+ }
+ }
+}
\ No newline at end of file
diff --git a/tools/parse-pqdif/src/main/java/com/njcn/gather/tool/parsepqdif/pojo/vo/PqdifParseResponse.java b/tools/parse-pqdif/src/main/java/com/njcn/gather/tool/parsepqdif/pojo/vo/PqdifParseResponse.java
index 313f56a..40d047a 100644
--- a/tools/parse-pqdif/src/main/java/com/njcn/gather/tool/parsepqdif/pojo/vo/PqdifParseResponse.java
+++ b/tools/parse-pqdif/src/main/java/com/njcn/gather/tool/parsepqdif/pojo/vo/PqdifParseResponse.java
@@ -4,9 +4,8 @@ import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
-/**
- * PQDIF 解析占位响应。
- */
+import java.util.List;
+
@Data
@ApiModel("PQDIF解析响应")
public class PqdifParseResponse {
@@ -19,4 +18,75 @@ public class PqdifParseResponse {
@ApiModelProperty("文件名")
private String fileName;
-}
+
+ @ApiModelProperty("native 解析库版本")
+ private String nativeVersion;
+
+ @ApiModelProperty("Record 总数")
+ private Long recordCount;
+
+ @ApiModelProperty("Observation Record 总数")
+ private Long observationCount;
+
+ @ApiModelProperty("每个 Series 返回的样例值数量")
+ private Integer sampleValueCount;
+
+ @ApiModelProperty("Record 列表")
+ private List records;
+
+ @ApiModelProperty("Observation 列表")
+ private List observations;
+
+ @Data
+ public static class RecordInfoVO {
+ private Long recordIndex;
+ private String typeGuid;
+ private String typeName;
+ private Boolean observation;
+ }
+
+ @Data
+ public static class ObservationVO {
+ private Long recordIndex;
+ private String name;
+ private Double timeStartExcelDays;
+ private String timeStartText;
+ private Long channelCount;
+ private List channels;
+ }
+
+ @Data
+ public static class ChannelInfoVO {
+ private Long channelIndex;
+ private String name;
+ private Long seriesCount;
+ private Long phaseId;
+ private String quantityTypeGuid;
+ private Long quantityMeasuredId;
+ private List series;
+ }
+
+ @Data
+ public static class SeriesInfoVO {
+ private Long seriesIndex;
+ private Long quantityUnitsId;
+ private String quantityCharacteristicGuid;
+ private String valueTypeGuid;
+ private Long seriesBaseType;
+ private Double scale;
+ private Double offset;
+
+ /**
+ * DATA_SUCCESS / DATA_FAILED
+ */
+ private String dataStatus;
+
+ /**
+ * 数据读取失败原因
+ */
+ private String dataMessage;
+
+ private Integer valueCount;
+ private List firstValues;
+ }
+}
\ No newline at end of file
diff --git a/tools/parse-pqdif/src/main/java/com/njcn/gather/tool/parsepqdif/reader/PqdifNativeReader.java b/tools/parse-pqdif/src/main/java/com/njcn/gather/tool/parsepqdif/reader/PqdifNativeReader.java
new file mode 100644
index 0000000..58061d6
--- /dev/null
+++ b/tools/parse-pqdif/src/main/java/com/njcn/gather/tool/parsepqdif/reader/PqdifNativeReader.java
@@ -0,0 +1,186 @@
+package com.njcn.gather.tool.parsepqdif.reader;
+
+import com.njcn.gather.tool.parsepqdif.nativebridge.PqdifNativeLibraryLoader;
+import com.njcn.gather.tool.parsepqdif.pojo.vo.PqdifParseResponse;
+import com.njcn.pqdif.nativebridge.PqdifBasicNative;
+import com.njcn.pqdif.nativebridge.PqdifNativeSession;
+
+import java.nio.file.Path;
+import java.time.LocalDateTime;
+import java.util.*;
+import java.util.ArrayList;
+
+public class PqdifNativeReader {
+
+ private static final String STATUS_SUCCESS = "SUCCESS";
+ private static final int DEFAULT_SAMPLE_VALUE_COUNT = 5;
+
+ public PqdifParseResponse read(Path pqdifPath, String fileName) {
+ PqdifNativeLibraryLoader.ensurePrepared();
+
+ PqdifParseResponse response = new PqdifParseResponse();
+ response.setStatus(STATUS_SUCCESS);
+ response.setMessage("PQDIF解析完成");
+ response.setFileName(fileName);
+ response.setNativeVersion(PqdifBasicNative.INSTANCE.pqdif_basic_version());
+ response.setSampleValueCount(DEFAULT_SAMPLE_VALUE_COUNT);
+ response.setRecords(new ArrayList());
+ response.setObservations(new ArrayList());
+
+ try (PqdifNativeSession session = new PqdifNativeSession()) {
+ session.open(pqdifPath.toAbsolutePath().toString());
+
+ long recordCount = session.getRecordCount();
+ response.setRecordCount(recordCount);
+
+ for (long recordIndex = 0; recordIndex < recordCount; recordIndex++) {
+ PqdifNativeSession.RecordInfo recordInfo = session.getRecordInfo(recordIndex);
+
+ PqdifParseResponse.RecordInfoVO recordVO = new PqdifParseResponse.RecordInfoVO();
+ recordVO.setRecordIndex(recordIndex);
+ recordVO.setTypeGuid(recordInfo.typeGuid);
+ recordVO.setTypeName(recordInfo.typeName);
+ recordVO.setObservation(isObservation(recordInfo));
+ response.getRecords().add(recordVO);
+
+ if (!isObservation(recordInfo)) {
+ continue;
+ }
+
+ try (PqdifNativeSession.Observation observation = session.openObservation(recordIndex)) {
+ response.getObservations().add(readObservation(recordIndex, observation));
+ }
+ }
+ }
+
+ response.setObservationCount((long) response.getObservations().size());
+ return response;
+ }
+
+ private PqdifParseResponse.ObservationVO readObservation(
+ long recordIndex,
+ PqdifNativeSession.Observation observation) {
+
+ PqdifNativeSession.ObservationInfo info = observation.getInfo();
+
+ PqdifParseResponse.ObservationVO vo = new PqdifParseResponse.ObservationVO();
+ vo.setRecordIndex(recordIndex);
+ vo.setName(info.name);
+ vo.setTimeStartExcelDays(info.timeStartExcelDays);
+ vo.setTimeStartText(toExcelDateTimeText(info.timeStartExcelDays));
+ vo.setChannelCount(info.channelCount);
+ vo.setChannels(new ArrayList());
+
+ for (long channelIndex = 0; channelIndex < info.channelCount; channelIndex++) {
+ PqdifNativeSession.ChannelInfo channelInfo = observation.getChannelInfo(channelIndex);
+ vo.getChannels().add(readChannel(observation, channelIndex, channelInfo));
+ }
+
+ return vo;
+ }
+
+ private PqdifParseResponse.ChannelInfoVO readChannel(
+ PqdifNativeSession.Observation observation,
+ long channelIndex,
+ PqdifNativeSession.ChannelInfo channelInfo) {
+
+ PqdifParseResponse.ChannelInfoVO vo = new PqdifParseResponse.ChannelInfoVO();
+ vo.setChannelIndex(channelIndex);
+ vo.setName(channelInfo.name);
+ vo.setSeriesCount(channelInfo.seriesCount);
+ vo.setPhaseId(channelInfo.phaseId);
+ vo.setQuantityTypeGuid(channelInfo.quantityTypeGuid);
+ vo.setQuantityMeasuredId(channelInfo.quantityMeasuredId);
+ vo.setSeries(new ArrayList());
+
+ for (long seriesIndex = 0; seriesIndex < channelInfo.seriesCount; seriesIndex++) {
+ vo.getSeries().add(readSeries(observation, channelIndex, seriesIndex));
+ }
+
+ return vo;
+ }
+
+ private PqdifParseResponse.SeriesInfoVO readSeries(
+ PqdifNativeSession.Observation observation,
+ long channelIndex,
+ long seriesIndex) {
+
+ PqdifParseResponse.SeriesInfoVO vo = new PqdifParseResponse.SeriesInfoVO();
+ vo.setSeriesIndex(seriesIndex);
+
+ PqdifNativeSession.SeriesInfo seriesInfo = null;
+
+ try {
+ seriesInfo = observation.getSeriesInfo(channelIndex, seriesIndex);
+
+ vo.setQuantityUnitsId(seriesInfo.quantityUnitsId);
+ vo.setQuantityCharacteristicGuid(seriesInfo.quantityCharacteristicGuid);
+ vo.setValueTypeGuid(seriesInfo.valueTypeGuid);
+ vo.setSeriesBaseType(seriesInfo.seriesBaseType);
+ vo.setScale(seriesInfo.scale);
+ vo.setOffset(seriesInfo.offset);
+ } catch (Throwable e) {
+ vo.setDataStatus("DATA_FAILED");
+ vo.setDataMessage("getSeriesInfo failed, channel=" + channelIndex
+ + ", series=" + seriesIndex
+ + ", error=" + e.getMessage());
+ vo.setValueCount(0);
+ vo.setFirstValues(new ArrayList());
+ return vo;
+ }
+
+ try {
+ double[] values = observation.getSeriesData(channelIndex, seriesIndex);
+
+ vo.setDataStatus("DATA_SUCCESS");
+ vo.setDataMessage(null);
+ vo.setValueCount(values == null ? 0 : values.length);
+ vo.setFirstValues(firstValues(values, DEFAULT_SAMPLE_VALUE_COUNT));
+ } catch (Throwable e) {
+ vo.setDataStatus("DATA_FAILED");
+ vo.setDataMessage("getSeriesData failed, channel=" + channelIndex
+ + ", series=" + seriesIndex
+ + ", seriesBaseType=" + vo.getSeriesBaseType()
+ + ", valueTypeGuid=" + vo.getValueTypeGuid()
+ + ", characteristicGuid=" + vo.getQuantityCharacteristicGuid()
+ + ", error=" + e.getMessage());
+ vo.setValueCount(0);
+ vo.setFirstValues(new ArrayList());
+ }
+
+ return vo;
+ }
+
+ private static boolean isObservation(PqdifNativeSession.RecordInfo recordInfo) {
+ if (recordInfo == null || recordInfo.typeName == null) {
+ return false;
+ }
+ String type = recordInfo.typeName.toLowerCase(Locale.ROOT);
+ return type.contains("observation") || type.contains("tagrecobservation");
+ }
+
+ private static List firstValues(double[] values, int maxCount) {
+ if (values == null || values.length == 0) {
+ return new ArrayList();
+ }
+
+ int count = Math.min(values.length, maxCount);
+ List result = new ArrayList();
+ for (int i = 0; i < count; i++) {
+ result.add(values[i]);
+ }
+ return result;
+ }
+
+ private static String toExcelDateTimeText(double excelDays) {
+ long days = (long) Math.floor(excelDays);
+ double dayFraction = excelDays - days;
+ long nanos = Math.round(dayFraction * 24D * 60D * 60D * 1_000_000_000D);
+
+ LocalDateTime dateTime = LocalDateTime.of(1899, 12, 30, 0, 0)
+ .plusDays(days)
+ .plusNanos(nanos);
+
+ return dateTime.toString();
+ }
+}
\ No newline at end of file
diff --git a/tools/parse-pqdif/src/main/java/com/njcn/gather/tool/parsepqdif/service/impl/ParsePqdifServiceImpl.java b/tools/parse-pqdif/src/main/java/com/njcn/gather/tool/parsepqdif/service/impl/ParsePqdifServiceImpl.java
index 78b9fe4..3bd0d3d 100644
--- a/tools/parse-pqdif/src/main/java/com/njcn/gather/tool/parsepqdif/service/impl/ParsePqdifServiceImpl.java
+++ b/tools/parse-pqdif/src/main/java/com/njcn/gather/tool/parsepqdif/service/impl/ParsePqdifServiceImpl.java
@@ -1,24 +1,95 @@
package com.njcn.gather.tool.parsepqdif.service.impl;
import com.njcn.gather.tool.parsepqdif.pojo.vo.PqdifParseResponse;
+import com.njcn.gather.tool.parsepqdif.reader.PqdifNativeReader;
import com.njcn.gather.tool.parsepqdif.service.ParsePqdifService;
+import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
-/**
- * PQDIF 解析服务实现。
- */
+import java.io.InputStream;
+import java.nio.file.*;
+import java.util.ArrayList;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.nio.file.StandardCopyOption;
+
+@Slf4j
@Service
public class ParsePqdifServiceImpl implements ParsePqdifService {
- private static final String STATUS_NOT_SUPPORTED = "NOT_SUPPORTED";
+ private static final String STATUS_FAILED = "FAILED";
+
+ private final PqdifNativeReader pqdifNativeReader = new PqdifNativeReader();
@Override
public PqdifParseResponse parse(MultipartFile pqdifFile) {
+ if (pqdifFile == null || pqdifFile.isEmpty()) {
+ return failed(null, "PQDIF文件不能为空");
+ }
+
+ Path tempFile = null;
+
+ try {
+ tempFile = createTempPqdifFile(pqdifFile);
+ return pqdifNativeReader.read(tempFile, pqdifFile.getOriginalFilename());
+ } catch (Throwable e) {
+ log.error("PQDIF解析失败,fileName={}", pqdifFile.getOriginalFilename(), e);
+ return failed(pqdifFile.getOriginalFilename(), e.getMessage());
+ } finally {
+ if (tempFile != null) {
+ try {
+ Files.deleteIfExists(tempFile);
+ } catch (Exception e) {
+ log.warn("删除PQDIF临时文件失败,path={}", tempFile, e);
+ }
+ }
+ }
+ }
+
+ private Path createTempPqdifFile(MultipartFile pqdifFile) throws Exception {
+ String originalFilename = pqdifFile.getOriginalFilename();
+ String suffix = getSuffix(originalFilename);
+
+ Path uploadDir = Paths.get("D:", "CN_Tool_Runtime", "pqdif-upload");
+ Files.createDirectories(uploadDir);
+
+ String safeFileName = "parse-pqdif-" + System.currentTimeMillis() + "-" +
+ java.util.UUID.randomUUID().toString().replace("-", "") + suffix;
+
+ Path tempFile = uploadDir.resolve(safeFileName);
+
+ try (InputStream inputStream = pqdifFile.getInputStream()) {
+ Files.copy(inputStream, tempFile, StandardCopyOption.REPLACE_EXISTING);
+ }
+
+ return tempFile;
+ }
+
+ private String getSuffix(String originalFilename) {
+ if (originalFilename == null) {
+ return ".pqd";
+ }
+
+ int index = originalFilename.lastIndexOf('.');
+ if (index < 0 || index == originalFilename.length() - 1) {
+ return ".pqd";
+ }
+
+ return originalFilename.substring(index);
+ }
+
+ private PqdifParseResponse failed(String fileName, String message) {
PqdifParseResponse response = new PqdifParseResponse();
- response.setStatus(STATUS_NOT_SUPPORTED);
- response.setMessage("PQDIF解析功能待实现");
- response.setFileName(pqdifFile == null ? null : pqdifFile.getOriginalFilename());
+ response.setStatus(STATUS_FAILED);
+ response.setMessage(message == null ? "PQDIF解析失败" : message);
+ response.setFileName(fileName);
+ response.setRecordCount(0L);
+ response.setObservationCount(0L);
+ response.setSampleValueCount(0);
+ response.setRecords(new ArrayList());
+ response.setObservations(new ArrayList());
return response;
}
-}
+}
\ No newline at end of file