Files
OpenCode 68a9071cf1 feat: reconcile local master with origin (exa/hot-platform hunters, leaks ledger, latest results)
Local branch had diverged from origin/master (sibling commits on the same base). Rewrote local history linearly on top of origin/master, folding in all local content: exa/hot-platform discovery hunters, leaks ledger, nightly sweep orchestrator, updated .gitignore and skill docs, plus the latest hunt output and vault state. Remote-only files (vault keys, channel scripts) were restored rather than dropped, so the resulting tree is a full union of both sides.
2026-08-07 12:53:47 +08:00

1 line
92 KiB
JSON
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{"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<T>`,自定义 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 <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<T>`,自定义 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 <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<MappingRecordMapper, MappingRecord> 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: <EMPTY or NULL>\");\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<MappingRecordVO> pageRecords(MappingRecordPageRequest request) {\n LambdaQueryWrapper<MappingRecord> 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<MappingRecord> page = mappingRecordMapper.selectPage(new Page<>(pageNum, pageSize), wrapper);\n\n List<MappingRecordVO> voList = page.getRecords().stream()\n .map(this::toRecordVO)\n .collect(Collectors.toList());\n\n Page<MappingRecordVO> 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<ResourceInfo> 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<String, Long> 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<MappingRecord> wrapper = Wrappers.lambdaQuery();\n wrapper.eq(MappingRecord::getBatchId, batch.getId())\n .eq(MappingRecord::getConfidenceLevel, \"low\");\n List<MappingRecord> 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<String, Long> 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<MappingRecord> wrapper = buildBaseRecordQueryWrapper(request);\n List<MappingRecord> 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<MappingRecord> allRecords = mappingRecordMapper.selectList(Wrappers.lambdaQuery(MappingRecord.class));\n List<MappingBatch> allBatches = mappingBatchMapper.selectList(\n Wrappers.<MappingBatch>lambdaQuery().orderByDesc(MappingBatch::getCreatedAt));\n\n Set<String> courses = new HashSet<>();\n Set<String> 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<KnowledgePointVO> pageKnowledgePoints(String keyword, String course, Integer page, Integer pageSize) {\n LambdaQueryWrapper<KnowledgePoint> 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<KnowledgePoint> kpPage = knowledgePointMapper.selectPage(new Page<>(p, ps), wrapper);\n\n Page<KnowledgePointVO> 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<MappingBatchVO> pageBatches(String keyword, Integer page, Integer pageSize) {\n LambdaQueryWrapper<MappingBatch> 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<MappingBatch> batchPage = mappingBatchMapper.selectPage(new Page<>(p, ps), wrapper);\n\n Page<MappingBatchVO> 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<MappingCandidate> cw = Wrappers.lambdaQuery();\n cw.eq(MappingCandidate::getMappingRecordId, record.getId())\n .orderByAsc(MappingCandidate::getId);\n List<MappingCandidate> candidates = mappingCandidateMapper.selectList(cw);\n List<MappingCandidateVO> 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<MappingRecord> buildRecordQueryWrapper(MappingRecordPageRequest request) {\n LambdaQueryWrapper<MappingRecord> 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<MappingRecord> buildBaseRecordQueryWrapper(MappingRecordPageRequest request) {\n LambdaQueryWrapper<MappingRecord> 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<SelectOption> buildSelectOptions(Set<String> values, String allLabel) {\n List<SelectOption> 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"}}}}