"""Create compliance snapshot and feedback tables. Revision ID: 20260428_squash_0004 Revises: 20260428_squash_0003 Create Date: 2026-04-15 00:10:00 Squashed history: keeps iOS anonymous-session snapshots and user feedback as separate product surfaces while removing the intervening unrelated revisions. """ from typing import Sequence, Union from alembic import op import sqlalchemy as sa from sqlalchemy.dialects import postgresql revision: str = "20260428_squash_0004" down_revision: Union[str, Sequence[str], None] = "20260428_squash_0003" branch_labels: Union[str, Sequence[str], None] = None depends_on: Union[str, Sequence[str], None] = None def upgrade() -> None: op.create_table( "anonymous_session_snapshots", sa.Column("id", sa.UUID(), nullable=False), sa.Column("anonymous_id", sa.UUID(), nullable=False), sa.Column("session_type", sa.String(length=20), nullable=False), sa.Column("message_count", sa.Integer(), nullable=True), sa.Column("status", sa.String(length=20), nullable=True), sa.Column("question_type", sa.String(length=50), nullable=True), sa.Column("tool_name", sa.String(length=100), nullable=True), sa.Column("gua_name", sa.String(length=50), nullable=True), sa.Column("gua_name_hant", sa.String(length=50), nullable=True), sa.Column("target_gua_name", sa.String(length=50), nullable=True), sa.Column("has_changing_yao", sa.Boolean(), nullable=True), sa.Column("sign_level", sa.String(length=20), nullable=True), sa.Column("keywords", postgresql.ARRAY(sa.Text()), nullable=True), sa.Column("model_code", sa.String(length=50), nullable=True), sa.Column("total_tokens", sa.Integer(), nullable=True), sa.Column("total_cost", sa.Numeric(12, 6), nullable=True), sa.Column("total_latency_ms", sa.Integer(), nullable=True), sa.Column("created_at", sa.DateTime(timezone=True), nullable=False), sa.Column("last_activity_at", sa.DateTime(timezone=True), nullable=True), sa.Column("anonymized_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False), sa.PrimaryKeyConstraint("id"), ) op.create_index("ix_anonymous_session_snapshots_anonymous_id", "anonymous_session_snapshots", ["anonymous_id"]) op.create_index("ix_anonymous_session_snapshots_created_at", "anonymous_session_snapshots", ["created_at"]) op.create_index("ix_anonymous_session_snapshots_question_type", "anonymous_session_snapshots", ["question_type"]) _enable_service_role_all_rls("anonymous_session_snapshots") op.create_table( "user_feedback", sa.Column("id", postgresql.UUID(as_uuid=True), server_default=sa.text("gen_random_uuid()"), nullable=False), sa.Column("user_id", postgresql.UUID(as_uuid=True), nullable=True), sa.Column("feedback_type", sa.String(length=20), server_default=sa.text("'other'"), nullable=False), sa.Column("content", sa.Text(), nullable=False), sa.Column("images", postgresql.JSONB(astext_type=sa.Text()), server_default=sa.text("'[]'::jsonb"), nullable=False), sa.Column("device_info", postgresql.JSONB(astext_type=sa.Text()), server_default=sa.text("'{}'::jsonb"), nullable=False), sa.Column("app_version", sa.String(length=20), nullable=False), sa.Column("os_version", sa.String(length=50), nullable=False), sa.Column("status", sa.String(length=20), server_default=sa.text("'pending'"), nullable=False), sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False), sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False), sa.ForeignKeyConstraint(["user_id"], ["auth.users.id"], ondelete="SET NULL"), sa.PrimaryKeyConstraint("id"), ) op.create_index("ix_user_feedback_user_id", "user_feedback", ["user_id"]) op.create_index("ix_user_feedback_created_at", "user_feedback", ["created_at"]) op.create_index("ix_user_feedback_status", "user_feedback", ["status"]) op.execute("COMMENT ON TABLE user_feedback IS '用户反馈表'") op.execute("COMMENT ON COLUMN user_feedback.user_id IS '用户ID,NULL表示匿名(勾选不上传我的个人信息)'") op.execute("COMMENT ON COLUMN user_feedback.feedback_type IS '反馈类型: bug/suggestion/other'") op.execute("COMMENT ON COLUMN user_feedback.content IS '反馈内容,最多500字'") op.execute("COMMENT ON COLUMN user_feedback.images IS '图片Storage路径列表,最多3张'") op.execute("COMMENT ON COLUMN user_feedback.device_info IS '设备信息JSON,匿名时照样采集(不涉及隐私)'") op.execute("COMMENT ON COLUMN user_feedback.status IS '处理状态: pending/processed'") _enable_service_role_all_rls("user_feedback") def downgrade() -> None: _drop_service_role_all_rls("user_feedback") op.drop_table("user_feedback") _drop_service_role_all_rls("anonymous_session_snapshots") op.drop_table("anonymous_session_snapshots") def _enable_service_role_all_rls(table_name: str) -> None: op.execute(f"ALTER TABLE {table_name} ENABLE ROW LEVEL SECURITY") op.execute(f"CREATE POLICY service_role_all_{table_name} ON {table_name} FOR ALL TO service_role USING (true) WITH CHECK (true)") def _drop_service_role_all_rls(table_name: str) -> None: op.execute(f"DROP POLICY IF EXISTS service_role_all_{table_name} ON {table_name}") op.execute(f"ALTER TABLE {table_name} DISABLE ROW LEVEL SECURITY")