Files
nanocode/new-media-platform/website-new/backend/migrate_news_to_columns.py
T
Ubuntu 32b43f85bd a
2026-07-03 16:58:28 +08:00

193 lines
8.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
新闻资讯模块迁移脚本
将老的新闻分类(news_categories)和新闻文章(news_posts)迁移到新的栏目(columns)和内容(articles)体系
执行方式: cd backend && .\venv-win\Scripts\python.exe migrate_news_to_columns.py
"""
import sys
import time
from sqlalchemy import create_engine, text
from pypinyin import lazy_pinyin
import re
DB_PATH = "sqlite:///cms.db"
engine = create_engine(DB_PATH)
def generate_slug(text: str) -> str:
"""将中文标题转为拼音 slug"""
if not text:
return f"article-{int(time.time())}"
py = lazy_pinyin(text, style=0)
slug = "-".join(py)
slug = re.sub(r"[^a-zA-Z0-9-]", "", slug).lower()
slug = re.sub(r"-+", "-", slug).strip("-")
return slug or f"article-{int(time.time())}"
def migrate():
with engine.connect() as conn:
trans = conn.begin()
try:
print("=" * 60)
print("新闻资讯模块迁移开始")
print("=" * 60)
# ========== 阶段 0: 清空现有数据 ==========
print("\n[阶段 0] 清空 columns 和 articles 表...")
# 先删 articles(外键依赖)
result = conn.execute(text("DELETE FROM articles"))
print(f" 已清空 articles 表")
# 再删 columns
result = conn.execute(text("DELETE FROM columns"))
print(f" 已清空 columns 表")
# ========== 阶段 1: 创建"新闻资讯"顶级栏目 ==========
print("\n[阶段 1] 创建'新闻资讯'顶级栏目...")
result = conn.execute(text("""
INSERT INTO columns (name, slug, description, parent_id, sort_order, is_enabled, display_mode, layout_style)
VALUES (:name, :slug, :description, NULL, 0, 1, 'list', 'left')
"""), {
"name": "新闻资讯",
"slug": "xin-wen-zi-xun",
"description": ""
})
# 获取刚插入的 ID
row = conn.execute(text("SELECT id FROM columns WHERE slug = 'xin-wen-zi-xun'")).fetchone()
news_top_id = row[0]
print(f" 顶级栏目 ID: {news_top_id}, slug: xin-wen-zi-xun")
# ========== 阶段 2: 迁移启用的 news_categories ==========
print("\n[阶段 2] 迁移启用的 news_categories(作为二级子栏目)...")
categories = conn.execute(text("""
SELECT id, name, slug, description, sort_order, is_enabled
FROM news_categories
WHERE is_enabled = 1
ORDER BY id
""")).fetchall()
cat_id_map = {} # old_id -> new_id
for cat in categories:
old_id, name, slug, description, sort_order, is_enabled = cat
result = conn.execute(text("""
INSERT INTO columns (name, slug, description, parent_id, sort_order, is_enabled, display_mode, layout_style)
VALUES (:name, :slug, :description, :parent_id, :sort_order, :is_enabled, 'list', 'left')
"""), {
"name": name,
"slug": slug,
"description": description or "",
"parent_id": news_top_id,
"sort_order": sort_order or 0,
"is_enabled": is_enabled
})
new_id = conn.execute(text("SELECT last_insert_rowid()")).fetchone()[0]
cat_id_map[old_id] = new_id
print(f" [{old_id}] {name} -> new_id={new_id}")
print(f" 共迁移 {len(cat_id_map)} 个子栏目")
# ========== 阶段 3: 迁移 news_posts ==========
print("\n[阶段 3] 迁移 news_posts(重新生成拼音 slug)...")
posts = conn.execute(text("""
SELECT id, title, slug, content, content_type, summary, cover_image,
category_id, is_published, is_draft, view_count, created_at, updated_at
FROM news_posts
ORDER BY id
""")).fetchall()
# 用于检测 slug 冲突
used_slugs = set()
slug_conflicts = 0
migrated_count = 0
skipped_count = 0
for post in posts:
(old_id, title, old_slug, content, content_type, summary, cover_image,
category_id, is_published, is_draft, view_count, created_at, updated_at) = post
# 只迁移已发布或有效的文章
# 状态映射: is_published=True -> published, 否则 draft
status = "published" if is_published else "draft"
# 重新生成拼音 slug
base_slug = generate_slug(title)
new_slug = base_slug
suffix = 1
while new_slug in used_slugs:
new_slug = f"{base_slug}-{suffix}"
suffix += 1
slug_conflicts += 1
used_slugs.add(new_slug)
# 栏目映射
new_column_id = cat_id_map.get(category_id)
if not new_column_id:
print(f" 跳过 post_id={old_id},category_id={category_id} 未找到映射")
skipped_count += 1
continue
conn.execute(text("""
INSERT INTO articles (title, slug, content, content_type, summary, cover_image,
column_id, author, status, sort_order, view_count, created_at, updated_at)
VALUES (:title, :slug, :content, :content_type, :summary, :cover_image,
:column_id, '', :status, 0, :view_count, :created_at, :updated_at)
"""), {
"title": title,
"slug": new_slug,
"content": content or "",
"content_type": content_type or "markdown",
"summary": summary or "",
"cover_image": cover_image or "",
"column_id": new_column_id,
"status": status,
"view_count": view_count or 0,
"created_at": created_at,
"updated_at": updated_at
})
migrated_count += 1
if migrated_count % 50 == 0:
print(f" 已迁移 {migrated_count} / {len(posts)} 篇...")
print(f" 共迁移 {migrated_count} 篇,跳过 {skipped_count} 篇,slug 冲突解决 {slug_conflicts} 次")
# ========== 阶段 4: 验证 ==========
print("\n[阶段 4] 验证迁移结果...")
col_count = conn.execute(text("SELECT COUNT(*) FROM columns")).fetchone()[0]
art_count = conn.execute(text("SELECT COUNT(*) FROM articles")).fetchone()[0]
top_col = conn.execute(text("SELECT id, name, slug FROM columns WHERE parent_id IS NULL")).fetchone()
child_cols = conn.execute(text("SELECT COUNT(*) FROM columns WHERE parent_id IS NOT NULL")).fetchone()[0]
print(f" columns 表总数: {col_count}")
print(f" 顶级栏目: {top_col}")
print(f" 子栏目数: {child_cols}")
print(f" articles 表总数: {art_count}")
# 每个子栏目下的文章数
print("\n 各子栏目文章分布:")
for row in conn.execute(text("""
SELECT c.name, COUNT(a.id) as cnt
FROM columns c
LEFT JOIN articles a ON a.column_id = c.id
WHERE c.parent_id IS NOT NULL
GROUP BY c.id
ORDER BY cnt DESC
""")):
print(f" {row[0]}: {row[1]} 篇")
trans.commit()
print("\n" + "=" * 60)
print("迁移成功完成!")
print("=" * 60)
except Exception as e:
trans.rollback()
print(f"\n迁移失败,已回滚: {e}")
import traceback
traceback.print_exc()
sys.exit(1)
if __name__ == "__main__":
migrate()