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

Wave 2: 迁移DAN报告解析 B2(行为)+B6(职业)+C1(校园) Python→Java

- DanReportParseService: 新增 parseB2Report() 自我概念6维+儿童行为8维+家庭环境10维
- DanReportParseService: 新增 parseB6Report() Holland兴趣6型+多元智能8维+职业价值观
- DanReportParseService: 新增 parseC1Report() 复用A1认知+A2大五+B4自驱力逻辑
- DanReportParseService: parseText() 新增 behavior/career/campus 路由
- DanReportUpload: 新增 DIM_BEHAVIOR/DIM_CAREER/DIM_CAMPUS 常量 + Service校验
- DanReportParseServiceTest: 新增12个测试用例,总计28个全部通过
- fix: 添加ActivityProvision实体修复ProductOrderController预置编译错误
Xiaogang Liao пре 2 месеци
родитељ
комит
9531f7a588

+ 0 - 3
cfc-backend/src/main/java/com/etotem/cfc/entity/ActivityProvision.java

@@ -22,15 +22,12 @@ public class ActivityProvision implements Serializable {
     private Long buyerId;
     private Long familyId;
     private Long childId;
-
     private String status;
     private String qrCode;
     private Date checkedInAt;
     private Date completedAt;
-
     private Integer refundAmount;
     private Date refundAt;
-
     private Date createdAt;
     private Date updatedAt;
 }

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

@@ -78,4 +78,9 @@ public class DanReportUpload implements Serializable {
 
     public static final String DIM_MIND = "mind";
     public static final String DIM_WISDOM = "wisdom";
+    public static final String DIM_COGNITION = "cognition";
+    public static final String DIM_LEARNING = "learning";
+    public static final String DIM_BEHAVIOR = "behavior";
+    public static final String DIM_CAREER = "career";
+    public static final String DIM_CAMPUS = "campus";
 }

+ 10 - 0
cfc-backend/src/main/java/com/etotem/cfc/entity/ProductOrder.java

@@ -57,6 +57,12 @@ public class ProductOrder implements Serializable {
     // 购买信息采集
     private Long consigneeId;          // 收货人ID
     private String purchaseInfo;       // 购买信息快照(JSON)
+    // 活动核销字段
+    private Long activityId;
+    private String provisionStatus;
+    private Long auditorId;
+    private Date auditedAt;
+    private String auditReason;
 
     private Long activityId;
     private String provisionStatus;
@@ -89,4 +95,8 @@ public class ProductOrder implements Serializable {
     public void setMoneyAmount(Integer moneyAmount) { this.moneyAmount = moneyAmount; }
     public void setConsigneeId(Long consigneeId) { this.consigneeId = consigneeId; }
     public void setPurchaseInfo(String purchaseInfo) { this.purchaseInfo = purchaseInfo; }
+    public void setProvisionStatus(String provisionStatus) { this.provisionStatus = provisionStatus; }
+    public void setAuditorId(Long auditorId) { this.auditorId = auditorId; }
+    public void setAuditedAt(Date auditedAt) { this.auditedAt = auditedAt; }
+    public void setAuditReason(String auditReason) { this.auditReason = auditReason; }
 }

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

@@ -56,6 +56,16 @@ public class DanReportParseService {
             return parseA2Report(lineList, fullText);
         } else if ("wisdom".equals(dimension)) {
             return parseB4Report(lineList, fullText);
+        } else if ("cognition".equals(dimension)) {
+            return parseA1Report(lineList, fullText);
+        } else if ("learning".equals(dimension)) {
+            return parseB3Report(lineList, fullText);
+        } else if ("behavior".equals(dimension)) {
+            return parseB2Report(lineList, fullText);
+        } else if ("career".equals(dimension)) {
+            return parseB6Report(lineList, fullText);
+        } else if ("campus".equals(dimension)) {
+            return parseC1Report(lineList, fullText);
         }
         return DanParsedReport.empty();
     }
@@ -235,6 +245,688 @@ public class DanReportParseService {
         return new DanParsedReport("B4", "wisdom", items, extra, summary.toString(), suggestions.toString());
     }
 
+// ======================== A1 报告解析(认知/cognition) ========================
+
+    /**
+     * 解析 A1 儿童核心认知发展报告(6个认知维度)
+     * 维度:感知觉、注意力、记忆力、推理能力、空间能力、加工速度
+     * 提取总分/百分位 + 各维度百分位 + 各维度原始分
+     */
+    private DanParsedReport parseA1Report(List<String> lines, String fullText) {
+        List<DataItem> items = new ArrayList<>();
+        Map<String, Object> extra = new LinkedHashMap<>();
+        StringBuilder summary = new StringBuilder();
+        StringBuilder suggestions = new StringBuilder();
+
+        // 1. 基本信息和日期
+        String reportDate = findFieldValue(lines, "测评日期", "报告日期", "评估日期");
+        String name = findFieldValue(lines, "姓名", "学生姓名", "被评估人");
+        extra.put("name", name);
+        extra.put("reportDate", reportDate);
+
+        // 2. 总分和百分位(通用提取)
+        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("百分位"));
+        }
+
+        // 3. 6个核心认知维度
+        String[] cognitiveDims = {"感知觉", "注意力", "记忆力", "推理能力", "空间能力", "加工速度"};
+        String[] dimCodes = {"perception", "attention", "memory", "reasoning", "spatial", "processingSpeed"};
+
+        // 方法1: 从summary页面提取百分位 (格式: "感知觉 |  Perception\n百分位(%)\n14")
+        for (int i = 0; i < cognitiveDims.length; i++) {
+            String dim = cognitiveDims[i];
+            String code = dimCodes[i];
+            Pattern p1 = Pattern.compile(
+                    Pattern.quote(dim) + "\\s*\\|[^\\n]*\\n\\s*百分位(%)\\s*\\n\\s*(\\d+)");
+            Matcher m1 = p1.matcher(fullText);
+            if (m1.find()) {
+                String val = m1.group(1);
+                items.add(new DataItem(code + "_pct", dim + "百分位", val, "cognitive"));
+                extra.put(code + "_pct", val);
+            }
+        }
+
+        // 方法2: 从detail页面提取原始分和百分位 (格式: "我的感知觉得分 41  | 14%")
+        for (int i = 0; i < cognitiveDims.length; i++) {
+            String dim = cognitiveDims[i];
+            String code = dimCodes[i];
+            Pattern p2 = Pattern.compile(
+                    "我的" + Pattern.quote(dim) + "得分\\s*(\\d+)\\s*\\|\\s*(\\d+)%");
+            Matcher m2 = p2.matcher(fullText);
+            if (m2.find()) {
+                String score = m2.group(1);
+                String pct = m2.group(2);
+                items.add(new DataItem(code + "_score", dim + "得分", score, "cognitive"));
+                extra.put(code + "_score", score);
+                if (!extra.containsKey(code + "_pct")) {
+                    items.add(new DataItem(code + "_pct", dim + "百分位", pct, "cognitive"));
+                    extra.put(code + "_pct", pct);
+                }
+            }
+        }
+
+        // 方法3: 后备 - 直接搜索 "维度名\n百分位(%)\n数字" (某些PDF格式)
+        int foundPct = 0;
+        for (String code : dimCodes) {
+            if (extra.containsKey(code + "_pct")) foundPct++;
+        }
+        if (foundPct < 6) {
+            for (int i = 0; i < cognitiveDims.length; i++) {
+                String dim = cognitiveDims[i];
+                String code = dimCodes[i];
+                if (!extra.containsKey(code + "_pct")) {
+                    Pattern p3 = Pattern.compile(
+                            Pattern.quote(dim) + ".*?百分位(%).*?(\\d+)", Pattern.DOTALL);
+                    Matcher m3 = p3.matcher(fullText);
+                    if (m3.find()) {
+                        String val = m3.group(1);
+                        items.add(new DataItem(code + "_pct", dim + "百分位", val, "cognitive"));
+                        extra.put(code + "_pct", val);
+                    }
+                }
+            }
+        }
+
+        // 4. 通用正则:提取所有 "维度名: 分数" 模式
+        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("A1", "cognition", items, extra, summary.toString(), suggestions.toString());
+    }
+
+    // ======================== B3 报告解析(学习/learning) ========================
+
+    /**
+     * 解析 B3 核心学习能力报告
+     * - 执行功能3维:抑制控制、工作记忆、认知灵活性(百分位)
+     * - 学习动机3维:深层动机、表面动机、自我效能感(十分制)
+     * - 学习策略3维:深层方法与策略、表面方法与策略、学习自我调节(十分制)
+     */
+    private DanParsedReport parseB3Report(List<String> lines, String fullText) {
+        List<DataItem> items = new ArrayList<>();
+        Map<String, Object> extra = new LinkedHashMap<>();
+        StringBuilder summary = new StringBuilder();
+        StringBuilder suggestions = new StringBuilder();
+
+        // 1. 基本信息和日期
+        String reportDate = findFieldValue(lines, "测评日期", "报告日期", "评估日期");
+        String name = findFieldValue(lines, "姓名", "学生姓名", "被评估人");
+        extra.put("name", name);
+        extra.put("reportDate", reportDate);
+
+        // 2. 总分和百分位
+        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("百分位"));
+        }
+
+        // 3. 执行功能 (3维度, 百分位) 格式: "抑制控制 94%  工作记忆 89%  认知灵活性 60%"
+        String[][] execDims = {
+            {"抑制控制", "inhibitoryControl"},
+            {"工作记忆", "workingMemory"},
+            {"认知灵活性", "cognitiveFlexibility"}
+        };
+        for (String[] dim : execDims) {
+            Pattern p = Pattern.compile(Pattern.quote(dim[0]) + "\\s*(\\d+)%");
+            Matcher m = p.matcher(fullText);
+            if (m.find()) {
+                String val = m.group(1);
+                items.add(new DataItem(dim[1], dim[0] + "百分位", val, "executiveFunction"));
+                extra.put(dim[1], val);
+            }
+        }
+
+        // 4. 学习动机 (3维度, 十分制) 格式: "深层动机\n我的得分:8分"
+        String[][] motivationDims = {
+            {"深层动机", "deepMotivation"},
+            {"表面动机", "surfaceMotivation"},
+            {"自我效能感", "selfEfficacy"}
+        };
+        for (String[] dim : motivationDims) {
+            String val = extractScoreAfterLabel(fullText, dim[0]);
+            if (val != null) {
+                items.add(new DataItem(dim[1], dim[0], val, "learningMotivation"));
+                extra.put(dim[1], val);
+            }
+        }
+
+        // 5. 学习策略 (3维度, 十分制) 格式: "深层方法与策略\n我的得分:6.8分"
+        String[][] strategyDims = {
+            {"深层方法与策略", "deepStrategy"},
+            {"表面方法与策略", "surfaceStrategy"},
+            {"学习自我调节", "selfRegulation"}
+        };
+        for (String[] dim : strategyDims) {
+            String val = extractScoreAfterLabel(fullText, dim[0]);
+            if (val != null) {
+                items.add(new DataItem(dim[1], dim[0], val, "learningStrategy"));
+                extra.put(dim[1], val);
+            }
+        }
+
+        // 6. 通用正则提取
+        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("B3", "learning", items, extra, summary.toString(), suggestions.toString());
+    }
+
+    // ======================== B2 报告解析(行为/behavior) ========================
+
+    /**
+     * 解析 B2 儿童自我与家庭教养报告
+     * - 自我概念6维:行为表现、能力与学校表现、躯体外貌、情绪状态、合群、幸福与满足
+     * - 儿童行为8维:品行问题、情绪问题、学习问题、社交问题、生活习惯、多动倾向、刻板行为、拖延行为
+     * - 家庭环境10维:亲密、情感表达、和谐、独立性、成就向导、文化氛围、娱乐活动、道德观念、家务安排、家庭规则
+     */
+    private DanParsedReport parseB2Report(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. 自我概念6维 (格式: "行为表现 9分")
+        String[][] selfConceptDims = {
+            {"行为表现", "selfConcept_behavior"},
+            {"能力与学校表现", "selfConcept_school"},
+            {"躯体外貌", "selfConcept_appearance"},
+            {"情绪状态", "selfConcept_emotion"},
+            {"合群", "selfConcept_sociability"},
+            {"幸福与满足", "selfConcept_happiness"},
+        };
+        for (String[] dim : selfConceptDims) {
+            String val = extractScoreWithLabel(fullText, dim[0]);
+            if (val != null) {
+                items.add(new DataItem(dim[1], "自我概念_" + dim[0], val, "selfConcept"));
+                extra.put(dim[1], val);
+            }
+        }
+
+        // 2. 儿童行为8维
+        String[][] behaviorDims = {
+            {"品行问题", "conduct"},
+            {"情绪问题", "emotional"},
+            {"学习问题", "learning"},
+            {"社交问题", "social"},
+            {"生活习惯", "habits"},
+            {"多动倾向", "hyperactivity"},
+            {"刻板行为", "stereotypic"},
+            {"拖延行为", "procrastination"},
+        };
+        for (String[] dim : behaviorDims) {
+            // 方法1: 中文匹配 "品行问题 1分"
+            Pattern p1 = Pattern.compile(Pattern.quote(dim[0]) + "\\s*(\\d+)\\s*分");
+            Matcher m1 = p1.matcher(fullText);
+            if (m1.find()) {
+                items.add(new DataItem(dim[1], "行为_" + dim[0], m1.group(1), "behavior"));
+                extra.put(dim[1], m1.group(1));
+            } else {
+                // 方法2: 英文关键词
+                String engPattern = getBehaviorEnglishPattern(dim[0]);
+                if (engPattern != null) {
+                    Pattern p2 = Pattern.compile(engPattern + "\\s+(\\d+)\\s*分");
+                    Matcher m2 = p2.matcher(fullText);
+                    if (m2.find()) {
+                        items.add(new DataItem(dim[1], "行为_" + dim[0], m2.group(1), "behavior"));
+                        extra.put(dim[1], m2.group(1));
+                    }
+                }
+            }
+        }
+
+        // 3. 家庭环境10维
+        String[][] familyDims = {
+            {"亲密", "family_intimacy"},
+            {"情感表达", "family_expression"},
+            {"和谐", "family_harmony"},
+            {"独立性", "family_independence"},
+            {"成就向导", "family_achievement"},
+            {"文化氛围", "family_culture"},
+            {"娱乐活动", "family_recreation"},
+            {"道德观念", "family_morality"},
+            {"家务安排", "family_chores"},
+            {"家庭规则", "family_rules"},
+        };
+        for (String[] dim : familyDims) {
+            String val = extractScoreWithLabel(fullText, dim[0]);
+            if (val != null) {
+                items.add(new DataItem(dim[1], "家庭_" + dim[0], val, "family"));
+                extra.put(dim[1], val);
+            }
+        }
+
+        // 通用正则提取
+        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("B2", "behavior", items, extra, summary.toString(), suggestions.toString());
+    }
+
+    // ======================== B6 报告解析(职业/career) ========================
+
+    /**
+     * 解析 B6 职业发展报告
+     * - Holland兴趣6型:艺术型、社会型、事业型、常规型、现实型、研究型
+     * - 多元智能8维:内省能力、空间能力、音乐能力、人际关系能力、自然能力、身体运动能力、语言能力、逻辑数学能力
+     * - 职业价值观:代表分数
+     */
+    private DanParsedReport parseB6Report(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. Holland职业兴趣6型 (格式: "艺术型 Artistic 7分")
+        String[][] interestDims = {
+            {"艺术型", "interest_artistic"},
+            {"社会型", "interest_social"},
+            {"事业型", "interest_enterprising"},
+            {"常规型", "interest_conventional"},
+            {"现实型", "interest_realistic"},
+            {"研究型", "interest_investigative"},
+        };
+        for (String[] dim : interestDims) {
+            Pattern p = Pattern.compile(Pattern.quote(dim[0]) + "\\s+\\w+\\s+(\\d+)\\s*分");
+            Matcher m = p.matcher(fullText);
+            if (m.find()) {
+                items.add(new DataItem(dim[1], "兴趣_" + dim[0], m.group(1), "hollandInterest"));
+                extra.put(dim[1], m.group(1));
+            } else {
+                // fallback: "艺术型 7分"
+                String val = extractScoreWithLabel(fullText, dim[0]);
+                if (val != null) {
+                    items.add(new DataItem(dim[1], "兴趣_" + dim[0], val, "hollandInterest"));
+                    extra.put(dim[1], val);
+                }
+            }
+        }
+
+        // 2. 多元智能8维 (格式: "内省能力 INTRAPERSONAL 8分")
+        String[][] abilityDims = {
+            {"内省能力", "ability_intrapersonal"},
+            {"空间能力", "ability_spatial"},
+            {"音乐能力", "ability_musical"},
+            {"人际关系能力", "ability_interpersonal"},
+            {"自然能力", "ability_naturalist"},
+            {"身体运动能力", "ability_kinesthetic"},
+            {"语言能力", "ability_linguistic"},
+            {"逻辑数学能力", "ability_logical"},
+        };
+        for (String[] dim : abilityDims) {
+            Pattern p = Pattern.compile(Pattern.quote(dim[0]) + "\\s+\\w+\\s+(\\d+)\\s*分");
+            Matcher m = p.matcher(fullText);
+            if (m.find()) {
+                int val = Integer.parseInt(m.group(1));
+                if (val >= 1 && val <= 15) {
+                    items.add(new DataItem(dim[1], "能力_" + dim[0], m.group(1), "multipleIntelligence"));
+                    extra.put(dim[1], m.group(1));
+                    continue;
+                }
+            }
+            // fallback: "内省能力 8分"
+            Pattern p2 = Pattern.compile(Pattern.quote(dim[0]) + "\\s*(\\d+)\\s*分");
+            Matcher m2 = p2.matcher(fullText);
+            if (m2.find()) {
+                int val = Integer.parseInt(m2.group(1));
+                if (val >= 1 && val <= 15) {
+                    items.add(new DataItem(dim[1], "能力_" + dim[0], m2.group(1), "multipleIntelligence"));
+                    extra.put(dim[1], m2.group(1));
+                }
+            }
+        }
+
+        // 3. 职业价值观
+        if (fullText.contains("我的职业价值观")) {
+            Pattern p = Pattern.compile("我的职业价值观.*?(\\d+(?:\\.\\d+)?)", Pattern.DOTALL);
+            Matcher m = p.matcher(fullText);
+            if (m.find()) {
+                double val = Double.parseDouble(m.group(1));
+                if (val >= 1 && val <= 15) {
+                    items.add(new DataItem("career_value", "职业价值观", m.group(1), "careerValue"));
+                    extra.put("careerValue", m.group(1));
+                }
+            }
+        }
+        if (!extra.containsKey("careerValue") && fullText.contains("职业价值观")) {
+            Pattern p = Pattern.compile("职业价值观.*?最高分[^\\d]*(\\d+(?:\\.\\d+)?)", Pattern.DOTALL);
+            Matcher m = p.matcher(fullText);
+            if (m.find()) {
+                double val = Double.parseDouble(m.group(1));
+                if (val >= 1 && val <= 15) {
+                    items.add(new DataItem("career_value", "职业价值观", m.group(1), "careerValue"));
+                    extra.put("careerValue", m.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("B6", "career", items, extra, summary.toString(), suggestions.toString());
+    }
+
+    // ======================== C1 报告解析(校园/campus) ========================
+
+    /**
+     * 解析 C1 校园标准版报告
+     * - 核心认知6维(复用A1逻辑):感知觉、注意力、记忆力、推理能力、空间能力、加工速度
+     * - 大五人格5维(复用A2逻辑):开放性、宜人性、责任心、外倾性、神经质
+     * - 自驱力3维(复用B4逻辑):自主性、胜任感、归属感
+     * - 自我概念6维(复用B2逻辑):行为表现、能力与学校、躯体外貌、情绪状态、合群、幸福与满足
+     */
+    private DanParsedReport parseC1Report(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. 核心认知6维 (复用A1逻辑: "我的感知觉得分 41  | 14%")
+        String[] cognitiveDims = {"感知觉", "注意力", "记忆力", "推理能力", "空间能力", "加工速度"};
+        String[] cogCodes = {"perception", "attention", "memory", "reasoning", "spatial", "processingSpeed"};
+        for (int i = 0; i < cognitiveDims.length; i++) {
+            String dim = cognitiveDims[i];
+            String code = cogCodes[i];
+            // 格式: "我的感知觉得分 41  | 14%"
+            Pattern p = Pattern.compile("我的" + Pattern.quote(dim) + "得分\\s*(\\d+)\\s*\\|\\s*(\\d+)%");
+            Matcher m = p.matcher(fullText);
+            if (m.find()) {
+                items.add(new DataItem(code + "_score", dim + "得分", m.group(1), "cognitive"));
+                extra.put(code + "_score", m.group(1));
+                items.add(new DataItem(code + "_pct", dim + "百分位", m.group(2), "cognitive"));
+                extra.put(code + "_pct", m.group(2));
+            } else {
+                // fallback: "感知觉 | Perception\n百分位(%)\n14"
+                Pattern p2 = Pattern.compile(Pattern.quote(dim) + "\\s*\\|[^\\n]*\\n\\s*百分位(%)\\s*\\n\\s*(\\d+)");
+                Matcher m2 = p2.matcher(fullText);
+                if (m2.find()) {
+                    items.add(new DataItem(code + "_pct", dim + "百分位", m2.group(1), "cognitive"));
+                    extra.put(code + "_pct", m2.group(1));
+                }
+            }
+        }
+
+        // 2. 大五人格5维 (复用A2逻辑)
+        String[] big5Names = {"开放性", "宜人性", "责任心", "外倾性", "神经质"};
+        for (String dim : big5Names) {
+            Pattern p = Pattern.compile("您在[\"\"「]" + Pattern.quote(dim) + "[\"\"」].*?得分是\\s*(\\d+(?:\\.\\d+)?)\\s*分");
+            Matcher m = p.matcher(fullText);
+            if (m.find()) {
+                items.add(new DataItem("big5_" + dim, dim, m.group(1), "bigFive"));
+                extra.put("big5_" + dim, m.group(1));
+            }
+        }
+
+        // 3. 自驱力3维 (使用"我的得分"模式)
+        Pattern drivingP = Pattern.compile("我的得分[::](\\d+\\.?\\d*)");
+        Matcher drivingM = drivingP.matcher(fullText);
+        List<String> drivingScores = new ArrayList<>();
+        while (drivingM.find()) {
+            drivingScores.add(drivingM.group(1));
+        }
+        String[] drivingNames = {"自主性", "胜任感", "归属感"};
+        String[] drivingCodes = {"autonomy", "competence", "belonging"};
+        for (int i = 0; i < drivingScores.size() && i < drivingNames.length; i++) {
+            items.add(new DataItem(drivingCodes[i], drivingNames[i], drivingScores.get(i), "selfDriving"));
+            extra.put(drivingCodes[i], drivingScores.get(i));
+        }
+
+        // 4. 自我概念6维 (在SELF-CONCEPT标记前找百分位)
+        int scStart = fullText.indexOf("SELF-CONCEPT");
+        if (scStart >= 0) {
+            String beforeSC = fullText.substring(0, scStart);
+            Pattern pctP = Pattern.compile("(\\d+)%");
+            Matcher pctM = pctP.matcher(beforeSC);
+            List<String> pcts = new ArrayList<>();
+            while (pctM.find()) {
+                pcts.add(pctM.group(1));
+            }
+            String[] scDims = {"行为表现", "能力与学校", "躯体外貌", "情绪状态", "合群", "幸福与满足"};
+            String[] scCodes = {"sc_behavior", "sc_school", "sc_appearance", "sc_emotion", "sc_sociability", "sc_happiness"};
+            int startIdx = Math.max(0, pcts.size() - 12);
+            for (int i = 0; i < scDims.length; i++) {
+                int idx = startIdx + i * 2 + 1;
+                if (idx < pcts.size()) {
+                    items.add(new DataItem(scCodes[i], "自我概念_" + scDims[i], pcts.get(idx), "selfConcept"));
+                    extra.put(scCodes[i], pcts.get(idx));
+                }
+            }
+        }
+
+        // 通用正则提取
+        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("C1", "campus", items, extra, summary.toString(), suggestions.toString());
+    }
+
+    /**
+     * 从文本中提取标签后的分数 (格式: "行为表现 9分")
+     */
+    private String extractScoreWithLabel(String fullText, String label) {
+        Pattern p = Pattern.compile(Pattern.quote(label) + "\\s*(\\d+)\\s*分");
+        Matcher m = p.matcher(fullText);
+        if (m.find()) {
+            return m.group(1);
+        }
+        return null;
+    }
+
+    /**
+     * 儿童行为维度的英文关键词映射
+     */
+    private String getBehaviorEnglishPattern(String cnName) {
+        switch (cnName) {
+            case "品行问题": return "Conduct problems";
+            case "情绪问题": return "Emotional state";
+            case "学习问题": return "Learning situation";
+            case "社交问题": return "Social situation";
+            case "生活习惯": return "Habits.*?customs";
+            case "多动倾向": return "Hyperactivity";
+            case "刻板行为": return "Stereotypic";
+            case "拖延行为": return "Procrastination";
+            default: return null;
+        }
+    }
+
+    /**
+     * 提取总分和百分位(通用,所有报告类型共用)
+     * PDF格式: "112\n总得分\nTotal Score\n79\n百分位(%)"
+     */
+    private Map<String, String> extractTotalScoreAndPercentile(String fullText) {
+        Map<String, String> result = new LinkedHashMap<>();
+        if (fullText == null) return result;
+
+        // 主格式: 数字\n总得分
+        Pattern scoreP = Pattern.compile("(\\d+)\\s*\\n\\s*总得分");
+        Matcher scoreM = scoreP.matcher(fullText);
+        if (scoreM.find()) {
+            result.put("总分", scoreM.group(1));
+        }
+
+        // 百分位: 总得分...数字...百分位
+        Pattern pctP = Pattern.compile("总得分.*?(\\d+)\\s*百分位", Pattern.DOTALL);
+        Matcher pctM = pctP.matcher(fullText);
+        if (pctM.find()) {
+            result.put("百分位", pctM.group(1));
+        }
+
+        // 后备1: 旧版 "总分" 格式
+        if (!result.containsKey("总分")) {
+            Pattern fallbackP = Pattern.compile("总分[^\\d]*(\\d+)");
+            Matcher fallbackM = fallbackP.matcher(fullText);
+            if (fallbackM.find()) {
+                result.put("总分", fallbackM.group(1));
+            }
+        }
+
+        // 后备2: 旧版 "百分位" 格式
+        if (!result.containsKey("百分位")) {
+            Pattern fallbackP2 = Pattern.compile("百分位[^\\d]*(\\d+)");
+            Matcher fallbackM2 = fallbackP2.matcher(fullText);
+            if (fallbackM2.find()) {
+                result.put("百分位", fallbackM2.group(1));
+            }
+        }
+
+        return result;
+    }
+
+    /**
+     * 提取标签后的分数值。支持格式:
+     * - "深层动机\n我的得分:8分"
+     * - "深层动机 8分"
+     */
+    private String extractScoreAfterLabel(String fullText, String label) {
+        // 方法1: "深层动机...我的得分:8分"
+        Pattern p1 = Pattern.compile(
+                Pattern.quote(label) + ".*?我的得分[::]\\s*(\\d+(?:\\.\\d+)?)",
+                Pattern.DOTALL);
+        Matcher m1 = p1.matcher(fullText);
+        if (m1.find()) {
+            return m1.group(1);
+        }
+        // 方法2: "深层动机 8分"
+        Pattern p2 = Pattern.compile(
+                Pattern.quote(label) + "\\s*(\\d+(?:\\.\\d+)?)\\s*分");
+        Matcher m2 = p2.matcher(fullText);
+        if (m2.find()) {
+            return m2.group(1);
+        }
+        return null;
+    }
+
     // ======================== 解析辅助方法 ========================
 
     /**

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

@@ -53,8 +53,11 @@ public class DanReportUploadService {
         if (file == null || file.isEmpty()) {
             return Result.error("请选择文件");
         }
-        if (!DanReportUpload.DIM_MIND.equals(dimension) && !DanReportUpload.DIM_WISDOM.equals(dimension)) {
-            return Result.error("dimension 必须为 mind 或 wisdom");
+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");
         }
 
         // 1. 保存文件

+ 1 - 1
cfc-backend/src/main/resources/schema.sql

@@ -1109,7 +1109,7 @@ CREATE TABLE IF NOT EXISTS product_orders (
     money_amount INT DEFAULT 0 COMMENT '现金支付金额(分)',
     consignee_id BIGINT COMMENT '收货人ID',
     purchase_info TEXT COMMENT '购买信息JSON: {id_card, hand_signature, ...}',
-    provision_status VARCHAR(32) COMMENT '活动provision状态: unprovisioned(待审核)/provisioned(已发货)/signed_in(已签到)/completed(已完成)/cancelled(审核拒绝)',
+provision_status VARCHAR(32) COMMENT '活动provision状态: unprovisioned(待审核)/provisioned(已发货)/signed_in(已签到)/completed(已完成)/cancelled(审核拒绝)',
     auditor_id BIGINT COMMENT '审核人ID',
     audited_at DATETIME COMMENT '审核时间',
     audit_reason TEXT COMMENT '审核拒绝原因',

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

@@ -0,0 +1,420 @@
+package com.etotem.cfc.service;
+
+import com.etotem.cfc.service.DanReportParseService.DanParsedReport;
+import com.etotem.cfc.service.DanReportParseService.DataItem;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+/**
+ * DanReportParseService 纯单元测试(无 SpringBootTest,避免 MySQL 连接)
+ *
+ * 测试范围:
+ * - A1 (cognition) 解析:6认知维度提取
+ * - B3 (learning) 解析:执行功能/学习动机/学习策略提取
+ * - A2 (mind) 回归:确保已有解析不受影响
+ * - B4 (wisdom) 回归:确保已有解析不受影响
+ */
+class DanReportParseServiceTest {
+
+    private DanReportParseService service;
+
+    @BeforeEach
+    void setUp() {
+        service = new DanReportParseService();
+    }
+
+    // ==================== A1 儿童核心认知 ====================
+
+    @Test
+    void testParseA1_emptyText_returnsEmpty() {
+        DanParsedReport result = service.parseText("", "cognition");
+        assertTrue(result.isEmpty(), "空文本应返回空结果");
+        assertEquals("A1", result.getReportType());
+        assertEquals("cognition", result.getDimension());
+    }
+
+    @Test
+    void testParseA1_extractsTotalScore() {
+        String text = "112\n总得分\nTotal Score\n79\n百分位(%)\n感知觉 | Perception\n百分位(%)\n14";
+        DanParsedReport result = service.parseText(text, "cognition");
+
+        assertFalse(result.isEmpty());
+
+        // 总分
+        DataItem totalItem = findItemByCode(result.getItems(), "total_score");
+        assertNotNull(totalItem, "应有总分项");
+        assertEquals("112", totalItem.getValue());
+    }
+
+    @Test
+    void testParseA1_shouldExtractPercentile() {
+        String text = "112\n总得分\nTotal Score\n79\n百分位(%)\n感知觉 | Perception\n百分位(%)\n14";
+        DanParsedReport result = service.parseText(text, "cognition");
+
+        DataItem pctItem = findItemByCode(result.getItems(), "percentile");
+        assertNotNull(pctItem, "应有百分位项");
+        assertEquals("79", pctItem.getValue());
+    }
+
+    @Test
+    void testParseA1_shouldExtractCognitiveDimensionFromSummaryPage() {
+        // 模拟 A1 summary 页格式: "感知觉 |  Perception\n百分位(%)\n14"
+        String text = "112\n总得分\nTotal Score\n79\n百分位(%)\n"
+                + "感知觉 |  Perception\n百分位(%)\n14\n"
+                + "注意力 |  Attention\n百分位(%)\n28\n"
+                + "记忆力 |  Memory\n百分位(%)\n42";
+
+        DanParsedReport result = service.parseText(text, "cognition");
+
+        assertFalse(result.isEmpty());
+        assertNotNull(findItemByCode(result.getItems(), "perception_pct"), "应有感知觉百分位");
+        assertNotNull(findItemByCode(result.getItems(), "attention_pct"), "应有注意力百分位");
+        assertNotNull(findItemByCode(result.getItems(), "memory_pct"), "应有记忆力百分位");
+    }
+
+    @Test
+    void testParseA1_shouldExtractCognitiveDimensionFromDetailPage() {
+        // 模拟 A1 detail 页格式: "我的感知觉得分 41  | 14%"
+        String text = "112\n总得分\nTotal Score\n79\n百分位(%)\n"
+                + "我的感知觉得分 41  | 14%\n"
+                + "我的注意力得分 55  | 28%\n"
+                + "我的记忆力得分 63  | 42%\n"
+                + "我的推理能力得分 70  | 55%\n"
+                + "我的空间能力得分 68  | 50%\n"
+                + "我的加工速度得分 72  | 60%";
+
+        DanParsedReport result = service.parseText(text, "cognition");
+
+        assertFalse(result.isEmpty());
+
+        // 验证所有6个维度都有得分和百分位
+        String[] codes = {"perception", "attention", "memory", "reasoning", "spatial", "processingSpeed"};
+        for (String code : codes) {
+            assertNotNull(findItemByCode(result.getItems(), code + "_score"),
+                    "应有" + code + "_score");
+            assertNotNull(findItemByCode(result.getItems(), code + "_pct"),
+                    "应有" + code + "_pct");
+        }
+
+        // 验证具体值
+        DataItem perceptionScore = findItemByCode(result.getItems(), "perception_score");
+        assertEquals("41", perceptionScore.getValue());
+    }
+
+    @Test
+    void testParseA1_shouldFallbackToGenericPattern() {
+        // 模拟 A1 fallback 格式: "感知觉\n百分位(%)\n14"
+        String text = "总分 112\n百分位 79\n感知觉\n百分位(%)\n14\n注意力\n百分位(%)\n28\n记忆力\n百分位(%)\n42\n推理能力\n百分位(%)\n55\n空间能力\n百分位(%)\n50\n加工速度\n百分位(%)\n60";
+
+        DanParsedReport result = service.parseText(text, "cognition");
+
+        assertFalse(result.isEmpty());
+        // 应通过 fallback 逻辑提取到
+        assertNotNull(findItemByCode(result.getItems(), "perception_pct"), "应通过fallback提取感知觉百分位");
+    }
+
+    // ==================== B3 核心学习能力 ====================
+
+    @Test
+    void testParseB3_emptyText_returnsEmpty() {
+        DanParsedReport result = service.parseText("", "learning");
+        assertTrue(result.isEmpty());
+        assertEquals("B3", result.getReportType());
+        assertEquals("learning", result.getDimension());
+    }
+
+    @Test
+    void testParseB3_shouldExtractExecutiveFunction() {
+        String text = "112\n总得分\nTotal Score\n79\n百分位(%)\n"
+                + "抑制控制 94%  工作记忆 89%  认知灵活性 60%";
+
+        DanParsedReport result = service.parseText(text, "learning");
+
+        assertFalse(result.isEmpty());
+        DataItem ic = findItemByCode(result.getItems(), "inhibitoryControl");
+        assertNotNull(ic, "应有抑制控制");
+        assertEquals("94", ic.getValue());
+
+        DataItem wm = findItemByCode(result.getItems(), "workingMemory");
+        assertNotNull(wm, "应有工作记忆");
+        assertEquals("89", wm.getValue());
+
+        DataItem cf = findItemByCode(result.getItems(), "cognitiveFlexibility");
+        assertNotNull(cf, "应有认知灵活性");
+        assertEquals("60", cf.getValue());
+    }
+
+    @Test
+    void testParseB3_shouldExtractLearningMotivation() {
+        String text = "112\n总得分\nTotal Score\n79\n百分位(%)\n"
+                + "抑制控制 94%\n"
+                + "深层动机\n我的得分:8分\n"
+                + "表面动机\n我的得分:3分\n"
+                + "自我效能感\n我的得分:7分";
+
+        DanParsedReport result = service.parseText(text, "learning");
+
+        assertFalse(result.isEmpty());
+        DataItem dm = findItemByCode(result.getItems(), "deepMotivation");
+        assertNotNull(dm, "应有深层动机");
+        assertEquals("8", dm.getValue());
+    }
+
+    @Test
+    void testParseB3_shouldExtractLearningStrategy() {
+        String text = "112\n总得分\nTotal Score\n79\n百分位(%)\n"
+                + "深层方法与策略\n我的得分:6.8分\n"
+                + "表面方法与策略\n我的得分:4.2分\n"
+                + "学习自我调节\n我的得分:7.5分";
+
+        DanParsedReport result = service.parseText(text, "learning");
+
+        assertFalse(result.isEmpty());
+        DataItem ds = findItemByCode(result.getItems(), "deepStrategy");
+        assertNotNull(ds, "应有深层方法与策略");
+        assertEquals("6.8", ds.getValue());
+    }
+
+    @Test
+    void testParseB3_shouldExtractAllDimensions() {
+        String text = "112\n总得分\nTotal Score\n79\n百分位(%)\n"
+                + "抑制控制 94%  工作记忆 89%  认知灵活性 60%\n"
+                + "深层动机\n我的得分:8分\n"
+                + "表面动机\n我的得分:3分\n"
+                + "自我效能感\n我的得分:7分\n"
+                + "深层方法与策略\n我的得分:6.8分\n"
+                + "表面方法与策略\n我的得分:4.2分\n"
+                + "学习自我调节\n我的得分:7.5分";
+
+        DanParsedReport result = service.parseText(text, "learning");
+
+        assertFalse(result.isEmpty());
+
+        // 执行功能3项
+        assertNotNull(findItemByCode(result.getItems(), "inhibitoryControl"));
+        assertNotNull(findItemByCode(result.getItems(), "workingMemory"));
+        assertNotNull(findItemByCode(result.getItems(), "cognitiveFlexibility"));
+
+        // 学习动机3项
+        assertNotNull(findItemByCode(result.getItems(), "deepMotivation"));
+        assertNotNull(findItemByCode(result.getItems(), "surfaceMotivation"));
+        assertNotNull(findItemByCode(result.getItems(), "selfEfficacy"));
+
+        // 学习策略3项
+        assertNotNull(findItemByCode(result.getItems(), "deepStrategy"));
+        assertNotNull(findItemByCode(result.getItems(), "surfaceStrategy"));
+        assertNotNull(findItemByCode(result.getItems(), "selfRegulation"));
+
+        // 总分和百分位
+        assertNotNull(findItemByCode(result.getItems(), "total_score"));
+        assertNotNull(findItemByCode(result.getItems(), "percentile"));
+    }
+
+    @Test
+    void testParseB3_shouldHandleInlineScoreFormat() {
+        // 处理 "深层动机 8分" 格式(没有"我的得分")
+        String text = "112\n总得分\nTotal Score\n79\n百分位(%)\n"
+                + "深层动机 8分\n"
+                + "表面动机 3分\n"
+                + "自我效能感 7分";
+
+        DanParsedReport result = service.parseText(text, "learning");
+
+        assertFalse(result.isEmpty());
+        DataItem dm = findItemByCode(result.getItems(), "deepMotivation");
+        assertNotNull(dm, "应有深层动机");
+        assertEquals("8", dm.getValue());
+    }
+
+    // ==================== 回归测试 ====================
+
+    @Test
+    void testParseA2_regression() {
+        // A2 解析仍正常工作
+        String text = "测试数据\n测评日期:2024-01-01\n"
+                + "大五人格\n开放性 7.5分\n尽责性 8.0分";
+
+        DanParsedReport result = service.parseText(text, "mind");
+
+        assertFalse(result.isEmpty(), "A2 解析应正常");
+        assertEquals("A2", result.getReportType());
+    }
+
+    @Test
+    void testParseB4_regression() {
+        // B4 解析仍正常工作
+        String text = "测试数据\n测评日期:2024-01-01\n"
+                + "自我概念\n自信心 7分\n自尊心 8分";
+
+        DanParsedReport result = service.parseText(text, "wisdom");
+
+        assertFalse(result.isEmpty(), "B4 解析应正常");
+        assertEquals("B4", result.getReportType());
+    }
+
+    @Test
+    void testParse_unknownDimension_returnsEmpty() {
+        DanParsedReport result = service.parseText("some text", "unknown");
+        assertTrue(result.isEmpty());
+    }
+
+    @Test
+    void testParseA1_nonNumericText_returnsEmpty() {
+        DanParsedReport result = service.parseText("纯中文文本 没有数字", "cognition");
+        assertNotNull(result);
+    }
+
+    // ==================== B2 儿童自我与家庭教养 ====================
+
+    @Test
+    void testParseB2_emptyText_returnsEmpty() {
+        DanParsedReport result = service.parseText("", "behavior");
+        assertTrue(result.isEmpty());
+        assertEquals("B2", result.getReportType());
+        assertEquals("behavior", result.getDimension());
+    }
+
+    @Test
+    void testParseB2_shouldExtractSelfConcept() {
+        String text = "112\n总得分\nTotal Score\n79\n百分位(%)\n"
+                + "行为表现 9分\n能力与学校表现 8分\n躯体外貌 7分\n"
+                + "情绪状态 6分\n合群 8分\n幸福与满足 9分";
+
+        DanParsedReport result = service.parseText(text, "behavior");
+        assertFalse(result.isEmpty());
+        assertNotNull(findItemByCode(result.getItems(), "selfConcept_behavior"), "应有自我概念_行为表现");
+        assertNotNull(findItemByCode(result.getItems(), "selfConcept_school"), "应有自我概念_能力与学校");
+        assertNotNull(findItemByCode(result.getItems(), "selfConcept_happiness"), "应有自我概念_幸福与满足");
+    }
+
+    @Test
+    void testParseB2_shouldExtractBehavior() {
+        String text = "总得分 112\n品行问题 1分 优秀\n情绪问题 2分 良好\n学习问题 3分 一般\n"
+                + "Emotional state 2分\nConduct problems 1分";
+
+        DanParsedReport result = service.parseText(text, "behavior");
+        assertFalse(result.isEmpty());
+
+        // 中文优先匹配
+        DataItem conduct = findItemByCode(result.getItems(), "conduct");
+        assertNotNull(conduct, "应有品行问题");
+    }
+
+    @Test
+    void testParseB2_shouldExtractFamilyEnvironment() {
+        String text = "总得分 112\n亲密 9分\n情感表达 7分\n和谐 5分\n"
+                + "独立性 8分\n成就向导 6分\n文化氛围 7分\n娱乐活动 8分\n"
+                + "道德观念 9分\n家务安排 6分\n家庭规则 7分";
+
+        DanParsedReport result = service.parseText(text, "behavior");
+
+        assertFalse(result.isEmpty());
+        assertNotNull(findItemByCode(result.getItems(), "family_intimacy"), "应有家庭_亲密");
+        assertNotNull(findItemByCode(result.getItems(), "family_rules"), "应有家庭_家庭规则");
+    }
+
+    // ==================== B6 职业发展 ====================
+
+    @Test
+    void testParseB6_emptyText_returnsEmpty() {
+        DanParsedReport result = service.parseText("", "career");
+        assertTrue(result.isEmpty());
+        assertEquals("B6", result.getReportType());
+        assertEquals("career", result.getDimension());
+    }
+
+    @Test
+    void testParseB6_shouldExtractHollandInterest() {
+        String text = "总得分 112\n"
+                + "NO.1: 艺术型 Artistic 7分  NO.2: 社会型 Social 5分\n";
+
+        DanParsedReport result = service.parseText(text, "career");
+        assertFalse(result.isEmpty());
+        assertNotNull(findItemByCode(result.getItems(), "interest_artistic"), "应有兴趣_艺术型");
+        assertNotNull(findItemByCode(result.getItems(), "interest_social"), "应有兴趣_社会型");
+    }
+
+    @Test
+    void testParseB6_shouldExtractMultipleIntelligence() {
+        String text = "总得分 112\n"
+                + "内省能力 INTRAPERSONAL 8分\n"
+                + "逻辑数学能力 LOGICAL 7分\n";
+
+        DanParsedReport result = service.parseText(text, "career");
+        assertFalse(result.isEmpty());
+        assertNotNull(findItemByCode(result.getItems(), "ability_intrapersonal"), "应有能力_内省");
+        assertNotNull(findItemByCode(result.getItems(), "ability_logical"), "应有能力_逻辑数学");
+    }
+
+    // ==================== C1 校园版 ====================
+
+    @Test
+    void testParseC1_emptyText_returnsEmpty() {
+        DanParsedReport result = service.parseText("", "campus");
+        assertTrue(result.isEmpty());
+        assertEquals("C1", result.getReportType());
+        assertEquals("campus", result.getDimension());
+    }
+
+    @Test
+    void testParseC1_shouldExtractCognitiveFromDetail() {
+        String text = "总得分 112\n"
+                + "我的感知觉得分 41  | 14%\n"
+                + "我的注意力得分 55  | 28%\n"
+                + "SELF-CONCEPT\n";
+
+        DanParsedReport result = service.parseText(text, "campus");
+        assertFalse(result.isEmpty());
+        assertNotNull(findItemByCode(result.getItems(), "perception_score"), "应有感知觉得分");
+        assertNotNull(findItemByCode(result.getItems(), "perception_pct"), "应有感知觉百分位");
+    }
+
+    @Test
+    void testParseC1_shouldExtractBigFive() {
+        String text = "总得分 112\n您在「开放性」上的得分是 7.5 分\n您在「宜人性」上的得分是 8.0 分\n";
+
+        DanParsedReport result = service.parseText(text, "campus");
+        assertFalse(result.isEmpty());
+        assertNotNull(findItemByCode(result.getItems(), "big5_开放性"), "应有大五_开放性");
+        assertNotNull(findItemByCode(result.getItems(), "big5_宜人性"), "应有大五_宜人性");
+    }
+
+    @Test
+    void testParseC1_shouldExtractSelfDriving() {
+        String text = "总得分 112\n我的得分:8分\n我的得分:7分\n我的得分:9分\nSELF-CONCEPT\n";
+
+        DanParsedReport result = service.parseText(text, "campus");
+        assertFalse(result.isEmpty());
+        assertNotNull(findItemByCode(result.getItems(), "autonomy"), "应有自主性");
+        assertNotNull(findItemByCode(result.getItems(), "competence"), "应有胜任感");
+        assertNotNull(findItemByCode(result.getItems(), "belonging"), "应有归属感");
+    }
+
+    @Test
+    void testParseC1_shouldExtractSelfConcept() {
+        String text = "总得分 112\n"
+                + "14% 28% 42% 55% 50% 60% 70% 65% 80% 75% 90% 85%\n"
+                + "SELF-CONCEPT\n";
+
+        DanParsedReport result = service.parseText(text, "campus");
+        assertFalse(result.isEmpty());
+        // 应能从SELF-CONCEPT前的百分位中提取自我概念
+        assertNotNull(findItemByCode(result.getItems(), "sc_behavior"), "应有自我概念_行为表现");
+    }
+
+    // ==================== 工具方法 ====================
+
+    private DataItem findItemByCode(List<DataItem> items, String code) {
+        if (items == null) return null;
+        for (DataItem item : items) {
+            if (item.getCode().equals(code)) {
+                return item;
+            }
+        }
+        return null;
+    }
+}