feat: 初始化考培练系统项目

- 从服务器拉取完整代码
- 按框架规范整理项目结构
- 配置 Drone CI 测试环境部署
- 包含后端(FastAPI)、前端(Vue3)、管理端

技术栈: Vue3 + TypeScript + FastAPI + MySQL
This commit is contained in:
111
2026-01-24 19:33:28 +08:00
commit 998211c483
1197 changed files with 228429 additions and 0 deletions

View 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)

View 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"])

View File

@@ -0,0 +1 @@
# services package

View File

@@ -0,0 +1 @@
# AI services package

View File

@@ -0,0 +1 @@
# Dify integration package

View 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 "报告生成失败"