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

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

475 lines
15 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 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)