feat(route-sections): add dynamic route sections

Replace hardcoded homepage featured route groups with a fully managed dynamic system:
- add database models RouteSection and RouteSectionProduct for storing route groups and their associated products
- create Alembic migration 0004_route_sections for the new tables
- extend SiteConfigPatchIn schema with subtitle and productIds fields
- refactor shared site_config utility to load dynamic route sections instead of fixed groups
- implement admin CRUD API with validation for duplicate/conflicting product associations
- update public and admin API documentation to reflect the new system
- add default route section seed data and comprehensive test coverage
This commit is contained in:
duanshuwen
2026-07-02 23:05:36 +08:00
parent 521e501992
commit 72d388a047
10 changed files with 610 additions and 43 deletions

View File

@@ -149,6 +149,7 @@ class Product(Base):
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")
routeSectionLinks: Mapped[list["RouteSectionProduct"]] = relationship(back_populates="product", cascade="all, delete-orphan")
leads: Mapped[list["Lead"]] = relationship(back_populates="sourceProduct")
orders: Mapped[list["Order"]] = relationship(back_populates="product")
@@ -196,6 +197,31 @@ class CampaignProduct(Base):
product: Mapped[Product] = relationship(back_populates="campaignLinks")
class RouteSection(Base):
__tablename__ = "RouteSection"
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["RouteSectionProduct"]] = relationship(back_populates="section", cascade="all, delete-orphan")
class RouteSectionProduct(Base):
__tablename__ = "RouteSectionProduct"
sectionId: Mapped[str] = mapped_column(String, ForeignKey("RouteSection.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)
section: Mapped[RouteSection] = relationship(back_populates="products")
product: Mapped[Product] = relationship(back_populates="routeSectionLinks")
class Lead(Base):
__tablename__ = "Lead"