197 lines
6.8 KiB
Python
197 lines
6.8 KiB
Python
import math
|
|
import uuid
|
|
from datetime import datetime, timezone
|
|
from typing import Literal
|
|
from zoneinfo import ZoneInfo
|
|
|
|
from fastapi import APIRouter, HTTPException, Query, status
|
|
from sqlalchemy import case, func, select, update
|
|
from sqlalchemy.orm import joinedload, selectinload
|
|
|
|
from app.deps import AdminUser, DbSession
|
|
from app.models import Cloud, Profile, User
|
|
from app.schemas import (
|
|
AdminCloudStatusIn,
|
|
AdminCloudVisibilityIn,
|
|
AdminStatsOut,
|
|
AdminUserOut,
|
|
AdminUserUpdateIn,
|
|
BatchIdsIn,
|
|
BatchUpdateOut,
|
|
CloudOut,
|
|
DeleteResultOut,
|
|
PageOut,
|
|
)
|
|
from app.serializers import cloud_out, profile_out, user_out
|
|
from app.services.storage import delete_files
|
|
|
|
|
|
router = APIRouter(prefix="/admin", tags=["管理后台"])
|
|
|
|
|
|
@router.get("/stats", response_model=AdminStatsOut)
|
|
async def get_stats(_: AdminUser, db: DbSession) -> AdminStatsOut:
|
|
local_tz = ZoneInfo("Asia/Shanghai")
|
|
local_now = datetime.now(local_tz)
|
|
today_start = local_now.replace(hour=0, minute=0, second=0, microsecond=0).astimezone(timezone.utc)
|
|
users = await db.scalar(select(func.count()).select_from(User)) or 0
|
|
row = (
|
|
await db.execute(
|
|
select(
|
|
func.count(Cloud.id).label("images"),
|
|
func.sum(case((Cloud.created_at >= today_start, 1), else_=0)).label("today"),
|
|
func.sum(case((Cloud.status == "pending", 1), else_=0)).label("pending"),
|
|
func.sum(case((Cloud.status == "approved", 1), else_=0)).label("approved"),
|
|
func.sum(case((Cloud.status == "rejected", 1), else_=0)).label("rejected"),
|
|
func.sum(case((Cloud.is_hidden.is_(True), 1), else_=0)).label("hidden"),
|
|
)
|
|
)
|
|
).one()
|
|
return AdminStatsOut(
|
|
users=users,
|
|
images=row.images or 0,
|
|
today_uploads=row.today or 0,
|
|
pending=row.pending or 0,
|
|
approved=row.approved or 0,
|
|
rejected=row.rejected or 0,
|
|
hidden=row.hidden or 0,
|
|
)
|
|
|
|
|
|
@router.get("/users", response_model=PageOut[AdminUserOut])
|
|
async def list_users(
|
|
_: AdminUser,
|
|
db: DbSession,
|
|
page: int = Query(1, ge=1),
|
|
page_size: int = Query(100, ge=1, le=100),
|
|
) -> PageOut[AdminUserOut]:
|
|
total = await db.scalar(select(func.count()).select_from(User)) or 0
|
|
result = await db.execute(
|
|
select(User)
|
|
.options(selectinload(User.profile))
|
|
.order_by(User.created_at.desc(), User.id.desc())
|
|
.offset((page - 1) * page_size)
|
|
.limit(page_size)
|
|
)
|
|
items = [AdminUserOut(user=user_out(user), profile=profile_out(user.profile)) for user in result.scalars()]
|
|
return PageOut(
|
|
items=items,
|
|
page=page,
|
|
page_size=page_size,
|
|
total=total,
|
|
total_pages=max(1, math.ceil(total / page_size)),
|
|
)
|
|
|
|
|
|
@router.patch("/users/{user_id}", response_model=AdminUserOut)
|
|
async def update_user(
|
|
user_id: uuid.UUID,
|
|
payload: AdminUserUpdateIn,
|
|
admin: AdminUser,
|
|
db: DbSession,
|
|
) -> AdminUserOut:
|
|
result = await db.execute(
|
|
select(User).options(selectinload(User.profile)).where(User.id == user_id)
|
|
)
|
|
user = result.scalar_one_or_none()
|
|
if not user:
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="用户不存在")
|
|
if user.id == admin.id and (
|
|
payload.is_disabled is True or (payload.role is not None and payload.role != "admin")
|
|
):
|
|
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="不能禁用自己或移除自己的管理员权限")
|
|
if payload.role is not None:
|
|
user.profile.role = payload.role
|
|
if payload.is_disabled is not None:
|
|
user.profile.is_disabled = payload.is_disabled
|
|
await db.commit()
|
|
return AdminUserOut(user=user_out(user), profile=profile_out(user.profile))
|
|
|
|
|
|
@router.get("/clouds", response_model=PageOut[CloudOut])
|
|
async def list_admin_clouds(
|
|
_: AdminUser,
|
|
db: DbSession,
|
|
page: int = Query(1, ge=1),
|
|
page_size: int = Query(100, ge=1, le=120),
|
|
cloud_status: Literal["pending", "approved", "rejected"] | None = Query(None, alias="status"),
|
|
is_hidden: bool | None = None,
|
|
) -> PageOut[CloudOut]:
|
|
filters = []
|
|
if cloud_status:
|
|
filters.append(Cloud.status == cloud_status)
|
|
if is_hidden is not None:
|
|
filters.append(Cloud.is_hidden == is_hidden)
|
|
total = await db.scalar(select(func.count()).select_from(Cloud).where(*filters)) or 0
|
|
result = await db.execute(
|
|
select(Cloud)
|
|
.options(joinedload(Cloud.user).joinedload(User.profile), joinedload(Cloud.cloud_type))
|
|
.where(*filters)
|
|
.order_by(Cloud.created_at.desc(), Cloud.id.desc())
|
|
.offset((page - 1) * page_size)
|
|
.limit(page_size)
|
|
)
|
|
return PageOut(
|
|
items=[cloud_out(item) for item in result.scalars().unique().all()],
|
|
page=page,
|
|
page_size=page_size,
|
|
total=total,
|
|
total_pages=max(1, math.ceil(total / page_size)),
|
|
)
|
|
|
|
|
|
async def _require_all_cloud_ids(db: DbSession, ids: list[uuid.UUID]) -> None:
|
|
count = await db.scalar(select(func.count()).select_from(Cloud).where(Cloud.id.in_(ids))) or 0
|
|
if count != len(ids):
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="部分图片不存在")
|
|
|
|
|
|
@router.patch("/clouds/status", response_model=BatchUpdateOut)
|
|
async def update_cloud_status(
|
|
payload: AdminCloudStatusIn,
|
|
_: AdminUser,
|
|
db: DbSession,
|
|
) -> BatchUpdateOut:
|
|
await _require_all_cloud_ids(db, payload.ids)
|
|
await db.execute(
|
|
update(Cloud)
|
|
.where(Cloud.id.in_(payload.ids))
|
|
.values(status=payload.status, updated_at=func.now())
|
|
)
|
|
await db.commit()
|
|
return BatchUpdateOut(updated=len(payload.ids))
|
|
|
|
|
|
@router.patch("/clouds/visibility", response_model=BatchUpdateOut)
|
|
async def update_cloud_visibility(
|
|
payload: AdminCloudVisibilityIn,
|
|
_: AdminUser,
|
|
db: DbSession,
|
|
) -> BatchUpdateOut:
|
|
await _require_all_cloud_ids(db, payload.ids)
|
|
await db.execute(
|
|
update(Cloud)
|
|
.where(Cloud.id.in_(payload.ids))
|
|
.values(is_hidden=payload.is_hidden, updated_at=func.now())
|
|
)
|
|
await db.commit()
|
|
return BatchUpdateOut(updated=len(payload.ids))
|
|
|
|
|
|
@router.post("/clouds/batch-delete", response_model=DeleteResultOut)
|
|
async def delete_admin_clouds(
|
|
payload: BatchIdsIn,
|
|
_: AdminUser,
|
|
db: DbSession,
|
|
) -> DeleteResultOut:
|
|
result = await db.execute(select(Cloud).where(Cloud.id.in_(payload.ids)))
|
|
clouds = result.scalars().all()
|
|
if len(clouds) != len(payload.ids):
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="部分图片不存在")
|
|
paths = [path for cloud in clouds for path in (cloud.image_path, cloud.thumbnail_path)]
|
|
for cloud in clouds:
|
|
await db.delete(cloud)
|
|
await db.commit()
|
|
await delete_files(paths)
|
|
return DeleteResultOut(deleted=len(clouds))
|