"""init content tables Revision ID: 0001_init_content_tables Revises: Create Date: 2026-02-01 """ from __future__ import annotations from alembic import op import sqlalchemy as sa from sqlalchemy.dialects import mysql # revision identifiers, used by Alembic. revision = "0001_init_content_tables" down_revision = None branch_labels = None depends_on = None def upgrade() -> None: op.create_table( "contents", sa.Column( "content_id", mysql.BIGINT(unsigned=True), primary_key=True, autoincrement=True, comment="文案唯一 ID(自增;文案微调时保持不变)", ), sa.Column("text_en", sa.Text(), nullable=True, comment="英文文案(可空;若为空则必须提供 text_tc)"), sa.Column("text_tc", sa.Text(), nullable=True, comment="繁体中文文案(可空;若为空则必须提供 text_en)"), sa.Column("author_id", sa.String(length=255), nullable=True, comment="作者/来源 ID(可空;用于多样性与频控)"), sa.Column("template_id", sa.String(length=255), nullable=True, comment="模板 ID(可空;用于多样性与频控)"), sa.Column("created_at", sa.DateTime(), server_default=sa.func.now(), nullable=False, comment="创建时间"), sa.Column("updated_at", sa.DateTime(), server_default=sa.func.now(), nullable=False, comment="更新时间"), sa.CheckConstraint( "(text_en IS NOT NULL) OR (text_tc IS NOT NULL)", name="chk_contents_text_present", ), mysql_charset="utf8mb4", ) op.create_index("idx_contents_author_id", "contents", ["author_id"], unique=False) op.create_index("idx_contents_template_id", "contents", ["template_id"], unique=False) op.create_table( "content_profiles", sa.Column( "content_id", mysql.BIGINT(unsigned=True), sa.ForeignKey("contents.content_id", ondelete="CASCADE"), primary_key=True, comment="FK -> contents.content_id", ), sa.Column( "stage", sa.Enum("general", "expecting", "parenting", "unknown", name="content_stage"), server_default="general", nullable=False, comment="母职阶段定位(general/expecting/parenting/unknown)", ), sa.Column("emotion_score", sa.Numeric(3, 2), nullable=True, comment="情绪调性 0~1;NULL 表示 general"), sa.Column( "context_suitability_json", sa.JSON(), nullable=False, comment="各 context 的适配度(JSON:0/0.5/1;必须包含 5 个 key)", ), sa.Column( "need_suitability_json", sa.JSON(), nullable=False, comment="各 need 的适配度(JSON:0/0.5/1;必须包含 5 个 key)", ), sa.Column( "personalization_power", sa.SmallInteger(), server_default="0", nullable=False, comment="个性化力度(约定只允许 0/5/10,分别映射 0/0.5/1)", ), sa.Column( "review_confidence", sa.Numeric(3, 2), nullable=True, comment="标注置信度 0~1;NULL 表示由推荐侧按 0.7 兜底", ), sa.Column( "is_safe_pool", sa.Boolean(), server_default=sa.text("0"), nullable=False, comment="是否属于通用安全池(L3 兜底)", ), sa.Column("updated_at", sa.DateTime(), server_default=sa.func.now(), nullable=False, comment="画像更新时间"), mysql_charset="utf8mb4", ) op.create_index("idx_profiles_is_safe_pool", "content_profiles", ["is_safe_pool"], unique=False) op.create_index("idx_profiles_personalization_power", "content_profiles", ["personalization_power"], unique=False) op.create_index("idx_profiles_stage", "content_profiles", ["stage"], unique=False) op.create_table( "content_risk_flags", sa.Column( "id", mysql.BIGINT(unsigned=True), primary_key=True, autoincrement=True, comment="主键", ), sa.Column( "content_id", mysql.BIGINT(unsigned=True), sa.ForeignKey("contents.content_id", ondelete="CASCADE"), nullable=False, comment="FK -> contents.content_id", ), sa.Column( "flag", sa.String(length=64), nullable=False, comment="风险标记(unsafe_for_* / block_* / soft_*)", ), sa.Column("created_at", sa.DateTime(), server_default=sa.func.now(), nullable=False, comment="创建时间"), sa.UniqueConstraint("content_id", "flag", name="uniq_content_flag"), mysql_charset="utf8mb4", ) op.create_index("idx_content_id", "content_risk_flags", ["content_id"], unique=False) op.create_index("idx_flag", "content_risk_flags", ["flag"], unique=False) def downgrade() -> None: op.drop_index("idx_flag", table_name="content_risk_flags") op.drop_index("idx_content_id", table_name="content_risk_flags") op.drop_table("content_risk_flags") op.drop_index("idx_profiles_stage", table_name="content_profiles") op.drop_index("idx_profiles_personalization_power", table_name="content_profiles") op.drop_index("idx_profiles_is_safe_pool", table_name="content_profiles") op.drop_table("content_profiles") op.drop_index("idx_contents_template_id", table_name="contents") op.drop_index("idx_contents_author_id", table_name="contents") op.drop_table("contents")