feat: add demand page modules and admin endpoints

Add full backend support for a demand request page, including:
- New database models for demand hero, feature cards, form configuration, and product recommendations
- Alembic migration for the new tables
- Default seed data for demand page components
- Public site config endpoint exposing demand data
- Admin CRUD operations and configuration for demand modules
- Extended lead query filters for source page, keyword, and date range
- Updated Pydantic schemas for lead query parameters
- Comprehensive test coverage for all new endpoints
This commit is contained in:
duanshuwen
2026-07-07 22:03:57 +08:00
parent d11c361aa2
commit fe85197e68
9 changed files with 749 additions and 15 deletions

View File

@@ -207,6 +207,7 @@ class Product(Base):
images: Mapped[list["ProductImage"]] = relationship(back_populates="product", cascade="all, delete-orphan")
campaignLinks: Mapped[list["CampaignProduct"]] = relationship(back_populates="product", cascade="all, delete-orphan")
routeSectionLinks: Mapped[list["RouteSectionProduct"]] = relationship(back_populates="product", cascade="all, delete-orphan")
demandRecommendationLinks: Mapped[list["DemandRecommendationProduct"]] = relationship(back_populates="product", cascade="all, delete-orphan")
leads: Mapped[list["Lead"]] = relationship(back_populates="sourceProduct")
orders: Mapped[list["Order"]] = relationship(back_populates="product")
@@ -279,6 +280,74 @@ class RouteSectionProduct(Base):
product: Mapped[Product] = relationship(back_populates="routeSectionLinks")
class DemandHero(Base):
__tablename__ = "DemandHero"
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)
description: Mapped[str | None] = mapped_column(Text)
steps: Mapped[list[str]] = mapped_column(ARRAY(String), default=list, 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)
class DemandFeatureCard(Base):
__tablename__ = "DemandFeatureCard"
id: Mapped[str] = mapped_column(String, primary_key=True, default=new_id)
title: Mapped[str] = mapped_column(String, nullable=False)
description: Mapped[str | None] = mapped_column(Text)
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 DemandForm(Base):
__tablename__ = "DemandForm"
id: Mapped[str] = mapped_column(String, primary_key=True, default=new_id)
destinationLabel: Mapped[str] = mapped_column(String, default="目的地/玩法", nullable=False)
destinationPlaceholder: Mapped[str | None] = mapped_column(String)
phoneLabel: Mapped[str] = mapped_column(String, default="联系方式", nullable=False)
phonePlaceholder: Mapped[str | None] = mapped_column(String)
noteLabel: Mapped[str] = mapped_column(String, default="补充说明", nullable=False)
notePlaceholder: Mapped[str | None] = mapped_column(String)
submitLabel: Mapped[str] = mapped_column(String, default="提交出行需求", nullable=False)
chips: Mapped[list[str]] = mapped_column(ARRAY(String), default=list, 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 DemandRecommendation(Base):
__tablename__ = "DemandRecommendation"
id: Mapped[str] = mapped_column(String, primary_key=True, default=new_id)
title: Mapped[str] = mapped_column(String, nullable=False)
subtitle: Mapped[str | None] = mapped_column(Text)
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)
products: Mapped[list["DemandRecommendationProduct"]] = relationship(back_populates="recommendation", cascade="all, delete-orphan")
class DemandRecommendationProduct(Base):
__tablename__ = "DemandRecommendationProduct"
recommendationId: Mapped[str] = mapped_column(String, ForeignKey("DemandRecommendation.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)
recommendation: Mapped[DemandRecommendation] = relationship(back_populates="products")
product: Mapped[Product] = relationship(back_populates="demandRecommendationLinks")
class Lead(Base):
__tablename__ = "Lead"