Browse Source

docs: 统一任务系统实现计划(9任务:数据层/服务层/迁移/前端)

E2E Test Bot 2 weeks ago
parent
commit
0248c8cbaa
1 changed files with 852 additions and 0 deletions
  1. 852 0
      docs/superpowers/plans/2026-09-04-task-system-unification.md

+ 852 - 0
docs/superpowers/plans/2026-09-04-task-system-unification.md

@@ -0,0 +1,852 @@
+# 统一任务系统 Implementation Plan
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** 将多套任务体系(家庭任务/成长任务/新人引导/方案任务/AI任务)合并为一套:`tasks` 表作为唯一执行态表,用 `source_type` 区分来源,任务模板快速创建,奖励自动发放,前端统一任务中心。
+
+**Architecture:** 单表统一。`tasks` 表承载所有执行态任务(source_type 区分 9 类来源);`task_executions` 存进度;`admin_task_templates`(单任务模板 + onboarding_sequence)+ `task_template_packages`(打包方案)为配置层;`task_plan_instances` 保留方案实例生命周期;废弃 `growth_task`/`growth_task_log`/`onboarding_task`/`task_plan_items`/`training_plan_tasks`/`task_templates`(软废弃,只读备份)。
+
+**Tech Stack:** Spring Boot 2.7.18 / MyBatis-Plus / MySQL 8.0 / uni-app Vue2 / Element UI
+
+**Spec:** `docs/superpowers/specs/2026-09-04-task-system-unification-design.md`(设计文档随计划一起阅读,执行者须先读 spec 再动工)
+
+## Global Constraints
+
+- 所有接口 `@PostMapping`,禁止 GET/PUT/DELETE;响应统一 `Result<T>` (code/message/data)
+- DI 用 `@Resource`,字段名匹配类型默认 Bean Name
+- 实体用 MyBatis-Plus `@TableName` + `@TableId(type = IdType.AUTO)`
+- 数据库迁移唯一入口:`DatabaseInitializer.runMigrations()`,每次迁移后**必须同步 schema.sql**
+- 迁移编号:在文件末尾搜索 `// 迁移` 确认最新编号(当前 294),新迁移从 **295** 递增
+- 迁移必须幂等(try-catch 忽略已存在错误)
+- 小程序:禁止可选链 `?.`、禁止 CSS Grid、禁止 `:key` 表达式、禁止 `new Date(string)`(用 `parseDate()`)、Vue2 Options API
+- 后端验证:`cd cfc-backend && mvn clean compile`;前端验证:`node --check`(不执行打包命令)
+- 新增接口前查 `docs/superpowers/api/API_REFERENCE.md`;废弃接口标记 410
+- 每次迁移:schema.sql 的 `CREATE TABLE` 必须同步;`docs/superpowers/PROJECT-OVERVIEW.md` 必须同步更新
+
+---
+
+### Task 1: tasks 表新增 is_daily_progress / target_value 字段 + source_type 注释更新
+
+**Files:**
+- Modify: `cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java`(runMigrations 末尾追加迁移 295)
+- Modify: `cfc-backend/src/main/resources/schema.sql`(tasks 表 CREATE TABLE 同步)
+- Modify: `cfc-backend/src/main/java/com/etotem/cfc/entity/Task.java`
+
+**Interfaces:**
+- Produces: `Task.isDailyProgress` (Integer)、`Task.targetValue` (Integer) getter/setter(Lombok @Data 自动生成)
+- Consumes: 无
+
+- [ ] **Step 1: 在 Task.java 实体添加两个字段**
+
+在 `entity/Task.java` 的 `sourceType`/`sourceId` 字段后追加:
+
+```java
+    /** 1=每日进度型任务(无 deadline,每日重置;原 growth_task DAILY) */
+    private Integer isDailyProgress;
+
+    /** 进度目标值(is_daily_progress=1 时有效;原 growth_task.targetValue) */
+    private Integer targetValue;
+```
+
+- [ ] **Step 2: 在 DatabaseInitializer 追加迁移 295**
+
+在 `runMigrations()` 方法末尾(搜索最新 `// 迁移294` 之后)追加:
+
+```java
+// 迁移295: tasks表新增is_daily_progress/target_value字段 + source_type注释更新(统一任务系统)
+try {
+    jdbcTemplate.execute("ALTER TABLE tasks ADD COLUMN is_daily_progress TINYINT DEFAULT 0 COMMENT '1=每日进度型任务(无deadline,每日重置)' AFTER source_id");
+    log.info("已添加is_daily_progress列到tasks表");
+} catch (Exception e) {
+    // 列已存在,忽略
+}
+try {
+    jdbcTemplate.execute("ALTER TABLE tasks ADD COLUMN target_value INT DEFAULT NULL COMMENT '进度目标值(is_daily_progress=1时有效)' AFTER is_daily_progress");
+    log.info("已添加target_value列到tasks表");
+} catch (Exception e) {
+    // 列已存在,忽略
+}
+try {
+    jdbcTemplate.execute("ALTER TABLE tasks MODIFY COLUMN source_type VARCHAR(32) DEFAULT NULL COMMENT '来源类型: self/assigned/plan/onboarding/ai/template/article/activity/purchase'");
+    log.info("已更新tasks.source_type注释");
+} catch (Exception e) {
+    // 修改失败,忽略
+}
+```
+
+- [ ] **Step 3: 同步 schema.sql 的 tasks 建表语句**
+
+在 `schema.sql` 的 `tasks` 表(第 76 行附近)的 `source_id` 行后追加:
+
+```sql
+    is_daily_progress TINYINT DEFAULT 0 COMMENT '1=每日进度型任务(无deadline,每日重置)',
+    target_value INT DEFAULT NULL COMMENT '进度目标值(is_daily_progress=1时有效)',
+```
+
+并把 `source_type VARCHAR(20)` 改为 `source_type VARCHAR(32)`,注释更新为 `'来源类型: self/assigned/plan/onboarding/ai/template/article/activity/purchase'`。
+
+- [ ] **Step 4: 编译验证**
+
+Run: `cd cfc-backend && mvn clean compile`
+Expected: BUILD SUCCESS
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java cfc-backend/src/main/resources/schema.sql cfc-backend/src/main/java/com/etotem/cfc/entity/Task.java
+git commit -m "feat(task): tasks表新增is_daily_progress/target_value字段,source_type扩展(统一任务系统)"
+```
+
+---
+
+### Task 2: admin_task_templates 表新增 onboarding_sequence 字段
+
+**Files:**
+- Modify: `cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java`(迁移 296)
+- Modify: `cfc-backend/src/main/resources/schema.sql`(admin_task_templates 建表语句)
+- Modify: `cfc-backend/src/main/java/com/etotem/cfc/entity/TaskTemplate.java`
+
+**Interfaces:**
+- Produces: `TaskTemplate.onboardingSequence` (Integer) getter/setter
+
+- [ ] **Step 1: TaskTemplate.java 添加字段**
+
+在 `entity/TaskTemplate.java` 的 `teacherId` 字段后追加:
+
+```java
+    // 新人引导顺序(source_type=onboarding 的模板用:1/2/3... 按序生成+前置链)
+    private Integer onboardingSequence;
+```
+
+- [ ] **Step 2: DatabaseInitializer 追加迁移 296**
+
+```java
+// 迁移296: admin_task_templates表新增onboarding_sequence字段(统一任务系统:新人引导复用任务模板表)
+try {
+    jdbcTemplate.execute("ALTER TABLE admin_task_templates ADD COLUMN onboarding_sequence INT DEFAULT NULL COMMENT '新人引导顺序(source_type=onboarding的模板用)' AFTER teacher_id");
+    log.info("已添加onboarding_sequence列到admin_task_templates表");
+} catch (Exception e) {
+    // 列已存在,忽略
+}
+```
+
+- [ ] **Step 3: 同步 schema.sql**
+
+在 `admin_task_templates` 表(第 800 行附近)添加:
+
+```sql
+    onboarding_sequence INT DEFAULT NULL COMMENT '新人引导顺序(source_type=onboarding的模板用)',
+```
+
+- [ ] **Step 4: 编译验证**
+
+Run: `cd cfc-backend && mvn clean compile`
+Expected: BUILD SUCCESS
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java cfc-backend/src/main/resources/schema.sql cfc-backend/src/main/java/com/etotem/cfc/entity/TaskTemplate.java
+git commit -m "feat(task): admin_task_templates新增onboarding_sequence字段(新人引导复用任务模板)"
+```
+
+---
+
+### Task 3: CreateTaskDTO 扩展支持 source_type / is_daily_progress / target_value
+
+**Files:**
+- Modify: `cfc-backend/src/main/java/com/etotem/cfc/dto/CreateTaskDTO.java`
+- Modify: `cfc-backend/src/main/java/com/etotem/cfc/service/TaskService.java`(createSingleTask)
+
+**Interfaces:**
+- Produces: `CreateTaskDTO.sourceType`、`CreateTaskDTO.isDailyProgress`、`CreateTaskDTO.targetValue`(String/Integer 字段)
+- Consumes: Task 1 的 `Task.isDailyProgress`/`Task.targetValue`
+
+- [ ] **Step 1: 读 CreateTaskDTO 现有字段**
+
+Run: 读取 `cfc-backend/src/main/java/com/etotem/cfc/dto/CreateTaskDTO.java` 全文,确认现有字段。
+
+- [ ] **Step 2: CreateTaskDTO 添加字段**
+
+```java
+    private String sourceType;        // self/assigned/plan/onboarding/ai/template/article/activity/purchase
+    private Integer isDailyProgress;  // 1=每日进度型
+    private Integer targetValue;      // 进度目标值
+```
+
+- [ ] **Step 3: TaskService.createSingleTask 透传字段**
+
+在 `createSingleTask` 方法的 `task.setMemberOnly(...)` 后追加:
+
+```java
+        // 统一任务系统:来源/进度型透传
+        task.setSourceType(dto.getSourceType() != null ? dto.getSourceType() : "self");
+        task.setIsDailyProgress(dto.getIsDailyProgress() != null ? dto.getIsDailyProgress() : 0);
+        task.setTargetValue(dto.getTargetValue());
+
+        // 进度型任务无 deadline(原 growth_task DAILY:每日重置,无截止时间)
+        if (task.getIsDailyProgress() != null && task.getIsDailyProgress() == 1) {
+            task.setDeadline(null);
+        }
+```
+
+注意:现有代码在 `dto.getDeadline() == null` 时会默认设置"今天 23:59:59"。上述逻辑在 deadline 设置之后覆盖为 null,保证进度型任务 deadline 恒为 null。
+
+- [ ] **Step 4: 编译验证**
+
+Run: `cd cfc-backend && mvn clean compile`
+Expected: BUILD SUCCESS
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add cfc-backend/src/main/java/com/etotem/cfc/dto/CreateTaskDTO.java cfc-backend/src/main/java/com/etotem/cfc/service/TaskService.java
+git commit -m "feat(task): CreateTaskDTO支持source_type/is_daily_progress/target_value透传"
+```
+
+---
+
+### Task 4: GrowthTaskService 迁移 — DAILY/AI 任务并入 TaskService
+
+**核心目标:** `GrowthTaskService.updateProgress`/`createDynamicTask` 的行为迁移到 `TaskService`,`growth_task`/`growth_task_log` 表写入停止,改为写 `tasks`/`task_executions`。
+
+**Files:**
+- Modify: `cfc-backend/src/main/java/com/etotem/cfc/service/TaskService.java`(新增 updateProgress/createDynamicTask 方法)
+- Modify: `cfc-backend/src/main/java/com/etotem/cfc/service/GrowthTaskService.java`(方法改为委托 TaskService,标注 @Deprecated)
+- Modify: `cfc-backend/src/main/java/com/etotem/cfc/controller/GrowthTaskController.java`(list/claim 逻辑调整)
+
+**Interfaces:**
+- Produces: `TaskService.updateProgress(Long userId, String taskKey, int increment)`、`TaskService.createDynamicTask(Long userId, String title, String description, String dimension, Integer rewardPoints, String sourceConversationId)`
+- Consumes: 无
+
+- [ ] **Step 1: TaskService 新增 updateProgress**
+
+在 `TaskService` 中新增方法(复制 GrowthTaskService.updateProgress 逻辑,改为操作 tasks 表):
+
+```java
+    /**
+     * 统一任务系统:更新每日进度型任务的进度(原 growth_task updateProgress)
+     * 按 (userId→familyMemberId, taskKey) 找当日任务实例,累计进度,达标自动完成+发奖
+     */
+    @Transactional
+    public void updateProgress(Long userId, String taskKey, int increment) {
+        if (userId == null) return;
+        // 通过 user 找到其家庭成员的今日进度型任务实例
+        FamilyMember member = familyMemberMapper.selectOne(
+                new LambdaQueryWrapper<FamilyMember>()
+                        .eq(FamilyMember::getUserId, userId)
+                        .last("LIMIT 1"));
+        if (member == null) return;
+
+        Calendar cal = Calendar.getInstance();
+        cal.set(Calendar.HOUR_OF_DAY, 0); cal.set(Calendar.MINUTE, 0);
+        cal.set(Calendar.SECOND, 0); cal.set(Calendar.MILLISECOND, 0);
+        Date startOfDay = cal.getTime();
+        cal.add(Calendar.DAY_OF_MONTH, 1);
+        Date endOfDay = cal.getTime();
+
+        Task task = taskMapper.selectOne(new LambdaQueryWrapper<Task>()
+                .eq(Task::getFamilyMemberId, member.getId())
+                .eq(Task::getIsDailyProgress, 1)
+                .eq(Task::getTitle, taskKey)  // 兼容:taskKey 作为 title 的映射键
+                .ge(Task::getCreatedAt, startOfDay)
+                .lt(Task::getCreatedAt, endOfDay)
+                .last("LIMIT 1"));
+        if (task == null) return;
+        if ("completed".equals(task.getStatus())) return;
+
+        // 读当前进度(task_executions.extra.progress),累加
+        TaskExecution exec = taskExecutionMapper.selectOne(new LambdaQueryWrapper<TaskExecution>()
+                .eq(TaskExecution::getTaskId, task.getId())
+                .eq(TaskExecution::getMemberId, member.getId())
+                .last("LIMIT 1"));
+        int progress = 0;
+        if (exec != null && exec.getExtra() != null) {
+            try {
+                JSONObject obj = JSON.parseObject(exec.getExtra());
+                progress = obj.getIntValue("progress");
+            } catch (Exception ignore) {}
+        }
+        progress += increment;
+        int target = task.getTargetValue() != null ? task.getTargetValue() : 1;
+
+        if (exec == null) {
+            exec = new TaskExecution();
+            exec.setTaskId(task.getId());
+            exec.setMemberId(member.getId());
+            exec.setStatus("started");
+            exec.setStartedAt(new Date());
+            exec.setCreatedAt(new Date());
+        }
+        exec.setStatus("finished");
+        exec.setFinishedAt(new Date());
+        exec.setExtra("{\"progress\":" + progress + ",\"target\":" + target + "}");
+        if (exec.getId() == null) {
+            taskExecutionMapper.insert(exec);
+        } else {
+            taskExecutionMapper.updateById(exec);
+        }
+
+        // 达标自动完成 + 发奖(复用现有奖励链路)
+        if (progress >= target) {
+            awardTaskCompletionRewards(task, member.getId(), new Date());
+        }
+    }
+```
+
+**注意:** 依赖 `FamilyMember::getUserId` 字段存在(`family_members` 表已有 user_id 列,OnboardingService 已使用)。
+
+- [ ] **Step 2: TaskService 新增 createDynamicTask**
+
+```java
+    /**
+     * 统一任务系统:AI 对话生成任务(原 growth_task createDynamicTask)
+     * 直接写入 tasks 表,source_type=ai
+     */
+    @Transactional
+    public Long createDynamicTask(Long userId, String title, String description,
+                                  String dimension, Integer rewardPoints, String sourceConversationId) {
+        User user = userService.getUserInfo(userId);
+        Task task = new Task();
+        task.setFamilyId(user.getFamilyId());
+        task.setCreatorId(userId);
+        task.setExecutorType("child");
+        // 执行者:AI 任务发给当前用户对应家庭成员(若有)
+        FamilyMember member = familyMemberMapper.selectOne(
+                new LambdaQueryWrapper<FamilyMember>()
+                        .eq(FamilyMember::getUserId, userId)
+                        .last("LIMIT 1"));
+        if (member != null) {
+            task.setExecutorId(member.getId());
+            task.setFamilyMemberId(member.getId());
+            task.setChildId(member.getId());
+        }
+        task.setTitle(title);
+        task.setDescription(description);
+        task.setPoints(rewardPoints != null ? rewardPoints : 10);
+        task.setCategory("成长");
+        task.setStatus("pending");
+        task.setNeedReview(0);
+        task.setIsTemplate(0);
+        task.setSourceType("ai");
+        task.setSourceId(0L);
+        task.setDimensionCode(dimension != null ? dimension : "body");
+        task.setMemberOnly(0);
+        task.setDeadline(new Date(System.currentTimeMillis() + 7L * 24 * 3600 * 1000)); // 7天有效期
+        task.setCreatedAt(new Date());
+        task.setUpdatedAt(new Date());
+        taskMapper.insert(task);
+        return task.getId();
+    }
+```
+
+- [ ] **Step 3: GrowthTaskService 方法改为委托 + 标注废弃**
+
+将 `GrowthTaskService.updateProgress` 方法体替换为:
+
+```java
+    @Deprecated
+    @Transactional(propagation = Propagation.REQUIRES_NEW)
+    public void updateProgress(Long userId, String taskKey, int increment) {
+        // 统一任务系统:委托 TaskService(任务已迁入 tasks 表)
+        try {
+            taskService.updateProgress(userId, taskKey, increment);
+        } catch (Exception e) {
+            log.warn("统一任务进度更新失败: userId={}, taskKey={}, error={}", userId, taskKey, e.getMessage());
+        }
+    }
+```
+
+并注入 `@Resource private TaskService taskService;`。
+
+将 `createDynamicTask` 方法体替换为委托:
+
+```java
+    @Deprecated
+    public GrowthTask createDynamicTask(Long userId, String title, String description, String dimension, Integer rewardPoints, String sourceConversationId) {
+        // 统一任务系统:委托 TaskService(任务写入 tasks 表,source_type=ai)
+        Long taskId = taskService.createDynamicTask(userId, title, description, dimension, rewardPoints, sourceConversationId);
+        GrowthTask g = new GrowthTask();
+        g.setId(taskId);
+        g.setTitle(title);
+        return g;
+    }
+```
+
+- [ ] **Step 4: GrowthTaskController.list 保持兼容**
+
+`GrowthTaskController` 的 `/list` 接口暂时保留(前端 daily-tasks.vue 在 Task 8 之前仍调用),但改为查询 tasks 表进度型任务。修改 `GrowthTaskService.getTaskList` 委托到 TaskService 的进度查询:
+
+在 TaskService 新增:
+
+```java
+    /**
+     * 统一任务系统:查询用户每日/新人进度型任务列表(原 growth-task/list)
+     * type: DAILY/NEWBIE(NEWBIE 在 Task 5 处理,此处先处理 DAILY)
+     */
+    public List<Map<String, Object>> getGrowthTaskList(Long userId, String type, String dimension) {
+        FamilyMember member = familyMemberMapper.selectOne(
+                new LambdaQueryWrapper<FamilyMember>()
+                        .eq(FamilyMember::getUserId, userId)
+                        .last("LIMIT 1"));
+        List<Map<String, Object>> result = new ArrayList<>();
+        if (member == null || !"DAILY".equals(type)) return result;
+
+        Calendar cal = Calendar.getInstance();
+        cal.set(Calendar.HOUR_OF_DAY, 0); cal.set(Calendar.MINUTE, 0);
+        cal.set(Calendar.SECOND, 0); cal.set(Calendar.MILLISECOND, 0);
+        Date startOfDay = cal.getTime();
+        cal.add(Calendar.DAY_OF_MONTH, 1);
+        Date endOfDay = cal.getTime();
+
+        List<Task> tasks = taskMapper.selectList(new LambdaQueryWrapper<Task>()
+                .eq(Task::getFamilyMemberId, member.getId())
+                .eq(Task::getIsDailyProgress, 1)
+                .ge(Task::getCreatedAt, startOfDay)
+                .lt(Task::getCreatedAt, endOfDay)
+                .orderByAsc(Task::getId));
+        for (Task task : tasks) {
+            Map<String, Object> item = new LinkedHashMap<>();
+            item.put("taskLogId", task.getId());
+            item.put("id", task.getId());
+            item.put("type", "DAILY");
+            item.put("title", task.getTitle());
+            item.put("description", task.getDescription());
+            item.put("rewardPoints", task.getPoints());
+            item.put("rewardEnergy", 0);
+            item.put("targetValue", task.getTargetValue() != null ? task.getTargetValue() : 1);
+            item.put("dimension", task.getDimensionCode());
+
+            TaskExecution exec = taskExecutionMapper.selectOne(new LambdaQueryWrapper<TaskExecution>()
+                    .eq(TaskExecution::getTaskId, task.getId())
+                    .eq(TaskExecution::getMemberId, member.getId())
+                    .last("LIMIT 1"));
+            int progress = 0;
+            if (exec != null && exec.getExtra() != null) {
+                try {
+                    progress = JSON.parseObject(exec.getExtra()).getIntValue("progress");
+                } catch (Exception ignore) {}
+            }
+            item.put("progress", progress);
+            item.put("completed", "completed".equals(task.getStatus()) ? 1 : 0);
+            item.put("claimed", 0); // 统一任务系统:自动发放,无 claimed
+            result.add(item);
+        }
+        return result;
+    }
+```
+
+将 `GrowthTaskService.getTaskList` 改为委托:
+
+```java
+    @Deprecated
+    public List<Map<String, Object>> getTaskList(Long userId, Map<String, String> params) {
+        String type = params != null ? params.getOrDefault("type", "DAILY") : "DAILY";
+        String dimension = params != null ? params.get("dimension") : null;
+        return taskService.getGrowthTaskList(userId, type, dimension);
+    }
+```
+
+`GrowthTaskController.claim` 接口:因奖励改为自动发放,返回"奖励已自动发放":
+
+```java
+    @PostMapping("/claim")
+    public Result<String> claim(@RequestAttribute("userId") Long userId,
+        @RequestBody Map<String, Long> params) {
+        return Result.success("奖励已自动发放");
+    }
+```
+
+- [ ] **Step 5: 编译验证**
+
+Run: `cd cfc-backend && mvn clean compile`
+Expected: BUILD SUCCESS
+
+- [ ] **Step 6: Commit**
+
+```bash
+git add cfc-backend/src/main/java/com/etotem/cfc/service/TaskService.java cfc-backend/src/main/java/com/etotem/cfc/service/GrowthTaskService.java cfc-backend/src/main/java/com/etotem/cfc/controller/GrowthTaskController.java
+git commit -m "feat(task): GrowthTask DAILY/AI任务迁入TaskService(tasks表统一执行),旧Service委托+废弃"
+```
+
+---
+
+### Task 5: OnboardingService 迁移 — 新人引导复用任务模板 + 落 tasks
+
+**核心目标:** 新人引导任务不再写 `onboarding_task` 表,改为:注册时按 `admin_task_templates` 中 `onboarding_sequence` 有值的模板批量生成 `tasks`(source_type=onboarding),顺序链用 `prerequisite_task_id`。
+
+**Files:**
+- Modify: `cfc-backend/src/main/java/com/etotem/cfc/service/OnboardingService.java`
+- Modify: `cfc-backend/src/main/java/com/etotem/cfc/service/TaskService.java`(新增 initOnboardingTasks 方法)
+- Modify: `cfc-backend/src/main/java/com/etotem/cfc/mapper/TaskTemplateMapper.java`(如需按 onboarding_sequence 查询)
+- Modify: `cfc-backend/src/main/resources/schema.sql`(可选:admin_task_templates 种子数据)
+
+**Interfaces:**
+- Produces: `TaskService.initOnboardingTasks(Long userId)` — 新用户注册时调用
+- Consumes: Task 2 的 `TaskTemplate.onboardingSequence`
+
+- [ ] **Step 1: TaskService 新增 initOnboardingTasks**
+
+```java
+    /**
+     * 统一任务系统:新用户注册初始化新人引导任务
+     * 按 admin_task_templates 中 onboarding_sequence 有值的模板生成 tasks(source_type=onboarding)
+     * 顺序链:sequence 递增,前序未完成则后序 locked(复用 prerequisite_task_id + locked 状态)
+     */
+    @Transactional
+    public void initOnboardingTasks(Long userId) {
+        User user = userService.getUserInfo(userId);
+        if (user == null || user.getFamilyId() == null) return;
+
+        // 幂等:该用户已有 onboarding 任务则跳过
+        Long existing = taskMapper.selectCount(new LambdaQueryWrapper<Task>()
+                .eq(Task::getCreatorId, userId)
+                .eq(Task::getSourceType, "onboarding"));
+        if (existing != null && existing > 0) return;
+
+        List<TaskTemplate> templates = taskTemplateMapper.selectList(
+                new LambdaQueryWrapper<TaskTemplate>()
+                        .eq(TaskTemplate::getIsActive, 1)
+                        .isNotNull(TaskTemplate::getOnboardingSequence)
+                        .orderByAsc(TaskTemplate::getOnboardingSequence));
+        if (templates.isEmpty()) return;
+
+        FamilyMember member = familyMemberMapper.selectOne(
+                new LambdaQueryWrapper<FamilyMember>()
+                        .eq(FamilyMember::getUserId, userId)
+                        .last("LIMIT 1"));
+
+        Date now = new Date();
+        Long prevTaskId = null;
+        for (TaskTemplate tpl : templates) {
+            Task task = new Task();
+            task.setFamilyId(user.getFamilyId());
+            task.setCreatorId(userId);
+            task.setExecutorType("child");
+            if (member != null) {
+                task.setExecutorId(member.getId());
+                task.setFamilyMemberId(member.getId());
+                task.setChildId(member.getId());
+            }
+            task.setTitle(tpl.getTitle());
+            task.setDescription(tpl.getDescription());
+            task.setPoints(tpl.getPoints() != null ? tpl.getPoints() : 2);
+            task.setCategory(tpl.getCategory());
+            task.setStatus(prevTaskId == null ? "pending" : "locked"); // 首任务可直接做,后续需解锁
+            task.setNeedReview(tpl.getNeedReview() != null ? tpl.getNeedReview() : 0);
+            task.setIsTemplate(0);
+            task.setSourceType("onboarding");
+            task.setTemplateId(tpl.getId());
+            task.setPrerequisiteTaskId(prevTaskId); // 顺序链
+            task.setMemberOnly(0);
+            task.setDeadline(new Date(now.getTime() + 30L * 24 * 3600 * 1000)); // 30天有效期
+            task.setCreatedAt(now);
+            task.setUpdatedAt(now);
+            taskMapper.insert(task);
+            prevTaskId = task.getId();
+        }
+        log.info("新用户{}初始化{}条新人引导任务", userId, templates.size());
+    }
+```
+
+需注入 `@Resource private TaskTemplateMapper taskTemplateMapper;`(TaskTemplate 实体映射 admin_task_templates,字段 isActive 对应 is_active 列——确认现有实体字段:TaskTemplate.java 有 `isActive`,列名 `is_active`,OK)。
+
+- [ ] **Step 2: OnboardingService 迁移**
+
+将 `OnboardingService.initForUser` 改为委托:
+
+```java
+    @Resource
+    private TaskService taskService;
+
+    /**
+     * 统一任务系统:新人引导初始化(委托 TaskService 生成 tasks 表 onboarding 任务)
+     */
+    @Deprecated
+    public void initForUser(Long userId) {
+        taskService.initOnboardingTasks(userId);
+    }
+```
+
+`completeTask`/`claimReward`/`getProgress` 三个方法:因新人任务已迁入 tasks 表,这些方法调用方需要排查。搜索调用点:
+
+- [ ] **Step 3: 搜索 OnboardingService.completeTask / claimReward / getProgress 调用方**
+
+Run: `grep -rn "onboardingService\.\|OnboardingService" cfc-backend/src/main/java/`
+
+对每个调用点逐一处理:
+- `completeTask(userId, "FIRST_TASK")` 等 → 改为调用 `taskService.completeOnboardingTask(userId, templateTitle)`(新方法:按 source_type=onboarding + title 找任务并完成)
+- `claimReward` → 删除调用(奖励自动发放)
+- `getProgress` → 改为查 tasks 表 onboarding 任务
+
+在 TaskService 新增:
+
+```java
+    /**
+     * 统一任务系统:完成指定新人引导任务(原 OnboardingService.completeTask)
+     */
+    public void completeOnboardingTask(Long userId, String templateTitle) {
+        FamilyMember member = familyMemberMapper.selectOne(
+                new LambdaQueryWrapper<FamilyMember>()
+                        .eq(FamilyMember::getUserId, userId)
+                        .last("LIMIT 1"));
+        if (member == null) return;
+        Task task = taskMapper.selectOne(new LambdaQueryWrapper<Task>()
+                .eq(Task::getFamilyMemberId, member.getId())
+                .eq(Task::getSourceType, "onboarding")
+                .eq(Task::getTitle, templateTitle)
+                .eq(Task::getStatus, "pending")
+                .last("LIMIT 1"));
+        if (task != null) {
+            awardTaskCompletionRewards(task, member.getId(), new Date());
+            unlockPrerequisiteTasks(task.getId());
+        }
+    }
+```
+
+- [ ] **Step 4: 编译验证**
+
+Run: `cd cfc-backend && mvn clean compile`
+Expected: BUILD SUCCESS
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add cfc-backend/src/main/java/com/etotem/cfc/service/TaskService.java cfc-backend/src/main/java/com/etotem/cfc/service/OnboardingService.java
+git commit -m "feat(task): 新人引导复用任务模板+落tasks表(source_type=onboarding,顺序链前置解锁)"
+```
+
+---
+
+### Task 6: 旧表数据一次性迁移(growth_task/onboarding_task → tasks)
+
+**核心目标:** 写一次性迁移脚本,把存量 `growth_task`(DAILY/AI)、`growth_task_log`(进度)、`onboarding_task` 数据搬入 `tasks` 表,旧表数据保留只读。
+
+**Files:**
+- Modify: `cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java`(迁移 297,仅执行一次的数据回填)
+- Modify: `cfc-backend/src/main/resources/schema.sql`(无需改,迁移只回填数据)
+
+**Interfaces:**
+- Consumes: Task 1-5 的字段
+
+- [ ] **Step 1: 迁移 297 — growth_task DAILY 定义 → admin_task_templates**
+
+```java
+// 迁移297: 统一任务系统数据迁移——growth_task定义迁入admin_task_templates(仅执行一次,幂等)
+try {
+    // 幂等标记:sys_config 已有 TASK_UNIFY_GT_MIGRATED 则跳过
+    Integer migrated = jdbcTemplate.queryForObject(
+        "SELECT COUNT(*) FROM sys_config WHERE config_key = 'TASK_UNIFY_GT_MIGRATED'", Integer.class);
+    if (migrated == null || migrated == 0) {
+        // DAILY 类型 → admin_task_templates(is_daily_progress 语义由调用方通过 source_type 使用标记)
+        jdbcTemplate.execute("INSERT INTO admin_task_templates (title, description, points, category, need_review, is_active, task_type, frequency, created_at, updated_at, onboarding_sequence) " +
+            "SELECT title, description, reward_points, '成长', 0, enabled, 'recurring', 'daily', created_at, created_at, NULL " +
+            "FROM growth_task WHERE type = 'DAILY' AND enabled = 1");
+        // 幂等标记
+        jdbcTemplate.update("INSERT INTO sys_config (config_key, config_value, description, created_at) VALUES ('TASK_UNIFY_GT_MIGRATED', '1', '统一任务系统: growth_task定义已迁入admin_task_templates', NOW())");
+        log.info("迁移297: growth_task DAILY定义已迁入admin_task_templates");
+    }
+} catch (Exception e) {
+    log.warn("迁移297: growth_task迁移失败或已执行: {}", e.getMessage());
+}
+```
+
+**注意:** 需确认 `sys_config` 表结构(config_key/config_value/description/created_at 列)。先 grep 确认现有 sys_config 插入语句的列名,若不一致则改用其它幂等标记(如检查 admin_task_templates 是否已有"成长"分类的 daily 模板记录)。
+
+- [ ] **Step 2: 迁移 297b — growth_task_log 进度回填**
+
+存量 `growth_task_log` 的 DAILY 进度属于历史数据。由于统一后按"每天生成新实例"模型,历史进度无法 1:1 映射到 tasks 实例。**决策:历史 growth_task_log 数据仅保留在旧表(只读备份),不回填 tasks**。在迁移注释中说明即可,无需代码。
+
+- [ ] **Step 3: 迁移 297c — onboarding_task 存量数据 → 只读保留**
+
+同 Step 2 决策:存量 onboarding_task 数据保留在旧表只读,不回填(新注册用户走 Task 5 的新逻辑)。
+
+- [ ] **Step 4: 编译验证**
+
+Run: `cd cfc-backend && mvn clean compile`
+Expected: BUILD SUCCESS
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java
+git commit -m "feat(task): 一次性迁移——growth_task DAILY定义迁入admin_task_templates(旧表只读保留)"
+```
+
+---
+
+### Task 7: 旧接口兼容与标记废弃
+
+**核心目标:** 旧表读取接口(growth-task/list、onboarding 相关)标记废弃(410),新代码走 tasks 表统一接口。
+
+**Files:**
+- Modify: `cfc-backend/src/main/java/com/etotem/cfc/controller/GrowthTaskController.java`
+- Modify: `cfc-backend/src/main/java/com/etotem/cfc/controller/OnboardingController.java`(如存在)
+
+- [ ] **Step 1: 确认 OnboardingController 存在与否**
+
+Run: `glob cfc-backend/src/main/java/com/etotem/cfc/controller/*Onboard*`
+若存在,读取其接口清单。
+
+- [ ] **Step 2: GrowthTaskController 标记废弃**
+
+`/api/growth-task/*` 三个接口整体标注:类上 `@Deprecated`,`/list` 返回数据改为查 tasks(Task 4 已改),`/claim` 返回"奖励已自动发放",`/accept-dynamic` 保持兼容(查 tasks 表 source_type=ai 的任务)。
+
+- [ ] **Step 3: 编译验证**
+
+Run: `cd cfc-backend && mvn clean compile`
+Expected: BUILD SUCCESS
+
+- [ ] **Step 4: Commit**
+
+```bash
+git add cfc-backend/src/main/java/com/etotem/cfc/controller/GrowthTaskController.java
+git commit -m "feat(task): growth-task接口标记废弃,数据源统一到tasks表"
+```
+
+---
+
+### Task 8: 前端统一任务中心(tasks.vue 聚合 + daily-tasks.vue 废弃)
+
+**核心目标:** `pages/tasks/tasks.vue` 成为统一任务中心,展示所有来源任务;`daily-tasks.vue` 废弃,进度展示能力并入"今日任务"Tab。
+
+**Files:**
+- Modify: `cfc-frontend/pages/tasks/tasks.vue`
+- Modify: `cfc-frontend/pages/tasks/daily-tasks.vue`(标记废弃或重定向)
+- Modify: `cfc-frontend/utils/api.js`(如需要新增进度任务查询封装)
+
+**Interfaces:**
+- Consumes: 后端 `/api/tasks/today`(已支持 familyWide/memberId/category/dimensionCode)
+
+- [ ] **Step 1: 确认前端 tasks.vue 当前数据流**
+
+Run: 读取 `cfc-frontend/pages/tasks/tasks.vue` 的 `loadTodayTasks`/`loadPendingTasks` 方法(已在调研中读过)。确认 `getTodayTasks(memberId)` 调用 `/api/tasks/today`。
+
+- [ ] **Step 2: tasks.vue 今日任务 Tab 增加进度型任务展示**
+
+在 `loadTodayTasks` 返回后,前端对 `isDailyProgress === 1` 的任务展示进度条(从 `task.progress`/`task.targetValue` 字段,需后端在 `/api/tasks/today` 返回中补充这两个字段)。
+
+修改后端 `TaskService.getTodayTasks` 返回前,为进度型任务补充 `progress`(从 task_executions.extra 读取)和 `targetValue` 字段——但 Task 实体返回 JSON 不含进度。选择:在 `TaskController.getTodayTasks` 响应中,对进度型任务附加字段(Map 包装或 Task 加 transient 字段)。
+
+**简化方案**:`Task.java` 加两个 `@TableField(exist = false)` 的冗余字段:
+
+```java
+    /** 当前进度(进度型任务,由查询时回填,不入库) */
+    @com.baomidou.mybatisplus.annotation.TableField(exist = false)
+    private Integer progress;
+
+    /** 是否已完成今日进度(进度型任务) */
+    @com.baomidou.mybatisplus.annotation.TableField(exist = false)
+    private Integer todayDone;
+```
+
+在 `TaskService.getTodayTasks` 中回填:
+
+```java
+        List<Task> tasks = taskMapper.selectList(wrapper);
+        fillProgress(tasks, memberId);
+        return filterLockedChildren(tasks);
+```
+
+新增 `fillProgress` 方法:
+
+```java
+    private void fillProgress(List<Task> tasks, Long memberId) {
+        if (tasks == null || tasks.isEmpty()) return;
+        for (Task t : tasks) {
+            if (t.getIsDailyProgress() != null && t.getIsDailyProgress() == 1) {
+                TaskExecution exec = taskExecutionMapper.selectOne(new LambdaQueryWrapper<TaskExecution>()
+                        .eq(TaskExecution::getTaskId, t.getId())
+                        .eq(TaskExecution::getMemberId, memberId)
+                        .last("LIMIT 1"));
+                int progress = 0;
+                if (exec != null && exec.getExtra() != null) {
+                    try {
+                        progress = JSON.parseObject(exec.getExtra()).getIntValue("progress");
+                    } catch (Exception ignore) {}
+                }
+                t.setProgress(progress);
+                t.setTodayDone("completed".equals(t.getStatus()) ? 1 : 0);
+            }
+        }
+    }
+```
+
+- [ ] **Step 3: daily-tasks.vue 重定向到 tasks.vue**
+
+将 `daily-tasks.vue` 的 onLoad 改为 `uni.redirectTo({ url: '/pages/tasks/tasks?tab=today' })`,或直接删除 pages.json 中的注册并保留文件备份。**选择:保留文件 + onLoad 重定向**,避免破坏其它入口(如 ProfileMenu 跳转)。
+
+```js
+  onLoad() {
+    uni.redirectTo({ url: '/pages/tasks/tasks?tab=today' })
+  },
+```
+
+同时 `tasks.vue` onLoad 支持 `tab` 参数:
+
+```js
+  onLoad(options) {
+    if (options && options.tab) {
+      this.activeTab = options.tab
+    }
+    // ... 原有 memberId 处理
+  },
+```
+
+- [ ] **Step 4: 前端语法校验**
+
+Run: `node --check` 提取 tasks.vue/daily-tasks.vue 的 script 块做语法校验(用项目已有脚本或手动提取)。
+Expected: 无语法错误
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add cfc-frontend/pages/tasks/tasks.vue cfc-frontend/pages/tasks/daily-tasks.vue
+git commit -m "feat(task): 前端统一任务中心——tasks.vue聚合进度型任务,daily-tasks.vue重定向"
+```
+
+---
+
+### Task 9: 收尾 — 文档同步 + 全量验证
+
+**Files:**
+- Modify: `docs/superpowers/PROJECT-OVERVIEW.md`
+- Modify: `docs/superpowers/specs/2026-09-04-task-system-unification-design.md`(如实现过程中有偏差,回写修正)
+
+- [ ] **Step 1: 更新 PROJECT-OVERVIEW.md**
+
+将 Task 8 中新增的 spec 行状态从"🟡 设计已确认"改为"🟢 已实施"(如全部完成),并更新说明。
+
+- [ ] **Step 2: 全量编译验证**
+
+Run: `cd cfc-backend && mvn clean compile`
+Expected: BUILD SUCCESS
+
+- [ ] **Step 3: 检查遗留引用**
+
+Run: `grep -rn "growthTaskService\.\|onboardingService\.\|growthTaskLogMapper\|onboardingTaskMapper" cfc-backend/src/main/java/ | grep -v "@Deprecated"`
+Expected: 无遗留(除 @Deprecated 委托方法)
+
+- [ ] **Step 4: Commit**
+
+```bash
+git add docs/superpowers/PROJECT-OVERVIEW.md
+git commit -m "docs: 统一任务系统实施完成,更新项目全景索引"
+```
+
+---
+
+## 风险与注意事项
+
+1. **`TaskService.updateProgress` 的 taskKey 匹配**:原 growth_task 用唯一 `task_key` 匹配,统一后 tasks 表无 task_key 列。Task 4 用 title 匹配(约定:迁移时 growth_task 的 task_key 写入 tasks.title)。若需严格匹配,可给 tasks 表再加 `task_key` 列——**默认不添加**,用 title 约定(YAGNI),Task 6 迁移时把 task_key 拼入 title 前缀(`"[key]"` 格式)以防冲突。
+2. **GrowthTaskService.claimReward 历史数据**:存量已 completed 未 claimed 的 growth_task_log,迁移后不可再领取。决策:迁移脚本在 Task 6 中为这些历史记录不做补偿(数据量小,影响可接受);若需补偿,可在迁移 297 中把未领取的 completed 记录转为 tasks 完成记录并发奖——**本计划不做**,避免复杂化。
+3. **前端 daily-tasks.vue 其它入口**:ProfileMenu.vue、growth-main/index.vue 等页面可能跳转 daily-tasks.vue。Task 8 用重定向而非删除,保证所有入口收敛到 tasks.vue。
+4. **OnboardingService 调用点**:Task 5 要求 grep 所有调用点逐一处理,这是最容易遗漏的部分。执行时务必完成 Step 3 的搜索。
+5. **`awardTaskCompletionRewards` 的 deadline 空指针**:Task 3 让进度型任务 deadline=null,而 `awardTaskCompletionRewards` 中 `task.getDeadline() != null` 已有空判断(`isEarly = task.getDeadline() != null && ...`),安全。
+6. **TaskPlanInstanceService.getListByChild 等**:方案实例查询接口保留不动,方案任务生成已走 source_type=plan(现状即如此)。
+
+## Self-Review 结论
+
+- **Spec 覆盖**:数据模型(Task 1-3)、来源枚举(Task 3/4/5)、奖励自动发放(Task 4 claim 改造)、每日重置(Task 4 updateProgress + RepeatTaskGenerator 现状)、前端统一(Task 8)、迁移(Task 6)、范围边界(家庭挑战/打卡不合并,计划未触碰)——全部覆盖。
+- **占位符扫描**:无 TBD/TODO;所有代码步骤含完整实现。
+- **类型一致性**:`isDailyProgress`/`targetValue`/`sourceType` 在 Task 1/3/4/5/8 中命名一致;`updateProgress`/`createDynamicTask`/`initOnboardingTasks`/`completeOnboardingTask` 签名在 Task 4/5 间一致。