|
|
@@ -0,0 +1,547 @@
|
|
|
+# 饮食/睡眠/运动打卡接入任务完成链路 实现计划
|
|
|
+
|
|
|
+> **面向 AI 代理的工作者:** 必需子技能:使用 superpowers:subagent-driven-development(推荐)或 superpowers:executing-plans 逐任务实现此计划。步骤使用复选框(`- [ ]`)语法来跟踪进度。
|
|
|
+
|
|
|
+**目标:** 让饮食/睡眠/运动打卡可作为任务完成凭证,覆盖两类任务来源:手动创建的一次性任务(点击跳转打卡页自动完成)+ 系统每日自动生成的打卡任务(打卡累计进度达标自动完成)。
|
|
|
+
|
|
|
+**架构:** 双轨复用现有机制——一次性任务走 `actionType` 联动(diet/sleep/exercise 三个新值),任务列表跳转对应打卡页带 `taskId`,打卡 Controller 成功后在 `create()` 内回写 `completeTask`;每日任务走 `is_daily_progress=1` + `updateProgress(userId, taskKey, 1)` 累计进度。判定以任务真实类型为准(后端反查 tasks 表)。
|
|
|
+
|
|
|
+**技术栈:** Java 8 + Spring Boot 2.7.18 + MyBatis-Plus / uni-app Vue 2 小程序
|
|
|
+
|
|
|
+**规格文档:** `docs/superpowers/specs/2026-10-13-health-checkin-task-linkage-design.md`
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 文件结构
|
|
|
+
|
|
|
+| 文件 | 操作 | 职责 |
|
|
|
+|---|---|---|
|
|
|
+| `cfc-backend/.../entity/HealthMealRecord.java` | 修改 | 新增 `@TableField(exist=false) Long taskId` |
|
|
|
+| `cfc-backend/.../entity/HealthSleepRecord.java` | 修改 | 新增 `@TableField(exist=false) Long taskId` |
|
|
|
+| `cfc-backend/.../entity/HealthExerciseRecord.java` | 修改 | 新增 `@TableField(exist=false) Long taskId` |
|
|
|
+| `cfc-backend/.../entity/Task.java` | 修改 | `actionType` 注释补充 diet/sleep/exercise |
|
|
|
+| `cfc-backend/.../dto/CreateTaskDTO.java` | 修改 | `actionType` 注释更新 |
|
|
|
+| `cfc-backend/.../controller/HealthMealController.java` | 修改 | `create()` 挂钩任务回写(taskKey=DIET) |
|
|
|
+| `cfc-backend/.../controller/HealthSleepController.java` | 修改 | `create()` 挂钩任务回写(taskKey=SLEEP) |
|
|
|
+| `cfc-backend/.../controller/HealthExerciseController.java` | 修改 | `create()` 挂钩任务回写(taskKey=EXERCISE) |
|
|
|
+| `cfc-backend/.../config/DatabaseInitializer.java` | 修改 | 新增迁移:三条打卡任务模板种子 |
|
|
|
+| `cfc-backend/.../task/RepeatTaskGenerator.java` | 修改 | 每日打卡任务实例生成 |
|
|
|
+| `cfc-frontend/pages/tasks/tasks.vue` | 修改 | `getActionLabel` + `navigateByAction` 加 diet/sleep/exercise 分支 |
|
|
|
+| `cfc-frontend/pages/growth/diet-checkin.vue` | 修改 | `onLoad` 读 taskId,提交带上 |
|
|
|
+| `cfc-frontend/pages/growth/sleep-checkin.vue` | 修改 | 同上 |
|
|
|
+| `cfc-frontend/pages/growth/exercise-checkin.vue` | 修改 | 同上 |
|
|
|
+| `docs/superpowers/api/API_REFERENCE.md` | 修改 | 三个打卡接口补充 taskId 参数 |
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 任务 1:实体新增非持久化 taskId 字段
|
|
|
+
|
|
|
+**文件:**
|
|
|
+- 修改:`cfc-backend/src/main/java/com/etotem/cfc/entity/HealthMealRecord.java`
|
|
|
+- 修改:`cfc-backend/src/main/java/com/etotem/cfc/entity/HealthSleepRecord.java`
|
|
|
+- 修改:`cfc-backend/src/main/java/com/etotem/cfc/entity/HealthExerciseRecord.java`
|
|
|
+
|
|
|
+- [ ] **步骤 1:三个实体各加一个 `taskId` 字段**
|
|
|
+
|
|
|
+HealthMealRecord.java 在 `private Date createdAt;` 之前加:
|
|
|
+
|
|
|
+```java
|
|
|
+import com.baomidou.mybatisplus.annotation.TableField;
|
|
|
+
|
|
|
+/** 关联任务ID(一次性任务打卡后自动完成;null=走每日任务累计路径) */
|
|
|
+@TableField(exist = false)
|
|
|
+private Long taskId;
|
|
|
+```
|
|
|
+
|
|
|
+HealthSleepRecord.java 同样(import 在 `import lombok.Data;` 旁):
|
|
|
+
|
|
|
+```java
|
|
|
+import com.baomidou.mybatisplus.annotation.TableField;
|
|
|
+
|
|
|
+/** 关联任务ID(一次性任务打卡后自动完成;null=走每日任务累计路径) */
|
|
|
+@TableField(exist = false)
|
|
|
+private Long taskId;
|
|
|
+```
|
|
|
+
|
|
|
+HealthExerciseRecord.java 同样。
|
|
|
+
|
|
|
+- [ ] **步骤 2:编译验证**
|
|
|
+
|
|
|
+运行:`cd /sc-data/cfc/cfc-backend && mvn clean compile 2>&1 | tail -10`
|
|
|
+预期:`BUILD SUCCESS`
|
|
|
+
|
|
|
+- [ ] **步骤 3:Commit**
|
|
|
+
|
|
|
+```bash
|
|
|
+cd /sc-data/cfc && git add cfc-backend/src/main/java/com/etotem/cfc/entity/HealthMealRecord.java cfc-backend/src/main/java/com/etotem/cfc/entity/HealthSleepRecord.java cfc-backend/src/main/java/com/etotem/cfc/entity/HealthExerciseRecord.java && \
|
|
|
+git commit -m "feat(task): 三个健康打卡实体新增 taskId 非持久化字段"
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 任务 2:Task.java / CreateTaskDTO.java 注释补充
|
|
|
+
|
|
|
+**文件:**
|
|
|
+- 修改:`cfc-backend/src/main/java/com/etotem/cfc/entity/Task.java`(约第 131 行)
|
|
|
+- 修改:`cfc-backend/src/main/java/com/etotem/cfc/dto/CreateTaskDTO.java`(约第 67 行)
|
|
|
+
|
|
|
+- [ ] **步骤 1:更新 Task.java 的 actionType 注释**
|
|
|
+
|
|
|
+```java
|
|
|
+// 原:
|
|
|
+/** TASK-SYS-248: 联动动作: minigame/focus/survey/article/activity/cart/order */
|
|
|
+// 改为:
|
|
|
+/** TASK-SYS-248: 联动动作: minigame/focus/survey/article/activity/cart/order/diet/sleep/exercise */
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **步骤 2:更新 CreateTaskDTO.java 的 actionType 注释**
|
|
|
+
|
|
|
+```java
|
|
|
+// 原(第 66 行):
|
|
|
+// TASK-SYS-248: 联动动作: minigame/focus/survey/article/activity/cart/order
|
|
|
+// 改为:
|
|
|
+// TASK-SYS-248: 联动动作: minigame/focus/survey/article/activity/cart/order/diet/sleep/exercise
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **步骤 3:编译验证**
|
|
|
+
|
|
|
+运行:`cd /sc-data/cfc/cfc-backend && mvn clean compile 2>&1 | tail -5`
|
|
|
+预期:`BUILD SUCCESS`
|
|
|
+
|
|
|
+- [ ] **步骤 4:Commit**
|
|
|
+
|
|
|
+```bash
|
|
|
+cd /sc-data/cfc && git add cfc-backend/src/main/java/com/etotem/cfc/entity/Task.java cfc-backend/src/main/java/com/etotem/cfc/dto/CreateTaskDTO.java && \
|
|
|
+git commit -m "docs(task): actionType 注释补充 diet/sleep/exercise 取值"
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 任务 3:三个 Health 打卡 Controller 挂钩任务回写
|
|
|
+
|
|
|
+**文件:**
|
|
|
+- 修改:`cfc-backend/src/main/java/com/etotem/cfc/controller/HealthMealController.java`(create 方法第 204-251 行)
|
|
|
+- 修改:`cfc-backend/src/main/java/com/etotem/cfc/controller/HealthSleepController.java`(create 方法第 42-63 行)
|
|
|
+- 修改:`cfc-backend/src/main/java/com/etotem/cfc/controller/HealthExerciseController.java`(create 方法第 59-85 行)
|
|
|
+
|
|
|
+- [ ] **步骤 1:三个 Controller 注入依赖**
|
|
|
+
|
|
|
+在三个 Controller 的字段区新增:
|
|
|
+
|
|
|
+```java
|
|
|
+@Resource
|
|
|
+private com.etotem.cfc.service.TaskService taskService;
|
|
|
+
|
|
|
+@Resource
|
|
|
+private com.etotem.cfc.mapper.TaskMapper taskMapper;
|
|
|
+
|
|
|
+@Resource
|
|
|
+private com.etotem.cfc.mapper.FamilyMemberMapper familyMemberMapper;
|
|
|
+```
|
|
|
+
|
|
|
+确认类上已有 `@Slf4j`(HealthMealController 目前无 @Slf4j,需补加;Sleep 需确认;Exercise 需确认)。
|
|
|
+
|
|
|
+- [ ] **步骤 2:HealthMealController.create() — 打卡成功后挂钩任务回写**
|
|
|
+
|
|
|
+在 `healthMealRecordMapper.insert(record);`(约第 231 行)之后、积分发放之前插入:
|
|
|
+
|
|
|
+```java
|
|
|
+// ===== 任务回写挂钩 =====
|
|
|
+try {
|
|
|
+ boolean isBackfill = record.getIsBackfill() != null && record.getIsBackfill() == 1;
|
|
|
+ if (!isBackfill) {
|
|
|
+ if (record.getTaskId() != null) {
|
|
|
+ com.etotem.cfc.entity.Task linked = taskMapper.selectById(record.getTaskId());
|
|
|
+ if (linked != null && linked.getIsDailyProgress() != null && linked.getIsDailyProgress() == 1) {
|
|
|
+ // 每日进度型任务:累计进度
|
|
|
+ com.etotem.cfc.entity.FamilyMember fm = familyMemberMapper.selectById(record.getMemberId());
|
|
|
+ if (fm != null && fm.getUserId() != null) {
|
|
|
+ taskService.updateProgress(fm.getUserId(), "DIET", 1);
|
|
|
+ }
|
|
|
+ } else if (linked != null) {
|
|
|
+ // 一次性任务:直接完成(凭证用打卡记录内容)
|
|
|
+ String photo = record.getPhotoUrls() != null ? record.getPhotoUrls() : "";
|
|
|
+ String content = record.getFoodItems() != null ? record.getFoodItems() : (record.getRemark() != null ? record.getRemark() : "");
|
|
|
+ taskService.completeTask(record.getTaskId(), record.getMemberId(), photo, content, "text");
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ // 未带 taskId:走每日任务累计路径
|
|
|
+ com.etotem.cfc.entity.FamilyMember fm = familyMemberMapper.selectById(record.getMemberId());
|
|
|
+ if (fm != null && fm.getUserId() != null) {
|
|
|
+ taskService.updateProgress(fm.getUserId(), "DIET", 1);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+} catch (Exception e) {
|
|
|
+ log.warn("饮食打卡任务回写失败: {}", e.getMessage());
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **步骤 3:HealthSleepController.create() — taskKey 换 `"SLEEP"`**
|
|
|
+
|
|
|
+在 `healthSleepRecordMapper.insert(record);`(约第 52 行)之后、画像重算之前插入(Sleep 无 isBackfill 字段,直接执行,不带补打判断):
|
|
|
+
|
|
|
+```java
|
|
|
+// ===== 任务回写挂钩 =====
|
|
|
+try {
|
|
|
+ if (record.getTaskId() != null) {
|
|
|
+ com.etotem.cfc.entity.Task linked = taskMapper.selectById(record.getTaskId());
|
|
|
+ if (linked != null && linked.getIsDailyProgress() != null && linked.getIsDailyProgress() == 1) {
|
|
|
+ com.etotem.cfc.entity.FamilyMember fm = familyMemberMapper.selectById(record.getMemberId());
|
|
|
+ if (fm != null && fm.getUserId() != null) {
|
|
|
+ taskService.updateProgress(fm.getUserId(), "SLEEP", 1);
|
|
|
+ }
|
|
|
+ } else if (linked != null) {
|
|
|
+ taskService.completeTask(record.getTaskId(), record.getMemberId(),
|
|
|
+ "", record.getRemark() != null ? record.getRemark() : "睡眠打卡", "text");
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ com.etotem.cfc.entity.FamilyMember fm = familyMemberMapper.selectById(record.getMemberId());
|
|
|
+ if (fm != null && fm.getUserId() != null) {
|
|
|
+ taskService.updateProgress(fm.getUserId(), "SLEEP", 1);
|
|
|
+ }
|
|
|
+ }
|
|
|
+} catch (Exception e) {
|
|
|
+ log.warn("睡眠打卡任务回写失败: {}", e.getMessage());
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **步骤 4:HealthExerciseController.create() — taskKey 换 `"EXERCISE"`**
|
|
|
+
|
|
|
+在 `healthExerciseRecordMapper.insert(record);`(约第 74 行)之后、画像重算之前插入:
|
|
|
+
|
|
|
+```java
|
|
|
+// ===== 任务回写挂钩 =====
|
|
|
+try {
|
|
|
+ if (record.getTaskId() != null) {
|
|
|
+ com.etotem.cfc.entity.Task linked = taskMapper.selectById(record.getTaskId());
|
|
|
+ if (linked != null && linked.getIsDailyProgress() != null && linked.getIsDailyProgress() == 1) {
|
|
|
+ com.etotem.cfc.entity.FamilyMember fm = familyMemberMapper.selectById(record.getMemberId());
|
|
|
+ if (fm != null && fm.getUserId() != null) {
|
|
|
+ taskService.updateProgress(fm.getUserId(), "EXERCISE", 1);
|
|
|
+ }
|
|
|
+ } else if (linked != null) {
|
|
|
+ String photo = record.getPhotoUrls() != null ? record.getPhotoUrls() : "";
|
|
|
+ String content = record.getRemark() != null ? record.getRemark() : "运动打卡";
|
|
|
+ taskService.completeTask(record.getTaskId(), record.getMemberId(), photo, content, "text");
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ com.etotem.cfc.entity.FamilyMember fm = familyMemberMapper.selectById(record.getMemberId());
|
|
|
+ if (fm != null && fm.getUserId() != null) {
|
|
|
+ taskService.updateProgress(fm.getUserId(), "EXERCISE", 1);
|
|
|
+ }
|
|
|
+ }
|
|
|
+} catch (Exception e) {
|
|
|
+ log.warn("运动打卡任务回写失败: {}", e.getMessage());
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+> 注意:HealthExerciseRecord **无 isBackfill 字段**,故不判断补打(与设计文档决策 3 略有出入——设计文档要求补打不触发回写。实现时若 Exercise 无补打能力则无需判断;确认实体字段后决定)。
|
|
|
+
|
|
|
+- [ ] **步骤 5:编译验证**
|
|
|
+
|
|
|
+运行:`cd /sc-data/cfc/cfc-backend && mvn clean compile 2>&1 | tail -15`
|
|
|
+预期:`BUILD SUCCESS`
|
|
|
+
|
|
|
+- [ ] **步骤 6:Commit**
|
|
|
+
|
|
|
+```bash
|
|
|
+cd /sc-data/cfc && git add cfc-backend/src/main/java/com/etotem/cfc/controller/HealthMealController.java cfc-backend/src/main/java/com/etotem/cfc/controller/HealthSleepController.java cfc-backend/src/main/java/com/etotem/cfc/controller/HealthExerciseController.java && \
|
|
|
+git commit -m "feat(task): 三个健康打卡Controller挂钩任务回写(一次性完成+每日进度累计)"
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 任务 4:打卡任务模板种子数据
|
|
|
+
|
|
|
+**文件:**
|
|
|
+- 修改:`cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java`
|
|
|
+
|
|
|
+- [ ] **步骤 1:查找最新迁移编号**
|
|
|
+
|
|
|
+运行:`grep -n "// 迁移" cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java | tail -3`
|
|
|
+取当前最大编号 N。
|
|
|
+
|
|
|
+- [ ] **步骤 2:在 runMigrations() 末尾追加迁移 N+1**
|
|
|
+
|
|
|
+```java
|
|
|
+// 迁移 N+1: 插入饮食/睡眠/运动每日打卡任务模板(action_type 驱动)
|
|
|
+try {
|
|
|
+ Integer checkinTplCount = jdbcTemplate.queryForObject(
|
|
|
+ "SELECT COUNT(*) FROM admin_task_templates WHERE category = '健康打卡'",
|
|
|
+ Integer.class);
|
|
|
+ if (checkinTplCount == null || checkinTplCount == 0) {
|
|
|
+ jdbcTemplate.execute(
|
|
|
+ "INSERT INTO admin_task_templates " +
|
|
|
+ "(title, description, points, category, need_review, is_active, task_type, frequency, action_type, created_at, updated_at) VALUES " +
|
|
|
+ "('[DIET]饮食打卡', '每日记录饮食打卡', 5, '健康打卡', 0, 1, 'recurring', 'daily', 'diet', NOW(), NOW()), " +
|
|
|
+ "('[SLEEP]睡眠打卡', '每日记录睡眠打卡', 5, '健康打卡', 0, 1, 'recurring', 'daily', 'sleep', NOW(), NOW()), " +
|
|
|
+ "('[EXERCISE]运动打卡', '每日记录运动打卡', 5, '健康打卡', 0, 1, 'recurring', 'daily', 'exercise', NOW(), NOW())");
|
|
|
+ log.info("迁移 N+1: 已插入3条健康打卡任务模板(DIET/SLEEP/EXERCISE)");
|
|
|
+ }
|
|
|
+} catch (Exception e) {
|
|
|
+ log.warn("迁移 N+1: 打卡任务模板插入失败或已存在: {}", e.getMessage());
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+> ⚠️ 先确认 `admin_task_templates` 表是否已有 `action_type` 列;若无,先 `ensureColumn("admin_task_templates", "action_type", "VARCHAR(32) DEFAULT NULL COMMENT '联动动作类型'")`。**注意:`TaskTemplate` 实体可能没有 actionType 字段**——RepeatTaskGenerator 查询模板用的是 `Task` 实体(tasks 表),模板种子实际需以 `tasks` 表为准(见任务 5)。
|
|
|
+
|
|
|
+- [ ] **步骤 3:编译验证**
|
|
|
+
|
|
|
+运行:`cd /sc-data/cfc/cfc-backend && mvn clean compile 2>&1 | tail -5`
|
|
|
+预期:`BUILD SUCCESS`
|
|
|
+
|
|
|
+- [ ] **步骤 4:Commit**
|
|
|
+
|
|
|
+```bash
|
|
|
+cd /sc-data/cfc && git add cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java && \
|
|
|
+git commit -m "feat(task): 添加饮食/睡眠/运动每日打卡任务模板种子"
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 任务 5:RepeatTaskGenerator 新增打卡任务实例生成
|
|
|
+
|
|
|
+**文件:**
|
|
|
+- 修改:`cfc-backend/src/main/java/com/etotem/cfc/task/RepeatTaskGenerator.java`
|
|
|
+
|
|
|
+> ⚠️ **关键澄清**:`RepeatTaskGenerator` 现有逻辑只处理 `is_template=1` 的**模板行**(在 `tasks` 表内),用 `frequency`/`repeat_type` 分派生成实例。打卡任务模板种子(任务 4)应插入 `tasks` 表(is_template=1, action_type=diet/sleep/exercise, frequency=daily),而非 `admin_task_templates`——因为生成器查询的是 `tasks` 表。实现时先确认种子应落 `tasks` 表。
|
|
|
+
|
|
|
+- [ ] **步骤 1:注入 familyMemberMapper**
|
|
|
+
|
|
|
+```java
|
|
|
+@Resource
|
|
|
+private com.etotem.cfc.mapper.FamilyMemberMapper familyMemberMapper;
|
|
|
+```
|
|
|
+
|
|
|
+文件顶部确认 `import java.util.Arrays;` 存在,若无则加。
|
|
|
+
|
|
|
+- [ ] **步骤 2:在 generateDailyTasks() 的每日任务处理块(第 56-68 行)前插入打卡任务生成**
|
|
|
+
|
|
|
+```java
|
|
|
+// 每日打卡任务生成(diet/sleep/exercise)——为每个家庭成员生成当日进度型任务
|
|
|
+List<Task> checkinTemplates = taskMapper.selectList(
|
|
|
+ new LambdaQueryWrapper<Task>()
|
|
|
+ .eq(Task::getIsTemplate, 1)
|
|
|
+ .in(Task::getActionType, Arrays.asList("diet", "sleep", "exercise"))
|
|
|
+);
|
|
|
+for (Task tpl : checkinTemplates) {
|
|
|
+ List<com.etotem.cfc.entity.FamilyMember> members =
|
|
|
+ familyMemberMapper.selectList(
|
|
|
+ new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<com.etotem.cfc.entity.FamilyMember>()
|
|
|
+ .eq(com.etotem.cfc.entity.FamilyMember::getFamilyId, tpl.getFamilyId()));
|
|
|
+ for (com.etotem.cfc.entity.FamilyMember member : members) {
|
|
|
+ if (member.getUserId() == null) continue; // 仅对能登录用户生成
|
|
|
+ Long exists = taskMapper.selectCount(
|
|
|
+ new LambdaQueryWrapper<Task>()
|
|
|
+ .eq(Task::getTemplateId, tpl.getId())
|
|
|
+ .eq(Task::getFamilyMemberId, member.getId())
|
|
|
+ .eq(Task::getIsDailyProgress, 1)
|
|
|
+ .ge(Task::getCreatedAt, today)
|
|
|
+ .lt(Task::getCreatedAt, new Date(today.getTime() + 86400000L))
|
|
|
+ );
|
|
|
+ if (exists == null || exists == 0) {
|
|
|
+ Task instance = new Task();
|
|
|
+ instance.setFamilyId(tpl.getFamilyId());
|
|
|
+ instance.setCreatorId(tpl.getCreatorId());
|
|
|
+ instance.setExecutorType("child");
|
|
|
+ instance.setExecutorId(member.getId());
|
|
|
+ instance.setFamilyMemberId(member.getId());
|
|
|
+ instance.setChildId(member.getId());
|
|
|
+ instance.setTitle(tpl.getTitle());
|
|
|
+ instance.setDescription(tpl.getDescription());
|
|
|
+ instance.setPoints(tpl.getPoints());
|
|
|
+ instance.setCategory(tpl.getCategory());
|
|
|
+ instance.setStatus("pending");
|
|
|
+ instance.setNeedReview(0);
|
|
|
+ instance.setIsTemplate(0);
|
|
|
+ instance.setIsDailyProgress(1);
|
|
|
+ instance.setTargetValue(1);
|
|
|
+ instance.setActionType(tpl.getActionType());
|
|
|
+ instance.setTemplateId(tpl.getId());
|
|
|
+ instance.setMemberOnly(0);
|
|
|
+ instance.setCreatedAt(new Date());
|
|
|
+ instance.setUpdatedAt(new Date());
|
|
|
+ taskMapper.insert(instance);
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+log.info("每日打卡任务生成完成,处理{}个模板", checkinTemplates.size());
|
|
|
+```
|
|
|
+
|
|
|
+> 注:`generateDailyTasks()` 方法内已有 `today` 变量(第 51 行),直接复用。
|
|
|
+
|
|
|
+- [ ] **步骤 3:编译验证**
|
|
|
+
|
|
|
+运行:`cd /sc-data/cfc/cfc-backend && mvn clean compile 2>&1 | tail -5`
|
|
|
+预期:`BUILD SUCCESS`
|
|
|
+
|
|
|
+- [ ] **步骤 4:Commit**
|
|
|
+
|
|
|
+```bash
|
|
|
+cd /sc-data/cfc && git add cfc-backend/src/main/java/com/etotem/cfc/task/RepeatTaskGenerator.java && \
|
|
|
+git commit -m "feat(task): RepeatTaskGenerator 每日生成饮食/睡眠/运动打卡任务实例"
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 任务 6:前端 tasks.vue — 按钮文案与跳转
|
|
|
+
|
|
|
+**文件:**
|
|
|
+- 修改:`cfc-frontend/pages/tasks/tasks.vue`(第 413-503 行)
|
|
|
+
|
|
|
+- [ ] **步骤 1:修改 getActionLabel() 方法**
|
|
|
+
|
|
|
+在第 422 行 `if (at === 'cart' || at === 'order') return '去购物'` 之后加:
|
|
|
+
|
|
|
+```js
|
|
|
+ if (at === 'diet') return '去打卡'
|
|
|
+ if (at === 'sleep') return '去打卡'
|
|
|
+ if (at === 'exercise') return '去打卡'
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **步骤 2:修改 navigateByAction() 方法**
|
|
|
+
|
|
|
+在第 479 行 `else if (at === 'cart' || at === 'order')` 分支之后加:
|
|
|
+
|
|
|
+```js
|
|
|
+ } else if (at === 'diet') {
|
|
|
+ uni.navigateTo({ url: '/pages/growth/diet-checkin?taskId=' + taskId })
|
|
|
+ } else if (at === 'sleep') {
|
|
|
+ uni.navigateTo({ url: '/pages/growth/sleep-checkin?taskId=' + taskId })
|
|
|
+ } else if (at === 'exercise') {
|
|
|
+ uni.navigateTo({ url: '/pages/growth/exercise-checkin?taskId=' + taskId })
|
|
|
+ }
|
|
|
+```
|
|
|
+
|
|
|
+注意闭合 if-else 链的 `}`。
|
|
|
+
|
|
|
+- [ ] **步骤 3:语法校验**
|
|
|
+
|
|
|
+运行:`cd /sc-data/cfc/cfc-frontend && node -e "const s=require('fs').readFileSync('pages/tasks/tasks.vue','utf8'); const m=s.match(/<script>([\s\S]*)<\/script>/); if(m) new Function(m[1])" 2>&1 | head -5`
|
|
|
+预期:无语法错误
|
|
|
+
|
|
|
+- [ ] **步骤 4:Commit**
|
|
|
+
|
|
|
+```bash
|
|
|
+cd /sc-data/cfc && git add cfc-frontend/pages/tasks/tasks.vue && \
|
|
|
+git commit -m "feat(tasks): 任务列表按钮支持 diet/sleep/exercise 打卡跳转"
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 任务 7:前端三个打卡页 — 接收 taskId 并传入
|
|
|
+
|
|
|
+**文件:**
|
|
|
+- 修改:`cfc-frontend/pages/growth/diet-checkin.vue`
|
|
|
+- 修改:`cfc-frontend/pages/growth/sleep-checkin.vue`
|
|
|
+- 修改:`cfc-frontend/pages/growth/exercise-checkin.vue`
|
|
|
+
|
|
|
+- [ ] **步骤 1:diet-checkin.vue — data 加 taskId,onLoad 读取**
|
|
|
+
|
|
|
+在 `data()` 新增:`taskId: null, // 来自任务列表跳转的 taskId(一次性任务)`
|
|
|
+
|
|
|
+修改 `onLoad(options)`:`if (options.taskId) { this.taskId = options.taskId }`
|
|
|
+
|
|
|
+- [ ] **步骤 2:diet-checkin.vue — submitCheckin() 提交时带 taskId**
|
|
|
+
|
|
|
+在 `uni.request` 的 data 对象(约第 450 行)新增:
|
|
|
+
|
|
|
+```js
|
|
|
+taskId: self.taskId || undefined, // 新增:关联任务
|
|
|
+```
|
|
|
+
|
|
|
+成功 toast 区分:
|
|
|
+
|
|
|
+```js
|
|
|
+uni.showToast({ title: self.taskId ? '打卡成功,任务已完成' : '打卡成功', icon: 'success' })
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **步骤 3:sleep-checkin.vue / exercise-checkin.vue — 同样改动**
|
|
|
+
|
|
|
+先读取两文件 `data()`/`onLoad`/`submitCheckin` 结构,按步骤 1-2 模式添加。
|
|
|
+
|
|
|
+- [ ] **步骤 4:语法校验**
|
|
|
+
|
|
|
+```bash
|
|
|
+cd /sc-data/cfc/cfc-frontend && for f in pages/growth/diet-checkin.vue pages/growth/sleep-checkin.vue pages/growth/exercise-checkin.vue; do node -e "const s=require('fs').readFileSync('$f','utf8'); const m=s.match(/<script>([\s\S]*)<\/script>/); if(m) new Function(m[1])" 2>&1 | head -3 && echo "OK: $f"; done
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **步骤 5:Commit**
|
|
|
+
|
|
|
+```bash
|
|
|
+cd /sc-data/cfc && git add cfc-frontend/pages/growth/diet-checkin.vue cfc-frontend/pages/growth/sleep-checkin.vue cfc-frontend/pages/growth/exercise-checkin.vue && \
|
|
|
+git commit -m "feat(checkin): 三个打卡页接收 taskId 参数,打卡成功提示任务完成"
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 任务 8:API 文档更新
|
|
|
+
|
|
|
+**文件:**
|
|
|
+- 修改:`docs/superpowers/api/API_REFERENCE.md`
|
|
|
+
|
|
|
+- [ ] **步骤 1:找到三个打卡接口条目,补充 taskId 参数**
|
|
|
+
|
|
|
+在 `POST /api/health/meal/create`、`POST /api/health/sleep/create`、`POST /api/health/exercise/create` 条目下补充:
|
|
|
+
|
|
|
+```markdown
|
|
|
+**新增可选参数**(2026-10-13):
|
|
|
+
|
|
|
+| 参数 | 类型 | 说明 |
|
|
|
+|------|------|------|
|
|
|
+| taskId | Long | 关联任务 ID。一次性任务(actionType=diet)打卡后自动完成该任务;每日进度型任务打卡后累计进度 |
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **步骤 2:Commit**
|
|
|
+
|
|
|
+```bash
|
|
|
+cd /sc-data/cfc && git add docs/superpowers/api/API_REFERENCE.md && \
|
|
|
+git commit -m "docs: 健康打卡接口文档补充 taskId 参数说明"
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 任务 9:全量验证
|
|
|
+
|
|
|
+- [ ] **步骤 1:后端全量编译**
|
|
|
+
|
|
|
+运行:`cd /sc-data/cfc/cfc-backend && mvn clean compile 2>&1 | tail -5`
|
|
|
+预期:`BUILD SUCCESS`
|
|
|
+
|
|
|
+- [ ] **步骤 2:前端全部改动文件语法校验**
|
|
|
+
|
|
|
+```bash
|
|
|
+cd /sc-data/cfc/cfc-frontend && for f in pages/tasks/tasks.vue pages/growth/diet-checkin.vue pages/growth/sleep-checkin.vue pages/growth/exercise-checkin.vue; do node -e "const s=require('fs').readFileSync('$f','utf8'); const m=s.match(/<script>([\s\S]*)<\/script>/); if(m) new Function(m[1])" 2>&1 | head -3 && echo "OK: $f"; done
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **步骤 3:更新 PROJECT-OVERVIEW.md 状态**
|
|
|
+
|
|
|
+将 `docs/superpowers/PROJECT-OVERVIEW.md` 中打卡任务链路条目从"📐 设计已提交"改为"✅ 全栈完成"。
|
|
|
+
|
|
|
+```bash
|
|
|
+cd /sc-data/cfc && git add docs/superpowers/PROJECT-OVERVIEW.md && \
|
|
|
+git commit -m "docs: 打卡任务链路标记为已完成"
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 规格覆盖度自检
|
|
|
+
|
|
|
+| 规格要求 | 覆盖任务 |
|
|
|
+|---|---|
|
|
|
+| actionType 新增 diet/sleep/exercise(Task.java + CreateTaskDTO) | 任务 2 |
|
|
|
+| 三个 Record 实体加 taskId 非持久化字段 | 任务 1 |
|
|
|
+| 三个 Controller create() 挂钩任务回写 | 任务 3 |
|
|
|
+| 打卡任务模板种子数据 | 任务 4 |
|
|
|
+| RepeatTaskGenerator 每日生成打卡任务实例 | 任务 5 |
|
|
|
+| tasks.vue getActionLabel + navigateByAction | 任务 6 |
|
|
|
+| 三个打卡页 onLoad 接收 taskId,提交带上 | 任务 7 |
|
|
|
+| API 文档更新 | 任务 8 |
|
|
|
+| 全量编译 + PROJECT-OVERVIEW 更新 | 任务 9 |
|
|
|
+
|
|
|
+**关键实现注意:**
|
|
|
+1. **种子落表**:任务 4 的模板种子应插入 **`tasks` 表**(is_template=1, action_type=diet/sleep/exercise, frequency=daily),因为 RepeatTaskGenerator 查询的是 tasks 表。若使用 `admin_task_templates` 表,需同时确认 `TaskTemplate` 实体有 actionType 字段且生成器改为查该表——推荐直接落 tasks 表,与现有生成器一致。
|
|
|
+2. **isBackfill 差异**:Meal 有 is_backfill 字段需跳过补打;Sleep/Exercise 无该字段(确认实体后决定是否需要补打判断)。
|
|
|
+3. **HealthMealController 无 @Slf4j**:需补加注解和 import,否则 `log.warn` 无法编译。
|
|
|
+4. **userId 反查**:`updateProgress` 需要 `userId`,通过 `familyMemberMapper.selectById(memberId).getUserId()` 获得;不能登录用户(userId=null)打卡只存记录,不触发任务(与现有 updateProgress 容错一致)。
|