feat: 初始化考培练系统项目
- 从服务器拉取完整代码 - 按框架规范整理项目结构 - 配置 Drone CI 测试环境部署 - 包含后端(FastAPI)、前端(Vue3)、管理端 技术栈: Vue3 + TypeScript + FastAPI + MySQL
This commit is contained in:
8
backend/app/api/v1/manager/__init__.py
Normal file
8
backend/app/api/v1/manager/__init__.py
Normal file
@@ -0,0 +1,8 @@
|
||||
"""
|
||||
管理员相关API模块
|
||||
"""
|
||||
from .student_scores import router as student_scores_router
|
||||
from .student_practice import router as student_practice_router
|
||||
|
||||
__all__ = ["student_scores_router", "student_practice_router"]
|
||||
|
||||
345
backend/app/api/v1/manager/student_practice.py
Normal file
345
backend/app/api/v1/manager/student_practice.py
Normal file
@@ -0,0 +1,345 @@
|
||||
"""
|
||||
管理员查看学员陪练记录API
|
||||
"""
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
from sqlalchemy import and_, func, or_, select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.core.deps import get_current_user, get_db
|
||||
from app.core.logger import logger
|
||||
from app.models.position import Position
|
||||
from app.models.position_member import PositionMember
|
||||
from app.models.practice import PracticeReport, PracticeSession, PracticeDialogue
|
||||
from app.models.user import User
|
||||
from app.schemas.base import PaginatedResponse, ResponseModel
|
||||
|
||||
router = APIRouter(prefix="/manager/student-practice", tags=["manager-student-practice"])
|
||||
|
||||
|
||||
@router.get("/", response_model=ResponseModel[PaginatedResponse])
|
||||
async def get_student_practice_records(
|
||||
page: int = Query(1, ge=1, description="页码"),
|
||||
size: int = Query(20, ge=1, le=100, description="每页数量"),
|
||||
student_name: Optional[str] = Query(None, description="学员姓名搜索"),
|
||||
position: Optional[str] = Query(None, description="岗位筛选"),
|
||||
scene_type: Optional[str] = Query(None, description="场景类型筛选"),
|
||||
result: Optional[str] = Query(None, description="结果筛选: excellent/good/average/needs_improvement"),
|
||||
start_date: Optional[str] = Query(None, description="开始日期 YYYY-MM-DD"),
|
||||
end_date: Optional[str] = Query(None, description="结束日期 YYYY-MM-DD"),
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
) -> ResponseModel:
|
||||
"""
|
||||
获取所有用户的陪练记录列表(管理员和manager可访问)
|
||||
|
||||
包含所有角色(trainee/admin/manager)的陪练记录,方便测试和全面管理
|
||||
|
||||
支持筛选:
|
||||
- student_name: 按用户姓名模糊搜索
|
||||
- position: 按岗位筛选
|
||||
- scene_type: 按场景类型筛选
|
||||
- result: 按结果筛选(优秀/良好/一般/需改进)
|
||||
- start_date/end_date: 按日期范围筛选
|
||||
"""
|
||||
try:
|
||||
# 权限检查
|
||||
if current_user.role not in ['admin', 'manager']:
|
||||
return ResponseModel(code=403, message="无权访问", data=None)
|
||||
|
||||
# 构建基础查询
|
||||
# 关联User、PracticeReport来获取完整信息
|
||||
query = (
|
||||
select(
|
||||
PracticeSession,
|
||||
User.full_name.label('student_name'),
|
||||
User.id.label('student_id'),
|
||||
PracticeReport.total_score
|
||||
)
|
||||
.join(User, PracticeSession.user_id == User.id)
|
||||
.outerjoin(
|
||||
PracticeReport,
|
||||
PracticeSession.session_id == PracticeReport.session_id
|
||||
)
|
||||
.where(
|
||||
# 管理员可以查看所有人的陪练记录(包括其他管理员的),方便测试和全面管理
|
||||
PracticeSession.status == 'completed', # 只查询已完成的陪练
|
||||
PracticeSession.is_deleted == False
|
||||
)
|
||||
)
|
||||
|
||||
# 学员姓名筛选
|
||||
if student_name:
|
||||
query = query.where(User.full_name.contains(student_name))
|
||||
|
||||
# 岗位筛选
|
||||
if position:
|
||||
# 通过position_members关联查询
|
||||
query = query.join(
|
||||
PositionMember,
|
||||
and_(
|
||||
PositionMember.user_id == User.id,
|
||||
PositionMember.is_deleted == False
|
||||
)
|
||||
).join(
|
||||
Position,
|
||||
Position.id == PositionMember.position_id
|
||||
).where(
|
||||
Position.name == position
|
||||
)
|
||||
|
||||
# 场景类型筛选
|
||||
if scene_type:
|
||||
query = query.where(PracticeSession.scene_type == scene_type)
|
||||
|
||||
# 结果筛选(根据分数)
|
||||
if result:
|
||||
if result == 'excellent':
|
||||
query = query.where(PracticeReport.total_score >= 90)
|
||||
elif result == 'good':
|
||||
query = query.where(and_(
|
||||
PracticeReport.total_score >= 80,
|
||||
PracticeReport.total_score < 90
|
||||
))
|
||||
elif result == 'average':
|
||||
query = query.where(and_(
|
||||
PracticeReport.total_score >= 70,
|
||||
PracticeReport.total_score < 80
|
||||
))
|
||||
elif result == 'needs_improvement':
|
||||
query = query.where(PracticeReport.total_score < 70)
|
||||
|
||||
# 日期范围筛选
|
||||
if start_date:
|
||||
try:
|
||||
start_dt = datetime.strptime(start_date, '%Y-%m-%d')
|
||||
query = query.where(PracticeSession.start_time >= start_dt)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
if end_date:
|
||||
try:
|
||||
end_dt = datetime.strptime(end_date, '%Y-%m-%d')
|
||||
end_dt = end_dt.replace(hour=23, minute=59, second=59)
|
||||
query = query.where(PracticeSession.start_time <= end_dt)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
# 按开始时间倒序
|
||||
query = query.order_by(PracticeSession.start_time.desc())
|
||||
|
||||
# 计算总数
|
||||
count_query = select(func.count()).select_from(query.subquery())
|
||||
total_result = await db.execute(count_query)
|
||||
total = total_result.scalar() or 0
|
||||
|
||||
# 分页查询
|
||||
offset = (page - 1) * size
|
||||
results = await db.execute(query.offset(offset).limit(size))
|
||||
|
||||
# 构建响应数据
|
||||
items = []
|
||||
for session, student_name, student_id, total_score in results:
|
||||
# 查询该学员的所有岗位
|
||||
position_query = (
|
||||
select(Position.name)
|
||||
.join(PositionMember, Position.id == PositionMember.position_id)
|
||||
.where(
|
||||
PositionMember.user_id == student_id,
|
||||
PositionMember.is_deleted == False,
|
||||
Position.is_deleted == False
|
||||
)
|
||||
)
|
||||
position_result = await db.execute(position_query)
|
||||
positions = position_result.scalars().all()
|
||||
position_str = ', '.join(positions) if positions else None
|
||||
|
||||
# 根据分数计算结果等级
|
||||
result_level = "needs_improvement"
|
||||
if total_score:
|
||||
if total_score >= 90:
|
||||
result_level = "excellent"
|
||||
elif total_score >= 80:
|
||||
result_level = "good"
|
||||
elif total_score >= 70:
|
||||
result_level = "average"
|
||||
|
||||
items.append({
|
||||
"id": session.id,
|
||||
"student_id": student_id,
|
||||
"student_name": student_name,
|
||||
"position": position_str, # 所有岗位,逗号分隔
|
||||
"session_id": session.session_id,
|
||||
"scene_name": session.scene_name,
|
||||
"scene_type": session.scene_type,
|
||||
"duration_seconds": session.duration_seconds,
|
||||
"round_count": session.turns, # turns字段表示对话轮数
|
||||
"score": total_score,
|
||||
"result": result_level,
|
||||
"practice_time": session.start_time.strftime('%Y-%m-%d %H:%M:%S') if session.start_time else None
|
||||
})
|
||||
|
||||
# 计算分页信息
|
||||
pages = (total + size - 1) // size
|
||||
|
||||
return ResponseModel(
|
||||
code=200,
|
||||
message="success",
|
||||
data=PaginatedResponse(
|
||||
items=items,
|
||||
total=total,
|
||||
page=page,
|
||||
page_size=size,
|
||||
pages=pages
|
||||
)
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"获取学员陪练记录失败: {e}", exc_info=True)
|
||||
return ResponseModel(code=500, message=f"获取学员陪练记录失败: {str(e)}", data=None)
|
||||
|
||||
|
||||
@router.get("/statistics", response_model=ResponseModel)
|
||||
async def get_student_practice_statistics(
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
) -> ResponseModel:
|
||||
"""
|
||||
获取学员陪练统计数据
|
||||
|
||||
返回:
|
||||
- total_count: 总陪练次数
|
||||
- avg_score: 平均评分
|
||||
- total_duration_hours: 总陪练时长(小时)
|
||||
- excellent_rate: 优秀率
|
||||
"""
|
||||
try:
|
||||
# 权限检查
|
||||
if current_user.role not in ['admin', 'manager']:
|
||||
return ResponseModel(code=403, message="无权访问", data=None)
|
||||
|
||||
# 查询所有已完成陪练(包括所有角色)
|
||||
query = (
|
||||
select(PracticeSession, PracticeReport.total_score)
|
||||
.join(User, PracticeSession.user_id == User.id)
|
||||
.outerjoin(
|
||||
PracticeReport,
|
||||
PracticeSession.session_id == PracticeReport.session_id
|
||||
)
|
||||
.where(
|
||||
PracticeSession.status == 'completed',
|
||||
PracticeSession.is_deleted == False
|
||||
)
|
||||
)
|
||||
|
||||
result = await db.execute(query)
|
||||
records = result.all()
|
||||
|
||||
if not records:
|
||||
return ResponseModel(
|
||||
code=200,
|
||||
message="success",
|
||||
data={
|
||||
"total_count": 0,
|
||||
"avg_score": 0,
|
||||
"total_duration_hours": 0,
|
||||
"excellent_rate": 0
|
||||
}
|
||||
)
|
||||
|
||||
total_count = len(records)
|
||||
|
||||
# 计算总时长(秒转小时)
|
||||
total_duration_seconds = sum(
|
||||
session.duration_seconds for session, _ in records if session.duration_seconds
|
||||
)
|
||||
total_duration_hours = round(total_duration_seconds / 3600, 1)
|
||||
|
||||
# 计算平均分
|
||||
scores = [score for _, score in records if score is not None]
|
||||
avg_score = round(sum(scores) / len(scores), 1) if scores else 0
|
||||
|
||||
# 计算优秀率(>=90分)
|
||||
excellent = sum(1 for _, score in records if score and score >= 90)
|
||||
excellent_rate = round((excellent / total_count) * 100, 1) if total_count > 0 else 0
|
||||
|
||||
return ResponseModel(
|
||||
code=200,
|
||||
message="success",
|
||||
data={
|
||||
"total_count": total_count,
|
||||
"avg_score": avg_score,
|
||||
"total_duration_hours": total_duration_hours,
|
||||
"excellent_rate": excellent_rate
|
||||
}
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"获取学员陪练统计失败: {e}", exc_info=True)
|
||||
return ResponseModel(code=500, message=f"获取学员陪练统计失败: {str(e)}", data=None)
|
||||
|
||||
|
||||
@router.get("/{session_id}/conversation", response_model=ResponseModel)
|
||||
async def get_session_conversation(
|
||||
session_id: str,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
) -> ResponseModel:
|
||||
"""
|
||||
获取指定会话的对话记录
|
||||
|
||||
返回对话列表,按sequence排序
|
||||
"""
|
||||
try:
|
||||
# 权限检查
|
||||
if current_user.role not in ['admin', 'manager']:
|
||||
return ResponseModel(code=403, message="无权访问", data=None)
|
||||
|
||||
# 1. 查询会话是否存在
|
||||
session_query = select(PracticeSession).where(
|
||||
PracticeSession.session_id == session_id,
|
||||
PracticeSession.is_deleted == False
|
||||
)
|
||||
session_result = await db.execute(session_query)
|
||||
session = session_result.scalar_one_or_none()
|
||||
|
||||
if not session:
|
||||
return ResponseModel(code=404, message="会话不存在", data=None)
|
||||
|
||||
# 2. 查询对话记录
|
||||
dialogue_query = (
|
||||
select(PracticeDialogue)
|
||||
.where(PracticeDialogue.session_id == session_id)
|
||||
.order_by(PracticeDialogue.sequence)
|
||||
)
|
||||
dialogue_result = await db.execute(dialogue_query)
|
||||
dialogues = dialogue_result.scalars().all()
|
||||
|
||||
# 3. 构建响应数据
|
||||
conversation = []
|
||||
for dialogue in dialogues:
|
||||
conversation.append({
|
||||
"role": dialogue.speaker, # "user" 或 "ai"
|
||||
"content": dialogue.content,
|
||||
"timestamp": dialogue.timestamp.strftime('%Y-%m-%d %H:%M:%S') if dialogue.timestamp else None,
|
||||
"sequence": dialogue.sequence
|
||||
})
|
||||
|
||||
logger.info(f"获取会话对话记录: session_id={session_id}, 对话数={len(conversation)}")
|
||||
|
||||
return ResponseModel(
|
||||
code=200,
|
||||
message="success",
|
||||
data={
|
||||
"session_id": session_id,
|
||||
"conversation": conversation,
|
||||
"total_count": len(conversation)
|
||||
}
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"获取会话对话记录失败: {e}, session_id={session_id}", exc_info=True)
|
||||
return ResponseModel(code=500, message=f"获取对话记录失败: {str(e)}", data=None)
|
||||
|
||||
447
backend/app/api/v1/manager/student_scores.py
Normal file
447
backend/app/api/v1/manager/student_scores.py
Normal file
@@ -0,0 +1,447 @@
|
||||
"""
|
||||
管理员查看学员考试成绩API
|
||||
"""
|
||||
from datetime import datetime
|
||||
from typing import List, Optional
|
||||
|
||||
from fastapi import APIRouter, Body, Depends, Query
|
||||
from pydantic import BaseModel
|
||||
from sqlalchemy import and_, delete, func, or_, select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm import selectinload
|
||||
|
||||
from app.core.deps import get_current_user, get_db
|
||||
from app.core.logger import logger
|
||||
from app.models.course import Course
|
||||
from app.models.exam import Exam
|
||||
from app.models.exam_mistake import ExamMistake
|
||||
from app.models.position_member import PositionMember
|
||||
from app.models.position import Position
|
||||
from app.models.user import User
|
||||
from app.schemas.base import PaginatedResponse, ResponseModel
|
||||
|
||||
router = APIRouter(prefix="/manager/student-scores", tags=["manager-student-scores"])
|
||||
|
||||
|
||||
class BatchDeleteRequest(BaseModel):
|
||||
"""批量删除请求"""
|
||||
ids: List[int]
|
||||
|
||||
|
||||
@router.get("/{exam_id}/mistakes", response_model=ResponseModel[PaginatedResponse])
|
||||
async def get_exam_mistakes(
|
||||
exam_id: int,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
) -> ResponseModel:
|
||||
"""
|
||||
获取指定考试的错题记录(管理员和manager可访问)
|
||||
"""
|
||||
try:
|
||||
# 权限检查
|
||||
if current_user.role not in ['admin', 'manager']:
|
||||
return ResponseModel(code=403, message="无权访问", data=None)
|
||||
|
||||
# 查询错题记录
|
||||
query = (
|
||||
select(ExamMistake)
|
||||
.options(selectinload(ExamMistake.question))
|
||||
.where(ExamMistake.exam_id == exam_id)
|
||||
.order_by(ExamMistake.created_at.desc())
|
||||
)
|
||||
|
||||
result = await db.execute(query)
|
||||
mistakes = result.scalars().all()
|
||||
|
||||
items = []
|
||||
for mistake in mistakes:
|
||||
# 获取解析:优先从关联题目获取,如果是AI生成的题目可能没有关联题目
|
||||
analysis = ""
|
||||
if mistake.question and mistake.question.explanation:
|
||||
analysis = mistake.question.explanation
|
||||
|
||||
items.append({
|
||||
"id": mistake.id,
|
||||
"question_content": mistake.question_content,
|
||||
"correct_answer": mistake.correct_answer,
|
||||
"user_answer": mistake.user_answer,
|
||||
"question_type": mistake.question_type,
|
||||
"analysis": analysis,
|
||||
"created_at": mistake.created_at.strftime('%Y-%m-%d %H:%M:%S') if mistake.created_at else None
|
||||
})
|
||||
|
||||
return ResponseModel(
|
||||
code=200,
|
||||
message="success",
|
||||
data=PaginatedResponse(
|
||||
items=items,
|
||||
total=len(items),
|
||||
page=1,
|
||||
page_size=len(items),
|
||||
pages=1
|
||||
)
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"获取错题记录失败: {e}", exc_info=True)
|
||||
return ResponseModel(code=500, message=f"获取错题记录失败: {str(e)}", data=None)
|
||||
|
||||
|
||||
@router.get("/", response_model=ResponseModel[PaginatedResponse])
|
||||
async def get_student_scores(
|
||||
page: int = Query(1, ge=1, description="页码"),
|
||||
size: int = Query(20, ge=1, le=100, description="每页数量"),
|
||||
student_name: Optional[str] = Query(None, description="学员姓名搜索"),
|
||||
position: Optional[str] = Query(None, description="岗位筛选"),
|
||||
course_id: Optional[int] = Query(None, description="课程ID筛选"),
|
||||
score_range: Optional[str] = Query(None, description="成绩范围: excellent/good/pass/fail"),
|
||||
start_date: Optional[str] = Query(None, description="开始日期 YYYY-MM-DD"),
|
||||
end_date: Optional[str] = Query(None, description="结束日期 YYYY-MM-DD"),
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
) -> ResponseModel:
|
||||
"""
|
||||
获取所有学员的考试成绩列表(管理员和manager可访问)
|
||||
|
||||
支持筛选:
|
||||
- student_name: 按学员姓名模糊搜索
|
||||
- position: 按岗位筛选
|
||||
- course_id: 按课程筛选
|
||||
- score_range: 按成绩范围筛选(excellent>=90, good>=80, pass>=60, fail<60)
|
||||
- start_date/end_date: 按日期范围筛选
|
||||
"""
|
||||
try:
|
||||
# 权限检查
|
||||
if current_user.role not in ['admin', 'manager']:
|
||||
return ResponseModel(code=403, message="无权访问", data=None)
|
||||
|
||||
# 构建基础查询
|
||||
# 关联User、Course、ExamMistake来获取完整信息
|
||||
query = (
|
||||
select(
|
||||
Exam,
|
||||
User.full_name.label('student_name'),
|
||||
User.id.label('student_id'),
|
||||
Course.name.label('course_name'),
|
||||
func.count(ExamMistake.id).label('wrong_count')
|
||||
)
|
||||
.join(User, Exam.user_id == User.id)
|
||||
.join(Course, Exam.course_id == Course.id)
|
||||
.outerjoin(ExamMistake, and_(
|
||||
ExamMistake.exam_id == Exam.id,
|
||||
ExamMistake.user_id == User.id
|
||||
))
|
||||
.where(
|
||||
Exam.status.in_(['completed', 'submitted']) # 只查询已完成的考试
|
||||
)
|
||||
.group_by(Exam.id, User.id, User.full_name, Course.id, Course.name)
|
||||
)
|
||||
|
||||
# 学员姓名筛选
|
||||
if student_name:
|
||||
query = query.where(User.full_name.contains(student_name))
|
||||
|
||||
# 岗位筛选
|
||||
if position:
|
||||
# 通过position_members关联查询
|
||||
query = query.join(
|
||||
PositionMember,
|
||||
and_(
|
||||
PositionMember.user_id == User.id,
|
||||
PositionMember.is_deleted == False
|
||||
)
|
||||
).join(
|
||||
Position,
|
||||
Position.id == PositionMember.position_id
|
||||
).where(
|
||||
Position.name == position
|
||||
)
|
||||
|
||||
# 课程筛选
|
||||
if course_id:
|
||||
query = query.where(Exam.course_id == course_id)
|
||||
|
||||
# 成绩范围筛选
|
||||
if score_range:
|
||||
score_field = Exam.round1_score # 使用第一轮成绩
|
||||
if score_range == 'excellent':
|
||||
query = query.where(score_field >= 90)
|
||||
elif score_range == 'good':
|
||||
query = query.where(and_(score_field >= 80, score_field < 90))
|
||||
elif score_range == 'pass':
|
||||
query = query.where(and_(score_field >= 60, score_field < 80))
|
||||
elif score_range == 'fail':
|
||||
query = query.where(score_field < 60)
|
||||
|
||||
# 日期范围筛选
|
||||
if start_date:
|
||||
try:
|
||||
start_dt = datetime.strptime(start_date, '%Y-%m-%d')
|
||||
query = query.where(Exam.created_at >= start_dt)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
if end_date:
|
||||
try:
|
||||
end_dt = datetime.strptime(end_date, '%Y-%m-%d')
|
||||
end_dt = end_dt.replace(hour=23, minute=59, second=59)
|
||||
query = query.where(Exam.created_at <= end_dt)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
# 按创建时间倒序
|
||||
query = query.order_by(Exam.created_at.desc())
|
||||
|
||||
# 计算总数
|
||||
count_query = select(func.count()).select_from(query.subquery())
|
||||
total_result = await db.execute(count_query)
|
||||
total = total_result.scalar() or 0
|
||||
|
||||
# 分页查询
|
||||
offset = (page - 1) * size
|
||||
results = await db.execute(query.offset(offset).limit(size))
|
||||
|
||||
# 构建响应数据
|
||||
items = []
|
||||
for exam, student_name, student_id, course_name, wrong_count in results:
|
||||
# 查询该学员的所有岗位
|
||||
position_query = (
|
||||
select(Position.name)
|
||||
.join(PositionMember, Position.id == PositionMember.position_id)
|
||||
.where(
|
||||
PositionMember.user_id == student_id,
|
||||
PositionMember.is_deleted == False,
|
||||
Position.is_deleted == False
|
||||
)
|
||||
)
|
||||
position_result = await db.execute(position_query)
|
||||
positions = position_result.scalars().all()
|
||||
position_str = ', '.join(positions) if positions else None
|
||||
|
||||
# 计算正确率和用时
|
||||
accuracy = None
|
||||
correct_count = None
|
||||
duration_seconds = None
|
||||
|
||||
if exam.question_count and exam.question_count > 0:
|
||||
correct_count = exam.question_count - wrong_count
|
||||
accuracy = round((correct_count / exam.question_count) * 100, 1)
|
||||
|
||||
if exam.start_time and exam.end_time:
|
||||
duration_seconds = int((exam.end_time - exam.start_time).total_seconds())
|
||||
|
||||
items.append({
|
||||
"id": exam.id,
|
||||
"student_id": student_id,
|
||||
"student_name": student_name,
|
||||
"position": position_str, # 所有岗位,逗号分隔
|
||||
"course_id": exam.course_id,
|
||||
"course_name": course_name,
|
||||
"exam_type": "assessment", # 简化处理,统一为assessment
|
||||
"score": float(exam.round1_score) if exam.round1_score else 0,
|
||||
"round1_score": float(exam.round1_score) if exam.round1_score else None,
|
||||
"round2_score": float(exam.round2_score) if exam.round2_score else None,
|
||||
"round3_score": float(exam.round3_score) if exam.round3_score else None,
|
||||
"total_score": float(exam.total_score) if exam.total_score else 100,
|
||||
"accuracy": accuracy,
|
||||
"correct_count": correct_count,
|
||||
"wrong_count": wrong_count,
|
||||
"total_count": exam.question_count,
|
||||
"duration_seconds": duration_seconds,
|
||||
"exam_date": exam.created_at.strftime('%Y-%m-%d %H:%M:%S') if exam.created_at else None
|
||||
})
|
||||
|
||||
# 计算分页信息
|
||||
pages = (total + size - 1) // size
|
||||
|
||||
return ResponseModel(
|
||||
code=200,
|
||||
message="success",
|
||||
data=PaginatedResponse(
|
||||
items=items,
|
||||
total=total,
|
||||
page=page,
|
||||
page_size=size,
|
||||
pages=pages
|
||||
)
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"获取学员考试成绩失败: {e}", exc_info=True)
|
||||
return ResponseModel(code=500, message=f"获取学员考试成绩失败: {str(e)}", data=None)
|
||||
|
||||
|
||||
@router.get("/statistics", response_model=ResponseModel)
|
||||
async def get_student_scores_statistics(
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
) -> ResponseModel:
|
||||
"""
|
||||
获取学员考试成绩统计数据
|
||||
|
||||
返回:
|
||||
- total_exams: 总考试次数
|
||||
- avg_score: 平均分
|
||||
- pass_rate: 通过率
|
||||
- excellent_rate: 优秀率
|
||||
"""
|
||||
try:
|
||||
# 权限检查
|
||||
if current_user.role not in ['admin', 'manager']:
|
||||
return ResponseModel(code=403, message="无权访问", data=None)
|
||||
|
||||
# 查询所有用户的已完成考试
|
||||
query = (
|
||||
select(Exam)
|
||||
.join(User, Exam.user_id == User.id)
|
||||
.where(
|
||||
Exam.status.in_(['completed', 'submitted']),
|
||||
Exam.round1_score.isnot(None)
|
||||
)
|
||||
)
|
||||
|
||||
result = await db.execute(query)
|
||||
exams = result.scalars().all()
|
||||
|
||||
if not exams:
|
||||
return ResponseModel(
|
||||
code=200,
|
||||
message="success",
|
||||
data={
|
||||
"total_exams": 0,
|
||||
"avg_score": 0,
|
||||
"pass_rate": 0,
|
||||
"excellent_rate": 0
|
||||
}
|
||||
)
|
||||
|
||||
total_exams = len(exams)
|
||||
total_score = sum(exam.round1_score for exam in exams if exam.round1_score)
|
||||
avg_score = round(total_score / total_exams, 1) if total_exams > 0 else 0
|
||||
|
||||
passed = sum(1 for exam in exams if exam.round1_score and exam.round1_score >= 60)
|
||||
pass_rate = round((passed / total_exams) * 100, 1) if total_exams > 0 else 0
|
||||
|
||||
excellent = sum(1 for exam in exams if exam.round1_score and exam.round1_score >= 90)
|
||||
excellent_rate = round((excellent / total_exams) * 100, 1) if total_exams > 0 else 0
|
||||
|
||||
return ResponseModel(
|
||||
code=200,
|
||||
message="success",
|
||||
data={
|
||||
"total_exams": total_exams,
|
||||
"avg_score": avg_score,
|
||||
"pass_rate": pass_rate,
|
||||
"excellent_rate": excellent_rate
|
||||
}
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"获取学员考试成绩统计失败: {e}", exc_info=True)
|
||||
return ResponseModel(code=500, message=f"获取学员考试成绩统计失败: {str(e)}", data=None)
|
||||
|
||||
|
||||
@router.delete("/{exam_id}", response_model=ResponseModel)
|
||||
async def delete_exam_record(
|
||||
exam_id: int,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
) -> ResponseModel:
|
||||
"""
|
||||
删除单条考试记录(管理员可访问)
|
||||
|
||||
会同时删除关联的错题记录
|
||||
"""
|
||||
try:
|
||||
# 权限检查 - 仅管理员可删除
|
||||
if current_user.role != 'admin':
|
||||
return ResponseModel(code=403, message="无权操作,仅管理员可删除考试记录", data=None)
|
||||
|
||||
# 查询考试记录
|
||||
result = await db.execute(
|
||||
select(Exam).where(Exam.id == exam_id)
|
||||
)
|
||||
exam = result.scalar_one_or_none()
|
||||
|
||||
if not exam:
|
||||
return ResponseModel(code=404, message="考试记录不存在", data=None)
|
||||
|
||||
# 删除关联的错题记录
|
||||
await db.execute(
|
||||
delete(ExamMistake).where(ExamMistake.exam_id == exam_id)
|
||||
)
|
||||
|
||||
# 删除考试记录
|
||||
await db.delete(exam)
|
||||
await db.commit()
|
||||
|
||||
logger.info(f"管理员 {current_user.username} 删除了考试记录 {exam_id}")
|
||||
|
||||
return ResponseModel(
|
||||
code=200,
|
||||
message="考试记录已删除",
|
||||
data={"deleted_id": exam_id}
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
await db.rollback()
|
||||
logger.error(f"删除考试记录失败: {e}", exc_info=True)
|
||||
return ResponseModel(code=500, message=f"删除考试记录失败: {str(e)}", data=None)
|
||||
|
||||
|
||||
@router.delete("/batch/delete", response_model=ResponseModel)
|
||||
async def batch_delete_exam_records(
|
||||
request: BatchDeleteRequest,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
) -> ResponseModel:
|
||||
"""
|
||||
批量删除考试记录(管理员可访问)
|
||||
|
||||
会同时删除关联的错题记录
|
||||
"""
|
||||
try:
|
||||
# 权限检查 - 仅管理员可删除
|
||||
if current_user.role != 'admin':
|
||||
return ResponseModel(code=403, message="无权操作,仅管理员可删除考试记录", data=None)
|
||||
|
||||
if not request.ids:
|
||||
return ResponseModel(code=400, message="请选择要删除的记录", data=None)
|
||||
|
||||
# 查询存在的考试记录
|
||||
result = await db.execute(
|
||||
select(Exam.id).where(Exam.id.in_(request.ids))
|
||||
)
|
||||
existing_ids = [row[0] for row in result.all()]
|
||||
|
||||
if not existing_ids:
|
||||
return ResponseModel(code=404, message="未找到要删除的记录", data=None)
|
||||
|
||||
# 删除关联的错题记录
|
||||
await db.execute(
|
||||
delete(ExamMistake).where(ExamMistake.exam_id.in_(existing_ids))
|
||||
)
|
||||
|
||||
# 删除考试记录
|
||||
await db.execute(
|
||||
delete(Exam).where(Exam.id.in_(existing_ids))
|
||||
)
|
||||
await db.commit()
|
||||
|
||||
deleted_count = len(existing_ids)
|
||||
logger.info(f"管理员 {current_user.username} 批量删除了 {deleted_count} 条考试记录")
|
||||
|
||||
return ResponseModel(
|
||||
code=200,
|
||||
message=f"成功删除 {deleted_count} 条考试记录",
|
||||
data={
|
||||
"deleted_count": deleted_count,
|
||||
"deleted_ids": existing_ids
|
||||
}
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
await db.rollback()
|
||||
logger.error(f"批量删除考试记录失败: {e}", exc_info=True)
|
||||
return ResponseModel(code=500, message=f"批量删除考试记录失败: {str(e)}", data=None)
|
||||
|
||||
Reference in New Issue
Block a user