# 统一任务系统 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` (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() .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() .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() .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() .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> getGrowthTaskList(Long userId, String type, String dimension) { FamilyMember member = familyMemberMapper.selectOne( new LambdaQueryWrapper() .eq(FamilyMember::getUserId, userId) .last("LIMIT 1")); List> 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 tasks = taskMapper.selectList(new LambdaQueryWrapper() .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 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() .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> getTaskList(Long userId, Map 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 claim(@RequestAttribute("userId") Long userId, @RequestBody Map 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() .eq(Task::getCreatorId, userId) .eq(Task::getSourceType, "onboarding")); if (existing != null && existing > 0) return; List templates = taskTemplateMapper.selectList( new LambdaQueryWrapper() .eq(TaskTemplate::getIsActive, 1) .isNotNull(TaskTemplate::getOnboardingSequence) .orderByAsc(TaskTemplate::getOnboardingSequence)); if (templates.isEmpty()) return; FamilyMember member = familyMemberMapper.selectOne( new LambdaQueryWrapper() .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() .eq(FamilyMember::getUserId, userId) .last("LIMIT 1")); if (member == null) return; Task task = taskMapper.selectOne(new LambdaQueryWrapper() .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 tasks = taskMapper.selectList(wrapper); fillProgress(tasks, memberId); return filterLockedChildren(tasks); ``` 新增 `fillProgress` 方法: ```java private void fillProgress(List 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() .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 间一致。