2026-04-29 00:37:45 +08:00
|
|
|
"""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),
|
2026-05-21 16:26:58 +08:00
|
|
|
sa.Column(
|
|
|
|
|
"anonymized_at",
|
|
|
|
|
sa.DateTime(timezone=True),
|
|
|
|
|
server_default=sa.text("now()"),
|
|
|
|
|
nullable=False,
|
|
|
|
|
),
|
2026-04-29 00:37:45 +08:00
|
|
|
sa.PrimaryKeyConstraint("id"),
|
|
|
|
|
)
|
2026-05-21 16:26:58 +08:00
|
|
|
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"],
|
|
|
|
|
)
|
2026-04-29 00:37:45 +08:00
|
|
|
_enable_service_role_all_rls("anonymous_session_snapshots")
|
|
|
|
|
|
|
|
|
|
op.create_table(
|
|
|
|
|
"user_feedback",
|
2026-05-21 16:26:58 +08:00
|
|
|
sa.Column(
|
|
|
|
|
"id",
|
|
|
|
|
postgresql.UUID(as_uuid=True),
|
|
|
|
|
server_default=sa.text("gen_random_uuid()"),
|
|
|
|
|
nullable=False,
|
|
|
|
|
),
|
2026-04-29 00:37:45 +08:00
|
|
|
sa.Column("user_id", postgresql.UUID(as_uuid=True), nullable=True),
|
2026-05-21 16:26:58 +08:00
|
|
|
sa.Column(
|
|
|
|
|
"feedback_type",
|
|
|
|
|
sa.String(length=20),
|
|
|
|
|
server_default=sa.text("'other'"),
|
|
|
|
|
nullable=False,
|
|
|
|
|
),
|
2026-04-29 00:37:45 +08:00
|
|
|
sa.Column("content", sa.Text(), nullable=False),
|
2026-05-21 16:26:58 +08:00
|
|
|
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,
|
|
|
|
|
),
|
2026-04-29 00:37:45 +08:00
|
|
|
sa.Column("app_version", sa.String(length=20), nullable=False),
|
|
|
|
|
sa.Column("os_version", sa.String(length=50), nullable=False),
|
2026-05-21 16:26:58 +08:00
|
|
|
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,
|
|
|
|
|
),
|
2026-04-29 00:37:45 +08:00
|
|
|
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 '用户反馈表'")
|
2026-05-21 16:26:58 +08:00
|
|
|
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'"
|
|
|
|
|
)
|
2026-04-29 00:37:45 +08:00
|
|
|
op.execute("COMMENT ON COLUMN user_feedback.content IS '反馈内容,最多500字'")
|
2026-05-21 16:26:58 +08:00
|
|
|
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'"
|
|
|
|
|
)
|
2026-04-29 00:37:45 +08:00
|
|
|
_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")
|
2026-05-21 16:26:58 +08:00
|
|
|
op.execute(
|
|
|
|
|
f"CREATE POLICY service_role_all_{table_name} ON {table_name} FOR ALL TO service_role USING (true) WITH CHECK (true)"
|
|
|
|
|
)
|
2026-04-29 00:37:45 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
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")
|