最近新增的工作

This commit is contained in:
2026-04-23 11:06:04 +08:00
parent c9461f8b8a
commit 03df9d3a10
140 changed files with 5591 additions and 2767 deletions

View File

@@ -1,135 +0,0 @@
package com.njcn.gather.icd.mapping.application;
import com.njcn.gather.icd.mapping.application.command.GenerateFromIcdCommand;
import com.njcn.gather.icd.mapping.application.result.GenerateMappingResult;
import com.njcn.gather.icd.mapping.domain.model.analysis.IndexAnalysis;
import com.njcn.gather.icd.mapping.domain.model.analysis.ValidationResult;
import com.njcn.gather.icd.mapping.domain.model.icd.IcdDocument;
import com.njcn.gather.icd.mapping.domain.model.mapping.MappingDocument;
import com.njcn.gather.icd.mapping.domain.model.template.DefaultTemplate;
import com.njcn.gather.icd.mapping.domain.service.DefaultTemplateLoader;
import com.njcn.gather.icd.mapping.domain.service.IcdParserService;
import com.njcn.gather.icd.mapping.domain.service.IndexAnalysisService;
import com.njcn.gather.icd.mapping.domain.service.IndexValidationService;
import com.njcn.gather.icd.mapping.domain.service.MappingGenerationService;
import com.njcn.gather.icd.mapping.enums.GenerateStatus;
import com.njcn.gather.icd.mapping.infrastructure.serializer.MappingDocumentSerializer;
import com.njcn.gather.icd.mapping.infrastructure.storage.FileStorageService;
import org.springframework.stereotype.Service;
/**
* 生成任务应用服务。
*
* 完整流程:
* 1. 解析 ICD
* 2. 读取 DefaultCfg.txt
* 3. 按业务分组生成候选项;
* 4. 如果用户未提交绑定关系,返回 NEED_INDEX_SELECTION
* 5. 如果提交了绑定关系,先做合法性校验;
* 6. 校验通过后生成正式 MappingDocument
* 7. 序列化并按需落盘。
*/
@Service
public class MappingTaskAppService {
private final IcdParserService icdParserService;
private final DefaultTemplateLoader defaultTemplateLoader;
private final IndexAnalysisService indexAnalysisService;
private final IndexValidationService indexValidationService;
private final MappingGenerationService mappingGenerationService;
private final MappingDocumentSerializer mappingDocumentSerializer;
private final FileStorageService fileStorageService;
public MappingTaskAppService(IcdParserService icdParserService,
DefaultTemplateLoader defaultTemplateLoader,
IndexAnalysisService indexAnalysisService,
IndexValidationService indexValidationService,
MappingGenerationService mappingGenerationService,
MappingDocumentSerializer mappingDocumentSerializer,
FileStorageService fileStorageService) {
this.icdParserService = icdParserService;
this.defaultTemplateLoader = defaultTemplateLoader;
this.indexAnalysisService = indexAnalysisService;
this.indexValidationService = indexValidationService;
this.mappingGenerationService = mappingGenerationService;
this.mappingDocumentSerializer = mappingDocumentSerializer;
this.fileStorageService = fileStorageService;
}
public GenerateMappingResult generateFromIcd(GenerateFromIcdCommand command) {
GenerateMappingResult result = new GenerateMappingResult();
try {
// 1. 解析 ICD
IcdDocument icdDocument = icdParserService.parse(command.getFileBytes(), command.getFileName());
result.setIedName(icdDocument.getIedName());
result.setLdInst(icdDocument.getLdInst());
// 2. 加载 DefaultCfg.txt
DefaultTemplate template = defaultTemplateLoader.load();
result.getProblems().addAll(template.verify());
// 3. 分析索引候选
IndexAnalysis indexAnalysis = indexAnalysisService.analyze(icdDocument, template);
result.setIndexAnalysis(indexAnalysis);
result.getProblems().addAll(indexAnalysis.getProblems());
// 4. 如果没有提交任何绑定关系,则直接返回待匹配项
if (command.getIndexSelection() == null || command.getIndexSelection().isEmpty()) {
result.setStatus(GenerateStatus.NEED_INDEX_SELECTION);
result.setMessage("索引配置缺失或不合法,请根据候选信息完成标签与数字索引的绑定后重新提交");
return result;
}
// 5. 校验用户提交的绑定关系
ValidationResult validationResult = indexValidationService.validate(indexAnalysis, command.getIndexSelection());
if (!validationResult.isValid()) {
result.setStatus(GenerateStatus.NEED_INDEX_SELECTION);
result.setMessage("索引配置缺失或不合法,请根据候选信息完成标签与数字索引的绑定后重新提交");
result.getProblems().addAll(validationResult.getProblems());
return result;
}
// 6. 生成正式映射结构
MappingDocument mappingDocument = mappingGenerationService.generate(
icdDocument,
template,
indexAnalysis,
command.getIndexSelection(),
command.getVersion(),
command.getAuthor()
);
result.setMappingDocument(mappingDocument);
// 7. 序列化输出
String mappingJson = command.isPrettyJson()
? mappingDocumentSerializer.toPrettyJson(mappingDocument)
: mappingDocumentSerializer.toCompactJson(mappingDocument);
result.setMappingJson(mappingJson);
if (command.isSaveToDisk()) {
String fileName = buildOutputFileName(icdDocument, command.isPrettyJson());
String savedPath = fileStorageService.save(fileName, mappingJson, command.getOutputDir());
result.setSavedPath(savedPath);
}
result.setStatus(GenerateStatus.SUCCESS);
result.setMessage("映射生成成功");
return result;
} catch (Exception ex) {
result.setStatus(GenerateStatus.FAILED);
result.setMessage("映射生成失败:" + ex.getMessage());
result.getProblems().add(ex.getMessage());
return result;
}
}
private String buildOutputFileName(IcdDocument icdDocument, boolean prettyJson) {
String baseName = icdDocument.getIedName() == null ? "mapping" : icdDocument.getIedName();
// 落盘文件名只保留安全字符,避免 IED 名称携带路径分隔符导致越界写入。
String safeBaseName = baseName.replaceAll("[\\\\/:*?\"<>|]+", "_").trim();
if (safeBaseName.isEmpty()) {
safeBaseName = "mapping";
}
return safeBaseName + (prettyJson ? "-mapping-pretty.json" : "-mapping.json");
}
}

View File

@@ -1,102 +0,0 @@
package com.njcn.gather.icd.mapping.application.command;
import java.util.ArrayList;
import java.util.List;
/**
* 生成命令对象。
*
* 说明:
* controller 层不要把 MultipartFile 和 request 直接传进领域服务;
* 统一转成 command便于应用层做流程编排。
*/
public class GenerateFromIcdCommand {
/** 原始文件名。 */
private String fileName;
/** ICD 文件字节数组。 */
private byte[] fileBytes;
/** 输出版本号。 */
private String version;
/** 作者。 */
private String author;
/** 是否保存到磁盘。 */
private boolean saveToDisk;
/** 是否输出美化 JSON。 */
private boolean prettyJson;
/** 输出目录。 */
private String outputDir;
/** 用户上送的索引选择结果。 */
private List<IndexSelectionGroupCommand> indexSelection = new ArrayList<IndexSelectionGroupCommand>();
public String getFileName() {
return fileName;
}
public void setFileName(String fileName) {
this.fileName = fileName;
}
public byte[] getFileBytes() {
return fileBytes;
}
public void setFileBytes(byte[] fileBytes) {
this.fileBytes = fileBytes;
}
public String getVersion() {
return version;
}
public void setVersion(String version) {
this.version = version;
}
public String getAuthor() {
return author;
}
public void setAuthor(String author) {
this.author = author;
}
public boolean isSaveToDisk() {
return saveToDisk;
}
public void setSaveToDisk(boolean saveToDisk) {
this.saveToDisk = saveToDisk;
}
public boolean isPrettyJson() {
return prettyJson;
}
public void setPrettyJson(boolean prettyJson) {
this.prettyJson = prettyJson;
}
public String getOutputDir() {
return outputDir;
}
public void setOutputDir(String outputDir) {
this.outputDir = outputDir;
}
public List<IndexSelectionGroupCommand> getIndexSelection() {
return indexSelection;
}
public void setIndexSelection(List<IndexSelectionGroupCommand> indexSelection) {
this.indexSelection = indexSelection;
}
}

View File

@@ -1,51 +0,0 @@
package com.njcn.gather.icd.mapping.application.command;
/**
* 应用层单条绑定命令。
*/
public class IndexBindingCommand {
/** 报告名。 */
private String reportName;
/** 数据集名。 */
private String dataSetName;
/** 标签。 */
private String label;
/** 绑定到的 lnInst 数字。 */
private String lnInst;
public String getReportName() {
return reportName;
}
public void setReportName(String reportName) {
this.reportName = reportName;
}
public String getDataSetName() {
return dataSetName;
}
public void setDataSetName(String dataSetName) {
this.dataSetName = dataSetName;
}
public String getLabel() {
return label;
}
public void setLabel(String label) {
this.label = label;
}
public String getLnInst() {
return lnInst;
}
public void setLnInst(String lnInst) {
this.lnInst = lnInst;
}
}

View File

@@ -1,43 +0,0 @@
package com.njcn.gather.icd.mapping.application.command;
import java.util.ArrayList;
import java.util.List;
/**
* 应用层分组选择命令。
*/
public class IndexSelectionGroupCommand {
/** 分组唯一键。 */
private String groupKey;
/** 分组中文描述。 */
private String groupDesc;
/** 当前分组下的多条绑定关系。 */
private List<IndexBindingCommand> bindings = new ArrayList<IndexBindingCommand>();
public String getGroupKey() {
return groupKey;
}
public void setGroupKey(String groupKey) {
this.groupKey = groupKey;
}
public String getGroupDesc() {
return groupDesc;
}
public void setGroupDesc(String groupDesc) {
this.groupDesc = groupDesc;
}
public List<IndexBindingCommand> getBindings() {
return bindings;
}
public void setBindings(List<IndexBindingCommand> bindings) {
this.bindings = bindings;
}
}

View File

@@ -1,42 +0,0 @@
package com.njcn.gather.icd.mapping.application.result;
import com.njcn.gather.icd.mapping.domain.model.analysis.IndexAnalysis;
import com.njcn.gather.icd.mapping.domain.model.mapping.MappingDocument;
import com.njcn.gather.icd.mapping.enums.GenerateStatus;
import java.util.ArrayList;
import java.util.List;
/**
* 应用层返回对象。统一封装成功、需要选择索引、失败三类结果。
*/
public class GenerateMappingResult {
private GenerateStatus status;
private String message;
private String iedName;
private String ldInst;
private IndexAnalysis indexAnalysis;
private MappingDocument mappingDocument;
private String mappingJson;
private String savedPath;
private List<String> problems = new ArrayList<String>();
public GenerateStatus getStatus() { return status; }
public void setStatus(GenerateStatus status) { this.status = status; }
public String getMessage() { return message; }
public void setMessage(String message) { this.message = message; }
public String getIedName() { return iedName; }
public void setIedName(String iedName) { this.iedName = iedName; }
public String getLdInst() { return ldInst; }
public void setLdInst(String ldInst) { this.ldInst = ldInst; }
public IndexAnalysis getIndexAnalysis() { return indexAnalysis; }
public void setIndexAnalysis(IndexAnalysis indexAnalysis) { this.indexAnalysis = indexAnalysis; }
public MappingDocument getMappingDocument() { return mappingDocument; }
public void setMappingDocument(MappingDocument mappingDocument) { this.mappingDocument = mappingDocument; }
public String getMappingJson() { return mappingJson; }
public void setMappingJson(String mappingJson) { this.mappingJson = mappingJson; }
public String getSavedPath() { return savedPath; }
public void setSavedPath(String savedPath) { this.savedPath = savedPath; }
public List<String> getProblems() { return problems; }
public void setProblems(List<String> problems) { this.problems = problems; }
}

View File

@@ -1,9 +1,9 @@
package com.njcn.gather.icd.mapping.domain.service;
package com.njcn.gather.icd.mapping.component;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.njcn.gather.icd.mapping.config.MappingModuleConfig;
import com.njcn.gather.icd.mapping.domain.model.template.DefaultTemplate;
import com.njcn.gather.icd.mapping.pojo.bo.template.DefaultTemplate;
import com.njcn.gather.icd.mapping.utils.JsonUtils;
import org.springframework.core.io.ClassPathResource;
import org.springframework.stereotype.Service;
@@ -23,7 +23,10 @@ import java.util.List;
@Service
public class DefaultTemplateLoader {
/** 模块配置,提供默认模板路径等运行参数。 */
private final MappingModuleConfig moduleConfig;
/** 模板反序列化使用的 Jackson 实例。 */
private final ObjectMapper objectMapper;
public DefaultTemplateLoader(MappingModuleConfig moduleConfig) {
@@ -32,6 +35,11 @@ public class DefaultTemplateLoader {
this.objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
}
/**
* 加载并校验默认映射模板
*
* 返回值会直接参与索引候选分析和最终 MappingDocument 生成
*/
public DefaultTemplate load() {
try {
ClassPathResource resource = new ClassPathResource(moduleConfig.getDefaultTemplatePath());
@@ -55,6 +63,9 @@ public class DefaultTemplateLoader {
}
}
/**
* 读取 classpath 资源的完整字节内容
*/
private byte[] readAllBytes(ClassPathResource resource) throws Exception {
try (InputStream inputStream = resource.getInputStream();
ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {

View File

@@ -1,4 +1,4 @@
package com.njcn.gather.icd.mapping.infrastructure.storage;
package com.njcn.gather.icd.mapping.component;
import org.springframework.stereotype.Component;
@@ -11,8 +11,14 @@ import java.nio.charset.StandardCharsets;
*/
@Component
public class FileStorageService {
/**
* 将映射 JSON 写入目标目录
*
* outputDir 为空时使用当前工作目录返回最终文件绝对路径
*/
public String save(String fileName, String content, String outputDir) {
try {
// 输出目录允许由请求覆盖未传时落到当前工作目录
File dir = outputDir == null || outputDir.trim().isEmpty() ? new File(".") : new File(outputDir);
if (!dir.exists() && !dir.mkdirs()) {
throw new IllegalStateException("输出目录创建失败:" + dir.getAbsolutePath());

View File

@@ -1,6 +1,6 @@
package com.njcn.gather.icd.mapping.domain.service;
package com.njcn.gather.icd.mapping.component;
import com.njcn.gather.icd.mapping.domain.model.icd.IcdDocument;
import com.njcn.gather.icd.mapping.pojo.bo.icd.IcdDocument;
import com.njcn.gather.icd.mapping.infrastructure.parser.SclParserAdapter;
import org.springframework.stereotype.Service;
@@ -11,12 +11,16 @@ import org.springframework.stereotype.Service;
@Service
public class IcdParserService {
/** SCL 底层解析适配器,封装 JAXB generated 模型处理。 */
private final SclParserAdapter parserAdapter;
public IcdParserService(SclParserAdapter parserAdapter) {
this.parserAdapter = parserAdapter;
}
/**
* 解析 ICD 文件内容为模块内部统一领域模型
*/
public IcdDocument parse(byte[] bytes, String fileName) {
return parserAdapter.parse(bytes, fileName);
}

View File

@@ -1,13 +1,13 @@
package com.njcn.gather.icd.mapping.domain.service;
package com.njcn.gather.icd.mapping.component;
import com.njcn.gather.icd.mapping.domain.model.analysis.IndexAnalysis;
import com.njcn.gather.icd.mapping.domain.model.analysis.IndexCandidate;
import com.njcn.gather.icd.mapping.domain.model.analysis.IndexCandidateReportItem;
import com.njcn.gather.icd.mapping.domain.model.icd.DataSetNode;
import com.njcn.gather.icd.mapping.domain.model.icd.FcdaNode;
import com.njcn.gather.icd.mapping.domain.model.icd.IcdDocument;
import com.njcn.gather.icd.mapping.domain.model.icd.ReportControlNode;
import com.njcn.gather.icd.mapping.domain.model.template.DefaultTemplate;
import com.njcn.gather.icd.mapping.pojo.bo.analysis.IndexAnalysis;
import com.njcn.gather.icd.mapping.pojo.bo.analysis.IndexCandidate;
import com.njcn.gather.icd.mapping.pojo.bo.analysis.IndexCandidateReportItem;
import com.njcn.gather.icd.mapping.pojo.bo.icd.DataSetNode;
import com.njcn.gather.icd.mapping.pojo.bo.icd.FcdaNode;
import com.njcn.gather.icd.mapping.pojo.bo.icd.IcdDocument;
import com.njcn.gather.icd.mapping.pojo.bo.icd.ReportControlNode;
import com.njcn.gather.icd.mapping.pojo.bo.template.DefaultTemplate;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
@@ -21,17 +21,15 @@ import java.util.Set;
/**
* 索引候选分析服务
*
* 说明
* 1. 新版不再按单个报告平铺返回而是按 DefaultCfg.ReportList 的业务配置项聚合
* 2. 一个业务配置项下可能包含多个报告因此这里会计算 reportCount并返回 reports 子列表
* 3. templateLabels 只是模板参考不要求与 ICD 解析到的 lnInst 数量完全一一对应
* 4. 关键修正
* 在这里就把 DefaultCfg.ReportList inst / Select / TrgOps 一并带入 IndexCandidate
* 后续正式生成阶段直接使用不再重新查模板
* `DefaultCfg.ReportList` 的业务分组聚合 ICD 中的报告信息
* 同时把后续正式生成阶段需要的模板字段一并带入候选结果
*/
@Service
public class IndexAnalysisService {
/**
* 根据 ICD 报告控制块和 DefaultCfg.ReportList 生成索引候选分组
*/
public IndexAnalysis analyze(IcdDocument icdDocument, DefaultTemplate template) {
IndexAnalysis analysis = new IndexAnalysis();
if (icdDocument == null) {
@@ -43,7 +41,7 @@ public class IndexAnalysisService {
return analysis;
}
// 先按模板分组聚合
// 先按模板分组聚合候选项而不是按单个报告平铺返回
for (DefaultTemplate.ReportCfgItem reportCfg : template.getReportList()) {
List<ReportControlNode> matchedReports = collectMatchedReports(icdDocument, reportCfg);
if (matchedReports.isEmpty()) {
@@ -55,7 +53,7 @@ public class IndexAnalysisService {
candidate.setGroupDesc(reportCfg.getDesc());
candidate.setReportCount(matchedReports.size());
// 关键 DefaultCfg.ReportList 配置直接带入候选对象
// 把模板中的关键配置直接带入候选对象避免正式生成阶段再次回查模板
candidate.setReportInst(reportCfg.getInst());
candidate.setSelect(reportCfg.getSelect());
candidate.setTrgOps(reportCfg.getTrgOps());
@@ -76,7 +74,7 @@ public class IndexAnalysisService {
analysis.getCandidates().add(candidate);
}
// 检查是否有 ICD 报告没有被模板覆盖
// 检查 ICD 中是否存在未被模板覆盖的报告配置
if (icdDocument.getReportControls() != null) {
for (ReportControlNode reportControl : icdDocument.getReportControls()) {
if (!isCoveredByTemplate(reportControl, template)) {
@@ -91,6 +89,9 @@ public class IndexAnalysisService {
return analysis;
}
/**
* 收集命中当前模板报告分组 DataSetList 的报告控制块
*/
private List<ReportControlNode> collectMatchedReports(IcdDocument icdDocument, DefaultTemplate.ReportCfgItem reportCfg) {
List<ReportControlNode> result = new ArrayList<ReportControlNode>();
if (icdDocument.getReportControls() == null || reportCfg.getDataSetList() == null) {
@@ -104,6 +105,9 @@ public class IndexAnalysisService {
return result;
}
/**
* 判断 ICD 中的报告是否已经被模板 ReportList 覆盖
*/
private boolean isCoveredByTemplate(ReportControlNode reportControl, DefaultTemplate template) {
if (template == null || template.getReportList() == null) {
return false;
@@ -116,6 +120,9 @@ public class IndexAnalysisService {
return false;
}
/**
* 从指定 DataSet FCDA 中提取可选 lnInst 数字并按数字优先排序
*/
private List<String> collectLnInstValues(IcdDocument icdDocument, String dataSetName) {
if (icdDocument.getDataSets() == null) {
return Collections.emptyList();
@@ -151,6 +158,9 @@ public class IndexAnalysisService {
return result;
}
/**
* 构造前端回传使用的稳定分组 key
*/
private String buildGroupKey(DefaultTemplate.ReportCfgItem reportCfg) {
String desc = reportCfg.getDesc() == null ? "GROUP" : reportCfg.getDesc();
String firstDataSet = (reportCfg.getDataSetList() == null || reportCfg.getDataSetList().isEmpty())
@@ -158,6 +168,9 @@ public class IndexAnalysisService {
return normalize(desc) + "__" + normalize(firstDataSet);
}
/**
* 将中文描述和 DataSet 名转换为可比较的 key 片段
*/
private String normalize(String value) {
return value.replaceAll("[^0-9A-Za-z\\u4e00-\\u9fa5]+", "_")
.replaceAll("_+", "_")

View File

@@ -1,11 +1,11 @@
package com.njcn.gather.icd.mapping.domain.service;
package com.njcn.gather.icd.mapping.component;
import com.njcn.gather.icd.mapping.application.command.IndexBindingCommand;
import com.njcn.gather.icd.mapping.application.command.IndexSelectionGroupCommand;
import com.njcn.gather.icd.mapping.domain.model.analysis.IndexAnalysis;
import com.njcn.gather.icd.mapping.domain.model.analysis.IndexCandidate;
import com.njcn.gather.icd.mapping.domain.model.analysis.IndexCandidateReportItem;
import com.njcn.gather.icd.mapping.domain.model.analysis.ValidationResult;
import com.njcn.gather.icd.mapping.pojo.bo.analysis.IndexAnalysis;
import com.njcn.gather.icd.mapping.pojo.bo.analysis.IndexCandidate;
import com.njcn.gather.icd.mapping.pojo.bo.analysis.IndexCandidateReportItem;
import com.njcn.gather.icd.mapping.pojo.bo.analysis.ValidationResult;
import com.njcn.gather.icd.mapping.pojo.dto.IndexBindingCommand;
import com.njcn.gather.icd.mapping.pojo.dto.IndexSelectionGroupCommand;
import org.springframework.stereotype.Service;
import java.util.List;
@@ -15,13 +15,16 @@ import java.util.List;
*
* 校验原则
* 1. 只校验用户明确提交的绑定项
* 2. 要求模板的所有标签都必须配置
* 2. 强制模板的所有标签都必须配置
* 3. label 必须属于当前业务分组的 templateLabels
* 4. lnInst 必须属于当前报告对应的 ICD 候选数字
*/
@Service
public class IndexValidationService {
/**
* 校验前端提交的分组绑定是否能落到当前 ICD 候选范围内
*/
public ValidationResult validate(IndexAnalysis analysis, List<IndexSelectionGroupCommand> selections) {
ValidationResult result = new ValidationResult();
@@ -33,7 +36,7 @@ public class IndexValidationService {
if (selections == null || selections.isEmpty()) {
for (IndexCandidate candidate : analysis.getCandidates()) {
result.getProblems().add(
"报告组【" + candidate.getGroupDesc() + "】未提交绑定关系,请根据 templateLabels 与 reports[*].availableLnInstValues 完成配置"
"组【" + candidate.getGroupDesc() + "】未提交绑定关系,请根据 templateLabels 与 reports[*].availableLnInstValues 完成配置"
);
}
return result;
@@ -62,6 +65,9 @@ public class IndexValidationService {
return result;
}
/**
* 校验单条 label + report + lnInst 绑定
*/
private void validateBinding(IndexCandidate candidate, IndexBindingCommand binding, ValidationResult result) {
if (binding == null) {
result.getProblems().add("存在空的绑定项");
@@ -93,6 +99,9 @@ public class IndexValidationService {
}
}
/**
* 优先按 groupKey 匹配候选分组兼容只传 groupDesc 的旧调用
*/
private IndexCandidate findCandidate(IndexAnalysis analysis, String groupKey, String groupDesc) {
for (IndexCandidate candidate : analysis.getCandidates()) {
if (same(candidate.getGroupKey(), groupKey)) {
@@ -105,6 +114,9 @@ public class IndexValidationService {
return null;
}
/**
* 在候选分组内定位具体报告dataSetName 为空时只按报告名匹配
*/
private IndexCandidateReportItem findReport(IndexCandidate candidate, String reportName, String dataSetName) {
if (candidate.getReports() == null) {
return null;

View File

@@ -1,9 +1,9 @@
package com.njcn.gather.icd.mapping.infrastructure.serializer;
package com.njcn.gather.icd.mapping.component;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.njcn.gather.icd.mapping.domain.model.mapping.MappingDocument;
import com.njcn.gather.icd.mapping.pojo.bo.mapping.MappingDocument;
import org.springframework.stereotype.Component;
/**
@@ -12,7 +12,10 @@ import org.springframework.stereotype.Component;
@Component
public class MappingDocumentSerializer {
/** 紧凑 JSON 输出器,用于减少响应体体积。 */
private final ObjectMapper compactMapper;
/** 格式化 JSON 输出器,用于人工查看和落盘审阅。 */
private final ObjectMapper prettyMapper;
public MappingDocumentSerializer() {
@@ -24,6 +27,9 @@ public class MappingDocumentSerializer {
prettyMapper.enable(SerializationFeature.INDENT_OUTPUT);
}
/**
* 序列化为紧凑 JSON
*/
public String toCompactJson(MappingDocument document) {
try {
return compactMapper.writeValueAsString(document);
@@ -32,6 +38,9 @@ public class MappingDocumentSerializer {
}
}
/**
* 序列化为格式化 JSON
*/
public String toPrettyJson(MappingDocument document) {
try {
return prettyMapper.writeValueAsString(document);

View File

@@ -1,29 +1,29 @@
package com.njcn.gather.icd.mapping.domain.service;
package com.njcn.gather.icd.mapping.component;
import com.njcn.gather.icd.mapping.application.command.IndexBindingCommand;
import com.njcn.gather.icd.mapping.application.command.IndexSelectionGroupCommand;
import com.njcn.gather.icd.mapping.domain.model.analysis.IndexAnalysis;
import com.njcn.gather.icd.mapping.domain.model.analysis.IndexCandidate;
import com.njcn.gather.icd.mapping.domain.model.analysis.IndexCandidateReportItem;
import com.njcn.gather.icd.mapping.domain.model.icd.DataSetNode;
import com.njcn.gather.icd.mapping.domain.model.icd.DoiElementNode;
import com.njcn.gather.icd.mapping.domain.model.icd.DoiNode;
import com.njcn.gather.icd.mapping.domain.model.icd.FcdaNode;
import com.njcn.gather.icd.mapping.domain.model.icd.IcdDocument;
import com.njcn.gather.icd.mapping.domain.model.icd.LnNode;
import com.njcn.gather.icd.mapping.domain.model.icd.ReportControlNode;
import com.njcn.gather.icd.mapping.domain.model.intermediate.DataSetSelectionState;
import com.njcn.gather.icd.mapping.domain.model.intermediate.ReportAndDataSetState;
import com.njcn.gather.icd.mapping.domain.model.intermediate.ReportBindingState;
import com.njcn.gather.icd.mapping.domain.model.intermediate.ReportGroupState;
import com.njcn.gather.icd.mapping.domain.model.mapping.DataSetGroupItem;
import com.njcn.gather.icd.mapping.domain.model.mapping.DoiItem;
import com.njcn.gather.icd.mapping.domain.model.mapping.InstItem;
import com.njcn.gather.icd.mapping.domain.model.mapping.MappingDocument;
import com.njcn.gather.icd.mapping.domain.model.mapping.ReportMapItem;
import com.njcn.gather.icd.mapping.domain.model.mapping.SdiItem;
import com.njcn.gather.icd.mapping.domain.model.mapping.TypeItem;
import com.njcn.gather.icd.mapping.domain.model.template.DefaultTemplate;
import com.njcn.gather.icd.mapping.pojo.dto.IndexBindingCommand;
import com.njcn.gather.icd.mapping.pojo.dto.IndexSelectionGroupCommand;
import com.njcn.gather.icd.mapping.pojo.bo.analysis.IndexAnalysis;
import com.njcn.gather.icd.mapping.pojo.bo.analysis.IndexCandidate;
import com.njcn.gather.icd.mapping.pojo.bo.analysis.IndexCandidateReportItem;
import com.njcn.gather.icd.mapping.pojo.bo.icd.DataSetNode;
import com.njcn.gather.icd.mapping.pojo.bo.icd.DoiElementNode;
import com.njcn.gather.icd.mapping.pojo.bo.icd.DoiNode;
import com.njcn.gather.icd.mapping.pojo.bo.icd.FcdaNode;
import com.njcn.gather.icd.mapping.pojo.bo.icd.IcdDocument;
import com.njcn.gather.icd.mapping.pojo.bo.icd.LnNode;
import com.njcn.gather.icd.mapping.pojo.bo.icd.ReportControlNode;
import com.njcn.gather.icd.mapping.pojo.bo.state.DataSetSelectionState;
import com.njcn.gather.icd.mapping.pojo.bo.state.ReportAndDataSetState;
import com.njcn.gather.icd.mapping.pojo.bo.state.ReportBindingState;
import com.njcn.gather.icd.mapping.pojo.bo.state.ReportGroupState;
import com.njcn.gather.icd.mapping.pojo.bo.mapping.DataSetGroupItem;
import com.njcn.gather.icd.mapping.pojo.bo.mapping.DoiItem;
import com.njcn.gather.icd.mapping.pojo.bo.mapping.InstItem;
import com.njcn.gather.icd.mapping.pojo.bo.mapping.MappingDocument;
import com.njcn.gather.icd.mapping.pojo.bo.mapping.ReportMapItem;
import com.njcn.gather.icd.mapping.pojo.bo.mapping.SdiItem;
import com.njcn.gather.icd.mapping.pojo.bo.mapping.TypeItem;
import com.njcn.gather.icd.mapping.pojo.bo.template.DefaultTemplate;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
@@ -46,6 +46,9 @@ import java.time.format.DateTimeFormatter;
@Service
public class MappingGenerationService {
/**
* 根据 ICD默认模板和用户索引绑定生成最终映射文档
*/
public MappingDocument generate(IcdDocument icdDocument,
DefaultTemplate template,
IndexAnalysis analysis,
@@ -103,6 +106,7 @@ public class MappingGenerationService {
}
// 2. 生成 DataSetList
// DataSetList desc + lnClass 聚合保持输出顺序稳定
Map<String, DataSetGroupItem> dataSetGroupMap = new LinkedHashMap<String, DataSetGroupItem>();
for (DataSetSelectionState selectionState : state.getDataSetSelections()) {
if (selectionState.getLnNodes() == null || selectionState.getLnNodes().isEmpty()) {
@@ -140,6 +144,11 @@ public class MappingGenerationService {
return document;
}
/**
* 构建正式生成前的中间态
*
* 中间态把报告分组用户绑定和命中的 LN 节点拆开保存避免生成 JSON 时重复查找
*/
private ReportAndDataSetState buildState(IcdDocument icdDocument,
DefaultTemplate template,
IndexAnalysis analysis,
@@ -263,6 +272,9 @@ public class MappingGenerationService {
return result;
}
/**
* lnClass 分组下查找或创建指定 lnInst 的输出节点
*/
private InstItem findOrCreateInst(DataSetGroupItem groupItem, String inst, String desc) {
for (InstItem item : groupItem.getInstList()) {
if (equalsTrim(item.getInst(), inst)) {
@@ -372,6 +384,9 @@ public class MappingGenerationService {
instItem.getDoiList().add(doiItem);
}
/**
* 判断 queueList 命中的对象是否应按原 C# 规则跳过
*/
private boolean shouldSkipQueueItem(boolean queueMode, int icdCount, DefaultTemplate.ObjectCfgItem objectCfg) {
if (!queueMode || objectCfg == null) {
return false;
@@ -418,6 +433,9 @@ public class MappingGenerationService {
return false;
}
/**
* 根据用户绑定标签查找对应的 DataObjectsList 配置项
*/
private List<DefaultTemplate.DataObjectCfgItem> findDataObjectCfgItems(DefaultTemplate template, String label) {
List<DefaultTemplate.DataObjectCfgItem> result = new ArrayList<DefaultTemplate.DataObjectCfgItem>();
if (template == null || template.getDataObjectsList() == null) {
@@ -639,6 +657,11 @@ public class MappingGenerationService {
}
}
/**
* 解析 DOI 的倍率系数
*
* 找不到模板倍率配置时默认返回 1.0
*/
private float resolveCoefficient(DoiNode doiNode, DefaultTemplate template) {
Set<String> values = collectAllLeafValues(doiNode.getChildren());
for (String value : values) {
@@ -837,6 +860,9 @@ public class MappingGenerationService {
return false;
}
/**
* 从模板 LnClassList 中查找当前 LN 的业务分组配置
*/
private DefaultTemplate.LnClassCfgItem findLnClassCfg(DefaultTemplate template, String lnClass) {
if (template == null || template.getLnClassList() == null) {
return null;
@@ -850,6 +876,9 @@ public class MappingGenerationService {
}
/**
* 在候选分析结果中定位当前前端回传的业务分组
*/
private IndexCandidate findCandidate(IndexAnalysis analysis, String groupKey, String groupDesc) {
if (analysis == null || analysis.getCandidates() == null) {
return null;
@@ -862,6 +891,9 @@ public class MappingGenerationService {
return null;
}
/**
* 递归收集 DOI 子树下所有 Val 文本用于匹配倍率等模板配置
*/
private Set<String> collectAllLeafValues(List<DoiElementNode> nodes) {
Set<String> result = new LinkedHashSet<String>();
if (nodes == null) {
@@ -880,6 +912,9 @@ public class MappingGenerationService {
return result;
}
/**
* 从模板 UnitList 中查找指定 DAI 值对应的单位配置
*/
private DefaultTemplate.UnitCfgItem findUnitCfg(DefaultTemplate template, String value) {
if (template == null || template.getUnitList() == null) {
return null;
@@ -892,6 +927,9 @@ public class MappingGenerationService {
return null;
}
/**
* 从模板 MultiplierList 中查找指定 DAI 值对应的倍率配置
*/
private DefaultTemplate.MultiplierCfgItem findMultiplierCfg(DefaultTemplate template, String value) {
if (template == null || template.getMultiplierList() == null) {
return null;
@@ -904,6 +942,9 @@ public class MappingGenerationService {
return null;
}
/**
* 将相别节点名称转换为模板中的中文描述
*/
private String resolvePhaseDesc(DefaultTemplate template, String value) {
if (template == null || template.getPhaseList() == null) {
return value;
@@ -916,6 +957,9 @@ public class MappingGenerationService {
return value;
}
/**
* 将类型节点名称转换为模板中的中文描述
*/
private String resolveTypeDesc(DefaultTemplate template, String value) {
if (template == null || template.getTypeList() == null) {
return value;
@@ -929,6 +973,9 @@ public class MappingGenerationService {
}
/**
* 空安全去首尾空白的字符串比较
*/
private boolean equalsTrim(String left, String right) {
if (left == null && right == null) {
return true;
@@ -1002,6 +1049,9 @@ public class MappingGenerationService {
return "96";
}
/**
* 解析输出版本号请求未传时使用当天日期
*/
private String resolveVersion(String version) {
if (version != null && !version.trim().isEmpty()) {
return version.trim();
@@ -1009,6 +1059,9 @@ public class MappingGenerationService {
return LocalDate.now().format(DateTimeFormatter.ISO_LOCAL_DATE);
}
/**
* 按原 C# 规则转换 reportCount单报告写 0多报告写实际数量
*/
private int resolveReportCount(int size) {
return size <= 1 ? 0 : size;
}

View File

@@ -0,0 +1,132 @@
package com.njcn.gather.icd.mapping.component;
import com.njcn.gather.icd.mapping.config.MappingModuleConfig;
import com.njcn.gather.icd.mapping.pojo.dto.GenerateFromIcdCommand;
import com.njcn.gather.icd.mapping.pojo.dto.IndexBindingCommand;
import com.njcn.gather.icd.mapping.pojo.dto.IndexSelectionGroupCommand;
import com.njcn.gather.icd.mapping.pojo.param.GenerateMappingFromIcdRequest;
import com.njcn.gather.icd.mapping.pojo.param.IndexBindingRequest;
import com.njcn.gather.icd.mapping.pojo.param.IndexSelectionGroupRequest;
import com.njcn.gather.icd.mapping.pojo.param.SubmitIndexSelectionRequest;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;
import java.util.List;
/**
* 请求转换器。
*
* 作用:
* 1. 将接口层 request 转成应用层 command
* 2. 统一处理 null 和空集合,避免后续业务层到处判空。
*/
@Component
public class MappingRequestConverter {
/** 模块默认配置,用于补齐作者、输出目录等缺省值。 */
private final MappingModuleConfig moduleConfig;
public MappingRequestConverter(MappingModuleConfig moduleConfig) {
this.moduleConfig = moduleConfig;
}
/**
* 将候选接口上传文件转换为应用层命令,其他参数走模块默认值。
*/
public GenerateFromIcdCommand toCommand(MultipartFile icdFile) {
return toCommand(icdFile, null);
}
/**
* 将上传文件和请求体转换为应用层命令。
*/
public GenerateFromIcdCommand toCommand(MultipartFile icdFile, GenerateMappingFromIcdRequest request) {
try {
if (icdFile == null || icdFile.isEmpty()) {
throw new IllegalArgumentException("ICD 文件不能为空");
}
GenerateFromIcdCommand command = new GenerateFromIcdCommand();
command.setFileName(icdFile.getOriginalFilename());
command.setFileBytes(icdFile.getBytes());
command.setVersion(request == null ? null : request.getVersion());
command.setAuthor(resolveText(request == null ? null : request.getAuthor(), moduleConfig.getDefaultAuthor()));
command.setSaveToDisk(request != null && request.isSaveToDisk());
command.setPrettyJson(request != null && request.isPrettyJson());
command.setOutputDir(resolveText(request == null ? null : request.getOutputDir(), moduleConfig.getDefaultOutputDir()));
fillIndexSelection(command, request == null ? null : request.getIndexSelection());
return command;
} catch (Exception ex) {
throw new IllegalArgumentException("请求转换失败:" + ex.getMessage(), ex);
}
}
/**
* 将提交绑定接口请求体转换为应用层命令。
*/
public GenerateFromIcdCommand toCommand(SubmitIndexSelectionRequest request) {
try {
if (request == null) {
throw new IllegalArgumentException("请求体不能为空");
}
if (request.getIcdDocument() == null) {
throw new IllegalArgumentException("ICD 解析结果不能为空");
}
GenerateFromIcdCommand command = new GenerateFromIcdCommand();
command.setIcdDocument(request.getIcdDocument());
command.setFileName(request.getIcdDocument().getFileName());
command.setVersion(request.getVersion());
command.setAuthor(resolveText(request.getAuthor(), moduleConfig.getDefaultAuthor()));
command.setSaveToDisk(request.isSaveToDisk());
command.setPrettyJson(request.isPrettyJson());
command.setOutputDir(resolveText(request.getOutputDir(), moduleConfig.getDefaultOutputDir()));
fillIndexSelection(command, request.getIndexSelection());
return command;
} catch (Exception ex) {
throw new IllegalArgumentException("请求转换失败:" + ex.getMessage(), ex);
}
}
/**
* 复制前端提交的索引绑定结构。
*/
private void fillIndexSelection(GenerateFromIcdCommand command, List<IndexSelectionGroupRequest> indexSelection) {
if (indexSelection == null) {
return;
}
for (IndexSelectionGroupRequest groupRequest : indexSelection) {
if (groupRequest == null) {
continue;
}
IndexSelectionGroupCommand groupCommand = new IndexSelectionGroupCommand();
groupCommand.setGroupKey(groupRequest.getGroupKey());
groupCommand.setGroupDesc(groupRequest.getGroupDesc());
if (groupRequest.getBindings() != null) {
for (IndexBindingRequest bindingRequest : groupRequest.getBindings()) {
if (bindingRequest == null) {
continue;
}
IndexBindingCommand bindingCommand = new IndexBindingCommand();
bindingCommand.setReportName(bindingRequest.getReportName());
bindingCommand.setDataSetName(bindingRequest.getDataSetName());
bindingCommand.setLabel(bindingRequest.getLabel());
bindingCommand.setLnInst(bindingRequest.getLnInst());
groupCommand.getBindings().add(bindingCommand);
}
}
command.getIndexSelection().add(groupCommand);
}
}
/**
* 统一处理文本参数的 trim 和默认值回退。
*/
private String resolveText(String value, String defaultValue) {
if (value != null && !value.trim().isEmpty()) {
return value.trim();
}
return defaultValue == null ? null : defaultValue.trim();
}
}

View File

@@ -0,0 +1,85 @@
package com.njcn.gather.icd.mapping.component;
import com.njcn.gather.icd.mapping.pojo.bo.GenerateMappingResult;
import com.njcn.gather.icd.mapping.pojo.bo.analysis.IndexCandidate;
import com.njcn.gather.icd.mapping.pojo.bo.analysis.IndexCandidateReportItem;
import com.njcn.gather.icd.mapping.pojo.enums.GenerateStatus;
import com.njcn.gather.icd.mapping.pojo.vo.IndexCandidateReportItemResponse;
import com.njcn.gather.icd.mapping.pojo.vo.IndexCandidateResponse;
import com.njcn.gather.icd.mapping.pojo.vo.MappingTaskResponse;
import org.springframework.stereotype.Component;
/**
* 接口响应转换器。
*
* 按接口阶段仅组装当前场景必需字段,避免返回冗余信息。
*/
@Component
public class MappingResponseConverter {
/**
* 候选接口响应。
*/
public MappingTaskResponse fromCandidateResult(GenerateMappingResult result) {
MappingTaskResponse response = initBaseResponse(result);
response.setIcdDocument(result.getIcdDocument());
fillIndexCandidates(response, result);
return response;
}
/**
* 正式提交类接口响应。
*
* SUCCESS仅返回最终结果
* NEED_INDEX_SELECTION返回重新选择所需的候选信息
* FAILED仅返回错误信息。
*/
public MappingTaskResponse fromSubmitResult(GenerateMappingResult result) {
MappingTaskResponse response = initBaseResponse(result);
if (result.getStatus() == GenerateStatus.SUCCESS) {
response.setMappingJson(result.getMappingJson());
response.setSavedPath(result.getSavedPath());
return response;
}
if (result.getStatus() == GenerateStatus.NEED_INDEX_SELECTION) {
response.setIcdDocument(result.getIcdDocument());
fillIndexCandidates(response, result);
}
return response;
}
private MappingTaskResponse initBaseResponse(GenerateMappingResult result) {
MappingTaskResponse response = new MappingTaskResponse();
response.setStatus(result.getStatus());
response.setMessage(result.getMessage());
response.getProblems().addAll(result.getProblems());
return response;
}
private void fillIndexCandidates(MappingTaskResponse response, GenerateMappingResult result) {
if (result.getIndexAnalysis() == null || result.getIndexAnalysis().getCandidates() == null) {
return;
}
for (IndexCandidate candidate : result.getIndexAnalysis().getCandidates()) {
IndexCandidateResponse candidateResponse = new IndexCandidateResponse();
candidateResponse.setGroupKey(candidate.getGroupKey());
candidateResponse.setGroupDesc(candidate.getGroupDesc());
candidateResponse.setReportCount(candidate.getReportCount());
candidateResponse.getTemplateLabels().addAll(candidate.getTemplateLabels());
if (candidate.getReports() != null) {
for (IndexCandidateReportItem item : candidate.getReports()) {
IndexCandidateReportItemResponse itemResponse = new IndexCandidateReportItemResponse();
itemResponse.setReportName(item.getReportName());
itemResponse.setDataSetName(item.getDataSetName());
itemResponse.setReportDesc(item.getReportDesc());
itemResponse.getAvailableLnInstValues().addAll(item.getAvailableLnInstValues());
candidateResponse.getReports().add(itemResponse);
}
}
response.getIndexCandidates().add(candidateResponse);
}
}
}

View File

@@ -1,17 +1,18 @@
package com.njcn.gather.icd.mapping.config;
import lombok.Getter;
import lombok.Setter;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
/**
* 模块配置。
* 模块配置类。集中管理默认作者、默认模板路径等可配置项。
* `mms-mapping` 模块配置。
*
* 说明:
* 1. 这里把模板路径、输出目录、默认作者等集中管理。
* 2. 当前先用 @Value + 默认值,后续你也可以改成 @ConfigurationProperties。
* 统一管理默认模板路径、默认作者和默认输出目录等配置项。
*/
@Component
@Getter
@Setter
public class MappingModuleConfig {
/** 默认模板资源路径。 */
@@ -25,28 +26,4 @@ public class MappingModuleConfig {
/** 默认输出目录。 */
@Value("${icd.mapping.default-output-dir:}")
private String defaultOutputDir;
public String getDefaultTemplatePath() {
return defaultTemplatePath;
}
public void setDefaultTemplatePath(String defaultTemplatePath) {
this.defaultTemplatePath = defaultTemplatePath;
}
public String getDefaultAuthor() {
return defaultAuthor;
}
public void setDefaultAuthor(String defaultAuthor) {
this.defaultAuthor = defaultAuthor;
}
public String getDefaultOutputDir() {
return defaultOutputDir;
}
public void setDefaultOutputDir(String defaultOutputDir) {
this.defaultOutputDir = defaultOutputDir;
}
}
}

View File

@@ -1,50 +1,92 @@
package com.njcn.gather.icd.mapping.controller;
import com.njcn.gather.icd.mapping.application.MappingTaskAppService;
import com.njcn.gather.icd.mapping.application.command.GenerateFromIcdCommand;
import com.njcn.gather.icd.mapping.application.result.GenerateMappingResult;
import com.njcn.gather.icd.mapping.controller.request.GenerateMappingFromIcdRequest;
import com.njcn.gather.icd.mapping.controller.response.MappingTaskResponse;
import com.njcn.gather.icd.mapping.converter.MappingRequestConverter;
import com.njcn.gather.icd.mapping.converter.MappingResponseConverter;
import com.njcn.common.pojo.annotation.OperateInfo;
import com.njcn.common.pojo.enums.common.LogEnum;
import com.njcn.common.utils.LogUtil;
import com.njcn.gather.icd.mapping.component.MappingRequestConverter;
import com.njcn.gather.icd.mapping.component.MappingResponseConverter;
import com.njcn.gather.icd.mapping.pojo.bo.GenerateMappingResult;
import com.njcn.gather.icd.mapping.pojo.dto.GenerateFromIcdCommand;
import com.njcn.gather.icd.mapping.pojo.param.GenerateMappingFromIcdRequest;
import com.njcn.gather.icd.mapping.pojo.param.SubmitIndexSelectionRequest;
import com.njcn.gather.icd.mapping.pojo.vo.MappingTaskResponse;
import com.njcn.gather.icd.mapping.service.MappingTaskService;
import com.njcn.gather.icd.mapping.utils.DateUtils;
import com.njcn.web.controller.BaseController;
import io.swagger.annotations.Api;
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.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;
/**
* ICD 映射接口。
* ICD 映射接口入口
*/
@Slf4j
@Api(tags = "ICD 映射")
@RestController
@RequestMapping("/api/mms-mapping")
public class MappingController {
@RequiredArgsConstructor
public class MappingController extends BaseController {
private final MappingTaskAppService mappingTaskAppService;
/** 映射任务编排服务,负责从 ICD 解析到映射生成的完整链路。 */
private final MappingTaskService mappingTaskService;
/** 请求参数转换器,将接口入参转换为应用层命令。 */
private final MappingRequestConverter requestConverter;
/** 响应转换器,按接口阶段裁剪最小返回字段。 */
private final MappingResponseConverter responseConverter;
public MappingController(MappingTaskAppService mappingTaskAppService,
MappingRequestConverter requestConverter,
MappingResponseConverter responseConverter) {
this.mappingTaskAppService = mappingTaskAppService;
this.requestConverter = requestConverter;
this.responseConverter = responseConverter;
/**
* 上传 ICD 文件,返回候选结果和可编辑的 ICD 解析结果。
*/
@OperateInfo(info = LogEnum.BUSINESS_COMMON)
@ApiOperation("上传 ICD 文件并生成索引候选")
@PostMapping(value = "/get-icd", consumes = {"multipart/form-data"})
public MappingTaskResponse getICD(@RequestPart("icdFile") MultipartFile icdFile) {
String methodDescribe = getMethodDescribe("getICD");
LogUtil.njcnDebug(log, "{},开始解析 ICD 文件并生成索引候选fileName={}", methodDescribe, DateUtils.resolveFileName(icdFile));
GenerateFromIcdCommand command = requestConverter.toCommand(icdFile);
GenerateMappingResult result = mappingTaskService.getICD(command);
return responseConverter.fromCandidateResult(result);
}
/**
* 上传 ICD 并生成映射
*
* 表单参数:
* 1. icdFileICD 文件
* 2. requestJSON 请求体
* 根据前端确认后的索引绑定关系生成 MMS JSON
*/
@PostMapping(value = "/generate-from-icd", consumes = {"multipart/form-data"})
public MappingTaskResponse generateFromIcd(@RequestPart("icdFile") MultipartFile icdFile,
@Validated @RequestPart("request") GenerateMappingFromIcdRequest request) {
@OperateInfo(info = LogEnum.BUSINESS_COMMON)
@ApiOperation("获取 MMS JSON")
@PostMapping("/get-mms-json")
public MappingTaskResponse getMmsJson(@Validated @RequestBody SubmitIndexSelectionRequest request) {
String methodDescribe = getMethodDescribe("getMmsJson");
LogUtil.njcnDebug(log, "{},开始生成 MMS JSONindexSelectionCount={}",
methodDescribe, DateUtils.resolveSelectionCount(request == null ? null : request.getIndexSelection()));
GenerateFromIcdCommand command = requestConverter.toCommand(request);
GenerateMappingResult result = mappingTaskService.getMmsJson(command);
return responseConverter.fromSubmitResult(result);
}
/**
* 上传 ICD 后直接串联候选生成和索引提交,统一返回正式提交阶段结果。
*/
@OperateInfo(info = LogEnum.BUSINESS_COMMON)
@ApiOperation("上传 ICD 后直接获取 MMS JSON")
@PostMapping(value = "/get-icd-mms-json", consumes = {"multipart/form-data"})
public MappingTaskResponse getIcdMmsJson(@RequestPart("icdFile") MultipartFile icdFile,
@Validated @RequestPart("request") GenerateMappingFromIcdRequest request) {
String methodDescribe = getMethodDescribe("getIcdMmsJson");
LogUtil.njcnDebug(log, "{},开始获取 ICD MMS JSONfileName={}, indexSelectionCount={}",
methodDescribe, DateUtils.resolveFileName(icdFile),
DateUtils.resolveSelectionCount(request == null ? null : request.getIndexSelection()));
GenerateFromIcdCommand command = requestConverter.toCommand(icdFile, request);
GenerateMappingResult result = mappingTaskAppService.generateFromIcd(command);
return responseConverter.fromResult(result);
GenerateMappingResult result = mappingTaskService.getIcdMmsJson(command);
return responseConverter.fromSubmitResult(result);
}
}

View File

@@ -1,89 +0,0 @@
package com.njcn.gather.icd.mapping.controller.request;
import java.util.ArrayList;
import java.util.List;
/**
* 生成映射接口请求体。
*
* 说明:
* 1. 旧版结构中indexSelection 是 Map<String, String>,只能表达“一个报告对应一个值”,
* 无法表达“一个业务分组下有多个报告、一个报告下又有多条标签绑定”的真实场景。
* 2. 新版结构改成 List<IndexSelectionGroupRequest>,用来完整承载用户在前端完成的绑定结果。
* 3. 第一次只上传 ICD 时,这个字段可以为空;第二次用户确认绑定后再把完整结构上送即可。
*/
public class GenerateMappingFromIcdRequest {
/** 输出版本号。为空时后端默认补 1.0。 */
private String version;
/** 作者。为空时默认空字符串。 */
private String author;
/** 是否保存到磁盘。 */
private boolean saveToDisk;
/** 是否返回美化 JSON。 */
private boolean prettyJson;
/** 输出目录。saveToDisk=true 时才会用到。 */
private String outputDir;
/**
* 索引选择结果。
*
* 说明:
* 1. 每一个元素代表一个“业务分组”,例如:实时数据、统计数据、波动闪变。
* 2. 每个业务分组下面又包含多条绑定关系。
* 3. 允许为空;为空时后端返回 NEED_INDEX_SELECTION给前端候选参考项。
*/
private List<IndexSelectionGroupRequest> indexSelection = new ArrayList<IndexSelectionGroupRequest>();
public String getVersion() {
return version;
}
public void setVersion(String version) {
this.version = version;
}
public String getAuthor() {
return author;
}
public void setAuthor(String author) {
this.author = author;
}
public boolean isSaveToDisk() {
return saveToDisk;
}
public void setSaveToDisk(boolean saveToDisk) {
this.saveToDisk = saveToDisk;
}
public boolean isPrettyJson() {
return prettyJson;
}
public void setPrettyJson(boolean prettyJson) {
this.prettyJson = prettyJson;
}
public String getOutputDir() {
return outputDir;
}
public void setOutputDir(String outputDir) {
this.outputDir = outputDir;
}
public List<IndexSelectionGroupRequest> getIndexSelection() {
return indexSelection;
}
public void setIndexSelection(List<IndexSelectionGroupRequest> indexSelection) {
this.indexSelection = indexSelection;
}
}

View File

@@ -1,59 +0,0 @@
package com.njcn.gather.icd.mapping.controller.request;
/**
* 单条索引绑定请求。
*
* 一条绑定只表达一个最小关系:
* 某个报告(reportName)下,使用某个标签(label)与某个 lnInst 数字做绑定。
*
* 这样做的好处:
* 1. 一个报告可以出现多次,对应多个标签。
* 2. 一个业务分组下也可以有多个报告。
* 3. 后端校验、生成映射时都更容易逐条处理。
*/
public class IndexBindingRequest {
/** 绑定发生在哪个报告上例如brcbStHarm。 */
private String reportName;
/** 绑定发生在哪个数据集上例如dsStHarm。 */
private String dataSetName;
/** 业务标签,例如:最大值、最小值、实时数据。 */
private String label;
/** 当前标签最终绑定到的 lnInst 数字例如1、2、8。 */
private String lnInst;
public String getReportName() {
return reportName;
}
public void setReportName(String reportName) {
this.reportName = reportName;
}
public String getDataSetName() {
return dataSetName;
}
public void setDataSetName(String dataSetName) {
this.dataSetName = dataSetName;
}
public String getLabel() {
return label;
}
public void setLabel(String label) {
this.label = label;
}
public String getLnInst() {
return lnInst;
}
public void setLnInst(String lnInst) {
this.lnInst = lnInst;
}
}

View File

@@ -1,53 +0,0 @@
package com.njcn.gather.icd.mapping.controller.request;
import java.util.ArrayList;
import java.util.List;
/**
* 单个业务分组的索引选择请求。
*
* 例如:
* - groupKey = REALTIME_DATA
* - groupDesc = 实时数据
* - bindings = 多条“报告 + 标签 + lnInst”的绑定关系
*/
public class IndexSelectionGroupRequest {
/**
* 分组唯一键。
*
* 说明:
* 这个值由后端在 NEED_INDEX_SELECTION 时返回,前端原样带回即可,避免仅靠中文描述做匹配。
*/
private String groupKey;
/** 分组中文描述,例如:实时数据、统计数据。 */
private String groupDesc;
/** 当前业务分组下,用户最终确认的绑定关系。 */
private List<IndexBindingRequest> bindings = new ArrayList<IndexBindingRequest>();
public String getGroupKey() {
return groupKey;
}
public void setGroupKey(String groupKey) {
this.groupKey = groupKey;
}
public String getGroupDesc() {
return groupDesc;
}
public void setGroupDesc(String groupDesc) {
this.groupDesc = groupDesc;
}
public List<IndexBindingRequest> getBindings() {
return bindings;
}
public void setBindings(List<IndexBindingRequest> bindings) {
this.bindings = bindings;
}
}

View File

@@ -1,54 +0,0 @@
package com.njcn.gather.icd.mapping.controller.response;
import java.util.ArrayList;
import java.util.List;
/**
* 索引候选分组下的单个报告响应。
*/
public class IndexCandidateReportItemResponse {
/** 报告名称。 */
private String reportName;
/** 数据集名称。 */
private String dataSetName;
/** 报告描述。 */
private String reportDesc;
/** 当前报告可选的 lnInst 数字。 */
private List<String> availableLnInstValues = new ArrayList<String>();
public String getReportName() {
return reportName;
}
public void setReportName(String reportName) {
this.reportName = reportName;
}
public String getDataSetName() {
return dataSetName;
}
public void setDataSetName(String dataSetName) {
this.dataSetName = dataSetName;
}
public String getReportDesc() {
return reportDesc;
}
public void setReportDesc(String reportDesc) {
this.reportDesc = reportDesc;
}
public List<String> getAvailableLnInstValues() {
return availableLnInstValues;
}
public void setAvailableLnInstValues(List<String> availableLnInstValues) {
this.availableLnInstValues = availableLnInstValues;
}
}

View File

@@ -1,71 +0,0 @@
package com.njcn.gather.icd.mapping.controller.response;
import java.util.ArrayList;
import java.util.List;
/**
* 索引候选返回对象。
*
* 说明:
* 这是给前端“待匹配界面”使用的正式结构:
* - 一个候选就是一个业务分组;
* - 分组下面再挂多个报告;
* - 前端根据 templateLabels 与 reports[*].availableLnInstValues 做人工绑定。
*/
public class IndexCandidateResponse {
/** 分组唯一键。 */
private String groupKey;
/** 分组中文描述。 */
private String groupDesc;
/** 当前分组包含的报告数。 */
private int reportCount;
/** 模板里配置的可选标签。 */
private List<String> templateLabels = new ArrayList<String>();
/** 当前分组下的报告候选列表。 */
private List<IndexCandidateReportItemResponse> reports = new ArrayList<IndexCandidateReportItemResponse>();
public String getGroupKey() {
return groupKey;
}
public void setGroupKey(String groupKey) {
this.groupKey = groupKey;
}
public String getGroupDesc() {
return groupDesc;
}
public void setGroupDesc(String groupDesc) {
this.groupDesc = groupDesc;
}
public int getReportCount() {
return reportCount;
}
public void setReportCount(int reportCount) {
this.reportCount = reportCount;
}
public List<String> getTemplateLabels() {
return templateLabels;
}
public void setTemplateLabels(List<String> templateLabels) {
this.templateLabels = templateLabels;
}
public List<IndexCandidateReportItemResponse> getReports() {
return reports;
}
public void setReports(List<IndexCandidateReportItemResponse> reports) {
this.reports = reports;
}
}

View File

@@ -1,26 +0,0 @@
package com.njcn.gather.icd.mapping.controller.response;
/**
* 映射摘要响应。
* 映射摘要响应 DTO。用于把最终映射的关键信息单独封装返回。
*/
public class MappingDocumentResponse {
private String version;
private String author;
private String ied;
private String ld;
private int reportCount;
private int dataSetCount;
public String getVersion() { return version; }
public void setVersion(String version) { this.version = version; }
public String getAuthor() { return author; }
public void setAuthor(String author) { this.author = author; }
public String getIed() { return ied; }
public void setIed(String ied) { this.ied = ied; }
public String getLd() { return ld; }
public void setLd(String ld) { this.ld = ld; }
public int getReportCount() { return reportCount; }
public void setReportCount(int reportCount) { this.reportCount = reportCount; }
public int getDataSetCount() { return dataSetCount; }
public void setDataSetCount(int dataSetCount) { this.dataSetCount = dataSetCount; }
}

View File

@@ -1,40 +0,0 @@
package com.njcn.gather.icd.mapping.controller.response;
import com.njcn.gather.icd.mapping.enums.GenerateStatus;
import java.util.ArrayList;
import java.util.List;
/**
* 第一个接口统一响应。
* 接口统一响应 DTO。返回生成状态、映射内容、候选索引、问题列表等。
*/
public class MappingTaskResponse {
private GenerateStatus status;
private String message;
private String iedName;
private String ldInst;
private String mappingJson;
private String savedPath;
private MappingDocumentResponse mappingDocument;
private List<IndexCandidateResponse> indexCandidates = new ArrayList<IndexCandidateResponse>();
private List<String> problems = new ArrayList<String>();
public GenerateStatus getStatus() { return status; }
public void setStatus(GenerateStatus status) { this.status = status; }
public String getMessage() { return message; }
public void setMessage(String message) { this.message = message; }
public String getIedName() { return iedName; }
public void setIedName(String iedName) { this.iedName = iedName; }
public String getLdInst() { return ldInst; }
public void setLdInst(String ldInst) { this.ldInst = ldInst; }
public String getMappingJson() { return mappingJson; }
public void setMappingJson(String mappingJson) { this.mappingJson = mappingJson; }
public String getSavedPath() { return savedPath; }
public void setSavedPath(String savedPath) { this.savedPath = savedPath; }
public MappingDocumentResponse getMappingDocument() { return mappingDocument; }
public void setMappingDocument(MappingDocumentResponse mappingDocument) { this.mappingDocument = mappingDocument; }
public List<IndexCandidateResponse> getIndexCandidates() { return indexCandidates; }
public void setIndexCandidates(List<IndexCandidateResponse> indexCandidates) { this.indexCandidates = indexCandidates; }
public List<String> getProblems() { return problems; }
public void setProblems(List<String> problems) { this.problems = problems; }
}

View File

@@ -1,82 +0,0 @@
package com.njcn.gather.icd.mapping.converter;
import com.njcn.gather.icd.mapping.application.command.GenerateFromIcdCommand;
import com.njcn.gather.icd.mapping.application.command.IndexBindingCommand;
import com.njcn.gather.icd.mapping.application.command.IndexSelectionGroupCommand;
import com.njcn.gather.icd.mapping.config.MappingModuleConfig;
import com.njcn.gather.icd.mapping.controller.request.GenerateMappingFromIcdRequest;
import com.njcn.gather.icd.mapping.controller.request.IndexBindingRequest;
import com.njcn.gather.icd.mapping.controller.request.IndexSelectionGroupRequest;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;
/**
* 请求转换器。
*
* 作用:
* 1. 把接口层 request 转成应用层 command。
* 2. 统一处理 null 和空集合,避免后面业务层到处判空。
*/
@Component
public class MappingRequestConverter {
private final MappingModuleConfig moduleConfig;
public MappingRequestConverter(MappingModuleConfig moduleConfig) {
this.moduleConfig = moduleConfig;
}
public GenerateFromIcdCommand toCommand(MultipartFile icdFile, GenerateMappingFromIcdRequest request) {
try {
if (icdFile == null || icdFile.isEmpty()) {
throw new IllegalArgumentException("ICD 文件不能为空");
}
GenerateFromIcdCommand command = new GenerateFromIcdCommand();
command.setFileName(icdFile.getOriginalFilename());
command.setFileBytes(icdFile.getBytes());
command.setVersion(request == null ? null : request.getVersion());
command.setAuthor(resolveText(request == null ? null : request.getAuthor(), moduleConfig.getDefaultAuthor()));
command.setSaveToDisk(request != null && request.isSaveToDisk());
command.setPrettyJson(request == null || request.isPrettyJson());
command.setOutputDir(resolveText(request == null ? null : request.getOutputDir(), moduleConfig.getDefaultOutputDir()));
if (request != null && request.getIndexSelection() != null) {
for (IndexSelectionGroupRequest groupRequest : request.getIndexSelection()) {
if (groupRequest == null) {
continue;
}
IndexSelectionGroupCommand groupCommand = new IndexSelectionGroupCommand();
groupCommand.setGroupKey(groupRequest.getGroupKey());
groupCommand.setGroupDesc(groupRequest.getGroupDesc());
if (groupRequest.getBindings() != null) {
for (IndexBindingRequest bindingRequest : groupRequest.getBindings()) {
if (bindingRequest == null) {
continue;
}
IndexBindingCommand bindingCommand = new IndexBindingCommand();
bindingCommand.setReportName(bindingRequest.getReportName());
bindingCommand.setDataSetName(bindingRequest.getDataSetName());
bindingCommand.setLabel(bindingRequest.getLabel());
bindingCommand.setLnInst(bindingRequest.getLnInst());
groupCommand.getBindings().add(bindingCommand);
}
}
command.getIndexSelection().add(groupCommand);
}
}
return command;
} catch (Exception ex) {
throw new IllegalArgumentException("请求转换失败:" + ex.getMessage(), ex);
}
}
private String resolveText(String value, String defaultValue) {
if (value != null && !value.trim().isEmpty()) {
return value.trim();
}
return defaultValue == null ? null : defaultValue.trim();
}
}

View File

@@ -1,70 +0,0 @@
package com.njcn.gather.icd.mapping.converter;
import com.njcn.gather.icd.mapping.application.result.GenerateMappingResult;
import com.njcn.gather.icd.mapping.controller.response.IndexCandidateReportItemResponse;
import com.njcn.gather.icd.mapping.controller.response.IndexCandidateResponse;
import com.njcn.gather.icd.mapping.controller.response.MappingDocumentResponse;
import com.njcn.gather.icd.mapping.controller.response.MappingTaskResponse;
import com.njcn.gather.icd.mapping.domain.model.analysis.IndexCandidate;
import com.njcn.gather.icd.mapping.domain.model.analysis.IndexCandidateReportItem;
import org.springframework.stereotype.Component;
/**
* 响应转换器。
*
* 作用:
* 1. 把应用层结果转换成接口层响应对象;
* 2. 对待匹配索引场景,输出新的“按业务分组返回”的结构。
*/
@Component
public class MappingResponseConverter {
public MappingTaskResponse fromResult(GenerateMappingResult result) {
MappingTaskResponse response = new MappingTaskResponse();
response.setStatus(result.getStatus());
response.setMessage(result.getMessage());
response.setIedName(result.getIedName());
response.setLdInst(result.getLdInst());
response.setMappingJson(result.getMappingJson());
response.setSavedPath(result.getSavedPath());
response.getProblems().addAll(result.getProblems());
if (result.getIndexAnalysis() != null && result.getIndexAnalysis().getCandidates() != null) {
for (IndexCandidate candidate : result.getIndexAnalysis().getCandidates()) {
IndexCandidateResponse candidateResponse = new IndexCandidateResponse();
candidateResponse.setGroupKey(candidate.getGroupKey());
candidateResponse.setGroupDesc(candidate.getGroupDesc());
candidateResponse.setReportCount(candidate.getReportCount());
candidateResponse.getTemplateLabels().addAll(candidate.getTemplateLabels());
if (candidate.getReports() != null) {
for (IndexCandidateReportItem item : candidate.getReports()) {
IndexCandidateReportItemResponse itemResponse = new IndexCandidateReportItemResponse();
itemResponse.setReportName(item.getReportName());
itemResponse.setDataSetName(item.getDataSetName());
itemResponse.setReportDesc(item.getReportDesc());
itemResponse.getAvailableLnInstValues().addAll(item.getAvailableLnInstValues());
candidateResponse.getReports().add(itemResponse);
}
}
response.getIndexCandidates().add(candidateResponse);
}
}
if (result.getMappingDocument() != null) {
MappingDocumentResponse doc = new MappingDocumentResponse();
doc.setVersion(result.getMappingDocument().getVersion());
doc.setAuthor(result.getMappingDocument().getAuthor());
doc.setIed(result.getMappingDocument().getIed());
doc.setLd(result.getMappingDocument().getLd());
doc.setReportCount(result.getMappingDocument().getReportMap() == null
? 0 : result.getMappingDocument().getReportMap().size());
doc.setDataSetCount(result.getMappingDocument().getDataSetList() == null
? 0 : result.getMappingDocument().getDataSetList().size());
response.setMappingDocument(doc);
}
return response;
}
}

View File

@@ -1,113 +0,0 @@
package com.njcn.gather.icd.mapping.domain.model.analysis;
import java.util.ArrayList;
import java.util.List;
/**
* 索引候选分组。
*
* 说明:
* 1. 一条候选对应一个业务分组,例如:统计数据、实时数据;
* 2. 一个业务分组下可以包含多个报告;
* 3. 这里不仅保存返回给前端的候选项,也保存从 DefaultCfg.ReportList 带下来的配置项,
* 供后续 MappingGenerationService 直接使用,避免“二次查模板”失败。
*/
public class IndexCandidate {
/** 分组唯一键。 */
private String groupKey;
/** 分组描述。 */
private String groupDesc;
/** 该分组下实际匹配到的报告数量。 */
private int reportCount;
/** DefaultCfg.txt 中该分组可用的标签模板。 */
private List<String> templateLabels = new ArrayList<String>();
/** 当前分组下匹配到的报告列表。 */
private List<IndexCandidateReportItem> reports = new ArrayList<IndexCandidateReportItem>();
/**
* DefaultCfg.ReportList.inst
* 例如01 / 02 / 03 / 04
*/
private String reportInst;
/**
* DefaultCfg.ReportList.Select
* 例如DataStatFileMap / DataRealFileMap / FlickerFileMap
*/
private String select;
/**
* DefaultCfg.ReportList.TrgOps
* 例如40 / 96
*/
private String trgOps;
public String getGroupKey() {
return groupKey;
}
public void setGroupKey(String groupKey) {
this.groupKey = groupKey;
}
public String getGroupDesc() {
return groupDesc;
}
public void setGroupDesc(String groupDesc) {
this.groupDesc = groupDesc;
}
public int getReportCount() {
return reportCount;
}
public void setReportCount(int reportCount) {
this.reportCount = reportCount;
}
public List<String> getTemplateLabels() {
return templateLabels;
}
public void setTemplateLabels(List<String> templateLabels) {
this.templateLabels = templateLabels;
}
public List<IndexCandidateReportItem> getReports() {
return reports;
}
public void setReports(List<IndexCandidateReportItem> reports) {
this.reports = reports;
}
public String getReportInst() {
return reportInst;
}
public void setReportInst(String reportInst) {
this.reportInst = reportInst;
}
public String getSelect() {
return select;
}
public void setSelect(String select) {
this.select = select;
}
public String getTrgOps() {
return trgOps;
}
public void setTrgOps(String trgOps) {
this.trgOps = trgOps;
}
}

View File

@@ -1,56 +0,0 @@
package com.njcn.gather.icd.mapping.domain.model.analysis;
import java.util.ArrayList;
import java.util.List;
/**
* 候选分组下的单个报告项。
*
* 这个对象专门给前端展示“这个分组下面有哪些报告,以及每个报告对应哪些可用 lnInst 数字”。
*/
public class IndexCandidateReportItem {
/** 报告名称。 */
private String reportName;
/** 数据集名称。 */
private String dataSetName;
/** 报告中文描述。一般和分组描述相同,保留它是为了前端渲染更灵活。 */
private String reportDesc;
/** 当前报告在 ICD 中解析出来的所有可选 lnInst 数字。 */
private List<String> availableLnInstValues = new ArrayList<String>();
public String getReportName() {
return reportName;
}
public void setReportName(String reportName) {
this.reportName = reportName;
}
public String getDataSetName() {
return dataSetName;
}
public void setDataSetName(String dataSetName) {
this.dataSetName = dataSetName;
}
public String getReportDesc() {
return reportDesc;
}
public void setReportDesc(String reportDesc) {
this.reportDesc = reportDesc;
}
public List<String> getAvailableLnInstValues() {
return availableLnInstValues;
}
public void setAvailableLnInstValues(List<String> availableLnInstValues) {
this.availableLnInstValues = availableLnInstValues;
}
}

View File

@@ -1,18 +0,0 @@
package com.njcn.gather.icd.mapping.domain.model.analysis;
import java.util.ArrayList;
import java.util.List;
/**
* 校验结果。
* 索引校验结果模型。保存是否通过以及问题列表。
*/
public class ValidationResult {
private boolean valid;
private List<String> problems = new ArrayList<String>();
public boolean isValid() { return valid; }
public void setValid(boolean valid) { this.valid = valid; }
public List<String> getProblems() { return problems; }
public void setProblems(List<String> problems) { this.problems = problems; }
}

View File

@@ -1,17 +0,0 @@
package com.njcn.gather.icd.mapping.domain.model.icd;
import java.util.ArrayList;
import java.util.List;
/**
* DataSet 节点。
* 数据集模型。用于承接 DataSet 下的 FCDA 列表。
*/
public class DataSetNode {
private String name;
private List<FcdaNode> fcdas = new ArrayList<FcdaNode>();
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public List<FcdaNode> getFcdas() { return fcdas; }
public void setFcdas(List<FcdaNode> fcdas) { this.fcdas = fcdas; }
}

View File

@@ -1,31 +0,0 @@
package com.njcn.gather.icd.mapping.domain.model.icd;
import java.util.ArrayList;
import java.util.List;
/**
* DOI/SDI/DAI 统一节点。
* DOI/SDI/DAI 细项模型。用于递归承接 DOI 树明细。
*
* kind 常见值:
* - SDI
* - DAI
*/
public class DoiElementNode {
private String kind;
private String name;
private Long ix;
private List<String> values = new ArrayList<String>();
private List<DoiElementNode> children = new ArrayList<DoiElementNode>();
public String getKind() { return kind; }
public void setKind(String kind) { this.kind = kind; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public Long getIx() { return ix; }
public void setIx(Long ix) { this.ix = ix; }
public List<String> getValues() { return values; }
public void setValues(List<String> values) { this.values = values; }
public List<DoiElementNode> getChildren() { return children; }
public void setChildren(List<DoiElementNode> children) { this.children = children; }
}

View File

@@ -1,30 +0,0 @@
package com.njcn.gather.icd.mapping.domain.model.icd;
import java.util.ArrayList;
import java.util.List;
/**
* DOI 节点。
* DOI 模型。表示逻辑节点下的一个数据对象节点。
*/
public class DoiNode {
private String name;
private Long ix;
private String lnClass;
private String lnInst;
private int sequenceCount;
private List<DoiElementNode> children = new ArrayList<DoiElementNode>();
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public Long getIx() { return ix; }
public void setIx(Long ix) { this.ix = ix; }
public String getLnClass() { return lnClass; }
public void setLnClass(String lnClass) { this.lnClass = lnClass; }
public String getLnInst() { return lnInst; }
public void setLnInst(String lnInst) { this.lnInst = lnInst; }
public int getSequenceCount() { return sequenceCount; }
public void setSequenceCount(int sequenceCount) { this.sequenceCount = sequenceCount; }
public List<DoiElementNode> getChildren() { return children; }
public void setChildren(List<DoiElementNode> children) { this.children = children; }
}

View File

@@ -1,36 +0,0 @@
package com.njcn.gather.icd.mapping.domain.model.icd;
/**
* FCDA 节点。
* FCDA 模型。保存 lnClass、lnInst、doName、daName、fc、ix 等信息。
*/
public class FcdaNode {
private String ldInst;
private String prefix;
private String lnClass;
private String lnInst;
private String doName;
private String daName;
private String fc;
private Long ix;
private int sequenceCount;
public String getLdInst() { return ldInst; }
public void setLdInst(String ldInst) { this.ldInst = ldInst; }
public String getPrefix() { return prefix; }
public void setPrefix(String prefix) { this.prefix = prefix; }
public String getLnClass() { return lnClass; }
public void setLnClass(String lnClass) { this.lnClass = lnClass; }
public String getLnInst() { return lnInst; }
public void setLnInst(String lnInst) { this.lnInst = lnInst; }
public String getDoName() { return doName; }
public void setDoName(String doName) { this.doName = doName; }
public String getDaName() { return daName; }
public void setDaName(String daName) { this.daName = daName; }
public String getFc() { return fc; }
public void setFc(String fc) { this.fc = fc; }
public Long getIx() { return ix; }
public void setIx(Long ix) { this.ix = ix; }
public int getSequenceCount() { return sequenceCount; }
public void setSequenceCount(int sequenceCount) { this.sequenceCount = sequenceCount; }
}

View File

@@ -1,46 +0,0 @@
package com.njcn.gather.icd.mapping.domain.model.icd;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
* ICD 文档聚合根。
* ICD 统一领域模型的聚合根。承接 IED、LDevice、ReportControl、DataSet、LN 等解析结果。
*
* 说明:
* 1. 这是系统内部统一的 ICD 模型。
* 2. 外部 JAXB generated 类只在 parser 层使用。
* 3. 业务层全部依赖这个标准化模型,便于后续替换解析实现。
*/
public class IcdDocument {
private String fileName;
private String iedName;
private String ldInst;
private String ldPrefix;
private IedNode primaryIed;
private LogicalDeviceNode logicalDevice;
private List<LnNode> logicalNodes = new ArrayList<LnNode>();
private List<ReportControlNode> reportControls = new ArrayList<ReportControlNode>();
private Map<String, DataSetNode> dataSets = new LinkedHashMap<String, DataSetNode>();
public String getFileName() { return fileName; }
public void setFileName(String fileName) { this.fileName = fileName; }
public String getIedName() { return iedName; }
public void setIedName(String iedName) { this.iedName = iedName; }
public String getLdInst() { return ldInst; }
public void setLdInst(String ldInst) { this.ldInst = ldInst; }
public String getLdPrefix() { return ldPrefix; }
public void setLdPrefix(String ldPrefix) { this.ldPrefix = ldPrefix; }
public IedNode getPrimaryIed() { return primaryIed; }
public void setPrimaryIed(IedNode primaryIed) { this.primaryIed = primaryIed; }
public LogicalDeviceNode getLogicalDevice() { return logicalDevice; }
public void setLogicalDevice(LogicalDeviceNode logicalDevice) { this.logicalDevice = logicalDevice; }
public List<LnNode> getLogicalNodes() { return logicalNodes; }
public void setLogicalNodes(List<LnNode> logicalNodes) { this.logicalNodes = logicalNodes; }
public List<ReportControlNode> getReportControls() { return reportControls; }
public void setReportControls(List<ReportControlNode> reportControls) { this.reportControls = reportControls; }
public Map<String, DataSetNode> getDataSets() { return dataSets; }
public void setDataSets(Map<String, DataSetNode> dataSets) { this.dataSets = dataSets; }
}

View File

@@ -1,20 +0,0 @@
package com.njcn.gather.icd.mapping.domain.model.icd;
import java.util.ArrayList;
import java.util.List;
/**
* IED 节点。
* IED 节点模型。保存 IED 名称以及其下逻辑设备引用。
*/
public class IedNode {
private String name;
private List<String> accessPointNames = new ArrayList<String>();
private List<String> lDeviceInstList = new ArrayList<String>();
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public List<String> getAccessPointNames() { return accessPointNames; }
public void setAccessPointNames(List<String> accessPointNames) { this.accessPointNames = accessPointNames; }
public List<String> getLDeviceInstList() { return lDeviceInstList; }
public void setLDeviceInstList(List<String> lDeviceInstList) { this.lDeviceInstList = lDeviceInstList; }
}

View File

@@ -1,32 +0,0 @@
package com.njcn.gather.icd.mapping.domain.model.icd;
import java.util.ArrayList;
import java.util.List;
/**
* 逻辑节点。
* 逻辑节点模型。保存 lnClass、lnInst、prefix、DOI 等信息。
*
* LN0 和 LN 最终都统一抽象为这个模型。
*/
public class LnNode {
private boolean ln0;
private String prefix;
private String lnClass;
private String lnInst;
private String lnType;
private List<DoiNode> doiList = new ArrayList<DoiNode>();
public boolean isLn0() { return ln0; }
public void setLn0(boolean ln0) { this.ln0 = ln0; }
public String getPrefix() { return prefix; }
public void setPrefix(String prefix) { this.prefix = prefix; }
public String getLnClass() { return lnClass; }
public void setLnClass(String lnClass) { this.lnClass = lnClass; }
public String getLnInst() { return lnInst; }
public void setLnInst(String lnInst) { this.lnInst = lnInst; }
public String getLnType() { return lnType; }
public void setLnType(String lnType) { this.lnType = lnType; }
public List<DoiNode> getDoiList() { return doiList; }
public void setDoiList(List<DoiNode> doiList) { this.doiList = doiList; }
}

View File

@@ -1,14 +0,0 @@
package com.njcn.gather.icd.mapping.domain.model.icd;
/**
* 逻辑设备节点。
* 逻辑设备模型。保存 inst、prefix 以及其下 LN/LN0 列表。
*/
public class LogicalDeviceNode {
private String inst;
private String ldName;
public String getInst() { return inst; }
public void setInst(String inst) { this.inst = inst; }
public String getLdName() { return ldName; }
public void setLdName(String ldName) { this.ldName = ldName; }
}

View File

@@ -1,29 +0,0 @@
package com.njcn.gather.icd.mapping.domain.model.icd;
/**
* ReportControl 节点。
* 报告控制块模型。用于保存报告名称、关联 DataSet、缓冲属性等。
*/
public class ReportControlNode {
private String name;
private String rptId;
private boolean buffered;
private String dataSetName;
private String trgOps;
private String confRev;
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getRptId() { return rptId; }
public void setRptId(String rptId) { this.rptId = rptId; }
public boolean isBuffered() { return buffered; }
public void setBuffered(boolean buffered) { this.buffered = buffered; }
public String getDataSetName() { return dataSetName; }
public void setDataSetName(String dataSetName) { this.dataSetName = dataSetName; }
public String getTrgOps() { return trgOps; }
public void setTrgOps(String trgOps) { this.trgOps = trgOps; }
public String getConfRev() { return confRev; }
public void setConfRev(String confRev) { this.confRev = confRev; }
public Boolean getBuffered() { return buffered; }
public void setBuffered(Boolean buffered) { this.buffered = buffered; }
}

View File

@@ -1,103 +0,0 @@
package com.njcn.gather.icd.mapping.domain.model.intermediate;
import com.njcn.gather.icd.mapping.domain.model.icd.LnNode;
import java.util.ArrayList;
import java.util.List;
/**
* 最终参与生成 DataSetList 的选择状态。
*
* 关键修正:
* 旧版本一个绑定只保存一个 LnNode导致
* - MSQI 整组丢失
* - MHAI 只生成一半
*
* 新版本改成:
* 一个绑定可以关联多个 LnNode后续生成阶段再逐个展开。
*/
public class DataSetSelectionState {
/** 所属分组 key。 */
private String groupKey;
/** 所属分组 desc。 */
private String groupDesc;
/** 报告名。 */
private String reportName;
/** 数据集名。 */
private String dataSetName;
/** 绑定标签。 */
private String label;
/** 绑定的 lnInst。 */
private String lnInst;
/**
* 当前绑定最终命中的 LN 节点列表。
*
* 说明:
* 同一个 lnInst 在不同 lnClass 下可能都需要展开,
* 例如MMXU / MSQI / MHAI。
*/
private List<LnNode> lnNodes = new ArrayList<LnNode>();
public String getGroupKey() {
return groupKey;
}
public void setGroupKey(String groupKey) {
this.groupKey = groupKey;
}
public String getGroupDesc() {
return groupDesc;
}
public void setGroupDesc(String groupDesc) {
this.groupDesc = groupDesc;
}
public String getReportName() {
return reportName;
}
public void setReportName(String reportName) {
this.reportName = reportName;
}
public String getDataSetName() {
return dataSetName;
}
public void setDataSetName(String dataSetName) {
this.dataSetName = dataSetName;
}
public String getLabel() {
return label;
}
public void setLabel(String label) {
this.label = label;
}
public String getLnInst() {
return lnInst;
}
public void setLnInst(String lnInst) {
this.lnInst = lnInst;
}
public List<LnNode> getLnNodes() {
return lnNodes;
}
public void setLnNodes(List<LnNode> lnNodes) {
this.lnNodes = lnNodes;
}
}

View File

@@ -1,59 +0,0 @@
package com.njcn.gather.icd.mapping.domain.model.intermediate;
import java.util.ArrayList;
import java.util.List;
/**
* 中间态总对象。
*
* 作用:
* 1. 对应原 C# 里“先形成中间态,再做最终 JSON 组装”的思路;
* 2. 把业务分组、用户绑定、最终 DataSet 选择结果集中保存;
* 3. 避免在 MappingGenerationService 里直接边遍历边拼 JSON便于后续扩展和排查。
*/
public class ReportAndDataSetState {
/** IED 名称。 */
private String iedName;
/** LD 实例名。 */
private String ldInst;
/** 分组状态列表。 */
private List<ReportGroupState> reportGroups = new ArrayList<ReportGroupState>();
/** 数据集选择状态列表。 */
private List<DataSetSelectionState> dataSetSelections = new ArrayList<DataSetSelectionState>();
public String getIedName() {
return iedName;
}
public void setIedName(String iedName) {
this.iedName = iedName;
}
public String getLdInst() {
return ldInst;
}
public void setLdInst(String ldInst) {
this.ldInst = ldInst;
}
public List<ReportGroupState> getReportGroups() {
return reportGroups;
}
public void setReportGroups(List<ReportGroupState> reportGroups) {
this.reportGroups = reportGroups;
}
public List<DataSetSelectionState> getDataSetSelections() {
return dataSetSelections;
}
public void setDataSetSelections(List<DataSetSelectionState> dataSetSelections) {
this.dataSetSelections = dataSetSelections;
}
}

View File

@@ -1,73 +0,0 @@
package com.njcn.gather.icd.mapping.domain.model.intermediate;
/**
* 单条最终有效绑定关系的中间态。
*/
public class ReportBindingState {
/** 分组 key。 */
private String groupKey;
/** 分组描述。 */
private String groupDesc;
/** 报告名。 */
private String reportName;
/** 数据集名。 */
private String dataSetName;
/** 标签。 */
private String label;
/** 绑定的 lnInst 数字。 */
private String lnInst;
public String getGroupKey() {
return groupKey;
}
public void setGroupKey(String groupKey) {
this.groupKey = groupKey;
}
public String getGroupDesc() {
return groupDesc;
}
public void setGroupDesc(String groupDesc) {
this.groupDesc = groupDesc;
}
public String getReportName() {
return reportName;
}
public void setReportName(String reportName) {
this.reportName = reportName;
}
public String getDataSetName() {
return dataSetName;
}
public void setDataSetName(String dataSetName) {
this.dataSetName = dataSetName;
}
public String getLabel() {
return label;
}
public void setLabel(String label) {
this.label = label;
}
public String getLnInst() {
return lnInst;
}
public void setLnInst(String lnInst) {
this.lnInst = lnInst;
}
}

View File

@@ -1,100 +0,0 @@
package com.njcn.gather.icd.mapping.domain.model.intermediate;
import com.njcn.gather.icd.mapping.domain.model.analysis.IndexCandidateReportItem;
import java.util.ArrayList;
import java.util.List;
/**
* 单个业务分组的中间态。
*/
public class ReportGroupState {
/** 分组唯一键。 */
private String groupKey;
/** 分组描述。 */
private String groupDesc;
/** 当前分组匹配到的报告数量。 */
private int reportCount;
/** 报表 inst来自 DefaultCfg.ReportList.inst。 */
private String reportInst;
/** Select来自 DefaultCfg.ReportList.Select。 */
private String select;
/** TrgOps来自 DefaultCfg.ReportList.TrgOps。 */
private String trgOps;
/** 当前分组包含的报告列表。 */
private List<IndexCandidateReportItem> reportItems = new ArrayList<IndexCandidateReportItem>();
/** 用户最终确认的绑定关系。 */
private List<ReportBindingState> bindings = new ArrayList<ReportBindingState>();
public String getGroupKey() {
return groupKey;
}
public void setGroupKey(String groupKey) {
this.groupKey = groupKey;
}
public String getGroupDesc() {
return groupDesc;
}
public void setGroupDesc(String groupDesc) {
this.groupDesc = groupDesc;
}
public int getReportCount() {
return reportCount;
}
public void setReportCount(int reportCount) {
this.reportCount = reportCount;
}
public String getReportInst() {
return reportInst;
}
public void setReportInst(String reportInst) {
this.reportInst = reportInst;
}
public String getSelect() {
return select;
}
public void setSelect(String select) {
this.select = select;
}
public String getTrgOps() {
return trgOps;
}
public void setTrgOps(String trgOps) {
this.trgOps = trgOps;
}
public List<IndexCandidateReportItem> getReportItems() {
return reportItems;
}
public void setReportItems(List<IndexCandidateReportItem> reportItems) {
this.reportItems = reportItems;
}
public List<ReportBindingState> getBindings() {
return bindings;
}
public void setBindings(List<ReportBindingState> bindings) {
this.bindings = bindings;
}
}

View File

@@ -1,43 +0,0 @@
package com.njcn.gather.icd.mapping.domain.model.mapping;
import java.util.ArrayList;
import java.util.List;
/**
* 最终映射中的 DataSetList 单项。
*/
public class DataSetGroupItem {
/** 分组描述。一般来自 LnClassList.desc。 */
private String desc;
/** lnClass。 */
private String lnClass;
/** 该 lnClass 下的 inst 列表。 */
private List<InstItem> instList = new ArrayList<InstItem>();
public String getDesc() {
return desc;
}
public void setDesc(String desc) {
this.desc = desc;
}
public String getLnClass() {
return lnClass;
}
public void setLnClass(String lnClass) {
this.lnClass = lnClass;
}
public List<InstItem> getInstList() {
return instList;
}
public void setInstList(List<InstItem> instList) {
this.instList = instList;
}
}

View File

@@ -1,120 +0,0 @@
package com.njcn.gather.icd.mapping.domain.model.mapping;
import java.util.ArrayList;
import java.util.List;
/**
* 最终映射中的 doiList 单项。
*/
public class DoiItem {
/** DOI 名称。 */
private String name;
/** DOI 描述。 */
private String desc;
/** 起始序号。 */
private int start;
/** 结束序号。 */
private int end;
/** 单位。 */
private String unit;
/** 系数。 */
private float coefficient;
/** 基波标志。 */
private int baseflag;
/** 基波数量。 */
private int basecount;
/** ICD 实际序列数。 */
private int icdcout;
/** SDI 列表。 */
private List<SdiItem> sdiList = new ArrayList<SdiItem>();
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getDesc() {
return desc;
}
public void setDesc(String desc) {
this.desc = desc;
}
public int getStart() {
return start;
}
public void setStart(int start) {
this.start = start;
}
public int getEnd() {
return end;
}
public void setEnd(int end) {
this.end = end;
}
public String getUnit() {
return unit;
}
public void setUnit(String unit) {
this.unit = unit;
}
public float getCoefficient() {
return coefficient;
}
public void setCoefficient(float coefficient) {
this.coefficient = coefficient;
}
public int getBaseflag() {
return baseflag;
}
public void setBaseflag(int baseflag) {
this.baseflag = baseflag;
}
public int getBasecount() {
return basecount;
}
public void setBasecount(int basecount) {
this.basecount = basecount;
}
public int getIcdcout() {
return icdcout;
}
public void setIcdcout(int icdcout) {
this.icdcout = icdcout;
}
public List<SdiItem> getSdiList() {
return sdiList;
}
public void setSdiList(List<SdiItem> sdiList) {
this.sdiList = sdiList;
}
}

View File

@@ -1,43 +0,0 @@
package com.njcn.gather.icd.mapping.domain.model.mapping;
import java.util.ArrayList;
import java.util.List;
/**
* 最终映射中的 instList 单项。
*/
public class InstItem {
/** lnInst。 */
private String inst;
/** 该 inst 的描述。通常使用当前绑定的 label。 */
private String desc;
/** DOI 列表。 */
private List<DoiItem> doiList = new ArrayList<DoiItem>();
public String getInst() {
return inst;
}
public void setInst(String inst) {
this.inst = inst;
}
public String getDesc() {
return desc;
}
public void setDesc(String desc) {
this.desc = desc;
}
public List<DoiItem> getDoiList() {
return doiList;
}
public void setDoiList(List<DoiItem> doiList) {
this.doiList = doiList;
}
}

View File

@@ -1,127 +0,0 @@
package com.njcn.gather.icd.mapping.domain.model.mapping;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.ArrayList;
import java.util.List;
/**
* 最终映射文档。
*
* 关键说明:
* 1. Java 字段统一使用 lowerCamelCase避免 Jackson 同时输出 ied/IED 这类重复字段。
* 2. JSON 输出名通过 @JsonProperty 显式指定,确保与原 C# 输出格式一致。
*/
public class MappingDocument {
@JsonProperty("version")
private String version;
@JsonProperty("author")
private String author;
@JsonProperty("IED")
private String ied;
@JsonProperty("LD")
private String ld;
/**
* 原 C# mainFrom.txt 固定调用:
* Set_WaveTimeFlag("BeiJing")
*/
@JsonProperty("WaveTimeFlag")
private String waveTimeFlag;
/**
* 原 C# mainFrom.txt 固定调用:
* Set_DataType("1")
*/
@JsonProperty("DataType")
private String dataType;
/**
* 原 C# mainFrom.txt 固定调用:
* Set_unit("1")
*/
@JsonProperty("unit")
private String unit;
@JsonProperty("ReportMap")
private List<ReportMapItem> reportMap = new ArrayList<ReportMapItem>();
@JsonProperty("DataSetList")
private List<DataSetGroupItem> dataSetList = new ArrayList<DataSetGroupItem>();
public String getVersion() {
return version;
}
public void setVersion(String version) {
this.version = version;
}
public String getAuthor() {
return author;
}
public void setAuthor(String author) {
this.author = author;
}
public String getIed() {
return ied;
}
public void setIed(String ied) {
this.ied = ied;
}
public String getLd() {
return ld;
}
public void setLd(String ld) {
this.ld = ld;
}
public String getWaveTimeFlag() {
return waveTimeFlag;
}
public void setWaveTimeFlag(String waveTimeFlag) {
this.waveTimeFlag = waveTimeFlag;
}
public String getDataType() {
return dataType;
}
public void setDataType(String dataType) {
this.dataType = dataType;
}
public String getUnit() {
return unit;
}
public void setUnit(String unit) {
this.unit = unit;
}
public List<ReportMapItem> getReportMap() {
return reportMap;
}
public void setReportMap(List<ReportMapItem> reportMap) {
this.reportMap = reportMap;
}
public List<DataSetGroupItem> getDataSetList() {
return dataSetList;
}
public void setDataSetList(List<DataSetGroupItem> dataSetList) {
this.dataSetList = dataSetList;
}
}

View File

@@ -1,124 +0,0 @@
package com.njcn.gather.icd.mapping.domain.model.mapping;
import com.fasterxml.jackson.annotation.JsonProperty;
/**
* 最终映射中的 ReportMap 单项。
*
* 关键说明:
* 1. 原 C# 不是把同组报告合并成一条,而是组内每个报告各自输出一条。
* 2. 但 reportCount 仍然写该分组总数。
* 3. buffered 不是 boolean而是
* - true -> BR
* - false -> RP
*/
public class ReportMapItem {
@JsonProperty("desc")
private String desc;
@JsonProperty("reportCount")
private int reportCount;
@JsonProperty("rptID")
private String rptId;
@JsonProperty("name")
private String name;
@JsonProperty("buffered")
private String buffered;
@JsonProperty("inst")
private String inst;
/**
* 原 C# Set_FlickerFlag() 当前固定写 "0"
*/
@JsonProperty("FlickerFlag")
private String flickerFlag;
/**
* 原 C# 来自 DefaultCfg.ReportList.Select
*/
@JsonProperty("Select")
private String select;
/**
* 原 C# 来自 DefaultCfg.ReportList.TrgOps
*/
@JsonProperty("TrgOps")
private String trgOps;
public String getDesc() {
return desc;
}
public void setDesc(String desc) {
this.desc = desc;
}
public int getReportCount() {
return reportCount;
}
public void setReportCount(int reportCount) {
this.reportCount = reportCount;
}
public String getRptId() {
return rptId;
}
public void setRptId(String rptId) {
this.rptId = rptId;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getBuffered() {
return buffered;
}
public void setBuffered(String buffered) {
this.buffered = buffered;
}
public String getInst() {
return inst;
}
public void setInst(String inst) {
this.inst = inst;
}
public String getFlickerFlag() {
return flickerFlag;
}
public void setFlickerFlag(String flickerFlag) {
this.flickerFlag = flickerFlag;
}
public String getSelect() {
return select;
}
public void setSelect(String select) {
this.select = select;
}
public String getTrgOps() {
return trgOps;
}
public void setTrgOps(String trgOps) {
this.trgOps = trgOps;
}
}

View File

@@ -1,43 +0,0 @@
package com.njcn.gather.icd.mapping.domain.model.mapping;
import java.util.ArrayList;
import java.util.List;
/**
* 最终映射中的 sdiList 单项。
*/
public class SdiItem {
/** SDI 名称。 */
private String name;
/** SDI 描述。 */
private String desc;
/** 类型列表。 */
private List<TypeItem> typeList = new ArrayList<TypeItem>();
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getDesc() {
return desc;
}
public void setDesc(String desc) {
this.desc = desc;
}
public List<TypeItem> getTypeList() {
return typeList;
}
public void setTypeList(List<TypeItem> typeList) {
this.typeList = typeList;
}
}

View File

@@ -1,29 +0,0 @@
package com.njcn.gather.icd.mapping.domain.model.mapping;
/**
* 最终映射中的 typeList 单项。
*/
public class TypeItem {
/** 类型名称。 */
private String name;
/** 类型描述。 */
private String desc;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getDesc() {
return desc;
}
public void setDesc(String desc) {
this.desc = desc;
}
}

View File

@@ -1,345 +0,0 @@
package com.njcn.gather.icd.mapping.domain.model.template;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* 默认模板。
* 默认模板模型。把 default-template.json 解析成可直接使用的对象。
*
* 当前只保留第一个接口真正会用到的部分。
*/
@JsonIgnoreProperties(ignoreUnknown = true)
public class DefaultTemplate {
@JsonProperty("ReportList")
private List<ReportCfgItem> reportList = new ArrayList<ReportCfgItem>();
@JsonProperty("LnClassList")
private List<LnClassCfgItem> lnClassList = new ArrayList<LnClassCfgItem>();
@JsonProperty("PhaseList")
private List<PhaseCfgItem> phaseList = new ArrayList<PhaseCfgItem>();
@JsonProperty("MultiplierList")
private List<MultiplierCfgItem> multiplierList = new ArrayList<MultiplierCfgItem>();
@JsonProperty("UnitList")
private List<UnitCfgItem> unitList = new ArrayList<UnitCfgItem>();
@JsonProperty("TypeList")
private List<TypeCfgItem> typeList = new ArrayList<TypeCfgItem>();
@JsonProperty("DataObjectsList")
private List<DataObjectCfgItem> dataObjectsList = new ArrayList<DataObjectCfgItem>();
/**
* 基础校验。
*
* 返回问题列表;为空表示通过。
*/
public List<String> verify() {
List<String> problems = new ArrayList<String>();
ensureDuplicateNames("LnClassList", extractNames(lnClassList), problems);
ensureDuplicateNames("PhaseList", extractNames(phaseList), problems);
ensureDuplicateNames("MultiplierList", extractNames(multiplierList), problems);
ensureDuplicateNames("UnitList", extractNames(unitList), problems);
ensureDuplicateNames("TypeList", extractNames(typeList), problems);
ensureDuplicateObjectNames(problems);
if (reportList == null || reportList.isEmpty()) {
problems.add("DefaultCfg.ReportList 为空");
}
return problems;
}
private List<String> extractNames(List<? extends NameListSupport> list) {
List<String> names = new ArrayList<String>();
if (list == null) {
return names;
}
for (NameListSupport item : list) {
if (item.getNameList() != null) {
names.addAll(item.getNameList());
}
}
return names;
}
private void ensureDuplicateNames(String section, List<String> names, List<String> problems) {
Set<String> set = new HashSet<String>();
for (String name : names) {
if (name == null) {
continue;
}
String key = name.trim();
if (!set.add(key)) {
problems.add(section + " 中存在重复配置项:" + key);
}
}
}
private void ensureDuplicateObjectNames(List<String> problems) {
if (dataObjectsList == null) {
return;
}
for (DataObjectCfgItem dataObject : dataObjectsList) {
List<String> names = new ArrayList<String>();
if (dataObject.getObjectList() != null) {
for (ObjectCfgItem object : dataObject.getObjectList()) {
if (object.getNameList() != null) {
names.addAll(object.getNameList());
}
}
}
ensureDuplicateNames("DataObjectsList[" + dataObject.getDesc() + "]", names, problems);
}
}
public List<ReportCfgItem> getReportList() {
return reportList;
}
public void setReportList(List<ReportCfgItem> reportList) {
this.reportList = reportList;
}
public List<LnClassCfgItem> getLnClassList() {
return lnClassList;
}
public void setLnClassList(List<LnClassCfgItem> lnClassList) {
this.lnClassList = lnClassList;
}
public List<PhaseCfgItem> getPhaseList() {
return phaseList;
}
public void setPhaseList(List<PhaseCfgItem> phaseList) {
this.phaseList = phaseList;
}
public List<MultiplierCfgItem> getMultiplierList() {
return multiplierList;
}
public void setMultiplierList(List<MultiplierCfgItem> multiplierList) {
this.multiplierList = multiplierList;
}
public List<UnitCfgItem> getUnitList() {
return unitList;
}
public void setUnitList(List<UnitCfgItem> unitList) {
this.unitList = unitList;
}
public List<TypeCfgItem> getTypeList() {
return typeList;
}
public void setTypeList(List<TypeCfgItem> typeList) {
this.typeList = typeList;
}
public List<DataObjectCfgItem> getDataObjectsList() {
return dataObjectsList;
}
public void setDataObjectsList(List<DataObjectCfgItem> dataObjectsList) {
this.dataObjectsList = dataObjectsList;
}
/** 统一抽象:凡是有 nameList 的配置项都实现它。 */
public interface NameListSupport {
List<String> getNameList();
}
@JsonIgnoreProperties(ignoreUnknown = true)
public static class ReportCfgItem {
/**
* 报告分组描述,例如:统计数据、实时数据、波动闪变
*/
@JsonProperty("desc")
private String desc;
/**
* 报告 inst例如01
*/
@JsonProperty("inst")
private String inst;
/**
* 原始配置中的 TrgOps。
* 例如40 / 96
*
* 这里必须显式加 @JsonProperty("TrgOps")
* 否则在当前项目里很容易反序列化后为 null。
*/
@JsonProperty("TrgOps")
private String trgOps;
/**
* 原始配置中的 Select。
* 例如DataStatFileMap / DataRealFileMap / FlickerFileMap
*
* 这里必须显式加 @JsonProperty("Select")
* 否则在当前项目里很容易反序列化后为 null。
*/
@JsonProperty("Select")
private String select;
/**
* 该分组覆盖的数据集名称列表
*/
@JsonProperty("DataSetList")
private List<String> dataSetList = new ArrayList<String>();
/**
* 该分组可配置的标签模板列表
*/
@JsonProperty("LnInstList")
private List<String> lnInstList = new ArrayList<String>();
public String getDesc() {
return desc;
}
public void setDesc(String desc) {
this.desc = desc;
}
public String getInst() {
return inst;
}
public void setInst(String inst) {
this.inst = inst;
}
public String getTrgOps() {
return trgOps;
}
public void setTrgOps(String trgOps) {
this.trgOps = trgOps;
}
public String getSelect() {
return select;
}
public void setSelect(String select) {
this.select = select;
}
public List<String> getDataSetList() {
return dataSetList;
}
public void setDataSetList(List<String> dataSetList) {
this.dataSetList = dataSetList;
}
public List<String> getLnInstList() {
return lnInstList;
}
public void setLnInstList(List<String> lnInstList) {
this.lnInstList = lnInstList;
}
}
@JsonIgnoreProperties(ignoreUnknown = true)
public static class LnClassCfgItem implements NameListSupport {
private String desc;
private List<String> nameList = new ArrayList<String>();
public String getDesc() { return desc; }
public void setDesc(String desc) { this.desc = desc; }
public List<String> getNameList() { return nameList; }
public void setNameList(List<String> nameList) { this.nameList = nameList; }
}
@JsonIgnoreProperties(ignoreUnknown = true)
public static class PhaseCfgItem implements NameListSupport {
private String desc;
private List<String> nameList = new ArrayList<String>();
public String getDesc() { return desc; }
public void setDesc(String desc) { this.desc = desc; }
public List<String> getNameList() { return nameList; }
public void setNameList(List<String> nameList) { this.nameList = nameList; }
}
@JsonIgnoreProperties(ignoreUnknown = true)
public static class MultiplierCfgItem implements NameListSupport {
private int multiplier;
private List<String> nameList = new ArrayList<String>();
public int getMultiplier() { return multiplier; }
public void setMultiplier(int multiplier) { this.multiplier = multiplier; }
public List<String> getNameList() { return nameList; }
public void setNameList(List<String> nameList) { this.nameList = nameList; }
}
@JsonIgnoreProperties(ignoreUnknown = true)
public static class UnitCfgItem implements NameListSupport {
private String desc;
private List<String> nameList = new ArrayList<String>();
public String getDesc() { return desc; }
public void setDesc(String desc) { this.desc = desc; }
public List<String> getNameList() { return nameList; }
public void setNameList(List<String> nameList) { this.nameList = nameList; }
}
@JsonIgnoreProperties(ignoreUnknown = true)
public static class TypeCfgItem implements NameListSupport {
private String desc;
private List<String> nameList = new ArrayList<String>();
public String getDesc() { return desc; }
public void setDesc(String desc) { this.desc = desc; }
public List<String> getNameList() { return nameList; }
public void setNameList(List<String> nameList) { this.nameList = nameList; }
}
@JsonIgnoreProperties(ignoreUnknown = true)
public static class DataObjectCfgItem {
private String desc;
@JsonProperty("LnInstList")
private List<String> lnInstList = new ArrayList<String>();
@JsonProperty("ObjectList")
private List<ObjectCfgItem> objectList = new ArrayList<ObjectCfgItem>();
public String getDesc() { return desc; }
public void setDesc(String desc) { this.desc = desc; }
public List<String> getLnInstList() { return lnInstList; }
public void setLnInstList(List<String> lnInstList) { this.lnInstList = lnInstList; }
public List<ObjectCfgItem> getObjectList() { return objectList; }
public void setObjectList(List<ObjectCfgItem> objectList) { this.objectList = objectList; }
}
@JsonIgnoreProperties(ignoreUnknown = true)
public static class ObjectCfgItem implements NameListSupport {
private String desc;
private int baseflag;
private int basecount;
private int queuecount;
private List<String> nameList = new ArrayList<String>();
private List<String> queueList = new ArrayList<String>();
public String getDesc() { return desc; }
public void setDesc(String desc) { this.desc = desc; }
public int getBaseflag() { return baseflag; }
public void setBaseflag(int baseflag) { this.baseflag = baseflag; }
public int getBasecount() { return basecount; }
public void setBasecount(int basecount) { this.basecount = basecount; }
public int getQueuecount() { return queuecount; }
public void setQueuecount(int queuecount) { this.queuecount = queuecount; }
public List<String> getNameList() { return nameList; }
public void setNameList(List<String> nameList) { this.nameList = nameList; }
public List<String> getQueueList() { return queueList; }
public void setQueueList(List<String> queueList) { this.queueList = queueList; }
}
}

View File

@@ -1,26 +1,29 @@
package com.njcn.gather.icd.mapping.infrastructure.parser;
import com.njcn.gather.icd.mapping.domain.model.icd.*;
import com.njcn.gather.icd.mapping.pojo.bo.icd.*;
import com.njcn.gather.icd.mapping.infrastructure.parser.generated.*;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
* JAXB generated 模型读取器。
* generated 模型读取器。遍历 SCL/IED/LDevice/LN0/LN/ReportControl/DataSet/DOI 树并转换。
* JAXB 生成模型读取器。
*
* 说明:
* 1. 这里只负责“把 generated 模型读取出来”
* 2. 不直接做接口编排,也不做 JSON 序列化。
* 负责遍历 `SCL/IED/LDevice/LN0/LN/ReportControl/DataSet/DOI` 结构,
* 并转换为模块内部使用的 ICD 领域模型
*/
public class SclGeneratedModelReader {
/**
* 从 JAXB SCL 根对象中读取当前模块需要的 ICD 结构。
*/
public IcdDocument read(SCL scl, String fileName) {
IcdDocument document = new IcdDocument();
document.setFileName(fileName);
// 当前业务只处理第一个可用的非 LD0/PQLD0 逻辑设备。
TIED targetIed = null;
TLDevice targetDevice = null;
@@ -82,15 +85,16 @@ public class SclGeneratedModelReader {
document.getLogicalNodes().add(readLogicalNode(ln, false));
}
}
// 关键修正:
// C# 原版的 icdcout 优先来自 DataTypeTemplates -> LNodeType -> DO -> DOType -> DA.count
// 不是靠 DataSet/FCDA 重复数来反推。
// 这里改成:先按模板 count 回填 DOI.sequenceCount找不到再退回 FCDA。
// `icdcout` 优先取自模板链路 `DataTypeTemplates -> LNodeType -> DO -> DOType -> DA.count`
// 只有模板未提供时才退回到 `DataSet/FCDA` 反查,保持与原 C# 行为一致。
syncDoiSequenceCount(scl, document);
return document;
}
/**
* 从 LN0 或 LN 中读取 DataSet 与 ReportControl 信息。
*/
private void readReportAndDataSetFromAnyLn(TAnyLN anyLn, IcdDocument document) {
List<FcdaNode> allFcdas = new ArrayList<FcdaNode>();
if (anyLn.getDataSet() != null) {
@@ -127,6 +131,9 @@ public class SclGeneratedModelReader {
}
}
/**
* 将 ReportControl.TrgOps 转成后续模板使用的触发编码。
*/
private String readTrgOps(TReportControl reportControl) {
if (reportControl.getTrgOps() == null) {
return null;
@@ -146,24 +153,22 @@ public class SclGeneratedModelReader {
/**
* 读取逻辑节点。
*
* 注意:
* 当前这套 JAXB 生成类中TAnyLN 只提供了通用能力,
* prefix / lnClass / inst 这些属性实际定义在子类 TLN / TLN0 上,
* 所以这里不能直接对 TAnyLN 调用 getPrefix()/getLnClass()/getInst()。
* `TAnyLN` 只暴露通用能力,`prefix / lnClass / inst` 实际定义在 `TLN / TLN0` 子类上,
* 因此这里需要按具体类型分别读取。
*/
private LnNode readLogicalNode(TAnyLN anyLn, boolean isLn0) {
LnNode node = new LnNode();
node.setLn0(isLn0);
// lnType 在 TAnyLN 上是存在的,可直接读取
// `lnType` 定义`TAnyLN` 上,可直接读取
node.setLnType(anyLn.getLnType());
// prefix / lnClass / inst 需要根据具体子类读取
// `prefix / lnClass / inst` 需要根据具体子类读取
node.setPrefix(resolveLnPrefix(anyLn));
node.setLnClass(resolveLnClass(anyLn));
node.setLnInst(resolveLnInst(anyLn));
// DOI 定义在 TAnyLN 上,可直接读取
// `DOI` 定义在 `TAnyLN` 上,可直接读取
if (anyLn.getDOI() != null) {
for (TDOI doi : anyLn.getDOI()) {
DoiNode doiNode = new DoiNode();
@@ -188,10 +193,9 @@ public class SclGeneratedModelReader {
return node;
}
/**
* 解析逻辑节点 prefix。
* 解析逻辑节点的 `prefix`
*
* TLN 有 prefix
* TLN0 按这套生成类没有单独 prefix 字段,返回空字符串即可。
* `TLN``prefix` 字段,`TLN0` 在当前生成模型中没有单独字段,统一返回空字符串。
*/
private String resolveLnPrefix(TAnyLN anyLn) {
if (anyLn instanceof TLN) {
@@ -201,11 +205,9 @@ public class SclGeneratedModelReader {
}
/**
* 解析逻辑节点 lnClass。
* 解析逻辑节点的 `lnClass`
*
* 当前生成类里:
* - TLN.getLnClass() 返回 List<String>
* - TLN0.getLnClass() 返回 List<String>
* 当前生成模型中,`TLN` 和 `TLN0` 都返回 `List<String>`,这里只取首个值。
*/
private String resolveLnClass(TAnyLN anyLn) {
if (anyLn instanceof TLN) {
@@ -218,11 +220,9 @@ public class SclGeneratedModelReader {
}
/**
* 解析逻辑节点 inst。
* 解析逻辑节点的 `inst`
*
* 当前生成类里:
* - TLN.getInst() 存在
* - TLN0.getInst() 也存在
* 当前生成模型中,`TLN` 和 `TLN0` 都提供了该字段。
*/
private String resolveLnInst(TAnyLN anyLn) {
if (anyLn instanceof TLN) {
@@ -234,6 +234,9 @@ public class SclGeneratedModelReader {
return null;
}
/**
* 递归读取 DOI 下的 SDI/DAI 混合子节点。
*/
private DoiElementNode readUnNamingNode(TUnNaming source) {
if (source == null) {
return null;
@@ -272,6 +275,9 @@ public class SclGeneratedModelReader {
return null;
}
/**
* 将 JAXB FCDA 节点转换为内部 FCDA 模型。
*/
private FcdaNode toFcdaNode(TFCDA fcda) {
FcdaNode node = new FcdaNode();
node.setLdInst(fcda.getLdInst());
@@ -285,10 +291,16 @@ public class SclGeneratedModelReader {
return node;
}
/**
* 读取 JAXB 列表型字段的首个值。
*/
private String first(List<String> values) {
return values == null || values.isEmpty() ? null : values.get(0);
}
/**
* 从 LD 实例名中提取非数字前缀,例如 PQM1 -> PQM。
*/
private String extractLdPrefix(String ldInst) {
if (ldInst == null) {
return null;
@@ -305,19 +317,16 @@ public class SclGeneratedModelReader {
}
/**
* DOI 的 sequenceCount 同步出来
* 回填 DOI 的 `sequenceCount`
*
* 规则严格贴近原 C#
* 1. 优先从 DataTypeTemplates -> LNodeType -> DO -> DOType -> DA.count 取值;
* 2. 如果模板里没有 count再退回 DataSet/FCDA 反查;
* 3. 这样后续 MappingGenerationService 里的 icdcout 才会和 C# 一致。
* 优先按模板链路取值,模板缺失时再退回 `DataSet/FCDA` 反查,确保后续生成结果与原 C# 逻辑一致。
*/
private void syncDoiSequenceCount(SCL scl, IcdDocument document) {
if (document == null || document.getLogicalNodes() == null) {
return;
}
// 先把 “lnType + doName -> count” 建好缓存
// 先构建 `lnType + doName -> count` 缓存
Map<String, Integer> templateSequenceCountMap = buildTemplateSequenceCountMap(scl);
for (LnNode lnNode : document.getLogicalNodes()) {
@@ -338,23 +347,17 @@ public class SclGeneratedModelReader {
int fcdaCount = findDoiSequenceCountFromFcda(document, doiNode);
// 关键:优先使用模板 count保持与 C# 原版一致
// 优先使用模板中的 `count`,保持与 C# 行为一致
doiNode.setSequenceCount(templateCount > 0 ? templateCount : fcdaCount);
}
}
}
/**
* 构建 lnType + doName -> 序列数量 缓存。
* 构建 `lnType + doName -> 序列数量` 缓存。
*
* 来源:
* DataTypeTemplates
* -> LNodeType(id = lnType)
* -> DO(name/type)
* -> DOType(id = type)
* -> DA.count
*
* C# 原版本质上就是沿这条链把 doi.tNUM 算出来。
* 数据来源为 `DataTypeTemplates -> LNodeType -> DO -> DOType -> DA.count`
* 原 C# 版本也是沿着这条链路计算 `doi.tNUM`。
*/
private Map<String, Integer> buildTemplateSequenceCountMap(SCL scl) {
Map<String, Integer> result = new LinkedHashMap<String, Integer>();
@@ -367,7 +370,7 @@ public class SclGeneratedModelReader {
return result;
}
// 1. 先建 DOType.id -> count
// 1. 先`DOType.id -> count`。
Map<String, Integer> doTypeCountMap = new LinkedHashMap<String, Integer>();
for (TDOType doType : templates.getDOType()) {
if (doType == null || doType.getId() == null) {
@@ -378,7 +381,7 @@ public class SclGeneratedModelReader {
doTypeCountMap.put(doType.getId(), count);
}
// 2. 再建 lnType + doName -> count
// 2. 再`lnType + doName -> count`。
for (TLNodeType lNodeType : templates.getLNodeType()) {
if (lNodeType == null || lNodeType.getId() == null || lNodeType.getDO() == null) {
continue;
@@ -402,10 +405,9 @@ public class SclGeneratedModelReader {
}
/**
* 从个 DOType 中提取序列数量。
* 从`DOType` 中提取序列数量。
*
* C# 原版用的是 DOType 下 DA.count
* 这里取所有顶层 DA 里最大的正整数 count。
* 这里取顶层 `DA.count` 中最大的正整数,贴近原 C# 的处理方式
*/
private int extractDoTypeSequenceCount(TDOType doType) {
int max = 0;
@@ -429,10 +431,9 @@ public class SclGeneratedModelReader {
}
/**
* 解析 DA.count。
* 解析 `DA.count`
*
* JAXB 这套生成类把 count 生成为 List<String>
* 所以这里要做一次安全转换。
* 当前 JAXB 生成类把该字段定义`List<String>`,这里统一做安全转换。
*/
private int parseDaCount(TAbstractDataAttribute dataAttribute) {
if (dataAttribute == null || dataAttribute.getCount() == null || dataAttribute.getCount().isEmpty()) {
@@ -456,16 +457,16 @@ public class SclGeneratedModelReader {
max = value;
}
} catch (NumberFormatException ignore) {
// 非法 count 直接忽略,保持容错
// 非法 `count` 直接忽略,保持容错
}
}
return max;
}
/**
* 退回到 DataSet/FCDAsequenceCount 反查
* 从 `DataSet/FCDA` 反查 `sequenceCount`
*
* 这里只作为模板 count 找不到时的兜底逻辑。
* 这里只在模板未提供 `count` 时作为兜底逻辑使用
*/
private int findDoiSequenceCountFromFcda(IcdDocument document, DoiNode doiNode) {
int max = 0;
@@ -501,6 +502,9 @@ public class SclGeneratedModelReader {
return max;
}
/**
* 构造模板序列数量缓存使用的复合 key。
*/
private String buildLnTypeDoKey(String lnType, String doName) {
String left = lnType == null ? "" : lnType.trim();
String right = doName == null ? "" : doName.trim();
@@ -508,7 +512,7 @@ public class SclGeneratedModelReader {
}
/**
* 空安全字符串比较。
* 空安全字符串比较。
*/
private boolean equalsTrim(String left, String right) {
if (left == null && right == null) {

View File

@@ -1,6 +1,6 @@
package com.njcn.gather.icd.mapping.infrastructure.parser;
import com.njcn.gather.icd.mapping.domain.model.icd.IcdDocument;
import com.njcn.gather.icd.mapping.pojo.bo.icd.IcdDocument;
import com.njcn.gather.icd.mapping.infrastructure.parser.generated.SCL;
import org.springframework.stereotype.Component;
@@ -11,18 +11,18 @@ import java.io.ByteArrayInputStream;
/**
* SCL 解析适配器。
* JAXB 解析适配器。负责把 ICD XML 反序列化为 SCL 根对象,再转成内部模型。
*
* 说明:
* 1. 这是真正会用到的 JAXB 版解析入口。
* 2. 这里只负责把 ICD XML 反序列化成 SCL 根对象,再交给 reader 转成内部模型。
* 3. 业务层不会直接依赖 JAXB generated 类。
* 负责把 ICD XML 反序列化为 SCL 根对象,再交给读取器转换为内部模型。
*/
@Component
public class SclParserAdapter {
/** JAXB generated 模型读取器,负责转成内部 ICD 领域模型。 */
private final SclGeneratedModelReader modelReader = new SclGeneratedModelReader();
/**
* 解析 ICD/SCL XML 字节内容。
*/
public IcdDocument parse(byte[] content, String fileName) {
if (content == null || content.length == 0) {
throw new IllegalArgumentException("ICD 文件内容不能为空");
@@ -38,6 +38,9 @@ public class SclParserAdapter {
}
}
/**
* 兼容 JAXB 直接返回 SCL 或 JAXBElement<SCL> 两种根对象形式。
*/
private SCL resolveSclRoot(Object raw) {
if (raw instanceof SCL) {
return (SCL) raw;

View File

@@ -1,14 +1,16 @@
package com.njcn.gather.icd.mapping.infrastructure.parser;
import com.njcn.gather.icd.mapping.domain.model.icd.DoiElementNode;
import com.njcn.gather.icd.mapping.domain.model.icd.DoiNode;
import com.njcn.gather.icd.mapping.domain.model.icd.FcdaNode;
import com.njcn.gather.icd.mapping.pojo.bo.icd.DoiElementNode;
import com.njcn.gather.icd.mapping.pojo.bo.icd.DoiNode;
import com.njcn.gather.icd.mapping.pojo.bo.icd.FcdaNode;
import java.util.ArrayList;
import java.util.List;
/**
* SCL 遍历辅助工具。
*
* 提供序列数量统计和 DOI 叶子节点展开等公共能力。
*/
public final class SclTraversalSupport {
@@ -16,7 +18,7 @@ public final class SclTraversalSupport {
}
/**
* 统计同一数据对象在数据集出现次数,作为 ICD 实际序列数参考。
* 统计同一数据对象在数据集中的出现次数,作为 ICD 实际序列数量的参考
*/
public static int calculateSequenceCount(List<FcdaNode> allFcdas, FcdaNode current) {
int count = 0;
@@ -30,6 +32,9 @@ public final class SclTraversalSupport {
return count <= 1 ? 0 : count;
}
/**
* 展开 DOI 树下所有 DAI 节点,便于后续读取单位、倍率等叶子值。
*/
public static List<DoiElementNode> flattenLeafDai(DoiNode doi) {
List<DoiElementNode> result = new ArrayList<DoiElementNode>();
if (doi == null || doi.getChildren() == null) {
@@ -41,6 +46,9 @@ public final class SclTraversalSupport {
return result;
}
/**
* 递归收集 DAI 叶子节点。
*/
private static void collectLeafDai(DoiElementNode node, List<DoiElementNode> result) {
if (node == null) {
return;
@@ -55,6 +63,9 @@ public final class SclTraversalSupport {
}
}
/**
* 空安全字符串比较,供解析阶段统计序列数量使用。
*/
public static boolean safeEquals(String a, String b) {
return a == null ? b == null : a.equals(b);
}

View File

@@ -0,0 +1,48 @@
package com.njcn.gather.icd.mapping.pojo.bo;
import lombok.Data;
import com.njcn.gather.icd.mapping.pojo.bo.analysis.IndexAnalysis;
import com.njcn.gather.icd.mapping.pojo.bo.icd.IcdDocument;
import com.njcn.gather.icd.mapping.pojo.bo.mapping.MappingDocument;
import com.njcn.gather.icd.mapping.pojo.enums.GenerateStatus;
import java.util.ArrayList;
import java.util.List;
/**
* 应用层返回对象。
*
* 统一封装成功、需选择索引、失败三类结果。
*/
@Data
public class GenerateMappingResult {
/** 本次生成流程状态。 */
private GenerateStatus status;
/** 给前端或调用方展示的处理结果说明。 */
private String message;
/** ICD 中解析到的 IED 名称。 */
private String iedName;
/** ICD 中解析到的 LD 实例名。 */
private String ldInst;
/** 前端可编辑的 ICD 解析结果。 */
private IcdDocument icdDocument;
/** 需要人工绑定索引时返回的候选分析结果。 */
private IndexAnalysis indexAnalysis;
/** 生成成功后的结构化映射文档。 */
private MappingDocument mappingDocument;
/** 生成成功后的 JSON 字符串。 */
private String mappingJson;
/** saveToDisk=true 时的文件保存路径。 */
private String savedPath;
/** 解析、校验或生成过程中收集到的问题。 */
private List<String> problems = new ArrayList<String>();
}

View File

@@ -1,5 +1,6 @@
package com.njcn.gather.icd.mapping.domain.model.analysis;
package com.njcn.gather.icd.mapping.pojo.bo.analysis;
import lombok.Data;
import java.util.ArrayList;
import java.util.List;
@@ -9,12 +10,11 @@ import java.util.List;
*
* key = reportName
*/
@Data
public class IndexAnalysis {
/** 按 DefaultCfg.ReportList 分组后的索引候选。 */
private List<IndexCandidate> candidates = new ArrayList<IndexCandidate>();
private List<String> problems = new ArrayList<String>();
public List<IndexCandidate> getCandidates() { return candidates; }
public void setCandidates(List<IndexCandidate> candidates) { this.candidates = candidates; }
public List<String> getProblems() { return problems; }
public void setProblems(List<String> problems) { this.problems = problems; }
/** 分析过程中发现但不一定阻断流程的问题。 */
private List<String> problems = new ArrayList<String>();
}

View File

@@ -0,0 +1,51 @@
package com.njcn.gather.icd.mapping.pojo.bo.analysis;
import lombok.Data;
import java.util.ArrayList;
import java.util.List;
/**
* 索引候选分组。
*
* 说明:
* 1. 一条候选对应一个业务分组,例如:统计数据、实时数据;
* 2. 一个业务分组下可以包含多个报告;
* 3. 这里不仅保存返回给前端的候选项,也保存从 DefaultCfg.ReportList 带下来的配置项,
* 供后续 MappingGenerationService 直接使用,避免“二次查模板”失败。
*/
@Data
public class IndexCandidate {
/** 分组唯一键。 */
private String groupKey;
/** 分组描述。 */
private String groupDesc;
/** 该分组下实际匹配到的报告数量。 */
private int reportCount;
/** DefaultCfg.txt 中该分组可用的标签模板。 */
private List<String> templateLabels = new ArrayList<String>();
/** 当前分组下匹配到的报告列表。 */
private List<IndexCandidateReportItem> reports = new ArrayList<IndexCandidateReportItem>();
/**
* DefaultCfg.ReportList.inst
* 例如01 / 02 / 03 / 04
*/
private String reportInst;
/**
* DefaultCfg.ReportList.Select
* 例如DataStatFileMap / DataRealFileMap / FlickerFileMap
*/
private String select;
/**
* DefaultCfg.ReportList.TrgOps
* 例如40 / 96
*/
private String trgOps;
}

View File

@@ -0,0 +1,26 @@
package com.njcn.gather.icd.mapping.pojo.bo.analysis;
import lombok.Data;
import java.util.ArrayList;
import java.util.List;
/**
* 候选分组下的单个报告项。
*
* 这个对象专门给前端展示“这个分组下面有哪些报告,以及每个报告对应哪些可用 lnInst 数字”。
*/
@Data
public class IndexCandidateReportItem {
/** 报告名称。 */
private String reportName;
/** 数据集名称。 */
private String dataSetName;
/** 报告中文描述。一般和分组描述相同,保留它是为了前端渲染更灵活。 */
private String reportDesc;
/** 当前报告在 ICD 中解析出来的所有可选 lnInst 数字。 */
private List<String> availableLnInstValues = new ArrayList<String>();
}

View File

@@ -0,0 +1,18 @@
package com.njcn.gather.icd.mapping.pojo.bo.analysis;
import lombok.Data;
import java.util.ArrayList;
import java.util.List;
/**
* 校验结果。
* 索引校验结果模型。保存是否通过以及问题列表。
*/
@Data
public class ValidationResult {
/** 是否通过索引绑定校验。 */
private boolean valid;
/** 未通过校验时的具体问题列表。 */
private List<String> problems = new ArrayList<String>();
}

View File

@@ -0,0 +1,18 @@
package com.njcn.gather.icd.mapping.pojo.bo.icd;
import lombok.Data;
import java.util.ArrayList;
import java.util.List;
/**
* DataSet 节点。
* 数据集模型。用于承接 DataSet 下的 FCDA 列表。
*/
@Data
public class DataSetNode {
/** DataSet 名称。 */
private String name;
/** DataSet 下的 FCDA 引用列表。 */
private List<FcdaNode> fcdas = new ArrayList<FcdaNode>();
}

View File

@@ -0,0 +1,31 @@
package com.njcn.gather.icd.mapping.pojo.bo.icd;
import lombok.Data;
import java.util.ArrayList;
import java.util.List;
/**
* DOI/SDI/DAI 统一节点。
* DOI/SDI/DAI 细项模型。用于递归承接 DOI 树明细。
*
* kind 常见值:
* - SDI
* - DAI
*/
@Data
public class DoiElementNode {
/** 节点类型,当前主要为 SDI 或 DAI。 */
private String kind;
/** SDI/DAI 名称。 */
private String name;
/** SDI/DAI 自带的 ix 序号。 */
private Long ix;
/** DAI 节点下的 Val 文本值。 */
private List<String> values = new ArrayList<String>();
/** SDI 节点下继续嵌套的子节点。 */
private List<DoiElementNode> children = new ArrayList<DoiElementNode>();
}

View File

@@ -0,0 +1,30 @@
package com.njcn.gather.icd.mapping.pojo.bo.icd;
import lombok.Data;
import java.util.ArrayList;
import java.util.List;
/**
* DOI 节点。
* DOI 模型。表示逻辑节点下的一个数据对象节点。
*/
@Data
public class DoiNode {
/** DOI 名称,例如 PhV、Hz。 */
private String name;
/** DOI 自带的 ix 序号。 */
private Long ix;
/** DOI 所属 LN 的 lnClass。 */
private String lnClass;
/** DOI 所属 LN 的 lnInst。 */
private String lnInst;
/** 从模板或 FCDA 反查得到的实际序列数量。 */
private int sequenceCount;
/** DOI 下的 SDI/DAI 子节点树。 */
private List<DoiElementNode> children = new ArrayList<DoiElementNode>();
}

View File

@@ -0,0 +1,36 @@
package com.njcn.gather.icd.mapping.pojo.bo.icd;
import lombok.Data;
/**
* FCDA 节点。
* FCDA 模型。保存 lnClass、lnInst、doName、daName、fc、ix 等信息。
*/
@Data
public class FcdaNode {
/** FCDA.ldInst。 */
private String ldInst;
/** FCDA.prefix。 */
private String prefix;
/** FCDA.lnClass。 */
private String lnClass;
/** FCDA.lnInst。 */
private String lnInst;
/** FCDA.doName对应 DOI 名称。 */
private String doName;
/** FCDA.daName对应 DAI 名称。 */
private String daName;
/** FCDA.fc 功能约束。 */
private String fc;
/** FCDA.ix 序号。 */
private Long ix;
/** 同一数据对象在数据集中的序列数量。 */
private int sequenceCount;
}

View File

@@ -0,0 +1,46 @@
package com.njcn.gather.icd.mapping.pojo.bo.icd;
import lombok.Data;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
* ICD 文档聚合根。
* ICD 统一领域模型的聚合根。承接 IED、LDevice、ReportControl、DataSet、LN 等解析结果。
*
* 说明:
* 1. 这是系统内部统一的 ICD 模型。
* 2. 外部 JAXB generated 类只在 parser 层使用。
* 3. 业务层全部依赖这个标准化模型,便于后续替换解析实现。
*/
@Data
public class IcdDocument {
/** 原始 ICD 文件名。 */
private String fileName;
/** 当前选中的 IED 名称。 */
private String iedName;
/** 当前选中的逻辑设备实例名。 */
private String ldInst;
/** LD 输出前缀,通常去掉实例名尾部数字。 */
private String ldPrefix;
/** 当前解析到的主 IED 节点。 */
private IedNode primaryIed;
/** 当前参与映射生成的逻辑设备。 */
private LogicalDeviceNode logicalDevice;
/** 当前逻辑设备下的 LN0 和 LN 列表。 */
private List<LnNode> logicalNodes = new ArrayList<LnNode>();
/** 当前逻辑设备下的报告控制块列表。 */
private List<ReportControlNode> reportControls = new ArrayList<ReportControlNode>();
/** 当前逻辑设备下的数据集key 为 DataSet 名称。 */
private Map<String, DataSetNode> dataSets = new LinkedHashMap<String, DataSetNode>();
}

View File

@@ -0,0 +1,21 @@
package com.njcn.gather.icd.mapping.pojo.bo.icd;
import lombok.Data;
import java.util.ArrayList;
import java.util.List;
/**
* IED 节点。
* IED 节点模型。保存 IED 名称以及其下逻辑设备引用。
*/
@Data
public class IedNode {
/** IED 名称。 */
private String name;
/** IED 下的 AccessPoint 名称列表。 */
private List<String> accessPointNames = new ArrayList<String>();
/** IED 下的逻辑设备实例名列表。 */
private List<String> lDeviceInstList = new ArrayList<String>();
}

View File

@@ -0,0 +1,32 @@
package com.njcn.gather.icd.mapping.pojo.bo.icd;
import lombok.Data;
import java.util.ArrayList;
import java.util.List;
/**
* 逻辑节点。
* 逻辑节点模型。保存 lnClass、lnInst、prefix、DOI 等信息。
*
* LN0 和 LN 最终都统一抽象为这个模型。
*/
@Data
public class LnNode {
/** 是否来自 LN0 节点。 */
private boolean ln0;
/** LN 前缀。 */
private String prefix;
/** LN 类型类别,例如 MMXU、MHAI。 */
private String lnClass;
/** LN 实例号,用于和前端选择的数字索引绑定。 */
private String lnInst;
/** LN 类型模板 ID用于反查 DataTypeTemplates。 */
private String lnType;
/** LN 下解析出的 DOI 列表。 */
private List<DoiNode> doiList = new ArrayList<DoiNode>();
}

View File

@@ -0,0 +1,15 @@
package com.njcn.gather.icd.mapping.pojo.bo.icd;
import lombok.Data;
/**
* 逻辑设备节点。
* 逻辑设备模型。保存 inst、prefix 以及其下 LN/LN0 列表。
*/
@Data
public class LogicalDeviceNode {
/** LDevice.inst。 */
private String inst;
/** LDevice.ldName。 */
private String ldName;
}

View File

@@ -0,0 +1,33 @@
package com.njcn.gather.icd.mapping.pojo.bo.icd;
import lombok.Data;
/**
* ReportControl 节点。
* 报告控制块模型。用于保存报告名称、关联 DataSet、缓冲属性等。
*/
@Data
public class ReportControlNode {
/** ReportControl.name。 */
private String name;
/** ReportControl.rptID。 */
private String rptId;
/** 是否为缓存报告,后续会转换为 BR/RP。 */
private boolean buffered;
/** 当前报告关联的 DataSet 名称。 */
private String dataSetName;
/** 解析后的触发选项编码。 */
private String trgOps;
/** ReportControl.confRev。 */
private String confRev;
/** 兼容现有调用链,继续保留 Boolean 形式的 buffered 访问器。 */
public Boolean getBuffered() { return buffered; }
public void setBuffered(Boolean buffered) { this.buffered = buffered; }
}

View File

@@ -0,0 +1,21 @@
package com.njcn.gather.icd.mapping.pojo.bo.mapping;
import lombok.Data;
import java.util.ArrayList;
import java.util.List;
/**
* 最终映射中的 DataSetList 单项。
*/
@Data
public class DataSetGroupItem {
/** 分组描述。一般来自 LnClassList.desc。 */
private String desc;
/** lnClass。 */
private String lnClass;
/** 该 lnClass 下的 inst 列表。 */
private List<InstItem> instList = new ArrayList<InstItem>();
}

View File

@@ -0,0 +1,42 @@
package com.njcn.gather.icd.mapping.pojo.bo.mapping;
import lombok.Data;
import java.util.ArrayList;
import java.util.List;
/**
* 最终映射中的 doiList 单项。
*/
@Data
public class DoiItem {
/** DOI 名称。 */
private String name;
/** DOI 描述。 */
private String desc;
/** 起始序号。 */
private int start;
/** 结束序号。 */
private int end;
/** 单位。 */
private String unit;
/** 系数。 */
private float coefficient;
/** 基波标志。 */
private int baseflag;
/** 基波数量。 */
private int basecount;
/** ICD 实际序列数。 */
private int icdcout;
/** SDI 列表。 */
private List<SdiItem> sdiList = new ArrayList<SdiItem>();
}

View File

@@ -0,0 +1,21 @@
package com.njcn.gather.icd.mapping.pojo.bo.mapping;
import lombok.Data;
import java.util.ArrayList;
import java.util.List;
/**
* 最终映射中的 instList 单项。
*/
@Data
public class InstItem {
/** lnInst。 */
private String inst;
/** 该 inst 的描述。通常使用当前绑定的 label。 */
private String desc;
/** DOI 列表。 */
private List<DoiItem> doiList = new ArrayList<DoiItem>();
}

View File

@@ -0,0 +1,63 @@
package com.njcn.gather.icd.mapping.pojo.bo.mapping;
import lombok.Data;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.ArrayList;
import java.util.List;
/**
* 最终映射文档。
*
* 关键说明:
* 1. Java 字段统一使用 lowerCamelCase避免 Jackson 同时输出 ied/IED 这类重复字段。
* 2. JSON 输出名通过 @JsonProperty 显式指定,确保与原 C# 输出格式一致。
*/
@Data
public class MappingDocument {
/** 映射文档版本。 */
@JsonProperty("version")
private String version;
/** 映射文档作者。 */
@JsonProperty("author")
private String author;
/** ICD 中的 IED 名称,输出字段固定为 IED。 */
@JsonProperty("IED")
private String ied;
/** LD 前缀,输出字段固定为 LD。 */
@JsonProperty("LD")
private String ld;
/**
* 原 C# mainFrom.txt 固定调用:
* Set_WaveTimeFlag("BeiJing")
*/
@JsonProperty("WaveTimeFlag")
private String waveTimeFlag;
/**
* 原 C# mainFrom.txt 固定调用:
* Set_DataType("1")
*/
@JsonProperty("DataType")
private String dataType;
/**
* 原 C# mainFrom.txt 固定调用:
* Set_unit("1")
*/
@JsonProperty("unit")
private String unit;
/** 报告映射列表,每个报告控制块输出一条。 */
@JsonProperty("ReportMap")
private List<ReportMapItem> reportMap = new ArrayList<ReportMapItem>();
/** 数据集映射列表,按 lnClass 和 inst 聚合。 */
@JsonProperty("DataSetList")
private List<DataSetGroupItem> dataSetList = new ArrayList<DataSetGroupItem>();
}

View File

@@ -0,0 +1,60 @@
package com.njcn.gather.icd.mapping.pojo.bo.mapping;
import lombok.Data;
import com.fasterxml.jackson.annotation.JsonProperty;
/**
* 最终映射中的 ReportMap 单项。
*
* 关键说明:
* 1. 原 C# 不是把同组报告合并成一条,而是组内每个报告各自输出一条。
* 2. 但 reportCount 仍然写该分组总数。
* 3. buffered 不是 boolean而是
* - true -> BR
* - false -> RP
*/
@Data
public class ReportMapItem {
/** 报告分组描述。 */
@JsonProperty("desc")
private String desc;
/** 同一分组下的报告数量,单报告按原规则写 0。 */
@JsonProperty("reportCount")
private int reportCount;
/** ICD ReportControl.rptID。 */
@JsonProperty("rptID")
private String rptId;
/** ICD ReportControl.name。 */
@JsonProperty("name")
private String name;
/** 报告缓存类型编码,取值 BR 或 RP。 */
@JsonProperty("buffered")
private String buffered;
/** 模板 ReportList.inst限定 01~04。 */
@JsonProperty("inst")
private String inst;
/**
* 原 C# Set_FlickerFlag() 当前固定写 "0"
*/
@JsonProperty("FlickerFlag")
private String flickerFlag;
/**
* 原 C# 来自 DefaultCfg.ReportList.Select
*/
@JsonProperty("Select")
private String select;
/**
* 原 C# 来自 DefaultCfg.ReportList.TrgOps
*/
@JsonProperty("TrgOps")
private String trgOps;
}

View File

@@ -0,0 +1,21 @@
package com.njcn.gather.icd.mapping.pojo.bo.mapping;
import lombok.Data;
import java.util.ArrayList;
import java.util.List;
/**
* 最终映射中的 sdiList 单项。
*/
@Data
public class SdiItem {
/** SDI 名称。 */
private String name;
/** SDI 描述。 */
private String desc;
/** 类型列表。 */
private List<TypeItem> typeList = new ArrayList<TypeItem>();
}

View File

@@ -0,0 +1,15 @@
package com.njcn.gather.icd.mapping.pojo.bo.mapping;
import lombok.Data;
/**
* 最终映射中的 typeList 单项。
*/
@Data
public class TypeItem {
/** 类型名称。 */
private String name;
/** 类型描述。 */
private String desc;
}

View File

@@ -0,0 +1,49 @@
package com.njcn.gather.icd.mapping.pojo.bo.state;
import lombok.Data;
import com.njcn.gather.icd.mapping.pojo.bo.icd.LnNode;
import java.util.ArrayList;
import java.util.List;
/**
* 最终参与生成 DataSetList 的选择状态。
*
* 关键修正:
* 旧版本一个绑定只保存一个 LnNode导致
* - MSQI 整组丢失
* - MHAI 只生成一半
*
* 新版本改成:
* 一个绑定可以关联多个 LnNode后续生成阶段再逐个展开。
*/
@Data
public class DataSetSelectionState {
/** 所属分组 key。 */
private String groupKey;
/** 所属分组 desc。 */
private String groupDesc;
/** 报告名。 */
private String reportName;
/** 数据集名。 */
private String dataSetName;
/** 绑定标签。 */
private String label;
/** 绑定的 lnInst。 */
private String lnInst;
/**
* 当前绑定最终命中的 LN 节点列表。
*
* 说明:
* 同一个 lnInst 在不同 lnClass 下可能都需要展开,
* 例如MMXU / MSQI / MHAI。
*/
private List<LnNode> lnNodes = new ArrayList<LnNode>();
}

View File

@@ -0,0 +1,29 @@
package com.njcn.gather.icd.mapping.pojo.bo.state;
import lombok.Data;
import java.util.ArrayList;
import java.util.List;
/**
* 中间态总对象。
*
* 作用:
* 1. 对应原 C# 里“先形成中间态,再做最终 JSON 组装”的思路;
* 2. 把业务分组、用户绑定、最终 DataSet 选择结果集中保存;
* 3. 避免在 MappingGenerationService 里直接边遍历边拼 JSON便于后续扩展和排查。
*/
@Data
public class ReportAndDataSetState {
/** IED 名称。 */
private String iedName;
/** LD 实例名。 */
private String ldInst;
/** 分组状态列表。 */
private List<ReportGroupState> reportGroups = new ArrayList<ReportGroupState>();
/** 数据集选择状态列表。 */
private List<DataSetSelectionState> dataSetSelections = new ArrayList<DataSetSelectionState>();
}

View File

@@ -0,0 +1,27 @@
package com.njcn.gather.icd.mapping.pojo.bo.state;
import lombok.Data;
/**
* 单条最终有效绑定关系的中间态。
*/
@Data
public class ReportBindingState {
/** 分组 key。 */
private String groupKey;
/** 分组描述。 */
private String groupDesc;
/** 报告名。 */
private String reportName;
/** 数据集名。 */
private String dataSetName;
/** 标签。 */
private String label;
/** 绑定的 lnInst 数字。 */
private String lnInst;
}

View File

@@ -0,0 +1,38 @@
package com.njcn.gather.icd.mapping.pojo.bo.state;
import lombok.Data;
import com.njcn.gather.icd.mapping.pojo.bo.analysis.IndexCandidateReportItem;
import java.util.ArrayList;
import java.util.List;
/**
* 单个业务分组的中间态。
*/
@Data
public class ReportGroupState {
/** 分组唯一键。 */
private String groupKey;
/** 分组描述。 */
private String groupDesc;
/** 当前分组匹配到的报告数量。 */
private int reportCount;
/** 报表 inst来自 DefaultCfg.ReportList.inst。 */
private String reportInst;
/** Select来自 DefaultCfg.ReportList.Select。 */
private String select;
/** TrgOps来自 DefaultCfg.ReportList.TrgOps。 */
private String trgOps;
/** 当前分组包含的报告列表。 */
private List<IndexCandidateReportItem> reportItems = new ArrayList<IndexCandidateReportItem>();
/** 用户最终确认的绑定关系。 */
private List<ReportBindingState> bindings = new ArrayList<ReportBindingState>();
}

View File

@@ -0,0 +1,255 @@
package com.njcn.gather.icd.mapping.pojo.bo.template;
import lombok.Data;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* 默认模板模型。
*
* 用于承接 `DefaultCfg.txt` 解析后的配置内容,当前仅保留映射接口实际使用的字段。
*/
@JsonIgnoreProperties(ignoreUnknown = true)
@Data
public class DefaultTemplate {
/** 报告分组配置,决定 ReportMap 和前端候选分组。 */
@JsonProperty("ReportList")
private List<ReportCfgItem> reportList = new ArrayList<ReportCfgItem>();
/** LN 类别配置,决定 DataSetList 的业务分组描述。 */
@JsonProperty("LnClassList")
private List<LnClassCfgItem> lnClassList = new ArrayList<LnClassCfgItem>();
/** 相别配置,用于生成 sdiList 的相别描述。 */
@JsonProperty("PhaseList")
private List<PhaseCfgItem> phaseList = new ArrayList<PhaseCfgItem>();
/** 倍率配置,用于从 DOI/DAI 值解析 coefficient。 */
@JsonProperty("MultiplierList")
private List<MultiplierCfgItem> multiplierList = new ArrayList<MultiplierCfgItem>();
/** 单位配置,用于从 DOI/DAI 值解析 unit。 */
@JsonProperty("UnitList")
private List<UnitCfgItem> unitList = new ArrayList<UnitCfgItem>();
/** 类型配置,用于识别 mag/ang 等 typeList 节点。 */
@JsonProperty("TypeList")
private List<TypeCfgItem> typeList = new ArrayList<TypeCfgItem>();
/** 数据对象配置,决定每个绑定标签应生成哪些 DOI。 */
@JsonProperty("DataObjectsList")
private List<DataObjectCfgItem> dataObjectsList = new ArrayList<DataObjectCfgItem>();
/**
* 执行模板基础校验。
*
* 返回问题列表;为空表示校验通过。
*/
public List<String> verify() {
List<String> problems = new ArrayList<String>();
ensureDuplicateNames("LnClassList", extractNames(lnClassList), problems);
ensureDuplicateNames("PhaseList", extractNames(phaseList), problems);
ensureDuplicateNames("MultiplierList", extractNames(multiplierList), problems);
ensureDuplicateNames("UnitList", extractNames(unitList), problems);
ensureDuplicateNames("TypeList", extractNames(typeList), problems);
ensureDuplicateObjectNames(problems);
if (reportList == null || reportList.isEmpty()) {
problems.add("DefaultCfg.ReportList 为空");
}
return problems;
}
/**
* 汇总配置项中的所有 nameList 名称,用于重复项校验。
*/
private List<String> extractNames(List<? extends NameListSupport> list) {
List<String> names = new ArrayList<String>();
if (list == null) {
return names;
}
for (NameListSupport item : list) {
if (item.getNameList() != null) {
names.addAll(item.getNameList());
}
}
return names;
}
/**
* 校验指定配置段是否存在重复名称。
*/
private void ensureDuplicateNames(String section, List<String> names, List<String> problems) {
Set<String> set = new HashSet<String>();
for (String name : names) {
if (name == null) {
continue;
}
String key = name.trim();
if (!set.add(key)) {
problems.add(section + " 中存在重复配置项:" + key);
}
}
}
/**
* 校验每个 DataObjectsList 分组内的对象名称是否重复。
*/
private void ensureDuplicateObjectNames(List<String> problems) {
if (dataObjectsList == null) {
return;
}
for (DataObjectCfgItem dataObject : dataObjectsList) {
List<String> names = new ArrayList<String>();
if (dataObject.getObjectList() != null) {
for (ObjectCfgItem object : dataObject.getObjectList()) {
if (object.getNameList() != null) {
names.addAll(object.getNameList());
}
}
}
ensureDuplicateNames("DataObjectsList[" + dataObject.getDesc() + "]", names, problems);
}
}
/** 统一抽象:所有包含 `nameList` 的配置项都实现该接口。 */
public interface NameListSupport {
List<String> getNameList();
}
@JsonIgnoreProperties(ignoreUnknown = true)
@Data
public static class ReportCfgItem {
/**
* 报告分组描述,例如“统计数据”“实时数据”“波动闪变”。
*/
@JsonProperty("desc")
private String desc;
/**
* 报告 `inst`,例如 `01`。
*/
@JsonProperty("inst")
private String inst;
/**
* 原始配置中的 `TrgOps`,例如 `40`、`96`。
*
* 这里必须显式声明 `@JsonProperty("TrgOps")`,否则在当前项目中容易反序列化为 `null`。
*/
@JsonProperty("TrgOps")
private String trgOps;
/**
* 原始配置中的 `Select`,例如 `DataStatFileMap`、`DataRealFileMap`、`FlickerFileMap`。
*
* 这里必须显式声明 `@JsonProperty("Select")`,否则在当前项目中容易反序列化为 `null`。
*/
@JsonProperty("Select")
private String select;
/**
* 当前分组覆盖的数据集名称列表。
*/
@JsonProperty("DataSetList")
private List<String> dataSetList = new ArrayList<String>();
/**
* 当前分组允许配置的标签模板列表。
*/
@JsonProperty("LnInstList")
private List<String> lnInstList = new ArrayList<String>();
}
@JsonIgnoreProperties(ignoreUnknown = true)
@Data
public static class LnClassCfgItem implements NameListSupport {
/** LN 类别描述。 */
private String desc;
/** 当前描述覆盖的 lnClass 名称列表。 */
private List<String> nameList = new ArrayList<String>();
}
@JsonIgnoreProperties(ignoreUnknown = true)
@Data
public static class PhaseCfgItem implements NameListSupport {
/** 相别中文描述。 */
private String desc;
/** 当前相别描述覆盖的 SDI 名称列表。 */
private List<String> nameList = new ArrayList<String>();
}
@JsonIgnoreProperties(ignoreUnknown = true)
@Data
public static class MultiplierCfgItem implements NameListSupport {
/** 倍率数值。 */
private int multiplier;
/** 能映射到该倍率的 DAI 值列表。 */
private List<String> nameList = new ArrayList<String>();
}
@JsonIgnoreProperties(ignoreUnknown = true)
@Data
public static class UnitCfgItem implements NameListSupport {
/** 单位描述。 */
private String desc;
/** 能映射到该单位的 DAI 值列表。 */
private List<String> nameList = new ArrayList<String>();
}
@JsonIgnoreProperties(ignoreUnknown = true)
@Data
public static class TypeCfgItem implements NameListSupport {
/** 类型中文描述。 */
private String desc;
/** 能映射到该类型的 SDI 名称列表。 */
private List<String> nameList = new ArrayList<String>();
}
@JsonIgnoreProperties(ignoreUnknown = true)
@Data
public static class DataObjectCfgItem {
/** 数据对象业务描述。 */
private String desc;
/** 当前数据对象配置适用的绑定标签列表。 */
@JsonProperty("LnInstList")
private List<String> lnInstList = new ArrayList<String>();
/** 当前标签下需要输出的 DOI 对象配置。 */
@JsonProperty("ObjectList")
private List<ObjectCfgItem> objectList = new ArrayList<ObjectCfgItem>();
}
@JsonIgnoreProperties(ignoreUnknown = true)
@Data
public static class ObjectCfgItem implements NameListSupport {
/** DOI 输出描述。 */
private String desc;
/** 基波标志,直接写入最终 doiList.baseflag。 */
private int baseflag;
/** 普通 DOI 分支下的基波数量。 */
private int basecount;
/** queueList 分支下的队列数量。 */
private int queuecount;
/** 普通匹配优先使用的 DOI 名称列表。 */
private List<String> nameList = new ArrayList<String>();
/** nameList 未命中时才使用的 DOI 兜底名称列表。 */
private List<String> queueList = new ArrayList<String>();
}
}

View File

@@ -0,0 +1,43 @@
package com.njcn.gather.icd.mapping.pojo.dto;
import lombok.Data;
import com.njcn.gather.icd.mapping.pojo.bo.icd.IcdDocument;
import java.util.ArrayList;
import java.util.List;
/**
* ICD 映射生成命令。
*
* 用于隔离接口层参数,便于应用层统一编排生成流程。
*/
@Data
public class GenerateFromIcdCommand {
/** 原始文件名。 */
private String fileName;
/** ICD 文件字节数组。 */
private byte[] fileBytes;
/** 前端确认或修改后的 ICD 解析结果。 */
private IcdDocument icdDocument;
/** 输出版本号。 */
private String version;
/** 作者。 */
private String author;
/** 是否保存到磁盘。 */
private boolean saveToDisk;
/** 是否输出美化 JSON。 */
private boolean prettyJson;
/** 输出目录。 */
private String outputDir;
/** 用户提交的索引选择结果。 */
private List<IndexSelectionGroupCommand> indexSelection = new ArrayList<IndexSelectionGroupCommand>();
}

View File

@@ -0,0 +1,21 @@
package com.njcn.gather.icd.mapping.pojo.dto;
import lombok.Data;
/**
* 应用层单条绑定命令。
*/
@Data
public class IndexBindingCommand {
/** 报告名。 */
private String reportName;
/** 数据集名。 */
private String dataSetName;
/** 标签。 */
private String label;
/** 绑定到的 lnInst 数字。 */
private String lnInst;
}

View File

@@ -0,0 +1,21 @@
package com.njcn.gather.icd.mapping.pojo.dto;
import lombok.Data;
import java.util.ArrayList;
import java.util.List;
/**
* 应用层业务分组选择命令。
*/
@Data
public class IndexSelectionGroupCommand {
/** 分组唯一键。 */
private String groupKey;
/** 分组中文描述。 */
private String groupDesc;
/** 当前分组下的多条绑定关系。 */
private List<IndexBindingCommand> bindings = new ArrayList<IndexBindingCommand>();
}

View File

@@ -1,12 +1,12 @@
package com.njcn.gather.icd.mapping.enums;
package com.njcn.gather.icd.mapping.pojo.enums;
/**
* 接口生成状态
* 映射生成状态
*/
public enum GenerateStatus {
/** 生成成功。 */
SUCCESS,
/** 需要前端重新选择索引。 */
/** 需要前端补充或重新选择索引。 */
NEED_INDEX_SELECTION,
/** 生成失败。 */
FAILED

View File

@@ -0,0 +1,38 @@
package com.njcn.gather.icd.mapping.pojo.param;
import lombok.Data;
import java.util.ArrayList;
import java.util.List;
/**
* 候选接口请求体。
*
* 当前主要承载输出版本、作者、落盘选项等基础参数。
* 为兼容旧调用,仍保留 indexSelection 字段,但候选接口本身不依赖该字段。
*/
@Data
public class GenerateMappingFromIcdRequest {
/** 输出版本号。为空时后端默认补当天日期。 */
private String version;
/** 作者。为空时使用模块默认作者。 */
private String author;
/** 是否保存到磁盘。 */
private boolean saveToDisk;
/** 是否返回格式化 JSON。 */
private boolean prettyJson;
/** 输出目录。saveToDisk=true 时才会用到。 */
private String outputDir;
/**
* 兼容保留的索引选择结果。
*
* 当前候选接口会直接返回 indexCandidates 和 icdDocument
* 正式提交时请改用 get-mms-json 接口。
*/
private List<IndexSelectionGroupRequest> indexSelection = new ArrayList<IndexSelectionGroupRequest>();
}

View File

@@ -0,0 +1,24 @@
package com.njcn.gather.icd.mapping.pojo.param;
import lombok.Data;
/**
* 单条索引绑定请求。
*
* 一条绑定只表达一个最小关系:
* 某个报告 reportName 下,使用某个标签 label 与某个 lnInst 数字做绑定。
*/
@Data
public class IndexBindingRequest {
/** 绑定发生在哪个报告上,例如 brcbStHarm。 */
private String reportName;
/** 绑定发生在哪个数据集上,例如 dsStHarm。 */
private String dataSetName;
/** 业务标签,例如最大值、最小值、实时数据。 */
private String label;
/** 当前标签最终绑定到的 lnInst 数字,例如 1、2、3。 */
private String lnInst;
}

View File

@@ -0,0 +1,28 @@
package com.njcn.gather.icd.mapping.pojo.param;
import lombok.Data;
import java.util.ArrayList;
import java.util.List;
/**
* 单个业务分组的索引选择请求。
*
* 用于回传某个业务分组下,前端最终确认的多条绑定关系。
*/
@Data
public class IndexSelectionGroupRequest {
/**
* 分组唯一键。
*
* 该值由后端在 NEED_INDEX_SELECTION 场景返回,前端应原样回传,
* 避免仅依赖中文描述做匹配。
*/
private String groupKey;
/** 分组中文描述,例如“实时数据”“统计数据”。 */
private String groupDesc;
/** 当前业务分组下,用户最终确认的绑定关系。 */
private List<IndexBindingRequest> bindings = new ArrayList<IndexBindingRequest>();
}

View File

@@ -0,0 +1,37 @@
package com.njcn.gather.icd.mapping.pojo.param;
import lombok.Data;
import com.njcn.gather.icd.mapping.pojo.bo.icd.IcdDocument;
import java.util.ArrayList;
import java.util.List;
/**
* 提交索引绑定并生成映射的请求体。
*
* 第二个接口不再重复上传 ICD 文件,而是直接接收前端确认或修改后的 ICD 解析结果。
*/
@Data
public class SubmitIndexSelectionRequest {
/** 前端基于候选接口返回值确认或修改后的 ICD 解析结果。 */
private IcdDocument icdDocument;
/** 输出版本号。为空时后端默认补当天日期。 */
private String version;
/** 作者。为空时使用模块默认作者。 */
private String author;
/** 是否保存到磁盘。 */
private boolean saveToDisk;
/** 是否返回格式化 JSON。 */
private boolean prettyJson;
/** 输出目录。saveToDisk=true 时才会用到。 */
private String outputDir;
/** 用户最终确认的索引绑定关系。 */
private List<IndexSelectionGroupRequest> indexSelection = new ArrayList<IndexSelectionGroupRequest>();
}

View File

@@ -0,0 +1,27 @@
package com.njcn.gather.icd.mapping.pojo.vo;
import lombok.Data;
import com.fasterxml.jackson.annotation.JsonInclude;
import java.util.ArrayList;
import java.util.List;
/**
* 业务分组下的单个报告候选响应。
*/
@JsonInclude(JsonInclude.Include.NON_EMPTY)
@Data
public class IndexCandidateReportItemResponse {
/** 报告名称。 */
private String reportName;
/** 数据集名称。 */
private String dataSetName;
/** 报告描述。 */
private String reportDesc;
/** 当前报告可选的 `lnInst` 数值。 */
private List<String> availableLnInstValues = new ArrayList<String>();
}

View File

@@ -0,0 +1,33 @@
package com.njcn.gather.icd.mapping.pojo.vo;
import lombok.Data;
import com.fasterxml.jackson.annotation.JsonInclude;
import java.util.ArrayList;
import java.util.List;
/**
* 索引候选响应对象。
*
* 一个候选对应一个业务分组,分组下可包含多个报告,
* 前端据此完成模板标签与 `lnInst` 的人工绑定。
*/
@JsonInclude(JsonInclude.Include.NON_EMPTY)
@Data
public class IndexCandidateResponse {
/** 分组唯一键。 */
private String groupKey;
/** 分组中文描述。 */
private String groupDesc;
/** 当前分组包含的报告数。 */
private int reportCount;
/** 模板里配置的可选标签。 */
private List<String> templateLabels = new ArrayList<String>();
/** 当前分组下的报告候选列表。 */
private List<IndexCandidateReportItemResponse> reports = new ArrayList<IndexCandidateReportItemResponse>();
}

View File

@@ -0,0 +1,28 @@
package com.njcn.gather.icd.mapping.pojo.vo;
import lombok.Data;
/**
* 映射文档摘要响应。
*
* 用于返回最终映射结果中的关键信息摘要。
*/
@Data
public class MappingDocumentResponse {
/** 映射文档版本。 */
private String version;
/** 映射文档作者。 */
private String author;
/** 输出 JSON 中的 IED。 */
private String ied;
/** 输出 JSON 中的 LD。 */
private String ld;
/** ReportMap 条目数量。 */
private int reportCount;
/** DataSetList 分组数量。 */
private int dataSetCount;
}

View File

@@ -0,0 +1,39 @@
package com.njcn.gather.icd.mapping.pojo.vo;
import lombok.Data;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.njcn.gather.icd.mapping.pojo.bo.icd.IcdDocument;
import com.njcn.gather.icd.mapping.pojo.enums.GenerateStatus;
import java.util.ArrayList;
import java.util.List;
/**
* ICD 映射接口统一响应。
*
* 按接口阶段仅返回当前场景必需字段;空字段和空集合不参与序列化。
*/
@JsonInclude(JsonInclude.Include.NON_EMPTY)
@Data
public class MappingTaskResponse {
/** 本次接口处理状态。 */
private GenerateStatus status;
/** 状态说明或错误提示。 */
private String message;
/** 候选接口或需要重新选择索引时返回的 ICD 解析结果。 */
private IcdDocument icdDocument;
/** 正式生成成功后的完整映射 JSON。 */
private String mappingJson;
/** 生成文件落盘后的绝对路径。 */
private String savedPath;
/** 待绑定状态下返回的索引候选分组。 */
private List<IndexCandidateResponse> indexCandidates = new ArrayList<IndexCandidateResponse>();
/** 模板校验、候选分析或绑定校验问题。 */
private List<String> problems = new ArrayList<String>();
}

View File

@@ -0,0 +1,36 @@
package com.njcn.gather.icd.mapping.service;
import com.njcn.gather.icd.mapping.pojo.bo.GenerateMappingResult;
import com.njcn.gather.icd.mapping.pojo.dto.GenerateFromIcdCommand;
/**
* ICD 映射任务编排服务。
*
* 统一封装 ICD 解析、索引候选分析、索引绑定校验和正式映射生成流程。
*/
public interface MappingTaskService {
/**
* 解析 ICD 并返回索引候选结果。
*
* @param command ICD 文件与生成参数
* @return 候选分析结果
*/
GenerateMappingResult getICD(GenerateFromIcdCommand command);
/**
* 串联 getICD 与索引提交流程,直接返回正式提交阶段的处理结果。
*
* @param command ICD 文件与前端提交参数
* @return 正式提交阶段结果
*/
GenerateMappingResult getIcdMmsJson(GenerateFromIcdCommand command);
/**
* 根据前端确认后的索引绑定关系生成正式映射。
*
* @param command 已确认的 ICD 解析结果与索引绑定关系
* @return 映射生成结果
*/
GenerateMappingResult getMmsJson(GenerateFromIcdCommand command);
}

View File

@@ -0,0 +1,271 @@
package com.njcn.gather.icd.mapping.service.impl;
import com.njcn.gather.icd.mapping.component.DefaultTemplateLoader;
import com.njcn.gather.icd.mapping.component.FileStorageService;
import com.njcn.gather.icd.mapping.component.IcdParserService;
import com.njcn.gather.icd.mapping.component.IndexAnalysisService;
import com.njcn.gather.icd.mapping.component.IndexValidationService;
import com.njcn.gather.icd.mapping.component.MappingDocumentSerializer;
import com.njcn.gather.icd.mapping.component.MappingGenerationService;
import com.njcn.gather.icd.mapping.pojo.bo.GenerateMappingResult;
import com.njcn.gather.icd.mapping.pojo.bo.analysis.IndexAnalysis;
import com.njcn.gather.icd.mapping.pojo.bo.analysis.ValidationResult;
import com.njcn.gather.icd.mapping.pojo.bo.icd.IcdDocument;
import com.njcn.gather.icd.mapping.pojo.bo.mapping.MappingDocument;
import com.njcn.gather.icd.mapping.pojo.bo.template.DefaultTemplate;
import com.njcn.gather.icd.mapping.pojo.dto.GenerateFromIcdCommand;
import com.njcn.gather.icd.mapping.pojo.dto.IndexSelectionGroupCommand;
import com.njcn.gather.icd.mapping.pojo.enums.GenerateStatus;
import com.njcn.gather.icd.mapping.service.MappingTaskService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.util.List;
/**
* ICD 映射任务编排服务实现。
*
* 按固定链路组织 ICD 解析、索引分析、校验和映射生成,避免 Controller 关注业务编排细节。
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class MappingTaskServiceImpl implements MappingTaskService {
/** ICD 解析阶段名称。 */
private static final String ICD_PARSE_TASK_NAME = "ICD 解析";
/** 正式映射生成阶段名称。 */
private static final String MAPPING_GENERATE_TASK_NAME = "映射生成";
/** 候选分析成功提示。 */
private static final String INDEX_SELECTION_SUCCESS_MESSAGE = "ICD 解析成功,请确认解析结果并完成索引绑定后提交";
/** 索引绑定缺失提示。 */
private static final String INDEX_SELECTION_MISSING_MESSAGE = "索引配置缺失,请根据候选信息完成标签与数字索引的绑定后重新提交";
/** 索引绑定非法提示。 */
private static final String INDEX_SELECTION_INVALID_MESSAGE = "索引配置不合法,请根据候选信息完成标签与数字索引的绑定后重新提交";
/** 映射生成成功提示。 */
private static final String MAPPING_GENERATE_SUCCESS_MESSAGE = "映射生成成功";
/** ICD/SCL 文件解析服务。 */
private final IcdParserService icdParserService;
/** DefaultCfg.txt 默认模板加载器。 */
private final DefaultTemplateLoader defaultTemplateLoader;
/** 根据 ICD 和模板生成前端可选索引候选。 */
private final IndexAnalysisService indexAnalysisService;
/** 校验前端回传的标签与 lnInst 绑定关系。 */
private final IndexValidationService indexValidationService;
/** 根据有效绑定关系生成最终 MappingDocument。 */
private final MappingGenerationService mappingGenerationService;
/** 将 MappingDocument 序列化为 JSON。 */
private final MappingDocumentSerializer mappingDocumentSerializer;
/** 按需把生成结果保存到本地磁盘。 */
private final FileStorageService fileStorageService;
@Override
public GenerateMappingResult getICD(GenerateFromIcdCommand command) {
return executeTask(ICD_PARSE_TASK_NAME, result -> {
parseTaskContext(command, result);
result.setStatus(GenerateStatus.NEED_INDEX_SELECTION);
result.setMessage(INDEX_SELECTION_SUCCESS_MESSAGE);
});
}
@Override
public GenerateMappingResult getIcdMmsJson(GenerateFromIcdCommand command) {
GenerateMappingResult icdResult = getICD(command);
if (icdResult.getStatus() == GenerateStatus.FAILED || icdResult.getIcdDocument() == null) {
return icdResult;
}
command.setIcdDocument(icdResult.getIcdDocument());
return getMmsJson(command);
}
@Override
public GenerateMappingResult getMmsJson(GenerateFromIcdCommand command) {
return executeTask(MAPPING_GENERATE_TASK_NAME, result -> {
MappingTaskContext context = buildTaskContext(requireIcdDocument(command), result);
if (isIndexSelectionEmpty(command.getIndexSelection())) {
markNeedIndexSelection(result, INDEX_SELECTION_MISSING_MESSAGE);
return;
}
ValidationResult validationResult = indexValidationService.validate(context.indexAnalysis, command.getIndexSelection());
if (!validationResult.isValid()) {
markNeedIndexSelection(result, INDEX_SELECTION_INVALID_MESSAGE);
result.getProblems().addAll(validationResult.getProblems());
return;
}
MappingDocument mappingDocument = mappingGenerationService.generate(
context.icdDocument,
context.template,
context.indexAnalysis,
command.getIndexSelection(),
command.getVersion(),
command.getAuthor()
);
result.setMappingDocument(mappingDocument);
String mappingJson = serializeMappingDocument(mappingDocument, command.isPrettyJson());
result.setMappingJson(mappingJson);
if (command.isSaveToDisk()) {
String fileName = buildOutputFileName(context.icdDocument, command.isPrettyJson());
String savedPath = fileStorageService.save(fileName, mappingJson, command.getOutputDir());
result.setSavedPath(savedPath);
}
result.setStatus(GenerateStatus.SUCCESS);
result.setMessage(MAPPING_GENERATE_SUCCESS_MESSAGE);
});
}
/**
* 统一包装任务执行结果,避免每个入口重复编写异常兜底逻辑。
*/
private GenerateMappingResult executeTask(String taskName, MappingTaskAction action) {
GenerateMappingResult result = new GenerateMappingResult();
try {
action.execute(result);
} catch (Exception ex) {
handleTaskFailure(result, taskName, ex);
}
return result;
}
/**
* 解析上传的 ICD 文件,并补齐候选分析上下文。
*/
private MappingTaskContext parseTaskContext(GenerateFromIcdCommand command, GenerateMappingResult result) {
IcdDocument icdDocument = icdParserService.parse(command.getFileBytes(), command.getFileName());
return buildTaskContext(icdDocument, result);
}
/**
* 基于 ICD 解析结果补齐模板和候选分析信息,供后续提交流程复用。
*/
private MappingTaskContext buildTaskContext(IcdDocument icdDocument, GenerateMappingResult result) {
fillIcdSummary(result, icdDocument);
DefaultTemplate template = loadTemplate(result);
IndexAnalysis indexAnalysis = analyzeIndexCandidates(icdDocument, template, result);
result.setIndexAnalysis(indexAnalysis);
return new MappingTaskContext(icdDocument, template, indexAnalysis);
}
/**
* 提交流程必须带上前端确认后的 ICD 解析结果。
*/
private IcdDocument requireIcdDocument(GenerateFromIcdCommand command) {
if (command.getIcdDocument() == null) {
throw new IllegalArgumentException("ICD 解析结果不能为空");
}
return command.getIcdDocument();
}
/**
* 加载并校验默认模板。
*/
private DefaultTemplate loadTemplate(GenerateMappingResult result) {
DefaultTemplate template = defaultTemplateLoader.load();
result.getProblems().addAll(template.verify());
return template;
}
/**
* 分析 ICD 对应的索引候选。
*/
private IndexAnalysis analyzeIndexCandidates(IcdDocument icdDocument,
DefaultTemplate template,
GenerateMappingResult result) {
IndexAnalysis indexAnalysis = indexAnalysisService.analyze(icdDocument, template);
result.getProblems().addAll(indexAnalysis.getProblems());
return indexAnalysis;
}
/**
* 回填响应公共信息。
*/
private void fillIcdSummary(GenerateMappingResult result, IcdDocument icdDocument) {
result.setIedName(icdDocument.getIedName());
result.setLdInst(icdDocument.getLdInst());
result.setIcdDocument(icdDocument);
}
/**
* 标记当前仍需用户重新确认索引绑定。
*/
private void markNeedIndexSelection(GenerateMappingResult result, String message) {
result.setStatus(GenerateStatus.NEED_INDEX_SELECTION);
result.setMessage(message);
}
/**
* 根据输出模式选择紧凑或美化 JSON。
*/
private String serializeMappingDocument(MappingDocument mappingDocument, boolean prettyJson) {
return prettyJson
? mappingDocumentSerializer.toPrettyJson(mappingDocument)
: mappingDocumentSerializer.toCompactJson(mappingDocument);
}
/**
* 构造落盘文件名。
*
* 文件名优先使用 IED 名称;如 IED 名称为空或清理后为空,则回退使用 mapping。
*/
private String buildOutputFileName(IcdDocument icdDocument, boolean prettyJson) {
String baseName = icdDocument.getIedName() == null ? "mapping" : icdDocument.getIedName();
String safeBaseName = baseName.replaceAll("[\\\\/:*?\"<>|]+", "_").trim();
if (safeBaseName.isEmpty()) {
safeBaseName = "mapping";
}
return safeBaseName + (prettyJson ? "-mapping-pretty.json" : "-mapping.json");
}
/**
* 统一处理任务异常,避免控制层再补失败分支。
*/
private void handleTaskFailure(GenerateMappingResult result, String taskName, Exception ex) {
log.error("{}失败", taskName, ex);
String errorMessage = resolveErrorMessage(ex);
result.setStatus(GenerateStatus.FAILED);
result.setMessage(taskName + "失败:" + errorMessage);
result.getProblems().add(errorMessage);
}
private boolean isIndexSelectionEmpty(List<IndexSelectionGroupCommand> indexSelection) {
return indexSelection == null || indexSelection.isEmpty();
}
private String resolveErrorMessage(Exception ex) {
if (ex.getMessage() == null || ex.getMessage().trim().isEmpty()) {
return ex.getClass().getSimpleName();
}
return ex.getMessage();
}
@FunctionalInterface
private interface MappingTaskAction {
void execute(GenerateMappingResult result) throws Exception;
}
/**
* 供多个任务阶段复用的编排上下文。
*/
private static class MappingTaskContext {
private final IcdDocument icdDocument;
private final DefaultTemplate template;
private final IndexAnalysis indexAnalysis;
private MappingTaskContext(IcdDocument icdDocument, DefaultTemplate template, IndexAnalysis indexAnalysis) {
this.icdDocument = icdDocument;
this.template = template;
this.indexAnalysis = indexAnalysis;
}
}
}

View File

@@ -1,17 +1,37 @@
package com.njcn.gather.icd.mapping.utils;
import java.text.SimpleDateFormat;
import java.util.Collection;
import java.util.Date;
import org.springframework.web.multipart.MultipartFile;
/**
* 日期工具。
* 日期格式化工具。
*/
public final class DateUtils {
private DateUtils() {
}
/**
* 返回当前时间文本,格式为 yyyy-MM-dd HH:mm:ss。
*/
public static String nowText() {
return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date());
}
}
/**
* 统一提取上传文件名,避免控制器重复判空。
*/
public static String resolveFileName(MultipartFile file) {
return file == null ? null : file.getOriginalFilename();
}
/**
* 统一统计选择项数量,空集合按 0 处理。
*/
public static int resolveSelectionCount(Collection<?> selections) {
return selections == null ? 0 : selections.size();
}
}

View File

@@ -1,22 +1,23 @@
package com.njcn.gather.icd.mapping.utils;
/**
* JSON 文本处理工具。
* JSON 文本清洗工具。
*
* 作用:
* 1. 兼容老配置文件中常见的尾逗号问题。
* 2. 降低 DefaultCfg.txt 因历史书写习惯造成的解析失败概率。
* 当前主要用于兼容历史模板中常见的尾逗号写法,减少 `DefaultCfg.txt` 解析失败。
*/
public final class JsonUtils {
private JsonUtils() {
}
/**
* 清理历史 JSON 模板中对象或数组结束前的尾逗号。
*/
public static String sanitizeJson(String text) {
if (text == null) {
return null;
}
// 去掉对象或数组结束前的尾逗号。
// 删除对象或数组结束前的尾逗号,兼容历史配置写法
return text.replaceAll(",\\s*([}\\]])", "$1");
}
}