Browse Source

docs(task): 任务系统升级为行为调度中心-整体设计文档

- 现状勘察结论:tasks 模型 + RepeatTaskGenerator(仅 daily/weekly)+ 小游戏联动 + 套餐任务体系
- 缺口:无前置任务/频率单一/购物零联动/require_input 无强制/无 start 两阶段
- 设计:action_type 声明式联动、TaskEngine 集中判定、task_executions 执行记录、三阶段实施路径

Ultraworked with [Sisyphus](https://github.com/OhMyOpenCode)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
E2E Test Bot 1 month ago
parent
commit
c0826c5c19
1 changed files with 295 additions and 0 deletions
  1. 295 0
      docs/architecture/2026-08-19-task-system-upgrade.md

+ 295 - 0
docs/architecture/2026-08-19-task-system-upgrade.md

@@ -0,0 +1,295 @@
+# 任务系统升级设计文档(行为调度中心)
+
+> 状态:设计稿 · 待评审
+> 日期: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` 表新增字段(迁移)
+
+```sql
+-- 迁移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 内 `cron` 或 `intervalHours` 驱动 |
+
+**实例生成器改造(RepeatTaskGenerator 重构):**
+
+```java
+// 每小时调度入口(原每日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` 表一个状态字段不够。新增:
+
+```sql
+-- 迁移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 内统一):
+
+```java
+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` | 返回 `lockedByPrerequisite`、`actionType`、`actionConfig`、`canStart`(供前端渲染联动按钮) |
+| `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 + 手工联调
+
+### 阶段二(外部联动)
+7. 购物任务(cart/order):start 自动加购 + 支付/收货事件钩子 + 里程碑判定
+8. 文章阅读任务:阅读时长上报与自动完成
+9. 专注任务:focus_sessions 挂钩
+10. 活动任务:报名成功回写
+
+### 阶段三(增强)
+11. 前置任务链 UI(任务详情页)
+12. cfc-web 管理端模板编辑增强
+13. 任务编排页(拖拽式前置关系)——远期
+
+## 11. 风险与注意
+
+- **频率生成器并发**:RepeatTaskGenerator 重构后需保证幂等(去重键 + 唯一约束),防止重复实例。
+- **支付回调时序**:购物任务依赖支付回调,测试模式(wechat.test-mode=true)需 mock 事件。
+- **前置环检测**:创建任务时 BFS 校验,避免 A→B→A 死锁。
+- **存量数据**:新字段全部可空,旧任务零影响;`locked` 状态仅新任务使用。
+- **前端兼容**:任务卡渲染改动需保证无 action_type 的旧任务显示原按钮(默认"完成")。