from __future__ import annotations import argparse import json import re from pathlib import Path 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, 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, DestinationRegion, HeroSlide, HotelGroup, MediaAsset, Product, ProductImage, RouteSection, RouteSectionProduct, SiteVersion, ThemeCard, VehicleOption, utc_now, ) from .route_sections import ROUTE_SECTION_DEFAULTS ROOT_DIR = Path(__file__).resolve().parent.parent def slugify(value: str) -> str: return quote(value, safe="").replace("%", "").lower() def create_media(db: Session, url: str | None, group: str, name: str | None = None) -> None: if not url: return media = db.scalar(select(MediaAsset).where(MediaAsset.url == url)) if media: media.group = group media.name = name else: db.add(MediaAsset(url=url, group=group, name=name)) def default_detail_sections(summary: str, location: str) -> list[dict]: return [ { "key": "overview", "label": "行程概述", "title": "小包团专属概览", "blocks": [{"type": "text", "text": f"{summary}。万趣会按同行人、预算、酒店偏好和体力强度重排细节,保留小车小团、错峰入园与在地向导服务。"}], }, { "key": "itinerary", "label": "每日行程", "title": f"{location} 弹性安排", "blocks": [{"type": "text", "text": "默认按抵达接站、核心景点游览、特色体验、酒店休整和返程送站安排每日节奏;具体天数、停留时长和餐食可在行前由服务管家二次确认。"}], }, { "key": "service", "label": "包含/不含服务", "title": "费用边界清晰", "blocks": [{"type": "text", "text": "通常包含当地用车、行程内住宿、列明门票/体验、必要讲解和服务管家跟进;大交通、个人消费、未列明餐食和自选项目以最终方案为准。"}], }, { "key": "notice", "label": "出行须知", "title": "贵州山地旅行提示", "blocks": [{"type": "text", "text": "贵州多山多雨,建议准备防滑鞋、轻便雨具和薄外套;溶洞、漂流、徒步等体验会按天气和同行人体力调整。"}], }, { "key": "price", "label": "价格区间", "title": "按人数、酒店和季节报价", "blocks": [{"type": "text", "text": "页面价格为参考起价,节假日、旺季房态、用车车型和体验资源会影响最终报价;提交需求后由服务管家给出可执行方案。"}], }, { "key": "manager", "label": "服务管家", "title": "直接添加服务管家", "blocks": [{"type": "text", "text": "点击底部“服务管家”或拨打 18786174929,可直接添加服务管家沟通出行人数、日期、酒店偏好和预算。"}], }, ] def product_subtitle(title: str) -> str: return re.sub(r"^【.*?】\s*", "", title).split("·")[0] def load_products() -> list[dict]: with (ROOT_DIR / "data" / "generated-products.json").open(encoding="utf-8") as handle: return json.load(handle) def reset_guizhou_content(db: Session) -> dict: products = load_products() 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() for index, slide in enumerate(HERO_SLIDES): create_media(db, slide["image"], "hero", slide["title"]) db.add( HeroSlide( title=slide["title"], kicker=slide["kicker"], actionLabel=slide["action"], image=slide["image"], targetType="campaign", targetValue=slide["targetValue"], sortOrder=index, ) ) destination_map: dict[str, str] = {} for index, (name, image) in enumerate(DESTINATIONS): create_media(db, image, "destination", name) destination = Destination(name=name, slug=slugify(name), image=image, isHot=index < 8, sortOrder=index) db.add(destination) db.flush() destination_map[name] = destination.id 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)) for index, (alt, image, target_type, target_value) in enumerate(CTAS): create_media(db, image, "cta", alt) db.add(CtaBanner(alt=alt, image=image, targetType=target_type, targetValue=target_value, sortOrder=index)) for index, group in enumerate(HOTEL_GROUPS): create_media(db, group["image"], "hotel-group", group["title"]) db.add( HotelGroup( title=group["title"], description=group.get("description"), image=group.get("image"), coverImage=group.get("coverImage") or group.get("image"), priceAmount=group.get("priceAmount"), priceUnit=group.get("priceUnit") or "起/晚", tags=group.get("tags", []), status=group.get("status", "published"), isActive=group.get("isActive", True), sortOrder=index, ) ) for index, option in enumerate(VEHICLE_OPTIONS): create_media(db, option["image"], "vehicle-option", option["title"]) db.add( VehicleOption( title=option["title"], description=option.get("description"), image=option.get("image"), 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 if not matched_destination: matched_destination = next( (name for name in destination_map if name in product["title"] or name in product.get("tags", [])), None, ) summary = product.get("summary") or " · ".join(product.get("tags", [])) created = Product( sourceId=product["id"], title=product["title"], subtitle=product_subtitle(product["title"]), destinationId=destination_map.get(matched_destination) if matched_destination else None, priceAmount=int(product["price"]), tags=product.get("tags", []), coverImage=product.get("image"), summary=summary, detailSections=default_detail_sections(summary, matched_destination or "贵州省内定制"), status="published", sortWeight=product["id"], publishedAt=utc_now(), ) db.add(created) db.flush() db.add(ProductImage(productId=created.id, url=product["image"], alt=product["title"])) for seed in CAMPAIGNS: campaign = Campaign( slug=seed["slug"], title=seed["title"], description="万趣贵州小包团活动专题。", coverImage=seed["coverImage"], status="published", ) db.add(campaign) 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(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)) 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", publishedAt=utc_now(), 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), "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) db.flush() 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), "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, } def seed_database(reset: bool) -> dict: Base.metadata.create_all(bind=engine) with SessionLocal() as db: user = db.scalar(select(AdminUser).where(AdminUser.email == "admin@example.com")) if user: user.passwordHash = hash_password("ChangeMe123!") user.isActive = True user.name = "后台管理员" user.role = "super_admin" else: db.add( AdminUser( email="admin@example.com", name="后台管理员", passwordHash=hash_password("ChangeMe123!"), role="super_admin", ) ) result = reset_guizhou_content(db) if reset else {"reset": False} db.commit() return result def main() -> None: parser = argparse.ArgumentParser(description="Seed WonderQ Admin database") parser.add_argument("--no-reset", action="store_true", help="Only ensure admin user exists") args = parser.parse_args() result = seed_database(reset=not args.no_reset) print(f"Seed complete. Admin login: admin@example.com / ChangeMe123!") print(json.dumps(result, ensure_ascii=False)) if __name__ == "__main__": main()