Browse Source

三院详情病历原文显示 与 excel文件上传处理

zhaohan 8 months ago
parent
commit
00d6fdefdd

+ 58 - 0
ruoyi-modules-api/ruoyi-knowledge-api/src/main/java/org/ruoyi/chain/loader/ExcelFileLoader.java

@@ -0,0 +1,58 @@
+package org.ruoyi.chain.loader;
+
+import lombok.AllArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.poi.ss.usermodel.*;
+import org.springframework.stereotype.Component;
+import org.ruoyi.chain.split.TextSplitter;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.List;
+
+@Component
+@AllArgsConstructor
+@Slf4j
+public class ExcelFileLoader implements ResourceLoader {
+
+    private final TextSplitter textSplitter;
+
+    @Override
+    public String getContent(InputStream inputStream) {
+        StringBuilder sb = new StringBuilder();
+        try (Workbook workbook = WorkbookFactory.create(inputStream)) {
+            // 默认读取第一个sheet,可以根据需要循环所有sheet
+            Sheet sheet = workbook.getSheetAt(0);
+            for (Row row : sheet) {
+                for (Cell cell : row) {
+                    sb.append(getCellValue(cell)).append(" ");
+                }
+                sb.append("\n");
+            }
+        } catch (IOException e) {
+            log.error("读取Excel失败", e);
+            throw new RuntimeException("读取Excel失败", e);
+        }
+        return sb.toString();
+    }
+
+    @Override
+    public List<String> getChunkList(String content, String kid) {
+        return textSplitter.split(content, kid);
+    }
+
+    private String getCellValue(Cell cell) {
+        if (cell == null) return "";
+        switch (cell.getCellType()) {
+            case STRING: return cell.getStringCellValue();
+            case NUMERIC:
+                if (DateUtil.isCellDateFormatted(cell)) {
+                    return cell.getDateCellValue().toString();
+                }
+                return String.valueOf(cell.getNumericCellValue());
+            case BOOLEAN: return String.valueOf(cell.getBooleanCellValue());
+            case FORMULA: return cell.getCellFormula();
+            default: return "";
+        }
+    }
+}

+ 3 - 1
ruoyi-modules-api/ruoyi-knowledge-api/src/main/java/org/ruoyi/chain/loader/ResourceLoaderFactory.java

@@ -26,7 +26,9 @@ public class ResourceLoaderFactory {
             return new MarkDownFileLoader(markdownTextSplitter);
         }else if (FileType.isCodeFile(fileType)) {
             return new CodeFileLoader(codeTextSplitter);
-        }else {
+        } else if (FileType.isExcel(fileType)) {
+            return new ExcelFileLoader(characterTextSplitter);
+        } else {
             return new TextFileLoader(characterTextSplitter);
         }
     }

+ 35 - 0
ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/controller/chat/FileParseController.java

@@ -0,0 +1,35 @@
+package org.ruoyi.chat.controller.chat;
+
+import org.ruoyi.chat.service.FileParseService;
+import org.ruoyi.common.core.domain.R;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.multipart.MultipartFile;
+
+@RestController
+@RequestMapping("/parse")
+public class FileParseController {
+
+    private final FileParseService fileParseService;
+
+    @Autowired
+    public FileParseController(FileParseService fileParseService) {
+        this.fileParseService = fileParseService;
+    }
+
+    /**
+     * 上传文件并直接解析返回原文
+     */
+    @PostMapping("/file")
+    public R<String> parseFile(@RequestParam("file") MultipartFile file) {
+        try {
+            String content = fileParseService.parseFileContent(file);
+            return R.ok(content);
+        } catch (Exception e) {
+            return R.fail("文件解析失败: " + e.getMessage());
+        }
+    }
+}

+ 1 - 0
ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/controller/knowledge/KnowledgeController.java

@@ -25,6 +25,7 @@ import org.ruoyi.service.IKnowledgeAttachService;
 import org.ruoyi.service.IKnowledgeFragmentService;
 import org.ruoyi.service.IKnowledgeInfoService;
 import org.ruoyi.service.VectorStoreService;
+import org.springframework.http.MediaType;
 import org.springframework.validation.annotation.Validated;
 import org.springframework.web.bind.annotation.*;
 import org.springframework.web.multipart.MultipartFile;

+ 46 - 0
ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/FileParseService.java

@@ -0,0 +1,46 @@
+package org.ruoyi.chat.service;
+
+import org.ruoyi.chain.loader.ResourceLoader;
+import org.ruoyi.chain.loader.ResourceLoaderFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.io.InputStream;
+
+@Service
+public class FileParseService {
+
+    private final ResourceLoaderFactory resourceLoaderFactory;
+
+    @Autowired
+    public FileParseService(ResourceLoaderFactory resourceLoaderFactory) {
+        this.resourceLoaderFactory = resourceLoaderFactory;
+    }
+
+    /**
+     * 根据文件类型解析文件内容
+     */
+    public String parseFileContent(MultipartFile file) {
+        if (file == null || file.isEmpty()) {
+            throw new RuntimeException("文件不能为空");
+        }
+
+        String fileName = file.getOriginalFilename();
+        if (fileName == null) {
+            throw new RuntimeException("文件名为空");
+        }
+
+        // 获取文件后缀
+        String fileType = fileName.substring(fileName.lastIndexOf(".") + 1);
+
+        try (InputStream inputStream = file.getInputStream()) {
+            // 根据文件类型选择对应的 loader
+            ResourceLoader loader = resourceLoaderFactory.getLoaderByFileType(fileType);
+            // 解析原文
+            return loader.getContent(inputStream);
+        } catch (Exception e) {
+            throw new RuntimeException("文件解析失败: " + e.getMessage(), e);
+        }
+    }
+}

+ 5 - 5
ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/MedicalRecordQc123Service.java

@@ -12,7 +12,6 @@ import org.ruoyi.mapper.PatientRecordMapper;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.annotation.Value;
 import org.springframework.stereotype.Service;
-import org.springframework.transaction.annotation.Propagation;
 import org.springframework.transaction.annotation.Transactional;
 
 import java.time.LocalDateTime;
@@ -323,7 +322,7 @@ public class MedicalRecordQc123Service {
         sb.append("【质控规则】:\n")
                 .append(ragResult).append("\n\n");
 
-        sb.append("请根据以上内容,严格输出 **JSON 数组**,不得输出任何解释、总结或多余文字。\n\n");
+        sb.append("请根据以上内容,对病历进行质控,严格输出 **JSON 数组**,不得输出任何解释、总结或多余文字。\n\n");
 
         sb.append("输出格式要求如下:\n");
         sb.append("1. 如果病历完全符合所有规则:\n");
@@ -335,7 +334,7 @@ public class MedicalRecordQc123Service {
         sb.append("- content: 取一段违反规则的病历原文中的部分(四到六个字符,与病历原文中完全一致)\n");
         sb.append("- ruleCode: 规则编码(如:RYJL0001,必须与提供的质控规则中的编码一致)\n");
         sb.append("- ruleContent: 规则描述(必须与提供的质控规则中的规则详情一致)\n");
-        sb.append("- qcResult: 质控结论(指出不符合的内容或缺失项)\n\n");
+        sb.append("- qcResult: 质控结论(指出违反该条规则的原因)\n\n");
 
         sb.append("示例:\n");
         sb.append("[\n");
@@ -344,11 +343,12 @@ public class MedicalRecordQc123Service {
         sb.append("]\n\n");
 
         sb.append("⚠️ 注意:\n");
-        sb.append("1. 必须输出有效 JSON格式,不能有多余的符号或文字。\n");
+        sb.append("1. 必须输出有效 JSON 数组,不能有多余的符号或文字。\n");
         sb.append("2. 输出的规则部分必须严格来源于提供的规则,不得自行拟造\n");
         sb.append("3. 所有 key 必须用双引号。\n");
         sb.append("4. 如果没有违规,必须只输出 pass 的 JSON 数组。\n");
         sb.append("5. content 字段必须是从病历原文中提取的4-6个连续字符,与原文完全一致。\n");
+        sb.append("6. 禁止使用任何 Markdown 格式(如 ```json 或 ```),只能输出纯 JSON 数组。\n");
 
         return sb.toString();
     }
@@ -371,7 +371,7 @@ public class MedicalRecordQc123Service {
                     put("model", "qwen3-32B");
                     put("stream", false);
                     put("messages", messages);
-                    put("max_tokens", 4096); // ✅ 限制最大输出token数
+                    put("max_tokens", 10240); // ✅ 限制最大输出token数
                     put("chat_template_kwargs", "{\"enable_thinking\": false}");
                 }}
         );

+ 204 - 23
ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/PatientRecordServiceImpl.java

@@ -6,7 +6,9 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 
 import com.baomidou.mybatisplus.core.toolkit.Wrappers;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
-import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.core.type.TypeReference;
+
+import com.fasterxml.jackson.databind.DeserializationFeature;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import com.fasterxml.jackson.databind.node.ObjectNode;
 import jakarta.annotation.Resource;
@@ -506,12 +508,12 @@ public class PatientRecordServiceImpl implements PatientRecordService {
 
     @Override
     public List<RecordDetailItemDTO> getDetailListById(String id) {
-        // 查询数据库返回的记录列表
         List<Map<String, Object>> mapList = patientRecordMapper.selectDetailMapById(id);
         List<RecordDetailItemDTO> dtoList = new ArrayList<>();
 
         for (Map<String, Object> map : mapList) {
             String content = (String) map.get("content");
+            String typeName = (String) map.get("typeName");
 
             // 判断 content 是否为空或为 "[]"
             if (content == null || content.isEmpty() || "[]".equals(content)) {
@@ -521,13 +523,16 @@ public class PatientRecordServiceImpl implements PatientRecordService {
             // 创建 RecordDetailItemDTO 对象,并填充数据
             RecordDetailItemDTO dto = new RecordDetailItemDTO();
             dto.setType(map.get("type") != null ? ((Number) map.get("type")).longValue() : null);
-            dto.setTypeName((String) map.get("typeName"));
-            dto.setContent(content);
+            dto.setTypeName(typeName);
+
+            // 根据 typeName 判断是否需要拆分 content 或格式化
+            String formattedContent = formatContentByType(content, typeName);
+            dto.setContent(formattedContent);  // 设置格式化后的内容
 
             // 处理 qcComments
             String qcCommentsStr = (String) map.get("qcComments");
             List<Map<String, Object>> qcCommentsList = processQcComments(qcCommentsStr);
-            dto.setQcCommentsList(qcCommentsList);  // 将处理后的结果设置到 dto 中
+            dto.setQcCommentsList(qcCommentsList);
 
             // 处理其他字段
             dto.setQcResult(map.get("qcResult") != null ? ((Number) map.get("qcResult")).intValue() : null);
@@ -539,9 +544,147 @@ public class PatientRecordServiceImpl implements PatientRecordService {
             dtoList.add(dto);
         }
 
-        return dtoList;  // 返回处理好的详情列表
+        return dtoList;
+    }
+
+    private String formatContentByType(String content, String typeName) {
+        StringBuilder formattedContent = new StringBuilder();
+
+        switch (typeName) {
+            case "病程记录":
+            case "阶段小结":
+            case "查房记录":
+            case "会诊记录":
+            case "疑难病例讨论记录":
+            case "超长住院讨论记录":
+            case "死亡病例讨论记录":
+            case "抢救记录":
+            case "手术记录":
+            case "医嘱记录":
+            case "知情同意书":
+                // 这些类型是数组,需要逐条拆分
+                formattedContent.append(formatMedicalRecords(content));
+                break;
+
+            case "检验报告单":
+            case "检查报告单":
+                // 检验 / 检查报告单单独格式化
+                formattedContent.append(formatLabReportContent(content));
+                break;
+
+            default:
+                // 其他类型直接返回
+                formattedContent.append(content);
+                break;
+        }
+
+        return formattedContent.toString();
+    }
+
+    /**
+     * 通用数组类的处理(病程记录、医嘱等)
+     */
+    private String formatMedicalRecords(String content) {
+        StringBuilder formattedContent = new StringBuilder();
+
+        try {
+            List<String> records = objectMapper.readValue(content, List.class);
+
+            for (String record : records) {
+                if (!record.trim().isEmpty()) {
+                    formattedContent.append(record.trim()).append("\n\n");
+                }
+            }
+        } catch (Exception e) {
+            e.printStackTrace();
+            return "解析记录失败";
+        }
+
+        return formattedContent.toString().trim();
+    }
+
+    /**
+     * 检验/检查报告单的专用格式化
+     */
+    public String formatLabReportContent(String content) {
+        if (content == null || content.trim().isEmpty()) {
+            return "";
+        }
+
+        StringBuilder formattedContent = new StringBuilder();
+
+        try {
+            // 解析JSON数组,每个元素为一份报告
+            List<String> reports = objectMapper.readValue(content, List.class);
+
+            for (String report : reports) {
+                if (report == null || report.trim().isEmpty()) {
+                    continue;
+                }
+
+                String text = report;
+
+                // 1. 统一长横线分隔
+                text = text.replaceAll("-{5,}", "\n-------------------------------------------------------------------------------------------------\n")
+                        .replaceAll("检验结果[-]+", "\n检验结果:\n")
+                        .replaceAll("◆", "\n◆");
+
+                // 2. 病人信息整合到一行,确保格式一致
+                text = text.replaceAll("(病人姓名|性别|年龄|科室|门诊号|住院号|床号)([^\\n]+)", "$1:$2");
+
+                // 3. 处理检验项目和检验结果格式
+                text = text.replaceAll("检验项目", "\n检验项目")
+                        .replaceAll("检验结果", "检验结果");
+
+                // 4. 保持原始的 "检验项目 检验结果 单位 结果标志 结果参考" 行不变,统一格式
+                text = text.replaceAll("(检验项目|检验结果|单位|结果标志|结果参考)", "$1");
+
+                // 5. 处理特殊简短报告(如乙肝、梅毒等)
+                if (text.contains("检验结果:") && (text.contains("HBsAg") || text.contains("抗-HBs") || text.contains("梅毒螺旋体抗体"))) {
+                    text = text.replaceAll("检验结果:", "\n检验结果:\n");
+                }
+
+                text = text.replaceAll("结果参考", "结果参考 ");
+
+                // >>>>>>>> 新增:自动在每个检验项目前换行 <<<<<<<<
+                text = text.replaceAll("(◆?)([\\u4e00-\\u9fa5]+\\([^)]+\\))", "\n$1$2"); // 特别处理包含-的项
+
+                // 修复:◆开头的行,如果下一行以中文开头,且上一行以字母/数字结尾,则合并
+                text = text.replaceAll("(◆[\\S&&[^◆]]*)\n([\\u4e00-\\u9fa5])", "$1$2");
+
+
+                // 6. 统一处理检验项目格式,保持一致
+                text = text.replaceAll("(◆[^\n]+)([\\d\\.]+)", "$1   $2");
+
+                // 7. 去除多余的空格和空行
+                StringBuilder sb = new StringBuilder();
+                String[] lines = text.split("\n");
+                for (String line : lines) {
+                    if (!line.trim().isEmpty()) {
+                        sb.append(line.trim()).append("\n");
+                    }
+                }
+
+                // 拼接报告,并确保报告之间有空行分隔
+                formattedContent.append(sb.toString().trim())
+                        .append("\n\n========================================================== 分隔线 ==========================================================\n\n");
+            }
+
+        } catch (Exception e) {
+            e.printStackTrace();
+            return "解析检验报告失败";
+        }
+
+        return formattedContent.toString().trim();
     }
 
+
+
+
+
+
+
+
     private List<RecordDetailItemDTO> convertToDtoList(List<Map<String, Object>> mapList) {
         List<RecordDetailItemDTO> dtoList = new ArrayList<>();
 
@@ -617,27 +760,65 @@ public class PatientRecordServiceImpl implements PatientRecordService {
     private List<Map<String, Object>> processQcComments(String qcCommentsStr) {
         List<Map<String, Object>> qcCommentsList = new ArrayList<>();
 
-        // 确保 qcCommentsStr 不为空
-        if (qcCommentsStr != null && !qcCommentsStr.isEmpty()) {
-            try {
-                // 使用 Jackson ObjectMapper 来解析 JSON 字符串
-                ObjectMapper objectMapper = new ObjectMapper();
-                // 解析字符串为 List<Map<String, Object>> 类型
-                List<Map<String, Object>> comments = objectMapper.readValue(qcCommentsStr, List.class);
-
-                // 遍历并过滤出 result 为 "fail" 的记录
-                for (Map<String, Object> comment : comments) {
-                    if ("fail".equals(comment.get("result"))) {
-                        qcCommentsList.add(comment);  // 只保留审核失败的记录
-                    }
+        if (qcCommentsStr == null || qcCommentsStr.trim().isEmpty()) {
+            return qcCommentsList;
+        }
+
+        try {
+            // 多步骤清理
+            String cleanedJson = qcCommentsStr.trim();
+
+            // 步骤1: 移除Markdown代码块
+            cleanedJson = removeMarkdownWrapper(cleanedJson);
+
+            // 步骤2: 移除其他可能的非法字符
+            cleanedJson = removeIllegalCharacters(cleanedJson);
+
+
+
+            ObjectMapper objectMapper = new ObjectMapper();
+            objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
+
+            List<Map<String, Object>> comments = objectMapper.readValue(cleanedJson,
+                    new TypeReference<List<Map<String, Object>>>() {});
+
+            for (Map<String, Object> comment : comments) {
+                if ("fail".equals(comment.get("result"))) {
+                    qcCommentsList.add(comment);
                 }
-            } catch (JsonProcessingException e) {
-                // 处理 JSON 解析异常
-                e.printStackTrace();  // 可以根据需求添加更多的异常处理逻辑
             }
+
+        } catch (Exception e) {
+            System.err.println("处理qc_comments失败: " + e.getMessage());
+            System.err.println("请检查数据库中的qc_comments字段内容,应该是纯JSON格式");
+            e.printStackTrace();
         }
 
-        return qcCommentsList;  // 返回处理后的审核失败记录列表
+        return qcCommentsList;
+    }
+
+    private String removeMarkdownWrapper(String str) {
+        if (str == null) return null;
+
+        String cleaned = str.trim();
+
+        // 移除各种Markdown代码块格式
+        cleaned = cleaned.replaceAll("^```(json|JSON)?\\s*", "");
+        cleaned = cleaned.replaceAll("\\s*```$", "");
+        cleaned = cleaned.replaceAll("^``(json|JSON)?\\s*", "");
+        cleaned = cleaned.replaceAll("\\s*``$", "");
+
+        return cleaned.trim();
+    }
+
+    private String removeIllegalCharacters(String str) {
+        if (str == null) return null;
+
+        // 移除BOM字符和其他控制字符
+        return str.replace("\uFEFF", "")
+                .replaceAll("^[\\p{C}]+", "")
+                .replaceAll("[\\p{C}]+$", "")
+                .trim();
     }