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
+26
View File
@@ -0,0 +1,26 @@
from app.models.user import User
from app.models.spot import Spot, SpotImage
from app.models.favorite import Favorite
from app.models.point_ledger import PointLedger
from app.models.audit_log import AuditLog
from app.models.comment import Comment
from app.models.report import Report
from app.models.rating import Rating
from app.models.tag import Tag, SpotTag
from app.models.correction import Correction
from app.models.notification import Notification
from app.models.shooting import ShootingRequest, ShootingApplication
from app.models.event import Event, EventRegistration, EventPhoto
from app.models.promotion import Promotion
from app.models.membership import MembershipPlan, UserMembership
from app.models.app_nav_config import AppNavConfig
from app.models.system_config import SystemConfig
__all__ = [
"User", "Spot", "SpotImage", "Favorite", "PointLedger", "AuditLog",
"Comment", "Report", "Rating", "Tag", "SpotTag", "Correction", "Notification",
"ShootingRequest", "ShootingApplication",
"Event", "EventRegistration", "EventPhoto",
"Promotion", "MembershipPlan", "UserMembership",
"AppNavConfig", "SystemConfig",
]
+26
View File
@@ -0,0 +1,26 @@
from datetime import datetime
from sqlalchemy import Boolean, DateTime, Integer, String, func
from sqlalchemy.orm import Mapped, mapped_column
from app.db.base import Base
class AppNavConfig(Base):
__tablename__ = "app_nav_configs"
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
key: Mapped[str] = mapped_column(String(50), nullable=False, unique=True, index=True)
label: Mapped[str] = mapped_column(String(50), nullable=False)
page_path: Mapped[str] = mapped_column(String(200), nullable=False)
icon: Mapped[str] = mapped_column(String(50), nullable=False)
active_icon: Mapped[str] = mapped_column(String(50), nullable=False)
color: Mapped[str] = mapped_column(String(20), default="#999999")
active_color: Mapped[str] = mapped_column(String(20), default="#6366f1")
is_active: Mapped[bool] = mapped_column(Boolean, default=True, index=True)
sort_order: Mapped[int] = mapped_column(Integer, default=0)
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())
def __repr__(self) -> str:
return f"<AppNavConfig {self.key}>"
+27
View File
@@ -0,0 +1,27 @@
from datetime import datetime
from sqlalchemy import DateTime, ForeignKey, Integer, String, Text, func
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.db.base import Base
class AuditLog(Base):
__tablename__ = "audit_logs"
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
operator_id: Mapped[int] = mapped_column(
Integer, ForeignKey("users.id"), nullable=False
)
action: Mapped[str] = mapped_column(String(100), nullable=False, index=True)
target_type: Mapped[str] = mapped_column(String(50), nullable=False)
target_id: Mapped[int | None] = mapped_column(Integer)
detail: Mapped[str | None] = mapped_column(Text)
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now()
)
operator = relationship("User", lazy="selectin")
def __repr__(self) -> str:
return f"<AuditLog {self.id} {self.action} by={self.operator_id}>"
+35
View File
@@ -0,0 +1,35 @@
from datetime import datetime
from sqlalchemy import DateTime, ForeignKey, Integer, String, Text, func
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.db.base import Base
class Comment(Base):
__tablename__ = "comments"
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
spot_id: Mapped[int] = mapped_column(Integer, ForeignKey("spots.id"), nullable=False, index=True)
user_id: Mapped[int] = mapped_column(Integer, ForeignKey("users.id"), nullable=False)
parent_id: Mapped[int | None] = mapped_column(Integer, ForeignKey("comments.id"), nullable=True)
content: Mapped[str] = mapped_column(Text, nullable=False)
audit_status: Mapped[str] = mapped_column(String(20), default="approved")
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
user = relationship("User", lazy="selectin")
replies = relationship(
"Comment",
back_populates="parent",
lazy="noload",
foreign_keys=[parent_id],
)
parent = relationship(
"Comment",
remote_side=[id],
back_populates="replies",
lazy="noload",
)
def __repr__(self) -> str:
return f"<Comment {self.id} spot={self.spot_id}>"
+27
View File
@@ -0,0 +1,27 @@
from datetime import datetime
from sqlalchemy import DateTime, ForeignKey, Integer, String, Text, func
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.db.base import Base
class Correction(Base):
__tablename__ = "corrections"
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
spot_id: Mapped[int] = mapped_column(Integer, ForeignKey("spots.id"), nullable=False)
user_id: Mapped[int] = mapped_column(Integer, ForeignKey("users.id"), nullable=False)
field_name: Mapped[str] = mapped_column(String(50), nullable=False)
suggested_value: Mapped[str] = mapped_column(Text, nullable=False)
reason: Mapped[str | None] = mapped_column(Text, nullable=True)
status: Mapped[str] = mapped_column(String(20), default="pending")
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now()
)
user = relationship("User", lazy="joined")
spot = relationship("Spot", lazy="joined")
def __repr__(self) -> str:
return f"<Correction {self.id} spot={self.spot_id} field={self.field_name}>"
+74
View File
@@ -0,0 +1,74 @@
from datetime import datetime
from sqlalchemy import (
Boolean, DateTime, ForeignKey, Integer,
String, Text, func,
)
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.db.base import Base
class Event(Base):
__tablename__ = "events"
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
creator_id: Mapped[int] = mapped_column(Integer, ForeignKey("users.id"), nullable=False, index=True)
title: Mapped[str] = mapped_column(String(200), nullable=False)
city: Mapped[str] = mapped_column(String(100), nullable=False, index=True)
description: Mapped[str | None] = mapped_column(Text)
cover_url: Mapped[str | None] = mapped_column(String(500))
location_name: Mapped[str | None] = mapped_column(String(200))
start_time: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
end_time: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
max_participants: Mapped[int] = mapped_column(Integer, default=0)
spot_id: Mapped[int | None] = mapped_column(Integer, ForeignKey("spots.id"), nullable=True)
status: Mapped[str] = mapped_column(String(20), default="upcoming", index=True)
audit_status: Mapped[str] = mapped_column(String(20), default="pending")
reject_reason: Mapped[str | None] = mapped_column(String(500))
registration_count: Mapped[int] = mapped_column(Integer, default=0)
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")
spot = relationship("Spot", lazy="selectin")
registrations = relationship("EventRegistration", back_populates="event", lazy="selectin")
photos = relationship("EventPhoto", back_populates="event", lazy="selectin")
def __repr__(self) -> str:
return f"<Event {self.id} {self.title}>"
class EventRegistration(Base):
__tablename__ = "event_registrations"
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
event_id: Mapped[int] = mapped_column(Integer, ForeignKey("events.id"), nullable=False, index=True)
user_id: Mapped[int] = mapped_column(Integer, ForeignKey("users.id"), nullable=False, index=True)
status: Mapped[str] = mapped_column(String(20), default="registered")
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
event = relationship("Event", back_populates="registrations")
user = relationship("User", lazy="selectin")
def __repr__(self) -> str:
return f"<EventRegistration {self.id}>"
class EventPhoto(Base):
__tablename__ = "event_photos"
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
event_id: Mapped[int] = mapped_column(Integer, ForeignKey("events.id"), nullable=False, index=True)
uploader_id: Mapped[int] = mapped_column(Integer, ForeignKey("users.id"), nullable=False)
image_url: Mapped[str] = mapped_column(String(500), nullable=False)
caption: Mapped[str | None] = mapped_column(String(200))
spot_id: Mapped[int | None] = mapped_column(Integer, ForeignKey("spots.id"), nullable=True)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
event = relationship("Event", back_populates="photos")
uploader = relationship("User", lazy="selectin")
spot = relationship("Spot", lazy="selectin")
def __repr__(self) -> str:
return f"<EventPhoto {self.id}>"
+28
View File
@@ -0,0 +1,28 @@
from datetime import datetime
from sqlalchemy import DateTime, ForeignKey, Integer, UniqueConstraint, func
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.db.base import Base
class Favorite(Base):
__tablename__ = "favorites"
__table_args__ = (UniqueConstraint("user_id", "spot_id", name="uq_user_spot"),)
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
user_id: Mapped[int] = mapped_column(
Integer, ForeignKey("users.id"), nullable=False
)
spot_id: Mapped[int] = mapped_column(
Integer, ForeignKey("spots.id"), nullable=False
)
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now()
)
user = relationship("User", lazy="selectin")
spot = relationship("Spot", lazy="selectin")
def __repr__(self) -> str:
return f"<Favorite user={self.user_id} spot={self.spot_id}>"
+48
View File
@@ -0,0 +1,48 @@
from datetime import datetime
from sqlalchemy import (
Boolean, DateTime, ForeignKey, Integer, Numeric,
String, Text, func,
)
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.db.base import Base
class MembershipPlan(Base):
"""会员方案"""
__tablename__ = "membership_plans"
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
name: Mapped[str] = mapped_column(String(100), nullable=False)
description: Mapped[str | None] = mapped_column(Text)
duration_days: Mapped[int] = mapped_column(Integer, nullable=False)
price: Mapped[float] = mapped_column(Numeric(10, 2), nullable=False)
benefits: Mapped[str | None] = mapped_column(Text)
extra_uploads: Mapped[int] = mapped_column(Integer, default=0)
extra_top_count: Mapped[int] = mapped_column(Integer, default=0)
is_active: Mapped[bool] = mapped_column(Boolean, default=True)
sort_order: Mapped[int] = mapped_column(Integer, default=0)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
def __repr__(self) -> str:
return f"<MembershipPlan {self.id} {self.name}>"
class UserMembership(Base):
"""用户会员记录"""
__tablename__ = "user_memberships"
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
user_id: Mapped[int] = mapped_column(Integer, ForeignKey("users.id"), nullable=False, index=True)
plan_id: Mapped[int] = mapped_column(Integer, ForeignKey("membership_plans.id"), nullable=False)
start_date: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
end_date: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
is_active: Mapped[bool] = mapped_column(Boolean, default=True)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
user = relationship("User", lazy="selectin")
plan = relationship("MembershipPlan", lazy="selectin")
def __repr__(self) -> str:
return f"<UserMembership {self.id} user={self.user_id}>"
+25
View File
@@ -0,0 +1,25 @@
from datetime import datetime
from sqlalchemy import Boolean, DateTime, ForeignKey, Integer, String, Text, func
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.db.base import Base
class Notification(Base):
__tablename__ = "notifications"
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
user_id: Mapped[int] = mapped_column(Integer, ForeignKey("users.id"), nullable=False, index=True)
type: Mapped[str] = mapped_column(String(50), nullable=False, index=True)
title: Mapped[str] = mapped_column(String(200), nullable=False)
content: Mapped[str | None] = mapped_column(Text)
ref_type: Mapped[str | None] = mapped_column(String(50))
ref_id: Mapped[int | None] = mapped_column(Integer)
is_read: Mapped[bool] = mapped_column(Boolean, default=False)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
user = relationship("User", lazy="selectin")
def __repr__(self) -> str:
return f"<Notification {self.id} user={self.user_id} type={self.type}>"
+28
View File
@@ -0,0 +1,28 @@
from datetime import datetime
from sqlalchemy import DateTime, ForeignKey, Integer, String, func
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.db.base import Base
class PointLedger(Base):
__tablename__ = "point_ledger"
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
user_id: Mapped[int] = mapped_column(
Integer, ForeignKey("users.id"), nullable=False
)
change: Mapped[int] = mapped_column(Integer, nullable=False)
balance: Mapped[int] = mapped_column(Integer, nullable=False)
reason: Mapped[str] = mapped_column(String(200), nullable=False)
ref_type: Mapped[str | None] = mapped_column(String(50))
ref_id: Mapped[int | None] = mapped_column(Integer)
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now()
)
user = relationship("User", lazy="selectin")
def __repr__(self) -> str:
return f"<PointLedger {self.id} user={self.user_id} change={self.change}>"
+38
View File
@@ -0,0 +1,38 @@
from datetime import datetime
from sqlalchemy import (
DateTime, ForeignKey, Integer, String, Text, func,
)
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.db.base import Base
class Promotion(Base):
"""推广位/Banner"""
__tablename__ = "promotions"
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
title: Mapped[str] = mapped_column(String(200), nullable=False)
image_url: Mapped[str] = mapped_column(String(500), nullable=False)
link_type: Mapped[str] = mapped_column(String(20), default="spot")
link_id: Mapped[int | None] = mapped_column(Integer)
spot_id: Mapped[int | None] = mapped_column(Integer, ForeignKey("spots.id"), nullable=True)
event_id: Mapped[int | None] = mapped_column(Integer, ForeignKey("events.id"), nullable=True)
shooting_id: Mapped[int | None] = mapped_column(Integer, ForeignKey("shooting_requests.id"), nullable=True)
link_url: Mapped[str | None] = mapped_column(String(500))
position: Mapped[str] = mapped_column(String(30), default="home_banner", index=True)
sort_order: Mapped[int] = mapped_column(Integer, default=0)
start_time: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
end_time: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
is_active: Mapped[bool] = mapped_column(default=True, index=True)
impressions: Mapped[int] = mapped_column(Integer, default=0)
clicks: Mapped[int] = mapped_column(Integer, default=0)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
spot = relationship("Spot", lazy="selectin")
event = relationship("Event", lazy="selectin")
shooting = relationship("ShootingRequest", lazy="selectin")
def __repr__(self) -> str:
return f"<Promotion {self.id} {self.title}>"
+23
View File
@@ -0,0 +1,23 @@
from datetime import datetime
from sqlalchemy import DateTime, ForeignKey, Integer, String, UniqueConstraint, func
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.db.base import Base
class Rating(Base):
__tablename__ = "ratings"
__table_args__ = (UniqueConstraint("user_id", "spot_id", name="uq_user_spot_rating"),)
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
spot_id: Mapped[int] = mapped_column(Integer, ForeignKey("spots.id"), nullable=False, index=True)
user_id: Mapped[int] = mapped_column(Integer, ForeignKey("users.id"), nullable=False)
score: Mapped[int] = mapped_column(Integer, nullable=False)
short_comment: Mapped[str | None] = mapped_column(String(200))
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
user = relationship("User", lazy="selectin")
def __repr__(self) -> str:
return f"<Rating {self.id} spot={self.spot_id} score={self.score}>"
+27
View File
@@ -0,0 +1,27 @@
from datetime import datetime
from sqlalchemy import DateTime, ForeignKey, Integer, String, Text, func
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.db.base import Base
class Report(Base):
__tablename__ = "reports"
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
reporter_id: Mapped[int] = mapped_column(Integer, ForeignKey("users.id"), nullable=False)
target_type: Mapped[str] = mapped_column(String(50), nullable=False, index=True)
target_id: Mapped[int] = mapped_column(Integer, nullable=False)
reason: Mapped[str] = mapped_column(Text, nullable=False)
status: Mapped[str] = mapped_column(String(20), default="pending")
handler_id: Mapped[int | None] = mapped_column(Integer, ForeignKey("users.id"), nullable=True)
conclusion: Mapped[str | None] = mapped_column(Text)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
resolved_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
reporter = relationship("User", foreign_keys=[reporter_id], lazy="selectin")
handler = relationship("User", foreign_keys=[handler_id], lazy="selectin")
def __repr__(self) -> str:
return f"<Report {self.id} {self.target_type}:{self.target_id}>"
+57
View File
@@ -0,0 +1,57 @@
from datetime import datetime
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 ShootingRequest(Base):
__tablename__ = "shooting_requests"
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
creator_id: Mapped[int] = mapped_column(Integer, ForeignKey("users.id"), nullable=False, index=True)
title: Mapped[str] = mapped_column(String(200), nullable=False)
city: Mapped[str] = mapped_column(String(100), nullable=False, index=True)
description: Mapped[str | None] = mapped_column(Text)
style: Mapped[str | None] = mapped_column(String(100))
shoot_date: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
is_free: Mapped[bool] = mapped_column(Boolean, default=False)
budget_min: Mapped[float | None] = mapped_column(Numeric(10, 2))
budget_max: Mapped[float | None] = mapped_column(Numeric(10, 2))
role_needed: Mapped[str] = mapped_column(String(20), default="photographer")
max_applicants: Mapped[int] = mapped_column(Integer, default=1)
contact_info: Mapped[str | None] = mapped_column(String(200))
spot_id: Mapped[int | None] = mapped_column(Integer, ForeignKey("spots.id"), nullable=True)
status: Mapped[str] = mapped_column(String(20), default="open", index=True)
audit_status: Mapped[str] = mapped_column(String(20), default="pending")
reject_reason: Mapped[str | None] = mapped_column(String(500))
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")
spot = relationship("Spot", lazy="selectin")
applications = relationship("ShootingApplication", back_populates="request", lazy="selectin")
def __repr__(self) -> str:
return f"<ShootingRequest {self.id} {self.title}>"
class ShootingApplication(Base):
__tablename__ = "shooting_applications"
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
request_id: Mapped[int] = mapped_column(Integer, ForeignKey("shooting_requests.id"), nullable=False, index=True)
applicant_id: Mapped[int] = mapped_column(Integer, ForeignKey("users.id"), nullable=False, index=True)
message: Mapped[str | None] = mapped_column(Text)
status: Mapped[str] = mapped_column(String(20), default="pending")
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
request = relationship("ShootingRequest", back_populates="applications")
applicant = relationship("User", lazy="selectin")
def __repr__(self) -> str:
return f"<ShootingApplication {self.id} req={self.request_id}>"
+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}>"
+25
View File
@@ -0,0 +1,25 @@
from datetime import datetime
from sqlalchemy import Boolean, DateTime, Integer, String, Text, func
from sqlalchemy.orm import Mapped, mapped_column
from app.db.base import Base
class SystemConfig(Base):
__tablename__ = "system_configs"
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
config_key: Mapped[str] = mapped_column(String(100), nullable=False, unique=True, index=True)
category: Mapped[str] = mapped_column(String(50), nullable=False, index=True)
title: Mapped[str] = mapped_column(String(200), nullable=False)
config_json: Mapped[str] = mapped_column(Text, nullable=False, default="{}")
description: Mapped[str | None] = mapped_column(Text)
is_active: Mapped[bool] = mapped_column(Boolean, default=True, index=True)
sort_order: Mapped[int] = mapped_column(Integer, default=0)
updated_by: Mapped[int | None] = mapped_column(Integer)
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())
def __repr__(self) -> str:
return f"<SystemConfig {self.config_key}>"
+30
View File
@@ -0,0 +1,30 @@
from datetime import datetime
from sqlalchemy import Boolean, DateTime, ForeignKey, Integer, String, UniqueConstraint, func
from sqlalchemy.orm import Mapped, mapped_column
from app.db.base import Base
class Tag(Base):
__tablename__ = "tags"
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
name: Mapped[str] = mapped_column(String(50), unique=True, nullable=False)
category: Mapped[str | None] = mapped_column(String(50))
is_active: Mapped[bool] = mapped_column(Boolean, default=True)
usage_count: Mapped[int] = mapped_column(Integer, default=0)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
def __repr__(self) -> str:
return f"<Tag {self.id} {self.name}>"
class SpotTag(Base):
__tablename__ = "spot_tags"
__table_args__ = (UniqueConstraint("spot_id", "tag_id", name="uq_spot_tag"),)
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
spot_id: Mapped[int] = mapped_column(Integer, ForeignKey("spots.id"), nullable=False)
tag_id: Mapped[int] = mapped_column(Integer, ForeignKey("tags.id"), nullable=False)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
+31
View File
@@ -0,0 +1,31 @@
from datetime import datetime
from sqlalchemy import Boolean, DateTime, String, Text, func
from sqlalchemy.orm import Mapped, mapped_column
from app.db.base import Base
class User(Base):
__tablename__ = "users"
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
phone: Mapped[str | None] = mapped_column(String(20), unique=True)
email: Mapped[str | None] = mapped_column(String(255), unique=True)
password_hash: Mapped[str] = mapped_column(String(255), nullable=False)
nickname: Mapped[str] = mapped_column(String(50), nullable=False)
avatar_url: Mapped[str | None] = mapped_column(String(500))
city: Mapped[str | None] = mapped_column(String(100))
bio: Mapped[str | None] = mapped_column(Text, nullable=True)
identity: Mapped[str] = mapped_column(String(20), default="both")
role: Mapped[str] = mapped_column(String(20), default="user")
is_active: Mapped[bool] = mapped_column(Boolean, default=True)
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()
)
def __repr__(self) -> str:
return f"<User {self.id} {self.nickname}>"