193 lines
8.2 KiB
Python
193 lines
8.2 KiB
Python
"""
|
||
新闻资讯模块迁移脚本
|
||
将老的新闻分类(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()
|