{"repo": "ba1uga/resource-center", "files": {"README.md": {"b8b80f59949f1a012f5c0b5dd8e6b305333bdd5d": {"t": 1785746605.3732119, "n": 23013, "h": "9add9785549996af", "c": "# resource-center(资源中台)\n\n> 基于大模型的个性化资源生成与学习多智能体系统 —— 资源中台子系统\n\n资源中台是 AI 驱动智能学习平台的**核心资源管理枢纽**,负责教学资源的全生命周期管理,并通过多策略 AI 引擎将资源智能挂载到教育知识图谱中,实现\"教育资源 → 知识体系节点\"的语义映射。\n\n---\n\n## 目录\n\n- [1. 项目介绍](#1-项目介绍)\n- [2. 系统架构](#2-系统架构)\n- [3. 技术栈](#3-技术栈)\n- [4. 项目结构](#4-项目结构)\n- [5. 环境要求](#5-环境要求)\n- [6. 快速开始](#6-快速开始)\n- [7. 中间件启动](#7-中间件启动)\n- [8. 数据库初始化](#8-数据库初始化)\n- [9. 项目启动](#9-项目启动)\n- [10. API 文档](#10-api-文档)\n- [11. AI 功能说明](#11-ai-功能说明)\n- [12. 开发规范](#12-开发规范)\n- [13. 常见问题](#13-常见问题)\n- [14. 后续规划](#14-后续规划)\n\n---\n\n## 1. 项目介绍\n\n### 1.1 项目定位\n\n资源中台是整个《基于大模型的个性化资源生成与学习多智能体系统》的**资源管理子系统**,向上对接 AI 教学智能体,向下管理各类教学资源的存储、解析、向量化和语义挂载。\n\n### 1.2 核心职责\n\n| 职责 | 说明 |\n|---|---|\n| 教材管理 | 教材元数据 CRUD、版本管理、资产关联 |\n| 课件管理 | PPT/PDF/DOC 课件上传、解析、全文检索 |\n| 视频管理 | 视频资源元数据、处理状态、发布管理 |\n| 题库管理 | 选择题/填空题/简答题/编程题全类型管理 |\n| 大纲管理 | 课程大纲版本管理、完成度追踪、归档 |\n| 资源解析 | PDF/Word/PPT/Markdown 自动解析提取全文 |\n| 智能分块 | 语义分块、固定大小分块、题目分块 |\n| 向量化 | BGE-large-zh-v1.5 中文嵌入(1024维) |\n| AI 资源挂载 | 多策略融合引擎,将资源自动挂载到知识图谱节点 |\n| 人工审核 | 挂载结果审核、修正、反馈收集 |\n\n### 1.3 AI 能力总览\n\n- **多策略挂载引擎**:规则引擎 + 向量语义匹配 + LLM 推理 + 关键词匹配,四种策略加权融合\n- **RAG 检索增强**:基于 pgvector 的知识点语义检索,为 LLM 提供候选知识点上下文\n- **文档智能解析**:支持 PDF、Word (.docx)、PPT (.pptx)、Markdown 四种格式的自动解析\n- **语义分块**:按段落检测内容类型(定义/示例/练习/总结),生成结构化 Chunk\n- **反馈闭环**:人工审核结果导出 JSONL,支持后续模型微调\n\n---\n\n## 2. 系统架构\n\n### 2.1 整体架构\n\n```\n┌──────────────────────────────────────────────────────────────┐\n│ resource-center (Monorepo) │\n├──────────────────────────────────────────────────────────────┤\n│ │\n│ ┌─────────────────┐ ┌──────────────────┐ ┌────────────┐ │\n│ │ Vue 3 Frontend │ │ Spring Boot │ │ Embedding │ │\n│ │ (Port 5175) │ │ Backend (8080) │ │ Service │ │\n│ │ │ │ │ │ (8000) │ │\n│ │ TypeScript │ │ Java 17 │ │ Python │ │\n│ │ Vite 8 │ │ MyBatis-Plus │ │ FastAPI │ │\n│ │ Pinia │ │ Spring Boot 3.5 │ │ BGE-zh │ │\n│ └────────┬────────┘ └───────┬──────────┘ └─────┬──────┘ │\n│ │ │ │ │\n│ │ /api/* proxy │ │ │\n│ ├──────────────────►│ │ │\n│ │ │ HTTP /embed │ │\n│ │ ├───────────────────►│ │\n│ │ │ │ │\n└───────────┼───────────────────┼────────────────────┼─────────┘\n │ │ │\n ▼ ▼ ▼\n ┌──────────┐ ┌──────────────┐ ┌──────────────┐\n │ Browser │ │ MySQL :3306 │ │ PostgreSQL │\n │ (User) │ │ (Business) │ │ :5432 │\n └──────────┘ └──────────────┘ │ (pgvector) │\n └──────────────┘\n```\n\n### 2.2 后端模块架构\n\n```\ncom.baluga.backend\n├── common/ # 全局共享\n│ ├── api/R.java # 统一响应体\n│ └── exception/ # 全局异常处理\n├── config/ # Spring 配置\n│ ├── MybatisPlusConfig # MyBatis-Plus + 分页\n│ ├── PgVectorConfig # pgvector 独立数据源\n│ └── WebMvcConfig # CORS\n├── infrastructure/ # 技术基础设施\n│ └── integration/\n│ ├── ai/ # AI 匹配提供者(DeepSeek)\n│ ├── chunking/ # 文本分块策略\n│ ├── embedding/ # 嵌入服务客户端\n│ ├── matching/ # 资源收集器\n│ └── parsing/ # 文档解析器(PDF/Word/PPT/MD)\n└── modules/ # 业务模块\n ├── textbook/ # 教材管理\n ├── outline/ # 大纲版本管理\n ├── courseware/ # 课件管理\n ├── question/ # 题库管理\n ├── video/ # 视频管理\n ├── storage/ # 文件上传\n ├── mapping/ # 资源映射(批量 AI 匹配)\n └── mount/ # AI 挂载引擎(多策略融合)\n ├── engine/ # 规则/LLM/嵌入/融合引擎\n ├── orchestrator/ # 6 阶段挂载编排器\n ├── rag/ # RAG 知识点检索器\n ├── task/ # 异步任务管理\n ├── review/ # 人工审核\n └── feedback/ # 反馈收集(JSONL 导出)\n```\n\n### 2.3 AI 挂载流水线\n\n```\n MountOrchestrator\n │\n ┌───────────────┼───────────────┐\n ▼ ▼ ▼\n 1. 文档解析 2. 语义分块 3. 构建上下文\n (PDF/Word/ (Semantic/ (ResourceContext\n PPT/Markdown) Fixed/Question) + KnowledgeGraphScope)\n │ │ │\n └───────────────┼───────────────┘\n ▼\n 4. 多策略融合\n ┌───────────────┼───────────────┐\n ▼ ▼ ▼\n RuleBased Embedding LLMReasoning\n (精确规则) (向量语义) (DeepSeek推理)\n │ │ │\n └───────────────┼───────────────┘\n ▼\n FusionMountEngine\n (加权融合: R=1.5, E=2.0, L=4.0, K=1.5)\n │\n ▼\n ConfidenceCalculator\n (4维置信度计算)\n │\n ┌────────────┼────────────┐\n ▼ ▼ ▼\n score≥0.85 0.4≤score<0.85 score<0.4\n 自动挂载 人工审核 丢弃\n │ │\n ▼ ▼\n ResourceMountRelation MountReviewRecord\n │\n ▼\n FeedbackCollector\n (JSONL 训练数据导出)\n```\n\n### 2.4 RAG 检索架构\n\n```\n资源文本\n │\n ▼\nEmbeddingService (BGE-large-zh-v1.5, 1024维)\n │\n ▼\npgvector 余弦相似度检索 (<=>)\n │\n ▼\nTop-K 相似知识点 (默认 Top-15)\n │\n ▼\nLLM 推理上下文 (DeepSeek Chat)\n │\n ▼\n挂载决策\n```\n\n---\n\n## 3. 技术栈\n\n### 3.1 后端\n\n| 技术 | 版本 | 说明 |\n|---|---|---|\n| Java | 17 | 编译目标版本 |\n| Spring Boot | 3.5.14 | 应用框架 |\n| Spring Framework | 6.2.18 | 核心框架 |\n| MyBatis-Plus | 3.5.16 | ORM + 分页 + 逻辑删除 |\n| MySQL Connector | 9.7.0 | MySQL 驱动 |\n| PostgreSQL Driver | 42.7.10 | pgvector 数据源驱动 |\n| HikariCP | 6.3.3 | 数据库连接池 |\n| Apache POI | 5.3.0 | Word/PPT 文档解析 |\n| Apache PDFBox | 3.0.3 | PDF 文档解析 |\n| Flexmark | 0.64.8 | Markdown 解析 |\n| Apache Tika | 2.9.2 | 文件元数据提取 |\n| Lombok | 1.18.46 | 代码简化 |\n| Jackson | 2.21.2 | JSON 序列化 |\n| Tomcat | 10.1.54 | 嵌入式 Web 容器 |\n| JUnit Jupiter | 5.12.2 | 单元测试 |\n| Mockito | 5.17.0 | 测试 Mock |\n\n### 3.2 前端\n\n| 技术 | 版本 | 说明 |\n|---|---|---|\n| Vue | 3.5.31 | UI 框架 |\n| TypeScript | 6.0 | 类型系统 |\n| Vite | 8.0.3 | 构建工具 |\n| Pinia | 3.0.3 | 状态管理 |\n| Vue Router | 4.6.3 | 路由管理 |\n| Node.js | ≥20.19.0 / ≥22.12.0 | 运行时 |\n\n### 3.3 AI / 嵌入服务\n\n| 技术 | 版本 | 说明 |\n|---|---|---|\n| Python | 3.10+ | 嵌入服务运行时 |\n| FastAPI | ≥0.110.0 | Python Web 框架 |\n| sentence-transformers | ≥3.0.0 | 嵌入模型框架 |\n| BGE-large-zh-v1.5 | - | 中文嵌入模型(1024维) |\n| Uvicorn | ≥0.29.0 | ASGI 服务器 |\n\n### 3.4 数据库与中间件\n\n| 中间件 | 版本要求 | 端口 | 用途 |\n|---|---|---|---|\n| MySQL | 8.0+ | 3306 | 业务数据主库 |\n| PostgreSQL | 15+ (需 pgvector 扩展) | 5432 | 向量嵌入存储与检索 |\n| DeepSeek API | - | HTTPS | LLM 推理服务 |\n\n---\n\n## 4. 项目结构\n\n```\nresource-center/\n├── backend/ # Spring Boot 后端\n│ ├── pom.xml # Maven 配置\n│ ├── mvnw / mvnw.cmd # Maven Wrapper\n│ ├── STRUCTURE.md # 后端结构说明\n│ └── src/\n│ ├── main/java/com/baluga/backend/\n│ │ ├── BackendApplication.java # 启动入口\n│ │ ├── common/ # 全局共享代码\n│ │ ├── config/ # Spring 配置类\n│ │ ├── infrastructure/ # 技术基础设施\n│ │ │ └── integration/\n│ │ │ ├── ai/ # AI 匹配提供者\n│ │ │ ├── chunking/ # 分块策略\n│ │ │ ├── embedding/ # 嵌入服务客户端\n│ │ │ ├── matching/ # 资源收集器\n│ │ │ └── parsing/ # 文档解析器\n│ │ └── modules/ # 业务模块\n│ │ ├── textbook/ # 教材管理\n│ │ ├── outline/ # 大纲管理\n│ │ ├── courseware/ # 课件管理\n│ │ ├── question/ # 题库管理\n│ │ ├── video/ # 视频管理\n│ │ ├── storage/ # 文件上传\n│ │ ├── mapping/ # 批量 AI 映射\n│ │ └── mount/ # 多策略挂载引擎\n│ │ ├── engine/ # 挂载策略引擎\n│ │ ├── orchestrator/ # 编排器\n│ │ ├── rag/ # RAG 检索\n│ │ ├── task/ # 异步任务\n│ │ ├── review/ # 人工审核\n│ │ └── feedback/ # 反馈收集\n│ ├── main/resources/\n│ │ ├── application.yml # 主配置\n│ │ ├── application-local.yml # 本地开发覆盖\n│ │ ├── application-dev.yml # 开发环境\n│ │ ├── application-prod.yml # 生产环境\n│ │ ├── mapper/ # MyBatis XML 映射\n│ │ └── db/migration/ # 数据库迁移脚本 (V1-V23)\n│ └── test/ # 测试代码\n├── src/ # Vue 3 前端\n│ ├── app/ # 应用入口 + 路由 + 全局样式\n│ ├── api/ # API 客户端层\n│ ├── views/ # 页面视图\n│ └── features/resource-center/ # 功能模块\n│ ├── navigation/ # 侧边栏导航\n│ ├── profile/ # 用户信息\n│ └── workbench/ # 工作台\n│ ├── textbook/ # 教材工作台\n│ ├── outline/ # 大纲工作台\n│ ├── courseware/ # 课件工作台\n│ ├── video/ # 视频工作台\n│ ├── question/ # 题目工作台\n│ └── mapping/ # 知识映射工作台\n├── embedding-service/ # Python 嵌入服务\n│ ├── main.py # FastAPI 应用\n│ ├── Dockerfile # 嵌入服务镜像\n│ └── requirements.txt # Python 依赖\n├── docker/ # Docker 辅助文件\n│ └── init-pgvector.sql # pgvector 初始化脚本\n├── docker-compose.yml # Docker Compose 编排\n├── Dockerfile.frontend # 前端镜像(Vue → Nginx)\n├── nginx.conf # Nginx 配置(API 代理)\n├── docs/ # 设计文档\n│ ├── resource-mounting-system-design.md\n│ ├── requirements/\n│ └── superpowers/\n├── public/ # 前端静态资源\n├── package.json # 前端依赖\n├── vite.config.ts # Vite 构建配置\n└── tsconfig.json # TypeScript 配置\n```\n\n---\n\n## 5. 环境要求\n\n### 5.1 必需环境\n\n| 环境 | 最低版本 | 说明 |\n|---|---|---|\n| Docker | 26+ | 容器运行时(使用 Docker Compose 时) |\n| JDK | 17 | 编译与运行后端(手动启动时) |\n| Maven | 3.9+ | 后端构建(Wrapper 已包含) |\n| Node.js | 20.19.0 / 22.12.0+ | 前端开发与构建(手动启动时) |\n| MySQL | 8.0+ | 业务数据库(手动启动时) |\n| PostgreSQL | 15+ (含 pgvector 扩展) | 向量数据库(手动启动时) |\n| Python | 3.10+ | 嵌入服务运行(手动启动时) |\n\n> **推荐使用 Docker Compose 一键启动**,无需手动安装 JDK/Maven/Node/MySQL/PostgreSQL/Python。\n\n### 5.2 可选环境\n\n| 环境 | 说明 |\n|---|---|\n| DeepSeek API Key | LLM 挂载推理(无 Key 时仅能使用关键词 fallback) |\n\n### 5.3 端口占用\n\n| 服务 | 端口 | 可配置 |\n|---|---|---|\n| 前端 (Docker Nginx) | 80 | `FRONTEND_PORT` |\n| 后端 (Spring Boot) | 8080 | `BACKEND_PORT` |\n| 前端 (Vite Dev) | 5175 | `vite.config.ts` |\n| 嵌入服务 (FastAPI) | 8000 | `EMBEDDING_PORT` |\n| MySQL | 3307 | `DB_PORT` 环境变量 (Docker 默认 3307) |\n| PostgreSQL | 5432 | `PG_PORT` 环境变量 |\n\n> 使用 Docker Compose 时,MySQL 主机端口默认映射为 3307(避免与本机已有 MySQL 冲突),容器内部仍为 3306。\n\n---\n\n## 6. 快速开始\n\n### 6.1 环境变量\n\n启动前配置以下环境变量(不配置则使用默认值):\n\n```bash\n# MySQL\nexport DB_HOST=localhost\nexport DB_PORT=3306\nexport DB_NAME=resource_center\nexport DB_USERNAME=root\nexport DB_PASSWORD=your_mysql_password\n\n# PostgreSQL (pgvector)\nexport PG_HOST=localhost\nexport PG_PORT=5432\nexport PG_DB=resource_center\nexport PG_USER=postgres\nexport PG_PASSWORD=your_pg_password\n\n# DeepSeek API (AI 挂载功能需要)\nexport DEEPSEEK_API_KEY=sk-your-deepseek-api-key\n\n# 嵌入服务地址\nexport EMBEDDING_URL=http://localhost:8000\n\n# 文件上传目录(可选,默认使用系统临时目录)\nexport BALUGA_UPLOAD_DIR=/path/to/uploads\n```\n\n### 6.2 一键启动(Docker Compose,推荐)\n\n```bash\n# 1. 编辑环境变量(可选,默认值即可运行)\ncp .env.example .env\n# 按需修改 .env 中的数据库密码、API Key 等\n\n# 2. 一键启动所有服务\ndocker compose up -d\n\n# 3. 等待所有容器 healthy(首次启动需下载 2GB 模型,约 5 分钟)\ndocker compose ps\n\n# 4. 访问\n# 前端: http://localhost\n# 后端健康: http://localhost:8080/actuator/health\n# 嵌入服务: http://localhost:8000/health\n```\n\n常用命令:\n\n```bash\ndocker compose logs -f # 查看所有日志\ndocker compose logs -f backend # 只看后端日志\ndocker compose restart backend # 重启某个服务\ndocker compose down # 停止所有服务\ndocker compose down -v # 停止并清除所有数据卷\ndocker compose up -d --build # 重新构建并启动\n```\n\n### 6.3 手动启动(3 个终端,开发调试时使用)\n\n**终端 1** — 启动嵌入服务:\n\n```bash\ncd embedding-service\npip install -r requirements.txt\npython main.py\n# → http://localhost:8000 (首次启动下载模型约 2GB)\n```\n\n**终端 2** — 启动后端:\n\n```bash\ncd backend\n./mvnw spring-boot:run\n# → http://localhost:8080\n```\n\n**终端 3** — 启动前端:\n\n```bash\nnpm install\nnpm run dev\n# → http://localhost:5175\n```\n\n前端开发服务器已将 `/api` 代理到 `http://localhost:8080`,无需额外配置。\n\n---\n\n## 7. 中间件启动\n\n### 7.1 Docker Compose 一键启动(推荐)\n\n```bash\ndocker compose up -d mysql postgres embedding\n```\n\n此命令启动 MySQL、PostgreSQL/pgvector 和嵌入服务,数据库迁移脚本和 pgvector 初始化会自动执行。\n\n### 7.2 MySQL(手动)\n\n```bash\ndocker run -d --name mysql \\\n -e MYSQL_ROOT_PASSWORD=your_password \\\n -e MYSQL_DATABASE=resource_center \\\n -p 3306:3306 \\\n mysql:8.0\n\n# 或使用本地安装的 MySQL,手动创建数据库:\nmysql -u root -p -e \"CREATE DATABASE IF NOT EXISTS resource_center DEFAULT CHARACTER SET utf8mb4;\"\n```\n\n### 7.3 PostgreSQL + pgvector(手动)\n\n```bash\ndocker run -d --name pgvector \\\n -e POSTGRES_USER=postgres \\\n -e POSTGRES_PASSWORD=baluga123 \\\n -e POSTGRES_DB=resource_center \\\n -p 5432:5432 \\\n pgvector/pgvector:pg16\n```\n\n### 7.4 嵌入服务(手动)\n\n```bash\ncd embedding-service\npip install -r requirements.txt\npython main.py\n```\n\n首次启动会自动下载 `BAAI/bge-large-zh-v1.5` 模型(约 2GB),请确保网络通畅。国内用户可使用 modelscope 加速下载。\n\n---\n\n## 8. 数据库初始化\n\n> **使用 Docker Compose 时,迁移脚本会自动执行,无需手动操作。** MySQL 容器首次启动时,`db/migration/` 目录下的 SQL 文件会按文件名序(V01 → V23)自动执行。pgvector 扩展和 `resource_embedding` 表也通过 `docker/init-pgvector.sql` 自动创建。\n\n### 8.1 执行迁移脚本(手动)\n\n数据库迁移脚本位于 `backend/src/main/resources/db/migration/`,按版本号顺序手动执行:\n\n```bash\ncd backend/src/main/resources/db/migration\n\n# 按 V1 → V23 顺序执行(注意没有 V20)\nfor f in V1__*.sql V2__*.sql V3__*.sql V4__*.sql V5__*.sql \\\n V6__*.sql V7__*.sql V8__*.sql V9__*.sql V10__*.sql \\\n V11__*.sql V12__*.sql V13__*.sql V14__*.sql V15__*.sql \\\n V16__*.sql V17__*.sql V18__*.sql V19__*.sql V21__*.sql \\\n V22__*.sql V23__*.sql; do\n mysql -u root -p resource_center < \"$f\"\ndone\n```\n\n或在 MySQL 客户端中逐个执行。\n\n### 8.2 pgvector 初始化\n\n```sql\n-- 连接到 PostgreSQL\npsql -U postgres -d resource_center\n\n-- 安装 pgvector 扩展\nCREATE EXTENSION IF NOT EXISTS vector;\n\n-- 创建资源嵌入表(后端运行时也会自动创建)\nCREATE TABLE IF NOT EXISTS resource_embedding (\n id BIGSERIAL PRIMARY KEY,\n target_type VARCHAR(32) NOT NULL,\n target_id BIGINT NOT NULL,\n embedding vector(1024),\n created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP\n);\n\nCREATE INDEX IF NOT EXISTS idx_resource_embedding_type_id\n ON resource_embedding (target_type, target_id);\n```\n\n### 8.3 迁移脚本说明\n\n| 脚本 | 说明 |\n|---|---|\n| V1-V6 | 核心业务表:textbook, outline, courseware, question, video |\n| V7 | 映射基础表:knowledge_point, mapping_batch, mapping_record, mapping_candidate |\n| V8-V13 | 资源资产与关联:resource_asset, asset_id 外键 |\n| V14 | 种子知识点数据 |\n| V15-V17 | 知识图谱层级化 + 映射记录扩展 |\n| V18-V19 | 资源全文 + 语义分块 |\n| V21-V23 | AI 挂载关系 + 异步任务 + 审核记录 |\n\n---\n\n## 9. 项目启动\n\n### 9.1 IntelliJ IDEA 启动\n\n1. 用 IDEA 打开 `backend/` 目录(作为 Maven 项目)\n2. 等待 Maven 依赖下载完成\n3. 配置环境变量(见第 6.1 节)→ Run → Edit Configurations → Environment Variables\n4. 运行 `BackendApplication.main()`\n\n### 9.2 Maven 命令行启动\n\n```bash\ncd backend\n\n# 开发环境(使用 application-local.yml)\n./mvnw spring-boot:run -Dspring-boot.run.profiles=local\n\n# 跳过测试启动\n./mvnw spring-boot:run -DskipTests\n```\n\n### 9.3 前端启动\n\n```bash\nnpm install\nnpm run dev # 开发服务器,端口 5175\nnpm run build # 生产构建\nnpm run preview # 预览生产构建\n```\n\n### 9.4 启动顺序建议\n\n```\n1. MySQL ── 必须最先启动\n2. PostgreSQL ── 向量数据库(不使用 AI 挂载时可跳过)\n3. 嵌入服务 ── Python 微服务(不使用向量功能时可跳过)\n4. 后端 ── Spring Boot(检查 /actuator/health)\n5. 前端 ── Vite 开发服务器\n```\n\n### 9.5 启动验证\n\n```bash\n# 检查后端健康状态\ncurl http://localhost:8080/actuator/health\n\n# 检查嵌入服务\ncurl http://localhost:8000/health\n\n# 检查前端\ncurl http://localhost:5175\n```\n\n---\n\n## 10. API 文档\n\n### 10.1 核心 API 端点\n\n| 模块 | 基础路径 | 说明 |\n|---|---|---|\n| 教材管理 | `/api/textbooks` | 教材 CRUD、分页查询 |\n| 大纲管理 | `/api/outlines` | 课程大纲版本管理 |\n| 课件管理 | `/api/coursewares` | 课件资源 CRUD |\n| 题库管理 | `/api/questions` | 题目 CRUD、分类筛选 |\n| 视频管理 | `/api/videos` | 视频资源管理 |\n| 文件上传 | `/api/upload` | 分片上传、断点续传 |\n| 批量映射 | `/api/mapping` | AI 批量资源匹配 |\n| AI 挂载 | `/api/mount` | 多策略挂载引擎 |\n| 挂载审核 | `/api/mount/reviews` | 人工审核挂载结果 |\n| 健康检查 | `/actuator/health` | 服务健康状态 |\n\n### 10.2 通用响应格式\n\n```json\n{\n \"code\": 200,\n \"message\": \"success\",\n \"data\": {}\n}\n```\n\n### 10.3 当前状态\n\n项目暂未集成 Swagger/Knife4j。API 接口可通过以下方式查看:\n\n- 阅读各模块 `controller/` 目录下的 `@RestController` 类\n- 使用 Postman/Insomnia 导入抓取的请求\n- 后续计划集成 SpringDoc OpenAPI\n\n---\n\n## 11. AI 功能说明\n\n### 11.1 AI 资源挂载(核心能力)\n\n**目标**: 给定一个教学资源(教材章节、课件、视频、题目、大纲),自动判断它属于知识图谱中的哪个课程、哪个章节、哪个知识点。\n\n**流程**:\n\n```\n资源上传 → 文档解析 → 语义分块 → 向量嵌入 → 多策略匹配 → 融合决策 → 自动挂载/人工审核\n```\n\n### 11.2 四种挂载策略\n\n#### 策略 1:规则引擎 (RuleBasedMountEngine)\n\n- **优先级**: 1(最先执行)\n- **逻辑**:\n - 课程字段直接匹配\n - 中文章节号正则匹配(如\"第三章\")\n - 知识点名称在资源内容中的精确出现\n- **适用**: 结构良好、有明确课程归属的资源\n\n#### 策略 2:嵌入语义匹配 (EmbeddingMatchEngine)\n\n- **优先级**: 3\n- **逻辑**:\n - 将资源的所有 Chunk 通过 BGE-large-zh-v1.5 嵌入为向量\n - 平均池化得到资源整体向量\n - 在 pgvector 中通过余弦距离检索 Top-20 相似知识点\n - 相似度转换为置信度分数\n- **适用**: 文本丰富、语义明确的资源\n- **前置条件**: Chunk 嵌入已生成 + 知识点嵌入已存在\n\n#### 策略 3:LLM 推理 (LLMReasoningMountEngine)\n\n- **优先级**: 5\n- **逻辑**(三阶段流水线):\n 1. **课程分类**: LLM 判断资源属于哪个课程\n 2. **章节定位**: LLM 判断资源属于哪个章节/节\n 3. **知识点匹配**: RAG 检索 Top-15 知识点 → LLM 从中选择最匹配的\n- **模型**: DeepSeek Chat(通过 OpenAI 兼容 API)\n- **温度**: 0.1(保证输出一致性)\n- **回退**: LLM 调用失败时降级为关键词匹配\n\n#### 策略 4:关键词相似度 (Jaccard)\n\n- **逻辑**: 字符二元组 Jaccard 相似度(资源标题 vs 知识点名称)\n- **阈值**: ≥0.5 高置信度, ≥0.2 中置信度\n- **适用**: 作为所有其他策略的 fallback\n\n### 11.3 融合引擎 (FusionMountEngine)\n\n四种策略的结果通过加权融合:\n\n| 策略 | 权重 |\n|---|---|\n| LLM 推理 | 4.0 |\n| 嵌入语义 | 2.0 |\n| 规则引擎 | 1.5 |\n| 关键词 | 1.5 |\n\n融合后的候选人按加权平均分数排序,再通过 `ConfidenceCalculator` 计算最终置信度(4 个维度):\n\n- 原始融合分数:40%\n- 策略一致性(越多策略同意越高):25%\n- 最高置信度标签:15%\n- 历史准确率代理:20%\n\n### 11.4 决策分级\n\n| 分数区间 | 决策 | 行为 |\n|---|---|---|\n| ≥ 0.85 | 自动挂载 | 直接创建 ResourceMountRelation |\n| 0.4 ~ 0.85 | 待审核 | 创建 MountReviewRecord,等待人工确认 |\n| < 0.4 | 丢弃 | 不挂载 |\n\n### 11.5 RAG 知识点检索\n\n`KnowledgePointRetriever` 组件:\n- 将所有知识点 (名称 + 描述) 批量嵌入并存入 pgvector\n- 查询时:将资源文本嵌入 → pgvector 余弦搜索 → 返回 Top-K 相似知识点\n- 供 LLM 推理作为候选集,将知识点范围从可能的上千个缩小到 15 个\n\n### 11.6 文档解析能力\n\n| 格式 | 解析库 | 提取内容 |\n|---|---|---|\n| .docx | Apache POI | 段落文本、表格、样式 |\n| .pptx | Apache POI | 幻灯片文本、备注 |\n| .pdf | Apache PDFBox | 页面文本 |\n| .md | Flexmark | Markdown AST → 纯文本 |\n\n### 11.7 反馈闭环\n\n人工审核结果通过 `FeedbackCollector` 导出为 JSONL:\n\n```jsonl\n{\"type\":\"approve\",\"resource_title\":\"...\",\"node_name\":\"...\",\"ai_confidence\":0.92}\n{\"type\":\"modify\",\"resource_title\":\"...\",\"original_node\":\"...\",\"corrected_node\":\"...\",\"ai_confidence\":0.65}\n{\"type\":\"reject\",\"resource_title\":\"...\",\"node_name\":\"...\",\"ai_confidence\":0.45}\n```\n\n该数据可用于后续 LLM 微调,持续提升挂载准确率。\n\n---\n\n## 12. 开发规范\n\n### 12.1 分支规范\n\n| 分支 | 说明 |\n|---|---|\n| `main` | 生产分支,受保护 |\n| `dev` | 开发主分支,日常开发合并到此 |\n| `feature/*` | 功能分支,从 dev 拉出,完成后合并回 dev |\n| `fix/*` | 修复分支 |\n| `release/*` | 发布分支 |\n\n### 12.2 提交规范\n\n遵循 Conventional Commits:\n\n```\nfeat: 添加 AI 多策略挂载引擎\nfix: 修复 pgvector 余弦距离计算错误\ndocs: 更新 README 中间件说明\nrefactor: 重构 ChunkService 抽取公共接口\ntest: 添加 MountOrchestrator 单元测试\n```\n\n### 12.3 后端代码规范\n\n- 包结构遵循 `modules/{业务模块}/{层级}` 模式\n- Controller 只做参数接收和校验,不写业务逻辑\n- Service 接口 + ServiceImpl 实现,使用构造函数注入\n- Mapper 继承 MyBatis-Plus `BaseMapper`,自定义 SQL 写在 XML 中\n- DTO 分为 `request/` 和 `response/`,使用 Lombok `@Data`\n- Entity 使用 `@TableName` 映射数据库表,启用逻辑删除\n\n### 12.4 前端代码规范\n\n- 按功能模块组织 `features/resource-center/workbench/`\n- 每个工作台模块独立管理自己的 API、组件、状态、样式\n- 使用 Pinia 进行状态管理\n- API 调用统一通过 `src/api/` 下的模块化客户端\n\n---\n\n## 13. 常见问题\n\n### 13.1 端口占用\n\n```bash\n# Windows 查看端口占用\nnetstat -ano | findstr :8080\n# 终止进程\ntaskkill /PID /F\n```\n\n### 13.2 MySQL 连接失败\n\n```\nCommunicationsException: Communications link failure\n```\n\n- 检查 MySQL 服务是否启动\n- 检查 `DB_HOST`、`DB_PORT` 环境变量是否正确\n- 检查数据库 `resource_center` 是否已创建\n- 检查 `application.yml` 中的用户名密码\n\n### 13.3 PostgreSQL / pgvector 连接失败\n\n- 确认 PostgreSQL 已安装 pgvector 扩展:`SELECT * FROM pg_extension WHERE extname='vector';`\n- 如未安装:`CREATE EXTENSION vector;`\n- 确认 `PG_HOST`、`PG_PORT`、`PG_PASSWORD` 配置正确\n- **不使用 AI 向量功能时,此错误不影响基本 CRUD 操作**\n\n### 13.4 嵌入服务启动失败\n\n```\nOSError: [WinError 1455] 页面文件太小,无法完成操作\n```\n\n- Windows 下加载 2GB 模型需要足够的虚拟内存\n- 解决:增加 Windows 页面文件大小(系统属性 → 高级 → 性能 → 虚拟内存)\n- 或降低模型规模换用 `bge-small-zh-v1.5`\n\n### 13.5 AI 挂载返回空结果\n\n- 确认 `DEEPSEEK_API_KEY` 已正确配置\n- 确认 DeepSeek API 余额充足\n- 关键词 fallback 不需要 API Key,但准确率较低\n- 确认 `baluga.ai.matching.openai-base-url` 指向正确的 API 地址\n\n### 13.6 前端 Vite 代理不生效\n\n- 确认 `vite.config.ts` 中 proxy 配置指向 `http://localhost:8080`\n- 确认后端已启动并监听 8080 端口\n- 前端 API 调用路径必须以 `/api` 开头才能触发代理\n\n### 13.7 Docker 容器启动失败\n\n```bash\n# 查看容器日志\ndocker logs rc-mysql\ndocker logs rc-backend\ndocker logs rc-embedding\n\n# 端口冲突(如 MySQL 3306 被占用)\n# 编辑 .env 文件修改 DB_PORT=3307\n\n# 镜像拉取失败(国内网络)\n# 在 Docker Desktop Settings → Docker Engine 中添加镜像加速:\n# \"registry-mirrors\": [\"https://docker.1ms.run\"]\n\n# 数据乱码\n# 重置数据卷重建:docker compose down -v && docker compose up -d\n\n# 嵌入服务 DNS 失败\n# 重建网络:docker compose down && docker compose up -d\n```\n\n### 13.8 文件上传 500 错误\n\n- 检查 `BALUGA_UPLOAD_DIR` 目录是否存在且有写入权限\n- 默认使用系统临时目录,确认磁盘空间充足\n- 文件大小上限 500MB,检查是否超出\n\n---\n\n## 14. 后续规划\n\n| 规划项 | 说明 |\n|---|---|\n| SpringDoc OpenAPI | 集成 Swagger/Knife4j 自动生成 API 文档 |\n| ~~Docker Compose~~ | ~~一键启动所有中间件和服务~~ ✅ 已完成 |\n| Redis 缓存 | 热点知识点缓存、会话管理 |\n| Elasticsearch | 资源全文检索、混合搜索 |\n| MinIO / OSS | 对象存储替代本地文件存储 |\n| 多 Agent 架构 | 将挂载流水线升级为多 Agent 协作模式 |\n| 教育知识图谱 | 完整的三层知识体系(课程→章→节→知识点→能力目标) |\n| 自动知识抽取 | 从教材/课件中自动抽取新知识点 |\n| LLM 微调 | 基于 FeedbackCollector 收集的 JSONL 数据微调挂载模型 |\n| AI Tutor 联动 | 挂载结果驱动个性化学习路径推荐 |\n| 视频 OCR/转录 | 视频内容自动提取与标注 |\n| Sa-Token 认证 | 统一身份认证与权限管理 |\n| Nacos 配置中心 | 动态配置管理与服务发现 |\n\n---\n\n## 附录\n\n### A. 系统设计文档\n\n- [资源挂载系统架构设计](docs/resource-mounting-system-design.md)\n- [AI 资源知识挂载架构](docs/requirements/2026-05-19-ai-resource-knowledge-mounting-architecture.md)\n- [AI 自动映射需求澄清](docs/requirements/2026-05-12-clarify-requirement-ai-auto-mapping-...)\n\n### B. 后端模块设计\n\n- [后端目录结构说明](backend/STRUCTURE.md)\n\n### C. 技术参考\n\n- [BGE-large-zh-v1.5 模型](https://huggingface.co/BAAI/bge-large-zh-v1.5) — 中文嵌入模型\n- [pgvector](https://github.com/pgvector/pgvector) — PostgreSQL 向量扩展\n- [DeepSeek API](https://platform.deepseek.com/api-docs) — LLM 推理服务\n- [MyBatis-Plus](https://baomidou.com/) — 增强版 MyBatis\n"}, "5dbc3dc4933fec5cb224563eac96449e7642b184": {"t": 1785766219.9326708, "n": 21084, "h": "7c08addbcf6d7d4a", "c": "# resource-center(资源中台)\n\n> 基于大模型的个性化资源生成与学习多智能体系统 —— 资源中台子系统\n\n资源中台是 AI 驱动智能学习平台的**核心资源管理枢纽**,负责教学资源的全生命周期管理,并通过多策略 AI 引擎将资源智能挂载到教育知识图谱中,实现\"教育资源 → 知识体系节点\"的语义映射。\n\n---\n\n## 目录\n\n- [1. 项目介绍](#1-项目介绍)\n- [2. 系统架构](#2-系统架构)\n- [3. 技术栈](#3-技术栈)\n- [4. 项目结构](#4-项目结构)\n- [5. 环境要求](#5-环境要求)\n- [6. 快速开始](#6-快速开始)\n- [7. 中间件启动](#7-中间件启动)\n- [8. 数据库初始化](#8-数据库初始化)\n- [9. 项目启动](#9-项目启动)\n- [10. API 文档](#10-api-文档)\n- [11. AI 功能说明](#11-ai-功能说明)\n- [12. 开发规范](#12-开发规范)\n- [13. 常见问题](#13-常见问题)\n- [14. 后续规划](#14-后续规划)\n\n---\n\n## 1. 项目介绍\n\n### 1.1 项目定位\n\n资源中台是整个《基于大模型的个性化资源生成与学习多智能体系统》的**资源管理子系统**,向上对接 AI 教学智能体,向下管理各类教学资源的存储、解析、向量化和语义挂载。\n\n### 1.2 核心职责\n\n| 职责 | 说明 |\n|---|---|\n| 教材管理 | 教材元数据 CRUD、版本管理、资产关联 |\n| 课件管理 | PPT/PDF/DOC 课件上传、解析、全文检索 |\n| 视频管理 | 视频资源元数据、处理状态、发布管理 |\n| 题库管理 | 选择题/填空题/简答题/编程题全类型管理 |\n| 大纲管理 | 课程大纲版本管理、完成度追踪、归档 |\n| 资源解析 | PDF/Word/PPT/Markdown 自动解析提取全文 |\n| 智能分块 | 语义分块、固定大小分块、题目分块 |\n| 向量化 | BGE-large-zh-v1.5 中文嵌入(1024维) |\n| AI 资源挂载 | 多策略融合引擎,将资源自动挂载到知识图谱节点 |\n| 人工审核 | 挂载结果审核、修正、反馈收集 |\n\n### 1.3 AI 能力总览\n\n- **多策略挂载引擎**:规则引擎 + 向量语义匹配 + LLM 推理 + 关键词匹配,四种策略加权融合\n- **RAG 检索增强**:基于 pgvector 的知识点语义检索,为 LLM 提供候选知识点上下文\n- **文档智能解析**:支持 PDF、Word (.docx)、PPT (.pptx)、Markdown 四种格式的自动解析\n- **语义分块**:按段落检测内容类型(定义/示例/练习/总结),生成结构化 Chunk\n- **反馈闭环**:人工审核结果导出 JSONL,支持后续模型微调\n\n---\n\n## 2. 系统架构\n\n### 2.1 整体架构\n\n```\n┌──────────────────────────────────────────────────────────────┐\n│ resource-center (Monorepo) │\n├──────────────────────────────────────────────────────────────┤\n│ │\n│ ┌─────────────────┐ ┌──────────────────┐ ┌────────────┐ │\n│ │ Vue 3 Frontend │ │ Spring Boot │ │ Embedding │ │\n│ │ (Port 5175) │ │ Backend (8080) │ │ Service │ │\n│ │ │ │ │ │ (8000) │ │\n│ │ TypeScript │ │ Java 17 │ │ Python │ │\n│ │ Vite 8 │ │ MyBatis-Plus │ │ FastAPI │ │\n│ │ Pinia │ │ Spring Boot 3.5 │ │ BGE-zh │ │\n│ └────────┬────────┘ └───────┬──────────┘ └─────┬──────┘ │\n│ │ │ │ │\n│ │ /api/* proxy │ │ │\n│ ├──────────────────►│ │ │\n│ │ │ HTTP /embed │ │\n│ │ ├───────────────────►│ │\n│ │ │ │ │\n└───────────┼───────────────────┼────────────────────┼─────────┘\n │ │ │\n ▼ ▼ ▼\n ┌──────────┐ ┌──────────────┐ ┌──────────────┐\n │ Browser │ │ MySQL :3306 │ │ PostgreSQL │\n │ (User) │ │ (Business) │ │ :5432 │\n └──────────┘ └──────────────┘ │ (pgvector) │\n └──────────────┘\n```\n\n### 2.2 后端模块架构\n\n```\ncom.baluga.backend\n├── common/ # 全局共享\n│ ├── api/R.java # 统一响应体\n│ └── exception/ # 全局异常处理\n├── config/ # Spring 配置\n│ ├── MybatisPlusConfig # MyBatis-Plus + 分页\n│ ├── PgVectorConfig # pgvector 独立数据源\n│ └── WebMvcConfig # CORS\n├── infrastructure/ # 技术基础设施\n│ └── integration/\n│ ├── ai/ # AI 匹配提供者(DeepSeek)\n│ ├── chunking/ # 文本分块策略\n│ ├── embedding/ # 嵌入服务客户端\n│ ├── matching/ # 资源收集器\n│ └── parsing/ # 文档解析器(PDF/Word/PPT/MD)\n└── modules/ # 业务模块\n ├── textbook/ # 教材管理\n ├── outline/ # 大纲版本管理\n ├── courseware/ # 课件管理\n ├── question/ # 题库管理\n ├── video/ # 视频管理\n ├── storage/ # 文件上传\n ├── mapping/ # 资源映射(批量 AI 匹配)\n └── mount/ # AI 挂载引擎(多策略融合)\n ├── engine/ # 规则/LLM/嵌入/融合引擎\n ├── orchestrator/ # 6 阶段挂载编排器\n ├── rag/ # RAG 知识点检索器\n ├── task/ # 异步任务管理\n ├── review/ # 人工审核\n └── feedback/ # 反馈收集(JSONL 导出)\n```\n\n### 2.3 AI 挂载流水线\n\n```\n MountOrchestrator\n │\n ┌───────────────┼───────────────┐\n ▼ ▼ ▼\n 1. 文档解析 2. 语义分块 3. 构建上下文\n (PDF/Word/ (Semantic/ (ResourceContext\n PPT/Markdown) Fixed/Question) + KnowledgeGraphScope)\n │ │ │\n └───────────────┼───────────────┘\n ▼\n 4. 多策略融合\n ┌───────────────┼───────────────┐\n ▼ ▼ ▼\n RuleBased Embedding LLMReasoning\n (精确规则) (向量语义) (DeepSeek推理)\n │ │ │\n └───────────────┼───────────────┘\n ▼\n FusionMountEngine\n (加权融合: R=1.5, E=2.0, L=4.0, K=1.5)\n │\n ▼\n ConfidenceCalculator\n (4维置信度计算)\n │\n ┌────────────┼────────────┐\n ▼ ▼ ▼\n score≥0.85 0.4≤score<0.85 score<0.4\n 自动挂载 人工审核 丢弃\n │ │\n ▼ ▼\n ResourceMountRelation MountReviewRecord\n │\n ▼\n FeedbackCollector\n (JSONL 训练数据导出)\n```\n\n### 2.4 RAG 检索架构\n\n```\n资源文本\n │\n ▼\nEmbeddingService (BGE-large-zh-v1.5, 1024维)\n │\n ▼\npgvector 余弦相似度检索 (<=>)\n │\n ▼\nTop-K 相似知识点 (默认 Top-15)\n │\n ▼\nLLM 推理上下文 (DeepSeek Chat)\n │\n ▼\n挂载决策\n```\n\n---\n\n## 3. 技术栈\n\n### 3.1 后端\n\n| 技术 | 版本 | 说明 |\n|---|---|---|\n| Java | 17 | 编译目标版本 |\n| Spring Boot | 3.5.14 | 应用框架 |\n| Spring Framework | 6.2.18 | 核心框架 |\n| MyBatis-Plus | 3.5.16 | ORM + 分页 + 逻辑删除 |\n| MySQL Connector | 9.7.0 | MySQL 驱动 |\n| PostgreSQL Driver | 42.7.10 | pgvector 数据源驱动 |\n| HikariCP | 6.3.3 | 数据库连接池 |\n| Apache POI | 5.3.0 | Word/PPT 文档解析 |\n| Apache PDFBox | 3.0.3 | PDF 文档解析 |\n| Flexmark | 0.64.8 | Markdown 解析 |\n| Apache Tika | 2.9.2 | 文件元数据提取 |\n| Lombok | 1.18.46 | 代码简化 |\n| Jackson | 2.21.2 | JSON 序列化 |\n| Tomcat | 10.1.54 | 嵌入式 Web 容器 |\n| JUnit Jupiter | 5.12.2 | 单元测试 |\n| Mockito | 5.17.0 | 测试 Mock |\n\n### 3.2 前端\n\n| 技术 | 版本 | 说明 |\n|---|---|---|\n| Vue | 3.5.31 | UI 框架 |\n| TypeScript | 6.0 | 类型系统 |\n| Vite | 8.0.3 | 构建工具 |\n| Pinia | 3.0.3 | 状态管理 |\n| Vue Router | 4.6.3 | 路由管理 |\n| Node.js | ≥20.19.0 / ≥22.12.0 | 运行时 |\n\n### 3.3 AI / 嵌入服务\n\n| 技术 | 版本 | 说明 |\n|---|---|---|\n| Python | 3.10+ | 嵌入服务运行时 |\n| FastAPI | ≥0.110.0 | Python Web 框架 |\n| sentence-transformers | ≥3.0.0 | 嵌入模型框架 |\n| BGE-large-zh-v1.5 | - | 中文嵌入模型(1024维) |\n| Uvicorn | ≥0.29.0 | ASGI 服务器 |\n\n### 3.4 数据库与中间件\n\n| 中间件 | 版本要求 | 端口 | 用途 |\n|---|---|---|---|\n| MySQL | 8.0+ | 3306 | 业务数据主库 |\n| PostgreSQL | 15+ (需 pgvector 扩展) | 5432 | 向量嵌入存储与检索 |\n| DeepSeek API | - | HTTPS | LLM 推理服务 |\n\n---\n\n## 4. 项目结构\n\n```\nresource-center/\n├── backend/ # Spring Boot 后端\n│ ├── pom.xml # Maven 配置\n│ ├── mvnw / mvnw.cmd # Maven Wrapper\n│ ├── STRUCTURE.md # 后端结构说明\n│ └── src/\n│ ├── main/java/com/baluga/backend/\n│ │ ├── BackendApplication.java # 启动入口\n│ │ ├── common/ # 全局共享代码\n│ │ ├── config/ # Spring 配置类\n│ │ ├── infrastructure/ # 技术基础设施\n│ │ │ └── integration/\n│ │ │ ├── ai/ # AI 匹配提供者\n│ │ │ ├── chunking/ # 分块策略\n│ │ │ ├── embedding/ # 嵌入服务客户端\n│ │ │ ├── matching/ # 资源收集器\n│ │ │ └── parsing/ # 文档解析器\n│ │ └── modules/ # 业务模块\n│ │ ├── textbook/ # 教材管理\n│ │ ├── outline/ # 大纲管理\n│ │ ├── courseware/ # 课件管理\n│ │ ├── question/ # 题库管理\n│ │ ├── video/ # 视频管理\n│ │ ├── storage/ # 文件上传\n│ │ ├── mapping/ # 批量 AI 映射\n│ │ └── mount/ # 多策略挂载引擎\n│ │ ├── engine/ # 挂载策略引擎\n│ │ ├── orchestrator/ # 编排器\n│ │ ├── rag/ # RAG 检索\n│ │ ├── task/ # 异步任务\n│ │ ├── review/ # 人工审核\n│ │ └── feedback/ # 反馈收集\n│ ├── main/resources/\n│ │ ├── application.yml # 主配置\n│ │ ├── application-local.yml # 本地开发覆盖\n│ │ ├── application-dev.yml # 开发环境\n│ │ ├── application-prod.yml # 生产环境\n│ │ ├── mapper/ # MyBatis XML 映射\n│ │ └── db/migration/ # 数据库迁移脚本 (V1-V23)\n│ └── test/ # 测试代码\n├── src/ # Vue 3 前端\n│ ├── app/ # 应用入口 + 路由 + 全局样式\n│ ├── api/ # API 客户端层\n│ ├── views/ # 页面视图\n│ └── features/resource-center/ # 功能模块\n│ ├── navigation/ # 侧边栏导航\n│ ├── profile/ # 用户信息\n│ └── workbench/ # 工作台\n│ ├── textbook/ # 教材工作台\n│ ├── outline/ # 大纲工作台\n│ ├── courseware/ # 课件工作台\n│ ├── video/ # 视频工作台\n│ ├── question/ # 题目工作台\n│ └── mapping/ # 知识映射工作台\n├── embedding-service/ # Python 嵌入服务\n│ ├── main.py # FastAPI 应用\n│ └── requirements.txt # Python 依赖\n├── docs/ # 设计文档\n│ ├── resource-mounting-system-design.md\n│ ├── requirements/\n│ └── superpowers/\n├── public/ # 前端静态资源\n├── package.json # 前端依赖\n├── vite.config.ts # Vite 构建配置\n└── tsconfig.json # TypeScript 配置\n```\n\n---\n\n## 5. 环境要求\n\n### 5.1 必需环境\n\n| 环境 | 最低版本 | 说明 |\n|---|---|---|\n| JDK | 17 | 编译与运行后端 |\n| Maven | 3.9+ | 后端构建(Wrapper 已包含) |\n| Node.js | 20.19.0 / 22.12.0+ | 前端开发与构建 |\n| MySQL | 8.0+ | 业务数据库 |\n| PostgreSQL | 15+ (含 pgvector 扩展) | 向量数据库 |\n| Python | 3.10+ | 嵌入服务运行 |\n\n### 5.2 可选环境\n\n| 环境 | 说明 |\n|---|---|\n| DeepSeek API Key | LLM 挂载推理(无 Key 时仅能使用关键词 fallback) |\n\n### 5.3 端口占用\n\n| 服务 | 端口 | 可配置 |\n|---|---|---|\n| 后端 (Spring Boot) | 8080 | `server.port` |\n| 前端 (Vite Dev) | 5175 | `vite.config.ts` |\n| 嵌入服务 (FastAPI) | 8000 | `embedding-service/main.py` |\n| MySQL | 3306 | `DB_PORT` 环境变量 |\n| PostgreSQL | 5432 | `PG_PORT` 环境变量 |\n\n---\n\n## 6. 快速开始\n\n### 6.1 环境变量\n\n启动前配置以下环境变量(不配置则使用默认值):\n\n```bash\n# MySQL\nexport DB_HOST=localhost\nexport DB_PORT=3306\nexport DB_NAME=resource_center\nexport DB_USERNAME=root\nexport DB_PASSWORD=your_mysql_password\n\n# PostgreSQL (pgvector)\nexport PG_HOST=localhost\nexport PG_PORT=5432\nexport PG_DB=resource_center\nexport PG_USER=postgres\nexport PG_PASSWORD=your_pg_password\n\n# DeepSeek API (AI 挂载功能需要)\nexport DEEPSEEK_API_KEY=sk-your-deepseek-api-key\n\n# 嵌入服务地址\nexport EMBEDDING_URL=http://localhost:8000\n\n# 文件上传目录(可选,默认使用系统临时目录)\nexport BALUGA_UPLOAD_DIR=/path/to/uploads\n```\n\n### 6.2 一键启动(3 个终端)\n\n**终端 1** — 启动嵌入服务:\n\n```bash\ncd embedding-service\npip install -r requirements.txt\npython main.py\n# → http://localhost:8000 (首次启动下载模型约 2GB)\n```\n\n**终端 2** — 启动后端:\n\n```bash\ncd backend\n./mvnw spring-boot:run\n# → http://localhost:8080\n```\n\n**终端 3** — 启动前端:\n\n```bash\nnpm install\nnpm run dev\n# → http://localhost:5175\n```\n\n前端开发服务器已将 `/api` 代理到 `http://localhost:8080`,无需额外配置。\n\n---\n\n## 7. 中间件启动\n\n### 7.1 MySQL\n\n```bash\n# Docker\ndocker run -d --name mysql \\\n -e MYSQL_ROOT_PASSWORD=your_password \\\n -e MYSQL_DATABASE=resource_center \\\n -p 3306:3306 \\\n mysql:8.0\n\n# 或使用本地安装的 MySQL,手动创建数据库:\nmysql -u root -p -e \"CREATE DATABASE IF NOT EXISTS resource_center DEFAULT CHARACTER SET utf8mb4;\"\n```\n\n### 7.2 PostgreSQL + pgvector\n\n```bash\n# 推荐使用 pgvector 官方镜像\ndocker run -d --name pgvector \\\n -e POSTGRES_USER=postgres \\\n -e POSTGRES_PASSWORD=baluga123 \\\n -e POSTGRES_DB=resource_center \\\n -p 5432:5432 \\\n pgvector/pgvector:pg16\n\n# 或手动安装 pgvector 扩展:\n# CREATE EXTENSION IF NOT EXISTS vector;\n```\n\n### 7.3 嵌入服务\n\n```bash\ncd embedding-service\npip install -r requirements.txt\npython main.py\n```\n\n首次启动会自动从 HuggingFace 下载 `BAAI/bge-large-zh-v1.5` 模型(约 2GB),请确保网络通畅。\n\n---\n\n## 8. 数据库初始化\n\n### 8.1 执行迁移脚本\n\n数据库迁移脚本位于 `backend/src/main/resources/db/migration/`,按版本号顺序手动执行:\n\n```bash\ncd backend/src/main/resources/db/migration\n\n# 按 V1 → V23 顺序执行(注意没有 V20)\nfor f in V1__*.sql V2__*.sql V3__*.sql V4__*.sql V5__*.sql \\\n V6__*.sql V7__*.sql V8__*.sql V9__*.sql V10__*.sql \\\n V11__*.sql V12__*.sql V13__*.sql V14__*.sql V15__*.sql \\\n V16__*.sql V17__*.sql V18__*.sql V19__*.sql V21__*.sql \\\n V22__*.sql V23__*.sql; do\n mysql -u root -p resource_center < \"$f\"\ndone\n```\n\n或在 MySQL 客户端中逐个执行。\n\n### 8.2 pgvector 初始化\n\n```sql\n-- 连接到 PostgreSQL\npsql -U postgres -d resource_center\n\n-- 安装 pgvector 扩展\nCREATE EXTENSION IF NOT EXISTS vector;\n\n-- 创建资源嵌入表(后端运行时也会自动创建)\nCREATE TABLE IF NOT EXISTS resource_embedding (\n id BIGSERIAL PRIMARY KEY,\n target_type VARCHAR(32) NOT NULL,\n target_id BIGINT NOT NULL,\n embedding vector(1024),\n created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP\n);\n\nCREATE INDEX IF NOT EXISTS idx_resource_embedding_type_id\n ON resource_embedding (target_type, target_id);\n```\n\n### 8.3 迁移脚本说明\n\n| 脚本 | 说明 |\n|---|---|\n| V1-V6 | 核心业务表:textbook, outline, courseware, question, video |\n| V7 | 映射基础表:knowledge_point, mapping_batch, mapping_record, mapping_candidate |\n| V8-V13 | 资源资产与关联:resource_asset, asset_id 外键 |\n| V14 | 种子知识点数据 |\n| V15-V17 | 知识图谱层级化 + 映射记录扩展 |\n| V18-V19 | 资源全文 + 语义分块 |\n| V21-V23 | AI 挂载关系 + 异步任务 + 审核记录 |\n\n---\n\n## 9. 项目启动\n\n### 9.1 IntelliJ IDEA 启动\n\n1. 用 IDEA 打开 `backend/` 目录(作为 Maven 项目)\n2. 等待 Maven 依赖下载完成\n3. 配置环境变量(见第 6.1 节)→ Run → Edit Configurations → Environment Variables\n4. 运行 `BackendApplication.main()`\n\n### 9.2 Maven 命令行启动\n\n```bash\ncd backend\n\n# 开发环境(使用 application-local.yml)\n./mvnw spring-boot:run -Dspring-boot.run.profiles=local\n\n# 跳过测试启动\n./mvnw spring-boot:run -DskipTests\n```\n\n### 9.3 前端启动\n\n```bash\nnpm install\nnpm run dev # 开发服务器,端口 5175\nnpm run build # 生产构建\nnpm run preview # 预览生产构建\n```\n\n### 9.4 启动顺序建议\n\n```\n1. MySQL ── 必须最先启动\n2. PostgreSQL ── 向量数据库(不使用 AI 挂载时可跳过)\n3. 嵌入服务 ── Python 微服务(不使用向量功能时可跳过)\n4. 后端 ── Spring Boot(检查 /actuator/health)\n5. 前端 ── Vite 开发服务器\n```\n\n### 9.5 启动验证\n\n```bash\n# 检查后端健康状态\ncurl http://localhost:8080/actuator/health\n\n# 检查嵌入服务\ncurl http://localhost:8000/health\n\n# 检查前端\ncurl http://localhost:5175\n```\n\n---\n\n## 10. API 文档\n\n### 10.1 核心 API 端点\n\n| 模块 | 基础路径 | 说明 |\n|---|---|---|\n| 教材管理 | `/api/textbooks` | 教材 CRUD、分页查询 |\n| 大纲管理 | `/api/outlines` | 课程大纲版本管理 |\n| 课件管理 | `/api/coursewares` | 课件资源 CRUD |\n| 题库管理 | `/api/questions` | 题目 CRUD、分类筛选 |\n| 视频管理 | `/api/videos` | 视频资源管理 |\n| 文件上传 | `/api/upload` | 分片上传、断点续传 |\n| 批量映射 | `/api/mapping` | AI 批量资源匹配 |\n| AI 挂载 | `/api/mount` | 多策略挂载引擎 |\n| 挂载审核 | `/api/mount/reviews` | 人工审核挂载结果 |\n| 健康检查 | `/actuator/health` | 服务健康状态 |\n\n### 10.2 通用响应格式\n\n```json\n{\n \"code\": 200,\n \"message\": \"success\",\n \"data\": {}\n}\n```\n\n### 10.3 当前状态\n\n项目暂未集成 Swagger/Knife4j。API 接口可通过以下方式查看:\n\n- 阅读各模块 `controller/` 目录下的 `@RestController` 类\n- 使用 Postman/Insomnia 导入抓取的请求\n- 后续计划集成 SpringDoc OpenAPI\n\n---\n\n## 11. AI 功能说明\n\n### 11.1 AI 资源挂载(核心能力)\n\n**目标**: 给定一个教学资源(教材章节、课件、视频、题目、大纲),自动判断它属于知识图谱中的哪个课程、哪个章节、哪个知识点。\n\n**流程**:\n\n```\n资源上传 → 文档解析 → 语义分块 → 向量嵌入 → 多策略匹配 → 融合决策 → 自动挂载/人工审核\n```\n\n### 11.2 四种挂载策略\n\n#### 策略 1:规则引擎 (RuleBasedMountEngine)\n\n- **优先级**: 1(最先执行)\n- **逻辑**:\n - 课程字段直接匹配\n - 中文章节号正则匹配(如\"第三章\")\n - 知识点名称在资源内容中的精确出现\n- **适用**: 结构良好、有明确课程归属的资源\n\n#### 策略 2:嵌入语义匹配 (EmbeddingMatchEngine)\n\n- **优先级**: 3\n- **逻辑**:\n - 将资源的所有 Chunk 通过 BGE-large-zh-v1.5 嵌入为向量\n - 平均池化得到资源整体向量\n - 在 pgvector 中通过余弦距离检索 Top-20 相似知识点\n - 相似度转换为置信度分数\n- **适用**: 文本丰富、语义明确的资源\n- **前置条件**: Chunk 嵌入已生成 + 知识点嵌入已存在\n\n#### 策略 3:LLM 推理 (LLMReasoningMountEngine)\n\n- **优先级**: 5\n- **逻辑**(三阶段流水线):\n 1. **课程分类**: LLM 判断资源属于哪个课程\n 2. **章节定位**: LLM 判断资源属于哪个章节/节\n 3. **知识点匹配**: RAG 检索 Top-15 知识点 → LLM 从中选择最匹配的\n- **模型**: DeepSeek Chat(通过 OpenAI 兼容 API)\n- **温度**: 0.1(保证输出一致性)\n- **回退**: LLM 调用失败时降级为关键词匹配\n\n#### 策略 4:关键词相似度 (Jaccard)\n\n- **逻辑**: 字符二元组 Jaccard 相似度(资源标题 vs 知识点名称)\n- **阈值**: ≥0.5 高置信度, ≥0.2 中置信度\n- **适用**: 作为所有其他策略的 fallback\n\n### 11.3 融合引擎 (FusionMountEngine)\n\n四种策略的结果通过加权融合:\n\n| 策略 | 权重 |\n|---|---|\n| LLM 推理 | 4.0 |\n| 嵌入语义 | 2.0 |\n| 规则引擎 | 1.5 |\n| 关键词 | 1.5 |\n\n融合后的候选人按加权平均分数排序,再通过 `ConfidenceCalculator` 计算最终置信度(4 个维度):\n\n- 原始融合分数:40%\n- 策略一致性(越多策略同意越高):25%\n- 最高置信度标签:15%\n- 历史准确率代理:20%\n\n### 11.4 决策分级\n\n| 分数区间 | 决策 | 行为 |\n|---|---|---|\n| ≥ 0.85 | 自动挂载 | 直接创建 ResourceMountRelation |\n| 0.4 ~ 0.85 | 待审核 | 创建 MountReviewRecord,等待人工确认 |\n| < 0.4 | 丢弃 | 不挂载 |\n\n### 11.5 RAG 知识点检索\n\n`KnowledgePointRetriever` 组件:\n- 将所有知识点 (名称 + 描述) 批量嵌入并存入 pgvector\n- 查询时:将资源文本嵌入 → pgvector 余弦搜索 → 返回 Top-K 相似知识点\n- 供 LLM 推理作为候选集,将知识点范围从可能的上千个缩小到 15 个\n\n### 11.6 文档解析能力\n\n| 格式 | 解析库 | 提取内容 |\n|---|---|---|\n| .docx | Apache POI | 段落文本、表格、样式 |\n| .pptx | Apache POI | 幻灯片文本、备注 |\n| .pdf | Apache PDFBox | 页面文本 |\n| .md | Flexmark | Markdown AST → 纯文本 |\n\n### 11.7 反馈闭环\n\n人工审核结果通过 `FeedbackCollector` 导出为 JSONL:\n\n```jsonl\n{\"type\":\"approve\",\"resource_title\":\"...\",\"node_name\":\"...\",\"ai_confidence\":0.92}\n{\"type\":\"modify\",\"resource_title\":\"...\",\"original_node\":\"...\",\"corrected_node\":\"...\",\"ai_confidence\":0.65}\n{\"type\":\"reject\",\"resource_title\":\"...\",\"node_name\":\"...\",\"ai_confidence\":0.45}\n```\n\n该数据可用于后续 LLM 微调,持续提升挂载准确率。\n\n---\n\n## 12. 开发规范\n\n### 12.1 分支规范\n\n| 分支 | 说明 |\n|---|---|\n| `main` | 生产分支,受保护 |\n| `dev` | 开发主分支,日常开发合并到此 |\n| `feature/*` | 功能分支,从 dev 拉出,完成后合并回 dev |\n| `fix/*` | 修复分支 |\n| `release/*` | 发布分支 |\n\n### 12.2 提交规范\n\n遵循 Conventional Commits:\n\n```\nfeat: 添加 AI 多策略挂载引擎\nfix: 修复 pgvector 余弦距离计算错误\ndocs: 更新 README 中间件说明\nrefactor: 重构 ChunkService 抽取公共接口\ntest: 添加 MountOrchestrator 单元测试\n```\n\n### 12.3 后端代码规范\n\n- 包结构遵循 `modules/{业务模块}/{层级}` 模式\n- Controller 只做参数接收和校验,不写业务逻辑\n- Service 接口 + ServiceImpl 实现,使用构造函数注入\n- Mapper 继承 MyBatis-Plus `BaseMapper`,自定义 SQL 写在 XML 中\n- DTO 分为 `request/` 和 `response/`,使用 Lombok `@Data`\n- Entity 使用 `@TableName` 映射数据库表,启用逻辑删除\n\n### 12.4 前端代码规范\n\n- 按功能模块组织 `features/resource-center/workbench/`\n- 每个工作台模块独立管理自己的 API、组件、状态、样式\n- 使用 Pinia 进行状态管理\n- API 调用统一通过 `src/api/` 下的模块化客户端\n\n---\n\n## 13. 常见问题\n\n### 13.1 端口占用\n\n```bash\n# Windows 查看端口占用\nnetstat -ano | findstr :8080\n# 终止进程\ntaskkill /PID /F\n```\n\n### 13.2 MySQL 连接失败\n\n```\nCommunicationsException: Communications link failure\n```\n\n- 检查 MySQL 服务是否启动\n- 检查 `DB_HOST`、`DB_PORT` 环境变量是否正确\n- 检查数据库 `resource_center` 是否已创建\n- 检查 `application.yml` 中的用户名密码\n\n### 13.3 PostgreSQL / pgvector 连接失败\n\n- 确认 PostgreSQL 已安装 pgvector 扩展:`SELECT * FROM pg_extension WHERE extname='vector';`\n- 如未安装:`CREATE EXTENSION vector;`\n- 确认 `PG_HOST`、`PG_PORT`、`PG_PASSWORD` 配置正确\n- **不使用 AI 向量功能时,此错误不影响基本 CRUD 操作**\n\n### 13.4 嵌入服务启动失败\n\n```\nOSError: [WinError 1455] 页面文件太小,无法完成操作\n```\n\n- Windows 下加载 2GB 模型需要足够的虚拟内存\n- 解决:增加 Windows 页面文件大小(系统属性 → 高级 → 性能 → 虚拟内存)\n- 或降低模型规模换用 `bge-small-zh-v1.5`\n\n### 13.5 AI 挂载返回空结果\n\n- 确认 `DEEPSEEK_API_KEY` 已正确配置\n- 确认 DeepSeek API 余额充足\n- 关键词 fallback 不需要 API Key,但准确率较低\n- 确认 `baluga.ai.matching.openai-base-url` 指向正确的 API 地址\n\n### 13.6 前端 Vite 代理不生效\n\n- 确认 `vite.config.ts` 中 proxy 配置指向 `http://localhost:8080`\n- 确认后端已启动并监听 8080 端口\n- 前端 API 调用路径必须以 `/api` 开头才能触发代理\n\n### 13.7 文件上传 500 错误\n\n- 检查 `BALUGA_UPLOAD_DIR` 目录是否存在且有写入权限\n- 默认使用系统临时目录,确认磁盘空间充足\n- 文件大小上限 500MB,检查是否超出\n\n---\n\n## 14. 后续规划\n\n| 规划项 | 说明 |\n|---|---|\n| SpringDoc OpenAPI | 集成 Swagger/Knife4j 自动生成 API 文档 |\n| Flyway 集成 | 自动化数据库迁移,替代手动执行 SQL |\n| Docker Compose | 一键启动所有中间件和服务 |\n| Redis 缓存 | 热点知识点缓存、会话管理 |\n| Elasticsearch | 资源全文检索、混合搜索 |\n| MinIO / OSS | 对象存储替代本地文件存储 |\n| 多 Agent 架构 | 将挂载流水线升级为多 Agent 协作模式 |\n| 教育知识图谱 | 完整的三层知识体系(课程→章→节→知识点→能力目标) |\n| 自动知识抽取 | 从教材/课件中自动抽取新知识点 |\n| LLM 微调 | 基于 FeedbackCollector 收集的 JSONL 数据微调挂载模型 |\n| AI Tutor 联动 | 挂载结果驱动个性化学习路径推荐 |\n| 视频 OCR/转录 | 视频内容自动提取与标注 |\n| Sa-Token 认证 | 统一身份认证与权限管理 |\n| Nacos 配置中心 | 动态配置管理与服务发现 |\n\n---\n\n## 附录\n\n### A. 系统设计文档\n\n- [资源挂载系统架构设计](docs/resource-mounting-system-design.md)\n- [AI 资源知识挂载架构](docs/requirements/2026-05-19-ai-resource-knowledge-mounting-architecture.md)\n- [AI 自动映射需求澄清](docs/requirements/2026-05-12-clarify-requirement-ai-auto-mapping-...)\n\n### B. 后端模块设计\n\n- [后端目录结构说明](backend/STRUCTURE.md)\n\n### C. 技术参考\n\n- [BGE-large-zh-v1.5 模型](https://huggingface.co/BAAI/bge-large-zh-v1.5) — 中文嵌入模型\n- [pgvector](https://github.com/pgvector/pgvector) — PostgreSQL 向量扩展\n- [DeepSeek API](https://platform.deepseek.com/api-docs) — LLM 推理服务\n- [MyBatis-Plus](https://baomidou.com/) — 增强版 MyBatis\n"}, "f38fedb771e0592a7e0678b7a025a5ec450c61f2": {"t": 1785766221.1372473, "n": 1057, "h": "fbd83265e8e745bd", "c": "# resource-center\n\n这是一个基于 Vue 3 与 Vite 的资源中台前端项目,目前以功能 demo 和交互原型为主。\n\n## 推荐的 IDE 配置\n\n[VS Code](https://code.visualstudio.com/) + [Vue 官方插件(Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.volar),并禁用 Vetur。\n\n## 推荐的浏览器配置\n\n- Chromium 内核浏览器(Chrome、Edge、Brave 等):\n - 安装 [Vue.js devtools](https://chromewebstore.google.com/detail/vuejs-devtools/nhdogjmejiglipccpnnnanhbledajbpd)\n - 在 Chrome DevTools 中启用 [Custom Object Formatter](http://bit.ly/object-formatters)\n- Firefox:\n - 安装 [Vue.js devtools](https://addons.mozilla.org/en-US/firefox/addon/vue-js-devtools/)\n - 在 Firefox DevTools 中启用 [Custom Object Formatter](https://fxdx.dev/firefox-devtools-custom-object-formatters/)\n\n## `.vue` 导入的 TypeScript 支持\n\nTypeScript 默认无法直接处理 `.vue` 文件的类型信息,因此本项目使用 `vue-tsc` 代替 `tsc` 做类型检查。在编辑器中需要使用 Volar,让 TypeScript 语言服务能够正确识别 `.vue` 类型。\n\n## 配置参考\n\n详见 [Vite 配置文档](https://vite.dev/config/)。\n\n## 项目安装\n\n```sh\nnpm install\n```\n\n## 本地开发\n\n```sh\nnpm run dev\n```\n\n## 运行测试\n\n```sh\nnpm test\n```\n\n## 生产构建\n\n```sh\nnpm run build\n```\n\n## 本地预览构建结果\n\n```sh\nnpm run preview\n```\n"}, "f00be948189c3a7a0876d40778289361f238c294": {"t": 1785766224.4033062, "n": 1376, "h": "aef4943f94ce5c2d", "c": "# resource-center\n\nThis template should help get you started developing with Vue 3 in Vite.\n\n## Recommended IDE Setup\n\n[VS Code](https://code.visualstudio.com/) + [Vue (Official)](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur).\n\n## Recommended Browser Setup\n\n- Chromium-based browsers (Chrome, Edge, Brave, etc.):\n - [Vue.js devtools](https://chromewebstore.google.com/detail/vuejs-devtools/nhdogjmejiglipccpnnnanhbledajbpd)\n - [Turn on Custom Object Formatter in Chrome DevTools](http://bit.ly/object-formatters)\n- Firefox:\n - [Vue.js devtools](https://addons.mozilla.org/en-US/firefox/addon/vue-js-devtools/)\n - [Turn on Custom Object Formatter in Firefox DevTools](https://fxdx.dev/firefox-devtools-custom-object-formatters/)\n\n## Type Support for `.vue` Imports in TS\n\nTypeScript cannot handle type information for `.vue` imports by default, so we replace the `tsc` CLI with `vue-tsc` for type checking. In editors, we need [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) to make the TypeScript language service aware of `.vue` types.\n\n## Customize configuration\n\nSee [Vite Configuration Reference](https://vite.dev/config/).\n\n## Project Setup\n\n```sh\nnpm install\n```\n\n### Compile and Hot-Reload for Development\n\n```sh\nnpm run dev\n```\n\n### Type-Check, Compile and Minify for Production\n\n```sh\nnpm run build\n```\n"}}, "backend/src/main/java/com/baluga/backend/modules/mapping/service/impl/MappingServiceImpl.java": {"b8b80f59949f1a012f5c0b5dd8e6b305333bdd5d": {"t": 1786068306.7293518, "n": 23403, "h": "adb12d311c85c4cf", "c": "package com.baluga.backend.modules.mapping.service.impl;\n\nimport com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;\nimport com.baomidou.mybatisplus.core.toolkit.Wrappers;\nimport com.baomidou.mybatisplus.extension.plugins.pagination.Page;\nimport com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;\nimport com.baluga.backend.infrastructure.integration.ai.AiMatchingConfig;\nimport com.baluga.backend.infrastructure.integration.ai.AiMatchingProvider;\nimport com.baluga.backend.infrastructure.integration.ai.AiMatchingProvider.ResourceInfo;\nimport com.baluga.backend.infrastructure.integration.ai.KeywordFallbackMatchingProvider;\nimport com.baluga.backend.infrastructure.integration.ai.OpenAiCompatibleMatchingProvider;\nimport com.baluga.backend.infrastructure.integration.matching.ResourceCollector;\nimport com.baluga.backend.modules.mapping.dto.request.MappingBatchCreateRequest;\nimport com.baluga.backend.modules.mapping.dto.request.MappingRecordPageRequest;\nimport com.baluga.backend.modules.mapping.dto.response.KnowledgePointVO;\nimport com.baluga.backend.modules.mapping.dto.response.MappingBatchVO;\nimport com.baluga.backend.modules.mapping.dto.response.MappingCandidateVO;\nimport com.baluga.backend.modules.mapping.dto.response.MappingFilterOptionsVO;\nimport com.baluga.backend.modules.mapping.dto.response.MappingFilterOptionsVO.SelectOption;\nimport com.baluga.backend.modules.mapping.dto.response.MappingRecordVO;\nimport com.baluga.backend.modules.mapping.dto.response.MappingSummaryVO;\nimport com.baluga.backend.modules.mapping.entity.KnowledgePoint;\nimport com.baluga.backend.modules.mapping.entity.MappingBatch;\nimport com.baluga.backend.modules.mapping.entity.MappingCandidate;\nimport com.baluga.backend.modules.mapping.entity.MappingRecord;\nimport com.baluga.backend.modules.mapping.mapper.KnowledgePointMapper;\nimport com.baluga.backend.modules.mapping.mapper.MappingBatchMapper;\nimport com.baluga.backend.modules.mapping.mapper.MappingCandidateMapper;\nimport com.baluga.backend.modules.mapping.mapper.MappingRecordMapper;\nimport com.baluga.backend.modules.mapping.service.MappingService;\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.springframework.stereotype.Service;\nimport org.springframework.transaction.annotation.Transactional;\nimport org.springframework.util.StringUtils;\n\nimport java.time.LocalDateTime;\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\nimport java.util.HashSet;\nimport java.util.stream.Collectors;\n\n\n@Service\npublic class MappingServiceImpl extends ServiceImpl implements MappingService {\n\n private static final Logger log = LoggerFactory.getLogger(MappingServiceImpl.class);\n\n private final MappingRecordMapper mappingRecordMapper;\n private final MappingCandidateMapper mappingCandidateMapper;\n private final MappingBatchMapper mappingBatchMapper;\n private final KnowledgePointMapper knowledgePointMapper;\n private final ResourceCollector resourceCollector;\n private final ObjectMapper objectMapper;\n private final AiMatchingConfig aiConfig;\n private final MappingBatchExecutor batchExecutor;\n private AiMatchingProvider currentProvider;\n\n public MappingServiceImpl(\n MappingRecordMapper mappingRecordMapper,\n MappingCandidateMapper mappingCandidateMapper,\n MappingBatchMapper mappingBatchMapper,\n KnowledgePointMapper knowledgePointMapper,\n ResourceCollector resourceCollector,\n ObjectMapper objectMapper,\n AiMatchingConfig aiConfig,\n MappingBatchExecutor batchExecutor) {\n this.mappingRecordMapper = mappingRecordMapper;\n this.mappingCandidateMapper = mappingCandidateMapper;\n this.mappingBatchMapper = mappingBatchMapper;\n this.knowledgePointMapper = knowledgePointMapper;\n this.resourceCollector = resourceCollector;\n this.objectMapper = objectMapper;\n this.batchExecutor = batchExecutor;\n\n // Diagnostic: print actual config values at startup\n log.info(\"AI Matching Config — provider: '{}'\", aiConfig.getProvider());\n log.info(\"AI Matching Config — base-url: '{}'\", aiConfig.getOpenaiBaseUrl());\n log.info(\"AI Matching Config — model: '{}'\", aiConfig.getOpenaiModel());\n String keyPreview = aiConfig.getOpenaiApiKey();\n if (keyPreview != null && !keyPreview.isEmpty()) {\n log.info(\"AI Matching Config — api-key: '{}...{}' (length={})\",\n keyPreview.substring(0, Math.min(8, keyPreview.length())),\n keyPreview.substring(Math.max(0, keyPreview.length() - 4)),\n keyPreview.length());\n } else {\n log.info(\"AI Matching Config — api-key: \");\n }\n\n // Select AI provider based on config\n this.aiConfig = aiConfig;\n this.currentProvider = resolveProvider(aiConfig.getProvider());\n log.info(\"Mapping service using AI provider: {}\", this.currentProvider.getProviderName());\n }\n\n private AiMatchingProvider resolveProvider(String name) {\n boolean isOpenAi = \"openai\".equalsIgnoreCase(name);\n boolean hasKey = aiConfig.getOpenaiApiKey() != null && !aiConfig.getOpenaiApiKey().isBlank();\n if (isOpenAi && hasKey) {\n return new OpenAiCompatibleMatchingProvider(aiConfig, objectMapper);\n }\n if (isOpenAi && !hasKey) {\n log.warn(\"AI Matching — provider is 'openai' but API key is empty. \"\n + \"Set DEEPSEEK_API_KEY environment variable or configure openai-api-key in yml.\");\n }\n return new KeywordFallbackMatchingProvider();\n }\n\n // ========== Query ==========\n\n @Override\n public Page pageRecords(MappingRecordPageRequest request) {\n LambdaQueryWrapper wrapper = buildRecordQueryWrapper(request);\n\n int pageNum = request.getPage() != null && request.getPage() > 0 ? request.getPage() : 1;\n int pageSize = request.getPageSize() != null && request.getPageSize() > 0 ? request.getPageSize() : 10;\n Page page = mappingRecordMapper.selectPage(new Page<>(pageNum, pageSize), wrapper);\n\n List voList = page.getRecords().stream()\n .map(this::toRecordVO)\n .collect(Collectors.toList());\n\n Page result = new Page<>(page.getCurrent(), page.getSize(), page.getTotal());\n result.setRecords(voList);\n return result;\n }\n\n @Override\n public MappingRecordVO getRecordDetail(Long id) {\n MappingRecord record = mappingRecordMapper.selectById(id);\n if (record == null) {\n return null;\n }\n return toRecordVO(record);\n }\n\n // ========== Mutations ==========\n\n @Override\n @Transactional(rollbackFor = Exception.class)\n public MappingRecordVO reviewRecord(Long id, String action) {\n MappingRecord record = mappingRecordMapper.selectById(id);\n if (record == null) {\n throw new IllegalArgumentException(\"映射记录不存在\");\n }\n\n if (\"approve\".equals(action)) {\n record.setReviewStatus(\"approved\");\n // persist the selected candidate as primary\n if (record.getSelectedCandidateId() != null) {\n MappingCandidate candidate = mappingCandidateMapper.selectById(record.getSelectedCandidateId());\n if (candidate != null) {\n record.setPrimaryKnowledgePointId(candidate.getKnowledgePointId());\n }\n }\n } else {\n record.setReviewStatus(\"rejected\");\n record.setSelectedCandidateId(null);\n record.setPrimaryKnowledgePointId(null);\n }\n\n mappingRecordMapper.updateById(record);\n return toRecordVO(record);\n }\n\n @Override\n @Transactional(rollbackFor = Exception.class)\n public MappingRecordVO selectCandidate(Long recordId, Long candidateId) {\n MappingRecord record = mappingRecordMapper.selectById(recordId);\n if (record == null) {\n throw new IllegalArgumentException(\"映射记录不存在\");\n }\n\n record.setSelectedCandidateId(candidateId);\n\n if (candidateId != null) {\n MappingCandidate candidate = mappingCandidateMapper.selectById(candidateId);\n if (candidate != null) {\n record.setPrimaryKnowledgePointId(candidate.getKnowledgePointId());\n }\n } else {\n record.setPrimaryKnowledgePointId(null);\n }\n\n mappingRecordMapper.updateById(record);\n return toRecordVO(record);\n }\n\n // ========== Batch ==========\n\n @Override\n @Transactional(rollbackFor = Exception.class)\n public MappingBatchVO createBatch(MappingBatchCreateRequest request) {\n MappingBatch batch = MappingBatch.builder()\n .label(normalize(request.getLabel()))\n .status(\"pending\")\n .courseFilter(normalize(request.getCourse()))\n .totalResources(0)\n .matchedCount(0)\n .failedCount(0)\n .createdBy(normalize(request.getCreatedBy()))\n .deleted(0)\n .build();\n mappingBatchMapper.insert(batch);\n\n List resources = resourceCollector.collectResources(\n request.getCourse(), request.getResourceType());\n\n for (ResourceInfo ri : resources) {\n MappingRecord record = MappingRecord.builder()\n .batchId(batch.getId())\n .resourceType(ri.type())\n .resourceId(ri.resourceId())\n .resourceTitle(ri.title())\n .courseName(ri.course())\n .chapterName(ri.chapter())\n .reviewStatus(\"pending\")\n .confidenceLevel(\"low\")\n .deleted(0)\n .build();\n mappingRecordMapper.insert(record);\n }\n\n batch.setTotalResources(resources.size());\n mappingBatchMapper.updateById(batch);\n\n return MappingBatchVO.fromEntity(batch);\n }\n\n @Override\n public MappingBatchVO runBatch(Long batchId) {\n MappingBatch batch = mappingBatchMapper.selectById(batchId);\n if (batch == null) {\n throw new IllegalArgumentException(\"批次不存在\");\n }\n MappingBatchVO vo = MappingBatchVO.fromEntity(batch);\n // Run asynchronously via separate bean so @Async proxy works\n batchExecutor.executeAsync(batch);\n return vo;\n }\n\n public void setProvider(String name) {\n this.currentProvider = resolveProvider(name);\n log.info(\"AI 匹配提供者已切换: {}\", currentProvider.getProviderName());\n }\n\n public String getCurrentProviderName() {\n return currentProvider.getProviderName();\n }\n\n @Override\n public Map batchRemap(Long batchId) {\n MappingBatch batch = mappingBatchMapper.selectById(batchId);\n if (batch == null) {\n throw new IllegalArgumentException(\"批次不存在\");\n }\n\n // Reset failed records to pending\n LambdaQueryWrapper wrapper = Wrappers.lambdaQuery();\n wrapper.eq(MappingRecord::getBatchId, batch.getId())\n .eq(MappingRecord::getConfidenceLevel, \"low\");\n List lowRecords = mappingRecordMapper.selectList(wrapper);\n long count = 0;\n for (MappingRecord r : lowRecords) {\n r.setReviewStatus(\"pending\");\n r.setConfidenceLevel(\"low\");\n r.setSelectedCandidateId(null);\n r.setPrimaryKnowledgePointId(null);\n mappingRecordMapper.updateById(r);\n count++;\n }\n\n // Re-run matching\n runBatch(batch.getId());\n\n Map result = new HashMap<>();\n result.put(\"resetCount\", count);\n return result;\n }\n\n // ========== Summary & Filters ==========\n\n @Override\n public MappingSummaryVO getSummary(MappingRecordPageRequest request) {\n // Query without overviewStatus filter\n LambdaQueryWrapper wrapper = buildBaseRecordQueryWrapper(request);\n List records = mappingRecordMapper.selectList(wrapper);\n\n int pendingCount = 0, matchedCount = 0, manualReviewCount = 0, confirmedCount = 0, failedCount = 0, lowConfidenceCount = 0;\n\n for (MappingRecord r : records) {\n String overview = resolveOverviewStatus(r);\n switch (overview) {\n case \"pending\": pendingCount++; break;\n case \"matched\": matchedCount++; break;\n case \"manual-review\": manualReviewCount++; break;\n case \"confirmed\": confirmedCount++; break;\n case \"failed\": failedCount++; break;\n }\n if (\"low\".equals(r.getConfidenceLevel())) {\n lowConfidenceCount++;\n }\n }\n\n return MappingSummaryVO.builder()\n .pendingCount(pendingCount)\n .matchedCount(matchedCount)\n .manualReviewCount(manualReviewCount)\n .confirmedCount(confirmedCount)\n .failedCount(failedCount)\n .lowConfidenceCount(lowConfidenceCount)\n .build();\n }\n\n @Override\n public MappingFilterOptionsVO getFilterOptions() {\n List allRecords = mappingRecordMapper.selectList(Wrappers.lambdaQuery(MappingRecord.class));\n List allBatches = mappingBatchMapper.selectList(\n Wrappers.lambdaQuery().orderByDesc(MappingBatch::getCreatedAt));\n\n Set courses = new HashSet<>();\n Set chapters = new HashSet<>();\n for (MappingRecord r : allRecords) {\n if (StringUtils.hasText(r.getCourseName())) courses.add(r.getCourseName());\n if (StringUtils.hasText(r.getChapterName())) chapters.add(r.getChapterName());\n }\n\n return MappingFilterOptionsVO.builder()\n .resourceTypeOptions(List.of(\n SelectOption.builder().value(\"all\").label(\"全部资源类型\").build(),\n SelectOption.builder().value(\"article\").label(\"图文\").build(),\n SelectOption.builder().value(\"courseware\").label(\"课件\").build(),\n SelectOption.builder().value(\"question\").label(\"习题\").build(),\n SelectOption.builder().value(\"video\").label(\"录屏\").build(),\n SelectOption.builder().value(\"excerpt\").label(\"节选\").build()\n ))\n .courseOptions(buildSelectOptions(courses, \"全部课程\"))\n .chapterOptions(buildSelectOptions(chapters, \"全部章节\"))\n .batchOptions(allBatches.stream()\n .map(b -> SelectOption.builder().value(b.getId().toString()).label(b.getLabel()).build())\n .collect(Collectors.toList()))\n .build();\n }\n\n @Override\n public Page pageKnowledgePoints(String keyword, String course, Integer page, Integer pageSize) {\n LambdaQueryWrapper wrapper = Wrappers.lambdaQuery();\n String kw = normalize(keyword);\n if (StringUtils.hasText(kw)) {\n wrapper.like(KnowledgePoint::getName, kw);\n }\n String cs = normalize(course);\n if (StringUtils.hasText(cs)) {\n wrapper.eq(KnowledgePoint::getCourse, cs);\n }\n wrapper.orderByAsc(KnowledgePoint::getCourse).orderByAsc(KnowledgePoint::getName);\n\n int p = page != null && page > 0 ? page : 1;\n int ps = pageSize != null && pageSize > 0 ? pageSize : 20;\n Page kpPage = knowledgePointMapper.selectPage(new Page<>(p, ps), wrapper);\n\n Page result = new Page<>(kpPage.getCurrent(), kpPage.getSize(), kpPage.getTotal());\n result.setRecords(kpPage.getRecords().stream().map(KnowledgePointVO::fromEntity).collect(Collectors.toList()));\n return result;\n }\n\n @Override\n public Page pageBatches(String keyword, Integer page, Integer pageSize) {\n LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(MappingBatch.class);\n String kw = normalize(keyword);\n if (StringUtils.hasText(kw)) {\n wrapper.like(MappingBatch::getLabel, kw);\n }\n\n int p = page != null && page > 0 ? page : 1;\n int ps = pageSize != null && pageSize > 0 ? pageSize : 20;\n Page batchPage = mappingBatchMapper.selectPage(new Page<>(p, ps), wrapper);\n\n Page result = new Page<>(batchPage.getCurrent(), batchPage.getSize(), batchPage.getTotal());\n result.setRecords(batchPage.getRecords().stream().map(MappingBatchVO::fromEntity).collect(Collectors.toList()));\n return result;\n }\n\n // ========== Private Helpers ==========\n\n private MappingRecordVO toRecordVO(MappingRecord record) {\n // Get candidates\n LambdaQueryWrapper cw = Wrappers.lambdaQuery();\n cw.eq(MappingCandidate::getMappingRecordId, record.getId())\n .orderByAsc(MappingCandidate::getId);\n List candidates = mappingCandidateMapper.selectList(cw);\n List candidateVOs = candidates.stream()\n .map(MappingCandidateVO::fromEntity)\n .collect(Collectors.toList());\n\n // Get batch label\n String batchLabel = \"\";\n if (record.getBatchId() != null) {\n MappingBatch batch = mappingBatchMapper.selectById(record.getBatchId());\n if (batch != null) {\n batchLabel = batch.getLabel();\n }\n }\n\n // Get primary knowledge point name\n String primaryKpName = null;\n if (record.getPrimaryKnowledgePointId() != null) {\n KnowledgePoint kp = knowledgePointMapper.selectById(record.getPrimaryKnowledgePointId());\n if (kp != null) {\n primaryKpName = kp.getName();\n }\n }\n\n return MappingRecordVO.builder()\n .id(record.getId())\n .resourceTitle(record.getResourceTitle())\n .resourceType(record.getResourceType())\n .courseName(record.getCourseName())\n .chapterName(record.getChapterName())\n .batchId(record.getBatchId())\n .batchLabel(batchLabel)\n .reviewStatus(record.getReviewStatus())\n .confidenceLevel(record.getConfidenceLevel())\n .primaryKnowledgePoint(primaryKpName)\n .selectedCandidateId(record.getSelectedCandidateId())\n .candidates(candidateVOs)\n .build();\n }\n\n private LambdaQueryWrapper buildRecordQueryWrapper(MappingRecordPageRequest request) {\n LambdaQueryWrapper wrapper = buildBaseRecordQueryWrapper(request);\n\n // Apply derived overviewStatus filter\n String overview = normalize(request.getOverviewStatus());\n if (StringUtils.hasText(overview)) {\n switch (overview) {\n case \"failed\":\n wrapper.eq(MappingRecord::getReviewStatus, \"rejected\");\n break;\n case \"confirmed\":\n wrapper.eq(MappingRecord::getReviewStatus, \"approved\")\n .isNotNull(MappingRecord::getSelectedCandidateId);\n break;\n case \"manual-review\":\n wrapper.eq(MappingRecord::getConfidenceLevel, \"low\")\n .ne(MappingRecord::getReviewStatus, \"rejected\");\n break;\n case \"matched\":\n wrapper.isNotNull(MappingRecord::getSelectedCandidateId)\n .ne(MappingRecord::getReviewStatus, \"approved\");\n break;\n case \"pending\":\n wrapper.eq(MappingRecord::getReviewStatus, \"pending\")\n .isNull(MappingRecord::getSelectedCandidateId)\n .ne(MappingRecord::getConfidenceLevel, \"low\");\n break;\n }\n }\n\n return wrapper;\n }\n\n private LambdaQueryWrapper buildBaseRecordQueryWrapper(MappingRecordPageRequest request) {\n LambdaQueryWrapper wrapper = Wrappers.lambdaQuery();\n\n String keyword = normalize(request.getKeyword());\n if (StringUtils.hasText(keyword)) {\n wrapper.and(w -> w.like(MappingRecord::getResourceTitle, keyword)\n .or()\n .like(MappingRecord::getCourseName, keyword)\n .or()\n .like(MappingRecord::getChapterName, keyword));\n }\n\n String resourceType = normalize(request.getResourceType());\n if (StringUtils.hasText(resourceType)) {\n wrapper.eq(MappingRecord::getResourceType, resourceType);\n }\n\n String course = normalize(request.getCourse());\n if (StringUtils.hasText(course)) {\n wrapper.eq(MappingRecord::getCourseName, course);\n }\n\n String chapter = normalize(request.getChapter());\n if (StringUtils.hasText(chapter)) {\n wrapper.eq(MappingRecord::getChapterName, chapter);\n }\n\n String batchId = normalize(request.getBatchId());\n if (StringUtils.hasText(batchId)) {\n try {\n wrapper.eq(MappingRecord::getBatchId, Long.parseLong(batchId));\n } catch (NumberFormatException ignored) {}\n }\n\n String reviewStatus = normalize(request.getReviewStatus());\n if (StringUtils.hasText(reviewStatus)) {\n wrapper.eq(MappingRecord::getReviewStatus, reviewStatus);\n }\n\n String confidenceLevel = normalize(request.getConfidenceLevel());\n if (StringUtils.hasText(confidenceLevel)) {\n wrapper.eq(MappingRecord::getConfidenceLevel, confidenceLevel);\n }\n\n wrapper.orderByDesc(MappingRecord::getUpdatedAt).orderByDesc(MappingRecord::getId);\n return wrapper;\n }\n\n private String resolveOverviewStatus(MappingRecord record) {\n if (\"rejected\".equals(record.getReviewStatus())) return \"failed\";\n if (\"approved\".equals(record.getReviewStatus()) && record.getSelectedCandidateId() != null) return \"confirmed\";\n if (\"low\".equals(record.getConfidenceLevel())) return \"manual-review\";\n if (record.getSelectedCandidateId() != null) return \"matched\";\n return \"pending\";\n }\n\n private List buildSelectOptions(Set values, String allLabel) {\n List options = new ArrayList<>();\n options.add(SelectOption.builder().value(\"all\").label(allLabel).build());\n for (String v : values.stream().sorted().collect(Collectors.toList())) {\n options.add(SelectOption.builder().value(v).label(v).build());\n }\n return options;\n }\n\n private String normalize(String value) {\n return value == null ? \"\" : value.trim();\n }\n}\n"}}}}