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

@@ -8,13 +8,18 @@ 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, DESTINATION_HERO, DESTINATION_REGIONS, DESTINATIONS, HERO_SLIDES, HOTEL_GROUPS, THEMES, VEHICLE_OPTIONS
from .content import ALIASES, CAMPAIGNS, CTAS, DEMAND_FEATURE_CARDS, DEMAND_FORM, DEMAND_HERO, DEMAND_RECOMMENDATIONS, DESTINATION_HERO, DESTINATION_REGIONS, DESTINATIONS, HERO_SLIDES, HOTEL_GROUPS, THEMES, VEHICLE_OPTIONS
from .database import Base, SessionLocal, engine
from .models import (
AdminUser,
Campaign,
CampaignProduct,
CtaBanner,
DemandFeatureCard,
DemandForm,
DemandHero,
DemandRecommendation,
DemandRecommendationProduct,
Destination,
DestinationAlias,
DestinationHero,
@@ -104,7 +109,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, DestinationRegion, DestinationHero, HeroSlide, DestinationAlias, Destination, MediaAsset]:
for model in [SiteVersion, DemandRecommendationProduct, DemandRecommendation, DemandForm, DemandFeatureCard, DemandHero, CampaignProduct, RouteSectionProduct, Campaign, RouteSection, ProductImage, Product, VehicleOption, HotelGroup, CtaBanner, ThemeCard, DestinationRegion, DestinationHero, HeroSlide, DestinationAlias, Destination, MediaAsset]:
db.execute(delete(model))
db.flush()
@@ -188,6 +193,29 @@ def reset_guizhou_content(db: Session) -> dict:
sortOrder=index,
)
)
for index, item in enumerate(DEMAND_HERO):
db.add(
DemandHero(
title=item["title"],
kicker=item.get("kicker"),
description=item.get("description"),
steps=item.get("steps", []),
sortOrder=index,
)
)
for index, item in enumerate(DEMAND_FEATURE_CARDS):
db.add(
DemandFeatureCard(
title=item["title"],
description=item.get("description"),
sortOrder=index,
)
)
db.add(DemandForm(**DEMAND_FORM))
for product in products:
create_media(db, product.get("image"), "product", product["title"])
matched_destination = product.get("destinationName") if product.get("destinationName") in destination_map else None
@@ -245,6 +273,22 @@ def reset_guizhou_content(db: Session) -> dict:
for index, product in enumerate(section_products):
db.add(RouteSectionProduct(sectionId=section.id, productId=product.id, sortOrder=index))
for sort_order, seed in enumerate(DEMAND_RECOMMENDATIONS):
recommendation = DemandRecommendation(
title=seed["title"],
subtitle=seed.get("subtitle"),
sortOrder=sort_order,
)
db.add(recommendation)
db.flush()
linked_products = db.scalars(
select(Product)
.where(Product.sourceId >= seed["start"] + 1, Product.sourceId <= seed["end"])
.order_by(Product.sourceId.asc())
).all()
for index, product in enumerate(linked_products):
db.add(DemandRecommendationProduct(recommendationId=recommendation.id, productId=product.id, sortOrder=index))
snapshot = SiteVersion(
title="guizhou-content-reset",
status="published",
@@ -259,6 +303,10 @@ def reset_guizhou_content(db: Session) -> dict:
"routeSections": len(ROUTE_SECTION_DEFAULTS),
"hotelGroups": len(HOTEL_GROUPS),
"vehicleOptions": len(VEHICLE_OPTIONS),
"demandHero": len(DEMAND_HERO),
"demandFeatureCards": len(DEMAND_FEATURE_CARDS),
"demandForm": 1,
"demandRecommendations": len(DEMAND_RECOMMENDATIONS),
},
)
db.add(snapshot)
@@ -274,6 +322,10 @@ def reset_guizhou_content(db: Session) -> dict:
"routeSections": len(ROUTE_SECTION_DEFAULTS),
"hotelGroups": len(HOTEL_GROUPS),
"vehicleOptions": len(VEHICLE_OPTIONS),
"demandHero": len(DEMAND_HERO),
"demandFeatureCards": len(DEMAND_FEATURE_CARDS),
"demandForm": 1,
"demandRecommendations": len(DEMAND_RECOMMENDATIONS),
"siteVersionId": snapshot.id,
}