Files
012-kaopeilian/backend/app/api/v1/users.py
yuliang_guo 149cc5f6b0
All checks were successful
continuous-integration/drone/push Build is passing
fix: 权限和显示优化
1. 侧边栏:根据角色过滤菜单,无可访问子菜单时隐藏父菜单
2. Dashboard:智能工牌分析、统计卡片、最近考试仅对学员显示
3. 快捷操作:根据角色显示不同的操作入口
4. 欢迎语:根据角色显示不同的欢迎信息
5. 学习天数:改为基于注册日期计算(至少为1天)
6. 成长路径:AI分析按钮仅对学员显示

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-03 15:41:56 +08:00

479 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, 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: 学习天数从注册日期到今天的天数至少为1
- totalHours: 学习总时长小时取整到1位小数
- practiceQuestions: 练习题数(答题记录条数汇总)
- averageScore: 平均成绩已提交考试的平均分保留1位小数
- examsCompleted: 已完成考试数量
"""
try:
from datetime import date
user_id = current_user.id
# 学习天数从注册日期到今天的天数至少为1天
if current_user.created_at:
registration_date = current_user.created_at.date() if hasattr(current_user.created_at, 'date') else current_user.created_at
learning_days = (date.today() - registration_date).days + 1 # +1 是因为注册当天也算第1天
learning_days = max(1, learning_days) # 确保至少为1
else:
learning_days = 1
# 总时长(小时)
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)