import os import sys from fastapi import HTTPException from sqlalchemy import select from sqlalchemy.orm import Session from app.db import Base, SessionLocal, get_engine from app.models.rbac import AdminResource, AdminRole, AdminRoleResource, AdminUser, AdminUserRole, utcnow from app.services.password import hash_password, validate_password SUPER_CODE = "R_SUPER" SUPER_NAME = "超级管理员" PAGES = [ { "name": "User", "title": "用户管理", "path": "/system/user", "component": "/system/user", "sort": 1, "buttons": [ ("user:add", "新增"), ("user:edit", "编辑"), ("user:reset-password", "重置密码"), ("user:disable", "停用"), ("user:cancel", "注销"), ], }, { "name": "Role", "title": "角色管理", "path": "/system/role", "component": "/system/role", "sort": 2, "buttons": [ ("role:add", "新增"), ("role:edit", "编辑"), ("role:disable", "停用"), ("role:grant", "授权"), ], }, { "name": "Menu", "title": "菜单管理", "path": "/system/menu", "component": "/system/menu", "sort": 3, "buttons": [ ("menu:add", "新增"), ("menu:edit", "编辑"), ("menu:delete", "删除"), ], }, { "name": "AuditLog", "title": "操作日志", "path": "/system/audit-log", "component": "/system/audit-log", "sort": 4, "buttons": [], }, { "name": "LoginSecurity", "title": "登录安全", "path": "/system/login-security", "component": "/system/login-security", "sort": 5, "buttons": [("security:edit", "编辑")], }, { "name": "File", "title": "附件", "path": "/system/file", "component": "/system/file", "sort": 6, "buttons": [("file:upload", "上传"), ("file:delete", "删除")], }, { "name": "Site", "title": "站点配置", "path": "/system/site", "component": "/system/site", "sort": 7, "buttons": [("site:edit", "编辑")], }, { "name": "SensitiveWord", "title": "敏感词", "path": "/system/sensitive-word", "component": "/system/sensitive-word", "sort": 8, "buttons": [("word:add", "新增"), ("word:delete", "删除"), ("word:preview", "预览")], }, ] def _find_resource(db: Session, parent_id: int | None, name: str) -> AdminResource | None: stmt = select(AdminResource).where(AdminResource.name == name) if parent_id is None: stmt = stmt.where(AdminResource.parent_id.is_(None)) else: stmt = stmt.where(AdminResource.parent_id == parent_id) return db.scalars(stmt.order_by(AdminResource.id.asc())).first() def _ensure_resource( db: Session, *, parent_id: int | None, resource_type: str, name: str, title: str, path: str, component: str, icon: str, permission: str, sort: int, visible: bool, ) -> AdminResource: row = _find_resource(db, parent_id, name) if row is None: row = AdminResource( parent_id=parent_id, type=resource_type, name=name, title=title, path=path, component=component, icon=icon, permission=permission, sort=sort, visible=visible, status="active", ) db.add(row) db.flush() return row row.parent_id = parent_id row.type = resource_type row.title = title row.path = path row.component = component row.icon = icon row.permission = permission row.sort = sort row.visible = visible row.status = "active" return row HOME_MENUS = ( ("HomeTemplate", "模板", "/home/template", "ri:layout-2-line", 1), ("HomeSlides", "轮播", "/home/slides", "ri:slideshow-3-line", 2), ("HomeArticles", "文章", "/home/articles", "ri:article-line", 3), ("HomeEntries", "入口与菜单", "/home/entries", "ri:apps-2-line", 4), ("HomeMarquees", "荣誉与会员", "/home/marquees", "ri:award-line", 5), ("HomeFooter", "页脚", "/home/footer", "ri:layout-bottom-line", 6), ) ABOUT_ORG_MENUS = ( ("AboutIntro", "协会简介", "/about/intro", "ri:building-line", 1), ("AboutOrg", "组织机构", "/about/org", "ri:node-tree", 2), ("AboutExperts", "专家委员会", "/about/experts", "ri:team-line", 3), ("AboutReports", "年度报告/工作计划", "/about/reports", "ri:file-list-3-line", 4), ("AboutBriefs", "工作简报", "/about/briefs", "ri:newspaper-line", 5), ) def ensure_home_menu(db: Session) -> None: directory = _ensure_resource( db, parent_id=None, resource_type="directory", name="Home", title="首页配置", path="/home", component="", icon="ri:home-4-line", permission="", sort=2, visible=True, ) legacy = _find_resource(db, directory.id, "HomeConfig") if legacy is not None and _find_resource(db, directory.id, "HomeTemplate") is None: legacy.name = "HomeTemplate" db.flush() pages = [ _ensure_resource( db, parent_id=directory.id, resource_type="menu", name=name, title=title, path=path, component=path, icon=icon, permission="", sort=sort, visible=True, ) for name, title, path, icon, sort in HOME_MENUS ] button = _ensure_resource( db, parent_id=pages[0].id, resource_type="button", name="home:edit", title="保存", path="", component="", icon="", permission="home:edit", sort=1, visible=False, ) role = db.scalar(select(AdminRole).where(AdminRole.code == SUPER_CODE)) if role is None: db.flush() return linked = set( db.scalars( select(AdminRoleResource.resource_id).where(AdminRoleResource.role_id == role.id) ).all() ) for resource_id in (directory.id, button.id, *(page.id for page in pages)): if resource_id not in linked: db.add(AdminRoleResource(role_id=role.id, resource_id=resource_id)) db.flush() def ensure_party_menu(db: Session) -> None: party = _ensure_resource( db, parent_id=None, resource_type="menu", name="PartyBuilding", title="行业党建", path="/party", component="/party", icon="iconoven:national-emblem", permission="", sort=5, visible=True, ) button = _ensure_resource( db, parent_id=party.id, resource_type="button", name="party:edit", title="保存", path="", component="", icon="", permission="party:edit", sort=1, visible=False, ) role = db.scalar(select(AdminRole).where(AdminRole.code == SUPER_CODE)) if role is None: db.flush() return linked = set( db.scalars( select(AdminRoleResource.resource_id).where(AdminRoleResource.role_id == role.id) ).all() ) for resource_id in (party.id, button.id): if resource_id not in linked: db.add(AdminRoleResource(role_id=role.id, resource_id=resource_id)) db.flush() def ensure_expert_menu(db: Session) -> None: directory = _ensure_resource( db, parent_id=None, resource_type="directory", name="ExpertService", title="专家服务", path="/expert", component="", icon="ri:customer-service-2-line", permission="", sort=6, visible=True, ) guides = _ensure_resource( db, parent_id=directory.id, resource_type="menu", name="ExpertGuides", title="估价指南", path="/expert/guides", component="/expert/guides", icon="ri:book-open-line", permission="", sort=1, visible=True, ) qa = _ensure_resource( db, parent_id=directory.id, resource_type="menu", name="ExpertQa", title="专家答疑", path="/expert/qa", component="/expert/qa", icon="ri:question-answer-line", permission="", sort=2, visible=True, ) edit_button = _ensure_resource( db, parent_id=guides.id, resource_type="button", name="expert:edit", title="保存", path="", component="", icon="", permission="expert:edit", sort=1, visible=False, ) reply_button = _ensure_resource( db, parent_id=qa.id, resource_type="button", name="expert:reply", title="回复", path="", component="", icon="", permission="expert:reply", sort=1, visible=False, ) role = db.scalar(select(AdminRole).where(AdminRole.code == SUPER_CODE)) if role is None: db.flush() return linked = set( db.scalars( select(AdminRoleResource.resource_id).where(AdminRoleResource.role_id == role.id) ).all() ) for resource_id in (directory.id, guides.id, qa.id, edit_button.id, reply_button.id): if resource_id not in linked: db.add(AdminRoleResource(role_id=role.id, resource_id=resource_id)) db.flush() def ensure_directory_menu(db: Session) -> None: directory = _ensure_resource( db, parent_id=None, resource_type="menu", name="InstitutionDirectory", title="机构名录", path="/institutions", component="/institutions", icon="ri:building-2-line", permission="", sort=8, visible=True, ) button = _ensure_resource( db, parent_id=directory.id, resource_type="button", name="directory:edit", title="保存", path="", component="", icon="", permission="directory:edit", sort=1, visible=False, ) role = db.scalar(select(AdminRole).where(AdminRole.code == SUPER_CODE)) if role is None: db.flush() return linked = set( db.scalars( select(AdminRoleResource.resource_id).where(AdminRoleResource.role_id == role.id) ).all() ) for resource_id in (directory.id, button.id): if resource_id not in linked: db.add(AdminRoleResource(role_id=role.id, resource_id=resource_id)) db.flush() def ensure_library_menu(db: Session) -> None: directory = _ensure_resource( db, parent_id=None, resource_type="directory", name="Library", title="资料库", path="/library", component="", icon="ri:folder-open-line", permission="", sort=9, visible=True, ) policies = _ensure_resource( db, parent_id=directory.id, resource_type="menu", name="LibraryPolicies", title="政策分析", path="/library/policies", component="/library/policies", icon="ri:file-text-line", permission="", sort=1, visible=True, ) cases = _ensure_resource( db, parent_id=directory.id, resource_type="menu", name="LibraryCases", title="案例库", path="/library/cases", component="/library/cases", icon="ri:archive-line", permission="", sort=2, visible=True, ) policy_button = _ensure_resource( db, parent_id=policies.id, resource_type="button", name="library:edit", title="保存", path="", component="", icon="", permission="library:edit", sort=1, visible=False, ) case_button = _ensure_resource( db, parent_id=cases.id, resource_type="button", name="library:edit", title="保存", path="", component="", icon="", permission="library:edit", sort=1, visible=False, ) uploads = _ensure_resource( db, parent_id=directory.id, resource_type="menu", name="LibraryUploads", title="上传的案例库", path="/library/uploads", component="/library/uploads", icon="ri:upload-2-line", permission="", sort=3, visible=True, ) role = db.scalar(select(AdminRole).where(AdminRole.code == SUPER_CODE)) if role is None: db.flush() return linked = set( db.scalars( select(AdminRoleResource.resource_id).where(AdminRoleResource.role_id == role.id) ).all() ) for resource_id in (directory.id, policies.id, cases.id, uploads.id, policy_button.id, case_button.id): if resource_id not in linked: db.add(AdminRoleResource(role_id=role.id, resource_id=resource_id)) db.flush() def ensure_report_menu(db: Session) -> None: menu = _ensure_resource( db, parent_id=None, resource_type="menu", name="ReportCenter", title="报告中心", path="/reports", component="/reports", icon="ri:file-list-3-line", permission="", sort=10, visible=True, ) button = _ensure_resource( db, parent_id=menu.id, resource_type="button", name="report:edit", title="保存", path="", component="", icon="", permission="report:edit", sort=1, visible=False, ) role = db.scalar(select(AdminRole).where(AdminRole.code == SUPER_CODE)) if role is None: db.flush() return linked = set( db.scalars(select(AdminRoleResource.resource_id).where(AdminRoleResource.role_id == role.id)).all() ) for resource_id in (menu.id, button.id): if resource_id not in linked: db.add(AdminRoleResource(role_id=role.id, resource_id=resource_id)) db.flush() def ensure_discipline_menu(db: Session) -> None: directory = _ensure_resource( db, parent_id=None, resource_type="directory", name="IndustryDiscipline", title="行业自律", path="/discipline", component="", icon="ri:shield-check-line", permission="", sort=11, visible=True, ) complaints = _ensure_resource( db, parent_id=directory.id, resource_type="menu", name="DisciplineComplaints", title="投诉举报", path="/discipline/complaints", component="/discipline/complaints", icon="ri:feedback-line", permission="", sort=1, visible=True, ) warnings = _ensure_resource( db, parent_id=directory.id, resource_type="menu", name="DisciplineWarnings", title="警示案例", path="/discipline/warnings", component="/discipline/warnings", icon="ri:alarm-warning-line", permission="", sort=2, visible=True, ) integrity = _ensure_resource( db, parent_id=directory.id, resource_type="menu", name="DisciplineIntegrity", title="诚信档案", path="/discipline/integrity", component="/discipline/integrity", icon="ri:medal-line", permission="", sort=3, visible=True, ) results = _ensure_resource( db, parent_id=directory.id, resource_type="menu", name="DisciplineResults", title="结果公示", path="/discipline/results", component="/discipline/results", icon="ri:megaphone-line", permission="", sort=4, visible=True, ) handle_button = _ensure_resource( db, parent_id=complaints.id, resource_type="button", name="discipline:handle", title="处理", path="", component="", icon="", permission="discipline:handle", sort=1, visible=False, ) edit_buttons = [] for parent in (warnings, integrity, results): edit_buttons.append( _ensure_resource( db, parent_id=parent.id, resource_type="button", name="discipline:edit", title="保存", path="", component="", icon="", permission="discipline:edit", sort=1, visible=False, ) ) role = db.scalar(select(AdminRole).where(AdminRole.code == SUPER_CODE)) if role is None: db.flush() return linked = set( db.scalars( select(AdminRoleResource.resource_id).where(AdminRoleResource.role_id == role.id) ).all() ) resource_ids = [ directory.id, complaints.id, warnings.id, integrity.id, results.id, handle_button.id, *(button.id for button in edit_buttons), ] for resource_id in resource_ids: if resource_id not in linked: db.add(AdminRoleResource(role_id=role.id, resource_id=resource_id)) db.flush() def ensure_edu_menu(db: Session) -> None: menu = _ensure_resource( db, parent_id=None, resource_type="menu", name="ContinuingEducation", title="继续教育", path="/edu", component="/edu", icon="ri:graduation-cap-line", permission="", sort=4, visible=True, ) button = _ensure_resource( db, parent_id=menu.id, resource_type="button", name="edu:edit", title="保存", path="", component="", icon="", permission="edu:edit", sort=1, visible=False, ) role = db.scalar(select(AdminRole).where(AdminRole.code == SUPER_CODE)) if role is None: db.flush() return linked = set( db.scalars( select(AdminRoleResource.resource_id).where(AdminRoleResource.role_id == role.id) ).all() ) for resource_id in (menu.id, button.id): if resource_id not in linked: db.add(AdminRoleResource(role_id=role.id, resource_id=resource_id)) db.flush() def ensure_tech_menu(db: Session) -> None: tech = _ensure_resource( db, parent_id=None, resource_type="menu", name="TechExchange", title="技术交流", path="/tech", component="/tech", icon="ri:discuss-line", permission="", sort=7, visible=True, ) button = _ensure_resource( db, parent_id=tech.id, resource_type="button", name="tech:edit", title="保存", path="", component="", icon="", permission="tech:edit", sort=1, visible=False, ) role = db.scalar(select(AdminRole).where(AdminRole.code == SUPER_CODE)) if role is None: db.flush() return linked = set( db.scalars( select(AdminRoleResource.resource_id).where(AdminRoleResource.role_id == role.id) ).all() ) for resource_id in (tech.id, button.id): if resource_id not in linked: db.add(AdminRoleResource(role_id=role.id, resource_id=resource_id)) db.flush() def ensure_about_menu(db: Session) -> None: org_dir = _ensure_resource( db, parent_id=None, resource_type="directory", name="AboutOrgDir", title="组织概况", path="/about", component="", icon="ri:building-4-line", permission="", sort=3, visible=True, ) org_pages = [ _ensure_resource( db, parent_id=org_dir.id, resource_type="menu", name=name, title=title, path=path, component=path, icon=icon, permission="", sort=sort, visible=True, ) for name, title, path, icon, sort in ABOUT_ORG_MENUS ] qualifications = _ensure_resource( db, parent_id=None, resource_type="menu", name="AboutQualifications", title="资质荣誉", path="/about/qualifications", component="/about/qualifications", icon="ri:medal-line", permission="", sort=4, visible=True, ) contact = _ensure_resource( db, parent_id=None, resource_type="menu", name="AboutContact", title="联系合作", path="/about/contact", component="/about/contact", icon="ri:contacts-line", permission="", sort=6, visible=True, ) button = _ensure_resource( db, parent_id=org_pages[0].id, resource_type="button", name="about:edit", title="保存", path="", component="", icon="", permission="about:edit", sort=1, visible=False, ) role = db.scalar(select(AdminRole).where(AdminRole.code == SUPER_CODE)) if role is None: db.flush() return linked = set( db.scalars( select(AdminRoleResource.resource_id).where(AdminRoleResource.role_id == role.id) ).all() ) resource_ids = ( org_dir.id, button.id, qualifications.id, contact.id, *(page.id for page in org_pages), ) for resource_id in resource_ids: if resource_id not in linked: db.add(AdminRoleResource(role_id=role.id, resource_id=resource_id)) db.flush() def seed_superuser(db: Session, username: str, password: str) -> None: username = username.strip() if not username: raise HTTPException(status_code=400, detail="请填写用户名") if len(username) > 64: raise HTTPException(status_code=400, detail="用户名过长") validate_password(password) now = utcnow() role = db.scalar(select(AdminRole).where(AdminRole.code == SUPER_CODE)) if role is None: role = AdminRole( code=SUPER_CODE, name=SUPER_NAME, description="系统内置角色", status="active", created_at=now, updated_at=now, ) db.add(role) db.flush() else: role.name = SUPER_NAME role.status = "active" if not role.description: role.description = "系统内置角色" role.updated_at = now directory = _ensure_resource( db, parent_id=None, resource_type="directory", name="System", title="系统管理", path="/system", component="", icon="ri:settings-3-line", permission="", sort=1, visible=True, ) resource_ids = [directory.id] for page in PAGES: menu = _ensure_resource( db, parent_id=directory.id, resource_type="menu", name=page["name"], title=page["title"], path=page["path"], component=page["component"], icon="", permission="", sort=page["sort"], visible=True, ) resource_ids.append(menu.id) for index, (code, title) in enumerate(page["buttons"], start=1): button = _ensure_resource( db, parent_id=menu.id, resource_type="button", name=code, title=title, path="", component="", icon="", permission=code, sort=index, visible=False, ) resource_ids.append(button.id) linked = set( db.scalars(select(AdminRoleResource.resource_id).where(AdminRoleResource.role_id == role.id)).all() ) for resource_id in resource_ids: if resource_id not in linked: db.add(AdminRoleResource(role_id=role.id, resource_id=resource_id)) user = db.scalar(select(AdminUser).where(AdminUser.username == username)) password_hash = hash_password(password) if user is None: user = AdminUser( username=username, password_hash=password_hash, display_name=username, mobile_mask="", status="active", failed_login_count=0, locked_until=None, created_at=now, updated_at=now, ) db.add(user) db.flush() else: user.password_hash = password_hash user.status = "active" user.failed_login_count = 0 user.locked_until = None if not user.display_name: user.display_name = username user.updated_at = now if db.get(AdminUserRole, (user.id, role.id)) is None: db.add(AdminUserRole(user_id=user.id, role_id=role.id)) db.flush() ensure_home_menu(db) def main() -> int: try: from dotenv import load_dotenv load_dotenv() except ImportError: pass username = os.environ.get("BOOTSTRAP_ADMIN_USERNAME", "").strip() password = os.environ.get("BOOTSTRAP_ADMIN_PASSWORD", "") if username == "" or password == "": print("缺少环境变量 BOOTSTRAP_ADMIN_USERNAME 或 BOOTSTRAP_ADMIN_PASSWORD", file=sys.stderr) return 1 try: validate_password(password) except HTTPException as exc: print(exc.detail, file=sys.stderr) return 1 db: Session | None = None try: import app.models.content # noqa: F401 import app.models.security # noqa: F401 from app.models.content import ensure_site_defaults from app.services.ip_guard import seed_ip_blacklist engine = get_engine() Base.metadata.create_all(bind=engine) SessionLocal.configure(bind=engine) db = SessionLocal() ensure_site_defaults(db) seed_ip_blacklist(db) seed_superuser(db, username, password) db.commit() except RuntimeError as exc: print(str(exc), file=sys.stderr) return 1 except HTTPException as exc: if db is not None: db.rollback() print(exc.detail, file=sys.stderr) return 1 except Exception: if db is not None: db.rollback() print("创建超级管理员失败", file=sys.stderr) return 1 finally: if db is not None: db.close() print(f"超级管理员已就绪:{username}") return 0 if __name__ == "__main__": raise SystemExit(main())