# FastAPI 项目

fastapi Github：https://github.com/tiangolo/fastapi

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

fastapi 更新说明：https://fastapi.tiangolo.com/zh/release-notes/

pydantic 官方文档：https://pydantic-docs.helpmanual.io/

pydantic 数据模型代码生成器官方文档 （Json -> Pydantic）：https://koxudaxi.github.io/datamodel-code-generator/

SQLAlchemy-Utils：https://sqlalchemy-utils.readthedocs.io/en/latest/

alembic 中文文档：https://hellowac.github.io/alembic_doc/zh/_front_matter.html

Typer 官方文档：https://typer.tiangolo.com/

SQLAlchemy 2.0 （官方）: https://docs.sqlalchemy.org/en/20/intro.html#installation

SQLAlchemy 1.4 迁移到 2.0 （官方）：https://docs.sqlalchemy.org/en/20/changelog/whatsnew_20.html#whatsnew-20-orm-declarative-typing

PEP 484 语法（官方）：https://peps.python.org/pep-0484/


## 项目结构

使用的是仿照 Django 项目结构：

- alembic：数据库迁移配置目录
  - versions_dev：开发环境数据库迁移文件目录
  - versions_pro：生产环境数据库迁移文件目录
  - env.py：映射类配置文件
- application：主项目配置目录，也存放了主路由文件
  - config：基础环境配置文件
    - development.py：开发环境
    - production.py：生产环境
  - settings.py：主项目配置文件
  - urls.py：主路由文件
- apps：项目的app存放目录
  - vadmin：基础服务
    - auth：用户 - 角色 - 菜单接口服务
      - models：ORM 模型目录
      - params：查询参数依赖项目录
      - schemas：pydantic 模型，用于数据库序列化操作目录
      - utils：登录认证功能接口服务
      - curd.py：数据库操作
      - views.py：视图函数
- core：核心文件目录
  - crud.py：关系型数据库操作核心封装
  - database.py：关系型数据库核心配置
  - data_types.py：自定义数据类型
  - exception.py：异常处理
  - logger：日志处理核心配置
  - middleware.py：中间件核心配置
  - dependencies.py：常用依赖项
  - event.py：全局事件
  - mongo_manage.py：mongodb 数据库操作核心封装
  - validator.py：pydantic 模型重用验证器
- db：ORM模型基类
- logs：日志目录
- static：静态资源存放目录
- utils：封装的一些工具类目录
- main.py：主程序入口文件
- alembic.ini：数据库迁移配置文件

## 开发环境

开发语言：Python 3.10

开发框架：Fastapi 0.101.1

ORM 框架：SQLAlchemy 2.0.20

## 开发工具

Pycharm 2022.3.2

推荐插件：Chinese (Simplified) Language Pack / 中文语言包

代码样式配置：

![image-20230315194534959](https://ktianc.oss-cn-beijing.aliyuncs.com/kinit/public/images/image-20230315194534959.png)

## 使用

```
source /opt/env/kinit-pro/bin/activate

# 安装依赖库
pip3 install -r requirements.txt -i https://mirrors.aliyun.com/pypi/simple/

# 第三方源：

1. 阿里源： https://mirrors.aliyun.com/pypi/simple/

# 线上安装更新依赖库
/opt/env/kinit-pro-310/bin/pip install -r requirements.txt -i https://mirrors.aliyun.com/pypi/simple/
```

### 数据初始化

```shell
# 项目根目录下执行，需提前创建好数据库，并且数据库应该为空
# 会自动将模型迁移到数据库，并生成初始化数据

# 在执行前一定要确认要操作的环境与application/settings.DEBUG 设置的环境是一致的，
# 不然会导致创建表和生成数据不在一个数据库中！！！！！！！！！！！！！！！！！！！！！！

# 比如要初始化开发环境，那么env参数应该为 dev，并且 application/settings.DEBUG 应该 = True
# 比如要初始化生产环境，那么env参数应该为 pro，并且 application/settings.DEBUG 应该 = False

# 生产环境
python main.py init

# 开发环境
python main.py init --env dev
```

### 运行启动

```shell
# 直接运行main文件
python main.py run
```

## 其他操作

在线文档地址(在配置文件里面设置路径或者关闭)

```
http://127.0.0.1:9000/docs
```

Git更新ignore文件直接修改gitignore是不会生效的，需要先去掉已经托管的文件，修改完成之后再重新添加并提交。
```
第一步：
git rm -r --cached .
去掉已经托管的文件

第二步：
修改自己的igonre文件内容

第三步：
git add .
git commit -m "clear cached"
```

## 新的数据迁移

- 新建模型：
  - 在你的app目录下新建一个models目录，`__init__.py`导入你需要迁移的models
  ```python
  # app/.../your_app/models/__init__.py
  from .your_model import YourModel,YourModel2
  
  ```
  ```python
  # app/.../your_app/models/your_model.py
  from db.db_base import BaseModel
  
  class YourModel(BaseModel):
    # 定义你的model
    ...
  
  class YourModel2(BaseModel):
    # 定义你的model
    ...
  ```
- 根据模型配置你的alembic：
  ```
  # alembic.ini
  [dev]
  ...
  sqlalchemy.url = mysql+pymysql://your_username:password@ip:port/kinit
  ...
  ```
  ```python
  # alembic/env.py
  # 导入项目中的基本映射类，与 需要迁移的 ORM 模型
  from apps.vadmin.auth.models import *
  ...
  from apps.xxx.your_app.models import *
  ```
- 执行数据库迁移命令（终端执行执行脚本）：
  ```shell
  # 执行命令（生产环境）：
  python main.py migrate
  
  # 执行命令（开发环境）：
  python main.py migrate --env dev
  
  # 开发环境的原命令
  alembic --name dev revision --autogenerate -m 2.0
  alembic --name dev upgrade head
  ```

生成迁移文件后，会在alembic迁移目录中的version目录中多个迁移文件

## 新的CRUD

- 新的模型文件已经建好（上一步迁移时必须）
- 在 scripts/crud_generate/main.py 添加执行命令

```python
# scripts/crud_generate/main.py
if __name__ == '__main__':
    from apps.xxx.your_app.models import YourModel

    crud = CrudGenerate(YourModel, "中文名", "en_name")
    # 只打印代码，不执行创建写入
    crud.generate_codes()
    # 创建并写入代码
    crud.main()
```

- 生成后会自动创建crud, params,schema, views

## 新的路由配置

```python
# application/urls.py

from apps.xxx.your_app.views import app as your_app

urlpatterns = [
    ...,
    {"ApiRouter": your_app, "prefix": "/your_router", "tags": ["your_tag"]},
]
```

完成后在 http://127.0.0.1:9000/docs 验证生成的接口

## 查询数据

### 自定义的一些查询过滤

```python
# 日期查询
# 值的类型：str
# 值得格式：%Y-%m-%d：2023-05-14
字段名称=("date", 值)

# 模糊查询
# 值的类型: str
字段名称=("like", 值)

# in 查询
# 值的类型：list
字段名称=("in", 值)

# 时间区间查询
# 值的类型：tuple 或者 list
字段名称=("between", 值)

# 月份查询
# 值的类型：str
# 值的格式：%Y-%m：2023-05
字段名称=("month", 值)

# 不等于查询
字段名称=("!=", 值)

# 大于查询
字段名称=(">", 值)

# 等于 None
字段名称=("None")

# 不等于 None
字段名称=("not None")
```

代码部分：

```python
def __dict_filter(self, **kwargs) -> list[BinaryExpression]:
    """
    字典过滤
    :param model:
    :param kwargs:
    """
    conditions = []
    for field, value in kwargs.items():
        if value is not None and value != "":
            attr = getattr(self.model, field)
            if isinstance(value, tuple):
                if len(value) == 1:
                    if value[0] == "None":
                        conditions.append(attr.is_(None))
                    elif value[0] == "not None":
                        conditions.append(attr.isnot(None))
                    else:
                        raise CustomException("SQL查询语法错误")
                elif len(value) == 2 and value[1] not in [None, [], ""]:
                    if value[0] == "date":
                        # 根据日期查询， 关键函数是：func.time_format和func.date_format
                        conditions.append(func.date_format(attr, "%Y-%m-%d") == value[1])
                    elif value[0] == "like":
                        conditions.append(attr.like(f"%{value[1]}%"))
                    elif value[0] == "in":
                        conditions.append(attr.in_(value[1]))
                    elif value[0] == "between" and len(value[1]) == 2:
                        conditions.append(attr.between(value[1][0], value[1][1]))
                    elif value[0] == "month":
                        conditions.append(func.date_format(attr, "%Y-%m") == value[1])
                    elif value[0] == "!=":
                        conditions.append(attr != value[1])
                    elif value[0] == ">":
                        conditions.append(attr > value[1])
                    elif value[0] == "<=":
                        conditions.append(attr <= value[1])
                    else:
                        raise CustomException("SQL查询语法错误")
            else:
                conditions.append(attr == value)
    return conditions
```

示例：

查询所有用户id为1或2或 4或6，并且email不为空，并且名称包括李：

```python
users = UserDal(db).get_datas(limit=0, id=("in", [1,2,4,6]), email=("not None", ), name=("like", "李"))

# limit=0：表示返回所有结果数据
# 这里的 get_datas 默认返回的是 pydantic 模型数据
# 如果需要返回用户对象列表，使用如下语句：
users = UserDal(db).get_datas(
    limit=0,
    id=("in", [1,2,4,6]),
    email=("not None", ),
    name=("like", "李"),
    v_return_objs=True
)
```

查询所有用户id为1或2或 4或6，并且email不为空，并且名称包括李：

查询第一页，每页两条数据，并返回总数，同样可以通过 `get_datas` 实现原始查询方式：

```python
v_where = [VadminUser.id.in_([1,2,4,6]), VadminUser.email.isnot(None), VadminUser.name.like(f"%李%")]
users, count = UserDal(db).get_datas(limit=2, v_where=v_where, v_return_count=True)

# 这里的 get_datas 默认返回的是 pydantic 模型数据
# 如果需要返回用户对象列表，使用如下语句：
users, count = UserDal(db).get_datas(
    limit=2,
    v_where=v_where,
    v_return_count=True
    v_return_objs=True
)
```

### 外键查询示例

以常见问题表为主表，查询出创建用户名称为kinit的用户，创建了哪些常见问题，并加载出用户信息：

```python
v_options = [joinedload(VadminIssue.create_user)]
v_join = [["create_user"]]
v_where = [VadminUser.name == "kinit"]
datas = await crud.IssueCategoryDal(auth.db).get_datas(
    limit=0,
    v_options=options,
    v_join=v_join,
    v_where=v_where,
    v_return_objs=True
)
```

