zhaohan hai 7 meses
pai
achega
df34f75334

+ 8 - 4
ruoyi-admin/src/main/resources/application-lzsy.yml

@@ -18,7 +18,7 @@ spring:
           driverClassName: com.mysql.cj.jdbc.Driver
           url: jdbc:mysql://192.168.0.77:3306/emoon_ai?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&autoReconnect=true&rewriteBatchedStatements=true
           username: emoon_ai
-          password: AZZDHC5ZZzDeMJkN
+          password: r4JbaAac2rzRnyJ4
         open-db:
           type: com.zaxxer.hikari.HikariDataSource
           driverClassName: com.mysql.cj.jdbc.Driver
@@ -101,12 +101,16 @@ file:
   upload-path: /www/knowledge/files/
   #上传附件访问地址
   base-url: http://192.168.0.77:5666
-
 knowledge:
-  kid: 1965740023724711937
+  kid: 1972459392479571969
+# 跨域配置
+cors:
+  file-parse:
+    origins: http://192.168.0.77:3335
+
   #燧原硬件信息接口
 enflame:
-  url: http://192.168.0.78:19997
+  url: http://192.168.0.77:19997
 mcp:
   server:
     url: http://localhost:8085

+ 4 - 0
ruoyi-admin/src/main/resources/application-prod.yml

@@ -103,6 +103,10 @@ file:
   base-url: http://8.137.127.56:5666
 knowledge:
   kid: 1971858425499365378
+# 跨域配置
+cors:
+  file-parse:
+    origins: http://8.137.127.56:3335
 
   #燧原硬件信息接口
 enflame:

+ 7 - 4
ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/controller/chat/FileParseController.java

@@ -1,16 +1,19 @@
 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.bind.annotation.*;
 import org.springframework.web.multipart.MultipartFile;
 
 @RestController
 @RequestMapping("/parse")
+@CrossOrigin(
+        origins = "${cors.file-parse.origins}",
+        allowedHeaders = "*",
+        methods = {RequestMethod.GET, RequestMethod.POST, RequestMethod.PUT, RequestMethod.DELETE}
+)
 public class FileParseController {
 
     private final FileParseService fileParseService;

+ 3 - 0
ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/controller/chat/ragController.java

@@ -125,6 +125,7 @@ public class ragController {
     public TableDataInfo<ThinkRag> ragList() {
         return sseService.ragList();
     }
+
     /**
      * 修改项目内容
      */
@@ -134,6 +135,7 @@ public class ragController {
         int result =sseService.deleteProjectList(map);
         return R.ok(result);
     }
+
     /**
      * 查询住院人数和门诊人数
      */
@@ -151,4 +153,5 @@ public class ragController {
         int result =sseService.addProject(map);
         return R.ok(result);
     }
+
 }

+ 1 - 0
ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/HardwareInfoService.java

@@ -39,6 +39,7 @@ public class HardwareInfoService {
         long totalMemory = memory.getTotal();
         long availableMemory = memory.getAvailable();
 
+
         // 3. 磁盘信息
         List<DiskData> disks = systemInfo.getOperatingSystem().getFileSystem().getFileStores().stream()
                 .map(store -> new DiskData(

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

@@ -3,6 +3,7 @@ package org.ruoyi.chat.service;
 import com.alibaba.fastjson.JSON;
 import com.alibaba.fastjson.JSONArray;
 import com.alibaba.fastjson.JSONObject;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import lombok.extern.slf4j.Slf4j;
 import org.ruoyi.chat.util.HttpUtils;
 import org.ruoyi.domain.MedicalRecordResult;
@@ -117,8 +118,24 @@ public class MedicalRecordQc123Service {
             Map.entry("lab_reports", "检查检验规则清单"),                 // 检验报告单
             Map.entry("exam_reports", "检查检验规则清单"),                // 检查报告单
             Map.entry("orders", "医嘱规则清单"),                     // 医嘱记录
-            Map.entry("discharge_summary", "出院(死亡)记录规则清单"),      // 出院小结
-            Map.entry("consent_forms", "知情同意书")              // 知情同意书
+            Map.entry("discharge_summary", "出院(死亡)记录规则清单")      // 出院小结
+            // Map.entry("consent_forms", "知情同意书")              // 知情同意书 - 暂时注释掉
+    );
+
+    /**
+     * 质控规则名称 → 病历部分名称的映射
+     */
+    private static final Map<String, String> RULE_TO_SECTION_MAP = Map.ofEntries(
+            Map.entry("主诉记录不完整,不能支持第一诊断", "入院志"),
+            Map.entry("首次病程记录规则清单", "首次病程记录"),
+            Map.entry("病程记录规则清单", "病程记录"),
+            Map.entry("阶段小结规则清单", "阶段小结"),
+            Map.entry("查房记录", "查房记录"),
+            Map.entry("病程记录", "病程记录"),
+            Map.entry("其他", "手术记录"),
+            Map.entry("检查检验规则清单", "检查检验报告"),
+            Map.entry("医嘱规则清单", "医嘱记录"),
+            Map.entry("出院(死亡)记录规则清单", "出院小结")
     );
 
     /**
@@ -160,11 +177,29 @@ public class MedicalRecordQc123Service {
                 // 3.1 调用知识库
                 String ragResult = fetchMatchingRules(step.label);
 
-                // 3.2 构造提示词
+                // 3.2 针对医嘱(orders,type=13)分段质控(按10条一批)
+                if (step.type == 13) {
+                    batchQcAggregate(recordId, step.type, step.label, step.content, 30, ragResult);
+                    continue;
+                }
+
+                // 3.2 针对病程记录(type=2)与查房记录(type=4) 三条一批质控并聚合
+                if (step.type == 2 || step.type == 4) {
+                    batchQcAggregate(recordId, step.type, step.label, step.content, 5, ragResult);
+                    continue;
+                }
+                if (step.type == 11 || step.type == 12) {
+                    batchQcAggregate(recordId, step.type, step.label, step.content, 5, ragResult);
+                    continue;
+                }
+
+                // 3.2 构造提示词(非 orders 默认整段质控)
                 String prompt = buildPrompt(step.label, step.content, ragResult);
 
                 // 3.3 调用大模型
                 String modelOutput = callLLM(prompt);
+                // 3.3.1 清理模型返回内容(去掉<think>之前的部分)
+                String processedOutput = processModelOutput(modelOutput);
 
                 // 3.4 解析结果
                 int qcResult = parseQcResult(modelOutput);
@@ -177,7 +212,7 @@ public class MedicalRecordQc123Service {
                 result.setQcEndTime(LocalDateTime.now());
                 result.setQcResult(qcResult);
                 result.setQcScore("85"); // 可以改成模型评分
-                result.setQcComments(modelOutput);
+                result.setQcComments(processedOutput);
                 result.setCreateDate(LocalDateTime.now());
                 resultMapper.insert(result);
 
@@ -216,8 +251,8 @@ public class MedicalRecordQc123Service {
                 new QcStep(11, SECTION_LABEL_MAP.get("lab_reports"), record.getLabReports() != null ? record.getLabReports().toString() : null),
                 new QcStep(12, SECTION_LABEL_MAP.get("exam_reports"), record.getExamReports() != null ? record.getExamReports().toString() : null),
                 new QcStep(13, SECTION_LABEL_MAP.get("orders"), record.getOrders() != null ? record.getOrders().toString() : null),
-                new QcStep(14, SECTION_LABEL_MAP.get("discharge_summary"), record.getDischargeSummary()),
-                new QcStep(15, SECTION_LABEL_MAP.get("consent_forms"), record.getConsentForms() != null ? record.getConsentForms().toString() : null)
+                new QcStep(14, SECTION_LABEL_MAP.get("discharge_summary"), record.getDischargeSummary())
+                // new QcStep(15, SECTION_LABEL_MAP.get("consent_forms"), record.getConsentForms() != null ? record.getConsentForms().toString() : null) // 暂时注释掉知情同意书质控
         );
     }
 
@@ -262,6 +297,13 @@ public class MedicalRecordQc123Service {
         }
     }
 
+    /**
+     * 根据质控规则名称获取病历部分名称
+     */
+    private String getSectionName(String sectionLabel) {
+        return RULE_TO_SECTION_MAP.getOrDefault(sectionLabel, sectionLabel);
+    }
+
     /**
      * 构造大模型提示词
      */
@@ -313,8 +355,8 @@ public class MedicalRecordQc123Service {
     private String buildPrompt(String sectionLabel, String content, String ragResult) {
         StringBuilder sb = new StringBuilder();
 
-        sb.append("你是一名医疗病历质控员,需要依据【质控规则】对【")
-                .append(sectionLabel).append("】进行审查。\n\n");
+        sb.append("你是一名医疗病历质控员,需要依据【质控规则】对病历的【")
+                .append(getSectionName(sectionLabel)).append("】部分进行审查。\n\n");
 
         sb.append("【病历原文】:\n")
                 .append(content).append("\n\n");
@@ -328,11 +370,11 @@ public class MedicalRecordQc123Service {
 
         sb.append("如果存在不符合规则:逐条输出 JSON 对象,数组形式,每个对象包含以下字段:\n");
         sb.append("- result: 固定为 \"fail\"\n");
-        sb.append("- detail: 违规位置及内容(如“首次病程记录-主诉”)\n");
-        sb.append("- 从病历原文中提取 违规内容所在的一小段(约10字左右),保持完整病历内容,便于定位。\n");
+        sb.append("- detail: 违规位置及内容(如“入院记录-主诉”)\n");
+        sb.append("- content:从病历原文中提取 违规内容所在的一小段(约10字左右),保持完整病历内容,便于定位。\n");
         sb.append("- ruleCode: 对应规则编号(如 \"BCJL001\")\n");
         sb.append("- ruleContent: 对应规则详情,必须与质控规则完全一致,不要显示()里面的内容,如XXX(YYYYYYYYYY) 只显示XXX\n");
-        sb.append("- qcResult: 结论,给出违反规则的原因,简要描述。\n\n");
+        sb.append("- qcResult: 结论,给出违反本条规则的原因,不要牵扯其他规则,简要描述。禁止在本条结论中关联或提及其他规则。\n\n");
 
         sb.append("示例:\n");
         sb.append("[\n");
@@ -341,11 +383,18 @@ public class MedicalRecordQc123Service {
         sb.append("]\n\n");
 
         sb.append("2.违规情况:\n");
+        sb.append("规则详情括号中的内容是对规则的补充解释,如XXX(YYYYYYYYYY),YYYYYYYYYY是对XXX补充解释,\n");
         sb.append("每条违规单独输出一个 JSON 对象 \n");
         sb.append("字段必须取自病历原文,且保持完整语义(不必限制为固定字数) \n");
         sb.append("3.禁止事项:\n");
         sb.append("禁止输出 Markdown 代码块符号(如 ```json)\n");
         sb.append("禁止生成与质控规则不一致的规则编号\n");
+        sb.append("3.注意事项:\n");
+        sb.append("质控时仅针对规则本身对病历内容进行判断。所有结论必须严格基于病历原文记载,不得依赖病历外的推测、常识或假设。不做超出规则范围的额外临床分析或诊断。\n");
+        sb.append("语义理解原则(最重要):严禁进行僵化的关键词匹配。判断一项内容是否被记录,核心是审查病历原文是否描述了该事项的实质内容。只要语义相同或高度相近,即使未使用规则中的原词,也应判定为已记录。\n");
+        sb.append("4.宽松原则:\n");
+        sb.append("对于规则中涉及\"是否存在\"、\"是否包含\"、\"缺***\"的判断,采取宽松标准。只要病历中有所体现或间接包含相关信息,即使不够完整详细,也应酌情通过。仅在完全缺失或明显违反核心要求时判定为不通过。\n");
+        sb.append("对于规则中涉及\"是否存在\"、\"是否包含\"、\"缺***\"的判断,采取宽松标准。只要病历中有所体现或间接包含相关信息,即使不够完整详细,也应酌情通过。仅在完全缺失或明显违反核心要求时判定为不通过。\n");
 
         return sb.toString();
     }
@@ -367,9 +416,21 @@ public class MedicalRecordQc123Service {
                     put("stream", false);
                     put("temperature", 0.0);
                     put("messages", messages);
-                    put("max_tokens", 5120); // ✅ 限制最大输出token数
+                    put("max_tokens", 10240); // ✅ 限制最大输出token数
                     put("chat_template_kwargs", "{\"enable_thinking\": false}");
+//                new HashMap<String, Object>() {{
+//                    put("model", "deepseek-r1-70B");  // ✅ 改成 R1-70B
+//                    put("stream", false);
+//                    put("temperature", 0.0);
+//                    put("max_tokens", 8192);
+//                    put("messages", messages);
+//                    put("chat_template_kwargs", "{\"enable_thinking\": false}");
+//
+//                    // ✅ 禁用思考输出
+////                    put("reasoning_mode", "disabled");
                 }}
+
+
         );
         String modelUrl = getLlmModelUrl();
         System.out.println("模型路径:"+ modelUrl);
@@ -434,4 +495,119 @@ public class MedicalRecordQc123Service {
         String str = obj.toString().trim();
         return str.isEmpty() || str.equals("[]") || str.equalsIgnoreCase("null");
     }
+
+    /**
+     * 处理模型返回的内容,去掉 <think> 或 </think> 之前的推理部分,只保留 JSON 结果。
+     */
+    private String processModelOutput(String modelOutput) {
+        if (modelOutput == null) {
+            return "";
+        }
+
+        // 正则匹配:去掉 </think> 之前的所有内容(包括 <think>... </think>)
+        // 匹配 <think> 开头、</think> 结束的内容,以及前面的任何文字
+        String cleaned = modelOutput.replaceAll("(?s)^.*?</think>\\s*", "");
+
+        // 如果模型没有输出 </think>,则直接返回原始内容
+        if (cleaned.isEmpty()) {
+            cleaned = modelOutput;
+        }
+
+        // 去掉前后多余空白符
+        return cleaned.trim();
+    }
+
+    /**
+     * 通用分段质控聚合:按 batchSize 批量质控,仅聚合 fail 项,清理旧记录后写入一条结果。
+     */
+    private void batchQcAggregate(String recordId,
+                                  int qcType,
+                                  String sectionLabel,
+                                  String jsonArrayContent,
+                                  int batchSize,
+                                  String ragResult) {
+        LocalDateTime start = LocalDateTime.now();
+
+        try {
+            JSONArray array = JSON.parseArray(jsonArrayContent);
+            if (array == null || array.isEmpty()) {
+                log.info("⚠️ 病历 {} 的 [{}] 数组为空,跳过", recordId, sectionLabel);
+                return;
+            }
+
+            List<Object> aggregated = new ArrayList<>();
+            int total = array.size();
+            int totalBatches = (total + batchSize - 1) / batchSize;
+            log.info("➡️ 开始批量质控 病历={} [{}],总条数={},batchSize={},批次数={}", recordId, sectionLabel, total, batchSize, totalBatches);
+            for (int startIdx = 0; startIdx < total; startIdx += batchSize) {
+                int endIdx = Math.min(startIdx + batchSize, total);
+                List<String> batchItems = new ArrayList<>();
+                for (int k = startIdx; k < endIdx; k++) {
+                    String item = array.getString(k);
+                    if (item != null && !item.trim().isEmpty()) {
+                        batchItems.add((k + 1) + ". " + item);
+                    }
+                }
+                if (batchItems.isEmpty()) {
+                    continue;
+                }
+
+                int batchNo = (startIdx / batchSize) + 1;
+                log.info("🧪 执行批次 {}/{}(索引 {}..{},实际条数={})", batchNo, totalBatches, startIdx, endIdx - 1, batchItems.size());
+                String batchContent = String.join("\n\n", batchItems);
+                String batchPrompt = buildPrompt(sectionLabel, batchContent, ragResult);
+                String batchOutput = callLLM(batchPrompt);
+                String processedOutput = processModelOutput(batchOutput);
+
+                try {
+                    JSONArray arr = JSON.parseArray(processedOutput);
+                    if (arr != null && !arr.isEmpty()) {
+                        int before = aggregated.size();
+                        for (int j = 0; j < arr.size(); j++) {
+                            JSONObject obj = arr.getJSONObject(j);
+                            if (obj != null && !"pass".equalsIgnoreCase(obj.getString("result"))) {
+                                aggregated.add(obj);
+                            }
+                        }
+                        int added = aggregated.size() - before;
+                        log.info("📌 批次 {}/{} 解析成功:新增不通过 {} 条", batchNo, totalBatches, added);
+                    }
+                } catch (Exception ignore) {
+                    aggregated.add(processedOutput);
+                    log.warn("⚠️ 批次 {}/{} 返回非JSON格式,已按文本纳入聚合", batchNo, totalBatches);
+                }
+            }
+
+            // 清理旧记录,仅保留一条最新聚合
+            try {
+                int rows = resultMapper.delete(new LambdaQueryWrapper<MedicalRecordResult>()
+                        .eq(MedicalRecordResult::getPatientRecordId, recordId)
+                        .eq(MedicalRecordResult::getQcType, qcType));
+                log.info("🧹 清理旧记录完成:patientRecordId={}, qcType={}, 删除条数={}", recordId, qcType, rows);
+            } catch (Exception ignore) { }
+
+            MedicalRecordResult agg = new MedicalRecordResult();
+            agg.setPatientRecordId(recordId);
+            agg.setQcType(qcType);
+            agg.setQcStartTime(start);
+            agg.setQcEndTime(LocalDateTime.now());
+            agg.setQcScore("85");
+            agg.setQcComments(aggregated.isEmpty()
+                    ? "[{\"result\":\"pass\",\"message\":\"病例校验通过\"}]"
+                    : JSON.toJSONString(aggregated));
+            agg.setQcResult(aggregated.isEmpty() ? 1 : 0);
+            agg.setCreateDate(LocalDateTime.now());
+            resultMapper.insert(agg);
+
+            if (aggregated.isEmpty()) {
+                log.info("✅ 病历 {} [{}] 批量质控完成:全部通过,已存PASS记录(batchSize={})", recordId, sectionLabel, batchSize);
+            } else {
+                log.info("✅ 病历 {} [{}] 批量质控完成:共有不通过 {} 条,已聚合入库(batchSize={})", recordId, sectionLabel, aggregated.size(), batchSize);
+            }
+
+        } catch (Exception ex) {
+            log.error("❌ 病历 {} [{}] 批量质控失败", recordId, sectionLabel, ex);
+            throw ex;
+        }
+    }
 }

+ 54 - 47
ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/PatientRecordServiceImpl.java

@@ -226,40 +226,42 @@ public class PatientRecordServiceImpl implements PatientRecordService {
                 List<Map<String, Object>> emrDocs = (List<Map<String, Object>>) emrDocsObj;
 
                 for (Map<String, Object> doc : emrDocs) {
-                    String docName = getString(doc, "docName", "");
-                    String content = stripXml(getString(doc, "content", ""));
+                    String subDocTitle = getString(doc, "subDocTitle", "");
+                    String contentText = getString(doc, "contentText", "");
+                    String docTitle = getString(doc, "docTitle", "");
+                    String editor = getString(doc, "editor", "");
 
-                    if ((docName.contains("入院志") || docName.contains("入院记录"))
+                    if ((subDocTitle.contains("入院志") || subDocTitle.contains("入院记录"))
                             && record.getAdmissionNote() == null) {
-                        record.setAdmissionNote(content);
-                    } else if (docName.contains("首次病程") && record.getFirstCourseRecord() == null) {
-                        record.setFirstCourseRecord(content);
-                    } else if (docName.contains("病程记录")) {
-                        courseRecords.add(content);
-                    } else if (docName.contains("阶段小结")) {
-                        stageSummary.add(content);
-                    } else if (docName.contains("查房记录")) {
-                        wardRounds.add(content);
-                    } else if (docName.contains("会诊记录")) {
-                        consultationRecords.add(content);
-                    } else if (docName.contains("疑难病例讨论")) {
-                        difficultCaseDiscussion.add(content);
-                    } else if (docName.contains("超长住院讨论")) {
-                        longStayDiscussion.add(content);
-                    } else if (docName.contains("死亡病例讨论")) {
-                        deathCaseDiscussion.add(content);
-                    } else if (docName.contains("抢救记录")) {
-                        rescueRecords.add(content);
-                    } else if (docName.matches(".*(手术记录|麻醉记录).*")) {
-                        surgeryRecords.add(content);
-                    } else if (docName.contains("检验")) {
-                        labReports.add(content);
-                    } else if (docName.contains("检查")) {
-                        examReports.add(content);
-                    } else if (docName.contains("出院记录") && record.getDischargeSummary() == null) {
-                        record.setDischargeSummary(content);
-                    } else if (docName.matches(".*(知情同意书|病情告知书|病危通知书).*")) {
-                        consentForms.add(content);
+                        record.setAdmissionNote(contentText);
+                    } else if (subDocTitle.contains("首次病程") && record.getFirstCourseRecord() == null) {
+                        record.setFirstCourseRecord(contentText);
+                    } else if (subDocTitle.contains("病程记录")) {
+                        courseRecords.add(contentText);
+                    } else if (subDocTitle.contains("阶段小结")) {
+                        stageSummary.add(contentText);
+                    } else if (subDocTitle.contains("查房记录")) {
+                        wardRounds.add(contentText);
+                    } else if (subDocTitle.contains("会诊记录")) {
+                        consultationRecords.add(contentText);
+                    } else if (subDocTitle.contains("疑难病例讨论")) {
+                        difficultCaseDiscussion.add(contentText);
+                    } else if (subDocTitle.contains("超长住院讨论")) {
+                        longStayDiscussion.add(contentText);
+                    } else if (subDocTitle.contains("死亡病例讨论")) {
+                        deathCaseDiscussion.add(contentText);
+                    } else if (subDocTitle.contains("抢救记录")) {
+                        rescueRecords.add(contentText);
+                    } else if (subDocTitle.matches(".*(手术记录|麻醉记录).*")) {
+                        surgeryRecords.add(contentText);
+                    } else if (subDocTitle.contains("检验")) {
+                        labReports.add(contentText);
+                    } else if (subDocTitle.contains("检查")) {
+                        examReports.add(contentText);
+                    } else if (subDocTitle.contains("出院记录") && record.getDischargeSummary() == null) {
+                        record.setDischargeSummary(contentText);
+                    } else if (docTitle.matches(".*(同意书|告知书|通知书).*")) {
+                        consentForms.add(contentText);
                     }
                 }
             }
@@ -283,22 +285,26 @@ public class PatientRecordServiceImpl implements PatientRecordService {
             Object ordersObj = data.get("orders");
             List<String> orderStrings = new ArrayList<>();
 
-            if (ordersObj instanceof List) {
+            if (ordersObj instanceof List<?>) {
                 List<Map<String, Object>> ordersList = (List<Map<String, Object>>) ordersObj;
                 for (Map<String, Object> order : ordersList) {
                     try {
-                        // 提取字段
-                        String content = getString(order, "content", "未知内容");
+                        // === 1️⃣ 提取主要字段 ===
+                        String content = getString(order, "content", "未知医嘱");
                         String execStartTime = getString(order, "execStartTime", "");
                         String execStopTime = getString(order, "execStopTime", "");
                         String orderDoctor = getString(order, "orderDoctor", "未知医生");
                         String stopOrderDoctor = getString(order, "stopOrderDoctor", "无");
+                        String execDept = getString(order, "execDept", "未知科室");
+                        String status = getString(order, "status", "未知状态");
+                        String orderType = getString(order, "orderType", "未知类型");
+                        String medicalType = getString(order, "medicalType", "");
+                        String execFrequency = getString(order, "execFrequency", "未知频率");
 
-                        // ✅ 使用你原有的 parseDateTime 方法解析时间
+                        // === 2️⃣ 时间解析与格式化 ===
                         LocalDateTime startDt = parseDateTime(execStartTime);
                         LocalDateTime endDt = parseDateTime(execStopTime);
 
-                        // ✅ 格式化为 "yyyy-MM-dd HH:mm",如果解析失败则显示“未知”
                         String startShort = (startDt != null)
                                 ? startDt.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"))
                                 : "未知开始时间";
@@ -306,25 +312,32 @@ public class PatientRecordServiceImpl implements PatientRecordService {
                                 ? endDt.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"))
                                 : "暂无结束时间";
 
-                        // ✅ 构造标准格式字符串(不加外层引号)
+                        // === 3️⃣ 组装格式化输出 ===
                         String orderStr = String.format(
-                                "%s(执行时间:%s 至 %s,开嘱医生:%s,停嘱医生:%s)",
+                                "%s(类型:%s,状态:%s,医嘱分类:%s,频率:%s,执行时间:%s ~ %s,开嘱医生:%s,停嘱医生:%s,执行科室:%s)",
                                 content,
+                                orderType,
+                                status,
+                                medicalType,
+                                execFrequency,
                                 startShort,
                                 stopShort,
                                 orderDoctor,
-                                stopOrderDoctor
+                                stopOrderDoctor,
+                                execDept
                         );
 
                         orderStrings.add(orderStr);
 
                     } catch (Exception e) {
                         System.err.println("⚠️ 解析单条 order 失败: " + e.getMessage());
-                        continue;
                     }
                 }
+            } else {
+                System.err.println("⚠️ orders 字段不是列表类型或为空");
             }
 
+
             // 将字符串列表转为 JSON 存入 record
             record.setOrders(toJson(orderStrings));
 
@@ -679,12 +692,6 @@ public class PatientRecordServiceImpl implements PatientRecordService {
     }
 
 
-
-
-
-
-
-
     private List<RecordDetailItemDTO> convertToDtoList(List<Map<String, Object>> mapList) {
         List<RecordDetailItemDTO> dtoList = new ArrayList<>();
 

+ 54 - 17
ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/SseServiceImpl.java

@@ -286,23 +286,60 @@ public class SseServiceImpl implements ISseService {
         } else {
             sysPrompt = chatModelVo.getSystemPrompt();
             if (StringUtils.isEmpty(sysPrompt)) {
-                sysPrompt = "你是一个医疗数据分析智能体,负责查询门诊人次和住院人次。\n" +
-                        "\n" +
-                        "使用规则:\n" +
-                        "1. 用户可能给出以下查询方式:\n" +
-                        "   - 单日(如 \"2025-03-01\")\n" +
-                        "   - 日期范围(如 \"2025-03-01 到 2025-03-05\")\n" +
-                        "   - 模糊日期(如 \"昨天\"、\"上周\"、\"本月\")\n" +
-                        "2. 如果用户只给出单日,则自动扩展为「该日 + 前 3 天」,形成连续四天的日期范围。但是不需要显示前三天的数据,只需用在后面分析即可\n" +
-                        "   例如:用户输入 `2025-03-01` → 查询 `2025-02-27 ~ 2025-03-01 数据在返回的`\n" +
-                        "3. 如果用户给出范围或模糊时间,则按用户意图解析为日期范围,不做额外扩展。\n" +
-                        "4. 查询时,只调用一次 API,并传入 `开始日期` 和 `结束日期`。\n" +
-                        "5. 输出需包含:\n" +
-                        "   - 查询的当天数据\n" +
-                        "   - 未来趋势分析:结合前三天的数据给未来几天做个分析,但是不要告诉过去几天的数据\n" +
-                        "6.输出时尽量换行,注意美观\n" +
-                        " 当前时间为:" + DateUtils.getDate() +
-                        "#注意:回复之前注意结合上下文和工具返回内容进行回复。";
+                sysPrompt =
+                        "你是一个医疗数据分析智能体,负责查询门诊人次和住院人次。\n" +
+                                "\n" +
+                                "使用规则:\n" +
+                                "1. 用户可能给出以下查询方式:\n" +
+                                "   - 单日(如 \"2025-03-01\")\n" +
+                                "   - 日期范围(如 \"2025-03-01 到 2025-03-05\")\n" +
+                                "   - 模糊日期(如 \"昨天\"、\"上周\"、\"本月\")\n" +
+                                "2. 如果用户只给出单日,则自动扩展为「该日 + 前 3 天」,形成连续四天的日期范围。但是不需要显示前三天的数据,只需用在后面分析即可。\n" +
+                                "   例如:用户输入 `2025-03-01` → 查询 `2025-02-27 ~ 2025-03-01` 的数据。\n" +
+                                "3. 如果用户给出范围或模糊时间,则按用户意图解析为日期范围,不做额外扩展。\n" +
+                                "4. 查询时,只调用一次 API,并传入 `开始日期` 和 `结束日期`。\n" +
+                                "5. 输出需包含:\n" +
+                                "   输出统一为 JSON 格式,包含以下四个部分:\n" +
+                                "   ① query_info:查询信息,包括以下字段:\n" +
+                                "      - hospital:医院名称(从上下文或工具返回中获取)\n" +
+                                "      - target_date:查询目标日期(如用户输入单日,则为该日期;如为范围,则为结束日期)\n" +
+                                "      - data_range:查询的数据范围(如 \"2025-07-06至2025-07-10\")\n" +
+                                "\n" +
+                                "   ② line_chart_data:折线图数据,用于展示门诊人次趋势,包含字段:\n" +
+                                "      - chart_type:固定为 \"line\"\n" +
+                                "      - title:图表标题(格式示例:\"2025年7月6日-10日各科室门诊人次趋势\")\n" +
+                                "      - x_axis_name:固定为 \"日期\"\n" +
+                                "      - y_axis_name:固定为 \"门诊人次\"\n" +
+                                "      - dates:日期数组(包含扩展后的查询范围)\n" +
+                                "      - departments:各科室数据数组,每个元素包含:\n" +
+                                "         * name:科室名称(若数据库中为英文名称,请根据下方映射表自动翻译为中文)\n" +
+                                "         * data:对应日期的门诊人次\n" +
+                                "         * color:科室对应颜色\n" +
+                                "\n" +
+                                "   ③ pie_chart_data:饼图数据,用于展示目标日期各科室的住院人次占比,包含字段:\n" +
+                                "      - chart_type:固定为 \"pie\"\n" +
+                                "      - title:图表标题(格式示例:\"2025年7月10日各科室住院占比分布\")\n" +
+                                "      - data:数组形式,每个元素包含:\n" +
+                                "         * name:科室名称(同样需根据映射表翻译为中文)\n" +
+                                "         * value:住院人数\n" +
+                                "         * color:科室对应颜色\n" +
+                                "\n" +
+                                "   ④ advice:文字分析与建议,总结趋势和潜在问题,给出 3~5 条建议。内容需结合折线图和饼图进行分析,格式为多行文字。\n" +
+                                "\n" +
+                                "6. 输出时注意排版美观,JSON 字段与缩进保持一致。\n" +
+                                "7. 如果查询结果为空或数据缺失,请返回字段结构但留空数组,不要报错。\n" +
+                                "\n" +
+                                "当前时间为:" + DateUtils.getDate() + "\n" +
+                                "# 注意:回复之前请结合上下文和工具返回内容进行分析与生成结果。\n" +
+                                "\n" +
+                                "【英中文科室名称映射表】\n" +
+                                "Internal Medicine → 内科\n" +
+                                "Surgery → 外科\n" +
+                                "Emergency Department → 急诊科\n" +
+                                "Obstetrics & Gynecology → 妇产科\n" +
+                                "Pediatrics → 儿科\n";
+
+
             }
         }
         // 设置系统默认提示词