Эх сурвалжийг харах

feat(task): 每日自动生成饮食/睡眠/运动打卡进度任务

- RepeatTaskGenerator 新增 CHECKIN_TEMPLATES 常量与 generateBuiltinCheckinTasks()
- cron 主流程接入,每日为所有能登录用户幂等生成 3 条 is_daily_progress 任务
- 新增实现计划文档 docs/superpowers/plans/2026-10-13-health-checkin-task-linkage-plan.md
iwt 1 долоо хоног өмнө
parent
commit
4860ba77d8

+ 61 - 0
cfc-backend/src/main/java/com/etotem/cfc/task/RepeatTaskGenerator.java

@@ -148,11 +148,72 @@ public class RepeatTaskGenerator {
             }
             log.info("每小时重复任务生成完成,处理{}个模板", hourlyTasks.size());
 
+            // 6. 内置健康打卡任务([DIET]/[SLEEP]/[EXERCISE])——每日为能登录用户生成
+            generateBuiltinCheckinTasks(today);
+
         } catch (Exception e) {
             log.error("生成重复任务失败", e);
         }
     }
 
+    /**
+     * 每日为所有能登录用户生成 3 条内置健康打卡任务(is_daily_progress=1)
+     * title 前缀约定:[DIET]/[SLEEP]/[EXERCISE],与 TaskService.updateProgress 的 taskKey 匹配一致
+     * 幂等:当天已有对应实例则跳过
+     */
+    private void generateBuiltinCheckinTasks(Date today) {
+        Date tomorrow = new Date(today.getTime() + 86400000L);
+        // 所有能登录用户(family_members.userId 非空)
+        List<FamilyMember> members = familyMemberMapper.selectList(
+                new LambdaQueryWrapper<FamilyMember>()
+                        .isNotNull(FamilyMember::getUserId)
+        );
+        int created = 0;
+        for (FamilyMember member : members) {
+            for (String[] tpl : CHECKIN_TEMPLATES) {
+                String title = tpl[0];
+                String actionType = tpl[1];
+                Long exists = taskMapper.selectCount(
+                        new LambdaQueryWrapper<Task>()
+                                .eq(Task::getFamilyMemberId, member.getId())
+                                .eq(Task::getTitle, title)
+                                .eq(Task::getIsDailyProgress, 1)
+                                .ge(Task::getCreatedAt, today)
+                                .lt(Task::getCreatedAt, tomorrow)
+                );
+                if (exists != null && exists > 0) {
+                    continue;
+                }
+                Task checkinTask = new Task();
+                checkinTask.setFamilyId(member.getFamilyId());
+                checkinTask.setFamilyMemberId(member.getId());
+                checkinTask.setCreatorId(member.getUserId());
+                checkinTask.setExecutorType("child");
+                checkinTask.setExecutorId(member.getId());
+                checkinTask.setChildId(member.getId());
+                checkinTask.setTitle(title);
+                checkinTask.setDescription("每日健康打卡任务");
+                checkinTask.setPoints(5);
+                checkinTask.setCategory("健康打卡");
+                checkinTask.setStatus("pending");
+                checkinTask.setNeedReview(0);
+                checkinTask.setIsTemplate(0);
+                checkinTask.setIsDailyProgress(1);
+                checkinTask.setTargetValue(1);
+                checkinTask.setActionType(actionType);
+                checkinTask.setMemberOnly(0);
+                checkinTask.setSourceType("self");
+                checkinTask.setCreatedAt(new Date());
+                checkinTask.setUpdatedAt(new Date());
+                taskMapper.insert(checkinTask);
+                created++;
+            }
+        }
+        if (created > 0) {
+            log.info("内置健康打卡任务生成完成,新增{}条(覆盖{}个能登录用户)", created, members.size());
+        }
+    }
+
     /**
      * 隔天判定:模板创建时间到今天的间隔天数为偶数 → 今天生成
      */

+ 116 - 0
docs/superpowers/plans/2026-10-13-health-checkin-task-linkage-plan.md

@@ -0,0 +1,116 @@
+# 饮食/睡眠/运动打卡接入任务完成链路 — 实现计划(最终版)
+
+> 本文档为已实现成果的归档计划。前置设计文档:`docs/superpowers/plans/2026-10-13-health-checkin-task-linkage.md`、规格:`docs/superpowers/specs/2026-10-13-health-checkin-task-linkage-design.md`。
+
+## 1. 功能目标
+
+让饮食/睡眠/运动三类健康打卡可作为任务完成凭证,打通"打卡 ⇄ 任务"双向链路:
+
+- 家长/孩子在任务列表点击打卡类任务,跳转对应打卡页,打卡成功后任务自动完成。
+- 系统每日为能登录用户自动生成 3 条打卡进度任务,打卡时自动累计进度,达标即完成。
+
+## 2. 架构方案(双轨)
+
+| 轨道 | 任务来源 | 完成机制 |
+|------|----------|----------|
+| 一次性联动任务 | 手动创建,`actionType` 新增 `diet/sleep/exercise` 三个取值 | 任务列表跳转打卡页携带 `taskId`,打卡 Controller 落库成功后回写 `taskService.completeTask(taskId)` |
+| 每日进度任务 | `RepeatTaskGenerator` 定时生成,`is_daily_progress=1` | 打卡 Controller 回写 `taskService.updateProgress(userId, taskKey, 1)`,taskKey ∈ {DIET, SLEEP, EXERCISE} |
+
+判定与配对原则:
+
+- 打卡请求体带 `taskId`(非空)→ 走一次性任务完成路径;为 null → 走每日任务累计路径。两个 Controller 内分支互斥。
+- 每日任务实例的 `title` 前缀约定 `[DIET]/[SLEEP]/[EXERCISE]`,与 `TaskService.updateProgress` 的 taskKey 匹配一致。
+- `taskId` 字段用 `@TableField(exist = false)` 非持久化,仅作请求透传载体,无需建表迁移。
+
+## 3. 实现阶段分解与完成状态
+
+### 阶段 1:实体层 ✅(已提交)
+
+- `cfc-backend/src/main/java/com/etotem/cfc/entity/HealthMealRecord.java:59` — 新增 `@TableField(exist=false) private Long taskId;`
+- `cfc-backend/src/main/java/com/etotem/cfc/entity/HealthSleepRecord.java:43` — 同上
+- `cfc-backend/src/main/java/com/etotem/cfc/entity/HealthExerciseRecord.java:38` — 同上
+- `cfc-backend/src/main/java/com/etotem/cfc/entity/Task.java:130` — `actionType` 注释补充 `diet/sleep/exercise`(commit `814f30712`)
+- `cfc-backend/src/main/java/com/etotem/cfc/dto/CreateTaskDTO.java:66` — 同上
+
+对应提交:`e81e05bee feat(task): 三个健康打卡Controller挂钩任务回写`、`814f30712 docs(task): actionType 注释补充`
+
+### 阶段 2:控制器层 ✅(已提交,commit `e81e05bee`)
+
+三个打卡 Controller 的 `create()` 在记录落库成功后挂钩任务回写:
+
+| 文件 | 行号 | 回写逻辑 |
+|------|------|----------|
+| `HealthMealController.java` | 257 `completeTask` / 268 `updateProgress(userId, "DIET", 1)` | taskId 非空→一次性完成;否则累计 DIET 进度 |
+| `HealthSleepController.java` | 75 `completeTask` / 86 `updateProgress(userId, "SLEEP", 1)` | taskKey=SLEEP |
+| `HealthExerciseController.java` | 98 `completeTask` / 109 `updateProgress(userId, "EXERCISE", 1)` | taskKey=EXERCISE |
+
+回写包 try-catch,失败仅记日志,不影响打卡主流程。
+
+### 阶段 3:定时生成器 ✅(本次提交)
+
+`cfc-backend/src/main/java/com/etotem/cfc/task/RepeatTaskGenerator.java`:
+
+- `:41-45` — 新增 `CHECKIN_TEMPLATES` 常量:`[DIET]饮食打卡 / [SLEEP]睡眠打卡 / [EXERCISE]运动打卡`,actionType 对应 `diet/sleep/exercise`
+- `:152` — cron 主流程第 6 步调用 `generateBuiltinCheckinTasks(today)`
+- `:164` — `generateBuiltinCheckinTasks(Date today)`:遍历所有能登录用户(`family_members.userId IS NOT NULL`),为每人每天生成 3 条 `is_daily_progress=1, target_value=1, points=5, category=健康打卡, needReview=0` 的任务实例;按 `familyMemberId + title + 当天 createdAt` 判重,幂等可重复执行
+
+原计划的 DatabaseInitializer 模板种子迁移**取消**:模板以代码常量内置(`CHECKIN_TEMPLATES`),无需 DDL,减少迁移面。
+
+### 阶段 4:前端任务页面 ⚠️ 未实现(遗留)
+
+`cfc-frontend/pages/tasks/tasks.vue`:
+
+- `getActionLabel`(:413)尚无 `diet/sleep/exercise` 分支(应返回"去打卡")
+- `navigateByAction`(:462)尚无对应跳转分支(应跳转 `/pages/growth/diet-checkin?taskId=` 等)
+
+三个打卡页(`diet-checkin.vue` / `sleep-checkin.vue` / `exercise-checkin.vue`)均未读取/透传 `taskId`。
+
+**影响**:后端链路已通,但前端无法从任务列表跳到打卡页,一次性联动任务的入口缺失;每日进度任务不依赖前端跳转,可正常累计完成。
+
+### 阶段 5:API 文档 ⚠️ 未实现(遗留)
+
+`docs/superpowers/api/API_REFERENCE.md` 中三个打卡接口(`/api/health/meal`、`/api/health/sleep`、`/api/health/exercise`)尚未补充 `taskId` 可选参数说明。
+
+## 4. 文件变更清单
+
+| 文件 | 操作 | 关键行 | 提交状态 |
+|------|------|--------|----------|
+| `entity/HealthMealRecord.java` | 修改 | :59 | ✅ e81e05bee |
+| `entity/HealthSleepRecord.java` | 修改 | :43 | ✅ e81e05bee |
+| `entity/HealthExerciseRecord.java` | 修改 | :38 | ✅ e81e05bee |
+| `entity/Task.java` | 修改(注释) | :130 | ✅ 814f30712 |
+| `dto/CreateTaskDTO.java` | 修改(注释) | :66 | ✅ 814f30712 |
+| `controller/HealthMealController.java` | 修改 | :250-275 | ✅ e81e05bee |
+| `controller/HealthSleepController.java` | 修改 | :68-92 | ✅ e81e05bee |
+| `controller/HealthExerciseController.java` | 修改 | :91-115 | ✅ e81e05bee |
+| `task/RepeatTaskGenerator.java` | 修改 | :41, :152, :164-221 | ✅ 本次提交 |
+| `pages/tasks/tasks.vue` | 待修改 | :413, :462 | ❌ 遗留 |
+| `pages/growth/{diet,sleep,exercise}-checkin.vue` | 待修改 | onLoad/提交 | ❌ 遗留 |
+| `docs/superpowers/api/API_REFERENCE.md` | 待修改 | health 三节 | ❌ 遗留 |
+
+## 5. 测试验证策略
+
+1. **编译**:`cd cfc-backend && mvn clean compile`(本次提交前必跑)
+2. **每日任务生成**:启动后端后等 cron(每小时第 5 分)触发,或临时改 cron 验证;检查 `tasks` 表:每个能登录用户当天应有 3 条 `is_daily_progress=1`、title 带 `[DIET]/[SLEEP]/[EXERCISE]` 前缀的记录;重复触发不产生重复实例(幂等校验)
+3. **每日进度路径**:不带 taskId 调 `POST /api/health/meal` → 当天 `[DIET]` 任务 `current_value` 变 1、`status=completed`
+4. **一次性任务路径**:手动创建 `actionType=diet` 任务 → 带 `taskId` 调打卡接口 → 该任务 `status=completed` 且发积分
+5. **隔离性**:回写异常时(如 taskId 指向不存在任务)打卡仍返回 200,记录正常落库
+6. **回归**:不带 taskId 的旧打卡请求行为不变
+
+## 6. 风险与回滚
+
+- **风险**:每日生成器遍历全部能登录用户,用户量大时单次 cron 插入量上升(当前规模 3×N 条/天,可接受);title 前缀与 updateProgress 的 taskKey 是字符串约定,改任一侧需同步
+- **回滚**:
+  - 生成器:`git revert` 本次提交即可,已生成的任务实例为 `is_daily_progress=1` 普通数据,不影响其他任务
+  - Controller 回写:revert `e81e05bee`,打卡主流程不受影响(回写本就包 try-catch)
+  - 数据无需回滚:`taskId` 非持久化,无 DDL 变更
+
+## 7. Commit 建议
+
+```
+feat(task): 每日自动生成饮食/睡眠/运动打卡进度任务
+
+- RepeatTaskGenerator 新增 CHECKIN_TEMPLATES 常量与 generateBuiltinCheckinTasks()
+- cron 主流程接入,每日为所有能登录用户幂等生成 3 条 is_daily_progress 任务
+- 关联计划文档 docs/superpowers/plans/2026-10-13-health-checkin-task-linkage-plan.md
+```