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

@@ -21,10 +21,13 @@ from .models import (
MediaAsset,
Product,
ProductImage,
RouteSection,
RouteSectionProduct,
SiteVersion,
ThemeCard,
utc_now,
)
from .route_sections import ROUTE_SECTION_DEFAULTS
ROOT_DIR = Path(__file__).resolve().parent.parent
@@ -97,7 +100,7 @@ def load_products() -> list[dict]:
def reset_guizhou_content(db: Session) -> dict:
products = load_products()
for model in [SiteVersion, CampaignProduct, Campaign, ProductImage, Product, CtaBanner, ThemeCard, HeroSlide, DestinationAlias, Destination, MediaAsset]:
for model in [SiteVersion, CampaignProduct, RouteSectionProduct, Campaign, RouteSection, ProductImage, Product, CtaBanner, ThemeCard, HeroSlide, DestinationAlias, Destination, MediaAsset]:
db.execute(delete(model))
db.flush()
@@ -178,6 +181,18 @@ def reset_guizhou_content(db: Session) -> dict:
for index, product in enumerate(linked_products):
db.add(CampaignProduct(campaignId=campaign.id, productId=product.id, sortOrder=index))
for sort_order, (section_id, title, subtitle, start, end) in enumerate(ROUTE_SECTION_DEFAULTS):
section = RouteSection(id=section_id, title=title, subtitle=subtitle, sortOrder=sort_order)
db.add(section)
db.flush()
section_products = db.scalars(
select(Product)
.where(Product.sourceId >= start + 1, Product.sourceId <= end)
.order_by(Product.sourceId.asc())
).all()
for index, product in enumerate(section_products):
db.add(RouteSectionProduct(sectionId=section.id, productId=product.id, sortOrder=index))
snapshot = SiteVersion(
title="guizhou-content-reset",
status="published",
@@ -187,6 +202,7 @@ def reset_guizhou_content(db: Session) -> dict:
"destinations": len(DESTINATIONS),
"themeCards": len(THEMES),
"products": len(products),
"routeSections": len(ROUTE_SECTION_DEFAULTS),
},
)
db.add(snapshot)
@@ -197,6 +213,7 @@ def reset_guizhou_content(db: Session) -> dict:
"themes": len(THEMES),
"ctaBanners": len(CTAS),
"products": len(products),
"routeSections": len(ROUTE_SECTION_DEFAULTS),
"siteVersionId": snapshot.id,
}