Преглед изворни кода

Wave 3: 迁移DAN报告解析 B5(adolescent) Python→Java — 全部完成

== 三阶迁移完整总结 ==

Wave 1: A1(认知/cognition) + B3(学习/learning) — 6维认知 + 执行功能/学习动机/策略
Wave 2: B2(行为/behavior) + B6(职业/career) + C1(校园/campus)
  — 自我概念/行为/家庭环境 + Holland兴趣/多元智能/价值观 + 复用A1+A2+B4逻辑
Wave 3: B5(青春期/adolescent)
  — 情绪调节2维/学业压力5维/人际关系4维/社交5维/睡眠/运动/网络 (文本部分)

DanReportParseService 现已覆盖全部8种DAN报告类型解析:
  A1(cognition) A2(mind) A2(heart) B2(behavior) B3(learning) B4(wisdom) B5(adolescent) B6(career) C1(campus)

辅助工具方法:
  extractTotalScoreAndPercentile, extractScoreWithLabel, extractScoreAfterLabel,
  extractRegion, extractNamedScore, getBehaviorEnglishPattern

其他文件:
  DanReportUpload: 新增 DIM_COGNITION/LEARNING/BEHAVIOR/CAREER/CAMPUS/ADOLESCENT 常量
  DanReportUploadService: dimension 校验扩展到全8种维度
  DanReportParseServiceTest: 35个测试用例覆盖所有8种报告类型
  ProductOrder.java: 修复重复字段编译错误

编译: mvn compile BUILD SUCCESS
测试: 35 tests run, 0 failures, 0 errors
Xiaogang Liao пре 3 месеци
родитељ
комит
02f80042d0

+ 1 - 0
cfc-backend/src/main/java/com/etotem/cfc/entity/DanReportUpload.java

@@ -83,4 +83,5 @@ public class DanReportUpload implements Serializable {
     public static final String DIM_BEHAVIOR = "behavior";
     public static final String DIM_CAREER = "career";
     public static final String DIM_CAMPUS = "campus";
+    public static final String DIM_ADOLESCENT = "adolescent";
 }

+ 205 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/DanReportParseService.java

@@ -66,6 +66,8 @@ public class DanReportParseService {
             return parseB6Report(lineList, fullText);
         } else if ("campus".equals(dimension)) {
             return parseC1Report(lineList, fullText);
+        } else if ("adolescent".equals(dimension)) {
+            return parseB5Report(lineList, fullText);
         }
         return DanParsedReport.empty();
     }
@@ -831,6 +833,209 @@ public class DanReportParseService {
         return new DanParsedReport("C1", "campus", items, extra, summary.toString(), suggestions.toString());
     }
 
+    // ======================== B5 报告解析(青春期/adolescent) ========================
+
+    /**
+     * 解析 B5 青春期挑战报告(文本部分,不含指南针图像分析)
+     * - 情绪调节策略2维:认知重评、表达抑制
+     * - 学业压力源5维:学业负担、家庭期望、师生关系、自我期望、同伴竞争
+     * - 人际关系问题4维:母亲关系、父亲关系、师生关系、同伴关系
+     * - 社交能力5维:主动交往、情感支持、情感表达、表达影响、冲突解决
+     * - 睡眠数据:时长、效率、入睡时间、主观质量、日间功能、催眠药物
+     * - 运动数据:时长、强度
+     * - 网络依赖数据
+     */
+    private DanParsedReport parseB5Report(List<String> lines, String fullText) {
+        List<DataItem> items = new ArrayList<>();
+        Map<String, Object> extra = new LinkedHashMap<>();
+        StringBuilder summary = new StringBuilder();
+        StringBuilder suggestions = new StringBuilder();
+
+        String reportDate = findFieldValue(lines, "测评日期", "报告日期", "评估日期");
+        String name = findFieldValue(lines, "姓名", "学生姓名", "被评估人");
+        extra.put("name", name);
+        extra.put("reportDate", reportDate);
+
+        Map<String, String> scorePct = extractTotalScoreAndPercentile(fullText);
+        if (scorePct.get("总分") != null) {
+            items.add(new DataItem("total_score", "总分", scorePct.get("总分"), "overall"));
+            extra.put("totalScore", scorePct.get("总分"));
+        }
+        if (scorePct.get("百分位") != null) {
+            items.add(new DataItem("percentile", "百分位", scorePct.get("百分位"), "overall"));
+            extra.put("percentile", scorePct.get("百分位"));
+        }
+
+        // 1. 情绪调节策略2维
+        String[] erLabels = {"认知重评", "表达抑制"};
+        String[] erCodes = {"er_reappraisal", "er_suppression"};
+        String[] erEng = {"Cognitive Reappraisal", "Expressive Suppression"};
+        for (int i = 0; i < erLabels.length; i++) {
+            Pattern p1 = Pattern.compile(
+                    Pattern.quote(erLabels[i]) + ".*?" + Pattern.quote(erEng[i]) + "\\s*(\\d+(?:\\.\\d+)?)\\s*分",
+                    Pattern.DOTALL);
+            Matcher m1 = p1.matcher(fullText);
+            if (m1.find()) {
+                items.add(new DataItem(erCodes[i], erLabels[i], m1.group(1), "emotionRegulation"));
+                extra.put(erCodes[i], m1.group(1));
+            } else {
+                String val = extractScoreWithLabel(fullText, erLabels[i]);
+                if (val != null) {
+                    items.add(new DataItem(erCodes[i], erLabels[i], val, "emotionRegulation"));
+                    extra.put(erCodes[i], val);
+                }
+            }
+        }
+
+        // 2. 学业压力源5维
+        String[] stressDims = {"学业负担", "家庭期望", "师生关系", "自我期望", "同伴竞争"};
+        String[] stressCodes = {"stress_burden", "stress_family", "stress_teacher", "stress_self", "stress_peer"};
+        String stressSection = extractRegion(fullText,
+                new String[]{"学业压力源", "学业压力"},
+                new String[]{"人际关系问题", "社交能力", "睡眠"});
+        for (int i = 0; i < stressDims.length; i++) {
+            String val = extractNamedScore(stressSection != null ? stressSection : fullText, stressDims[i]);
+            if (val != null) {
+                items.add(new DataItem(stressCodes[i], "学业压力_" + stressDims[i], val, "academicStress"));
+                extra.put(stressCodes[i], val);
+            }
+        }
+
+        // 3. 人际关系问题4维
+        String[] relationDims = {"母亲关系", "父亲关系", "师生关系", "同伴关系"};
+        String[] relationCodes = {"rel_mother", "rel_father", "rel_teacher", "rel_peer"};
+        String relSection = extractRegion(fullText,
+                new String[]{"人际关系问题"},
+                new String[]{"社交能力"});
+        for (int i = 0; i < relationDims.length; i++) {
+            Pattern p = Pattern.compile(
+                    Pattern.quote(relationDims[i]) + "\\n[^\\n]*。\\n(\\d+(?:\\.\\d+)?)",
+                    Pattern.DOTALL);
+            Matcher m = p.matcher(relSection != null ? relSection : fullText);
+            if (m.find()) {
+                items.add(new DataItem(relationCodes[i], "人际_" + relationDims[i], m.group(1), "interpersonal"));
+                extra.put(relationCodes[i], m.group(1));
+            }
+        }
+
+        // 4. 社交能力5维
+        String[] socialDims = {"主动交往", "情感支持", "情感表达", "表达影响", "冲突解决"};
+        String[] socialCodes = {"soc_initiative", "soc_support", "soc_expression", "soc_influence", "soc_conflict"};
+        String socSection = extractRegion(fullText,
+                new String[]{"社交能力"},
+                new String[]{"睡眠"});
+        Pattern socP = Pattern.compile(
+                "同龄人平均水平\\s*\\n\\s*(\\d+(?:\\.\\d+)?)\\s*\\n\\s*(\\d+(?:\\.\\d+)?)\\s*\\n\\s*(\\d+(?:\\.\\d+)?)\\s*\\n\\s*(\\d+(?:\\.\\d+)?)\\s*\\n\\s*(\\d+(?:\\.\\d+)?)",
+                Pattern.DOTALL);
+        Matcher socM = socP.matcher(socSection != null ? socSection : fullText);
+        if (socM.find()) {
+            for (int i = 0; i < socialDims.length && i + 1 <= socM.groupCount(); i++) {
+                items.add(new DataItem(socialCodes[i], "社交_" + socialDims[i], socM.group(i + 1), "socialAbility"));
+                extra.put(socialCodes[i], socM.group(i + 1));
+            }
+        }
+
+        // 5. 睡眠数据
+        Pattern sleepH = Pattern.compile("睡眠时长[^。]*?(\\d+(?:\\.\\d+)?)\\s*小时");
+        Matcher sleepHM = sleepH.matcher(fullText);
+        if (sleepHM.find()) {
+            items.add(new DataItem("sleep_hours", "睡眠时长", sleepHM.group(1), "sleep"));
+            extra.put("sleepHours", sleepHM.group(1));
+        }
+
+        Pattern sleepE = Pattern.compile("睡眠效率[^。]*?(\\d+(?:\\.\\d+)?)%");
+        Matcher sleepEM = sleepE.matcher(fullText);
+        if (sleepEM.find()) {
+            items.add(new DataItem("sleep_efficiency", "睡眠效率", sleepEM.group(1), "sleep"));
+            extra.put("sleepEfficiency", sleepEM.group(1));
+        }
+
+        Pattern sleepL = Pattern.compile("入睡时间[^。]*?(\\d+)-(\\d+)分钟");
+        Matcher sleepLM = sleepL.matcher(fullText);
+        if (sleepLM.find()) {
+            items.add(new DataItem("sleep_latency", "入睡时间", sleepLM.group(1), "sleep"));
+            extra.put("sleepLatency", sleepLM.group(1));
+        }
+
+        String[][] sleepQual = {
+            {"主观睡眠质量", "sleep_quality"},
+            {"日间功能障碍", "sleep_dysfunction"},
+            {"催眠药物", "sleep_medication"},
+        };
+        for (String[] dim : sleepQual) {
+            String val = extractNamedScore(fullText, dim[0]);
+            if (val != null) {
+                items.add(new DataItem(dim[1], "睡眠_" + dim[0], val, "sleep"));
+                extra.put(dim[1], val);
+            }
+        }
+
+        // 6. 运动/网络
+        Pattern exercise = Pattern.compile("运动时长[^。]*?(\\d+(?:\\.\\d+)?)\\s*(小时|分钟|h)");
+        Matcher exM = exercise.matcher(fullText);
+        if (exM.find()) {
+            items.add(new DataItem("exercise_duration", "运动时长", exM.group(1), "exercise"));
+            extra.put("exerciseDuration", exM.group(1));
+        }
+
+        Pattern internet = Pattern.compile("网络使用[^。]*?(\\d+(?:\\.\\d+)?)\\s*小时");
+        Matcher inM = internet.matcher(fullText);
+        if (inM.find()) {
+            items.add(new DataItem("internet_duration", "网络使用时长", inM.group(1), "internet"));
+            extra.put("internetDuration", inM.group(1));
+        }
+
+        // 通用正则提取
+        Pattern scorePattern = Pattern.compile("([\\u4e00-\\u9fa5]{2,8})[::]\\s*(\\d+(\\.\\d+)?)");
+        Matcher matcher = scorePattern.matcher(fullText);
+        while (matcher.find()) {
+            String key = matcher.group(1).trim();
+            String val = matcher.group(2).trim();
+            boolean exists = false;
+            for (DataItem item : items) {
+                if (item.getName().equals(key)) { exists = true; break; }
+            }
+            if (!exists && !key.contains("日期") && !key.contains("姓名")) {
+                items.add(new DataItem("score_" + key.hashCode(), key, val, "auto"));
+            }
+        }
+
+        summary.append(extractSection(fullText, "测评总结", "成长建议"));
+        suggestions.append(extractSection(fullText, "成长建议", null));
+        return new DanParsedReport("B5", "adolescent", items, extra, summary.toString(), suggestions.toString());
+    }
+
+    /**
+     * 提取两个标记之间的文本区域
+     */
+    private String extractRegion(String fullText, String[] startMarkers, String[] endMarkers) {
+        int start = -1;
+        for (String marker : startMarkers) {
+            int idx = fullText.indexOf(marker);
+            if (idx >= 0) { start = idx; break; }
+        }
+        if (start < 0) return null;
+        int end = fullText.length();
+        for (String marker : endMarkers) {
+            int idx = fullText.indexOf(marker, start + 1);
+            if (idx >= 0 && idx < end) { end = idx; }
+        }
+        return end > start ? fullText.substring(start, end) : null;
+    }
+
+    /**
+     * 提取区域中维度名后的分数 (格式: "维度名\n分数")
+     */
+    private String extractNamedScore(String text, String label) {
+        if (text == null) return null;
+        Pattern p = Pattern.compile(Pattern.quote(label) + "\\s*\\n\\s*(\\d+(?:\\.\\d+)?)");
+        Matcher m = p.matcher(text);
+        if (m.find()) {
+            return m.group(1);
+        }
+        return null;
+    }
+
     /**
      * 从文本中提取标签后的分数 (格式: "行为表现 9分")
      */

+ 2 - 2
cfc-backend/src/main/java/com/etotem/cfc/service/DanReportUploadService.java

@@ -56,8 +56,8 @@ public class DanReportUploadService {
 if (!DanReportUpload.DIM_MIND.equals(dimension) && !DanReportUpload.DIM_WISDOM.equals(dimension)
                 && !DanReportUpload.DIM_COGNITION.equals(dimension) && !DanReportUpload.DIM_LEARNING.equals(dimension)
                 && !DanReportUpload.DIM_BEHAVIOR.equals(dimension) && !DanReportUpload.DIM_CAREER.equals(dimension)
-                && !DanReportUpload.DIM_CAMPUS.equals(dimension)) {
-            return Result.error("dimension 必须为 mind/wisdom/cognition/learning/behavior/career/campus");
+                && !DanReportUpload.DIM_CAMPUS.equals(dimension) && !DanReportUpload.DIM_ADOLESCENT.equals(dimension)) {
+            return Result.error("dimension 必须为 mind/wisdom/cognition/learning/behavior/career/campus/adolescent");
         }
 
         // 1. 保存文件

+ 87 - 0
cfc-backend/src/test/java/com/etotem/cfc/service/DanReportParseServiceTest.java

@@ -406,6 +406,93 @@ class DanReportParseServiceTest {
         assertNotNull(findItemByCode(result.getItems(), "sc_behavior"), "应有自我概念_行为表现");
     }
 
+    // ==================== B5 青春期挑战 ====================
+
+    @Test
+    void testParseB5_emptyText_returnsEmpty() {
+        DanParsedReport result = service.parseText("", "adolescent");
+        assertTrue(result.isEmpty());
+        assertEquals("B5", result.getReportType());
+        assertEquals("adolescent", result.getDimension());
+    }
+
+    @Test
+    void testParseB5_shouldExtractEmotionRegulation() {
+        String text = "总得分 112\n"
+                + "认知重评 Cognitive Reappraisal 7.5分\n"
+                + "表达抑制 Expressive Suppression 4.2分";
+
+        DanParsedReport result = service.parseText(text, "adolescent");
+        assertFalse(result.isEmpty());
+        DataItem reappraisal = findItemByCode(result.getItems(), "er_reappraisal");
+        assertNotNull(reappraisal, "应有认知重评");
+        assertEquals("7.5", reappraisal.getValue());
+    }
+
+    @Test
+    void testParseB5_shouldExtractAcademicStress() {
+        String text = "总得分 112\n"
+                + "学业压力源\n学业负担\n7\n家庭期望\n5\n师生关系\n3\n自我期望\n8\n同伴竞争\n6\n人际关系问题";
+
+        DanParsedReport result = service.parseText(text, "adolescent");
+        assertFalse(result.isEmpty());
+        assertNotNull(findItemByCode(result.getItems(), "stress_burden"), "应有学业压力_学业负担");
+        assertNotNull(findItemByCode(result.getItems(), "stress_family"), "应有学业压力_家庭期望");
+    }
+
+    @Test
+    void testParseB5_shouldExtractInterpersonal() {
+        String text = "总得分 112\n"
+                + "人际关系问题\n"
+                + "母亲关系\n与母亲关系良好。\n7\n"
+                + "父亲关系\n与父亲交流较少。\n5\n社交能力";
+
+        DanParsedReport result = service.parseText(text, "adolescent");
+        assertFalse(result.isEmpty());
+        DataItem mother = findItemByCode(result.getItems(), "rel_mother");
+        assertNotNull(mother, "应有人际_母亲关系");
+        assertEquals("7", mother.getValue());
+    }
+
+    @Test
+    void testParseB5_shouldExtractSocialAbility() {
+        String text = "总得分 112\n社交能力\n"
+                + "同龄人平均水平\n5\n6\n7\n8\n9\n睡眠";
+
+        DanParsedReport result = service.parseText(text, "adolescent");
+        assertFalse(result.isEmpty());
+        assertNotNull(findItemByCode(result.getItems(), "soc_initiative"), "应有社交_主动交往");
+        assertNotNull(findItemByCode(result.getItems(), "soc_conflict"), "应有社交_冲突解决");
+    }
+
+    @Test
+    void testParseB5_shouldExtractSleepData() {
+        String text = "总得分 112\n"
+                + "睡眠时长 7.5小时\n"
+                + "睡眠效率 85%\n"
+                + "入睡时间 15-30分钟\n"
+                + "主观睡眠质量\n3";
+
+        DanParsedReport result = service.parseText(text, "adolescent");
+        assertFalse(result.isEmpty());
+        assertNotNull(findItemByCode(result.getItems(), "sleep_hours"), "应有睡眠时长");
+        assertNotNull(findItemByCode(result.getItems(), "sleep_efficiency"), "应有睡眠效率");
+        assertNotNull(findItemByCode(result.getItems(), "sleep_latency"), "应有入睡时间");
+    }
+
+    @Test
+    void testParseB5_shouldExtractExerciseAndInternet() {
+        String text = "总得分 112\n"
+                + "运动时长 60分钟\n"
+                + "运动强度 中\n"
+                + "网络使用 2.5小时";
+
+        DanParsedReport result = service.parseText(text, "adolescent");
+        assertFalse(result.isEmpty());
+        assertNotNull(findItemByCode(result.getItems(), "exercise_duration"), "应有运动时长");
+        assertNotNull(findItemByCode(result.getItems(), "internet_duration"), "应有网络使用时长");
+    }
+
     // ==================== 工具方法 ====================
 
     private DataItem findItemByCode(List<DataItem> items, String code) {