"""migrate sections to canonical_markdown 将旧章节(有 sections 但 canonical_markdown 为空)从 sections 推导并写入 canonical_markdown。 同时创建 chapter_version 记录(source_type=migration)。 Revision ID: 0004_migrate_md Revises: 0003_story_first Create Date: 2026-03-19 """ from typing import Sequence, Union import sqlalchemy as sa from alembic import op from sqlalchemy.orm import Session, selectinload revision: str = "0004_migrate_md" down_revision: Union[str, Sequence[str], None] = "0003_story_first" branch_labels: Union[str, Sequence[str], None] = None depends_on: Union[str, Sequence[str], None] = None def _sections_to_markdown(chapter) -> str: """从 sections 推导 markdown,与 helpers.sections_to_content_and_images 一致。""" sections = getattr(chapter, "sections", None) or [] ordered = sorted(sections, key=lambda s: getattr(s, "order_index", 0)) parts = [] for s in ordered: text = (getattr(s, "content", None) or "").strip() if text: parts.append(text) img = _section_image_to_dict(s) if img: placeholder = (img.get("placeholder") or "").strip() if placeholder: parts.append(placeholder) return "\n\n".join(parts) if parts else "" def _section_image_to_dict(section) -> dict | None: """与 helpers.section_image_to_dict 一致。""" from app.features.memoir.memoir_images.serializers import memoir_image_to_dict if getattr(section, "image_record", None): return memoir_image_to_dict(section.image_record) return None def upgrade() -> None: from app.features.memoir.models import Chapter, ChapterSection, ChapterVersion conn = op.get_bind() session = Session(bind=conn) chapters = ( session.query(Chapter) .options( selectinload(Chapter.sections).selectinload(ChapterSection.image_record), ) .filter( sa.or_( Chapter.canonical_markdown.is_(None), Chapter.canonical_markdown == "", ), ) .all() ) for ch in chapters: md = _sections_to_markdown(ch) if not md.strip(): continue # 创建 chapter_version(source_type=migration) import uuid from sqlalchemy import func count_stmt = sa.select(func.count(ChapterVersion.id)).where( ChapterVersion.chapter_id == ch.id ) version_no = (session.execute(count_stmt).scalar() or 0) + 1 version = ChapterVersion( id=str(uuid.uuid4()), chapter_id=ch.id, version_no=version_no, markdown_snapshot=md, actor_type="system", source_type="migration", ) session.add(version) session.flush() ch.canonical_markdown = md ch.current_version_id = version.id # 由 alembic context 管理事务提交 session.close() def downgrade() -> None: # 数据迁移不可逆,downgrade 不清理 canonical_markdown pass