公司正在建设 AI 教育平台,需要一个后端模块支持教师上传教案文件(PDF/DOC/DOCX),并通过 AI 对教案内容进行结构化解析,提取教学目标、年级学科、关键词、摘要等信息,用于后续的教学资源管理和智能推荐。
- 教案数字化:将非结构化的教案文件转化为结构化数据
- 自动化提取:通过 AI 减少人工标注和整理的工作量
- 标准化管理:统一教案的元数据格式,便于检索和管理
- 教育平台的教案资源入库
- 教学质量分析(基于教案结构化数据)
- 教案智能推荐和搜索
| 技术 | 版本 | 用途 |
|---|---|---|
| Java | 21 | 编程语言 |
| Spring Boot | 3.2.5 | Web 框架 |
| MyBatis-Plus | 3.5.6 | ORM 框架 |
| MySQL | 8.0+ | 数据库 |
| Apache Tika | 2.9.2 | 文件类型检测 + 文本提取 |
| Lombok | - | 代码简化 |
| Maven | 3.8+ | 构建工具 |
- JDK 17+
- Maven 3.8+
- MySQL 8.0+
# 登录 MySQL,执行建表脚本
mysql -u root -p < schema.sql或者手动执行 schema.sql 中的 SQL 语句。
编辑 src/main/resources/application.yml,修改数据库连接信息:
spring:
datasource:
url: jdbc:mysql://localhost:3306/lesson_parser?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8mb4&allowPublicKeyRetrieval=true
username: root
password: root # 改为你的密码方式一:H2 内存数据库(零配置,推荐快速体验)
mvn spring-boot:run -Dspring-boot.run.profiles=dev无需安装 MySQL,使用 H2 内存数据库,启动即可用。
H2 控制台:http://localhost:8080/h2-console
方式二:MySQL 数据库(生产环境)
# 确保 MySQL 已启动,库表已创建
mvn clean package -DskipTests
java -jar target/lesson-plan-parser-1.0.0.jar启动后访问:http://localhost:8080
完整建表语句见 schema.sql,核心结构如下:
| 字段 | 类型 | 说明 |
|---|---|---|
| id | BIGINT (PK, AUTO) | 文件ID |
| original_filename | VARCHAR(500) | 原始文件名 |
| file_type | VARCHAR(20) | 文件类型 (pdf/doc/docx) |
| file_size | BIGINT | 文件大小 (字节) |
| storage_path | VARCHAR(1000) | 存储路径 |
| uploader | VARCHAR(100) | 上传人 |
| status | VARCHAR(20) | 状态: pending/parsing/success/failed |
| created_at | DATETIME | 创建时间 |
| updated_at | DATETIME | 更新时间 |
| 字段 | 类型 | 说明 |
|---|---|---|
| id | BIGINT (PK, AUTO) | 主键ID |
| file_id | BIGINT (FK) | 关联文件ID |
| title | VARCHAR(500) | 教案标题 |
| grade_subject | VARCHAR(100) | 年级学科 |
| teaching_objective | TEXT | 教学目标 |
| keywords | TEXT | 关键词 (JSON数组) |
| summary | TEXT | 摘要 |
| model_name | VARCHAR(100) | 模型名称/mock标识 |
| parse_duration_ms | BIGINT | 解析耗时 (毫秒) |
| fail_reason | VARCHAR(1000) | 失败原因 |
| created_at | DATETIME | 创建时间 |
| updated_at | DATETIME | 更新时间 |
POST /api/files/upload
Content-Type: multipart/form-data
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| file | MultipartFile | 是 | 文件 (PDF/DOC/DOCX, ≤50MB) |
| uploader | String | 否 | 上传人,默认 "anonymous" |
成功响应:
{
"code": 200,
"message": "上传成功",
"data": {
"id": 1,
"originalFilename": "高中数学-函数教案.pdf",
"fileType": "pdf",
"fileSize": 102400,
"uploader": "teacher_zhang",
"status": "pending",
"createdAt": "2026-07-03T22:00:00"
}
}错误响应(格式不支持):
{
"code": 400,
"message": "不支持的文件格式,仅允许 PDF、DOC、DOCX",
"data": null
}GET /api/files?page=1&size=10
响应:
{
"code": 200,
"message": "success",
"data": {
"records": [
{
"id": 1,
"originalFilename": "高中数学-函数教案.pdf",
"fileType": "pdf",
"fileSize": 102400,
"uploader": "teacher_zhang",
"status": "success",
"createdAt": "2026-07-03T22:00:00",
"updatedAt": "2026-07-03T22:00:05"
}
],
"total": 1,
"size": 10,
"current": 1
}
}GET /api/files/{id}
GET /api/files/{id}/status
响应:
{
"code": 200,
"message": "success",
"data": "success"
}POST /api/files/{id}/parse
响应:
{
"code": 200,
"message": "解析任务已提交",
"data": null
}状态流转:pending → parsing → success / failed
GET /api/files/{id}/parse-result
解析成功响应:
{
"code": 200,
"message": "success",
"data": {
"fileId": 1,
"status": "success",
"title": "高中数学-函数教案 - 教学设计",
"gradeSubject": "高中数学",
"teachingObjective": "理解并掌握核心概念,能够运用所学知识解决实际问题",
"keywords": "[\"函数\", \"导数\", \"极值\", \"单调性\", \"数学思维\"]",
"summary": "本教案围绕高中数学核心知识点展开...",
"modelName": "mock-v1.0",
"parseDurationMs": 3000,
"failReason": null,
"createdAt": "2026-07-03T22:00:03"
}
}解析中响应:
{
"code": 200,
"message": "success",
"data": {
"fileId": 1,
"status": "parsing",
"title": null,
"failReason": null
}
}解析失败响应:
{
"code": 200,
"message": "success",
"data": {
"fileId": 1,
"status": "failed",
"title": null,
"failReason": "模拟失败:文件内容格式不规范,无法提取有效教学结构",
"modelName": "mock-v1.0",
"parseDurationMs": 3000
}
}curl -X POST http://localhost:8080/api/files/upload \
-F "file=@test-lesson.pdf" \
-F "uploader=teacher_zhang"结果:✅ 返回 200,文件记录创建,状态为 pending
curl -X POST http://localhost:8080/api/files/upload \
-F "file=@test-image.png" \
-F "uploader=teacher_zhang"结果:✅ 返回 400,提示 "不支持的文件格式,仅允许 PDF、DOC、DOCX"
# 1. 上传文件
curl -X POST http://localhost:8080/api/files/upload \
-F "file=@test-lesson.docx" -F "uploader=teacher_li"
# 返回 id=2
# 2. 触发解析
curl -X POST http://localhost:8080/api/files/2/parse
# 3. 查询状态(解析中)
curl http://localhost:8080/api/files/2/status
# 返回 "parsing"
# 4. 等待 3 秒后查询结果
curl http://localhost:8080/api/files/2/parse-result
# 返回解析成功,包含 title/gradeSubject/teachingObjective 等结构化数据结果:✅ 状态正确流转 pending → parsing → success,解析结果完整
# 多次上传+解析,约 10% 概率触发 mock 失败
curl -X POST http://localhost:8080/api/files/3/parse
sleep 4
curl http://localhost:8080/api/files/3/parse-result结果:✅ 失败时 status 为 failed,failReason 包含失败描述
# 对已完成的文件再次触发解析
curl -X POST http://localhost:8080/api/files/2/parse结果:✅ 允许重新解析,旧结果被清除,新结果覆盖
- OpenClaw (MIMO v2.5 Pro):作为主要开发助手,协助完成项目结构设计、代码生成、文档编写
- 项目整体架构设计(分层结构、接口设计、数据库设计)
- 全部代码框架和核心逻辑的生成
- Mock AI 解析提供商的实现(含随机结果生成逻辑)
- README 文档的编写
- 异常处理和统一响应封装
- 数据库表结构设计的合理性审查(字段类型、索引策略)
- Mock 解析逻辑的随机性和失败率调整
- 文件类型校验策略(扩展名 + MIME 双重校验)
- 接口路径和参数命名的规范化
- SQL 建表脚本的完整性检查
现象:
在实现文件上传校验时,先用 file.getInputStream() 获取输入流进行 Tika 类型检测,检测通过后再调用 file.transferTo() 存储文件。但发现 transferTo 报错 "Stream closed",或 Tika 检测结果为 application/octet-stream 而非预期的 PDF/DOCX 类型。
排查过程:
- 查看 Tika 文档发现
tika.detect(InputStream)会消费流内容 - Spring 的
MultipartFile.getInputStream()每次调用返回新的流,但某些实现下底层临时文件可能已被清理 - 在 debug 模式下发现 Tika 消费流后,
transferTo的底层流已不可用
解决方案:
改为先用扩展名做初步校验,再用 Tika 检测 MIME 类型。检测完成后使用 transferTo 的 File 重载版本而非流版本:
// 先检测类型
try (InputStream is = file.getInputStream()) {
String detectedType = tika.detect(is, originalName);
// 校验...
}
// 再存储文件(使用 File 方式,不依赖流状态)
file.transferTo(target.toFile());这样确保 Tika 检测和文件存储使用独立的流操作,互不影响。
lesson-plan-parser/
├── pom.xml
├── schema.sql # 建表 SQL
├── README.md
└── src/
├── main/
│ ├── java/com/example/lessonparser/
│ │ ├── LessonParserApplication.java
│ │ ├── controller/
│ │ │ └── FileController.java
│ │ ├── service/
│ │ │ ├── FileService.java
│ │ │ ├── AiParseProvider.java # AI 解析接口(策略模式)
│ │ │ └── impl/
│ │ │ ├── FileServiceImpl.java
│ │ │ └── MockAiParseProvider.java
│ │ ├── entity/
│ │ │ ├── LessonFile.java
│ │ │ └── LessonParseResult.java
│ │ ├── dto/
│ │ │ ├── ApiResponse.java
│ │ │ ├── FileUploadResponse.java
│ │ │ ├── FileInfoResponse.java
│ │ │ └── ParseResultResponse.java
│ │ ├── mapper/
│ │ │ ├── LessonFileMapper.java
│ │ │ └── LessonParseResultMapper.java
│ │ ├── config/
│ │ │ ├── MyBatisPlusConfig.java
│ │ │ ├── FileStorageConfig.java
│ │ │ └── WebConfig.java
│ │ ├── enums/
│ │ │ └── FileStatus.java
│ │ └── exception/
│ │ ├── BusinessException.java
│ │ └── GlobalExceptionHandler.java
│ └── resources/
│ └── application.yml
└── test/
└── java/com/example/lessonparser/
项目使用策略模式 (AiParseProvider 接口),新增真实 AI 接入只需:
- 实现
AiParseProvider接口 - 添加
@ConditionalOnProperty(name = "ai.provider", havingValue = "openai")注解 - 在
application.yml中切换ai.provider配置
示例:接入 DeepSeek API 只需新建 DeepSeekAiParseProvider 实现类,调用其 Chat API 解析教案内容即可。