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

@@ -30,7 +30,7 @@
d.name AS name,
d.icd AS icdId,
p.Name AS icdName,
p.Path AS icdPath,
NULL AS icdPath,
p.Result AS icdResult,
p.Msg AS icdMsg,
d.power AS power,

View File

@@ -20,6 +20,7 @@ import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
@@ -37,6 +38,9 @@ public class IcdConsistencyCheckService {
private static final String ISSUE_FILE_NAME = "icd-consistency-issues.json";
private static final List<String> REQUIRED_REPORT_DESCS = Arrays.asList("统计数据", "波动闪变", "实时数据", "暂态事件");
private static final List<String> REQUIRED_LN_CLASSES = Arrays.asList("MMXU", "MSQI", "MHAI", "MFLK");
private static final Set<String> EXCLUDED_DOI_DESCS = new HashSet<String>(Arrays.asList(
"电压扰动事件启动", "电压暂降事件启动", "电压暂升事件启动", "电压中断事件启动",
"电压暂降启动定值", "电压暂升启动定值", "电压中断启动定值"));
private final FileStorageService fileStorageService;
private final ObjectMapper objectMapper = buildMapper();
@@ -45,14 +49,13 @@ public class IcdConsistencyCheckService {
if (request == null) {
throw new IllegalArgumentException("ICD 一致性校验请求不能为空");
}
log.info("ICD一致性校验标准ICD JSON={}", request.getStandardJson());
log.info("ICD一致性校验待校验ICD JSON={}", request.getCheckedJson());
MappingDocument checked = parseMapping(request.getCheckedJson(), "待校验 JSON");
MappingDocument standard = parseMapping(request.getStandardJson(), "标准 JSON");
List<IcdConsistencyIssue> issues = new ArrayList<IcdConsistencyIssue>();
validateSelfFormat(checked, "待校验映射", issues);
boolean corrected = applySelfMappingRules(checked, issues);
boolean corrected = applySelfMappingRules(checked, standard);
corrected = removeDuplicateDescDoiItems(checked, standard, issues) || corrected;
validateConsistency(checked, standard, issues);
IcdConsistencyCheckResponse response = new IcdConsistencyCheckResponse();
@@ -172,6 +175,9 @@ public class IcdConsistencyCheckService {
continue;
}
for (DoiItem doi : inst.getDoiList()) {
if (isExcludedDoiDesc(doi.getDesc())) {
continue;
}
if (isEmpty(doi.getSdiList())) {
addIssue(issues, "自身格式校验", instPath + ".doiList[" + buildKey(doi.getName(), doi.getDesc()) + "]",
"typeList 不能为空:" + joinDesc(group.getDesc(), inst.getDesc(), doi.getDesc()), null, null, false);
@@ -197,24 +203,28 @@ public class IcdConsistencyCheckService {
validateDataSetConsistency(checked, standard, issues);
}
private boolean applySelfMappingRules(MappingDocument checked, List<IcdConsistencyIssue> issues) {
private boolean applySelfMappingRules(MappingDocument checked, MappingDocument standard) {
if (checked.getReportMap() == null) {
return false;
}
Map<String, ReportMapItem> standardMap = indexReportMap(standard);
boolean hasRtFre = false;
boolean corrected = false;
for (ReportMapItem item : checked.getReportMap()) {
if ("实时数据".equals(trimToEmpty(item.getDesc())) && trimToEmpty(item.getRptId()).contains("RtFre")) {
if ("实时数据".equals(trimToEmpty(item.getDesc())) && containsRtFre(item.getRptId())) {
hasRtFre = true;
ReportMapItem standardItem = standardMap.get(buildReportKey(item));
boolean needRuleCorrection = standardItem == null
|| !equalsValue(String.valueOf(standardItem.getReportCount()), String.valueOf(item.getReportCount()))
|| !equalsValue(standardItem.getFlickerFlag(), item.getFlickerFlag());
if (!needRuleCorrection) {
continue;
}
if (!"1".equals(trimToEmpty(item.getFlickerFlag()))) {
addIssue(issues, "映射规则", "ReportMap[" + buildReportKey(item) + "].FlickerFlag",
"实时数据报告 rptID 包含 RtFreFlickerFlag 已按规则调整为 1", "1", item.getFlickerFlag(), true);
item.setFlickerFlag("1");
corrected = true;
}
if (item.getReportCount() != 0) {
addIssue(issues, "映射规则", "ReportMap[" + buildReportKey(item) + "].reportCount",
"实时数据报告 rptID 包含 RtFrereportCount 已按规则调整为 0", "0", String.valueOf(item.getReportCount()), true);
item.setReportCount(0);
corrected = true;
}
@@ -225,11 +235,12 @@ public class IcdConsistencyCheckService {
}
for (ReportMapItem item : checked.getReportMap()) {
if ("统计数据".equals(trimToEmpty(item.getDesc()))) {
ReportMapItem standardItem = standardMap.get(buildReportKey(item));
if (standardItem != null && equalsValue(String.valueOf(standardItem.getReportCount()), String.valueOf(item.getReportCount()))) {
continue;
}
int adjustedCount = item.getReportCount() - 1;
if (item.getReportCount() != adjustedCount) {
addIssue(issues, "映射规则", "ReportMap[" + buildReportKey(item) + "].reportCount",
"存在 RtFre 实时数据报告,统计数据 reportCount 已按规则减 1",
String.valueOf(adjustedCount), String.valueOf(item.getReportCount()), true);
item.setReportCount(adjustedCount);
corrected = true;
}
@@ -238,6 +249,47 @@ public class IcdConsistencyCheckService {
return corrected;
}
private boolean removeDuplicateDescDoiItems(MappingDocument checked, MappingDocument standard, List<IcdConsistencyIssue> issues) {
if (checked.getDataSetList() == null) {
return false;
}
Map<String, Set<String>> standardDoiKeys = indexStandardDoiKeysByInst(standard);
boolean corrected = false;
for (DataSetGroupItem group : checked.getDataSetList()) {
if (group.getInstList() == null) {
continue;
}
for (InstItem inst : group.getInstList()) {
if (inst.getDoiList() == null) {
continue;
}
Map<String, List<DoiItem>> doiItemsByDesc = new HashMap<String, List<DoiItem>>();
for (DoiItem doi : inst.getDoiList()) {
String desc = trimToEmpty(doi.getDesc());
if (!doiItemsByDesc.containsKey(desc)) {
doiItemsByDesc.put(desc, new ArrayList<DoiItem>());
}
doiItemsByDesc.get(desc).add(doi);
}
String instPath = buildInstPath(group, inst);
Set<String> currentStandardKeys = standardDoiKeys.get(buildInstKey(group, inst));
for (Map.Entry<String, List<DoiItem>> entry : doiItemsByDesc.entrySet()) {
if (entry.getValue().size() <= 1) {
continue;
}
DoiItem retained = chooseRetainedDoi(entry.getValue(), currentStandardKeys);
String message = "同一个 doiList 下存在 desc 相同的指标,已仅保留 " + buildKey(retained.getDesc(), retained.getName())
+ ";重复组合:" + describeDoiStandardMatches(entry.getValue(), currentStandardKeys);
addIssue(issues, "映射规则", instPath + ".doiList[desc=" + entry.getKey() + "]", message,
null, describeDoiKeys(entry.getValue()), true);
removeDuplicatedDoiItems(inst.getDoiList(), entry.getValue(), retained);
corrected = true;
}
}
}
return corrected;
}
private void validateReportMapConsistency(MappingDocument checked, MappingDocument standard, List<IcdConsistencyIssue> issues) {
Map<String, ReportMapItem> checkedMap = new HashMap<String, ReportMapItem>();
if (checked.getReportMap() != null) {
@@ -303,6 +355,9 @@ public class IcdConsistencyCheckService {
return;
}
for (DoiItem standardDoi : standardInst.getDoiList()) {
if (isExcludedDoiDesc(standardDoi.getDesc())) {
continue;
}
String doiKey = buildKey(standardDoi.getName(), standardDoi.getDesc());
DoiItem checkedDoi = checkedDoiMap.get(doiKey);
String path = "DataSetList[" + groupKey + "].instList[" + instKey + "].doiList[" + doiKey + "]";
@@ -343,6 +398,17 @@ public class IcdConsistencyCheckService {
issues.add(issue);
}
private Map<String, ReportMapItem> indexReportMap(MappingDocument document) {
Map<String, ReportMapItem> result = new HashMap<String, ReportMapItem>();
if (document.getReportMap() == null) {
return result;
}
for (ReportMapItem item : document.getReportMap()) {
result.put(buildReportKey(item), item);
}
return result;
}
private Map<String, DataSetGroupItem> indexGroups(MappingDocument document) {
Map<String, DataSetGroupItem> result = new HashMap<String, DataSetGroupItem>();
if (document.getDataSetList() == null) {
@@ -376,10 +442,36 @@ public class IcdConsistencyCheckService {
return result;
}
private Map<String, Set<String>> indexStandardDoiKeysByInst(MappingDocument standard) {
Map<String, Set<String>> result = new HashMap<String, Set<String>>();
if (standard.getDataSetList() == null) {
return result;
}
for (DataSetGroupItem group : standard.getDataSetList()) {
if (group.getInstList() == null) {
continue;
}
for (InstItem inst : group.getInstList()) {
Set<String> doiKeys = new HashSet<String>();
if (inst.getDoiList() != null) {
for (DoiItem doi : inst.getDoiList()) {
doiKeys.add(buildKey(doi.getDesc(), doi.getName()));
}
}
result.put(buildInstKey(group, inst), doiKeys);
}
}
return result;
}
private String buildReportKey(ReportMapItem item) {
return buildKey(item.getDesc(), item.getRptId(), item.getName());
}
private String buildInstKey(DataSetGroupItem group, InstItem inst) {
return buildKey(group.getLnClass(), group.getDesc(), inst.getInst(), inst.getDesc());
}
private String buildGroupPath(DataSetGroupItem group) {
return "DataSetList[" + buildKey(group.getLnClass(), group.getDesc()) + "]";
}
@@ -396,10 +488,57 @@ public class IcdConsistencyCheckService {
return String.join("+", parts);
}
private DoiItem chooseRetainedDoi(List<DoiItem> doiItems, Set<String> standardDoiKeys) {
if (standardDoiKeys != null) {
for (DoiItem item : doiItems) {
if (standardDoiKeys.contains(buildKey(item.getDesc(), item.getName()))) {
return item;
}
}
}
return doiItems.get(0);
}
private void removeDuplicatedDoiItems(List<DoiItem> allDoiItems, List<DoiItem> duplicatedItems, DoiItem retained) {
Iterator<DoiItem> iterator = allDoiItems.iterator();
while (iterator.hasNext()) {
DoiItem item = iterator.next();
if (duplicatedItems.contains(item) && item != retained) {
iterator.remove();
}
}
}
private String describeDoiStandardMatches(List<DoiItem> doiItems, Set<String> standardDoiKeys) {
List<String> values = new ArrayList<String>();
for (DoiItem item : doiItems) {
String key = buildKey(item.getDesc(), item.getName());
boolean existsInStandard = standardDoiKeys != null && standardDoiKeys.contains(key);
values.add(key + (existsInStandard ? " 在标准映射中存在" : " 不在标准映射中"));
}
return String.join("", values);
}
private String describeDoiKeys(List<DoiItem> doiItems) {
List<String> values = new ArrayList<String>();
for (DoiItem item : doiItems) {
values.add(buildKey(item.getDesc(), item.getName()));
}
return String.join("", values);
}
private boolean equalsValue(String left, String right) {
return trimToEmpty(left).equals(trimToEmpty(right));
}
private boolean containsRtFre(String value) {
return trimToEmpty(value).toLowerCase().contains("rtfre");
}
private boolean isExcludedDoiDesc(String desc) {
return EXCLUDED_DOI_DESCS.contains(trimToEmpty(desc));
}
private boolean isBlank(String value) {
return value == null || value.trim().isEmpty();
}

View File

@@ -5,8 +5,10 @@ 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.fasterxml.jackson.databind.JsonNode;
import com.njcn.gather.icd.mapping.pojo.param.CsIcdPathParam;
import com.njcn.gather.icd.mapping.pojo.param.IcdCheckResultSaveParam;
import com.njcn.gather.icd.mapping.pojo.vo.CsIcdPathDetailVO;
import com.njcn.gather.icd.mapping.pojo.vo.CsIcdPathVO;
import com.njcn.gather.icd.mapping.service.CsIcdPathService;
import com.njcn.web.controller.BaseController;
@@ -50,6 +52,16 @@ public class CsIcdPathController extends BaseController {
return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, result, methodDescribe);
}
@OperateInfo(info = LogEnum.BUSINESS_COMMON)
@ApiOperation("查询参照ICD列表")
@PostMapping("/reference-list")
public HttpResult<List<CsIcdPathVO>> referenceList() {
String methodDescribe = getMethodDescribe("referenceList");
LogUtil.njcnDebug(log, "{}开始查询参照ICD列表", methodDescribe);
List<CsIcdPathVO> result = csIcdPathService.listReferenceIcdPaths();
return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, result, methodDescribe);
}
@OperateInfo(info = LogEnum.BUSINESS_COMMON)
@ApiOperation("新增ICD存储记录")
@PostMapping(value = "/add", consumes = {"application/json"})
@@ -115,6 +127,28 @@ public class CsIcdPathController extends BaseController {
return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, result, methodDescribe);
}
@OperateInfo(info = LogEnum.BUSINESS_COMMON)
@ApiOperation("查询ICD校验结果详情")
@ApiImplicitParam(name = "id", value = "ICD记录ID", required = true)
@PostMapping("/{id}/icd-check-msg")
public HttpResult<JsonNode> getIcdCheckMsg(@PathVariable("id") String id) {
String methodDescribe = getMethodDescribe("getIcdCheckMsg");
LogUtil.njcnDebug(log, "{}开始查询ICD校验结果详情icdId={}", methodDescribe, id);
JsonNode result = csIcdPathService.getIcdCheckMsg(id);
return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, result, methodDescribe);
}
@OperateInfo(info = LogEnum.BUSINESS_COMMON)
@ApiOperation("查询ICD映射文件详情")
@ApiImplicitParam(name = "id", value = "ICD记录ID", required = true)
@PostMapping("/{id}/mapping-detail")
public HttpResult<CsIcdPathDetailVO> getMappingDetail(@PathVariable("id") String id) {
String methodDescribe = getMethodDescribe("getMappingDetail");
LogUtil.njcnDebug(log, "{}开始查询ICD映射文件详情icdId={}", methodDescribe, id);
CsIcdPathDetailVO result = csIcdPathService.getMappingDetail(id);
return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, result, methodDescribe);
}
@OperateInfo(info = LogEnum.BUSINESS_COMMON)
@ApiOperation("保存ICD唯一性校验结果")
@ApiImplicitParam(name = "id", value = "ICD记录ID", required = true)
@@ -147,9 +181,6 @@ public class CsIcdPathController extends BaseController {
}
try {
param.setIcdContent(icdFile.getBytes());
if (param.getPath() == null || param.getPath().trim().isEmpty()) {
param.setPath(resolveFileName(icdFile));
}
} catch (IOException ex) {
throw new IllegalArgumentException("读取ICD文件失败" + ex.getMessage(), ex);
}

View File

@@ -2,6 +2,7 @@ package com.njcn.gather.icd.mapping.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.njcn.gather.icd.mapping.pojo.po.CsIcdPathPO;
import com.njcn.gather.icd.mapping.pojo.vo.CsIcdPathDetailVO;
import com.njcn.gather.icd.mapping.pojo.vo.CsIcdPathVO;
import org.apache.ibatis.annotations.Param;
@@ -15,4 +16,10 @@ public interface CsIcdPathMapper extends BaseMapper<CsIcdPathPO> {
List<CsIcdPathVO> selectIcdPathList(@Param("keyword") String keyword,
@Param("type") Integer type,
@Param("result") Integer result);
List<CsIcdPathVO> selectReferenceIcdPathList();
CsIcdPathVO selectIcdCheckMsgById(@Param("id") String id);
CsIcdPathDetailVO selectIcdPathDetailById(@Param("id") String id);
}

View File

@@ -7,12 +7,9 @@
type="com.njcn.gather.icd.mapping.pojo.vo.CsIcdPathVO">
<id column="id" property="id"/>
<result column="name" property="name"/>
<result column="path" property="path"/>
<result column="angle" property="angle"/>
<result column="usePhaseIndex" property="usePhaseIndex"/>
<result column="state" property="state"/>
<result column="jsonStr" property="jsonStr"/>
<result column="xmlStr" property="xmlStr"/>
<result column="result" property="result"/>
<result column="msg" property="msg"
typeHandler="com.njcn.gather.icd.mapping.typehandler.JsonNodeTypeHandler"/>
@@ -29,12 +26,9 @@
SELECT
ID AS id,
Name AS name,
Path AS path,
Angle AS angle,
Use_Phase_Index AS usePhaseIndex,
State AS state,
Json_Str AS jsonStr,
Xml_Str AS xmlStr,
Result AS result,
Msg AS msg,
Type AS type,
@@ -46,8 +40,7 @@
FROM cs_icd_path
WHERE State = 1
<if test="keyword != null and keyword != ''">
AND (Name LIKE CONCAT('%', #{keyword}, '%')
OR Path LIKE CONCAT('%', #{keyword}, '%'))
AND Name LIKE CONCAT('%', #{keyword}, '%')
</if>
<if test="type != null">
AND Type = #{type}
@@ -58,4 +51,47 @@
ORDER BY Update_Time DESC, Create_Time DESC
</select>
<select id="selectReferenceIcdPathList"
resultMap="CsIcdPathVOResultMap">
SELECT
ID AS id,
Name AS name,
Angle AS angle,
Use_Phase_Index AS usePhaseIndex,
State AS state,
Result AS result,
Msg AS msg,
Type AS type,
Reference_Icd_Id AS referenceIcdId,
Create_By AS createBy,
Create_Time AS createTime,
Update_By AS updateBy,
Update_Time AS updateTime
FROM cs_icd_path
WHERE State = 1
AND Type IN (1, 3)
ORDER BY Update_Time DESC, Create_Time DESC
</select>
<select id="selectIcdCheckMsgById"
resultMap="CsIcdPathVOResultMap">
SELECT Msg AS msg
FROM cs_icd_path
WHERE ID = #{id}
AND State = 1
</select>
<select id="selectIcdPathDetailById"
resultType="com.njcn.gather.icd.mapping.pojo.vo.CsIcdPathDetailVO">
SELECT
ID AS id,
Name AS name,
Json_Str AS jsonStr,
Xml_Str AS xmlStr,
Icd AS icdContent
FROM cs_icd_path
WHERE ID = #{id}
AND State = 1
</select>
</mapper>

View File

@@ -18,9 +18,6 @@ public class CsIcdPathParam {
@NotBlank(message = "ICD名称不能为空")
private String name;
@ApiModelProperty("ICD存储路径")
private String path;
@ApiModelProperty("ICD文件二进制内容")
private byte[] icdContent;
@@ -30,7 +27,7 @@ public class CsIcdPathParam {
@ApiModelProperty("是否使用相位索引")
private Integer usePhaseIndex;
@ApiModelProperty("ICD类型1-标准ICD")
@ApiModelProperty("ICD类型1-手动录入的标准ICD2-手动录入的非标准ICD3-上游解析传递的标准ICD4-上游解析传递的非标准ICD")
private Integer type;
/**
@@ -53,10 +50,10 @@ public class CsIcdPathParam {
@ApiModel("ICD存储记录列表查询参数")
public static class ListParam {
@ApiModelProperty("关键字匹配ICD名称或路径")
@ApiModelProperty("关键字匹配ICD名称")
private String keyword;
@ApiModelProperty("ICD类型")
@ApiModelProperty("ICD类型1-手动录入的标准ICD2-手动录入的非标准ICD3-上游解析传递的标准ICD4-上游解析传递的非标准ICD")
private Integer type;
@ApiModelProperty("ICD校验结果0-否1-是")

View File

@@ -25,9 +25,6 @@ public class CsIcdPathPO implements Serializable {
@TableField("Name")
private String name;
@TableField("Path")
private String path;
@TableField("Icd")
private byte[] icdContent;

View File

@@ -0,0 +1,32 @@
package com.njcn.gather.icd.mapping.pojo.vo;
import com.fasterxml.jackson.annotation.JsonIgnore;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
/**
* ICD 映射文件详情。
*/
@Data
@ApiModel("ICD映射文件详情")
public class CsIcdPathDetailVO {
@ApiModelProperty("ICD记录ID")
private String id;
@ApiModelProperty("ICD名称")
private String name;
@ApiModelProperty("MMS映射JSON")
private String jsonStr;
@ApiModelProperty("MMS映射XML")
private String xmlStr;
@ApiModelProperty("ICD源文件文本")
private String icdText;
@JsonIgnore
private byte[] icdContent;
}

View File

@@ -20,9 +20,6 @@ public class CsIcdPathVO {
@ApiModelProperty("ICD名称")
private String name;
@ApiModelProperty("ICD存储路径")
private String path;
@ApiModelProperty("角度")
private Integer angle;
@@ -32,19 +29,13 @@ public class CsIcdPathVO {
@ApiModelProperty("状态1-正常0-删除")
private Integer state;
@ApiModelProperty("MMS映射JSON")
private String jsonStr;
@ApiModelProperty("MMS映射XML")
private String xmlStr;
@ApiModelProperty("校验结论0-否1-是")
private Integer result;
@ApiModelProperty("校验结论详情JSON")
private JsonNode msg;
@ApiModelProperty("ICD类型1-标准ICD")
@ApiModelProperty("ICD类型1-手动录入的标准ICD2-手动录入的非标准ICD3-上游解析传递的标准ICD4-上游解析传递的非标准ICD")
private Integer type;
@ApiModelProperty("标准ICD引用ID")

View File

@@ -1,7 +1,9 @@
package com.njcn.gather.icd.mapping.service;
import com.fasterxml.jackson.databind.JsonNode;
import com.njcn.gather.icd.mapping.pojo.param.CsIcdPathParam;
import com.njcn.gather.icd.mapping.pojo.param.IcdCheckResultSaveParam;
import com.njcn.gather.icd.mapping.pojo.vo.CsIcdPathDetailVO;
import com.njcn.gather.icd.mapping.pojo.vo.CsIcdPathVO;
import java.util.List;
@@ -13,6 +15,12 @@ public interface CsIcdPathService {
List<CsIcdPathVO> listIcdPaths(CsIcdPathParam.ListParam param);
List<CsIcdPathVO> listReferenceIcdPaths();
JsonNode getIcdCheckMsg(String icdId);
CsIcdPathDetailVO getMappingDetail(String icdId);
boolean addIcdPath(CsIcdPathParam param);
boolean updateIcdPath(CsIcdPathParam.UpdateParam param);

View File

@@ -1,13 +1,14 @@
package com.njcn.gather.icd.mapping.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.njcn.gather.icd.mapping.mapper.CsIcdPathMapper;
import com.njcn.gather.icd.mapping.pojo.param.CsIcdPathParam;
import com.njcn.gather.icd.mapping.pojo.param.IcdCheckResultSaveParam;
import com.njcn.gather.icd.mapping.pojo.po.CsIcdPathPO;
import com.njcn.gather.icd.mapping.pojo.vo.CsIcdPathDetailVO;
import com.njcn.gather.icd.mapping.pojo.vo.CsIcdPathVO;
import com.njcn.gather.icd.mapping.service.CsIcdPathService;
import com.njcn.web.utils.RequestUtil;
@@ -15,6 +16,7 @@ import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime;
import java.util.List;
import java.util.UUID;
@@ -30,7 +32,13 @@ public class CsIcdPathServiceImpl implements CsIcdPathService {
private static final int STATE_DELETED = 0;
private static final int ICD_TYPE_STANDARD = 1;
private static final int ICD_TYPE_MANUAL_STANDARD = 1;
private static final int ICD_TYPE_MANUAL_NON_STANDARD = 2;
private static final int ICD_TYPE_UPSTREAM_STANDARD = 3;
private static final int ICD_TYPE_UPSTREAM_NON_STANDARD = 4;
private final CsIcdPathMapper csIcdPathMapper;
@@ -45,12 +53,38 @@ public class CsIcdPathServiceImpl implements CsIcdPathService {
checkedParam.getResult());
}
@Override
public List<CsIcdPathVO> listReferenceIcdPaths() {
return csIcdPathMapper.selectReferenceIcdPathList();
}
@Override
public JsonNode getIcdCheckMsg(String icdId) {
String id = requireText(icdId, "ICD璁板綍ID涓嶈兘涓虹┖");
CsIcdPathVO icdPath = csIcdPathMapper.selectIcdCheckMsgById(id);
return icdPath == null ? null : icdPath.getMsg();
}
@Override
public CsIcdPathDetailVO getMappingDetail(String icdId) {
String id = requireText(icdId, "ICD记录ID不能为空");
CsIcdPathDetailVO detail = csIcdPathMapper.selectIcdPathDetailById(id);
if (detail == null) {
return null;
}
byte[] icdContent = detail.getIcdContent();
if (icdContent != null && icdContent.length > 0) {
detail.setIcdText(new String(icdContent, StandardCharsets.UTF_8));
}
return detail;
}
@Override
@Transactional
public boolean addIcdPath(CsIcdPathParam param) {
CsIcdPathParam checkedParam = requireParam(param);
LocalDateTime now = LocalDateTime.now();
CsIcdPathPO icdPath = buildIcdPath(checkedParam);
CsIcdPathPO icdPath = buildIcdPath(checkedParam, true);
icdPath.setId(UUID.randomUUID().toString().replace("-", ""));
icdPath.setState(STATE_NORMAL);
icdPath.setCreateBy(currentUserId());
@@ -65,7 +99,7 @@ public class CsIcdPathServiceImpl implements CsIcdPathService {
public boolean updateIcdPath(CsIcdPathParam.UpdateParam param) {
CsIcdPathParam.UpdateParam checkedParam = requireUpdateParam(param);
requireIcdPath(checkedParam.getId());
CsIcdPathPO icdPath = buildIcdPath(checkedParam);
CsIcdPathPO icdPath = buildIcdPath(checkedParam, false);
icdPath.setId(checkedParam.getId());
icdPath.setUpdateBy(currentUserId());
icdPath.setUpdateTime(LocalDateTime.now());
@@ -80,13 +114,21 @@ public class CsIcdPathServiceImpl implements CsIcdPathService {
String currentUserId = currentUserId();
csIcdPathMapper.update(null, new LambdaUpdateWrapper<CsIcdPathPO>()
.set(CsIcdPathPO::getType, null)
.set(CsIcdPathPO::getType, ICD_TYPE_MANUAL_NON_STANDARD)
.set(CsIcdPathPO::getUpdateBy, currentUserId)
.set(CsIcdPathPO::getUpdateTime, now)
.eq(CsIcdPathPO::getState, STATE_NORMAL));
.eq(CsIcdPathPO::getState, STATE_NORMAL)
.eq(CsIcdPathPO::getType, ICD_TYPE_MANUAL_STANDARD));
csIcdPathMapper.update(null, new LambdaUpdateWrapper<CsIcdPathPO>()
.set(CsIcdPathPO::getType, ICD_TYPE_UPSTREAM_NON_STANDARD)
.set(CsIcdPathPO::getUpdateBy, currentUserId)
.set(CsIcdPathPO::getUpdateTime, now)
.eq(CsIcdPathPO::getState, STATE_NORMAL)
.eq(CsIcdPathPO::getType, ICD_TYPE_UPSTREAM_STANDARD));
CsIcdPathPO activeIcdPath = new CsIcdPathPO();
activeIcdPath.setType(ICD_TYPE_STANDARD);
activeIcdPath.setType(resolveStandardType(targetIcdPath.getType()));
activeIcdPath.setUpdateBy(currentUserId);
activeIcdPath.setUpdateTime(now);
return csIcdPathMapper.update(activeIcdPath, new LambdaUpdateWrapper<CsIcdPathPO>()
@@ -116,7 +158,7 @@ public class CsIcdPathServiceImpl implements CsIcdPathService {
throw new IllegalArgumentException("ICD校验结果不能为空");
}
CsIcdPathPO icdPath = requireIcdPath(icdId);
CsIcdPathPO referenceIcd = requireUniqueReferenceIcd();
CsIcdPathPO referenceIcd = requireReferenceIcd(icdPath.getReferenceIcdId());
icdPath.setJsonStr(trimToNull(param.getMappingJson()));
icdPath.setXmlStr(trimToNull(param.getXml()));
icdPath.setResult(normalizeResult(param.getResult()));
@@ -140,17 +182,34 @@ public class CsIcdPathServiceImpl implements CsIcdPathService {
}
}
private CsIcdPathPO buildIcdPath(CsIcdPathParam param) {
private CsIcdPathPO buildIcdPath(CsIcdPathParam param, boolean useDefaultType) {
CsIcdPathPO icdPath = new CsIcdPathPO();
icdPath.setName(requireText(param.getName(), "ICD名称不能为空"));
icdPath.setPath(requireText(param.getPath(), "ICD存储路径不能为空"));
icdPath.setIcdContent(param.getIcdContent());
icdPath.setAngle(param.getAngle());
icdPath.setUsePhaseIndex(param.getUsePhaseIndex());
icdPath.setType(param.getType());
icdPath.setType(useDefaultType ? resolveIcdType(param.getType()) : param.getType());
return icdPath;
}
/**
* 新增 ICD 记录未显式传类型时,默认归类为手动录入的非标准 ICD。
*/
private Integer resolveIcdType(Integer type) {
return type == null ? ICD_TYPE_MANUAL_NON_STANDARD : type;
}
/**
* 激活标准 ICD 时保留记录来源:手动录入升为 1上游解析传递升为 3。
*/
private Integer resolveStandardType(Integer type) {
if (Integer.valueOf(ICD_TYPE_UPSTREAM_STANDARD).equals(type)
|| Integer.valueOf(ICD_TYPE_UPSTREAM_NON_STANDARD).equals(type)) {
return ICD_TYPE_UPSTREAM_STANDARD;
}
return ICD_TYPE_MANUAL_STANDARD;
}
private CsIcdPathParam requireParam(CsIcdPathParam param) {
if (param == null) {
throw new IllegalArgumentException("ICD记录参数不能为空");
@@ -176,19 +235,15 @@ public class CsIcdPathServiceImpl implements CsIcdPathService {
}
/**
* 全系统只允许一个正常状态的标准 ICD 作为唯一参照
* ICD 校验保存时以当前记录绑定的 Reference_Icd_Id 作为参照来源
*/
private CsIcdPathPO requireUniqueReferenceIcd() {
List<CsIcdPathPO> referenceIcdList = csIcdPathMapper.selectList(new LambdaQueryWrapper<CsIcdPathPO>()
.eq(CsIcdPathPO::getState, STATE_NORMAL)
.eq(CsIcdPathPO::getType, ICD_TYPE_STANDARD));
if (referenceIcdList == null || referenceIcdList.isEmpty()) {
throw new IllegalArgumentException("未配置标准ICD无法执行唯一性校验");
private CsIcdPathPO requireReferenceIcd(String referenceIcdId) {
String id = requireText(referenceIcdId, "未配置参照ICD无法保存校验结果");
CsIcdPathPO referenceIcd = csIcdPathMapper.selectById(id);
if (referenceIcd == null || !Integer.valueOf(STATE_NORMAL).equals(referenceIcd.getState())) {
throw new IllegalArgumentException("参照ICD不存在或已删除无法保存校验结果");
}
if (referenceIcdList.size() > 1) {
throw new IllegalArgumentException("存在多个标准ICD无法确定唯一参照");
}
return referenceIcdList.get(0);
return referenceIcd;
}
private Integer normalizeResult(Integer result) {

View File

@@ -36,20 +36,62 @@ class IcdConsistencyCheckServiceTest {
}
@Test
void checkShouldOnlyReturnCorrectedJsonForRtFreSelfMappingRule() {
void checkShouldReturnPassAndCorrectedJsonForRtFreSelfMappingRule() {
IcdConsistencyCheckRequest request = new IcdConsistencyCheckRequest();
request.setStandardJson(buildRtFreStandardJson());
request.setCheckedJson(buildRtFreCheckedJson());
IcdConsistencyCheckResponse response = service.check(request);
Assertions.assertEquals(0, response.getResult());
Assertions.assertEquals(1, response.getResult());
Assertions.assertTrue(response.getIssues().isEmpty());
Assertions.assertTrue(response.getCorrectedJson().contains("\"rptID\" : \"demoRtFre\""));
Assertions.assertTrue(response.getCorrectedJson().contains("\"FlickerFlag\" : \"1\""));
Assertions.assertTrue(response.getCorrectedJson().contains("\"reportCount\" : 0"));
Assertions.assertTrue(response.getCorrectedJson().contains("\"reportCount\" : 1"));
}
@Test
void checkShouldNotCorrectRtFreWhenReportMapAlreadyMatchesStandard() {
IcdConsistencyCheckRequest request = new IcdConsistencyCheckRequest();
request.setStandardJson(buildRtFreStandardJson());
request.setCheckedJson(buildRtFreStandardJson());
IcdConsistencyCheckResponse response = service.check(request);
Assertions.assertEquals(1, response.getResult());
Assertions.assertNull(response.getCorrectedJson());
}
@Test
void checkShouldKeepStandardDoiWhenSameDoiListContainsDuplicateDesc() {
IcdConsistencyCheckRequest request = new IcdConsistencyCheckRequest();
request.setStandardJson(buildStandardJson());
request.setCheckedJson(buildDuplicateDoiDescJson());
IcdConsistencyCheckResponse response = service.check(request);
Assertions.assertEquals(0, response.getResult());
Assertions.assertTrue(response.getIssuesJson().contains("同一个 doiList 下存在 desc 相同的指标"));
Assertions.assertTrue(response.getIssuesJson().contains("频率+Hz 在标准映射中存在"));
Assertions.assertTrue(response.getIssuesJson().contains("频率+Hz2 不在标准映射中"));
Assertions.assertNotNull(response.getCorrectedJson());
Assertions.assertTrue(response.getCorrectedJson().contains("\"name\" : \"Hz\""));
Assertions.assertFalse(response.getCorrectedJson().contains("\"name\" : \"Hz2\""));
}
@Test
void checkShouldIgnoreVoltageStartMetricsDuringDoiConsistency() {
IcdConsistencyCheckRequest request = new IcdConsistencyCheckRequest();
request.setStandardJson(buildVoltageStartMetricsStandardJson());
request.setCheckedJson(buildStandardJson());
IcdConsistencyCheckResponse response = service.check(request);
Assertions.assertEquals(1, response.getResult());
Assertions.assertTrue(response.getIssues().isEmpty());
}
@Test
void checkShouldReportEmptySdiListAsTypeListProblem() {
IcdConsistencyCheckRequest request = new IcdConsistencyCheckRequest();
@@ -63,6 +105,18 @@ class IcdConsistencyCheckServiceTest {
Assertions.assertFalse(response.getIssuesJson().contains("sdiList 不能为空"));
}
@Test
void checkShouldIgnoreVoltageStartMetricsDuringSelfFormatTypeListValidation() {
IcdConsistencyCheckRequest request = new IcdConsistencyCheckRequest();
request.setStandardJson(buildVoltageStartMetricsWithoutSdiJson());
request.setCheckedJson(buildVoltageStartMetricsWithoutSdiJson());
IcdConsistencyCheckResponse response = service.check(request);
Assertions.assertEquals(1, response.getResult());
Assertions.assertTrue(response.getIssues().isEmpty());
}
@Test
void checkShouldReturnPassWhenCheckedJsonMatchesStandardJson() {
IcdConsistencyCheckRequest request = new IcdConsistencyCheckRequest();
@@ -161,6 +215,69 @@ class IcdConsistencyCheckServiceTest {
"}";
}
private String buildDuplicateDoiDescJson() {
return "{\n" +
" \"IED\":\"IED1\",\n" +
" \"LD\":\"LD0\",\n" +
" \"DataType\":\"1\",\n" +
" \"unit\":\"s\",\n" +
" \"ReportMap\":[\n" +
" {\"desc\":\"统计数据\",\"reportCount\":2,\"rptID\":\"rpt-stat\",\"name\":\"brcbStat\",\"buffered\":\"BR\",\"inst\":\"01\",\"FlickerFlag\":\"0\",\"Select\":\"all\",\"TrgOps\":\"dchg\"},\n" +
" {\"desc\":\"波动闪变\",\"reportCount\":1,\"rptID\":\"rpt-flk\",\"name\":\"brcbFlk\",\"buffered\":\"BR\",\"inst\":\"02\",\"FlickerFlag\":\"0\",\"Select\":\"all\",\"TrgOps\":\"dchg\"},\n" +
" {\"desc\":\"实时数据\",\"reportCount\":1,\"rptID\":\"rpt-rt\",\"name\":\"brcbRt\",\"buffered\":\"RP\",\"inst\":\"03\",\"FlickerFlag\":\"0\",\"Select\":\"all\",\"TrgOps\":\"dchg\"},\n" +
" {\"desc\":\"暂态事件\",\"reportCount\":1,\"rptID\":\"rpt-tran\",\"name\":\"brcbTran\",\"buffered\":\"BR\",\"inst\":\"04\",\"FlickerFlag\":\"0\",\"Select\":\"all\",\"TrgOps\":\"dchg\"}\n" +
" ],\n" +
" \"DataSetList\":[\n" +
buildDataSetWithDuplicateDoiDesc("MMXU", "统计数据", "1", "A相") + ",\n" +
buildDataSet("MSQI", "实时数据", "1", "A相", "A", "电流", 1, 2, "A") + ",\n" +
buildDataSet("MHAI", "谐波数据", "1", "A相", "Har", "谐波", 1, 2, "%") + ",\n" +
buildDataSet("MFLK", "波动闪变", "1", "A相", "Flk", "闪变", 1, 2, "pu") + "\n" +
" ]\n" +
"}";
}
private String buildVoltageStartMetricsStandardJson() {
return "{\n" +
" \"IED\":\"IED1\",\n" +
" \"LD\":\"LD0\",\n" +
" \"DataType\":\"1\",\n" +
" \"unit\":\"s\",\n" +
" \"ReportMap\":[\n" +
" {\"desc\":\"统计数据\",\"reportCount\":2,\"rptID\":\"rpt-stat\",\"name\":\"brcbStat\",\"buffered\":\"BR\",\"inst\":\"01\",\"FlickerFlag\":\"0\",\"Select\":\"all\",\"TrgOps\":\"dchg\"},\n" +
" {\"desc\":\"波动闪变\",\"reportCount\":1,\"rptID\":\"rpt-flk\",\"name\":\"brcbFlk\",\"buffered\":\"BR\",\"inst\":\"02\",\"FlickerFlag\":\"0\",\"Select\":\"all\",\"TrgOps\":\"dchg\"},\n" +
" {\"desc\":\"实时数据\",\"reportCount\":1,\"rptID\":\"rpt-rt\",\"name\":\"brcbRt\",\"buffered\":\"RP\",\"inst\":\"03\",\"FlickerFlag\":\"0\",\"Select\":\"all\",\"TrgOps\":\"dchg\"},\n" +
" {\"desc\":\"暂态事件\",\"reportCount\":1,\"rptID\":\"rpt-tran\",\"name\":\"brcbTran\",\"buffered\":\"BR\",\"inst\":\"04\",\"FlickerFlag\":\"0\",\"Select\":\"all\",\"TrgOps\":\"dchg\"}\n" +
" ],\n" +
" \"DataSetList\":[\n" +
buildDataSetWithVoltageStartMetrics("MMXU", "统计数据", "1", "A相") + ",\n" +
buildDataSet("MSQI", "实时数据", "1", "A相", "A", "电流", 1, 2, "A") + ",\n" +
buildDataSet("MHAI", "谐波数据", "1", "A相", "Har", "谐波", 1, 2, "%") + ",\n" +
buildDataSet("MFLK", "波动闪变", "1", "A相", "Flk", "闪变", 1, 2, "pu") + "\n" +
" ]\n" +
"}";
}
private String buildVoltageStartMetricsWithoutSdiJson() {
return "{\n" +
" \"IED\":\"IED1\",\n" +
" \"LD\":\"LD0\",\n" +
" \"DataType\":\"1\",\n" +
" \"unit\":\"s\",\n" +
" \"ReportMap\":[\n" +
" {\"desc\":\"统计数据\",\"reportCount\":2,\"rptID\":\"rpt-stat\",\"name\":\"brcbStat\",\"buffered\":\"BR\",\"inst\":\"01\",\"FlickerFlag\":\"0\",\"Select\":\"all\",\"TrgOps\":\"dchg\"},\n" +
" {\"desc\":\"波动闪变\",\"reportCount\":1,\"rptID\":\"rpt-flk\",\"name\":\"brcbFlk\",\"buffered\":\"BR\",\"inst\":\"02\",\"FlickerFlag\":\"0\",\"Select\":\"all\",\"TrgOps\":\"dchg\"},\n" +
" {\"desc\":\"实时数据\",\"reportCount\":1,\"rptID\":\"rpt-rt\",\"name\":\"brcbRt\",\"buffered\":\"RP\",\"inst\":\"03\",\"FlickerFlag\":\"0\",\"Select\":\"all\",\"TrgOps\":\"dchg\"},\n" +
" {\"desc\":\"暂态事件\",\"reportCount\":1,\"rptID\":\"rpt-tran\",\"name\":\"brcbTran\",\"buffered\":\"BR\",\"inst\":\"04\",\"FlickerFlag\":\"0\",\"Select\":\"all\",\"TrgOps\":\"dchg\"}\n" +
" ],\n" +
" \"DataSetList\":[\n" +
buildDataSetWithVoltageStartMetricsWithoutSdi("MMXU", "统计数据", "1", "A相") + ",\n" +
buildDataSet("MSQI", "实时数据", "1", "A相", "A", "电流", 1, 2, "A") + ",\n" +
buildDataSet("MHAI", "谐波数据", "1", "A相", "Har", "谐波", 1, 2, "%") + ",\n" +
buildDataSet("MFLK", "波动闪变", "1", "A相", "Flk", "闪变", 1, 2, "pu") + "\n" +
" ]\n" +
"}";
}
private String buildEmptySdiListJson() {
return "{\n" +
" \"IED\":\"IED1\",\n" +
@@ -196,4 +313,51 @@ class IcdConsistencyCheckServiceTest {
"\",\"desc\":\"" + instDesc + "\",\"doiList\":[{\"name\":\"" + doiName + "\",\"desc\":\"" + doiDesc +
"\",\"start\":1,\"end\":4,\"unit\":\"Hz\",\"coefficient\":1.0,\"baseflag\":1,\"basecount\":1,\"icdcout\":10,\"sdiList\":[]}]}]}";
}
private String buildDataSetWithDuplicateDoiDesc(String lnClass, String groupDesc, String inst, String instDesc) {
return " {\"desc\":\"" + groupDesc + "\",\"lnClass\":\"" + lnClass + "\",\"instList\":[{\"inst\":\"" + inst +
"\",\"desc\":\"" + instDesc + "\",\"doiList\":[" +
buildDoi("Hz2", "频率", 5, 8, "Hz") + "," +
buildDoi("Hz", "频率", 1, 4, "Hz") +
"]}]}";
}
private String buildDataSetWithVoltageStartMetrics(String lnClass, String groupDesc, String inst, String instDesc) {
return " {\"desc\":\"" + groupDesc + "\",\"lnClass\":\"" + lnClass + "\",\"instList\":[{\"inst\":\"" + inst +
"\",\"desc\":\"" + instDesc + "\",\"doiList\":[" +
buildDoi("Hz", "频率", 1, 4, "Hz") + "," +
buildDoi("VolDistStr", "电压扰动事件启动", 5, 6, "") + "," +
buildDoi("VolDipStr", "电压暂降事件启动", 7, 8, "") + "," +
buildDoi("VolSwellStr", "电压暂升事件启动", 9, 10, "") + "," +
buildDoi("VolInterStr", "电压中断事件启动", 11, 12, "") + "," +
buildDoi("VolDipSet", "电压暂降启动定值", 13, 14, "V") + "," +
buildDoi("VolSwellSet", "电压暂升启动定值", 15, 16, "V") + "," +
buildDoi("VolInterSet", "电压中断启动定值", 17, 18, "V") +
"]}]}";
}
private String buildDataSetWithVoltageStartMetricsWithoutSdi(String lnClass, String groupDesc, String inst, String instDesc) {
return " {\"desc\":\"" + groupDesc + "\",\"lnClass\":\"" + lnClass + "\",\"instList\":[{\"inst\":\"" + inst +
"\",\"desc\":\"" + instDesc + "\",\"doiList\":[" +
buildDoiWithoutSdi("VolDistStr", "电压扰动事件启动", 5, 6, "") + "," +
buildDoiWithoutSdi("VolDipStr", "电压暂降事件启动", 7, 8, "") + "," +
buildDoiWithoutSdi("VolSwellStr", "电压暂升事件启动", 9, 10, "") + "," +
buildDoiWithoutSdi("VolInterStr", "电压中断事件启动", 11, 12, "") + "," +
buildDoiWithoutSdi("VolDipSet", "电压暂降启动定值", 13, 14, "V") + "," +
buildDoiWithoutSdi("VolSwellSet", "电压暂升启动定值", 15, 16, "V") + "," +
buildDoiWithoutSdi("VolInterSet", "电压中断启动定值", 17, 18, "V") +
"]}]}";
}
private String buildDoi(String doiName, String doiDesc, int start, int end, String unit) {
return "{\"name\":\"" + doiName + "\",\"desc\":\"" + doiDesc +
"\",\"start\":" + start + ",\"end\":" + end + ",\"unit\":\"" + unit +
"\",\"coefficient\":1.0,\"baseflag\":1,\"basecount\":1,\"icdcout\":10,\"sdiList\":[{\"name\":\"mag\",\"desc\":\"幅值\",\"typeList\":[{\"name\":\"f\",\"desc\":\"浮点\"}]}]}";
}
private String buildDoiWithoutSdi(String doiName, String doiDesc, int start, int end, String unit) {
return "{\"name\":\"" + doiName + "\",\"desc\":\"" + doiDesc +
"\",\"start\":" + start + ",\"end\":" + end + ",\"unit\":\"" + unit +
"\",\"coefficient\":1.0,\"baseflag\":1,\"basecount\":1,\"icdcout\":10,\"sdiList\":[]}";
}
}

View File

@@ -5,6 +5,8 @@ import com.njcn.gather.icd.mapping.pojo.bo.icd.IcdDocument;
import com.njcn.gather.icd.mapping.pojo.param.CsIcdPathParam;
import com.njcn.gather.icd.mapping.pojo.param.IcdCheckResultSaveParam;
import com.njcn.gather.icd.mapping.pojo.po.CsIcdPathPO;
import com.njcn.gather.icd.mapping.pojo.vo.CsIcdPathDetailVO;
import com.njcn.gather.icd.mapping.pojo.vo.CsIcdPathVO;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Assertions;
@@ -47,6 +49,26 @@ class CsIcdPathServiceImplTest {
verify(csIcdPathMapper).selectIcdPathList(eq("standard"), eq(null), eq(null));
}
@Test
void listReferenceIcdPathsShouldQueryStandardIcdTypes() {
service.listReferenceIcdPaths();
verify(csIcdPathMapper).selectReferenceIcdPathList();
}
@Test
void getIcdCheckMsgShouldQueryMsgByTrimmedId() {
JsonNode msg = objectMapper.createObjectNode().put("summary", "通过");
CsIcdPathVO vo = new CsIcdPathVO();
vo.setMsg(msg);
when(csIcdPathMapper.selectIcdCheckMsgById(eq("icd-001"))).thenReturn(vo);
JsonNode result = service.getIcdCheckMsg(" icd-001 ");
Assertions.assertSame(msg, result);
verify(csIcdPathMapper).selectIcdCheckMsgById(eq("icd-001"));
}
@Test
void addIcdPathShouldInsertEnabledRecord() {
CsIcdPathParam param = buildParam("标准ICD");
@@ -58,7 +80,6 @@ class CsIcdPathServiceImplTest {
verify(csIcdPathMapper).insert(captor.capture());
Assertions.assertTrue(result);
Assertions.assertEquals("标准ICD", captor.getValue().getName());
Assertions.assertEquals("D:/icd/standard.icd", captor.getValue().getPath());
Assertions.assertEquals(1, captor.getValue().getState());
Assertions.assertNotNull(captor.getValue().getId());
Assertions.assertNotNull(captor.getValue().getCreateTime());
@@ -79,12 +100,25 @@ class CsIcdPathServiceImplTest {
Assertions.assertArrayEquals(fileContent, captor.getValue().getIcdContent());
}
@Test
void addIcdPathShouldDefaultTypeToManualNonStandard() {
CsIcdPathParam param = buildParam("手动录入非标准ICD");
param.setType(null);
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(2, captor.getValue().getType());
}
@Test
void updateIcdPathShouldRejectDeletedRecord() {
CsIcdPathParam.UpdateParam param = new CsIcdPathParam.UpdateParam();
param.setId("icd-001");
param.setName("标准ICD");
param.setPath("D:/icd/standard.icd");
CsIcdPathPO deleted = new CsIcdPathPO();
deleted.setId("icd-001");
@@ -103,7 +137,6 @@ class CsIcdPathServiceImplTest {
byte[] fileContent = "<SCL version=\"1\"></SCL>".getBytes();
param.setId("icd-001");
param.setName("标准ICD");
param.setPath("standard.icd");
param.setIcdContent(fileContent);
CsIcdPathPO existed = new CsIcdPathPO();
@@ -134,21 +167,40 @@ class CsIcdPathServiceImplTest {
}
@Test
void activateIcdPathShouldOnlyKeepTargetAsStandardIcd() {
void activateIcdPathShouldOnlyKeepTargetAsManualStandardIcd() {
CsIcdPathPO icdPath = new CsIcdPathPO();
icdPath.setId("icd-001");
icdPath.setState(1);
icdPath.setType(2);
when(csIcdPathMapper.selectById(eq("icd-001"))).thenReturn(icdPath);
when(csIcdPathMapper.update(any(CsIcdPathPO.class), any())).thenReturn(1);
boolean result = service.activateIcdPath("icd-001");
ArgumentCaptor<CsIcdPathPO> captor = ArgumentCaptor.forClass(CsIcdPathPO.class);
verify(csIcdPathMapper, times(2)).update(captor.capture(), any());
verify(csIcdPathMapper, times(3)).update(captor.capture(), any());
Assertions.assertTrue(result);
Assertions.assertNull(captor.getAllValues().get(0));
Assertions.assertEquals(1, captor.getAllValues().get(1).getType());
Assertions.assertNotNull(captor.getAllValues().get(1).getUpdateTime());
Assertions.assertNull(captor.getAllValues().get(1));
Assertions.assertEquals(1, captor.getAllValues().get(2).getType());
Assertions.assertNotNull(captor.getAllValues().get(2).getUpdateTime());
}
@Test
void activateIcdPathShouldKeepUpstreamSourceWhenTargetIsUpstreamIcd() {
CsIcdPathPO icdPath = new CsIcdPathPO();
icdPath.setId("icd-001");
icdPath.setState(1);
icdPath.setType(4);
when(csIcdPathMapper.selectById(eq("icd-001"))).thenReturn(icdPath);
when(csIcdPathMapper.update(any(CsIcdPathPO.class), any())).thenReturn(1);
boolean result = service.activateIcdPath("icd-001");
ArgumentCaptor<CsIcdPathPO> captor = ArgumentCaptor.forClass(CsIcdPathPO.class);
verify(csIcdPathMapper, times(3)).update(captor.capture(), any());
Assertions.assertTrue(result);
Assertions.assertEquals(3, captor.getAllValues().get(2).getType());
}
@Test
@@ -235,10 +287,36 @@ class CsIcdPathServiceImplTest {
Assertions.assertArrayEquals(objectMapper.writeValueAsBytes(icdDocument), captor.getValue().getIcdContent());
}
@Test
void getMappingDetailShouldReturnJsonXmlAndUtf8IcdText() {
CsIcdPathDetailVO detail = new CsIcdPathDetailVO();
detail.setId("icd-001");
detail.setName("标准ICD");
detail.setJsonStr("{\"ied\":\"IED1\"}");
detail.setXmlStr("<Root></Root>");
detail.setIcdContent("<SCL name=\"demo\"></SCL>".getBytes(java.nio.charset.StandardCharsets.UTF_8));
when(csIcdPathMapper.selectIcdPathDetailById(eq("icd-001"))).thenReturn(detail);
CsIcdPathDetailVO result = service.getMappingDetail(" icd-001 ");
Assertions.assertEquals("{\"ied\":\"IED1\"}", result.getJsonStr());
Assertions.assertEquals("<Root></Root>", result.getXmlStr());
Assertions.assertEquals("<SCL name=\"demo\"></SCL>", result.getIcdText());
verify(csIcdPathMapper).selectIcdPathDetailById(eq("icd-001"));
}
@Test
void getMappingDetailShouldReturnNullWhenRecordMissing() {
when(csIcdPathMapper.selectIcdPathDetailById(eq("missing"))).thenReturn(null);
CsIcdPathDetailVO result = service.getMappingDetail("missing");
Assertions.assertNull(result);
}
private CsIcdPathParam buildParam(String name) {
CsIcdPathParam param = new CsIcdPathParam();
param.setName(name);
param.setPath("D:/icd/standard.icd");
param.setAngle(0);
param.setUsePhaseIndex(1);
param.setType(1);

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);
}
}
}