2026-03-13 11:12:10 +08:00
|
|
|
|
"""
|
|
|
|
|
|
将 chapters 的 content + images 迁移到 chapter_sections,并删除 chapters.content / chapters.images。
|
|
|
|
|
|
|
|
|
|
|
|
前置:已执行 api/migrations/add_chapter_sections.sql(创建 chapter_sections 表、chapters.cover_image 列)。
|
|
|
|
|
|
|
|
|
|
|
|
用法(在 api 目录下):
|
|
|
|
|
|
python -m scripts.migrate_chapters_to_sections
|
|
|
|
|
|
"""
|
|
|
|
|
|
import json
|
|
|
|
|
|
import os
|
|
|
|
|
|
import sys
|
|
|
|
|
|
import uuid
|
|
|
|
|
|
|
|
|
|
|
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
|
|
|
|
|
|
|
|
|
|
from sqlalchemy import text
|
2026-03-18 17:18:23 +08:00
|
|
|
|
from app.core.db import sync_engine as engine
|
|
|
|
|
|
from app.features.memoir.memoir_images.parser import split_narrative_to_sections
|
|
|
|
|
|
from app.core.logging import get_logger, setup_logging
|
2026-03-13 11:12:10 +08:00
|
|
|
|
|
2026-03-18 17:18:23 +08:00
|
|
|
|
setup_logging()
|
|
|
|
|
|
logger = get_logger(__name__)
|
2026-03-13 11:12:10 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def run():
|
|
|
|
|
|
with engine.connect() as conn:
|
|
|
|
|
|
# 检查是否还存在 content 列(若已删则跳过)
|
|
|
|
|
|
r = conn.execute(text("""
|
|
|
|
|
|
SELECT column_name FROM information_schema.columns
|
|
|
|
|
|
WHERE table_schema = 'public' AND table_name = 'chapters' AND column_name = 'content'
|
|
|
|
|
|
"""))
|
|
|
|
|
|
if r.fetchone() is None:
|
|
|
|
|
|
logger.info("chapters.content 已不存在,跳过迁移")
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
# 读取所有有 content 的章节(原始列)
|
|
|
|
|
|
rows = conn.execute(text("""
|
|
|
|
|
|
SELECT id, content, images FROM chapters WHERE content IS NOT NULL AND trim(content) != ''
|
|
|
|
|
|
""")).fetchall()
|
|
|
|
|
|
|
|
|
|
|
|
for row in rows:
|
|
|
|
|
|
ch_id, content, images_raw = row[0], row[1], row[2]
|
|
|
|
|
|
images = json.loads(images_raw) if isinstance(images_raw, str) else (images_raw or [])
|
|
|
|
|
|
if not isinstance(images, list):
|
|
|
|
|
|
images = []
|
|
|
|
|
|
|
|
|
|
|
|
sections = split_narrative_to_sections(content or "")
|
|
|
|
|
|
if not sections:
|
|
|
|
|
|
# 无占位符:整段为一条 section,无图
|
|
|
|
|
|
section_id = str(uuid.uuid4()).replace("-", "")[:32]
|
|
|
|
|
|
conn.execute(text("""
|
|
|
|
|
|
INSERT INTO chapter_sections (id, chapter_id, order_index, content, image, updated_at)
|
|
|
|
|
|
VALUES (:id, :ch_id, 0, :content, NULL, NOW())
|
|
|
|
|
|
"""), {"id": section_id, "ch_id": ch_id, "content": (content or "").strip()})
|
|
|
|
|
|
conn.commit()
|
|
|
|
|
|
logger.info("章节 %s: 1 条 section(无图)", ch_id)
|
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
|
|
first_cover = None
|
|
|
|
|
|
img_index = 0
|
|
|
|
|
|
for order_idx, seg in enumerate(sections):
|
|
|
|
|
|
section_id = str(uuid.uuid4()).replace("-", "")[:32]
|
|
|
|
|
|
seg_content = seg.get("content") or ""
|
|
|
|
|
|
ph = seg.get("placeholder_info")
|
|
|
|
|
|
image_json = None
|
|
|
|
|
|
if ph is not None and img_index < len(images):
|
|
|
|
|
|
image_json = json.dumps(images[img_index]) if isinstance(images[img_index], dict) else None
|
|
|
|
|
|
if first_cover is None and image_json:
|
|
|
|
|
|
first_cover = image_json
|
|
|
|
|
|
img_index += 1
|
|
|
|
|
|
|
|
|
|
|
|
conn.execute(text("""
|
|
|
|
|
|
INSERT INTO chapter_sections (id, chapter_id, order_index, content, image, updated_at)
|
|
|
|
|
|
VALUES (:id, :ch_id, :ord, :content, :img::jsonb, NOW())
|
|
|
|
|
|
"""), {
|
|
|
|
|
|
"id": section_id,
|
|
|
|
|
|
"ch_id": ch_id,
|
|
|
|
|
|
"ord": order_idx,
|
|
|
|
|
|
"content": seg_content,
|
|
|
|
|
|
"img": image_json,
|
|
|
|
|
|
})
|
|
|
|
|
|
if first_cover:
|
|
|
|
|
|
conn.execute(
|
|
|
|
|
|
text("UPDATE chapters SET cover_image = :img::jsonb WHERE id = :id"),
|
|
|
|
|
|
{"img": first_cover, "id": ch_id},
|
|
|
|
|
|
)
|
|
|
|
|
|
conn.commit()
|
|
|
|
|
|
logger.info("章节 %s: %d 条 sections", ch_id, len(sections))
|
|
|
|
|
|
|
|
|
|
|
|
# 删除 chapters.content 和 chapters.images
|
|
|
|
|
|
conn.execute(text("ALTER TABLE chapters DROP COLUMN IF EXISTS content"))
|
|
|
|
|
|
conn.execute(text("ALTER TABLE chapters DROP COLUMN IF EXISTS images"))
|
|
|
|
|
|
conn.commit()
|
|
|
|
|
|
logger.info("已删除 chapters.content 与 chapters.images")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
|
|
run()
|