Просмотр исходного кода

docs: SB3 upgrade implementation plan with task breakdown

asus 3 месяцев назад
Родитель
Сommit
d3821e7e0b
1 измененных файлов с 853 добавлено и 0 удалено
  1. 853 0
      docs/architecture/2026-07-20-SB3-upgrade-plan.md

+ 853 - 0
docs/architecture/2026-07-20-SB3-upgrade-plan.md

@@ -0,0 +1,853 @@
+# Spring Boot 3.x 升级 + Dify 替代 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:** 将 cfc-backend 从 Spring Boot 2.7.18 + Java 8 升级到 Spring Boot 3.4.x + Java 21 LTS,并用 Spring AI 替换 Dify 全部调用。
+
+**Architecture:** 5 阶段串行,每阶段有独立编译/测试验证 + git tag 回退点。Phase 0-2 为纯框架升级和行为适配,Phase 3 为 Spring AI 集成和 Dify 替代,Phase 4 为回归验证和灰度上线。
+
+**Tech Stack:** Java 21, Spring Boot 3.4.x, Spring AI 1.x, MyBatis-Plus 3.5.9, springdoc-openapi 2.7, exp4j 0.4.8, jjwt 0.12.6, mysql-connector-java 8.0.33, sqlite-vec
+
+---
+
+## Global Constraints
+
+1. **Java 21 LTS** — JDK 21 必须安装到所有开发/CI/生产环境,maven-compiler-plugin source/target=21
+2. **Spring Boot BOM** — 以 spring-boot-starter-parent 3.4.x 为根 BOM,避免传递依赖冲突
+3. **javax → jakarta** — 所有 javax.* import 必须改为 jakarta.*,包括但不限于 persistence/annotation/validation/servlet/sql
+4. **Nashorn 已废弃** — WisdomMathService 中的 ScriptEngine eval 必须替换为 exp4j
+5. **springdoc v2** — artifactId 从 `springdoc-openapi-ui` 改为 `springdoc-openapi-starter-webmvc-ui`
+6. **jjwt 0.12** — `Jwts.parserBuilder()` 改为 `Jwts.parser()`, Keys API 变化
+7. **统一 @PostMapping** — 遵循项目现有规范,禁止 @GetMapping/@PutMapping/@DeleteMapping
+8. **统一 Result 响应** — 所有接口返回 `Result<T>` (code/message/data)
+9. **不再依赖 Dify** — 全部 8 个 Service + 3 个 Controller 必须改为 Spring AI ChatClient
+
+---
+
+## 文件变更清单
+
+### 创建
+| 文件 | 用途 |
+|------|------|
+| `src/main/java/com/etotem/cfc/service/ai/LLMService.java` | Spring AI ChatClient 封装 |
+| `src/main/java/com/etotem/cfc/service/ai/SystemPromptLoader.java` | System Prompt 外部化加载 |
+| `src/main/java/com/etotem/cfc/service/ai/RAGService.java` | 本地知识库查询 |
+| `src/main/java/com/etotem/cfc/config/SpringAiConfig.java` | Spring AI ChatClient Bean 配置 |
+| `resources/prompts/family-advisor.yaml` | 家庭顾问 System Prompt |
+| `resources/prompts/growth-plan.yaml` | 成长规划 System Prompt |
+| `resources/prompts/diet-recommend.yaml` | 饮食推荐 System Prompt |
+| `resources/prompts/math-tutor.yaml` | 数学辅导 System Prompt |
+
+### 修改
+| 文件 | 变更类型 |
+|------|---------|
+| `pom.xml` | 全部依赖版本更新 + 新增 |
+| `src/main/resources/application.yml` | Spring AI 配置、springdoc 配置调整 |
+| 所有 `@Entity` 类 (~80) | javax.persistence → jakarta.persistence |
+| 所有 DTO 类 (~200) | javax.validation → jakarta.validation |
+| 所有 Service 类 (~60) | javax.annotation → jakarta.annotation |
+| 所有 Controller 类 (~48) | javax.servlet → jakarta.servlet (如果使用) |
+| 所有 Config 类 | 同上 |
+| `JwtUtil.java` | jjwt 0.12 API 适配 |
+| `WisdomMathService.java` | Nashorn → exp4j |
+| `AIService.java` | 重构为 LLMService 封装 |
+| `DifySyncService.java` | 替换为本地知识库同步 |
+| `AiConversationSummaryService.java` | 替换为 Spring AI MemoryAdvisor |
+| `AiUserFactService.java` | 替换为 @Tool + ChatClient |
+| 其余 Dify 相关 4 个 Service | 逐一替换 |
+| `AiChatController.java` + 2 个相关 Controller | Spring AI 实现 |
+| 删除 `springdoc-openapi-ui` 依赖 | v1→v2 artifact 替换 |
+
+---
+
+## 任务分解
+
+### Phase 0 — 前置准备(3 天)
+
+#### Task 0.1: 环境准备 + BOM 对齐
+
+**Files:**
+- Modify: `pom.xml`
+
+**Interfaces:**
+- Produces: `mvn clean compile` 通过(尚未改 javax,暂时只对齐依赖)
+
+- [ ] **Step 1:更新 parent 和 properties**
+
+在 `pom.xml` 中修改:
+```xml
+<parent>
+    <groupId>org.springframework.boot</groupId>
+    <artifactId>spring-boot-starter-parent</artifactId>
+    <version>3.4.3</version>      <!-- 2026年最新 3.4.x -->
+    <relativePath/>
+</parent>
+<properties>
+    <java.version>21</java.version>
+    <mybatis-plus.version>3.5.9</mybatis-plus.version>
+    <jjwt.version>0.12.6</jjwt.version>
+</properties>
+```
+
+- [ ] **Step 2:替换 springdoc v1 → v2**
+
+找到并替换:
+```xml
+<!-- 删除 -->
+<dependency>
+    <groupId>org.springdoc</groupId>
+    <artifactId>springdoc-openapi-ui</artifactId>
+    <version>1.8.0</version>
+</dependency>
+
+<!-- 添加 -->
+<dependency>
+    <groupId>org.springdoc</groupId>
+    <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
+    <version>2.7.0</version>
+</dependency>
+```
+
+- [ ] **Step 3:移除 jjwt 旧版本,确认 BOM 覆盖**
+
+```xml
+<!-- jjwt 版本已通过 properties 控制 -->
+<dependency>
+    <groupId>io.jsonwebtoken</groupId>
+    <artifactId>jjwt-api</artifactId>
+    <version>${jjwt.version}</version>
+</dependency>
+<dependency>
+    <groupId>io.jsonwebtoken</groupId>
+    <artifactId>jjwt-impl</artifactId>
+    <version>${jjwt.version}</version>
+    <scope>runtime</scope>
+</dependency>
+<dependency>
+    <groupId>io.jsonwebtoken</groupId>
+    <artifactId>jjwt-jackson</artifactId>
+    <version>${jjwt.version}</version>
+    <scope>runtime</scope>
+</dependency>
+```
+
+- [ ] **Step 4:添加 exp4j 依赖**
+
+```xml
+<dependency>
+    <groupId>net.objecthunter</groupId>
+    <artifactId>exp4j</artifactId>
+    <version>0.4.8</version>
+</dependency>
+```
+
+- [ ] **Step 5:编译验证**
+
+运行:
+```bash
+mvn clean compile
+```
+Expected: 编译失败(因为 javax 包名不存在,这是预期的。失败信息用来确认 Phase 1 的替换范围)
+
+- [ ] **Step 6:录制 javax 报错清单**
+
+```bash
+mvn clean compile 2>&1 | Select-String "javax\." | Sort-Object -Unique > docs/javax-errors.txt
+```
+
+查看输出行数:`Get-Content docs/javax-errors.txt | Measure-Object`
+
+- [ ] **Step 7:打基线 tag**
+
+```bash
+git tag v2.7.18-baseline
+git push origin v2.7.18-baseline
+```
+
+---
+
+#### Task 0.2: 确认 Nashorn 影响范围
+
+**Files:**
+- Read: `src/main/java/.../WisdomMathService.java`
+
+- [ ] **Step 1:提取 Nashorn 表达式求值用例**
+
+在 WisdomMathService 中搜索所有 `engine.eval(` 和 `ScriptEngine` 出现的位置,提取所有使用的表达式样例(记录输入和输出)。
+
+```java
+// 按行记录类似这样的调用
+engine.eval("12+38*2");        // → 88
+engine.eval("Math.round(a)");  // → 四舍五入
+// ... 记录全部
+```
+
+- [ ] **Step 2:验证 exp4j 是否覆盖全部语法**
+
+对照提取的表达式,确认 exp4j 支持:
+- 四则运算 + - * / ✅
+- 括号 ✅
+- 幂运算 ^ ✅
+- Math 函数: abs, sin, cos, log, round, ceil, floor ✅
+
+对于不支持的函数(如果有),记录到 Phase 2 的自定义函数实现方案。
+
+---
+
+### Phase 1 — javax → jakarta 迁移(5 天)
+
+#### Task 1.1: 实体类包名替换
+
+**Files:**
+- Modify: `src/main/java/com/etotem/cfc/entity/` 下全部 Entity 类(~80 个)
+
+**Interfaces:**
+- Produces: 全部 Entity 类编译通过(包名迁移后)
+
+- [ ] **Step 1:全局替换 javax.persistence → jakarta.persistence**
+
+```bash
+# PowerShell 在 entity 目录下执行
+Get-ChildItem -Path "src/main/java/com/etotem/cfc/entity" -Filter "*.java" -Recurse | 
+  ForEach-Object { (Get-Content $_.FullName) -replace "javax\.persistence\.", "jakarta.persistence." | 
+  Set-Content $_.FullName }
+```
+
+- [ ] **Step 2:编译验证**
+
+```bash
+mvn clean compile 2>&1 | Select-String "ERROR"
+```
+Expected: javax 错误行数减少(仅 entity 包部分消除)
+
+---
+
+#### Task 1.2: DTO 校验注解替换
+
+**Files:**
+- Modify: `src/main/java/com/etotem/cfc/dto/` 下全部 DTO 类(~200 个)
+
+- [ ] **Step 1:全局替换 javax.validation → jakarta.validation**
+
+```bash
+Get-ChildItem -Path "src/main/java/com/etotem/cfc/dto" -Filter "*.java" -Recurse | 
+  ForEach-Object { (Get-Content $_.FullName) -replace "javax\.validation\.", "jakarta.validation." | 
+  Set-Content $_.FullName }
+```
+
+- [ ] **Step 2:编译验证**
+
+```bash
+mvn clean compile 2>&1 | Select-String "ERROR" | Select-String "javax"
+```
+Expected: 仅剩非 persistence/validation 的 javax 错误
+
+---
+
+#### Task 1.3: 全局 javax.* 收尾替换
+
+**Files:**
+- Modify: 所有剩余包含 `javax.*` 的 Java 文件
+
+- [ ] **Step 1:扫描所有剩余 javax.* import**
+
+```bash
+Get-ChildItem -Path "src/main/java" -Filter "*.java" -Recurse | 
+  Select-String "^import javax\." | 
+  ForEach-Object { $_.Filename + ": " + $_.Line.Trim() } | 
+  Sort-Object -Unique
+```
+
+- [ ] **Step 2:按以下规则逐条替换**
+
+| 原 import | 目标 import |
+|-----------|------------|
+| `javax.annotation.Resource` | `jakarta.annotation.Resource` |
+| `javax.annotation.PostConstruct` | `jakarta.annotation.PostConstruct` |
+| `javax.annotation.PreDestroy` | `jakarta.annotation.PreDestroy` |
+| `javax.servlet.http.HttpServletRequest` | `jakarta.servlet.http.HttpServletRequest` |
+| `javax.servlet.http.HttpServletResponse` | `jakarta.servlet.http.HttpServletResponse` |
+| `javax.servlet.Filter` | `jakarta.servlet.Filter` |
+| `javax.servlet.FilterChain` | `jakarta.servlet.FilterChain` |
+| `javax.servlet.ServletException` | `jakarta.servlet.ServletException` |
+| `javax.sql.DataSource` | `jakarta.sql.DataSource` |
+| `javax.xml.bind.*` | 确认是否使用,如使用需加 jakarta.xml.bind 依赖 |
+
+- [ ] **Step 3:逐文件编译验证**
+
+每替换完一批,运行:
+```bash
+mvn clean compile
+```
+
+修复残留错误,直到零错。
+
+- [ ] **Step 4:打阶段 tag**
+
+```bash
+git add -A
+git commit -m "phase1: javax → jakarta migration complete"
+git tag v3.0-javax-migrated
+```
+
+---
+
+### Phase 2 — 行为适配 + Nashorn 替换(4 天)
+
+#### Task 2.1: WisdomMathService Nashorn → exp4j
+
+**Files:**
+- Modify: `src/main/java/.../service/WisdomMathService.java`
+
+- [ ] **Step 1:编写 exp4j 替换实现**
+
+```java
+// 替换前
+import javax.script.ScriptEngineManager;
+import javax.script.ScriptEngine;
+
+ScriptEngineManager mgr = new ScriptEngineManager();
+ScriptEngine engine = mgr.getEngineByName("nashorn");
+Object result = engine.eval("12+38*2");
+
+// 替换后
+import net.objecthunter.exp4j.ExpressionBuilder;
+
+double result = new ExpressionBuilder("12+38*2").build().evaluate();
+```
+
+如果需要支持 Math.round/ceil/floor 等函数,注册自定义函数:
+```java
+import net.objecthunter.exp4j.function.Function;
+
+Function roundFunc = new Function("round", 1) {
+    @Override
+    public double apply(double... args) {
+        return Math.round(args[0]);
+    }
+};
+
+Expression exp = new ExpressionBuilder(expression)
+    .function(roundFunc)
+    .build();
+```
+
+- [ ] **Step 2:编译验证**
+
+```bash
+mvn clean compile
+```
+Expected: 零错误
+
+---
+
+#### Task 2.2: jjwt 0.12 API 适配
+
+**Files:**
+- Modify: 搜索所有使用 jjwt 的文件
+
+- [ ] **Step 1:找到所有 jjwt 调用**
+
+```bash
+Get-ChildItem -Path "src/main/java" -Filter "*.java" -Recurse | 
+  Select-String "Jwts\.|Keys\.|SignatureAlgorithm" | 
+  ForEach-Object { $_.Filename + "(" + $_.LineNumber + "): " + $_.Line.Trim() }
+```
+
+- [ ] **Step 2:按规则适配**
+
+```java
+// jjwt 0.11                          // jjwt 0.12
+Jwts.builder()                         // 基本不变(返回 Builder 类型更新)
+    .signWith(key, SignatureAlgorithm.HS256)  // → .signWith(key) 自动推导算法
+Jwts.parserBuilder()                   // → Jwts.parser()
+    .setSigningKey(key)                // → .verifyWith(key) 或 .decryptWith(key)
+    .build()
+    .parseClaimsJws(token)             // 基本不变
+Keys.secretKeyFor(SignatureAlgorithm.HS256)  // → Jwts.SIG.HS256.keyBuilder().build()
+```
+
+- [ ] **Step 3:编译验证**
+
+```bash
+mvn clean compile
+```
+
+---
+
+#### Task 2.3: Spring Boot 3 行为变更适配
+
+**Files:**
+- Modify: `src/main/resources/application.yml`
+
+- [ ] **Step 1:检查路径匹配策略**
+
+添加/确认配置(如果现有代码依赖 Ant 风格路径匹配):
+```yaml
+spring:
+  mvc:
+    pathmatch:
+      matching-strategy: ant-path-matcher
+```
+
+- [ ] **Step 2:检查 springdoc 配置**
+
+```yaml
+# v1 (如有)
+springdoc:
+  swagger-ui:
+    path: /swagger-ui.html
+
+# v2 相同,但验证可用
+```
+
+- [ ] **Step 3:编译运行验证**
+
+```bash
+mvn clean compile
+```
+
+验证应用启动:
+```bash
+mvn spring-boot:run
+```
+Expected: 应用启动成功,Swagger UI 可访问 /swagger-ui.html(或 v2 默认路径)
+
+---
+
+#### Task 2.4: springdoc v2 Swagger UI 验证
+
+- [ ] **Step 1:启动应用**
+
+```bash
+mvn spring-boot:run
+```
+
+- [ ] **Step 2:访问 Swagger UI 验证 API 文档正确渲染**
+
+```bash
+curl http://localhost:9082/v3/api-docs | Select-Object -First 20
+```
+Expected: 返回 OpenAPI 3.0 JSON,paths 字段包含 /api/ 路由
+
+- [ ] **Step 3:打阶段 tag**
+
+```bash
+git commit -am "phase2: Spring Boot 3 behavior adaptation complete"
+git tag v3.0-build-ready
+```
+
+---
+
+### Phase 3 — Spring AI 集成 + Dify 替换(5 天)
+
+#### Task 3.1: 引入 Spring AI 依赖 + 配置
+
+**Files:**
+- Modify: `pom.xml`
+- Modify: `src/main/resources/application.yml`
+
+- [ ] **Step 1:添加 Spring AI BOM + starter**
+
+```xml
+<dependencyManagement>
+    <dependencies>
+        <dependency>
+            <groupId>org.springframework.ai</groupId>
+            <artifactId>spring-ai-bom</artifactId>
+            <version>1.0.0</version>   <!-- 2026年最新 1.x -->
+            <type>pom</type>
+            <scope>import</scope>
+        </dependency>
+    </dependencies>
+</dependencyManagement>
+
+<dependencies>
+    <dependency>
+        <groupId>org.springframework.ai</groupId>
+        <artifactId>spring-ai-openai-spring-boot-starter</artifactId>
+    </dependency>
+    <dependency>
+        <groupId>org.springframework.ai</groupId>
+        <artifactId>spring-ai-pdf-document-reader</artifactId>
+    </dependency>
+</dependencies>
+```
+
+- [ ] **Step 2:配置 Spring AI(application.yml)**
+
+```yaml
+spring:
+  ai:
+    openai:
+      base-url: https://api.deepseek.com
+      api-key: ${DEEPSEEK_API_KEY}
+      chat:
+        options:
+          model: deepseek-chat
+          temperature: 0.7
+    vectorstore:
+      sqlite:
+        path: ${cfc.data.dir:/data/cfc}/knowledge.db
+```
+
+- [ ] **Step 3:编译验证**
+
+```bash
+mvn clean compile
+```
+
+---
+
+#### Task 3.2: 创建 LLMService 核心封装
+
+**Files:**
+- Create: `src/main/java/com/etotem/cfc/service/ai/LLMService.java`
+- Create: `src/main/java/com/etotem/cfc/config/SpringAiConfig.java`
+
+- [ ] **Step 1:创建 SpringAiConfig**
+
+```java
+package com.etotem.cfc.config;
+
+import org.springframework.ai.chat.client.ChatClient;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+@Configuration
+public class SpringAiConfig {
+
+    @Bean
+    ChatClient chatClient(ChatClient.Builder builder) {
+        return builder.build();
+    }
+}
+```
+
+- [ ] **Step 2:创建 LLMService**
+
+```java
+package com.etotem.cfc.service.ai;
+
+import jakarta.annotation.Resource;
+import org.springframework.ai.chat.client.ChatClient;
+import org.springframework.ai.chat.memory.ChatMemory;
+import org.springframework.ai.chat.memory.InMemoryChatMemory;
+import org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor;
+import org.springframework.stereotype.Service;
+import reactor.core.publisher.Flux;
+
+@Service
+public class LLMService {
+
+    @Resource
+    private ChatClient chatClient;
+
+    private final ChatMemory chatMemory = new InMemoryChatMemory();
+
+    public String chat(String query, String systemPrompt, String conversationId) {
+        return chatClient.prompt()
+                .system(systemPrompt)
+                .user(query)
+                .advisors(new MessageChatMemoryAdvisor(chatMemory, conversationId, 10))
+                .call()
+                .content();
+    }
+
+    public Flux<String> stream(String query, String systemPrompt, String conversationId) {
+        return chatClient.prompt()
+                .system(systemPrompt)
+                .user(query)
+                .advisors(new MessageChatMemoryAdvisor(chatMemory, conversationId, 10))
+                .stream()
+                .content();
+    }
+}
+```
+
+- [ ] **Step 3:编译验证**
+
+```bash
+mvn clean compile
+```
+
+---
+
+#### Task 3.3: System Prompt 外部化
+
+**Files:**
+- Create: `resources/prompts/family-advisor.yaml`
+- Create: `resources/prompts/growth-plan.yaml`
+- Create: `resources/prompts/diet-recommend.yaml`
+- Create: `resources/prompts/math-tutor.yaml`
+- Create: `src/main/java/.../service/ai/SystemPromptLoader.java`
+
+- [ ] **Step 1:创建 SystemPromptLoader**
+
+```java
+package com.etotem.cfc.service.ai;
+
+import org.springframework.core.io.Resource;
+import org.springframework.core.io.ResourceLoader;
+import org.springframework.stereotype.Component;
+import org.yaml.snakeyaml.Yaml;
+import jakarta.annotation.Resource;
+import java.util.Map;
+
+@Component
+public class SystemPromptLoader {
+
+    @Resource
+    private ResourceLoader resourceLoader;
+
+    public String load(String promptName) {
+        Resource resource = resourceLoader.getResource("classpath:prompts/" + promptName + ".yaml");
+        // 解析 YAML,返回 prompt 字段
+        Map<String, String> data = new Yaml().load(resource.getInputStream());
+        return data.get("prompt");
+    }
+}
+```
+
+- [ ] **Step 2:创建可替换的原 Dify Prompt 映射表**
+
+从数据库 chat_assistant 表的 description/system_prompt 字段提取:
+```yaml
+# resources/prompts/family-advisor.yaml
+prompt: |
+  你是家庭成长顾问,帮助家长理解孩子的成长需求。
+  请基于五维能量体系(身/心/智/行/富)提供建议。
+  ...
+```
+
+---
+
+#### Task 3.4: AIService 重构为 LLMService
+
+**Files:**
+- Modify: `src/main/java/.../service/AIService.java`
+- Modify: `src/main/java/.../controller/AiChatController.java`
+
+- [ ] **Step 1:重构 AIService**
+
+```java
+// 原有 AIService 用 OkHttp 调 Dify API,改为注入 LLMService
+@Service
+public class AIService {
+
+    @Resource
+    private LLMService llmService;
+
+    @Resource
+    private SystemPromptLoader promptLoader;
+
+    public String sendMessage(String assistantType, String query, String conversationId) {
+        String systemPrompt = promptLoader.load(assistantType);
+        return llmService.chat(query, systemPrompt, conversationId);
+    }
+
+    // 流式版本
+    public Flux<String> sendMessageStream(String assistantType, String query, String conversationId) {
+        String systemPrompt = promptLoader.load(assistantType);
+        return llmService.stream(query, systemPrompt, conversationId);
+    }
+}
+```
+
+- [ ] **Step 2:编译验证 + 冒烟测试**
+
+```bash
+mvn clean compile
+mvn spring-boot:run
+# 调用 /api/ai/chat/send 验证响应
+```
+
+---
+
+#### Task 3.5: DifySyncService → RAGService
+
+**Files:**
+- Create: `src/main/java/.../service/ai/RAGService.java`
+- Modify: `src/main/java/.../service/DifySyncService.java`
+
+- [ ] **Step 1:创建 RAGService**
+
+```java
+package com.etotem.cfc.service.ai;
+
+import org.springframework.ai.vectorstore.VectorStore;
+import org.springframework.ai.document.Document;
+import org.springframework.stereotype.Service;
+import jakarta.annotation.Resource;
+import java.util.List;
+
+@Service
+public class RAGService {
+
+    // sqlite-vec 或其他 VectorStore 实现
+    // 先使用搜索+vector 方案,第一期可简化为 keyword search
+    @Resource
+    private VectorStore vectorStore;
+
+    public List<Document> search(String query, int topK) {
+        return vectorStore.similaritySearch(query, topK);
+    }
+
+    public void addDocument(Document doc) {
+        vectorStore.add(List.of(doc));
+    }
+}
+```
+
+- [ ] **Step 2:DifySyncService 标记为待迁移**
+
+保留原有接口签名,内部改为从本地知识库读取:
+```java
+@Service
+public class DifySyncService {
+
+    @Resource
+    private RAGService ragService;
+
+    public List<String> searchKnowledge(String query) {
+        return ragService.search(query, 5).stream()
+                .map(Document::getContent)
+                .collect(Collectors.toList());
+    }
+}
+```
+
+---
+
+#### Task 3.6: 剩余 6 个 Dify 相关 Service 替换
+
+**Files:**
+- Modify: 逐个修改以下 Service(每个独立的文件修改,验证后继续下一个)
+  - `AiConversationSummaryService.java`
+  - `AiUserFactService.java`
+  - 其余 4 个
+
+- [ ] **Step 1-6:逐一替换**
+
+每个 Service 的工作模式:
+1. 读取当前代码,理解其调用的 Dify API(chat-messages / workflows / datasets)
+2. 映射到 LLMService 或 RAGService 的对应方法
+3. 替换实现,保持接口签名不变(最小化 Controller 层变动)
+4. 编译验证
+
+---
+
+#### Task 3.7: 打阶段 tag
+
+- [ ] **Step 1:提交 + tag**
+
+```bash
+git commit -am "phase3: Spring AI integration + Dify migration complete"
+git tag v3.0-spring-ai
+```
+
+---
+
+### Phase 4 — 回归验证 + 上线(3 天)
+
+#### Task 4.1: 全量功能回归
+
+- [ ] **Step 1:Spring Boot 启动验证**
+
+```bash
+mvn spring-boot:run
+```
+Expected: 应用启动成功,无 Error 日志
+
+- [ ] **Step 2:所有 Controller API 冒烟测试**
+
+使用 Postman 用例集或 curl 脚本验证以下关键路径:
+```
+POST /api/ai/chat/send          → AI 聊天正常
+POST /api/ai/chat/stream        → 流式返回正常
+POST /api/auth/login             → JWT 正常签发
+POST /api/user/info              → 用户信息正常
+... 以及全部 48 个 Controller
+```
+
+- [ ] **Step 3:数学表达式精度对比验证**
+
+```
+输入: "12+38*2"   → 期望: 88.0
+输入: "(1+2)^3"   → 期望: 27.0
+输入: "round(3.7)" → 期望: 4.0
+输入: "abs(-5)"   → 期望: 5.0
+... 提取全部原表达式用例
+```
+
+---
+
+#### Task 4.2: 性能基准对比
+
+- [ ] **Step 1:记录原 Dify 链路延迟(基線)**
+
+如果 Dify 仍可用,取 10 次请求的 P50/P95/P99 延迟。
+如果 Dify 已不可用,使用 Phase 3 完成后的测试数据。
+
+- [ ] **Step 2:记录 Spring AI 链路延迟**
+
+对相同查询,取 10 次请求的 P50/P95/P99 延迟。
+
+- [ ] **Step 3:确认不比原链路差**
+
+延迟、错误率均不劣化则可进入上线。
+
+---
+
+#### Task 4.3: 灰度上线
+
+- [ ] **Step 1:灰度 10% 流量(1 天)**
+
+Nginx/网关配置 10% 流量指向新版本,监控错误率。
+
+- [ ] **Step 2:灰度 50% 流量(1 天)**
+
+确认 10% 稳定后扩大到 50%,继续监控。
+
+- [ ] **Step 3:全量上线 + Dify 实例保留下线**
+
+100% 流量切到新版本,Dify 实例暂停但不销毁(数据保留 30 天)。
+
+- [ ] **Step 4:打最终 tag**
+
+```bash
+git tag v3.0-production
+```
+
+---
+
+## 执行顺序图
+
+```
+Phase 0 ── Task 0.1 (BOM对齐)
+         └── Task 0.2 (Nashorn范围)
+
+Phase 1 ── Task 1.1 (Entity)
+         ├── Task 1.2 (DTO)
+         └── Task 1.3 (全局收尾)
+
+Phase 2 ── Task 2.1 (exp4j)
+         ├── Task 2.2 (jjwt)
+         ├── Task 2.3 (SB3行为)
+         └── Task 2.4 (springdoc验证)
+
+Phase 3 ── Task 3.1 (Spring AI依赖)
+         ├── Task 3.2 (LLMService)
+         ├── Task 3.3 (System Prompt)
+         ├── Task 3.4 (AIService重构)
+         ├── Task 3.5 (RAGService)
+         └── Task 3.6 (剩余Service)
+
+Phase 4 ── Task 4.1 (功能回归)
+         ├── Task 4.2 (性能对比)
+         └── Task 4.3 (灰度上线)
+```
+
+Phase 1 可在 Task 1.1 + 1.2 中 2 人并行。
+Phase 3 的 Task 3.2-3.6 建议 1 人连续完成(上下文依赖强)。
+其余阶段按顺序执行。