2026-07-20-SB3-upgrade-plan.md 27 KB

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 + AxonHub 替换 Dify 全部功能。

Architecture: 分层替代 Dify:Spring AI 承担 AI 应用层(对话/RAG/工具),AxonHub 承担模型路由层(分发/降级/计费),专用工具承担文档预处理。5 阶段串行,每阶段有独立编译/测试验证 + git tag 回退点。

Tech Stack: Java 21, Spring Boot 3.4.x, Spring AI 1.x, AxonHub (axonhub.iwintrue.com), 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, docling (PDF预处理)


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.12Jwts.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
  10. AxonHub 统一网关 — 所有模型调用经 axonhub.iwintrue.com,Spring AI 配置 base-url 指向 AxonHub
  11. 多模型编排 — 支持工作流各节点使用不同 ChatModel Bean(DeepSeek/Qwen/本地模型等),通过 @Qualifier 注入
  12. PDF 预处理 — 复杂文档预处理使用 Python 专有工具(docling / unstructured.io),不在 Spring AI ETL 中处理

文件变更清单

创建

文件 用途
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
scripts/doc-preprocess/ PDF 预处理 Python 脚本(docling)

修改

文件 变更类型
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 中修改:

<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

找到并替换:

<!-- 删除 -->
<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 覆盖

    <!-- 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 依赖

    <dependency>
    <groupId>net.objecthunter</groupId>
    <artifactId>exp4j</artifactId>
    <version>0.4.8</version>
    </dependency>
    
  • [ ] Step 5:编译验证

运行:

mvn clean compile

Expected: 编译失败(因为 javax 包名不存在,这是预期的。失败信息用来确认 Phase 1 的替换范围)

  • [ ] Step 6:录制 javax 报错清单

    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

    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 出现的位置,提取所有使用的表达式样例(记录输入和输出)。

// 按行记录类似这样的调用
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

    # 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:编译验证

    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

    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:编译验证

    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

    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:逐文件编译验证

每替换完一批,运行:

mvn clean compile

修复残留错误,直到零错。

  • [ ] Step 4:打阶段 tag

    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 替换实现

    // 替换前
    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 等函数,注册自定义函数:

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:编译验证

    mvn clean compile
    

Expected: 零错误


Task 2.2: jjwt 0.12 API 适配

Files:

  • Modify: 搜索所有使用 jjwt 的文件

  • [ ] Step 1:找到所有 jjwt 调用

    Get-ChildItem -Path "src/main/java" -Filter "*.java" -Recurse | 
    Select-String "Jwts\.|Keys\.|SignatureAlgorithm" | 
    ForEach-Object { $_.Filename + "(" + $_.LineNumber + "): " + $_.Line.Trim() }
    
  • [ ] Step 2:按规则适配

    // 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:编译验证

    mvn clean compile
    

Task 2.3: Spring Boot 3 行为变更适配

Files:

  • Modify: src/main/resources/application.yml

  • [ ] Step 1:检查路径匹配策略

添加/确认配置(如果现有代码依赖 Ant 风格路径匹配):

spring:
  mvc:
    pathmatch:
      matching-strategy: ant-path-matcher
  • [ ] Step 2:检查 springdoc 配置

    # v1 (如有)
    springdoc:
    swagger-ui:
    path: /swagger-ui.html
    
    # v2 相同,但验证可用
    
  • [ ] Step 3:编译运行验证

    mvn clean compile
    

验证应用启动:

mvn spring-boot:run

Expected: 应用启动成功,Swagger UI 可访问 /swagger-ui.html(或 v2 默认路径)


Task 2.4: springdoc v2 Swagger UI 验证

  • [ ] Step 1:启动应用

    mvn spring-boot:run
    
  • [ ] Step 2:访问 Swagger UI 验证 API 文档正确渲染

    curl http://localhost:9082/v3/api-docs | Select-Object -First 20
    

Expected: 返回 OpenAPI 3.0 JSON,paths 字段包含 /api/ 路由

  • [ ] Step 3:打阶段 tag

    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

    <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:配置 AxonHub 模型网关(application.yml)

所有模型调用走 AxonHub 统一网关,不直连任何模型 Provider:

spring:
  ai:
    openai:                                     # 默认模型 → AxonHub → DeepSeek
      base-url: https://axonhub.iwintrue.com/v1
      api-key: ${AXONHUB_API_KEY}
      chat:
        options:
          model: deepseek-chat
          temperature: 0.7
    dashscope:                                   # 第二模型 → AxonHub → Qwen (可选)
      api-key: ${AXONHUB_API_KEY}
      chat:
        options:
          model: qwen-max
    ollama:                                      # 本地模型 (不经过 AxonHub)
      base-url: http://localhost:11434
      chat:
        options:
          model: qwen2.5:7b
    vectorstore:
      sqlite:
        path: ${cfc.data.dir:/data/cfc}/knowledge.db
  • [ ] Step 3:编译验证

    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(含多模型支持)

    package com.etotem.cfc.config;
    
    import org.springframework.ai.chat.client.ChatClient;
    import org.springframework.ai.chat.model.ChatModel;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    
    @Configuration
    public class SpringAiConfig {
    
    /** 默认聊天客户端(经 AxonHub 路由) */
    @Bean
    ChatClient chatClient(ChatClient.Builder builder) {
        return builder.build();
    }
    
    /**
     * 如需在工作流中使用不同模型,注入命名后的 ChatModel Bean:
     * @Resource @Qualifier("openAiChatModel")  private ChatModel deepSeek;
     * @Resource @Qualifier("dashScopeChatModel") private ChatModel qwen;
     * @Resource @Qualifier("ollamaChatModel")   private ChatModel localModel;
     *
     * Spring AI 自动注册以下 Bean(按 application.yml 配置):
     *   openAiChatModel     → AxonHub → DeepSeek
     *   dashScopeChatModel  → AxonHub → Qwen (如果配置了 dashscope)
     *   ollamaChatModel     → localhost:11434 (如果配置了 ollama)
     */
    }
    
  • [ ] Step 2:创建 LLMService(含多模型编排)

    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.model.ChatModel;
    import org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor;
    import org.springframework.beans.factory.annotation.Qualifier;
    import org.springframework.stereotype.Service;
    import reactor.core.publisher.Flux;
    
    @Service
    public class LLMService {
    
    @Resource
    private ChatClient chatClient;
    
    @Resource @Qualifier("openAiChatModel")
    private ChatModel deepSeekModel;         // → AxonHub → DeepSeek
    
    @Resource @Qualifier("dashScopeChatModel")
    private ChatModel qwenModel;             // → AxonHub → Qwen
    
    @Resource @Qualifier("ollamaChatModel")
    private ChatModel localModel;            // → 本地 Ollama
    
    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 String chatWithModel(String query, String systemPrompt,
                                 String conversationId, ChatModel model) {
        return ChatClient.builder(model).build()
                .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();
    }
    
    // 多模型工作流示例
    public WorkflowResult executeMultiModelWorkflow(String input) {
        // Step 1: DeepSeek 做意图分类(强推理)
        String intent = ChatClient.builder(deepSeekModel).build()
                .prompt().user("分类用户意图: " + input).call().content();
        // Step 2: Qwen 做长文本分析(长上下文)
        String analysis = ChatClient.builder(qwenModel).build()
                .prompt().system("分析专家").user(intent).call().content();
        // Step 3: 本地模型格式化输出(零成本)
        String output = ChatClient.builder(localModel).build()
                .prompt().user("格式化: " + analysis).call().content();
        return new WorkflowResult(intent, analysis, output);
    }
    }
    
  • [ ] Step 3:编译验证

    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

    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 字段提取:

# 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

    // 原有 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:编译验证 + 冒烟测试

    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

    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 标记为待迁移

保留原有接口签名,内部改为从本地知识库读取:

@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

    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 启动验证

    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

    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 人连续完成(上下文依赖强)。 其余阶段按顺序执行。