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