Initial project commit

This commit is contained in:
2026-05-09 16:40:29 +08:00
commit 02b0259a9e
267 changed files with 54891 additions and 0 deletions
+88
View File
@@ -0,0 +1,88 @@
from datetime import datetime
from geoalchemy2 import Geometry
from sqlalchemy import Boolean, DateTime, Float, ForeignKey, Integer, Numeric, String, Text, func
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.db.base import Base
class Spot(Base):
__tablename__ = "spots"
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
title: Mapped[str] = mapped_column(String(200), nullable=False, index=True)
city: Mapped[str] = mapped_column(String(100), nullable=False, index=True)
location: Mapped[str] = mapped_column(
Geometry("POINT", srid=4326), nullable=False
)
description: Mapped[str | None] = mapped_column(Text)
transport: Mapped[str | None] = mapped_column(Text)
best_time: Mapped[str | None] = mapped_column(String(200))
difficulty: Mapped[str | None] = mapped_column(Text)
is_free: Mapped[bool] = mapped_column(Boolean, default=True)
price_min: Mapped[float | None] = mapped_column(Numeric(10, 2), nullable=True)
price_max: Mapped[float | None] = mapped_column(Numeric(10, 2), nullable=True)
audit_status: Mapped[str] = mapped_column(String(20), default="pending")
reject_reason: Mapped[str | None] = mapped_column(String(500))
avg_rating: Mapped[float | None] = mapped_column(Float, default=None)
rating_count: Mapped[int] = mapped_column(Integer, default=0)
favorite_count: Mapped[int] = mapped_column(Integer, default=0)
creator_id: Mapped[int] = mapped_column(
Integer, ForeignKey("users.id"), nullable=False
)
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now()
)
updated_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now(), onupdate=func.now()
)
creator = relationship("User", lazy="selectin")
images = relationship(
"SpotImage", back_populates="spot", lazy="selectin", order_by="SpotImage.sort_order"
)
tags = relationship("Tag", secondary="spot_tags", lazy="selectin")
@property
def longitude(self) -> float | None:
if self.location is None:
return None
# WKBElement → use ST_X via a query; for serialisation we parse the WKT
from geoalchemy2.shape import to_shape
point = to_shape(self.location)
return point.x
@property
def latitude(self) -> float | None:
if self.location is None:
return None
from geoalchemy2.shape import to_shape
point = to_shape(self.location)
return point.y
def __repr__(self) -> str:
return f"<Spot {self.id} {self.title}>"
class SpotImage(Base):
__tablename__ = "spot_images"
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
spot_id: Mapped[int] = mapped_column(
Integer, ForeignKey("spots.id"), nullable=False
)
image_url: Mapped[str] = mapped_column(String(500), nullable=False)
is_cover: Mapped[bool] = mapped_column(Boolean, default=False)
audit_status: Mapped[str] = mapped_column(String(20), default="pending")
sort_order: Mapped[int] = mapped_column(Integer, default=0)
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now()
)
spot = relationship("Spot", back_populates="images")
def __repr__(self) -> str:
return f"<SpotImage {self.id} spot={self.spot_id}>"