面向 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 |
修改 |
文件:
cfc-backend/src/main/java/com/etotem/cfc/entity/SelfCheckIgnore.javacfc-backend/src/main/java/com/etotem/cfc/mapper/SelfCheckIgnoreMapper.javacfc-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:
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;
}
创建文件 cfc-backend/src/main/java/com/etotem/cfc/mapper/SelfCheckIgnoreMapper.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> {
}
在 DatabaseInitializer.java 中,于 migrateCouponStatus() 方法结束后、migrateSysMenuReorg() 方法开始前,插入:
// 迁移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 行定义,直接复用。
在 cfc-backend/src/main/resources/schema.sql 中找到 five_dimension_self_checks 建表语句(约第 4285 行),在 created_at DATETIME DEFAULT CURRENT_TIMESTAMP 之后追加:
question_ids_json VARCHAR(255) DEFAULT NULL COMMENT '本次自检使用的题号JSON: [1,5,9,...]',
在 schema.sql 末尾追加 self_check_ignores 建表语句:
-- ============================================================
-- 五维自检忽略提醒记录表(迁移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:编译验证
cd cfc-backend && mvn clean compile -q
预期:BUILD SUCCESS
[ ] 步骤 6:Commit
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)"
文件:
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; 之前追加:
/** 本次自检使用的题号JSON,如 [1,2,3,101] */
private String questionIdsJson;
在 FiveDimensionSelfCheckService.java 中,于 SUB_DIMENSION_QUESTION_BANK 字段之后,追加:
/** 合并题库:主题库(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:
import java.util.Collections;
getQuestionsForRetake 方法在 getQuestions() 方法后追加:
/**
* 返回轮换后的题库(用于再次自检):
* 合并题库按维度分组,每维度随机抽 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();
}
}
submitSelfCheck — 按提交题号计分 + 落库题号将 submitSelfCheck 方法中第 348-380 行的计分逻辑替换为:
// 构建 题号→题目 映射(从合并池)
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) 之前)追加:
record.setQuestionIdsJson(toJson(submittedQuestionIds));
同时确保 submittedQuestionIds 升序排列:
Collections.sort(submittedQuestionIds);
[ ] 步骤 5:Service 新增 getStatus 方法
/**
* 返回自检状态:是否有记录、是否可检、距下次提醒天数、上次题号
*/
@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 注入:
@Resource
private SelfCheckIgnoreMapper ignoreMapper;
[ ] 步骤 6:Service 新增 ignore 方法
/**
* 记录忽略动作(幂等: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:编译验证
cd cfc-backend && mvn clean compile -q
预期:BUILD SUCCESS
[ ] 步骤 8:Commit
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 层抽题/状态/忽略/计分改造"
文件:
修改:cfc-backend/src/main/java/com/etotem/cfc/controller/family/FiveDimensionSelfCheckController.java
[ ] 步骤 1:注入 ignoreMapper
在 FiveDimensionSelfCheckController.java 中追加字段:
@Resource
private com.etotem.cfc.mapper.SelfCheckIgnoreMapper selfCheckIgnoreMapper;
/status 接口在类中追加方法:
@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 接口
@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 方法改为:
@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:编译验证
cd cfc-backend && mvn clean compile -q
预期:BUILD SUCCESS
[ ] 步骤 6:Commit
git add cfc-backend/src/main/java/com/etotem/cfc/controller/family/FiveDimensionSelfCheckController.java
git commit -m "feat(self-check): 新增 /status 和 /ignore 接口;/questions 支持 retake 参数"
文件:
修改:cfc-frontend/utils/api.js(在自检相关方法后追加)
[ ] 步骤 1:追加两个 API 方法
在 cfc-frontend/utils/api.js 中,于 getSelfCheckHistory 方法之后追加:
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:语法校验
node --check cfc-frontend/utils/api.js
预期:无输出(语法正确)
[ ] 步骤 3:Commit
git add cfc-frontend/utils/api.js
git commit -m "feat(self-check): 新增 getSelfCheckStatus 和 ignoreSelfCheck API 方法"
文件:
新建:cfc-frontend/pages/family/self-check-entry.vue
[ ] 步骤 1:创建中间页文件
创建 cfc-frontend/pages/family/self-check-entry.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:语法校验
node --check cfc-frontend/pages/family/self-check-entry.vue 2>&1 || echo "Vue 文件跳过 node --check"
预期:Vue 文件无法直接 node --check,跳过即可;重点检查 script 块语法。
[ ] 步骤 3:Commit
git add cfc-frontend/pages/family/self-check-entry.vue
git commit -m "feat(self-check): 新增自检中间页 self-check-entry.vue"
文件:
cfc-frontend/pages.json修改:cfc-frontend/pages/family/self-check-result.vue
[ ] 步骤 1:pages.json 注册中间页
在 cfc-frontend/pages.json 的 pages/family 分包中找到 self-check-result 注册,在其后追加:
{
"path": "self-check-entry",
"style": {
"navigationBarTitleText": "自检"
}
}
在 cfc-frontend/pages/family/self-check-result.vue 中,找到 retake 方法(约第 394 行),将其修改为:
retake: function() {
uni.navigateTo({ url: '/pages/family/self-check-entry' })
}
[ ] 步骤 3:Commit
git add cfc-frontend/pages.json cfc-frontend/pages/family/self-check-result.vue
git commit -m "feat(self-check): 注册 self-check-entry 页面;结果页「重新自检」改跳中间页"
文件:
cfc-frontend/pages/index-home/index.vue修改:cfc-frontend/pages/home-pages/parent-index.vue
[ ] 步骤 1:index-home/index.vue — 加载自检状态 + 显示分数
在 index.vue 的 data() 中追加:
selfCheckScore: null,
selfCheckLoading: false,
在 methods 中追加方法(放入 loadFamilyMembers 调用链之后):
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 方法中追加调用:
loadDimensionData: function() {
this.loadSandboxData()
this.loadEnergyOverview()
this.loadActionArticles()
this.loadChallenges()
this.loadSelfCheckStatus()
}
修改 selfTestSub 计算属性(约第 538 行):
selfTestSub: function() {
if (this.sandboxData) {
if (this.selfCheckScore != null) {
return '最近自检得分 ' + this.selfCheckScore + '/45 分 · 点击查看'
}
return '最近综合得分 ' + (this.sandboxData.overallScore || 0) + ' 分 · 点击查看'
}
return '15 题五维自检 · 约 20 秒'
}
修改自检入口点击事件,改为跳转中间页:
// 找到原 goToSelfCheck 或类似方法,改为:
goToSelfCheck: function() {
uni.navigateTo({ url: '/pages/family/self-check-entry' })
}
同时确保文件头部已导入 getSelfCheckStatus:
import { getSelfCheckStatus } from '@/utils/api'
在 data() 中追加:
selfCheckScore: null,
在 methods 中追加:
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 的模板部分),改为:
<text class="selfcheck-subtitle" v-if="selfCheckScore != null">
最近得分 {{ selfCheckScore }}/45 分
</text>
<text class="selfcheck-subtitle" v-else>
15题 · 5分钟 · 生成五行相生寻源建议
</text>
修改 goToSelfCheck 方法(约第 1086 行):
goToSelfCheck: function() {
uni.navigateTo({ url: '/pages/family/self-check-entry' })
}
在文件头部导入 getSelfCheckStatus:
import { getSelfCheckStatus } from '@/utils/api'
[ ] 步骤 3:语法校验
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
git add cfc-frontend/pages/index-home/index.vue cfc-frontend/pages/home-pages/parent-index.vue
git commit -m "feat(self-check): 首页入口显示自检总分 + 跳转中间页"
文件:
修改:cfc-frontend/pages/family/self-check.vue
[ ] 步骤 1:onLoad 读取 retake 参数并传给 questions 接口
在 cfc-frontend/pages/family/self-check.vue 中,修改 loadQuestions 方法:
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 支持传参:
export const getSelfCheckQuestions = (params) => {
return request('/api/family/self-check/questions', 'POST', params || {})
}
[ ] 步骤 2:语法校验
node --check cfc-frontend/utils/api.js
[ ] 步骤 3:Commit
git add cfc-frontend/utils/api.js cfc-frontend/pages/family/self-check.vue
git commit -m "feat(self-check): 支持 retake 参数获取轮换题库"
[ ] 步骤 1:后端编译
cd cfc-backend && mvn clean compile -q
预期:BUILD SUCCESS
[ ] 步骤 2:前端语法校验
node --check cfc-frontend/utils/api.js && echo "api.js OK"
[ ] 步骤 3:提交最终 commit(如有遗漏)
git add -A
git diff --cached --stat
git commit -m "feat(self-check): 五维自检 15 天复检周期全栈实现"
规格覆盖度:
占位符扫描: 无"待定"/"TODO"/"后续实现"
类型一致性:
SelfCheckIgnore / SelfCheckIgnoreMapper — 任务 1 定义,任务 3 注入getStatus / ignoreSelfCheck / getQuestionsForRetake — 任务 2 定义,任务 3 Controller 调用getSelfCheckStatus / ignoreSelfCheck — 任务 4 定义,任务 5/7 调用