""" 管理员查看学员陪练记录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)