"""Create independent detail presentation records for frontend pages. Revision ID: 0021_detail_records Revises: 0020_home_team_building_details """ from datetime import datetime from alembic import context, op import sqlalchemy as sa from sqlalchemy import inspect from sqlalchemy.dialects import postgresql from app.migration_compat import baseline_table_names revision = "0021_detail_records" down_revision = "0020_home_team_building_details" branch_labels = None depends_on = None DETAIL_RECORD_INDEXES = { "ix_DetailRecord_isActive": ["isActive"], "ix_DetailRecord_sortOrder": ["sortOrder"], } def missing_detail_record_indexes(existing_indexes: set[str]) -> set[str]: return set(DETAIL_RECORD_INDEXES).difference(existing_indexes) def build_detail_seed_rows(routes, existing_keys: set[str], timestamp: datetime) -> list[dict]: return [ { "id": f"detail-{route['id']}", "key": route["id"], "eyebrow": "玩法推荐", "duration": "5天4晚", "title": route["title"], "subtitle": route["subtitle"], "intro": route["subtitle"], "highlights": ["核心景观串联", "小团出行,按同行人的节奏灵活调整"], "included": ["行程内用车与接送服务"], "excluded": ["往返大交通及个人消费"], "notes": ["最终方案以出行日期、人数和资源确认结果为准。"], "gallery": [route["image"]], "isActive": True, "sortOrder": route["sortOrder"], "createdAt": timestamp, "updatedAt": timestamp, } for route in routes if route["id"] not in existing_keys ] def is_offline_mode() -> bool: try: return context.is_offline_mode() except (NameError, AttributeError): return False def _create_detail_table() -> None: op.create_table( "DetailRecord", sa.Column("id", sa.String(), nullable=False), sa.Column("key", sa.String(), nullable=False), sa.Column("eyebrow", sa.String(), nullable=False), sa.Column("duration", sa.String(), nullable=False), sa.Column("title", sa.String(), nullable=False), sa.Column("subtitle", sa.Text(), nullable=False), sa.Column("intro", sa.Text(), nullable=False), sa.Column("highlights", postgresql.JSONB(astext_type=sa.Text()), nullable=False, server_default=sa.text("'[]'::jsonb")), sa.Column("included", postgresql.JSONB(astext_type=sa.Text()), nullable=False, server_default=sa.text("'[]'::jsonb")), sa.Column("excluded", postgresql.JSONB(astext_type=sa.Text()), nullable=False, server_default=sa.text("'[]'::jsonb")), sa.Column("notes", postgresql.JSONB(astext_type=sa.Text()), nullable=False, server_default=sa.text("'[]'::jsonb")), sa.Column("gallery", postgresql.JSONB(astext_type=sa.Text()), nullable=False, server_default=sa.text("'[]'::jsonb")), sa.Column("isActive", sa.Boolean(), nullable=False, server_default=sa.text("true")), sa.Column("sortOrder", sa.Integer(), nullable=False, server_default="0"), sa.Column("createdAt", sa.DateTime(), nullable=False), sa.Column("updatedAt", sa.DateTime(), nullable=False), sa.PrimaryKeyConstraint("id"), sa.UniqueConstraint("key"), ) def upgrade() -> None: if is_offline_mode(): table_exists = "DetailRecord" in baseline_table_names() existing_index_names = set() else: bind = op.get_bind() inspector = inspect(bind) table_exists = "DetailRecord" in set(inspector.get_table_names()) existing_index_names = { index["name"] for index in inspector.get_indexes("DetailRecord") } if table_exists else set() created_table = not table_exists if created_table: _create_detail_table() existing_index_names = set() if created_table: for index_name in missing_detail_record_indexes(existing_index_names): op.create_index(index_name, "DetailRecord", DETAIL_RECORD_INDEXES[index_name]) detail_table = sa.table( "DetailRecord", sa.column("id", sa.String()), sa.column("key", sa.String()), sa.column("eyebrow", sa.String()), sa.column("duration", sa.String()), sa.column("title", sa.String()), sa.column("subtitle", sa.Text()), sa.column("intro", sa.Text()), sa.column("highlights", postgresql.JSONB()), sa.column("included", postgresql.JSONB()), sa.column("excluded", postgresql.JSONB()), sa.column("notes", postgresql.JSONB()), sa.column("gallery", postgresql.JSONB()), sa.column("isActive", sa.Boolean()), sa.column("sortOrder", sa.Integer()), sa.column("createdAt", sa.DateTime()), sa.column("updatedAt", sa.DateTime()), ) timestamp = datetime(2026, 1, 1) if is_offline_mode(): return conn = op.get_bind() routes = conn.execute( sa.text('SELECT "id", "title", "subtitle", "image", "sortOrder" FROM "WanfaRoute" ORDER BY "sortOrder" ASC') ).mappings().all() existing_key_result = conn.execute(sa.text('SELECT "key" FROM "DetailRecord"')) if hasattr(existing_key_result, "scalars"): existing_key_values = existing_key_result.scalars() elif hasattr(existing_key_result, "all"): existing_key_values = [row[0] for row in existing_key_result.all()] else: existing_key_values = existing_key_result existing_keys = set(existing_key_values) seed_rows = build_detail_seed_rows(routes, existing_keys, timestamp) if seed_rows: op.bulk_insert(detail_table, seed_rows) def downgrade() -> None: op.drop_index("ix_DetailRecord_sortOrder", table_name="DetailRecord") op.drop_index("ix_DetailRecord_isActive", table_name="DetailRecord") op.drop_table("DetailRecord")