|
|
@@ -31,14 +31,26 @@ public class DanReportParseService {
|
|
|
PDFTextStripper stripper = new PDFTextStripper();
|
|
|
stripper.setSortByPosition(true);
|
|
|
String fullText = stripper.getText(document);
|
|
|
- return parseText(fullText, dimension);
|
|
|
+
|
|
|
+ if ("adolescent".equals(dimension)) {
|
|
|
+ java.awt.image.BufferedImage compassImage = extractB5CompassImage(document);
|
|
|
+ return parseText(fullText, dimension, compassImage);
|
|
|
+ }
|
|
|
+ return parseText(fullText, dimension, null);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * 从纯文本解析 DAN 报告
|
|
|
+ * 从纯文本解析 DAN 报告(无图像版本)
|
|
|
*/
|
|
|
public DanParsedReport parseText(String fullText, String dimension) {
|
|
|
+ return parseText(fullText, dimension, null);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 从纯文本解析 DAN 报告(含可选图像用于B5指南针分析)
|
|
|
+ */
|
|
|
+ private DanParsedReport parseText(String fullText, String dimension, java.awt.image.BufferedImage compassImage) {
|
|
|
String[] lines = fullText.split("\\r?\\n");
|
|
|
List<String> lineList = new ArrayList<>();
|
|
|
for (String line : lines) {
|
|
|
@@ -56,6 +68,18 @@ 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);
|
|
|
+ } else if ("adolescent".equals(dimension)) {
|
|
|
+ return parseB5Report(lineList, fullText, compassImage);
|
|
|
}
|
|
|
return DanParsedReport.empty();
|
|
|
}
|
|
|
@@ -235,6 +259,908 @@ 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());
|
|
|
+ }
|
|
|
+
|
|
|
+ // ======================== B5 报告解析(青春期/adolescent) ========================
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 解析 B5 青春期挑战报告(文本部分,不含指南针图像分析)
|
|
|
+ * - 情绪调节策略2维:认知重评、表达抑制
|
|
|
+ * - 学业压力源5维:学业负担、家庭期望、师生关系、自我期望、同伴竞争
|
|
|
+ * - 人际关系问题4维:母亲关系、父亲关系、师生关系、同伴关系
|
|
|
+ * - 社交能力5维:主动交往、情感支持、情感表达、表达影响、冲突解决
|
|
|
+ * - 睡眠数据:时长、效率、入睡时间、主观质量、日间功能、催眠药物
|
|
|
+ * - 运动数据:时长、强度
|
|
|
+ * - 网络依赖数据
|
|
|
+ */
|
|
|
+ private DanParsedReport parseB5Report(List<String> lines, String fullText, java.awt.image.BufferedImage compassImage) {
|
|
|
+ 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));
|
|
|
+ }
|
|
|
+
|
|
|
+ // 7. 人际关系指南针图19个子维度分数(第9页图像分析)
|
|
|
+ if (compassImage != null) {
|
|
|
+ try {
|
|
|
+ Map<String, Double> compassScores = extractB5CompassScores(compassImage, fullText);
|
|
|
+ for (Map.Entry<String, Double> entry : compassScores.entrySet()) {
|
|
|
+ items.add(new DataItem(
|
|
|
+ entry.getKey().replace('_', '.'),
|
|
|
+ "人际指南针_" + entry.getKey(),
|
|
|
+ String.valueOf(entry.getValue()),
|
|
|
+ "compass"));
|
|
|
+ extra.put(entry.getKey(), entry.getValue());
|
|
|
+ }
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.warn("B5指南针图像提取失败,跳过: {}", e.getMessage());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 通用正则提取
|
|
|
+ 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分")
|
|
|
+ */
|
|
|
+ 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;
|
|
|
+ }
|
|
|
+
|
|
|
// ======================== 解析辅助方法 ========================
|
|
|
|
|
|
/**
|
|
|
@@ -512,6 +1438,177 @@ public class DanReportParseService {
|
|
|
return "";
|
|
|
}
|
|
|
|
|
|
+ // ======================== B5 指南针图像分析常量 ========================
|
|
|
+
|
|
|
+ /** 象限角度范围 (图像坐标系: 0°=右, 90°=下, 180°=左, 270°=上) */
|
|
|
+ private static final Map<String, int[]> B5_QUAD_ANGLES = new LinkedHashMap<String, int[]>() {{
|
|
|
+ put("母亲关系", new int[]{185, 265});
|
|
|
+ put("父亲关系", new int[]{140, 178});
|
|
|
+ put("师生关系", new int[]{275, 350});
|
|
|
+ put("同伴关系", new int[]{2, 42});
|
|
|
+ }};
|
|
|
+
|
|
|
+ /** 每度fill到分数的线性映射系数 */
|
|
|
+ private static final Map<String, Double> B5_QUAD_FACTORS = new LinkedHashMap<String, Double>() {{
|
|
|
+ put("母亲关系", 0.006974);
|
|
|
+ put("父亲关系", 0.005834);
|
|
|
+ put("师生关系", 0.006820);
|
|
|
+ put("同伴关系", 0.005049);
|
|
|
+ }};
|
|
|
+
|
|
|
+ /** 指南针中心坐标 */
|
|
|
+ private static final int B5_CENTER_X = 857;
|
|
|
+ private static final int B5_CENTER_Y = 758;
|
|
|
+
|
|
|
+ /** 各象限的子维度名称 */
|
|
|
+ private static final Map<String, String[]> B5_SUB_DIMS = new LinkedHashMap<String, String[]>() {{
|
|
|
+ put("母亲关系", new String[]{"控制压迫", "情感疏离", "价值冲突", "双重标准", "过度期待"});
|
|
|
+ put("父亲关系", new String[]{"控制压迫", "情感疏离", "价值冲突", "双重标准", "过度期待"});
|
|
|
+ put("师生关系", new String[]{"回避型", "敌对型", "高控制型", "关系疏离型"});
|
|
|
+ put("同伴关系", new String[]{"回避型", "被排斥型", "攻击型", "边缘型", "特殊因素型"});
|
|
|
+ }};
|
|
|
+
|
|
|
+ // ======================== B5 指南针图像分析方法 ========================
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 从B5 PDF文档第9页提取人际关系指南针图像
|
|
|
+ * 返回PNG图像解析出的BufferedImage,不含数据则表示未找到图像
|
|
|
+ */
|
|
|
+ private java.awt.image.BufferedImage extractB5CompassImage(PDDocument document) {
|
|
|
+ try {
|
|
|
+ if (document.getNumberOfPages() < 9) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ org.apache.pdfbox.pdmodel.PDPage page = document.getPage(8);
|
|
|
+ org.apache.pdfbox.pdmodel.PDResources resources = page.getResources();
|
|
|
+ if (resources == null) return null;
|
|
|
+
|
|
|
+ int imgIndex = 0;
|
|
|
+ for (org.apache.pdfbox.cos.COSName name : resources.getXObjectNames()) {
|
|
|
+ org.apache.pdfbox.pdmodel.graphics.PDXObject xobj = resources.getXObject(name);
|
|
|
+ if (xobj instanceof org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject) {
|
|
|
+ if (imgIndex == 3) {
|
|
|
+ org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject img =
|
|
|
+ (org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject) xobj;
|
|
|
+ return img.getImage();
|
|
|
+ }
|
|
|
+ imgIndex++;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.warn("B5指南针图像提取失败: {}", e.getMessage());
|
|
|
+ }
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 沿指定角度测量填充范围(像素半径)
|
|
|
+ * 从r=150开始扫描跳过中心色块, 25px间隙容忍用于跨越填充分区内的环线
|
|
|
+ */
|
|
|
+ private Integer measureFillAtAngle(java.awt.image.BufferedImage img, int cx, int cy, double angleDeg) {
|
|
|
+ double angle = Math.toRadians(angleDeg);
|
|
|
+ double cosA = Math.cos(angle);
|
|
|
+ double sinA = Math.sin(angle);
|
|
|
+ int w = img.getWidth();
|
|
|
+ int h = img.getHeight();
|
|
|
+ int maxR = Math.min(Math.min(w - cx, cx), Math.min(cy, h - cy)) - 2;
|
|
|
+ Integer fillEnd = null;
|
|
|
+ int gap = 0;
|
|
|
+
|
|
|
+ for (int r = 150; r < maxR; r++) {
|
|
|
+ int x = cx + (int)(r * cosA);
|
|
|
+ int y = cy + (int)(r * sinA);
|
|
|
+ if (x < 0 || x >= w || y < 0 || y >= h) break;
|
|
|
+
|
|
|
+ int rgb = img.getRGB(x, y);
|
|
|
+ int red = (rgb >> 16) & 0xFF;
|
|
|
+ int green = (rgb >> 8) & 0xFF;
|
|
|
+ int blue = rgb & 0xFF;
|
|
|
+
|
|
|
+ boolean isWhite = red > 230 && green > 230 && blue > 230;
|
|
|
+ boolean isGray = red > 155 && red < 195 && green > 155 && green < 195 && blue > 155 && blue < 195;
|
|
|
+ boolean isBlack = red < 20 && green < 20 && blue < 20;
|
|
|
+ boolean isColored = !(isWhite || isGray || isBlack);
|
|
|
+
|
|
|
+ if (isColored) {
|
|
|
+ fillEnd = r;
|
|
|
+ gap = 0;
|
|
|
+ } else if (fillEnd != null) {
|
|
|
+ gap++;
|
|
|
+ if (gap > 25) break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return fillEnd;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 从B5指南针图像提取19个子维度分数
|
|
|
+ * 返回 { "人际_母亲关系_控制压迫": score, ... }
|
|
|
+ */
|
|
|
+ private Map<String, Double> extractB5CompassScores(
|
|
|
+ java.awt.image.BufferedImage img, String fullText) {
|
|
|
+ Map<String, Double> result = new LinkedHashMap<>();
|
|
|
+ if (img == null) return result;
|
|
|
+
|
|
|
+ int cx = B5_CENTER_X;
|
|
|
+ int cy = B5_CENTER_Y;
|
|
|
+
|
|
|
+ // 计算每象限的per-student校准因子
|
|
|
+ Map<String, Double> quadFactors = new LinkedHashMap<>();
|
|
|
+ for (Map.Entry<String, int[]> entry : B5_QUAD_ANGLES.entrySet()) {
|
|
|
+ String qname = entry.getKey();
|
|
|
+ int a1 = entry.getValue()[0];
|
|
|
+ int a2 = entry.getValue()[1];
|
|
|
+
|
|
|
+ List<Integer> fills = new ArrayList<>();
|
|
|
+ for (int a = a1; a <= a2; a++) {
|
|
|
+ Integer fe = measureFillAtAngle(img, cx, cy, a);
|
|
|
+ if (fe != null) fills.add(fe);
|
|
|
+ }
|
|
|
+ double avg = fills.isEmpty() ? 1.0
|
|
|
+ : fills.stream().mapToInt(Integer::intValue).average().orElse(1.0);
|
|
|
+
|
|
|
+ // 尝试从第9页文本提取关系维度的总分作为校准基准
|
|
|
+ double txtScore = 3.0;
|
|
|
+ Pattern p = Pattern.compile(Pattern.quote(qname) + "\\s*\\n\\s*(\\d+(?:\\.\\d+)?)");
|
|
|
+ Matcher m = p.matcher(fullText);
|
|
|
+ if (m.find()) {
|
|
|
+ try { txtScore = Double.parseDouble(m.group(1)); } catch (NumberFormatException ignored) {}
|
|
|
+ }
|
|
|
+
|
|
|
+ double factor = avg > 0 ? txtScore / avg : B5_QUAD_FACTORS.getOrDefault(qname, 0.005);
|
|
|
+ quadFactors.put(qname, factor);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 提取各象限的子维度分数
|
|
|
+ for (Map.Entry<String, int[]> entry : B5_QUAD_ANGLES.entrySet()) {
|
|
|
+ String quadName = entry.getKey();
|
|
|
+ int a1 = entry.getValue()[0];
|
|
|
+ int a2 = entry.getValue()[1];
|
|
|
+ String[] dims = B5_SUB_DIMS.get(quadName);
|
|
|
+ if (dims == null) continue;
|
|
|
+
|
|
|
+ int n = dims.length;
|
|
|
+ double step = (a2 - a1) / (double)(n + 1);
|
|
|
+ double factor = quadFactors.getOrDefault(quadName,
|
|
|
+ B5_QUAD_FACTORS.getOrDefault(quadName, 0.005));
|
|
|
+
|
|
|
+ for (int i = 0; i < n; i++) {
|
|
|
+ double angle = a1 + (i + 1) * step;
|
|
|
+ List<Integer> fills = new ArrayList<>();
|
|
|
+ for (int offset = -2; offset <= 2; offset += 2) {
|
|
|
+ Integer fe = measureFillAtAngle(img, cx, cy, angle + offset);
|
|
|
+ if (fe != null) fills.add(fe);
|
|
|
+ }
|
|
|
+ double avgFill = fills.isEmpty() ? 0
|
|
|
+ : fills.stream().mapToInt(Integer::intValue).average().orElse(0);
|
|
|
+ double score = Math.min(5.0, Math.max(1.0, Math.round(avgFill * factor * 10.0) / 10.0));
|
|
|
+ result.put("人际_" + quadName + "_" + dims[i], score);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
// ======================== 内部数据类 ========================
|
|
|
|
|
|
/**
|