""" 将 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 logging import os import sys import uuid sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) from sqlalchemy import text from database.database import engine from services.memoir_images.parser import split_narrative_to_sections logging.basicConfig(level=logging.INFO, format="%(message)s") logger = logging.getLogger(__name__) 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()