Przeglądaj źródła

feat(backend): DAN报告按category分组组装blocks(类型分别处理)

E2E Test Bot 2 miesięcy temu
rodzic
commit
8ec46c3e00

+ 93 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/ReportBlockAssembler.java

@@ -481,6 +481,99 @@ public class ReportBlockAssembler {
         return blocks;
     }
 
+    /** category → 中文组标题 映射(DAN 各报告类型的指标分组) */
+    private static final java.util.Map<String, String> DAN_CATEGORY_TITLES = buildDanCategoryTitles();
+
+    private static java.util.Map<String, String> buildDanCategoryTitles() {
+        java.util.Map<String, String> m = new LinkedHashMap<>();
+        m.put("bigFive", "大五人格");
+        m.put("social", "社会关系");
+        m.put("emotion", "情绪状态");
+        m.put("ability", "综合能力");
+        m.put("selfConcept", "自我概念");
+        m.put("growthMindset", "成长型思维");
+        m.put("selfDriving", "自驱力");
+        m.put("cognitive", "认知能力");
+        m.put("executiveFunction", "执行功能");
+        m.put("learningMotivation", "学习动机");
+        m.put("learningStrategy", "学习策略");
+        m.put("behavior", "行为表现");
+        m.put("family", "家庭环境");
+        m.put("hollandInterest", "职业兴趣");
+        m.put("multipleIntelligence", "多元智能");
+        m.put("careerValue", "职业价值观");
+        m.put("emotionRegulation", "情绪调节");
+        m.put("academicStress", "学业压力");
+        m.put("interpersonal", "人际关系");
+        m.put("socialAbility", "社交能力");
+        m.put("sleep", "睡眠状况");
+        m.put("exercise", "运动状况");
+        m.put("internet", "网络使用");
+        m.put("compass", "人际指南针");
+        m.put("overall", "综合评估");
+        m.put("auto", "其他指标");
+        return m;
+    }
+
+    /**
+     * DAN 报告 → blocks(按 category 分组)。
+     * score 块(总分/百分位)+ indicator 块(每个 category 一组)+ text 块(评语/建议)。
+     * 不同报告类型(A1/A2/B2/B3/B4/B5/B6/C1)的 category 集合不同,天然分别展示。
+     */
+    public List<Map<String, Object>> assembleDanByType(String itemsJson, String summary, String suggestions) {
+        List<Map<String, Object>> blocks = new ArrayList<>();
+        List<DanReportParseService.DataItem> items = new ArrayList<>();
+        if (itemsJson != null && !itemsJson.isEmpty()) {
+            try {
+                items = new com.fasterxml.jackson.databind.ObjectMapper().readValue(
+                        itemsJson,
+                        new com.fasterxml.jackson.core.type.TypeReference<List<DanReportParseService.DataItem>>() {});
+            } catch (Exception e) {
+                // 解析失败按空处理
+            }
+        }
+        // 1. score 块:total_score / percentile
+        List<Map<String, Object>> scoreItems = new ArrayList<>();
+        // 2. indicator 块:按 category 分组
+        Map<String, List<Map<String, Object>>> byCategory = new LinkedHashMap<>();
+        for (DanReportParseService.DataItem item : items) {
+            if (item.getValue() == null || item.getValue().isEmpty()) continue;
+            if ("total_score".equals(item.getCode())) {
+                scoreItems.add(scoreItem("总得分", item.getValue()));
+            } else if ("percentile".equals(item.getCode())) {
+                scoreItems.add(scoreItem("百分位", item.getValue()));
+            } else {
+                String cat = item.getCategory() == null || item.getCategory().isEmpty() ? "auto" : item.getCategory();
+                Map<String, Object> m = new LinkedHashMap<>();
+                m.put("name", item.getName());
+                m.put("value", item.getValue());
+                byCategory.computeIfAbsent(cat, k -> new ArrayList<>()).add(m);
+            }
+        }
+        if (!scoreItems.isEmpty()) {
+            Map<String, Object> block = new LinkedHashMap<>();
+            block.put("type", "score");
+            block.put("title", "测评结果");
+            block.put("items", scoreItems);
+            blocks.add(block);
+        }
+        // 按固定 category 顺序输出(保证同类型报告展示顺序稳定)
+        for (Map.Entry<String, String> e : DAN_CATEGORY_TITLES.entrySet()) {
+            List<Map<String, Object>> items2 = byCategory.get(e.getKey());
+            if (items2 != null && !items2.isEmpty()) {
+                Map<String, Object> block = new LinkedHashMap<>();
+                block.put("type", "indicator");
+                block.put("title", e.getValue());
+                block.put("items", items2);
+                blocks.add(block);
+            }
+        }
+        // text 块:summary / suggestions
+        if (summary != null && !summary.isEmpty()) blocks.add(textBlock("报告评语", summary));
+        if (suggestions != null && !suggestions.isEmpty()) blocks.add(textBlock("成长建议", suggestions));
+        return blocks;
+    }
+
     private Map<String, Object> scoreItem(String label, String value) {
         Map<String, Object> m = new LinkedHashMap<>();
         m.put("label", label);

+ 29 - 0
cfc-backend/src/test/java/com/etotem/cfc/integration/service/ReportBlockAssemblerTest.java

@@ -115,4 +115,33 @@ class ReportBlockAssemblerTest {
         assertNotNull(listBlock, "应有list块");
         assertEquals(2, ((List<Map<String, Object>>) listBlock.get("items")).size());
     }
+
+    @Test
+    void assembleDanByType_shouldGroupItemsByCategory() {
+        String itemsJson = "[{\"code\":\"total_score\",\"name\":\"总得分\",\"value\":\"112\",\"category\":\"overall\"},"
+                + "{\"code\":\"percentile\",\"name\":\"百分位\",\"value\":\"79\",\"category\":\"overall\"},"
+                + "{\"code\":\"perception_pct\",\"name\":\"感知觉百分位\",\"value\":\"14\",\"category\":\"cognitive\"},"
+                + "{\"code\":\"attention_pct\",\"name\":\"注意力百分位\",\"value\":\"28\",\"category\":\"cognitive\"},"
+                + "{\"code\":\"openness\",\"name\":\"开放性\",\"value\":\"7.5\",\"category\":\"bigFive\"}]";
+        List<Map<String, Object>> blocks = assembler.assembleDanByType(itemsJson, "综合评语", "成长建议");
+        // score 块
+        Map<String, Object> scoreBlock = blocks.stream()
+                .filter(b -> "score".equals(b.get("type"))).findFirst().orElse(null);
+        assertNotNull(scoreBlock, "应有score块");
+        assertEquals("总得分", ((List<Map<String, Object>>) scoreBlock.get("items")).get(0).get("label"));
+        // 按 category 分组为 indicator 块:cognitive 组 2 项、bigFive 组 1 项
+        @SuppressWarnings("unchecked")
+        List<Map<String, Object>> indBlocks = (List<Map<String, Object>>)(List<?>) blocks.stream()
+                .filter(b -> "indicator".equals(b.get("type"))).collect(java.util.stream.Collectors.toList());
+        assertEquals(2, indBlocks.size(), "应有 cognitive/bigFive 两个 indicator 块");
+        // DAN_CATEGORY_TITLES 插入顺序:bigFive 在前、cognitive 在后 → 固定输出顺序
+        assertEquals("大五人格", indBlocks.get(0).get("title"));
+        assertEquals(1, ((List<Map<String, Object>>) indBlocks.get(0).get("items")).size());
+        assertEquals("开放性", ((List<Map<String, Object>>) indBlocks.get(0).get("items")).get(0).get("name"));
+        assertEquals("认知能力", indBlocks.get(1).get("title"));
+        assertEquals(2, ((List<Map<String, Object>>) indBlocks.get(1).get("items")).size());
+        // text 块
+        long textCount = blocks.stream().filter(b -> "text".equals(b.get("type"))).count();
+        assertEquals(2, textCount, "应有summary和suggestions两个text块");
+    }
 }