瀏覽代碼

Merge remote-tracking branch 'origin/cfclub' for upstream changes

Xiaogang Liao 2 月之前
父節點
當前提交
71f6b57db3

+ 12 - 0
cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java

@@ -6123,5 +6123,17 @@ try {
     } catch (Exception e) {
         log.error("迁移96 失败: " + e.getMessage());
     }
+
+    // 迁移97: growth_task表扩展 — 支持AI_GENERATED类型 + 五维维度/来源会话字段
+    try {
+        jdbcTemplate.execute("ALTER TABLE growth_task MODIFY COLUMN type "
+                + "VARCHAR(16) NOT NULL COMMENT 'DAILY/NEWBIE/AI_GENERATED'");
+        log.info("已扩展growth_task.type为支持AI_GENERATED");
+    } catch (Exception e) {
+        log.warn("扩展growth_task.type失败(可能已经扩展): " + e.getMessage());
+    }
+    ensureColumn("growth_task", "dimension", "VARCHAR(20) NULL COMMENT '五维维度(body/mind/wisdom/action/wealth)'");
+    ensureColumn("growth_task", "source_conversation_id", "VARCHAR(100) NULL COMMENT '来源AI会话ID'");
+    log.info("迁移97: growth_task 表扩展完成");
 }
 }

+ 24 - 0
cfc-backend/src/main/java/com/etotem/cfc/dto/TaskParseResult.java

@@ -0,0 +1,24 @@
+package com.etotem.cfc.dto;
+
+public class TaskParseResult {
+    private String title;
+    private String description;
+    private String deadline;
+    private String dimension;
+    private Integer rewardPoints;
+
+    public String getTitle() { return title; }
+    public void setTitle(String title) { this.title = title; }
+
+    public String getDescription() { return description; }
+    public void setDescription(String description) { this.description = description; }
+
+    public String getDeadline() { return deadline; }
+    public void setDeadline(String deadline) { this.deadline = deadline; }
+
+    public String getDimension() { return dimension; }
+    public void setDimension(String dimension) { this.dimension = dimension; }
+
+    public Integer getRewardPoints() { return rewardPoints; }
+    public void setRewardPoints(Integer rewardPoints) { this.rewardPoints = rewardPoints; }
+}

+ 6 - 0
cfc-backend/src/main/java/com/etotem/cfc/entity/GrowthTask.java

@@ -34,4 +34,10 @@ public class GrowthTask implements Serializable {
     private Integer sortOrder;
 
     private Date createdAt;
+
+    /** 五维维度: body/mind/wisdom/action/wealth */
+    private String dimension;
+
+    /** 来源AI会话ID(AI_GENERATED类型任务使用) */
+    private String sourceConversationId;
 }

+ 73 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/TaskParseService.java

@@ -0,0 +1,73 @@
+package com.etotem.cfc.service;
+
+import com.etotem.cfc.dto.TaskParseResult;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Service;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * 解析 Dify 返回的 [TASK] 标记,提取任务参数
+ */
+@Service
+public class TaskParseService {
+
+    private static final Logger log = LoggerFactory.getLogger(TaskParseService.class);
+
+    private static final Pattern TASK_PATTERN =
+            Pattern.compile("\\[TASK:\\s*(\\{.*?\\})\\]", Pattern.DOTALL);
+
+    private final ObjectMapper objectMapper = new ObjectMapper();
+
+    /**
+     * 从 Dify answer 中解析所有 [TASK] 标记
+     * @param answer Dify 原始回复
+     * @return 解析后的任务参数列表(title 必填,不满足校验的跳过)
+     */
+    public List<TaskParseResult> parseTasks(String answer) {
+        List<TaskParseResult> results = new ArrayList<>();
+        if (answer == null || answer.isEmpty()) return results;
+
+        Matcher matcher = TASK_PATTERN.matcher(answer);
+        while (matcher.find()) {
+            String json = matcher.group(1);
+            try {
+                @SuppressWarnings("unchecked")
+                Map<String, Object> map = objectMapper.readValue(json, Map.class);
+                TaskParseResult task = new TaskParseResult();
+                task.setTitle(map.containsKey("title") ? map.get("title").toString() : null);
+                task.setDescription(map.containsKey("description") ? map.get("description").toString() : null);
+                task.setDeadline(map.containsKey("deadline") ? map.get("deadline").toString() : null);
+                task.setDimension(map.containsKey("dimension") ? map.get("dimension").toString() : null);
+                if (map.containsKey("rewardPoints")) {
+                    Object rp = map.get("rewardPoints");
+                    task.setRewardPoints(rp instanceof Number ? ((Number) rp).intValue() : null);
+                }
+                if (task.getTitle() == null || task.getTitle().isEmpty()) {
+                    log.warn("跳过无标题的 TASK 标记: {}", json);
+                    continue;
+                }
+                results.add(task);
+            } catch (Exception e) {
+                log.warn("解析 TASK 标记失败, 跳过: json={}, error={}", json, e.getMessage());
+            }
+        }
+        return results;
+    }
+
+    /**
+     * 从 Dify answer 中移除所有 [TASK] 标记
+     * @param answer Dify 原始回复
+     * @return 清洗后的回复
+     */
+    public String stripTaskMarkers(String answer) {
+        if (answer == null) return null;
+        return TASK_PATTERN.matcher(answer).replaceAll("").trim();
+    }
+}

+ 3 - 1
cfc-backend/src/main/resources/schema.sql

@@ -1827,7 +1827,7 @@ CREATE TABLE IF NOT EXISTS onboarding_task (
 
 CREATE TABLE IF NOT EXISTS growth_task (
     id BIGINT AUTO_INCREMENT PRIMARY KEY,
-    type VARCHAR(16) NOT NULL COMMENT 'DAILY/NEWBIE',
+    type VARCHAR(16) NOT NULL COMMENT 'DAILY/NEWBIE/AI_GENERATED',
     title VARCHAR(100) NOT NULL,
     description TEXT,
     reward_points INT DEFAULT 0,
@@ -1837,6 +1837,8 @@ CREATE TABLE IF NOT EXISTS growth_task (
     enabled TINYINT DEFAULT 1,
     sort_order INT DEFAULT 0,
     created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+    dimension VARCHAR(20) NULL COMMENT '五维维度(body/mind/wisdom/action/wealth)',
+    source_conversation_id VARCHAR(100) NULL COMMENT '来源AI会话ID',
     INDEX idx_type (type)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='成长任务配置表';
 

+ 60 - 66
cfc-frontend/manifest.json

@@ -1,68 +1,62 @@
 {
-    "name" : "浠艾福",
-    "appid" : "__UNI__DA59D6D",
-    "description" : "关爱家庭成长小程序",
-    "versionName" : "1.0.0",
-    "versionCode" : "100",
-    "transformPx" : false,
-    "app-plus" : {
-        "usingComponents" : true,
-        "nvueStyleCompiler" : "uni-app",
-        "compilerVersion" : 3,
-        "splashscreen" : {
-            "alwaysShowBeforeRender" : true,
-            "waiting" : true,
-            "autoclose" : true,
-            "delay" : 0
-        },
-        "modules" : {},
-        "distribute" : {
-            "android" : {
-                "permissions" : [
-                    "<uses-permission android:name=\"android.permission.CHANGE_NETWORK_STATE\"/>",
-                    "<uses-permission android:name=\"android.permission.MOUNT_UNMOUNT_FILESYSTEMS\"/>",
-                    "<uses-permission android:name=\"android.permission.VIBRATE\"/>",
-                    "<uses-permission android:name=\"android.permission.READ_LOGS\"/>",
-                    "<uses-permission android:name=\"android.permission.ACCESS_WIFI_STATE\"/>",
-                    "<uses-feature android:name=\"android.hardware.camera.autofocus\"/>",
-                    "<uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\"/>",
-                    "<uses-permission android:name=\"android.permission.CAMERA\"/>",
-                    "<uses-permission android:name=\"android.permission.GET_ACCOUNTS\"/>",
-                    "<uses-permission android:name=\"android.permission.READ_PHONE_STATE\"/>",
-                    "<uses-permission android:name=\"android.permission.CHANGE_WIFI_STATE\"/>",
-                    "<uses-permission android:name=\"android.permission.WAKE_LOCK\"/>",
-                    "<uses-permission android:name=\"android.permission.FLASHLIGHT\"/>",
-                    "<uses-feature android:name=\"android.hardware.camera\"/>",
-                    "<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>"
-                ]
-            },
-            "ios" : {},
-            "sdkConfigs" : {}
-        }
+  "name": "浠艾福",
+  "appid": "__UNI__DA59D6D",
+  "description": "关爱家庭成长小程序",
+  "versionName": "1.0.0",
+  "versionCode": "100",
+  "transformPx": false,
+  "app-plus": {
+    "nvueStyleCompiler": "uni-app",
+    "compilerVersion": 3,
+    "splashscreen": {
+      "alwaysShowBeforeRender": true,
+      "waiting": true,
+      "autoclose": true,
+      "delay": 0
     },
-    "quickapp" : {},
-    "mp-weixin" : {
-        "appid" : "wx5ba8038ef16fb245",
-        "setting" : {
-            "urlCheck" : false,
-            "es6" : true,
-            "postcss" : true,
-            "minified" : true,
-            "lazyCodeLoading" : "requiredComponents"
-        },
-        "usingComponents" : true
-    },
-    "mp-alipay" : {
-        "usingComponents" : true
-    },
-    "mp-baidu" : {
-        "usingComponents" : true
-    },
-    "mp-toutiao" : {
-        "usingComponents" : true
-    },
-    "uniStatistics" : {
-        "enable" : false
-    },
-    "vueVersion" : "2"
-}
+    "modules": {},
+    "distribute": {
+      "android": {
+        "permissions": [
+          "<uses-permission android:name=\"android.permission.CHANGE_NETWORK_STATE\"/>",
+          "<uses-permission android:name=\"android.permission.MOUNT_UNMOUNT_FILESYSTEMS\"/>",
+          "<uses-permission android:name=\"android.permission.VIBRATE\"/>",
+          "<uses-permission android:name=\"android.permission.READ_LOGS\"/>",
+          "<uses-permission android:name=\"android.permission.ACCESS_WIFI_STATE\"/>",
+          "<uses-feature android:name=\"android.hardware.camera.autofocus\"/>",
+          "<uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\"/>",
+          "<uses-permission android:name=\"android.permission.CAMERA\"/>",
+          "<uses-permission android:name=\"android.permission.GET_ACCOUNTS\"/>",
+          "<uses-permission android:name=\"android.permission.READ_PHONE_STATE\"/>",
+          "<uses-permission android:name=\"android.permission.CHANGE_WIFI_STATE\"/>",
+          "<uses-permission android:name=\"android.permission.WAKE_LOCK\"/>",
+          "<uses-permission android:name=\"android.permission.FLASHLIGHT\"/>",
+          "<uses-feature android:name=\"android.hardware.camera\"/>",
+          "<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>"
+        ]
+      },
+      "ios": {},
+      "sdkConfigs": {}
+    }
+  },
+  "quickapp": {},
+  "mp-weixin": {
+    "appid": "wx5ba8038ef16fb245",
+    "setting": {
+      "urlCheck": false,
+      "es6": true,
+      "postcss": true,
+      "minified": true,
+      "lazyCodeLoading": "requiredComponents"
+    }
+  },
+  "mp-alipay": {},
+  "mp-baidu": {},
+  "mp-toutiao": {
+    "usingComponents": true
+  },
+  "uniStatistics": {
+    "enable": false
+  },
+  "vueVersion": "2"
+}

+ 6 - 0
cfc-frontend/pages.json

@@ -1340,5 +1340,11 @@
         "selectedIconPath": "static/tab-profile-active.png"
       }
     ]
+  },
+  "easycom": {
+    "auto": "components/@dcloudio/uni-$2/components/uni-$1/uni-$1.vue",
+    "pages": {
+      "pages/*": "components/@dcloudio/uni-$2/components/uni-$1/uni-$1.vue"
+    }
   }
 }

+ 388 - 0
docs/实现计划.md

@@ -0,0 +1,388 @@
+# 数据与 AI 结合 — 实现计划
+
+## 依赖关系图
+
+```
+Phase 1.1 (DB迁移) ──→ 1.3 (createDynamicTask) ──→ 1.4 (Controller改造)
+                                                       │
+Phase 1.2 (TaskParseService) ──────────────────────────┘
+                                                       │
+Phase 1.8 (FamilyContext增强) ─────────────────────────┘
+                                                       ↓
+                                             1.9 (编译验证)
+                                                       │
+               ┌───────────────────────────────────────┘
+               ↓
+Phase 1.5 (AITaskCard组件) ──→ 1.6 (chat.vue改造) ──→ 1.7 (报告详情页入口)
+                                                                  │
+                                                                  ↓
+                                                         Phase 2.x (持续增强)
+```
+
+## Phase 1: 基础能力 — 2周
+
+### 1.1 数据库迁移 — growth_tasks 表扩展
+
+**目标:** 支持 AI_GENERATED 类型任务,新增维度跟踪字段
+
+**文件:**
+- `cfc-backend/src/main/resources/schema.sql` — 同步建表语句
+- `cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java` — 迁移代码
+- `cfc-backend/src/main/java/com/etotem/cfc/entity/GrowthTask.java` — 新增字段
+
+**迁移内容:**
+```sql
+-- migration N: growth_tasks 支持 AI 生成任务
+ALTER TABLE growth_tasks MODIFY COLUMN type
+  ENUM('DAILY','NEWBIE','AI_GENERATED') NOT NULL DEFAULT 'DAILY';
+ALTER TABLE growth_tasks ADD COLUMN dimension VARCHAR(20) NULL COMMENT '五维维度';
+ALTER TABLE growth_tasks ADD COLUMN source_conversation_id VARCHAR(100) NULL COMMENT '来源AI会话ID';
+```
+
+**实体类新增字段:**
+```java
+@TableField("dimension")
+private String dimension;
+
+@TableField("source_conversation_id")
+private String sourceConversationId;
+```
+
+**验收:** `mvn clean compile` 通过,迁移可重复执行不报错
+
+---
+
+### 1.2 TaskParseService — [TASK] 标记解析
+
+**目标:** 新建服务解析 Dify 返回的 `[TASK:{...}]` 标记,提取任务参数
+
+**文件:** 新建 `cfc-backend/src/main/java/com/etotem/cfc/service/TaskParseService.java`
+
+```
+1. TASK: 解析 Dify answer 中的 [TASK:{"title":"...","description":"...",...}] 标记
+2. EXPECTED OUTCOME: 返回解析后的 TaskParseResult 对象列表
+3. REQUIRED TOOLS: 正则 Pattern + Jackson ObjectMapper
+4. MUST DO:
+   - 正则 `\[TASK:\s*(\{.*?\})\](?:\s*\[TASK:...)*` 提取所有 TASK 标记
+   - 解析 JSON → TaskParseResult 对象
+   - 校验 title 必填
+   - 返回解析结果列表
+   - 从原 answer 中移除所有 TASK 标记
+5. MUST NOT DO: 不写入数据库(由调用方控制)
+```
+
+**TaskParseResult DTO:**
+```java
+public class TaskParseResult {
+    private String title;          // 必填
+    private String description;    // 选填
+    private String deadline;       // 选填 yyyy-MM-dd
+    private String dimension;      // 选填 body/mind/wisdom/action/wealth
+    private Integer rewardPoints;  // 选填
+}
+```
+
+**验收:** `"[TASK:{\"title\":\"读书30分钟\",\"dimension\":\"wisdom\"}]"` → 解析出 title=读书30分钟, dimension=wisdom
+
+---
+
+### 1.3 GrowthTaskService.createDynamicTask()
+
+**目标:** 新增方法,根据 AI 生成的任务参数创建 growth_tasks + growth_task_log
+
+**文件:** `cfc-backend/src/main/java/com/etotem/cfc/service/GrowthTaskService.java`
+
+**新增方法:**
+```java
+/**
+ * 创建 AI 动态生成任务
+ * @param userId 目标用户ID
+ * @param task 解析后的任务参数
+ * @param conversationId 来源AI会话ID
+ * @return 创建的任务ID
+ */
+@Transactional
+public Long createDynamicTask(Long userId, TaskParseResult task, String conversationId) {
+    // 1. 插入 growth_tasks (type=AI_GENERATED, enabled=1)
+    // 2. 插入 growth_task_log (progress=0, completed=0, claimed=0)
+    // 3. 返回 taskId
+}
+```
+
+**验收:** 调用后 growth_tasks 和 growth_task_log 各多一条记录
+
+---
+
+### 1.4 AIChatController 通用化改造
+
+**目标:** 统一处理 `[RECOMMEND]` 和 `[TASK]` 标记,全模式支持推荐
+
+**文件:** `cfc-backend/src/main/java/com/etotem/cfc/controller/ai/AIChatController.java`
+
+**改动点:**
+
+1. **sendMessage() 方法改造:**
+   - 将原来只在 sendNutritionMessage() 中的 RECOMMEND 解析提取为通用方法
+   - 在 sendMessage() 返回前统一调用 `parseAndHandleMarkers(answer)`
+   - 返回值结构扩展:`{ answer, conversationId, recommendations, tasks }`
+
+2. **parseAndHandleMarkers() 新增方法:**
+   ```java
+   private Map<String, Object> parseAndHandleMarkers(String answer, Long userId, String conversationId) {
+       // 1. 解析 [RECOMMEND:...] → RecommendationService.search
+       // 2. 解析 [TASK:...] → TaskParseService → GrowthTaskService.createDynamicTask
+       // 3. 返回 { cleanAnswer, recommendations, tasks }
+   }
+   ```
+
+3. **sendNutritionMessage() 简化:** 复用 parseAndHandleMarkers() 避免重复
+
+**验收:**
+- 普通对话输入"帮我推荐一些健康食品" → response 含 recommendations 列表
+- 普通对话输入"给我安排一个每天读书的任务" → response 含 tasks 列表
+- 旧功能不受影响(回归:营养对话仍能正常推荐)
+
+---
+
+### 1.5 AITaskCard 组件
+
+**目标:** 可复用的任务卡片组件,展示任务信息 + 接受按钮
+
+**文件:** 新建 `cfc-frontend/components/AITaskCard.vue`
+
+```vue
+<template>
+  <view class="ai-task-card">
+    <view class="task-header">
+      <text class="task-icon">📋</text>
+      <text class="task-title">{{ task.title }}</text>
+    </view>
+    <text class="task-desc" v-if="task.description">{{ task.description }}</text>
+    <view class="task-footer">
+      <text class="task-dimension" v-if="task.dimension">维度:{{ dimLabel }}</text>
+      <text class="task-deadline" v-if="task.deadline">截止:{{ task.deadline }}</text>
+    </view>
+    <button class="task-accept-btn" :loading="accepting" @click="handleAccept">接受任务</button>
+  </view>
+</template>
+
+<script>
+export default {
+  props: {
+    task: Object,
+    conversationId: String
+  },
+  data() { return { accepting: false } },
+  methods: {
+    async handleAccept() {
+      // 调用 POST /api/tasks/accept-dynamic
+      // 参数: { taskTitle, taskDescription, deadline, dimension, conversationId }
+      this.accepting = true
+      try {
+        const res = await api.acceptDynamicTask({...})
+        if (res.code === 200) {
+          uni.showToast({ title: '任务已接受' })
+          this.$emit('accepted', res.data)
+        }
+      } finally {
+        this.accepting = false
+      }
+    }
+  }
+}
+</script>
+```
+
+**验收:** 组件在 chat.vue 中导入后可正常渲染和点击接受
+
+---
+
+### 1.6 chat.vue 改造
+
+**目标:** 支持任务卡片渲染、全模式推荐展示、报告解读模式
+
+**文件:** `cfc-frontend/pages/ai/chat.vue`
+
+**改动点:**
+
+1. **消息结构扩展** — 在 data 中支持 tasks 字段
+2. **模板新增任务卡片区域** — messages 循环中插入 AITaskCard
+3. **推荐卡片区域增强** — 从 only nutrition 模式扩展到所有模式
+4. **新模版:`mode=report`** — 接收 reportId 参数,sendMessage 时自动传入
+5. **消息气泡标注来源** — 如果是报告解读,气泡上显示"基于健康报告"标签
+
+```
+改动区段:
+- <template> 中 #80~90 行,推荐卡片区域改为无条件渲染
+- <template> 消息气泡内新增任务卡片区域
+- <script> data() 中新增支持任务字段
+- <script> sendMessage() 中参数扩展:reportId
+- <script> onLoad() 中新增 mode=report 处理
+```
+
+**验收:**
+- 普通对话返回推荐数据 → 显示推荐卡片
+- AI 返回任务 → 显示任务卡片 + 可点击接受
+- 跳转 `chat.vue?mode=report&reportId=123` → 发消息时自动带 reportId
+
+---
+
+### 1.7 报告详情页 AI 解读入口
+
+**目标:** 各报告详情页底部增加"让浠宝/福宝解读"入口
+
+**文件:**
+- `cfc-frontend/pages/body-detail/health-report.vue`
+- `cfc-frontend/pages/health/gut-index.vue`
+- `cfc-frontend/pages/health/tongue-index.vue`
+- `cfc-frontend/pages/health/report-upload.vue`
+- `cfc-frontend/pages/body-detail/health-report.vue`
+
+**实现方式:** 各页面的模板底部新增:
+```vue
+<view class="ai-interpret-entry" @click="goAIChat">
+  <image class="ai-mascot-avatar" :src="mascotAvatar" />
+  <view class="ai-entry-text">
+    <text class="ai-entry-title">让 {{ mascotName }} 帮你解读</text>
+    <text class="ai-entry-desc">基于这份报告,{{ mascotName }} 会给出个建议</text>
+  </view>
+  <text class="ai-entry-arrow">›</text>
+</view>
+```
+
+**goAIChat 方法:**
+```javascript
+goAIChat() {
+  const userInfo = uni.getStorageSync('userInfo')
+  const mascot = userInfo && userInfo.mascot === 'fubao' ? '福宝' : '浠宝'
+  uni.navigateTo({
+    url: '/pages/ai/chat?mode=report&reportId=' + this.reportId + '&firstMessage=' 
+         + encodeURIComponent('你好' + mascot + ',帮我看看这份报告有什么需要注意的地方?')
+  })
+}
+```
+
+**验收:** 每个报告详情页底部可见 AI 入口,点击后跳转 chat.vue 并自动发送解读请求
+
+---
+
+### 1.8 FamilyContextService 任务上下文注入
+
+**目标:** 在 AI 上下文中注入用户当前任务状态,让 Dify 感知任务进度
+
+**文件:** `cfc-backend/src/main/java/com/etotem/cfc/service/FamilyContextService.java`
+
+**改动点:** `buildContext()` 末尾增加:
+```java
+// 6. 当前未完成任务列表
+List<Map<String, Object>> pendingTasks = getPendingTasks(userId);
+if (!pendingTasks.isEmpty()) {
+    ctx.put("pendingTasks", pendingTasks);
+}
+```
+
+```java
+private List<Map<String, Object>> getPendingTasks(Long userId) {
+    // 查询当前用户未完成的 growth_task_log 关联的 growth_tasks
+    // 返回 title, type, dimension 等信息
+}
+```
+
+**验收:** 用户有未完成任务时,Dify inputs 中包含 `pendingTasks` 字段
+
+---
+
+### 1.9 编译验证
+
+**目标:** 确保 Phase 1 所有后端改动编译通过,无路由冲突
+
+```bash
+cd cfc-backend && mvn clean compile
+grep -rn '@Mapping' src/main/java/com/etotem/cfc/controller/ | grep -oP '@\w+Mapping\("\K[^"]*' | sort -u | grep -E '(task|ai)'
+```
+
+**验收:** `mvn clean compile` exit 0,新增路由无重复
+
+---
+
+## Phase 2: 智能增强 — 2周
+
+### 2.1 Dify Workflow 合并
+
+**平台配置(在 Dify 平台操作,非代码):**
+1. 在 Dify 中创建新的 App(含 Workflow)
+2. 配置 Classifier Node,基于 query+inputs 做场景分类
+3. 配置 Report Analysis Agent / Nutrition Advisor Agent / Task Manager Agent / Family Assistant Agent
+4. 配置 Post Processor 格式化输出(合并 RECOMMEND / TASK 标记)
+5. 配置回调 URL 指向 `/api/ai/context`(保留原有记忆逻辑)
+6. 生成新的 API Key
+
+**后端适配:**
+- `AIService.java`:统一使用新 API Key,删除 nutritionApiKey/tongueApiKey
+- 响应兼容处理(workflow 返回格式可能与 chat-messages 有差异)
+
+**验收:** 现有所有 AI 功能在新 App 上正常运作
+
+### 2.2 推荐排序优化
+
+**文件:** `cfc-backend/src/main/java/com/etotem/cfc/service/RecommendationService.java`
+
+**改动点:**
+1. `search()` 方法中增加排序逻辑:
+   - 标签匹配度排序(匹配标签数多的优先)
+   - 已购商品降权(`ProductOrder` 中已购买过的商品排后)
+   - 维度匹配加分(与用户当前关注维度对齐的优先)
+   - 按 `createdAt` 时效性排序
+2. 前端推荐卡片增加"已购买"标记
+
+### 2.3 任务展示与接受完整流程
+
+**后端:** 新建 `POST /api/tasks/accept-dynamic`
+```java
+@PostMapping("/api/tasks/accept-dynamic")
+public Result<Long> acceptDynamicTask(@RequestAttribute("userId") Long userId,
+                                      @RequestBody Map<String, Object> params) {
+    // 参数:title, description, deadline, dimension, conversationId
+    // 调用 growthTaskService.createDynamicTask()
+    // 返回 taskId
+}
+```
+
+**前端:** AITaskCard 组件点击"接受任务" → 调用此接口 → 显示成功提示
+
+---
+
+## Phase 3: 体验优化 — 持续
+
+### 3.1 Mascot 动效与个性化
+
+- 浠宝(🌟):暖橙色系动画,温柔语音提示
+- 福宝(💪):金色系动画,活泼语气
+- 表情随对话内容变化(开心/思考/鼓励)
+
+### 3.2 A/B 测试
+
+- 对照组:无 AI 解读入口的报告详情页
+- 实验组:有 AI 解读入口
+- 指标:解读点击率、任务接受率、次日回访率
+
+---
+
+## 团队任务分配建议
+
+| 开发 | 任务 |
+|------|------|
+| 后端 A | 1.1 DB迁移 + 1.2 TaskParseService + 1.3 createDynamicTask |
+| 后端 B | 1.4 Controller改造 + 1.8 FamilyContext增强 + 1.9 编译验证 |
+| 前端 A | 1.5 AITaskCard组件 + 1.6 chat.vue改造 |
+| 前端 B | 1.7 各报告详情页AI入口 |
+| 并行启动 | 1.1/1.2 可先启动(无依赖),1.5 可先启动(无依赖) |
+
+---
+
+## 回滚策略
+
+- 数据库迁移:`ALTER TABLE ... MODIFY COLUMN` 改回原 ENUM,`DROP COLUMN` 新字段
+- 后端:回滚 Git 提交
+- 前端:`git revert` 对应提交
+- Dify:保留旧 App 配置,新 App 验证通过后再切换