FastAPIAdmin后台管理系统

目录

  • FastAPIAdmin后台管理系统:现代化、高性能的管理系统解决方案
    • 一、FastAPIAdmin系统架构设计
      • [1.1 系统架构概述](#1.1 系统架构概述)
      • [1.2 技术栈选择](#1.2 技术栈选择)
    • 二、核心模块设计与实现
      • [2.1 项目结构](#2.1 项目结构)
      • [2.2 配置管理系统](#2.2 配置管理系统)
      • [2.3 数据库模型设计](#2.3 数据库模型设计)
      • [2.4 数据库连接与会话管理](#2.4 数据库连接与会话管理)
      • [2.5 安全与认证模块](#2.5 安全与认证模块)
      • [2.6 API路由设计](#2.6 API路由设计)
      • [2.7 前端管理界面(Jinja2模板)](#2.7 前端管理界面(Jinja2模板))
    • 三、高级功能实现
      • [3.1 异步任务队列(Celery)](#3.1 异步任务队列(Celery))
      • [3.2 WebSocket实时通信](#3.2 WebSocket实时通信)
    • 四、部署与监控
      • [4.1 Docker部署配置](#4.1 Docker部署配置)
      • [4.2 Dockerfile](#4.2 Dockerfile)
      • [4.3 监控配置](#4.3 监控配置)
    • 五、测试与质量保证
      • [5.1 单元测试](#5.1 单元测试)
    • 六、总结与最佳实践
      • [6.1 架构设计总结](#6.1 架构设计总结)
      • [6.2 安全最佳实践](#6.2 安全最佳实践)
      • [6.3 性能优化建议](#6.3 性能优化建议)
      • [6.4 扩展性考虑](#6.4 扩展性考虑)
      • [6.5 部署与监控](#6.5 部署与监控)
      • [6.6 未来发展路线图](#6.6 未来发展路线图)

『宝藏代码胶囊开张啦!』------ 我的 CodeCapsule 来咯!✨写代码不再头疼!我的新站点 CodeCapsule 主打一个 "白菜价"+"量身定制 "!无论是卡脖子的毕设/课设/文献复现 ,需要灵光一现的算法改进 ,还是想给项目加个"外挂",这里都有便宜又好用的代码方案等你发现!低成本,高适配,助你轻松通关!速来围观 👉 CodeCapsule官网

FastAPIAdmin后台管理系统:现代化、高性能的管理系统解决方案

一、FastAPIAdmin系统架构设计

1.1 系统架构概述

FastAPIAdmin 是一个基于 FastAPI、SQLAlchemy、Vue.js(或 Jinja2)构建的现代化后台管理系统。它采用前后端分离架构,支持 RESTful API 和 WebSocket,具备高性能、易扩展的特点。
客户端 Client
FastAPI 服务器
认证授权层
业务逻辑层
数据访问层
用户认证
权限控制
用户管理
角色管理
日志管理
文件管理
SQLAlchemy ORM
Redis 缓存
消息队列
关系数据库
数据库迁移

1.2 技术栈选择

组件 技术选择 说明
Web框架 FastAPI 高性能异步框架,自动生成API文档
ORM SQLAlchemy + Alembic 强大的ORM,支持数据库迁移
数据库 PostgreSQL/MySQL 生产级关系数据库
缓存 Redis 高速缓存和会话存储
认证 JWT + OAuth2 安全的认证机制
前端 Vue.js 3 + Element Plus 现代化前端框架
部署 Docker + Nginx 容器化部署
监控 Prometheus + Grafana 系统监控和告警

二、核心模块设计与实现

2.1 项目结构

复制代码
fastapi-admin/
├── app/
│   ├── __init__.py
│   ├── main.py              # 应用入口
│   ├── config.py            # 配置文件
│   ├── database.py          # 数据库连接
│   ├── models.py            # 数据模型
│   ├── schemas.py           # Pydantic模型
│   ├── crud.py              # 数据库操作
│   ├── api/
│   │   ├── __init__.py
│   │   ├── deps.py          # 依赖注入
│   │   ├── auth.py          # 认证路由
│   │   ├── users.py         # 用户管理
│   │   ├── roles.py         # 角色管理
│   │   ├── menus.py         # 菜单管理
│   │   └── logs.py          # 日志管理
│   ├── core/
│   │   ├── __init__.py
│   │   ├── security.py      # 安全相关
│   │   ├── config.py        # 核心配置
│   │   └── exceptions.py    # 异常处理
│   ├── utils/
│   │   ├── __init__.py
│   │   ├── pagination.py    # 分页工具
│   │   └── validator.py     # 验证工具
│   └── admin/
│       ├── __init__.py
│       ├── views.py         # 管理视图
│       └── templates/       # Jinja2模板
├── alembic/                 # 数据库迁移
├── tests/                   # 测试目录
├── static/                  # 静态文件
├── frontend/                # 前端项目
├── docker-compose.yml       # Docker编排
├── requirements.txt         # Python依赖
└── README.md                # 项目说明

2.2 配置管理系统

python 复制代码
"""
FastAPIAdmin 配置文件
支持多种环境配置和敏感信息保护
"""

import os
import secrets
from typing import Dict, List, Optional, Any, Union
from pydantic import BaseSettings, PostgresDsn, validator, Field
from pathlib import Path

class Settings(BaseSettings):
    """
    应用配置类
    
    配置优先级:环境变量 > .env文件 > 默认值
    """
    
    # 基础配置
    APP_NAME: str = "FastAPIAdmin"
    APP_VERSION: str = "1.0.0"
    DEBUG: bool = Field(False, env="DEBUG")
    ENVIRONMENT: str = Field("development", env="ENVIRONMENT")
    
    # 服务器配置
    HOST: str = Field("0.0.0.0", env="HOST")
    PORT: int = Field(8000, env="PORT")
    WORKERS: int = Field(4, env="WORKERS")
    API_V1_STR: str = "/api/v1"
    
    # 安全配置
    SECRET_KEY: str = Field(secrets.token_urlsafe(32), env="SECRET_KEY")
    ALGORITHM: str = "HS256"
    ACCESS_TOKEN_EXPIRE_MINUTES: int = Field(30, env="ACCESS_TOKEN_EXPIRE_MINUTES")
    REFRESH_TOKEN_EXPIRE_DAYS: int = Field(7, env="REFRESH_TOKEN_EXPIRE_DAYS")
    
    # CORS配置
    BACKEND_CORS_ORIGINS: List[str] = Field(["*"], env="BACKEND_CORS_ORIGINS")
    
    @validator("BACKEND_CORS_ORIGINS", pre=True)
    def parse_cors_origins(cls, v: Union[str, List[str]]) -> List[str]:
        if isinstance(v, str) and not v.startswith("["):
            return [i.strip() for i in v.split(",")]
        elif isinstance(v, (list, str)):
            return v
        raise ValueError(v)
    
    # 数据库配置
    DATABASE_HOST: str = Field("localhost", env="DATABASE_HOST")
    DATABASE_PORT: str = Field("5432", env="DATABASE_PORT")
    DATABASE_USER: str = Field("postgres", env="DATABASE_USER")
    DATABASE_PASSWORD: str = Field("postgres", env="DATABASE_PASSWORD")
    DATABASE_NAME: str = Field("fastapi_admin", env="DATABASE_NAME")
    
    # Redis配置
    REDIS_HOST: str = Field("localhost", env="REDIS_HOST")
    REDIS_PORT: int = Field(6379, env="REDIS_PORT")
    REDIS_PASSWORD: Optional[str] = Field(None, env="REDIS_PASSWORD")
    REDIS_DB: int = Field(0, env="REDIS_DB")
    
    # 数据库URL(自动生成)
    SQLALCHEMY_DATABASE_URI: Optional[PostgresDsn] = None
    
    @validator("SQLALCHEMY_DATABASE_URI", pre=True)
    def assemble_db_connection(cls, v: Optional[str], values: Dict[str, Any]) -> Any:
        if isinstance(v, str):
            return v
        
        return PostgresDsn.build(
            scheme="postgresql+asyncpg",
            user=values.get("DATABASE_USER"),
            password=values.get("DATABASE_PASSWORD"),
            host=values.get("DATABASE_HOST"),
            port=values.get("DATABASE_PORT"),
            path=f"/{values.get('DATABASE_NAME') or ''}",
        )
    
    # Redis URL(自动生成)
    REDIS_URL: Optional[str] = None
    
    @validator("REDIS_URL", pre=True)
    def assemble_redis_connection(cls, v: Optional[str], values: Dict[str, Any]) -> str:
        if isinstance(v, str):
            return v
        
        password = values.get("REDIS_PASSWORD")
        auth_part = f":{password}@" if password else ""
        
        return f"redis://{auth_part}{values.get('REDIS_HOST')}:{values.get('REDIS_PORT')}/{values.get('REDIS_DB')}"
    
    # 文件上传配置
    UPLOAD_DIR: Path = Field(Path("./uploads"), env="UPLOAD_DIR")
    MAX_UPLOAD_SIZE: int = Field(100 * 1024 * 1024, env="MAX_UPLOAD_SIZE")  # 100MB
    ALLOWED_EXTENSIONS: List[str] = Field([
        "jpg", "jpeg", "png", "gif", "pdf", "doc", "docx", 
        "xls", "xlsx", "txt", "csv", "zip", "rar"
    ], env="ALLOWED_EXTENSIONS")
    
    # 日志配置
    LOG_LEVEL: str = Field("INFO", env="LOG_LEVEL")
    LOG_FORMAT: str = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
    LOG_DIR: Path = Field(Path("./logs"), env="LOG_DIR")
    
    # 邮件配置
    SMTP_HOST: Optional[str] = Field(None, env="SMTP_HOST")
    SMTP_PORT: Optional[int] = Field(587, env="SMTP_PORT")
    SMTP_USER: Optional[str] = Field(None, env="SMTP_USER")
    SMTP_PASSWORD: Optional[str] = Field(None, env="SMTP_PASSWORD")
    EMAILS_FROM_EMAIL: Optional[str] = Field(None, env="EMAILS_FROM_EMAIL")
    EMAILS_FROM_NAME: Optional[str] = Field(None, env="EMAILS_FROM_NAME")
    
    # 监控配置
    ENABLE_METRICS: bool = Field(True, env="ENABLE_METRICS")
    PROMETHEUS_PORT: int = Field(9090, env="PROMETHEUS_PORT")
    
    # 任务队列配置
    CELERY_BROKER_URL: Optional[str] = Field(None, env="CELERY_BROKER_URL")
    CELERY_RESULT_BACKEND: Optional[str] = Field(None, env="CELERY_RESULT_BACKEND")
    
    # API文档配置
    ENABLE_SWAGGER: bool = Field(True, env="ENABLE_SWAGGER")
    ENABLE_REDOC: bool = Field(True, env="ENABLE_REDOC")
    
    class Config:
        env_file = ".env"
        case_sensitive = True
        validate_assignment = True


# 创建全局配置实例
settings = Settings()

# 创建必要的目录
def create_directories():
    """创建必要的目录结构"""
    directories = [
        settings.UPLOAD_DIR,
        settings.LOG_DIR,
        settings.UPLOAD_DIR / "avatars",
        settings.UPLOAD_DIR / "documents",
        settings.UPLOAD_DIR / "images",
        settings.LOG_DIR / "access",
        settings.LOG_DIR / "error",
        settings.LOG_DIR / "application",
    ]
    
    for directory in directories:
        directory.mkdir(parents=True, exist_ok=True)


# 初始化时创建目录
create_directories()


def get_settings() -> Settings:
    """获取配置实例(用于依赖注入)"""
    return settings

2.3 数据库模型设计

python 复制代码
"""
FastAPIAdmin 数据库模型
使用SQLAlchemy ORM定义所有数据表
"""

from datetime import datetime, timedelta
from typing import List, Optional, Dict, Any
from sqlalchemy import (
    Column, String, Integer, Boolean, DateTime, 
    ForeignKey, Text, JSON, Enum, BigInteger,
    UniqueConstraint, Index, Float, Numeric
)
from sqlalchemy.orm import relationship, declarative_base, validates
from sqlalchemy.sql import func
from sqlalchemy.dialects.postgresql import UUID, ARRAY
import enum

Base = declarative_base()


class UserStatus(str, enum.Enum):
    """用户状态枚举"""
    ACTIVE = "active"
    INACTIVE = "inactive"
    SUSPENDED = "suspended"
    DELETED = "deleted"


class User(Base):
    """
    用户表
    管理系统用户,支持多种认证方式
    """
    __tablename__ = "users"
    
    id = Column(Integer, primary_key=True, index=True, autoincrement=True)
    uuid = Column(UUID(as_uuid=True), unique=True, index=True, nullable=False,
                  server_default=func.gen_random_uuid())
    username = Column(String(50), unique=True, index=True, nullable=False)
    email = Column(String(100), unique=True, index=True, nullable=False)
    phone = Column(String(20), nullable=True)
    full_name = Column(String(100), nullable=True)
    
    # 密码相关
    hashed_password = Column(String(255), nullable=False)
    password_changed_at = Column(DateTime, nullable=True)
    
    # 状态信息
    is_active = Column(Boolean, default=True, nullable=False)
    is_superuser = Column(Boolean, default=False, nullable=False)
    is_verified = Column(Boolean, default=False, nullable=False)
    status = Column(Enum(UserStatus), default=UserStatus.ACTIVE, nullable=False)
    
    # 个人信息
    avatar = Column(String(500), nullable=True)
    gender = Column(String(10), nullable=True)
    birth_date = Column(DateTime, nullable=True)
    address = Column(Text, nullable=True)
    
    # 时间戳
    created_at = Column(DateTime, server_default=func.now(), nullable=False)
    updated_at = Column(DateTime, server_default=func.now(), 
                        onupdate=func.now(), nullable=False)
    last_login_at = Column(DateTime, nullable=True)
    
    # 关联关系
    roles = relationship("Role", secondary="user_roles", back_populates="users")
    logs = relationship("AuditLog", back_populates="user")
    sessions = relationship("UserSession", back_populates="user")
    
    # 索引
    __table_args__ = (
        Index("idx_user_status", "status"),
        Index("idx_user_created", "created_at"),
    )
    
    @validates('email')
    def validate_email(self, key, email):
        """验证邮箱格式"""
        if '@' not in email:
            raise ValueError("Invalid email address")
        return email.lower()
    
    def __repr__(self):
        return f"<User(id={self.id}, username='{self.username}', email='{self.email}')>"


class Role(Base):
    """
    角色表
    定义用户角色和权限组
    """
    __tablename__ = "roles"
    
    id = Column(Integer, primary_key=True, index=True, autoincrement=True)
    name = Column(String(50), unique=True, index=True, nullable=False)
    code = Column(String(50), unique=True, index=True, nullable=False)
    description = Column(Text, nullable=True)
    is_system = Column(Boolean, default=False, nullable=False)  # 是否为系统角色
    
    # 权限配置
    permissions = Column(JSON, nullable=True, default=list)  # 权限列表
    data_scope = Column(JSON, nullable=True)  # 数据权限范围
    
    # 时间戳
    created_at = Column(DateTime, server_default=func.now(), nullable=False)
    updated_at = Column(DateTime, server_default=func.now(), 
                        onupdate=func.now(), nullable=False)
    
    # 关联关系
    users = relationship("User", secondary="user_roles", back_populates="roles")
    menus = relationship("Menu", secondary="role_menus", back_populates="roles")
    
    # 索引
    __table_args__ = (
        Index("idx_role_code", "code"),
    )
    
    def __repr__(self):
        return f"<Role(id={self.id}, name='{self.name}', code='{self.code}')>"


class UserRole(Base):
    """
    用户角色关联表
    多对多关系
    """
    __tablename__ = "user_roles"
    
    id = Column(Integer, primary_key=True, index=True, autoincrement=True)
    user_id = Column(Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=False)
    role_id = Column(Integer, ForeignKey("roles.id", ondelete="CASCADE"), nullable=False)
    
    # 时间戳
    created_at = Column(DateTime, server_default=func.now(), nullable=False)
    
    # 唯一约束
    __table_args__ = (
        UniqueConstraint('user_id', 'role_id', name='uq_user_role'),
    )


class Menu(Base):
    """
    菜单表
    管理系统导航菜单
    """
    __tablename__ = "menus"
    
    id = Column(Integer, primary_key=True, index=True, autoincrement=True)
    parent_id = Column(Integer, ForeignKey("menus.id", ondelete="CASCADE"), nullable=True)
    title = Column(String(50), nullable=False)
    name = Column(String(50), unique=True, nullable=False)  # 路由名称
    path = Column(String(200), nullable=True)  # 路由路径
    component = Column(String(200), nullable=True)  # 组件路径
    redirect = Column(String(200), nullable=True)  # 重定向路径
    
    # 菜单信息
    icon = Column(String(50), nullable=True)
    order = Column(Integer, default=0, nullable=False)
    hidden = Column(Boolean, default=False, nullable=False)  # 是否隐藏
    always_show = Column(Boolean, default=False, nullable=False)  # 是否总是显示
    no_cache = Column(Boolean, default=False, nullable=False)  # 是否不缓存
    affix = Column(Boolean, default=False, nullable=False)  # 是否固定标签
    
    # 权限控制
    permission = Column(String(100), nullable=True)  # 权限标识
    menu_type = Column(Enum('directory', 'menu', 'button'), default='menu', nullable=False)
    
    # 元信息
    meta = Column(JSON, nullable=True, default=dict)  # 额外信息
    
    # 时间戳
    created_at = Column(DateTime, server_default=func.now(), nullable=False)
    updated_at = Column(DateTime, server_default=func.now(), 
                        onupdate=func.now(), nullable=False)
    
    # 关联关系
    parent = relationship("Menu", remote_side=[id], backref="children")
    roles = relationship("Role", secondary="role_menus", back_populates="menus")
    
    # 索引
    __table_args__ = (
        Index("idx_menu_parent", "parent_id"),
        Index("idx_menu_order", "order"),
    )
    
    def __repr__(self):
        return f"<Menu(id={self.id}, title='{self.title}', path='{self.path}')>"


class RoleMenu(Base):
    """
    角色菜单关联表
    多对多关系
    """
    __tablename__ = "role_menus"
    
    id = Column(Integer, primary_key=True, index=True, autoincrement=True)
    role_id = Column(Integer, ForeignKey("roles.id", ondelete="CASCADE"), nullable=False)
    menu_id = Column(Integer, ForeignKey("menus.id", ondelete="CASCADE"), nullable=False)
    
    # 权限操作
    actions = Column(JSON, nullable=True, default=list)  # 允许的操作
    
    # 时间戳
    created_at = Column(DateTime, server_default=func.now(), nullable=False)
    
    # 唯一约束
    __table_args__ = (
        UniqueConstraint('role_id', 'menu_id', name='uq_role_menu'),
    )


class AuditLogType(str, enum.Enum):
    """审计日志类型"""
    LOGIN = "login"
    LOGOUT = "logout"
    CREATE = "create"
    UPDATE = "update"
    DELETE = "delete"
    QUERY = "query"
    EXPORT = "export"
    IMPORT = "import"
    UPLOAD = "upload"
    DOWNLOAD = "download"
    SYSTEM = "system"


class AuditLog(Base):
    """
    审计日志表
    记录所有重要操作
    """
    __tablename__ = "audit_logs"
    
    id = Column(BigInteger, primary_key=True, index=True, autoincrement=True)
    user_id = Column(Integer, ForeignKey("users.id", ondelete="SET NULL"), nullable=True)
    
    # 日志信息
    log_type = Column(Enum(AuditLogType), nullable=False)
    module = Column(String(100), nullable=False)  # 模块名称
    action = Column(String(200), nullable=False)  # 操作描述
    request_url = Column(String(500), nullable=True)  # 请求URL
    request_method = Column(String(10), nullable=True)  # 请求方法
    request_ip = Column(String(50), nullable=True)  # 请求IP
    user_agent = Column(Text, nullable=True)  # 用户代理
    
    # 请求/响应数据
    request_params = Column(JSON, nullable=True)  # 请求参数
    request_body = Column(JSON, nullable=True)  # 请求体
    response_body = Column(JSON, nullable=True)  # 响应体
    response_status = Column(Integer, nullable=True)  # 响应状态码
    
    # 执行信息
    execution_time = Column(Float, nullable=True)  # 执行时间(秒)
    error_message = Column(Text, nullable=True)  # 错误信息
    
    # 时间戳
    created_at = Column(DateTime, server_default=func.now(), nullable=False)
    
    # 关联关系
    user = relationship("User", back_populates="logs")
    
    # 索引
    __table_args__ = (
        Index("idx_audit_user", "user_id"),
        Index("idx_audit_type", "log_type"),
        Index("idx_audit_module", "module"),
        Index("idx_audit_created", "created_at"),
    )
    
    def __repr__(self):
        return f"<AuditLog(id={self.id}, module='{self.module}', action='{self.action}')>"


class UserSession(Base):
    """
    用户会话表
    管理用户登录会话
    """
    __tablename__ = "user_sessions"
    
    id = Column(Integer, primary_key=True, index=True, autoincrement=True)
    user_id = Column(Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=False)
    
    # 会话信息
    session_id = Column(String(255), unique=True, index=True, nullable=False)
    access_token = Column(Text, nullable=False)
    refresh_token = Column(Text, nullable=False)
    user_agent = Column(Text, nullable=True)
    ip_address = Column(String(50), nullable=True)
    
    # 会话状态
    is_active = Column(Boolean, default=True, nullable=False)
    expires_at = Column(DateTime, nullable=False)
    
    # 时间戳
    created_at = Column(DateTime, server_default=func.now(), nullable=False)
    updated_at = Column(DateTime, server_default=func.now(), 
                        onupdate=func.now(), nullable=False)
    
    # 关联关系
    user = relationship("User", back_populates="sessions")
    
    # 索引
    __table_args__ = (
        Index("idx_session_user", "user_id"),
        Index("idx_session_expires", "expires_at"),
        Index("idx_session_active", "is_active"),
    )
    
    def __repr__(self):
        return f"<UserSession(id={self.id}, user_id={self.user_id}, session_id='{self.session_id}')>"


class SystemConfig(Base):
    """
    系统配置表
    存储系统动态配置
    """
    __tablename__ = "system_configs"
    
    id = Column(Integer, primary_key=True, index=True, autoincrement=True)
    config_key = Column(String(100), unique=True, index=True, nullable=False)
    config_value = Column(JSON, nullable=False)
    config_type = Column(Enum('string', 'number', 'boolean', 'array', 'object'), 
                        default='string', nullable=False)
    description = Column(Text, nullable=True)
    
    # 权限控制
    is_public = Column(Boolean, default=False, nullable=False)  # 是否公开配置
    is_system = Column(Boolean, default=False, nullable=False)  # 是否为系统配置
    
    # 时间戳
    created_at = Column(DateTime, server_default=func.now(), nullable=False)
    updated_at = Column(DateTime, server_default=func.now(), 
                        onupdate=func.now(), nullable=False)
    
    def __repr__(self):
        return f"<SystemConfig(id={self.id}, key='{self.config_key}')>"


class FileStorage(Base):
    """
    文件存储表
    管理系统上传的文件
    """
    __tablename__ = "file_storage"
    
    id = Column(Integer, primary_key=True, index=True, autoincrement=True)
    user_id = Column(Integer, ForeignKey("users.id", ondelete="SET NULL"), nullable=True)
    
    # 文件信息
    filename = Column(String(255), nullable=False)
    original_name = Column(String(255), nullable=False)
    file_path = Column(String(500), nullable=False)
    file_size = Column(BigInteger, nullable=False)
    file_type = Column(String(100), nullable=False)
    file_ext = Column(String(20), nullable=False)
    
    # 存储信息
    storage_type = Column(Enum('local', 's3', 'oss', 'cos'), default='local', nullable=False)
    bucket_name = Column(String(100), nullable=True)
    object_key = Column(String(500), nullable=True)
    
    # 安全信息
    md5_hash = Column(String(32), nullable=True)
    sha256_hash = Column(String(64), nullable=True)
    is_private = Column(Boolean, default=False, nullable=False)
    access_url = Column(String(500), nullable=True)
    
    # 元数据
    meta_data = Column(JSON, nullable=True, default=dict)
    
    # 时间戳
    created_at = Column(DateTime, server_default=func.now(), nullable=False)
    updated_at = Column(DateTime, server_default=func.now(), 
                        onupdate=func.now(), nullable=False)
    
    # 关联关系
    user = relationship("User")
    
    # 索引
    __table_args__ = (
        Index("idx_file_user", "user_id"),
        Index("idx_file_type", "file_type"),
        Index("idx_file_created", "created_at"),
        Index("idx_file_hash", "md5_hash"),
    )
    
    def __repr__(self):
        return f"<FileStorage(id={self.id}, filename='{self.filename}')>"


class Notification(Base):
    """
    通知表
    管理系统通知
    """
    __tablename__ = "notifications"
    
    id = Column(Integer, primary_key=True, index=True, autoincrement=True)
    user_id = Column(Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=False)
    
    # 通知内容
    title = Column(String(200), nullable=False)
    content = Column(Text, nullable=False)
    notification_type = Column(Enum('info', 'success', 'warning', 'error'), 
                              default='info', nullable=False)
    
    # 状态
    is_read = Column(Boolean, default=False, nullable=False)
    read_at = Column(DateTime, nullable=True)
    
    # 动作
    action_url = Column(String(500), nullable=True)
    action_text = Column(String(100), nullable=True)
    
    # 时间戳
    created_at = Column(DateTime, server_default=func.now(), nullable=False)
    expires_at = Column(DateTime, nullable=True)
    
    # 关联关系
    user = relationship("User")
    
    # 索引
    __table_args__ = (
        Index("idx_notification_user", "user_id"),
        Index("idx_notification_read", "is_read"),
        Index("idx_notification_created", "created_at"),
    )
    
    def __repr__(self):
        return f"<Notification(id={self.id}, title='{self.title}')>"


# 创建所有表(生产环境使用Alembic迁移)
def create_tables(engine):
    """创建所有数据库表(仅用于开发)"""
    Base.metadata.create_all(bind=engine)

2.4 数据库连接与会话管理

python 复制代码
"""
FastAPIAdmin 数据库模块
使用SQLAlchemy异步ORM和会话管理
"""

from typing import AsyncGenerator, Optional
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine, async_sessionmaker
from sqlalchemy.orm import sessionmaker, declarative_base
from sqlalchemy.pool import NullPool, QueuePool
import redis.asyncio as redis
from contextlib import asynccontextmanager
import aioredis
import logging

from app.config import settings

logger = logging.getLogger(__name__)

# 创建异步数据库引擎
engine = create_async_engine(
    str(settings.SQLALCHEMY_DATABASE_URI),
    echo=settings.DEBUG,
    poolclass=QueuePool if settings.ENVIRONMENT == "production" else NullPool,
    pool_size=20,
    max_overflow=30,
    pool_pre_ping=True,
    pool_recycle=3600,
    future=True,
)

# 创建异步会话工厂
AsyncSessionLocal = async_sessionmaker(
    engine,
    class_=AsyncSession,
    expire_on_commit=False,
    autocommit=False,
    autoflush=False,
)

# Redis连接池
redis_pool: Optional[redis.Redis] = None


async def init_redis() -> redis.Redis:
    """初始化Redis连接池"""
    global redis_pool
    if redis_pool is None:
        try:
            redis_pool = redis.from_url(
                settings.REDIS_URL,
                encoding="utf-8",
                decode_responses=True,
                max_connections=20,
            )
            # 测试连接
            await redis_pool.ping()
            logger.info("Redis connection established successfully")
        except Exception as e:
            logger.error(f"Failed to connect to Redis: {e}")
            raise
    return redis_pool


async def get_redis() -> redis.Redis:
    """获取Redis连接"""
    return await init_redis()


async def close_redis():
    """关闭Redis连接"""
    global redis_pool
    if redis_pool:
        await redis_pool.close()
        redis_pool = None
        logger.info("Redis connection closed")


@asynccontextmanager
async def get_db() -> AsyncGenerator[AsyncSession, None]:
    """
    获取数据库会话上下文管理器
    
    Yields:
        AsyncSession: 异步数据库会话
        
    Example:
        async with get_db() as db:
            user = await db.get(User, user_id)
    """
    async with AsyncSessionLocal() as session:
        try:
            yield session
            await session.commit()
        except Exception as e:
            await session.rollback()
            logger.error(f"Database session error: {e}")
            raise
        finally:
            await session.close()


class DatabaseManager:
    """
    数据库管理器
    提供数据库操作的高级接口
    """
    
    def __init__(self):
        self.engine = engine
        self.session_factory = AsyncSessionLocal
    
    async def execute_query(self, query, params=None):
        """执行原始SQL查询"""
        async with get_db() as db:
            if params:
                result = await db.execute(query, params)
            else:
                result = await db.execute(query)
            return result
    
    async def health_check(self) -> bool:
        """数据库健康检查"""
        try:
            async with self.engine.connect() as conn:
                await conn.execute("SELECT 1")
            return True
        except Exception as e:
            logger.error(f"Database health check failed: {e}")
            return False
    
    async def get_table_stats(self, table_name: str) -> dict:
        """获取表统计信息"""
        query = f"""
        SELECT 
            COUNT(*) as row_count,
            pg_size_pretty(pg_total_relation_size('{table_name}')) as total_size,
            pg_size_pretty(pg_relation_size('{table_name}')) as table_size
        FROM {table_name}
        """
        
        async with get_db() as db:
            result = await db.execute(query)
            row = result.fetchone()
            return dict(row._mapping) if row else {}
    
    async def backup_database(self, backup_path: str):
        """备份数据库(仅PostgreSQL)"""
        import subprocess
        import asyncio
        
        # 构建pg_dump命令
        cmd = [
            "pg_dump",
            "-h", settings.DATABASE_HOST,
            "-p", settings.DATABASE_PORT,
            "-U", settings.DATABASE_USER,
            "-d", settings.DATABASE_NAME,
            "-f", backup_path,
            "-F", "c",  # 自定义格式
            "-v"  # 详细模式
        ]
        
        # 设置密码环境变量
        env = {
            **os.environ,
            "PGPASSWORD": settings.DATABASE_PASSWORD
        }
        
        # 执行备份
        process = await asyncio.create_subprocess_exec(
            *cmd,
            env=env,
            stdout=asyncio.subprocess.PIPE,
            stderr=asyncio.subprocess.PIPE
        )
        
        stdout, stderr = await process.communicate()
        
        if process.returncode != 0:
            logger.error(f"Database backup failed: {stderr.decode()}")
            raise Exception(f"Backup failed: {stderr.decode()}")
        
        logger.info(f"Database backup completed: {backup_path}")
        return backup_path


# 全局数据库管理器实例
db_manager = DatabaseManager()


async def init_db():
    """
    初始化数据库
    
    在应用启动时调用,创建必要的表和初始数据
    """
    from sqlalchemy import text
    
    try:
        async with engine.begin() as conn:
            # 检查数据库连接
            await conn.execute(text("SELECT 1"))
            logger.info("Database connection established successfully")
            
            # 在开发环境中自动创建表
            if settings.ENVIRONMENT == "development":
                from app.models import Base
                await conn.run_sync(Base.metadata.create_all)
                logger.info("Database tables created (development mode)")
            
            # 创建初始数据
            await create_initial_data(conn)
            
    except Exception as e:
        logger.error(f"Failed to initialize database: {e}")
        raise


async def create_initial_data(conn):
    """创建初始数据(超级管理员、默认角色等)"""
    from app.models import User, Role, UserRole, Menu
    from app.core.security import get_password_hash
    from sqlalchemy import select
    
    # 检查是否已存在管理员
    result = await conn.execute(select(User).where(User.is_superuser == True))
    admin_exists = result.scalar_one_or_none()
    
    if admin_exists:
        logger.info("Admin user already exists, skipping initial data creation")
        return
    
    # 创建超级管理员
    admin_user = User(
        username="admin",
        email="admin@example.com",
        full_name="系统管理员",
        hashed_password=get_password_hash("admin123"),
        is_superuser=True,
        is_active=True,
        is_verified=True,
    )
    
    conn.add(admin_user)
    await conn.flush()
    
    # 创建默认角色
    roles_data = [
        {
            "name": "超级管理员",
            "code": "super_admin",
            "description": "系统超级管理员,拥有所有权限",
            "is_system": True,
            "permissions": ["*:*:*"]  # 所有权限
        },
        {
            "name": "系统管理员",
            "code": "admin",
            "description": "系统管理员,拥有大部分管理权限",
            "is_system": True,
            "permissions": ["system:*", "user:*", "role:*", "menu:*"]
        },
        {
            "name": "普通用户",
            "code": "user",
            "description": "普通用户,拥有基本权限",
            "is_system": True,
            "permissions": ["user:view", "user:edit"]
        },
        {
            "name": "访客",
            "code": "guest",
            "description": "访客用户,拥有只读权限",
            "is_system": True,
            "permissions": ["user:view"]
        }
    ]
    
    roles = {}
    for role_data in roles_data:
        role = Role(**role_data)
        conn.add(role)
        await conn.flush()
        roles[role_data["code"]] = role
    
    # 关联管理员角色
    admin_role = roles["super_admin"]
    user_role = UserRole(user_id=admin_user.id, role_id=admin_role.id)
    conn.add(user_role)
    
    # 创建默认菜单
    menus_data = [
        {
            "title": "仪表盘",
            "name": "Dashboard",
            "path": "/dashboard",
            "component": "/dashboard/index",
            "icon": "dashboard",
            "order": 1,
            "permission": "dashboard:view"
        },
        {
            "title": "系统管理",
            "name": "System",
            "path": "/system",
            "component": "Layout",
            "icon": "system",
            "order": 100,
            "menu_type": "directory",
            "children": [
                {
                    "title": "用户管理",
                    "name": "UserManagement",
                    "path": "user",
                    "component": "/system/user/index",
                    "icon": "user",
                    "order": 1,
                    "permission": "user:*"
                },
                {
                    "title": "角色管理",
                    "name": "RoleManagement",
                    "path": "role",
                    "component": "/system/role/index",
                    "icon": "role",
                    "order": 2,
                    "permission": "role:*"
                },
                {
                    "title": "菜单管理",
                    "name": "MenuManagement",
                    "path": "menu",
                    "component": "/system/menu/index",
                    "icon": "menu",
                    "order": 3,
                    "permission": "menu:*"
                }
            ]
        }
    ]
    
    # 递归创建菜单
    async def create_menu(data, parent_id=None):
        children = data.pop("children", [])
        menu = Menu(**data, parent_id=parent_id)
        conn.add(menu)
        await conn.flush()
        
        for child_data in children:
            await create_menu(child_data, menu.id)
    
    for menu_data in menus_data:
        await create_menu(menu_data)
    
    await conn.commit()
    logger.info("Initial data created successfully")

2.5 安全与认证模块

python 复制代码
"""
FastAPIAdmin 安全模块
处理用户认证、授权和密码安全
"""

import jwt
import secrets
from datetime import datetime, timedelta
from typing import Optional, Dict, Any, Union, List
from fastapi import HTTPException, status, Depends
from fastapi.security import OAuth2PasswordBearer, HTTPBearer, HTTPAuthorizationCredentials
from passlib.context import CryptContext
import hashlib
import uuid
import logging

from app.config import settings
from app.models import User, UserSession, AuditLogType
from app.database import get_db
from app.utils.audit_log import create_audit_log

logger = logging.getLogger(__name__)

# 密码哈希上下文
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")

# OAuth2密码承载令牌
oauth2_scheme = OAuth2PasswordBearer(
    tokenUrl=f"{settings.API_V1_STR}/auth/login",
    auto_error=False
)

# HTTP Bearer认证
http_bearer = HTTPBearer(auto_error=False)


class SecurityManager:
    """安全管理器"""
    
    @staticmethod
    def verify_password(plain_password: str, hashed_password: str) -> bool:
        """验证密码"""
        return pwd_context.verify(plain_password, hashed_password)
    
    @staticmethod
    def get_password_hash(password: str) -> str:
        """获取密码哈希"""
        return pwd_context.hash(password)
    
    @staticmethod
    def generate_token_data(
        user_id: int,
        username: str,
        is_superuser: bool = False,
        permissions: List[str] = None,
        expires_delta: Optional[timedelta] = None
    ) -> Dict[str, Any]:
        """生成令牌数据"""
        if expires_delta:
            expire = datetime.utcnow() + expires_delta
        else:
            expire = datetime.utcnow() + timedelta(
                minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES
            )
        
        token_data = {
            "sub": str(user_id),
            "username": username,
            "is_superuser": is_superuser,
            "permissions": permissions or [],
            "exp": expire,
            "iat": datetime.utcnow(),
            "jti": str(uuid.uuid4()),  # JWT ID
        }
        
        return token_data
    
    @staticmethod
    def create_access_token(data: Dict[str, Any]) -> str:
        """创建访问令牌"""
        to_encode = data.copy()
        encoded_jwt = jwt.encode(
            to_encode, 
            settings.SECRET_KEY, 
            algorithm=settings.ALGORITHM
        )
        return encoded_jwt
    
    @staticmethod
    def create_refresh_token(user_id: int) -> str:
        """创建刷新令牌"""
        refresh_data = {
            "sub": str(user_id),
            "exp": datetime.utcnow() + timedelta(days=settings.REFRESH_TOKEN_EXPIRE_DAYS),
            "iat": datetime.utcnow(),
            "type": "refresh",
            "jti": str(uuid.uuid4()),
        }
        
        return jwt.encode(
            refresh_data,
            settings.SECRET_KEY,
            algorithm=settings.ALGORITHM
        )
    
    @staticmethod
    def verify_token(token: str) -> Dict[str, Any]:
        """验证令牌"""
        try:
            payload = jwt.decode(
                token,
                settings.SECRET_KEY,
                algorithms=[settings.ALGORITHM]
            )
            return payload
        except jwt.ExpiredSignatureError:
            raise HTTPException(
                status_code=status.HTTP_401_UNAUTHORIZED,
                detail="Token has expired",
                headers={"WWW-Authenticate": "Bearer"},
            )
        except jwt.InvalidTokenError:
            raise HTTPException(
                status_code=status.HTTP_401_UNAUTHORIZED,
                detail="Invalid token",
                headers={"WWW-Authenticate": "Bearer"},
            )
    
    @staticmethod
    def generate_session_id() -> str:
        """生成会话ID"""
        return secrets.token_urlsafe(32)
    
    @staticmethod
    def get_client_ip(request) -> str:
        """获取客户端IP"""
        if request.client:
            return request.client.host
        return "unknown"
    
    @staticmethod
    def get_user_agent(request) -> str:
        """获取用户代理"""
        return request.headers.get("user-agent", "")


class PermissionChecker:
    """权限检查器"""
    
    @staticmethod
    def has_permission(user_permissions: List[str], required_permission: str) -> bool:
        """
        检查用户是否拥有指定权限
        
        Args:
            user_permissions: 用户权限列表
            required_permission: 需要的权限,格式为"资源:操作:实例"
            
        Returns:
            bool: 是否有权限
        """
        # 超级管理员拥有所有权限
        if "*:*:*" in user_permissions:
            return True
        
        # 解析需要的权限
        required_parts = required_permission.split(":")
        if len(required_parts) != 3:
            return False
        
        required_resource, required_action, required_instance = required_parts
        
        # 检查每个用户权限
        for user_permission in user_permissions:
            parts = user_permission.split(":")
            if len(parts) != 3:
                continue
            
            resource, action, instance = parts
            
            # 检查资源匹配
            if resource != "*" and resource != required_resource:
                continue
            
            # 检查操作匹配
            if action != "*" and action != required_action:
                continue
            
            # 检查实例匹配
            if instance != "*" and instance != required_instance:
                continue
            
            return True
        
        return False
    
    @staticmethod
    def check_permission(
        user_permissions: List[str],
        required_permission: str,
        raise_exception: bool = True
    ) -> bool:
        """
        检查权限,可选择性抛出异常
        
        Args:
            user_permissions: 用户权限列表
            required_permission: 需要的权限
            raise_exception: 是否抛出异常
            
        Returns:
            bool: 是否有权限
        """
        has_perm = PermissionChecker.has_permission(user_permissions, required_permission)
        
        if not has_perm and raise_exception:
            raise HTTPException(
                status_code=status.HTTP_403_FORBIDDEN,
                detail="Insufficient permissions",
            )
        
        return has_perm


# 依赖注入函数
async def get_current_user(
    token: Optional[str] = Depends(oauth2_scheme),
    credentials: Optional[HTTPAuthorizationCredentials] = Depends(http_bearer)
) -> User:
    """
    获取当前认证用户
    
    Args:
        token: OAuth2令牌
        credentials: HTTP Bearer凭证
        
    Returns:
        User: 当前用户
        
    Raises:
        HTTPException: 认证失败
    """
    # 优先使用Bearer令牌
    if credentials and credentials.scheme == "Bearer":
        token = credentials.credentials
    
    if not token:
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Not authenticated",
            headers={"WWW-Authenticate": "Bearer"},
        )
    
    try:
        # 验证令牌
        payload = SecurityManager.verify_token(token)
        user_id = int(payload.get("sub"))
        username = payload.get("username")
        
        if not user_id or not username:
            raise HTTPException(
                status_code=status.HTTP_401_UNAUTHORIZED,
                detail="Invalid token payload",
            )
        
        # 从数据库获取用户
        async with get_db() as db:
            from sqlalchemy import select
            result = await db.execute(
                select(User).where(User.id == user_id, User.is_active == True)
            )
            user = result.scalar_one_or_none()
            
            if not user:
                raise HTTPException(
                    status_code=status.HTTP_401_UNAUTHORIZED,
                    detail="User not found or inactive",
                )
            
            return user
            
    except HTTPException:
        raise
    except Exception as e:
        logger.error(f"Authentication error: {e}")
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Authentication failed",
        )


async def get_current_active_user(
    current_user: User = Depends(get_current_user)
) -> User:
    """获取当前活跃用户"""
    if not current_user.is_active:
        raise HTTPException(status_code=400, detail="Inactive user")
    return current_user


async def get_current_superuser(
    current_user: User = Depends(get_current_user)
) -> User:
    """获取当前超级管理员"""
    if not current_user.is_superuser:
        raise HTTPException(
            status_code=status.HTTP_403_FORBIDDEN,
            detail="Not enough permissions",
        )
    return current_user


async def get_user_permissions(
    current_user: User = Depends(get_current_user)
) -> List[str]:
    """获取用户权限列表"""
    async with get_db() as db:
        from sqlalchemy import select
        from app.models import Role
        
        # 获取用户所有角色
        result = await db.execute(
            select(Role).join(Role.users).where(User.id == current_user.id)
        )
        roles = result.scalars().all()
        
        # 合并所有权限
        permissions = set()
        for role in roles:
            if role.permissions:
                permissions.update(role.permissions)
        
        return list(permissions)


def require_permission(permission: str):
    """
    权限检查装饰器工厂
    
    Args:
        permission: 需要的权限字符串
        
    Returns:
        Callable: 依赖函数
    """
    async def permission_dependency(
        current_user: User = Depends(get_current_user),
        user_permissions: List[str] = Depends(get_user_permissions)
    ) -> User:
        PermissionChecker.check_permission(user_permissions, permission)
        return current_user
    
    return permission_dependency


class RateLimiter:
    """API限流器"""
    
    def __init__(self, redis_client, max_requests: int = 100, window: int = 3600):
        """
        初始化限流器
        
        Args:
            redis_client: Redis客户端
            max_requests: 最大请求数
            window: 时间窗口(秒)
        """
        self.redis = redis_client
        self.max_requests = max_requests
        self.window = window
    
    async def is_rate_limited(self, key: str) -> bool:
        """
        检查是否被限流
        
        Args:
            key: 限流键(如用户ID或IP)
            
        Returns:
            bool: 是否被限流
        """
        current = await self.redis.get(key)
        
        if current is None:
            await self.redis.setex(key, self.window, 1)
            return False
        
        if int(current) >= self.max_requests:
            return True
        
        await self.redis.incr(key)
        return False


# 密码强度验证器
class PasswordValidator:
    """密码强度验证器"""
    
    MIN_LENGTH = 8
    MAX_LENGTH = 128
    
    @staticmethod
    def validate(password: str) -> Dict[str, bool]:
        """
        验证密码强度
        
        Returns:
            Dict[str, bool]: 验证结果
        """
        result = {
            "length": len(password) >= PasswordValidator.MIN_LENGTH and 
                     len(password) <= PasswordValidator.MAX_LENGTH,
            "has_upper": any(c.isupper() for c in password),
            "has_lower": any(c.islower() for c in password),
            "has_digit": any(c.isdigit() for c in password),
            "has_special": any(not c.isalnum() for c in password),
        }
        
        result["is_valid"] = all(result.values())
        return result
    
    @staticmethod
    def get_strength(password: str) -> str:
        """获取密码强度"""
        validation = PasswordValidator.validate(password)
        score = sum(validation.values())
        
        if score == 5:
            return "strong"
        elif score >= 3:
            return "medium"
        else:
            return "weak"
    
    @staticmethod
    def generate_secure_password(length: int = 16) -> str:
        """生成安全密码"""
        import random
        import string
        
        if length < 8:
            length = 8
        
        # 字符集
        lower = string.ascii_lowercase
        upper = string.ascii_uppercase
        digits = string.digits
        special = "!@#$%^&*()_+-=[]{}|;:,.<>?"
        
        # 确保每种类型至少一个
        password = [
            random.choice(lower),
            random.choice(upper),
            random.choice(digits),
            random.choice(special)
        ]
        
        # 填充剩余长度
        all_chars = lower + upper + digits + special
        password.extend(random.choice(all_chars) for _ in range(length - 4))
        
        # 随机打乱
        random.shuffle(password)
        
        return "".join(password)

2.6 API路由设计

python 复制代码
"""
FastAPIAdmin API路由
实现RESTful API接口
"""

from datetime import datetime, timedelta
from typing import List, Optional, Dict, Any
from fastapi import APIRouter, Depends, HTTPException, status, Query, Path, Body, Form, File, UploadFile
from fastapi.responses import JSONResponse, FileResponse
from sqlalchemy import select, func, and_, or_
from sqlalchemy.ext.asyncio import AsyncSession
import pandas as pd
from io import BytesIO

from app.config import settings
from app.database import get_db
from app.models import User, Role, Menu, AuditLog, UserSession, FileStorage, Notification
from app.schemas import (
    UserCreate, UserUpdate, UserResponse, UserListResponse,
    RoleCreate, RoleUpdate, RoleResponse,
    MenuCreate, MenuUpdate, MenuResponse,
    LoginRequest, TokenResponse, RefreshTokenRequest,
    PaginationParams, SearchParams,
    AuditLogFilter, FileUploadResponse, NotificationCreate
)
from app.core.security import (
    SecurityManager, PermissionChecker, get_current_user,
    get_current_active_user, get_current_superuser, get_user_permissions,
    require_permission, RateLimiter, PasswordValidator
)
from app.utils.pagination import paginate
from app.utils.audit_log import create_audit_log
from app.utils.file_upload import save_upload_file, validate_file_extension

# 创建路由器
router = APIRouter()


# ==================== 认证模块 ====================
@router.post("/auth/login", response_model=TokenResponse)
async def login(
    login_data: LoginRequest,
    db: AsyncSession = Depends(get_db),
    request = None
):
    """
    用户登录
    
    Args:
        login_data: 登录数据
        db: 数据库会话
        request: HTTP请求
        
    Returns:
        TokenResponse: 令牌响应
    """
    # 查找用户
    query = select(User).where(
        or_(
            User.username == login_data.username,
            User.email == login_data.username
        )
    )
    result = await db.execute(query)
    user = result.scalar_one_or_none()
    
    # 验证用户
    if not user or not SecurityManager.verify_password(login_data.password, user.hashed_password):
        await create_audit_log(
            db=db,
            user_id=None,
            log_type="login",
            module="auth",
            action=f"登录失败: {login_data.username}",
            request_ip=SecurityManager.get_client_ip(request),
            user_agent=SecurityManager.get_user_agent(request),
            success=False
        )
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="用户名或密码错误",
        )
    
    # 检查用户状态
    if not user.is_active:
        raise HTTPException(
            status_code=status.HTTP_403_FORBIDDEN,
            detail="用户已被禁用",
        )
    
    # 获取用户权限
    user_permissions = await get_user_permissions_internal(db, user.id)
    
    # 生成令牌
    token_data = SecurityManager.generate_token_data(
        user_id=user.id,
        username=user.username,
        is_superuser=user.is_superuser,
        permissions=user_permissions
    )
    
    access_token = SecurityManager.create_access_token(token_data)
    refresh_token = SecurityManager.create_refresh_token(user.id)
    session_id = SecurityManager.generate_session_id()
    
    # 创建会话
    user_session = UserSession(
        user_id=user.id,
        session_id=session_id,
        access_token=access_token,
        refresh_token=refresh_token,
        user_agent=SecurityManager.get_user_agent(request),
        ip_address=SecurityManager.get_client_ip(request),
        expires_at=datetime.utcnow() + timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
    )
    
    db.add(user_session)
    await db.commit()
    
    # 更新用户最后登录时间
    user.last_login_at = datetime.utcnow()
    await db.commit()
    
    # 记录审计日志
    await create_audit_log(
        db=db,
        user_id=user.id,
        log_type="login",
        module="auth",
        action="用户登录",
        request_ip=SecurityManager.get_client_ip(request),
        user_agent=SecurityManager.get_user_agent(request),
        success=True
    )
    
    return TokenResponse(
        access_token=access_token,
        refresh_token=refresh_token,
        token_type="bearer",
        expires_in=settings.ACCESS_TOKEN_EXPIRE_MINUTES * 60,
        user=UserResponse.from_orm(user)
    )


@router.post("/auth/logout")
async def logout(
    current_user: User = Depends(get_current_active_user),
    db: AsyncSession = Depends(get_db),
    request = None
):
    """用户登出"""
    # 获取当前令牌
    token = request.headers.get("Authorization", "").replace("Bearer ", "")
    
    if token:
        # 使会话失效
        query = select(UserSession).where(
            UserSession.access_token == token,
            UserSession.user_id == current_user.id,
            UserSession.is_active == True
        )
        result = await db.execute(query)
        session = result.scalar_one_or_none()
        
        if session:
            session.is_active = False
            await db.commit()
    
    # 记录审计日志
    await create_audit_log(
        db=db,
        user_id=current_user.id,
        log_type="logout",
        module="auth",
        action="用户登出",
        request_ip=SecurityManager.get_client_ip(request),
        user_agent=SecurityManager.get_user_agent(request),
        success=True
    )
    
    return {"message": "登出成功"}


@router.post("/auth/refresh", response_model=TokenResponse)
async def refresh_token(
    refresh_data: RefreshTokenRequest,
    db: AsyncSession = Depends(get_db)
):
    """刷新访问令牌"""
    try:
        # 验证刷新令牌
        payload = SecurityManager.verify_token(refresh_data.refresh_token)
        
        if payload.get("type") != "refresh":
            raise HTTPException(
                status_code=status.HTTP_401_UNAUTHORIZED,
                detail="Invalid token type",
            )
        
        user_id = int(payload.get("sub"))
        
        # 查找用户
        query = select(User).where(User.id == user_id, User.is_active == True)
        result = await db.execute(query)
        user = result.scalar_one_or_none()
        
        if not user:
            raise HTTPException(
                status_code=status.HTTP_401_UNAUTHORIZED,
                detail="User not found",
            )
        
        # 获取用户权限
        user_permissions = await get_user_permissions_internal(db, user.id)
        
        # 生成新令牌
        token_data = SecurityManager.generate_token_data(
            user_id=user.id,
            username=user.username,
            is_superuser=user.is_superuser,
            permissions=user_permissions
        )
        
        access_token = SecurityManager.create_access_token(token_data)
        new_refresh_token = SecurityManager.create_refresh_token(user.id)
        
        # 更新会话
        query = select(UserSession).where(
            UserSession.refresh_token == refresh_data.refresh_token,
            UserSession.user_id == user.id,
            UserSession.is_active == True
        )
        result = await db.execute(query)
        session = result.scalar_one_or_none()
        
        if session:
            session.access_token = access_token
            session.refresh_token = new_refresh_token
            session.expires_at = datetime.utcnow() + timedelta(
                minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES
            )
            await db.commit()
        
        return TokenResponse(
            access_token=access_token,
            refresh_token=new_refresh_token,
            token_type="bearer",
            expires_in=settings.ACCESS_TOKEN_EXPIRE_MINUTES * 60,
            user=UserResponse.from_orm(user)
        )
        
    except jwt.ExpiredSignatureError:
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Refresh token has expired",
        )
    except jwt.InvalidTokenError:
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Invalid refresh token",
        )


@router.get("/auth/me", response_model=UserResponse)
async def get_current_user_info(
    current_user: User = Depends(get_current_active_user)
):
    """获取当前用户信息"""
    return current_user


# ==================== 用户管理模块 ====================
@router.get("/users", response_model=UserListResponse)
@require_permission("user:read:*")
async def list_users(
    pagination: PaginationParams = Depends(),
    search: SearchParams = Depends(),
    current_user: User = Depends(get_current_user),
    db: AsyncSession = Depends(get_db)
):
    """
    获取用户列表
    
    Args:
        pagination: 分页参数
        search: 搜索参数
        current_user: 当前用户
        db: 数据库会话
        
    Returns:
        UserListResponse: 用户列表响应
    """
    query = select(User).where(User.status != "deleted")
    
    # 应用搜索条件
    if search.q:
        search_term = f"%{search.q}%"
        query = query.where(
            or_(
                User.username.ilike(search_term),
                User.email.ilike(search_term),
                User.full_name.ilike(search_term),
                User.phone.ilike(search_term)
            )
        )
    
    # 应用状态过滤
    if search.status:
        query = query.where(User.status == search.status)
    
    # 应用排序
    if pagination.sort_by:
        sort_column = getattr(User, pagination.sort_by, None)
        if sort_column:
            if pagination.desc:
                query = query.order_by(sort_column.desc())
            else:
                query = query.order_by(sort_column.asc())
    else:
        query = query.order_by(User.created_at.desc())
    
    # 执行分页查询
    total, users = await paginate(
        db=db,
        query=query,
        page=pagination.page,
        page_size=pagination.page_size,
        model=User
    )
    
    # 记录审计日志
    await create_audit_log(
        db=db,
        user_id=current_user.id,
        log_type="query",
        module="user",
        action="查询用户列表",
        request_params={
            "page": pagination.page,
            "page_size": pagination.page_size,
            "search": search.q,
            "status": search.status
        },
        success=True
    )
    
    return UserListResponse(
        items=[UserResponse.from_orm(user) for user in users],
        total=total,
        page=pagination.page,
        page_size=pagination.page_size,
        total_pages=(total + pagination.page_size - 1) // pagination.page_size
    )


@router.get("/users/{user_id}", response_model=UserResponse)
@require_permission("user:read:*")
async def get_user(
    user_id: int = Path(..., title="用户ID"),
    current_user: User = Depends(get_current_user),
    db: AsyncSession = Depends(get_db)
):
    """获取用户详情"""
    query = select(User).where(User.id == user_id, User.status != "deleted")
    result = await db.execute(query)
    user = result.scalar_one_or_none()
    
    if not user:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail="用户不存在",
        )
    
    # 记录审计日志
    await create_audit_log(
        db=db,
        user_id=current_user.id,
        log_type="query",
        module="user",
        action=f"查询用户详情: {user_id}",
        success=True
    )
    
    return UserResponse.from_orm(user)


@router.post("/users", response_model=UserResponse)
@require_permission("user:create:*")
async def create_user(
    user_data: UserCreate,
    current_user: User = Depends(get_current_user),
    db: AsyncSession = Depends(get_db)
):
    """创建用户"""
    # 检查用户名和邮箱是否已存在
    query = select(User).where(
        or_(
            User.username == user_data.username,
            User.email == user_data.email
        )
    )
    result = await db.execute(query)
    existing_user = result.scalar_one_or_none()
    
    if existing_user:
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail="用户名或邮箱已存在",
        )
    
    # 验证密码强度
    password_validation = PasswordValidator.validate(user_data.password)
    if not password_validation["is_valid"]:
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail="密码强度不足",
            headers={"X-Password-Validation": str(password_validation)}
        )
    
    # 创建用户
    hashed_password = SecurityManager.get_password_hash(user_data.password)
    user = User(
        username=user_data.username,
        email=user_data.email,
        full_name=user_data.full_name,
        phone=user_data.phone,
        hashed_password=hashed_password,
        is_active=user_data.is_active,
        is_superuser=user_data.is_superuser if current_user.is_superuser else False,
        status=user_data.status if user_data.status else "active"
    )
    
    db.add(user)
    await db.commit()
    await db.refresh(user)
    
    # 记录审计日志
    await create_audit_log(
        db=db,
        user_id=current_user.id,
        log_type="create",
        module="user",
        action=f"创建用户: {user.username}",
        request_body=user_data.dict(exclude={"password"}),
        success=True
    )
    
    return UserResponse.from_orm(user)


@router.put("/users/{user_id}", response_model=UserResponse)
@require_permission("user:update:*")
async def update_user(
    user_id: int,
    user_data: UserUpdate,
    current_user: User = Depends(get_current_user),
    db: AsyncSession = Depends(get_db)
):
    """更新用户"""
    # 获取用户
    query = select(User).where(User.id == user_id, User.status != "deleted")
    result = await db.execute(query)
    user = result.scalar_one_or_none()
    
    if not user:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail="用户不存在",
        )
    
    # 检查邮箱是否被其他用户使用
    if user_data.email and user_data.email != user.email:
        query = select(User).where(
            User.email == user_data.email,
            User.id != user_id
        )
        result = await db.execute(query)
        if result.scalar_one_or_none():
            raise HTTPException(
                status_code=status.HTTP_400_BAD_REQUEST,
                detail="邮箱已被其他用户使用",
            )
    
    # 更新用户信息
    update_data = user_data.dict(exclude_unset=True)
    
    # 如果更新密码,需要哈希处理
    if "password" in update_data:
        password_validation = PasswordValidator.validate(update_data["password"])
        if not password_validation["is_valid"]:
            raise HTTPException(
                status_code=status.HTTP_400_BAD_REQUEST,
                detail="密码强度不足",
            )
        update_data["hashed_password"] = SecurityManager.get_password_hash(update_data.pop("password"))
        update_data["password_changed_at"] = datetime.utcnow()
    
    # 普通用户不能修改超级管理员状态
    if not current_user.is_superuser and "is_superuser" in update_data:
        del update_data["is_superuser"]
    
    # 应用更新
    for field, value in update_data.items():
        setattr(user, field, value)
    
    user.updated_at = datetime.utcnow()
    await db.commit()
    await db.refresh(user)
    
    # 记录审计日志
    await create_audit_log(
        db=db,
        user_id=current_user.id,
        log_type="update",
        module="user",
        action=f"更新用户: {user_id}",
        request_body=user_data.dict(exclude={"password"}),
        success=True
    )
    
    return UserResponse.from_orm(user)


@router.delete("/users/{user_id}")
@require_permission("user:delete:*")
async def delete_user(
    user_id: int,
    current_user: User = Depends(get_current_user),
    db: AsyncSession = Depends(get_db)
):
    """删除用户(软删除)"""
    # 不能删除自己
    if user_id == current_user.id:
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail="不能删除自己的账户",
        )
    
    # 获取用户
    query = select(User).where(User.id == user_id, User.status != "deleted")
    result = await db.execute(query)
    user = result.scalar_one_or_none()
    
    if not user:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail="用户不存在",
        )
    
    # 软删除:标记为已删除状态
    user.status = "deleted"
    user.is_active = False
    user.updated_at = datetime.utcnow()
    
    await db.commit()
    
    # 记录审计日志
    await create_audit_log(
        db=db,
        user_id=current_user.id,
        log_type="delete",
        module="user",
        action=f"删除用户: {user_id}",
        success=True
    )
    
    return {"message": "用户删除成功"}


@router.post("/users/{user_id}/reset-password")
@require_permission("user:update:*")
async def reset_user_password(
    user_id: int,
    new_password: str = Body(..., embed=True),
    current_user: User = Depends(get_current_user),
    db: AsyncSession = Depends(get_db)
):
    """重置用户密码"""
    # 获取用户
    query = select(User).where(User.id == user_id, User.status != "deleted")
    result = await db.execute(query)
    user = result.scalar_one_or_none()
    
    if not user:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail="用户不存在",
        )
    
    # 验证密码强度
    password_validation = PasswordValidator.validate(new_password)
    if not password_validation["is_valid"]:
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail="密码强度不足",
        )
    
    # 重置密码
    user.hashed_password = SecurityManager.get_password_hash(new_password)
    user.password_changed_at = datetime.utcnow()
    user.updated_at = datetime.utcnow()
    
    await db.commit()
    
    # 记录审计日志
    await create_audit_log(
        db=db,
        user_id=current_user.id,
        log_type="update",
        module="user",
        action=f"重置用户密码: {user_id}",
        success=True
    )
    
    return {"message": "密码重置成功"}


# ==================== 角色管理模块 ====================
@router.get("/roles", response_model=List[RoleResponse])
@require_permission("role:read:*")
async def list_roles(
    db: AsyncSession = Depends(get_db),
    current_user: User = Depends(get_current_user)
):
    """获取角色列表"""
    query = select(Role).order_by(Role.created_at.desc())
    result = await db.execute(query)
    roles = result.scalars().all()
    
    return [RoleResponse.from_orm(role) for role in roles]


@router.post("/roles", response_model=RoleResponse)
@require_permission("role:create:*")
async def create_role(
    role_data: RoleCreate,
    db: AsyncSession = Depends(get_db),
    current_user: User = Depends(get_current_user)
):
    """创建角色"""
    # 检查角色代码是否已存在
    query = select(Role).where(Role.code == role_data.code)
    result = await db.execute(query)
    if result.scalar_one_or_none():
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail="角色代码已存在",
        )
    
    # 创建角色
    role = Role(**role_data.dict())
    db.add(role)
    await db.commit()
    await db.refresh(role)
    
    # 记录审计日志
    await create_audit_log(
        db=db,
        user_id=current_user.id,
        log_type="create",
        module="role",
        action=f"创建角色: {role.name}",
        request_body=role_data.dict(),
        success=True
    )
    
    return RoleResponse.from_orm(role)


# ==================== 菜单管理模块 ====================
@router.get("/menus", response_model=List[MenuResponse])
@require_permission("menu:read:*")
async def list_menus(
    db: AsyncSession = Depends(get_db),
    current_user: User = Depends(get_current_user)
):
    """获取菜单列表(树形结构)"""
    # 获取所有菜单
    query = select(Menu).order_by(Menu.order)
    result = await db.execute(query)
    all_menus = result.scalars().all()
    
    # 构建树形结构
    def build_tree(parent_id=None):
        tree = []
        for menu in all_menus:
            if menu.parent_id == parent_id:
                menu_dict = MenuResponse.from_orm(menu).dict()
                children = build_tree(menu.id)
                if children:
                    menu_dict["children"] = children
                tree.append(menu_dict)
        return tree
    
    return build_tree()


# ==================== 文件上传模块 ====================
@router.post("/upload", response_model=FileUploadResponse)
@require_permission("file:upload:*")
async def upload_file(
    file: UploadFile = File(...),
    is_private: bool = Form(False),
    current_user: User = Depends(get_current_user),
    db: AsyncSession = Depends(get_db)
):
    """上传文件"""
    # 验证文件类型
    if not validate_file_extension(file.filename, settings.ALLOWED_EXTENSIONS):
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail=f"不支持的文件类型,支持的类型: {', '.join(settings.ALLOWED_EXTENSIONS)}",
        )
    
    # 保存文件
    file_info = await save_upload_file(
        file=file,
        user_id=current_user.id,
        is_private=is_private,
        upload_dir=settings.UPLOAD_DIR
    )
    
    # 保存到数据库
    file_storage = FileStorage(
        user_id=current_user.id,
        **file_info
    )
    
    db.add(file_storage)
    await db.commit()
    await db.refresh(file_storage)
    
    # 记录审计日志
    await create_audit_log(
        db=db,
        user_id=current_user.id,
        log_type="upload",
        module="file",
        action=f"上传文件: {file.filename}",
        request_params={"filename": file.filename, "size": file_info["file_size"]},
        success=True
    )
    
    return FileUploadResponse(
        id=file_storage.id,
        filename=file_storage.filename,
        original_name=file_storage.original_name,
        file_size=file_storage.file_size,
        file_type=file_storage.file_type,
        access_url=file_storage.access_url,
        created_at=file_storage.created_at
    )


# ==================== 数据导出模块 ====================
@router.get("/export/users")
@require_permission("user:export:*")
async def export_users(
    format: str = Query("excel", regex="^(excel|csv|json)$"),
    db: AsyncSession = Depends(get_db),
    current_user: User = Depends(get_current_user)
):
    """导出用户数据"""
    # 查询用户数据
    query = select(User).where(User.status != "deleted")
    result = await db.execute(query)
    users = result.scalars().all()
    
    # 转换为字典列表
    user_list = []
    for user in users:
        user_dict = {
            "ID": user.id,
            "用户名": user.username,
            "邮箱": user.email,
            "姓名": user.full_name or "",
            "手机号": user.phone or "",
            "状态": user.status.value,
            "是否激活": "是" if user.is_active else "否",
            "是否超级管理员": "是" if user.is_superuser else "否",
            "创建时间": user.created_at.strftime("%Y-%m-%d %H:%M:%S"),
            "最后登录": user.last_login_at.strftime("%Y-%m-%d %H:%M:%S") if user.last_login_at else ""
        }
        user_list.append(user_dict)
    
    # 根据格式导出
    if format == "excel":
        df = pd.DataFrame(user_list)
        output = BytesIO()
        with pd.ExcelWriter(output, engine='openpyxl') as writer:
            df.to_excel(writer, index=False, sheet_name='用户数据')
        output.seek(0)
        
        # 记录审计日志
        await create_audit_log(
            db=db,
            user_id=current_user.id,
            log_type="export",
            module="user",
            action="导出用户数据(Excel)",
            success=True
        )
        
        return FileResponse(
            output,
            media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
            filename=f"users_export_{datetime.now().strftime('%Y%m%d_%H%M%S')}.xlsx"
        )
    
    elif format == "csv":
        df = pd.DataFrame(user_list)
        output = BytesIO()
        df.to_csv(output, index=False, encoding='utf-8-sig')
        output.seek(0)
        
        # 记录审计日志
        await create_audit_log(
            db=db,
            user_id=current_user.id,
            log_type="export",
            module="user",
            action="导出用户数据(CSV)",
            success=True
        )
        
        return FileResponse(
            output,
            media_type="text/csv",
            filename=f"users_export_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv"
        )
    
    else:  # json
        # 记录审计日志
        await create_audit_log(
            db=db,
            user_id=current_user.id,
            log_type="export",
            module="user",
            action="导出用户数据(JSON)",
            success=True
        )
        
        return JSONResponse(
            content={"users": user_list},
            media_type="application/json"
        )


# ==================== 系统监控模块 ====================
@router.get("/system/stats")
@require_permission("system:view:*")
async def get_system_stats(
    db: AsyncSession = Depends(get_db),
    current_user: User = Depends(get_current_user)
):
    """获取系统统计信息"""
    # 用户统计
    user_query = select(func.count(User.id)).where(User.status != "deleted")
    user_result = await db.execute(user_query)
    user_count = user_result.scalar()
    
    active_user_query = select(func.count(User.id)).where(
        User.status == "active",
        User.is_active == True
    )
    active_user_result = await db.execute(active_user_query)
    active_user_count = active_user_result.scalar()
    
    # 角色统计
    role_query = select(func.count(Role.id))
    role_result = await db.execute(role_query)
    role_count = role_result.scalar()
    
    # 文件统计
    file_query = select(
        func.count(FileStorage.id),
        func.sum(FileStorage.file_size)
    )
    file_result = await db.execute(file_query)
    file_count, total_file_size = file_result.one()
    
    # 日志统计(最近7天)
    week_ago = datetime.utcnow() - timedelta(days=7)
    log_query = select(func.count(AuditLog.id)).where(AuditLog.created_at >= week_ago)
    log_result = await db.execute(log_query)
    log_count = log_result.scalar()
    
    return {
        "user_stats": {
            "total": user_count,
            "active": active_user_count,
            "inactive": user_count - active_user_count
        },
        "role_count": role_count,
        "file_stats": {
            "total": file_count or 0,
            "total_size": total_file_size or 0,
            "total_size_human": format_file_size(total_file_size or 0)
        },
        "recent_logs": log_count or 0,
        "server_time": datetime.now().isoformat()
    }


def format_file_size(size_bytes: int) -> str:
    """格式化文件大小"""
    for unit in ['B', 'KB', 'MB', 'GB', 'TB']:
        if size_bytes < 1024.0:
            return f"{size_bytes:.2f} {unit}"
        size_bytes /= 1024.0
    return f"{size_bytes:.2f} PB"


# 辅助函数
async def get_user_permissions_internal(db: AsyncSession, user_id: int) -> List[str]:
    """内部函数:获取用户权限"""
    from sqlalchemy import select
    from app.models import Role
    
    query = select(Role).join(Role.users).where(User.id == user_id)
    result = await db.execute(query)
    roles = result.scalars().all()
    
    permissions = set()
    for role in roles:
        if role.permissions:
            permissions.update(role.permissions)
    
    return list(permissions)

2.7 前端管理界面(Jinja2模板)

html 复制代码
<!-- templates/admin/index.html -->
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>{{ settings.APP_NAME }} - 后台管理系统</title>
    
    <!-- 引入CSS -->
    <link rel="stylesheet" href="https://unpkg.com/element-plus@2.3.8/dist/index.css">
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
    <link rel="stylesheet" href="/static/css/admin.css">
    
    <!-- 引入Vue 3和Element Plus -->
    <script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
    <script src="https://unpkg.com/element-plus@2.3.8/dist/index.full.js"></script>
    <script src="https://unpkg.com/axios/dist/axios.min.js"></script>
</head>
<body>
    <div id="app">
        <!-- 页面加载动画 -->
        <div v-if="loading" class="page-loading">
            <div class="loading-spinner"></div>
            <p>加载中...</p>
        </div>

        <!-- 登录页面 -->
        <div v-if="!isAuthenticated && !loading" class="login-container">
            <div class="login-card">
                <div class="login-header">
                    <h1>{{ settings.APP_NAME }}</h1>
                    <p>后台管理系统</p>
                </div>
                
                <el-form 
                    ref="loginForm" 
                    :model="loginForm" 
                    :rules="loginRules" 
                    class="login-form"
                    @submit.prevent="handleLogin"
                >
                    <el-form-item prop="username">
                        <el-input
                            v-model="loginForm.username"
                            placeholder="请输入用户名或邮箱"
                            prefix-icon="User"
                            size="large"
                        />
                    </el-form-item>
                    
                    <el-form-item prop="password">
                        <el-input
                            v-model="loginForm.password"
                            type="password"
                            placeholder="请输入密码"
                            prefix-icon="Lock"
                            size="large"
                            show-password
                        />
                    </el-form-item>
                    
                    <el-form-item>
                        <el-checkbox v-model="loginForm.remember">记住我</el-checkbox>
                    </el-form-item>
                    
                    <el-form-item>
                        <el-button 
                            type="primary" 
                            size="large" 
                            :loading="loginLoading"
                            @click="handleLogin"
                            style="width: 100%;"
                        >
                            登录
                        </el-button>
                    </el-form-item>
                </el-form>
                
                <div class="login-footer">
                    <p>© {{ new Date().getFullYear() }} {{ settings.APP_NAME }}. All rights reserved.</p>
                    <p>Version: {{ settings.APP_VERSION }}</p>
                </div>
            </div>
        </div>

        <!-- 主页面 -->
        <div v-if="isAuthenticated && !loading" class="admin-container">
            <!-- 侧边栏 -->
            <div class="sidebar" :class="{ collapsed: sidebarCollapsed }">
                <div class="sidebar-header">
                    <div class="logo">
                        <i class="fas fa-cogs"></i>
                        <span v-if="!sidebarCollapsed">{{ settings.APP_NAME }}</span>
                    </div>
                    <el-button 
                        @click="toggleSidebar" 
                        class="collapse-btn" 
                        type="text"
                    >
                        <i :class="sidebarCollapsed ? 'fas fa-chevron-right' : 'fas fa-chevron-left'"></i>
                    </el-button>
                </div>
                
                <div class="sidebar-menu">
                    <el-menu
                        :default-active="activeMenu"
                        :collapse="sidebarCollapsed"
                        router
                        @select="handleMenuSelect"
                    >
                        <!-- 动态生成菜单 -->
                        <template v-for="menu in menuList" :key="menu.id">
                            <el-menu-item 
                                v-if="!menu.children || menu.children.length === 0"
                                :index="menu.path"
                            >
                                <template #title>
                                    <i :class="menu.icon || 'fas fa-folder'"></i>
                                    <span>{{ menu.title }}</span>
                                </template>
                            </el-menu-item>
                            
                            <el-sub-menu 
                                v-else
                                :index="menu.path"
                            >
                                <template #title>
                                    <i :class="menu.icon || 'fas fa-folder'"></i>
                                    <span>{{ menu.title }}</span>
                                </template>
                                
                                <el-menu-item
                                    v-for="child in menu.children"
                                    :key="child.id"
                                    :index="child.path"
                                >
                                    <template #title>
                                        <i :class="child.icon || 'fas fa-file'"></i>
                                        <span>{{ child.title }}</span>
                                    </template>
                                </el-menu-item>
                            </el-sub-menu>
                        </template>
                    </el-menu>
                </div>
                
                <div class="sidebar-footer">
                    <div class="user-info">
                        <el-avatar :size="40" :src="currentUser.avatar || '/static/images/default-avatar.png'">
                            {{ currentUser.username[0].toUpperCase() }}
                        </el-avatar>
                        <div v-if="!sidebarCollapsed" class="user-details">
                            <p class="username">{{ currentUser.username }}</p>
                            <p class="role">{{ currentUserRoles.join(', ') }}</p>
                        </div>
                    </div>
                </div>
            </div>
            
            <!-- 主内容区 -->
            <div class="main-content" :class="{ 'sidebar-collapsed': sidebarCollapsed }">
                <!-- 顶部导航栏 -->
                <div class="navbar">
                    <div class="navbar-left">
                        <el-breadcrumb separator="/">
                            <el-breadcrumb-item v-for="item in breadcrumb" :key="item.path">
                                {{ item.title }}
                            </el-breadcrumb-item>
                        </el-breadcrumb>
                    </div>
                    
                    <div class="navbar-right">
                        <el-dropdown @command="handleUserCommand">
                            <span class="user-dropdown">
                                <el-avatar :size="32" :src="currentUser.avatar || '/static/images/default-avatar.png'">
                                    {{ currentUser.username[0].toUpperCase() }}
                                </el-avatar>
                                <span class="username">{{ currentUser.username }}</span>
                                <i class="fas fa-chevron-down"></i>
                            </span>
                            
                            <template #dropdown>
                                <el-dropdown-menu>
                                    <el-dropdown-item command="profile">
                                        <i class="fas fa-user"></i> 个人资料
                                    </el-dropdown-item>
                                    <el-dropdown-item command="settings">
                                        <i class="fas fa-cog"></i> 系统设置
                                    </el-dropdown-item>
                                    <el-dropdown-item divided command="logout">
                                        <i class="fas fa-sign-out-alt"></i> 退出登录
                                    </el-dropdown-item>
                                </el-dropdown-menu>
                            </template>
                        </el-dropdown>
                        
                        <el-button 
                            @click="toggleTheme" 
                            type="text" 
                            class="theme-toggle"
                        >
                            <i :class="isDarkTheme ? 'fas fa-sun' : 'fas fa-moon'"></i>
                        </el-button>
                        
                        <el-button 
                            @click="toggleFullscreen" 
                            type="text"
                        >
                            <i class="fas fa-expand"></i>
                        </el-button>
                    </div>
                </div>
                
                <!-- 标签页 -->
                <div class="tabs" v-if="tabs.length > 0">
                    <el-tabs
                        v-model="activeTab"
                        type="card"
                        closable
                        @tab-click="handleTabClick"
                        @tab-remove="handleTabRemove"
                    >
                        <el-tab-pane
                            v-for="tab in tabs"
                            :key="tab.path"
                            :label="tab.title"
                            :name="tab.path"
                        />
                    </el-tabs>
                </div>
                
                <!-- 内容区域 -->
                <div class="content">
                    <router-view />
                </div>
            </div>
        </div>
    </div>

    <script>
        const { createApp, ref, reactive, computed, onMounted, watch } = Vue;
        const { ElMessage, ElMessageBox, ElNotification } = ElementPlus;
        
        createApp({
            setup() {
                // 状态管理
                const state = reactive({
                    loading: true,
                    isAuthenticated: false,
                    sidebarCollapsed: false,
                    isDarkTheme: false,
                    fullscreen: false,
                    currentUser: {},
                    menuList: [],
                    tabs: [],
                    activeTab: '',
                    activeMenu: '',
                    breadcrumb: [],
                    loginForm: {
                        username: '',
                        password: '',
                        remember: false
                    },
                    loginLoading: false,
                    loginRules: {
                        username: [
                            { required: true, message: '请输入用户名或邮箱', trigger: 'blur' }
                        ],
                        password: [
                            { required: true, message: '请输入密码', trigger: 'blur' }
                        ]
                    }
                });
                
                // 计算属性
                const currentUserRoles = computed(() => {
                    return state.currentUser.roles?.map(role => role.name) || [];
                });
                
                // API配置
                const api = axios.create({
                    baseURL: '/api/v1',
                    timeout: 10000,
                    headers: {
                        'Content-Type': 'application/json'
                    }
                });
                
                // 请求拦截器
                api.interceptors.request.use(config => {
                    const token = localStorage.getItem('access_token');
                    if (token) {
                        config.headers.Authorization = `Bearer ${token}`;
                    }
                    return config;
                });
                
                // 响应拦截器
                api.interceptors.response.use(
                    response => response.data,
                    error => {
                        if (error.response?.status === 401) {
                            // 未授权,跳转到登录页
                            state.isAuthenticated = false;
                            localStorage.removeItem('access_token');
                            localStorage.removeItem('refresh_token');
                            localStorage.removeItem('user_info');
                            ElMessage.error('登录已过期,请重新登录');
                        }
                        return Promise.reject(error);
                    }
                );
                
                // 方法
                const checkAuth = async () => {
                    try {
                        const token = localStorage.getItem('access_token');
                        if (!token) {
                            state.isAuthenticated = false;
                            return;
                        }
                        
                        const userInfo = JSON.parse(localStorage.getItem('user_info') || '{}');
                        if (userInfo.id) {
                            state.currentUser = userInfo;
                            state.isAuthenticated = true;
                            await loadMenus();
                            await loadUserPermissions();
                        }
                    } catch (error) {
                        console.error('Auth check failed:', error);
                        state.isAuthenticated = false;
                    } finally {
                        state.loading = false;
                    }
                };
                
                const handleLogin = async () => {
                    state.loginLoading = true;
                    try {
                        const response = await api.post('/auth/login', state.loginForm);
                        
                        // 保存令牌
                        localStorage.setItem('access_token', response.access_token);
                        localStorage.setItem('refresh_token', response.refresh_token);
                        localStorage.setItem('user_info', JSON.stringify(response.user));
                        
                        // 更新状态
                        state.currentUser = response.user;
                        state.isAuthenticated = true;
                        
                        // 加载菜单和权限
                        await loadMenus();
                        await loadUserPermissions();
                        
                        ElMessage.success('登录成功');
                        
                        // 跳转到首页
                        router.push('/dashboard');
                    } catch (error) {
                        ElMessage.error(error.response?.data?.detail || '登录失败');
                    } finally {
                        state.loginLoading = false;
                    }
                };
                
                const handleLogout = async () => {
                    try {
                        await ElMessageBox.confirm(
                            '确定要退出登录吗?',
                            '提示',
                            {
                                confirmButtonText: '确定',
                                cancelButtonText: '取消',
                                type: 'warning'
                            }
                        );
                        
                        await api.post('/auth/logout');
                        
                        // 清除本地存储
                        localStorage.removeItem('access_token');
                        localStorage.removeItem('refresh_token');
                        localStorage.removeItem('user_info');
                        
                        // 重置状态
                        state.isAuthenticated = false;
                        state.currentUser = {};
                        state.menuList = [];
                        
                        ElMessage.success('退出成功');
                        
                        // 跳转到登录页
                        router.push('/login');
                    } catch (error) {
                        if (error !== 'cancel') {
                            console.error('Logout failed:', error);
                        }
                    }
                };
                
                const loadMenus = async () => {
                    try {
                        const response = await api.get('/menus');
                        state.menuList = response;
                    } catch (error) {
                        console.error('Failed to load menus:', error);
                    }
                };
                
                const loadUserPermissions = async () => {
                    try {
                        const response = await api.get('/auth/permissions');
                        localStorage.setItem('user_permissions', JSON.stringify(response));
                    } catch (error) {
                        console.error('Failed to load permissions:', error);
                    }
                };
                
                const toggleSidebar = () => {
                    state.sidebarCollapsed = !state.sidebarCollapsed;
                };
                
                const toggleTheme = () => {
                    state.isDarkTheme = !state.isDarkTheme;
                    document.documentElement.setAttribute(
                        'data-theme',
                        state.isDarkTheme ? 'dark' : 'light'
                    );
                    localStorage.setItem('theme', state.isDarkTheme ? 'dark' : 'light');
                };
                
                const toggleFullscreen = () => {
                    if (!document.fullscreenElement) {
                        document.documentElement.requestFullscreen();
                        state.fullscreen = true;
                    } else {
                        document.exitFullscreen();
                        state.fullscreen = false;
                    }
                };
                
                const handleUserCommand = (command) => {
                    switch (command) {
                        case 'profile':
                            router.push('/profile');
                            break;
                        case 'settings':
                            router.push('/settings');
                            break;
                        case 'logout':
                            handleLogout();
                            break;
                    }
                };
                
                const handleMenuSelect = (index) => {
                    router.push(index);
                };
                
                const handleTabClick = (tab) => {
                    router.push(tab.props.name);
                };
                
                const handleTabRemove = (tabName) => {
                    const index = state.tabs.findIndex(tab => tab.path === tabName);
                    if (index !== -1) {
                        state.tabs.splice(index, 1);
                    }
                    
                    if (state.activeTab === tabName && state.tabs.length > 0) {
                        state.activeTab = state.tabs[state.tabs.length - 1].path;
                        router.push(state.activeTab);
                    }
                };
                
                // 初始化
                onMounted(async () => {
                    // 检查主题
                    const savedTheme = localStorage.getItem('theme') || 'light';
                    state.isDarkTheme = savedTheme === 'dark';
                    document.documentElement.setAttribute('data-theme', savedTheme);
                    
                    // 检查认证状态
                    await checkAuth();
                    
                    // 监听路由变化
                    watch(() => router.currentRoute.value, (route) => {
                        state.activeMenu = route.path;
                        state.breadcrumb = generateBreadcrumb(route);
                        updateTabs(route);
                    }, { immediate: true });
                });
                
                // 生成面包屑
                const generateBreadcrumb = (route) => {
                    const breadcrumb = [];
                    const pathArray = route.path.split('/').filter(p => p);
                    
                    let currentPath = '';
                    for (let i = 0; i < pathArray.length; i++) {
                        currentPath += '/' + pathArray[i];
                        const menuItem = findMenuItem(currentPath);
                        if (menuItem) {
                            breadcrumb.push({
                                path: currentPath,
                                title: menuItem.title
                            });
                        }
                    }
                    
                    return breadcrumb;
                };
                
                // 查找菜单项
                const findMenuItem = (path) => {
                    const search = (menus) => {
                        for (const menu of menus) {
                            if (menu.path === path) {
                                return menu;
                            }
                            if (menu.children) {
                                const found = search(menu.children);
                                if (found) return found;
                            }
                        }
                        return null;
                    };
                    
                    return search(state.menuList);
                };
                
                // 更新标签页
                const updateTabs = (route) => {
                    const menuItem = findMenuItem(route.path);
                    if (!menuItem) return;
                    
                    const existingTab = state.tabs.find(tab => tab.path === route.path);
                    if (!existingTab) {
                        state.tabs.push({
                            path: route.path,
                            title: menuItem.title
                        });
                    }
                    
                    state.activeTab = route.path;
                };
                
                // 模拟路由
                const router = {
                    currentRoute: ref({ path: '/' }),
                    push: (path) => {
                        router.currentRoute.value = { path };
                        window.history.pushState({}, '', path);
                    }
                };
                
                // 监听浏览器路由变化
                window.addEventListener('popstate', () => {
                    router.currentRoute.value = { path: window.location.pathname };
                });
                
                return {
                    ...Vue.toRefs(state),
                    currentUserRoles,
                    handleLogin,
                    handleLogout,
                    toggleSidebar,
                    toggleTheme,
                    toggleFullscreen,
                    handleUserCommand,
                    handleMenuSelect,
                    handleTabClick,
                    handleTabRemove
                };
            }
        }).use(ElementPlus).mount('#app');
    </script>
</body>
</html>

三、高级功能实现

3.1 异步任务队列(Celery)

python 复制代码
"""
FastAPIAdmin 异步任务模块
使用Celery处理后台任务
"""

import asyncio
from typing import Any, Dict, List, Optional, Union
from datetime import datetime, timedelta
from celery import Celery
from celery.schedules import crontab
import redis
import logging

from app.config import settings
from app.database import get_db
from app.models import User, Notification, AuditLog
from app.utils.email import send_email_async
from app.utils.file_export import generate_excel_report

logger = logging.getLogger(__name__)

# 创建Celery应用
celery_app = Celery(
    'fastapi_admin',
    broker=settings.CELERY_BROKER_URL or settings.REDIS_URL,
    backend=settings.CELERY_RESULT_BACKEND or settings.REDIS_URL,
    include=['app.tasks']
)

# 配置Celery
celery_app.conf.update(
    task_serializer='json',
    accept_content=['json'],
    result_serializer='json',
    timezone='Asia/Shanghai',
    enable_utc=True,
    worker_max_tasks_per_child=1000,
    worker_prefetch_multiplier=1,
)

# 定时任务配置
celery_app.conf.beat_schedule = {
    # 每天凌晨清理过期会话
    'clean-expired-sessions': {
        'task': 'app.tasks.clean_expired_sessions',
        'schedule': crontab(hour=0, minute=0),
    },
    # 每小时发送待处理通知
    'send-pending-notifications': {
        'task': 'app.tasks.send_pending_notifications',
        'schedule': crontab(minute=0),
    },
    # 每天备份数据库
    'backup-database': {
        'task': 'app.tasks.backup_database',
        'schedule': crontab(hour=2, minute=0),
    },
    # 每周清理旧日志
    'clean-old-logs': {
        'task': 'app.tasks.clean_old_logs',
        'schedule': crontab(day_of_week=0, hour=3, minute=0),
    },
}


@celery_app.task(bind=True, max_retries=3)
def send_notification_task(
    self,
    user_id: int,
    title: str,
    content: str,
    notification_type: str = "info",
    action_url: Optional[str] = None
):
    """
    发送通知任务
    
    Args:
        user_id: 用户ID
        title: 通知标题
        content: 通知内容
        notification_type: 通知类型
        action_url: 动作链接
    """
    try:
        from sqlalchemy.orm import Session
        from app.database import engine
        
        # 创建同步会话
        with Session(engine) as db:
            notification = Notification(
                user_id=user_id,
                title=title,
                content=content,
                notification_type=notification_type,
                action_url=action_url,
                expires_at=datetime.utcnow() + timedelta(days=7)
            )
            
            db.add(notification)
            db.commit()
            
            logger.info(f"Notification sent to user {user_id}: {title}")
            
            # 如果配置了邮件,发送邮件通知
            if settings.EMAILS_ENABLED:
                user = db.query(User).filter(User.id == user_id).first()
                if user and user.email:
                    asyncio.run(
                        send_email_async(
                            email_to=user.email,
                            subject=f"系统通知: {title}",
                            body=content
                        )
                    )
    
    except Exception as e:
        logger.error(f"Failed to send notification: {e}")
        raise self.retry(exc=e, countdown=60)


@celery_app.task
def export_data_task(
    user_id: int,
    data_type: str,
    filters: Dict[str, Any],
    export_format: str = "excel"
):
    """
    数据导出任务
    
    Args:
        user_id: 用户ID
        data_type: 数据类型
        filters: 过滤条件
        export_format: 导出格式
    """
    try:
        from sqlalchemy.orm import Session
        from app.database import engine
        
        with Session(engine) as db:
            # 根据数据类型查询数据
            if data_type == "users":
                query = db.query(User).filter(User.status != "deleted")
                
                # 应用过滤条件
                if filters.get("status"):
                    query = query.filter(User.status == filters["status"])
                if filters.get("start_date"):
                    query = query.filter(User.created_at >= filters["start_date"])
                if filters.get("end_date"):
                    query = query.filter(User.created_at <= filters["end_date"])
                
                data = query.all()
                
                # 生成报告
                report_path = generate_excel_report(
                    data=data,
                    data_type=data_type,
                    user_id=user_id,
                    export_format=export_format
                )
                
                # 创建通知
                notification = Notification(
                    user_id=user_id,
                    title="数据导出完成",
                    content=f"您的{data_type}数据导出已完成,文件已准备就绪。",
                    notification_type="success",
                    action_url=f"/download/{report_path}"
                )
                
                db.add(notification)
                db.commit()
                
                logger.info(f"Data export completed for user {user_id}: {data_type}")
                
                return {
                    "success": True,
                    "message": "Export completed",
                    "file_path": report_path
                }
    
    except Exception as e:
        logger.error(f"Data export failed: {e}")
        
        # 发送失败通知
        from sqlalchemy.orm import Session
        from app.database import engine
        
        with Session(engine) as db:
            notification = Notification(
                user_id=user_id,
                title="数据导出失败",
                content=f"您的{data_type}数据导出失败: {str(e)}",
                notification_type="error"
            )
            
            db.add(notification)
            db.commit()
        
        return {
            "success": False,
            "message": str(e)
        }


@celery_app.task
def clean_expired_sessions():
    """清理过期会话"""
    try:
        from sqlalchemy.orm import Session
        from app.database import engine
        from app.models import UserSession
        
        with Session(engine) as db:
            expired_count = db.query(UserSession).filter(
                UserSession.expires_at < datetime.utcnow(),
                UserSession.is_active == True
            ).update({"is_active": False})
            
            db.commit()
            
            logger.info(f"Cleaned {expired_count} expired sessions")
            
            return {"cleaned_count": expired_count}
    
    except Exception as e:
        logger.error(f"Failed to clean expired sessions: {e}")
        raise


@celery_app.task
def backup_database():
    """备份数据库"""
    try:
        import subprocess
        import os
        from datetime import datetime
        
        # 创建备份目录
        backup_dir = "backups"
        os.makedirs(backup_dir, exist_ok=True)
        
        # 生成备份文件名
        timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
        backup_file = f"{backup_dir}/backup_{timestamp}.sql"
        
        # 执行备份命令(PostgreSQL)
        cmd = [
            "pg_dump",
            "-h", settings.DATABASE_HOST,
            "-p", settings.DATABASE_PORT,
            "-U", settings.DATABASE_USER,
            "-d", settings.DATABASE_NAME,
            "-f", backup_file,
            "-F", "c"
        ]
        
        env = os.environ.copy()
        env["PGPASSWORD"] = settings.DATABASE_PASSWORD
        
        result = subprocess.run(cmd, env=env, capture_output=True, text=True)
        
        if result.returncode == 0:
            logger.info(f"Database backup completed: {backup_file}")
            
            # 记录审计日志
            from sqlalchemy.orm import Session
            from app.database import engine
            from app.models import AuditLog
            
            with Session(engine) as db:
                log = AuditLog(
                    user_id=None,
                    log_type="system",
                    module="system",
                    action="数据库备份",
                    request_params={"backup_file": backup_file},
                    success=True
                )
                db.add(log)
                db.commit()
            
            return {
                "success": True,
                "backup_file": backup_file,
                "size": os.path.getsize(backup_file)
            }
        else:
            logger.error(f"Database backup failed: {result.stderr}")
            raise Exception(result.stderr)
    
    except Exception as e:
        logger.error(f"Database backup failed: {e}")
        raise


@celery_app.task
def send_bulk_email(
    subject: str,
    content: str,
    recipient_ids: List[int],
    template_name: Optional[str] = None
):
    """批量发送邮件"""
    try:
        from sqlalchemy.orm import Session
        from app.database import engine
        from app.models import User
        
        with Session(engine) as db:
            # 获取收件人
            recipients = db.query(User).filter(
                User.id.in_(recipient_ids),
                User.is_active == True,
                User.email.isnot(None)
            ).all()
            
            sent_count = 0
            failed_count = 0
            
            for recipient in recipients:
                try:
                    asyncio.run(
                        send_email_async(
                            email_to=recipient.email,
                            subject=subject,
                            body=content,
                            template_name=template_name
                        )
                    )
                    sent_count += 1
                except Exception as e:
                    logger.error(f"Failed to send email to {recipient.email}: {e}")
                    failed_count += 1
            
            logger.info(f"Bulk email sent: {sent_count} succeeded, {failed_count} failed")
            
            return {
                "success": True,
                "sent": sent_count,
                "failed": failed_count
            }
    
    except Exception as e:
        logger.error(f"Bulk email task failed: {e}")
        raise


@celery_app.task
def generate_statistics_report(
    report_type: str,
    start_date: datetime,
    end_date: datetime,
    user_id: int
):
    """生成统计报告"""
    try:
        from sqlalchemy.orm import Session
        from app.database import engine
        from app.models import AuditLog, User
        import pandas as pd
        from datetime import datetime
        import json
        
        with Session(engine) as db:
            if report_type == "user_activity":
                # 查询用户活动日志
                logs = db.query(AuditLog).filter(
                    AuditLog.created_at >= start_date,
                    AuditLog.created_at <= end_date,
                    AuditLog.user_id.isnot(None)
                ).all()
                
                # 分析数据
                df_data = []
                for log in logs:
                    df_data.append({
                        "user_id": log.user_id,
                        "username": log.user.username if log.user else "未知",
                        "action": log.action,
                        "module": log.module,
                        "created_at": log.created_at,
                        "ip_address": log.request_ip
                    })
                
                if df_data:
                    df = pd.DataFrame(df_data)
                    
                    # 生成统计
                    stats = {
                        "total_actions": len(df_data),
                        "unique_users": df["user_id"].nunique(),
                        "top_modules": df["module"].value_counts().head(10).to_dict(),
                        "top_users": df["username"].value_counts().head(10).to_dict(),
                        "activity_by_hour": df.groupby(df["created_at"].dt.hour).size().to_dict()
                    }
                    
                    # 保存报告
                    report_dir = "reports"
                    os.makedirs(report_dir, exist_ok=True)
                    
                    timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
                    report_file = f"{report_dir}/user_activity_{timestamp}.json"
                    
                    with open(report_file, "w") as f:
                        json.dump(stats, f, indent=2, default=str)
                    
                    # 发送通知
                    send_notification_task.delay(
                        user_id=user_id,
                        title="统计报告生成完成",
                        content=f"用户活动统计报告已生成,包含{len(df_data)}条记录。",
                        notification_type="success",
                        action_url=f"/reports/{report_file}"
                    )
                    
                    return {
                        "success": True,
                        "report_file": report_file,
                        "stats": stats
                    }
                else:
                    return {
                        "success": True,
                        "message": "No data found for the specified period",
                        "stats": {}
                    }
    
    except Exception as e:
        logger.error(f"Failed to generate statistics report: {e}")
        
        # 发送失败通知
        send_notification_task.delay(
            user_id=user_id,
            title="统计报告生成失败",
            content=f"统计报告生成失败: {str(e)}",
            notification_type="error"
        )
        
        raise


@celery_app.task
def system_health_check():
    """系统健康检查"""
    try:
        import psutil
        from sqlalchemy.orm import Session
        from app.database import engine
        
        health_info = {
            "timestamp": datetime.now().isoformat(),
            "system": {
                "cpu_percent": psutil.cpu_percent(interval=1),
                "memory_percent": psutil.virtual_memory().percent,
                "disk_percent": psutil.disk_usage("/").percent,
            },
            "database": False,
            "redis": False,
            "celery": False
        }
        
        # 检查数据库
        try:
            with Session(engine) as db:
                db.execute("SELECT 1")
                health_info["database"] = True
        except Exception as e:
            health_info["database_error"] = str(e)
        
        # 检查Redis
        try:
            import redis as redis_client
            r = redis_client.from_url(settings.REDIS_URL)
            r.ping()
            health_info["redis"] = True
        except Exception as e:
            health_info["redis_error"] = str(e)
        
        # 检查Celery
        try:
            inspector = celery_app.control.inspect()
            if inspector.active():
                health_info["celery"] = True
        except Exception as e:
            health_info["celery_error"] = str(e)
        
        # 记录健康检查结果
        logger.info(f"System health check: {health_info}")
        
        # 如果发现问题,发送警报
        if (health_info["system"]["cpu_percent"] > 80 or 
            health_info["system"]["memory_percent"] > 80 or
            health_info["system"]["disk_percent"] > 90 or
            not all([health_info["database"], health_info["redis"], health_info["celery"]])):
            
            # 发送警报通知
            alert_message = f"系统健康检查发现异常: CPU={health_info['system']['cpu_percent']}%, "
            alert_message += f"Memory={health_info['system']['memory_percent']}%, "
            alert_message += f"Disk={health_info['system']['disk_percent']}%"
            
            send_notification_task.delay(
                user_id=1,  # 管理员用户
                title="系统健康警报",
                content=alert_message,
                notification_type="warning"
            )
        
        return health_info
    
    except Exception as e:
        logger.error(f"Health check failed: {e}")
        return {
            "success": False,
            "error": str(e)
        }


# 启动Celery worker
def start_celery_worker():
    """启动Celery worker"""
    import subprocess
    import sys
    
    cmd = [
        sys.executable, "-m", "celery",
        "-A", "app.tasks.celery_app",
        "worker",
        "--loglevel=info",
        "--concurrency=4"
    ]
    
    subprocess.Popen(cmd)


# 启动Celery beat
def start_celery_beat():
    """启动Celery beat(定时任务)"""
    import subprocess
    import sys
    
    cmd = [
        sys.executable, "-m", "celery",
        "-A", "app.tasks.celery_app",
        "beat",
        "--loglevel=info"
    ]
    
    subprocess.Popen(cmd)

3.2 WebSocket实时通信

python 复制代码
"""
FastAPIAdmin WebSocket模块
实现实时通信功能
"""

from typing import Dict, List, Set, Optional
from fastapi import WebSocket, WebSocketDisconnect, Depends
from sqlalchemy.ext.asyncio import AsyncSession
import json
import asyncio
import logging

from app.database import get_db
from app.models import User, Notification
from app.core.security import get_current_user_ws

logger = logging.getLogger(__name__)


class ConnectionManager:
    """WebSocket连接管理器"""
    
    def __init__(self):
        self.active_connections: Dict[int, List[WebSocket]] = {}
        self.connection_info: Dict[WebSocket, Dict] = {}
    
    async def connect(self, websocket: WebSocket, user_id: int):
        """连接WebSocket"""
        await websocket.accept()
        
        if user_id not in self.active_connections:
            self.active_connections[user_id] = []
        
        self.active_connections[user_id].append(websocket)
        self.connection_info[websocket] = {
            "user_id": user_id,
            "connected_at": asyncio.get_event_loop().time()
        }
        
        logger.info(f"User {user_id} connected via WebSocket")
    
    def disconnect(self, websocket: WebSocket):
        """断开WebSocket连接"""
        if websocket in self.connection_info:
            user_id = self.connection_info[websocket]["user_id"]
            
            if user_id in self.active_connections:
                self.active_connections[user_id].remove(websocket)
                if not self.active_connections[user_id]:
                    del self.active_connections[user_id]
            
            del self.connection_info[websocket]
            
            logger.info(f"User {user_id} disconnected from WebSocket")
    
    async def send_personal_message(self, message: dict, user_id: int):
        """发送个人消息"""
        if user_id in self.active_connections:
            for connection in self.active_connections[user_id]:
                try:
                    await connection.send_json(message)
                except Exception as e:
                    logger.error(f"Failed to send message to user {user_id}: {e}")
    
    async def broadcast(self, message: dict, exclude_user_ids: List[int] = None):
        """广播消息"""
        exclude_user_ids = exclude_user_ids or []
        
        for user_id, connections in self.active_connections.items():
            if user_id not in exclude_user_ids:
                for connection in connections:
                    try:
                        await connection.send_json(message)
                    except Exception as e:
                        logger.error(f"Failed to broadcast to user {user_id}: {e}")
    
    def get_connected_users(self) -> List[int]:
        """获取已连接的用户ID列表"""
        return list(self.active_connections.keys())


# 全局连接管理器
manager = ConnectionManager()


async def websocket_endpoint(
    websocket: WebSocket,
    token: str,
    db: AsyncSession = Depends(get_db)
):
    """
    WebSocket端点
    处理实时通信
    """
    try:
        # 验证用户
        user = await get_current_user_ws(token, db)
        if not user:
            await websocket.close(code=1008)
            return
        
        # 连接
        await manager.connect(websocket, user.id)
        
        # 发送连接成功消息
        await manager.send_personal_message({
            "type": "connected",
            "message": "WebSocket连接成功",
            "timestamp": asyncio.get_event_loop().time(),
            "user": {
                "id": user.id,
                "username": user.username,
                "email": user.email
            }
        }, user.id)
        
        # 发送未读通知
        unread_notifications = await get_unread_notifications(db, user.id)
        if unread_notifications:
            await manager.send_personal_message({
                "type": "notifications",
                "data": unread_notifications,
                "count": len(unread_notifications)
            }, user.id)
        
        # 处理消息
        try:
            while True:
                data = await websocket.receive_text()
                await handle_websocket_message(data, user, db)
                
        except WebSocketDisconnect:
            manager.disconnect(websocket)
            
    except Exception as e:
        logger.error(f"WebSocket error: {e}")
        try:
            await websocket.close(code=1011)
        except:
            pass


async def handle_websocket_message(
    message: str,
    user: User,
    db: AsyncSession
):
    """处理WebSocket消息"""
    try:
        data = json.loads(message)
        message_type = data.get("type")
        
        if message_type == "ping":
            # 心跳检测
            await manager.send_personal_message({
                "type": "pong",
                "timestamp": asyncio.get_event_loop().time()
            }, user.id)
        
        elif message_type == "notification_read":
            # 标记通知为已读
            notification_id = data.get("notification_id")
            await mark_notification_as_read(db, notification_id, user.id)
            
            await manager.send_personal_message({
                "type": "notification_read",
                "success": True,
                "notification_id": notification_id
            }, user.id)
        
        elif message_type == "subscribe":
            # 订阅特定频道
            channel = data.get("channel")
            await subscribe_to_channel(user.id, channel)
            
            await manager.send_personal_message({
                "type": "subscribed",
                "channel": channel,
                "success": True
            }, user.id)
        
        elif message_type == "unsubscribe":
            # 取消订阅
            channel = data.get("channel")
            await unsubscribe_from_channel(user.id, channel)
            
            await manager.send_personal_message({
                "type": "unsubscribed",
                "channel": channel,
                "success": True
            }, user.id)
        
        else:
            logger.warning(f"Unknown message type: {message_type}")
            
    except json.JSONDecodeError:
        logger.error("Invalid JSON message received")
    except Exception as e:
        logger.error(f"Error handling WebSocket message: {e}")


async def get_unread_notifications(db: AsyncSession, user_id: int) -> List[dict]:
    """获取未读通知"""
    from sqlalchemy import select
    
    query = select(Notification).where(
        Notification.user_id == user_id,
        Notification.is_read == False,
        Notification.expires_at > datetime.utcnow()
    ).order_by(Notification.created_at.desc()).limit(20)
    
    result = await db.execute(query)
    notifications = result.scalars().all()
    
    return [
        {
            "id": n.id,
            "title": n.title,
            "content": n.content,
            "type": n.notification_type,
            "created_at": n.created_at.isoformat(),
            "action_url": n.action_url,
            "action_text": n.action_text
        }
        for n in notifications
    ]


async def mark_notification_as_read(
    db: AsyncSession,
    notification_id: int,
    user_id: int
):
    """标记通知为已读"""
    from sqlalchemy import select, update
    
    query = select(Notification).where(
        Notification.id == notification_id,
        Notification.user_id == user_id
    )
    
    result = await db.execute(query)
    notification = result.scalar_one_or_none()
    
    if notification and not notification.is_read:
        notification.is_read = True
        notification.read_at = datetime.utcnow()
        await db.commit()


# 实时通知发送函数
async def send_real_time_notification(
    user_id: int,
    title: str,
    content: str,
    notification_type: str = "info",
    action_url: Optional[str] = None
):
    """发送实时通知"""
    notification_data = {
        "type": "new_notification",
        "data": {
            "title": title,
            "content": content,
            "type": notification_type,
            "action_url": action_url,
            "timestamp": datetime.now().isoformat()
        }
    }
    
    await manager.send_personal_message(notification_data, user_id)
    logger.info(f"Real-time notification sent to user {user_id}: {title}")


# 实时数据更新广播
async def broadcast_data_update(
    data_type: str,
    data: dict,
    action: str = "update",
    exclude_user_ids: List[int] = None
):
    """广播数据更新"""
    message = {
        "type": "data_update",
        "data_type": data_type,
        "action": action,
        "data": data,
        "timestamp": datetime.now().isoformat()
    }
    
    await manager.broadcast(message, exclude_user_ids)
    logger.info(f"Data update broadcast: {data_type} {action}")


# 用户状态管理
class UserStatusManager:
    """用户状态管理器"""
    
    def __init__(self):
        self.user_status: Dict[int, Dict] = {}
    
    def set_user_online(self, user_id: int, client_info: Dict = None):
        """设置用户在线"""
        self.user_status[user_id] = {
            "status": "online",
            "last_seen": datetime.now(),
            "client_info": client_info or {}
        }
    
    def set_user_offline(self, user_id: int):
        """设置用户离线"""
        if user_id in self.user_status:
            self.user_status[user_id]["status"] = "offline"
            self.user_status[user_id]["last_seen"] = datetime.now()
    
    def get_user_status(self, user_id: int) -> Dict:
        """获取用户状态"""
        return self.user_status.get(user_id, {"status": "offline"})
    
    def get_online_users(self) -> List[int]:
        """获取在线用户列表"""
        return [
            user_id for user_id, status in self.user_status.items()
            if status.get("status") == "online"
        ]


# 全局用户状态管理器
user_status_manager = UserStatusManager()


async def update_user_status_websocket(user_id: int, status: str):
    """通过WebSocket更新用户状态"""
    status_message = {
        "type": "user_status",
        "user_id": user_id,
        "status": status,
        "timestamp": datetime.now().isoformat()
    }
    
    # 广播给所有连接的用户(除了自己)
    await manager.broadcast(status_message, exclude_user_ids=[user_id])
    
    # 更新状态管理器
    if status == "online":
        user_status_manager.set_user_online(user_id)
    elif status == "offline":
        user_status_manager.set_user_offline(user_id)


# WebSocket依赖注入
async def get_current_user_ws(token: str, db: AsyncSession) -> Optional[User]:
    """WebSocket用户认证"""
    from app.core.security import SecurityManager
    from sqlalchemy import select
    
    try:
        payload = SecurityManager.verify_token(token)
        user_id = int(payload.get("sub"))
        
        query = select(User).where(User.id == user_id, User.is_active == True)
        result = await db.execute(query)
        return result.scalar_one_or_none()
        
    except Exception as e:
        logger.error(f"WebSocket authentication failed: {e}")
        return None

四、部署与监控

4.1 Docker部署配置

yaml 复制代码
# docker-compose.yml
version: '3.8'

services:
  # 主应用服务
  backend:
    build:
      context: .
      dockerfile: Dockerfile
    ports:
      - "8000:8000"
    environment:
      - ENVIRONMENT=production
      - DATABASE_HOST=postgres
      - DATABASE_PORT=5432
      - DATABASE_USER=${DATABASE_USER:-postgres}
      - DATABASE_PASSWORD=${DATABASE_PASSWORD:-postgres}
      - DATABASE_NAME=${DATABASE_NAME:-fastapi_admin}
      - REDIS_HOST=redis
      - REDIS_PORT=6379
      - SECRET_KEY=${SECRET_KEY}
    depends_on:
      - postgres
      - redis
    volumes:
      - ./uploads:/app/uploads
      - ./logs:/app/logs
    networks:
      - fastapi-network
    restart: unless-stopped

  # PostgreSQL数据库
  postgres:
    image: postgres:15-alpine
    environment:
      - POSTGRES_USER=${DATABASE_USER:-postgres}
      - POSTGRES_PASSWORD=${DATABASE_PASSWORD:-postgres}
      - POSTGRES_DB=${DATABASE_NAME:-fastapi_admin}
    volumes:
      - postgres_data:/var/lib/postgresql/data
      - ./backups:/backups
    ports:
      - "5432:5432"
    networks:
      - fastapi-network
    restart: unless-stopped
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 30s
      timeout: 10s
      retries: 3

  # Redis缓存
  redis:
    image: redis:7-alpine
    ports:
      - "6379:6379"
    volumes:
      - redis_data:/data
    networks:
      - fastapi-network
    restart: unless-stopped
    command: redis-server --appendonly yes

  # Celery worker
  celery_worker:
    build:
      context: .
      dockerfile: Dockerfile
    command: celery -A app.tasks.celery_app worker --loglevel=info --concurrency=4
    environment:
      - ENVIRONMENT=production
      - DATABASE_HOST=postgres
      - DATABASE_PORT=5432
      - DATABASE_USER=${DATABASE_USER:-postgres}
      - DATABASE_PASSWORD=${DATABASE_PASSWORD:-postgres}
      - DATABASE_NAME=${DATABASE_NAME:-fastapi_admin}
      - REDIS_HOST=redis
      - REDIS_PORT=6379
      - CELERY_BROKER_URL=redis://redis:6379/0
      - CELERY_RESULT_BACKEND=redis://redis:6379/0
    depends_on:
      - postgres
      - redis
    volumes:
      - ./uploads:/app/uploads
      - ./logs:/app/logs
    networks:
      - fastapi-network
    restart: unless-stopped

  # Celery beat(定时任务)
  celery_beat:
    build:
      context: .
      dockerfile: Dockerfile
    command: celery -A app.tasks.celery_app beat --loglevel=info
    environment:
      - ENVIRONMENT=production
      - DATABASE_HOST=postgres
      - DATABASE_PORT=5432
      - DATABASE_USER=${DATABASE_USER:-postgres}
      - DATABASE_PASSWORD=${DATABASE_PASSWORD:-postgres}
      - DATABASE_NAME=${DATABASE_NAME:-fastapi_admin}
      - REDIS_HOST=redis
      - REDIS_PORT=6379
      - CELERY_BROKER_URL=redis://redis:6379/0
      - CELERY_RESULT_BACKEND=redis://redis:6379/0
    depends_on:
      - postgres
      - redis
    volumes:
      - ./uploads:/app/uploads
      - ./logs:/app/logs
    networks:
      - fastapi-network
    restart: unless-stopped

  # Nginx反向代理
  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx/nginx.conf:/etc/nginx/nginx.conf
      - ./nginx/ssl:/etc/nginx/ssl
      - ./static:/var/www/static
      - ./uploads:/var/www/uploads
    depends_on:
      - backend
    networks:
      - fastapi-network
    restart: unless-stopped

  # Prometheus监控
  prometheus:
    image: prom/prometheus:latest
    ports:
      - "9090:9090"
    volumes:
      - ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml
      - prometheus_data:/prometheus
    command:
      - '--config.file=/etc/prometheus/prometheus.yml'
      - '--storage.tsdb.path=/prometheus'
      - '--web.console.libraries=/etc/prometheus/console_libraries'
      - '--web.console.templates=/etc/prometheus/consoles'
      - '--storage.tsdb.retention.time=200h'
      - '--web.enable-lifecycle'
    networks:
      - fastapi-network
    restart: unless-stopped

  # Grafana仪表板
  grafana:
    image: grafana/grafana:latest
    ports:
      - "3000:3000"
    environment:
      - GF_SECURITY_ADMIN_PASSWORD=${GRAFANA_PASSWORD:-admin}
    volumes:
      - grafana_data:/var/lib/grafana
      - ./grafana/dashboards:/etc/grafana/provisioning/dashboards
      - ./grafana/datasources:/etc/grafana/provisioning/datasources
    networks:
      - fastapi-network
    restart: unless-stopped
    depends_on:
      - prometheus

volumes:
  postgres_data:
  redis_data:
  prometheus_data:
  grafana_data:

networks:
  fastapi-network:
    driver: bridge

4.2 Dockerfile

dockerfile 复制代码
# Dockerfile
FROM python:3.11-slim as builder

# 安装编译依赖
RUN apt-get update && apt-get install -y \
    gcc \
    g++ \
    libpq-dev \
    curl \
    && rm -rf /var/lib/apt/lists/*

# 设置工作目录
WORKDIR /app

# 复制依赖文件
COPY requirements.txt .

# 安装Python依赖
RUN pip install --no-cache-dir --upgrade pip && \
    pip install --no-cache-dir -r requirements.txt

# 生产阶段
FROM python:3.11-slim

# 安装运行时依赖
RUN apt-get update && apt-get install -y \
    libpq-dev \
    curl \
    && rm -rf /var/lib/apt/lists/*

# 创建非root用户
RUN useradd -m -u 1000 fastapi && \
    mkdir -p /app && \
    chown -R fastapi:fastapi /app

# 切换用户
USER fastapi

# 设置工作目录
WORKDIR /app

# 从builder阶段复制已安装的包
COPY --from=builder /usr/local/lib/python3.11/site-packages /usr/local/lib/python3.11/site-packages
COPY --from=builder /usr/local/bin /usr/local/bin

# 复制应用代码
COPY --chown=fastapi:fastapi . .

# 创建必要的目录
RUN mkdir -p uploads logs

# 设置环境变量
ENV PYTHONPATH=/app
ENV PYTHONUNBUFFERED=1
ENV PYTHONDONTWRITEBYTECODE=1

# 暴露端口
EXPOSE 8000

# 健康检查
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
    CMD curl -f http://localhost:8000/health || exit 1

# 启动命令
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "4"]

4.3 监控配置

yaml 复制代码
# prometheus/prometheus.yml
global:
  scrape_interval: 15s
  evaluation_interval: 15s

scrape_configs:
  - job_name: 'fastapi-admin'
    static_configs:
      - targets: ['backend:8000']
    metrics_path: '/metrics'

  - job_name: 'postgres'
    static_configs:
      - targets: ['postgres:9187']

  - job_name: 'redis'
    static_configs:
      - targets: ['redis:9121']

  - job_name: 'node-exporter'
    static_configs:
      - targets: ['node-exporter:9100']

alerting:
  alertmanagers:
    - static_configs:
        - targets:
          # - alertmanager:9093

rule_files:
  # - "first_rules.yml"
  # - "second_rules.yml"

五、测试与质量保证

5.1 单元测试

python 复制代码
"""
FastAPIAdmin 单元测试
使用pytest进行测试
"""

import pytest
from fastapi.testclient import TestClient
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from unittest.mock import Mock, patch

from app.main import app
from app.database import Base, get_db
from app.models import User, Role
from app.core.security import get_password_hash

# 测试数据库
SQLALCHEMY_DATABASE_URL = "sqlite:///./test.db"

engine = create_engine(
    SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False}
)
TestingSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)


def override_get_db():
    """覆盖数据库依赖"""
    try:
        db = TestingSessionLocal()
        yield db
    finally:
        db.close()


# 覆盖依赖
app.dependency_overrides[get_db] = override_get_db

# 测试客户端
client = TestClient(app)


@pytest.fixture(scope="function")
def test_db():
    """测试数据库fixture"""
    # 创建表
    Base.metadata.create_all(bind=engine)
    
    db = TestingSessionLocal()
    
    # 创建测试数据
    # 创建测试用户
    hashed_password = get_password_hash("testpassword123")
    test_user = User(
        username="testuser",
        email="test@example.com",
        hashed_password=hashed_password,
        full_name="Test User",
        is_active=True,
        is_superuser=False
    )
    
    db.add(test_user)
    db.commit()
    db.refresh(test_user)
    
    # 创建测试角色
    test_role = Role(
        name="测试角色",
        code="test_role",
        description="测试角色",
        permissions=["test:read:*", "test:create:*"]
    )
    
    db.add(test_role)
    db.commit()
    db.refresh(test_role)
    
    yield db
    
    # 清理
    db.close()
    Base.metadata.drop_all(bind=engine)


class TestAuthentication:
    """认证测试"""
    
    def test_login_success(self, test_db):
        """测试成功登录"""
        response = client.post("/api/v1/auth/login", json={
            "username": "testuser",
            "password": "testpassword123"
        })
        
        assert response.status_code == 200
        data = response.json()
        assert "access_token" in data
        assert "refresh_token" in data
        assert data["user"]["username"] == "testuser"
    
    def test_login_invalid_credentials(self):
        """测试无效凭证登录"""
        response = client.post("/api/v1/auth/login", json={
            "username": "nonexistent",
            "password": "wrongpassword"
        })
        
        assert response.status_code == 401
        data = response.json()
        assert data["detail"] == "用户名或密码错误"
    
    def test_login_inactive_user(self, test_db):
        """测试非活跃用户登录"""
        # 创建非活跃用户
        hashed_password = get_password_hash("password123")
        inactive_user = User(
            username="inactive",
            email="inactive@example.com",
            hashed_password=hashed_password,
            is_active=False
        )
        
        test_db.add(inactive_user)
        test_db.commit()
        
        response = client.post("/api/v1/auth/login", json={
            "username": "inactive",
            "password": "password123"
        })
        
        assert response.status_code == 403
        data = response.json()
        assert data["detail"] == "用户已被禁用"


class TestUserManagement:
    """用户管理测试"""
    
    def test_get_current_user(self, test_db):
        """测试获取当前用户"""
        # 先登录获取令牌
        login_response = client.post("/api/v1/auth/login", json={
            "username": "testuser",
            "password": "testpassword123"
        })
        
        token = login_response.json()["access_token"]
        
        # 使用令牌获取当前用户
        response = client.get(
            "/api/v1/auth/me",
            headers={"Authorization": f"Bearer {token}"}
        )
        
        assert response.status_code == 200
        data = response.json()
        assert data["username"] == "testuser"
        assert data["email"] == "test@example.com"
    
    def test_list_users_unauthorized(self):
        """测试未授权访问用户列表"""
        response = client.get("/api/v1/users")
        
        assert response.status_code == 401
    
    def test_create_user_success(self, test_db):
        """测试创建用户"""
        # 先登录获取令牌(需要管理员权限)
        hashed_password = get_password_hash("admin123")
        admin_user = User(
            username="admin",
            email="admin@example.com",
            hashed_password=hashed_password,
            is_active=True,
            is_superuser=True
        )
        
        test_db.add(admin_user)
        test_db.commit()
        
        login_response = client.post("/api/v1/auth/login", json={
            "username": "admin",
            "password": "admin123"
        })
        
        token = login_response.json()["access_token"]
        
        # 创建新用户
        response = client.post(
            "/api/v1/users",
            headers={"Authorization": f"Bearer {token}"},
            json={
                "username": "newuser",
                "email": "newuser@example.com",
                "password": "StrongPassword123!",
                "full_name": "New User",
                "phone": "13800138000",
                "is_active": True
            }
        )
        
        assert response.status_code == 200
        data = response.json()
        assert data["username"] == "newuser"
        assert data["email"] == "newuser@example.com"
        assert "password" not in data  # 密码不应返回
    
    def test_create_user_weak_password(self, test_db):
        """测试创建用户时密码强度不足"""
        # 登录
        login_response = client.post("/api/v1/auth/login", json={
            "username": "testuser",
            "password": "testpassword123"
        })
        
        token = login_response.json()["access_token"]
        
        # 尝试创建用户(弱密码)
        response = client.post(
            "/api/v1/users",
            headers={"Authorization": f"Bearer {token}"},
            json={
                "username": "weakuser",
                "email": "weak@example.com",
                "password": "123",  # 弱密码
                "full_name": "Weak User"
            }
        )
        
        assert response.status_code == 400
        data = response.json()
        assert "密码强度不足" in data["detail"]


class TestRoleManagement:
    """角色管理测试"""
    
    def test_list_roles(self, test_db):
        """测试获取角色列表"""
        # 登录
        login_response = client.post("/api/v1/auth/login", json={
            "username": "testuser",
            "password": "testpassword123"
        })
        
        token = login_response.json()["access_token"]
        
        response = client.get(
            "/api/v1/roles",
            headers={"Authorization": f"Bearer {token}"}
        )
        
        assert response.status_code == 200
        data = response.json()
        assert isinstance(data, list)
    
    def test_create_role(self, test_db):
        """测试创建角色"""
        # 创建管理员用户
        hashed_password = get_password_hash("admin123")
        admin_user = User(
            username="admin_role",
            email="admin_role@example.com",
            hashed_password=hashed_password,
            is_active=True,
            is_superuser=True
        )
        
        test_db.add(admin_user)
        test_db.commit()
        
        login_response = client.post("/api/v1/auth/login", json={
            "username": "admin_role",
            "password": "admin123"
        })
        
        token = login_response.json()["access_token"]
        
        # 创建新角色
        response = client.post(
            "/api/v1/roles",
            headers={"Authorization": f"Bearer {token}"},
            json={
                "name": "测试管理员",
                "code": "test_admin",
                "description": "测试管理员角色",
                "permissions": ["system:*", "user:*"],
                "is_system": False
            }
        )
        
        assert response.status_code == 200
        data = response.json()
        assert data["name"] == "测试管理员"
        assert data["code"] == "test_admin"
        assert data["permissions"] == ["system:*", "user:*"]


class TestFileUpload:
    """文件上传测试"""
    
    @patch("app.utils.file_upload.save_upload_file")
    def test_upload_file(self, mock_save, test_db):
        """测试文件上传"""
        mock_save.return_value = {
            "filename": "test_123.jpg",
            "original_name": "test.jpg",
            "file_path": "uploads/images/test_123.jpg",
            "file_size": 1024,
            "file_type": "image/jpeg",
            "file_ext": "jpg",
            "access_url": "/uploads/images/test_123.jpg"
        }
        
        # 登录
        login_response = client.post("/api/v1/auth/login", json={
            "username": "testuser",
            "password": "testpassword123"
        })
        
        token = login_response.json()["access_token"]
        
        # 模拟文件上传
        files = {"file": ("test.jpg", b"fake image content", "image/jpeg")}
        data = {"is_private": False}
        
        response = client.post(
            "/api/v1/upload",
            headers={"Authorization": f"Bearer {token}"},
            files=files,
            data=data
        )
        
        assert response.status_code == 200
        data = response.json()
        assert data["filename"] == "test_123.jpg"
        assert data["file_type"] == "image/jpeg"
    
    def test_upload_invalid_file_type(self, test_db):
        """测试上传无效文件类型"""
        # 登录
        login_response = client.post("/api/v1/auth/login", json={
            "username": "testuser",
            "password": "testpassword123"
        })
        
        token = login_response.json()["access_token"]
        
        # 尝试上传不支持的文件类型
        files = {"file": ("test.exe", b"fake exe content", "application/octet-stream")}
        
        response = client.post(
            "/api/v1/upload",
            headers={"Authorization": f"Bearer {token}"},
            files=files
        )
        
        assert response.status_code == 400
        data = response.json()
        assert "不支持的文件类型" in data["detail"]


class TestSystemStats:
    """系统统计测试"""
    
    def test_get_system_stats(self, test_db):
        """测试获取系统统计"""
        # 创建管理员用户
        hashed_password = get_password_hash("admin123")
        admin_user = User(
            username="admin_stats",
            email="admin_stats@example.com",
            hashed_password=hashed_password,
            is_active=True,
            is_superuser=True
        )
        
        test_db.add(admin_user)
        test_db.commit()
        
        login_response = client.post("/api/v1/auth/login", json={
            "username": "admin_stats",
            "password": "admin123"
        })
        
        token = login_response.json()["access_token"]
        
        response = client.get(
            "/api/v1/system/stats",
            headers={"Authorization": f"Bearer {token}"}
        )
        
        assert response.status_code == 200
        data = response.json()
        assert "user_stats" in data
        assert "role_count" in data
        assert "file_stats" in data
        assert "server_time" in data


# 运行测试
if __name__ == "__main__":
    pytest.main(["-v", "--tb=short"])

六、总结与最佳实践

6.1 架构设计总结

FastAPIAdmin 系统架构设计体现了以下特点:

  1. 模块化设计:清晰的目录结构,各模块职责分明
  2. 异步处理:全面使用 async/await,提高并发性能
  3. 前后端分离:RESTful API + 现代化前端框架
  4. 微服务就绪:易于扩展为微服务架构
  5. 容器化部署:完整的Docker支持,便于CI/CD

6.2 安全最佳实践

  1. 认证授权:JWT + RBAC权限控制
  2. 密码安全:bcrypt哈希 + 密码强度验证
  3. 输入验证:Pydantic模型验证所有输入
  4. SQL注入防护:SQLAlchemy ORM + 参数化查询
  5. 文件上传安全:文件类型验证 + 病毒扫描

6.3 性能优化建议

  1. 数据库优化

    • 使用索引优化查询性能
    • 连接池管理数据库连接
    • 读写分离架构
  2. 缓存策略

    • Redis缓存热点数据
    • 分布式缓存架构
    • 缓存失效策略
  3. 异步处理

    • Celery处理后台任务
    • 异步文件上传
    • WebSocket实时通信

6.4 扩展性考虑

  1. 水平扩展:无状态设计,支持多实例部署
  2. 微服务拆分:可按模块拆分为独立服务
  3. 插件系统:支持功能插件扩展
  4. API网关:支持API版本管理和路由

6.5 部署与监控

  1. 容器化部署:Docker + Kubernetes
  2. 健康检查:应用级健康检查端点
  3. 日志聚合:集中式日志收集和分析
  4. 性能监控:Prometheus + Grafana监控

6.6 未来发展路线图

阶段 功能 描述
1.0 基础版本 用户管理、角色权限、系统监控
1.5 增强版本 工作流引擎、报表系统、消息中心
2.0 企业版 多租户支持、单点登录、审计日志
2.5 云原生 容器编排、服务网格、自动伸缩
3.0 智能化 AI辅助决策、智能监控、预测分析

FastAPIAdmin 是一个现代化、高性能的后台管理系统解决方案,它结合了FastAPI的高性能和现代Web开发的最佳实践,为构建企业级后台管理系统提供了完整的框架和工具链。通过采用模块化设计、异步编程和容器化部署,系统具有良好的可扩展性和维护性,能够满足从小型项目到大型企业应用的不同需求。

相关推荐
Cherry的跨界思维10 小时前
【AI测试全栈:Vue核心】19、Vue3+ECharts实战:构建AI测试可视化仪表盘全攻略
前端·人工智能·python·echarts·vue3·ai全栈·ai测试全栈
海棠AI实验室10 小时前
第十七章 调试与排错:读懂 Traceback 的方法论
python·pandas·调试
2501_9418787410 小时前
在奥克兰云原生实践中构建动态配置中心以支撑系统稳定演进的工程经验总结
开发语言·python
Rabbit_QL10 小时前
【Pytorch使用】CUDA 显存管理与 OOM 排查实战:以 PyTorch 联邦学习训练为例
人工智能·pytorch·python
weixin_4432978810 小时前
Python打卡训练营第31天
开发语言·python
宏基骑士10 小时前
Python之类中函数间的参数传递(有继承和无继承)
python
540_54010 小时前
ADVANCE Day41
人工智能·python·深度学习
0思必得010 小时前
[Web自动化] BeautifulSoup导航文档树
前端·python·自动化·html·beautifulsoup
vyuvyucd11 小时前
Python条件与循环语句全解析
python
gf132111111 小时前
制作卡点视频
数据库·python·音视频