Files
012-kaopeilian/backend/app/api/v1/manager/student_practice.py
111 998211c483 feat: 初始化考培练系统项目
- 从服务器拉取完整代码
- 按框架规范整理项目结构
- 配置 Drone CI 测试环境部署
- 包含后端(FastAPI)、前端(Vue3)、管理端

技术栈: Vue3 + TypeScript + FastAPI + MySQL
2026-01-24 19:33:28 +08:00

346 lines
13 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
管理员查看学员陪练记录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)