Files
WonderQ-Admin/app/models.py
2026-07-02 08:57:49 +08:00

286 lines
14 KiB
Python

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")