服务端

架构

技术架构

  • 后台端: Vue3 + TypeScript + Pinia + Element Plus
  • 前台端: NuxtJs3 + TypeScript + Pinia + Element Plus
  • 接口端: FastAPI + Pydantic + Tortoise-orm
  • 数据库: MySQL>=5.7
  • 缓存层: Redis
  • 服务部署: Nginx

主要特性

  • 路由自动根据目录自动注册
  • 采用常见的MVC结构(上手更容易)
  • 内置基于RBAC的权限管理的功能
  • 开箱即用,内置常用的工具和组件

内置功能

  • 用户管理:该功能主要完成系统用户配置。
  • 部门管理:配置系统组织机构(公司、部门、小组)
  • 岗位管理:配置系统用户所属担任职务。
  • 菜单管理:配置系统菜单操作权限访问路径等。
  • 角色管理:配置角色菜单权限分配
  • 邮件配置:配置电子邮件发送功能
  • 操作日志:系统操作日志记录和查询
  • 定时任务:管理定时任务的(新增、修改、删除)
  • 系统缓存:管理系统产生的缓存(可自行清理)
  • 附件管理:管理用户上传的图片和视频
  • 文章管理:管理文章的(新增、修改、删除)
  • 文件存储:管理文件的存储(本地存储、阿里云OSS、腾讯云OSS、七牛云OSS)
  • 操作日志 记录用户的登录、操作等日志信息,便于追踪和审计。
  • 接口文档 FastAPI自动生成的Swagger UI接口文档,方便前后端开发人员对接。
  • ....

目录结构

├─📂 server
│  ├─📂 apps                 // 应用目录
│  │  ├─📂 admin             // 后台应用
│  │  │  ├─📂 routers        // 控制器
│  │  │  ├─📂 schemas        // 响应层
│  │  │  ├─📂 service        // 逻辑层
│  │  │  ├─📄 config.py      // 配置
│  │  │  ├─📄 interceptor.py // 拦截器
│  │  │  ├─📄 middleware.py  // 中间键
│  │  ├─📂 api               // 前台应用
│  │  │  ├─📂 routers        // 控制器
│  │  │  ├─📂 schemas        // 响应层
│  │  │  ├─📂 service        // 逻辑层
│  │  │  ├─📄 config.py      // 配置
│  │  │  ├─📄 interceptor.py // 拦截器
│  │  │  ├─📄 middleware.py  // 中间键
│  │  ├─...
│  │
│  ├─📂 common              // 公共目录
│  │  ├─📂 enums            // 枚举目录
│  │  ├─📂 models           // 模型目录
│  │  ├─📂 utils            // 工具目录
│  │  ├─...
│  │
│  ├─📂 kernels             // 核心逻辑
│  │  ├─📄 cache.py          
│  │  ├─📄 database.py         
│  │  ├─📄 events.py            
│  │  ├─...
│  │
│  ├─📂 plugins           // 插件目录
│  │  ├─📂 mail           // 邮件服务
│  │  ├─📂 msg            // 消息服务
│  │  ├─📂 sms            // 短信服务
│  │  ├─📂 storage        // 存储服务
│  │  ├─📂 wechat         // 微信服务
│  │  ├─...
│  │
│  ├─📂 public            // 公开目录
│  │  ├─📂 static         // 静态文件目录
│  │  ├─📂 storage        // 资源存储目录
│  │  ├─...
│  │
│  ├─📂 sql                // 安装SQL
│  │  ├─📄 install.sql  
│  │
│  ├─📄 .env               // 环境配置
│  ├─📄 .example.env       // 配置模板
│  ├─📄 .gitignore         // Git配置
│  ├─📄 config.py          // 全局配置
│  ├─📄 events.py          // 事件管理
│  ├─📄 exception.py       // 异常管理
│  ├─📄 hypertext.py       // Http管理
│  ├─📄 manager.py         // 启动的文件
│  ├─📄 middleware.py      // 全局中间件
│  ├─📄 README.md          // README
│  ├─📄 requirement.txt    // 依赖包

异常处理

  • 异常处理位于根目录的 exception.py 里面主要用于捕获各种异常,并且抛出json格式
  • AppException 是自定义异常,平时逻辑层需要抛出异常可以使用这个类。
  • 示例:
# 导入异常类
from exception import AppException

# 抛出异常
if await AuthAdminModel.filter(nickname=post.nickname).first().only("id"):
    raise AppException("昵称已被占用")

中间件

FastApi是单应用单模块的框架,他是不支持多模块的,但是我们项目是多模块的, 所以在我们使用中间件的时候需要自行判断一下请求来源,再决定是否要走中间件。

中间件定义位置

  • 不是那个位置都可以放中间件,中间件的加载需要遵循一定的规则才可以。
  • 1、可以放在对应模块的目录下 admin/middleware.py, api/middleware.py
  • 2、可以方法源码的根目录下面 server/middleware.py
  • 3、文件名称只能是 middleware.py 不能是其它的

如何定义中间件

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

# 此方法是用来注入自定义中间件的
def init_middlewares(app: FastAPI):
    logs_middleware: typing.Type[any] = LogsMiddleware
    app.add_middleware(logs_middleware)


# 这个类就是自定义的中间
class LogsMiddleware(BaseHTTPMiddleware):
    def __init__(self, app: ASGIApp):
        super().__init__(app)
        pass

    async def dispatch(self, request: Request, call_next: RequestResponseEndpoint) -> Response:
        # 执行方法
        response = None
        try:
            response = await call_next(request)
        except Exception as e:
            errno = e

        # 发送异常
        if errno:
            raise errno

        # 执行完成
        return response

拦截器

注意: 拦截器文件只能放到对应的模块目录下,并且命名为 interceptor.py

# 注入拦截器
obstruction: Dict[str, List[str]] = {
    "LoginInterceptor": [str], # 列表里面的值是忽略不要拦截的路由
    "PermsInterceptor": [str]
}

# 拦截器1
class LoginInterceptor:
    @staticmethod
    async def handler(request: Request, bearer: HTTPAuthorizationCredentials = Depends(HTTPBearer())):
        # 这里可以做你的路由拦截逻辑
        return True

# 拦截器2   
class PermsInterceptor:
    @staticmethod
    async def handler(request: Request):
        # 拦截逻辑
        return True



部署

支持

注意

您的Star是我坚持下去的动力,如果您觉得还不错,帮忙点个Star吧!

准备工作

【源码下载】:

环境要求

运行环境要求版本推荐版本
Python>=3.10.*3.10.*
Mysql>=5.75.7
Nginx无限制最新LTS版
Node>=20.*v20.14.0

宝塔部署

第1步: 下载源码

1.到Github/Gitee下载源码压缩包,放到服务器上并解压。
2.那您就会得到如下图所示的目录结构 (下载时别忘记Star哦)。

第2步: 创建数据库

1.我们需要创建数据库,并且把我们的数据结构导入到数据库中。
2.数据结构文件放在下载的源码 server/sql/install.sql

第3步: 修改配置文件

1.到server目录下,把.example.env复制一份,并重命名为 .env 2.修改.env的数据库配置和Redis配置

第4步: 添加Python项目

第5步: 按要求填写好信息

第6步: 设置域名

第7步: 设置外网映射

第8步: 设置反向代理

Nginx反向代理

注意

  • Nginx的反向代理配置, 以下配置可以让你一个域名多个端访问。
  • 当然如果需要单独部署每个服务也是可以的, 如果你知道怎么做的话。

这里有必要了解一下, 我们的源码默认情况下是有3个端的源码。

  • server: (服务端) 提供前后台接口
  • admin: (后台端) Vue做的后台, 给管理员用的
  • nuxtJs: (前台端) Vue做的前台, 展示给用户的
# HTTP反向代理相关配置开始 >>>
location ~ /purge(/.*) {
    proxy_cache_purge cache_one 127.0.0.1$request_uri$is_args$args;
}

location ~ ^/(api|spi|static|storage)/ {
    proxy_pass http://127.0.0.1:8100;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;  
    proxy_set_header REMOTE-HOST $remote_addr;
    add_header X-Cache $upstream_cache_status;
    proxy_set_header X-Host $host:$server_port;
    proxy_set_header X-Scheme $scheme;
    proxy_connect_timeout 30s;
    proxy_read_timeout 86400s;
    proxy_send_timeout 30s;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
}

location /admin {
    alias /www/wwwroot/waitadmin_python/server/public/admin/;
    try_files $uri $uri/ /admin/index.html; 
}

location / {
    alias /www/wwwroot/waitadmin_python/server/public/pc/;
    try_files $uri $uri/ /index.html; 
}
# HTTP反向代理相关配置结束 <<<
第8步: 完成部署



路由

为了减少工作量, 本程序实现了自动注册路由的功能, 但前提是按照约定的规则进行书写代码才可以。
具体可以阅读代码,按照原有的代码风格进行书写即可。

# 约定的规则1: 这里接收的变量名只能是 router 不是是其它的
router = APIRouter(prefix="路由前缀", tags=["描述标签"])

# 约定的规则2: prefix前缀会自动拼接到路径上, 建议根据文件来命名前缀

配置路由

路由的配置放在了根目录下的 config.py 文件

名称默认快捷方法
ROUTER_ALIAS{"admin": "spi", "api": "api"}给模块指定别名
ROUTER_REMARK{"admin": "后台接口", "api": "前台接口"}给模块指定描述,用于接口文档
ROUTER_STYLESline路由风格,可选值: line, dot
ROUTER_PREFIXTrue是否自动补全目录前缀

注册路由

要使用Route类注册路由必须首先在路由定义文件开头创建实例

# 最基础的路由定义方法
router = APIRouter(prefix="路由前缀", tags=["描述标签"])

例如注册如下路由规则:

# 创建路由
router = APIRouter(prefix="/login", tags=["登录系统"])

# 使用路由
@router.post("/check", summary="登录验证")
async def check(request: Request, params: schema.LoginCheckIn):
    pass

最终我们访问的路径是:

https://serverName/api/login/check

# 路径说明:
#   https://serverName (域名)
#   api                (模块名)
#   login              (前缀名)
#   check              (方法名)

路由参数

FastAPI 允许开发者通过简洁的Python语法声明路径参数,实现动态路由及参数处理。

基础概念

  • 路径参数: 路径中包含的动态部分,用花括号 {} 包裹,如 /items/{item_id} 中的 item_id。
  • 示例: 路径 /items/123 将传递参数 item_id 值为 123。

声明路径参数

# 方法示例
@router.get("/items/{item_id}")   
async def read_item(item_id):     
    # item_id 为路径的参数       
    return {"item_id": item_id}

# 访问: http://127.0.0.1:8000/items/foo
# 响应: {"item_id": "foo"}。

数据类型转换 使用标准的 Python 类型标注为函数中的路径参数声明类型,并且自动转换标注的数据类型。

# 声明
@路由.请求方式("路径/{参数}")   
async def 函数名(参数名:类型):       
    return 返回数据   

#示例
@router.get("/items/{item_id}")   
async def read_item(item_id: int):
    return {"item_id": item_id}
    
# 访问: http://127.0.0.1:8000/items/3
# 响应: {"item_id": 3},自动将字符串 “3” 转换为整型。

# 访问: http://127.0.0.1:8000/items/foo
# 响应: 则返回错误信息,因为 “foo” 不能转换为整数。

参数类型

基本类型:

  • int: 整数类型,如 item_id: int,会自动将路径中的字符串转换为整数。
  • float: 浮点数类型,同理,会转换字符串到浮点数。
  • str: 字符串类型,这是默认类型,通常不需要显式声明,但可以用于明确意图。
  • bool: 布尔类型,虽然不常用作路径参数,但在某些场景下可能有其用途。

复合类型

  • List[类型]Sequence[类型]: 允许参数为一个列表或序列,如 tags: List[str] 表示多个标签。
  • Dict[str, 类型]Mapping[str, 类型]: 接受字典作为参数,键通常是字符串。
  • Union[类型1, 类型2, ...]: 表示参数可以是多个类型中的任意一种。

自定义类型

  • Pydantic模型: 可以定义Pydantic模型作为路径参数类型,用于复杂的数据结构验证, 如 user: UserInPath,其中 UserInPath 是一个Pydantic模型。
  • Enum类型: 如上文所述,使用枚举(Enum)来限制路径参数的取值范围,提供额外的验证和清晰性。

特殊类型转换器

  • 路径转换器(如 path)可以用于处理路径中包含斜杠的特殊参数,例如 file_path: Path 使用 path 转换器。

可选参数与默认值

  • 使用默认值(如 item_id: int = None)可以标记路径参数为可选,尽管这在路径参数中较少见,因为路径通常要求所有组件都是必需的。

请求方法

类型描述快捷方法
GETGET请求get
POSTPOST请求post
PUTPUT请求put
DELETEDELETE请求delete
PATCHPATCH请求patch
HEADHEAD请求head
OPTIONSOPTIONS请求options
*指定请求api_route

使用示例如下:

@router.get('/new')      // 定义GET请求路由规则
@router.post('/new')     // 定义POST请求路由规则
@router.put('/new');     // 定义PUT请求路由规则
@router.delete('/new');  // 定义DELETE请求路由规则
@router.api_route('/new', methods=["GET", "POST"]); // 定义请求指定的路由规则



模型

简介

Tortoise ORM 是一个为异步Python应用设计的ORM(对象关系映射)库;
Tortoise ORM 支持多种数据库后端,如PostgreSQL、MySQL和SQLite等;
它允许开发者以面向对象的方式与关系型数据库进行交互, 同时充分利用异步编程提高性能;

核心概念:

  • 模型(Models): 使用Python类来定义数据库表结构。每个类代表1个数据库表,类的属性对应表中的列。
  • 字段(Fields): 在模型中定义字段, 这些字段映射到数据库表的列。
  • 关系(Relations): 支持定义模型之间的关系, 如一对一、一对多、多对多等。

应用场景:

  • 构建高性能的异步Web应用,如使用FastAPI、Sanic或Starlette的应用;
  • 需要面向对象方式操作数据库的项目。
  • 需要异步数据库访问以提高应用性能的场景。

核心功能

  • 模型定义: 通过Python类定义数据库表结构。
  • 数据查询: 支持复杂的查询构建和执行。
  • 数据操作: 支持创建、更新、删除数据库记录。
  • 关系管理: 支持定义和查询模型之间的关系。
  • 迁移和同步: 提供数据库迁移工具,用于管理数据库模式的变更。

配置文件

配置文件在项目根目录的 config.py

DATABASES: Dict[str, object] = {
        'connections': {
            'mysql': {
                'engine': 'tortoise.backends.mysql',
                'prefix': os.getenv('DB:MYSQL_PREFIX', ''),
                'credentials': {
                    # 服务器地
                    'host': os.getenv('DB:MYSQL_HOST', '127.0.0.1'),
                    # 服务器端口
                    'port': int(os.getenv('DB:MYSQL_PORT', 3306)),
                    # 数据库用户
                    'user': os.getenv('DB:MYSQL_USERNAME', 'root'),
                    # 数据库密码
                    'password': os.getenv('DB:MYSQL_PASSWORD', 'root'),
                    # 数据库名称
                    'database': os.getenv('DB:MYSQL_DATABASE', ''),
                    # 最少连接数
                    'minsize': int(os.getenv('DB:MYSQL_MINSIZE', 1)),
                    # 最大连接数
                    'maxsize': int(os.getenv('DB:MYSQL_MAXSIZE', 100)),
                    # 数据库编码
                    'charset': os.getenv('DB:MYSQL_CHARSET', 'utf8mb4'),
                    # 打印SQL
                    'echo': True if os.getenv('DB:MYSQL_ECHO', 'False') == 'True' else False
                }
            },
            'pgsql': {
                'engine': 'tortoise.backends.asyncpg',
                'credentials': {
                    # 服务器地址
                    'host': os.getenv('DB:PGSQL_HOST', '127.0.0.1'),
                    # 服务器端口
                    'port': int(os.getenv('DB:PGSQL_PORT', 5432)),
                    # 数据库用户
                    'user': os.getenv('DB:PGSQL_USERNAME', 'postgres'),
                    # 数据库密码
                    'password': os.getenv('DB:PGSQL_PASSWORD', '123456'),
                    # 数据库名称
                    'database': os.getenv('DB:PGSQL_DATABASE', 'postgres'),
                    # 最少连接数
                    'minsize': int(os.getenv('DB:PGSQL_MINSIZE', 1)),
                    # 最大连接数
                    'maxsize': int(os.getenv('DB:PGSQL_MAXSIZE', 100)),
                    # 打印SQL
                    'echo': os.getenv('DB:MYSQL_ECHO', False)
                }
            }
        },
        'apps': {
            # 配置MySQL
            'mysql': {'models': 'common.models', 'default_connection': 'mysql'},
            # 配置PgSQL
            'pgsql': {'models': 'common.postgres', 'default_connection': 'pgsql'},
        },
        # 是否使用时区支持
        'use_tz': False,
        # 默认使用的时区
        'timezone': 'Asia/Shanghai'
    }

字段类型

CharField (字符串)

名称类型默认描述
pkboolFalse是否将此字段设置为主键
nullboolFalse是否允许字段为NULL
defaultAny-字段的默认值
uniqueboolFalse字段值是否必须在数据库中唯一
indexboolFalse是否为该字段创建索引
max_lengthint-字符串的最大长度
descriptionstr-字段的描述信息(主要用于文档和生成的SQL schema)
generatedboolFalse是否为自动生成的字段(如自增主键)

FloatField (浮点型)

名称类型默认描述
default, null, unique, index, description, pk, generated--与CharField相同
gt, lt, ge, lefloat-字段值的范围限制

IntegerField (整数型)

名称类型默认描述
default, null, unique, index, description, pk, generated--与CharField相同
gt, lt, ge, leint-字段值的范围限制

BooleanField (布尔型)

名称类型默认描述
default, null, description--与CharField相同

DateField 和 DateTimeField (日期时间型)

名称类型默认描述
default, null, unique, index, description, pk--与CharField相同
auto_nowboolFalse如果设置为True,则在对象保存时自动设置为当前日期/时间
auto_now_addboolFalse如果设置为True,则在对象第一次保存时自动设置为当前日期/时间

ForeignKeyField (关系型)

名称类型默认描述
default, null, description, pk, index--与CharField相同
tostr or Type[Model]-指定外键关联的模型
related_namestr-在关联模型上创建反向关系的名称
on_deletestr-当关联的对象被删除时的行为(如CASCADE、SET_NULL等)

ManyToManyField (关系型)

名称类型默认描述
default, null, description, pk, index--与CharField相同
throughstr or Type[Model]-用于定义多对多关系的中间表。如果不指定,Tortoise ORM将自动创建一个中间表
related_namestr-与ForeignKeyField中的用法相同,用于反向查询

TextField (文本型)

名称类型默认描述
default, null, description--与CharField相同。通常用于存储大量文本

JSONField (序列化型)

名称类型默认描述
default, null, description--与CharField相同。用于存储JSON格式的数据

添加数据

student = await Student.create(
  nickname='小明',
  username='wait'
)

print(student.id)

更新数据

# 根据id修改学生数据
update_num = await Student.filter(id=1).update(name="赵不柱")

# 批量更新
students = await Student.all()
for student in students:
    student.name += "@"
    student.save()

删除数据

delete_num = await Student.filter(id=1).delete()

查询数据

  • all 查询多个结果: (不存在返回 [])
  • get 查询单一对象: (不存在抛出 DoesNotExist)
  • first 查询单一对象: (不存在返回 None)
  • count 查询多个结果: (不存在返回 0)

获取单个数据

注意

模型使用 get 方法查询, 如果数据时抛出异常 DoesNotExist, 否则返回当前模型的对象实例。 另外还提供了 get_or_none 查询方法, 数据不存在是返回 None 不会抛出异常。

# 1、取出主键为1的数据
student = await Student.get(id=1)
print(student)

# 2、使用查询构造器查询满足条件的数据
student = await Student.filter(name="waitadmin").get(id=1)
print(student.name)

# 3、查询1条数据不存在时返回None, 否则返回数据对象
student = await Student.filter(id=1).get_or_none()
if student:
    print(student.name)
else:
    print("student not found")

注意

模型使用 first 方法查询, 如果数据不存在返回None, 否则返回当前模型的对象实例。
如果希望查询数据不存在则返回一个空模型, 可以使用以下的方法。

student = await Student.filter(id=1).first()



获取多个数据

注意

all() 方法用于查询所有数据, 返回所有数据集 (QuerySet对象)。
如果不添加任何条件, 它会返回表中的所有记录。

  • 取出多个数据:
# 根据状态获取多个数据
lists = await Student.filter(status=1).all()

# 对数据集进行遍历操作
for lists in student:
    print(student.name)
  • 更多查询支持:
# 使用查询构造器查询
lists = await Student.filter(status=1).limit(3).order_by("-sort", "id").all()
for lists in student:
    print(student.name)

用查询构造器

在模型中仍然可以调用数据库的链式操作和查询方法, 可以充分利用数据库的查询构造器的优势。

await Student.filter(id=1).first()
await Student.filter(status=1).order_by("-sort", "id").all()
await Student.filter(status=1)->limit(10).all()

只查指定字段

在模型中可以使用 values 指定查询那些字段, 这样可以过滤不需要的字段提高查询的效率。

# 只查询自定的字段
await Student.filter(id=1).first().values(["id", "name"])

# 最终生成的SQL:
SELECT id, name FROM student;

查询表达式

表达式符号含义
==等于
not<>不等于
gt>大于
gte>=大于等于
lt<小于
lte<=小于等于
contains%like%模糊查询(前后匹配, 不区分大小写)
icontains%ilike%模糊查询(前后匹配, 区分大小写)
startswithlike%模糊查询(尾部匹配, 不区分大小写)
istartswithilike%模糊查询(尾部匹配, 区分大小写)
endswith%like模糊查询(头部匹配, 不区分大小写)
iendswith%ilike模糊查询(头部匹配, 区分大小写)
isnullisnull是否为null
not_isnullnot_isnull是否不为null
iexactiexactnginx
searchsearchnginx
yearyearnginx
monthmonthnginx
daydaynginx
  • 表达式示例:
表达: __contains
条件: %like% (不区分大小写)
查询: ArticleModel.filter(title__contains='Python').values('id', 'title').sql()
返回: SELECT `id` `id`,`title` `title` FROM `wait_article` WHERE CAST(`title` AS CHAR) LIKE '%Python%'

# ------------------------------------------

表达: __icontains
条件: %like% (区分大小写)
查询: ArticleModel.filter(title__icontains='Python').values('id').sql()
返回: SELECT `id` `id`,`title` `title` FROM `wait_article` WHERE UPPER(CAST(`title` AS CHAR)) LIKE UPPER('%Python%')

# ------------------------------------------

表达: __startswith
条件: like%
查询: ArticleModel.filter(title__startswith='Python').values('id', 'title').sql()
返回: SELECT `id` `id`,`title` `title` FROM `wait_article` WHERE CAST(`title` AS CHAR) LIKE 'Python%'

# ------------------------------------------

表达: __istartswith
条件: ilike%
查询: ArticleModel.filter(title__istartswith='Python').values('id', 'title').sql()
返回: SELECT `id` `id`,`title` `title` FROM `wait_article` WHERE UPPER(CAST(`title` AS CHAR)) LIKE UPPER('Python%')

# ------------------------------------------

表达: __endswith
条件: %like
查询: ArticleModel.filter(title__endswith='Python').values('id', 'title').sql()
返回: SELECT `id` `id`,`title` `title` FROM `wait_article` WHERE CAST(`title` AS CHAR) LIKE '%Python'

# ------------------------------------------

表达: __iendswith
条件: %ilike
查询: ArticleModel.filter(title__iendswith='Python').values('id', 'title').sql()
返回: SELECT `id` `id`,`title` `title` FROM `wait_article` WHERE UPPER(CAST(`title` AS CHAR)) LIKE UPPER('%Python')

# ------------------------------------------

表达: __isnull
条件: isnull
查询: ArticleModel.filter(title__isnull='').values('id', 'title').sql()
返回: SELECT `id` `id`,`title` `title` FROM `wait_article` WHERE NOT `title` IS NULL

# ------------------------------------------

表达: __not_isnull
条件: not_isnull
查询: ArticleModel.filter(title__not_isnull='').values('id', 'title').sql()
返回: SELECT `id` `id`,`title` `title` FROM `wait_article` WHERE NOT `title` IS NULL

# ------------------------------------------

表达: __iexact
条件: iexact
查询: ArticleModel.filter(title__iexact='Python').values('id', 'title').sql()
返回: SELECT `id` `id`,`title` `title` FROM `wait_article` WHERE UPPER(CAST(`title` AS CHAR))=UPPER('Python')

用聚合查询

在SQL中, 聚合查询通常涉及 COUNT(), SUM(), AVG(), MAX(), MIN() 等函数。

  • 注意实现:

    • 1、聚合查询的结果通常是异步的, 因此你需要使用 await 关键字来获取结果。
    • 2、当你使用 annotate() 方法时, 你需为聚合函数提供1个字段名作为参数(例如: Count('id'))。
    • 3、你可以使用 values()values_list() 方法来选择要在结果集中返回的字段。
    • 4、values() 返回一个字典列表,而 values_list() 返回一个元组列表。
    • 5、having() 方法用于在聚合查询的结果上应用过滤条件。它接受一个或多个表达式作为参数。
  • 假设你有一个 User 模型:

from tortoise.models import Model  
from tortoise import fields  
  
class User(Model):  
    id = fields.IntField(pk=True)  
    name = fields.CharField(max_length=100)  
    age = fields.IntField() 
  • 计算表中的记录数:
from tortoise.functions import Count  
from .models import User  

async def count_users():  
    count = await User.all()\
        .annotate(Count('id'))\
        .values_list('id__count', flat=True)\
        .first()  

    return count
  • 复杂的聚合查询:
from tortoise.functions import Count  
from .models import User  
  
async def count_users_by_age():  
    # 按年龄分组并计算每个年龄组的用户数
    query = await User.all()\
        .annotate(Count('id'))\
        .values('age', 'id__count')\
        .order_by('age')  

    return query
    
# 在这个例子中, 我们使用了 annotate() 方法来添加聚合函数,
# 并使用 values() 方法来选择要在结果集中返回的字段。
  • 计算某个字段的平均值:
from tortoise.functions import Avg  
from .models import User  
  
async def avg_user_age():  
    avg_age = await User.all()\
        .annotate(Avg('age'))\
        .values_list('age__avg', flat=True)\
        .first()  

    return avg_age
  • 使用 HAVING 子句进行过滤:
from tortoise.functions import Count  
from .models import User  
  
async def count_users_by_age_with_filter():  
    query = await User.all()\
        .annotate(Count('id'))\
        .values('age', 'id__count')\
        .having(Count('id') > 10)\
        .order_by('age')  

    return query

F对象

之前的查询都是对象的属性与常量值比较,两个属性怎么比较呢?答:使用F对象。

语法如下:

filter(字段名__运算符=F('字段名'))

使用示例:

from tortoise.query_utils import F 
from .models import BookInfo  

# --------------- 示例1 ---------------

# 查询阅读量大于等于评论量的图书
results = await BookInfo.filter(readcount__gt=F('commentcount'))

# 输出的结果:
<QuerySet [<BookInfo: 雪山飞狐>]>

# --------------- 示例2 ---------------

# 可以在F对象上使用算数运算
# 查询阅读量大于2倍评论量的图书
results = await BookInfo.filter(readcount__gt=F('commentcount')*2)

# 输出的结果:
<QuerySet [<BookInfo: 雪山飞狐>]>

Q对象

Tortoise-ORM 的 Q 对象允许你通过逻辑和比较操作符构建复杂的查询条件。
通过使用 Q 对象,开发者可以组合多个查询条件,包括逻辑操作符(如 AND、OR、NOT)
和比较操作符(如 =、>、< 等),从而构建出复杂的查询逻辑,满足各种数据检索需求。

语法如下:

Q(属性名__运算符=)

使用示例1:

  • 如果需要实现逻辑或or的查, 需要使用 Q() 对象结合 | 运算符
from tortoise.query_utils import Q  
from .models import BookInfo  

# 查询阅读量大于20的图书,改写为Q对象如下
results = await BookInfo.filter(Q(readcount__gt=20)|Q(id__lt=3)).all()

# 输出的结果:
<QuerySet [<BookInfo: 射雕英雄传>, <BookInfo: 天龙八部>, <BookInfo: 雪山飞狐>]>

使用示例2:

  • Q对象可以使用 &、| 连接, &表示逻辑与, | 表示逻辑或
from tortoise.query_utils import Q  
from .models import BookInfo  

# 查询阅读量大于20,或编号小于3的图书,只能使用Q对象实现
results = await BookInfo.filter(Q(readcount__gt=20)|Q(id__lt=3)).all()

# 输出的结果:
<QuerySet [<BookInfo: 射雕英雄传>, <BookInfo: 天龙八部>, <BookInfo: 雪山飞狐>]>

使用示例3:

  • Q对象前可以使用~操作符,表示非not
from tortoise.query_utils import Q  
from .models import BookInfo  

# 查询编号不等于3的图书

# 方式1:
results = await BookInfo.exclude(id=3).all()

# 方式2:
results = await BookInfo.filter(~Q(id=3)).all()



工具

邮件发送

  • 邮件发送驱动库: pip3 install aiosmtplib
  • 该库的文档地址: https://aiosmtplib.readthedocs.io/en/stable/usage.html

邮件驱动类: MailDriver

方法名必填说明
subject(title: str)邮件标题
body(body: str)邮件内容
add_alternative(body: str, subtype: str = "html")HTML内容
add_attachment(path: str, filename: str, maintype: str, subtype: str)邮件附件
fromm(address: str)发送者邮箱
to(address: Union[str, List[str]])接受者邮箱
cc(address: Union[str, List[str]])抄送者邮箱
smtp邮箱的实例

发送场景1:

# 导入邮件发送驱动
from plugin.mail.driver import MailDriver

# 最基础的邮件发送
await MailDriver()
           .subject("邮件的标题")
           .body("邮件的内容")
           .to("fs@qq.com")
           .send()
           
# 说明:
# 1、以上方式是最基础的发送方式,需要在后台配置好邮件参数才可以发送。
# 2、看着没让你传递授权密码这些参数,其实这个类里面封装好了读取参数。
# 3、send 方法一定要在最后调用,表示链条已经构建好可以发送了。

# 其它:
# 1、如果你不想用后台配置的邮件参数,有需求需要另外的邮件参数作为发送。
# 2、您可以在类的初始化传递邮件配置的相关参数,具体参数如下:
    await MailDriver(
        host="smtp.163.com",
        port=25,
        username="...",
        password="...",
        ssl=False
    ).subject("邮件的标题")
     .body("邮件的内容")
     .send()

发送场景2:

from plugin.mail.driver import MailDriver

# 发送带附件的邮件
path: str = "/www/aa.png"
await MailDriver()
           .subject("邮件的标题")
           .body("邮件的内容")
           .add_attachment(path, "aa.png", "image", "png")
           .to("fs@qq.com")
           .send()

发送场景3:

from plugin.mail.driver import MailDriver

# 发送HTML格式内容
content: str = "<p>内容</p>"
await MailDriver()
           .subject("邮件的标题")
           .body("邮件的内容")
           .add_alternative(content, "html")
           .to("fs@qq.com")
           .send()

发送场景4:

from plugin.mail.driver import MailDriver
from email.mime.text import MIMEText

# 组装发送的数据内容
mime_message = MIMEText("Sent via aiosmtplib")
mime_message["From"] = "root@localhost"
mime_message["To"] = "somebody@example.com"
mime_message["Subject"] = "Hello World!"

# 获取邮件的发送对象 
smtp_client = await MailDriver().smtp()
await smtp_client.send_message(message)
await smtp_client.quit()

短信发送

上次更新:
贡献者: zero