From ed1dd56ad45d2770949613e561a715f5c2035ba0 Mon Sep 17 00:00:00 2001 From: chenqi Date: Fri, 19 Jun 2026 10:33:41 +0800 Subject: [PATCH] =?UTF-8?q?feat(kg):=20=E6=8E=A8=E7=90=86=E5=BC=95?= =?UTF-8?q?=E6=93=8E+=E6=95=B0=E6=8D=AE=E5=AF=BC=E5=85=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../appservice/IKgDataImportAppService.java | 13 + .../appservice/IKgReasoningAppService.java | 17 ++ .../impl/KgDataImportAppServiceImpl.java | 273 ++++++++++++++++++ .../impl/KgReasoningAppServiceImpl.java | 243 ++++++++++++++++ .../controller/KgDataImportController.java | 103 +++++++ .../controller/KgReasoningController.java | 76 +++++ 6 files changed, 725 insertions(+) create mode 100644 healthlink-his-server/healthlink-his-application/src/main/java/com/healthlink/his/web/knowledgegraph/appservice/IKgDataImportAppService.java create mode 100644 healthlink-his-server/healthlink-his-application/src/main/java/com/healthlink/his/web/knowledgegraph/appservice/IKgReasoningAppService.java create mode 100644 healthlink-his-server/healthlink-his-application/src/main/java/com/healthlink/his/web/knowledgegraph/appservice/impl/KgDataImportAppServiceImpl.java create mode 100644 healthlink-his-server/healthlink-his-application/src/main/java/com/healthlink/his/web/knowledgegraph/appservice/impl/KgReasoningAppServiceImpl.java create mode 100644 healthlink-his-server/healthlink-his-application/src/main/java/com/healthlink/his/web/knowledgegraph/controller/KgDataImportController.java create mode 100644 healthlink-his-server/healthlink-his-application/src/main/java/com/healthlink/his/web/knowledgegraph/controller/KgReasoningController.java diff --git a/healthlink-his-server/healthlink-his-application/src/main/java/com/healthlink/his/web/knowledgegraph/appservice/IKgDataImportAppService.java b/healthlink-his-server/healthlink-his-application/src/main/java/com/healthlink/his/web/knowledgegraph/appservice/IKgDataImportAppService.java new file mode 100644 index 000000000..13d15e98e --- /dev/null +++ b/healthlink-his-server/healthlink-his-application/src/main/java/com/healthlink/his/web/knowledgegraph/appservice/IKgDataImportAppService.java @@ -0,0 +1,13 @@ +package com.healthlink.his.web.knowledgegraph.appservice; + +import com.healthlink.his.web.knowledgegraph.dto.ImportResultDto; +import org.springframework.web.multipart.MultipartFile; + +public interface IKgDataImportAppService { + + ImportResultDto importDiseaseFromCsv(MultipartFile file); + + ImportResultDto importDrugFromCsv(MultipartFile file); + + ImportResultDto importRelationsFromCsv(MultipartFile file); +} diff --git a/healthlink-his-server/healthlink-his-application/src/main/java/com/healthlink/his/web/knowledgegraph/appservice/IKgReasoningAppService.java b/healthlink-his-server/healthlink-his-application/src/main/java/com/healthlink/his/web/knowledgegraph/appservice/IKgReasoningAppService.java new file mode 100644 index 000000000..cc0897194 --- /dev/null +++ b/healthlink-his-server/healthlink-his-application/src/main/java/com/healthlink/his/web/knowledgegraph/appservice/IKgReasoningAppService.java @@ -0,0 +1,17 @@ +package com.healthlink.his.web.knowledgegraph.appservice; + +import com.healthlink.his.web.knowledgegraph.dto.*; + +import java.util.List; +import java.util.Map; + +public interface IKgReasoningAppService { + + List suggestDiagnosis(List symptoms, Integer topN); + + List suggestExaminations(String diseaseCode, Integer topN); + + List checkDrugInteractions(List drugCodes); + + Map suggestPathway(String diseaseCode); +} diff --git a/healthlink-his-server/healthlink-his-application/src/main/java/com/healthlink/his/web/knowledgegraph/appservice/impl/KgDataImportAppServiceImpl.java b/healthlink-his-server/healthlink-his-application/src/main/java/com/healthlink/his/web/knowledgegraph/appservice/impl/KgDataImportAppServiceImpl.java new file mode 100644 index 000000000..d2a8291ba --- /dev/null +++ b/healthlink-his-server/healthlink-his-application/src/main/java/com/healthlink/his/web/knowledgegraph/appservice/impl/KgDataImportAppServiceImpl.java @@ -0,0 +1,273 @@ +package com.healthlink.his.web.knowledgegraph.appservice.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.healthlink.his.clinical.domain.KgEntityRelation; +import com.healthlink.his.clinical.service.IKgEntityRelationService; +import com.healthlink.his.knowledgegraph.domain.KgDisease; +import com.healthlink.his.knowledgegraph.domain.KgDrug; +import com.healthlink.his.knowledgegraph.service.IKgDiseaseService; +import com.healthlink.his.knowledgegraph.service.IKgDrugService; +import com.healthlink.his.web.knowledgegraph.appservice.IKgDataImportAppService; +import com.healthlink.his.web.knowledgegraph.dto.ImportResultDto; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; + +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.math.BigDecimal; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; + +@Slf4j +@Service +public class KgDataImportAppServiceImpl implements IKgDataImportAppService { + + @Autowired + private IKgDiseaseService diseaseService; + @Autowired + private IKgDrugService drugService; + @Autowired + private IKgEntityRelationService relationService; + + @Override + @Transactional(rollbackFor = Exception.class) + public ImportResultDto importDiseaseFromCsv(MultipartFile file) { + ImportResultDto result = new ImportResultDto(); + List batch = new ArrayList<>(); + int totalRows = 0; + int successCount = 0; + int failCount = 0; + + try (BufferedReader reader = new BufferedReader(new InputStreamReader(file.getInputStream(), StandardCharsets.UTF_8))) { + String header = reader.readLine(); + if (header == null) { + result.setMessage("CSV文件为空"); + result.setTotalRows(0); + result.setSuccessCount(0); + result.setFailCount(0); + return result; + } + + String line; + while ((line = reader.readLine()) != null) { + totalRows++; + try { + String[] parts = parseCsvLine(line); + if (parts.length < 2) { + failCount++; + continue; + } + + KgDisease disease = new KgDisease(); + disease.setDiseaseCode(getField(parts, 0)); + disease.setDiseaseName(getField(parts, 1)); + disease.setCategory(getField(parts, 2)); + disease.setDepartment(getField(parts, 3)); + disease.setSeverityLevel(getField(parts, 4)); + disease.setDescription(getField(parts, 5)); + disease.setKeywords(getField(parts, 6)); + + if (hasText(disease.getDiseaseCode()) && hasText(disease.getDiseaseName())) { + batch.add(disease); + } else { + failCount++; + } + } catch (Exception e) { + log.warn("解析疾病CSV行失败: {}", line, e); + failCount++; + } + } + + if (!batch.isEmpty()) { + diseaseService.saveBatch(batch); + successCount = batch.size(); + } + } catch (Exception e) { + log.error("导入疾病CSV失败", e); + failCount = totalRows - successCount; + } + + result.setTotalRows(totalRows); + result.setSuccessCount(successCount); + result.setFailCount(failCount); + result.setMessage(String.format("导入完成: 共%d行, 成功%d条, 失败%d条", totalRows, successCount, failCount)); + return result; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public ImportResultDto importDrugFromCsv(MultipartFile file) { + ImportResultDto result = new ImportResultDto(); + List batch = new ArrayList<>(); + int totalRows = 0; + int successCount = 0; + int failCount = 0; + + try (BufferedReader reader = new BufferedReader(new InputStreamReader(file.getInputStream(), StandardCharsets.UTF_8))) { + String header = reader.readLine(); + if (header == null) { + result.setMessage("CSV文件为空"); + result.setTotalRows(0); + result.setSuccessCount(0); + result.setFailCount(0); + return result; + } + + String line; + while ((line = reader.readLine()) != null) { + totalRows++; + try { + String[] parts = parseCsvLine(line); + if (parts.length < 2) { + failCount++; + continue; + } + + KgDrug drug = new KgDrug(); + drug.setDrugCode(getField(parts, 0)); + drug.setDrugName(getField(parts, 1)); + drug.setGenericName(getField(parts, 2)); + drug.setCategory(getField(parts, 3)); + drug.setDosageForm(getField(parts, 4)); + drug.setContraindications(getField(parts, 5)); + drug.setSideEffects(getField(parts, 6)); + + if (hasText(drug.getDrugCode()) && hasText(drug.getDrugName())) { + batch.add(drug); + } else { + failCount++; + } + } catch (Exception e) { + log.warn("解析药物CSV行失败: {}", line, e); + failCount++; + } + } + + if (!batch.isEmpty()) { + drugService.saveBatch(batch); + successCount = batch.size(); + } + } catch (Exception e) { + log.error("导入药物CSV失败", e); + failCount = totalRows - successCount; + } + + result.setTotalRows(totalRows); + result.setSuccessCount(successCount); + result.setFailCount(failCount); + result.setMessage(String.format("导入完成: 共%d行, 成功%d条, 失败%d条", totalRows, successCount, failCount)); + return result; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public ImportResultDto importRelationsFromCsv(MultipartFile file) { + ImportResultDto result = new ImportResultDto(); + List batch = new ArrayList<>(); + int totalRows = 0; + int successCount = 0; + int failCount = 0; + + try (BufferedReader reader = new BufferedReader(new InputStreamReader(file.getInputStream(), StandardCharsets.UTF_8))) { + String header = reader.readLine(); + if (header == null) { + result.setMessage("CSV文件为空"); + result.setTotalRows(0); + result.setSuccessCount(0); + result.setFailCount(0); + return result; + } + + String line; + while ((line = reader.readLine()) != null) { + totalRows++; + try { + String[] parts = parseCsvLine(line); + if (parts.length < 5) { + failCount++; + continue; + } + + KgEntityRelation relation = new KgEntityRelation(); + relation.setSourceType(getField(parts, 0)); + relation.setSourceId(getField(parts, 1)); + relation.setTargetType(getField(parts, 2)); + relation.setTargetId(getField(parts, 3)); + relation.setRelationType(getField(parts, 4)); + + String strengthStr = getField(parts, 5); + if (hasText(strengthStr)) { + try { + relation.setRelationStrength(new BigDecimal(strengthStr)); + } catch (NumberFormatException e) { + relation.setRelationStrength(BigDecimal.ONE); + } + } else { + relation.setRelationStrength(BigDecimal.ONE); + } + relation.setDescription(getField(parts, 6)); + relation.setEvidenceSource(getField(parts, 7)); + + if (hasText(relation.getSourceType()) && hasText(relation.getSourceId()) + && hasText(relation.getTargetType()) && hasText(relation.getTargetId())) { + batch.add(relation); + } else { + failCount++; + } + } catch (Exception e) { + log.warn("解析关系CSV行失败: {}", line, e); + failCount++; + } + } + + if (!batch.isEmpty()) { + relationService.saveBatch(batch); + successCount = batch.size(); + } + } catch (Exception e) { + log.error("导入关系CSV失败", e); + failCount = totalRows - successCount; + } + + result.setTotalRows(totalRows); + result.setSuccessCount(successCount); + result.setFailCount(failCount); + result.setMessage(String.format("导入完成: 共%d行, 成功%d条, 失败%d条", totalRows, successCount, failCount)); + return result; + } + + private String[] parseCsvLine(String line) { + List fields = new ArrayList<>(); + StringBuilder current = new StringBuilder(); + boolean inQuotes = false; + + for (char c : line.toCharArray()) { + if (c == '"') { + inQuotes = !inQuotes; + } else if (c == ',' && !inQuotes) { + fields.add(current.toString().trim()); + current = new StringBuilder(); + } else { + current.append(c); + } + } + fields.add(current.toString().trim()); + return fields.toArray(new String[0]); + } + + private String getField(String[] parts, int index) { + if (index < parts.length) { + String val = parts[index]; + return hasText(val) ? val : null; + } + return null; + } + + private boolean hasText(String s) { + return s != null && !s.trim().isEmpty(); + } +} diff --git a/healthlink-his-server/healthlink-his-application/src/main/java/com/healthlink/his/web/knowledgegraph/appservice/impl/KgReasoningAppServiceImpl.java b/healthlink-his-server/healthlink-his-application/src/main/java/com/healthlink/his/web/knowledgegraph/appservice/impl/KgReasoningAppServiceImpl.java new file mode 100644 index 000000000..3464524ea --- /dev/null +++ b/healthlink-his-server/healthlink-his-application/src/main/java/com/healthlink/his/web/knowledgegraph/appservice/impl/KgReasoningAppServiceImpl.java @@ -0,0 +1,243 @@ +package com.healthlink.his.web.knowledgegraph.appservice.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.healthlink.his.clinical.domain.KgClinicalPathway; +import com.healthlink.his.clinical.domain.KgEntityRelation; +import com.healthlink.his.clinical.domain.KgPathwayStep; +import com.healthlink.his.clinical.service.IKgClinicalPathwayService; +import com.healthlink.his.clinical.service.IKgEntityRelationService; +import com.healthlink.his.clinical.service.IKgPathwayStepService; +import com.healthlink.his.knowledgegraph.domain.KgDisease; +import com.healthlink.his.knowledgegraph.domain.KgDrug; +import com.healthlink.his.knowledgegraph.domain.KgExamination; +import com.healthlink.his.knowledgegraph.domain.KgSymptom; +import com.healthlink.his.knowledgegraph.service.IKgDiseaseService; +import com.healthlink.his.knowledgegraph.service.IKgDrugService; +import com.healthlink.his.knowledgegraph.service.IKgExaminationService; +import com.healthlink.his.knowledgegraph.service.IKgSymptomService; +import com.healthlink.his.web.knowledgegraph.appservice.IKgReasoningAppService; +import com.healthlink.his.web.knowledgegraph.dto.*; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.util.StringUtils; + +import java.math.BigDecimal; +import java.util.*; +import java.util.stream.Collectors; + +@Service +public class KgReasoningAppServiceImpl implements IKgReasoningAppService { + + @Autowired + private IKgEntityRelationService relationService; + @Autowired + private IKgSymptomService symptomService; + @Autowired + private IKgDiseaseService diseaseService; + @Autowired + private IKgExaminationService examinationService; + @Autowired + private IKgDrugService drugService; + @Autowired + private IKgClinicalPathwayService pathwayService; + @Autowired + private IKgPathwayStepService pathwayStepService; + + @Override + public List suggestDiagnosis(List symptoms, Integer topN) { + if (symptoms == null || symptoms.isEmpty()) { + return Collections.emptyList(); + } + if (topN == null || topN <= 0) { + topN = 5; + } + + // 1. Find symptom IDs by name/code + LambdaQueryWrapper sw = new LambdaQueryWrapper<>(); + sw.and(w -> { + for (String symptom : symptoms) { + w.or().like(KgSymptom::getSymptomName, symptom) + .or().like(KgSymptom::getSymptomCode, symptom); + } + }); + List matchedSymptoms = symptomService.list(sw); + if (matchedSymptoms.isEmpty()) { + return Collections.emptyList(); + } + + List symptomIds = matchedSymptoms.stream() + .map(s -> String.valueOf(s.getId())) + .collect(Collectors.toList()); + + // 2. Query relations: symptom -> disease + LambdaQueryWrapper rw = new LambdaQueryWrapper<>(); + rw.eq(KgEntityRelation::getSourceType, "symptom") + .eq(KgEntityRelation::getTargetType, "disease") + .in(KgEntityRelation::getSourceId, symptomIds) + .orderByDesc(KgEntityRelation::getRelationStrength); + List relations = relationService.list(rw); + + // 3. Group by disease, sum scores + Map diseaseScoreMap = new LinkedHashMap<>(); + Map> diseaseSymptomMap = new LinkedHashMap<>(); + for (KgEntityRelation rel : relations) { + String diseaseId = rel.getTargetId(); + BigDecimal strength = rel.getRelationStrength() != null ? rel.getRelationStrength() : BigDecimal.ONE; + diseaseScoreMap.merge(diseaseId, strength, BigDecimal::add); + diseaseSymptomMap.computeIfAbsent(diseaseId, k -> new ArrayList<>()) + .add(rel.getSourceId()); + } + + // 4. Sort by score descending, take top N + List> sorted = diseaseScoreMap.entrySet().stream() + .sorted(Map.Entry.comparingByValue().reversed()) + .limit(topN) + .collect(Collectors.toList()); + + // 5. Build results with disease info + List results = new ArrayList<>(); + for (Map.Entry entry : sorted) { + KgDisease disease = diseaseService.getById(Long.parseLong(entry.getKey())); + if (disease == null) continue; + + DiagnosisResultDto dto = new DiagnosisResultDto(); + dto.setDiseaseCode(disease.getDiseaseCode()); + dto.setDiseaseName(disease.getDiseaseName()); + dto.setCategory(disease.getCategory()); + dto.setDepartment(disease.getDepartment()); + dto.setScore(entry.getValue()); + + List matched = diseaseSymptomMap.getOrDefault(entry.getKey(), Collections.emptyList()); + dto.setMatchedSymptoms(String.join(",", matched)); + results.add(dto); + } + return results; + } + + @Override + public List suggestExaminations(String diseaseCode, Integer topN) { + if (!StringUtils.hasText(diseaseCode)) { + return Collections.emptyList(); + } + if (topN == null || topN <= 0) { + topN = 10; + } + + // 1. Find disease by code + LambdaQueryWrapper dw = new LambdaQueryWrapper<>(); + dw.eq(KgDisease::getDiseaseCode, diseaseCode); + KgDisease disease = diseaseService.getOne(dw); + if (disease == null) { + return Collections.emptyList(); + } + + // 2. Query relations: disease -> examination + LambdaQueryWrapper rw = new LambdaQueryWrapper<>(); + rw.eq(KgEntityRelation::getSourceType, "disease") + .eq(KgEntityRelation::getTargetType, "examination") + .eq(KgEntityRelation::getSourceId, String.valueOf(disease.getId())) + .orderByDesc(KgEntityRelation::getRelationStrength); + List relations = relationService.list(rw); + + // 3. Build results + List results = new ArrayList<>(); + for (KgEntityRelation rel : relations) { + if (results.size() >= topN) break; + KgExamination exam = examinationService.getById(Long.parseLong(rel.getTargetId())); + if (exam == null) continue; + + ExaminationResultDto dto = new ExaminationResultDto(); + dto.setExamCode(exam.getExamCode()); + dto.setExamName(exam.getExamName()); + dto.setExamType(exam.getExamType()); + dto.setClinicalSignificance(exam.getClinicalSignificance()); + dto.setScore(rel.getRelationStrength()); + results.add(dto); + } + return results; + } + + @Override + public List checkDrugInteractions(List drugCodes) { + if (drugCodes == null || drugCodes.size() < 2) { + return Collections.emptyList(); + } + + // 1. Find drug IDs by code + LambdaQueryWrapper dw = new LambdaQueryWrapper<>(); + dw.in(KgDrug::getDrugCode, drugCodes); + List drugs = drugService.list(dw); + if (drugs.isEmpty()) { + return Collections.emptyList(); + } + + Map drugMap = drugs.stream() + .collect(Collectors.toMap(KgDrug::getDrugCode, d -> d, (a, b) -> a)); + + List drugIds = drugs.stream() + .map(d -> String.valueOf(d.getId())) + .collect(Collectors.toList()); + + // 2. Query drug-drug interaction relations + LambdaQueryWrapper rw = new LambdaQueryWrapper<>(); + rw.eq(KgEntityRelation::getSourceType, "drug") + .eq(KgEntityRelation::getTargetType, "drug") + .in(KgEntityRelation::getSourceId, drugIds) + .in(KgEntityRelation::getTargetId, drugIds); + List relations = relationService.list(rw); + + // 3. Build results + List results = new ArrayList<>(); + Set added = new HashSet<>(); + + for (KgEntityRelation rel : relations) { + KgDrug drugA = drugService.getById(Long.parseLong(rel.getSourceId())); + KgDrug drugB = drugService.getById(Long.parseLong(rel.getTargetId())); + if (drugA == null || drugB == null) continue; + + String key = Collections.min(Arrays.asList(drugA.getDrugCode(), drugB.getDrugCode())) + + "-" + Collections.max(Arrays.asList(drugA.getDrugCode(), drugB.getDrugCode())); + if (!added.add(key)) continue; + + DrugInteractionResultDto dto = new DrugInteractionResultDto(); + dto.setDrugCodeA(drugA.getDrugCode()); + dto.setDrugNameA(drugA.getDrugName()); + dto.setDrugCodeB(drugB.getDrugCode()); + dto.setDrugNameB(drugB.getDrugName()); + dto.setInteractionType(rel.getRelationType()); + dto.setDescription(rel.getDescription()); + dto.setSeverity(rel.getRelationStrength() != null + ? (rel.getRelationStrength().compareTo(new BigDecimal("0.7")) >= 0 ? "严重" : "一般") + : "一般"); + results.add(dto); + } + return results; + } + + @Override + public Map suggestPathway(String diseaseCode) { + if (!StringUtils.hasText(diseaseCode)) { + return Collections.emptyMap(); + } + + // 1. Find pathway by disease code + LambdaQueryWrapper pw = new LambdaQueryWrapper<>(); + pw.eq(KgClinicalPathway::getDiseaseCode, diseaseCode) + .eq(KgClinicalPathway::getStatus, "ACTIVE"); + KgClinicalPathway pathway = pathwayService.getOne(pw); + if (pathway == null) { + return Collections.emptyMap(); + } + + // 2. Get pathway steps + LambdaQueryWrapper sw = new LambdaQueryWrapper<>(); + sw.eq(KgPathwayStep::getPathwayId, pathway.getId()) + .orderByAsc(KgPathwayStep::getStepOrder); + List steps = pathwayStepService.list(sw); + + Map result = new LinkedHashMap<>(); + result.put("pathway", pathway); + result.put("steps", steps); + return result; + } +} diff --git a/healthlink-his-server/healthlink-his-application/src/main/java/com/healthlink/his/web/knowledgegraph/controller/KgDataImportController.java b/healthlink-his-server/healthlink-his-application/src/main/java/com/healthlink/his/web/knowledgegraph/controller/KgDataImportController.java new file mode 100644 index 000000000..6881acba0 --- /dev/null +++ b/healthlink-his-server/healthlink-his-application/src/main/java/com/healthlink/his/web/knowledgegraph/controller/KgDataImportController.java @@ -0,0 +1,103 @@ +package com.healthlink.his.web.knowledgegraph.controller; + +import com.core.common.core.domain.R; +import com.healthlink.his.web.knowledgegraph.appservice.IKgDataImportAppService; +import com.healthlink.his.web.knowledgegraph.dto.ImportResultDto; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +import java.io.BufferedInputStream; +import java.io.InputStream; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; + +@Slf4j +@Tag(name = "知识图谱-数据导入") +@RestController +@RequestMapping("/knowledgegraph/import") +@AllArgsConstructor +public class KgDataImportController { + + private final IKgDataImportAppService kgDataImportAppService; + + @Operation(summary = "导入疾病数据") + @PreAuthorize("@ss.hasPermi('system:knowledgegraph:edit')") + @PostMapping("/disease") + public R importDisease(@RequestParam("file") MultipartFile file) { + try { + ImportResultDto result = kgDataImportAppService.importDiseaseFromCsv(file); + return R.ok(result); + } catch (Exception e) { + log.error("导入疾病数据失败", e); + return R.fail("导入疾病数据失败: " + e.getMessage()); + } + } + + @Operation(summary = "导入药物数据") + @PreAuthorize("@ss.hasPermi('system:knowledgegraph:edit')") + @PostMapping("/drug") + public R importDrug(@RequestParam("file") MultipartFile file) { + try { + ImportResultDto result = kgDataImportAppService.importDrugFromCsv(file); + return R.ok(result); + } catch (Exception e) { + log.error("导入药物数据失败", e); + return R.fail("导入药物数据失败: " + e.getMessage()); + } + } + + @Operation(summary = "导入关系数据") + @PreAuthorize("@ss.hasPermi('system:knowledgegraph:edit')") + @PostMapping("/relation") + public R importRelations(@RequestParam("file") MultipartFile file) { + try { + ImportResultDto result = kgDataImportAppService.importRelationsFromCsv(file); + return R.ok(result); + } catch (Exception e) { + log.error("导入关系数据失败", e); + return R.fail("导入关系数据失败: " + e.getMessage()); + } + } + + @Operation(summary = "下载导入模板") + @PreAuthorize("@ss.hasPermi('system:knowledgegraph:list')") + @GetMapping("/template/{type}") + public void downloadTemplate(@PathVariable String type, jakarta.servlet.http.HttpServletResponse response) throws Exception { + String filename; + String content; + + switch (type) { + case "disease": + filename = "疾病导入模板.csv"; + content = "疾病编码,疾病名称,分类,科室,严重等级,描述,关键词\n" + + "J06.900,急性上呼吸道感染,感染性疾病,呼吸内科,轻度,急性上呼吸道感染,发热;咳嗽;咽痛\n"; + break; + case "drug": + filename = "药物导入模板.csv"; + content = "药物编码,药物名称,通用名,分类,剂型,禁忌症,不良反应\n" + + "D00001,阿莫西林胶囊,阿莫西林,抗生素,胶囊剂,青霉素过敏者禁用,皮疹;腹泻\n"; + break; + case "relation": + filename = "关系导入模板.csv"; + content = "来源类型,来源ID,目标类型,目标ID,关系类型,关系强度,描述,证据来源\n" + + "symptom,1001,disease,2001,has_symptom,0.85,发热是急性上呼吸道感染的常见症状,临床指南\n"; + break; + default: + response.setStatus(400); + response.getWriter().write("不支持的模板类型"); + return; + } + + response.setContentType("text/csv;charset=UTF-8"); + response.setHeader("Content-Disposition", + "attachment; filename=" + URLEncoder.encode(filename, StandardCharsets.UTF_8)); + response.getOutputStream().write("\uFEFF".getBytes(StandardCharsets.UTF_8)); + response.getOutputStream().write(content.getBytes(StandardCharsets.UTF_8)); + response.getOutputStream().flush(); + } +} diff --git a/healthlink-his-server/healthlink-his-application/src/main/java/com/healthlink/his/web/knowledgegraph/controller/KgReasoningController.java b/healthlink-his-server/healthlink-his-application/src/main/java/com/healthlink/his/web/knowledgegraph/controller/KgReasoningController.java new file mode 100644 index 000000000..60d9c59c1 --- /dev/null +++ b/healthlink-his-server/healthlink-his-application/src/main/java/com/healthlink/his/web/knowledgegraph/controller/KgReasoningController.java @@ -0,0 +1,76 @@ +package com.healthlink.his.web.knowledgegraph.controller; + +import com.core.common.core.domain.R; +import com.healthlink.his.web.knowledgegraph.appservice.IKgReasoningAppService; +import com.healthlink.his.web.knowledgegraph.dto.*; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; + +import java.util.List; +import java.util.Map; + +@Slf4j +@Tag(name = "知识图谱-推理引擎") +@RestController +@RequestMapping("/knowledgegraph/reasoning") +@AllArgsConstructor +public class KgReasoningController { + + private final IKgReasoningAppService kgReasoningAppService; + + @Operation(summary = "诊断推荐 - 基于症状推荐诊断") + @PreAuthorize("@ss.hasPermi('system:knowledgegraph:list')") + @PostMapping("/diagnosis") + public R> suggestDiagnosis(@RequestBody DiagnosisSuggestDto dto) { + try { + List results = kgReasoningAppService.suggestDiagnosis(dto.getSymptoms(), dto.getTopN()); + return R.ok(results); + } catch (Exception e) { + log.error("诊断推荐失败", e); + return R.fail("诊断推荐失败: " + e.getMessage()); + } + } + + @Operation(summary = "检查推荐 - 基于诊断推荐检查") + @PreAuthorize("@ss.hasPermi('system:knowledgegraph:list')") + @PostMapping("/examination") + public R> suggestExaminations(@RequestBody ExaminationSuggestDto dto) { + try { + List results = kgReasoningAppService.suggestExaminations(dto.getDiseaseCode(), dto.getTopN()); + return R.ok(results); + } catch (Exception e) { + log.error("检查推荐失败", e); + return R.fail("检查推荐失败: " + e.getMessage()); + } + } + + @Operation(summary = "药物相互作用检查") + @PreAuthorize("@ss.hasPermi('system:knowledgegraph:list')") + @PostMapping("/drug-interaction") + public R> checkDrugInteractions(@RequestBody DrugInteractionDto dto) { + try { + List results = kgReasoningAppService.checkDrugInteractions(dto.getDrugCodes()); + return R.ok(results); + } catch (Exception e) { + log.error("药物相互作用检查失败", e); + return R.fail("药物相互作用检查失败: " + e.getMessage()); + } + } + + @Operation(summary = "临床路径推荐") + @PreAuthorize("@ss.hasPermi('system:knowledgegraph:list')") + @GetMapping("/pathway/{diseaseCode}") + public R> suggestPathway(@PathVariable String diseaseCode) { + try { + Map result = kgReasoningAppService.suggestPathway(diseaseCode); + return result.isEmpty() ? R.fail("未找到临床路径") : R.ok(result); + } catch (Exception e) { + log.error("临床路径推荐失败", e); + return R.fail("临床路径推荐失败: " + e.getMessage()); + } + } +}