Initial commit: hulai admin API backend

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
这个提交包含在:
刘涛
2026-03-22 22:01:23 +08:00
共同撰写人 Claude Sonnet 4.6
当前提交 8c0a5f489d
共修改 114 个文件,包含 7619 行新增和 0 行删除
查看文件
+41
查看文件
@@ -0,0 +1,41 @@
from __future__ import annotations
"""创建初始管理员账号
Usage: cd hulai-admin-api && python -m scripts.create_admin_user
"""
import sys
import os
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from app.database import SessionLocal, engine, Base
from app.models.user import AdminUser
from app.auth import hash_password
def main():
# Create tables if not exist
Base.metadata.create_all(bind=engine)
db = SessionLocal()
try:
existing = db.query(AdminUser).filter(AdminUser.username == "admin").first()
if existing:
print("管理员账号已存在")
return
user = AdminUser(
username="admin",
password_hash=hash_password("admin123"),
display_name="管理员",
role="admin",
is_active=True,
)
db.add(user)
db.commit()
print("管理员创建成功: admin / admin123")
print("⚠️ 请登录后立即修改密码!")
finally:
db.close()
if __name__ == "__main__":
main()
+540
查看文件
@@ -0,0 +1,540 @@
from __future__ import annotations
"""把前端 JSON 文件中的数据导入到后端数据库。
UPSERT 逻辑:已存在则更新所有字段,不存在则插入。
重新运行脚本即可将 JSON 的最新数据同步到数据库。
Usage: cd hulai-admin-api && python -m scripts.import_json_data
"""
import json
import os
import sys
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from app.config import settings
from app.database import SessionLocal, engine, Base
from app.models import *
def load_json(filename):
path = os.path.join(settings.NUXT_DATA_PATH, filename)
if not os.path.exists(path):
print(f" ⚠️ 文件不存在: {path},跳过")
return None
with open(path, "r", encoding="utf-8") as f:
return json.load(f)
# ─── Blog ────────────────────────────────────────────────────────────────────
def import_blog(db):
data = load_json("blog.json")
if not data:
return
articles = data.get("articles", [])
added = updated = 0
for i, a in enumerate(articles):
slug = a.get("id") or a.get("slug")
if not slug:
print(f" ⚠️ 博文缺少 id/slug,跳过: {a.get('title', '')[:30]}")
continue
# 章节内容合并成 HTML
content_parts = []
for ch in a.get("sections", []) or a.get("chapters", []):
if isinstance(ch, dict):
if ch.get("title"):
content_parts.append(f"<h2>{ch['title']}</h2>")
if ch.get("content"):
content_parts.append(ch["content"])
if ch.get("body"):
content_parts.append(ch["body"])
content = "\n".join(content_parts) if content_parts else a.get("content", "")
fields = dict(
title=a["title"],
cover_image=a.get("coverImage") or a.get("cover_image"),
summary=a.get("summary") or a.get("subtitle"),
content=content,
author=a.get("author"),
category=a.get("category"),
tags=a.get("tags"),
published_at=a.get("date") or a.get("publishedAt"),
is_visible=True,
sort_order=i,
)
existing = db.query(Blog).filter_by(slug=slug).first()
if existing:
for k, v in fields.items():
setattr(existing, k, v)
updated += 1
else:
db.add(Blog(slug=slug, **fields))
added += 1
print(f" blog_posts: 新增 {added} 条,更新 {updated} 条(共 {len(articles)} 条)")
# ─── Stories ─────────────────────────────────────────────────────────────────
def import_stories(db):
data = load_json("stories.json")
if not data:
return
stories = data.get("stories", [])
added = updated = 0
for i, s in enumerate(stories):
slug = s.get("slug") or s.get("id")
title = s.get("title", "")
# 章节内容合并
content_parts = []
for ch in s.get("chapters", []):
if isinstance(ch, dict):
if ch.get("title"):
content_parts.append(f"<h2>{ch['title']}</h2>")
body = ch.get("content") or ch.get("body", "")
if body:
content_parts.append(body)
content = "\n".join(content_parts) if content_parts else s.get("content", "")
fields = dict(
title=title,
cover_image=s.get("coverImage") or s.get("cover_image"),
summary=s.get("summary") or s.get("subtitle"),
content=content,
customer_name=s.get("customerName") or s.get("familyType"),
avatar=s.get("avatar"),
travel_date=s.get("travelDate"),
product_name=s.get("product"),
scenes=s.get("tags"),
is_visible=True,
sort_order=i,
)
# 优先按 slug 匹配,无 slug 则按 title
existing = db.query(Story).filter_by(title=title).first() if title else None
if existing:
for k, v in fields.items():
setattr(existing, k, v)
updated += 1
else:
db.add(Story(**fields))
added += 1
print(f" stories: 新增 {added} 条,更新 {updated} 条(共 {len(stories)} 条)")
# ─── News ─────────────────────────────────────────────────────────────────────
def import_news(db):
data = load_json("news.json")
if not data:
return
articles = data.get("articles", [])
added = updated = 0
for i, a in enumerate(articles):
slug = a.get("slug") or a.get("id")
title = a.get("title", "")
fields = dict(
title=title,
cover_image=a.get("coverImage") or a.get("cover_image"),
summary=a.get("summary"),
content=a.get("content", ""),
source=a.get("source"),
source_url=a.get("sourceUrl") or a.get("url"),
published_at=a.get("date") or a.get("publishedAt"),
is_visible=True,
sort_order=i,
)
existing = db.query(News).filter_by(title=title).first() if title else None
if existing:
for k, v in fields.items():
setattr(existing, k, v)
updated += 1
else:
db.add(News(**fields))
added += 1
print(f" news: 新增 {added} 条,更新 {updated} 条(共 {len(articles)} 条)")
# ─── Pricing ─────────────────────────────────────────────────────────────────
def import_pricing(db):
data = load_json("pricing.json")
if not data:
return
products = data.get("products", [])
added = updated = 0
for i, p in enumerate(products):
product_id = p.get("id") or p.get("slug")
fields = dict(
product_name=p.get("name", ""),
price_from=None,
price_unit=None,
price_label=p.get("priceLabel"),
description=p.get("priceNote") or p.get("audience"),
features=p.get("highlights"),
notes=None,
is_visible=True,
sort_order=i,
)
existing = None
if product_id:
existing = db.query(PricingItem).filter_by(product_slug=product_id).first()
if existing:
for k, v in fields.items():
setattr(existing, k, v)
updated += 1
else:
db.add(PricingItem(product_slug=product_id, **fields))
added += 1
print(f" pricing_items: 新增 {added} 条,更新 {updated} 条(共 {len(products)} 条)")
# ─── Qualifications ──────────────────────────────────────────────────────────
def import_qualifications(db):
data = load_json("qualifications.json")
if not data:
return
added = updated = 0
sort = 0
def upsert_qual(name, **fields):
nonlocal added, updated, sort
existing = db.query(Qualification).filter_by(title=name).first()
if existing:
for k, v in fields.items():
setattr(existing, k, v)
updated += 1
else:
db.add(Qualification(title=name, sort_order=sort, **fields))
added += 1
sort += 1
# 旅游经营许可证
for lic in data.get("licenses", []):
title = lic.get("title", "旅游经营许可证")
upsert_qual(
title,
issuer=lic.get("issuer"),
year=None,
image=None,
description=(
f"证件号:{lic.get('licenseNo', '')}|"
f"持证主体:{lic.get('holder', '')}|"
f"法人:{lic.get('legalPerson', '')}|"
f"经营范围:{lic.get('businessScope', '')}|"
f"统一信用代码:{lic.get('creditCode', '')}|"
f"{lic.get('note', '')}"
),
category="license",
)
# 旅游责任险
insurance = data.get("insurance")
if insurance:
policies = insurance.get("policies", [])
policy_desc = ";".join([
f"{p.get('insured','')}·{p.get('insurer','')}·保单号{p.get('policyNo','')}·保额{p.get('coverage','')}·有效期{p.get('period','')}"
for p in policies
])
upsert_qual(
insurance.get("title", "旅游责任险"),
issuer=policies[0].get("insurer") if policies else None,
year=None,
image=None,
description=f"{insurance.get('description', '')}|{policy_desc}",
category="insurance",
)
# 荣誉奖项
for award in data.get("awards", []):
title = award.get("title", "")
if not title:
continue
upsert_qual(
title,
issuer=award.get("issuer") or award.get("platform"),
year=str(award.get("year", "")) if award.get("year") else None,
image=None,
description=award.get("description") or award.get("detail"),
category="award",
)
# 品牌传承
heritage = data.get("heritage")
if heritage:
milestones_text = ";".join([
f"{m.get('year')}年:{m.get('event', '')}"
for m in heritage.get("milestones", [])
])
upsert_qual(
f"品牌资历:{heritage.get('brandYears', '')}年深耕呼伦贝尔",
issuer=None,
year=str(heritage.get("brandFounded")) if heritage.get("brandFounded") else None,
image=None,
description=f"{heritage.get('note', '')}|{milestones_text}",
category="heritage",
)
print(f" qualifications: 新增 {added} 条,更新 {updated} 条")
# ─── Partners ────────────────────────────────────────────────────────────────
def import_partners(db):
data = load_json("partners.json")
if not data:
return
added = updated = 0
sort = 0
cat_map = {
"govPartners": "政府合作",
"associations": "行业协会",
"academicCoops": "校企合作",
"scenicPartners": "景区合作",
"hotelPartners": "酒店合作",
"platformEndorsements": "平台认证",
"mediaReports": "媒体报道",
}
for cat_key, cat_label in cat_map.items():
for item in data.get(cat_key, []):
name = item.get("name") or item.get("platform") or item.get("outlet", "")
if not name or name.startswith("⚠️"):
continue
if not item.get("verified", True):
continue
# 构建描述
desc_parts = []
for field in ["type", "relationship", "description", "detail", "status", "summary"]:
val = item.get(field)
if val and not str(val).startswith("⚠️"):
desc_parts.append(str(val))
if item.get("followers"):
desc_parts.append(f"粉丝:{item['followers']}")
if item.get("account"):
desc_parts.append(f"账号:{item['account']}")
if item.get("since"):
desc_parts.append(f"合作始于:{item['since']}")
fields = dict(
logo=None,
website=item.get("url"),
description="|".join(desc_parts),
category=cat_label,
sort_order=sort,
)
existing = db.query(Partner).filter_by(name=name).first()
if existing:
for k, v in fields.items():
setattr(existing, k, v)
updated += 1
else:
db.add(Partner(name=name, **fields))
added += 1
sort += 1
print(f" partners: 新增 {added} 条,更新 {updated} 条")
# ─── Gallery ─────────────────────────────────────────────────────────────────
def import_gallery(db):
data = load_json("gallery.json")
if not data:
return
works = data.get("works", [])
added = updated = 0
for i, w in enumerate(works):
title = w.get("title")
tags = []
if w.get("category"):
tags.append(w["category"])
if w.get("season"):
tags.append(w["season"])
if w.get("costumeType"):
tags.append(w["costumeType"])
fields = dict(
image=w.get("image", ""),
photographer=None,
location=w.get("location"),
description=w.get("description"),
tags=tags if tags else None,
is_visible=True,
sort_order=i,
)
existing = db.query(GalleryItem).filter_by(title=title).first() if title else None
if existing:
for k, v in fields.items():
setattr(existing, k, v)
updated += 1
else:
db.add(GalleryItem(title=title, **fields))
added += 1
print(f" gallery_items: 新增 {added} 条,更新 {updated} 条(共 {len(works)} 条)")
# ─── Destination Details ──────────────────────────────────────────────────────
def import_destination_details(db):
data = load_json("destinations-detail.json")
if not data:
return
destinations = data.get("destinations", [])
added = updated = 0
for i, d in enumerate(destinations):
slug = d.get("id", "")
if not slug:
continue
# highlights 可能是 string list 或 dict list
raw_highlights = d.get("highlights", [])
if raw_highlights and isinstance(raw_highlights[0], str):
highlights = [{"text": h} for h in raw_highlights]
else:
highlights = raw_highlights
tags = []
if d.get("tag"):
tags.append(d["tag"])
fields = dict(
name=d.get("name", ""),
subtitle=d.get("subtitle"),
cover_image=d.get("heroImage") or d.get("coverImage"),
description=d.get("description", ""),
location=None,
best_season=(
d["bestSeason"].get("primary") if isinstance(d.get("bestSeason"), dict)
else d.get("bestSeason")
),
duration=None,
highlights=highlights,
gallery=None,
tags=tags if tags else None,
is_visible=True,
sort_order=i,
)
existing = db.query(DestinationDetail).filter_by(slug=slug).first()
if existing:
for k, v in fields.items():
setattr(existing, k, v)
updated += 1
else:
db.add(DestinationDetail(slug=slug, **fields))
added += 1
print(f" destination_details: 新增 {added} 条,更新 {updated} 条(共 {len(destinations)} 条)")
# ─── Selector Config ─────────────────────────────────────────────────────────
def import_selector(db):
data = load_json("selector.json")
if not data:
return
rules = {
"scoring": data.get("scoring", {}),
"matchReasons": data.get("matchReasons", {}),
}
existing = db.query(SelectorConfig).first()
if existing:
existing.questions = data.get("questions", [])
existing.rules = rules
print(" selector_config: ✓ 已更新")
else:
db.add(SelectorConfig(
questions=data.get("questions", []),
rules=rules,
))
print(" selector_config: ✓ 新增 1 条")
# ─── Courses Config ───────────────────────────────────────────────────────────
def import_courses(db):
data = load_json("courses.json")
if not data:
return
existing = db.query(CoursesConfig).first()
if existing:
existing.modules = data.get("modules", [])
existing.age_groups = data.get("ageGroups", [])
existing.faqs = data.get("faq", []) or data.get("faqs", [])
print(" courses_config: ✓ 已更新")
else:
db.add(CoursesConfig(
modules=data.get("modules", []),
age_groups=data.get("ageGroups", []),
faqs=data.get("faq", []) or data.get("faqs", []),
))
print(" courses_config: ✓ 新增 1 条")
# ─── Main ─────────────────────────────────────────────────────────────────────
def main():
print("确保数据库表存在...")
Base.metadata.create_all(bind=engine)
db = SessionLocal()
try:
print("\n开始 UPSERT JSON 数据到数据库:")
print("-" * 50)
import_blog(db)
import_stories(db)
import_news(db)
import_pricing(db)
import_qualifications(db)
import_partners(db)
import_gallery(db)
import_destination_details(db)
import_selector(db)
import_courses(db)
db.commit()
print("-" * 50)
print("\n✓ 数据同步完成!")
# 验证
print("\n数据库记录数验证:")
from app.models import (Blog, Story, News, PricingItem, Qualification,
Partner, GalleryItem, DestinationDetail,
SelectorConfig, CoursesConfig)
checks = [
("blog_posts", Blog),
("stories", Story),
("news", News),
("pricing_items", PricingItem),
("qualifications", Qualification),
("partners", Partner),
("gallery_items", GalleryItem),
("destination_details", DestinationDetail),
("selector_config", SelectorConfig),
("courses_config", CoursesConfig),
]
for name, model in checks:
cnt = db.query(model).count()
status = "✓" if cnt > 0 else "⚠ 空"
print(f" {status} {name}: {cnt}")
except Exception as e:
db.rollback()
print(f"\n✗ 导入失败: {e}")
import traceback
traceback.print_exc()
raise
finally:
db.close()
if __name__ == "__main__":
main()
+524
查看文件
@@ -0,0 +1,524 @@
from __future__ import annotations
"""将现有 JSON 数据导入 MySQL
Usage: cd hulai-admin-api && python -m scripts.migrate_json_to_db
"""
import json
import os
import sys
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from app.config import settings
from app.database import SessionLocal, engine, Base
from app.models import *
def load_json(filename):
path = os.path.join(settings.NUXT_DATA_PATH, filename)
if not os.path.exists(path):
print(f" ⚠️ 文件不存在: {path},跳过")
return None
with open(path, "r", encoding="utf-8") as f:
return json.load(f)
def migrate_brand(db):
data = load_json("brand.json")
if not data:
return
if db.query(Brand).first():
print(" brand: 已存在,跳过")
return
brand = Brand(
name=data["name"], full_name=data.get("fullName"), domain=data.get("domain"),
url=data.get("url"), slogan_emotional=data.get("slogan", {}).get("emotional"),
slogan_functional=data.get("slogan", {}).get("functional"),
icp_entity=data.get("icpEntity"), icp=data.get("icp"),
icp_url=data.get("icpUrl"), e_contract=data.get("eContract"),
)
db.add(brand)
db.flush()
for i, s in enumerate(data.get("trustStats", [])):
db.add(BrandTrustStat(brand_id=brand.id, value=s["value"], unit=s.get("unit"), label=s["label"], sort_order=i))
for i, d in enumerate(data.get("differentiators", [])):
db.add(BrandDifferentiator(brand_id=brand.id, title=d["title"], description=d.get("description"), sort_order=i))
print(" brand: ✓")
def migrate_products(db):
data = load_json("products.json")
if not data:
return
if db.query(ProductConfig).first():
print(" products: 已存在,跳过")
return
config = ProductConfig(narrative=data.get("narrative"), pricing_philosophy=data.get("pricingPhilosophy"))
db.add(config)
db.flush()
for i, v in enumerate(data.get("versions", [])):
pv = ProductVersion(
config_id=config.id, version_id=v["id"], name=v["name"],
days=v["days"], nights=v["nights"], audience=v.get("audience"),
description=v.get("description"), tag=v.get("tag"), sort_order=i,
)
db.add(pv)
db.flush()
for j, h in enumerate(v.get("highlights", [])):
db.add(ProductVersionHighlight(version_id=pv.id, text=h, sort_order=j))
camp_data = data.get("summerCamp")
if camp_data:
camp = SummerCamp(
config_id=config.id, name=camp_data.get("name"),
positioning=camp_data.get("positioning"),
days=camp_data.get("days"), nights=camp_data.get("nights"),
sessions_json=camp_data.get("sessions"),
difference_from_v9=camp_data.get("differenceFromV9"),
)
db.add(camp)
db.flush()
for i, p in enumerate(camp_data.get("principles", [])):
db.add(SummerCampPrinciple(camp_id=camp.id, text=p, sort_order=i))
for i, a in enumerate(camp_data.get("activities", [])):
db.add(SummerCampActivity(camp_id=camp.id, text=a, sort_order=i))
for i, it in enumerate(camp_data.get("itinerary", [])):
db.add(SummerCampItinerary(camp_id=camp.id, text=it, sort_order=i))
for i, f in enumerate(camp_data.get("faq", [])):
db.add(SummerCampFaq(camp_id=camp.id, question=f.get("q") or f.get("question", ""), answer=f.get("a") or f.get("answer", ""), sort_order=i))
guide_data = data.get("selectionGuide")
if guide_data:
db.add(SelectionGuide(
config_id=config.id,
by_vacation_length=guide_data.get("byVacationLength"),
by_child_age=guide_data.get("byChildAge"),
by_preference=guide_data.get("byPreference"),
))
print(" products: ✓")
def migrate_faq(db):
data = load_json("faq.json")
if not data:
return
if db.query(FaqCategory).first():
print(" faq: 已存在,跳过")
return
for i, cat in enumerate(data.get("categories", [])):
c = FaqCategory(category_id=cat["id"], name=cat["name"], sort_order=i)
db.add(c)
db.flush()
for j, q in enumerate(cat.get("questions", [])):
db.add(FaqQuestion(
category_id=c.id, question_id=q["id"], question=q["question"],
answer=q["answer"], related_links=q.get("relatedLinks"), sort_order=j,
))
print(" faq: ✓")
def migrate_reviews(db):
data = load_json("reviews.json")
if not data:
return
if db.query(Review).first():
print(" reviews: 已存在,跳过")
return
summary = data.get("summary", {})
db.add(ReviewSummary(
total_count=summary.get("totalCount", 0),
approval_rate=summary.get("approvalRate"),
keywords=summary.get("keywords"),
))
for i, r in enumerate(data.get("items", [])):
db.add(Review(
nickname=r["nickname"], travel_date=r.get("travelDate"),
product_version=r.get("productVersion"), screenshot=r.get("screenshot"),
content=r["content"], scenes=r.get("scenes"), concerns=r.get("concerns"),
sort_order=i, is_visible=True,
))
print(" reviews: ✓")
def migrate_about(db):
data = load_json("about.json")
if not data:
return
if db.query(AboutStory).first():
print(" about: 已存在,跳过")
return
story = data.get("story", {})
db.add(AboutStory(title=story.get("title"), content=story.get("content"), founding_moment=story.get("foundingMoment")))
for i, s in enumerate(data.get("subsidiaries", [])):
db.add(AboutSubsidiary(name=s["name"], role=s.get("role"), established=s.get("established"), sort_order=i))
for i, c in enumerate(data.get("certifications", [])):
db.add(AboutCertification(title=c["title"], detail=c.get("detail"), sort_order=i))
tm = data.get("trademark", {})
if tm:
db.add(AboutTrademark(name=tm.get("name"), scope=tm.get("scope"), holder=tm.get("holder"), description=tm.get("description")))
for i, c in enumerate(data.get("copyrights", [])):
db.add(AboutCopyright(
name=c["name"], reg_no=c.get("regNo"), category=c.get("category"),
holder=c.get("holder"), date=c.get("date"), description=c.get("description"), sort_order=i,
))
for i, g in enumerate(data.get("guarantees", [])):
db.add(AboutGuarantee(title=g["title"], detail=g.get("detail"), sort_order=i))
xhs = data.get("xiaohongshu", {})
if xhs:
db.add(AboutXiaohongshu(
account=xhs.get("account"), verified=xhs.get("verified", False),
verified_type=xhs.get("verifiedType"), followers=xhs.get("followers"),
likes=xhs.get("likes"), awards=xhs.get("awards"), tagline=xhs.get("tagline"),
tags=xhs.get("tags"), description=xhs.get("description"),
))
team = data.get("team", {})
if team:
db.add(AboutTeam(summary=team.get("summary")))
culture = data.get("culture", {})
if culture:
c = AboutCulture(transparency=culture.get("transparency"))
db.add(c)
db.flush()
for i, v in enumerate(culture.get("values", [])):
db.add(AboutCultureValue(culture_id=c.id, name=v["name"], expression=v.get("expression"), sort_order=i))
print(" about: ✓")
def migrate_contact(db):
data = load_json("contact.json")
if not data:
return
if db.query(ContactChannel).first():
print(" contact: 已存在,跳过")
return
for i, ch in enumerate(data.get("channels", [])):
db.add(ContactChannel(
type=ch["type"], label=ch["label"], value=ch.get("value"),
qr_image=ch.get("qrImage"), is_primary=ch.get("primary", False),
description=ch.get("description"), sort_order=i,
))
notice = data.get("securityNotice")
if notice:
db.add(ContactConfig(security_notice=notice))
print(" contact: ✓")
def migrate_seo(db):
data = load_json("seo.json")
if not data:
return
if db.query(SeoPage).first():
print(" seo: 已存在,跳过")
return
for key, page in data.get("pages", {}).items():
db.add(SeoPage(
page_key=key, title=page.get("title"), description=page.get("description"),
h1=page.get("h1"), og_image=page.get("ogImage"),
))
print(" seo: ✓")
def migrate_navigation(db):
data = load_json("navigation.json")
if not data:
return
if db.query(NavHeader).first():
print(" navigation: 已存在,跳过")
return
for i, h in enumerate(data.get("header", [])):
db.add(NavHeader(text=h["text"], to_path=h["to"], sort_order=i))
for i, g in enumerate(data.get("footer", [])):
group = NavFooterGroup(title=g["title"], sort_order=i)
db.add(group)
db.flush()
for j, l in enumerate(g.get("links", [])):
db.add(NavFooterLink(group_id=group.id, text=l["text"], to_path=l["to"], sort_order=j))
print(" navigation: ✓")
def migrate_versions(db):
data = load_json("versions.json")
if not data:
return
if db.query(VersionConfig).first():
print(" versions: 已存在,跳过")
return
stats = data.get("stats", {})
quote = data.get("quote", {})
config = VersionConfig(
stats_iterations=stats.get("iterations"), stats_years=stats.get("years"),
stats_guests=stats.get("guests"),
quote_text=quote.get("text") if quote else None,
quote_author=quote.get("author") if quote else None,
)
db.add(config)
db.flush()
for i, u in enumerate(data.get("upgrades2026", [])):
db.add(VersionUpgrade(config_id=config.id, tag=u.get("tag"), name=u.get("name"), description=u.get("description"), reason=u.get("reason"), sort_order=i))
for i, h in enumerate(data.get("highlights", [])):
db.add(VersionHighlight(config_id=config.id, label=h.get("label"), text=h.get("text"), sort_order=i))
cmp = data.get("compareV8V9", {})
if cmp:
db.add(VersionCompare(config_id=config.id, headers=cmp.get("headers"), rows=cmp.get("rows")))
for i, t in enumerate(data.get("timeline", [])):
tl = VersionTimeline(
config_id=config.id, version=t["version"], date=t.get("date"),
title=t.get("title"), reason=t.get("reason"), sort_order=i,
)
db.add(tl)
db.flush()
for j, ch in enumerate(t.get("changes", [])):
db.add(VersionTimelineChange(timeline_id=tl.id, type=ch["type"], text=ch["text"], sort_order=j))
for i, p in enumerate(data.get("philosophy", [])):
db.add(VersionPhilosophy(config_id=config.id, label=p.get("label"), text=p.get("text"), sort_order=i))
print(" versions: ✓")
def migrate_guides(db):
data = load_json("guides.json")
if not data:
return
if db.query(GuideConfig).first():
print(" guides: 已存在,跳过")
return
config = GuideConfig(page_intro=data.get("pageIntro"))
db.add(config)
db.flush()
for i, s in enumerate(data.get("sections", [])):
# Extract common fields, store the rest as JSON data
common_keys = {"id", "title", "subtitle", "icon", "content"}
extra_data = {k: v for k, v in s.items() if k not in common_keys}
db.add(GuideSection(
config_id=config.id, section_id=s["id"], title=s.get("title"),
subtitle=s.get("subtitle"), icon=s.get("icon"), content=s.get("content"),
data=extra_data if extra_data else None, sort_order=i,
))
print(" guides: ✓")
def migrate_images(db):
data = load_json("images.json")
if not data:
return
if db.query(SiteImage).first():
print(" images: 已存在,跳过")
return
# 图片元数据:label 和建议尺寸
meta = {
("logo", "main"): ("品牌Logo", "200x200px(正方形透明PNG)", "网站头部和底部的品牌标识"),
("logo", "square"): ("方形Logo", "200x200px(正方形)", "社交媒体头像等场景"),
("hero", "background"): ("首页大图", "1920x1080px(16:9横图)", "首页顶部全屏背景图"),
("brandStory", "photo"): ("品牌故事配图", "800x600px(4:3横图)", "关于我们-品牌故事旁配图"),
("mascot", "front"): ("IP形象正面", "560x760px(竖版透明PNG)", "关于我们-IP展示正面形象"),
("mascot", "banner"): ("IP场景横幅", "1600x600px(宽横幅)", "关于我们-IP草原场景插画"),
("mascot", "stickerQr"): ("表情包二维码", "320x320px(正方形)", "微信表情包扫码下载"),
("mascot", "xiaomengma"): ("小蒙马IP", "400x400px(正方形透明PNG)", "夏令营相关页面的小蒙马形象"),
("team", "guideGroup"): ("领队合影", "800x600px(4:3横图)", "团队介绍-领队团队合影"),
("team", "routeSurvey"): ("踩线照片", "800x600px(4:3横图)", "团队介绍-线路踩线工作照"),
("team", "guideTraining"): ("培训照片", "800x600px(4:3横图)", "团队介绍-领队培训照片"),
("team", "ranchStore"): ("牧场团队", "800x600px(4:3横图)", "团队介绍-牧场商贸团队照"),
("xiaohongshu", "storefront"): ("小红书店铺", "1200x400px(3:1横幅)", "关于我们-小红书店铺展示横幅"),
("summerCamp", "river"): ("夏令营河景", "800x600px(4:3横图)", "夏令营页面-河景团照"),
("summerCamp", "grassland"): ("夏令营草原", "800x600px(4:3横图)", "夏令营页面-草原活动照"),
("summerCamp", "birch"): ("夏令营白桦林", "800x600px(4:3横图)", "夏令营页面-白桦林研学照"),
("summerCamp", "graduation"): ("夏令营结营", "800x600px(4:3横图)", "夏令营页面-结营合影"),
}
for group_name, items in data.items():
if isinstance(items, dict):
for key, path in items.items():
m = meta.get((group_name, key), (f"{group_name}.{key}", "", ""))
db.add(SiteImage(
group_name=group_name, image_key=key, image_path=path,
label=m[0], size_hint=m[1], description=m[2],
))
print(" images: ✓")
def _migrate_seasonal(db, season, filename):
data = load_json(filename)
if not data:
return
if db.query(SeasonalProductConfig).filter_by(season=season).first():
print(f" {filename}: 已存在,跳过")
return
config = SeasonalProductConfig(
season=season, narrative=data.get("narrative"), style=data.get("style"),
)
db.add(config)
db.flush()
for i, v in enumerate(data.get("versions", [])):
db.add(SeasonalProductVersion(
season=season, version_id=v["id"], name=v["name"],
days=v["days"], nights=v["nights"], tag=v.get("tag"),
line=v.get("line"), route=v.get("route"), audience=v.get("audience"),
description=v.get("description"), highlights=v.get("highlights", []),
itinerary=v.get("itinerary", []), sort_order=i,
))
for cat_key in ["highlights", "southHighlights", "northHighlights"]:
cat_map = {"highlights": "shared", "southHighlights": "south", "northHighlights": "north"}
cat = cat_map[cat_key]
for i, h in enumerate(data.get(cat_key, [])):
db.add(SeasonalProductHighlight(
season=season, category=cat, title=h["title"],
description=h.get("description"), sort_order=i,
))
for i, t in enumerate(data.get("timeline", [])):
db.add(SeasonalProductTimeline(
season=season, version=t.get("version"), date=t.get("date"),
title=t.get("title"), changes=t.get("changes", []),
reason=t.get("reason"), sort_order=i,
))
print(f" {filename}: ✓")
def migrate_autumn_products(db):
_migrate_seasonal(db, "autumn", "autumn-products.json")
def migrate_winter_products(db):
_migrate_seasonal(db, "winter", "winter-products.json")
def migrate_winter_camp(db):
data = load_json("winter-camp.json")
if not data:
return
if db.query(WinterCampConfig).first():
print(" winter-camp: 已存在,跳过")
return
config = WinterCampConfig(
name=data.get("name"), positioning=data.get("positioning"),
days=data.get("days"), nights=data.get("nights"),
max_families=data.get("maxFamilies"), total_sessions=data.get("totalSessions"),
age_range=data.get("ageRange"), deposit=data.get("deposit"),
season=data.get("season"), route=data.get("route"),
why_hulunbuir=data.get("whyHulunbuir"), closing_note=data.get("closingNote"),
photographer=data.get("photographer"), winter_clothing=data.get("winterClothing"),
camp_advantages=data.get("campAdvantages", []),
service_config=data.get("serviceConfig", []),
camp_essentials=data.get("campEssentials", []),
)
db.add(config)
db.flush()
for i, h in enumerate(data.get("hotels", [])):
db.add(WinterCampHotel(name=h["name"], star=h.get("star"), nights=h.get("nights"), description=h.get("description"), sort_order=i))
for i, d in enumerate(data.get("itinerary", [])):
db.add(WinterCampItinerary(day=d["day"], title=d.get("title"), summary=d.get("summary"), highlights=d.get("highlights", []), hotel=d.get("hotel"), sort_order=i))
for i, f in enumerate(data.get("faq", [])):
db.add(WinterCampFaq(question=f["q"], answer=f["a"], sort_order=i))
print(" winter-camp: ✓")
def migrate_destinations(db):
data = load_json("destinations.json")
if not data:
return
if db.query(DestinationConfig).first():
print(" destinations: 已存在,跳过")
return
closing = data.get("closing", {})
honest = data.get("honestNote", {})
config = DestinationConfig(
title=data.get("title"), subtitle=data.get("subtitle"), intro=data.get("intro"),
closing_title=closing.get("title"), closing_text=closing.get("text"),
data_sources=data.get("dataSources"),
honest_title=honest.get("title"), honest_subtitle=honest.get("subtitle"),
)
db.add(config)
db.flush()
for i, d in enumerate(data.get("destinations", [])):
db.add(DestinationItem(dest_id=d.get("id", f"dest-{i}"), name=d["name"], tag=d.get("tag"), highlight=d.get("highlight", False), sort_order=i))
for i, dim in enumerate(data.get("dimensions", [])):
db.add(DestinationDimension(label=dim["label"], icon=dim.get("icon"), values=dim.get("values", []), sort_order=i))
for i, h in enumerate(honest.get("items", [])):
db.add(DestinationHonestItem(text=h, sort_order=i))
print(" destinations: ✓")
def main():
print("创建数据库表...")
Base.metadata.create_all(bind=engine)
db = SessionLocal()
try:
print("\n开始迁移数据:")
migrate_brand(db)
migrate_products(db)
migrate_faq(db)
migrate_reviews(db)
migrate_about(db)
migrate_contact(db)
migrate_seo(db)
migrate_navigation(db)
migrate_versions(db)
migrate_guides(db)
migrate_images(db)
migrate_autumn_products(db)
migrate_winter_products(db)
migrate_winter_camp(db)
migrate_destinations(db)
db.commit()
print("\n✓ 数据迁移完成!")
except Exception as e:
db.rollback()
print(f"\n✗ 迁移失败: {e}")
raise
finally:
db.close()
if __name__ == "__main__":
main()