All checks were successful
continuous-integration/drone/push Build is passing
1. 后端 admin.py: - 用户总数统计添加 is_deleted=False, is_active=True 过滤 - 现在只统计有效的活跃用户数 2. 前端 user-management.vue: - 岗位筛选从硬编码改为动态加载 positionOptions - 岗位列表从API获取,而不是写死的4个选项
546 lines
18 KiB
Python
546 lines
18 KiB
Python
"""
|
||
管理员相关API路由
|
||
"""
|
||
|
||
from typing import Optional, List, Dict, Any
|
||
from datetime import datetime, timedelta
|
||
from fastapi import APIRouter, Depends, Query
|
||
from sqlalchemy.ext.asyncio import AsyncSession
|
||
from sqlalchemy import select, func, case
|
||
|
||
from app.core.deps import get_current_active_user as get_current_user, get_db
|
||
from app.models.user import User
|
||
from app.models.course import Course, CourseStatus
|
||
from app.models.user_course_progress import UserCourseProgress, ProgressStatus
|
||
from app.schemas.base import ResponseModel
|
||
|
||
router = APIRouter(prefix="/admin")
|
||
|
||
|
||
@router.get("/dashboard/stats")
|
||
async def get_dashboard_stats(
|
||
current_user: User = Depends(get_current_user),
|
||
db: AsyncSession = Depends(get_db)
|
||
) -> ResponseModel:
|
||
"""
|
||
获取管理员仪表盘统计数据
|
||
|
||
需要管理员权限
|
||
"""
|
||
# 权限检查
|
||
if current_user.role != "admin":
|
||
return ResponseModel(
|
||
code=403,
|
||
message="权限不足,需要管理员权限"
|
||
)
|
||
|
||
# 用户统计 - 只统计未删除且活跃的用户
|
||
total_users = await db.scalar(
|
||
select(func.count(User.id))
|
||
.where(User.is_deleted == False, User.is_active == True)
|
||
)
|
||
|
||
# 计算最近30天的新增用户
|
||
thirty_days_ago = datetime.now() - timedelta(days=30)
|
||
new_users_count = await db.scalar(
|
||
select(func.count(User.id))
|
||
.where(User.created_at >= thirty_days_ago)
|
||
)
|
||
|
||
# 计算增长率(假设上个月也是30天)
|
||
sixty_days_ago = datetime.now() - timedelta(days=60)
|
||
last_month_users = await db.scalar(
|
||
select(func.count(User.id))
|
||
.where(User.created_at >= sixty_days_ago)
|
||
.where(User.created_at < thirty_days_ago)
|
||
)
|
||
|
||
growth_rate = 0.0
|
||
if last_month_users > 0:
|
||
growth_rate = ((new_users_count - last_month_users) / last_month_users) * 100
|
||
|
||
# 课程统计
|
||
total_courses = await db.scalar(
|
||
select(func.count(Course.id))
|
||
.where(Course.status == CourseStatus.PUBLISHED)
|
||
)
|
||
|
||
# 根据用户课程进度表计算完成的课程学习记录数
|
||
completed_courses = await db.scalar(
|
||
select(func.count(UserCourseProgress.id))
|
||
.where(UserCourseProgress.status == ProgressStatus.COMPLETED.value)
|
||
) or 0
|
||
|
||
# 考试统计(如果有考试表的话)
|
||
total_exams = 0
|
||
avg_score = 0.0
|
||
pass_rate = "0%"
|
||
|
||
# 学习时长统计 - 从用户课程进度表获取
|
||
total_study_seconds = await db.scalar(
|
||
select(func.coalesce(func.sum(UserCourseProgress.total_study_time), 0))
|
||
) or 0
|
||
total_learning_hours = round(total_study_seconds / 3600)
|
||
|
||
# 平均学习时长(每个活跃用户)
|
||
active_learners = await db.scalar(
|
||
select(func.count(func.distinct(UserCourseProgress.user_id)))
|
||
.where(UserCourseProgress.status != ProgressStatus.NOT_STARTED.value)
|
||
) or 0
|
||
avg_learning_hours = round(total_study_seconds / 3600 / max(active_learners, 1), 1)
|
||
|
||
# 活跃率 = 有学习记录的用户 / 总用户
|
||
active_rate = f"{round(active_learners / max(total_users, 1) * 100)}%"
|
||
|
||
# 构建响应数据
|
||
stats = {
|
||
"users": {
|
||
"total": total_users,
|
||
"growth": new_users_count,
|
||
"growthRate": f"{growth_rate:.1f}%"
|
||
},
|
||
"courses": {
|
||
"total": total_courses,
|
||
"completed": completed_courses,
|
||
"completionRate": f"{(completed_courses / total_courses * 100) if total_courses > 0 else 0:.1f}%"
|
||
},
|
||
"exams": {
|
||
"total": total_exams,
|
||
"avgScore": avg_score,
|
||
"passRate": pass_rate
|
||
},
|
||
"learning": {
|
||
"totalHours": total_learning_hours,
|
||
"avgHours": avg_learning_hours,
|
||
"activeRate": active_rate
|
||
}
|
||
}
|
||
|
||
return ResponseModel(
|
||
code=200,
|
||
message="获取仪表盘统计数据成功",
|
||
data=stats
|
||
)
|
||
|
||
|
||
@router.get("/dashboard/user-growth")
|
||
async def get_user_growth_data(
|
||
days: int = Query(30, description="统计天数", ge=7, le=90),
|
||
current_user: User = Depends(get_current_user),
|
||
db: AsyncSession = Depends(get_db)
|
||
) -> ResponseModel:
|
||
"""
|
||
获取用户增长数据
|
||
|
||
Args:
|
||
days: 统计天数,默认30天
|
||
|
||
需要管理员权限
|
||
"""
|
||
# 权限检查
|
||
if current_user.role != "admin":
|
||
return ResponseModel(
|
||
code=403,
|
||
message="权限不足,需要管理员权限"
|
||
)
|
||
|
||
# 准备日期列表
|
||
dates = []
|
||
new_users = []
|
||
active_users = []
|
||
|
||
end_date = datetime.now().date()
|
||
|
||
for i in range(days):
|
||
current_date = end_date - timedelta(days=days-1-i)
|
||
dates.append(current_date.strftime("%Y-%m-%d"))
|
||
|
||
# 统计当天新增用户
|
||
next_date = current_date + timedelta(days=1)
|
||
new_count = await db.scalar(
|
||
select(func.count(User.id))
|
||
.where(func.date(User.created_at) == current_date)
|
||
)
|
||
new_users.append(new_count or 0)
|
||
|
||
# 统计当天活跃用户(有登录记录)
|
||
active_count = await db.scalar(
|
||
select(func.count(User.id))
|
||
.where(func.date(User.last_login_at) == current_date)
|
||
)
|
||
active_users.append(active_count or 0)
|
||
|
||
return ResponseModel(
|
||
code=200,
|
||
message="获取用户增长数据成功",
|
||
data={
|
||
"dates": dates,
|
||
"newUsers": new_users,
|
||
"activeUsers": active_users
|
||
}
|
||
)
|
||
|
||
|
||
@router.get("/dashboard/course-completion")
|
||
async def get_course_completion_data(
|
||
current_user: User = Depends(get_current_user),
|
||
db: AsyncSession = Depends(get_db)
|
||
) -> ResponseModel:
|
||
"""
|
||
获取课程完成率数据
|
||
|
||
需要管理员权限
|
||
"""
|
||
# 权限检查
|
||
if current_user.role != "admin":
|
||
return ResponseModel(
|
||
code=403,
|
||
message="权限不足,需要管理员权限"
|
||
)
|
||
|
||
# 获取所有已发布的课程
|
||
courses_result = await db.execute(
|
||
select(Course.name, Course.id)
|
||
.where(Course.status == CourseStatus.PUBLISHED)
|
||
.order_by(Course.sort_order, Course.id)
|
||
.limit(10) # 限制显示前10个课程
|
||
)
|
||
courses = courses_result.all()
|
||
|
||
course_names = []
|
||
completion_rates = []
|
||
|
||
for course_name, course_id in courses:
|
||
course_names.append(course_name)
|
||
|
||
# 根据用户课程进度表计算完成率
|
||
# 统计该课程的完成用户数和总学习用户数
|
||
stats_result = await db.execute(
|
||
select(
|
||
func.count(UserCourseProgress.id).label('total'),
|
||
func.sum(
|
||
case(
|
||
(UserCourseProgress.status == ProgressStatus.COMPLETED.value, 1),
|
||
else_=0
|
||
)
|
||
).label('completed')
|
||
).where(UserCourseProgress.course_id == course_id)
|
||
)
|
||
stats = stats_result.one()
|
||
total_learners = stats.total or 0
|
||
completed_learners = stats.completed or 0
|
||
|
||
# 计算完成率
|
||
if total_learners > 0:
|
||
completion_rate = round(completed_learners / total_learners * 100)
|
||
else:
|
||
completion_rate = 0
|
||
completion_rates.append(completion_rate)
|
||
|
||
return ResponseModel(
|
||
code=200,
|
||
message="获取课程完成率数据成功",
|
||
data={
|
||
"courses": course_names,
|
||
"completionRates": completion_rates
|
||
}
|
||
)
|
||
|
||
|
||
# ===== 岗位管理(最小可用 stub 版本)=====
|
||
|
||
def _ensure_admin(user: User) -> Optional[ResponseModel]:
|
||
if user.role != "admin":
|
||
return ResponseModel(code=403, message="权限不足,需要管理员权限")
|
||
return None
|
||
|
||
|
||
# 注意:positions相关路由已移至positions.py
|
||
# _sample_positions函数和所有positions路由已删除,避免与positions.py冲突
|
||
|
||
|
||
# ===== 用户批量操作 =====
|
||
|
||
from pydantic import BaseModel
|
||
from app.models.position_member import PositionMember
|
||
|
||
|
||
class BatchUserOperation(BaseModel):
|
||
"""批量用户操作请求模型"""
|
||
ids: List[int]
|
||
action: str # delete, activate, deactivate, change_role, assign_position, assign_team
|
||
value: Optional[Any] = None # 角色值、岗位ID、团队ID等
|
||
|
||
|
||
@router.post("/users/batch", response_model=ResponseModel)
|
||
async def batch_user_operation(
|
||
operation: BatchUserOperation,
|
||
current_user: User = Depends(get_current_user),
|
||
db: AsyncSession = Depends(get_db)
|
||
) -> ResponseModel:
|
||
"""
|
||
批量用户操作
|
||
|
||
支持的操作类型:
|
||
- delete: 批量删除用户(软删除)
|
||
- activate: 批量启用用户
|
||
- deactivate: 批量禁用用户
|
||
- change_role: 批量修改角色(需要 value 参数)
|
||
- assign_position: 批量分配岗位(需要 value 参数为岗位ID)
|
||
- assign_team: 批量分配团队(需要 value 参数为团队ID)
|
||
|
||
权限:需要管理员权限
|
||
"""
|
||
# 权限检查
|
||
if current_user.role != "admin":
|
||
return ResponseModel(
|
||
code=403,
|
||
message="权限不足,需要管理员权限"
|
||
)
|
||
|
||
if not operation.ids:
|
||
return ResponseModel(
|
||
code=400,
|
||
message="请选择要操作的用户"
|
||
)
|
||
|
||
# 不能操作自己
|
||
if current_user.id in operation.ids:
|
||
return ResponseModel(
|
||
code=400,
|
||
message="不能对自己执行批量操作"
|
||
)
|
||
|
||
# 获取要操作的用户
|
||
result = await db.execute(
|
||
select(User).where(User.id.in_(operation.ids), User.is_deleted == False)
|
||
)
|
||
users = result.scalars().all()
|
||
|
||
if not users:
|
||
return ResponseModel(
|
||
code=404,
|
||
message="未找到要操作的用户"
|
||
)
|
||
|
||
success_count = 0
|
||
failed_count = 0
|
||
errors = []
|
||
|
||
try:
|
||
if operation.action == "delete":
|
||
# 批量软删除
|
||
for user in users:
|
||
try:
|
||
user.is_deleted = True
|
||
user.deleted_at = datetime.now()
|
||
success_count += 1
|
||
except Exception as e:
|
||
failed_count += 1
|
||
errors.append(f"删除用户 {user.username} 失败: {str(e)}")
|
||
|
||
await db.commit()
|
||
|
||
elif operation.action == "activate":
|
||
# 批量启用
|
||
for user in users:
|
||
try:
|
||
user.is_active = True
|
||
success_count += 1
|
||
except Exception as e:
|
||
failed_count += 1
|
||
errors.append(f"启用用户 {user.username} 失败: {str(e)}")
|
||
|
||
await db.commit()
|
||
|
||
elif operation.action == "deactivate":
|
||
# 批量禁用
|
||
for user in users:
|
||
try:
|
||
user.is_active = False
|
||
success_count += 1
|
||
except Exception as e:
|
||
failed_count += 1
|
||
errors.append(f"禁用用户 {user.username} 失败: {str(e)}")
|
||
|
||
await db.commit()
|
||
|
||
elif operation.action == "change_role":
|
||
# 批量修改角色
|
||
if not operation.value:
|
||
return ResponseModel(
|
||
code=400,
|
||
message="请指定要修改的角色"
|
||
)
|
||
|
||
valid_roles = ["trainee", "manager", "admin"]
|
||
if operation.value not in valid_roles:
|
||
return ResponseModel(
|
||
code=400,
|
||
message=f"无效的角色,可选值: {', '.join(valid_roles)}"
|
||
)
|
||
|
||
for user in users:
|
||
try:
|
||
user.role = operation.value
|
||
success_count += 1
|
||
except Exception as e:
|
||
failed_count += 1
|
||
errors.append(f"修改用户 {user.username} 角色失败: {str(e)}")
|
||
|
||
await db.commit()
|
||
|
||
elif operation.action == "assign_position":
|
||
# 批量分配岗位
|
||
if not operation.value:
|
||
return ResponseModel(
|
||
code=400,
|
||
message="请指定要分配的岗位ID"
|
||
)
|
||
|
||
position_id = int(operation.value)
|
||
|
||
# 获取岗位信息用于通知
|
||
from app.models.position import Position
|
||
position_result = await db.execute(
|
||
select(Position).where(Position.id == position_id)
|
||
)
|
||
position = position_result.scalar_one_or_none()
|
||
position_name = position.name if position else "未知岗位"
|
||
|
||
# 记录新分配成功的用户ID(用于发送通知)
|
||
newly_assigned_user_ids = []
|
||
|
||
for user in users:
|
||
try:
|
||
# 检查是否已有该岗位
|
||
existing = await db.execute(
|
||
select(PositionMember).where(
|
||
PositionMember.user_id == user.id,
|
||
PositionMember.position_id == position_id,
|
||
PositionMember.is_deleted == False
|
||
)
|
||
)
|
||
if existing.scalar_one_or_none():
|
||
# 已有该岗位,跳过
|
||
success_count += 1
|
||
continue
|
||
|
||
# 添加岗位关联(PositionMember模型没有created_by字段)
|
||
member = PositionMember(
|
||
position_id=position_id,
|
||
user_id=user.id,
|
||
joined_at=datetime.now()
|
||
)
|
||
db.add(member)
|
||
newly_assigned_user_ids.append(user.id)
|
||
success_count += 1
|
||
except Exception as e:
|
||
failed_count += 1
|
||
errors.append(f"为用户 {user.username} 分配岗位失败: {str(e)}")
|
||
|
||
await db.commit()
|
||
|
||
# 发送岗位分配通知给新分配的用户
|
||
if newly_assigned_user_ids:
|
||
try:
|
||
from app.services.notification_service import notification_service
|
||
from app.schemas.notification import NotificationBatchCreate, NotificationType
|
||
|
||
notification_batch = NotificationBatchCreate(
|
||
user_ids=newly_assigned_user_ids,
|
||
title="岗位分配通知",
|
||
content=f"您已被分配到「{position_name}」岗位,请查看相关培训课程。",
|
||
type=NotificationType.POSITION_ASSIGN,
|
||
related_id=position_id,
|
||
related_type="position",
|
||
sender_id=current_user.id
|
||
)
|
||
|
||
await notification_service.batch_create_notifications(
|
||
db=db,
|
||
batch_in=notification_batch
|
||
)
|
||
except Exception as e:
|
||
# 通知发送失败不影响岗位分配结果
|
||
import logging
|
||
logging.getLogger(__name__).error(f"发送岗位分配通知失败: {str(e)}")
|
||
|
||
elif operation.action == "assign_team":
|
||
# 批量分配团队
|
||
if not operation.value:
|
||
return ResponseModel(
|
||
code=400,
|
||
message="请指定要分配的团队ID"
|
||
)
|
||
|
||
from app.models.user import user_teams
|
||
|
||
team_id = int(operation.value)
|
||
|
||
for user in users:
|
||
try:
|
||
# 检查是否已在该团队
|
||
existing = await db.execute(
|
||
select(user_teams).where(
|
||
user_teams.c.user_id == user.id,
|
||
user_teams.c.team_id == team_id
|
||
)
|
||
)
|
||
if existing.first():
|
||
# 已在该团队,跳过
|
||
success_count += 1
|
||
continue
|
||
|
||
# 添加团队关联
|
||
await db.execute(
|
||
user_teams.insert().values(
|
||
user_id=user.id,
|
||
team_id=team_id,
|
||
role="member",
|
||
joined_at=datetime.now()
|
||
)
|
||
)
|
||
success_count += 1
|
||
except Exception as e:
|
||
failed_count += 1
|
||
errors.append(f"为用户 {user.username} 分配团队失败: {str(e)}")
|
||
|
||
await db.commit()
|
||
|
||
else:
|
||
return ResponseModel(
|
||
code=400,
|
||
message=f"不支持的操作类型: {operation.action}"
|
||
)
|
||
|
||
# 返回结果
|
||
action_names = {
|
||
"delete": "删除",
|
||
"activate": "启用",
|
||
"deactivate": "禁用",
|
||
"change_role": "修改角色",
|
||
"assign_position": "分配岗位",
|
||
"assign_team": "分配团队"
|
||
}
|
||
action_name = action_names.get(operation.action, operation.action)
|
||
|
||
return ResponseModel(
|
||
code=200,
|
||
message=f"批量{action_name}完成:成功 {success_count} 个,失败 {failed_count} 个",
|
||
data={
|
||
"success_count": success_count,
|
||
"failed_count": failed_count,
|
||
"errors": errors
|
||
}
|
||
)
|
||
|
||
except Exception as e:
|
||
await db.rollback()
|
||
return ResponseModel(
|
||
code=500,
|
||
message=f"批量操作失败: {str(e)}"
|
||
)
|
||
|
||
|