Files
opencloud-backend/app/models.py
T

269 lines
10 KiB
Python

import uuid
from datetime import datetime
from decimal import Decimal
from sqlalchemy import (
Boolean,
CheckConstraint,
DateTime,
ForeignKey,
Index,
Numeric,
SmallInteger,
Text,
UniqueConstraint,
Uuid,
func,
text,
)
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.database import Base
class TimestampMixin:
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now(), nullable=False
)
updated_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now(), onupdate=func.now(), nullable=False
)
class User(Base, TimestampMixin):
__tablename__ = "users"
id: Mapped[uuid.UUID] = mapped_column(Uuid, primary_key=True, default=uuid.uuid4)
email: Mapped[str] = mapped_column(Text, unique=True, nullable=False)
password_hash: Mapped[str] = mapped_column(Text, nullable=False)
email_verified_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
profile: Mapped["Profile"] = relationship(
back_populates="user", cascade="all, delete-orphan", uselist=False, lazy="raise"
)
clouds: Mapped[list["Cloud"]] = relationship(back_populates="user", lazy="raise")
class Profile(Base):
__tablename__ = "profiles"
__table_args__ = (
CheckConstraint("length(trim(username)) >= 2", name="ck_profiles_username_length"),
CheckConstraint("role IN ('user', 'admin')", name="ck_profiles_role"),
)
id: Mapped[uuid.UUID] = mapped_column(
Uuid, ForeignKey("users.id", ondelete="CASCADE"), primary_key=True
)
username: Mapped[str] = mapped_column(Text, unique=True, nullable=False)
avatar_path: Mapped[str | None] = mapped_column(Text)
role: Mapped[str] = mapped_column(Text, nullable=False, default="user", server_default="user")
is_disabled: Mapped[bool] = mapped_column(
Boolean, nullable=False, default=False, server_default=text("false")
)
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now(), nullable=False
)
updated_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now(), onupdate=func.now(), nullable=False
)
user: Mapped[User] = relationship(back_populates="profile", lazy="raise")
class RefreshSession(Base):
__tablename__ = "refresh_sessions"
__table_args__ = (
Index("ix_refresh_sessions_user_id", "user_id"),
Index("ix_refresh_sessions_replaced_by_id", "replaced_by_id"),
Index(
"ix_refresh_sessions_active_user",
"user_id",
"expires_at",
postgresql_where=text("revoked_at IS NULL"),
),
)
id: Mapped[uuid.UUID] = mapped_column(Uuid, primary_key=True, default=uuid.uuid4)
user_id: Mapped[uuid.UUID] = mapped_column(
Uuid, ForeignKey("users.id", ondelete="CASCADE"), nullable=False
)
token_hash: Mapped[str] = mapped_column(Text, unique=True, nullable=False)
expires_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
revoked_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
replaced_by_id: Mapped[uuid.UUID | None] = mapped_column(
Uuid, ForeignKey("refresh_sessions.id", ondelete="SET NULL")
)
user_agent: Mapped[str | None] = mapped_column(Text)
ip_address: Mapped[str | None] = mapped_column(Text)
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now(), nullable=False
)
user: Mapped[User] = relationship(lazy="raise")
class EmailToken(Base):
__tablename__ = "email_tokens"
__table_args__ = (
CheckConstraint(
"purpose IN ('confirm_email', 'reset_password')", name="ck_email_tokens_purpose"
),
Index("ix_email_tokens_user_purpose", "user_id", "purpose"),
)
id: Mapped[uuid.UUID] = mapped_column(Uuid, primary_key=True, default=uuid.uuid4)
user_id: Mapped[uuid.UUID] = mapped_column(
Uuid, ForeignKey("users.id", ondelete="CASCADE"), nullable=False
)
purpose: Mapped[str] = mapped_column(Text, nullable=False)
token_hash: Mapped[str] = mapped_column(Text, unique=True, nullable=False)
expires_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
used_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now(), nullable=False
)
class CloudType(Base):
__tablename__ = "cloud_types"
__table_args__ = (
CheckConstraint("rarity IN ('common', 'uncommon', 'rare')", name="ck_cloud_types_rarity"),
)
id: Mapped[int] = mapped_column(SmallInteger, primary_key=True, autoincrement=False)
name: Mapped[str] = mapped_column(Text, unique=True, nullable=False)
name_en: Mapped[str] = mapped_column(Text, unique=True, nullable=False)
genus: Mapped[str] = mapped_column(Text, nullable=False)
rarity: Mapped[str] = mapped_column(Text, nullable=False, default="common")
description: Mapped[str | None] = mapped_column(Text)
icon_url: Mapped[str | None] = mapped_column(Text)
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now(), nullable=False
)
class Cloud(Base, TimestampMixin):
__tablename__ = "clouds"
__table_args__ = (
CheckConstraint("status IN ('pending', 'approved', 'rejected')", name="ck_clouds_status"),
CheckConstraint(
"((latitude IS NULL AND longitude IS NULL) OR "
"(latitude IS NOT NULL AND longitude IS NOT NULL))",
name="ck_clouds_coordinate_pair",
),
CheckConstraint("latitude IS NULL OR latitude BETWEEN -90 AND 90", name="ck_clouds_latitude"),
CheckConstraint(
"longitude IS NULL OR longitude BETWEEN -180 AND 180", name="ck_clouds_longitude"
),
CheckConstraint(
"((cloud_type_id IS NOT NULL AND custom_cloud_type IS NULL) OR "
"(cloud_type_id IS NULL AND custom_cloud_type IS NOT NULL "
"AND length(trim(custom_cloud_type)) > 0))",
name="ck_clouds_type_choice",
),
Index("ix_clouds_user_id", "user_id"),
Index("ix_clouds_cloud_type_id", "cloud_type_id"),
Index("ix_clouds_user_captured", "user_id", text("captured_at DESC"), text("created_at DESC")),
Index("ix_clouds_status_created", "status", text("created_at DESC")),
Index(
"ix_clouds_public_gallery",
text("created_at DESC"),
text("id DESC"),
postgresql_where=text("status = 'approved' AND is_hidden = false"),
),
Index(
"ix_clouds_public_map",
"captured_at",
postgresql_where=text(
"status = 'approved' AND is_hidden = false "
"AND latitude IS NOT NULL AND longitude IS NOT NULL"
),
),
Index(
"ix_clouds_public_type",
"cloud_type_id",
text("captured_at DESC"),
postgresql_where=text("status = 'approved' AND is_hidden = false"),
),
)
id: Mapped[uuid.UUID] = mapped_column(Uuid, primary_key=True, default=uuid.uuid4)
user_id: Mapped[uuid.UUID] = mapped_column(
Uuid, ForeignKey("users.id", ondelete="CASCADE"), nullable=False
)
cloud_type_id: Mapped[int | None] = mapped_column(
SmallInteger, ForeignKey("cloud_types.id", ondelete="SET NULL")
)
custom_cloud_type: Mapped[str | None] = mapped_column(Text)
image_path: Mapped[str] = mapped_column(Text, unique=True, nullable=False)
thumbnail_path: Mapped[str] = mapped_column(Text, unique=True, nullable=False)
latitude: Mapped[Decimal | None] = mapped_column(Numeric(4, 2))
longitude: Mapped[Decimal | None] = mapped_column(Numeric(5, 2))
location_name: Mapped[str | None] = mapped_column(Text)
description: Mapped[str | None] = mapped_column(Text)
captured_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
status: Mapped[str] = mapped_column(Text, nullable=False, default="pending", server_default="pending")
is_hidden: Mapped[bool] = mapped_column(
Boolean, nullable=False, default=False, server_default=text("false")
)
user: Mapped[User] = relationship(back_populates="clouds", lazy="raise")
cloud_type: Mapped[CloudType | None] = relationship(lazy="raise")
class UserCollection(Base):
__tablename__ = "user_collections"
__table_args__ = (
UniqueConstraint("user_id", "cloud_type_id", name="uq_user_collections_user_type"),
Index("ix_user_collections_user_id", "user_id"),
Index("ix_user_collections_cloud_type_id", "cloud_type_id"),
Index("ix_user_collections_first_cloud_id", "first_cloud_id"),
)
id: Mapped[uuid.UUID] = mapped_column(Uuid, primary_key=True, default=uuid.uuid4)
user_id: Mapped[uuid.UUID] = mapped_column(
Uuid, ForeignKey("users.id", ondelete="CASCADE"), nullable=False
)
cloud_type_id: Mapped[int] = mapped_column(
SmallInteger, ForeignKey("cloud_types.id", ondelete="CASCADE"), nullable=False
)
first_cloud_id: Mapped[uuid.UUID | None] = mapped_column(
Uuid, ForeignKey("clouds.id", ondelete="SET NULL")
)
unlocked_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now(), nullable=False
)
cloud_type: Mapped[CloudType] = relationship(lazy="raise")
first_cloud: Mapped[Cloud | None] = relationship(lazy="raise")
class CloudFavorite(Base):
"""用户对照片的点赞/收藏记录。"""
__tablename__ = "cloud_favorites"
__table_args__ = (
UniqueConstraint("user_id", "cloud_id", name="uq_cloud_favorites_user_cloud"),
Index("ix_cloud_favorites_user_id", "user_id"),
Index("ix_cloud_favorites_cloud_id", "cloud_id"),
Index(
"ix_cloud_favorites_user_created",
"user_id",
text("created_at DESC"),
text("id DESC"),
),
)
id: Mapped[uuid.UUID] = mapped_column(Uuid, primary_key=True, default=uuid.uuid4)
user_id: Mapped[uuid.UUID] = mapped_column(
Uuid, ForeignKey("users.id", ondelete="CASCADE"), nullable=False
)
cloud_id: Mapped[uuid.UUID] = mapped_column(
Uuid, ForeignKey("clouds.id", ondelete="CASCADE"), nullable=False
)
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now(), nullable=False
)
cloud: Mapped[Cloud] = relationship(lazy="raise")