新增管理员接口 GET /profiles/{user_id}/stats,返回用户全部/公开上传数和收藏数
This commit is contained in:
+41
-3
@@ -6,9 +6,9 @@ from sqlalchemy import func, select
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
from sqlalchemy.orm import joinedload
|
||||
|
||||
from app.deps import CurrentUser, DbSession, OptionalUser
|
||||
from app.models import Cloud, Profile, User
|
||||
from app.schemas import CloudOut, PageOut, ProfileOut, ProfileUpdateIn, PublicProfileOut
|
||||
from app.deps import AdminUser, CurrentUser, DbSession, OptionalUser
|
||||
from app.models import Cloud, Profile, User, UserCollection
|
||||
from app.schemas import CloudOut, PageOut, ProfileOut, ProfileStatsOut, ProfileUpdateIn, PublicProfileOut
|
||||
from app.serializers import cloud_out, profile_out, public_profile_out
|
||||
|
||||
|
||||
@@ -38,6 +38,44 @@ async def update_my_profile(
|
||||
return profile_out(user.profile)
|
||||
|
||||
|
||||
@router.get("/{user_id}/stats", response_model=ProfileStatsOut)
|
||||
async def get_user_stats(
|
||||
user_id: uuid.UUID,
|
||||
_: AdminUser,
|
||||
db: DbSession,
|
||||
) -> ProfileStatsOut:
|
||||
result = await db.execute(
|
||||
select(User).options(joinedload(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="用户不存在")
|
||||
|
||||
cloud_count = await db.scalar(
|
||||
select(func.count()).select_from(Cloud).where(Cloud.user_id == user_id)
|
||||
) or 0
|
||||
public_cloud_count = await db.scalar(
|
||||
select(func.count()).select_from(Cloud).where(
|
||||
Cloud.user_id == user_id,
|
||||
Cloud.status == "approved",
|
||||
Cloud.is_hidden.is_(False),
|
||||
)
|
||||
) or 0
|
||||
collection_count = await db.scalar(
|
||||
select(func.count()).select_from(UserCollection).where(UserCollection.user_id == user_id)
|
||||
) or 0
|
||||
|
||||
return ProfileStatsOut(
|
||||
user_id=user.id,
|
||||
username=user.profile.username,
|
||||
email=user.email,
|
||||
created_at=user.created_at,
|
||||
cloud_count=cloud_count,
|
||||
public_cloud_count=public_cloud_count,
|
||||
collection_count=collection_count,
|
||||
)
|
||||
|
||||
|
||||
@router.get("/{user_id}/clouds", response_model=PageOut[CloudOut])
|
||||
async def get_profile_clouds(
|
||||
user_id: uuid.UUID,
|
||||
|
||||
@@ -252,3 +252,13 @@ class AdminCloudVisibilityIn(BatchIdsIn):
|
||||
|
||||
class BatchUpdateOut(BaseModel):
|
||||
updated: int
|
||||
|
||||
|
||||
class ProfileStatsOut(BaseModel):
|
||||
user_id: uuid.UUID
|
||||
username: str
|
||||
email: str
|
||||
created_at: datetime
|
||||
cloud_count: int
|
||||
public_cloud_count: int
|
||||
collection_count: int
|
||||
|
||||
@@ -136,3 +136,67 @@ def test_get_user_clouds_as_owner(client: TestClient, monkeypatch) -> None:
|
||||
def test_get_nonexistent_user_clouds(client: TestClient) -> None:
|
||||
r = client.get(f"/api/v1/profiles/{uuid.uuid4()}/clouds")
|
||||
assert r.status_code == 404
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# stats (admin only)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_get_user_stats_admin(client: TestClient, monkeypatch) -> None:
|
||||
info = register_and_confirm(
|
||||
client, monkeypatch,
|
||||
f"stats-{uuid.uuid4().hex[:8]}@example.com",
|
||||
"statspass", "统计用户",
|
||||
)
|
||||
# upload one cloud and approve it, upload another pending
|
||||
ur1 = client.post(
|
||||
"/api/v1/clouds",
|
||||
headers=bearer(info["token"]),
|
||||
files={"image": ("cloud.png", png_bytes(), "image/png")},
|
||||
data={"cloud_type_id": "1", "is_hidden": "false"},
|
||||
)
|
||||
approve_cloud(client, admin_token(client), ur1.json()["cloud"]["id"])
|
||||
client.post(
|
||||
"/api/v1/clouds",
|
||||
headers=bearer(info["token"]),
|
||||
files={"image": ("cloud.png", png_bytes(), "image/png")},
|
||||
data={"cloud_type_id": "2", "is_hidden": "false"},
|
||||
)
|
||||
|
||||
r = client.get(
|
||||
f"/api/v1/profiles/{info['user_id']}/stats",
|
||||
headers=bearer(admin_token(client)),
|
||||
)
|
||||
assert r.status_code == 200
|
||||
body = r.json()
|
||||
assert body["cloud_count"] == 2
|
||||
assert body["public_cloud_count"] == 1
|
||||
assert body["collection_count"] == 2
|
||||
assert body["username"] == "统计用户"
|
||||
|
||||
|
||||
def test_get_user_stats_non_admin(client: TestClient, monkeypatch) -> None:
|
||||
info = register_and_confirm(
|
||||
client, monkeypatch,
|
||||
f"stats-na-{uuid.uuid4().hex[:8]}@example.com",
|
||||
"statsnap", "非管理员统计",
|
||||
)
|
||||
r = client.get(
|
||||
f"/api/v1/profiles/{info['user_id']}/stats",
|
||||
headers=bearer(info["token"]),
|
||||
)
|
||||
assert r.status_code == 403
|
||||
|
||||
|
||||
def test_get_user_stats_unauthenticated(client: TestClient) -> None:
|
||||
r = client.get(f"/api/v1/profiles/{uuid.uuid4()}/stats")
|
||||
assert r.status_code == 401
|
||||
|
||||
|
||||
def test_get_user_stats_nonexistent(client: TestClient) -> None:
|
||||
r = client.get(
|
||||
f"/api/v1/profiles/{uuid.uuid4()}/stats",
|
||||
headers=bearer(admin_token(client)),
|
||||
)
|
||||
assert r.status_code == 404
|
||||
|
||||
Reference in New Issue
Block a user