"""admin_role 表(RBAC 角色 → 可见页面)+ 种子内建角色 新增后台 RBAC:角色持有一组页面 key,登录后左侧只展示这些页。super_admin 内建全权(pages 存空、 effective_pages 特判为全部)、不可编辑删除;operator/finance 播默认页集,可由 super_admin 增删改。 admin_user.role 弱引用本表 name。全新环境顺序应用即得三条内建角色。 Revision ID: admin_role_table Revises: comparison_product_names Create Date: 2026-07-04 00:00:00.000000 """ from collections.abc import Sequence import sqlalchemy as sa from sqlalchemy.dialects import postgresql from alembic import op revision: str = "admin_role_table" down_revision: str | Sequence[str] | None = "comparison_product_names" branch_labels: str | Sequence[str] | None = None depends_on: str | Sequence[str] | None = None _JSON = sa.JSON().with_variant(postgresql.JSONB(), "postgresql") # 与 app/admin/permissions.py 的 BUILTIN_ROLES 同口径(迁移内联一份,不耦合 app 常量)。 # 页集对齐 Prototypes/dashboard/permissions.md 的 ROLES。key 承重(require_role),label 为展示名。 _BUILTIN = [ {"name": "super_admin", "label": "管理员", "pages": [], "is_builtin": True}, {"name": "operator", "label": "运营", "pages": [ "dashboard", "coupon-data", "ad-revenue-report", "comparison-records", "cps", "device-liveness", "price-reports", "feedbacks", ], "is_builtin": False}, {"name": "finance", "label": "财务", "pages": [ "dashboard", "ad-revenue-report", "cps", "withdraws", ], "is_builtin": False}, {"name": "tech", "label": "技术", "pages": [ "dashboard", "device-liveness", "config", "ad-revenue", "event-logs", "audit-logs", ], "is_builtin": False}, ] def upgrade() -> None: op.create_table( "admin_role", sa.Column("id", sa.Integer(), primary_key=True, autoincrement=True), sa.Column("name", sa.String(length=32), nullable=False), sa.Column("label", sa.String(length=32), nullable=False, server_default=""), sa.Column("pages", _JSON, nullable=False), sa.Column("is_builtin", sa.Boolean(), nullable=False, server_default=sa.false()), sa.Column( "created_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False, ), ) op.create_index("ix_admin_role_name", "admin_role", ["name"], unique=True) tbl = sa.table( "admin_role", sa.column("name", sa.String), sa.column("label", sa.String), sa.column("pages", _JSON), sa.column("is_builtin", sa.Boolean), ) op.bulk_insert(tbl, _BUILTIN) def downgrade() -> None: op.drop_index("ix_admin_role_name", table_name="admin_role") op.drop_table("admin_role")