feat: add destination page configurable modules

This commit adds full support for two new destination page modules: a hero banner section and categorized destination regions.

Changes include:
- New SQLAlchemy models and alembic migration for DestinationHero and DestinationRegion tables
- Updated Pydantic SiteConfigPatchIn schema with new optional fields
- Integrated admin CRUD support for the new modules
- Added default seed data for Guizhou destination regions and hero content
- Exposed active modules via public site config API
- Added comprehensive test coverage for admin and public endpoints
This commit is contained in:
duanshuwen
2026-07-07 21:24:30 +08:00
parent 83906f481d
commit d11c361aa2
8 changed files with 262 additions and 13 deletions

View File

@@ -8,7 +8,7 @@ from urllib.parse import quote
from sqlalchemy import delete, select
from sqlalchemy.orm import Session
from .auth import hash_password
from .content import ALIASES, CAMPAIGNS, CTAS, DESTINATIONS, HERO_SLIDES, HOTEL_GROUPS, THEMES, VEHICLE_OPTIONS
from .content import ALIASES, CAMPAIGNS, CTAS, DESTINATION_HERO, DESTINATION_REGIONS, DESTINATIONS, HERO_SLIDES, HOTEL_GROUPS, THEMES, VEHICLE_OPTIONS
from .database import Base, SessionLocal, engine
from .models import (
AdminUser,
@@ -17,6 +17,8 @@ from .models import (
CtaBanner,
Destination,
DestinationAlias,
DestinationHero,
DestinationRegion,
HeroSlide,
HotelGroup,
MediaAsset,
@@ -102,7 +104,7 @@ def load_products() -> list[dict]:
def reset_guizhou_content(db: Session) -> dict:
products = load_products()
for model in [SiteVersion, CampaignProduct, RouteSectionProduct, Campaign, RouteSection, ProductImage, Product, VehicleOption, HotelGroup, CtaBanner, ThemeCard, HeroSlide, DestinationAlias, Destination, MediaAsset]:
for model in [SiteVersion, CampaignProduct, RouteSectionProduct, Campaign, RouteSection, ProductImage, Product, VehicleOption, HotelGroup, CtaBanner, ThemeCard, DestinationRegion, DestinationHero, HeroSlide, DestinationAlias, Destination, MediaAsset]:
db.execute(delete(model))
db.flush()
@@ -130,6 +132,27 @@ def reset_guizhou_content(db: Session) -> dict:
for alias in ALIASES.get(name, []):
db.add(DestinationAlias(destinationId=destination.id, alias=alias))
for index, item in enumerate(DESTINATION_HERO):
create_media(db, item.get("image"), "destination-hero", item["title"])
db.add(
DestinationHero(
title=item["title"],
kicker=item.get("kicker"),
image=item.get("image"),
sortOrder=index,
)
)
for index, item in enumerate(DESTINATION_REGIONS):
db.add(
DestinationRegion(
label=item["label"],
keyword=item.get("keyword"),
spots=item.get("spots"),
sortOrder=index,
)
)
for index, (label, image) in enumerate(THEMES):
create_media(db, image, "theme", label)
db.add(ThemeCard(label=label, image=image, targetType="search", targetValue=label, sortOrder=index))
@@ -229,6 +252,8 @@ def reset_guizhou_content(db: Session) -> dict:
snapshot={
"heroSlides": len(HERO_SLIDES),
"destinations": len(DESTINATIONS),
"destinationHero": len(DESTINATION_HERO),
"destinationRegions": len(DESTINATION_REGIONS),
"themeCards": len(THEMES),
"products": len(products),
"routeSections": len(ROUTE_SECTION_DEFAULTS),
@@ -241,6 +266,8 @@ def reset_guizhou_content(db: Session) -> dict:
return {
"heroSlides": len(HERO_SLIDES),
"destinations": len(DESTINATIONS),
"destinationHero": len(DESTINATION_HERO),
"destinationRegions": len(DESTINATION_REGIONS),
"themes": len(THEMES),
"ctaBanners": len(CTAS),
"products": len(products),