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