2026-09-04-task-system-unification.md 39 KB

统一任务系统 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.javasourceType/sourceId 字段后追加:

    /** 1=每日进度型任务(无 deadline,每日重置;原 growth_task DAILY) */
    private Integer isDailyProgress;

    /** 进度目标值(is_daily_progress=1 时有效;原 growth_task.targetValue) */
    private Integer targetValue;
  • Step 2: 在 DatabaseInitializer 追加迁移 295

runMigrations() 方法末尾(搜索最新 // 迁移294 之后)追加:

// 迁移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.sqltasks 表(第 76 行附近)的 source_id 行后追加:

    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

    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.javateacherId 字段后追加:

    // 新人引导顺序(source_type=onboarding 的模板用:1/2/3... 按序生成+前置链)
    private Integer onboardingSequence;
  • [ ] Step 2: DatabaseInitializer 追加迁移 296

    // 迁移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 行附近)添加:

    onboarding_sequence INT DEFAULT NULL COMMENT '新人引导顺序(source_type=onboarding的模板用)',
  • Step 4: 编译验证

Run: cd cfc-backend && mvn clean compile Expected: BUILD SUCCESS

  • [ ] Step 5: Commit

    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.sourceTypeCreateTaskDTO.isDailyProgressCreateTaskDTO.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 添加字段

    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(...) 后追加:

        // 统一任务系统:来源/进度型透传
        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

    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 的行为迁移到 TaskServicegrowth_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 表):

    /**
     * 统一任务系统:更新每日进度型任务的进度(原 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

    /**
     * 统一任务系统: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 方法体替换为:

    @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 方法体替换为委托:

    @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 新增:

    /**
     * 统一任务系统:查询用户每日/新人进度型任务列表(原 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 改为委托:

    @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 接口:因奖励改为自动发放,返回"奖励已自动发放":

    @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

    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_templatesonboarding_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

    /**
     * 统一任务系统:新用户注册初始化新人引导任务
     * 按 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 改为委托:

    @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 新增:

    /**
     * 统一任务系统:完成指定新人引导任务(原 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

    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

    // 迁移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

    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

    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.vueloadTodayTasks/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) 的冗余字段:

    /** 当前进度(进度型任务,由查询时回填,不入库) */
    @com.baomidou.mybatisplus.annotation.TableField(exist = false)
    private Integer progress;

    /** 是否已完成今日进度(进度型任务) */
    @com.baomidou.mybatisplus.annotation.TableField(exist = false)
    private Integer todayDone;

TaskService.getTodayTasks 中回填:

        List<Task> tasks = taskMapper.selectList(wrapper);
        fillProgress(tasks, memberId);
        return filterLockedChildren(tasks);

新增 fillProgress 方法:

    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 跳转)。

  onLoad() {
    uni.redirectTo({ url: '/pages/tasks/tasks?tab=today' })
  },

同时 tasks.vue onLoad 支持 tab 参数:

  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

    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

    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,而 awardTaskCompletionRewardstask.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 间一致。