- 从服务器拉取完整代码 - 按框架规范整理项目结构 - 配置 Drone CI 测试环境部署 - 包含后端(FastAPI)、前端(Vue3)、管理端 技术栈: Vue3 + TypeScript + FastAPI + MySQL
345 lines
10 KiB
Markdown
345 lines
10 KiB
Markdown
# 文件上传和AI分析问题修复报告
|
||
|
||
## 修复时间
|
||
2025-10-16 22:40 - 2025-10-17 06:54
|
||
|
||
## 问题描述
|
||
|
||
用户报告了两个关键问题:
|
||
|
||
### 问题1:文件上传后刷新页面消失
|
||
- **现象**:上传文件后显示成功,也看得到文件,但刷新页面后文件就消失了
|
||
- **根因**:数据库记录没有真正创建(数据库中没有对应的course_materials记录)
|
||
|
||
### 问题2:AI分析知识点未正确启动Dify工作流
|
||
- **现象**:文件上传后AI分析未自动启动
|
||
- **根因**:前端代码只改变UI状态,没有真正调用AI分析API
|
||
|
||
## 根本原因分析
|
||
|
||
### 原因1:http.ts的upload方法双重数据提取
|
||
|
||
**文件**:`/root/aiedu/kaopeilian-frontend/src/utils/http.ts`
|
||
|
||
**问题代码(第392行)**:
|
||
```typescript
|
||
upload<T = any>(url: string, file: File, config?: RequestConfig): Promise<ResponseData<T>> {
|
||
// ... FormData构建 ...
|
||
return this.instance.post(url, formData, config)
|
||
.then((response) => response.data as ResponseData<T>) // ❌ 多余的数据提取
|
||
}
|
||
```
|
||
|
||
**数据流转过程**:
|
||
1. 后端返回:`{ code: 200, data: { file_url: '...', file_type: '...', file_size: ... } }`
|
||
2. 响应拦截器处理后返回:`{ code: 200, data: { file_url: '...', ... } }`
|
||
3. upload方法又提取`.data`,最终返回:`{ file_url: '...', ... }` **(丢失code字段)**
|
||
4. 前端判断 `uploadRes.code === 200` 失败 → 抛出"文件上传失败"错误
|
||
|
||
**修复措施**:
|
||
```typescript
|
||
upload<T = any>(url: string, file: File, config?: RequestConfig): Promise<ResponseData<T>> {
|
||
// ... FormData构建 ...
|
||
return this.instance.post(url, formData, config) // ✅ 直接返回
|
||
}
|
||
```
|
||
|
||
### 原因2:上传成功后未启动AI分析
|
||
|
||
**文件**:`/root/aiedu/kaopeilian-frontend/src/views/manager/edit-course.vue`
|
||
|
||
**问题代码(第1158-1165行)**:
|
||
```typescript
|
||
// 上传成功后,设置状态为分析中
|
||
setTimeout(() => {
|
||
const material = materialList.value.find(m => m.id === newMaterial.id)
|
||
if (material) {
|
||
material.status = 'analyzing' // ❌ 只改变UI状态
|
||
ElMessage.info(`正在分析资料"${material.name}"的知识点...`)
|
||
}
|
||
}, 1000)
|
||
```
|
||
|
||
**问题**:只修改了UI状态,没有调用API启动AI分析
|
||
|
||
**修复措施**:
|
||
```typescript
|
||
// 上传成功后,自动启动AI分析
|
||
setTimeout(async () => {
|
||
const material = materialList.value.find(m => m.id === newMaterial.id)
|
||
if (material) {
|
||
console.log('自动启动AI知识点分析:', material.id, material.name)
|
||
await analyzeWithAI(material) // ✅ 真正调用AI分析
|
||
}
|
||
}, 1000)
|
||
```
|
||
|
||
### 原因3:AI分析API返回404
|
||
|
||
**现象**:`POST /api/v1/courses/1/materials/30/analyze` 返回404
|
||
|
||
**根因**:前端传递的material_id=30在数据库中不存在
|
||
- 数据库最新的material_id是26
|
||
- material_id=30是前端临时添加到列表的,但数据库记录创建失败
|
||
- 因为之前的http.ts bug导致创建资料记录的API调用失败
|
||
|
||
## 修复详情
|
||
|
||
### 修复1:http.ts的upload方法
|
||
|
||
**文件**:`/root/aiedu/kaopeilian-frontend/src/utils/http.ts`(第392行)
|
||
|
||
**修改前**:
|
||
```typescript
|
||
}).then((response) => response.data as ResponseData<T>)
|
||
```
|
||
|
||
**修改后**:
|
||
```typescript
|
||
}) // 直接返回响应拦截器处理后的结果
|
||
```
|
||
|
||
### 修复2:自动启动AI分析
|
||
|
||
**文件**:`/root/aiedu/kaopeilian-frontend/src/views/manager/edit-course.vue`(第1158-1165行)
|
||
|
||
**修改前**:
|
||
```typescript
|
||
setTimeout(() => {
|
||
const material = materialList.value.find(m => m.id === newMaterial.id)
|
||
if (material) {
|
||
material.status = 'analyzing'
|
||
ElMessage.info(`正在分析资料"${material.name}"的知识点...`)
|
||
}
|
||
}, 1000)
|
||
```
|
||
|
||
**修改后**:
|
||
```typescript
|
||
setTimeout(async () => {
|
||
const material = materialList.value.find(m => m.id === newMaterial.id)
|
||
if (material) {
|
||
console.log('自动启动AI知识点分析:', material.id, material.name)
|
||
await analyzeWithAI(material)
|
||
}
|
||
}, 1000)
|
||
```
|
||
|
||
## 验证结果
|
||
|
||
### 验证1:upload方法返回正确结构
|
||
✅ 修复后,`upload()` 方法正确返回 `{ code: 200, data: {...} }` 结构
|
||
|
||
### 验证2:数据库记录成功创建
|
||
✅ 调用 `courseApi.addMaterial()` 时能正确判断 `res.code === 200`
|
||
✅ 数据库中成功创建 `course_materials` 记录
|
||
✅ 刷新页面后文件不再消失
|
||
|
||
### 验证3:AI分析自动启动
|
||
✅ 文件上传成功后自动调用 `analyzeWithAI(material)`
|
||
✅ 正确调用 `/api/v1/courses/{course_id}/materials/{material_id}/analyze` API
|
||
✅ Dify工作流成功启动
|
||
|
||
## 完整的数据流程
|
||
|
||
### 修复后的正确流程
|
||
|
||
1. **用户选择文件并点击"确认上传"**
|
||
|
||
2. **前端上传文件到服务器**
|
||
```typescript
|
||
const uploadRes = await request.upload(
|
||
`/api/v1/upload/course/${courseId}/materials`,
|
||
file.raw
|
||
)
|
||
// 返回:{ code: 200, data: { file_url: '...', file_type: '...', file_size: ... } }
|
||
```
|
||
|
||
3. **判断上传成功**
|
||
```typescript
|
||
if (uploadRes.code === 200 && uploadRes.data) { // ✅ 判断通过
|
||
```
|
||
|
||
4. **创建数据库记录**
|
||
```typescript
|
||
const res = await courseApi.addMaterial(courseId, {
|
||
name: file.name,
|
||
file_url: uploadRes.data.file_url,
|
||
file_type: uploadRes.data.file_type,
|
||
file_size: uploadRes.data.file_size
|
||
})
|
||
// 返回:{ code: 200, data: { id: 27, name: '...', ... } }
|
||
```
|
||
|
||
5. **添加到前端列表**
|
||
```typescript
|
||
const newMaterial = {
|
||
id: res.data.id, // ✅ 真实的数据库ID
|
||
name: res.data.name,
|
||
status: 'pending',
|
||
knowledgePoints: []
|
||
}
|
||
materialList.value.push(newMaterial)
|
||
```
|
||
|
||
6. **自动启动AI分析**
|
||
```typescript
|
||
setTimeout(async () => {
|
||
await analyzeWithAI(material)
|
||
// 调用:POST /api/v1/courses/{course_id}/materials/{material.id}/analyze
|
||
}, 1000)
|
||
```
|
||
|
||
7. **Dify工作流启动**
|
||
- 后端接收分析请求
|
||
- 调用Dify API上传文件并启动知识点分析工作流
|
||
- 返回workflow_run_id给前端
|
||
- 工作流完成后自动保存知识点到数据库
|
||
|
||
8. **刷新页面**
|
||
- 从数据库加载 `course_materials`
|
||
- 文件和知识点都正常显示 ✅
|
||
|
||
## 技术要点总结
|
||
|
||
### 1. 响应拦截器与方法封装的关系
|
||
|
||
**核心原则**:
|
||
- 响应拦截器负责统一处理所有响应,返回标准的 `{code, message, data}` 结构
|
||
- 具体方法(get/post/upload等)只负责构造请求,**不应再次处理响应结构**
|
||
- 保持"单一职责":拦截器处理响应,方法构造请求
|
||
|
||
**常见错误**:
|
||
```typescript
|
||
// ❌ 错误:双重数据提取
|
||
upload() {
|
||
return this.instance.post(url, data).then(res => res.data)
|
||
// 响应拦截器已返回 {code, data},再次.data会丢失code
|
||
}
|
||
|
||
// ✅ 正确:直接返回
|
||
upload() {
|
||
return this.instance.post(url, data)
|
||
}
|
||
```
|
||
|
||
### 2. 异步操作的完整性
|
||
|
||
**原则**:
|
||
- UI状态更新必须基于实际的后端操作结果
|
||
- 不能"假装成功"——只改UI不调API
|
||
- 异步任务必须真正启动,不能只显示"进行中"
|
||
|
||
**修复前**:
|
||
```typescript
|
||
material.status = 'analyzing' // 假装在分析
|
||
ElMessage.info('正在分析...') // 但实际没调用API
|
||
```
|
||
|
||
**修复后**:
|
||
```typescript
|
||
await analyzeWithAI(material) // 真正调用API
|
||
// API内部会:
|
||
// 1. 设置状态为analyzing
|
||
// 2. 调用后端API
|
||
// 3. 根据结果更新状态为completed/failed
|
||
```
|
||
|
||
### 3. 数据库事务的正确性
|
||
|
||
虽然这次不是数据库事务的问题,但需要注意:
|
||
- FastAPI的 `get_db` 依赖会自动commit
|
||
- 但前提是API调用成功执行到结束
|
||
- 如果API调用失败(如前端判断错误没有调用),数据库操作根本不会发生
|
||
|
||
## 相关文件
|
||
|
||
### 修改的文件
|
||
1. `/root/aiedu/kaopeilian-frontend/src/utils/http.ts` - 修复upload方法
|
||
2. `/root/aiedu/kaopeilian-frontend/src/views/manager/edit-course.vue` - 修复AI分析启动
|
||
|
||
### 文档更新
|
||
1. `/root/aiedu/考培练系统规划/全链路联调/联调经验汇总.md` - 记录问题和修复
|
||
2. `/root/aiedu/考培练系统规划/全链路联调/规范与约定-团队基线.md` - 更新团队规范
|
||
|
||
### 后端相关文件(已验证)
|
||
1. `/root/aiedu/kaopeilian-backend/app/api/v1/upload.py` - 文件上传API ✓
|
||
2. `/root/aiedu/kaopeilian-backend/app/api/v1/courses.py` - 资料管理API ✓
|
||
3. `/root/aiedu/kaopeilian-backend/app/api/v1/knowledge_analysis.py` - 知识点分析API ✓
|
||
4. `/root/aiedu/kaopeilian-backend/app/services/course_service.py` - 课程服务 ✓
|
||
|
||
## 测试建议
|
||
|
||
### 测试步骤
|
||
1. 打开编辑课程页面
|
||
2. 点击"上传资料",选择一个PDF文件
|
||
3. 点击"确认上传"
|
||
4. **验证点1**:上传成功提示,文件出现在列表中
|
||
5. **验证点2**:1秒后显示"AI正在分析文件内容,提取知识点..."
|
||
6. **验证点3**:等待30-180秒,知识点分析完成
|
||
7. **验证点4**:刷新页面,文件和知识点仍然存在
|
||
|
||
### 数据库验证
|
||
```sql
|
||
-- 查询最新的资料记录
|
||
SELECT id, course_id, name, created_at
|
||
FROM course_materials
|
||
ORDER BY created_at DESC
|
||
LIMIT 5;
|
||
|
||
-- 查询知识点
|
||
SELECT id, course_id, material_id, name, type, source
|
||
FROM knowledge_points
|
||
WHERE material_id = {新资料ID}
|
||
ORDER BY created_at DESC;
|
||
```
|
||
|
||
### 日志验证
|
||
```bash
|
||
# 查看后端日志
|
||
docker compose logs backend --tail 100 | grep -E "(上传成功|添加课程资料|知识点分析)"
|
||
|
||
# 查看前端日志
|
||
# 浏览器控制台应显示:
|
||
# - 开始上传文件: xxx.pdf
|
||
# - 文件上传响应: { code: 200, data: {...} }
|
||
# - 创建资料记录响应: { code: 200, data: { id: 27, ... } }
|
||
# - 自动启动AI知识点分析: 27 xxx.pdf
|
||
# - AI分析API响应: { code: 200, data: { status: 'succeeded', ... } }
|
||
```
|
||
|
||
## 后续改进建议
|
||
|
||
1. **添加单元测试**
|
||
- 测试 `http.ts` 的 `upload` 方法返回结构
|
||
- 测试 `courseApi.addMaterial` 的完整流程
|
||
|
||
2. **增强错误处理**
|
||
- 文件上传失败时显示详细错误信息
|
||
- AI分析失败时允许手动重试
|
||
|
||
3. **优化用户体验**
|
||
- 显示上传进度条
|
||
- 显示AI分析进度(轮询任务状态)
|
||
- 支持批量上传文件
|
||
|
||
4. **监控和告警**
|
||
- 记录文件上传失败率
|
||
- 记录AI分析成功率
|
||
- 异常时发送告警
|
||
|
||
## 总结
|
||
|
||
本次修复解决了两个关键问题:
|
||
|
||
1. ✅ **http.ts双重数据提取** - 导致文件上传后无法创建数据库记录
|
||
2. ✅ **AI分析未真正启动** - 导致知识点无法自动提取
|
||
|
||
核心教训:
|
||
- 响应拦截器和方法封装要保持单一职责,避免重复处理
|
||
- UI状态必须基于真实的API调用结果,不能"假装成功"
|
||
- 异步操作的完整性至关重要,每一步都要真正执行
|
||
|
||
修复后的完整流程已验证通过,文件上传、数据库记录创建、AI知识点分析都能正常工作。
|
||
|