|
@@ -9,6 +9,8 @@ import com.etotem.cfc.entity.FamilyMember;
|
|
|
import com.etotem.cfc.entity.FiveDimensionSelfCheck;
|
|
import com.etotem.cfc.entity.FiveDimensionSelfCheck;
|
|
|
import com.etotem.cfc.mapper.FamilyMemberMapper;
|
|
import com.etotem.cfc.mapper.FamilyMemberMapper;
|
|
|
import com.etotem.cfc.mapper.FiveDimensionSelfCheckMapper;
|
|
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.core.JsonProcessingException;
|
|
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
|
import org.slf4j.Logger;
|
|
import org.slf4j.Logger;
|
|
@@ -20,7 +22,9 @@ import javax.annotation.Resource;
|
|
|
import java.math.BigDecimal;
|
|
import java.math.BigDecimal;
|
|
|
import java.math.RoundingMode;
|
|
import java.math.RoundingMode;
|
|
|
import java.util.ArrayList;
|
|
import java.util.ArrayList;
|
|
|
|
|
+import java.util.Collections;
|
|
|
import java.util.Date;
|
|
import java.util.Date;
|
|
|
|
|
+import java.util.HashMap;
|
|
|
import java.util.LinkedHashMap;
|
|
import java.util.LinkedHashMap;
|
|
|
import java.util.List;
|
|
import java.util.List;
|
|
|
import java.util.Map;
|
|
import java.util.Map;
|
|
@@ -50,6 +54,9 @@ public class FiveDimensionSelfCheckService {
|
|
|
@Resource
|
|
@Resource
|
|
|
private OnboardingService onboardingService;
|
|
private OnboardingService onboardingService;
|
|
|
|
|
|
|
|
|
|
+ @Resource
|
|
|
|
|
+ private SelfCheckIgnoreMapper ignoreMapper;
|
|
|
|
|
+
|
|
|
private final ObjectMapper objectMapper = new ObjectMapper();
|
|
private final ObjectMapper objectMapper = new ObjectMapper();
|
|
|
|
|
|
|
|
/** 维度中文名与五行 */
|
|
/** 维度中文名与五行 */
|
|
@@ -68,6 +75,58 @@ public class FiveDimensionSelfCheckService {
|
|
|
/** 子维度附加题库(行6维 + 心·自驱力/自我概念 + 智·成长型思维 + 富·家族传承) */
|
|
/** 子维度附加题库(行6维 + 心·自驱力/自我概念 + 智·成长型思维 + 富·家族传承) */
|
|
|
private static final List<SelfCheckQuestionVO> SUB_DIMENSION_QUESTION_BANK = buildSubDimensionQuestionBank();
|
|
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() {
|
|
private static List<SelfCheckQuestionVO> buildQuestionBank() {
|
|
|
List<SelfCheckQuestionVO> bank = new ArrayList<>();
|
|
List<SelfCheckQuestionVO> bank = new ArrayList<>();
|
|
|
// 身(1-3)
|
|
// 身(1-3)
|
|
@@ -228,6 +287,42 @@ public class FiveDimensionSelfCheckService {
|
|
|
return QUESTION_BANK;
|
|
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() {
|
|
public List<SelfCheckQuestionVO> getSubDimensionQuestions() {
|
|
|
return SUB_DIMENSION_QUESTION_BANK;
|
|
return SUB_DIMENSION_QUESTION_BANK;
|
|
@@ -345,39 +440,38 @@ public class FiveDimensionSelfCheckService {
|
|
|
answerMap.put(item.getQuestionId(), item.getAnswer().trim().toUpperCase());
|
|
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<>();
|
|
List<SelfCheckResultVO.DimensionScoreVO> dimensions = new ArrayList<>();
|
|
|
int totalScore = 0;
|
|
int totalScore = 0;
|
|
|
|
|
+ List<Integer> submittedQuestionIds = new ArrayList<>();
|
|
|
for (Map.Entry<String, String[]> entry : DIMENSION_META.entrySet()) {
|
|
for (Map.Entry<String, String[]> entry : DIMENSION_META.entrySet()) {
|
|
|
String dim = entry.getKey();
|
|
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 sum = 0;
|
|
|
int answered = 0;
|
|
int answered = 0;
|
|
|
for (SelfCheckQuestionVO qv : dimQuestions) {
|
|
for (SelfCheckQuestionVO qv : dimQuestions) {
|
|
|
String ans = answerMap.get(qv.getId());
|
|
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++;
|
|
answered++;
|
|
|
}
|
|
}
|
|
|
if (answered == 0) {
|
|
if (answered == 0) {
|
|
|
throw new IllegalArgumentException("维度[" + dim + "]至少回答1题");
|
|
throw new IllegalArgumentException("维度[" + dim + "]至少回答1题");
|
|
|
}
|
|
}
|
|
|
- // 按实际答题数折算到 9 分制(跳过1题时仍按比例折算)
|
|
|
|
|
int dimScore = new BigDecimal(sum * 3.0 / answered).setScale(0, RoundingMode.HALF_UP).intValue();
|
|
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;
|
|
totalScore += dimScore;
|
|
|
dimensions.add(buildDimensionScoreVO(dim, dimScore));
|
|
dimensions.add(buildDimensionScoreVO(dim, dimScore));
|
|
|
}
|
|
}
|
|
|
|
|
+ Collections.sort(submittedQuestionIds);
|
|
|
|
|
|
|
|
// 生成寻源建议(仅 ≤3 紧绷维度)
|
|
// 生成寻源建议(仅 ≤3 紧绷维度)
|
|
|
Map<String, Integer> scoreMap = new LinkedHashMap<>();
|
|
Map<String, Integer> scoreMap = new LinkedHashMap<>();
|
|
@@ -394,6 +488,7 @@ public class FiveDimensionSelfCheckService {
|
|
|
record.setScoresJson(toJson(scoreMap));
|
|
record.setScoresJson(toJson(scoreMap));
|
|
|
record.setTotalScore(totalScore);
|
|
record.setTotalScore(totalScore);
|
|
|
record.setAdviceJson(toJson(advices));
|
|
record.setAdviceJson(toJson(advices));
|
|
|
|
|
+ record.setQuestionIdsJson(toJson(submittedQuestionIds));
|
|
|
record.setCreatedAt(new Date());
|
|
record.setCreatedAt(new Date());
|
|
|
selfCheckMapper.insert(record);
|
|
selfCheckMapper.insert(record);
|
|
|
|
|
|
|
@@ -444,6 +539,75 @@ public class FiveDimensionSelfCheckService {
|
|
|
return result;
|
|
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) {
|
|
private SelfCheckResultVO.DimensionScoreVO buildDimensionScoreVO(String dimension, int score) {
|