feat(icd): 完善ICD映射管理功能

- 在AuthGlobalFilter中添加稳态检验相关接口的免认证路径
- 修改CsDevTypeMapper.xml移除icdPath字段返回避免数据冗余
- 在CsIcdPathController中新增查询参照ICD列表和ICD校验详情接口
- 更新CsIcdPathMapper添加selectReferenceIcdPathList等方法实现
- 移除CsIcdPath相关实体和参数中的path字段简化数据结构
- 扩展ICD类型定义支持手动录入和上游解析的标准/非标准分类
- 重构激活标准ICD逻辑支持不同类型间的正确转换
- 新增ICD一致性校验排除规则跳过特定描述的DOI项检查
- 优化报告映射规则应用逻辑提升校验准确性
- 添加去除重复DOI项功能确保数据唯一性
This commit is contained in:
2026-06-18 16:33:40 +08:00
parent 7fb4c8e78a
commit 97b1334714
48 changed files with 2373 additions and 264 deletions

View File

@@ -20,6 +20,12 @@
<version>0.0.1</version>
</dependency>
<dependency>
<groupId>com.njcn</groupId>
<artifactId>mybatis-plus</artifactId>
<version>0.0.1</version>
</dependency>
<dependency>
<groupId>com.njcn</groupId>
<artifactId>spingboot2.3.12</artifactId>
@@ -74,6 +80,14 @@
<exclude>pqdif-samples/**</exclude>
</excludes>
</resource>
<!-- Mapper XML 按当前项目约定放在 Java 包路径下,需要显式复制到 classpath。 -->
<resource>
<directory>src/main/java</directory>
<includes>
<include>**/*.xml</include>
</includes>
</resource>
</resources>
<plugins>

View File

@@ -0,0 +1,162 @@
package com.njcn.gather.tool.parsepqdif.controller;
import com.fasterxml.jackson.databind.JsonNode;
import com.njcn.common.pojo.annotation.OperateInfo;
import com.njcn.common.pojo.enums.common.LogEnum;
import com.njcn.common.pojo.enums.response.CommonResponseEnum;
import com.njcn.common.pojo.response.HttpResult;
import com.njcn.common.utils.LogUtil;
import com.njcn.gather.tool.parsepqdif.pojo.param.CsPqdifPathParam;
import com.njcn.gather.tool.parsepqdif.pojo.param.PqdifParseResultSaveParam;
import com.njcn.gather.tool.parsepqdif.pojo.vo.CsPqdifPathDetailVO;
import com.njcn.gather.tool.parsepqdif.pojo.vo.CsPqdifPathVO;
import com.njcn.gather.tool.parsepqdif.service.CsPqdifPathService;
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.PathVariable;
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.RequestPart;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.util.List;
/**
* PQDIF 存储记录维护入口。
*/
@Slf4j
@Api(tags = "PQDIF存储记录管理")
@RestController
@RequestMapping("/api/parse-pqdif/pqdif-paths")
@RequiredArgsConstructor
public class CsPqdifPathController extends BaseController {
private final CsPqdifPathService csPqdifPathService;
@OperateInfo(info = LogEnum.BUSINESS_COMMON)
@ApiOperation("查询PQDIF存储记录列表")
@PostMapping("/list")
public HttpResult<List<CsPqdifPathVO>> list(@RequestBody(required = false) CsPqdifPathParam.ListParam param) {
String methodDescribe = getMethodDescribe("list");
LogUtil.njcnDebug(log, "{}开始查询PQDIF存储记录列表", methodDescribe);
List<CsPqdifPathVO> result = csPqdifPathService.listPqdifPaths(param);
return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, result, methodDescribe);
}
@OperateInfo(info = LogEnum.BUSINESS_COMMON)
@ApiOperation("新增PQDIF存储记录")
@PostMapping(value = "/add", consumes = {"application/json"})
public HttpResult<Boolean> add(@RequestBody @Validated CsPqdifPathParam param) {
String methodDescribe = getMethodDescribe("add");
LogUtil.njcnDebug(log, "{}开始新增PQDIF存储记录", methodDescribe);
boolean result = csPqdifPathService.addPqdifPath(param);
return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, result, methodDescribe);
}
@OperateInfo(info = LogEnum.BUSINESS_COMMON)
@ApiOperation("上传并新增PQDIF存储记录")
@PostMapping(value = "/add", consumes = {"multipart/form-data"})
public HttpResult<Boolean> addWithFile(@RequestPart("pqdifFile") MultipartFile pqdifFile,
@RequestPart("request") @Validated CsPqdifPathParam param) {
String methodDescribe = getMethodDescribe("addWithFile");
LogUtil.njcnDebug(log, "{}开始上传并新增PQDIF存储记录fileName={}", methodDescribe, resolveFileName(pqdifFile));
fillPqdifFile(param, pqdifFile);
boolean result = csPqdifPathService.addPqdifPath(param);
return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, result, methodDescribe);
}
@OperateInfo(info = LogEnum.BUSINESS_COMMON)
@ApiOperation("编辑PQDIF存储记录")
@PostMapping(value = "/update", consumes = {"application/json"})
public HttpResult<Boolean> update(@RequestBody @Validated CsPqdifPathParam.UpdateParam param) {
String methodDescribe = getMethodDescribe("update");
LogUtil.njcnDebug(log, "{}开始编辑PQDIF存储记录pqdifId={}", methodDescribe, param.getId());
boolean result = csPqdifPathService.updatePqdifPath(param);
return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, result, methodDescribe);
}
@OperateInfo(info = LogEnum.BUSINESS_COMMON)
@ApiOperation("上传并编辑PQDIF存储记录")
@PostMapping(value = "/update", consumes = {"multipart/form-data"})
public HttpResult<Boolean> updateWithFile(@RequestPart("pqdifFile") MultipartFile pqdifFile,
@RequestPart("request") @Validated CsPqdifPathParam.UpdateParam param) {
String methodDescribe = getMethodDescribe("updateWithFile");
LogUtil.njcnDebug(log, "{}开始上传并编辑PQDIF存储记录pqdifId={}fileName={}",
methodDescribe, param.getId(), resolveFileName(pqdifFile));
fillPqdifFile(param, pqdifFile);
boolean result = csPqdifPathService.updatePqdifPath(param);
return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, result, methodDescribe);
}
@OperateInfo(info = LogEnum.BUSINESS_COMMON)
@ApiOperation("删除PQDIF存储记录")
@PostMapping("/delete")
public HttpResult<Boolean> delete(@RequestBody List<String> ids) {
String methodDescribe = getMethodDescribe("delete");
LogUtil.njcnDebug(log, "{}开始删除PQDIF存储记录ids={}", methodDescribe, ids);
boolean result = csPqdifPathService.deletePqdifPath(ids);
return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, result, methodDescribe);
}
@OperateInfo(info = LogEnum.BUSINESS_COMMON)
@ApiOperation("查询PQDIF解析结果详情")
@ApiImplicitParam(name = "id", value = "PQDIF记录ID", required = true)
@PostMapping("/{id}/parse-msg")
public HttpResult<JsonNode> getPqdifParseMsg(@PathVariable("id") String id) {
String methodDescribe = getMethodDescribe("getPqdifParseMsg");
LogUtil.njcnDebug(log, "{}开始查询PQDIF解析结果详情pqdifId={}", methodDescribe, id);
JsonNode result = csPqdifPathService.getPqdifParseMsg(id);
return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, result, methodDescribe);
}
@OperateInfo(info = LogEnum.BUSINESS_COMMON)
@ApiOperation("查询PQDIF文件和解析结果详情")
@ApiImplicitParam(name = "id", value = "PQDIF记录ID", required = true)
@PostMapping("/{id}/parse-detail")
public HttpResult<CsPqdifPathDetailVO> getPqdifParseDetail(@PathVariable("id") String id) {
String methodDescribe = getMethodDescribe("getPqdifParseDetail");
LogUtil.njcnDebug(log, "{}开始查询PQDIF文件和解析结果详情pqdifId={}", methodDescribe, id);
CsPqdifPathDetailVO result = csPqdifPathService.getPqdifParseDetail(id);
return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, result, methodDescribe);
}
@OperateInfo(info = LogEnum.BUSINESS_COMMON)
@ApiOperation("保存PQDIF解析结果")
@ApiImplicitParam(name = "id", value = "PQDIF记录ID", required = true)
@PostMapping(value = "/{id}/parse-result", consumes = {"application/json"})
public HttpResult<Boolean> savePqdifParseResult(@PathVariable("id") String id,
@RequestBody PqdifParseResultSaveParam param) {
String methodDescribe = getMethodDescribe("savePqdifParseResult");
LogUtil.njcnDebug(log, "{}开始保存PQDIF解析结果pqdifId={}", methodDescribe, id);
boolean result = csPqdifPathService.savePqdifParseResult(id, param);
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 String resolveFileName(MultipartFile pqdifFile) {
if (pqdifFile == null || pqdifFile.getOriginalFilename() == null) {
return null;
}
String fileName = pqdifFile.getOriginalFilename().trim();
return fileName.isEmpty() ? null : fileName;
}
}

View File

@@ -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;
@@ -38,7 +38,7 @@ public class ParsePqdifController extends BaseController {
@PostMapping(value = "/parse", consumes = {"multipart/form-data"})
public HttpResult<PqdifParseResponse> parse(@RequestPart("pqdifFile") MultipartFile pqdifFile) {
String methodDescribe = getMethodDescribe("parse");
LogUtil.njcnDebug(log, "{}PQDIF解析预留入口fileName={}",
LogUtil.njcnDebug(log, "{}PQDIF解析入口fileName={}",
methodDescribe, pqdifFile == null ? null : pqdifFile.getOriginalFilename());
PqdifParseResponse result = parsePqdifService.parse(pqdifFile);
return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, result, methodDescribe);

View File

@@ -0,0 +1,19 @@
package com.njcn.gather.tool.parsepqdif.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.njcn.gather.tool.parsepqdif.pojo.po.CsPqdifPathPO;
import com.njcn.gather.tool.parsepqdif.pojo.vo.CsPqdifPathDetailVO;
import com.njcn.gather.tool.parsepqdif.pojo.vo.CsPqdifPathVO;
import org.apache.ibatis.annotations.Param;
import java.util.List;
public interface CsPqdifPathMapper extends BaseMapper<CsPqdifPathPO> {
List<CsPqdifPathVO> selectPqdifPathList(@Param("keyword") String keyword,
@Param("result") Integer result);
CsPqdifPathVO selectPqdifParseMsgById(@Param("id") String id);
CsPqdifPathDetailVO selectPqdifPathDetailById(@Param("id") String id);
}

View File

@@ -0,0 +1,71 @@
<?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.tool.parsepqdif.mapper.CsPqdifPathMapper">
<resultMap id="CsPqdifPathVOResultMap"
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="recordCount" property="recordCount"/>
<result column="observationCount" property="observationCount"/>
<result column="sampleValueCount" property="sampleValueCount"/>
<result column="state" property="state"/>
<result column="result" property="result"/>
<result column="msg" property="msg"
typeHandler="com.njcn.gather.tool.parsepqdif.typehandler.JsonNodeTypeHandler"/>
<result column="createBy" property="createBy"/>
<result column="createTime" property="createTime"/>
<result column="updateBy" property="updateBy"/>
<result column="updateTime" property="updateTime"/>
</resultMap>
<select id="selectPqdifPathList"
resultMap="CsPqdifPathVOResultMap">
SELECT
ID AS id,
Name AS name,
Native_Version AS nativeVersion,
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,
Update_Time AS updateTime
FROM cs_pqdif_path
WHERE State = 1
<if test="keyword != null and keyword != ''">
AND Name LIKE CONCAT('%', #{keyword}, '%')
</if>
<if test="result != null">
AND Result = #{result}
</if>
ORDER BY Update_Time DESC, Create_Time DESC
</select>
<select id="selectPqdifParseMsgById"
resultMap="CsPqdifPathVOResultMap">
SELECT Msg AS msg
FROM cs_pqdif_path
WHERE ID = #{id}
AND State = 1
</select>
<select id="selectPqdifPathDetailById"
resultType="com.njcn.gather.tool.parsepqdif.pojo.vo.CsPqdifPathDetailVO">
SELECT
ID AS id,
Name AS name,
Json_Str AS jsonStr,
Pqdif AS pqdifContent
FROM cs_pqdif_path
WHERE ID = #{id}
AND State = 1
</select>
</mapper>

View File

@@ -1,12 +1,17 @@
package com.njcn.gather.tool.parsepqdif.nativebridge;
import com.sun.jna.NativeLibrary;
import lombok.extern.slf4j.Slf4j;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.*;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.util.Locale;
@Slf4j
public final class PqdifNativeLibraryLoader {
private static final String RESOURCE_DLL = "/pqdif-native/win-x64/pqdifbasic.dll";
@@ -41,8 +46,8 @@ public final class PqdifNativeLibraryLoader {
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());
log.info("PQDIF native dir = {}", nativeDir.toAbsolutePath());
log.info("PQDIF native dll = {}", dllPath.toAbsolutePath());
preparedNativeDir = nativeDir;
prepared = true;
@@ -81,4 +86,4 @@ public final class PqdifNativeLibraryLoader {
System.setProperty(propertyName, nativePath + separator + oldValue);
}
}
}
}

View File

@@ -0,0 +1,50 @@
package com.njcn.gather.tool.parsepqdif.pojo.param;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import javax.validation.constraints.NotBlank;
/**
* PQDIF 存储记录保存参数。
*/
@Data
@ApiModel("PQDIF存储记录保存参数")
public class CsPqdifPathParam {
@ApiModelProperty("PQDIF名称")
@NotBlank(message = "PQDIF名称不能为空")
private String name;
@ApiModelProperty("PQDIF文件二进制内容")
private byte[] pqdifContent;
/**
* PQDIF 存储记录编辑参数。
*/
@Data
@EqualsAndHashCode(callSuper = true)
@ApiModel("PQDIF存储记录编辑参数")
public static class UpdateParam extends CsPqdifPathParam {
@ApiModelProperty("PQDIF记录ID")
@NotBlank(message = "PQDIF记录ID不能为空")
private String id;
}
/**
* PQDIF 存储记录列表查询参数。
*/
@Data
@ApiModel("PQDIF存储记录列表查询参数")
public static class ListParam {
@ApiModelProperty("关键字匹配PQDIF名称")
private String keyword;
@ApiModelProperty("解析结果1-成功0-失败")
private Integer result;
}
}

View File

@@ -0,0 +1,35 @@
package com.njcn.gather.tool.parsepqdif.pojo.param;
import com.fasterxml.jackson.databind.JsonNode;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
/**
* PQDIF 解析结果保存参数。
*/
@Data
@ApiModel("PQDIF解析结果保存参数")
public class PqdifParseResultSaveParam {
@ApiModelProperty("native解析库版本")
private String nativeVersion;
@ApiModelProperty("Record总数")
private Long recordCount;
@ApiModelProperty("Observation Record总数")
private Long observationCount;
@ApiModelProperty("每个Series返回的样例采样值数量")
private Integer sampleValueCount;
@ApiModelProperty("解析结果1-成功0-失败")
private Integer result;
@ApiModelProperty("解析提示、失败原因或解析结论JSON")
private JsonNode msg;
@ApiModelProperty("完整PQDIF解析结果JSON")
private String jsonStr;
}

View File

@@ -0,0 +1,66 @@
package com.njcn.gather.tool.parsepqdif.pojo.po;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.databind.JsonNode;
import com.njcn.gather.tool.parsepqdif.typehandler.JsonNodeTypeHandler;
import lombok.Data;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
* PQDIF 文件存储和解析结果记录。
*/
@Data
@TableName(value = "cs_pqdif_path", autoResultMap = true)
public class CsPqdifPathPO implements Serializable {
private static final long serialVersionUID = 1L;
@TableId("ID")
private String id;
@TableField("Name")
private String name;
@TableField("Pqdif")
private byte[] pqdifContent;
@TableField("Native_Version")
private String nativeVersion;
@TableField("Record_Count")
private Long recordCount;
@TableField("Observation_Count")
private Long observationCount;
@TableField("Sample_Value_Count")
private Integer sampleValueCount;
@TableField("Result")
private Integer result;
@TableField(value = "Msg", typeHandler = JsonNodeTypeHandler.class)
private JsonNode msg;
@TableField("Json_Str")
private String jsonStr;
@TableField("State")
private Integer state;
@TableField("Create_By")
private String createBy;
@TableField("Create_Time")
private LocalDateTime createTime;
@TableField("Update_By")
private String updateBy;
@TableField("Update_Time")
private LocalDateTime updateTime;
}

View File

@@ -0,0 +1,26 @@
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;
/**
* PQDIF 文件和解析结果详情。
*/
@Data
@ApiModel("PQDIF文件和解析结果详情")
public class CsPqdifPathDetailVO {
@ApiModelProperty("PQDIF记录ID")
private String id;
@ApiModelProperty("PQDIF名称")
private String name;
@ApiModelProperty("完整PQDIF解析结果JSON")
private String jsonStr;
@JsonIgnore
private byte[] pqdifContent;
}

View File

@@ -0,0 +1,55 @@
package com.njcn.gather.tool.parsepqdif.pojo.vo;
import com.fasterxml.jackson.databind.JsonNode;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.time.LocalDateTime;
/**
* PQDIF 存储记录列表项。
*/
@Data
@ApiModel("PQDIF存储记录列表项")
public class CsPqdifPathVO {
@ApiModelProperty("PQDIF记录ID")
private String id;
@ApiModelProperty("PQDIF名称")
private String name;
@ApiModelProperty("native解析库版本")
private String nativeVersion;
@ApiModelProperty("Record总数")
private Long recordCount;
@ApiModelProperty("Observation Record总数")
private Long observationCount;
@ApiModelProperty("每个Series返回的样例采样值数量")
private Integer sampleValueCount;
@ApiModelProperty("状态1-正常0-删除")
private Integer state;
@ApiModelProperty("解析结果1-成功0-失败")
private Integer result;
@ApiModelProperty("解析提示、失败原因或解析结论JSON")
private JsonNode msg;
@ApiModelProperty("创建人")
private String createBy;
@ApiModelProperty("创建时间")
private LocalDateTime createTime;
@ApiModelProperty("更新人")
private String updateBy;
@ApiModelProperty("更新时间")
private LocalDateTime updateTime;
}

View File

@@ -4,15 +4,20 @@ 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 org.springframework.stereotype.Component;
import java.nio.file.Path;
import java.time.LocalDateTime;
import java.util.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
@Component
public class PqdifNativeReader {
private static final String STATUS_SUCCESS = "SUCCESS";
private static final String DATA_SUCCESS = "DATA_SUCCESS";
private static final String DATA_FAILED = "DATA_FAILED";
private static final int DEFAULT_SAMPLE_VALUE_COUNT = 5;
public PqdifParseResponse read(Path pqdifPath, String fileName) {
@@ -40,10 +45,11 @@ public class PqdifNativeReader {
recordVO.setRecordIndex(recordIndex);
recordVO.setTypeGuid(recordInfo.typeGuid);
recordVO.setTypeName(recordInfo.typeName);
recordVO.setObservation(isObservation(recordInfo));
boolean observationRecord = isObservation(recordInfo);
recordVO.setObservation(observationRecord);
response.getRecords().add(recordVO);
if (!isObservation(recordInfo)) {
if (!observationRecord) {
continue;
}
@@ -120,7 +126,7 @@ public class PqdifNativeReader {
vo.setScale(seriesInfo.scale);
vo.setOffset(seriesInfo.offset);
} catch (Throwable e) {
vo.setDataStatus("DATA_FAILED");
vo.setDataStatus(DATA_FAILED);
vo.setDataMessage("getSeriesInfo failed, channel=" + channelIndex
+ ", series=" + seriesIndex
+ ", error=" + e.getMessage());
@@ -132,12 +138,12 @@ public class PqdifNativeReader {
try {
double[] values = observation.getSeriesData(channelIndex, seriesIndex);
vo.setDataStatus("DATA_SUCCESS");
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.setDataStatus(DATA_FAILED);
vo.setDataMessage("getSeriesData failed, channel=" + channelIndex
+ ", series=" + seriesIndex
+ ", seriesBaseType=" + vo.getSeriesBaseType()
@@ -183,4 +189,4 @@ public class PqdifNativeReader {
return dateTime.toString();
}
}
}

View File

@@ -0,0 +1,29 @@
package com.njcn.gather.tool.parsepqdif.service;
import com.fasterxml.jackson.databind.JsonNode;
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;
import com.njcn.gather.tool.parsepqdif.pojo.vo.CsPqdifPathVO;
import java.util.List;
/**
* PQDIF 存储记录服务。
*/
public interface CsPqdifPathService {
List<CsPqdifPathVO> listPqdifPaths(CsPqdifPathParam.ListParam param);
JsonNode getPqdifParseMsg(String pqdifId);
CsPqdifPathDetailVO getPqdifParseDetail(String pqdifId);
boolean addPqdifPath(CsPqdifPathParam param);
boolean updatePqdifPath(CsPqdifPathParam.UpdateParam param);
boolean deletePqdifPath(List<String> ids);
boolean savePqdifParseResult(String pqdifId, PqdifParseResultSaveParam param);
}

View File

@@ -0,0 +1,185 @@
package com.njcn.gather.tool.parsepqdif.service.impl;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.fasterxml.jackson.databind.JsonNode;
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;
import com.njcn.gather.tool.parsepqdif.pojo.po.CsPqdifPathPO;
import com.njcn.gather.tool.parsepqdif.pojo.vo.CsPqdifPathDetailVO;
import com.njcn.gather.tool.parsepqdif.pojo.vo.CsPqdifPathVO;
import com.njcn.gather.tool.parsepqdif.service.CsPqdifPathService;
import com.njcn.web.utils.RequestUtil;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.util.List;
import java.util.UUID;
/**
* PQDIF 存储记录服务实现。
*/
@Service
@RequiredArgsConstructor
public class CsPqdifPathServiceImpl implements CsPqdifPathService {
private static final int STATE_NORMAL = 1;
private static final int STATE_DELETED = 0;
private final CsPqdifPathMapper csPqdifPathMapper;
@Override
public List<CsPqdifPathVO> listPqdifPaths(CsPqdifPathParam.ListParam param) {
CsPqdifPathParam.ListParam checkedParam = param == null ? new CsPqdifPathParam.ListParam() : param;
return csPqdifPathMapper.selectPqdifPathList(
trimToNull(checkedParam.getKeyword()),
checkedParam.getResult());
}
@Override
public JsonNode getPqdifParseMsg(String pqdifId) {
String id = requireText(pqdifId, "PQDIF记录ID不能为空");
CsPqdifPathVO pqdifPath = csPqdifPathMapper.selectPqdifParseMsgById(id);
return pqdifPath == null ? null : pqdifPath.getMsg();
}
@Override
public CsPqdifPathDetailVO getPqdifParseDetail(String pqdifId) {
String id = requireText(pqdifId, "PQDIF记录ID不能为空");
return csPqdifPathMapper.selectPqdifPathDetailById(id);
}
@Override
@Transactional
public boolean addPqdifPath(CsPqdifPathParam param) {
CsPqdifPathParam checkedParam = requireParam(param);
LocalDateTime now = LocalDateTime.now();
CsPqdifPathPO pqdifPath = buildPqdifPath(checkedParam);
pqdifPath.setId(UUID.randomUUID().toString().replace("-", ""));
pqdifPath.setState(STATE_NORMAL);
pqdifPath.setCreateBy(currentUserId());
pqdifPath.setCreateTime(now);
pqdifPath.setUpdateBy(currentUserId());
pqdifPath.setUpdateTime(now);
return csPqdifPathMapper.insert(pqdifPath) > 0;
}
@Override
@Transactional
public boolean updatePqdifPath(CsPqdifPathParam.UpdateParam param) {
CsPqdifPathParam.UpdateParam checkedParam = requireUpdateParam(param);
requirePqdifPath(checkedParam.getId());
CsPqdifPathPO pqdifPath = buildPqdifPath(checkedParam);
pqdifPath.setId(checkedParam.getId());
pqdifPath.setUpdateBy(currentUserId());
pqdifPath.setUpdateTime(LocalDateTime.now());
return csPqdifPathMapper.updateById(pqdifPath) > 0;
}
@Override
@Transactional
public boolean deletePqdifPath(List<String> ids) {
if (ids == null || ids.isEmpty()) {
throw new IllegalArgumentException("PQDIF记录ID不能为空");
}
CsPqdifPathPO pqdifPath = new CsPqdifPathPO();
pqdifPath.setState(STATE_DELETED);
pqdifPath.setUpdateBy(currentUserId());
pqdifPath.setUpdateTime(LocalDateTime.now());
return csPqdifPathMapper.update(pqdifPath, new LambdaUpdateWrapper<CsPqdifPathPO>()
.in(CsPqdifPathPO::getId, ids)
.eq(CsPqdifPathPO::getState, STATE_NORMAL)) > 0;
}
@Override
@Transactional
public boolean savePqdifParseResult(String pqdifId, PqdifParseResultSaveParam param) {
if (param == null) {
throw new IllegalArgumentException("PQDIF解析结果不能为空");
}
CsPqdifPathPO pqdifPath = requirePqdifPath(pqdifId);
pqdifPath.setNativeVersion(trimToNull(param.getNativeVersion()));
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()));
pqdifPath.setUpdateBy(currentUserId());
pqdifPath.setUpdateTime(LocalDateTime.now());
return csPqdifPathMapper.updateById(pqdifPath) > 0;
}
private CsPqdifPathPO buildPqdifPath(CsPqdifPathParam param) {
CsPqdifPathPO pqdifPath = new CsPqdifPathPO();
pqdifPath.setName(requireText(param.getName(), "PQDIF名称不能为空"));
pqdifPath.setPqdifContent(param.getPqdifContent());
return pqdifPath;
}
private CsPqdifPathParam requireParam(CsPqdifPathParam param) {
if (param == null) {
throw new IllegalArgumentException("PQDIF记录参数不能为空");
}
return param;
}
private CsPqdifPathParam.UpdateParam requireUpdateParam(CsPqdifPathParam.UpdateParam param) {
if (param == null) {
throw new IllegalArgumentException("PQDIF记录参数不能为空");
}
requireText(param.getId(), "PQDIF记录ID不能为空");
return param;
}
private CsPqdifPathPO requirePqdifPath(String pqdifId) {
String id = requireText(pqdifId, "PQDIF记录ID不能为空");
CsPqdifPathPO pqdifPath = csPqdifPathMapper.selectById(id);
if (pqdifPath == null || !Integer.valueOf(STATE_NORMAL).equals(pqdifPath.getState())) {
throw new IllegalArgumentException("PQDIF记录不存在或已删除");
}
return pqdifPath;
}
private Integer normalizeResult(Integer result) {
if (result == null) {
throw new IllegalArgumentException("解析结果不能为空");
}
if (result != 0 && result != 1) {
throw new IllegalArgumentException("解析结果只能是0或1");
}
return result;
}
private String requireText(String value, String message) {
String text = trimToNull(value);
if (text == null) {
throw new IllegalArgumentException(message);
}
return text;
}
private String trimToNull(String value) {
if (value == null) {
return null;
}
String text = value.trim();
return text.isEmpty() ? null : text;
}
private boolean isBlank(String value) {
return trimToNull(value) == null;
}
private String currentUserId() {
try {
String userId = RequestUtil.getUserId();
return isBlank(userId) ? "未知用户" : userId;
} catch (Exception ex) {
return "未知用户";
}
}
}

View File

@@ -3,87 +3,115 @@ 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.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
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;
import java.util.ArrayList;
import java.util.Locale;
@Slf4j
@Service
@RequiredArgsConstructor
public class ParsePqdifServiceImpl implements ParsePqdifService {
private static final String STATUS_FAILED = "FAILED";
private static final String DEFAULT_SUFFIX = ".pqd";
private static final String PQDIF_SUFFIX = ".pqdif";
private static final String TEMP_DIR_NAME = "cn-tool-pqdif-upload";
private static final String EMPTY_FILE_MESSAGE = "PQDIF文件不能为空";
private static final String UNSUPPORTED_FILE_MESSAGE = "仅支持 .pqd 或 .pqdif 格式文件";
private static final String DEFAULT_FAILED_MESSAGE = "PQDIF解析失败";
private static final String UNKNOWN_FAILED_REASON = "请检查文件内容或原生解析库状态";
private final PqdifNativeReader pqdifNativeReader = new PqdifNativeReader();
private final PqdifNativeReader pqdifNativeReader;
@Override
public PqdifParseResponse parse(MultipartFile pqdifFile) {
if (pqdifFile == null || pqdifFile.isEmpty()) {
return failed(null, "PQDIF文件不能为空");
return failed(null, EMPTY_FILE_MESSAGE);
}
String originalFilename = pqdifFile.getOriginalFilename();
String suffix = getSupportedSuffix(originalFilename);
if (suffix == null) {
return failed(originalFilename, UNSUPPORTED_FILE_MESSAGE);
}
Path tempFile = null;
try {
tempFile = createTempPqdifFile(pqdifFile);
return pqdifNativeReader.read(tempFile, pqdifFile.getOriginalFilename());
tempFile = createTempPqdifFile(pqdifFile, suffix);
return pqdifNativeReader.read(tempFile, originalFilename);
} catch (Exception e) {
log.error("PQDIF解析失败fileName={}", pqdifFile.getOriginalFilename(), e);
return failed(pqdifFile.getOriginalFilename(), e.getMessage());
log.error("PQDIF解析失败fileName={}", originalFilename, e);
return failed(originalFilename, buildFailedMessage(e));
} finally {
if (tempFile != null) {
try {
Files.deleteIfExists(tempFile);
} catch (Exception e) {
log.warn("删除PQDIF临时文件失败path={}", tempFile, e);
}
}
deleteTempFile(tempFile);
}
}
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");
private Path createTempPqdifFile(MultipartFile pqdifFile, String suffix) throws Exception {
// 原生解析库只接收文件路径,因此上传内容需先落到系统临时目录。
Path uploadDir = Paths.get(System.getProperty("java.io.tmpdir"), TEMP_DIR_NAME);
Files.createDirectories(uploadDir);
String safeFileName = "parse-pqdif-" + System.currentTimeMillis() + "-" +
java.util.UUID.randomUUID().toString().replace("-", "") + suffix;
Path tempFile = uploadDir.resolve(safeFileName);
Path tempFile = Files.createTempFile(uploadDir, "parse-pqdif-", suffix);
try (InputStream inputStream = pqdifFile.getInputStream()) {
Files.copy(inputStream, tempFile, StandardCopyOption.REPLACE_EXISTING);
}
return tempFile;
}
private String getSuffix(String originalFilename) {
if (originalFilename == null) {
return ".pqd";
private void deleteTempFile(Path tempFile) {
if (tempFile == null) {
return;
}
try {
Files.deleteIfExists(tempFile);
} catch (Exception e) {
log.warn("删除PQDIF临时文件失败path={}", tempFile, e);
}
}
private String getSupportedSuffix(String originalFilename) {
if (originalFilename == null || originalFilename.trim().isEmpty()) {
return null;
}
int index = originalFilename.lastIndexOf('.');
if (index < 0 || index == originalFilename.length() - 1) {
return ".pqd";
return null;
}
return originalFilename.substring(index);
String suffix = originalFilename.substring(index).toLowerCase(Locale.ROOT);
if (DEFAULT_SUFFIX.equals(suffix) || PQDIF_SUFFIX.equals(suffix)) {
return suffix;
}
return null;
}
private String resolveErrorMessage(Exception e) {
String message = e.getMessage();
if (message == null || message.trim().isEmpty()) {
return UNKNOWN_FAILED_REASON;
}
return message;
}
private String buildFailedMessage(Exception e) {
return DEFAULT_FAILED_MESSAGE + "" + resolveErrorMessage(e);
}
private PqdifParseResponse failed(String fileName, String message) {
PqdifParseResponse response = new PqdifParseResponse();
response.setStatus(STATUS_FAILED);
response.setMessage(message == null ? "PQDIF解析失败" : message);
response.setMessage(message == null ? DEFAULT_FAILED_MESSAGE : message);
response.setFileName(fileName);
response.setRecordCount(0L);
response.setObservationCount(0L);
@@ -92,4 +120,4 @@ public class ParsePqdifServiceImpl implements ParsePqdifService {
response.setObservations(new ArrayList<PqdifParseResponse.ObservationVO>());
return response;
}
}
}

View File

@@ -0,0 +1,51 @@
package com.njcn.gather.tool.parsepqdif.typehandler;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.ibatis.type.BaseTypeHandler;
import org.apache.ibatis.type.JdbcType;
import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
/**
* 将数据库 JSON 文本映射为 Jackson JsonNode便于接口直接返回结构化解析结果。
*/
public class JsonNodeTypeHandler extends BaseTypeHandler<JsonNode> {
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
@Override
public void setNonNullParameter(PreparedStatement ps, int i, JsonNode parameter, JdbcType jdbcType)
throws SQLException {
ps.setString(i, parameter.toString());
}
@Override
public JsonNode getNullableResult(ResultSet rs, String columnName) throws SQLException {
return parse(rs.getString(columnName));
}
@Override
public JsonNode getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
return parse(rs.getString(columnIndex));
}
@Override
public JsonNode getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
return parse(cs.getString(columnIndex));
}
private JsonNode parse(String value) throws SQLException {
if (value == null || value.trim().isEmpty()) {
return null;
}
try {
return OBJECT_MAPPER.readTree(value);
} catch (Exception ex) {
throw new SQLException("解析JSON字段失败", ex);
}
}
}