新增管理员接口 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.exc import IntegrityError
|
||||||
from sqlalchemy.orm import joinedload
|
from sqlalchemy.orm import joinedload
|
||||||
|
|
||||||
from app.deps import CurrentUser, DbSession, OptionalUser
|
from app.deps import AdminUser, CurrentUser, DbSession, OptionalUser
|
||||||
from app.models import Cloud, Profile, User
|
from app.models import Cloud, Profile, User, UserCollection
|
||||||
from app.schemas import CloudOut, PageOut, ProfileOut, ProfileUpdateIn, PublicProfileOut
|
from app.schemas import CloudOut, PageOut, ProfileOut, ProfileStatsOut, ProfileUpdateIn, PublicProfileOut
|
||||||
from app.serializers import cloud_out, profile_out, public_profile_out
|
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)
|
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])
|
@router.get("/{user_id}/clouds", response_model=PageOut[CloudOut])
|
||||||
async def get_profile_clouds(
|
async def get_profile_clouds(
|
||||||
user_id: uuid.UUID,
|
user_id: uuid.UUID,
|
||||||
|
|||||||
@@ -252,3 +252,13 @@ class AdminCloudVisibilityIn(BatchIdsIn):
|
|||||||
|
|
||||||
class BatchUpdateOut(BaseModel):
|
class BatchUpdateOut(BaseModel):
|
||||||
updated: int
|
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:
|
def test_get_nonexistent_user_clouds(client: TestClient) -> None:
|
||||||
r = client.get(f"/api/v1/profiles/{uuid.uuid4()}/clouds")
|
r = client.get(f"/api/v1/profiles/{uuid.uuid4()}/clouds")
|
||||||
assert r.status_code == 404
|
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