""" 用户管理 API """ from typing import List from fastapi import APIRouter, Depends, HTTPException, Query, status, Request from sqlalchemy import select, func from sqlalchemy.ext.asyncio import AsyncSession from app.core.deps import get_current_active_user, get_db, require_admin from app.core.logger import logger from app.models.user import User from app.schemas.base import PaginatedResponse, PaginationParams, ResponseModel from app.schemas.user import User as UserSchema from app.schemas.user import UserCreate, UserFilter, UserPasswordUpdate, UserUpdate from app.services.user_service import UserService from app.services.system_log_service import system_log_service from app.schemas.system_log import SystemLogCreate from app.models.exam import Exam, ExamResult from app.models.training import TrainingSession from app.models.position_member import PositionMember from app.models.position import Position from app.models.course import Course router = APIRouter() @router.get("/me", response_model=ResponseModel) async def get_current_user_info( current_user: dict = Depends(get_current_active_user), ) -> ResponseModel: """ 获取当前用户信息 权限:需要登录 """ return ResponseModel(data=UserSchema.model_validate(current_user)) @router.get("/me/statistics", response_model=ResponseModel) async def get_current_user_statistics( current_user: dict = Depends(get_current_active_user), db: AsyncSession = Depends(get_db), ) -> ResponseModel: """ 获取当前用户学习统计 返回字段: - learningDays: 学习天数(按陪练会话开始日期去重) - totalHours: 学习总时长(小时,取整到1位小数) - practiceQuestions: 练习题数(答题记录条数汇总) - averageScore: 平均成绩(已提交考试的平均分,保留1位小数) - examsCompleted: 已完成考试数量 """ try: user_id = current_user.id # 学习天数:按会话开始日期去重 learning_days_stmt = select(func.count(func.distinct(func.date(TrainingSession.start_time)))).where( TrainingSession.user_id == user_id ) learning_days = (await db.scalar(learning_days_stmt)) or 0 # 总时长(小时) total_seconds_stmt = select(func.coalesce(func.sum(TrainingSession.duration_seconds), 0)).where( TrainingSession.user_id == user_id ) total_seconds = (await db.scalar(total_seconds_stmt)) or 0 total_hours = round(float(total_seconds) / 3600.0, 1) if total_seconds else 0.0 # 练习题数:用户所有考试的题目总数 practice_questions_stmt = ( select(func.coalesce(func.sum(Exam.question_count), 0)) .where(Exam.user_id == user_id, Exam.status == "completed") ) practice_questions = (await db.scalar(practice_questions_stmt)) or 0 # 平均成绩:用户已完成考试的平均分 avg_score_stmt = select(func.avg(Exam.score)).where( Exam.user_id == user_id, Exam.status == "completed" ) avg_score_val = await db.scalar(avg_score_stmt) average_score = round(float(avg_score_val), 1) if avg_score_val is not None else 0.0 # 已完成考试数量 exams_completed_stmt = select(func.count(Exam.id)).where( Exam.user_id == user_id, Exam.status == "completed" ) exams_completed = (await db.scalar(exams_completed_stmt)) or 0 return ResponseModel( data={ "learningDays": int(learning_days), "totalHours": total_hours, "practiceQuestions": int(practice_questions), "averageScore": average_score, "examsCompleted": int(exams_completed), } ) except Exception as e: logger.error("获取用户学习统计失败", exc_info=True) raise HTTPException(status_code=500, detail=f"获取用户学习统计失败: {str(e)}") @router.get("/me/recent-exams", response_model=ResponseModel) async def get_recent_exams( limit: int = Query(5, ge=1, le=20, description="返回数量"), current_user: dict = Depends(get_current_active_user), db: AsyncSession = Depends(get_db), ) -> ResponseModel: """ 获取当前用户最近的考试记录 返回最近的考试列表,按创建时间降序排列 只返回已完成或已提交的考试(不包括started状态) """ try: user_id = current_user.id # 查询最近的考试记录,关联课程表获取课程名称 stmt = ( select(Exam, Course.name.label("course_name")) .join(Course, Exam.course_id == Course.id) .where( Exam.user_id == user_id, Exam.status.in_(["completed", "submitted"]) ) .order_by(Exam.created_at.desc()) .limit(limit) ) results = await db.execute(stmt) rows = results.all() # 构建返回数据 exams_list = [] for exam, course_name in rows: exams_list.append({ "id": exam.id, "title": exam.exam_name, "courseName": course_name, "courseId": exam.course_id, "time": exam.created_at.strftime("%Y-%m-%d %H:%M") if exam.created_at else "", "questions": exam.question_count or 0, "status": exam.status, "score": exam.score }) return ResponseModel(data=exams_list) except Exception as e: logger.error("获取最近考试记录失败", exc_info=True) raise HTTPException(status_code=500, detail=f"获取最近考试记录失败: {str(e)}") @router.put("/me", response_model=ResponseModel) async def update_current_user( user_in: UserUpdate, current_user: dict = Depends(get_current_active_user), db: AsyncSession = Depends(get_db), ) -> ResponseModel: """ 更新当前用户信息 权限:需要登录 """ user_service = UserService(db) user = await user_service.update_user( user_id=current_user.id, obj_in=user_in, updated_by=current_user.id, ) return ResponseModel(data=UserSchema.model_validate(user)) @router.put("/me/password", response_model=ResponseModel) async def update_current_user_password( password_in: UserPasswordUpdate, current_user: dict = Depends(get_current_active_user), db: AsyncSession = Depends(get_db), ) -> ResponseModel: """ 更新当前用户密码 权限:需要登录 """ user_service = UserService(db) user = await user_service.update_password( user_id=current_user.id, old_password=password_in.old_password, new_password=password_in.new_password, ) return ResponseModel(message="密码更新成功", data=UserSchema.model_validate(user)) @router.get("/", response_model=ResponseModel) async def get_users( pagination: PaginationParams = Depends(), role: str = Query(None, description="用户角色"), is_active: bool = Query(None, description="是否激活"), team_id: int = Query(None, description="团队ID"), keyword: str = Query(None, description="搜索关键词"), current_user: dict = Depends(get_current_active_user), db: AsyncSession = Depends(get_db), ) -> ResponseModel: """ 获取用户列表 权限:需要登录 - 普通用户只能看到激活的用户 - 管理员可以看到所有用户 """ # 构建筛选条件 filter_params = UserFilter( role=role, is_active=is_active, team_id=team_id, keyword=keyword, ) # 普通用户只能看到激活的用户 if current_user.role == "trainee": filter_params.is_active = True # 获取用户列表 user_service = UserService(db) users, total = await user_service.get_users_with_filter( skip=pagination.offset, limit=pagination.limit, filter_params=filter_params, ) # 构建分页响应 paginated = PaginatedResponse.create( items=[UserSchema.model_validate(user) for user in users], total=total, page=pagination.page, page_size=pagination.page_size, ) return ResponseModel(data=paginated.model_dump()) @router.post("/", response_model=ResponseModel, status_code=status.HTTP_201_CREATED) async def create_user( user_in: UserCreate, request: Request, current_user: dict = Depends(require_admin), db: AsyncSession = Depends(get_db), ) -> ResponseModel: """ 创建用户 权限:需要管理员权限 """ user_service = UserService(db) user = await user_service.create_user( obj_in=user_in, created_by=current_user.id, ) logger.info( "管理员创建用户", admin_id=current_user.id, admin_username=current_user.username, new_user_id=user.id, new_username=user.username, ) # 记录用户创建日志 await system_log_service.create_log( db, SystemLogCreate( level="INFO", type="user", message=f"管理员 {current_user.username} 创建用户: {user.username}", user_id=current_user.id, user=current_user.username, ip=request.client.host if request.client else None, path="/api/v1/users/", method="POST", user_agent=request.headers.get("user-agent") ) ) return ResponseModel(message="用户创建成功", data=UserSchema.model_validate(user)) @router.get("/{user_id}", response_model=ResponseModel) async def get_user( user_id: int, current_user: dict = Depends(get_current_active_user), db: AsyncSession = Depends(get_db), ) -> ResponseModel: """ 获取用户详情 权限:需要登录 - 普通用户只能查看自己的信息 - 管理员和经理可以查看所有用户信息 """ # 权限检查 if current_user.role == "trainee" and current_user.id != user_id: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="没有权限查看其他用户信息" ) # 获取用户 user_service = UserService(db) user = await user_service.get_by_id(user_id) if not user: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="用户不存在") return ResponseModel(data=UserSchema.model_validate(user)) @router.put("/{user_id}", response_model=ResponseModel) async def update_user( user_id: int, user_in: UserUpdate, current_user: dict = Depends(require_admin), db: AsyncSession = Depends(get_db), ) -> ResponseModel: """ 更新用户信息 权限:需要管理员权限 """ user_service = UserService(db) user = await user_service.update_user( user_id=user_id, obj_in=user_in, updated_by=current_user.id, ) logger.info( "管理员更新用户", admin_id=current_user.id, admin_username=current_user.username, updated_user_id=user.id, updated_username=user.username, ) return ResponseModel(data=UserSchema.model_validate(user)) @router.delete("/{user_id}", response_model=ResponseModel) async def delete_user( user_id: int, request: Request, current_user: dict = Depends(require_admin), db: AsyncSession = Depends(get_db), ) -> ResponseModel: """ 删除用户(软删除) 权限:需要管理员权限 """ # 不能删除自己 if user_id == current_user.id: raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="不能删除自己") # 获取用户 user_service = UserService(db) user = await user_service.get_by_id(user_id) if not user: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="用户不存在") # 软删除 await user_service.soft_delete(db_obj=user) logger.info( "管理员删除用户", admin_id=current_user.id, admin_username=current_user.username, deleted_user_id=user.id, deleted_username=user.username, ) # 记录用户删除日志 await system_log_service.create_log( db, SystemLogCreate( level="INFO", type="user", message=f"管理员 {current_user.username} 删除用户: {user.username}", user_id=current_user.id, user=current_user.username, ip=request.client.host if request.client else None, path=f"/api/v1/users/{user_id}", method="DELETE", user_agent=request.headers.get("user-agent") ) ) return ResponseModel(message="用户删除成功") @router.post("/{user_id}/teams/{team_id}", response_model=ResponseModel) async def add_user_to_team( user_id: int, team_id: int, role: str = Query("member", regex="^(member|leader)$"), current_user: dict = Depends(require_admin), db: AsyncSession = Depends(get_db), ) -> ResponseModel: """ 将用户添加到团队 权限:需要管理员权限 """ user_service = UserService(db) await user_service.add_user_to_team( user_id=user_id, team_id=team_id, role=role, ) return ResponseModel(message="用户已添加到团队") @router.delete("/{user_id}/teams/{team_id}", response_model=ResponseModel) async def remove_user_from_team( user_id: int, team_id: int, current_user: dict = Depends(require_admin), db: AsyncSession = Depends(get_db), ) -> ResponseModel: """ 从团队中移除用户 权限:需要管理员权限 """ user_service = UserService(db) await user_service.remove_user_from_team( user_id=user_id, team_id=team_id, ) return ResponseModel(message="用户已从团队中移除") @router.get("/{user_id}/positions", response_model=ResponseModel) async def get_user_positions( user_id: int, current_user: dict = Depends(get_current_active_user), db: AsyncSession = Depends(get_db), ) -> ResponseModel: """ 获取用户所属岗位列表(用于前端展示与编辑) 权限:登录即可;普通用户仅能查看自己的信息 返回:[{id,name,code}] """ # 权限检查 if current_user.role == "trainee" and current_user.id != user_id: raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="没有权限查看其他用户信息") stmt = ( select(Position) .join(PositionMember, PositionMember.position_id == Position.id) .where(PositionMember.user_id == user_id, PositionMember.is_deleted == False, Position.is_deleted == False) .order_by(Position.id) ) rows = (await db.execute(stmt)).scalars().all() data = [ {"id": p.id, "name": p.name, "code": p.code} for p in rows ] return ResponseModel(data=data)