2026-08-19-task-system-upgrade.md 17 KB

任务系统升级设计文档(行为调度中心)

状态:设计稿 · 待评审 日期:2026-08-19 范围:cfc-backend(Spring Boot 2.7.18)+ cfc-frontend(uni-app Vue 2)+ 关联子系统

1. 背景与目标

现有 tasks 体系是「家长给孩子布置任务 + 积分激励」模型:创建 → 完成 → 审核 → 积分/能量/打卡。其核心价值(积分/streak/审核/五维能量/小游戏)全部保留。

本次升级目标:把任务系统提升为调度用户行为的行为中心——任务成为填写问卷、购买商品、阅读文章、参加活动、玩小游戏、专注训练等功能入口,支持前置依赖、灵活频率、填写式完成、开始-完成两阶段与购物里程碑。

设计原则

  1. 不推翻现有模型tasks 表、积分/审核/streak/五维能量链路全部复用,只做增量扩展。
  2. 任务 = 模板 + 实例:现有 RepeatTaskGenerator 已确立「模板(is_template=1)→ 实例」模式,本次将频率/前置/联动全部建模在模板与实例两层。
  3. 联动不硬编码:任务通过「动作类型 action」+「动作参数」声明式关联子系统,子系统侧在关键节点发事件,由 TaskEngine 统一判定完成。
  4. 完成判定集中化:所有完成路径(打卡/填写/游戏/支付/收货/阅读)最终走同一个 TaskEngine.completeTask(),保证积分/能量/前置解锁逻辑唯一。

2. 现状差距总览(勘察结论)

能力 现状 结论
任务 CRUD / 状态机 tasks 表,7 状态,完整 ✅ 复用
完成计分 / streak / 审核 / 五维能量 TaskService.completeTask 全链路 ✅ 复用
小游戏联动 category=小游戏类 + minigame_code + complete-minigame 接口 ✅ 复用,补 start
频率任务 仅 daily/weekly,RepeatTaskGenerator 凌晨5点扫描 repeatType ⚠️ 扩展 frequency 语义
前置任务 ❌ 新增
填写式完成 complete_types 字段有,无强制校验 ❌ 补校验
购物任务 无联动;product_orders 有 paid/completed、fulfill_status=received_by_user ❌ 新增
文章阅读任务 article_reading_records 有 task_id 字段 ⚠️ 补自动判定
专注训练 focus_sessions 存在(child 维度) ⚠️ 补任务挂钩
活动任务 activity_registrations/activity_orders 存在 ❌ 新增

3. 数据模型扩展(核心)

3.1 tasks 表新增字段(迁移)

-- 迁移N: tasks 表任务系统升级扩展字段
ALTER TABLE tasks
    ADD COLUMN prerequisite_task_id BIGINT NULL COMMENT '前置任务ID:完成后才解锁本任务' AFTER template_id,
    ADD COLUMN action_type VARCHAR(32) NULL COMMENT '联动动作: minigame/focus/survey/article/activity/cart/order' AFTER source_id,
    ADD COLUMN action_config JSON NULL COMMENT '联动参数: {"gameCode":"schulte","productId":123,"minSeconds":30,"completeMilestone":"paid"/"received","surveyId":99}' AFTER action_type,
    ADD COLUMN require_input TINYINT DEFAULT 0 COMMENT '1=需填写内容才能完成' AFTER need_review,
    ADD COLUMN start_required TINYINT DEFAULT 0 COMMENT '1=需先开始再完成(两阶段)' AFTER require_input,
    ADD COLUMN start_count INT DEFAULT 0 COMMENT '已开始次数(累计)' AFTER completed_at,
    ADD COLUMN min_duration_seconds INT DEFAULT 0 COMMENT '最短持续秒数(专注/游戏类,未达标不计完成)' AFTER duration;

字段语义:

字段 说明
prerequisite_task_id 前置任务。本任务完成条件之一:该任务已 completed。支持链式。
action_type 联动动作类型(见 §4)。null/空 = 纯手动任务(现有行为不变)。
action_config 动作参数 JSON,按 action_type 解析。
require_input =1 时 completeTask 必须带 content(文本/图片/语音等),否则拒绝。
start_required =1 时必须先调 /start 进入 in_progress,再调 complete。小游戏/专注/购物任务的统一开关。
start_count 开始次数累计,供频率判定(如"每日最多开始 N 次")。
min_duration_seconds 开始到完成的最短间隔;不足则拒绝完成(防秒完成)。

兼容性:所有新字段可空,存量任务(action_type=NULL)走旧逻辑,零迁移风险。

3.2 频率语义扩展(复用 repeat_type + frequency)

现状:repeat_type ENUM('none','daily','weekly') + frequency(daily/weekly/monthly,无逻辑消费)。

改造:以 frequency 为唯一频率字段,repeat_type 保留兼容。

frequency 值 含义 生成策略
none 一次性 不生成
hourly 每小时 RepeatTaskGenerator 每小时执行
daily 每天 凌晨5点生成当日实例
every_other_day 隔天 按模板 created_at 奇偶日判定
weekly 每周 按模板 deadline 星期几判定
monthly 每月 按模板 deadline 日期判定
custom 自定义(配合 frequency_cron 由 action_config 内 cronintervalHours 驱动

实例生成器改造(RepeatTaskGenerator 重构):

// 每小时调度入口(原每日5点改为频率分派器)
@Scheduled(cron = "0 5 * * * ?")   // 每小时第5分
public void generateByFrequency() {
    // 按 frequency 分组扫描 is_template=1 的任务模板
    // daily/weekly/monthly 沿用现有 hasTaskForDate 去重逻辑
    // hourly: 检查最近1小时无实例则生成(去重键:title+child+family+1小时窗口)
    // every_other_day: 计算模板创建日至今的间隔天数,偶数天生成
}

去重键统一收敛为 title + childId + familyId + 时间窗口,与现有 hasTaskForDate 一致,避免重复实例。

3.3 任务实例记录(新表:任务执行进度明细)

两阶段(开始-完成)与购物里程碑需要记录进度快照,现有 tasks 表一个状态字段不够。新增:

-- 迁移N: 任务执行进度表(两阶段/里程碑/填写内容记录)
CREATE TABLE IF NOT EXISTS task_executions (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    task_id BIGINT NOT NULL COMMENT '任务实例ID',
    member_id BIGINT NOT NULL COMMENT '执行成员ID(family_members.id)',
    status VARCHAR(20) NOT NULL DEFAULT 'started' COMMENT 'started/finished/failed',
    started_at DATETIME NOT NULL COMMENT '开始时间',
    finished_at DATETIME COMMENT '完成时间',
    milestone VARCHAR(32) COMMENT '购物任务里程碑: cart_added/paid/received(当前进度)',
    content TEXT COMMENT '填写内容(require_input 任务)',
    content_type VARCHAR(20) COMMENT 'content 类型: text/image/audio/video',
    extra JSON COMMENT '扩展数据: {gameScore, focusMinutes, orderId, articleId, surveyId}',
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    INDEX idx_task_member (task_id, member_id),
    INDEX idx_status (status)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='任务执行进度表';

task_executions 是 task 的进度明细(1:N:一个实例任务可被多次开始/执行),tasks.status 仍是权威状态。

4. 联动动作(action_type)设计

action_type 触发点 完成判定 里程碑
minigame 小游戏页(schulte/1a2b/sudoku) 游戏结束 → complete-minigame(现有)
focus 专注计时器页 计时结束 → complete,需 ≥ min_duration_seconds
survey 问卷/测评页 问卷提交 → 回写任务
article 文章详情页 阅读 ≥ 指定秒数(minSeconds)→ 自动/手动完成
activity 活动详情页 活动报名成功 → 完成
cart 任务卡片"去购物"按钮 点开始:自动加购 → 跳购物车页 cart_added
order 购物车→结算页 点开始:自动加购+跳转;支付成功 → 里程碑 paid确认收货 → 里程碑 received paid / received(由 action_config.completeMilestone 决定哪个算完成)

4.1 购物任务两阶段流程(§需求5 核心)

任务卡"开始购物" → POST /api/tasks/{id}/start
  → 后端校验前置/频率 → 状态 pending→in_progress → 创建 task_executions(started)
  → 按 action_config.productId 调 CartController 加入购物车 → milestone=cart_added
  → 返回 {executionId, cartJumpUrl}
前端跳 /pages/shop/cart(或直接下单页)
  → 用户结算支付
  → PaymentController 支付成功回调(现成 hooks)→ 查 task_executions 关联
     → 若 completeMilestone=paid → TaskEngine.completeTask()
     → 若 completeMilestone=received → milestone=paid,等收货
  → 用户确认收货(ProductOrderController 确认收货接口现成)
     → 若 completeMilestone=received → TaskEngine.completeTask()

关联方式task_executions.extra.orderId 记录订单号,支付/收货回调按 orderId → executionId → taskId 反查。在 PaymentController 支付成功分支和订单确认收货分支各插入一行事件发布(taskEventPublisher.publish(paid/received, orderId)),不动订单主流程。

4.2 文章阅读任务

article_reading_records 已有 task_id 字段,但当前无自动判定。改造:

  • 文章页 onHide/onUnload 时上报阅读时长(现已有上报接口则复用;无则加轻量 POST /api/tasks/{id}/progress-article
  • 阅读时长 ≥ action_config.minSeconds 且任务存在 → 自动 complete

4.3 专注任务

focus_sessions 已有完整记录(started_at/completed_at/duration)。在专注结束保存处插入:若该会话绑定任务(focus_sessions 增加 task_id 字段,迁移),结束后调 TaskEngine 判定(duration ≥ min_duration_seconds)。

5. 前置任务(§需求1)

判定逻辑

completeTask() 完成前校验(TaskEngine 内统一):

if (task.getPrerequisiteTaskId() != null) {
    Task prereq = taskMapper.selectById(task.getPrerequisiteTaskId());
    if (prereq == null || !"completed".equals(prereq.getStatus())) {
        throw new BizException("需先完成前置任务:" + (prereq != null ? prereq.getTitle() : ""));
    }
}

解锁可见性

  • /api/tasks/today 返回任务时,带 lockedByPrerequisite 标记:前置未完成 → 前端灰态 + "🔒 完成前置任务后解锁"提示,不展示完成按钮(但展示任务内容供了解)。
  • 前置任务完成 → 调用 TaskEngine.checkUnlock() 把后置任务 status 从 locked(新状态)改为 pending

状态机扩展tasks.status ENUM 增加 locked(仅用于有前置且前置未完成的任务;无前置任务不受影响)。

循环依赖防护

创建/更新任务时校验 prerequisite_task_id 不成环(BFS 沿 prerequisite_task_id 上溯,遇到自身/超过 20 层即拒绝)。

6. 填写式完成(§需求4)

  • 任务创建时 require_input=1 + complete_types 声明允许的内容类型(text/image/audio/video,复用现有字段)。
  • POST /api/tasks/{id}/complete 增强:require_input=1 时必传 content + contentType,否则 Result.error("该任务需填写内容后完成")
  • 内容落 task_executions.content / content_type(兼容:现有 media_records 表继续可用,新逻辑优先写 executions)。
  • need_review=1 时,填写内容随审核流走(现有 task_reviews 逻辑不变)。

7. 接口清单(新增/修改)

新增接口

接口 方法 说明
POST /api/tasks/{id}/start POST 开始任务:校验前置/频率/会员 → pending→in_progress → 创建 execution;购物任务触发加购;返回 executionId + 跳转参数
POST /api/tasks/{id}/progress-article POST 文章阅读进度上报(article 联动)
POST /api/tasks/{id}/unlock-check POST 检查并解锁后置任务(前置完成时由 completeTask 自动调用,此接口供前端手动刷新)
POST /api/tasks/template/create POST 模板任务创建(含 frequency/action/前置配置;复用 CreateTaskDTO 扩展)
POST /api/tasks/template/list POST 模板列表(管理端/规划师端复用)

修改接口

接口 变更
POST /api/tasks/create CreateTaskDTO 增加 prerequisiteTaskId/actionType/actionConfig/requireInput/startRequired/minDurationSeconds 字段
POST /api/tasks/{id}/complete 校验前置 + require_input 强校验 + 两阶段任务必须已 start(start_required=1 且无 in_progress execution → 拒绝)
POST /api/tasks/{id}/complete-minigame 增加 minDurationSeconds 校验
POST /api/tasks/today 返回 lockedByPrerequisiteactionTypeactionConfigcanStart(供前端渲染联动按钮)
POST /api/tasks/history 返回 execution 摘要(进度/里程碑)

事件钩子(不动现有业务主流程)

现有位置 插入点 事件
PaymentController 支付成功分支 成功回调内 TaskEventPublisher.orderPaid(orderId)
订单确认收货接口 received_by_user 更新处 TaskEventPublisher.orderReceived(orderId)
ArticleController 阅读上报 时长更新处 由 progress-article 直接驱动
专注结束保存 focus_sessions.completed_at 更新处 直接驱动

8. 前端改造点(cfc-frontend)

8.1 任务卡渲染(pages/tasks/tasks.vue + daily-tasks.vue)

action_type 渲染不同主按钮:

action_type 按钮文案 行为
null(纯手动) 完成 现有逻辑 + require_input 弹填写面板
minigame 开始游戏 navigateTo 游戏页带 taskId(现有)+ 先调 /start
focus 开始专注 navigateTo 专注页带 taskId
survey 去填写 navigateTo 问卷页带 taskId
article 去阅读 navigateTo 文章页带 taskId
activity 去报名 navigateTo 活动页带 taskId
cart / order 去购物 调 /start(自动加购)→ navigateTo 购物车/下单页
前置未解锁(locked) 🔒 已完成前置解锁 灰态禁用 + 提示

新增交互:

  • require_input 任务:点完成 → 弹出填写面板(文本/拍照/录音,按 complete_types 显示)→ 提交 content。
  • start_required 任务:未 start 时按钮为"开始",已 start 返回后按钮变"完成"(依据 task_executions 状态)。

8.2 新增页面/组件

文件 说明
components/task-input-panel.vue 填写式完成面板(文本/图片/语音/视频切换,复用现有上传组件)
pages/tasks/task-detail.vue 任务详情(前置链展示、进度、里程碑、执行历史)——可选,MVP 可先不做

8.3 管理端(cfc-web,可选二期)

admin_task_templates 已有表结构,管理端任务模板编辑页增加:频率选择(hourly/daily/隔天/weekly/monthly)、联动动作配置、前置任务选择器。此项可排二期。

9. 与现有系统的兼容性

现有特性 影响 处理
积分/streak/审核/五维能量 无影响 completeTask 逻辑原样保留,TaskEngine 只在其前后加校验
RepeatTaskGenerator 重构 保留 daily/weekly 现有行为,扩展频率分派;去重逻辑不变
套餐任务(task_plan_instances) 无影响 其生成的 Task 不带 action_type,走手动完成
小游戏任务 兼容 现有 complete-minigame 保留;start_required=0 时行为不变
member 任务(assign/accept/reject) 无影响 accept→in_progress 已存在,与 start 语义并存(start 是执行态,accept 是接收态)

10. 分阶段实施计划

阶段一(MVP,核心闭环)

  1. 数据库迁移:tasks 新字段 + task_executions 表 + focus_sessions.task_id
  2. TaskEngine 重构:completeTask 增加前置校验 / require_input 强校验 / 两阶段校验(start_required)
  3. /start 接口 + task_executions 记录
  4. RepeatTaskGenerator 频率分派(hourly/daily/every_other_day/weekly/monthly)
  5. 前端:任务卡 action_type 渲染 + 填写面板 + 开始/完成两阶段
  6. 验证:mvn clean compile + node --check + 手工联调

阶段二(外部联动)

  1. 购物任务(cart/order):start 自动加购 + 支付/收货事件钩子 + 里程碑判定
  2. 文章阅读任务:阅读时长上报与自动完成
  3. 专注任务:focus_sessions 挂钩
  4. 活动任务:报名成功回写

阶段三(增强)

  1. 前置任务链 UI(任务详情页)
  2. cfc-web 管理端模板编辑增强
  3. 任务编排页(拖拽式前置关系)——远期

11. 风险与注意

  • 频率生成器并发:RepeatTaskGenerator 重构后需保证幂等(去重键 + 唯一约束),防止重复实例。
  • 支付回调时序:购物任务依赖支付回调,测试模式(wechat.test-mode=true)需 mock 事件。
  • 前置环检测:创建任务时 BFS 校验,避免 A→B→A 死锁。
  • 存量数据:新字段全部可空,旧任务零影响;locked 状态仅新任务使用。
  • 前端兼容:任务卡渲染改动需保证无 action_type 的旧任务显示原按钮(默认"完成")。