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 再动工)
@PostMapping,禁止 GET/PUT/DELETE;响应统一 Result<T> (code/message/data)@Resource,字段名匹配类型默认 Bean Name@TableName + @TableId(type = IdType.AUTO)DatabaseInitializer.runMigrations(),每次迁移后必须同步 schema.sql// 迁移 确认最新编号(当前 294),新迁移从 295 递增?.、禁止 CSS Grid、禁止 :key 表达式、禁止 new Date(string)(用 parseDate())、Vue2 Options APIcd cfc-backend && mvn clean compile;前端验证:node --check(不执行打包命令)docs/superpowers/api/API_REFERENCE.md;废弃接口标记 410CREATE TABLE 必须同步;docs/superpowers/PROJECT-OVERVIEW.md 必须同步更新Files:
cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java(runMigrations 末尾追加迁移 295)cfc-backend/src/main/resources/schema.sql(tasks 表 CREATE TABLE 同步)cfc-backend/src/main/java/com/etotem/cfc/entity/Task.javaInterfaces:
Task.isDailyProgress (Integer)、Task.targetValue (Integer) getter/setter(Lombok @Data 自动生成)Consumes: 无
[ ] Step 1: 在 Task.java 实体添加两个字段
在 entity/Task.java 的 sourceType/sourceId 字段后追加:
/** 1=每日进度型任务(无 deadline,每日重置;原 growth_task DAILY) */
private Integer isDailyProgress;
/** 进度目标值(is_daily_progress=1 时有效;原 growth_task.targetValue) */
private Integer targetValue;
在 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) {
// 修改失败,忽略
}
在 schema.sql 的 tasks 表(第 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'。
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扩展(统一任务系统)"
Files:
cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java(迁移 296)cfc-backend/src/main/resources/schema.sql(admin_task_templates 建表语句)cfc-backend/src/main/java/com/etotem/cfc/entity/TaskTemplate.javaInterfaces:
Produces: TaskTemplate.onboardingSequence (Integer) getter/setter
[ ] Step 1: TaskTemplate.java 添加字段
在 entity/TaskTemplate.java 的 teacherId 字段后追加:
// 新人引导顺序(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的模板用)',
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字段(新人引导复用任务模板)"
Files:
cfc-backend/src/main/java/com/etotem/cfc/dto/CreateTaskDTO.javacfc-backend/src/main/java/com/etotem/cfc/service/TaskService.java(createSingleTask)Interfaces:
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 添加字段
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。
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透传"
核心目标: GrowthTaskService.updateProgress/createDynamicTask 的行为迁移到 TaskService,growth_task/growth_task_log 表写入停止,改为写 tasks/task_executions。
Files:
cfc-backend/src/main/java/com/etotem/cfc/service/TaskService.java(新增 updateProgress/createDynamicTask 方法)cfc-backend/src/main/java/com/etotem/cfc/service/GrowthTaskService.java(方法改为委托 TaskService,标注 @Deprecated)cfc-backend/src/main/java/com/etotem/cfc/controller/GrowthTaskController.java(list/claim 逻辑调整)Interfaces:
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;
}
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("奖励已自动发放");
}
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委托+废弃"
核心目标: 新人引导任务不再写 onboarding_task 表,改为:注册时按 admin_task_templates 中 onboarding_sequence 有值的模板批量生成 tasks(source_type=onboarding),顺序链用 prerequisite_task_id。
Files:
cfc-backend/src/main/java/com/etotem/cfc/service/OnboardingService.javacfc-backend/src/main/java/com/etotem/cfc/service/TaskService.java(新增 initOnboardingTasks 方法)cfc-backend/src/main/java/com/etotem/cfc/mapper/TaskTemplateMapper.java(如需按 onboarding_sequence 查询)cfc-backend/src/main/resources/schema.sql(可选:admin_task_templates 种子数据)Interfaces:
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)。
将 OnboardingService.initForUser 改为委托:
@Resource
private TaskService taskService;
/**
* 统一任务系统:新人引导初始化(委托 TaskService 生成 tasks 表 onboarding 任务)
*/
@Deprecated
public void initForUser(Long userId) {
taskService.initOnboardingTasks(userId);
}
completeTask/claimReward/getProgress 三个方法:因新人任务已迁入 tasks 表,这些方法调用方需要排查。搜索调用点:
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());
}
}
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,顺序链前置解锁)"
核心目标: 写一次性迁移脚本,把存量 growth_task(DAILY/AI)、growth_task_log(进度)、onboarding_task 数据搬入 tasks 表,旧表数据保留只读。
Files:
cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java(迁移 297,仅执行一次的数据回填)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 模板记录)。
存量 growth_task_log 的 DAILY 进度属于历史数据。由于统一后按"每天生成新实例"模型,历史进度无法 1:1 映射到 tasks 实例。决策:历史 growth_task_log 数据仅保留在旧表(只读备份),不回填 tasks。在迁移注释中说明即可,无需代码。
同 Step 2 决策:存量 onboarding_task 数据保留在旧表只读,不回填(新注册用户走 Task 5 的新逻辑)。
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(旧表只读保留)"
核心目标: 旧表读取接口(growth-task/list、onboarding 相关)标记废弃(410),新代码走 tasks 表统一接口。
Files:
cfc-backend/src/main/java/com/etotem/cfc/controller/GrowthTaskController.javaModify: 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*
若存在,读取其接口清单。
/api/growth-task/* 三个接口整体标注:类上 @Deprecated,/list 返回数据改为查 tasks(Task 4 已改),/claim 返回"奖励已自动发放",/accept-dynamic 保持兼容(查 tasks 表 source_type=ai 的任务)。
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表"
核心目标: pages/tasks/tasks.vue 成为统一任务中心,展示所有来源任务;daily-tasks.vue 废弃,进度展示能力并入"今日任务"Tab。
Files:
cfc-frontend/pages/tasks/tasks.vuecfc-frontend/pages/tasks/daily-tasks.vue(标记废弃或重定向)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。
在 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);
}
}
}
将 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 处理
},
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重定向"
Files:
docs/superpowers/PROJECT-OVERVIEW.mdModify: docs/superpowers/specs/2026-09-04-task-system-unification-design.md(如实现过程中有偏差,回写修正)
[ ] Step 1: 更新 PROJECT-OVERVIEW.md
将 Task 8 中新增的 spec 行状态从"🟡 设计已确认"改为"🟢 已实施"(如全部完成),并更新说明。
Run: cd cfc-backend && mvn clean compile
Expected: BUILD SUCCESS
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: 统一任务系统实施完成,更新项目全景索引"
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]" 格式)以防冲突。awardTaskCompletionRewards 的 deadline 空指针:Task 3 让进度型任务 deadline=null,而 awardTaskCompletionRewards 中 task.getDeadline() != null 已有空判断(isEarly = task.getDeadline() != null && ...),安全。isDailyProgress/targetValue/sourceType 在 Task 1/3/4/5/8 中命名一致;updateProgress/createDynamicTask/initOnboardingTasks/completeOnboardingTask 签名在 Task 4/5 间一致。