重构表结构:合并 profiles→users,展平 API 响应,新增计数器字段

- models: 删除 Profile 模型,字段合并到 User;User 新增云朵/公开/收藏计数器;Cloud 新增 favorite_count
- schemas/serializers: 展平 UserOut/AuthOut/MeOut/AdminUserOut,移除嵌套 profile
- deps: user.profile.role→user.role,移除 selectinload profile
- auth/register: 注册时直接设置 user 字段,不再创建 Profile
- clouds: 创建/删除云朵时同步 user.cloud_count 计数器,状态变化时同步 public_cloud_count
- favorites: 点赞/取消时同步 cloud.favorite_count 计数器
- admin: 审批/隐藏/批量删时同步 public_cloud_count 计数器
- profiles/stats: 用计数器替代实时 COUNT 查询
- alembic: 新增 migration 合并 profiles 到 users,初始化 counters
This commit is contained in:
2026-07-28 23:48:57 +08:00
parent 118c5cbe96
commit e75833ee4b
14 changed files with 519 additions and 285 deletions
+20 -27
View File
@@ -1,12 +1,10 @@
from app.config import settings
from app.models import Cloud, CloudType, Profile, User
from app.models import Cloud, CloudType, User
from app.schemas import (
CloudOut,
CloudOwnerOut,
CloudTypeOut,
CloudTypeSummary,
ProfileOut,
PublicProfileOut,
UserOut,
)
@@ -17,31 +15,18 @@ def media_url(path: str | None) -> str | None:
return f"{settings.media_base_url}/{path.lstrip('/')}"
def profile_out(profile: Profile) -> ProfileOut:
return ProfileOut(
id=profile.id,
username=profile.username,
avatar_url=media_url(profile.avatar_path),
role=profile.role,
is_disabled=profile.is_disabled,
created_at=profile.created_at,
)
def public_profile_out(profile: Profile) -> PublicProfileOut:
return PublicProfileOut(
id=profile.id,
username=profile.username,
avatar_url=media_url(profile.avatar_path),
created_at=profile.created_at,
)
def user_out(user: User) -> UserOut:
return UserOut(
id=user.id,
email=user.email,
email_verified=user.email_verified_at is not None,
username=user.username,
avatar_url=media_url(user.avatar_path),
role=user.role,
is_disabled=user.is_disabled,
cloud_count=user.cloud_count,
public_cloud_count=user.public_cloud_count,
collection_count=user.collection_count,
created_at=user.created_at,
)
@@ -68,9 +53,15 @@ def cloud_type_out(cloud_type: CloudType) -> CloudTypeOut:
)
def cloud_out(cloud: Cloud, *, include_private: bool = True) -> CloudOut:
def cloud_out(
cloud: Cloud,
*,
include_private: bool = True,
favorite_count: int | None = None,
is_favorited: bool = False,
) -> CloudOut:
cloud_type = cloud.cloud_type
profile = cloud.user.profile
owner = cloud.user
type_name = cloud_type.name if cloud_type else (cloud.custom_cloud_type or "未知")
rarity = cloud_type.rarity if cloud_type else "common"
return CloudOut(
@@ -90,8 +81,10 @@ def cloud_out(cloud: Cloud, *, include_private: bool = True) -> CloudOut:
created_at=cloud.created_at,
updated_at=cloud.updated_at,
cloud_type=cloud_type_summary(cloud_type) if cloud_type else None,
owner=CloudOwnerOut(id=profile.id, username=profile.username),
owner=CloudOwnerOut(id=owner.id, username=owner.username),
cloud_type_name=type_name,
cloud_type_rarity=rarity,
username=profile.username,
username=owner.username,
favorite_count=favorite_count if favorite_count is not None else 0,
is_favorited=is_favorited,
)