Просмотр исходного кода

fix(dan): S-cfc-34 修复 DanReportParseServiceTest 4 例红(B-35)| 能力键 dan.report.parseFormat

按 B-35 登记,4 例红单测转绿(38/38 通过):
1. 根因修复: 清洗正则 [\uF000-\uFFFF] 误删全角冒号 U+FF1A → 收窄为 [\uE000-\uF8FF]
   (testParseA2_standardCoreLiteracyFormat 姓名俊辰为 null 的根因)
2. A2 standardCoreLiteracy 版式: 测试日期关键词 + 日期正则兜底; findFieldValue 增加粘连值解析
   (value 段内再出现冒号时截取纯汉字段)
3. B4 降级版式: 自我概念区 parseSelfConcept 漏「自信心 7分」带单位格式 → 增加自信/自尊正则兜底
   (testParseB4_regression 仍走降级)
4. A1 detail 版式: 「我的xxx得分 41 | 14%」同行「分数|百分位」优先提取(原逻辑取下一行错位)
5. A1 fallback 窗口: 300 字符收紧至 60 + 正则 [^0-9]{0,10},避免跨到下一维度百分位

改动仅限 cfc-backend/src/main/java/com/etotem/cfc/service/DanReportParseService.java(+59/-6)
未改测试断言;mvn -DskipTests=false -Dtest=DanReportParseServiceTest test: 4 fail → 0 fail, 38/38 通过
真实样本 /api/health/report/parse 人工比对(3 份)需后端在线环境执行,留待后续轮次
iwt 6 дней назад
Родитель
Сommit
9628df3d9d

+ 59 - 6
cfc-backend/src/main/java/com/etotem/cfc/service/DanReportParseService.java

@@ -56,8 +56,9 @@ public class DanReportParseService {
         for (String line : lines) {
             String trimmed = line.trim();
             if (!trimmed.isEmpty()) {
-                // 去除PDF提取中的特殊Unicode字符
-                trimmed = trimmed.replaceAll("[\\uF000-\\uFFFF]", "").trim();
+                // 去除PDF提取中的特殊Unicode字符(仅保留真正私用区 U+E000–U+F8FF,
+                // 避免误删全角冒号 U+FF1A 等常用 CJK 标点导致「姓名:俊辰」→「姓名俊辰」)
+                trimmed = trimmed.replaceAll("[\\uE000-\\uF8FF]", "").trim();
                 if (!trimmed.isEmpty()) {
                     lineList.add(trimmed);
                 }
@@ -215,7 +216,12 @@ public class DanReportParseService {
         StringBuilder suggestions = new StringBuilder();
 
         // 1. 基本信息和日期
-        String reportDate = findFieldValue(lines, "测评日期", "报告日期", "评估日期");
+        String reportDate = findFieldValue(lines, "测评日期", "报告日期", "评估日期", "测试日期");
+        if (reportDate == null) {
+            // 降级版式兜底: "测试日期:2026-07-03" 等自由文本中的日期字段
+            java.util.regex.Matcher dateFallback = Pattern.compile("测?试日期[::]\\s*(\\d{4}[-/]\\d{1,2}[-/]\\d{1,2})").matcher(fullText);
+            if (dateFallback.find()) reportDate = dateFallback.group(1);
+        }
         String name = findFieldValue(lines, "姓名", "学生姓名", "被评估人");
         extra.put("name", name);
         extra.put("reportDate", reportDate);
@@ -258,6 +264,19 @@ public class DanReportParseService {
         if (selfConceptStart >= 0) {
             int end = mindsetStart > selfConceptStart ? mindsetStart : lines.size();
             Map<String, String> selfConcept = parseSelfConcept(lines, selfConceptStart, end);
+            // 降级版式: "自我概念\n自信心 7分\n自尊心 8分"(parseSelfConcept 按「维度 分数」行匹配漏掉带"分"单位)
+            if (selfConcept.isEmpty()) {
+                Pattern confP = Pattern.compile("自信[性心]\\s*(\\d+(?:\\.\\d+)?)");
+                Matcher confM = confP.matcher(fullText);
+                if (confM.find()) {
+                    selfConcept.put("自信心", confM.group(1));
+                }
+                Pattern esteemP = Pattern.compile("自尊[性心]?\\s*(\\d+(?:\\.\\d+)?)");
+                Matcher esteemM = esteemP.matcher(fullText);
+                if (esteemM.find()) {
+                    selfConcept.put("自尊心", esteemM.group(1));
+                }
+            }
             if (!selfConcept.isEmpty()) {
                 for (Map.Entry<String, String> entry : selfConcept.entrySet()) {
                     items.add(new DataItem(entry.getKey(), entry.getKey(), entry.getValue(), "selfConcept"));
@@ -353,6 +372,17 @@ public class DanReportParseService {
         for (int i = 0; i < cognitiveDims.length; i++) {
             String dim = cognitiveDims[i];
             String code = dimCodes[i];
+            // 同版式优先: "我的xxx得分 41  | 14%"(分数与百分位在同一行,分隔符为 | 或全角|)
+            Pattern sameLineP = Pattern.compile(
+                    "我的" + Pattern.quote(dim) + "得分\\s*(\\d+)\\s*[||]\\s*(\\d+)\\s*%");
+            Matcher sameLineM = sameLineP.matcher(fullText);
+            if (sameLineM.find()) {
+                items.add(new DataItem(code + "_score", dim + "得分", sameLineM.group(1), "cognitive"));
+                extra.put(code + "_score", sameLineM.group(1));
+                items.add(new DataItem(code + "_pct", dim + "百分位", sameLineM.group(2), "cognitive"));
+                extra.put(code + "_pct", sameLineM.group(2));
+                continue;
+            }
             int idx = fullText.indexOf("我的" + dim + "得分");
             if (idx < 0) continue;
             // 跳到下一行
@@ -392,11 +422,11 @@ public class DanReportParseService {
                 if (!extra.containsKey(code + "_pct")) {
                     int dimIdx = fullText.indexOf(dim);
                     if (dimIdx < 0) continue;
-                    // 在该维度名之后 200 字符内找百分位数字
-                    int searchEnd = Math.min(dimIdx + 300, fullText.length());
+                    // 在该维度名之后 60 字符内找百分位数字(窗口收紧至单维度,避免跨到下一维度)
+                    int searchEnd = Math.min(dimIdx + 60, fullText.length());
                     String window = fullText.substring(dimIdx, searchEnd);
                     java.util.regex.Matcher pctMatcher =
-                            Pattern.compile("百分位[((]%[))].*?(\\d+)").matcher(window);
+                            Pattern.compile("百分位[((]%[))][^0-9]{0,10}(\\d+)").matcher(window);
                     if (pctMatcher.find()) {
                         String val = pctMatcher.group(1);
                         items.add(new DataItem(code + "_pct", dim + "百分位", val, "cognitive"));
@@ -1673,6 +1703,29 @@ public class DanReportParseService {
                 }
             }
         }
+        // PDF 抽字常把字段值与下一个字段名粘连(如「俊辰性别:男」):取冒号前的纯汉字片段
+        for (String keyword : keywords) {
+            String compactKw = keyword.replaceAll("\\s+", "");
+            for (String line : lines) {
+                String compact = line.replaceAll("\\s+", "");
+                int idx = compact.indexOf(compactKw + ":");
+                if (idx < 0) idx = compact.indexOf(compactKw + ":");
+                if (idx < 0) continue;
+                String rest = compact.substring(idx + compactKw.length() + 1);
+                if (rest.isEmpty()) continue;
+                // 粘连判定:若同行 value 段内还有分隔符(下一个字段的冒号),
+                // 说明值与后续字段名粘连(PDF 抽字常见「姓   名:俊辰性   别: 男」),截取中间纯汉字段
+                int colon = rest.indexOf(':');
+                if (colon < 0) colon = rest.indexOf(':');
+                if (colon > 0) {
+                    String seg = rest.substring(0, colon);
+                    Matcher cjk = Pattern.compile("^[\\u4e00-\\u9fa5]{2,8}$").matcher(seg);
+                    if (cjk.find()) return seg;
+                }
+                String val = cleanFieldValue(rest);
+                if (val != null && !val.isEmpty()) return val;
+            }
+        }
         return null;
     }