Parcourir la source

feat(self-check): Service 层抽题/状态/忽略/计分改造

iwt il y a 2 semaines
Parent
commit
843ecc30ce

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

@@ -37,5 +37,8 @@ public class FiveDimensionSelfCheck implements Serializable {
     /** 寻源建议JSON(五行相生寻源引擎输出) */
     private String adviceJson;
 
+    /** 本次自检使用的题号JSON,如 [1,2,3,101] */
+    private String questionIdsJson;
+
     private Date createdAt;
 }

+ 180 - 16
cfc-backend/src/main/java/com/etotem/cfc/service/FiveDimensionSelfCheckService.java

@@ -9,6 +9,8 @@ import com.etotem.cfc.entity.FamilyMember;
 import com.etotem.cfc.entity.FiveDimensionSelfCheck;
 import com.etotem.cfc.mapper.FamilyMemberMapper;
 import com.etotem.cfc.mapper.FiveDimensionSelfCheckMapper;
+import com.etotem.cfc.entity.SelfCheckIgnore;
+import com.etotem.cfc.mapper.SelfCheckIgnoreMapper;
 import com.fasterxml.jackson.core.JsonProcessingException;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import org.slf4j.Logger;
@@ -20,7 +22,9 @@ import javax.annotation.Resource;
 import java.math.BigDecimal;
 import java.math.RoundingMode;
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.Date;
+import java.util.HashMap;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
@@ -50,6 +54,9 @@ public class FiveDimensionSelfCheckService {
     @Resource
     private OnboardingService onboardingService;
 
+    @Resource
+    private SelfCheckIgnoreMapper ignoreMapper;
+
     private final ObjectMapper objectMapper = new ObjectMapper();
 
     /** 维度中文名与五行 */
@@ -68,6 +75,58 @@ public class FiveDimensionSelfCheckService {
     /** 子维度附加题库(行6维 + 心·自驱力/自我概念 + 智·成长型思维 + 富·家族传承) */
     private static final List<SelfCheckQuestionVO> SUB_DIMENSION_QUESTION_BANK = buildSubDimensionQuestionBank();
 
+    /** 合并题库:主题库(15题) + 子维度附加库(19题),用于轮换抽题 */
+    private static final List<SelfCheckQuestionVO> FULL_QUESTION_POOL = buildFullPool();
+
+    @SuppressWarnings("unchecked")
+    private static List<SelfCheckQuestionVO> buildFullPool() {
+        List<SelfCheckQuestionVO> pool = new ArrayList<>(QUESTION_BANK);
+        pool.addAll(SUB_DIMENSION_QUESTION_BANK);
+        return pool;
+    }
+
+    /** 按 dimension 分组的合并题库 Map<dimension, List<SelfCheckQuestionVO>> */
+    private Map<String, List<SelfCheckQuestionVO>> groupByDimension(List<SelfCheckQuestionVO> pool) {
+        Map<String, List<SelfCheckQuestionVO>> groups = new LinkedHashMap<>();
+        for (SelfCheckQuestionVO q : pool) {
+            String dim = q.getDimension();
+            if (dim == null) continue;
+            groups.computeIfAbsent(dim, k -> new ArrayList<>()).add(q);
+        }
+        return groups;
+    }
+
+    /** 随机抽题:从 pool 中抽 count 题,优先排除 excludeIds */
+    private List<SelfCheckQuestionVO> pickQuestions(List<SelfCheckQuestionVO> pool, List<Integer> excludeIds, int count) {
+        List<Integer> excludeSet = new ArrayList<>(excludeIds != null ? excludeIds : Collections.emptyList());
+        List<SelfCheckQuestionVO> candidates = new ArrayList<>();
+        for (SelfCheckQuestionVO q : pool) {
+            if (!excludeSet.contains(q.getId())) {
+                candidates.add(q);
+            }
+        }
+        // 洗牌
+        Collections.shuffle(candidates);
+        List<SelfCheckQuestionVO> result = new ArrayList<>();
+        int need = Math.min(count, candidates.size());
+        for (int i = 0; i < need; i++) {
+            result.add(candidates.get(i));
+        }
+        // 不足时从 excludeSet 补足(保持顺序)
+        if (result.size() < count) {
+            for (SelfCheckQuestionVO q : pool) {
+                if (result.size() >= count) break;
+                if (excludeSet.contains(q.getId())) continue;
+                boolean alreadyIn = false;
+                for (SelfCheckQuestionVO r : result) {
+                    if (r.getId().equals(q.getId())) { alreadyIn = true; break; }
+                }
+                if (!alreadyIn) result.add(q);
+            }
+        }
+        return result;
+    }
+
     private static List<SelfCheckQuestionVO> buildQuestionBank() {
         List<SelfCheckQuestionVO> bank = new ArrayList<>();
         // 身(1-3)
@@ -228,6 +287,42 @@ public class FiveDimensionSelfCheckService {
         return QUESTION_BANK;
     }
 
+    /**
+     * 返回轮换后的题库(用于再次自检):
+     * 合并题库按维度分组,每维度随机抽 3 题,优先排除上次使用的题号。
+     * body 维度仅 3 题,若排除后不足 3 题则允许复用。
+     */
+    public List<SelfCheckQuestionVO> getQuestionsForRetake(Long userId) {
+        // 查最近一次自检的题号
+        LambdaQueryWrapper<FiveDimensionSelfCheck> wrapper = new LambdaQueryWrapper<FiveDimensionSelfCheck>()
+                .eq(FiveDimensionSelfCheck::getUserId, userId)
+                .orderByDesc(FiveDimensionSelfCheck::getCreatedAt)
+                .last("LIMIT 1");
+        SortUtil.applySort(wrapper);
+        FiveDimensionSelfCheck lastRecord = selfCheckMapper.selectOne(wrapper);
+        List<Integer> lastQuestionIds = parseQuestionIds(lastRecord != null ? lastRecord.getQuestionIdsJson() : null);
+
+        // 按维度分组抽题
+        Map<String, List<SelfCheckQuestionVO>> dimGroups = groupByDimension(FULL_QUESTION_POOL);
+        String[] dimOrder = {"body", "wisdom", "wealth", "action", "mind"};
+        List<SelfCheckQuestionVO> result = new ArrayList<>();
+        for (String dim : dimOrder) {
+            List<SelfCheckQuestionVO> pool = dimGroups.getOrDefault(dim, Collections.emptyList());
+            result.addAll(pickQuestions(pool, lastQuestionIds, 3));
+        }
+        return result;
+    }
+
+    private List<Integer> parseQuestionIds(String json) {
+        if (json == null || json.isEmpty()) return Collections.emptyList();
+        try {
+            return objectMapper.readValue(json, objectMapper.getTypeFactory().constructCollectionType(List.class, Integer.class));
+        } catch (Exception e) {
+            log.warn("解析 questionIdsJson 失败: {}", e.getMessage());
+            return Collections.emptyList();
+        }
+    }
+
     /** 获取子维度附加题库 */
     public List<SelfCheckQuestionVO> getSubDimensionQuestions() {
         return SUB_DIMENSION_QUESTION_BANK;
@@ -345,39 +440,38 @@ public class FiveDimensionSelfCheckService {
             answerMap.put(item.getQuestionId(), item.getAnswer().trim().toUpperCase());
         }
 
-        // 逐维度计分(A=3,B=2,C=1,D=0;E=跳过不计入)
+        // 构建 题号→题目 映射(从合并池)
+        Map<Integer, SelfCheckQuestionVO> questionMap = new LinkedHashMap<>();
+        for (SelfCheckQuestionVO q : FULL_QUESTION_POOL) {
+            questionMap.put(q.getId(), q);
+        }
+
+        // 按维度分组计分
+        Map<String, List<SelfCheckQuestionVO>> dimGroups = groupByDimension(FULL_QUESTION_POOL);
         List<SelfCheckResultVO.DimensionScoreVO> dimensions = new ArrayList<>();
         int totalScore = 0;
+        List<Integer> submittedQuestionIds = new ArrayList<>();
         for (Map.Entry<String, String[]> entry : DIMENSION_META.entrySet()) {
             String dim = entry.getKey();
-            List<SelfCheckQuestionVO> dimQuestions = new ArrayList<>();
-            for (SelfCheckQuestionVO qv : QUESTION_BANK) {
-                if (dim.equals(qv.getDimension())) {
-                    dimQuestions.add(qv);
-                }
-            }
+            List<SelfCheckQuestionVO> dimQuestions = dimGroups.getOrDefault(dim, Collections.emptyList());
             int sum = 0;
             int answered = 0;
             for (SelfCheckQuestionVO qv : dimQuestions) {
                 String ans = answerMap.get(qv.getId());
-                if (ans == null || "E".equals(ans)) {
-                    continue;
-                }
-                int score = scoreOf(ans);
-                sum += score;
+                if (ans == null || "E".equals(ans)) continue;
+                submittedQuestionIds.add(qv.getId());
+                sum += scoreOf(ans);
                 answered++;
             }
             if (answered == 0) {
                 throw new IllegalArgumentException("维度[" + dim + "]至少回答1题");
             }
-            // 按实际答题数折算到 9 分制(跳过1题时仍按比例折算)
             int dimScore = new BigDecimal(sum * 3.0 / answered).setScale(0, RoundingMode.HALF_UP).intValue();
-            if (dimScore > 9) {
-                dimScore = 9;
-            }
+            if (dimScore > 9) dimScore = 9;
             totalScore += dimScore;
             dimensions.add(buildDimensionScoreVO(dim, dimScore));
         }
+        Collections.sort(submittedQuestionIds);
 
         // 生成寻源建议(仅 ≤3 紧绷维度)
         Map<String, Integer> scoreMap = new LinkedHashMap<>();
@@ -394,6 +488,7 @@ public class FiveDimensionSelfCheckService {
         record.setScoresJson(toJson(scoreMap));
         record.setTotalScore(totalScore);
         record.setAdviceJson(toJson(advices));
+        record.setQuestionIdsJson(toJson(submittedQuestionIds));
         record.setCreatedAt(new Date());
         selfCheckMapper.insert(record);
 
@@ -444,6 +539,75 @@ public class FiveDimensionSelfCheckService {
         return result;
     }
 
+    /**
+     * 返回自检状态:是否有记录、是否可检、距下次提醒天数、上次题号
+     */
+    @SuppressWarnings("unchecked")
+    public Map<String, Object> getStatus(Long userId) {
+        Map<String, Object> result = new HashMap<>();
+        // 最近一次自检
+        LambdaQueryWrapper<FiveDimensionSelfCheck> checkWrapper = new LambdaQueryWrapper<FiveDimensionSelfCheck>()
+                .eq(FiveDimensionSelfCheck::getUserId, userId)
+                .orderByDesc(FiveDimensionSelfCheck::getCreatedAt)
+                .last("LIMIT 1");
+        SortUtil.applySort(checkWrapper);
+        FiveDimensionSelfCheck lastCheck = selfCheckMapper.selectOne(checkWrapper);
+        result.put("hasCheck", lastCheck != null);
+        result.put("lastResult", lastCheck != null ? toVO(lastCheck) : null);
+        result.put("lastQuestionIds", parseQuestionIds(lastCheck != null ? lastCheck.getQuestionIdsJson() : null));
+
+        // 最近一次忽略
+        LambdaQueryWrapper<SelfCheckIgnore> ignoreWrapper = new LambdaQueryWrapper<SelfCheckIgnore>()
+                .eq(SelfCheckIgnore::getUserId, userId)
+                .orderByDesc(SelfCheckIgnore::getCreatedAt)
+                .last("LIMIT 1");
+        SelfCheckIgnore lastIgnore = ignoreMapper.selectOne(ignoreWrapper);
+
+        // 最近决定性时间
+        Date lastDecisiveTime = null;
+        if (lastCheck != null) lastDecisiveTime = lastCheck.getCreatedAt();
+        if (lastIgnore != null && (lastDecisiveTime == null || lastIgnore.getCreatedAt().after(lastDecisiveTime))) {
+            lastDecisiveTime = lastIgnore.getCreatedAt();
+        }
+
+        if (lastDecisiveTime == null) {
+            // 从未自检/忽略
+            result.put("canCheck", true);
+            result.put("daysLeft", 0);
+        } else {
+            long diffDays = (System.currentTimeMillis() - lastDecisiveTime.getTime()) / (1000L * 60 * 60 * 24);
+            boolean canCheck = diffDays >= 15;
+            int daysLeft = canCheck ? 0 : (int) (15 - diffDays);
+            result.put("canCheck", canCheck);
+            result.put("daysLeft", daysLeft);
+        }
+        return result;
+    }
+
+    /**
+     * 记录忽略动作(幂等:15 天内重复忽略不插入)
+     */
+    public Date ignoreSelfCheck(Long userId, Long checkId) {
+        // 查最近一次忽略(15 天内)
+        LambdaQueryWrapper<SelfCheckIgnore> wrapper = new LambdaQueryWrapper<SelfCheckIgnore>()
+                .eq(SelfCheckIgnore::getUserId, userId)
+                .orderByDesc(SelfCheckIgnore::getCreatedAt)
+                .last("LIMIT 1");
+        SelfCheckIgnore lastIgnore = ignoreMapper.selectOne(wrapper);
+        if (lastIgnore != null) {
+            long diffDays = (System.currentTimeMillis() - lastIgnore.getCreatedAt().getTime()) / (1000L * 60 * 60 * 24);
+            if (diffDays < 15) {
+                return lastIgnore.getCreatedAt(); // 幂等,返回已有忽略时间
+            }
+        }
+        SelfCheckIgnore ignore = new SelfCheckIgnore();
+        ignore.setUserId(userId);
+        ignore.setCheckId(checkId);
+        ignore.setCreatedAt(new Date());
+        ignoreMapper.insert(ignore);
+        return ignore.getCreatedAt();
+    }
+
     // === 内部方法 ===
 
     private SelfCheckResultVO.DimensionScoreVO buildDimensionScoreVO(String dimension, int score) {