|
|
@@ -0,0 +1,1160 @@
|
|
|
+# 五维家庭自检 · 15 天复检周期 + 首页分数 + 题目轮换 实现计划
|
|
|
+
|
|
|
+> **面向 AI 代理的工作者:** 必需子技能:使用 superpowers:subagent-driven-development(推荐)或 superpowers:executing-plans 逐任务实现此计划。步骤使用复选框(`- [ ]`)语法来跟踪进度。
|
|
|
+
|
|
|
+**目标:** 为五维家庭自检增加 15 天复检周期、首页显示上次自检分数、中间页二选一(再次自检/忽略)、以及合并题库随机抽题轮换。
|
|
|
+
|
|
|
+**架构:** 后端新增 `self_check_ignores` 表 + 两个新接口(`/status` + `/ignore`);改造现有 `/questions`(合并题库抽题)和 `/submit`(按提交题号计分 + 落库题号);前端新增中间页 `self-check-entry.vue`,首页入口接入 `/status` 并跳转中间页。
|
|
|
+
|
|
|
+**技术栈:** Java 8 / Spring Boot 2.7.18 / MyBatis-Plus / uni-app Vue 2 Options API / MySQL 8.0
|
|
|
+
|
|
|
+**规格文档:** `docs/superpowers/specs/2026-08-31-self-check-reminder-design.md`
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 文件结构
|
|
|
+
|
|
|
+| 文件 | 职责 | 变更类型 |
|
|
|
+|------|------|---------|
|
|
|
+| `cfc-backend/src/main/java/com/etotem/cfc/entity/SelfCheckIgnore.java` | 忽略记录实体 | 新建 |
|
|
|
+| `cfc-backend/src/main/java/com/etotem/cfc/mapper/SelfCheckIgnoreMapper.java` | 忽略记录 Mapper | 新建 |
|
|
|
+| `cfc-backend/src/main/java/com/etotem/cfc/entity/FiveDimensionSelfCheck.java` | 自检记录实体(新增 `questionIdsJson`) | 修改 |
|
|
|
+| `cfc-backend/src/main/java/com/etotem/cfc/service/FiveDimensionSelfCheckService.java` | 抽题/状态/忽略/计分改造 | 修改 |
|
|
|
+| `cfc-backend/src/main/java/com/etotem/cfc/controller/family/FiveDimensionSelfCheckController.java` | 新增 `/status` + `/ignore` | 修改 |
|
|
|
+| `cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java` | 迁移 269/270 | 修改 |
|
|
|
+| `cfc-backend/src/main/resources/schema.sql` | 表定义同步 | 修改 |
|
|
|
+| `cfc-frontend/pages/family/self-check-entry.vue` | 中间页(结果摘要 + 再次自检/忽略) | 新建 |
|
|
|
+| `cfc-frontend/pages/index-home/index.vue` | 首页入口显示自检总分 | 修改 |
|
|
|
+| `cfc-frontend/pages/home-pages/parent-index.vue` | 家长首页入口显示自检总分 | 修改 |
|
|
|
+| `cfc-frontend/pages/family/self-check-result.vue` | 「重新自检」改跳中间页 | 修改 |
|
|
|
+| `cfc-frontend/pages.json` | 注册中间页 | 修改 |
|
|
|
+| `cfc-frontend/utils/api.js` | 新增 `getSelfCheckStatus` / `ignoreSelfCheck` | 修改 |
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 任务 1:后端 — 数据库迁移(表 + 列)
|
|
|
+
|
|
|
+**文件:**
|
|
|
+- 新建:`cfc-backend/src/main/java/com/etotem/cfc/entity/SelfCheckIgnore.java`
|
|
|
+- 新建:`cfc-backend/src/main/java/com/etotem/cfc/mapper/SelfCheckIgnoreMapper.java`
|
|
|
+- 修改:`cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java:9574`(`migrateCouponStatus` 方法后追加迁移 269/270)
|
|
|
+- 修改:`cfc-backend/src/main/resources/schema.sql`(`five_dimension_self_checks` 表 + 追加 `self_check_ignores` 表)
|
|
|
+
|
|
|
+- [ ] **步骤 1:创建 SelfCheckIgnore 实体**
|
|
|
+
|
|
|
+创建文件 `cfc-backend/src/main/java/com/etotem/cfc/entity/SelfCheckIgnore.java`:
|
|
|
+
|
|
|
+```java
|
|
|
+package com.etotem.cfc.entity;
|
|
|
+
|
|
|
+import com.baomidou.mybatisplus.annotation.IdType;
|
|
|
+import com.baomidou.mybatisplus.annotation.TableId;
|
|
|
+import com.baomidou.mybatisplus.annotation.TableName;
|
|
|
+import lombok.Data;
|
|
|
+
|
|
|
+import java.io.Serializable;
|
|
|
+import java.util.Date;
|
|
|
+
|
|
|
+@Data
|
|
|
+@TableName("self_check_ignores")
|
|
|
+public class SelfCheckIgnore implements Serializable {
|
|
|
+
|
|
|
+ @TableId(type = IdType.AUTO)
|
|
|
+ private Long id;
|
|
|
+
|
|
|
+ private Long userId;
|
|
|
+
|
|
|
+ private Long checkId;
|
|
|
+
|
|
|
+ private Date createdAt;
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **步骤 2:创建 SelfCheckIgnoreMapper**
|
|
|
+
|
|
|
+创建文件 `cfc-backend/src/main/java/com/etotem/cfc/mapper/SelfCheckIgnoreMapper.java`:
|
|
|
+
|
|
|
+```java
|
|
|
+package com.etotem.cfc.mapper;
|
|
|
+
|
|
|
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
|
|
+import com.etotem.cfc.entity.SelfCheckIgnore;
|
|
|
+import org.apache.ibatis.annotations.Mapper;
|
|
|
+
|
|
|
+@Mapper
|
|
|
+public interface SelfCheckIgnoreMapper extends BaseMapper<SelfCheckIgnore> {
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **步骤 3:在 DatabaseInitializer 追加迁移 269/270**
|
|
|
+
|
|
|
+在 `DatabaseInitializer.java` 中,于 `migrateCouponStatus()` 方法结束后、`migrateSysMenuReorg()` 方法开始前,插入:
|
|
|
+
|
|
|
+```java
|
|
|
+// 迁移269: five_dimension_self_checks 表添加 question_ids_json 列(自检轮换用)
|
|
|
+ensureColumn("five_dimension_self_checks", "question_ids_json", "VARCHAR(255) COMMENT '本次自检使用的题号JSON: [1,5,9,...]'");
|
|
|
+
|
|
|
+// 迁移270: 创建 self_check_ignores 表(记录用户忽略自检提醒的动作,作为 15 天周期重置依据)
|
|
|
+try {
|
|
|
+ jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS self_check_ignores (" +
|
|
|
+ "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
|
|
|
+ "user_id BIGINT NOT NULL COMMENT '用户ID', " +
|
|
|
+ "check_id BIGINT COMMENT '忽略的自检记录ID(可空)', " +
|
|
|
+ "created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '忽略时间', " +
|
|
|
+ "INDEX idx_user_id (user_id)" +
|
|
|
+ ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='五维自检忽略提醒记录'");
|
|
|
+ log.info("已创建 self_check_ignores 表");
|
|
|
+} catch (Exception e) {
|
|
|
+ log.warn("创建 self_check_ignores 表失败: {}", e.getMessage());
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+`ensureColumn` 方法已在 DatabaseInitializer 第 4099 行定义,直接复用。
|
|
|
+
|
|
|
+- [ ] **步骤 4:同步 schema.sql**
|
|
|
+
|
|
|
+在 `cfc-backend/src/main/resources/schema.sql` 中找到 `five_dimension_self_checks` 建表语句(约第 4285 行),在 `created_at DATETIME DEFAULT CURRENT_TIMESTAMP` 之后追加:
|
|
|
+
|
|
|
+```sql
|
|
|
+ question_ids_json VARCHAR(255) DEFAULT NULL COMMENT '本次自检使用的题号JSON: [1,5,9,...]',
|
|
|
+```
|
|
|
+
|
|
|
+在 schema.sql 末尾追加 `self_check_ignores` 建表语句:
|
|
|
+
|
|
|
+```sql
|
|
|
+-- ============================================================
|
|
|
+-- 五维自检忽略提醒记录表(迁移270)
|
|
|
+-- ============================================================
|
|
|
+CREATE TABLE IF NOT EXISTS self_check_ignores (
|
|
|
+ id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
|
|
+ user_id BIGINT NOT NULL COMMENT '用户ID',
|
|
|
+ check_id BIGINT COMMENT '忽略的自检记录ID(可空)',
|
|
|
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '忽略时间',
|
|
|
+ INDEX idx_user_id (user_id)
|
|
|
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='五维自检忽略提醒记录';
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **步骤 5:编译验证**
|
|
|
+
|
|
|
+```bash
|
|
|
+cd cfc-backend && mvn clean compile -q
|
|
|
+```
|
|
|
+
|
|
|
+预期:BUILD SUCCESS
|
|
|
+
|
|
|
+- [ ] **步骤 6:Commit**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add cfc-backend/src/main/java/com/etotem/cfc/entity/SelfCheckIgnore.java \
|
|
|
+ cfc-backend/src/main/java/com/etotem/cfc/mapper/SelfCheckIgnoreMapper.java \
|
|
|
+ cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java \
|
|
|
+ cfc-backend/src/main/resources/schema.sql
|
|
|
+git commit -m "feat(self-check): 新增 self_check_ignores 表和 question_ids_json 列(迁移269/270)"
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 任务 2:后端 — Entity 与 Service 改造
|
|
|
+
|
|
|
+**文件:**
|
|
|
+- 修改:`cfc-backend/src/main/java/com/etotem/cfc/entity/FiveDimensionSelfCheck.java`(新增字段)
|
|
|
+- 修改:`cfc-backend/src/main/java/com/etotem/cfc/service/FiveDimensionSelfCheckService.java`(抽题/状态/忽略/计分改造)
|
|
|
+
|
|
|
+- [ ] **步骤 1:FiveDimensionSelfCheck 实体新增字段**
|
|
|
+
|
|
|
+在 `FiveDimensionSelfCheck.java` 的 `private Date createdAt;` 之前追加:
|
|
|
+
|
|
|
+```java
|
|
|
+ /** 本次自检使用的题号JSON,如 [1,2,3,101] */
|
|
|
+ private String questionIdsJson;
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **步骤 2:Service 新增私有字段 — 合并题库(34题)**
|
|
|
+
|
|
|
+在 `FiveDimensionSelfCheckService.java` 中,于 `SUB_DIMENSION_QUESTION_BANK` 字段之后,追加:
|
|
|
+
|
|
|
+```java
|
|
|
+ /** 合并题库:主题库(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;
|
|
|
+ }
|
|
|
+```
|
|
|
+
|
|
|
+同时在文件头部导入 `Collections`:
|
|
|
+
|
|
|
+```java
|
|
|
+import java.util.Collections;
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **步骤 3:Service 新增 `getQuestionsForRetake` 方法**
|
|
|
+
|
|
|
+在 `getQuestions()` 方法后追加:
|
|
|
+
|
|
|
+```java
|
|
|
+ /**
|
|
|
+ * 返回轮换后的题库(用于再次自检):
|
|
|
+ * 合并题库按维度分组,每维度随机抽 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();
|
|
|
+ }
|
|
|
+ }
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **步骤 4:Service 改造 `submitSelfCheck` — 按提交题号计分 + 落库题号**
|
|
|
+
|
|
|
+将 `submitSelfCheck` 方法中第 348-380 行的计分逻辑替换为:
|
|
|
+
|
|
|
+```java
|
|
|
+ // 构建 题号→题目 映射(从合并池)
|
|
|
+ Map<Integer, SelfCheckQuestionVO> questionMap = new LinkedHashMap<>();
|
|
|
+ for (SelfCheckQuestionVO q : FULL_QUESTION_POOL) {
|
|
|
+ questionMap.put(q.getId(), q);
|
|
|
+ }
|
|
|
+ // 题目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());
|
|
|
+ }
|
|
|
+
|
|
|
+ // 按维度分组计分
|
|
|
+ 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 = 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;
|
|
|
+ submittedQuestionIds.add(qv.getId());
|
|
|
+ sum += scoreOf(ans);
|
|
|
+ answered++;
|
|
|
+ }
|
|
|
+ if (answered == 0) {
|
|
|
+ throw new IllegalArgumentException("维度[" + dim + "]至少回答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));
|
|
|
+ }
|
|
|
+```
|
|
|
+
|
|
|
+并在落库部分(`selfCheckMapper.insert(record)` 之前)追加:
|
|
|
+
|
|
|
+```java
|
|
|
+ record.setQuestionIdsJson(toJson(submittedQuestionIds));
|
|
|
+```
|
|
|
+
|
|
|
+同时确保 `submittedQuestionIds` 升序排列:
|
|
|
+
|
|
|
+```java
|
|
|
+ Collections.sort(submittedQuestionIds);
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **步骤 5:Service 新增 `getStatus` 方法**
|
|
|
+
|
|
|
+```java
|
|
|
+ /**
|
|
|
+ * 返回自检状态:是否有记录、是否可检、距下次提醒天数、上次题号
|
|
|
+ */
|
|
|
+ @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;
|
|
|
+ }
|
|
|
+```
|
|
|
+
|
|
|
+需要新增 `ignoreMapper` 注入:
|
|
|
+
|
|
|
+```java
|
|
|
+ @Resource
|
|
|
+ private SelfCheckIgnoreMapper ignoreMapper;
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **步骤 6:Service 新增 `ignore` 方法**
|
|
|
+
|
|
|
+```java
|
|
|
+ /**
|
|
|
+ * 记录忽略动作(幂等: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();
|
|
|
+ }
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **步骤 7:编译验证**
|
|
|
+
|
|
|
+```bash
|
|
|
+cd cfc-backend && mvn clean compile -q
|
|
|
+```
|
|
|
+
|
|
|
+预期:BUILD SUCCESS
|
|
|
+
|
|
|
+- [ ] **步骤 8:Commit**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add cfc-backend/src/main/java/com/etotem/cfc/entity/FiveDimensionSelfCheck.java \
|
|
|
+ cfc-backend/src/main/java/com/etotem/cfc/service/FiveDimensionSelfCheckService.java
|
|
|
+git commit -m "feat(self-check): Service 层抽题/状态/忽略/计分改造"
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 任务 3:后端 — Controller 新增接口
|
|
|
+
|
|
|
+**文件:**
|
|
|
+- 修改:`cfc-backend/src/main/java/com/etotem/cfc/controller/family/FiveDimensionSelfCheckController.java`
|
|
|
+
|
|
|
+- [ ] **步骤 1:注入 ignoreMapper**
|
|
|
+
|
|
|
+在 `FiveDimensionSelfCheckController.java` 中追加字段:
|
|
|
+
|
|
|
+```java
|
|
|
+ @Resource
|
|
|
+ private com.etotem.cfc.mapper.SelfCheckIgnoreMapper selfCheckIgnoreMapper;
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **步骤 2:新增 `/status` 接口**
|
|
|
+
|
|
|
+在类中追加方法:
|
|
|
+
|
|
|
+```java
|
|
|
+ @Operation(summary = "获取自检状态(是否有记录、是否可检、距下次提醒天数)")
|
|
|
+ @PostMapping("/status")
|
|
|
+ public Result<Map<String, Object>> getStatus(@RequestAttribute("userId") Long userId) {
|
|
|
+ if (userId == null) {
|
|
|
+ return Result.error("请先登录");
|
|
|
+ }
|
|
|
+ return Result.success(selfCheckService.getStatus(userId));
|
|
|
+ }
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **步骤 3:新增 `/ignore` 接口**
|
|
|
+
|
|
|
+```java
|
|
|
+ @Operation(summary = "忽略本次自检提醒(重置 15 天计时)")
|
|
|
+ @PostMapping("/ignore")
|
|
|
+ public Result<Map<String, Object>> ignore(@RequestBody(required = false) Map<String, Object> body,
|
|
|
+ @RequestAttribute("userId") Long userId) {
|
|
|
+ if (userId == null) {
|
|
|
+ return Result.error("请先登录");
|
|
|
+ }
|
|
|
+ Long checkId = body != null ? ParamUtils.getLong(body.get("checkId")) : null;
|
|
|
+ Date ignoredAt = selfCheckService.ignoreSelfCheck(userId, checkId);
|
|
|
+ Map<String, Object> resp = new HashMap<>();
|
|
|
+ resp.put("nextRemindAt", new java.text.SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss").format(
|
|
|
+ new Date(ignoredAt.getTime() + 15L * 24 * 60 * 60 * 1000)));
|
|
|
+ return Result.success(resp);
|
|
|
+ }
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **步骤 4:修改 `/questions` 接口 — 区分首次/再次**
|
|
|
+
|
|
|
+将 `/questions` 方法改为:
|
|
|
+
|
|
|
+```java
|
|
|
+ @Operation(summary = "获取自检问卷题目(首次固定 15 题;再次合并题库随机抽)")
|
|
|
+ @PostMapping("/questions")
|
|
|
+ public Result<List<SelfCheckQuestionVO>> getQuestions(
|
|
|
+ @RequestAttribute("userId") Long userId,
|
|
|
+ @RequestBody(required = false) Map<String, Object> body) {
|
|
|
+ if (userId == null) {
|
|
|
+ return Result.error("请先登录");
|
|
|
+ }
|
|
|
+ Boolean retake = body != null && body.get("retake") != null && Boolean.TRUE.equals(body.get("retake"));
|
|
|
+ if (retake) {
|
|
|
+ return Result.success(selfCheckService.getQuestionsForRetake(userId));
|
|
|
+ }
|
|
|
+ return Result.success(selfCheckService.getQuestions());
|
|
|
+ }
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **步骤 5:编译验证**
|
|
|
+
|
|
|
+```bash
|
|
|
+cd cfc-backend && mvn clean compile -q
|
|
|
+```
|
|
|
+
|
|
|
+预期:BUILD SUCCESS
|
|
|
+
|
|
|
+- [ ] **步骤 6:Commit**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add cfc-backend/src/main/java/com/etotem/cfc/controller/family/FiveDimensionSelfCheckController.java
|
|
|
+git commit -m "feat(self-check): 新增 /status 和 /ignore 接口;/questions 支持 retake 参数"
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 任务 4:前端 — utils/api.js 新增方法
|
|
|
+
|
|
|
+**文件:**
|
|
|
+- 修改:`cfc-frontend/utils/api.js`(在自检相关方法后追加)
|
|
|
+
|
|
|
+- [ ] **步骤 1:追加两个 API 方法**
|
|
|
+
|
|
|
+在 `cfc-frontend/utils/api.js` 中,于 `getSelfCheckHistory` 方法之后追加:
|
|
|
+
|
|
|
+```js
|
|
|
+export const getSelfCheckStatus = () => {
|
|
|
+ return request('/api/family/self-check/status', 'POST', {})
|
|
|
+}
|
|
|
+
|
|
|
+export const ignoreSelfCheck = (data) => {
|
|
|
+ return request('/api/family/self-check/ignore', 'POST', data || {})
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **步骤 2:语法校验**
|
|
|
+
|
|
|
+```bash
|
|
|
+node --check cfc-frontend/utils/api.js
|
|
|
+```
|
|
|
+
|
|
|
+预期:无输出(语法正确)
|
|
|
+
|
|
|
+- [ ] **步骤 3:Commit**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add cfc-frontend/utils/api.js
|
|
|
+git commit -m "feat(self-check): 新增 getSelfCheckStatus 和 ignoreSelfCheck API 方法"
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 任务 5:前端 — 新增中间页 self-check-entry.vue
|
|
|
+
|
|
|
+**文件:**
|
|
|
+- 新建:`cfc-frontend/pages/family/self-check-entry.vue`
|
|
|
+
|
|
|
+- [ ] **步骤 1:创建中间页文件**
|
|
|
+
|
|
|
+创建 `cfc-frontend/pages/family/self-check-entry.vue`:
|
|
|
+
|
|
|
+```vue
|
|
|
+<template>
|
|
|
+ <view class="sce-page">
|
|
|
+ <view v-if="loading" class="sce-loading">
|
|
|
+ <text class="sce-loading-text">加载中...</text>
|
|
|
+ </view>
|
|
|
+ <template v-else>
|
|
|
+ <!-- 总分横幅 -->
|
|
|
+ <view class="sce-hero" :style="{ background: heroGradient }">
|
|
|
+ <view class="sce-hero-inner">
|
|
|
+ <text class="sce-hero-label">最近自检总分</text>
|
|
|
+ <view class="sce-hero-score-row">
|
|
|
+ <text class="sce-hero-score">{{ lastResult && lastResult.totalScore || 0 }}</text>
|
|
|
+ <text class="sce-hero-total">/45</text>
|
|
|
+ </view>
|
|
|
+ <text class="sce-hero-date" v-if="lastResult && lastResult.createdAt">
|
|
|
+ {{ formatCreatedDate(lastResult.createdAt) }}
|
|
|
+ </text>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 维度分数条 -->
|
|
|
+ <view class="sce-card" v-if="lastResult && lastResult.dimensions && lastResult.dimensions.length > 0">
|
|
|
+ <view class="sce-card-title">
|
|
|
+ <text class="sce-card-title-text">五维得分</text>
|
|
|
+ </view>
|
|
|
+ <view class="sce-dim-list">
|
|
|
+ <view class="sce-dim-item" v-for="dim in lastResult.dimensions" :key="dim.dimension">
|
|
|
+ <view class="sce-dim-head">
|
|
|
+ <view class="sce-dim-label">
|
|
|
+ <view class="sce-dim-dot" :style="{ background: dim.color }"></view>
|
|
|
+ <text class="sce-dim-name">{{ dim.name }}</text>
|
|
|
+ </view>
|
|
|
+ <view class="sce-dim-score-wrap">
|
|
|
+ <text class="sce-dim-score" :style="{ color: dim.color }">{{ dim.score }}</text>
|
|
|
+ <text class="sce-dim-max">/9</text>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ <view class="sce-dim-bar">
|
|
|
+ <view class="sce-dim-bar-fill" :style="{ width: (dim.score / 9 * 100) + '%', background: dim.color }"></view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 状态提示 -->
|
|
|
+ <view class="sce-status" v-if="!canCheck && lastResult">
|
|
|
+ <text class="sce-status-text">距下次自检还有 {{ daysLeft }} 天</text>
|
|
|
+ </view>
|
|
|
+ <view class="sce-status" v-else-if="canCheck && lastResult">
|
|
|
+ <text class="sce-status-text">已满 15 天,可再次自检</text>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 按钮区域 -->
|
|
|
+ <view class="sce-footer">
|
|
|
+ <button v-if="canCheck && lastResult" class="sce-btn sce-btn-primary" @click="retake">
|
|
|
+ 再次自检
|
|
|
+ </button>
|
|
|
+ <button v-if="canCheck && lastResult" class="sce-btn sce-btn-outline" @click="ignore">
|
|
|
+ 忽略本次
|
|
|
+ </button>
|
|
|
+ <button v-if="!lastResult" class="sce-btn sce-btn-primary" @click="retake">
|
|
|
+ 开始自检
|
|
|
+ </button>
|
|
|
+ <button v-if="!canCheck && lastResult" class="sce-btn sce-btn-disabled" disabled>
|
|
|
+ {{ daysLeft }} 天后可再次自检
|
|
|
+ </button>
|
|
|
+ </view>
|
|
|
+ </template>
|
|
|
+ </view>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script>
|
|
|
+import { getSelfCheckStatus, ignoreSelfCheck } from '@/utils/api'
|
|
|
+import { parseDate } from '@/utils/format.js'
|
|
|
+
|
|
|
+export default {
|
|
|
+ data() {
|
|
|
+ return {
|
|
|
+ loading: true,
|
|
|
+ lastResult: null,
|
|
|
+ canCheck: false,
|
|
|
+ daysLeft: 0
|
|
|
+ }
|
|
|
+ },
|
|
|
+ computed: {
|
|
|
+ heroGradient: function() {
|
|
|
+ if (!this.lastResult || !this.lastResult.dimensions || this.lastResult.dimensions.length === 0) {
|
|
|
+ return 'linear-gradient(135deg, #F97316, #FF8C42)'
|
|
|
+ }
|
|
|
+ var lowest = this.lastResult.dimensions.reduce(function(min, d) {
|
|
|
+ return d.score < min.score ? d : min
|
|
|
+ }, this.lastResult.dimensions[0])
|
|
|
+ return 'linear-gradient(135deg, ' + lowest.color + ', ' + (lowest.color + 'bb') + ')'
|
|
|
+ }
|
|
|
+ },
|
|
|
+ onLoad: function() {
|
|
|
+ this.loadStatus()
|
|
|
+ },
|
|
|
+ methods: {
|
|
|
+ loadStatus: function() {
|
|
|
+ var self = this
|
|
|
+ getSelfCheckStatus().then(function(res) {
|
|
|
+ self.loading = false
|
|
|
+ if (res.code === 200 && res.data) {
|
|
|
+ self.lastResult = res.data.lastResult || null
|
|
|
+ self.canCheck = res.data.canCheck
|
|
|
+ self.daysLeft = res.data.daysLeft || 0
|
|
|
+ } else {
|
|
|
+ uni.showToast({ title: (res && res.message) || '加载失败', icon: 'none' })
|
|
|
+ }
|
|
|
+ }).catch(function() {
|
|
|
+ self.loading = false
|
|
|
+ uni.showToast({ title: '网络异常', icon: 'none' })
|
|
|
+ })
|
|
|
+ },
|
|
|
+ retake: function() {
|
|
|
+ uni.navigateTo({ url: '/pages/family/self-check?retake=1' })
|
|
|
+ },
|
|
|
+ ignore: function() {
|
|
|
+ var self = this
|
|
|
+ ignoreSelfCheck({ checkId: this.lastResult && this.lastResult.id }).then(function(res) {
|
|
|
+ if (res.code === 200) {
|
|
|
+ uni.showToast({ title: '已忽略,15 天后再次提醒', icon: 'none' })
|
|
|
+ setTimeout(function() {
|
|
|
+ uni.navigateBack()
|
|
|
+ }, 800)
|
|
|
+ } else {
|
|
|
+ uni.showToast({ title: (res && res.message) || '操作失败', icon: 'none' })
|
|
|
+ }
|
|
|
+ }).catch(function() {
|
|
|
+ uni.showToast({ title: '操作失败', icon: 'none' })
|
|
|
+ })
|
|
|
+ },
|
|
|
+ formatCreatedDate: function(dateStr) {
|
|
|
+ if (!dateStr) return ''
|
|
|
+ var d = parseDate(dateStr)
|
|
|
+ if (!d) return ''
|
|
|
+ var pad = function(n) { return n < 10 ? '0' + n : '' + n }
|
|
|
+ return d.getFullYear() + '-' + pad(d.getMonth() + 1) + '-' + pad(d.getDate())
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+</script>
|
|
|
+
|
|
|
+<style scoped>
|
|
|
+.sce-page {
|
|
|
+ min-height: 100vh;
|
|
|
+ background: #F5FAFE;
|
|
|
+ padding: 24rpx 30rpx 120rpx;
|
|
|
+ box-sizing: border-box;
|
|
|
+}
|
|
|
+.sce-loading {
|
|
|
+ display: flex;
|
|
|
+ justify-content: center;
|
|
|
+ align-items: center;
|
|
|
+ height: 50vh;
|
|
|
+}
|
|
|
+.sce-loading-text {
|
|
|
+ font-size: 28rpx;
|
|
|
+ color: #999;
|
|
|
+}
|
|
|
+.sce-hero {
|
|
|
+ background: linear-gradient(135deg, #F97316, #FF8C42);
|
|
|
+ border-radius: 24rpx;
|
|
|
+ padding: 40rpx 30rpx;
|
|
|
+ margin-bottom: 24rpx;
|
|
|
+ box-shadow: 0 6rpx 20rpx rgba(249, 114, 22, 0.25);
|
|
|
+}
|
|
|
+.sce-hero-inner {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ align-items: center;
|
|
|
+}
|
|
|
+.sce-hero-label {
|
|
|
+ font-size: 26rpx;
|
|
|
+ color: rgba(255, 255, 255, 0.9);
|
|
|
+}
|
|
|
+.sce-hero-score-row {
|
|
|
+ display: flex;
|
|
|
+ align-items: baseline;
|
|
|
+ margin-top: 10rpx;
|
|
|
+}
|
|
|
+.sce-hero-score {
|
|
|
+ font-size: 88rpx;
|
|
|
+ font-weight: 700;
|
|
|
+ color: #fff;
|
|
|
+}
|
|
|
+.sce-hero-total {
|
|
|
+ font-size: 30rpx;
|
|
|
+ color: rgba(255, 255, 255, 0.7);
|
|
|
+ margin-left: 6rpx;
|
|
|
+}
|
|
|
+.sce-hero-date {
|
|
|
+ display: block;
|
|
|
+ text-align: center;
|
|
|
+ font-size: 22rpx;
|
|
|
+ color: rgba(255, 255, 255, 0.8);
|
|
|
+ margin-top: 16rpx;
|
|
|
+}
|
|
|
+.sce-card {
|
|
|
+ background: #fff;
|
|
|
+ border-radius: 24rpx;
|
|
|
+ padding: 30rpx 26rpx;
|
|
|
+ margin-bottom: 24rpx;
|
|
|
+ box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05);
|
|
|
+}
|
|
|
+.sce-card-title {
|
|
|
+ margin-bottom: 24rpx;
|
|
|
+}
|
|
|
+.sce-card-title-text {
|
|
|
+ font-size: 32rpx;
|
|
|
+ font-weight: 600;
|
|
|
+ color: #333;
|
|
|
+}
|
|
|
+.sce-dim-list {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+}
|
|
|
+.sce-dim-item {
|
|
|
+ margin-bottom: 24rpx;
|
|
|
+}
|
|
|
+.sce-dim-item:last-child {
|
|
|
+ margin-bottom: 0;
|
|
|
+}
|
|
|
+.sce-dim-head {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: space-between;
|
|
|
+}
|
|
|
+.sce-dim-label {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+}
|
|
|
+.sce-dim-dot {
|
|
|
+ width: 28rpx;
|
|
|
+ height: 28rpx;
|
|
|
+ border-radius: 50%;
|
|
|
+ margin-right: 12rpx;
|
|
|
+}
|
|
|
+.sce-dim-name {
|
|
|
+ font-size: 30rpx;
|
|
|
+ font-weight: 600;
|
|
|
+ color: #333;
|
|
|
+}
|
|
|
+.sce-dim-score-wrap {
|
|
|
+ display: flex;
|
|
|
+ align-items: baseline;
|
|
|
+}
|
|
|
+.sce-dim-score {
|
|
|
+ font-size: 40rpx;
|
|
|
+ font-weight: 700;
|
|
|
+}
|
|
|
+.sce-dim-max {
|
|
|
+ font-size: 22rpx;
|
|
|
+ color: #BBB;
|
|
|
+}
|
|
|
+.sce-dim-bar {
|
|
|
+ height: 12rpx;
|
|
|
+ background: #F0F0F0;
|
|
|
+ border-radius: 6rpx;
|
|
|
+ margin-top: 14rpx;
|
|
|
+ overflow: hidden;
|
|
|
+}
|
|
|
+.sce-dim-bar-fill {
|
|
|
+ height: 100%;
|
|
|
+ border-radius: 6rpx;
|
|
|
+ transition: width 0.5s ease;
|
|
|
+}
|
|
|
+.sce-status {
|
|
|
+ background: #FFF7ED;
|
|
|
+ border-radius: 16rpx;
|
|
|
+ padding: 24rpx 20rpx;
|
|
|
+ margin-bottom: 24rpx;
|
|
|
+ text-align: center;
|
|
|
+}
|
|
|
+.sce-status-text {
|
|
|
+ font-size: 26rpx;
|
|
|
+ color: #E65100;
|
|
|
+}
|
|
|
+.sce-footer {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 16rpx;
|
|
|
+ margin-top: 20rpx;
|
|
|
+}
|
|
|
+.sce-btn {
|
|
|
+ height: 88rpx;
|
|
|
+ line-height: 88rpx;
|
|
|
+ font-size: 30rpx;
|
|
|
+ border-radius: 44rpx;
|
|
|
+ text-align: center;
|
|
|
+ border: none;
|
|
|
+}
|
|
|
+.sce-btn-primary {
|
|
|
+ background: linear-gradient(135deg, #F97316, #FF8C42);
|
|
|
+ color: #fff;
|
|
|
+ font-weight: 600;
|
|
|
+}
|
|
|
+.sce-btn-outline {
|
|
|
+ background: #fff;
|
|
|
+ color: #F97316;
|
|
|
+ border: 2rpx solid #F97316;
|
|
|
+}
|
|
|
+.sce-btn-disabled {
|
|
|
+ background: #E5E7EB;
|
|
|
+ color: #999;
|
|
|
+ font-weight: 400;
|
|
|
+}
|
|
|
+</style>
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **步骤 2:语法校验**
|
|
|
+
|
|
|
+```bash
|
|
|
+node --check cfc-frontend/pages/family/self-check-entry.vue 2>&1 || echo "Vue 文件跳过 node --check"
|
|
|
+```
|
|
|
+
|
|
|
+预期:Vue 文件无法直接 node --check,跳过即可;重点检查 script 块语法。
|
|
|
+
|
|
|
+- [ ] **步骤 3:Commit**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add cfc-frontend/pages/family/self-check-entry.vue
|
|
|
+git commit -m "feat(self-check): 新增自检中间页 self-check-entry.vue"
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 任务 6:前端 — 注册中间页 + 修改结果页跳转
|
|
|
+
|
|
|
+**文件:**
|
|
|
+- 修改:`cfc-frontend/pages.json`
|
|
|
+- 修改:`cfc-frontend/pages/family/self-check-result.vue`
|
|
|
+
|
|
|
+- [ ] **步骤 1:pages.json 注册中间页**
|
|
|
+
|
|
|
+在 `cfc-frontend/pages.json` 的 `pages/family` 分包中找到 `self-check-result` 注册,在其后追加:
|
|
|
+
|
|
|
+```json
|
|
|
+{
|
|
|
+ "path": "self-check-entry",
|
|
|
+ "style": {
|
|
|
+ "navigationBarTitleText": "自检"
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **步骤 2:self-check-result.vue 修改「重新自检」按钮**
|
|
|
+
|
|
|
+在 `cfc-frontend/pages/family/self-check-result.vue` 中,找到 `retake` 方法(约第 394 行),将其修改为:
|
|
|
+
|
|
|
+```javascript
|
|
|
+retake: function() {
|
|
|
+ uni.navigateTo({ url: '/pages/family/self-check-entry' })
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **步骤 3:Commit**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add cfc-frontend/pages.json cfc-frontend/pages/family/self-check-result.vue
|
|
|
+git commit -m "feat(self-check): 注册 self-check-entry 页面;结果页「重新自检」改跳中间页"
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 任务 7:前端 — 首页入口接入自检状态
|
|
|
+
|
|
|
+**文件:**
|
|
|
+- 修改:`cfc-frontend/pages/index-home/index.vue`
|
|
|
+- 修改:`cfc-frontend/pages/home-pages/parent-index.vue`
|
|
|
+
|
|
|
+- [ ] **步骤 1:index-home/index.vue — 加载自检状态 + 显示分数**
|
|
|
+
|
|
|
+在 `index.vue` 的 `data()` 中追加:
|
|
|
+
|
|
|
+```javascript
|
|
|
+selfCheckScore: null,
|
|
|
+selfCheckLoading: false,
|
|
|
+```
|
|
|
+
|
|
|
+在 `methods` 中追加方法(放入 `loadFamilyMembers` 调用链之后):
|
|
|
+
|
|
|
+```javascript
|
|
|
+loadSelfCheckStatus: function() {
|
|
|
+ var self = this
|
|
|
+ getSelfCheckStatus().then(function(res) {
|
|
|
+ if (res.code === 200 && res.data && res.data.lastResult) {
|
|
|
+ self.selfCheckScore = res.data.lastResult.totalScore
|
|
|
+ }
|
|
|
+ }).catch(function() {})
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+在 `loadDimensionData` 方法中追加调用:
|
|
|
+
|
|
|
+```javascript
|
|
|
+loadDimensionData: function() {
|
|
|
+ this.loadSandboxData()
|
|
|
+ this.loadEnergyOverview()
|
|
|
+ this.loadActionArticles()
|
|
|
+ this.loadChallenges()
|
|
|
+ this.loadSelfCheckStatus()
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+修改 `selfTestSub` 计算属性(约第 538 行):
|
|
|
+
|
|
|
+```javascript
|
|
|
+selfTestSub: function() {
|
|
|
+ if (this.sandboxData) {
|
|
|
+ if (this.selfCheckScore != null) {
|
|
|
+ return '最近自检得分 ' + this.selfCheckScore + '/45 分 · 点击查看'
|
|
|
+ }
|
|
|
+ return '最近综合得分 ' + (this.sandboxData.overallScore || 0) + ' 分 · 点击查看'
|
|
|
+ }
|
|
|
+ return '15 题五维自检 · 约 20 秒'
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+修改自检入口点击事件,改为跳转中间页:
|
|
|
+
|
|
|
+```javascript
|
|
|
+// 找到原 goToSelfCheck 或类似方法,改为:
|
|
|
+goToSelfCheck: function() {
|
|
|
+ uni.navigateTo({ url: '/pages/family/self-check-entry' })
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+同时确保文件头部已导入 `getSelfCheckStatus`:
|
|
|
+
|
|
|
+```javascript
|
|
|
+import { getSelfCheckStatus } from '@/utils/api'
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **步骤 2:home-pages/parent-index.vue — 加载自检状态 + 显示分数**
|
|
|
+
|
|
|
+在 `data()` 中追加:
|
|
|
+
|
|
|
+```javascript
|
|
|
+selfCheckScore: null,
|
|
|
+```
|
|
|
+
|
|
|
+在 `methods` 中追加:
|
|
|
+
|
|
|
+```javascript
|
|
|
+loadSelfCheckStatus: function() {
|
|
|
+ var self = this
|
|
|
+ getSelfCheckStatus().then(function(res) {
|
|
|
+ if (res.code === 200 && res.data && res.data.lastResult) {
|
|
|
+ self.selfCheckScore = res.data.lastResult.totalScore
|
|
|
+ }
|
|
|
+ }).catch(function() {})
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+在 `onLoad` 或 `onShow` 中调用 `loadSelfCheckStatus()`(找到现有初始化调用处追加)。
|
|
|
+
|
|
|
+修改 `selfcheck-subtitle` 或相关文案显示逻辑(找到渲染 `selfcheck-subtitle` 的模板部分),改为:
|
|
|
+
|
|
|
+```html
|
|
|
+<text class="selfcheck-subtitle" v-if="selfCheckScore != null">
|
|
|
+ 最近得分 {{ selfCheckScore }}/45 分
|
|
|
+</text>
|
|
|
+<text class="selfcheck-subtitle" v-else>
|
|
|
+ 15题 · 5分钟 · 生成五行相生寻源建议
|
|
|
+</text>
|
|
|
+```
|
|
|
+
|
|
|
+修改 `goToSelfCheck` 方法(约第 1086 行):
|
|
|
+
|
|
|
+```javascript
|
|
|
+goToSelfCheck: function() {
|
|
|
+ uni.navigateTo({ url: '/pages/family/self-check-entry' })
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+在文件头部导入 `getSelfCheckStatus`:
|
|
|
+
|
|
|
+```javascript
|
|
|
+import { getSelfCheckStatus } from '@/utils/api'
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **步骤 3:语法校验**
|
|
|
+
|
|
|
+```bash
|
|
|
+node --check cfc-frontend/pages/index-home/index.vue 2>&1 || echo "跳过"
|
|
|
+node --check cfc-frontend/pages/home-pages/parent-index.vue 2>&1 || echo "跳过"
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **步骤 4:Commit**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add cfc-frontend/pages/index-home/index.vue cfc-frontend/pages/home-pages/parent-index.vue
|
|
|
+git commit -m "feat(self-check): 首页入口显示自检总分 + 跳转中间页"
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 任务 8:前端 — self-check.vue 支持 retake 参数
|
|
|
+
|
|
|
+**文件:**
|
|
|
+- 修改:`cfc-frontend/pages/family/self-check.vue`
|
|
|
+
|
|
|
+- [ ] **步骤 1:onLoad 读取 retake 参数并传给 questions 接口**
|
|
|
+
|
|
|
+在 `cfc-frontend/pages/family/self-check.vue` 中,修改 `loadQuestions` 方法:
|
|
|
+
|
|
|
+```javascript
|
|
|
+loadQuestions: function() {
|
|
|
+ var self = this
|
|
|
+ var retake = this.$route && this.$route.query && this.$route.query.retake === '1'
|
|
|
+ var apiCall = retake ? getSelfCheckQuestions({ retake: true }) : getSelfCheckQuestions()
|
|
|
+ apiCall.then(function(res) {
|
|
|
+ self.loading = false
|
|
|
+ if (res.code === 200 && res.data) {
|
|
|
+ self.questions = res.data || []
|
|
|
+ } else {
|
|
|
+ uni.showToast({ title: (res && res.message) || '加载失败', icon: 'none' })
|
|
|
+ }
|
|
|
+ }).catch(function() {
|
|
|
+ self.loading = false
|
|
|
+ uni.showToast({ title: '网络异常,请重试', icon: 'none' })
|
|
|
+ })
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+同时修改 `api.js` 中 `getSelfCheckQuestions` 支持传参:
|
|
|
+
|
|
|
+```javascript
|
|
|
+export const getSelfCheckQuestions = (params) => {
|
|
|
+ return request('/api/family/self-check/questions', 'POST', params || {})
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **步骤 2:语法校验**
|
|
|
+
|
|
|
+```bash
|
|
|
+node --check cfc-frontend/utils/api.js
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **步骤 3:Commit**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add cfc-frontend/utils/api.js cfc-frontend/pages/family/self-check.vue
|
|
|
+git commit -m "feat(self-check): 支持 retake 参数获取轮换题库"
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 任务 9:最终验证
|
|
|
+
|
|
|
+- [ ] **步骤 1:后端编译**
|
|
|
+
|
|
|
+```bash
|
|
|
+cd cfc-backend && mvn clean compile -q
|
|
|
+```
|
|
|
+
|
|
|
+预期:BUILD SUCCESS
|
|
|
+
|
|
|
+- [ ] **步骤 2:前端语法校验**
|
|
|
+
|
|
|
+```bash
|
|
|
+node --check cfc-frontend/utils/api.js && echo "api.js OK"
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **步骤 3:提交最终 commit(如有遗漏)**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add -A
|
|
|
+git diff --cached --stat
|
|
|
+git commit -m "feat(self-check): 五维自检 15 天复检周期全栈实现"
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 自检清单
|
|
|
+
|
|
|
+**规格覆盖度:**
|
|
|
+- [x] 15 天复检周期(个人维度)→ 任务 2(getStatus)、任务 3(Controller)
|
|
|
+- [x] 首页显示上次自检分数 → 任务 7
|
|
|
+- [x] 进入时显示上次结果 + 二选一 → 任务 5(中间页)
|
|
|
+- [x] 忽略后重置计时 → 任务 2(ignoreSelfCheck)、任务 3(/ignore)
|
|
|
+- [x] 题目轮换 → 任务 2(getQuestionsForRetake + pickQuestions)
|
|
|
+
|
|
|
+**占位符扫描:** 无"待定"/"TODO"/"后续实现"
|
|
|
+
|
|
|
+**类型一致性:**
|
|
|
+- `SelfCheckIgnore` / `SelfCheckIgnoreMapper` — 任务 1 定义,任务 3 注入
|
|
|
+- `getStatus` / `ignoreSelfCheck` / `getQuestionsForRetake` — 任务 2 定义,任务 3 Controller 调用
|
|
|
+- `getSelfCheckStatus` / `ignoreSelfCheck` — 任务 4 定义,任务 5/7 调用
|