Files
Cheng Zhou ab59476d10
Client Quality Gates / Shared client and Web (push) Has been cancelled
Client Quality Gates / macOS Desktop (push) Has been cancelled
feat(桌面与监控): 完善工作台导航和登录活动定位
- 优化桌面标签、上下文标题、前进后退、导航栏隐藏和原生菜单体验

- 补充登录会话 IP 采集、地理位置回退、管理端展示及数据库迁移

- 更新桌面发布检查、运维文档和前后端测试覆盖
2026-07-13 16:03:20 +08:00

151 lines
6.7 KiB
Python

import uuid
from fastapi import APIRouter, Depends, HTTPException, Query, Response, status
from sqlalchemy.exc import IntegrityError
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.deps import get_db_session, is_system_admin, require_roles
from app.schemas.common import PaginatedResponse
from app.crud import user as user_crud
from app.crud import member as member_crud
from app.utils.pagination import paginate
from app.schemas.user import LoginStatus, UserCreate, UserLoginActivityRead, UserRead, UserStatus, UserUpdate
from app.services.user_login_sessions import get_login_summaries, list_login_activities, login_activity_payload
router = APIRouter()
@router.get("/", response_model=PaginatedResponse[UserRead], summary="用户列表", description="仅 ADMIN 可用,支持 skip/limit。")
async def list_users(
skip: int = 0,
limit: int = 100,
keyword: str | None = Query(default=None),
user_status: UserStatus | None = Query(default=None, alias="status"),
login_status: LoginStatus | None = Query(default=None),
db: AsyncSession = Depends(get_db_session),
current_user=Depends(require_roles(["ADMIN"])),
) -> PaginatedResponse[UserRead]:
users = await user_crud.list_users(
db, skip=skip, limit=limit, keyword=keyword, status=user_status, login_status=login_status
)
total_users = await user_crud.count_users(
db, keyword=keyword, status=user_status, login_status=login_status
)
summaries = await get_login_summaries(db, [user.id for user in users])
items = []
for user in users:
summary = summaries.get(user.id)
item = UserRead.model_validate(user).model_dump()
if summary:
item.update(
{
"login_status": summary.status,
"last_login_at": summary.last_login_at,
"last_seen_at": summary.last_seen_at,
"last_client_type": summary.client_type,
"active_session_count": summary.active_session_count,
}
)
items.append(item)
return paginate(items, total=total_users)
@router.get("/{user_id}/login-activities", response_model=list[UserLoginActivityRead])
async def read_user_login_activities(
user_id: uuid.UUID,
response: Response,
limit: int = Query(default=30, ge=1, le=100),
db: AsyncSession = Depends(get_db_session),
current_user=Depends(require_roles(["ADMIN"])),
) -> list[UserLoginActivityRead]:
response.headers["Cache-Control"] = "no-store"
if not await user_crud.get_by_id(db, user_id):
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="用户不存在")
activities = await list_login_activities(db, user_id=user_id, limit=limit)
return [login_activity_payload(activity) for activity in activities]
@router.post("/", response_model=UserRead, status_code=status.HTTP_201_CREATED)
async def create_user(
user_in: UserCreate,
db: AsyncSession = Depends(get_db_session),
current_user=Depends(require_roles(["ADMIN"])),
) -> UserRead:
existing = await user_crud.get_by_email(db, user_in.email)
if existing:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT,
detail="邮箱已存在",
)
user = await user_crud.create_user(db, user_in)
return user
@router.patch("/{user_id}", response_model=UserRead)
async def update_user(
user_id: uuid.UUID,
user_in: UserUpdate,
db: AsyncSession = Depends(get_db_session),
current_user=Depends(require_roles(["ADMIN"])),
) -> UserRead:
db_user = await user_crud.get_by_id(db, user_id)
if not db_user:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="用户不存在")
if user_crud.is_protected_admin_user(db_user):
if user_in.email is not None and user_in.email != db_user.email:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="系统管理员邮箱不允许修改")
requested_status = user_in.status
if user_in.is_active is not None:
requested_status = "ACTIVE" if user_in.is_active else "DISABLED"
if requested_status is not None and requested_status != "ACTIVE":
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="系统管理员不允许停用")
if is_system_admin(db_user):
requested_status = user_in.status
if user_in.is_active is not None:
requested_status = "ACTIVE" if user_in.is_active else "DISABLED"
will_disable = requested_status is not None and requested_status != "ACTIVE"
if will_disable and db_user.status.value == "ACTIVE":
active_admins = await user_crud.count_active_admins(db)
if active_admins <= 1:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="至少保留一个管理员账号")
user = await user_crud.update_user(db, db_user, user_in)
return user
@router.delete("/{user_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_user(
user_id: uuid.UUID,
db: AsyncSession = Depends(get_db_session),
current_user=Depends(require_roles(["ADMIN"])),
):
db_user = await user_crud.get_by_id(db, user_id)
if not db_user:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="用户不存在")
if user_crud.is_protected_admin_user(db_user):
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="系统管理员账号不允许删除")
if db_user.id == current_user.id:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="不允许删除自己")
if is_system_admin(db_user) and db_user.status.value == "ACTIVE":
active_admins = await user_crud.count_active_admins(db)
if active_admins <= 1:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="至少保留一个管理员账号")
has_membership = await member_crud.user_has_memberships(db, db_user.id)
if has_membership:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="该账号仍在项目成员中,请先在项目成员配置中移除后再删除",
)
if await user_crud.user_has_retained_history(db, db_user.id):
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="该账号已有审计或权限访问记录,请停用账号以保留历史追溯",
)
try:
await user_crud.delete_user(db, db_user)
except IntegrityError as exc:
await db.rollback()
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="该账号已有业务记录,请停用账号以保留历史追溯",
) from exc