from datetime import datetime, timezone from uuid import uuid4 from sqlalchemy import Boolean, DateTime, ForeignKey, Integer, String, Text, UniqueConstraint from sqlalchemy.dialects.postgresql import ARRAY, JSONB from sqlalchemy.orm import Mapped, mapped_column, relationship from .database import Base def utc_now() -> datetime: return datetime.now(timezone.utc).replace(tzinfo=None) def new_id() -> str: return str(uuid4()) class AdminUser(Base): __tablename__ = "AdminUser" id: Mapped[str] = mapped_column(String, primary_key=True, default=new_id) email: Mapped[str] = mapped_column(String, unique=True, nullable=False) name: Mapped[str] = mapped_column(String, nullable=False) passwordHash: Mapped[str] = mapped_column(String, nullable=False) role: Mapped[str] = mapped_column(String, default="admin", nullable=False) isActive: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False) createdAt: Mapped[datetime] = mapped_column(DateTime, default=utc_now, nullable=False) updatedAt: Mapped[datetime] = mapped_column(DateTime, default=utc_now, onupdate=utc_now, nullable=False) auditLogs: Mapped[list["AuditLog"]] = relationship(back_populates="actor") assignedLeads: Mapped[list["Lead"]] = relationship(back_populates="assignedUser", foreign_keys="Lead.assignedUserId") class MediaAsset(Base): __tablename__ = "MediaAsset" id: Mapped[str] = mapped_column(String, primary_key=True, default=new_id) url: Mapped[str] = mapped_column(String, unique=True, nullable=False) name: Mapped[str | None] = mapped_column(String) mimeType: Mapped[str | None] = mapped_column(String) sizeBytes: Mapped[int | None] = mapped_column(Integer) group: Mapped[str | None] = mapped_column(String) createdAt: Mapped[datetime] = mapped_column(DateTime, default=utc_now, nullable=False) updatedAt: Mapped[datetime] = mapped_column(DateTime, default=utc_now, onupdate=utc_now, nullable=False) class HeroSlide(Base): __tablename__ = "HeroSlide" id: Mapped[str] = mapped_column(String, primary_key=True, default=new_id) title: Mapped[str] = mapped_column(String, nullable=False) kicker: Mapped[str | None] = mapped_column(String) actionLabel: Mapped[str | None] = mapped_column(String) image: Mapped[str] = mapped_column(String, nullable=False) targetType: Mapped[str | None] = mapped_column(String) targetValue: Mapped[str | None] = mapped_column(String) sortOrder: Mapped[int] = mapped_column(Integer, default=0, nullable=False) isActive: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False) createdAt: Mapped[datetime] = mapped_column(DateTime, default=utc_now, nullable=False) updatedAt: Mapped[datetime] = mapped_column(DateTime, default=utc_now, onupdate=utc_now, nullable=False) class Destination(Base): __tablename__ = "Destination" id: Mapped[str] = mapped_column(String, primary_key=True, default=new_id) name: Mapped[str] = mapped_column(String, unique=True, nullable=False) slug: Mapped[str] = mapped_column(String, unique=True, nullable=False) region: Mapped[str | None] = mapped_column(String) image: Mapped[str | None] = mapped_column(String) isHot: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False) sortOrder: Mapped[int] = mapped_column(Integer, default=0, nullable=False) isActive: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False) createdAt: Mapped[datetime] = mapped_column(DateTime, default=utc_now, nullable=False) updatedAt: Mapped[datetime] = mapped_column(DateTime, default=utc_now, onupdate=utc_now, nullable=False) aliases: Mapped[list["DestinationAlias"]] = relationship(back_populates="destination", cascade="all, delete-orphan") products: Mapped[list["Product"]] = relationship(back_populates="destination") class DestinationAlias(Base): __tablename__ = "DestinationAlias" __table_args__ = (UniqueConstraint("alias", "destinationId"),) id: Mapped[str] = mapped_column(String, primary_key=True, default=new_id) alias: Mapped[str] = mapped_column(String, nullable=False) destinationId: Mapped[str] = mapped_column(String, ForeignKey("Destination.id", ondelete="CASCADE"), nullable=False) destination: Mapped[Destination] = relationship(back_populates="aliases") class MapImage(Base): __tablename__ = "MapImage" id: Mapped[str] = mapped_column(String, primary_key=True, default=new_id) image: Mapped[str] = mapped_column(String, nullable=False) isActive: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False) createdAt: Mapped[datetime] = mapped_column(DateTime, default=utc_now, nullable=False) updatedAt: Mapped[datetime] = mapped_column(DateTime, default=utc_now, onupdate=utc_now, nullable=False) class ThemeCard(Base): __tablename__ = "ThemeCard" id: Mapped[str] = mapped_column(String, primary_key=True, default=new_id) label: Mapped[str] = mapped_column(String, nullable=False) image: Mapped[str] = mapped_column(String, nullable=False) targetType: Mapped[str | None] = mapped_column(String) targetValue: Mapped[str | None] = mapped_column(String) sortOrder: Mapped[int] = mapped_column(Integer, default=0, nullable=False) isActive: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False) createdAt: Mapped[datetime] = mapped_column(DateTime, default=utc_now, nullable=False) updatedAt: Mapped[datetime] = mapped_column(DateTime, default=utc_now, onupdate=utc_now, nullable=False) class CtaBanner(Base): __tablename__ = "CtaBanner" id: Mapped[str] = mapped_column(String, primary_key=True, default=new_id) alt: Mapped[str] = mapped_column(String, nullable=False) image: Mapped[str] = mapped_column(String, nullable=False) targetType: Mapped[str] = mapped_column(String, nullable=False) targetValue: Mapped[str | None] = mapped_column(String) sortOrder: Mapped[int] = mapped_column(Integer, default=0, nullable=False) isActive: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False) createdAt: Mapped[datetime] = mapped_column(DateTime, default=utc_now, nullable=False) updatedAt: Mapped[datetime] = mapped_column(DateTime, default=utc_now, onupdate=utc_now, nullable=False) class Product(Base): __tablename__ = "Product" id: Mapped[str] = mapped_column(String, primary_key=True, default=new_id) sourceId: Mapped[int | None] = mapped_column(Integer, unique=True) title: Mapped[str] = mapped_column(String, nullable=False) subtitle: Mapped[str | None] = mapped_column(String) destinationId: Mapped[str | None] = mapped_column(String, ForeignKey("Destination.id", ondelete="SET NULL")) priceAmount: Mapped[int | None] = mapped_column(Integer) priceUnit: Mapped[str] = mapped_column(String, default="起/人", nullable=False) tags: Mapped[list[str]] = mapped_column(ARRAY(String), default=list, nullable=False) coverImage: Mapped[str | None] = mapped_column(String) summary: Mapped[str | None] = mapped_column(Text) detailSections: Mapped[dict | list | None] = mapped_column(JSONB) status: Mapped[str] = mapped_column(String, default="published", nullable=False) sortWeight: Mapped[int] = mapped_column(Integer, default=0, nullable=False) publishedAt: Mapped[datetime | None] = mapped_column(DateTime) createdAt: Mapped[datetime] = mapped_column(DateTime, default=utc_now, nullable=False) updatedAt: Mapped[datetime] = mapped_column(DateTime, default=utc_now, onupdate=utc_now, nullable=False) destination: Mapped[Destination | None] = relationship(back_populates="products") images: Mapped[list["ProductImage"]] = relationship(back_populates="product", cascade="all, delete-orphan") campaignLinks: Mapped[list["CampaignProduct"]] = relationship(back_populates="product", cascade="all, delete-orphan") leads: Mapped[list["Lead"]] = relationship(back_populates="sourceProduct") orders: Mapped[list["Order"]] = relationship(back_populates="product") class ProductImage(Base): __tablename__ = "ProductImage" id: Mapped[str] = mapped_column(String, primary_key=True, default=new_id) productId: Mapped[str] = mapped_column(String, ForeignKey("Product.id", ondelete="CASCADE"), nullable=False) url: Mapped[str] = mapped_column(String, nullable=False) alt: Mapped[str | None] = mapped_column(String) sortOrder: Mapped[int] = mapped_column(Integer, default=0, nullable=False) product: Mapped[Product] = relationship(back_populates="images") class Campaign(Base): __tablename__ = "Campaign" id: Mapped[str] = mapped_column(String, primary_key=True, default=new_id) slug: Mapped[str] = mapped_column(String, unique=True, nullable=False) title: Mapped[str] = mapped_column(String, nullable=False) description: Mapped[str | None] = mapped_column(Text) coverImage: Mapped[str | None] = mapped_column(String) status: Mapped[str] = mapped_column(String, default="published", nullable=False) startsAt: Mapped[datetime | None] = mapped_column(DateTime) endsAt: Mapped[datetime | None] = mapped_column(DateTime) createdAt: Mapped[datetime] = mapped_column(DateTime, default=utc_now, nullable=False) updatedAt: Mapped[datetime] = mapped_column(DateTime, default=utc_now, onupdate=utc_now, nullable=False) products: Mapped[list["CampaignProduct"]] = relationship(back_populates="campaign", cascade="all, delete-orphan") class CampaignProduct(Base): __tablename__ = "CampaignProduct" campaignId: Mapped[str] = mapped_column(String, ForeignKey("Campaign.id", ondelete="CASCADE"), primary_key=True) productId: Mapped[str] = mapped_column(String, ForeignKey("Product.id", ondelete="CASCADE"), primary_key=True) sortOrder: Mapped[int] = mapped_column(Integer, default=0, nullable=False) campaign: Mapped[Campaign] = relationship(back_populates="products") product: Mapped[Product] = relationship(back_populates="campaignLinks") class Lead(Base): __tablename__ = "Lead" id: Mapped[str] = mapped_column(String, primary_key=True, default=new_id) destination: Mapped[str | None] = mapped_column(String) phone: Mapped[str] = mapped_column(String, nullable=False) travelDate: Mapped[datetime | None] = mapped_column(DateTime) peopleCount: Mapped[int | None] = mapped_column(Integer) budgetMin: Mapped[int | None] = mapped_column(Integer) budgetMax: Mapped[int | None] = mapped_column(Integer) note: Mapped[str | None] = mapped_column(Text) sourcePage: Mapped[str | None] = mapped_column(String) sourceProductId: Mapped[str | None] = mapped_column(String, ForeignKey("Product.id", ondelete="SET NULL")) status: Mapped[str] = mapped_column(String, default="new", nullable=False) assignedUserId: Mapped[str | None] = mapped_column(String, ForeignKey("AdminUser.id", ondelete="SET NULL")) createdAt: Mapped[datetime] = mapped_column(DateTime, default=utc_now, nullable=False) updatedAt: Mapped[datetime] = mapped_column(DateTime, default=utc_now, onupdate=utc_now, nullable=False) sourceProduct: Mapped[Product | None] = relationship(back_populates="leads") assignedUser: Mapped[AdminUser | None] = relationship(back_populates="assignedLeads", foreign_keys=[assignedUserId]) followups: Mapped[list["LeadFollowup"]] = relationship(back_populates="lead", cascade="all, delete-orphan") class LeadFollowup(Base): __tablename__ = "LeadFollowup" id: Mapped[str] = mapped_column(String, primary_key=True, default=new_id) leadId: Mapped[str] = mapped_column(String, ForeignKey("Lead.id", ondelete="CASCADE"), nullable=False) content: Mapped[str] = mapped_column(Text, nullable=False) nextAt: Mapped[datetime | None] = mapped_column(DateTime) createdAt: Mapped[datetime] = mapped_column(DateTime, default=utc_now, nullable=False) lead: Mapped[Lead] = relationship(back_populates="followups") class Customer(Base): __tablename__ = "Customer" id: Mapped[str] = mapped_column(String, primary_key=True, default=new_id) phone: Mapped[str] = mapped_column(String, unique=True, nullable=False) name: Mapped[str | None] = mapped_column(String) wechat: Mapped[str | None] = mapped_column(String) note: Mapped[str | None] = mapped_column(Text) createdAt: Mapped[datetime] = mapped_column(DateTime, default=utc_now, nullable=False) updatedAt: Mapped[datetime] = mapped_column(DateTime, default=utc_now, onupdate=utc_now, nullable=False) orders: Mapped[list["Order"]] = relationship(back_populates="customer") class Order(Base): __tablename__ = "Order" id: Mapped[str] = mapped_column(String, primary_key=True, default=new_id) customerId: Mapped[str | None] = mapped_column(String, ForeignKey("Customer.id", ondelete="SET NULL")) productId: Mapped[str | None] = mapped_column(String, ForeignKey("Product.id", ondelete="SET NULL")) status: Mapped[str] = mapped_column(String, default="draft", nullable=False) travelDate: Mapped[datetime | None] = mapped_column(DateTime) amount: Mapped[int | None] = mapped_column(Integer) note: Mapped[str | None] = mapped_column(Text) createdAt: Mapped[datetime] = mapped_column(DateTime, default=utc_now, nullable=False) updatedAt: Mapped[datetime] = mapped_column(DateTime, default=utc_now, onupdate=utc_now, nullable=False) customer: Mapped[Customer | None] = relationship(back_populates="orders") product: Mapped[Product | None] = relationship(back_populates="orders") class SiteVersion(Base): __tablename__ = "SiteVersion" id: Mapped[str] = mapped_column(String, primary_key=True, default=new_id) title: Mapped[str] = mapped_column(String, nullable=False) status: Mapped[str] = mapped_column(String, default="draft", nullable=False) snapshot: Mapped[dict | list] = mapped_column(JSONB, nullable=False) publishedAt: Mapped[datetime | None] = mapped_column(DateTime) createdAt: Mapped[datetime] = mapped_column(DateTime, default=utc_now, nullable=False) class AuditLog(Base): __tablename__ = "AuditLog" id: Mapped[str] = mapped_column(String, primary_key=True, default=new_id) actorId: Mapped[str | None] = mapped_column(String, ForeignKey("AdminUser.id", ondelete="SET NULL")) action: Mapped[str] = mapped_column(String, nullable=False) entity: Mapped[str] = mapped_column(String, nullable=False) entityId: Mapped[str | None] = mapped_column(String) before: Mapped[dict | list | None] = mapped_column(JSONB) after: Mapped[dict | list | None] = mapped_column(JSONB) createdAt: Mapped[datetime] = mapped_column(DateTime, default=utc_now, nullable=False) actor: Mapped[AdminUser | None] = relationship(back_populates="auditLogs")