|
|
@@ -0,0 +1,337 @@
|
|
|
+package com.etotem.cfc.service;
|
|
|
+
|
|
|
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
|
|
+import com.etotem.cfc.dto.SelfCheckQuestionVO;
|
|
|
+import com.etotem.cfc.dto.SelfCheckResultVO;
|
|
|
+import com.etotem.cfc.dto.SubmitSelfCheckDTO;
|
|
|
+import com.etotem.cfc.dto.WuxingSourcingAdviceVO;
|
|
|
+import com.etotem.cfc.entity.FiveDimensionSelfCheck;
|
|
|
+import com.etotem.cfc.mapper.FiveDimensionSelfCheckMapper;
|
|
|
+import com.fasterxml.jackson.core.JsonProcessingException;
|
|
|
+import com.fasterxml.jackson.databind.ObjectMapper;
|
|
|
+import org.slf4j.Logger;
|
|
|
+import org.slf4j.LoggerFactory;
|
|
|
+import org.springframework.stereotype.Service;
|
|
|
+import org.springframework.transaction.annotation.Transactional;
|
|
|
+
|
|
|
+import javax.annotation.Resource;
|
|
|
+import java.math.BigDecimal;
|
|
|
+import java.math.RoundingMode;
|
|
|
+import java.util.ArrayList;
|
|
|
+import java.util.Date;
|
|
|
+import java.util.LinkedHashMap;
|
|
|
+import java.util.List;
|
|
|
+import java.util.Map;
|
|
|
+
|
|
|
+/**
|
|
|
+ * 五维家庭自检问卷服务(P0-1)
|
|
|
+ * 对应书稿《终章-绘制你家的幸福全景图》15题自检问卷
|
|
|
+ *
|
|
|
+ * 计分:A=3分,B=2分,C=1分,D=0分,每维度 3 题满分 9 分
|
|
|
+ * 结果解读:≥7 健康 / 4-6 留意 / ≤3 紧绷(紧绷维度触发五行相生寻源建议)
|
|
|
+ */
|
|
|
+@Service
|
|
|
+public class FiveDimensionSelfCheckService {
|
|
|
+
|
|
|
+ private static final Logger log = LoggerFactory.getLogger(FiveDimensionSelfCheckService.class);
|
|
|
+
|
|
|
+ @Resource
|
|
|
+ private FiveDimensionSelfCheckMapper selfCheckMapper;
|
|
|
+
|
|
|
+ @Resource
|
|
|
+ private WuxingSourcingService wuxingSourcingService;
|
|
|
+
|
|
|
+ private final ObjectMapper objectMapper = new ObjectMapper();
|
|
|
+
|
|
|
+ /** 维度中文名与五行 */
|
|
|
+ private static final Map<String, String[]> DIMENSION_META = new LinkedHashMap<>();
|
|
|
+ static {
|
|
|
+ DIMENSION_META.put("body", new String[]{"身", "土", "#FF8C42"});
|
|
|
+ DIMENSION_META.put("wisdom", new String[]{"智", "金", "#6366F1"});
|
|
|
+ DIMENSION_META.put("wealth", new String[]{"富", "水", "#F59E0B"});
|
|
|
+ DIMENSION_META.put("action", new String[]{"行", "木", "#10B981"});
|
|
|
+ DIMENSION_META.put("mind", new String[]{"心", "火", "#FF6B9D"});
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 15 题静态题库(书稿终章原文) */
|
|
|
+ private static final List<SelfCheckQuestionVO> QUESTION_BANK = buildQuestionBank();
|
|
|
+
|
|
|
+ private static List<SelfCheckQuestionVO> buildQuestionBank() {
|
|
|
+ List<SelfCheckQuestionVO> bank = new ArrayList<>();
|
|
|
+ // 身(1-3)
|
|
|
+ bank.add(q(1, "body",
|
|
|
+ "过去一周,你每天平均睡眠时间大约是多少?",
|
|
|
+ "A", "7小时以上", "B", "6-7小时", "C", "5-6小时", "D", "经常小于5小时"));
|
|
|
+ bank.add(q(2, "body",
|
|
|
+ "过去一个月,你每周有几次让身体活动起来的时刻(散步、跑步、做操都可以)?",
|
|
|
+ "A", "几乎每天", "B", "每周3-4次", "C", "每周1-2次", "D", "几乎没有"));
|
|
|
+ bank.add(q(3, "body",
|
|
|
+ "你对自己的身体状况的整体感觉是?",
|
|
|
+ "A", "精力充足,很少疲劳", "B", "偶尔疲劳但能恢复", "C", "经常觉得累", "D", "长期处于疲劳状态"));
|
|
|
+ // 智(4-6)
|
|
|
+ bank.add(q(4, "wisdom",
|
|
|
+ "过去一个月,你有没有做过一件\"学到新东西\"的事(读书、听课、查资料、跟人请教都算)?",
|
|
|
+ "A", "经常有", "B", "有过几次", "C", "很少", "D", "几乎没有"));
|
|
|
+ bank.add(q(5, "wisdom",
|
|
|
+ "当你遇到不懂的问题时,你的第一反应是?",
|
|
|
+ "A", "主动去查、去学", "B", "会查但有时拖着", "C", "等别人告诉我", "D", "算了不想管"));
|
|
|
+ bank.add(q(6, "wisdom",
|
|
|
+ "你家孩子的学习状态,你感觉是?(如果你没有孩子,请选E跳过)",
|
|
|
+ "A", "孩子主动且开心", "B", "主动但有点压力", "C", "需要催促", "D", "长期抗拒", "E", "没有孩子,跳过此题"));
|
|
|
+ // 富(7-9)
|
|
|
+ bank.add(q(7, "wealth",
|
|
|
+ "你知道你家上个月的总支出大概是多少吗?",
|
|
|
+ "A", "很清楚,有记录", "B", "大概知道,没有精确记录", "C", "不太清楚", "D", "完全不知道"));
|
|
|
+ bank.add(q(8, "wealth",
|
|
|
+ "除了工资收入之外,你家有没有其他的收入来源(哪怕很小)?",
|
|
|
+ "A", "有,稳定且有增长", "B", "有,但不稳定", "C", "正在尝试", "D", "没有,也没想过"));
|
|
|
+ bank.add(q(9, "wealth",
|
|
|
+ "你家目前的储蓄状态让你感觉?",
|
|
|
+ "A", "有安全感", "B", "一般,能应对小意外", "C", "不太够", "D", "几乎没有储蓄"));
|
|
|
+ // 行(10-12)
|
|
|
+ bank.add(q(10, "action",
|
|
|
+ "过去一周,你跟伴侣之间有过一次\"非事务性\"的对话吗(不聊家务、孩子、钱)?",
|
|
|
+ "A", "有,好几次", "B", "有,一次", "C", "好像没有", "D", "已经不记得了"));
|
|
|
+ bank.add(q(11, "action",
|
|
|
+ "当你遇到困难时,你觉得自己有可以开口求助的人吗(不包括家人)?",
|
|
|
+ "A", "有,好几个", "B", "有一两个", "C", "不太确定", "D", "没有"));
|
|
|
+ bank.add(q(12, "action",
|
|
|
+ "你家跟双方父母的关系,整体上让你感觉?",
|
|
|
+ "A", "和谐且有边界", "B", "和谐但有点累", "C", "有些紧张", "D", "长期有冲突"));
|
|
|
+ // 心(13-15)
|
|
|
+ bank.add(q(13, "mind",
|
|
|
+ "在这个家里,你能做真实的自己吗?",
|
|
|
+ "A", "完全可以", "B", "大部分时候可以", "C", "有时候需要掩饰", "D", "常常觉得不能说真话"));
|
|
|
+ bank.add(q(14, "mind",
|
|
|
+ "你家有没有一些固定的\"家庭仪式\"(哪怕很小,比如睡前说晚安、周末一起做早餐)?",
|
|
|
+ "A", "有,且一直在坚持", "B", "有,但不稳定", "C", "以前有,现在没了", "D", "从来没有"));
|
|
|
+ bank.add(q(15, "mind",
|
|
|
+ "你对自己家庭的未来感觉是?",
|
|
|
+ "A", "乐观,方向明确", "B", "比较乐观,但有些不确定", "C", "有点焦虑", "D", "很焦虑,不知道怎么办"));
|
|
|
+ return bank;
|
|
|
+ }
|
|
|
+
|
|
|
+ private static SelfCheckQuestionVO q(int id, String dimension, String text, String... opts) {
|
|
|
+ SelfCheckQuestionVO vo = new SelfCheckQuestionVO();
|
|
|
+ vo.setId(id);
|
|
|
+ vo.setDimension(dimension);
|
|
|
+ String[] meta = DIMENSION_META.get(dimension);
|
|
|
+ vo.setDimensionName(meta[0]);
|
|
|
+ vo.setElement(meta[1]);
|
|
|
+ vo.setColor(meta[2]);
|
|
|
+ vo.setText(text);
|
|
|
+ List<SelfCheckQuestionVO.OptionVO> options = new ArrayList<>();
|
|
|
+ for (int i = 0; i < opts.length; i += 2) {
|
|
|
+ SelfCheckQuestionVO.OptionVO o = new SelfCheckQuestionVO.OptionVO();
|
|
|
+ o.setValue(opts[i]);
|
|
|
+ o.setLabel(opts[i + 1]);
|
|
|
+ options.add(o);
|
|
|
+ }
|
|
|
+ vo.setOptions(options);
|
|
|
+ return vo;
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 获取题库(15题) */
|
|
|
+ public List<SelfCheckQuestionVO> getQuestions() {
|
|
|
+ return QUESTION_BANK;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 提交自检问卷并计算评分
|
|
|
+ */
|
|
|
+ @Transactional(rollbackFor = Exception.class)
|
|
|
+ public SelfCheckResultVO submitSelfCheck(SubmitSelfCheckDTO dto, Long userId) {
|
|
|
+ if (dto.getAnswers() == null || dto.getAnswers().isEmpty()) {
|
|
|
+ throw new IllegalArgumentException("答案不能为空");
|
|
|
+ }
|
|
|
+ if (dto.getAnswers().size() < 10) {
|
|
|
+ throw new IllegalArgumentException("请完成至少10道题目");
|
|
|
+ }
|
|
|
+
|
|
|
+ // 题目ID → 答案
|
|
|
+ Map<Integer, String> answerMap = new LinkedHashMap<>();
|
|
|
+ for (SubmitSelfCheckDTO.AnswerItem item : dto.getAnswers()) {
|
|
|
+ if (item.getQuestionId() == null || item.getAnswer() == null || item.getAnswer().isEmpty()) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ answerMap.put(item.getQuestionId(), item.getAnswer().trim().toUpperCase());
|
|
|
+ }
|
|
|
+
|
|
|
+ // 逐维度计分(A=3,B=2,C=1,D=0;E=跳过不计入)
|
|
|
+ List<SelfCheckResultVO.DimensionScoreVO> dimensions = new ArrayList<>();
|
|
|
+ int totalScore = 0;
|
|
|
+ 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);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ 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;
|
|
|
+ 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;
|
|
|
+ }
|
|
|
+ totalScore += dimScore;
|
|
|
+ dimensions.add(buildDimensionScoreVO(dim, dimScore));
|
|
|
+ }
|
|
|
+
|
|
|
+ // 生成寻源建议(仅 ≤3 紧绷维度)
|
|
|
+ Map<String, Integer> scoreMap = new LinkedHashMap<>();
|
|
|
+ for (SelfCheckResultVO.DimensionScoreVO dvo : dimensions) {
|
|
|
+ scoreMap.put(dvo.getDimension(), dvo.getScore());
|
|
|
+ }
|
|
|
+ List<WuxingSourcingAdviceVO> advices = wuxingSourcingService.getAdvicesForLowScores(scoreMap);
|
|
|
+
|
|
|
+ // 保存记录
|
|
|
+ FiveDimensionSelfCheck record = new FiveDimensionSelfCheck();
|
|
|
+ record.setUserId(userId);
|
|
|
+ record.setFamilyMemberId(dto.getFamilyMemberId());
|
|
|
+ record.setAnswersJson(toJson(dto.getAnswers()));
|
|
|
+ record.setScoresJson(toJson(scoreMap));
|
|
|
+ record.setTotalScore(totalScore);
|
|
|
+ record.setAdviceJson(toJson(advices));
|
|
|
+ record.setCreatedAt(new Date());
|
|
|
+ selfCheckMapper.insert(record);
|
|
|
+
|
|
|
+ SelfCheckResultVO vo = new SelfCheckResultVO();
|
|
|
+ vo.setId(record.getId());
|
|
|
+ vo.setUserId(userId);
|
|
|
+ vo.setFamilyMemberId(record.getFamilyMemberId());
|
|
|
+ vo.setDimensions(dimensions);
|
|
|
+ vo.setTotalScore(totalScore);
|
|
|
+ vo.setAdvices(advices);
|
|
|
+ vo.setCreatedAt(record.getCreatedAt());
|
|
|
+ return vo;
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 获取最新一次自检结果 */
|
|
|
+ public SelfCheckResultVO getLatest(Long userId) {
|
|
|
+ LambdaQueryWrapper<FiveDimensionSelfCheck> wrapper = new LambdaQueryWrapper<FiveDimensionSelfCheck>()
|
|
|
+ .eq(FiveDimensionSelfCheck::getUserId, userId)
|
|
|
+ .orderByDesc(FiveDimensionSelfCheck::getCreatedAt)
|
|
|
+ .last("LIMIT 1");
|
|
|
+ FiveDimensionSelfCheck record = selfCheckMapper.selectOne(wrapper);
|
|
|
+ return record != null ? toVO(record) : null;
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 获取自检历史记录 */
|
|
|
+ public List<SelfCheckResultVO> getHistory(Long userId, int limit) {
|
|
|
+ if (limit <= 0 || limit > 50) {
|
|
|
+ limit = 20;
|
|
|
+ }
|
|
|
+ LambdaQueryWrapper<FiveDimensionSelfCheck> wrapper = new LambdaQueryWrapper<FiveDimensionSelfCheck>()
|
|
|
+ .eq(FiveDimensionSelfCheck::getUserId, userId)
|
|
|
+ .orderByDesc(FiveDimensionSelfCheck::getCreatedAt)
|
|
|
+ .last("LIMIT " + limit);
|
|
|
+ List<FiveDimensionSelfCheck> records = selfCheckMapper.selectList(wrapper);
|
|
|
+ List<SelfCheckResultVO> result = new ArrayList<>();
|
|
|
+ for (FiveDimensionSelfCheck record : records) {
|
|
|
+ result.add(toVO(record));
|
|
|
+ }
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ // === 内部方法 ===
|
|
|
+
|
|
|
+ private SelfCheckResultVO.DimensionScoreVO buildDimensionScoreVO(String dimension, int score) {
|
|
|
+ String[] meta = DIMENSION_META.get(dimension);
|
|
|
+ SelfCheckResultVO.DimensionScoreVO vo = new SelfCheckResultVO.DimensionScoreVO();
|
|
|
+ vo.setDimension(dimension);
|
|
|
+ vo.setName(meta[0]);
|
|
|
+ vo.setElement(meta[1]);
|
|
|
+ vo.setColor(meta[2]);
|
|
|
+ vo.setScore(score);
|
|
|
+ String level = wuxingSourcingService.levelOf(score);
|
|
|
+ vo.setLevel(level);
|
|
|
+ vo.setLevelName(wuxingSourcingService.levelName(level));
|
|
|
+ vo.setSummary(summaryOf(level));
|
|
|
+ return vo;
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 结果解读(书稿终章) */
|
|
|
+ private String summaryOf(String level) {
|
|
|
+ if ("healthy".equals(level)) {
|
|
|
+ return "这个维度比较健康。继续保持就好,不用刻意加码。";
|
|
|
+ }
|
|
|
+ if ("attention".equals(level)) {
|
|
|
+ return "这个维度需要留意。找到最小输入,从这个维度开始。";
|
|
|
+ }
|
|
|
+ return "这个维度可能已经很紧绷了。不要急着补短板,看看它的上游维度(五行相生)——也许补上游比直接补它更有效。";
|
|
|
+ }
|
|
|
+
|
|
|
+ private int scoreOf(String answer) {
|
|
|
+ if ("A".equals(answer)) return 3;
|
|
|
+ if ("B".equals(answer)) return 2;
|
|
|
+ if ("C".equals(answer)) return 1;
|
|
|
+ return 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ private String toJson(Object obj) {
|
|
|
+ try {
|
|
|
+ return objectMapper.writeValueAsString(obj);
|
|
|
+ } catch (JsonProcessingException e) {
|
|
|
+ log.warn("JSON序列化失败: {}", e.getMessage());
|
|
|
+ return "[]";
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ @SuppressWarnings("unchecked")
|
|
|
+ private SelfCheckResultVO toVO(FiveDimensionSelfCheck record) {
|
|
|
+ SelfCheckResultVO vo = new SelfCheckResultVO();
|
|
|
+ vo.setId(record.getId());
|
|
|
+ vo.setUserId(record.getUserId());
|
|
|
+ vo.setFamilyMemberId(record.getFamilyMemberId());
|
|
|
+ vo.setTotalScore(record.getTotalScore());
|
|
|
+ vo.setCreatedAt(record.getCreatedAt());
|
|
|
+
|
|
|
+ // 解析各维度得分
|
|
|
+ Map<String, Integer> scoreMap = new LinkedHashMap<>();
|
|
|
+ if (record.getScoresJson() != null) {
|
|
|
+ try {
|
|
|
+ scoreMap = objectMapper.readValue(record.getScoresJson(), LinkedHashMap.class);
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.warn("解析scoresJson失败: {}", e.getMessage());
|
|
|
+ }
|
|
|
+ }
|
|
|
+ List<SelfCheckResultVO.DimensionScoreVO> dimensions = new ArrayList<>();
|
|
|
+ for (Map.Entry<String, String[]> entry : DIMENSION_META.entrySet()) {
|
|
|
+ Integer score = scoreMap.get(entry.getKey());
|
|
|
+ if (score != null) {
|
|
|
+ dimensions.add(buildDimensionScoreVO(entry.getKey(), score));
|
|
|
+ }
|
|
|
+ }
|
|
|
+ vo.setDimensions(dimensions);
|
|
|
+
|
|
|
+ // 解析寻源建议
|
|
|
+ if (record.getAdviceJson() != null) {
|
|
|
+ try {
|
|
|
+ List<WuxingSourcingAdviceVO> advices = objectMapper.readValue(
|
|
|
+ record.getAdviceJson(),
|
|
|
+ objectMapper.getTypeFactory().constructCollectionType(List.class, WuxingSourcingAdviceVO.class));
|
|
|
+ vo.setAdvices(advices);
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.warn("解析adviceJson失败: {}", e.getMessage());
|
|
|
+ vo.setAdvices(new ArrayList<>());
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ vo.setAdvices(new ArrayList<>());
|
|
|
+ }
|
|
|
+ return vo;
|
|
|
+ }
|
|
|
+}
|