fastapi开发

文章目录

  • 0.已开发功能介绍
    • 0.1 登录页面:
    • 0.2 聊天页面支持接入本地知识库(基于本地部署的开源`AI`大模型)
    • 0.3 支持使用`Dockerfile`和`kubectl`部署项目
    • 0.4 内嵌生命科学项目ketcher-standalone支持单纯web侧化学表达式编辑
    • 0.5 首页
    • 0.6 列表页面
    • 0.7 编辑页面:
    • 0.8 websocket页面:
    • 0.9 内嵌Swagger和Redoc(加鉴权)
  • 1. 安装后端环境
  • 2. 开发工具配置`python`
  • 3. 使用`fastapi`
  • 4.`Annotated`用法
  • 5. `dataclasses`用法
  • 6. `Depends`用法(`fastapi`)
  • 7. 配置文件(`pydantic + env`文件)
  • 8. 同步连接`mysql(sqlachemy + pymysql)`
  • 9. 异步连接`mysql (sqlachemy + aiomysql)`
  • 10. 抛自定义异常`HTTPException`
  • 11. 自定义全局异常处理`app.exception_handler`
    • 11.1 方式1: 装饰器方式(以捕获`RequestValidationError`修改异常返回结果为例),此方式需要和`FastAPI()`示例对象在一个文件中,在其他地方导入无效(本人暂时发现)
    • 11.2 方式2:使用`FastAPI exception_handlers`参数
    • 11.3 方式3:通过`FastAPI `示例对象的方法`add_exception_handler`
  • 12. 中间件
    • 12.1 方式1: 装饰器`@app.middleware("http")`
    • 12.2 方式2:`FastAPI`示例对象的方法`add_middleware`
  • 13. `jsonable_encoder`(待整理)
  • 14. `Pydantic`中的`BaseModel`
  • 15. 跨域问题
  • 16. 前端路由响应状态码为 `307 Temporary Redirect `然后直接访问后端接口
  • 17. 自动生成模型类工具`sqlacodegen`
  • 18. `msyql8.0`登录报错`ERROR 1045 (28000) Access denied for user ‘root‘@‘localhost‘ (using password YES/NO)`加上`skip-grant-tables`后无法启动
  • 19. `jwt`验证
  • 20. 使用pydantic响应模型from_orm转化sqlalchemy查询结果运行提示"* 'orm_mode' has been renamed to 'from_attributes'"
    • 20.1 坑1:from_orm已改为from_attributes,会报错“pydantic.errors.PydanticUserError: You must set the config attribute `from_attributes=True` to use from_orm”
    • 20.2 坑2:报错“ pydantic_core._pydantic_core.ValidationError: 1 validation error for Menuweb_name
  • 21. websocket功能
    • 21.1 数据库建表
    • 21.2 后端
      • 21.2.1 关联查询问题:注意以上`sqlalchemy`关联查询使用的是`res.scalars()`只会返回`select(BaseUser, Messages)`中模型类`BaseUser`字段对应的查询结果,即第一个`select`中的内容,因为模型类和数据库没有使用外键。若使用外键的话需要再模型类字段使用`ForeignKey`指定外键和`relationship`指定表之间的关系
    • 21.3 前端页面设计
    • 21.4 报错1:`ChatWindow.vue:65 WebSocket connection to 'ws://localhost:3000/api/1' failed: WebSocket is closed before the connection is established.`
  • 22 菜单管理(按钮)和路由控制
    • 22.1 按钮权限管理:参考[20. 按钮权限管理: 用于管理前端页面标签和按钮的展示,用于前端层面鉴权](https://blog.csdn.net/weixin_44894663/article/details/140547092)用于管理
    • 22.2 路由管理:用于管理后端路由和角色之间的关系,用于后端鉴权
  • 23. `uvicorn`配置日志使用参数`log_config`
  • 24. `fastapi`中生命周期管理`lifespan`,`@app.on_event("startup")`, ` @app.on_event("shutdown") `
  • 25. `Python 3.11.9 , aiosmtplib 3.0.2`发送邮件报错`aiosmtplib.errors.SMTPServerDisconnected: Unexpected EOF received” `
  • 26. `stmplib`和`aiosmtplib`发送邮件
    • 26.1 消息结构
    • 26.2 `smtp`发送消息
    • 26.3 `aiosmtplib`发送消息
  • 27. `shell`方式启动项目
    • 27.1 启动方式1:
    • 27.2 启动方式2:
    • 27.3 启动方式3(推荐):
  • 28. `shell`脚本

前端: vue3+vite5前端(一)
前端: vue3+vite5前端(二)
部署: win10搭建minikube(driver=docker)部署项目
大模型与知识库部署:
sglang、vllm和llama.cpp部署大模型
win10使用ollama+docker搭建本地知识库(open webui && anythingllm)
win10使用ollama+docker搭建本地知识库(ragflow)
win10使用ollama+docker搭建本地知识库(dify)

0.已开发功能介绍

0.1 登录页面:

在这里插入图片描述

0.2 聊天页面支持接入本地知识库(基于本地部署的开源AI大模型)

在这里插入图片描述

0.3 支持使用Dockerfilekubectl部署项目

在这里插入图片描述

0.4 内嵌生命科学项目ketcher-standalone支持单纯web侧化学表达式编辑

在这里插入图片描述

0.5 首页

在这里插入图片描述

0.6 列表页面

0.7 编辑页面:

在这里插入图片描述

0.8 websocket页面:

【管理员】
在这里插入图片描述

【普通用户】:
在这里插入图片描述

0.9 内嵌Swagger和Redoc(加鉴权)

在这里插入图片描述

1. 安装后端环境

查看python可用版本:https://www.python.org/downloads/

查看miniconda支持python版本:https://docs.anaconda.com/miniconda/miniconda-other-installer-links/

# miniconda安装python
conda create --name python311 python=3.11

# 更新conda版本
conda update -n base -c defaults conda

# 切换环境python3.11
conda activate python311

# 安装fastapi
conda install fastapi     (0.103)
conda install uvicorn     (0.20)

# 密码加密和jwt: 
pip install passlib
pip install bcrypt
pip install pyjwt

2. 开发工具配置python

vscode配置widows本地环境:Ctrl+Shift+P --> Python:Select Interpreter

pycharm配置:可参考miniconda安装Python虚拟环境

3. 使用fastapi

__main__.py定义启动文件

from fastapi import FastAPI

app = FastAPI


@app.get("/")
async def get_hello_world():
    return {"message": "hello world!!!"}

if __name__ == '__main__':
    import uvicorn
    uvicorn.run(app, host="127.0.0.1", port=8080)
appfastapi应用实例
host运行的ip地址,默认127.0.0.1
port端口,默认8000
reloadtrue/false,文件改动自动重载载
log_level日志级别,debug,info,warning,error,critical
workers指定启动的工作进程数量
access_logtrue/false,记录访问日志
timeout_keep_alive定义空心链接在关闭前保持活动的秒数,默认5秒
proxy_headers是否信任X-Forwarded-*头部信息,设置为True会允许Uvicorn根据这些头信息正确解析客户端的真实IP地址等信息。
root_path指定根目录
limit_concurrency限制同时处理请求的最大数量,防止服务过载
limit_max_requests指定每个工作进程能处理的最大请求数量,达到后会重启

终端命令运行:uvicorn app文件名:app --reload 或者python -m main文件目录名称

4.Annotated用法

Annnotated是python标准库typing中的一种注解,用于为类型添加额外的元数据(即传递给类型检查器额外信息)

官方文档:https://docs.python.org/zh-tw/3/library/typing.html#typing.Annotated

语法:Annotated[<type>, <metadata1>, <metadata2>]

5. dataclasses用法

dataclasses自定义一种数据,可通过对象形式访问属性,并对属性添加注解。官方文档:https://docs.python.org/zh-cn/3.11/library/dataclasses.html

from dataclasses import dataclass, field
from typing import List

@dataclass
class User:
    name: str
    age: int
    email: str

@dataclass
class Team:
    city: str
    teammate: List[User] = field(default_factory=list)


user1 = User('1111', 444, '111111')
user2 = User('2222', 55, "22222")
team1 = Team('team111', [user1, user2])

# User(name='1111', age=444, email='111111')
# User(name='2222', age=55, email='22222')
for i in team1.teammate:
    print(i)

typingOptionalUnion说明:

from typing import Optional, Union

# 表示字段可选,可为None,str类型
Optinal[str]

# 表示字段在str,int中可选,也可为None
Union[str, int]

6. Depends用法(fastapi

Depends依赖注入是一种设计方法。用于降低组件间耦合度

from fastapi import Depends, FastAPI  # 2导入 Depends

app = FastAPI()


# 1创建依赖项
async def common_parameters(
        q: Union[str, None] = None, skip: int = 0, limit: int = 100
):
    return {"q": q, "skip": skip, "limit": limit}


@app.get("/items/")
async def read_items(commons: dict = Depends(common_parameters)):  # 3声明依赖项
    return commons


@app.get("/users/")
async def read_users(commons: dict = Depends(common_parameters)):  # 3声明依赖项
    return commons

7. 配置文件(pydantic + env文件)

安装依赖:

conda install python-dotenv   (0.21)
pip install pydantic_settings   (2.3.4)

使用pydantic_settings定义相关配置的注解类:

from pydantic_settings import BaseSettings

class Configs(BaseSettings):
    db_host: str
    db_port: int
    db_user: str
    db_passwd: str
    db_name: str

    class Config:
        env_type = ".env"

.env具体配置项:名称需要和上面定义的类属性名称对应, 但不区分大小

db_host="127.0.0.1"
db_port= 3306
from dotenv import load_dotenv
from functools import lru_cache
from setting.config_type import Configs


@lru_cache()
def get_config():
    # 从.env文件导入配置项
    load_dotenv()
    return Configs()

项目中使用配置

from setting.config import get_config

config = get_config()

# config类型:<class 'setting.config_type.Configs'>
print(type(config.db_host), config.db_host) # 类型:<class 'str'> 值:127.0.0.1

注意:可支持不同环境进行配置,此处省略

8. 同步连接mysql(sqlachemy + pymysql)

安装依赖:

# 安装sqlachemy
conda install sqlclchemy  (2.0.30)
conda install pymysql     (1.1.1)

数据库相关配置:

from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker, Session

# 此处和异步url不同
SQLALCHEMY_DATABASE_URL = "mysql+pymysql://{}:{}@{}:{}/{}".format(db_user, db_passwd, db_host, db_port, db_name)

engine = create_engine(SQLALCHEMY_DATABASE_URL, echo=True)
SessionLocal = sessionmaker(class_=AsyncSession,
                            autocommit=False,
                            autoflush=False,
                            bind=engine)
Base = declarative_base()

def get_db():
    db: Session = SessionLocal()
    try:
        yield db
    finally:
        db.close()

模型类(异步和同步一样):

import datetime

from setting.database import Base
from sqlalchemy import Column, Integer, String
from sqlalchemy import DateTime


class User(Base):
    __tablename__ = "users"
    # 2.创建模型属性/列
    id = Column(Integer, primary_key=True, index=True, autoincrement=True)
    username = Column(String(255), unique=True, nullable=False)
    email = Column(String(64), nullable=True)
    password = Column(String(255), nullable=False)
    telephone = Column(String(11), nullable=True)
    avatar = Column(String(255), nullable=True)
    status = Column(Integer, default=0)
    creator = Column(String(255), nullable=True)
    desc = Column(String(500), nullable=True)
    age = Column(Integer, nullable=False)
    is_delete = Column(Integer, default=0)
    create_time = Column(DateTime, default=datetime.datetime.now)
    update_time = Column(DateTime, onupdate=datetime.datetime.now, default=datetime.datetime.now)

增删改查:

from fastapi import APIRouter, Depends
from sqlalchemy.orm import Session
from db.db_models import User
from db.session import get_db


router = APIRouter()

# 查
def getByUsername(db: Session, *, username: str):
	return db.query(User).filter(User.username == username).first()
	
@router.get("/get_uer_list")
async def login(username: str, db: Session = Depends(get_db)):
    data = user_crud.getByUsername(db=db, username=username)
    return {'message': '登录成功', 'data': data}

9. 异步连接mysql (sqlachemy + aiomysql)

官方文档:https://docs.sqlalchemy.org/en/20/orm/queryguide/dml.html#orm-queryguide-upsert

安装依赖:

conda install sqlclchemy   (2.0.30)
conda install aiomysql   (0.2.0)
conda install pymysql     (1.1.1)

数据库相关配置:

sqlachemy官方文档:https://docs.sqlalchemy.org/en/20/orm/extensions/asyncio.html

from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine, async_sessionmaker
from sqlalchemy.orm import DeclarativeBase

SQLALCHEMY_DATABASE_URL = "mysql+aiomysql://{}:{}@{}:{}/{}".format(db_user,
                                                                   db_passwd,
                                                                   db_host,
                                                                   db_port,
                                                                   db_name)
engine = create_async_engine(SQLALCHEMY_DATABASE_URL, echo=True)
SessionLocal = async_sessionmaker(class_=AsyncSession,
                                  autocommit=False,
                                  autoflush=False,
                                  bind=engine)

# 此种方式Base创建模型类支持继承
# from sqlalchemy.ext.declarative import declarative_base
# Base = declarative_base()

# 此种方式Base创建模型类支持继承,可定义模型类中的公共属性,其他模型类继承Base
class Base(DeclarativeBase):
    is_delete = Column(Integer, default=0)
    create_time = Column(DateTime, default=datetime.datetime.now)
    update_time = Column(DateTime, onupdate=datetime.datetime.now, default=datetime.datetime.now)


async def get_db() -> AsyncSession:
    async with SessionLocal() as session:
        yield session

模型类(异步和同步一样),省略。

增删改查:

from sqlalchemy import select, insert, text, update,delete
from sqlalchemy.ext.asyncio import AsyncSession
from utils.crypto import set_password

from .models import User
from .vo import AddUser, UpdateUser


async def get_users_list(search_parm: dict, db: AsyncSession):
    sql = select(User)
    if search_parm.get("username"):
        sql = sql.filter(User.username==search_parm["username"]).offset(search_parm["skip"]).limit(search_parm["limit"])
    # 使用sql语句查询
    total = await db.execute(text("select count(id) from users"))
    # 使用函数func查询,提供max,min等函数
    # total = await db.execute(select(func.count()).select_from(User))
    try:
        res = await db.execute(sql)
    finally:
        await db.close()
    return res.scalars().all(), total


async def add_user(db: AsyncSession, user: AddUser):
    # try:
    #     newUser = User(**user.dict(), password=set_password())
    #     user = db.add(newUser)
    #     await db.flush()
    #     获取插入值的id
    #	  print(newUser.id)
    #     await db.commit()
    #     await db.flush(user)
    # except Exception as e:
    #     await db.rollback()
    try:
        sql = insert(User).values(password=set_password())
        newUser = await db.execute(sql, user.dict())
        await db.flush()
        # 获取插入值的id
        print(newUser.lastrowid)
        await db.commit()
    except Exception as e:
        await db.rollback()

    return ""


async def delete_user(db: AsyncSession, username: str):
    try:
        sql = delete(User).filter_by(username=username)
        await db.execute(sql)
        await db.commit()
    except Exception as e:
        await db.rollback()
    return ""


async def update_user(db: AsyncSession, username: str, item: UpdateUser):
    try:
        sql = update(User).filter_by(id=id).values(item.dict())
        await db.execute(sql)
        await db.commit()
    except Exception as e:
        await db.rollback()
    return ""


# 定义响应统一结构体
def common_json_resp_success(data: Union[list, dict, str, None]):
    return {
        "code": 200,
        "msg": "success",
        "data": data
    }


@router.get("/")
async def get_user_list(db: AsyncSession = Depends(get_db)):
    raise HTTPException(400)
    result = await UserApiHandler().get_user_list(db)
    return common_json_resp_success(result)


@router.post("/")
async def add_user(user: AddUser, db: AsyncSession = Depends(get_db)):
    result = await UserApiHandler().add_user(db, user)
    return common_json_resp_success(result)


@router.delete("/")
async def delete_user(username: str, db: AsyncSession = Depends(get_db)):
    result = await UserApiHandler().delete_user(db, username)
    return common_json_resp_success(result)

@router.put("/")
async def update_user(username: str, item: UpdateUser, db: AsyncSession = Depends(get_db)):
    result = await UserApiHandler().update_user(db, username, item)
    return common_json_resp_success(result)

10. 抛自定义异常HTTPException

只能raise

from fastapi import HTTPException

# status_code: int,  状态码
# detail: Any = None, 响应体
# headers: Optional[Dict[str, str]] = None, 设置响应头
raise HTTPException(
            status_code=404,
            detail="Item not found",
            headers={"X-Error": "There goes my error"},
        )

响应:

// 响应体
{
    "detail": "Item not found"
}

fastapi HTTPException继承自Starlette's HTTPException,但是fastapi支持any JSON-able dataStarlette只支持str

自定义异常:

from fastapi import HTTPException

class test_exception(HTTPException):

    def __init__(self):
        self.status_code = 400
        self.detail = "400, test raise exception"
        self.headers = {"Auth": "invalid"}

11. 自定义全局异常处理app.exception_handler

官方文档:https://fastapi.tiangolo.com/tutorial/handling-errors/?h=requestvalidationerror#use-the-requestvalidationerror-body

11.1 方式1: 装饰器方式(以捕获RequestValidationError修改异常返回结果为例),此方式需要和FastAPI()示例对象在一个文件中,在其他地方导入无效(本人暂时发现)

RequestValidationError用于fastapi中定义Pydantic model数据校验异常

from fastapi import FastAPI

app = FastAPI()


@app.exception_handler(RequestValidationError)
async def validation_exception_handler(request: Request, exc: RequestValidationError):
    errors = []
    for error in exc.errors():
        obj = {
            "type": error["type"],
            "loc": error["loc"],
            "msg": error["msg"]
        }
        errors.append(obj)

    return JSONResponse(
        status_code=status.HTTP_400_BAD_REQUEST,
        content={
            'code': status.HTTP_400_BAD_REQUEST,
            'msg': 'bad request',
            'data': errors
        }
    )

RequestValidationError继承自Pydantic's [ValidationError],异常信息示例:

{
    "detail": [
        {
            "type": "missing",
            "loc": [
                "body",
                "username"
            ],
            "msg": "Field required",
            "input": {
                "email": "string",
                "password": "123456",
                "telephone": "string",
                "avatar": "string",
                "status": 0,
                "desc": "string",
                "age": 0,
                "gender": 0
            },
            "url": "https://errors.pydantic.dev/2.8/v/missing"
        }
    ]
}

exception_handler原码:

    def exception_handler(
        self, exc_class_or_status_code: Union[int, Type[Exception]]
    ) -> Callable[[DecoratedCallable], DecoratedCallable]:
        def decorator(func: DecoratedCallable) -> DecoratedCallable:
            self.add_exception_handler(exc_class_or_status_code, func)
            return func

        return decorator

自定义异常处理函数返回的结果:

{
    "code": 400,
    "msg": "bad request",
    "data": [
        {
            "type": "missing",
            "loc": [
                "body",
                "username"
            ],
            "msg": "Field required"
        }
    ]
}

11.2 方式2:使用FastAPI exception_handlers参数

exception_handlers参数类型:

        exception_handlers: Optional[
            Dict[
                Union[int, Type[Exception]],
                Callable[[Request, Any], Coroutine[Any, Any, Response]],
            ]
        ] = None,

示例:

from fastapi import Request
from fastapi.exceptions import HTTPException
from fastapi.responses import JSONResponse

# 自定义异常
class TestException(HTTPException):

    def __init__(self, detail):
        self.status_code = 400
        self.detail = detail or "400, test raise exception"
        self.headers = {"Auth": "invalid"}

# 自定义异常捕获处理函数
async def test_exception_handler(request: Request, exc: HTTPException):
    return JSONResponse(
        status_code=400,
        content="test exption handler"
    )


exception_handlers = {
    # key可以是状态码,也可以是异常类名, value为自定义处理类
    400: test_exception_handler
}

from fastapi import FastAPI
app = FastAPI(exception_handlers=exception_handlers)

抛异常: TestException

raise TestException('user list test exception')

响应:

"test exption handler"

11.3 方式3:通过FastAPI示例对象的方法add_exception_handler

add_exception_handler原码:

    def add_exception_handler(
        self,
        exc_class_or_status_code: typing.Union[int, typing.Type[Exception]],
        handler: typing.Callable,
    ) -> None:  # pragma: no cover
        self.exception_handlers[exc_class_or_status_code] = handler

示例:

from fastapi import Request
from fastapi.exceptions import HTTPException
from fastapi.responses import JSONResponse

# 自定义异常
class TestException(HTTPException):

    def __init__(self, detail):
        self.status_code = 400
        self.detail = detail or "400, test raise exception"
        self.headers = {"Auth": "invalid"}

# 自定义异常捕获处理函数
async def test_exception_handler(request: Request, exc: HTTPException):
    return JSONResponse(
        status_code=400,
        content="test exption handler"
    )


from fastapi import FastAPI
app = FastAPI(exception_handlers=exception_handlers)
# 状态码或者异常类名,异常处理函数
app.add_exception_handler(400, test_exception_handler)

12. 中间件

中间件是一种函数,在每个请求被特定的路径处理之前,以及每个响应返回之前

12.1 方式1: 装饰器@app.middleware("http")

官方文档:https://fastapi.tiangolo.com/zh/advanced/middleware/

import time

from fastapi import FastAPI, Request

app = FastAPI()


@app.middleware("http")
async def add_process_time_header(request: Request, call_next):
    start_time = time.time()
    # 请求前
    resp = await call_next(request)
    # 请求后
    process_time = time.time() - start_time
    resp.headers["X-Process-Time"] = str(process_time)
    return resp


app.middleware("http")装饰器内部调用的也是方式2

    def middleware(
        self, middleware_type: str
    ) -> Callable[[DecoratedCallable], DecoratedCallable]:
        def decorator(func: DecoratedCallable) -> DecoratedCallable:
            self.add_middleware(BaseHTTPMiddleware, dispatch=func)
            return func

        return decorator

12.2 方式2:FastAPI示例对象的方法add_middleware

add_middleware源码:

    def add_middleware(self, middleware_class: type, **options: typing.Any) -> None:
        if self.middleware_stack is not None:  # pragma: no cover
            raise RuntimeError("Cannot add middleware after an application has started")
        self.user_middleware.insert(0, Middleware(middleware_class, **options))

self.user_middleware是一个列表,源码看作用:

def build_middleware_stack(self) -> ASGIApp:        
    
    	middleware = (
            [Middleware(ServerErrorMiddleware, handler=error_handler, debug=debug)]
            + self.user_middleware
            + [
                Middleware(
                    ExceptionMiddleware, handlers=exception_handlers, debug=debug
                )
            ]
        )

        app = self.router
        for cls, options in reversed(middleware):
            app = cls(app=app, **options)

每个自定义的中间件类继承自BaseHTTPMiddleware,所以每个自定义中间件必须实现dispatch方法,初始化方法app属性由上面add_middleware方法完成,只需定义函数需要的属性就行

class BaseHTTPMiddleware:
    def __init__(
        self, app: ASGIApp, dispatch: typing.Optional[DispatchFunction] = None
    ) -> None:
        self.app = app
        self.dispatch_func = self.dispatch if dispatch is None else dispatch
	
    async def dispatch(
        self, request: Request, call_next: RequestResponseEndpoint
    ) -> Response:
        raise NotImplementedError()  # pragma: no cover

示例:

import time

from fastapi import FastAPI, Request
from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint

app = FastAPI()


class TestMiddleware(BaseHTTPMiddleware):

    def __init__(self, app, get_request_method=None):
        super().__init__(app)
        self.request_method = get_request_method

    async def dispatch(
        self, request: Request, call_next: RequestResponseEndpoint
    ) -> Response:
        request_method = self.request_method if self.request_method else request.method
        print("before request!!!!!, {request_method}", request_method)
        resp = await call_next(request)
        print("before request!!!!!, {request_method}", request_method)

        return resp


app.add_middleware(TestMiddleware, get_request_method="888888")

13. jsonable_encoder(待整理)

作用:复杂的数据类型(特别是那些基于 Pydantic 的模型或包含特殊 Python 类型的对象)转换为与 JSON 兼容的数据结构

# 参数与行为:
  obj: 要转换的对象,可以是任意类型,但通常为 Pydantic 模型实例或其他复杂数据结构。
  include: 可选参数,用于传递给 Pydantic 模型的 include 参数,控制哪些属性应当被包含在序列化结果中。
  by_alias: 可选参数,如果为 True,序列化时会使用模型定义中的别名(alias)而非原始字段名。
  exclude_unset: 可选参数,如果为 True,未赋值的模型属性将不会出现在序列化结果中。
  exclude_defaults: 可选参数,如果为 True,默认值未被覆盖的属性将不会出现在序列化结果中。
  encoder: 可选参数,自定义 JSON 编码器,用于处理特定类型。
  converters: 可选参数,自定义转换器字典,用于处理特定类型到 JSON 兼容类型的转换。

示例:

from fastapi.encoders import jsonable_encoder
from myapp.models import User

user = User(name="John Doe", email="john.doe@example.com", joined_at=datetime.now())

# 将 User 模型实例转换为 JSON 兼容的字典
json_compatible_data = jsonable_encoder(user)

# 现在可以将 json_compatible_data 直接用于 JSON 序列化或存储到数据库等操作

14. Pydantic中的BaseModel

from typing import Self

from pydantic import BaseModel, Field, validator, field_validator, ValidationInfo, model_validator


class Student(BaseModel):
    name: str = Field(..., min_length=3, max_length=5)
    age: int = Field(..., gt=0, le=130)
    # 模型类中没有的字段(用于响应时,is_refresh: int必须定义)
    _is_refresh: int = 0
    is_refresh: int
    
    class Config:
        # orm_mode = True,表示使用from_orm方法从模型类查询结果中构建响应模型(大坑参考20章节)
        from_attributes = True
        
    @property
    def is_refresh(self):
        return self._is_refresh

    @is_refresh.setter
    def is_refresh(self, value: int):
        self._is_refresh = value

    # * 表示验证所有字段
    @field_validator("name", "age")
    @classmethod
    def validate_name(cls, v: str, info: ValidationInfo) -> str:
        # v 验证的字段值
        # ValidationInfo.data获取其他验证字段的值
        return v

    # mode表示设置验证前后获取的数据不同
    # mode='before'数据类型是dict[str, Any], 参数: 类方法cls,输入值,ValidationInfo
    # mode='after'验证失败不会调用。参数: 实例方法self
    @model_validator(mode='after')
    def check_passwords_match(self) -> Self:
        pw1 = self.password1
        pw2 = self.password2
        if pw1 is not None and pw2 is not None and pw1 != pw2:
            raise ValueError('passwords do not match')
        return self

15. 跨域问题

报错:Access to XMLHttpRequest at 'http://127.0.0.1:8080/user/' (redirected from 'http://127.0.0.1:3000/api/user') from origin 'http://127.0.0.1:3000' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource.

解决办法:fastapi添加中间件CORSMiddleware

from fastapi.middleware.cors import CORSMiddleware


app.add_middleware(CORSMiddleware,
                   allow_origins=["*"],  # 允许所有源,也可以指定具体的源
                   allow_credentials=True,
                   allow_methods=["*"],  # 允许所有方法
                   allow_headers=["*"])

vue3vite.config.ts配置允许跨域:

export default defineConfig((configEnv) => {
  const env = loadEnv(configEnv.mode, process.cwd())
  return {
    # 跨域
    server:{
      host: env.VITE_API_HOST || '0.0.0.0',
      # disableHostCheck: true,
      open: false,
      port: env.VITE_PORT || 8000,
      strictPort: true,
      proxy: {
        '/api': {
          target: env.VITE_API_TARGET_URL,
		  # 允许跨域
          changeOrigin: true,
		  # 将'/api'替换为'',后端接口不需要'/api'			
          rewrite: (path) => path.replace(/^\/api/, '')
        }
      }
    }
  }
})

16. 前端路由响应状态码为 307 Temporary Redirect然后直接访问后端接口

现象描述:前端跨域访问后端,页面请求前端路由状态码为 307 Temporary Redirect,然后重定向前端配置到后端的跨域接口200 OK。意思就是抓包看到2次请求

排查步骤:确认前端设置没有问题,发现是后端fastapi设置路由有问题

router = APIRouter(prefix='/user', tags=['用户'])

# 这样设置前端先访问前端服务http://localhost:3000/api/user(307 Temporary Redirect ),再访问后端http://127.0.0.1:8080/user/
@router.get("/")
async def get_user_list(db) ...

# 解决办法,删除/,改为router.get(""),前端不会再直接访问http://127.0.0.1:8080/user/
@router.get("")
async def get_user_list(db) ...

17. 自动生成模型类工具sqlacodegen

# 3.0.0rc4不支持python 3.11.9和sqlalchemy 2.0
pip install sqlacodegen

18. msyql8.0登录报错ERROR 1045 (28000) Access denied for user ‘root‘@‘localhost‘ (using password YES/NO)加上skip-grant-tables后无法启动

  1. 打开管理员窗口关闭msyql服务net stop mysql

  2. 另起管理员窗口,执行mysqld --console --skip-grant-tables --shared-memory,不关闭窗口
    提示:

2024-07-25T14:22:53.057681Z 0 [System] [MY-010116] [Server] D:\mysql-8.0.21-winx64\bin\mysqld.exe (mysqld 8.0.21) starting as process 2652
2024-07-25T14:22:53.059823Z 0 [Warning] [MY-013242] [Server] --character-set-server: 'utf8' is currently an alias for the character set UTF8MB3, but will be an alias for UTF8MB4 in a future release. Please consider using UTF8MB4 in order to be unambiguous.
2024-07-25T14:22:53.075479Z 1 [System] [MY-013576] [InnoDB] InnoDB initialization has started.
2024-07-25T14:22:53.598567Z 1 [System] [MY-013577] [InnoDB] InnoDB initialization has ended.
2024-07-25T14:22:53.962751Z 0 [Warning] [MY-011311] [Server] Plugin mysqlx reported: 'All I/O interfaces are disabled, X Protocol won't be accessible'
2024-07-25T14:22:54.041283Z 0 [Warning] [MY-010068] [Server] CA certificate ca.pem is self signed.
2024-07-25T14:22:54.042396Z 0 [System] [MY-013602] [Server] Channel mysql_main configured to support TLS. Encrypted connections are now supported for this channel.
  1. 新窗口执行mysql -uroot -p,直接回车,不需要输入密码
  2. 执行UPDATE mysql.user SET authentication_string='' WHERE user='root' and host='localhost',修改密码为空
  3. 登录之后修改密码:alter user 'root'@'localhost' identified with mysql_native_password by '新密码'

19. jwt验证

jwt: JSON Web Tokens
安装pyjwt: conda install pyjwt(会自动校验token过期时间)
示例:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZG1pbiIsImV4cCI6MTcyMjA3Njg1NX0.DWNtuuKFCsDOpT2a802DNMVHM6p-VovUdbv7ar3Ppw0

分三部分:

组件作用
Header(头部)描述 JWT 的元数据,如 {"alg": "HS256", "typ": "JWT"}
alg :生成签名算法,默认为 HS256 typ:令牌类型,默认为 JWT
Payload(负载)存放实际数据,官方字段如下
exp (Expiration Time):过期时间
nbf (Not Before Time):生效时间
iss (Issuer):签发人
aud (Audience):受众
iat (Issued At):签发时间
sub (Subject):主题
jti (JWT ID):编号
Signature(签名)HS256(自定义的key,base64后的header + b’.‘ + base64后的payload,digestmod=‘SHA256’)

示例代码:(支持接口依赖注入和中间件方式)

from typing import Annotated
from datetime import timedelta, datetime, timezone

from fastapi import FastAPI, Depends, HTTPException, Request
from fastapi.security import OAuth2PasswordBearer
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
import jwt
from starlette import status
from jwt import InvalidTokenError

from .models import User
# 基于pydanic的BaseModel自定义的数据类型
from .vo import LoginUser, TokenData

app = FastAPI()

# 中间件方式
@app.middleware("http")
async def check_jwt(request: Request, call_next):
    # 设置白名单
    white_list = ["/user/login", "/docs", "/openapi.json"]
    if request.url.path in white_list:
        res = await call_next(request)
        return res

    # 获取数据库连接对象
    db = await get_db().__anext__()
    cur_user = await get_current_user_v2(request, db)
    # 将用户id和username保存到请求上下文
    request.state.user_id = cur_user.id
    request.state.username = cur_user.username
    # 请求前----------------------------
    resp = await call_next(request)
    # 请求后----------------------------
    print("6666666565555555555666666")
    return resp


# 仅使用jwt可不需要这个,可直接操作request对象获取请求头
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/login")
SECRET_KEY = "123456"
ALGORITHM = "HS256"


def create_access_token(data: dict, expires_delta: timedelta | None = None):
    """
    生成jwt
    :param data: header和playload
    :param expires_delta:  token过期时间
    :return: 
    """
    to_encode = data.copy()
    if expires_delta:
        expire = datetime.now(timezone.utc) + expires_delta
    else:
        expire = datetime.now(timezone.utc) + timedelta(minutes=15)
    to_encode.update({"exp": expire})
    encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
    return encoded_jwt

async def get_db() -> AsyncSession:
    # 获取数据库连接对象
    async with SessionLocal() as session:
        yield session

async def get_users_by_username(username: str, db: AsyncSession):
    res = await db.execute(select(User).filter_by(username=username, status=0, is_delete=0))
    return res.scalars().first()

async def get_current_user_v1(token: Annotated[str, Depends(oauth2_scheme)], db: AsyncSession=Depends(get_db)):
    """
    获取当期登录用户
    场景: 接口中使用依赖注入
    :param token: 请求头中的token, oauth2_scheme方法已分离出token部分
    :param db: 数据库连接session
    :return:
    """
    credentials_exception = HTTPException(
        status_code=status.HTTP_401_UNAUTHORIZED,
        detail="Could not validate credentials",
        headers={"WWW-Authenticate": "Bearer"},
    )
    try:
        payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
        username: str = payload.get("sub")
        if username is None:
            raise credentials_exception
        token_data = TokenData(username=username)
    except InvalidTokenError:
        raise credentials_exception
    user = await get_users_by_username(username=token_data.username, db=db)
    if user is None:
        raise credentials_exception
    return user


async def get_current_user_v2(request: Request, db: AsyncSession):
    """
    获取当期登录用户
    场景: 中间件中使用
    :param token: 直接从request对象获取Authorization,未分离出token
    :param db:
    :return:
    """
    credentials_exception = HTTPException(
        status_code=status.HTTP_401_UNAUTHORIZED,
        detail="Could not validate credentials",
        headers={"WWW-Authenticate": "Bearer"},
    )
    # 通过request对象获取请求头中的Authorization
    authorization: str = request.headers.get("Authorization", "")
    if not authorization:
        raise credentials_exception
    try:
        token = authorization.replace("Bearer ", "")
        payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
        username: str = payload.get("sub")
        if username is None:
            raise credentials_exception
        token_data = TokenData(username=username)
    except InvalidTokenError:
        raise credentials_exception
    user = await get_users_by_username(username=token_data.username, db=db)
    if user is None:
        raise credentials_exception
    return user


async def check_user_password(user: LoginUser, db: AsyncSession):
    is_auth = False
    res = await db.execute(select(User).filter_by(username=user.username))
    obj = res.scalars().first()
    is_auth = check_password(user.password, obj.password)
    return is_auth


async def check_user(self, user: LoginUser, db: AsyncSession):
    """
    校验用户密码,生成jwt
    :param user: 
    :param db: 
    :return: 
    """
    is_auth = await check_user_password(user, db)
    if not is_auth:
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Incorrect username or password",
            headers={"WWW-Authenticate": "Bearer"},
        )

     # 生成jwt token
     access_token = create_access_token(
         data={"sub": user.username}
     )
     return {"is_auth": is_auth, "token": access_token, "token_type":"Bearer"}


@router.post("/login")
async def login(user: LoginUser, db: AsyncSession = Depends(get_db)):
    res = await check_user(user, db)
    return res


# 接口形式,使用依赖注入方式
@app.get("/user/{id}")
async def get_users_detail(id: int, cur_user: Annotated[User, Depends(get_current_user_v1)], db: AsyncSession = Depends(get_db)):
    # 若不需要使用仅鉴权可使用路由中的dependencies
    print(cur_user)
    res = await UserApiHandler().get_users_detail(id, db)
    return res

@app.get("", dependencies=[Depends(get_current_user_v1)])
async def get_users_list(search_parm: dict=Depends(common_search_params), db: AsyncSession = Depends(get_db)):
    res, total = await UserApiHandler().get_users_list(search_parm, db)
    return common_json_resp_success(res, total)

请求头实例:Authorization:Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZG1pbiIsImV4cCI6MTcyMjA4MDc1N30.FD0enTgHfWR4KwduVpNtZz5OvUiD6PQCUU5IkWIMcpc

20. 使用pydantic响应模型from_orm转化sqlalchemy查询结果运行提示"* ‘orm_mode’ has been renamed to ‘from_attributes’"

20.1 坑1:from_orm已改为from_attributes,会报错“pydantic.errors.PydanticUserError: You must set the config attribute from_attributes=True to use from_orm”

解决办法:

from pydantic import BaseModel


class Menu(BaseModel):
    ...
    web_name: Union[str, None] = None
    
   class Config:
    	# 老版本使用orm_mode
        # orm_mode = True
        # 新版本
        from_attributes = True
 
@router.get("/{id}", dependencies=[Depends(get_current_user_v1)])
async def get_menus_detail(id: int, db: AsyncSession = Depends(get_db)):
    """
    菜单详情
    :param id:
    :param db:
    :return:
    """
    sql = select(Menu).filter(Menu.id==menu_id)
    res = await db.execute(sql)
	Menu.from_orm(res)
    return Menu.from_orm(res)
        
# 具体使用
Menu.from_orm(模型类查询集)

20.2 坑2:报错“ pydantic_core._pydantic_core.ValidationError: 1 validation error for Menuweb_name

  |   Input should be a valid string [type=string_type, input_value=None, input_type=NoneType]”

解决办法:pydantic响应模型中定义的同名字段数据类型需要和模型类中定义的一样,因为web_name响应模型定义为str类型,但是查询结果为None

21. websocket功能

21.1 数据库建表

drop table if exists `send_messages`;
CREATE TABLE `messages` (
	`id` INT(10,0) NOT NULL AUTO_INCREMENT COMMENT '消息id',
	`receiver_id` INT(10,0) NOT NULL COMMENT '接收者id',
	`sender_id` INT(10,0) NOT NULL COMMENT '发送者id',
	`connection_id` INT(10,0) NULL DEFAULT NULL COMMENT '链接(会话)id',
	`message` VARCHAR(1000) NULL DEFAULT '' COMMENT '消息内容' COLLATE 'utf8mb4_0900_ai_ci',
	`message_type` TINYINT(3,0) NULL DEFAULT NULL COMMENT '消息类型,0-管理员消息,1-客户消息',
	`status` TINYINT(3,0) NOT NULL COMMENT '读取状态,0-已读,1-未读',
	`is_delete` TINYINT(3,0) NULL DEFAULT '0' COMMENT '是否删除,0-存在,1-删除',
	`create_time` DATETIME NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
	`update_time` DATETIME NULL DEFAULT CURRENT_TIMESTAMP COMMENT '更新时间,默认当前',
	PRIMARY KEY (`id`) USING BTREE
)
COMMENT='用户websocket消息记录表'
ENGINE=InnoDB
;


drop table if exists `websocket_connections`;
CREATE TABLE `websocket_connections` (
	`id` INT(10,0) NOT NULL AUTO_INCREMENT COMMENT 'websocket连接id',
	`create_time` DATETIME NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
	`is_delete` TINYINT(3,0) NULL DEFAULT '0' COMMENT '是否删除,0-存在,1-删除',
	`update_time` DATETIME NULL DEFAULT CURRENT_TIMESTAMP COMMENT '更新时间,默认当前',
	`user_id` INT(10,0) NOT NULL COMMENT '连接用户id',
	PRIMARY KEY (`id`) USING BTREE
)
COMMENT='websocket连接记录表'
ENGINE=InnoDB
;


drop table if exists `login_historys`;
CREATE TABLE `login_historys` (
	`id` INT(10,0) NOT NULL AUTO_INCREMENT COMMENT '登录记录id',
	`user_id` INT(10,0) NOT NULL COMMENT '连接用户id',
	`create_time` DATETIME NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
	`update_time` DATETIME NULL DEFAULT CURRENT_TIMESTAMP COMMENT '更新时间,默认当前',
	`is_delete` TINYINT(3,0) NULL DEFAULT '0' COMMENT '是否删除,0-存在,1-删除',
	`status` TINYINT(3,0) NOT NULL COMMENT '登录状态,0-成功,1-失败',
	`ip` VARCHAR(64) NULL DEFAULT '' COMMENT '登录ip' COLLATE 'utf8mb4_0900_ai_ci',
	PRIMARY KEY (`id`) USING BTREE
)
COMMENT='用户登录记录表'
ENGINE=InnoDB
AUTO_INCREMENT=18;

21.2 后端

安装websocket: conda install websockets

fastapi代码demo:

from fastapi import FastAPI, WebSocket, WebSocketDisconnect

app = FastAPI()

# 蓝图形式使用@router.websocket装饰器
# from fastapi import APIRouter, Depends
# router = APIRouter(prefix='/chat', tags=['聊天'])
# @router.websocket("/test/{user_id}")


@app.websocket("/{user_id}")
async def websocket_endpoint(user_id: int, cur_user: Annotated[User, Depends(get_current_user_v1)], db: AsyncSession = Depends(get_db),websocket: WebSocket):
    """
    测试websocket
    :param user_id:路径参数
    :param cur_user: 利用依赖注入get_current_user_v1鉴权并获取当前用户信息
    :param db:利用依赖注入get_db获取数据库连接
    :param websocket:fastapi框架WebSocket请求对象
    :return:
    """
    try:
        # 接收连接
        await websocket.accept()
        # 接收消息
        message = await websocket.receive_text()
        # 回复消息
        await websocket.send_text(f"hello, world!! {message}")
    except WebSocketDisconnect:
        # 客户端主动断开连接报错WebSocketDisconnect
        print("Client disconnected")
        

sqlalchemy关联查询:

from sqlalchemy.orm import DeclarativeBase
from sqlalchemy import Column, Integer, String
from sqlalchemy.ext.asyncio import AsyncSession


class Messages(DeclarativeBase):
    """消息表模型类"""
    __tablename__ = "messages"
    # 2.创建模型属性/列
    id = Column(Integer, primary_key=True, index=True, autoincrement=True)
    receiver_id = Column(Integer, nullable=False)
    sender_id = Column(Integer, nullable=False)
    connection_id = Column(Integer, nullable=True)
    message = Column(String(1000), default="")
    message_type = Column(Integer, nullable=True, doc="消息类型,0-管理员消息,1-客户消息")
    status = Column(Integer, default=1, doc="消息读取状态,0-已读,1-未读")
    is_delete = Column(Integer, default=0, doc="是否删除,0-存在,1-删除")
    
    
class BaseUser(DeclarativeBase):
    __tablename__ = "users"
    # 2.创建模型属性/列
    id = Column(Integer, primary_key=True, index=True, autoincrement=True)
    username = Column(String(255), unique=True, nullable=False)
    

# 查询当前用户可沟通的用户列表
async def get_chat_user_list(db: AsyncSession)
    search_parm = {"skip": 0, "limit": 10}
    join_sql = join(Basechat_user_list(), Messages, Messages.sender_id==BaseUser.id)
    sql = select(BaseUser).select_from(join_sql).where(Messages.receiver_id==user_id, Messages.sender_id!=user_id).group_by(BaseUser.id)
    res = await db.execute(sql.offset(search_parm["skip"]).limit(search_parm["limit"]))
    return res.scalars().all()

21.2.1 关联查询问题:注意以上sqlalchemy关联查询使用的是res.scalars()只会返回select(BaseUser, Messages)中模型类BaseUser字段对应的查询结果,即第一个select中的内容,因为模型类和数据库没有使用外键。若使用外键的话需要再模型类字段使用ForeignKey指定外键和relationship指定表之间的关系


async def get_one_role_menus_by_role_ids(db: AsyncSession, role_ids: list):
    """
    根据单角色id查询菜单
    :param db:
    :param role_ids:
    :return:
    """
    # 没有外键使用select(Menu, RoleMenus)和res.scalars().all()只会返回Menu字段数据,关联数据不会返回
    res = await db.execute(select(Menu).join(RoleMenus, RoleMenus.menu_id==Menu.id).where(RoleMenus.role_id.in_(role_ids), Menu.id!=1, Menu.menu_type.in_([MenuType.DYNAMIC_ROUTE.value, MenuType.DIR.value, MenuType.MENU.value]), Menu.status==Status.USABLE.value, Menu.is_delete==IsDelete.NOT_DELETED.value))
    return res.scalars().all()

# 解决办法是查询结果不使用res.scalars().all(),使用res.all()返回
async def get_multi_role_buttons_by_role_ids(db: AsyncSession, role_ids: list):
    """
    获取当前用户多角色id返回所有的按钮
    :param db:
    :param role_ids:
    :return:
    """
    res = await db.execute(select(Menu.permission_identifier, RoleMenus.role_id).join(RoleMenus, RoleMenus.menu_id==Menu.parent_menu_id).where(RoleMenus.role_id.in_(role_ids),Menu.id!=1, Menu.menu_type==MenuType.BUTTON.value, Menu.status==Status.USABLE.value, Menu.is_delete==IsDelete.NOT_DELETED.value))
    return res.all()


# 通过遍历res.all()的查询结果,可以获取Menu和RoleMenus表数据
menu_perms = await MenuDao.get_multi_role_buttons_by_role_ids(db, role_ids)
res = {}
for menu_perm, role_id in menu_perms:
    if res.get(role_id):
        res[role_id].append(menu_perm)
    else:
        res[role_id] = [menu_perm]

模型类定义

from sqlalchemy.orm import relationship
from sqlalchemy import Column, ForeignKey, Table

class User(DeclarativeBase):
    __tablename__ = "users"
    # 2.创建模型属性/列
    id = Column(Integer, primary_key=True, index=True, autoincrement=True)
    
    # uselist:指明一对一或者一对多的关系。查询结果中通过res.messages可获表User和Messages数据
    # backref,反向引用,双向性, 另外模型类不需要再定义,是使Messages可通过字段user获取User相关数据
    # back_populates:反向引用,单向性的,另外模型类需要再定义
    # secondary:指定多对多关系中的中间表模型类
    
    messages = relationship("Messages", secondary=message_users, back_populates="user", uselist=False)


class Messages(DeclarativeBase):
     __tablename__ = "messages"
    ...
    uid = Column(Integer, ForeignKey('users.id'))
    # remote_side指定多关系对应的外键,场景:插入嵌套数据时使用方便些
    user = relationship("User", secondary=message_users, remote_side=[id], back_populates="messages", uselist=False)
    
    
    
# 多对多关系, 第三张表
message_users = Table('message_users', Base.metadata,
    Column('user_id', Integer, ForeignKey('users.id')),
    Column('message_id', Integer, ForeignKey('messages.id'))
)

以上可以使用``

21.3 前端页面设计

聊天页面chat.vue基于原生javascript websocket开发:(需要检测消息值messages的变化改变滚动条位置,代码略)

<template>
      <el-scrollbar ref="scrollbarRef" height="250px">
          <p v-for="mes in messages" :key="mes.id">{{ mes.message }}</p>
      </el-scrollbar>

</template>
<script setup lang="ts">
    import { ref, onMounted, onBeforeUnmount, nextTick } from 'vue'
    
    const userId = 101
    
    const messages = [{
        id: 1000,
        message: 'hello world'
    },]
    const ws = ref(null)
    
    onMounted(() => {
        // 页面初始化数据
        if (props.userId && !ws.value) {
            connectWebSocket(props.userId)
        }
    })
    const sendMessage = async () => {
        // 发送消息
        if (ws.value && ws.value.readyState === WebSocket.OPEN) {
            ws.value.send("哈哈哈,早上好")
        }
      }
   
    const connectWebSocket = (userid) => {
        // 携带token建立连接
        let wsBaseUrl = import.meta.env.VITE_API_WEBSOCKET_URL
        let url = `${wsBaseUrl}/${userid}`
        ws.value = new WebSocket(url)

        ws.value.onopen = (event) => {
          ElMessage.success("后台连接已开启,可以开始沟通了!")
        }

        ws.value.onclose = (event) => {
          console.log('WebSocket后台连接已关闭')
          ws.value.close()
        }

        ws.value.onerror = (error) => {
          ws.value.close()
        }

        ws.value.onmessage = (event) => {
          console.log('接收消息:', event.data)
          messages.push({
            id: 1001,
        	message: event.data
          })
        }
  	}
    
  
</script>
<style scoped>
</style>

聊天用户切换使用 <router-link> :注意带参数动态路由onmouted之后切换页面不会再刷新页面,需要使用watch检测参数变化,重新赋值。

<template>
	<router-link v-for="user in users" :key="user.id" :to=`/admin/chat/${user.id}`>
        {{ user.id  }}
    </router-link>
</template>
<script setup lang="ts">
    import { ref } from 'vue'
    
    const users = ref([{
        id: 100,
        username: "孙悟空"
    },])
</script>
<style scoped>
</style>

对应用户的聊天窗口使用带参数动态路由(结构参考示例)进行加载:

import ChatWindow from '@/view/conponent/ChatWindow.vue'

[
  {
    path: '/home',
    name: 'Home',
    children: [
      {
        path: '/chat/:id',
        name: 'ChatWindow',
        component: ChatWindow
      },
    ]
   }
]

vue router编程式导航:

<script setup lang="ts">
    import router from '@/router/index'
    
    // vue router路由守卫对象
	router.push(`/admin/chat/${userId}`)
</script>

页面组件使用element-plus,如Tag 标签:(注意:使用element-plus某些组件使用html原生标签可能更改标签样式可能会不成功)

<template>
    <el-tag v-if="mes.status===MessageStatus.Read " type="info" effect="dark" round size="small">已读</el-tag>
</template>

<script setup lang="ts">
   enum MessageStatus {
        Read,
        UnRead,
	}
</script>

21.4 报错1:ChatWindow.vue:65 WebSocket connection to 'ws://localhost:3000/api/1' failed: WebSocket is closed before the connection is established.

原因: 就是前后端websocket建立url 不一致

22 菜单管理(按钮)和路由控制

22.1 按钮权限管理:参考20. 按钮权限管理: 用于管理前端页面标签和按钮的展示,用于前端层面鉴权用于管理

22.2 路由管理:用于管理后端路由和角色之间的关系,用于后端鉴权

数据库表:

drop table if exists `permissions`;
CREATE TABLE  if not exists `permissions` (
    `id` int not null primary key auto_increment comment '路由id',
    `permission_name` varchar(255) not null DEFAULT '' COMMENT '路由名称',
    `permission_identifier` varchar(255) not null comment '路由标识',
	`role_id` int NOT NULL COMMENT '角色id',
	`request_method` TINYINT NULL DEFAULT NULL COMMENT '请求方法,0-get,1-post,3-put,4-delete, 5-webscoket, 6-其它',
    `desc` VARCHAR(500) NULL DEFAULT '' COMMENT '备注',
    `creator` int NULL DEFAULT '' COMMENT '创建者'
    `sort` int NULL DEFAULT NULL COMMENT '排序',
	`status` TINYINT NOT NULL COMMENT '是否禁用,0-启用,1-禁用',
	`is_delete` TINYINT NULL DEFAULT '0' COMMENT '是否删除,0-存在,1-删除',
	`create_time` DATETIME NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
	`update_time` DATETIME NULL DEFAULT CURRENT_TIMESTAMP COMMENT '更新时间,默认当前'
)COMMENT='权限表' ENGINE=InnoDB;

drop table if exists `role_permissions`;
CREATE TABLE  if not exists `role_permissions` (
	`role_id` int NOT NULL COMMENT '角色id',
    `permission_id` int NOT NULL COMMENT '菜单id',
	`status` TINYINT NOT NULL COMMENT '是否禁用,0-启用,1-禁用',
	`is_delete` TINYINT NULL DEFAULT '0' COMMENT '是否删除,0-存在,1-删除',
	`create_time` DATETIME NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
	`update_time` DATETIME NULL DEFAULT CURRENT_TIMESTAMP COMMENT '更新时间,默认当前'
    primary key(`role_id`,`permission_id`),
)COMMENT='角色权限表' ENGINE=InnoDB;

代码省略,可参考19. jwt验证12. 中间件内容

23. uvicorn配置日志使用参数log_config

日志配置ini文件格式:

[loggers]
keys=root

[handlers]
keys=rotatingFileHandler,streamHandler,errorHandler

[formatters]
keys=simpleFmt, errorFmt, consoleFmt

[logger_root]
level=DEBUG
handlers=rotatingFileHandler,streamHandler,errorHandler

[handler_rotatingFileHandler]
class=handlers.RotatingFileHandler
level=INFO
formatter=simpleFmt
args=(os.path.join(sys.path[0], "logs/server.log"), 'a', 2*1024*10, 5, 'utf-8')

[handler_errorHandler]
class=handlers.RotatingFileHandler
level=ERROR
formatter=errorFmt
args=(os.path.join(sys.path[0], "logs/error.log"), 'a', 2*1024*10, 5, 'utf-8')

[handler_streamHandler]
level=INFO
class=StreamHandler
formatter=consoleFmt
args=(sys.stdout,)

[formatter_consoleFmt]
format=%(asctime)s.%(msecs)03d [%(levelname)s] [%(name)s] %(message)s

[formatter_simpleFmt]
format=%(asctime)s %(pathname)s(%(lineno)d): [%(levelname)s] [%(name)s] %(message)s

[formatter_errorFmt]
format=%(asctime)s %(pathname)s(%(lineno)d): [%(levelname)s] [%(name)s] %(message)s

uvicorn配置log_config:

if __name__ == '__main__':
	import uvicorn

    uvicorn.run(app="__main__:app", host="127.0.0.1", port=8080, reload=True, log_config=r"./common/log_config.ini")

项目中打印日志:

from uvicorn.server import logger

logger.info("项目日志")

24. fastapi中生命周期管理lifespan@app.on_event("startup")@app.on_event("shutdown")

都是生命周期函数。但是on_event is deprecated, use lifespan event handlers instead.

from fastapi import FastAPI

app = FastAPI()

@app.on_event("startup")
async def startup_app():
    logger.info("on_event startup Application is start")


@app.on_event("shutdown")
async def shutdown_app():
    logger.info("on_event shutdown Application is shutting down")

if __name__ == '__main__':
    import uvicorn
	uvicorn.run(app="__main__:app", host="127.0.0.1", port=8080)

会提示:DeprecationWarning: on_event is deprecated, use lifespan event handlers instead.
启动和关闭服务时日志可验证:

2024-11-07 22:24:11,346.346 [INFO] [uvicorn.error] Waiting for application startup.
2024-11-07 22:24:11,347.347 [INFO] [uvicorn.error] on_event startup Application is start
2024-11-07 22:24:11,347.347 [INFO] [uvicorn.error] Application startup complete.
...........

2024-11-07 22:24:26,702.702 [INFO] [uvicorn.error] on_event shutdown Application is shutting down
2024-11-07 22:24:26,705.705 [INFO] [uvicorn.error] Application shutdown complete.
2024-11-07 22:24:26,705.705 [INFO] [uvicorn.error] Finished server process [419]
2024-11-07 22:24:26,935.935 [INFO] [uvicorn.error] Stopping reloader process [417]

lifespan的使用示例:

from contextlib import asynccontextmanager


@asynccontextmanager
async def lifespan(app: FastAPI):
    """上下文管理"""
    logger.info("lifespan Application is starting up")
    yield
    # 关闭时的代码
    logger.info("lifespan Application is shutting down")
    
    
app = FastAPI(lifespan=lifespan)

启动和关闭服务时日志可验证:

2024-11-07 21:25:32,295.295 [INFO] [uvicorn.error] Started server process [347]
2024-11-07 21:25:32,296.296 [INFO] [uvicorn.error] Waiting for application startup.
2024-11-07 21:25:32,297.297 [INFO] [uvicorn.error] lifespan Application is starting
..........
2024-11-07 21:25:59,892.892 [INFO] [uvicorn.error] lifespan Application is shutting down
2024-11-07 21:25:59,893.893 [INFO] [uvicorn.error] Application shutdown complete.
2024-11-07 21:25:59,893.893 [INFO] [uvicorn.error] Finished server process [347]
2024-11-07 21:26:00,098.098 [INFO] [uvicorn.error] Stopping reloader process [345]

25. Python 3.11.9 , aiosmtplib 3.0.2发送邮件报错aiosmtplib.errors.SMTPServerDisconnected: Unexpected EOF received”

问题描述:

Python 3.11.9 , aiosmtplib 3.0.2 , “aiosmtplib.errors.SMTPServerDisconnected: Unexpected EOF received” will appear.it doesn't always.code like looks like this:
# one
smtp_client = aiosmtplib.SMTP(...)
async with smtp_client: 
    await smtp_client.sendmail(...)
# two
await smtp_client.sendmail(...)
await smtp_client.quit()

# debug raise error:  "asyncio/base_events.py", line 428, in create_future return futures.Future(loop=self): 
# partial source code:
# If we were disconnected, don't create a new waiter
if self.transport is None:
    self._response_waiter = None
    else:
        self._response_waiter = self._loop.create_future()


# actually: "aiosmtplib/protocol.py/SMTPProtocol"
def eof_received(self) -> bool:
    exc = SMTPServerDisconnected("Unexpected EOF received")
    if self._response_waiter and not self._response_waiter.done():
        self._response_waiter.set_exception(exc)

        # Returning false closes the transport
        return False

    async def quit(
        self, *, timeout: Optional[Union[float, Default]] = _default
    ) -> SMTPResponse:
        """
        Send the SMTP QUIT command, which closes the connection.
        Also closes the connection from our side after a response is received.

        :raises SMTPResponseException: on unexpected server response code
        """
        response = await self.execute_command(b"QUIT", timeout=timeout)
        if response.code != SMTPStatus.closing:
            raise SMTPResponseException(response.code, response.message)

        self.close()

        return response


# following code will have a high probability of error
async def send_email_message():
    async with smtp_client: 
        await smtp_client.sendmail(...)
import asyncio
asyncio.run(send_email_message())

#  Low probability of error
    loop = asyncio.get_event_loop()
    loop.run_until_complete(send_email_message())
    loop.close()

# I find that aiosmtplib executes the quit method, but the error thrown is asyncio
# eof_received set_exception
def eof_received(self) -> bool:
    exc = SMTPServerDisconnected("Unexpected EOF received")
    if self._response_waiter and not self._response_waiter.done():
        self._response_waiter.set_exception(exc)

        # Returning false closes the transport
        return False

解决办法:不使用asyncio.run(send_email_message()),而是使用显式时间循环的方法

async def send_email_message():
    print("发送邮件")

if __name__ == '__main__':  
    import asyncio
    # 使用asyncio.run会报错“aiosmtplib.errors.SMTPServerDisconnected: Unexpected EOF received”,aiosmtplib的bug
    # asyncio.run(send_email_message())
    loop = asyncio.get_event_loop()
    loop.run_until_complete(send_email_by_attachment())
    loop.close()

26. stmplibaiosmtplib发送邮件

26.1 消息结构

aiosmtplib

from email.message import EmailMessage

msg = EmailMessage()
from email import encoders
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from email.header import Header
from email.utils import formataddr

# 接收人
recipients = []

# 主题
report_subject = “hello world”

# 正文
msg = MIMEMultipart()
msg['From'] = formataddr(("别名", "发送人")
recipients = [formataddr(("接收人别名", recipient)) for recipient in recipients]
msg['To'] = ','.join(recipients)
msg['Subject'] = Header(report_subject, "utf-8")
                         

纯文本:

content = "hello world"
body = MIMEText(content, 'plain', 'utf-8')
msg.attach(body)

html格式:

with open(base_config.REPORT_FILE_PATH, "r", encoding="utf-8") as f:
    html_data = f.read()

html = MIMEText(html_data(), 'html', 'utf-8')
msg.attach(html)

带附件:

attachment_path = r'../test001.txt'
attachment_file = open(attachment_path, 'rb')
part = MIMEBase('application', 'octet-stream')
part.set_payload(attachment_file.read())
attachment_file.close()
encoders.encode_base64(part)
part.add_header(
    'Content-Disposition',
    'attachment',
    # 附件名称
    filename= os.path.basename(attachment_path)
)
msg.attach(part)

26.2 smtp发送消息

import smtplib

smtp_client = smtplib.SMTP(
    host="smtp服务器域名",
    port="端口, 587或者465",
    timeout=60
)

smtp_client.starttls()
smtp_client.login(“邮箱账号”, “鉴权密钥”)
smtp_client.send_message(msg)
smtp_client.close()

26.3 aiosmtplib发送消息

import aiosmtplib

async def send_one_email_message(recipients: Union[str, Sequence[str]], message: Union[str, bytes]):
	smtp_client = aiosmtplib.SMTP(
        hostname="smtp服务器域名",
        port="端口, 587或者465",
        username=“邮箱账号”,
        password=“鉴权密钥”,
        use_tls=True,
        timeout=60
	)

    async with smtp_client:
        result = await smtp_client.sendmail(“发送人影响”, recipients, mesg.as_string())
    
if __name__ == '__main__':
    import asyncio
    # asyncio.run(send_email_message())
    loop = asyncio.get_event_loop()
    loop.run_until_complete(send_email_by_attachment())
    loop.close()

27. shell方式启动项目

项目相对目录结构:

/backend
    |__  /manage_system
            |__  __main__.py
            |__  setup_app.py
            |__  /apps

当前所在目录:/backend/manage_system

27.1 启动方式1:

cd /backend/manage_system
uvicorn __main__:app --port=8080

运行报错:ERROR: Error loading ASGI app. Attribute "app" not found in module "__main__".

解决办法:

返回上一级目录执行如下:

cd /backend
uvicorn manage_system.__main__:app --port=8080

或者将__main__.py改成main.py,然后执行如下:

cd /backend/manage_system
uvicorn main:app --port=8080

27.2 启动方式2:

cd /backend/manage_system
python __main__.py

27.3 启动方式3(推荐):

返回上一级目录

cd /backend
python -m manage_system

报错 :ModuleNotFoundError: No module named 'setup_app'

解决办法:在__main__.py中最上面将路径manage_system添加到python导包路径

import os
import sys
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))

28. shell脚本

修改文件权限:

# 修改权限
chmod -R 740 management-system

# 修改属组
chown -R pywcc:pywcc management-system

检查python:

PYTHON_VERSION=“3.11”

python_version = `python --version 2>&1|awk '{print $2}'|awk -F '.' '{printf "%d.%d", $1,$2}'`

if [[ ${python_version} != “3.11” ]]; then
	echo "current python version is ${python_version}, target is ${PYTHON_VERSION}"
	exit 1
fi

检查shell函数是否执行成功

SERVER_NAME="management-system"

# 查询`python`服务进程
check_pid(){
	pid = `ps -ef|grep ${SERVER_NAME} | grep -v grep | awk '{print $2}'`
}
# 执行函数
check_pid

# 检查命令check_pid是否成功执行
if [ $? -eq 0 ]; then
	echo "命令执行成功"
else
	echo "命令执行失败"
fi

# 检查进程id是否存在
if [[ ! -n "${pid}" ]]; then
	echo "stop service ${SERVER_NAME} failed"
else
	echo "stop service ${SERVER_NAME} success"
fi

# 判断路径是否存在
if [[ ! -e '/management-system' ]];then
    echo "/management-system not exist"
    exit 1
fi

# 根据执行脚本所带参数执行不同的逻辑
case $1 in
	"start")
		start_app
	;;
	"stop")
		stop_app
	;;
	"restart")
		restart_app
	;;
	"status")
		app_status
	;;
	*)
		echo "启动命令:start.sh start|stop|restart|status"
		echo "选项说明:start(启动) | stop(停止) | restart(重启) | status(查询状态)"
	;;
esac

服务启动命令:

nohup python -m manage_system >/dev/null 2>&1 &

vi常用快捷键

# 跳到行首
ctrl + a

# 跳到行尾
ctrl + e

# 删除光标左边字符,直到行首(ctrl + u 含剪切功能)
ctrl + w

# 删除光标后字符,直到行尾(ctrl + k 含剪切功能)
ctrl + d

# 向前移动一个字符
ctrl + b

# 向后移动一个字符
ctrl + f

# 粘贴
ctrl + y

# 撤销
ctrl + _

# 在文件中向前查找text
/text

# 在文件中向后查找text
?text

# 打印行号
:set nu

# 转化为shell脚本
:set ff=unix

# 删除当前行
dd

# 复制当前行
yy

# 粘贴
y

# 前一个单词

# 行首
0

# 行尾
shift + $

# 文件第一行
gg

# 文件末尾
G

# 向前移动一个字词  符
b   h
# 向后移动一个字词  符
w   l

# 撤销
u

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值