diff --git a/.env.example b/.env.example index b4fe90d..8e8aeed 100644 --- a/.env.example +++ b/.env.example @@ -25,21 +25,10 @@ JWT_SECRET= # 开发环境使用 application-dev.yml 中的默认值,无需设置 CRYPTO_KEK= -# ========== MySQL ========== -MYSQL_HOST=localhost -MYSQL_PORT=3306 -MYSQL_DATABASE=eify -MYSQL_USERNAME=root -MYSQL_PASSWORD=change_me_mysql_password - -# MySQL root 密码(仅 docker-compose.yml 需要) -# MYSQL_ROOT_PASSWORD=change_me_mysql_root_password - -# JDBC URL(可直接覆盖自动拼接的 URL) -# MYSQL_URL=jdbc:mysql://localhost:3306/eify?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true - -# ========== Druid 监控面板(仅开发环境) ========== -# DRUID_PASSWORD=admin +# ========== PostgreSQL 17(主数据库,承载业务表 + 向量表) ========== +PG_URL=jdbc:postgresql://localhost:5432/eify?stringtype=unspecified +PG_USERNAME=postgres +PG_PASSWORD=change_me_postgres_password # ========== Redis ========== REDIS_HOST=localhost @@ -61,15 +50,6 @@ CLICKHOUSE_PASSWORD=change_me_clickhouse_password # ========== Grafana(日志栈可视化) ========== GRAFANA_PASSWORD=change_me_grafana_password -# ========== PostgreSQL(pgvector 向量存储) ========== -PGVECTOR_HOST=localhost -PGVECTOR_PORT=5432 -PGVECTOR_DATABASE=eify_vector -# 备选:直接指定完整 JDBC URL(优先于上述分离变量) -# PGVECTOR_URL=jdbc:postgresql://localhost:5432/eify_vector?stringtype=unspecified -PGVECTOR_USERNAME=postgres -PGVECTOR_PASSWORD=change_me_postgres_password - # ========== Embedding API(DashScope / OpenAI 兼容) ========== EMBEDDING_API_KEY=change_me_embedding_api_key diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 5849227..391b8ae 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -32,7 +32,7 @@ If applicable, add screenshots to help explain the problem. | **Deployment** | Docker / Local Dev | | **Browser** | Chrome / Firefox / Edge | | **Version** | 3.x | -| **Database** | MySQL 8.x | +| **Database** | PostgreSQL 17 | ## Logs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b4b1446..32796fe 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,14 +12,19 @@ jobs: runs-on: ubuntu-latest services: - mysql: - image: mysql:8.0 + postgres: + image: pgvector/pgvector:pg17 env: - MYSQL_ROOT_PASSWORD: root - MYSQL_DATABASE: eify + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + POSTGRES_DB: eify ports: - - 3306:3306 - options: --health-cmd "mysqladmin ping -h localhost" --health-interval 10s --health-timeout 5s --health-retries 5 + - 5432:5432 + options: >- + --health-cmd "pg_isready -U postgres" + --health-interval 10s + --health-timeout 5s + --health-retries 5 steps: - name: Checkout @@ -35,9 +40,9 @@ jobs: - name: Build & Test run: mvn verify -pl eify-app -am -DskipTests=false env: - MYSQL_URL: jdbc:mysql://localhost:3306/eify?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true - MYSQL_USERNAME: root - MYSQL_PASSWORD: root + PG_URL: jdbc:postgresql://localhost:5432/eify?stringtype=unspecified + PG_USERNAME: postgres + PG_PASSWORD: postgres REDIS_HOST: localhost REDIS_PORT: 6379 EMBEDDING_API_KEY: ${{ secrets.EMBEDDING_API_KEY }} @@ -91,9 +96,9 @@ jobs: - name: Build Backend JAR run: mvn package -pl eify-app -am -DskipTests env: - MYSQL_URL: jdbc:mysql://localhost:3306/eify - MYSQL_USERNAME: root - MYSQL_PASSWORD: root + PG_URL: jdbc:postgresql://localhost:5432/eify?stringtype=unspecified + PG_USERNAME: postgres + PG_PASSWORD: postgres - name: Setup Node uses: actions/setup-node@v4 diff --git a/CHANGELOG.md b/CHANGELOG.md index 7be9b3d..534e7b0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,7 +32,7 @@ - 工作流引擎:可视化编排,支持 LLM/API/ToolCall/条件分支/开始/结束节点 - 多工作空间多租户架构:JWT 认证 + 工作空间级数据隔离 - 完整日志系统:纯 JSON 格式(UTC)、ClickHouse + Vector 采集、OpenTelemetry 链路追踪 -- Docker 全栈部署:MySQL + Redis + pgvector + ClickHouse + Vector +- Docker 全栈部署:PostgreSQL 17 + Redis + ClickHouse + Vector - CI/CD:GitHub Actions 自动化测试与构建 + Jenkins CD 部署流水线 --- diff --git a/CLAUDE.md b/CLAUDE.md index cb8b3a5..6e411cb 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -44,7 +44,7 @@ mvn spring-boot:run -pl eify-app -Dspring-boot.run.profiles=dev ### Docker 部署 ```bash -# 启动所有服务(MySQL + Redis + pgvector + 应用) +# 启动所有服务(PostgreSQL 17 + Redis + 应用) docker-compose -f deploy/infra/deploy/docker-compose.yml up -d # 启动日志采集链路(ClickHouse + Vector + Grafana + Prometheus) @@ -85,7 +85,7 @@ grep 'YOUR_TRACE_ID' ./logs/eify.log | jq | 🔴 **日志格式** | 使用统一日志配置,输出纯 JSON 格式(UTC 时区) | 日志格式混乱 | [LOGGING.md](docs/guides/LOGGING.md) | | 🔴 **ClickHouse 类型** | Nullable 字段不能与 LowCardinality 嵌套 | 类型错误 | [DATABASE.md](docs/guides/DATABASE.md) | | 🔴 **工作空间数据隔离** | Service 层所有查询/更新/删除必须过滤 `workspace_id`,更新前验证归属。使用 `WorkspaceGuard` 工具类消除样板代码 | 跨工作空间数据泄露/篡改 | [AUTH-WORKSPACE.md](docs/guides/AUTH-WORKSPACE.md) | -| 🔴 **Flyway 迁移幂等** | 所有 DDL 语句必须检查对象是否已存在(ADD COLUMN → INFORMATION_SCHEMA.COLUMNS,ADD INDEX → INFORMATION_SCHEMA.STATISTICS),确保幂等可重入 | 应用启动失败,迁移阻塞 | [DATABASE.md](docs/guides/DATABASE.md) | +| 🔴 **Flyway 迁移幂等** | 所有 DDL 语句必须使用 PG 原生幂等语法(ADD COLUMN IF NOT EXISTS / CREATE INDEX IF NOT EXISTS / DO $$ ... ADD CONSTRAINT),确保幂等可重入 | 应用启动失败,迁移阻塞 | [DATABASE.md](docs/guides/DATABASE.md) | | 🔴 **ADR 命名规范** | 架构决策记录统一放在 `docs/ADRs/`,文件命名 `ADR-{四位递增序号}-{名称}.md`,序号按创建时间递增 | ADR 命名混乱,查找困难 | — | | 🔴 **ADR 格式规范** | 所有 ADR 文档必须遵循 `docs/ADRs/ADR-XXXX-Template.md` 模板格式,包含 6 个必填章节:`# Status`、`# Date`、`# Owner`、`# Deciders`、`# Context`、`# Decision`,以及 2 个推荐章节:`## Consequences`、`# Details`。`# Considered Options` 章节列出所有候选方案及其被拒绝原因 | ADR 结构不一致,难以阅读和对比 | `docs/ADRs/ADR-XXXX-Template.md` | | 🔴 **安全审查** | 涉及安全敏感代码时,审查前必须阅读 [SECURITY.md](docs/guides/SECURITY.md) 系统风险清单 | 遗漏安全检查 | [SECURITY.md](docs/guides/SECURITY.md) | @@ -196,7 +196,7 @@ private LocalDateTime updatedAt; - 软删除字段 `deleted` 必须有索引 - 分页查询优先使用游标分页(针对大表) - JSON 字段必须添加注释说明结构 -- Flyway 迁移必须幂等:DDL 通过 INFORMATION_SCHEMA 检查后执行,模板详见 [DATABASE.md](docs/guides/DATABASE.md) +- Flyway 迁移必须幂等:DDL 使用 PG 原生 `IF NOT EXISTS` 语法(ADD COLUMN / CREATE INDEX / DO $$ ... ADD CONSTRAINT),模板详见 [DATABASE.md](docs/guides/DATABASE.md) --- @@ -252,7 +252,7 @@ cd eify-web && npx vue-tsc --noEmit && cd .. # 前端类型检查 - [ ] **索引覆盖**:新查询有对应索引,EXPLAIN 确认无全表扫描 - [ ] **N+1 查询**:禁止循环内逐条查询,使用批量查询或联表查询 -- [ ] **Flyway 幂等**:DDL 使用 `INFORMATION_SCHEMA` 检查模板(ADD COLUMN / ADD INDEX / ADD UNIQUE KEY) +- [ ] **Flyway 幂等**:DDL 使用 PG 原生幂等语法(ADD COLUMN IF NOT EXISTS / CREATE INDEX IF NOT EXISTS / DO $$ ... ADD CONSTRAINT) - [ ] **ClickHouse 类型**:Nullable 不与 LowCardinality 嵌套;按 logType 设置 Nullable 字段 ### 涉及 API 接口时 @@ -328,7 +328,7 @@ cd eify-web && npx vue-tsc --noEmit && cd .. # 前端类型检查 | [docs/README.md](docs/README.md) | 项目总览 | 了解项目整体情况 | | [ARCHITECTURE.md](docs/ARCHITECTURE.md) | 架构设计 | 开发新功能前了解模块结构 | | [API-SPEC.md](docs/API-SPEC.md) | 接口规范 | 设计 API 接口 | -| [DATABASE.md](docs/guides/DATABASE.md) | 数据库规范 | MySQL 建表模板、索引分页、业务表 DDL、ClickHouse 日志库、游标分页优化 | +| [DATABASE.md](docs/guides/DATABASE.md) | 数据库规范 | PostgreSQL 建表模板、索引分页、业务表 DDL、ClickHouse 日志库、游标分页优化 | | [AUTH-WORKSPACE.md](docs/guides/AUTH-WORKSPACE.md) | 用户认证与工作空间 | 多租户架构、JWT 认证、数据隔离 | | [DESIGN.md](DESIGN.md) | 设计系统规范 | 生成前端 UI 时参照,包含颜色、字体、间距、组件、布局等视觉令牌 | diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 72fd3fd..2914f11 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -14,7 +14,7 @@ cp .env.example .env # 编辑 .env 填入本地配置 # 3. 启动依赖服务 -docker-compose -f deploy/infra/deploy/docker-compose.yml up -d mysql redis +docker-compose -f deploy/infra/deploy/docker-compose.yml up -d pgvector redis # 4. 启动后端 mvn spring-boot:run -pl eify-app -Dspring-boot.run.profiles=dev @@ -72,7 +72,7 @@ mvn test mvn test -pl eify-workflow ``` -CI 通过 GitHub Actions Service Container 提供 MySQL 8.0 测试数据库,CD 部署由 Jenkins 流水线完成。本地测试也可通过 `application-test.yml` 配置连接本地数据库。 +CI 通过 GitHub Actions Service Container 提供 PostgreSQL 17 测试数据库,CD 部署由 Jenkins 流水线完成。本地测试也可通过 `application-test.yml` 配置连接本地数据库。 ## Issue 规范 diff --git a/Jenkinsfile b/Jenkinsfile index a6b84fd..5eb363a 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -8,9 +8,8 @@ pipeline { description: '部署环境(auto = 根据分支/Tag 自动选择;手动触发 prod 部署时请指定 Tag)' ) string(name: 'TAG_NAME', defaultValue: '', description: 'Git Tag 名称(手动触发生产部署时指定,如 v1.0.0.20260520103045.42)') - string(name: 'MYSQL_HOST', defaultValue: '', description: 'MySQL 主机地址(留空使用 ConfigMap 默认值)') + string(name: 'PG_HOST', defaultValue: '', description: 'PostgreSQL 17 主机地址(留空使用 ConfigMap 默认值)') string(name: 'REDIS_HOST', defaultValue: '', description: 'Redis 主机地址') - string(name: 'PGVECTOR_HOST', defaultValue: '', description: 'PostgreSQL 主机地址') booleanParam(name: 'SKIP_TESTS', defaultValue: false, description: '跳过测试阶段') } @@ -127,30 +126,27 @@ pipeline { // 更新 ConfigMap(替换占位符) sh """ - sed -i 's//${params.MYSQL_HOST ?: 'localhost'}/g' deploy/k8s/configmap.yaml - sed -i 's//${params.REDIS_HOST ?: 'localhost'}/g' deploy/k8s/configmap.yaml - sed -i 's//${params.PGVECTOR_HOST ?: 'localhost'}/g' deploy/k8s/configmap.yaml + sed -i 's||${params.PG_HOST ?: 'localhost'}|g' deploy/k8s/configmap.yaml + sed -i 's||${params.REDIS_HOST ?: 'localhost'}|g' deploy/k8s/configmap.yaml sed -i 's/SPRING_PROFILES_ACTIVE: "prod"/SPRING_PROFILES_ACTIVE: "${DEPLOY_ENV}"/g' deploy/k8s/configmap.yaml """ sh "kubectl apply -f deploy/k8s/configmap.yaml" // Secret:使用 Jenkins credentials 动态创建 withCredentials([ - string(credentialsId: 'mysql-password', variable: 'MYSQL_PASS'), + string(credentialsId: 'pg-password', variable: 'PG_PASS'), string(credentialsId: 'redis-password', variable: 'REDIS_PASS'), string(credentialsId: 'jwt-secret', variable: 'JWT_SECRET'), string(credentialsId: 'crypto-kek', variable: 'CRYPTO_KEK'), - string(credentialsId: 'pgvector-password', variable: 'PGVECTOR_PASS'), string(credentialsId: 'embedding-api-key', variable: 'EMBEDDING_API_KEY') ]) { sh """ kubectl create secret generic eify-secret \ - --from-literal=MYSQL_USERNAME=root \ - --from-literal=MYSQL_PASSWORD=\${MYSQL_PASS} \ + --from-literal=PG_USERNAME=postgres \ + --from-literal=PG_PASSWORD=\${PG_PASS} \ --from-literal=REDIS_PASSWORD=\${REDIS_PASS} \ --from-literal=JWT_SECRET=\${JWT_SECRET} \ --from-literal=CRYPTO_KEK=\${CRYPTO_KEK} \ - --from-literal=PGVECTOR_PASSWORD=\${PGVECTOR_PASS} \ --from-literal=EMBEDDING_API_KEY=\${EMBEDDING_API_KEY} \ -n ${K8S_NAMESPACE} \ --dry-run=client -o yaml | kubectl apply -f - diff --git a/README.md b/README.md index 1058c6a..cdd9e9a 100644 --- a/README.md +++ b/README.md @@ -132,8 +132,8 @@ docker-compose -f deploy/infra/deploy/docker-compose.yml up -d ### Local Development ```bash -# 1. Start dependencies (MySQL + Redis) -docker-compose -f deploy/infra/deploy/docker-compose.yml up -d mysql redis +# 1. Start dependencies (PostgreSQL + Redis) +docker-compose -f deploy/infra/deploy/docker-compose.yml up -d pgvector redis # 2. Start backend ./start.sh dev @@ -167,9 +167,8 @@ docker-compose -f deploy/optional/docker-compose-jaeger.yml up -d |:---|:---| | **Backend Framework** | Spring Boot 4.0.6 | | **ORM** | MyBatis-Plus 3.5.15 | -| **Database** | MySQL 8.0 | +| **Database** | PostgreSQL 17 + pgvector | | **Cache** | Redis 7 | -| **Vector DB** | PostgreSQL 16 + pgvector | | **Frontend** | Vue 3.5.17 + TypeScript + Vite | | **UI Framework** | Element Plus 2.10.2 | | **State Management** | Pinia 2.3.1 | @@ -204,15 +203,15 @@ eify/ ``` ┌─────────────┐ ┌─────────────┐ ┌──────────────┐ -│ Vue 3 │────▶│ Spring Boot│────▶│ MySQL 8 │ -│ Frontend │ │ REST API │ │ Persistence │ +│ Vue 3 │────▶│ Spring Boot│────▶│ PostgreSQL 17│ +│ Frontend │ │ REST API │ │ + pgvector │ └─────────────┘ └──────┬──────┘ └──────────────┘ │ ┌───────────┼───────────┐ │ │ │ ▼ ▼ ▼ ┌──────────┐ ┌──────────┐ ┌──────────┐ - │ Redis │ │ pgvector │ │ External │ + │ Redis │ │ LLM/ │ │ ClickHouse │ │ Cache │ │ Vector │ │ LLM APIs │ └──────────┘ └──────────┘ └──────────┘ ``` @@ -231,7 +230,7 @@ eify/ | [ARCHITECTURE.md](docs/ARCHITECTURE.md) | Architecture & coding conventions | | [API-SPEC.md](docs/API-SPEC.md) | API design specification | | [AUTH-WORKSPACE.md](docs/guides/AUTH-WORKSPACE.md) | Auth & multi-tenant workspace | -| [DATABASE.md](docs/guides/DATABASE.md) | Database design (MySQL + ClickHouse) | +| [DATABASE.md](docs/guides/DATABASE.md) | Database design (PostgreSQL + ClickHouse) | | [LOGGING.md](docs/guides/LOGGING.md) | Logging system guide | | [WORKFLOW.md](docs/guides/WORKFLOW.md) | Workflow engine design | | [DEPLOYMENT.md](docs/DEPLOYMENT.md) | Deployment & CI/CD | diff --git a/README.zh-CN.md b/README.zh-CN.md index f0d0834..75522f9 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -133,8 +133,8 @@ docker-compose -f deploy/infra/deploy/docker-compose.yml up -d ### 本地开发 ```bash -# 1. 启动依赖服务(MySQL + Redis) -docker-compose -f deploy/infra/deploy/docker-compose.yml up -d mysql redis +# 1. 启动依赖服务(PostgreSQL + Redis) +docker-compose -f deploy/infra/deploy/docker-compose.yml up -d pgvector redis # 2. 启动后端 ./start.sh dev @@ -168,7 +168,7 @@ docker-compose -f deploy/optional/docker-compose-jaeger.yml up -d |:---|:---| | **后端框架** | Spring Boot 4.0.6 | | **ORM** | MyBatis-Plus 3.5.15 | -| **数据库** | MySQL 8.0 | +| **数据库** | PostgreSQL 17 + pgvector | | **缓存** | Redis 7 | | **向量数据库** | PostgreSQL 16 + pgvector | | **前端** | Vue 3.5.17 + TypeScript + Vite | diff --git a/deploy/infra/deploy/docker-compose.yml b/deploy/infra/deploy/docker-compose.yml index 39bd013..cc61a21 100644 --- a/deploy/infra/deploy/docker-compose.yml +++ b/deploy/infra/deploy/docker-compose.yml @@ -4,25 +4,6 @@ services: - mysql: - image: mysql:8.0 - container_name: eify-mysql - ports: - - "3306:3306" - environment: - MYSQL_ROOT_PASSWORD: ${MYSQL_PASSWORD:?MYSQL_PASSWORD_required} - MYSQL_DATABASE: eify - volumes: - - mysql-data:/var/lib/mysql - - command: --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci - healthcheck: - test: ["CMD", "mysqladmin", "ping", "-h", "localhost"] - interval: 10s - timeout: 5s - retries: 5 - restart: unless-stopped - redis: image: redis:7-alpine container_name: eify-redis @@ -39,14 +20,14 @@ services: restart: unless-stopped pgvector: - image: pgvector/pgvector:pg16 + image: pgvector/pgvector:pg17 container_name: eify-pgvector ports: - "5432:5432" environment: POSTGRES_USER: ${PGVECTOR_USERNAME:-postgres} POSTGRES_PASSWORD: ${PGVECTOR_PASSWORD} - POSTGRES_DB: ${PGVECTOR_DATABASE:-eify_vector} + POSTGRES_DB: ${PG_DATABASE:-eify} volumes: - pgvector-data:/var/lib/postgresql/data healthcheck: @@ -67,24 +48,14 @@ services: SPRING_PROFILES_ACTIVE: prod JWT_SECRET: ${JWT_SECRET:?JWT_SECRET_required} CRYPTO_KEK: ${CRYPTO_KEK:?CRYPTO_KEK_required} - MYSQL_HOST: mysql - MYSQL_PORT: 3306 - MYSQL_DATABASE: eify - MYSQL_URL: jdbc:mysql://mysql:3306/eify?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true - MYSQL_USERNAME: root - MYSQL_PASSWORD: ${MYSQL_PASSWORD:?MYSQL_PASSWORD_required} + PG_URL: jdbc:postgresql://pgvector:5432/${PG_DATABASE:-eify}?stringtype=unspecified + PG_USERNAME: ${PGVECTOR_USERNAME:-postgres} + PG_PASSWORD: ${PGVECTOR_PASSWORD} REDIS_HOST: redis REDIS_PORT: 6379 REDIS_PASSWORD: ${REDIS_PASSWORD} EMBEDDING_API_KEY: ${EMBEDDING_API_KEY} - PGVECTOR_HOST: ${PGVECTOR_HOST:-pgvector} - PGVECTOR_PORT: ${PGVECTOR_PORT:-5432} - PGVECTOR_DATABASE: ${PGVECTOR_DATABASE:-eify_vector} - PGVECTOR_USERNAME: ${PGVECTOR_USERNAME:-postgres} - PGVECTOR_PASSWORD: ${PGVECTOR_PASSWORD} depends_on: - mysql: - condition: service_healthy redis: condition: service_healthy pgvector: @@ -104,6 +75,5 @@ services: restart: unless-stopped volumes: - mysql-data: redis-data: pgvector-data: diff --git a/deploy/k8s/configmap.yaml b/deploy/k8s/configmap.yaml index 85faab5..bd02b93 100644 --- a/deploy/k8s/configmap.yaml +++ b/deploy/k8s/configmap.yaml @@ -13,21 +13,13 @@ data: # Default: prod (production) SPRING_PROFILES_ACTIVE: "prod" - # MySQL - 替换为实际数据库地址 - MYSQL_HOST: "" - MYSQL_PORT: "3306" - MYSQL_DATABASE: "eify" + # PostgreSQL 17(主数据库,承载业务表 + 向量表) + PG_URL: "jdbc:postgresql://:5432/eify?stringtype=unspecified" # Redis - 替换为实际 Redis 地址 REDIS_HOST: "" REDIS_PORT: "6379" REDIS_DATABASE: "0" - # PostgreSQL (pgvector) - 替换为实际地址 - PGVECTOR_HOST: "" - PGVECTOR_PORT: "5432" - PGVECTOR_DATABASE: "eify_vector" - PGVECTOR_USERNAME: "postgres" - # JVM options - 根据集群资源调整 JAVA_OPTS: "-Xms512m -Xmx1024m -XX:+UseG1GC -XX:MaxGCPauseMillis=200" diff --git a/deploy/k8s/secret.yaml b/deploy/k8s/secret.yaml index f4f43ab..768b56d 100644 --- a/deploy/k8s/secret.yaml +++ b/deploy/k8s/secret.yaml @@ -3,12 +3,11 @@ # # 动态创建命令: # kubectl create secret generic eify-secret \ -# --from-literal=MYSQL_USERNAME=root \ -# --from-literal=MYSQL_PASSWORD= \ +# --from-literal=PG_USERNAME=postgres \ +# --from-literal=PG_PASSWORD= \ # --from-literal=REDIS_PASSWORD= \ # --from-literal=JWT_SECRET= \ # --from-literal=CRYPTO_KEK= \ -# --from-literal=PGVECTOR_PASSWORD= \ # --from-literal=EMBEDDING_API_KEY= \ # -n eify --dry-run=client -o yaml | kubectl apply -f - # @@ -27,10 +26,9 @@ type: Opaque data: # ⚠️ 以下是占位符,部署前必须替换为实际值的 base64 编码 # 生成方式:echo -n 'your-real-value' | base64 - MYSQL_USERNAME: - MYSQL_PASSWORD: + PG_USERNAME: + PG_PASSWORD: REDIS_PASSWORD: JWT_SECRET: EMBEDDING_API_KEY: CRYPTO_KEK: - PGVECTOR_PASSWORD: diff --git a/deploy/sql/README.md b/deploy/sql/README.md index 7fc6357..8aea469 100644 --- a/deploy/sql/README.md +++ b/deploy/sql/README.md @@ -1,24 +1,19 @@ -# Eify 数据库 SQL 文件说明 +# Eify 数据库 SQL 参考 ## 概述 -本目录包含 Eify 项目的数据库参考脚本。MySQL 和 PostgreSQL/pgvector 的迁移均由 **Flyway** 在应用启动时自动执行。 +Eify 使用 **PostgreSQL 17** 作为唯一数据库,承载全部业务表 + `document_chunk` 向量表。 +数据库迁移由 **Flyway** 在应用启动时自动执行。 ## 迁移方式 -### Flyway(推荐) +### Flyway(推荐,自动执行) 数据库表结构由 Flyway 在应用启动时自动创建: ``` -# MySQL +# PostgreSQL 17(业务表 + 向量表,单库单 Flyway) eify-app/src/main/resources/db/migration/V1__init.sql -eify-app/src/main/resources/db/migration/V4__model_category_and_embedding_model_id.sql -eify-app/src/main/resources/db/migration/V5__name_workspace_deleted_unique.sql - -# PostgreSQL(pgvector 向量存储) -eify-app/src/main/resources/db/migration-pg/V1__init_pgvector.sql -eify-app/src/main/resources/db/migration-pg/V2__flexible_vector_dimension.sql ``` - **本地开发**:启动应用时自动执行迁移 @@ -30,33 +25,17 @@ eify-app/src/main/resources/db/migration-pg/V2__flexible_vector_dimension.sql 如需手动导入或参考表结构: ```bash -# MySQL -mysql -u root -p eify < deploy/sql/init_eify_mysql.sql - -# PostgreSQL(pgvector 向量存储) -psql -U postgres -d eify_vector < deploy/sql/init_eify_vector_pgsql.sql +# PostgreSQL 17 +psql -U postgres -d eify < deploy/sql/init_eify_pgsql.sql ``` -## 文件清单 - -| 文件 | 说明 | 用途 | -|------|------|------| -| `init_eify_mysql.sql` | MySQL 完整 DDL 参考 | Flyway 迁移源文件,手动导入备用 | -| `init_eify_vector_pgsql.sql` | PostgreSQL pgvector DDL 参考 | Flyway 迁移源文件,手动导入备用 | -| `README.md` | 本说明文件 | 文档说明 | +> `deploy/sql/init_eify_pgsql.sql` 为 `V1__init.sql` 的副本(如需要,手动生成)。 ## pgvector(PostgreSQL 向量存储) -Eify 使用 PostgreSQL + pgvector 扩展存储文档分块的向量嵌入,支撑知识库 RAG 检索。 +Eify 使用 PostgreSQL + pgvector 扩展存储文档分块的向量嵌入,与业务表共享同一 PG17 数据库。 -### 迁移管理 - -pgvector 表结构由 Flyway 独立实例管理: -- 迁移文件:`db/migration-pg/` -- 历史表:`flyway_schema_history_pg`(与 MySQL 的 `flyway_schema_history` 隔离) -- `baselineOnMigrate: true`:已有数据库自动基线,不影响存量数据 - -### 表结构 +### 向量表 - **`document_chunk`**:文档分块向量表 - `embedding VECTOR` — 向量嵌入字段(灵活维度,由知识库模型决定) @@ -65,21 +44,16 @@ pgvector 表结构由 Flyway 独立实例管理: ### 容器部署 -Docker Compose 仅挂载数据目录,表迁移由 Flyway 负责: +Docker Compose 使用 `pgvector/pgvector:pg17` 镜像,表迁移由 Flyway 负责: ```yaml pgvector: - image: pgvector/pgvector:pg16 + image: pgvector/pgvector:pg17 volumes: - pgvector-data:/var/lib/postgresql/data ``` -### 相关文档 - -- [DATABASE.md](../../docs/guides/DATABASE.md) — 数据库设计规范(含向量存储架构) -- [双存储设计决策](../../docs/ADRs/ADR-0004-dual-storage-mysql-pgvector.md) — 为什么选择 MySQL + pgvector 分离 - -### MySQL 初始化脚本包含的内容 +## 表结构清单(23 张表,全部在 eify 单库) 1. **用户与工作空间模块** - `ai_user` - 用户表 @@ -103,6 +77,7 @@ pgvector: 5. **Knowledge 模块** - `knowledge_base` - 知识库表 - `document` - 文档表 + - `document_chunk` - 文档分块向量表(pgvector) - `agent_knowledge` - Agent 与知识库关联表 6. **MCP 模块** @@ -116,37 +91,52 @@ pgvector: - `ai_workflow_edge` - 工作流连线表 - `ai_workflow_execution` - 工作流执行记录表 -8. **初始数据** - - 管理员用户(admin/admin123) - - 默认工作空间 - - 默认供应商(OpenAI、Ollama) +## 初始数据 + +- 管理员用户(admin/admin123) +- 默认工作空间(System Workspace) +- 默认供应商(OpenAI、Ollama) ## 数据库规范 ### 字段命名规范 -- 时间字段:`created_at`、`updated_at` -- 逻辑删除:`deleted`(0=正常,1=删除) +- 主键:`id`(`BIGINT GENERATED BY DEFAULT AS IDENTITY`) +- 时间字段:`created_at`、`updated_at`(`TIMESTAMPTZ`,默认 `NOW()`) +- 逻辑删除:`deleted`(0=正常,非零=删除) - 工作空间隔离:`workspace_id` ### 索引规范 -- 主键:`id`(自增) -- 唯一索引:`uk_` 前缀 +- 唯一约束:`uk_` 前缀 - 普通索引:`idx_` 前缀 - 工作空间索引:所有业务表必须有 `idx_workspace_id` - -### 字符集规范 - -- 字符集:`utf8mb4` -- 排序规则:`utf8mb4_unicode_ci` +- JSONB 字段使用 `->>` 操作符查询,数字比较需要 `::bigint` 强转 + +### Flyway 幂等规范 + +所有 DDL 使用 PG 原生幂等写法: + +```sql +-- ADD COLUMN(幂等) +ALTER TABLE my_table ADD COLUMN IF NOT EXISTS my_col SMALLINT NOT NULL DEFAULT 0; +-- ADD INDEX(幂等) +CREATE INDEX IF NOT EXISTS idx_my_col ON my_table(my_col); +-- ADD UNIQUE(幂等,约束无 IF NOT EXISTS,用 DO 块) +DO $$ BEGIN + IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'uk_my_key') THEN + ALTER TABLE my_table ADD CONSTRAINT uk_my_key UNIQUE (col_a, col_b); + END IF; +END $$; +``` ## 注意事项 -1. **工作空间隔离**:所有业务表都包含 `workspace_id` 字段,用于多租户数据隔离 -2. **逻辑删除**:使用 `deleted` 字段实现软删除,不物理删除数据 -3. **外键约束**:不使用外键约束,由应用层保证数据一致性 -4. **索引覆盖**:所有查询必须有索引覆盖,避免全表扫描 +1. **单库架构**:不再区分"业务库"和"向量库",全部在 PostgreSQL 17 的同一 database 中 +2. **工作空间隔离**:所有业务表都包含 `workspace_id` 字段,用于多租户数据隔离 +3. **逻辑删除**:使用 `deleted` 字段实现软删除,不物理删除数据 +4. **外键约束**:不使用外键约束,由应用层保证数据一致性 +5. **索引覆盖**:所有查询必须有索引覆盖,避免全表扫描 ## 相关文档 diff --git a/deploy/sql/init_eify_mysql.sql b/deploy/sql/init_eify_mysql.sql deleted file mode 100644 index 796716f..0000000 --- a/deploy/sql/init_eify_mysql.sql +++ /dev/null @@ -1,553 +0,0 @@ --- ============================================================ --- Eify 数据库初始化脚本(统一入口) --- 执行方式:mysql -u root -p < deploy/sql/init_eify_mysql.sql --- 适用环境:dev / prod --- 最后更新:2026-05-22 --- ============================================================ --- --- 表清单(按模块分组): --- 用户与工作空间:ai_user, ai_workspace, ai_workspace_member, ai_workspace_invite, ai_user_session --- Provider: provider, model_config, provider_health --- Agent: ai_agent --- Chat: ai_chat_session, ai_chat_message --- Knowledge: knowledge_base, document, agent_knowledge --- MCP: mcp_server, mcp_tool, agent_mcp_tool --- Workflow: ai_workflow, ai_workflow_node, ai_workflow_edge, ai_workflow_execution --- --- 规范来源:docs/guides/DATABASE.md, docs/guides/AUTH-WORKSPACE.md --- ============================================================ - -CREATE DATABASE IF NOT EXISTS `eify` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; - -USE `eify`; - -SET NAMES utf8mb4; -SET FOREIGN_KEY_CHECKS = 0; - --- ############################################################ --- 1. 用户与工作空间模块 --- ############################################################ - --- 用户表 -CREATE TABLE IF NOT EXISTS `ai_user` ( - `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '用户ID', - `username` VARCHAR(64) NOT NULL COMMENT '用户名(唯一)', - `email` VARCHAR(128) NOT NULL DEFAULT '' COMMENT '邮箱', - `password` VARCHAR(256) NOT NULL COMMENT 'BCrypt 密码哈希', - `display_name` VARCHAR(128) DEFAULT NULL COMMENT '显示名称', - `avatar_url` VARCHAR(512) DEFAULT NULL COMMENT '头像URL', - `status` TINYINT NOT NULL DEFAULT 1 COMMENT '状态:0=禁用,1=正常', - `last_login_at` DATETIME DEFAULT NULL COMMENT '最后登录时间', - - `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', - `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', - `deleted` TINYINT NOT NULL DEFAULT 0 COMMENT '逻辑删除:0=正常,1=删除', - `creator_id` BIGINT UNSIGNED DEFAULT NULL COMMENT '创建人ID(注册时为空)', - - PRIMARY KEY (`id`), - UNIQUE KEY `uk_username` (`username`), - UNIQUE KEY `uk_email` (`email`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户表'; - --- 工作空间表 -CREATE TABLE IF NOT EXISTS `ai_workspace` ( - `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '工作空间ID', - `name` VARCHAR(128) NOT NULL COMMENT '工作空间名称', - `description` VARCHAR(512) DEFAULT NULL COMMENT '描述', - - `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', - `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', - `deleted` TINYINT NOT NULL DEFAULT 0 COMMENT '逻辑删除:0=正常,1=删除', - `creator_id` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '创建人ID', - - PRIMARY KEY (`id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='工作空间表'; - --- 工作空间成员表 -CREATE TABLE IF NOT EXISTS `ai_workspace_member` ( - `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键', - `workspace_id` BIGINT UNSIGNED NOT NULL COMMENT '工作空间ID', - `user_id` BIGINT UNSIGNED NOT NULL COMMENT '用户ID', - `role` VARCHAR(32) NOT NULL DEFAULT 'member' COMMENT '角色:owner/admin/member', - `joined_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '加入时间', - - `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', - `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', - `deleted` TINYINT NOT NULL DEFAULT 0 COMMENT '逻辑删除:0=正常,1=删除', - `creator_id` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '创建人ID', - - PRIMARY KEY (`id`), - UNIQUE KEY `uk_workspace_user` (`workspace_id`, `user_id`), - KEY `idx_user_id` (`user_id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='工作空间成员表'; - --- 工作空间邀请码表 -CREATE TABLE IF NOT EXISTS `ai_workspace_invite` ( - `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键', - `workspace_id` BIGINT UNSIGNED NOT NULL COMMENT '工作空间ID', - `code` VARCHAR(16) NOT NULL COMMENT '邀请码', - `expires_at` DATETIME DEFAULT NULL COMMENT '过期时间(NULL=永不过期)', - `max_uses` INT NOT NULL DEFAULT 0 COMMENT '最大使用次数(0=无限制)', - `use_count` INT NOT NULL DEFAULT 0 COMMENT '已使用次数', - `enabled` TINYINT NOT NULL DEFAULT 1 COMMENT '是否启用', - `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', - `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', - `deleted` TINYINT NOT NULL DEFAULT 0 COMMENT '逻辑删除', - `creator_id` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '创建人ID', - - PRIMARY KEY (`id`), - UNIQUE KEY `uk_code` (`code`), - KEY `idx_workspace_id` (`workspace_id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='工作空间邀请码表'; - --- 用户会话表(refresh token 持久化) -CREATE TABLE IF NOT EXISTS `ai_user_session` ( - `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键', - `user_id` BIGINT UNSIGNED NOT NULL COMMENT '用户ID', - `refresh_token` VARCHAR(256) NOT NULL COMMENT '刷新令牌', - `expires_at` DATETIME NOT NULL COMMENT '过期时间', - - `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', - - PRIMARY KEY (`id`), - KEY `idx_user_id` (`user_id`), - KEY `idx_refresh_token` (`refresh_token`(64)) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户会话表'; - --- ############################################################ --- 2. Provider 模块 --- ############################################################ - --- 模型供应商表 -CREATE TABLE IF NOT EXISTS `provider` ( - `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID', - `workspace_id` BIGINT UNSIGNED NOT NULL DEFAULT 1 COMMENT '工作空间 ID', - `name` VARCHAR(100) NOT NULL COMMENT '供应商名称', - `type` VARCHAR(50) NOT NULL COMMENT '类型:OPENAI/ANTHROPIC/OLLAMA/OPENAI_COMPATIBLE', - `base_url` VARCHAR(500) NOT NULL COMMENT 'API 基础地址', - `auth_config` JSON NOT NULL COMMENT '鉴权配置(JSON)', - `enabled` TINYINT NOT NULL DEFAULT 1 COMMENT '启用状态:0=禁用,1=启用', - - `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', - `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', - `deleted` TINYINT NOT NULL DEFAULT 0 COMMENT '删除标识:0=正常,1=删除', - `creator_id` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '创建人ID', - - PRIMARY KEY (`id`), - UNIQUE KEY `uk_name_workspace_deleted` (`name`, `workspace_id`, `deleted`), - KEY `idx_type_enabled` (`type`, `enabled`), - KEY `idx_deleted` (`deleted`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='模型供应商表'; - --- 模型配置表 -CREATE TABLE IF NOT EXISTS `model_config` ( - `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID', - `provider_id` BIGINT UNSIGNED NOT NULL COMMENT '所属供应商ID', - `name` VARCHAR(100) NOT NULL COMMENT '展示名,如 GPT-4o', - `model_id` VARCHAR(100) NOT NULL COMMENT '调用时传给 API 的值', - `model_category` TINYINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '模型主类别:0=CHAT, 1=EMBEDDING, 2=RERANK, 3=MULTIMODAL', - `context_size` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '上下文窗口大小(token 数)', - `extra_params` JSON NOT NULL COMMENT '模型级别扩展参数(JSON)', - `enabled` TINYINT NOT NULL DEFAULT 1 COMMENT '启用状态:0=禁用,1=启用', - - `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', - `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', - `deleted` TINYINT NOT NULL DEFAULT 0 COMMENT '删除标识:0=正常,1=删除', - `creator_id` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '创建人ID', - `workspace_id` BIGINT UNSIGNED NOT NULL DEFAULT 1 COMMENT '工作空间 ID', - - PRIMARY KEY (`id`), - KEY `idx_provider_id` (`provider_id`), - KEY `idx_model_id` (`model_id`), - KEY `idx_model_category` (`model_category`), - KEY `idx_enabled_deleted` (`enabled`, `deleted`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='模型配置表'; - --- 供应商健康状态表 -CREATE TABLE IF NOT EXISTS `provider_health` ( - `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID', - `provider_id` BIGINT UNSIGNED NOT NULL COMMENT '供应商ID(唯一索引)', - `status` VARCHAR(20) NOT NULL DEFAULT 'UNKNOWN' COMMENT '状态:UP/DOWN/DEGRADED/UNKNOWN', - `last_check_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '最后探测时间', - `last_success_at` DATETIME DEFAULT NULL COMMENT '最后成功时间', - `fail_count` INT NOT NULL DEFAULT 0 COMMENT '连续失败次数', - `latency_ms` INT DEFAULT NULL COMMENT '最近一次延迟(毫秒)', - `error_message` VARCHAR(500) DEFAULT NULL COMMENT '最近失败原因', - - `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', - `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', - `deleted` TINYINT NOT NULL DEFAULT 0 COMMENT '删除标识:0=正常,1=删除', - `creator_id` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '创建人ID', - - PRIMARY KEY (`id`), - UNIQUE KEY `uk_provider_id` (`provider_id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='供应商健康状态表'; - --- ############################################################ --- 3. Agent 模块 --- ############################################################ - --- Agent 配置表 -CREATE TABLE IF NOT EXISTS `ai_agent` ( - `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID', - `workspace_id` BIGINT UNSIGNED NOT NULL DEFAULT 1 COMMENT '工作空间 ID', - `name` VARCHAR(100) NOT NULL COMMENT 'Agent名称', - `description` VARCHAR(500) DEFAULT NULL COMMENT '描述', - `avatar` VARCHAR(500) DEFAULT NULL COMMENT '头像URL', - - -- 模型配置 - `default_provider_id` BIGINT UNSIGNED NOT NULL COMMENT '默认供应商ID', - `default_model` VARCHAR(100) NOT NULL COMMENT '默认模型名称', - - -- 提示词配置 - `system_prompt` TEXT NOT NULL COMMENT '系统提示词', - `user_message_prefix` VARCHAR(1000) DEFAULT NULL COMMENT '用户消息前缀', - `welcome_message` VARCHAR(500) DEFAULT NULL COMMENT '欢迎语', - - -- 模型参数 - `temperature` DECIMAL(3,2) NOT NULL DEFAULT 0.70 COMMENT '温度:0.00-2.00', - `max_tokens` INT UNSIGNED NOT NULL DEFAULT 2000 COMMENT '最大生成tokens', - `top_p` DECIMAL(3,2) NOT NULL DEFAULT 1.00 COMMENT 'Top-p采样', - `frequency_penalty` DECIMAL(3,2) NOT NULL DEFAULT 0.00 COMMENT '频率惩罚:-2 到 2', - `presence_penalty` DECIMAL(3,2) NOT NULL DEFAULT 0.00 COMMENT '存在惩罚:-2 到 2', - - -- 对话配置 - `max_history_rounds` INT UNSIGNED NOT NULL DEFAULT 10 COMMENT '最大历史轮数', - `stream_enabled` TINYINT NOT NULL DEFAULT 1 COMMENT '是否启用流式输出:0=否,1=是', - - -- 工作流绑定 - `workflow_id` BIGINT UNSIGNED DEFAULT NULL COMMENT '绑定工作流ID,不为空时走工作流编排', - - -- RAG 配置 - `rag_enabled` TINYINT NOT NULL DEFAULT 0 COMMENT '是否启用RAG:0=禁用,1=启用', - `rag_top_k` INT NOT NULL DEFAULT 5 COMMENT 'RAG检索返回的片段数量', - `rag_strategy` VARCHAR(20) NOT NULL DEFAULT 'hybrid' COMMENT '检索策略:vector/keyword/hybrid', - - -- 扩展配置 - `agent_config` JSON DEFAULT NULL COMMENT '扩展配置:{"fallbackModels":[...],"toolsEnabled":false}', - `enabled` TINYINT NOT NULL DEFAULT 1 COMMENT '启用状态:0=禁用,1=启用', - - `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', - `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', - `deleted` TINYINT NOT NULL DEFAULT 0 COMMENT '删除标识:0=正常,1=删除', - `creator_id` BIGINT UNSIGNED DEFAULT NULL COMMENT '创建人ID', - - PRIMARY KEY (`id`), - UNIQUE KEY `uk_name_workspace_deleted` (`name`, `workspace_id`, `deleted`), - KEY `idx_default_provider_id` (`default_provider_id`), - KEY `idx_enabled_deleted` (`enabled`, `deleted`), - KEY `idx_created_at` (`created_at`), - KEY `idx_workspace_id` (`workspace_id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='Agent配置表'; - --- ############################################################ --- 4. Chat 模块 --- ############################################################ - --- 对话会话表 -CREATE TABLE IF NOT EXISTS `ai_chat_session` ( - `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID', - `workspace_id` BIGINT UNSIGNED NOT NULL DEFAULT 1 COMMENT '工作空间 ID', - `user_id` BIGINT UNSIGNED NOT NULL COMMENT '用户ID', - `agent_id` BIGINT UNSIGNED DEFAULT NULL COMMENT '使用的Agent ID', - `title` VARCHAR(200) NOT NULL COMMENT '对话标题', - `status` TINYINT NOT NULL DEFAULT 1 COMMENT '状态:0=已归档,1=进行中', - `workflow_id` BIGINT UNSIGNED DEFAULT NULL COMMENT '关联工作流ID', - - `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', - `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', - `deleted` TINYINT NOT NULL DEFAULT 0 COMMENT '删除标识:0=正常,1=删除', - `creator_id` BIGINT UNSIGNED NOT NULL COMMENT '创建人ID', - - PRIMARY KEY (`id`), - KEY `idx_user_status_updated_id` (`user_id`, `status`, `updated_at`, `id`) COMMENT '用户对话查询索引(支持按更新时间排序)', - KEY `idx_agent_updated_id` (`agent_id`, `updated_at`, `id`) COMMENT 'Agent 对话查询索引(支持按更新时间排序)', - KEY `idx_created_at` (`created_at`), - KEY `idx_workspace_id` (`workspace_id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='对话会话表'; - --- 聊天消息表(大表,使用游标分页) -CREATE TABLE IF NOT EXISTS `ai_chat_message` ( - `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID', - `session_id` BIGINT UNSIGNED NOT NULL COMMENT '所属会话ID', - `workspace_id` BIGINT UNSIGNED NOT NULL COMMENT '所属工作空间ID', - `role` VARCHAR(20) NOT NULL COMMENT '角色:user/assistant/system', - `content` TEXT NOT NULL COMMENT '消息内容', - `token_count` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT 'token数', - `model_id` BIGINT UNSIGNED DEFAULT NULL COMMENT '使用的模型ID', - `metadata` JSON DEFAULT NULL COMMENT '元数据:{"model":"gpt-4","latency_ms":1234}', - - `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', - `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', - `deleted` TINYINT NOT NULL DEFAULT 0 COMMENT '删除标识:0=正常,1=删除', - `creator_id` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '创建人ID', - - PRIMARY KEY (`id`), - KEY `idx_session_id_id` (`session_id`, `id`), - KEY `idx_created_at` (`created_at`), - KEY `idx_session_id_id_role_time` (`session_id`, `id`, `role`, `created_at`) COMMENT '会话消息查询覆盖索引', - KEY `idx_created_at_id` (`created_at`, `id`) COMMENT '时间范围查询索引', - KEY `idx_session_workspace_id` (`session_id`, `workspace_id`, `id`) COMMENT '会话内 workspace 隔离查询' -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='聊天消息表'; - --- ############################################################ --- 5. Knowledge 模块 --- ############################################################ - --- 知识库表 -CREATE TABLE IF NOT EXISTS `knowledge_base` ( - `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID', - `workspace_id` BIGINT UNSIGNED NOT NULL DEFAULT 1 COMMENT '工作空间 ID', - `name` VARCHAR(100) NOT NULL COMMENT '知识库名称', - `description` VARCHAR(500) DEFAULT NULL COMMENT '描述', - `embedding_model` VARCHAR(100) NOT NULL DEFAULT '' COMMENT '嵌入模型名称', - `embedding_model_id` BIGINT UNSIGNED DEFAULT NULL COMMENT '嵌入模型 FK -> model_config.id,NULL 时降级到全局配置', - `vector_dimension` INT NOT NULL DEFAULT 0 COMMENT '向量维度(由所选模型决定)', - `chunk_size` INT NOT NULL DEFAULT 0 COMMENT '分块大小(字符数)', - `chunk_overlap` INT NOT NULL DEFAULT 0 COMMENT '分块重叠(字符数)', - `document_count` INT NOT NULL DEFAULT 0 COMMENT '文档数', - `chunk_count` INT NOT NULL DEFAULT 0 COMMENT '分块数', - `retrieval_count` INT NOT NULL DEFAULT 0 COMMENT '检索次数', - `enabled` TINYINT NOT NULL DEFAULT 1 COMMENT '启用状态:0=禁用,1=启用', - - `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', - `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', - `deleted` TINYINT NOT NULL DEFAULT 0 COMMENT '删除标识:0=正常,1=删除', - `creator_id` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '创建人ID', - - PRIMARY KEY (`id`), - UNIQUE KEY `uk_name_workspace_deleted` (`name`, `workspace_id`, `deleted`), - KEY `idx_enabled` (`enabled`), - KEY `idx_deleted` (`deleted`), - KEY `idx_workspace_id` (`workspace_id`), - KEY `idx_embedding_model_id` (`embedding_model_id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='知识库表'; - --- 文档表 -CREATE TABLE IF NOT EXISTS `document` ( - `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID', - `workspace_id` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '工作空间ID', - `knowledge_id` BIGINT UNSIGNED NOT NULL COMMENT '所属知识库ID', - `file_name` VARCHAR(255) NOT NULL COMMENT '存储文件名', - `original_name` VARCHAR(255) NOT NULL COMMENT '原始文件名', - `file_type` VARCHAR(20) NOT NULL COMMENT '文件类型:pdf/docx/txt/md', - `file_size` BIGINT UNSIGNED NOT NULL COMMENT '文件大小(字节)', - `file_path` VARCHAR(500) NOT NULL COMMENT '文件存储路径', - `char_count` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '字符数', - `chunk_count` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '分块数', - `process_status` TINYINT NOT NULL DEFAULT 0 COMMENT '处理状态:0=待处理,1=处理中,2=已完成,3=失败', - `error_message` VARCHAR(500) DEFAULT NULL COMMENT '失败原因', - `enabled` TINYINT NOT NULL DEFAULT 1 COMMENT '启用状态:0=禁用,1=启用', - - `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', - `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', - `deleted` TINYINT NOT NULL DEFAULT 0 COMMENT '删除标识:0=正常,1=删除', - `creator_id` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '创建人ID', - - PRIMARY KEY (`id`), - KEY `idx_workspace` (`workspace_id`), - KEY `idx_knowledge` (`knowledge_id`), - KEY `idx_status` (`process_status`), - KEY `idx_deleted` (`deleted`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='文档表'; - --- Agent 与知识库关联表 -CREATE TABLE IF NOT EXISTS `agent_knowledge` ( - `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID', - `agent_id` BIGINT UNSIGNED NOT NULL COMMENT 'Agent ID', - `knowledge_id` BIGINT UNSIGNED NOT NULL COMMENT '知识库 ID', - - `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', - `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', - `deleted` TINYINT NOT NULL DEFAULT 0 COMMENT '删除标识:0=正常,1=删除', - `creator_id` BIGINT UNSIGNED DEFAULT NULL COMMENT '创建人ID', - - PRIMARY KEY (`id`), - UNIQUE KEY `uk_agent_knowledge` (`agent_id`, `knowledge_id`), - KEY `idx_agent_id` (`agent_id`), - KEY `idx_knowledge_id` (`knowledge_id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='Agent与知识库关联表'; - --- ############################################################ --- 6. MCP 模块 --- ############################################################ - --- MCP 服务器表 -CREATE TABLE IF NOT EXISTS `mcp_server` ( - `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID', - `name` VARCHAR(100) NOT NULL COMMENT '服务器名称', - `description` VARCHAR(500) NULL COMMENT '服务器描述', - `endpoint` VARCHAR(500) NOT NULL COMMENT 'MCP Server URL (Streamable HTTP)', - `enabled` TINYINT NOT NULL DEFAULT 1 COMMENT '启用状态:0=禁用,1=启用', - - `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', - `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', - `deleted` TINYINT NOT NULL DEFAULT 0 COMMENT '逻辑删除:0=正常,1=删除', - `creator_id` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '创建人ID', - `workspace_id` BIGINT UNSIGNED NOT NULL DEFAULT 1 COMMENT '工作空间 ID', - - PRIMARY KEY (`id`), - UNIQUE KEY `uk_name_workspace_deleted` (`name`, `workspace_id`, `deleted`), - KEY `idx_deleted` (`deleted`), - KEY `idx_workspace_id` (`workspace_id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='MCP 服务器'; - --- MCP 工具表 -CREATE TABLE IF NOT EXISTS `mcp_tool` ( - `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID', - `server_id` BIGINT UNSIGNED NOT NULL COMMENT '所属 MCP Server ID', - `name` VARCHAR(200) NOT NULL COMMENT '工具名称', - `description` TEXT DEFAULT NULL COMMENT '工具描述', - `input_schema` JSON DEFAULT NULL COMMENT '输入参数 JSON Schema', - - `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', - `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', - `deleted` TINYINT NOT NULL DEFAULT 0 COMMENT '逻辑删除:0=正常,1=删除', - `creator_id` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '创建人ID', - `workspace_id` BIGINT UNSIGNED NOT NULL DEFAULT 1 COMMENT '工作空间 ID', - - PRIMARY KEY (`id`), - KEY `idx_server_id` (`server_id`), - KEY `idx_workspace_id` (`workspace_id`), - KEY `idx_name_workspace` (`name`, `workspace_id`), - KEY `idx_deleted` (`deleted`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='MCP 工具'; - --- Agent 绑定的 MCP 工具表 -CREATE TABLE IF NOT EXISTS `agent_mcp_tool` ( - `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID', - `agent_id` BIGINT UNSIGNED NOT NULL COMMENT 'Agent ID', - `tool_id` BIGINT UNSIGNED NOT NULL COMMENT 'MCP 工具 ID', - - `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', - `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', - `deleted` TINYINT NOT NULL DEFAULT 0 COMMENT '删除标识:0=正常,1=删除', - `creator_id` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '创建人ID', - `workspace_id` BIGINT UNSIGNED NOT NULL DEFAULT 1 COMMENT '工作空间 ID', - - PRIMARY KEY (`id`), - UNIQUE KEY `uk_agent_tool_workspace` (`agent_id`, `tool_id`, `workspace_id`), - KEY `idx_agent_id` (`agent_id`), - KEY `idx_tool_id` (`tool_id`), - KEY `idx_workspace_id` (`workspace_id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='Agent 绑定的 MCP 工具'; - --- ############################################################ --- 7. Workflow 模块 --- ############################################################ - --- 工作流主表 -CREATE TABLE IF NOT EXISTS `ai_workflow` ( - `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键', - `workspace_id` BIGINT UNSIGNED NOT NULL DEFAULT 1 COMMENT '工作空间 ID', - `name` VARCHAR(100) NOT NULL COMMENT '工作流名称', - `description` VARCHAR(500) DEFAULT NULL COMMENT '工作流描述', - `status` TINYINT NOT NULL DEFAULT 0 COMMENT '状态:0=草稿,1=已发布,2=已禁用', - `version` INT NOT NULL DEFAULT 1 COMMENT '版本号,每次发布 +1', - `variables` JSON DEFAULT NULL COMMENT '全局变量定义:[{key,type,required,defaultVal}]', - - `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', - `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', - `deleted` TINYINT NOT NULL DEFAULT 0 COMMENT '逻辑删除:0=正常,1=已删除', - `creator_id` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '创建人ID', - - PRIMARY KEY (`id`), - UNIQUE KEY `uk_name_workspace_deleted` (`name`, `workspace_id`, `deleted`), - KEY `idx_workspace_id` (`workspace_id`), - KEY `idx_status` (`status`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='工作流主表'; - --- 工作流节点表 -CREATE TABLE IF NOT EXISTS `ai_workflow_node` ( - `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键', - `workflow_id` BIGINT UNSIGNED NOT NULL COMMENT '所属工作流 ID', - `node_key` VARCHAR(50) NOT NULL COMMENT '节点标识(工作流内唯一)', - `type` VARCHAR(30) NOT NULL COMMENT '节点类型:start/end/llm/api_call/condition/code', - `label` VARCHAR(100) DEFAULT NULL COMMENT '节点显示名称', - `position_x` DOUBLE DEFAULT NULL COMMENT '画布 X 坐标', - `position_y` DOUBLE DEFAULT NULL COMMENT '画布 Y 坐标', - `config` JSON DEFAULT NULL COMMENT '节点配置(不同 type 结构不同)', - - `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', - `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', - `deleted` TINYINT NOT NULL DEFAULT 0 COMMENT '逻辑删除:0=正常,1=已删除', - `creator_id` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '创建人ID', - - PRIMARY KEY (`id`), - KEY `idx_workflow` (`workflow_id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='工作流节点表'; - --- 工作流连线表 -CREATE TABLE IF NOT EXISTS `ai_workflow_edge` ( - `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键', - `workflow_id` BIGINT UNSIGNED NOT NULL COMMENT '所属工作流 ID', - `source_node_id` BIGINT UNSIGNED NOT NULL COMMENT '源节点 ID', - `target_node_id` BIGINT UNSIGNED NOT NULL COMMENT '目标节点 ID', - `source_handle` VARCHAR(50) NOT NULL DEFAULT 'default' COMMENT '源节点出口:default/true/false/分支名', - `label` VARCHAR(50) DEFAULT NULL COMMENT '连线显示文字', - - `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', - `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', - `deleted` TINYINT NOT NULL DEFAULT 0 COMMENT '逻辑删除:0=正常,1=已删除', - `creator_id` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '创建人ID', - - PRIMARY KEY (`id`), - KEY `idx_workflow` (`workflow_id`), - KEY `idx_source` (`source_node_id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='工作流连线表'; - --- 工作流执行记录表 -CREATE TABLE IF NOT EXISTS `ai_workflow_execution` ( - `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键', - `workflow_id` BIGINT UNSIGNED NOT NULL COMMENT '工作流 ID', - `workflow_version` INT DEFAULT NULL COMMENT '执行时工作流版本快照', - `status` VARCHAR(20) NOT NULL DEFAULT 'running' COMMENT '状态:running/completed/failed/cancelled', - `variables` JSON DEFAULT NULL COMMENT '运行时变量快照', - `current_node_id` BIGINT UNSIGNED DEFAULT NULL COMMENT '当前执行到的节点 ID', - `error_message` TEXT DEFAULT NULL COMMENT '失败原因', - `started_at` DATETIME DEFAULT NULL COMMENT '开始执行时间', - `completed_at` DATETIME DEFAULT NULL COMMENT '执行结束时间', - - `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', - `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', - `deleted` TINYINT NOT NULL DEFAULT 0 COMMENT '逻辑删除:0=正常,1=已删除', - `creator_id` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '创建人ID', - - PRIMARY KEY (`id`), - KEY `idx_workflow` (`workflow_id`), - KEY `idx_status` (`status`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='工作流执行记录表'; - -SET FOREIGN_KEY_CHECKS = 1; - --- ############################################################ --- 8. 开发环境初始数据(仅 dev 环境使用) --- ############################################################ - --- 管理员用户(密码:admin123,BCrypt 加密) -INSERT INTO `ai_user` (`id`, `username`, `email`, `password`, `display_name`, `status`) VALUES -(1, 'admin', 'admin@eify.local', '$2a$10$CfJnrJ65v1Oyt91xEG/tB.DzgQCLk6gX0reH9LpgHgB6boCapKH3C', 'Admin', 1) -ON DUPLICATE KEY UPDATE `password` = VALUES(`password`); - --- 默认工作空间 -INSERT INTO `ai_workspace` (`id`, `name`, `description`) VALUES -(1, 'System Workspace', 'System default workspace') -ON DUPLICATE KEY UPDATE `name` = `name`; - --- 管理员加入默认工作空间 -INSERT INTO `ai_workspace_member` (`workspace_id`, `user_id`, `role`) VALUES -(1, 1, 'owner') -ON DUPLICATE KEY UPDATE `role` = `role`; - --- ############################################################ --- 9. 已有部署迁移 SQL(V2/V3 变更已纳入新部署 DDL) --- ############################################################ --- V1→V2: ai_chat_message 添加 workspace_id 列 + 索引(已纳入上方 DDL) --- V2→V3: mcp_server 添加 uk_name_workspace 唯一约束 + idx_workspace_id(已纳入上方 DDL) --- --- 以下为早期部署可能需要的 ALTER: --- ALTER TABLE `document` ADD COLUMN `workspace_id` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '工作空间ID' AFTER `id`; --- UPDATE `document` d JOIN `knowledge_base` kb ON kb.id = d.knowledge_id SET d.workspace_id = kb.workspace_id WHERE d.workspace_id = 0; --- ALTER TABLE `document` ADD KEY `idx_workspace` (`workspace_id`); - - diff --git a/deploy/sql/init_eify_vector_pgsql.sql b/deploy/sql/init_eify_vector_pgsql.sql deleted file mode 100644 index 07c7727..0000000 --- a/deploy/sql/init_eify_vector_pgsql.sql +++ /dev/null @@ -1,38 +0,0 @@ --- ============================================ --- Eify pgvector 初始化脚本(参考副本) --- 用途:document_chunk 表(向量数据) --- 注意:表结构由 Flyway 自动管理,此文件仅供手动导入备用 --- 迁移文件:eify-app/src/main/resources/db/migration-pg/V1__init_pgvector.sql --- eify-app/src/main/resources/db/migration-pg/V2__flexible_vector_dimension.sql --- ============================================ - --- 启用 pgvector 扩展 -CREATE EXTENSION IF NOT EXISTS vector; - --- 向量分块表 -CREATE TABLE IF NOT EXISTS document_chunk ( - id BIGSERIAL PRIMARY KEY, - workspace_id BIGINT NOT NULL DEFAULT 0, - knowledge_id BIGINT NOT NULL, - document_id BIGINT NOT NULL, - chunk_index INT NOT NULL, - content TEXT NOT NULL, - embedding VECTOR, - chunk_hash CHAR(64) NOT NULL, - enabled SMALLINT NOT NULL DEFAULT 1, - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() -); - --- HNSW 向量索引(余弦距离) -CREATE INDEX IF NOT EXISTS idx_chunk_embedding ON document_chunk - USING hnsw (embedding vector_cosine_ops); - --- 业务索引 -CREATE INDEX IF NOT EXISTS idx_chunk_workspace ON document_chunk(workspace_id); -CREATE INDEX IF NOT EXISTS idx_chunk_knowledge ON document_chunk(knowledge_id); -CREATE INDEX IF NOT EXISTS idx_chunk_document ON document_chunk(document_id); -CREATE INDEX IF NOT EXISTS idx_chunk_hash ON document_chunk(chunk_hash); - -COMMENT ON TABLE document_chunk IS '文档分块向量表,存储切分后的文本及其 Embedding 向量'; -COMMENT ON COLUMN document_chunk.embedding IS '向量嵌入,维度由对应 knowledge_base.vector_dimension 决定'; -COMMENT ON COLUMN document_chunk.chunk_hash IS '内容 SHA-256 哈希,用于去重和增量更新'; diff --git a/docs/ADRs/ADR-0004-dual-storage-mysql-pgvector.md b/docs/ADRs/ADR-0004-dual-storage-mysql-pgvector.md index 76a1857..ac2f1cf 100644 --- a/docs/ADRs/ADR-0004-dual-storage-mysql-pgvector.md +++ b/docs/ADRs/ADR-0004-dual-storage-mysql-pgvector.md @@ -2,7 +2,11 @@ `ADR-0004 dual-storage-mysql-pgvector` # Status -Accepted +Superseded + +> 本 ADR 已被 [ADR-0013 单 PostgreSQL 17 数据库统一存储](./ADR-0013-single-postgresql17-database.md) 替代(2026-06)。 +> MySQL + pgvector 双存储架构已迁移为单 PostgreSQL 17 实例承载全部业务表与向量表。 +> 详见 [MySQL → PG17 迁移设计](../specs/2026-06-05-mysql-to-pg17-migration-design.md)。 # Date 2025-Q2 diff --git a/docs/ADRs/ADR-0013-single-postgresql17-database.md b/docs/ADRs/ADR-0013-single-postgresql17-database.md new file mode 100644 index 0000000..c3881b5 --- /dev/null +++ b/docs/ADRs/ADR-0013-single-postgresql17-database.md @@ -0,0 +1,106 @@ +# 单 PostgreSQL 17 数据库统一存储 +`ADR-0013 single-postgresql17-database` + +# Status +Accepted + +# Date +2026-06-05 + +# Owner +Eify 开发团队(1 人) + +# Deciders +Eify 开发团队 + +# Context + +项目最初采用 MySQL 8.0(业务数据)+ PostgreSQL 16 pgvector(向量数据)双存储架构([ADR-0004](./ADR-0004-dual-storage-mysql-pgvector.md))。 +随着 PostgreSQL 成熟度和 pgvector 生态的发展,双存储架构的运维成本逐渐超过其收益。 + +### Decision drivers +- 双数据库运维复杂:两个数据库需分别维护、备份、迁移 +- 双数据源代码冗余:Spring Boot 需配置两套 DataSource / Flyway / 连接池 +- 事务无法跨库:`document`(MySQL)与 `document_chunk`(pgvector)无法在同一事务中操作 +- pgvector 0.8+ 成熟度提升:HNSW 索引、IVFFlat、`halfvec` 等特性稳定 +- PostgreSQL 17 性能增强:增量备份、并行 VACUUM、逻辑复制改进 +- 团队 1 人,基础设施越简单越好 + +# Considered Options +* **方案 A:保持 MySQL + pgvector 双存储** — 维持现状,不解决运维复杂度和跨库事务问题。被拒绝:运维负担重,代码路径复杂。 +* **方案 B:全部迁入 PostgreSQL 17(单库)** — 业务表 + 向量表统一存储在 PostgreSQL 17,干净切换(丢弃旧数据,DDL 表达最终态)。被采纳。 +* **方案 C:引入分布式数据库(TiDB/CockroachDB)** — 功能强大但对 MVP 过度,运维学习曲线陡峭,被拒绝。 + +# Decision + +**选择方案 B:单 PostgreSQL 17 数据库统一存储全部数据。** + +- 23 张业务表 + `document_chunk` 向量表全部落在同一 PostgreSQL 17 database +- 单 HikariCP 连接池、单 Flyway 实例 +- MyBatis-Plus 业务 CRUD 与 `JdbcTemplate` 向量读写共用主数据源 +- Docker Compose 中仅保留 `pgvector/pgvector:pg17` 一个数据库容器 + +## Consequences + +### 优势 +- **运维简化**:从维护 MySQL + PostgreSQL 两个数据库降为仅需 PostgreSQL +- **代码简化**:删除 `PgVectorConfig`、`PgFlywayConfig`、`@Lazy` 延迟初始化等双源样板 +- **事务一致性**:元数据与向量数据可在同一事务中操作(如文档删除时可事务性清理分块) +- **连接池统一**:Druid 替换为 HikariCP,配置更简洁 +- **方言统一**:清理 4 处 MySQL 方言注解 SQL(ON DUPLICATE KEY、JSON_EXTRACT),统一为 PG 原生语法 + +### 权衡 +- 失去 MyBatis-Plus 对 MySQL 的深度适配(如 `AUTO_INCREMENT` → `IDENTITY`,`TINYINT` → `SMALLINT`) +- 测试需从 H2 MySQL 模式切换为 H2 PostgreSQL 模式,JSONB/upsert/向量盲区需 Testcontainers PG17 补充 +- `document_chunk` 现需显式包含 `workspace_id`(原通过 `knowledge_id` 间接关联) +- Docker 镜像从 `pgvector/pgvector:pg16` 升级到 `pg16` → `pg17`,需重新拉取 + +# Details + +## 迁移策略:干净切换 + +不进行数据迁移,直接丢弃旧 MySQL 数据,DDL 表达最终态。理由: +- MVP 阶段,无生产数据需保留 +- 增量 ALTER 迁移(V4~V7)折叠入新 V1,避免逐版翻译 +- Flyway 历史表重置,干净启动 + +## 数据库类型映射 + +| MySQL | PostgreSQL 17 | +|:---|:---| +| `BIGINT UNSIGNED AUTO_INCREMENT` | `BIGINT GENERATED BY DEFAULT AS IDENTITY` | +| `TINYINT` | `SMALLINT` | +| `DECIMAL(3,2)` | `NUMERIC(3,2)` | +| `DATETIME` | `TIMESTAMPTZ` | +| `JSON` | `JSONB` | +| `ENGINE=InnoDB` / `CHARSET=utf8mb4` | 删除(PG 不需要) | +| 内联 `KEY idx (...)` | 独立 `CREATE INDEX IF NOT EXISTS` | + +## 方言 SQL 翻译 + +| MySQL | PostgreSQL 17 | +|:---|:---| +| `ON DUPLICATE KEY UPDATE` | `ON CONFLICT (...) DO UPDATE SET` | +| `JSON_EXTRACT(config, '$.providerId')` | `(config->>'providerId')::bigint` | + +## 配置变更 + +- 数据源:`org.postgresql.Driver` + HikariCP(原 Druid + MySQL Connector/J) +- Flyway:原双实例(MySQL + PG)合并为单一 Flyway,迁移文件仅 `V1__init.sql` +- 环境变量:`MYSQL_*` + `PGVECTOR_*` → `PG_URL` + `PG_USERNAME` + `PG_PASSWORD` +- CI:GitHub Actions 的 MySQL service 容器替换为 `pgvector/pgvector:pg17` +- K8s:ConfigMap/Secret 中 MySQL 字段替换为 PG 字段 + +## 测试策略 + +| 层级 | 方案 | 覆盖 | +|:---|:---|:---| +| 单元测试 | H2 PostgreSQL 模式 | MyBatis-Plus CRUD、Service 逻辑 | +| 集成测试 | Testcontainers `pgvector/pgvector:pg17` | JSONB `->>::bigint`、`ON CONFLICT` upsert、`<=>` 向量检索、IDENTITY 序列回填 | + +## 参考 +- [ADR-0004 MySQL + pgvector 双存储](./ADR-0004-dual-storage-mysql-pgvector.md) — 被本 ADR 替代 +- [MySQL → PG17 迁移设计](../specs/2026-06-05-mysql-to-pg17-migration-design.md) +- [MySQL → PG17 迁移实现计划](../plans/2026-06-05-mysql-to-pg17-migration.md) +- [DATABASE.md](../guides/DATABASE.md) — 数据库设计规范 +- [DEPLOYMENT.md](../DEPLOYMENT.md) — 部署与 CI/CD diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 3e9eee1..d68daee 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -5,7 +5,7 @@ - **多工作空间多租户**:JWT 无状态认证 + ThreadLocal 上下文传递,所有业务数据按 `workspace_id` 隔离 - SSE 长连接支持流式输出 - Redis 缓存 Agent 配置、对话上下文 -- MySQL 持久化用户、Agent、对话、消息数据 +- PostgreSQL 17 持久化全部业务数据与向量数据 - Nginx 反向代理,优化 SSE 传输 ### 工作空间多租户架构 @@ -163,9 +163,9 @@ eify-auth/ | eify-agent | MyBatis-Plus、Redis(Agent 配置缓存) | | eify-chat | MyBatis-Plus、Redis(对话上下文)、SSE (SseEmitter) | | eify-mcp | MCP SDK (`io.modelcontextprotocol.sdk:mcp:1.1.1`) | -| eify-knowledge | pgvector、PostgreSQL、PDFBox 2.0.27、Apache POI 5.2.3、OkHttp(嵌入 API 调用)、Caffeine(双存储设计见 [ADR](ADRs/ADR-0004-dual-storage-mysql-pgvector.md))| +| eify-knowledge | pgvector、PostgreSQL、PDFBox 2.0.27、Apache POI 5.2.3、OkHttp(嵌入 API 调用)、Caffeine(单库设计见 [ADR](ADRs/ADR-0013-single-postgresql17-database.md))| | eify-workflow | GraalVM Polyglot 24.1.0(JS 引擎)、MyBatis-Plus | -| eify-app | Spring Boot 4.0.6、JwtAuthFilter(iss/aud 校验 + HttpOnly Cookie)、Flyway(MySQL + pgvector)、所有 8 个业务模块 | +| eify-app | Spring Boot 4.0.6、JwtAuthFilter(iss/aud 校验 + HttpOnly Cookie)、Flyway(PostgreSQL 17 单库)、所有 8 个业务模块 | --- diff --git a/docs/DEPLOYMENT.md b/docs/DEPLOYMENT.md index c94bdd3..04d796e 100644 --- a/docs/DEPLOYMENT.md +++ b/docs/DEPLOYMENT.md @@ -25,7 +25,7 @@ Nginx Ingress (K8s) │ ┌─────────────────┼─────────────────┐ ▼ ▼ ▼ -MySQL (业务数据) Redis (缓存) PostgreSQL + pgvector (向量) +PostgreSQL 17 (业务+向量) Redis (缓存) ``` 应用是模块化单体:`eify-app` 包含 agent / auth / chat / provider / knowledge / workflow / mcp 所有模块。 @@ -59,7 +59,7 @@ Profile 差异(测试 vs 生产): | 维度 | dev | test | staging | prod | |:---|:---|:---|:---|:---| | DB 连接池 | 30 | 20 | 40 | 50 | -| Druid 监控 | 开启 | 开启+认证 | 开启+认证 | 关闭 | +| HikariCP 连接池 | 30 | 20 | 40 | 50 | | Flyway repair | 开启 | 开启 | 关闭 | 关闭 | | 日志采样 | 100% | 100% | 50% | 1%(SQL)/ 正常(MSG) | | 凭据来源 | `.env` 默认值 | K8s Secret | K8s Secret | K8s Secret | @@ -90,7 +90,7 @@ npx vitest run # 单元测试 ### 全栈开发环境 ```bash -# 启动(MySQL + Redis + pgvector + 后端 + 前端) +# 启动(PostgreSQL 17 + Redis + 后端 + 前端) docker-compose -f deploy/infra/deploy/docker-compose.yml up -d # 查看日志 @@ -110,7 +110,7 @@ docker-compose -f deploy/infra/deploy/docker-compose.yml down docker-compose -f deploy/infra/deploy/docker-compose-logging.yml up -d ``` -> 数据库迁移由 Flyway 在应用启动时自动执行,无需手动导入 SQL。MySQL 和 pgvector 各有独立的 Flyway 实例。 +> 数据库迁移由 Flyway 在应用启动时自动执行,无需手动导入 SQL。所有表(业务 + 向量)由单 Flyway 实例管理。 --- @@ -120,7 +120,7 @@ docker-compose -f deploy/infra/deploy/docker-compose-logging.yml up -d - **平台**:华为云 CCE Turbo(containerd 运行时) - **镜像仓库**:华为云 SWR(`swr.cn-south-1.myhuaweicloud.com/eify`) -- **外部服务**:MySQL / Redis 运行在集群外 VM,通过 ConfigMap 指定地址 +- **外部服务**:PostgreSQL / Redis 运行在集群外 VM,通过 ConfigMap 指定地址 ### 部署清单 @@ -144,12 +144,11 @@ kubectl apply -f deploy/k8s/ # 2. 创建 Secret(替换为实际值) kubectl create secret generic eify-secret -n eify \ - --from-literal=MYSQL_USERNAME=root \ - --from-literal=MYSQL_PASSWORD='...' \ + --from-literal=PG_USERNAME=postgres \ + --from-literal=PG_PASSWORD='...' \ --from-literal=REDIS_PASSWORD='...' \ --from-literal=JWT_SECRET='...' \ --from-literal=CRYPTO_KEK='...' \ - --from-literal=PGVECTOR_PASSWORD='...' \ --from-literal=EMBEDDING_API_KEY='...' # 3. 验证 @@ -258,7 +257,7 @@ Git Push (分支或 Tag) **手动部署**:设置 `ENVIRONMENT` 参数为非 `auto` 值可强制指定部署目标。 -**凭据管理**:通过 Jenkins `withCredentials` 注入 SWR 登录凭证和 K8s Secret 值(MySQL/Redis/JWT/CRYPTO/PGVECTOR/Embedding 共 7 项),不在 Jenkinsfile 中硬编码。 +**凭据管理**:通过 Jenkins `withCredentials` 注入 SWR 登录凭证和 K8s Secret 值(PG/Redis/JWT/CRYPTO/Embedding 共 5 项),不在 Jenkinsfile 中硬编码。 > **构建环境**:Jenkins 运行在 Docker VM 上,通过 `docker.sock` 挂载使用宿主机 Docker daemon。若 Jenkins 运行在 containerd 环境(CCE 集群),需改用 Kaniko 构建镜像。 @@ -353,8 +352,8 @@ kubectl logs deployment/eify-backend -n eify | grep -i flyway |:---|:---|:---| | `JWT_SECRET` | JWT 签名密钥(≥32 字节) | prod 必须 | | `CRYPTO_KEK` | API Key 加密密钥(AES-256-GCM) | prod 必须 | -| `MYSQL_HOST` / `MYSQL_PORT` / `MYSQL_PASSWORD` | MySQL 连接 | 是 | +| `PG_URL` | PostgreSQL 17 JDBC URL | 是 | +| `PG_USERNAME` / `PG_PASSWORD` | PostgreSQL 认证 | 是 | | `REDIS_HOST` / `REDIS_PORT` / `REDIS_PASSWORD` | Redis 连接 | 是 | -| `PGVECTOR_HOST` / `PGVECTOR_PORT` / `PGVECTOR_PASSWORD` | PostgreSQL 连接 | 是 | | `EMBEDDING_API_KEY` | Embedding API 密钥 | 是 | | `CLICKHOUSE_HOST` / `CLICKHOUSE_PASSWORD` | 日志存储 | 否(可选) | diff --git a/docs/README.md b/docs/README.md index c783e3f..01aad04 100644 --- a/docs/README.md +++ b/docs/README.md @@ -25,7 +25,7 @@ | 层级 | 技术选型 | |:---|:---| | **后端** | Spring Boot 4.0.6 + MyBatis-Plus 3.5.15 | -| **数据库** | MySQL 8.0 + Redis 7 | +| **数据库** | PostgreSQL 17 + Redis 7 | | **前端** | Vue 3 + TypeScript + Element Plus | | **容器化** | Docker + Docker Compose | | **监控** | Micrometer Tracing + Brave + ClickHouse + Vector | diff --git a/docs/guides/DATABASE.md b/docs/guides/DATABASE.md index 615e7f5..dd0f564 100644 --- a/docs/guides/DATABASE.md +++ b/docs/guides/DATABASE.md @@ -2,7 +2,7 @@ ## 目录 -- [MySQL 核心设计](#mysql-核心设计) +- [PostgreSQL 核心设计](#postgresql-核心设计) - [核心数据表](#核心数据表) - [缓存策略](#缓存策略) - [表设计规范](#表设计规范) @@ -22,11 +22,11 @@ --- -# MySQL 核心设计 +# PostgreSQL 核心设计 ## 核心数据表 -> 完整 DDL 见 Flyway 迁移文件(`V1__init.sql`、`V4__model_category_and_embedding_model_id.sql`、`V5__name_workspace_deleted_unique.sql`),由 Flyway 自动执行 +> 完整 DDL 见 Flyway 迁移文件 `V1__init.sql`(PostgreSQL 17,已折叠所有历史增量变更),由 Flyway 自动执行 | 表名 | 用途 | | :--- | :--- | @@ -43,7 +43,7 @@ | `ai_chat_message` | 聊天消息表(大表,游标分页) | | `knowledge_base` | 知识库表 | | `document` | 文档表 | -| `document_chunk` | 文档分块 + Embedding 向量表(**PostgreSQL pgvector**,非 MySQL) | +| `document_chunk` | 文档分块 + Embedding 向量表(**pgvector**,与业务表同库) | | `agent_knowledge` | Agent 与知识库关联表 | | `mcp_server` | MCP 服务器表 | | `mcp_tool` | MCP 工具表 | @@ -53,7 +53,7 @@ | `ai_workflow_edge` | 工作流连线表 | | `ai_workflow_execution` | 工作流执行记录表 | -> **双存储架构**:`document_chunk` 表使用 PostgreSQL pgvector 存储向量数据(HNSW 索引),其他表使用 MySQL。详细设计见 [双存储架构设计决策](ADRs/ADR-0004-dual-storage-mysql-pgvector.md)。 +> **单库架构**:业务表与向量表 `document_chunk`(含 HNSW 向量索引,pgvector 扩展)现已合并到同一个 PostgreSQL 17 库中,统一事务、备份与运维。历史上曾采用 MySQL + pgvector 双存储(见 [ADR-0004 双存储架构设计决策](ADRs/ADR-0004-dual-storage-mysql-pgvector.md)),现状已演进为单库 PG17。 ## 缓存策略 @@ -70,16 +70,17 @@ - 使用小写字母,单词间用下划线分隔 - 格式:`{业务模块}_{实体}` - 示例:`ai_agent`、`ai_conversation`、`sys_user` -- 禁止使用 MySQL 保留字作为表名 +- 禁止使用 PostgreSQL 保留字作为表名 -### 存储引擎 -- 所有业务表必须使用 InnoDB -- 原因:支持事务、外键、行级锁 +### 存储与事务 +- PostgreSQL 无需像 MySQL 那样声明 `ENGINE=InnoDB` +- 事务(MVCC 多版本并发控制)、行级锁、外键均为内建能力,所有表默认支持 +- 原因:PG 单一存储引擎即提供 ACID 事务、行级锁和外键约束 ### 字符集 -- 字符集:`utf8mb4` -- 排序规则:`utf8mb4_unicode_ci` -- 原因:支持 Emoji、多语言 +- 数据库编码:UTF-8(`CREATE DATABASE eify ENCODING 'UTF8'`) +- 排序规则:通过 `LC_COLLATE` / `LC_CTYPE` 指定(如 `'en_US.UTF-8'`) +- 原因:PG 的 UTF-8 原生支持 Emoji 和多语言,无需 MySQL 的 `utf8mb4` 特殊处理 ## 字段设计规范 @@ -87,13 +88,15 @@ | 类型 | 使用场景 | 示例 | |:---|:---|:---| -| `BIGINT UNSIGNED` | 主键、外键 | `id` | +| `BIGINT`(`GENERATED BY DEFAULT AS IDENTITY` 自增) | 主键、外键 | `id` | | `VARCHAR(N)` | 短字符串 | `name` VARCHAR(100) | | `TEXT` | 长文本 | `content` | -| `DECIMAL(M,D)` | 金额、高精度数值 | `price` DECIMAL(10,2) | -| `DATETIME` | 业务时间 | `created_at` | -| `TINYINT` | 状态、枚举 | `status` TINYINT | -| `JSON` | 配置、元数据 | `config` JSON | +| `NUMERIC(M,D)` | 金额、高精度数值 | `price` NUMERIC(10,2) | +| `TIMESTAMPTZ` | 业务时间 | `created_at` | +| `SMALLINT` | 状态、枚举 | `status` SMALLINT | +| `JSONB` | 配置、元数据 | `config` JSONB | + +> **MySQL → PG 类型映射要点**:PG 无 `UNSIGNED`,主键自增用 `IDENTITY`(`GENERATED BY DEFAULT AS IDENTITY`);无 `TINYINT`,状态/枚举用 `SMALLINT`;`DATETIME` → `TIMESTAMPTZ`(带时区);`JSON` → `JSONB`(二进制存储,可建 GIN 索引);`DECIMAL` → `NUMERIC`。 ### NULL 值处理 - 所有字段必须指定 NULL 或 NOT NULL @@ -105,53 +108,57 @@ - 数值类型默认值:0 - 字符串类型默认值:'' - 枚举类型默认值:明确指定(如 status = 1) -- 时间字段:`CURRENT_TIMESTAMP` 或 `NOW()` +- 时间字段:`NOW()`(PG 通用,等价于 `CURRENT_TIMESTAMP`) ### 字段注释 -- 所有字段必须添加 COMMENT +- PG 使用独立的 `COMMENT ON COLUMN 表名.字段名 IS '...'` 语句,而非 MySQL 的列内 `COMMENT` - 枚举值必须说明含义(如 0=禁用,1=启用) -- JSON 字段必须说明结构 +- JSONB 字段必须说明结构 ## 通用字段约定 ```sql --- 主键:统一使用 id,BIGINT 自增 -id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID', +-- 主键:统一使用 id,BIGINT IDENTITY 自增 +id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, -- 创建时间:所有表必备 -created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', +created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), --- 更新时间:所有表必备,自动更新 -updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', +-- 更新时间:所有表必备(PG 无 ON UPDATE,由 MyBatis-Plus FieldMetaObjectHandler 自动填充) +updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), -- 软删除:所有业务表必备(非日志表) -deleted TINYINT NOT NULL DEFAULT 0 COMMENT '删除标识:0=正常,1=删除', +deleted SMALLINT NOT NULL DEFAULT 0, -- 创建人:业务表必备 -creator_id BIGINT UNSIGNED NOT NULL COMMENT '创建人ID', +creator_id BIGINT NOT NULL, ``` +> **注意**:PG 没有 MySQL 的 `ON UPDATE CURRENT_TIMESTAMP`,`updated_at` 不会在 UPDATE 时自动刷新。本项目由应用层填充——MyBatis-Plus 的 `@TableField(fill = FieldFill.INSERT_UPDATE)` + `FieldMetaObjectHandler` 在写入时统一设置时间戳。字段含义注释通过 `COMMENT ON COLUMN` 表达或在本文档说明。 + ## 索引设计原则 ```sql -- 索引命名规范 -- 普通索引:idx_字段名 -KEY `idx_user_id` (`user_id`) +CREATE INDEX IF NOT EXISTS idx_user_id ON my_table(user_id); --- 唯一索引:uk_字段名 -UNIQUE KEY `uk_name` (`name`) +-- 唯一约束:uk_字段名(PG 约束名 schema 级全局唯一,建议带表名前缀) +ALTER TABLE my_table ADD CONSTRAINT uk_my_table_name UNIQUE (name); -- 联合索引:idx_字段1_字段2 -KEY `idx_user_id_status` (`user_id`, `status`) +CREATE INDEX IF NOT EXISTS idx_user_id_status ON my_table(user_id, status); -- 索引设计原则 -- 1. where/order by/group by 的字段建索引 --- 2. 最左前缀原则 +-- 2. 最左前缀原则(PG B-tree 同样适用) -- 3. 区分度高的字段放左边 --- 4. 覆盖索引优化 +-- 4. 覆盖索引优化(可用 INCLUDE 列,如 CREATE INDEX ... (a) INCLUDE (b)) -- 5. 单表索引数 ≤ 5 ``` +> **PG 约束命名注意**:MySQL 的索引名是**表级**作用域,而 PG 的约束名(唯一约束、外键等)是 **schema 级全局唯一**。因此唯一约束命名必须带表名前缀避免冲突,例如 `uk_provider_name_workspace_deleted`、`uk_mcp_server_name_workspace_deleted`。普通索引(`CREATE INDEX`)的索引名也建议带表名前缀。 + ## 大表预判和应对策略 ``` @@ -204,7 +211,7 @@ LIMIT 20 OFFSET 0; SELECT * FROM ai_chat_message WHERE session_id = 123 ORDER BY created_at DESC -LIMIT 10000, 20; +LIMIT 20 OFFSET 10000; -- ✅ 好:游标分页(推荐) -- 第一页 @@ -248,7 +255,7 @@ LIMIT 21; ```sql -- 游标分页专用索引(覆盖查询 + 排序) -KEY `idx_session_id_id` (`session_id`, `id`) +CREATE INDEX IF NOT EXISTS idx_session_id_id ON ai_chat_message(session_id, id); ``` **索引设计原则**: @@ -262,7 +269,7 @@ KEY `idx_session_id_id` (`session_id`, `id`) | 技术 | 作用 | 核心 SQL | |:---|:---|:---| -| **覆盖索引** | 索引包含所有查询字段,避免回表 | `Extra: Using index` | +| **覆盖索引** | 索引包含所有查询字段,避免回表 | `EXPLAIN` → `Index Only Scan` | | **范围查询** | 利用索引有序性缩小扫描范围 | `WHERE created_at >= ... AND created_at < ...` | | **游标分页** | 用上一页最后 ID 定位,避免深分页 | `WHERE id < lastId ORDER BY id LIMIT N` | @@ -277,7 +284,7 @@ KEY `idx_session_id_id` (`session_id`, `id`) -- 查询:SELECT id, session_id, role, created_at -- WHERE session_id = 123 ORDER BY id DESC -- 索引:(session_id, id, role, created_at) --- 结果:Extra: Using where; Using index(无回表) +-- 结果:EXPLAIN 显示 Index Only Scan(无回表,PG 等价于 MySQL 的 Using index) ``` ### 范围查询规范 @@ -308,7 +315,7 @@ WHERE created_at >= '2024-01-01 00:00:00' - [ ] ORDER BY 字段在索引中 - [ ] 高选择性字段在索引左边 - [ ] 索引字段数量 <= 5 -- [ ] EXPLAIN 验证 Extra 包含 "Using index" +- [ ] EXPLAIN 验证执行计划为 "Index Only Scan"(覆盖索引生效) **查询优化**: - [ ] 大表使用游标分页(避免深分页) @@ -351,56 +358,88 @@ WHERE created_at >= '2024-01-01 00:00:00' ## 数据库配置优化 ```ini -[mysqld] -# 基础配置 -character-set-server=utf8mb4 -collation-server=utf8mb4_unicode_ci - -# InnoDB 配置 -innodb_buffer_pool_size=2G -innodb_log_file_size=512M -innodb_flush_log_at_trx_commit=2 - -# 连接配置 -max_connections=500 - -# 慢查询日志 -slow_query_log=1 -long_query_time=2 +# postgresql.conf 关键调优项 + +# 内存 +shared_buffers = 2GB # 数据/索引缓存,建议物理内存的 25% +effective_cache_size = 6GB # 优化器估算的可用缓存(含 OS page cache),建议 50-75% +work_mem = 16MB # 单次排序/哈希操作内存(注意并发会成倍放大) +maintenance_work_mem = 512MB # VACUUM / CREATE INDEX 等维护操作内存 + +# WAL / 持久化 +wal_compression = on +checkpoint_completion_target = 0.9 + +# 连接 +max_connections = 200 # 高并发建议前置 PgBouncer 连接池 + +# 慢查询日志(替代 MySQL 的 slow_query_log) +log_min_duration_statement = 2000 # 记录执行超过 2000ms 的语句(单位 ms) ``` +> 编码与排序规则在 `CREATE DATABASE ... ENCODING 'UTF8'` 时指定,不在 postgresql.conf 中配置(区别于 MySQL 的 `character-set-server`)。 + ## 建表标准模板 -所有业务表必须遵循以下模板结构: +所有业务表必须遵循以下模板结构(PostgreSQL 17): ```sql -CREATE TABLE `{table_prefix}_{entity}` ( +CREATE TABLE IF NOT EXISTS {table_prefix}_{entity} ( -- ============ 主键 ============ - `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID', + id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, -- ============ 业务字段(按需添加) ============ - `workspace_id` BIGINT UNSIGNED NOT NULL COMMENT '工作空间 ID', - `name` VARCHAR(100) NOT NULL DEFAULT '' COMMENT '名称', - `status` TINYINT NOT NULL DEFAULT 1 COMMENT '状态:0=禁用,1=启用', + workspace_id BIGINT NOT NULL DEFAULT 1, + name VARCHAR(100) NOT NULL DEFAULT '', + status SMALLINT NOT NULL DEFAULT 1, -- ============ 通用字段(所有表必备) ============ - `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', - `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', - `deleted` TINYINT NOT NULL DEFAULT 0 COMMENT '删除标识:0=正常,1=删除', - `creator_id` BIGINT UNSIGNED NOT NULL COMMENT '创建人ID', - - -- ============ 索引 ============ - PRIMARY KEY (`id`), - UNIQUE KEY `uk_name_workspace_deleted` (`name`, `workspace_id`, `deleted`), - KEY `idx_workspace_id` (`workspace_id`), - KEY `idx_created_at` (`created_at`), - KEY `idx_status_deleted` (`status`, `deleted`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='表说明'; + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + deleted SMALLINT NOT NULL DEFAULT 0, + creator_id BIGINT NOT NULL DEFAULT 0, + + -- ============ 唯一约束(约束名带表名前缀,全库唯一) ============ + CONSTRAINT uk_{entity}_name_workspace_deleted UNIQUE (name, workspace_id, deleted) +); + +-- ============ 索引(独立语句,索引名带表名前缀) ============ +CREATE INDEX IF NOT EXISTS idx_{entity}_workspace_id ON {table_prefix}_{entity}(workspace_id); +CREATE INDEX IF NOT EXISTS idx_{entity}_created_at ON {table_prefix}_{entity}(created_at); +CREATE INDEX IF NOT EXISTS idx_{entity}_status_deleted ON {table_prefix}_{entity}(status, deleted); + +-- ============ 字段注释(可选,PG 用独立语句) ============ +COMMENT ON TABLE {table_prefix}_{entity} IS '表说明'; +COMMENT ON COLUMN {table_prefix}_{entity}.status IS '状态:0=禁用,1=启用'; +COMMENT ON COLUMN {table_prefix}_{entity}.deleted IS '删除标识:0=正常,1=删除'; ``` +> 模板风格与 `eify-app/src/main/resources/db/migration/V1__init.sql`(最终态)保持一致:裸标识符(无反引号)、`BIGINT GENERATED BY DEFAULT AS IDENTITY`、`SMALLINT`、`TIMESTAMPTZ NOT NULL DEFAULT NOW()`、`JSONB`、独立 `CREATE INDEX IF NOT EXISTS`、`CONSTRAINT uk_xxx UNIQUE(...)`,不带 `ENGINE` / `CHARSET` / `COLLATE`。 + +### Flyway 幂等迁移(PostgreSQL) + +项目要求所有 Flyway DDL **幂等可重入**。PG 原生支持 `IF NOT EXISTS`,比 MySQL 时代的 `INFORMATION_SCHEMA` + `PREPARE/EXECUTE` 样板简洁得多: + +```sql +-- ADD COLUMN(幂等) +ALTER TABLE my_table ADD COLUMN IF NOT EXISTS my_col SMALLINT NOT NULL DEFAULT 0; + +-- CREATE INDEX(幂等) +CREATE INDEX IF NOT EXISTS idx_my_col ON my_table(my_col); + +-- ADD CONSTRAINT(约束无 IF NOT EXISTS,用 DO 块判断 pg_constraint) +DO $$ BEGIN + IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'uk_my_key') THEN + ALTER TABLE my_table ADD CONSTRAINT uk_my_key UNIQUE (col_a, col_b); + END IF; +END $$; +``` + +> 要点:`CREATE TABLE`、`ADD COLUMN`、`CREATE INDEX`、`DROP ...` 均支持 `IF [NOT] EXISTS`,直接使用即可,无需 MySQL 的 `INFORMATION_SCHEMA` 检查 + `PREPARE/EXECUTE` 动态 SQL。唯一例外是 `ADD CONSTRAINT` 没有 `IF NOT EXISTS`,用 `DO $$ ... $$` 匿名块查询 `pg_constraint.conname` 判断后再添加。 + ## Eify 业务表 DDL -> **完整 DDL**:Flyway 迁移文件(`V1__init.sql`、`V4__model_category_and_embedding_model_id.sql`、`V5__name_workspace_deleted_unique.sql`)— 包含所有表的 CREATE TABLE 和增量变更。 +> **完整 DDL**:Flyway 迁移文件 `V1__init.sql`(PostgreSQL 17,已折叠所有历史增量变更)— 包含所有表的 CREATE TABLE、索引、约束和初始数据。 > > 以下为各表结构说明,实际执行请使用 Flyway 自动迁移。 @@ -471,9 +510,9 @@ ORDER BY id DESC LIMIT 21; ### 执行方式 ```bash -# 初始化数据库(包含所有表 + 开发环境初始数据) -# 数据库由 Flyway 自动迁移,无需手动导入。备用方式: -mysql -u root -p eify < deploy/sql/init_eify_mysql.sql +# 初始化数据库:由 Flyway 在应用启动时自动迁移(V1__init.sql),无需手动导入。 +# 备用方式(手动执行迁移脚本): +psql -U postgres -d eify -f eify-app/src/main/resources/db/migration/V1__init.sql ``` ## 索引维护策略 @@ -481,69 +520,66 @@ mysql -u root -p eify < deploy/sql/init_eify_mysql.sql ### 索引监控 ```sql --- 查看未使用的索引 +-- 查看未使用的索引(基于 PG 统计视图 pg_stat_user_indexes) SELECT - t.table_schema, - t.table_name, - s.index_name, - s.seq_in_index, - s.column_name, - s.cardinality -FROM information_schema.tables t -JOIN information_schema.statistics s ON t.table_name = s.table_name -WHERE t.table_schema = 'eify' - AND t.table_name NOT LIKE 'sys_%' - AND s.index_name != 'PRIMARY' - AND s.cardinality IS NULL -ORDER BY t.table_name, s.index_name, s.seq_in_index; + schemaname, + relname AS table_name, + indexrelname AS index_name, + idx_scan AS scans, + pg_size_pretty(pg_relation_size(indexrelid)) AS index_size +FROM pg_stat_user_indexes +WHERE idx_scan = 0 + AND indexrelname NOT LIKE '%_pkey' +ORDER BY pg_relation_size(indexrelid) DESC; -- 查看表大小和索引大小 SELECT - table_name, - ROUND(((data_length + index_length) / 1024 / 1024), 2) AS '总大小MB', - ROUND((data_length / 1024 / 1024), 2) AS '数据大小MB', - ROUND((index_length / 1024 / 1024), 2) AS '索引大小MB' -FROM information_schema.tables -WHERE table_schema = 'eify' -ORDER BY (data_length + index_length) DESC; + relname AS table_name, + pg_size_pretty(pg_total_relation_size(relid)) AS total_size, + pg_size_pretty(pg_relation_size(relid)) AS data_size, + pg_size_pretty(pg_total_relation_size(relid) - pg_relation_size(relid)) AS index_size +FROM pg_catalog.pg_statio_user_tables +ORDER BY pg_total_relation_size(relid) DESC; ``` ### 索引优化 ```sql --- 分析表(更新统计信息) -ANALYZE TABLE ai_message; +-- 分析表(更新统计信息,供查询优化器使用) +ANALYZE ai_chat_message; --- 优化表(回收空间) -OPTIMIZE TABLE ai_message; +-- 回收空间 + 更新统计(PG 用 VACUUM,FULL 会重写表并加锁,谨慎使用) +VACUUM (ANALYZE) ai_chat_message; --- 检查表(检查完整性) -CHECK TABLE ai_message; +-- 重建索引(消除膨胀,CONCURRENTLY 不阻塞读写) +REINDEX INDEX CONCURRENTLY idx_ai_chat_message_session_id_id; ``` ## 数据归档策略 ```sql -- 归档 6 个月前的消息到历史表 --- 1. 创建历史表(结构相同) -CREATE TABLE `ai_chat_message_history` LIKE `ai_chat_message`; - --- 2. 归档数据(分批执行,避免锁表) +-- 1. 创建历史表(结构相同,PG 用 INCLUDING ALL 复制索引/约束/默认值) +CREATE TABLE IF NOT EXISTS ai_chat_message_history + (LIKE ai_chat_message INCLUDING ALL); + +-- 2. 归档数据(分批执行,避免长事务,PG 用 CTE + LIMIT) +WITH moved AS ( + SELECT id FROM ai_chat_message + WHERE created_at < NOW() - INTERVAL '6 months' + LIMIT 10000 +) INSERT INTO ai_chat_message_history -SELECT * FROM ai_chat_message -WHERE created_at < DATE_SUB(NOW(), INTERVAL 6 MONTH) -LIMIT 10000; +SELECT * FROM ai_chat_message WHERE id IN (SELECT id FROM moved); -- 3. 删除已归档数据(分批执行) DELETE FROM ai_chat_message -WHERE id IN ( - SELECT id FROM ai_chat_message_history -) -LIMIT 10000; +WHERE id IN (SELECT id FROM ai_chat_message_history) + AND created_at < NOW() - INTERVAL '6 months'; -- 4. 验证归档完成 SELECT COUNT(*) FROM ai_chat_message -WHERE created_at < DATE_SUB(NOW(), INTERVAL 6 MONTH); +WHERE created_at < NOW() - INTERVAL '6 months'; ``` --- diff --git a/docs/plans/2026-06-05-mysql-to-pg17-migration.md b/docs/plans/2026-06-05-mysql-to-pg17-migration.md new file mode 100644 index 0000000..7b7ab07 --- /dev/null +++ b/docs/plans/2026-06-05-mysql-to-pg17-migration.md @@ -0,0 +1,915 @@ +# MySQL 8.0 → PostgreSQL 17 迁移实现计划 + +> **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:** 将主业务库从 MySQL 8.0 迁移到 PostgreSQL 17,并把 pgvector(pg16)向量库合并进同一个 PG17 单库,去掉双数据源、Druid 与双 Flyway。 + +**Architecture:** 干净切换(丢弃旧数据,无数据迁移)。业务表 + `document_chunk` 向量表落同一 PG17 database,单 HikariCP 数据源、单 Flyway。MyBatis-Plus 业务 CRUD 与 `JdbcTemplate` 向量读写共用主数据源。4 处方言注解 SQL 人工翻译,其余可移植。测试采用 H2(PG 模式)为主 + Testcontainers PG17 专攻 JSONB/upsert/向量保真盲区。 + +**Tech Stack:** Spring Boot 4.x、MyBatis-Plus、PostgreSQL 17、pgvector、Flyway、HikariCP、H2、Testcontainers、Maven 多模块。 + +**Spec:** `docs/specs/2026-06-05-mysql-to-pg17-migration-design.md` + +--- + +## 约定(所有任务通用) + +- **分支**:已在 `spec/pg17-migration` 分支。所有实现 commit 落此分支。 +- **提交节奏**:每个 Task 末尾 commit 一次。 +- **验证基线命令**(按需在步骤中调用): + - 编译:`mvn compile -q` + - 单模块测试:`mvn test -pl eify- -am -q` + - 全量后端测试:`mvn test -q` + - 前端(仅 Phase 7 跑一次,前端零改动):`cd eify-web && npx vue-tsc --noEmit && npx vitest run && cd ..` +- **关键事实**:当前生效 schema = `V1__init.sql` + `V4`~`V7` 累积变更。PG 版必须表达**最终态**,不是逐版翻译。最终态差异点: + - `model_config` 含 `model_category`(V4) + - `knowledge_base` 含 `embedding_model_id`(V4) + - `provider`/`ai_agent`/`knowledge_base`/`mcp_server`/`ai_workflow` 的唯一键为 `uk_name_workspace_deleted (name, workspace_id, deleted)`(V5) + - `agent_mcp_tool` 唯一键为 `uk_agent_tool_workspace (agent_id, tool_id, workspace_id)`(V6) + - `mcp_tool` 含 `idx_workspace_id`、`idx_name_workspace`(V6) + +--- + +## Phase 1 — Schema 重写(PG 版 DDL) + +### Task 1: 新建 PG 版 `V1__init.sql`(最终态全表 DDL) + +**Files:** +- Create/覆盖: `eify-app/src/main/resources/db/migration/V1__init.sql`(PG 版) +- Delete: `eify-app/src/main/resources/db/migration/V4__model_category_and_embedding_model_id.sql` +- Delete: `eify-app/src/main/resources/db/migration/V5__name_workspace_deleted_unique.sql` +- Delete: `eify-app/src/main/resources/db/migration/V6__mcp_workspace_isolation.sql` +- Delete: `eify-app/src/main/resources/db/migration/V7__mcp_server_description.sql` +- Delete: `eify-app/src/main/resources/db/migration-pg/`(V1/V2 并入主 V1) + +> 干净切换、丢弃旧数据,V4~V7 的增量 ALTER 不再需要——最终效果直接写进新 V1。`document_chunk`(原 migration-pg)也并入主 V1。 + +**转换规则**(逐表套用,源文件 = 原 MySQL `V1__init.sql` + §约定的最终态差异点): + +| MySQL | PG17 | +|:---|:---| +| `BIGINT UNSIGNED AUTO_INCREMENT` | `BIGINT GENERATED BY DEFAULT AS IDENTITY` | +| `TINYINT` | `SMALLINT` | +| `INT/BIGINT UNSIGNED`(非主键) | `INT` / `BIGINT` | +| `DECIMAL(3,2)` | `NUMERIC(3,2)` | +| `DATETIME [ON UPDATE ...]` | `TIMESTAMPTZ`(去 ON UPDATE,默认 `NOW()`) | +| `JSON` | `JSONB` | +| `` `反引号` `` / `ENGINE=` / `CHARSET=` / 列内 `COMMENT` | 删除 | +| 内联 `KEY idx (...)` | 独立 `CREATE INDEX IF NOT EXISTS` | +| 前缀索引 `refresh_token(64)` | 整列 `idx_refresh_token(refresh_token)` | + +- [ ] **Step 1: 删除旧 MySQL 迁移与 pg 子目录** + +```bash +git rm eify-app/src/main/resources/db/migration/V4__model_category_and_embedding_model_id.sql \ + eify-app/src/main/resources/db/migration/V5__name_workspace_deleted_unique.sql \ + eify-app/src/main/resources/db/migration/V6__mcp_workspace_isolation.sql \ + eify-app/src/main/resources/db/migration/V7__mcp_server_description.sql +git rm -r eify-app/src/main/resources/db/migration-pg/ +``` + +- [ ] **Step 2: 写表头 + 用户/工作空间模块(IDENTITY 示范)** + +> **转录源**:覆盖前先读当前 `eify-app/src/main/resources/db/migration/V1__init.sql`(MySQL 版,23 张表的权威字段/索引清单)。本计划只对带风险的模式(IDENTITY、JSONB、NUMERIC、V4~V7 差异、document_chunk、种子)给出完整代码;其余字段按上方规则表逐列机械转换即可。 + +覆盖写 `V1__init.sql`。用 `ai_user` 作为 IDENTITY + UNIQUE 约束的范式,其余表照此套规则: + +```sql +-- ============================================================ +-- Eify 数据库初始化(PostgreSQL 17) +-- 干净切换:表达 MySQL V1+V4~V7 累积后的最终态 +-- ============================================================ +CREATE EXTENSION IF NOT EXISTS vector; + +CREATE TABLE IF NOT EXISTS ai_user ( + id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + username VARCHAR(64) NOT NULL, + email VARCHAR(128) NOT NULL DEFAULT '', + password VARCHAR(256) NOT NULL, + display_name VARCHAR(128), + avatar_url VARCHAR(512), + status SMALLINT NOT NULL DEFAULT 1, + last_login_at TIMESTAMPTZ, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + deleted SMALLINT NOT NULL DEFAULT 0, + creator_id BIGINT, + CONSTRAINT uk_username UNIQUE (username), + CONSTRAINT uk_email UNIQUE (email) +); +``` + +追加 `ai_workspace`、`ai_workspace_member`(`uk_workspace_user`、`idx_user_id`)、`ai_workspace_invite`(`uk_code`、`idx_workspace_id`)、`ai_user_session`(`idx_user_id` + 整列 `idx_refresh_token`)。 + +- [ ] **Step 3: Provider 模块(V4 最终态 + V5 唯一键)** + +`provider` 唯一键用 V5 最终态 `uk_name_workspace_deleted`;`model_config` 含 V4 的 `model_category SMALLINT NOT NULL DEFAULT 0` + `idx_model_category`: + +```sql +CREATE TABLE IF NOT EXISTS provider ( + id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + workspace_id BIGINT NOT NULL DEFAULT 1, + name VARCHAR(100) NOT NULL, + type VARCHAR(50) NOT NULL, + base_url VARCHAR(500) NOT NULL, + auth_config JSONB NOT NULL, + enabled SMALLINT NOT NULL DEFAULT 1, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + deleted SMALLINT NOT NULL DEFAULT 0, + creator_id BIGINT NOT NULL DEFAULT 0, + CONSTRAINT uk_name_workspace_deleted UNIQUE (name, workspace_id, deleted) +); +CREATE INDEX IF NOT EXISTS idx_type_enabled ON provider(type, enabled); +CREATE INDEX IF NOT EXISTS idx_deleted ON provider(deleted); + +CREATE TABLE IF NOT EXISTS model_config ( + id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + provider_id BIGINT NOT NULL, + name VARCHAR(100) NOT NULL, + model_id VARCHAR(100) NOT NULL, + model_category SMALLINT NOT NULL DEFAULT 0, + context_size INT NOT NULL DEFAULT 0, + extra_params JSONB NOT NULL, + enabled SMALLINT NOT NULL DEFAULT 1, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + deleted SMALLINT NOT NULL DEFAULT 0, + creator_id BIGINT NOT NULL DEFAULT 0, + workspace_id BIGINT NOT NULL DEFAULT 1 +); +CREATE INDEX IF NOT EXISTS idx_provider_id ON model_config(provider_id); +CREATE INDEX IF NOT EXISTS idx_model_id ON model_config(model_id); +CREATE INDEX IF NOT EXISTS idx_enabled_deleted ON model_config(enabled, deleted); +CREATE INDEX IF NOT EXISTS idx_model_category ON model_config(model_category); +``` + +追加 `provider_health`(`uk_provider_id`,状态字段 `VARCHAR`,`fail_count/latency_ms INT`)。 + +- [ ] **Step 4: Agent / Chat 模块** + +`ai_agent`:`temperature/top_p/frequency_penalty/presence_penalty` 用 `NUMERIC(3,2)`,`system_prompt` 用 `TEXT`,`agent_config` 用 `JSONB`,唯一键 V5 最终态 `uk_name_workspace_deleted`,索引 `idx_default_provider_id`/`idx_enabled_deleted`/`idx_created_at`/`idx_workspace_id`。 + +`ai_chat_session`:索引 `idx_user_status_updated_id (user_id, status, updated_at, id)`、`idx_agent_updated_id`、`idx_created_at`、`idx_workspace_id`。 + +`ai_chat_message`:`content TEXT`、`metadata JSONB`、`token_count INT`,索引 `idx_session_id_id`、`idx_created_at`、`idx_session_id_id_role_time`、`idx_created_at_id`、`idx_session_workspace_id`。 + +- [ ] **Step 5: Knowledge 模块(含 V4 embedding_model_id + V5 唯一键)** + +`knowledge_base`:含 V4 的 `embedding_model_id BIGINT`(+`idx_embedding_model_id`),唯一键 `uk_name_workspace_deleted`。`document`:含 `workspace_id`(注:原 V1 末尾注释提示早期需补列,PG 最终态直接含此列)+ 索引。`agent_knowledge`:`uk_agent_knowledge (agent_id, knowledge_id)` + `idx_agent_id`/`idx_knowledge_id`。 + +- [ ] **Step 6: MCP 模块(含 V6/V7 最终态)** + +`mcp_server`:含 V7 的 `description VARCHAR(500)`,唯一键 V5 `uk_name_workspace_deleted`,`idx_workspace_id`。`mcp_tool`:`description TEXT`、`input_schema JSONB`,含 V6 的 `idx_workspace_id` + `idx_name_workspace`。`agent_mcp_tool`:唯一键 V6 最终态 `uk_agent_tool_workspace (agent_id, tool_id, workspace_id)` + `idx_agent_id`/`idx_tool_id`/`idx_workspace_id`。 + +- [ ] **Step 7: Workflow 模块 + document_chunk 向量表** + +`ai_workflow`(唯一键 V5 `uk_name_workspace_deleted`,`variables JSONB`)、`ai_workflow_node`(`position_x/y DOUBLE PRECISION`,`config JSONB`)、`ai_workflow_edge`、`ai_workflow_execution`(`variables JSONB`,`error_message TEXT`)。 + +向量表并入(源自 migration-pg V1+V2 最终态,变长 VECTOR): + +```sql +CREATE TABLE IF NOT EXISTS document_chunk ( + id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + workspace_id BIGINT NOT NULL DEFAULT 0, + knowledge_id BIGINT NOT NULL, + document_id BIGINT NOT NULL, + chunk_index INT NOT NULL, + content TEXT NOT NULL, + embedding VECTOR, + chunk_hash CHAR(64) NOT NULL, + enabled SMALLINT NOT NULL DEFAULT 1, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); +CREATE INDEX IF NOT EXISTS idx_chunk_embedding ON document_chunk USING hnsw (embedding vector_cosine_ops); +CREATE INDEX IF NOT EXISTS idx_chunk_workspace ON document_chunk(workspace_id); +CREATE INDEX IF NOT EXISTS idx_chunk_knowledge ON document_chunk(knowledge_id); +CREATE INDEX IF NOT EXISTS idx_chunk_document ON document_chunk(document_id); +CREATE INDEX IF NOT EXISTS idx_chunk_hash ON document_chunk(chunk_hash); +``` + +- [ ] **Step 8: 种子数据 + IDENTITY 序列同步(spec §2.4)** + +固定 id 种子 + `ON CONFLICT` + `COALESCE` 兜底的序列同步(空表不报错): + +```sql +INSERT INTO ai_user (id, username, email, password, display_name, status) VALUES +(1, 'admin', 'admin@eify.local', '$2a$10$CfJnrJ65v1Oyt91xEG/tB.DzgQCLk6gX0reH9LpgHgB6boCapKH3C', 'Admin', 1) +ON CONFLICT (id) DO UPDATE SET password = EXCLUDED.password; + +INSERT INTO ai_workspace (id, name, description) VALUES +(1, 'System Workspace', 'System default workspace') +ON CONFLICT (id) DO UPDATE SET name = EXCLUDED.name; + +INSERT INTO ai_workspace_member (workspace_id, user_id, role) VALUES +(1, 1, 'owner') +ON CONFLICT (workspace_id, user_id) DO UPDATE SET role = EXCLUDED.role; + +SELECT setval(pg_get_serial_sequence('ai_user', 'id'), COALESCE((SELECT MAX(id) FROM ai_user), 1)); +SELECT setval(pg_get_serial_sequence('ai_workspace', 'id'), COALESCE((SELECT MAX(id) FROM ai_workspace), 1)); +``` + +- [ ] **Step 9: 校验 SQL 语法可解析(无法跑库时的静态检查)** + +Run: `git diff --stat eify-app/src/main/resources/db/migration/` +Expected: 仅 `V1__init.sql` 改动,V4~V7 与 migration-pg 已删除。完整库验证留到 Phase 7 起 PG17 容器。 + +- [ ] **Step 10: Commit** + +```bash +git add eify-app/src/main/resources/db/migration/ +git commit -m "feat(db): rewrite V1__init.sql as final-state PostgreSQL 17 schema + +Fold MySQL V1+V4~V7 into one PG DDL, merge document_chunk, drop migration-pg. +IDENTITY columns + seed sequence sync with COALESCE fallback." +``` + +### Task 2: 更新 DATABASE.md Flyway 幂等模板为 PG 版 + +**Files:** +- Modify: `docs/guides/DATABASE.md`(Flyway 幂等模板章节) + +- [ ] **Step 1: 定位 MySQL INFORMATION_SCHEMA 模板段落** + +Run: `grep -n "INFORMATION_SCHEMA" docs/guides/DATABASE.md` +Expected: 找到 ADD COLUMN / ADD INDEX / ADD UNIQUE KEY 三个 `SET @sql = IF(...)` 模板。 + +- [ ] **Step 2: 替换为 PG 原生幂等写法** + +将 MySQL 的 `INFORMATION_SCHEMA` + `PREPARE/EXECUTE` 模板替换为 PG 原生: + +```sql +-- ADD COLUMN(幂等) +ALTER TABLE my_table ADD COLUMN IF NOT EXISTS my_col SMALLINT NOT NULL DEFAULT 0; +-- ADD INDEX(幂等) +CREATE INDEX IF NOT EXISTS idx_my_col ON my_table(my_col); +-- ADD UNIQUE(幂等,约束无 IF NOT EXISTS,用 DO 块) +DO $$ BEGIN + IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'uk_my_key') THEN + ALTER TABLE my_table ADD CONSTRAINT uk_my_key UNIQUE (col_a, col_b); + END IF; +END $$; +``` + +- [ ] **Step 3: Commit** + +```bash +git add docs/guides/DATABASE.md +git commit -m "docs(db): switch Flyway idempotency template to PostgreSQL native syntax" +``` + +--- + +## Phase 2 — 数据访问层方言翻译 + +### Task 3: 翻译 `ProviderHealthMapper` upsert(ON DUPLICATE → ON CONFLICT) + +**Files:** +- Modify: `eify-provider/src/main/java/com/eify/provider/mapper/ProviderHealthMapper.java:14-24` +- Modify: `eify-provider/src/main/java/com/eify/provider/domain/entity/ProviderHealth.java:17`(注释方言字眼) + +- [ ] **Step 1: 翻译 SQL + 清理注释** + +将 `upsertHealth` 的 `@Insert` 改为 PG `ON CONFLICT`(依赖 `uk_provider_id`): + +```java + /** + * 使用 ON CONFLICT 原地更新健康状态。 + * provider_health 表以 provider_id 为唯一索引。 + */ + @Insert("INSERT INTO provider_health (provider_id, status, error_message, last_check_at, updated_at) " + + "VALUES (#{providerId}, #{status}, #{errorMessage}, #{lastCheckAt}, NOW()) " + + "ON CONFLICT (provider_id) DO UPDATE SET status = EXCLUDED.status, " + + "error_message = EXCLUDED.error_message, " + + "last_check_at = EXCLUDED.last_check_at, " + + "updated_at = NOW()") + int upsertHealth(ProviderHealth health); +``` + +同时把 `ProviderHealth.java:17` 的 Javadoc "更新时使用 ON DUPLICATE KEY UPDATE 实现原地更新" 改为 "更新时使用 ON CONFLICT 实现原地更新"。 + +- [ ] **Step 2: 编译** + +Run: `mvn compile -pl eify-provider -am -q` +Expected: BUILD SUCCESS + +- [ ] **Step 3: 单模块测试** + +Run: `mvn test -pl eify-provider -am -q` +Expected: PASS(H2 PG 模式现支持 `ON CONFLICT`;upsert 真值校验在 Phase 6 Testcontainers) + +- [ ] **Step 4: Commit** + +```bash +git add eify-provider/src/main/java/com/eify/provider/mapper/ProviderHealthMapper.java \ + eify-provider/src/main/java/com/eify/provider/domain/entity/ProviderHealth.java +git commit -m "feat(provider): translate health upsert to PostgreSQL ON CONFLICT" +``` + +### Task 4: 翻译 `AgentKnowledgeMapper` 批量 upsert + +**Files:** +- Modify: `eify-agent/src/main/java/com/eify/agent/mapper/AgentKnowledgeMapper.java:28-31` + +- [ ] **Step 1: 翻译批量 ON DUPLICATE → ON CONFLICT** + +依赖 `uk_agent_knowledge (agent_id, knowledge_id)`: + +```java + @Insert("") + int upsertKnowledgeIds(@Param("agentId") Long agentId, @Param("knowledgeIds") List knowledgeIds); +``` + +- [ ] **Step 2: 编译** + +Run: `mvn compile -pl eify-agent -am -q` +Expected: BUILD SUCCESS + +- [ ] **Step 3: 单模块测试** + +Run: `mvn test -pl eify-agent -am -q` +Expected: PASS + +- [ ] **Step 4: Commit** + +```bash +git add eify-agent/src/main/java/com/eify/agent/mapper/AgentKnowledgeMapper.java +git commit -m "feat(agent): translate agent_knowledge batch upsert to ON CONFLICT" +``` + +### Task 5: 翻译两处 JSONB 查询(JSON_EXTRACT → ->> + ::bigint) + +**Files:** +- Modify: `eify-provider/src/main/java/com/eify/provider/mapper/ProviderMapper.java:18-24` +- Modify: `eify-mcp/src/main/java/com/eify/mcp/mapper/McpServerMapper.java:13-19` + +- [ ] **Step 1: 翻译 `ProviderMapper.countWorkflowLlmReferences`** + +`->>` 返回 text,右侧绑定为 bigint,必须 `::bigint` 强转: + +```java + @Select("SELECT COUNT(*) FROM ai_workflow_node n " + + "INNER JOIN ai_workflow w ON n.workflow_id = w.id " + + "WHERE n.type = 'llm' " + + "AND (n.config->>'providerId')::bigint = #{providerId} " + + "AND n.deleted = 0 " + + "AND w.deleted = 0") + int countWorkflowLlmReferences(@Param("providerId") Long providerId); +``` + +- [ ] **Step 2: 翻译 `McpServerMapper.countWorkflowToolCallReferences`** + +```java + @Select("SELECT COUNT(*) FROM ai_workflow_node n " + + "INNER JOIN ai_workflow w ON n.workflow_id = w.id " + + "WHERE n.type = 'tool_call' " + + "AND (n.config->>'serverId')::bigint = #{serverId} " + + "AND n.deleted = 0 " + + "AND w.deleted = 0") + int countWorkflowToolCallReferences(@Param("serverId") Long serverId); +``` + +- [ ] **Step 3: 编译** + +Run: `mvn compile -pl eify-provider -am -q && mvn compile -pl eify-mcp -am -q` +Expected: BUILD SUCCESS + +- [ ] **Step 4: Commit** + +```bash +git add eify-provider/src/main/java/com/eify/provider/mapper/ProviderMapper.java \ + eify-mcp/src/main/java/com/eify/mcp/mapper/McpServerMapper.java +git commit -m "feat: translate JSON_EXTRACT to PostgreSQL ->> with ::bigint cast" +``` + +### Task 6: 清理 `ProviderServiceImpl` 吞异常 + 删除 `fixDatabase` 端点 + +**Files:** +- Modify: `eify-provider/src/main/java/com/eify/provider/service/impl/ProviderServiceImpl.java:286`(吞异常日志方言字眼) +- Modify: `eify-chat/src/main/java/com/eify/chat/controller/ChatController.java`(删除 `fixDatabase()` + `jdbcTemplate` 字段) +- Modify: `eify-chat/src/test/java/com/eify/chat/controller/ChatControllerTest.java`(清理 jdbcTemplate mock 注入) + +- [ ] **Step 1: 读 ProviderServiceImpl 上下文确认 try/catch 范围** + +Run: `sed -n '275,295p' eify-provider/src/main/java/com/eify/provider/service/impl/ProviderServiceImpl.java` +Expected: 看到包裹 `countWorkflowLlmReferences` 的 try/catch + 含 "JSON_EXTRACT" 的 `log.warn`。 + +- [ ] **Step 2: 改日志文案(PG 下 JSONB 查询正常,不再静默咽错)** + +把 `log.warn("跳过工作流 LLM 节点引用检查(可能是不支持 JSON_EXTRACT 的数据库): {}", e.getMessage())` 的文案改为不含方言字眼,例如 `log.warn("跳过工作流 LLM 节点引用检查(查询失败): {}", e.getMessage())`。(保留 try/catch 防御外部失败,但移除 grep 守卫会命中的 `JSON_EXTRACT` 字样。) + +- [ ] **Step 3: 删除 ChatController.fixDatabase + jdbcTemplate 依赖** + +删除 `ChatController.java` 的 `fixDatabase()` 方法(约 215-242 行)、`private final JdbcTemplate jdbcTemplate;` 字段、构造器中的 `jdbcTemplate` 形参与赋值、相关 import。 + +- [ ] **Step 4: 同步修 ChatControllerTest** + +`ChatControllerTest.java:45,51` 移除 `JdbcTemplate jdbcTemplate;` mock 与构造器实参,使其匹配新构造器签名。 + +- [ ] **Step 5: 编译 + 测试** + +Run: `mvn test -pl eify-chat -am -q && mvn compile -pl eify-provider -am -q` +Expected: PASS / BUILD SUCCESS + +- [ ] **Step 6: Commit** + +```bash +git add eify-chat/src/main/java/com/eify/chat/controller/ChatController.java \ + eify-chat/src/test/java/com/eify/chat/controller/ChatControllerTest.java \ + eify-provider/src/main/java/com/eify/provider/service/impl/ProviderServiceImpl.java +git commit -m "refactor: drop MySQL-only fixDatabase endpoint, clean dialect words in logs" +``` + +--- + +## Phase 3 — 数据源 / Flyway / 连接池收口 + +### Task 7: 去双数据源,向量 JdbcTemplate 指向主库 + +**Files:** +- Delete: `eify-knowledge/src/main/java/com/eify/knowledge/config/PgVectorConfig.java` +- Delete: `eify-app/src/main/java/com/eify/app/config/PgFlywayConfig.java` +- Modify: `eify-knowledge/src/main/java/com/eify/knowledge/repository/ChunkRepository.java:29-31` + +> `ChunkRepository` 的 SQL 已是 PG 原生(`::vector`、`<=>`、`ILIKE`),**不改 SQL**,只把注入的 `pgJdbcTemplate` 换成主数据源的 `JdbcTemplate`。 + +- [ ] **Step 1: 删除两个独立数据源/Flyway 配置类** + +```bash +git rm eify-knowledge/src/main/java/com/eify/knowledge/config/PgVectorConfig.java \ + eify-app/src/main/java/com/eify/app/config/PgFlywayConfig.java +``` + +- [ ] **Step 2: ChunkRepository 改用主 JdbcTemplate** + +把字段从 `@Lazy @Autowired private JdbcTemplate pgJdbcTemplate;` 改为构造器注入主数据源的 `JdbcTemplate`(`@RequiredArgsConstructor` 已在),并将方法体内 `pgJdbcTemplate` 全部改为 `jdbcTemplate`: + +```java +@Slf4j +@Repository +@RequiredArgsConstructor +public class ChunkRepository { + + private final JdbcTemplate jdbcTemplate; + private final VectorTypeHandler vectorTypeHandler; + // ... 方法体内 pgJdbcTemplate → jdbcTemplate(共 8 处调用) +``` + +> Spring Boot 单数据源下自动配置一个 `JdbcTemplate` bean,直接注入即可。`@Lazy` 不再需要(PG 现在是必备主库)。 + +- [ ] **Step 3: 全局搜残留引用** + +Run: `grep -rn "pgJdbcTemplate\|PgVectorConfig\|PgFlywayConfig\|pgvectorDataSource" eify-*/src/main/java/` +Expected: 无输出(全部清理)。 + +- [ ] **Step 4: 编译** + +Run: `mvn compile -pl eify-knowledge -am -q` +Expected: BUILD SUCCESS + +- [ ] **Step 5: Commit** + +```bash +git add eify-knowledge/ eify-app/ +git commit -m "refactor: single datasource — point vector JdbcTemplate at main PG DB" +``` + +### Task 8: Druid → HikariCP + 单 Flyway 指向 PG + +**Files:** +- Modify: `eify-app/pom.xml:73-93`(移除 mysql-connector-j、flyway-mysql、druid starter) +- Modify: `eify-app/src/main/java/com/eify/app/config/MySqlFlywayConfig.java`(重命名/改文案,仍指向主库) + +- [ ] **Step 1: 删除 pom 中 MySQL/Druid/flyway-mysql 依赖** + +移除 `eify-app/pom.xml` 中三个依赖块:`com.mysql:mysql-connector-j`、`org.flywaydb:flyway-mysql`、`com.alibaba:druid-spring-boot-4-starter`。保留 `flyway-database-postgresql`。新增 PG 驱动(确保 eify-app 直接依赖;eify-knowledge 已引 `org.postgresql:postgresql`,但主数据源在 app 层需保证可见): + +```xml + + + org.postgresql + postgresql + + + + com.pgvector + pgvector + +``` + +> 注:版本由父 pom `dependencyManagement` 统一管理(`pgvector.version=0.1.6` 已存在)。若 eify-app 通过 eify-knowledge 传递依赖已可见 postgresql,可省 postgresql 块——编译/启动时确认。 + +- [ ] **Step 2: 简化 Flyway 配置类(去掉双源耦合注释,仍走主数据源)** + +`MySqlFlywayConfig` 的 `@PostConstruct` 程序化迁移逻辑保留(指向主 `db/migration`、`flyway_schema_history`),但:去掉 Javadoc 中关于 PgVectorConfig/双源的描述。可选重命名类为 `FlywayConfig`(若重命名,同步改文件名与所有引用)。`@ConditionalOnProperty(eify.flyway.mysql.enabled)` 保留——test 用它关 Flyway(见 Phase 6)。 + +- [ ] **Step 3: 编译全量** + +Run: `mvn compile -q` +Expected: BUILD SUCCESS(无 MySQL 驱动符号缺失) + +- [ ] **Step 4: 搜 Druid 残留** + +Run: `grep -rn "druid\|Druid\|DruidDataSource" eify-*/src/main/java/` +Expected: 无 Java 引用(配置在 yml,Phase 4 处理)。 + +- [ ] **Step 5: Commit** + +```bash +git add eify-app/pom.xml eify-app/src/main/java/com/eify/app/config/ +git commit -m "build: drop MySQL/Druid/flyway-mysql deps, single Flyway on PG" +``` + +--- + +## Phase 4 — 四环境配置切换 + +### Task 9: 主 application.yml 切 PG + HikariCP + 合并向量数据源 + +**Files:** +- Modify: `eify-app/src/main/resources/application.yml:8-76` + +- [ ] **Step 1: 替换 datasource 块(PG 驱动 + HikariCP)** + +把 Druid datasource 块替换为 PG + HikariCP,删除整个 `datasource-pgvector` 块(合并入主源): + +```yaml + datasource: + driver-class-name: org.postgresql.Driver + url: ${PG_URL:jdbc:postgresql://localhost:5432/eify?stringtype=unspecified} + username: ${PG_USERNAME:postgres} + password: ${PG_PASSWORD:} + hikari: + maximum-pool-size: 20 + minimum-idle: 5 + connection-timeout: 60000 + idle-timeout: 300000 + keepalive-time: 60000 + connection-test-query: SELECT 1 +``` + +> 删除 `type: com.alibaba.druid...`、整段 `druid:` 子树、`datasource-pgvector:` 块。保留 `spring.sql.init.mode: never`、`autoconfigure.exclude`。 + +- [ ] **Step 2: 搜全局 datasource-pgvector 引用** + +Run: `grep -rn "datasource-pgvector\|spring.datasource-pgvector" eify-*/src/main/` +Expected: 无输出(含 eify-knowledge/application.yml 中的块也需删,见 Step 3)。 + +- [ ] **Step 3: 清理 eify-knowledge/application.yml 的 pgvector 块** + +删除 `eify-knowledge/src/main/resources/application.yml:14-18` 的 `datasource-pgvector` 块(该模块作为依赖运行,配置以 app 为准)。 + +- [ ] **Step 4: 编译** + +Run: `mvn compile -q` +Expected: BUILD SUCCESS + +- [ ] **Step 5: Commit** + +```bash +git add eify-app/src/main/resources/application.yml eify-knowledge/src/main/resources/application.yml +git commit -m "config: main datasource to PostgreSQL + HikariCP, merge vector source" +``` + +### Task 10: dev / prod / staging 环境的 Druid 配置改 HikariCP + +**Files:** +- Modify: `eify-app/src/main/resources/application-dev.yml:8-27` +- Modify: `eify-app/src/main/resources/application-prod.yml:16-38` +- Modify: `eify-app/src/main/resources/application-staging.yml`(同 prod 结构) + +- [ ] **Step 1: dev — 替换 druid 块为 hikari,删监控面板** + +`application-dev.yml` 的 `spring.datasource.druid:` 整段(含 `stat-view-servlet` 监控面板)替换为: + +```yaml +spring: + datasource: + hikari: + maximum-pool-size: 30 + minimum-idle: 5 + connection-timeout: 5000 + idle-timeout: 180000 + max-lifetime: 300000 +``` + +- [ ] **Step 2: prod — 替换 druid 块为 hikari** + +`application-prod.yml` 的 `spring.datasource.druid:` 整段替换为: + +```yaml +spring: + datasource: + hikari: + maximum-pool-size: 50 + minimum-idle: 10 + connection-timeout: 60000 + idle-timeout: 300000 + connection-test-query: SELECT 1 +``` + +- [ ] **Step 3: staging — 同 prod 处理** + +读 `application-staging.yml`,将其中的 `druid:` 块替换为等价 `hikari:` 配置(参数沿用 staging 原值)。慢查询监控由 PG `log_min_duration_statement` + `sql.logging.*` 切面承担(spec §4.2)。 + +- [ ] **Step 4: 搜残留 druid 配置** + +Run: `grep -rn "druid\|DRUID_PASSWORD\|stat-view-servlet" eify-app/src/main/resources/` +Expected: 无输出。 + +- [ ] **Step 5: Commit** + +```bash +git add eify-app/src/main/resources/application-*.yml +git commit -m "config: replace Druid pool with HikariCP across dev/prod/staging" +``` + +--- + +## Phase 5 — docker-compose 改造 + +### Task 11: 删 mysql 服务,pgvector 升 pg17 承载单库 + +**Files:** +- Modify: `deploy/infra/deploy/docker-compose.yml` + +- [ ] **Step 1: 删除 mysql 服务与卷,pgvector 升级** + +删除 `mysql:` 服务块(7-24 行)与 `volumes:` 下的 `mysql-data:`。将 `pgvector` 镜像 `pgvector/pgvector:pg16` → `pgvector/pgvector:pg17`,`POSTGRES_DB` 默认改为 `eify`(业务+向量单库)。 + +- [ ] **Step 2: backend 环境变量 MYSQL_* → PG_*** + +将 backend 服务的 `MYSQL_HOST/PORT/DATABASE/URL/USERNAME/PASSWORD` 替换为: + +```yaml + PG_URL: jdbc:postgresql://pgvector:5432/eify?stringtype=unspecified + PG_USERNAME: ${PGVECTOR_USERNAME:-postgres} + PG_PASSWORD: ${PGVECTOR_PASSWORD} +``` + +`depends_on` 删除 `mysql`,保留 `pgvector: condition: service_healthy`。删除 `redis` 之外无关项不动。 + +- [ ] **Step 3: 校验 compose 语法** + +Run: `docker compose -f deploy/infra/deploy/docker-compose.yml config -q` +Expected: 无错误输出(语法合法;需本机有 docker compose)。 + +- [ ] **Step 4: Commit** + +```bash +git add deploy/infra/deploy/docker-compose.yml +git commit -m "deploy: drop mysql service, pgvector pg17 hosts single DB" +``` + +--- + +## Phase 6 — 测试切 PG(H2 PG 模式 + Testcontainers PG17) + +### Task 12: H2 切 PostgreSQL 模式 + +**Files:** +- Modify: `eify-app/src/test/resources/application-test.yml:9-37` +- Modify: `eify-app/src/test/resources/schema-h2.sql` + +- [ ] **Step 1: application-test.yml 切 H2 PG 模式** + +把主 datasource 与 pgvector datasource 都改为 H2 PostgreSQL 模式,删 Druid type: + +```yaml + datasource: + driver-class-name: org.h2.Driver + url: jdbc:h2:mem:testdb;MODE=PostgreSQL;DATABASE_TO_LOWER=TRUE;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE + username: sa + password: + hikari: + maximum-pool-size: 5 + minimum-idle: 1 +``` + +删除 `datasource-pgvector` 块;`autoconfigure.exclude` 移除 `DruidDataSourceAutoConfigure`(已无 Druid);保留 Redis 排除与 `eify.flyway.mysql.enabled: false`(关 Flyway,用 schema-h2.sql 建表)。 + +- [ ] **Step 2: schema-h2.sql 适配 PG 模式** + +H2 PG 模式下 `AUTO_INCREMENT` 不识别,改用 `GENERATED BY DEFAULT AS IDENTITY`;`CLOB` → `TEXT`;`DOUBLE` → `DOUBLE PRECISION`。逐表替换(文件已无反引号、已去 MySQL 特性,主要是这三类): + +```sql +-- 范式(ai_user 等所有表的主键) +id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, +-- CLOB 列(auth_config / extra_params / content / metadata / *_schema / variables / config / system_prompt 等) +auth_config TEXT NOT NULL, +-- workflow_node 坐标 +position_x DOUBLE PRECISION, +``` + +> 向量表 `document_chunk` 在测试 schema 中**不建**(H2 无 VECTOR),向量检索由 Testcontainers 覆盖(Task 13);Service 单测用 Mockito 不碰库。 + +- [ ] **Step 3: 跑全量测试(H2 PG 模式)** + +Run: `mvn test -q` +Expected: PASS。重点关注走真实 H2 的 `@SpringBootTest` 集成测试(`ProviderControllerIntegrationTest`、`ChatControllerIntegrationTest`、`McpWorkspaceIsolationIntegrationTest`),它们的 `jdbcTemplate` 清理 SQL 与 schema-h2.sql 需在 PG 模式下解析通过。若有用例依赖 MySQL 模式行为,按报错定位修正。 + +- [ ] **Step 4: Commit** + +```bash +git add eify-app/src/test/resources/application-test.yml eify-app/src/test/resources/schema-h2.sql +git commit -m "test: switch H2 to PostgreSQL mode, IDENTITY/TEXT adaptations" +``` + +### Task 13: Testcontainers PG17 集成测试(JSONB / upsert / 向量) + +**Files:** +- Modify: `eify-app/pom.xml`(新增 testcontainers 依赖,test scope) +- Create: `eify-app/src/test/java/com/eify/app/integration/PgIntegrationTest.java` +- Create: `eify-app/src/test/resources/application-pgtest.yml` + +- [ ] **Step 1: 加 Testcontainers 依赖** + +`eify-app/pom.xml` 新增(test scope,版本走父 pom 或显式): + +```xml + + org.testcontainers + postgresql + test + + + org.springframework.boot + spring-boot-testcontainers + test + +``` + +- [ ] **Step 2: 写集成测试(@ServiceConnection 起 pg17)** + +覆盖 spec §5.2 三类盲区。Flyway 启用,跑真实 V1__init.sql: + +```java +package com.eify.app.integration; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.testcontainers.service.connection.ServiceConnection; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.test.context.ActiveProfiles; +import org.testcontainers.containers.PostgreSQLContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; + +import static org.assertj.core.api.Assertions.assertThat; + +@Testcontainers +@SpringBootTest +@ActiveProfiles("pgtest") +@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) +class PgIntegrationTest { + + @Container + @ServiceConnection + static PostgreSQLContainer pg = new PostgreSQLContainer<>("pgvector/pgvector:pg17"); + + @Autowired JdbcTemplate jdbcTemplate; + + @Test + void identitySequenceSurvivesSeedInsert() { + // 种子 id=1 已由 Flyway 写入;新插入不应撞主键 + Long id = jdbcTemplate.queryForObject( + "INSERT INTO ai_workspace (name, description) VALUES ('t','t') RETURNING id", Long.class); + assertThat(id).isGreaterThan(1L); + } + + @Test + void jsonbArrowQueryWithCast() { + jdbcTemplate.update("INSERT INTO ai_workflow (id, name, status) VALUES (900,'wf',1) ON CONFLICT (id) DO NOTHING"); + jdbcTemplate.update("INSERT INTO ai_workflow_node (workflow_id, node_key, type, config) " + + "VALUES (900,'n1','llm', '{\"providerId\": 42}'::jsonb)"); + Integer cnt = jdbcTemplate.queryForObject( + "SELECT COUNT(*) FROM ai_workflow_node WHERE (config->>'providerId')::bigint = 42", Integer.class); + assertThat(cnt).isEqualTo(1); + } + + @Test + void onConflictUpsert() { + jdbcTemplate.update("INSERT INTO provider_health (provider_id, status) VALUES (5,'UP') " + + "ON CONFLICT (provider_id) DO UPDATE SET status = EXCLUDED.status"); + jdbcTemplate.update("INSERT INTO provider_health (provider_id, status) VALUES (5,'DOWN') " + + "ON CONFLICT (provider_id) DO UPDATE SET status = EXCLUDED.status"); + String s = jdbcTemplate.queryForObject( + "SELECT status FROM provider_health WHERE provider_id = 5", String.class); + assertThat(s).isEqualTo("DOWN"); + } + + @Test + void vectorCosineSearch() { + jdbcTemplate.update("INSERT INTO document_chunk (knowledge_id, document_id, chunk_index, content, embedding, chunk_hash) " + + "VALUES (1,1,0,'hello','[1,0,0]'::vector, 'h1')"); + Double sim = jdbcTemplate.queryForObject( + "SELECT 1 - (embedding <=> '[1,0,0]'::vector) FROM document_chunk WHERE chunk_hash='h1'", Double.class); + assertThat(sim).isGreaterThan(0.99); + } +} +``` + +- [ ] **Step 3: 写 application-pgtest.yml(启用 Flyway,profile 隔离)** + +```yaml +spring: + sql: + init: + mode: never +eify: + flyway: + mysql: + enabled: true # 复用主 Flyway 程序化迁移,对 Testcontainers PG 跑 V1__init.sql +``` + +> `@ServiceConnection` 自动把容器 jdbcUrl/账号注入 `spring.datasource.*`,无需手填。 + +- [ ] **Step 4: 跑集成测试** + +Run: `mvn test -pl eify-app -Dtest=PgIntegrationTest -q` +Expected: PASS(需本机 Docker)。4 个用例全绿即证明 IDENTITY 回填、JSONB `->>::bigint`、`ON CONFLICT`、向量检索在真实 PG17 工作。 + +- [ ] **Step 5: Commit** + +```bash +git add eify-app/pom.xml eify-app/src/test/ +git commit -m "test: add Testcontainers PG17 coverage for JSONB/upsert/vector" +``` + +--- + +## Phase 7 — 全量验证 + +### Task 14: 全量编译、测试、方言守卫、容器冒烟 + +**Files:** +- Create: `scripts/check_sql_dialect.sh` + +- [ ] **Step 1: 写 CI 方言守卫脚本(spec §7.2)** + +```bash +#!/bin/bash +if grep -rnE "ON DUPLICATE KEY|JSON_EXTRACT|INFORMATION_SCHEMA|MODIFY COLUMN" eify-*/src/main/java/; then + echo "🚨 发现 MySQL 方言残留(破窗效应)!请使用 PostgreSQL 原生语法。" + exit 1 +else + echo "✅ SQL 方言检查通过。" + exit 0 +fi +``` + +- [ ] **Step 2: 跑方言守卫,必须通过** + +Run: `bash scripts/check_sql_dialect.sh` +Expected: `✅ SQL 方言检查通过。` 且 exit 0。(Phase 2 已清理全部 4 处 + 注释/日志方言字眼) + +- [ ] **Step 3: 全量后端测试** + +Run: `mvn test -q` +Expected: 全模块 PASS。 + +- [ ] **Step 4: 前端验证(零改动,确认无连带破坏)** + +Run: `cd eify-web && npx vue-tsc --noEmit && npx vitest run && cd ..` +Expected: 类型检查 + 单测全绿。 + +- [ ] **Step 5: PG17 容器端到端冒烟** + +```bash +docker compose -f deploy/infra/deploy/docker-compose.yml up -d pgvector +# 等 healthy 后启动应用(dev profile,PG_URL 指向本地容器) +PG_URL=jdbc:postgresql://localhost:5432/eify?stringtype=unspecified \ +PG_USERNAME=postgres PG_PASSWORD= \ +mvn spring-boot:run -pl eify-app -Dspring-boot.run.profiles=dev +``` + +Expected: 日志出现 Flyway 迁移成功(`Successfully applied 1 migration`)、应用启动无报错;`/api` 健康检查可用。验证完 `docker compose ... down`。 + +- [ ] **Step 6: 最终 grep 复查无 MySQL 残留** + +Run: `grep -rniE "mysql|druid|jdbc:mysql" eify-app/src/main/resources/ eify-app/pom.xml` +Expected: 无业务残留(注释引用允许,但配置/依赖应为 0)。 + +- [ ] **Step 7: Commit** + +```bash +git add scripts/check_sql_dialect.sh +git commit -m "ci: add SQL dialect regression guard, finalize PG17 migration" +``` + +--- + +## 验收对照(spec §8) + +- [ ] PG17 单库承载 23 张业务表 + `document_chunk`,Flyway 启动成功(Task 1, 14-S5) +- [ ] `mvn test` 全绿 + Testcontainers 覆盖 JSONB/upsert/向量(Task 12, 13) +- [ ] 前端 `vitest`+`vue-tsc` 通过(Task 14-S4) +- [ ] 无 MySQL 残留(mysql-connector/Druid/ON DUPLICATE/JSON_EXTRACT/INFORMATION_SCHEMA)(Task 14-S2,S6) +- [ ] CI grep 守卫生效(Task 14-S1,S2) +- [ ] 主键 ID 回填、`updated_at` 自动刷新经集成测试确认(Task 13) +- [ ] 配置无硬编码 IP/密码,遵循 `${ENV_VAR:默认值}`(Task 9-11) +- [ ] DATABASE.md Flyway 幂等模板已更新为 PG 版(Task 2) diff --git a/docs/specs/2026-06-05-mysql-to-pg17-migration-design.md b/docs/specs/2026-06-05-mysql-to-pg17-migration-design.md new file mode 100644 index 0000000..081ba52 --- /dev/null +++ b/docs/specs/2026-06-05-mysql-to-pg17-migration-design.md @@ -0,0 +1,258 @@ +# MySQL 8.0 → PostgreSQL 17 迁移设计 + +> **类型**:功能实现规格说明(Spec) +> **日期**:2026-06-05 +> **状态**:已评审,待实现 +> **范围**:主业务库 MySQL 8.0 → PG17;pgvector pg16 → pg17;业务表与向量表合并为 PG17 单库 + +--- + +## 1. 背景与目标 + +### 1.1 动机 + +- **统一技术栈 / 减组件**:业务库(MySQL)与向量库(pgvector)合并到同一 PG17 实例,去掉一套 SQL 数据源与一套 Flyway。 +- **要 PG 特有能力**:RAG 向量表与业务表同库后,可同事务、可按 `workspace_id` JOIN,降低 RAG 检索的数据隔离管控成本。 +- **License / 栈标准**:统一到 PostgreSQL。 +- **趁早切换(MVP 阶段)**:当前数据量小,切换成本最低,避免后期数据增长后迁移困难。 + +### 1.2 目标架构 + +| | 之前 | 之后 | +|:---|:---|:---| +| 业务库 | MySQL 8.0(Druid 数据源) | **PG17 单库**(HikariCP 单数据源) | +| 向量库 | pgvector pg16(独立 HikariCP + 独立 Flyway) | **并入同一 PG17 库**(`document_chunk` 同库) | +| SQL 数据源 | 2 个(Druid 主 + HikariCP 向量) | **1 个**(HikariCP) | +| Flyway | 2 套(`flyway_schema_history` + `_pg`) | **1 套** | +| 日志存储 | ClickHouse | ClickHouse(**不变**) | +| 缓存 | Redis | Redis(**不变**) | + +### 1.3 非目标(明确不做) + +- 不迁 ClickHouse(列存 OLAP,保留分析能力)。 +- 不迁 Redis(内存缓存,保留低延迟)。 +- 不做数据迁移(**干净切换、丢弃旧数据**,无需 ora2pg/pgloader 等工具链)。 +- 不重构业务逻辑(仅替换底层方言)。 +- 不保留 MySQL 兼容回退路径(物理隔离用 Git 分支,回滚靠 `git revert` 整版)。 +- 本期不建 JSONB GIN 索引(仅做 `JSON → JSONB` 类型转换,为将来留路;待迁移稳定后按慢查询场景单独加)。 + +--- + +## 2. Schema 重写(DDL 方言转换) + +将 `eify-app/src/main/resources/db/migration/V1__init.sql`(23 张表)整体重写为 PG17 方言,落到统一迁移目录 `db/migration`(PG 版)。`document_chunk` 向量表(原 `db/migration-pg/V1__init_pgvector.sql`)并入同一套迁移。 + +### 2.1 类型与语法转换规则 + +| MySQL | PG17 | 备注 | +|:---|:---|:---| +| `BIGINT UNSIGNED AUTO_INCREMENT` | `BIGINT GENERATED BY DEFAULT AS IDENTITY` | PG 无 UNSIGNED;用 `BY DEFAULT` 而非 `ALWAYS`,以允许种子数据显式插入 id(见 §2.4);实体 `@TableId(type=IdType.AUTO)` 仍兼容 | +| `TINYINT`(status/deleted/enabled/stream_enabled 等) | `SMALLINT` | PG 无 TINYINT;实体类 `Integer` 字段不变 | +| `INT UNSIGNED` / `BIGINT UNSIGNED`(非主键) | `INT` / `BIGINT` | 去 UNSIGNED | +| `DECIMAL(3,2)` | `NUMERIC(3,2)` | temperature/top_p/penalty 等 | +| `DATETIME ... ON UPDATE CURRENT_TIMESTAMP` | `TIMESTAMPTZ`(去 ON UPDATE) | 更新时间靠 `FieldMetaObjectHandler` 的 `strictUpdateFill` 填充,行为不变 | +| `JSON` | `JSONB` | auth_config / extra_params / metadata / variables / config 等 | +| `TEXT` | `TEXT` | 不变 | +| `` `反引号标识符` `` | 裸标识符(小写) | 配合 `map-underscore-to-camel-case` | +| 内联 `KEY idx (col)` | 独立 `CREATE INDEX IF NOT EXISTS` | | +| 前缀索引 `refresh_token(64)` | 整列索引 `idx_refresh_token(refresh_token)` | PG 不支持前缀索引 | +| `ENGINE=InnoDB CHARSET/COLLATE` | 删除 | PG 库级 UTF-8 | +| 列内 `COMMENT '...'` | `COMMENT ON COLUMN ...` 或省略 | 保留关键注释 | +| 种子 `INSERT ... ON DUPLICATE KEY UPDATE` | `INSERT ... ON CONFLICT (key) DO UPDATE` | admin 用户 / 默认工作空间种子数据 | + +### 2.2 向量表并入 + +- `CREATE EXTENSION IF NOT EXISTS vector;` 保留。 +- `document_chunk` 表与 HNSW 向量索引(`vector_cosine_ops`)不变,与业务表同库。 +- 向量维度策略沿用 `migration-pg/V2__flexible_vector_dimension.sql` 的现有逻辑。 + +### 2.3 Flyway 幂等性(更简单) + +PG 原生支持 `CREATE TABLE/INDEX IF NOT EXISTS`、`ADD COLUMN IF NOT EXISTS`,**不再需要** MySQL 的 `INFORMATION_SCHEMA` 检查样板。`docs/guides/DATABASE.md` 的 Flyway 幂等模板需同步更新为 PG 版。 + +### 2.4 种子数据与 IDENTITY 序列同步(关键坑) + +`V1__init.sql` 种子数据**显式插入固定主键**(`ai_user` id=1 admin、`ai_workspace` id=1 System Workspace),且全表 `workspace_id DEFAULT 1` 依赖这个确定 id,因此 id 必须固定、不能交给自增。 + +PG 的 IDENTITY 列处理与 MySQL `AUTO_INCREMENT` 不同: + +- 列定义必须用 `GENERATED BY DEFAULT AS IDENTITY`(非 `ALWAYS`),否则 `INSERT ... (id) VALUES (1)` 直接报错。 +- **即使显式插入成功,IDENTITY 底层序列也不会自动前进**——下一条自增仍从 1 开始,撞主键。 +- 因此种子数据插入后,必须对涉及显式 id 的表执行序列同步。**注意空表兜底**:若表无数据,`MAX(id)` 返回 `NULL` 会使 `setval` 报错并中断 Flyway,必须用 `COALESCE` 兜底: + ```sql + SELECT setval(pg_get_serial_sequence('ai_user', 'id'), COALESCE((SELECT MAX(id) FROM ai_user), 1)); + SELECT setval(pg_get_serial_sequence('ai_workspace', 'id'), COALESCE((SELECT MAX(id) FROM ai_workspace), 1)); + ``` +- 种子 upsert 用 `ON CONFLICT (id) DO UPDATE`(替代 MySQL `ON DUPLICATE KEY UPDATE`)。 +- 此 `setval` 步骤是 §8 "ID 回填"验证项的前置条件,集成测试需覆盖:种子插入后新建一条记录,确认 id 不与种子冲突。 + +--- + +## 3. 原生 SQL 方言翻译(穷尽清单) + +经全仓 grep 勘察确证:**XML mapper 仅 1 个**(`ConversationMapper.xml`,游标分页 `LIMIT`/`ORDER BY DESC`/`updated_at < ?`,全可移植,零改动);真正的方言坑全在 `@Select/@Insert/@Update` 注解和一处运行时 `JdbcTemplate` DDL。**总计仅 4 处需人工翻译**。 + +> ⚠️ ora2pg / pgloader 等工具只翻译 schema 与数据,**无法**翻译嵌入 Java 字符串里的注解 SQL。这 4 处必须人工逐个翻译。 + +| # | 文件:行 | MySQL 写法 | PG17 翻译 | 风险 | +|:---|:---|:---|:---|:---| +| 1 | `ProviderHealthMapper.java:18` | `ON DUPLICATE KEY UPDATE x = VALUES(x)` | `ON CONFLICT (provider_id) DO UPDATE SET x = EXCLUDED.x` | 中(热路径 upsert,依赖 `uk_provider_id`) | +| 2 | `AgentKnowledgeMapper.java:28` | 批量 `ON DUPLICATE KEY UPDATE deleted=0` | `ON CONFLICT (agent_id, knowledge_id) DO UPDATE SET deleted=0, updated_at=NOW()` | 中(依赖 `uk_agent_knowledge` 唯一约束) | +| 3 | `ProviderMapper.java:21` | `JSON_EXTRACT(n.config,'$.providerId') = #{providerId}` | `(n.config->>'providerId')::bigint = #{providerId}` | **高**(`->>` 返回 text,必须 `::bigint` 强转,否则 `operator does not exist: text = bigint`) | +| 4 | `McpServerMapper.java:16` | `JSON_EXTRACT(n.config,'$.serverId') = #{serverId}` | `(n.config->>'serverId')::bigint = #{serverId}` | **高**(同上) | + +> 注:`AgentMcpToolMapper.batchInsert`(`eify-mcp`,line 25)经核查是纯 `INSERT ... VALUES`(配合 `deleteByAgentId` 的"先删后插"模式),仅含可移植的 `NOW()`,**非方言坑,零改动**。 + +### 3.1 删除项 + +- `ChatController.java:220-242` `fixDatabase()`:临时开发接口,纯 MySQL(`INFORMATION_SCHEMA TABLE_SCHEMA='eify'` + 反引号 `ALTER ... MODIFY COLUMN JSON NULL`),且是无防护的 DDL 端点。**直接删除**——新 schema 中 `metadata JSONB` 本就可空。对应测试 `ChatControllerTest` 中 `jdbcTemplate` 注入相关用例一并清理。 + +### 3.2 清理吞异常的技术债 + +- `ProviderServiceImpl.java:286`:现用 `try/catch` 吞掉 `JSON_EXTRACT` 异常(注释"可能是不支持 JSON_EXTRACT 的数据库")。翻译为 `->>` 后,此处吞异常逻辑需重新评估——PG 下 JSONB 查询应正常工作,不应再静默咽掉错误。改由 Testcontainers 集成测试覆盖(见 §5)。 + +### 3.3 明确不算坑(避免范围扩大) + +- `NOW()`:PG 原生支持,零改动(全仓大量出现)。 +- `LIMIT #{x}`、`IN `、`COUNT(*)`、`(#{x} IS NULL OR col=#{x})`:全可移植。 +- MyBatis-Plus 逻辑删除、`id-type: AUTO`、分页插件:原生支持 PG。 + +### 3.4 注释 / 日志中的方言字眼(避免 CI 守卫误报) + +§7.2 的 grep 守卫扫的是全文本,会连**注释和日志字符串**里的方言词一起命中。翻译 SQL 时需同步清理这些残留,否则 CI 误报红: + +- `ProviderHealth.java:17`、`ProviderHealthMapper.java:15`:Javadoc 中"使用 ON DUPLICATE KEY UPDATE …" → 改为"使用 ON CONFLICT …"。 +- `ProviderServiceImpl.java:286`:日志"可能是不支持 JSON_EXTRACT 的数据库" → 随 §3.2 重构一并清理。 + +--- + +## 4. 数据源、Flyway 与连接池收口 + +### 4.1 单数据源(去双源) + +- 删除 `PgVectorConfig`(`eify-knowledge`,独立 HikariCP)与 `PgFlywayConfig`(`eify-app`,独立 Flyway + `flyway_schema_history_pg`)。 +- `pgJdbcTemplate` 改为指向主数据源(同库)。`ChunkRepository`(`eify-knowledge`,line 31)的向量读写仍走 `JdbcTemplate` + pgvector-java 编码 `VECTOR`,但已是同库,可与业务表同事务、按 `workspace_id` JOIN。 + +### 4.2 连接池:Druid → HikariCP + +- 移除 `druid-spring-boot-4-starter` 依赖与 `application.yml` 中整段 `druid:` 配置(连接池 + `stat-view-servlet` + `filter` + `connectionProperties`),以及 dev 的 Druid 监控面板配置。 +- 改用 Spring Boot 默认 HikariCP,连接池参数用 `spring.datasource.hikari.*` 表达(`max-active → maximum-pool-size`,`max-wait → connection-timeout`,`min-idle → minimum-idle` 等)。 +- **取舍(已确认接受)**:失去 Druid 监控面板与 slow-SQL stat filter。慢查询监控改由 PG 侧 `log_min_duration_statement` 或现有 SQL 日志切面(`sql.logging.*`)承担。 + +### 4.3 单 Flyway + +- 保留主 Flyway 自动配置但指向 PG,迁移目录统一 `db/migration`(PG 方言)。 +- `flyway-mysql` 依赖移除,`flyway-database-postgresql` 保留(已存在于 `eify-app/pom.xml`)。 + +--- + +## 5. 测试策略(混合:H2 为主 + Testcontainers 专攻) + +采用 Spring Boot 4.x 生态下处理数据库强绑定特性的黄金标准:**H2 求速度,Testcontainers 求保真**。 + +### 5.1 H2(PostgreSQL 模式)—— 承担大头 + +- `application-test.yml`:H2 URL 改 `jdbc:h2:mem:testdb;MODE=PostgreSQL;DATABASE_TO_LOWER=TRUE`;移除 `datasource-pgvector` 块。 +- `schema-h2.sql`:已是独立、去 MySQL 化 schema,微调为 PG 兼容(identity 语法、`SMALLINT` 等)。 +- 向量表:H2 PG 模式不支持 `VECTOR` 类型,`document_chunk` 在测试 schema 桩为 `TEXT` 或跳过(与现状一致)。 +- Service 单测(`@ExtendWith(MockitoExtension.class)`)不碰库,零影响。 + +### 5.2 Testcontainers PG17 + pgvector —— 精准打击保真盲区 + +仅覆盖 H2 表达不了的三类路径: + +1. 两条 `->>` JSONB 等值查询(`ProviderMapper` / `McpServerMapper`)——消灭 §3.2 的吞异常盲区。 +2. `ON CONFLICT` upsert(`ProviderHealthMapper` / `AgentKnowledgeMapper`)。 +3. 向量检索(`ChunkRepository` 的 HNSW 余弦查询)。 + +- 镜像:`pgvector/pgvector:pg17`。 +- 连接注入:使用 Spring Boot 的 `@ServiceConnection`,免去手动 `container.getJdbcUrl()` 塞环境变量的样板。 +- CI:单测仍走 H2(毫秒级反馈),Testcontainers 用例需 Docker,归入集成测试分组。 + +### 5.3 验证基线 + +- `mvn test`(全模块)+ 前端 `npx vitest run` + `npx vue-tsc --noEmit` 全绿(前端零改动,应直接通过)。 +- 起 PG17 容器冒烟:应用启动 Flyway 成功 + 关键接口可用。 + +--- + +## 6. 配置与部署 + +### 6.1 pom.xml + +- 移除:`mysql-connector-j`、`flyway-mysql`、`druid-spring-boot-4-starter`。 +- 保留:`postgresql`、`pgvector`、`flyway-database-postgresql`、`mybatis-plus-spring-boot4-starter`。 +- 新增(测试):Testcontainers PostgreSQL 模块(test scope)。 + +### 6.2 application*.yml(dev / test / staging / prod) + +- `url` → `jdbc:postgresql://...`,`driver-class-name` → `org.postgresql.Driver`。 +- 去掉 MySQL 专属参数(`serverTimezone` / `characterEncoding` / `zeroDateTimeBehavior` / `allowPublicKeyRetrieval`)。 +- 保留 `stringtype=unspecified`(JSONB 写入需要)。 +- 删除 `spring.datasource-pgvector` 整块(合并入主数据源)。 +- 遵循 `${ENV_VAR:默认值}` 规范,无硬编码 IP / 密码。 + +### 6.3 docker-compose.yml + +- 删除 `mysql` 服务与 `mysql-data` 卷。 +- `pgvector` 镜像 `pgvector/pgvector:pg16` → `pg17`,承载业务库 + 向量库(单库)。 +- backend 环境变量 `MYSQL_*` 改为 `PG_*`(`PG_HOST` / `PG_PORT` / `PG_DATABASE` / `PG_USERNAME` / `PG_PASSWORD`),单库后与向量库共用同一组;`depends_on` 去 mysql、留 pgvector。 + +--- + +## 7. 风险、回滚与交付顺序 + +### 7.1 主要风险 + +| 风险 | 缓解 | +|:---|:---| +| DDL 转换遗漏(UNSIGNED 边界、JSON→JSONB 需 `stringtype=unspecified`) | 应用启动 Flyway 成功 + 全量测试兜底 | +| 4 处注解 SQL 翻译错误(尤其 `->>` 漏 `::bigint`) | Testcontainers PG17 集成测试精准覆盖(§5.2) | +| `ON UPDATE CURRENT_TIMESTAMP` 语义改由 MyBatis-Plus 填充 | 集成测试覆盖一条 update 路径,确认 `updated_at` 刷新 | +| `@TableId(IdType.AUTO)` + IDENTITY 主键回填 | **验证项**:confirm insert 后 PgJDBC 经 `RETURNING`/`getGeneratedKeys` 正确回填 ID 到 Java 对象 | +| 失去 Druid 监控 | 已记录取舍,PG 侧 `log_min_duration_statement` + SQL 日志切面补位 | +| `main` 上方言回退(破窗效应) | CI grep 守卫脚本(§7.2) | + +### 7.2 CI 防回归守卫 + +新增 `scripts/check_sql_dialect.sh`,在 GitHub Actions / pre-commit 跑。一旦 `src/main/java/` 重新出现方言残留,CI 直接 fail。用退出码判断(比短路链 `&& ... || ...` 鲁棒,避免在精简 runner 上掩盖执行错误): + +```bash +#!/bin/bash +if grep -rnE "ON DUPLICATE KEY|JSON_EXTRACT|INFORMATION_SCHEMA|MODIFY COLUMN" eify-*/src/main/java/; then + echo "🚨 发现 MySQL 方言残留(破窗效应)!请使用 PostgreSQL 原生语法。" + exit 1 +else + echo "✅ SQL 方言检查通过。" + exit 0 +fi +``` + +(反引号标识符因 Java 文本转义复杂,可作为可选追加模式。) + +### 7.3 回滚 + +干净切换 + Git 分支物理隔离。MySQL 相关改动集中在迁移分支,回滚即 `git revert` 整版,代码中零双方言逻辑。旧 MySQL 容器数据卷在验证通过前不删。 + +### 7.4 交付顺序(自底向上) + +1. **Schema 重写**:`db/migration` PG 版 DDL(含 `document_chunk` 并入)+ 同步 `docs/guides/DATABASE.md` 幂等模板。 +2. **数据访问层翻译**:4 处注解 SQL(§3)+ 删除 `fixDatabase()`(§3.1)+ 清理吞异常(§3.2)。 +3. **数据源 / Flyway / 连接池改造**:删双源、Druid→HikariCP、单 Flyway(§4)。 +4. **配置文件四环境切换**(§6.2)。 +5. **docker-compose 改造**(§6.3)。 +6. **测试切 PG**:H2 PG 模式 + 新增 Testcontainers 用例(§5)。 +7. **全量验证**:编译 + 全量测试 + PG17 容器冒烟(§5.3)。 + +--- + +## 8. 验收标准 + +- [ ] PG17 单库承载全部 23 张业务表 + `document_chunk`,应用启动 Flyway 成功。 +- [ ] `mvn test` 全模块通过;新增 Testcontainers 用例覆盖 JSONB 查询 / upsert / 向量检索。 +- [ ] 前端 `vitest` + `vue-tsc` 通过(前端零改动)。 +- [ ] 代码中无 MySQL 残留(mysql-connector / Druid / `ON DUPLICATE` / `JSON_EXTRACT` / `INFORMATION_SCHEMA`)。 +- [ ] CI grep 守卫脚本生效。 +- [ ] 主键 ID 回填、`updated_at` 自动刷新经集成测试确认。 +- [ ] 配置无硬编码 IP / 密码,遵循 `${ENV_VAR:默认值}`。 +- [ ] `docs/guides/DATABASE.md` Flyway 幂等模板已更新为 PG 版。 diff --git a/eify-agent/src/main/java/com/eify/agent/mapper/AgentKnowledgeMapper.java b/eify-agent/src/main/java/com/eify/agent/mapper/AgentKnowledgeMapper.java index a7d5516..a373c71 100644 --- a/eify-agent/src/main/java/com/eify/agent/mapper/AgentKnowledgeMapper.java +++ b/eify-agent/src/main/java/com/eify/agent/mapper/AgentKnowledgeMapper.java @@ -27,7 +27,7 @@ public interface AgentKnowledgeMapper extends BaseMapper { @Insert("") + "ON CONFLICT (agent_id, knowledge_id) DO UPDATE SET deleted = 0, updated_at = NOW()") int upsertKnowledgeIds(@Param("agentId") Long agentId, @Param("knowledgeIds") List knowledgeIds); @Update("