Initial commit: hulai admin API backend
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
这个提交包含在:
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
在新工单中引用
屏蔽一个用户