feat: 初始化考培练系统项目
- 从服务器拉取完整代码 - 按框架规范整理项目结构 - 配置 Drone CI 测试环境部署 - 包含后端(FastAPI)、前端(Vue3)、管理端 技术栈: Vue3 + TypeScript + FastAPI + MySQL
This commit is contained in:
0
知识库/参考代码/python_dev_project/.env.example
Normal file
0
知识库/参考代码/python_dev_project/.env.example
Normal file
0
知识库/参考代码/python_dev_project/Makefile
Normal file
0
知识库/参考代码/python_dev_project/Makefile
Normal file
@@ -0,0 +1,590 @@
|
||||
openapi: 3.0.0
|
||||
info:
|
||||
title: 考试模块 API
|
||||
version: 1.0.0
|
||||
description: 考培练系统考试模块的 API 接口定义
|
||||
|
||||
servers:
|
||||
- url: http://localhost:8000/api/v1
|
||||
description: 本地开发服务器
|
||||
|
||||
paths:
|
||||
/exams/start:
|
||||
post:
|
||||
summary: 开始考试(动态组卷)
|
||||
description: 根据指定参数动态生成试卷并开始考试
|
||||
tags:
|
||||
- 考试管理
|
||||
security:
|
||||
- bearerAuth: []
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ExamStartRequest'
|
||||
responses:
|
||||
'200':
|
||||
description: 考试开始成功
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ExamSessionResponse'
|
||||
'400':
|
||||
description: 请求参数错误或存在未完成的考试
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
'401':
|
||||
$ref: '#/components/responses/UnauthorizedError'
|
||||
'500':
|
||||
$ref: '#/components/responses/InternalServerError'
|
||||
|
||||
/exams/{examId}/submit:
|
||||
post:
|
||||
summary: 提交考试
|
||||
description: 提交考试答案并生成成绩
|
||||
tags:
|
||||
- 考试管理
|
||||
security:
|
||||
- bearerAuth: []
|
||||
parameters:
|
||||
- name: examId
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: integer
|
||||
description: 考试ID
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ExamSubmitRequest'
|
||||
responses:
|
||||
'200':
|
||||
description: 考试提交成功
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ExamResultResponse'
|
||||
'400':
|
||||
description: 考试状态不正确或已超时
|
||||
'401':
|
||||
$ref: '#/components/responses/UnauthorizedError'
|
||||
'403':
|
||||
description: 无权访问此考试
|
||||
'404':
|
||||
description: 考试不存在
|
||||
|
||||
/exams/{examId}:
|
||||
get:
|
||||
summary: 获取考试详情
|
||||
description: 获取考试会话信息和题目列表(不包含答案)
|
||||
tags:
|
||||
- 考试管理
|
||||
security:
|
||||
- bearerAuth: []
|
||||
parameters:
|
||||
- name: examId
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: integer
|
||||
description: 考试ID
|
||||
responses:
|
||||
'200':
|
||||
description: 获取成功
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ExamSessionResponse'
|
||||
'401':
|
||||
$ref: '#/components/responses/UnauthorizedError'
|
||||
'404':
|
||||
description: 考试不存在
|
||||
|
||||
/exams/records:
|
||||
get:
|
||||
summary: 获取考试记录列表
|
||||
description: 分页获取用户的考试记录
|
||||
tags:
|
||||
- 考试管理
|
||||
security:
|
||||
- bearerAuth: []
|
||||
parameters:
|
||||
- name: page
|
||||
in: query
|
||||
schema:
|
||||
type: integer
|
||||
minimum: 1
|
||||
default: 1
|
||||
description: 页码
|
||||
- name: page_size
|
||||
in: query
|
||||
schema:
|
||||
type: integer
|
||||
minimum: 1
|
||||
maximum: 100
|
||||
default: 20
|
||||
description: 每页数量
|
||||
- name: status
|
||||
in: query
|
||||
schema:
|
||||
$ref: '#/components/schemas/ExamStatus'
|
||||
description: 考试状态筛选
|
||||
responses:
|
||||
'200':
|
||||
description: 获取成功
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ExamRecordListResponse'
|
||||
'401':
|
||||
$ref: '#/components/responses/UnauthorizedError'
|
||||
|
||||
/exams/{examId}/result:
|
||||
get:
|
||||
summary: 获取考试结果
|
||||
description: 获取详细的考试成绩、统计信息和答案详情
|
||||
tags:
|
||||
- 考试管理
|
||||
security:
|
||||
- bearerAuth: []
|
||||
parameters:
|
||||
- name: examId
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: integer
|
||||
description: 考试ID
|
||||
responses:
|
||||
'200':
|
||||
description: 获取成功
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ExamResultResponse'
|
||||
'401':
|
||||
$ref: '#/components/responses/UnauthorizedError'
|
||||
'404':
|
||||
description: 考试结果不存在
|
||||
|
||||
/exams/mistakes:
|
||||
get:
|
||||
summary: 获取错题列表
|
||||
description: 分页获取用户的错题记录
|
||||
tags:
|
||||
- 错题管理
|
||||
security:
|
||||
- bearerAuth: []
|
||||
parameters:
|
||||
- name: page
|
||||
in: query
|
||||
schema:
|
||||
type: integer
|
||||
minimum: 1
|
||||
default: 1
|
||||
description: 页码
|
||||
- name: page_size
|
||||
in: query
|
||||
schema:
|
||||
type: integer
|
||||
minimum: 1
|
||||
maximum: 100
|
||||
default: 20
|
||||
description: 每页数量
|
||||
- name: is_mastered
|
||||
in: query
|
||||
schema:
|
||||
type: boolean
|
||||
description: 是否已掌握
|
||||
responses:
|
||||
'200':
|
||||
description: 获取成功
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/MistakeListResponse'
|
||||
'401':
|
||||
$ref: '#/components/responses/UnauthorizedError'
|
||||
|
||||
components:
|
||||
securitySchemes:
|
||||
bearerAuth:
|
||||
type: http
|
||||
scheme: bearer
|
||||
bearerFormat: JWT
|
||||
|
||||
schemas:
|
||||
ExamStartRequest:
|
||||
type: object
|
||||
required:
|
||||
- exam_name
|
||||
properties:
|
||||
course_id:
|
||||
type: integer
|
||||
description: 课程ID
|
||||
exam_name:
|
||||
type: string
|
||||
description: 考试名称
|
||||
question_count:
|
||||
type: integer
|
||||
minimum: 1
|
||||
maximum: 100
|
||||
default: 20
|
||||
description: 题目数量
|
||||
time_limit:
|
||||
type: integer
|
||||
minimum: 1
|
||||
maximum: 300
|
||||
description: 考试时长(分钟)
|
||||
difficulty:
|
||||
type: integer
|
||||
minimum: 1
|
||||
maximum: 5
|
||||
description: 难度等级
|
||||
knowledge_points:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
description: 知识点范围
|
||||
question_types:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/QuestionType'
|
||||
description: 题型范围
|
||||
|
||||
ExamSubmitRequest:
|
||||
type: object
|
||||
required:
|
||||
- answers
|
||||
properties:
|
||||
answers:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/AnswerSubmitRequest'
|
||||
description: 答案列表
|
||||
force_submit:
|
||||
type: boolean
|
||||
default: false
|
||||
description: 是否强制提交
|
||||
|
||||
AnswerSubmitRequest:
|
||||
type: object
|
||||
required:
|
||||
- question_id
|
||||
- user_answer
|
||||
properties:
|
||||
question_id:
|
||||
type: integer
|
||||
description: 题目ID
|
||||
user_answer:
|
||||
type: string
|
||||
description: 用户答案
|
||||
time_spent:
|
||||
type: integer
|
||||
minimum: 0
|
||||
description: 答题用时(秒)
|
||||
|
||||
ExamStatus:
|
||||
type: string
|
||||
enum:
|
||||
- created
|
||||
- in_progress
|
||||
- submitted
|
||||
- graded
|
||||
- expired
|
||||
description: 考试状态
|
||||
|
||||
QuestionType:
|
||||
type: string
|
||||
enum:
|
||||
- single_choice
|
||||
- multiple_choice
|
||||
- true_false
|
||||
- fill_blank
|
||||
- short_answer
|
||||
- essay
|
||||
description: 题目类型
|
||||
|
||||
ExamSessionResponse:
|
||||
type: object
|
||||
properties:
|
||||
code:
|
||||
type: integer
|
||||
example: 200
|
||||
message:
|
||||
type: string
|
||||
example: success
|
||||
data:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: integer
|
||||
exam_name:
|
||||
type: string
|
||||
course_id:
|
||||
type: integer
|
||||
total_questions:
|
||||
type: integer
|
||||
total_score:
|
||||
type: number
|
||||
pass_score:
|
||||
type: number
|
||||
time_limit:
|
||||
type: integer
|
||||
status:
|
||||
$ref: '#/components/schemas/ExamStatus'
|
||||
started_at:
|
||||
type: string
|
||||
format: date-time
|
||||
submitted_at:
|
||||
type: string
|
||||
format: date-time
|
||||
questions:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/QuestionResponse'
|
||||
|
||||
QuestionResponse:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: integer
|
||||
question_order:
|
||||
type: integer
|
||||
question_type:
|
||||
$ref: '#/components/schemas/QuestionType'
|
||||
question_text:
|
||||
type: string
|
||||
options:
|
||||
type: object
|
||||
additionalProperties:
|
||||
type: string
|
||||
score:
|
||||
type: number
|
||||
knowledge_points:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
difficulty:
|
||||
type: integer
|
||||
|
||||
ExamResultResponse:
|
||||
type: object
|
||||
properties:
|
||||
code:
|
||||
type: integer
|
||||
example: 200
|
||||
message:
|
||||
type: string
|
||||
example: success
|
||||
data:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: integer
|
||||
exam_id:
|
||||
type: integer
|
||||
exam_name:
|
||||
type: string
|
||||
total_score:
|
||||
type: number
|
||||
actual_score:
|
||||
type: number
|
||||
percentage_score:
|
||||
type: number
|
||||
is_passed:
|
||||
type: boolean
|
||||
total_questions:
|
||||
type: integer
|
||||
answered_questions:
|
||||
type: integer
|
||||
correct_questions:
|
||||
type: integer
|
||||
question_type_stats:
|
||||
type: object
|
||||
knowledge_stats:
|
||||
type: object
|
||||
total_time_spent:
|
||||
type: integer
|
||||
average_time_per_question:
|
||||
type: number
|
||||
ai_analysis:
|
||||
type: string
|
||||
improvement_suggestions:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
answer_details:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/AnswerDetailResponse'
|
||||
|
||||
AnswerDetailResponse:
|
||||
type: object
|
||||
properties:
|
||||
question_id:
|
||||
type: integer
|
||||
question_order:
|
||||
type: integer
|
||||
question_type:
|
||||
$ref: '#/components/schemas/QuestionType'
|
||||
question_text:
|
||||
type: string
|
||||
user_answer:
|
||||
type: string
|
||||
correct_answer:
|
||||
type: string
|
||||
is_correct:
|
||||
type: boolean
|
||||
actual_score:
|
||||
type: number
|
||||
total_score:
|
||||
type: number
|
||||
answer_explanation:
|
||||
type: string
|
||||
ai_feedback:
|
||||
type: string
|
||||
|
||||
ExamRecordListResponse:
|
||||
type: object
|
||||
properties:
|
||||
code:
|
||||
type: integer
|
||||
example: 200
|
||||
message:
|
||||
type: string
|
||||
example: success
|
||||
data:
|
||||
type: object
|
||||
properties:
|
||||
items:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/ExamRecordResponse'
|
||||
total:
|
||||
type: integer
|
||||
page:
|
||||
type: integer
|
||||
page_size:
|
||||
type: integer
|
||||
total_pages:
|
||||
type: integer
|
||||
|
||||
ExamRecordResponse:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: integer
|
||||
exam_name:
|
||||
type: string
|
||||
course_id:
|
||||
type: integer
|
||||
course_name:
|
||||
type: string
|
||||
status:
|
||||
$ref: '#/components/schemas/ExamStatus'
|
||||
total_questions:
|
||||
type: integer
|
||||
actual_score:
|
||||
type: number
|
||||
percentage_score:
|
||||
type: number
|
||||
is_passed:
|
||||
type: boolean
|
||||
started_at:
|
||||
type: string
|
||||
format: date-time
|
||||
submitted_at:
|
||||
type: string
|
||||
format: date-time
|
||||
|
||||
MistakeListResponse:
|
||||
type: object
|
||||
properties:
|
||||
code:
|
||||
type: integer
|
||||
example: 200
|
||||
message:
|
||||
type: string
|
||||
example: success
|
||||
data:
|
||||
type: object
|
||||
properties:
|
||||
items:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/MistakeResponse'
|
||||
total:
|
||||
type: integer
|
||||
page:
|
||||
type: integer
|
||||
page_size:
|
||||
type: integer
|
||||
total_pages:
|
||||
type: integer
|
||||
|
||||
MistakeResponse:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: integer
|
||||
question_type:
|
||||
$ref: '#/components/schemas/QuestionType'
|
||||
question_text:
|
||||
type: string
|
||||
user_answer:
|
||||
type: string
|
||||
correct_answer:
|
||||
type: string
|
||||
knowledge_points:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
difficulty:
|
||||
type: integer
|
||||
review_count:
|
||||
type: integer
|
||||
is_mastered:
|
||||
type: boolean
|
||||
last_review_at:
|
||||
type: string
|
||||
format: date-time
|
||||
created_at:
|
||||
type: string
|
||||
format: date-time
|
||||
|
||||
ErrorResponse:
|
||||
type: object
|
||||
properties:
|
||||
code:
|
||||
type: integer
|
||||
message:
|
||||
type: string
|
||||
error:
|
||||
type: object
|
||||
properties:
|
||||
code:
|
||||
type: string
|
||||
message:
|
||||
type: string
|
||||
details:
|
||||
type: object
|
||||
|
||||
responses:
|
||||
UnauthorizedError:
|
||||
description: 未授权,需要登录
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
|
||||
InternalServerError:
|
||||
description: 服务器内部错误
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
220
知识库/参考代码/python_dev_project/docs/modules/exam_module.md
Normal file
220
知识库/参考代码/python_dev_project/docs/modules/exam_module.md
Normal file
@@ -0,0 +1,220 @@
|
||||
# 考试模块(Exam Module)
|
||||
|
||||
## 模块概述
|
||||
|
||||
考试模块是考培练系统的核心功能之一,提供动态组卷、在线考试、自动判题、成绩分析和错题管理等功能。该模块与Dify AI平台集成,支持智能出题和主观题自动评分。
|
||||
|
||||
## 主要功能
|
||||
|
||||
### 1. 动态组卷
|
||||
- 根据课程、知识点、难度等参数动态生成试卷
|
||||
- 支持多种题型:单选题、多选题、判断题、填空题、简答题、论述题
|
||||
- 通过Dify工作流实现智能出题
|
||||
|
||||
### 2. 考试管理
|
||||
- 考试计时和状态管理
|
||||
- 防作弊机制(防重复提交、超时控制)
|
||||
- 题目与答案分离存储,确保安全性
|
||||
|
||||
### 3. 自动判题
|
||||
- 客观题自动判分
|
||||
- 主观题通过Dify AI评分
|
||||
- 实时计算成绩和统计信息
|
||||
|
||||
### 4. 成绩分析
|
||||
- 详细的成绩报告
|
||||
- 题型正确率统计
|
||||
- 知识点掌握情况分析
|
||||
- AI生成的学习建议
|
||||
|
||||
### 5. 错题管理
|
||||
- 自动记录错题
|
||||
- 支持错题复习和标记掌握状态
|
||||
- 按知识点分类管理
|
||||
|
||||
## 技术架构
|
||||
|
||||
### 数据模型
|
||||
- `ExamSession`: 考试会话表
|
||||
- `ExamQuestion`: 考试题目表
|
||||
- `ExamAnswer`: 考试答案表
|
||||
- `ExamResult`: 考试结果表
|
||||
- `Mistake`: 错题记录表
|
||||
|
||||
### API接口
|
||||
|
||||
#### 1. 开始考试
|
||||
```
|
||||
POST /api/v1/exams/start
|
||||
```
|
||||
- 功能:动态生成试卷并开始考试
|
||||
- 权限:需要登录
|
||||
- 参数:课程ID、题目数量、时长、难度等
|
||||
|
||||
#### 2. 提交考试
|
||||
```
|
||||
POST /api/v1/exams/{examId}/submit
|
||||
```
|
||||
- 功能:提交答案并生成成绩
|
||||
- 权限:需要登录,只能提交自己的考试
|
||||
- 参数:答案列表
|
||||
|
||||
#### 3. 获取考试详情
|
||||
```
|
||||
GET /api/v1/exams/{examId}
|
||||
```
|
||||
- 功能:获取考试信息和题目(不含答案)
|
||||
- 权限:需要登录,只能查看自己的考试
|
||||
|
||||
#### 4. 获取考试记录
|
||||
```
|
||||
GET /api/v1/exams/records
|
||||
```
|
||||
- 功能:分页获取考试历史记录
|
||||
- 权限:需要登录
|
||||
- 支持按状态筛选
|
||||
|
||||
#### 5. 获取考试结果
|
||||
```
|
||||
GET /api/v1/exams/{examId}/result
|
||||
```
|
||||
- 功能:获取详细的考试成绩和分析
|
||||
- 权限:需要登录,只能查看自己的成绩
|
||||
|
||||
#### 6. 获取错题列表
|
||||
```
|
||||
GET /api/v1/exams/mistakes
|
||||
```
|
||||
- 功能:分页获取错题记录
|
||||
- 权限:需要登录
|
||||
- 支持按掌握状态筛选
|
||||
|
||||
## 配置说明
|
||||
|
||||
### 环境变量
|
||||
```env
|
||||
# Dify配置
|
||||
DIFY_API_BASE=https://api.dify.ai/v1
|
||||
DIFY_API_KEY=your_api_key
|
||||
DIFY_EXAM_WORKFLOW_ID=exam_workflow_id
|
||||
DIFY_EVAL_WORKFLOW_ID=eval_workflow_id
|
||||
DIFY_TIMEOUT=30
|
||||
```
|
||||
|
||||
### 考试参数限制
|
||||
- 题目数量:1-100题
|
||||
- 考试时长:1-300分钟
|
||||
- 难度等级:1-5级
|
||||
- 默认及格分:60分
|
||||
|
||||
## 使用示例
|
||||
|
||||
### 1. 开始考试
|
||||
```python
|
||||
# 请求
|
||||
POST /api/v1/exams/start
|
||||
{
|
||||
"exam_name": "Python基础测试",
|
||||
"question_count": 20,
|
||||
"time_limit": 60,
|
||||
"difficulty": 3,
|
||||
"knowledge_points": ["Python基础", "数据结构"],
|
||||
"question_types": ["single_choice", "true_false"]
|
||||
}
|
||||
|
||||
# 响应
|
||||
{
|
||||
"code": 200,
|
||||
"message": "考试开始成功",
|
||||
"data": {
|
||||
"id": 1,
|
||||
"exam_name": "Python基础测试",
|
||||
"total_questions": 20,
|
||||
"total_score": 100.0,
|
||||
"time_limit": 60,
|
||||
"status": "in_progress",
|
||||
"started_at": "2024-01-01T10:00:00",
|
||||
"questions": [...]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 提交考试
|
||||
```python
|
||||
# 请求
|
||||
POST /api/v1/exams/1/submit
|
||||
{
|
||||
"answers": [
|
||||
{
|
||||
"question_id": 1,
|
||||
"user_answer": "B",
|
||||
"time_spent": 30
|
||||
},
|
||||
{
|
||||
"question_id": 2,
|
||||
"user_answer": "True",
|
||||
"time_spent": 20
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
# 响应
|
||||
{
|
||||
"code": 200,
|
||||
"message": "考试提交成功",
|
||||
"data": {
|
||||
"exam_id": 1,
|
||||
"total_score": 100.0,
|
||||
"actual_score": 85.0,
|
||||
"percentage_score": 85.0,
|
||||
"is_passed": true,
|
||||
"correct_questions": 17,
|
||||
"ai_analysis": "您在Python基础部分表现优秀...",
|
||||
"improvement_suggestions": ["建议加强数据结构的学习"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 安全考虑
|
||||
|
||||
1. **认证控制**:所有接口需要JWT认证
|
||||
2. **权限隔离**:用户只能访问自己的考试数据
|
||||
3. **防作弊机制**:
|
||||
- 题目与答案分离存储
|
||||
- 考试状态严格控制
|
||||
- 超时自动结束
|
||||
- 防止重复提交
|
||||
|
||||
## 性能优化
|
||||
|
||||
1. **数据库索引**:
|
||||
- 用户ID和考试状态联合索引
|
||||
- 考试ID和题目顺序联合索引
|
||||
|
||||
2. **分页查询**:
|
||||
- 考试记录和错题列表支持分页
|
||||
- 默认每页20条,最大100条
|
||||
|
||||
3. **异步处理**:
|
||||
- 使用异步数据库操作
|
||||
- Dify API调用设置超时控制
|
||||
|
||||
## 扩展性
|
||||
|
||||
该模块设计考虑了未来的扩展需求:
|
||||
|
||||
1. **题库管理**:预留了题库查询接口
|
||||
2. **批量导入**:支持从Excel导入试题
|
||||
3. **考试模板**:可保存常用考试配置
|
||||
4. **团体考试**:支持班级或部门统一考试
|
||||
5. **证书生成**:考试通过后生成电子证书
|
||||
|
||||
## 测试覆盖
|
||||
|
||||
- 单元测试:覆盖所有服务层方法
|
||||
- 集成测试:覆盖所有API接口
|
||||
- 测试场景包括:
|
||||
- 正常流程测试
|
||||
- 异常情况处理
|
||||
- 权限控制验证
|
||||
- 边界条件测试
|
||||
187
知识库/参考代码/python_dev_project/src/api/v1/exams.py
Normal file
187
知识库/参考代码/python_dev_project/src/api/v1/exams.py
Normal file
@@ -0,0 +1,187 @@
|
||||
"""
|
||||
考试模块API路由
|
||||
"""
|
||||
from typing import Optional
|
||||
import logging
|
||||
|
||||
from fastapi import APIRouter, Depends, Query, status
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from ...core.deps import get_db, get_current_user
|
||||
from ...core.config import settings
|
||||
from ...models.user import User
|
||||
from ...models.exam import ExamStatus
|
||||
from ...schemas.base import BaseResponse, PaginatedResponse
|
||||
from ...schemas.exam import (
|
||||
ExamStartRequest, ExamSubmitRequest,
|
||||
ExamSessionResponse, ExamResultResponse,
|
||||
ExamRecordResponse, MistakeResponse
|
||||
)
|
||||
from ...services.exam_service import ExamService
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter(prefix="/exams", tags=["exams"])
|
||||
|
||||
|
||||
@router.post("/start", response_model=BaseResponse[ExamSessionResponse])
|
||||
async def start_exam(
|
||||
request: ExamStartRequest,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
开始考试(动态组卷)
|
||||
|
||||
- **course_id**: 课程ID(可选)
|
||||
- **exam_name**: 考试名称
|
||||
- **question_count**: 题目数量(1-100)
|
||||
- **time_limit**: 考试时长(分钟,可选)
|
||||
- **difficulty**: 难度等级(1-5,可选)
|
||||
- **knowledge_points**: 知识点范围(可选)
|
||||
- **question_types**: 题型范围(可选)
|
||||
"""
|
||||
service = ExamService(db)
|
||||
|
||||
try:
|
||||
exam_session = await service.start_exam(
|
||||
user_id=current_user.id,
|
||||
request=request,
|
||||
exam_workflow_id=settings.DIFY_EXAM_WORKFLOW_ID
|
||||
)
|
||||
|
||||
return BaseResponse(
|
||||
data=exam_session,
|
||||
message="考试开始成功"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Start exam failed: user_id={current_user.id}, error={str(e)}")
|
||||
raise
|
||||
|
||||
|
||||
@router.post("/{exam_id}/submit", response_model=BaseResponse[ExamResultResponse])
|
||||
async def submit_exam(
|
||||
exam_id: int,
|
||||
request: ExamSubmitRequest,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
提交考试
|
||||
|
||||
- **answers**: 答案列表
|
||||
- **force_submit**: 是否强制提交(忽略未答题目)
|
||||
"""
|
||||
service = ExamService(db)
|
||||
|
||||
try:
|
||||
exam_result = await service.submit_exam(
|
||||
user_id=current_user.id,
|
||||
exam_id=exam_id,
|
||||
request=request,
|
||||
eval_workflow_id=settings.DIFY_EVAL_WORKFLOW_ID
|
||||
)
|
||||
|
||||
return BaseResponse(
|
||||
data=exam_result,
|
||||
message="考试提交成功"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Submit exam failed: exam_id={exam_id}, user_id={current_user.id}, error={str(e)}")
|
||||
raise
|
||||
|
||||
|
||||
@router.get("/{exam_id}", response_model=BaseResponse[ExamSessionResponse])
|
||||
async def get_exam_detail(
|
||||
exam_id: int,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
获取考试详情
|
||||
|
||||
返回考试会话信息和题目列表(不包含答案)
|
||||
"""
|
||||
service = ExamService(db)
|
||||
|
||||
exam_session = await service.get_exam_detail(
|
||||
user_id=current_user.id,
|
||||
exam_id=exam_id
|
||||
)
|
||||
|
||||
return BaseResponse(data=exam_session)
|
||||
|
||||
|
||||
@router.get("/records", response_model=BaseResponse[PaginatedResponse[ExamRecordResponse]])
|
||||
async def get_exam_records(
|
||||
page: int = Query(1, ge=1, description="页码"),
|
||||
page_size: int = Query(20, ge=1, le=100, description="每页数量"),
|
||||
status: Optional[ExamStatus] = Query(None, description="考试状态筛选"),
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
获取考试记录列表(分页)
|
||||
|
||||
- **page**: 页码(从1开始)
|
||||
- **page_size**: 每页数量(1-100)
|
||||
- **status**: 考试状态筛选(可选)
|
||||
"""
|
||||
service = ExamService(db)
|
||||
|
||||
result = await service.get_exam_records(
|
||||
user_id=current_user.id,
|
||||
page=page,
|
||||
page_size=page_size,
|
||||
status=status
|
||||
)
|
||||
|
||||
return BaseResponse(data=result)
|
||||
|
||||
|
||||
@router.get("/{exam_id}/result", response_model=BaseResponse[ExamResultResponse])
|
||||
async def get_exam_result(
|
||||
exam_id: int,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
获取考试结果
|
||||
|
||||
返回详细的考试成绩、统计信息和答案详情
|
||||
"""
|
||||
service = ExamService(db)
|
||||
|
||||
exam_result = await service.get_exam_result(
|
||||
user_id=current_user.id,
|
||||
exam_id=exam_id
|
||||
)
|
||||
|
||||
return BaseResponse(data=exam_result)
|
||||
|
||||
|
||||
@router.get("/mistakes", response_model=BaseResponse[PaginatedResponse[MistakeResponse]])
|
||||
async def get_mistakes(
|
||||
page: int = Query(1, ge=1, description="页码"),
|
||||
page_size: int = Query(20, ge=1, le=100, description="每页数量"),
|
||||
is_mastered: Optional[bool] = Query(None, description="是否已掌握"),
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
获取错题列表(分页)
|
||||
|
||||
- **page**: 页码(从1开始)
|
||||
- **page_size**: 每页数量(1-100)
|
||||
- **is_mastered**: 是否已掌握(可选)
|
||||
"""
|
||||
service = ExamService(db)
|
||||
|
||||
result = await service.get_mistakes(
|
||||
user_id=current_user.id,
|
||||
page=page,
|
||||
page_size=page_size,
|
||||
is_mastered=is_mastered
|
||||
)
|
||||
|
||||
return BaseResponse(data=result)
|
||||
15
知识库/参考代码/python_dev_project/src/api/v1/router.py
Normal file
15
知识库/参考代码/python_dev_project/src/api/v1/router.py
Normal file
@@ -0,0 +1,15 @@
|
||||
"""
|
||||
API v1 路由集合
|
||||
"""
|
||||
from fastapi import APIRouter
|
||||
|
||||
from .health import router as health_router
|
||||
from .users import router as users_router
|
||||
from .exams import router as exams_router
|
||||
|
||||
api_router = APIRouter()
|
||||
|
||||
# 注册所有路由
|
||||
api_router.include_router(health_router, prefix="/health", tags=["health"])
|
||||
api_router.include_router(users_router, prefix="/users", tags=["users"])
|
||||
api_router.include_router(exams_router, prefix="/exams", tags=["exams"])
|
||||
0
知识库/参考代码/python_dev_project/src/core/deps.py
Normal file
0
知识库/参考代码/python_dev_project/src/core/deps.py
Normal file
0
知识库/参考代码/python_dev_project/src/models/__init__.py
Normal file
0
知识库/参考代码/python_dev_project/src/models/__init__.py
Normal file
0
知识库/参考代码/python_dev_project/src/models/base.py
Normal file
0
知识库/参考代码/python_dev_project/src/models/base.py
Normal file
0
知识库/参考代码/python_dev_project/src/models/exam.py
Normal file
0
知识库/参考代码/python_dev_project/src/models/exam.py
Normal file
0
知识库/参考代码/python_dev_project/src/models/user.py
Normal file
0
知识库/参考代码/python_dev_project/src/models/user.py
Normal file
0
知识库/参考代码/python_dev_project/src/schemas/__init__.py
Normal file
0
知识库/参考代码/python_dev_project/src/schemas/__init__.py
Normal file
0
知识库/参考代码/python_dev_project/src/schemas/base.py
Normal file
0
知识库/参考代码/python_dev_project/src/schemas/base.py
Normal file
0
知识库/参考代码/python_dev_project/src/schemas/exam.py
Normal file
0
知识库/参考代码/python_dev_project/src/schemas/exam.py
Normal file
1
知识库/参考代码/python_dev_project/src/services/__init__.py
Normal file
1
知识库/参考代码/python_dev_project/src/services/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# services package
|
||||
1
知识库/参考代码/python_dev_project/src/services/ai/__init__.py
Normal file
1
知识库/参考代码/python_dev_project/src/services/ai/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# AI services package
|
||||
@@ -0,0 +1 @@
|
||||
# Dify integration package
|
||||
217
知识库/参考代码/python_dev_project/src/services/ai/dify/client.py
Normal file
217
知识库/参考代码/python_dev_project/src/services/ai/dify/client.py
Normal file
@@ -0,0 +1,217 @@
|
||||
"""
|
||||
Dify API客户端
|
||||
"""
|
||||
import httpx
|
||||
import json
|
||||
from typing import Dict, Any, Optional, List
|
||||
from datetime import datetime
|
||||
import logging
|
||||
|
||||
from ....core.config import settings
|
||||
from ....core.exceptions import ExternalServiceError
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class DifyClient:
|
||||
"""Dify API客户端"""
|
||||
|
||||
def __init__(self):
|
||||
self.api_base = settings.DIFY_API_BASE.rstrip('/')
|
||||
self.api_key = settings.DIFY_API_KEY
|
||||
self.timeout = settings.DIFY_TIMEOUT
|
||||
|
||||
self.headers = {
|
||||
"Authorization": f"Bearer {self.api_key}",
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
|
||||
async def run_workflow(
|
||||
self,
|
||||
workflow_id: str,
|
||||
inputs: Dict[str, Any],
|
||||
user: str,
|
||||
conversation_id: Optional[str] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
运行Dify工作流
|
||||
|
||||
Args:
|
||||
workflow_id: 工作流ID
|
||||
inputs: 输入参数
|
||||
user: 用户标识
|
||||
conversation_id: 会话ID(可选)
|
||||
|
||||
Returns:
|
||||
工作流执行结果
|
||||
"""
|
||||
url = f"{self.api_base}/workflows/run"
|
||||
|
||||
payload = {
|
||||
"workflow_id": workflow_id,
|
||||
"inputs": inputs,
|
||||
"user": user,
|
||||
"response_mode": "blocking" # 同步模式
|
||||
}
|
||||
|
||||
if conversation_id:
|
||||
payload["conversation_id"] = conversation_id
|
||||
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=self.timeout) as client:
|
||||
response = await client.post(
|
||||
url,
|
||||
json=payload,
|
||||
headers=self.headers
|
||||
)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
|
||||
except httpx.TimeoutException:
|
||||
logger.error(f"Dify API timeout: workflow_id={workflow_id}")
|
||||
raise ExternalServiceError("Dify服务响应超时")
|
||||
except httpx.HTTPStatusError as e:
|
||||
logger.error(f"Dify API error: {e.response.status_code} - {e.response.text}")
|
||||
raise ExternalServiceError(f"Dify服务错误: {e.response.status_code}")
|
||||
except Exception as e:
|
||||
logger.error(f"Dify API unexpected error: {str(e)}")
|
||||
raise ExternalServiceError("Dify服务异常")
|
||||
|
||||
async def generate_exam_questions(
|
||||
self,
|
||||
workflow_id: str,
|
||||
course_id: Optional[int],
|
||||
question_count: int,
|
||||
difficulty: Optional[int],
|
||||
knowledge_points: Optional[List[str]],
|
||||
question_types: Optional[List[str]]
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
生成考试题目
|
||||
|
||||
Args:
|
||||
workflow_id: 考试工作流ID
|
||||
course_id: 课程ID
|
||||
question_count: 题目数量
|
||||
difficulty: 难度等级
|
||||
knowledge_points: 知识点列表
|
||||
question_types: 题型列表
|
||||
|
||||
Returns:
|
||||
题目列表
|
||||
"""
|
||||
inputs = {
|
||||
"question_count": question_count,
|
||||
"difficulty": difficulty or 3,
|
||||
"knowledge_points": json.dumps(knowledge_points or [], ensure_ascii=False),
|
||||
"question_types": json.dumps(question_types or [], ensure_ascii=False)
|
||||
}
|
||||
|
||||
if course_id:
|
||||
inputs["course_id"] = str(course_id)
|
||||
|
||||
# 生成唯一用户标识
|
||||
user = f"exam_user_{datetime.utcnow().timestamp()}"
|
||||
|
||||
result = await self.run_workflow(
|
||||
workflow_id=workflow_id,
|
||||
inputs=inputs,
|
||||
user=user
|
||||
)
|
||||
|
||||
# 解析结果
|
||||
if "data" in result and "outputs" in result["data"]:
|
||||
outputs = result["data"]["outputs"]
|
||||
if "questions" in outputs:
|
||||
# 假设Dify返回的questions是JSON字符串
|
||||
try:
|
||||
questions = json.loads(outputs["questions"])
|
||||
return questions
|
||||
except json.JSONDecodeError:
|
||||
logger.error("Failed to parse questions from Dify")
|
||||
return []
|
||||
|
||||
return []
|
||||
|
||||
async def evaluate_answer(
|
||||
self,
|
||||
workflow_id: str,
|
||||
question: str,
|
||||
answer: str,
|
||||
correct_answer: str,
|
||||
question_type: str
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
评估答案(主观题)
|
||||
|
||||
Args:
|
||||
workflow_id: 评估工作流ID
|
||||
question: 题目
|
||||
answer: 用户答案
|
||||
correct_answer: 参考答案
|
||||
question_type: 题型
|
||||
|
||||
Returns:
|
||||
评估结果
|
||||
"""
|
||||
inputs = {
|
||||
"question": question,
|
||||
"user_answer": answer,
|
||||
"correct_answer": correct_answer,
|
||||
"question_type": question_type
|
||||
}
|
||||
|
||||
user = f"eval_user_{datetime.utcnow().timestamp()}"
|
||||
|
||||
result = await self.run_workflow(
|
||||
workflow_id=workflow_id,
|
||||
inputs=inputs,
|
||||
user=user
|
||||
)
|
||||
|
||||
# 解析评估结果
|
||||
if "data" in result and "outputs" in result["data"]:
|
||||
outputs = result["data"]["outputs"]
|
||||
return {
|
||||
"score": outputs.get("score", 0),
|
||||
"feedback": outputs.get("feedback", ""),
|
||||
"is_correct": outputs.get("is_correct", False)
|
||||
}
|
||||
|
||||
return {
|
||||
"score": 0,
|
||||
"feedback": "评估失败",
|
||||
"is_correct": False
|
||||
}
|
||||
|
||||
async def generate_exam_report(
|
||||
self,
|
||||
workflow_id: str,
|
||||
exam_data: Dict[str, Any]
|
||||
) -> str:
|
||||
"""
|
||||
生成考试报告
|
||||
|
||||
Args:
|
||||
workflow_id: 报告工作流ID
|
||||
exam_data: 考试数据
|
||||
|
||||
Returns:
|
||||
考试报告文本
|
||||
"""
|
||||
inputs = {
|
||||
"exam_data": json.dumps(exam_data, ensure_ascii=False)
|
||||
}
|
||||
|
||||
user = f"report_user_{datetime.utcnow().timestamp()}"
|
||||
|
||||
result = await self.run_workflow(
|
||||
workflow_id=workflow_id,
|
||||
inputs=inputs,
|
||||
user=user
|
||||
)
|
||||
|
||||
if "data" in result and "outputs" in result["data"]:
|
||||
return result["data"]["outputs"].get("report", "报告生成失败")
|
||||
|
||||
return "报告生成失败"
|
||||
0
知识库/参考代码/python_dev_project/src/utils/cache.py
Normal file
0
知识库/参考代码/python_dev_project/src/utils/cache.py
Normal file
0
知识库/参考代码/python_dev_project/tests/conftest.py
Normal file
0
知识库/参考代码/python_dev_project/tests/conftest.py
Normal file
0
知识库/参考代码/python_dev_project/tests/test_exam.py
Normal file
0
知识库/参考代码/python_dev_project/tests/test_exam.py
Normal file
0
知识库/参考代码/python_dev_project/tests/test_exam_api.py
Normal file
0
知识库/参考代码/python_dev_project/tests/test_exam_api.py
Normal file
Reference in New Issue
Block a user