""" 用户管理 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, UserSelfUpdate 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: UserSelfUpdate, current_user: dict = Depends(get_current_active_user), db: AsyncSession = Depends(get_db), ) -> ResponseModel: """ 更新当前用户信息 权限:需要登录 注意:用户只能修改自己的基本信息,不能修改角色(role)和激活状态(is_active) """ 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)