中文汉化
Deploy to Staging / deploy (push) Has been cancelled
Conflict detector / main (push) Has been cancelled
Lint Backend / lint-backend (push) Has been cancelled
Playwright Tests / changes (push) Has been cancelled
Test Backend / test-backend (push) Has been cancelled
Test Docker Compose / test-docker-compose (push) Has been cancelled
Playwright Tests / test-playwright (1, 4) (push) Has been cancelled
Playwright Tests / test-playwright (2, 4) (push) Has been cancelled
Playwright Tests / test-playwright (3, 4) (push) Has been cancelled
Playwright Tests / test-playwright (4, 4) (push) Has been cancelled
Playwright Tests / merge-playwright-reports (push) Has been cancelled
Playwright Tests / alls-green-playwright (push) Has been cancelled
Issue Manager / issue-manager (push) Has been cancelled
Deploy to Staging / deploy (push) Has been cancelled
Conflict detector / main (push) Has been cancelled
Lint Backend / lint-backend (push) Has been cancelled
Playwright Tests / changes (push) Has been cancelled
Test Backend / test-backend (push) Has been cancelled
Test Docker Compose / test-docker-compose (push) Has been cancelled
Playwright Tests / test-playwright (1, 4) (push) Has been cancelled
Playwright Tests / test-playwright (2, 4) (push) Has been cancelled
Playwright Tests / test-playwright (3, 4) (push) Has been cancelled
Playwright Tests / test-playwright (4, 4) (push) Has been cancelled
Playwright Tests / merge-playwright-reports (push) Has been cancelled
Playwright Tests / alls-green-playwright (push) Has been cancelled
Issue Manager / issue-manager (push) Has been cancelled
This commit is contained in:
+12
-12
@@ -4,24 +4,24 @@ ENV PYTHONUNBUFFERED=1
|
||||
|
||||
WORKDIR /app/
|
||||
|
||||
# Install uv
|
||||
# Ref: https://docs.astral.sh/uv/guides/integration/docker/#installing-uv
|
||||
# 安装 uv
|
||||
# 参考: https://docs.astral.sh/uv/guides/integration/docker/#installing-uv
|
||||
COPY --from=ghcr.io/astral-sh/uv:0.5.11 /uv /uvx /bin/
|
||||
|
||||
# Place executables in the environment at the front of the path
|
||||
# Ref: https://docs.astral.sh/uv/guides/integration/docker/#using-the-environment
|
||||
# 将可执行文件放在环境路径的最前面
|
||||
# 参考: https://docs.astral.sh/uv/guides/integration/docker/#using-the-environment
|
||||
ENV PATH="/app/.venv/bin:$PATH"
|
||||
|
||||
# Compile bytecode
|
||||
# Ref: https://docs.astral.sh/uv/guides/integration/docker/#compiling-bytecode
|
||||
# 编译字节码
|
||||
# 参考: https://docs.astral.sh/uv/guides/integration/docker/#compiling-bytecode
|
||||
ENV UV_COMPILE_BYTECODE=1
|
||||
|
||||
# uv Cache
|
||||
# Ref: https://docs.astral.sh/uv/guides/integration/docker/#caching
|
||||
# uv 缓存
|
||||
# 参考: https://docs.astral.sh/uv/guides/integration/docker/#caching
|
||||
ENV UV_LINK_MODE=copy
|
||||
|
||||
# Install dependencies
|
||||
# Ref: https://docs.astral.sh/uv/guides/integration/docker/#intermediate-layers
|
||||
# 安装依赖
|
||||
# 参考: https://docs.astral.sh/uv/guides/integration/docker/#intermediate-layers
|
||||
RUN --mount=type=cache,target=/root/.cache/uv \
|
||||
--mount=type=bind,source=uv.lock,target=uv.lock \
|
||||
--mount=type=bind,source=pyproject.toml,target=pyproject.toml \
|
||||
@@ -36,8 +36,8 @@ COPY ./pyproject.toml ./uv.lock ./alembic.ini /app/
|
||||
COPY ./app /app/app
|
||||
COPY ./tests /app/tests
|
||||
|
||||
# Sync the project
|
||||
# Ref: https://docs.astral.sh/uv/guides/integration/docker/#intermediate-layers
|
||||
# 同步项目
|
||||
# 参考: https://docs.astral.sh/uv/guides/integration/docker/#intermediate-layers
|
||||
RUN --mount=type=cache,target=/root/.cache/uv \
|
||||
uv sync
|
||||
|
||||
|
||||
+64
-54
@@ -1,172 +1,182 @@
|
||||
# FastAPI Project - Backend
|
||||
# FastAPI 项目 - 后端(Backend)
|
||||
|
||||
## Requirements
|
||||
## 前置要求
|
||||
|
||||
* [Docker](https://www.docker.com/).
|
||||
* [uv](https://docs.astral.sh/uv/) for Python package and environment management.
|
||||
* 已安装 [Docker](https://www.docker.com/)。
|
||||
* 已安装 [uv](https://docs.astral.sh/uv/),用于管理 Python 包与虚拟环境。
|
||||
|
||||
## Docker Compose
|
||||
## 使用 Docker Compose
|
||||
|
||||
Start the local development environment with Docker Compose following the guide in [../development.md](../development.md).
|
||||
按照 [../development.md](../development.md) 中的说明,使用 Docker Compose 启动本地开发环境。
|
||||
|
||||
## General Workflow
|
||||
## 一般工作流
|
||||
|
||||
By default, the dependencies are managed with [uv](https://docs.astral.sh/uv/), go there and install it.
|
||||
本项目默认使用 [uv](https://docs.astral.sh/uv/) 管理依赖,请先安装它。
|
||||
|
||||
From `./backend/` you can install all the dependencies with:
|
||||
在 `./backend/` 目录下安装所有依赖:
|
||||
|
||||
```console
|
||||
$ uv sync
|
||||
```
|
||||
|
||||
Then you can activate the virtual environment with:
|
||||
然后激活虚拟环境:
|
||||
|
||||
```console
|
||||
$ source .venv/bin/activate
|
||||
```
|
||||
|
||||
Make sure your editor is using the correct Python virtual environment, with the interpreter at `backend/.venv/bin/python`.
|
||||
请确保你的编辑器使用的是该虚拟环境中的 Python 解释器:`backend/.venv/bin/python`。
|
||||
|
||||
Modify or add SQLModel models for data and SQL tables in `./backend/app/models.py`, API endpoints in `./backend/app/api/`, CRUD (Create, Read, Update, Delete) utils in `./backend/app/crud.py`.
|
||||
你可以在 `./backend/app/models.py` 中修改或新增 SQLModel 模型(数据表结构),在 `./backend/app/api/` 中添加 API 路由,在 `./backend/app/crud.py` 中添加 / 修改 CRUD(创建、读取、更新、删除)工具函数。
|
||||
|
||||
## VS Code
|
||||
|
||||
There are already configurations in place to run the backend through the VS Code debugger, so that you can use breakpoints, pause and explore variables, etc.
|
||||
项目中已经包含了 VS Code 的调试配置,你可以直接使用断点、单步调试、查看变量等功能来运行后端。
|
||||
|
||||
The setup is also already configured so you can run the tests through the VS Code Python tests tab.
|
||||
测试相关配置也已就绪,可以通过 VS Code 的 Python 测试面板直接运行测试。
|
||||
|
||||
## Docker Compose Override
|
||||
## Docker Compose 覆盖配置
|
||||
|
||||
During development, you can change Docker Compose settings that will only affect the local development environment in the file `docker-compose.override.yml`.
|
||||
开发阶段,你可以在 `docker-compose.override.yml` 中修改仅作用于本地开发环境的 Docker Compose 配置。
|
||||
|
||||
The changes to that file only affect the local development environment, not the production environment. So, you can add "temporary" changes that help the development workflow.
|
||||
这些改动只影响本地开发环境,不会影响生产环境。因此你可以在这里加入一些“临时”设置来提升本地开发效率。
|
||||
|
||||
For example, the directory with the backend code is synchronized in the Docker container, copying the code you change live to the directory inside the container. That allows you to test your changes right away, without having to build the Docker image again. It should only be done during development, for production, you should build the Docker image with a recent version of the backend code. But during development, it allows you to iterate very fast.
|
||||
例如:后端代码目录会以 volume 形式挂载进容器,你在本机修改代码后会实时同步到容器中,无需重新构建镜像即可看到效果。生产环境则应该基于最新代码重新构建镜像,而不是使用挂载的源码。
|
||||
|
||||
There is also a command override that runs `fastapi run --reload` instead of the default `fastapi run`. It starts a single server process (instead of multiple, as would be for production) and reloads the process whenever the code changes. Have in mind that if you have a syntax error and save the Python file, it will break and exit, and the container will stop. After that, you can restart the container by fixing the error and running again:
|
||||
配置中还包含一个覆盖命令,将默认的 `fastapi run` 改为 `fastapi run --reload`。它只启动一个进程(适合开发环境),并在检测到代码变更时自动重载。注意,如果保存了含语法错误的 Python 文件,进程会直接退出、容器也会停止。修复错误后,可以再次运行:
|
||||
|
||||
```console
|
||||
$ docker compose watch
|
||||
```
|
||||
|
||||
There is also a commented out `command` override, you can uncomment it and comment the default one. It makes the backend container run a process that does "nothing", but keeps the container alive. That allows you to get inside your running container and execute commands inside, for example a Python interpreter to test installed dependencies, or start the development server that reloads when it detects changes.
|
||||
文件中还提供了一个被注释掉的 `command` 覆盖项,你可以取消注释它并注释掉默认命令。该命令会让后端容器运行一个“空转”的进程,只是保持容器存活,方便你进入容器内部执行命令,例如打开 Python 交互解释器测试依赖,或手动启动支持热重载的开发服务器。
|
||||
|
||||
To get inside the container with a `bash` session you can start the stack with:
|
||||
如果你想通过 `bash` 进入容器,可以先启动整个 stack:
|
||||
|
||||
```console
|
||||
$ docker compose watch
|
||||
```
|
||||
|
||||
and then in another terminal, `exec` inside the running container:
|
||||
然后在另一个终端中执行:
|
||||
|
||||
```console
|
||||
$ docker compose exec backend bash
|
||||
```
|
||||
|
||||
You should see an output like:
|
||||
你会看到类似输出:
|
||||
|
||||
```console
|
||||
root@7f2607af31c3:/app#
|
||||
```
|
||||
|
||||
that means that you are in a `bash` session inside your container, as a `root` user, under the `/app` directory, this directory has another directory called "app" inside, that's where your code lives inside the container: `/app/app`.
|
||||
这说明你已经以 `root` 用户进入了容器中的 `/app` 目录,该目录下还有一个 `app` 子目录,即你的后端代码在容器中的位置:`/app/app`。
|
||||
|
||||
There you can use the `fastapi run --reload` command to run the debug live reloading server.
|
||||
在这里可以运行支持热重载的开发服务器:
|
||||
|
||||
```console
|
||||
$ fastapi run --reload app/main.py
|
||||
```
|
||||
|
||||
...it will look like:
|
||||
终端看起来会类似:
|
||||
|
||||
```console
|
||||
root@7f2607af31c3:/app# fastapi run --reload app/main.py
|
||||
```
|
||||
|
||||
and then hit enter. That runs the live reloading server that auto reloads when it detects code changes.
|
||||
然后按回车即可启动,它会在检测到代码变化时自动重载。
|
||||
|
||||
Nevertheless, if it doesn't detect a change but a syntax error, it will just stop with an error. But as the container is still alive and you are in a Bash session, you can quickly restart it after fixing the error, running the same command ("up arrow" and "Enter").
|
||||
如果碰到的不是代码变更而是语法错误,进程会直接退出。不过因为容器还在且你仍在 Bash 会话中,修复错误后按方向键上翻历史命令并回车即可快速重启。
|
||||
|
||||
...this previous detail is what makes it useful to have the container alive doing nothing and then, in a Bash session, make it run the live reload server.
|
||||
正是因为这一点,让“容器空转 + Bash 中手动启动热重载服务”的方式在开发中非常实用。
|
||||
|
||||
## Backend tests
|
||||
## 后端测试
|
||||
|
||||
To test the backend run:
|
||||
运行后端测试:
|
||||
|
||||
```console
|
||||
$ bash ./scripts/test.sh
|
||||
```
|
||||
|
||||
The tests run with Pytest, modify and add tests to `./backend/tests/`.
|
||||
测试使用 Pytest,测试文件位于 `./backend/tests/`,你可以在其中新增或修改测试。
|
||||
|
||||
If you use GitHub Actions the tests will run automatically.
|
||||
如果你使用 GitHub Actions,推送代码后测试会自动运行。
|
||||
|
||||
### Test running stack
|
||||
### 在已运行的 stack 中执行测试
|
||||
|
||||
If your stack is already up and you just want to run the tests, you can use:
|
||||
如果 Docker Compose 已经启动,只想在当前 stack 中运行测试,可以使用:
|
||||
|
||||
```bash
|
||||
docker compose exec backend bash scripts/tests-start.sh
|
||||
```
|
||||
|
||||
That `/app/scripts/tests-start.sh` script just calls `pytest` after making sure that the rest of the stack is running. If you need to pass extra arguments to `pytest`, you can pass them to that command and they will be forwarded.
|
||||
`/app/scripts/tests-start.sh` 脚本会在确认依赖服务已就绪后调用 `pytest`。如果需要给 `pytest` 传额外参数,可以直接附加在命令后面,它们会被转发给 `pytest`。
|
||||
|
||||
For example, to stop on first error:
|
||||
例如,遇到第一个错误就停止:
|
||||
|
||||
```bash
|
||||
docker compose exec backend bash scripts/tests-start.sh -x
|
||||
```
|
||||
|
||||
### Test Coverage
|
||||
### 测试覆盖率
|
||||
|
||||
When the tests are run, a file `htmlcov/index.html` is generated, you can open it in your browser to see the coverage of the tests.
|
||||
测试运行后会生成 `htmlcov/index.html` 文件,可在浏览器中打开查看覆盖率报告。
|
||||
|
||||
## Migrations
|
||||
## 数据库迁移(Migrations)
|
||||
|
||||
As during local development your app directory is mounted as a volume inside the container, you can also run the migrations with `alembic` commands inside the container and the migration code will be in your app directory (instead of being only inside the container). So you can add it to your git repository.
|
||||
本地开发时,应用代码目录以 volume 形式挂载到容器中,因此可以在容器内使用 `alembic` 命令生成迁移文件,这些文件会直接写入你的项目目录,方便你提交到 git 仓库。
|
||||
|
||||
Make sure you create a "revision" of your models and that you "upgrade" your database with that revision every time you change them. As this is what will update the tables in your database. Otherwise, your application will have errors.
|
||||
每次修改模型后,请确保:
|
||||
|
||||
* Start an interactive session in the backend container:
|
||||
1. 创建新的迁移“revision”,描述本次模型变更;
|
||||
2. 使用该 revision 升级数据库。
|
||||
|
||||
否则数据库表结构不会更新,应用就会报错。
|
||||
|
||||
基本流程如下:
|
||||
|
||||
* 在后端容器中启动交互会话:
|
||||
|
||||
```console
|
||||
$ docker compose exec backend bash
|
||||
```
|
||||
|
||||
* Alembic is already configured to import your SQLModel models from `./backend/app/models.py`.
|
||||
* `alembic` 已配置好从 `./backend/app/models.py` 导入 SQLModel 模型。
|
||||
|
||||
* After changing a model (for example, adding a column), inside the container, create a revision, e.g.:
|
||||
* 修改模型(例如新增字段)后,在容器内创建迁移 revision,例如:
|
||||
|
||||
```console
|
||||
$ alembic revision --autogenerate -m "Add column last_name to User model"
|
||||
```
|
||||
|
||||
* Commit to the git repository the files generated in the alembic directory.
|
||||
* 将 Alembic 目录中生成的 Python 迁移文件提交到 git。
|
||||
|
||||
* After creating the revision, run the migration in the database (this is what will actually change the database):
|
||||
* 创建完 revision 后,在数据库中执行迁移(真正修改数据库结构):
|
||||
|
||||
```console
|
||||
$ alembic upgrade head
|
||||
```
|
||||
|
||||
If you don't want to use migrations at all, uncomment the lines in the file at `./backend/app/core/db.py` that end in:
|
||||
如果你完全不想使用迁移,可以在 `./backend/app/core/db.py` 中取消注释以 `SQLModel.metadata.create_all(engine)` 结尾的代码:
|
||||
|
||||
```python
|
||||
SQLModel.metadata.create_all(engine)
|
||||
```
|
||||
|
||||
and comment the line in the file `scripts/prestart.sh` that contains:
|
||||
同时在 `scripts/prestart.sh` 中注释掉包含以下内容的那一行:
|
||||
|
||||
```console
|
||||
$ alembic upgrade head
|
||||
```
|
||||
|
||||
If you don't want to start with the default models and want to remove them / modify them, from the beginning, without having any previous revision, you can remove the revision files (`.py` Python files) under `./backend/app/alembic/versions/`. And then create a first migration as described above.
|
||||
如果你不想从默认模型开始,而是从零开始定义自己的模型,并且不希望有任何已有迁移,可以删除 `./backend/app/alembic/versions/` 下的所有迁移文件(`.py`),然后按上述步骤创建第一条迁移。
|
||||
|
||||
## Email Templates
|
||||
## 邮件模板
|
||||
|
||||
The email templates are in `./backend/app/email-templates/`. Here, there are two directories: `build` and `src`. The `src` directory contains the source files that are used to build the final email templates. The `build` directory contains the final email templates that are used by the application.
|
||||
邮件模板位于 `./backend/app/email-templates/` 目录,其中:
|
||||
|
||||
Before continuing, ensure you have the [MJML extension](https://marketplace.visualstudio.com/items?itemName=attilabuti.vscode-mjml) installed in your VS Code.
|
||||
- `src`:存放源模板(`.mjml` 文件等),用于生成最终 HTML 模板;
|
||||
- `build`:存放构建后的 HTML 邮件模板,实际由应用使用。
|
||||
|
||||
Once you have the MJML extension installed, you can create a new email template in the `src` directory. After creating the new email template and with the `.mjml` file open in your editor, open the command palette with `Ctrl+Shift+P` and search for `MJML: Export to HTML`. This will convert the `.mjml` file to a `.html` file and now you can save it in the build directory.
|
||||
继续之前,请先在 VS Code 中安装 [MJML 扩展](https://marketplace.visualstudio.com/items?itemName=attilabuti.vscode-mjml)。
|
||||
|
||||
安装完成后,你可以在 `src` 目录中创建新的邮件模板。创建好 `.mjml` 文件并在编辑器中打开后,按 `Ctrl+Shift+P` 打开命令面板,搜索 `MJML: Export to HTML`。该命令会将 `.mjml` 文件转换为 `.html` 文件,然后你可以将其保存到 `build` 目录中供应用使用。
|
||||
|
||||
+15
-20
@@ -1,41 +1,36 @@
|
||||
# A generic, single database configuration.
|
||||
# 通用单数据库配置
|
||||
|
||||
[alembic]
|
||||
# path to migration scripts
|
||||
# 迁移脚本路径
|
||||
script_location = app/alembic
|
||||
|
||||
# template used to generate migration files
|
||||
# 用于生成迁移文件的模板
|
||||
# file_template = %%(rev)s_%%(slug)s
|
||||
|
||||
# timezone to use when rendering the date
|
||||
# within the migration file as well as the filename.
|
||||
# string value is passed to dateutil.tz.gettz()
|
||||
# leave blank for localtime
|
||||
# 在迁移文件和文件名中渲染日期时使用的时区
|
||||
# 字符串值传递给 dateutil.tz.gettz()
|
||||
# 留空则使用本地时间
|
||||
# timezone =
|
||||
|
||||
# max length of characters to apply to the
|
||||
# "slug" field
|
||||
# 应用于 "slug" 字段的最大字符长度
|
||||
#truncate_slug_length = 40
|
||||
|
||||
# set to 'true' to run the environment during
|
||||
# the 'revision' command, regardless of autogenerate
|
||||
# 设置为 'true' 以在 'revision' 命令期间运行环境,
|
||||
# 无论是否自动生成
|
||||
# revision_environment = false
|
||||
|
||||
# set to 'true' to allow .pyc and .pyo files without
|
||||
# a source .py file to be detected as revisions in the
|
||||
# versions/ directory
|
||||
# 设置为 'true' 以允许在没有源 .py 文件的情况下
|
||||
# 将 .pyc 和 .pyo 文件检测为 versions/ 目录中的修订版本
|
||||
# sourceless = false
|
||||
|
||||
# version location specification; this defaults
|
||||
# to alembic/versions. When using multiple version
|
||||
# directories, initial revisions must be specified with --version-path
|
||||
# 版本位置规范;默认为 alembic/versions
|
||||
# 使用多个版本目录时,必须使用 --version-path 指定初始修订版本
|
||||
# version_locations = %(here)s/bar %(here)s/bat alembic/versions
|
||||
|
||||
# the output encoding used when revision files
|
||||
# are written from script.py.mako
|
||||
# 从 script.py.mako 写入修订文件时使用的输出编码
|
||||
# output_encoding = utf-8
|
||||
|
||||
# Logging configuration
|
||||
# 日志配置
|
||||
[loggers]
|
||||
keys = root,sqlalchemy,alembic
|
||||
|
||||
|
||||
+15
-19
@@ -4,16 +4,15 @@ from logging.config import fileConfig
|
||||
from alembic import context
|
||||
from sqlalchemy import engine_from_config, pool
|
||||
|
||||
# this is the Alembic Config object, which provides
|
||||
# access to the values within the .ini file in use.
|
||||
# 这是 Alembic 配置对象,提供对正在使用的 .ini 文件中值的访问。
|
||||
config = context.config
|
||||
|
||||
# Interpret the config file for Python logging.
|
||||
# This line sets up loggers basically.
|
||||
# 解析配置文件以进行 Python 日志记录。
|
||||
# 这行代码用于设置日志记录器。
|
||||
fileConfig(config.config_file_name)
|
||||
|
||||
# add your model's MetaData object here
|
||||
# for 'autogenerate' support
|
||||
# 在此处添加模型的 MetaData 对象
|
||||
# 以支持 'autogenerate'
|
||||
# from myapp import mymodel
|
||||
# target_metadata = mymodel.Base.metadata
|
||||
# target_metadata = None
|
||||
@@ -23,10 +22,9 @@ from app.core.config import settings # noqa
|
||||
|
||||
target_metadata = SQLModel.metadata
|
||||
|
||||
# other values from the config, defined by the needs of env.py,
|
||||
# can be acquired:
|
||||
# 根据 env.py 的需要,可以从配置中获取其他值:
|
||||
# my_important_option = config.get_main_option("my_important_option")
|
||||
# ... etc.
|
||||
# ... 等等。
|
||||
|
||||
|
||||
def get_url():
|
||||
@@ -34,15 +32,13 @@ def get_url():
|
||||
|
||||
|
||||
def run_migrations_offline():
|
||||
"""Run migrations in 'offline' mode.
|
||||
"""在"离线"模式下运行迁移。
|
||||
|
||||
This configures the context with just a URL
|
||||
and not an Engine, though an Engine is acceptable
|
||||
here as well. By skipping the Engine creation
|
||||
we don't even need a DBAPI to be available.
|
||||
这使用 URL 而不是 Engine 来配置上下文,
|
||||
尽管 Engine 在这里也是可以接受的。
|
||||
通过跳过 Engine 的创建,我们甚至不需要 DBAPI 可用。
|
||||
|
||||
Calls to context.execute() here emit the given string to the
|
||||
script output.
|
||||
在这里调用 context.execute() 会将给定的字符串输出到脚本输出。
|
||||
|
||||
"""
|
||||
url = get_url()
|
||||
@@ -55,10 +51,10 @@ def run_migrations_offline():
|
||||
|
||||
|
||||
def run_migrations_online():
|
||||
"""Run migrations in 'online' mode.
|
||||
"""在"在线"模式下运行迁移。
|
||||
|
||||
In this scenario we need to create an Engine
|
||||
and associate a connection with the context.
|
||||
在这种情况下,我们需要创建一个 Engine
|
||||
并将连接与上下文关联。
|
||||
|
||||
"""
|
||||
configuration = config.get_section(config.config_ini_section)
|
||||
|
||||
@@ -36,13 +36,13 @@ def get_current_user(session: SessionDep, token: TokenDep) -> User:
|
||||
except (InvalidTokenError, ValidationError):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Could not validate credentials",
|
||||
detail="无法验证凭据",
|
||||
)
|
||||
user = session.get(User, token_data.sub)
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail="User not found")
|
||||
raise HTTPException(status_code=404, detail="用户未找到")
|
||||
if not user.is_active:
|
||||
raise HTTPException(status_code=400, detail="Inactive user")
|
||||
raise HTTPException(status_code=400, detail="用户未激活")
|
||||
return user
|
||||
|
||||
|
||||
@@ -52,6 +52,6 @@ CurrentUser = Annotated[User, Depends(get_current_user)]
|
||||
def get_current_active_superuser(current_user: CurrentUser) -> User:
|
||||
if not current_user.is_superuser:
|
||||
raise HTTPException(
|
||||
status_code=403, detail="The user doesn't have enough privileges"
|
||||
status_code=403, detail="用户权限不足"
|
||||
)
|
||||
return current_user
|
||||
|
||||
@@ -15,7 +15,7 @@ def read_items(
|
||||
session: SessionDep, current_user: CurrentUser, skip: int = 0, limit: int = 100
|
||||
) -> Any:
|
||||
"""
|
||||
Retrieve items.
|
||||
获取 Item 列表。
|
||||
"""
|
||||
|
||||
if current_user.is_superuser:
|
||||
@@ -44,13 +44,13 @@ def read_items(
|
||||
@router.get("/{id}", response_model=ItemPublic)
|
||||
def read_item(session: SessionDep, current_user: CurrentUser, id: uuid.UUID) -> Any:
|
||||
"""
|
||||
Get item by ID.
|
||||
通过 ID 获取 Item。
|
||||
"""
|
||||
item = session.get(Item, id)
|
||||
if not item:
|
||||
raise HTTPException(status_code=404, detail="Item not found")
|
||||
raise HTTPException(status_code=404, detail="项目未找到")
|
||||
if not current_user.is_superuser and (item.owner_id != current_user.id):
|
||||
raise HTTPException(status_code=400, detail="Not enough permissions")
|
||||
raise HTTPException(status_code=400, detail="权限不足")
|
||||
return item
|
||||
|
||||
|
||||
@@ -59,7 +59,7 @@ def create_item(
|
||||
*, session: SessionDep, current_user: CurrentUser, item_in: ItemCreate
|
||||
) -> Any:
|
||||
"""
|
||||
Create new item.
|
||||
创建新的 Item。
|
||||
"""
|
||||
item = Item.model_validate(item_in, update={"owner_id": current_user.id})
|
||||
session.add(item)
|
||||
@@ -77,13 +77,13 @@ def update_item(
|
||||
item_in: ItemUpdate,
|
||||
) -> Any:
|
||||
"""
|
||||
Update an item.
|
||||
更新 Item。
|
||||
"""
|
||||
item = session.get(Item, id)
|
||||
if not item:
|
||||
raise HTTPException(status_code=404, detail="Item not found")
|
||||
raise HTTPException(status_code=404, detail="项目未找到")
|
||||
if not current_user.is_superuser and (item.owner_id != current_user.id):
|
||||
raise HTTPException(status_code=400, detail="Not enough permissions")
|
||||
raise HTTPException(status_code=400, detail="权限不足")
|
||||
update_dict = item_in.model_dump(exclude_unset=True)
|
||||
item.sqlmodel_update(update_dict)
|
||||
session.add(item)
|
||||
@@ -97,13 +97,13 @@ def delete_item(
|
||||
session: SessionDep, current_user: CurrentUser, id: uuid.UUID
|
||||
) -> Message:
|
||||
"""
|
||||
Delete an item.
|
||||
删除 Item。
|
||||
"""
|
||||
item = session.get(Item, id)
|
||||
if not item:
|
||||
raise HTTPException(status_code=404, detail="Item not found")
|
||||
raise HTTPException(status_code=404, detail="项目未找到")
|
||||
if not current_user.is_superuser and (item.owner_id != current_user.id):
|
||||
raise HTTPException(status_code=400, detail="Not enough permissions")
|
||||
raise HTTPException(status_code=400, detail="权限不足")
|
||||
session.delete(item)
|
||||
session.commit()
|
||||
return Message(message="Item deleted successfully")
|
||||
return Message(message="项目删除成功")
|
||||
|
||||
@@ -26,15 +26,15 @@ def login_access_token(
|
||||
session: SessionDep, form_data: Annotated[OAuth2PasswordRequestForm, Depends()]
|
||||
) -> Token:
|
||||
"""
|
||||
OAuth2 compatible token login, get an access token for future requests
|
||||
OAuth2 兼容的令牌登录,获取用于后续请求的访问令牌
|
||||
"""
|
||||
user = crud.authenticate(
|
||||
session=session, email=form_data.username, password=form_data.password
|
||||
)
|
||||
if not user:
|
||||
raise HTTPException(status_code=400, detail="Incorrect email or password")
|
||||
raise HTTPException(status_code=400, detail="邮箱或密码错误")
|
||||
elif not user.is_active:
|
||||
raise HTTPException(status_code=400, detail="Inactive user")
|
||||
raise HTTPException(status_code=400, detail="用户未激活")
|
||||
access_token_expires = timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
|
||||
return Token(
|
||||
access_token=security.create_access_token(
|
||||
@@ -46,7 +46,7 @@ def login_access_token(
|
||||
@router.post("/login/test-token", response_model=UserPublic)
|
||||
def test_token(current_user: CurrentUser) -> Any:
|
||||
"""
|
||||
Test access token
|
||||
测试访问令牌
|
||||
"""
|
||||
return current_user
|
||||
|
||||
@@ -54,14 +54,14 @@ def test_token(current_user: CurrentUser) -> Any:
|
||||
@router.post("/password-recovery/{email}")
|
||||
def recover_password(email: str, session: SessionDep) -> Message:
|
||||
"""
|
||||
Password Recovery
|
||||
密码找回
|
||||
"""
|
||||
user = crud.get_user_by_email(session=session, email=email)
|
||||
|
||||
if not user:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail="The user with this email does not exist in the system.",
|
||||
detail="系统中不存在该邮箱的用户。",
|
||||
)
|
||||
password_reset_token = generate_password_reset_token(email=email)
|
||||
email_data = generate_reset_password_email(
|
||||
@@ -72,30 +72,30 @@ def recover_password(email: str, session: SessionDep) -> Message:
|
||||
subject=email_data.subject,
|
||||
html_content=email_data.html_content,
|
||||
)
|
||||
return Message(message="Password recovery email sent")
|
||||
return Message(message="密码找回邮件已发送")
|
||||
|
||||
|
||||
@router.post("/reset-password/")
|
||||
def reset_password(session: SessionDep, body: NewPassword) -> Message:
|
||||
"""
|
||||
Reset password
|
||||
重置密码
|
||||
"""
|
||||
email = verify_password_reset_token(token=body.token)
|
||||
if not email:
|
||||
raise HTTPException(status_code=400, detail="Invalid token")
|
||||
raise HTTPException(status_code=400, detail="无效的令牌")
|
||||
user = crud.get_user_by_email(session=session, email=email)
|
||||
if not user:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail="The user with this email does not exist in the system.",
|
||||
detail="系统中不存在该邮箱的用户。",
|
||||
)
|
||||
elif not user.is_active:
|
||||
raise HTTPException(status_code=400, detail="Inactive user")
|
||||
raise HTTPException(status_code=400, detail="用户未激活")
|
||||
hashed_password = get_password_hash(password=body.new_password)
|
||||
user.hashed_password = hashed_password
|
||||
session.add(user)
|
||||
session.commit()
|
||||
return Message(message="Password updated successfully")
|
||||
return Message(message="密码更新成功")
|
||||
|
||||
|
||||
@router.post(
|
||||
@@ -105,14 +105,14 @@ def reset_password(session: SessionDep, body: NewPassword) -> Message:
|
||||
)
|
||||
def recover_password_html_content(email: str, session: SessionDep) -> Any:
|
||||
"""
|
||||
HTML Content for Password Recovery
|
||||
获取密码找回邮件的 HTML 内容
|
||||
"""
|
||||
user = crud.get_user_by_email(session=session, email=email)
|
||||
|
||||
if not user:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail="The user with this username does not exist in the system.",
|
||||
detail="系统中不存在该用户名的用户。",
|
||||
)
|
||||
password_reset_token = generate_password_reset_token(email=email)
|
||||
email_data = generate_reset_password_email(
|
||||
|
||||
@@ -23,7 +23,7 @@ class PrivateUserCreate(BaseModel):
|
||||
@router.post("/users/", response_model=UserPublic)
|
||||
def create_user(user_in: PrivateUserCreate, session: SessionDep) -> Any:
|
||||
"""
|
||||
Create a new user.
|
||||
创建新用户。
|
||||
"""
|
||||
|
||||
user = User(
|
||||
|
||||
@@ -36,7 +36,7 @@ router = APIRouter(prefix="/users", tags=["users"])
|
||||
)
|
||||
def read_users(session: SessionDep, skip: int = 0, limit: int = 100) -> Any:
|
||||
"""
|
||||
Retrieve users.
|
||||
获取用户列表。
|
||||
"""
|
||||
|
||||
count_statement = select(func.count()).select_from(User)
|
||||
@@ -53,13 +53,13 @@ def read_users(session: SessionDep, skip: int = 0, limit: int = 100) -> Any:
|
||||
)
|
||||
def create_user(*, session: SessionDep, user_in: UserCreate) -> Any:
|
||||
"""
|
||||
Create new user.
|
||||
创建新用户。
|
||||
"""
|
||||
user = crud.get_user_by_email(session=session, email=user_in.email)
|
||||
if user:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="The user with this email already exists in the system.",
|
||||
detail="系统中已存在该邮箱的用户。",
|
||||
)
|
||||
|
||||
user = crud.create_user(session=session, user_create=user_in)
|
||||
@@ -80,14 +80,14 @@ def update_user_me(
|
||||
*, session: SessionDep, user_in: UserUpdateMe, current_user: CurrentUser
|
||||
) -> Any:
|
||||
"""
|
||||
Update own user.
|
||||
更新当前用户信息。
|
||||
"""
|
||||
|
||||
if user_in.email:
|
||||
existing_user = crud.get_user_by_email(session=session, email=user_in.email)
|
||||
if existing_user and existing_user.id != current_user.id:
|
||||
raise HTTPException(
|
||||
status_code=409, detail="User with this email already exists"
|
||||
status_code=409, detail="该邮箱已被其他用户使用"
|
||||
)
|
||||
user_data = user_in.model_dump(exclude_unset=True)
|
||||
current_user.sqlmodel_update(user_data)
|
||||
@@ -102,25 +102,25 @@ def update_password_me(
|
||||
*, session: SessionDep, body: UpdatePassword, current_user: CurrentUser
|
||||
) -> Any:
|
||||
"""
|
||||
Update own password.
|
||||
更新当前用户密码。
|
||||
"""
|
||||
if not verify_password(body.current_password, current_user.hashed_password):
|
||||
raise HTTPException(status_code=400, detail="Incorrect password")
|
||||
raise HTTPException(status_code=400, detail="密码错误")
|
||||
if body.current_password == body.new_password:
|
||||
raise HTTPException(
|
||||
status_code=400, detail="New password cannot be the same as the current one"
|
||||
status_code=400, detail="新密码不能与当前密码相同"
|
||||
)
|
||||
hashed_password = get_password_hash(body.new_password)
|
||||
current_user.hashed_password = hashed_password
|
||||
session.add(current_user)
|
||||
session.commit()
|
||||
return Message(message="Password updated successfully")
|
||||
return Message(message="密码更新成功")
|
||||
|
||||
|
||||
@router.get("/me", response_model=UserPublic)
|
||||
def read_user_me(current_user: CurrentUser) -> Any:
|
||||
"""
|
||||
Get current user.
|
||||
获取当前用户信息。
|
||||
"""
|
||||
return current_user
|
||||
|
||||
@@ -128,27 +128,27 @@ def read_user_me(current_user: CurrentUser) -> Any:
|
||||
@router.delete("/me", response_model=Message)
|
||||
def delete_user_me(session: SessionDep, current_user: CurrentUser) -> Any:
|
||||
"""
|
||||
Delete own user.
|
||||
删除当前用户。
|
||||
"""
|
||||
if current_user.is_superuser:
|
||||
raise HTTPException(
|
||||
status_code=403, detail="Super users are not allowed to delete themselves"
|
||||
status_code=403, detail="超级用户不允许删除自己"
|
||||
)
|
||||
session.delete(current_user)
|
||||
session.commit()
|
||||
return Message(message="User deleted successfully")
|
||||
return Message(message="用户删除成功")
|
||||
|
||||
|
||||
@router.post("/signup", response_model=UserPublic)
|
||||
def register_user(session: SessionDep, user_in: UserRegister) -> Any:
|
||||
"""
|
||||
Create new user without the need to be logged in.
|
||||
无需登录即可创建新用户(用户注册)。
|
||||
"""
|
||||
user = crud.get_user_by_email(session=session, email=user_in.email)
|
||||
if user:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="The user with this email already exists in the system",
|
||||
detail="系统中已存在该邮箱的用户",
|
||||
)
|
||||
user_create = UserCreate.model_validate(user_in)
|
||||
user = crud.create_user(session=session, user_create=user_create)
|
||||
@@ -160,7 +160,7 @@ def read_user_by_id(
|
||||
user_id: uuid.UUID, session: SessionDep, current_user: CurrentUser
|
||||
) -> Any:
|
||||
"""
|
||||
Get a specific user by id.
|
||||
通过 ID 获取指定用户。
|
||||
"""
|
||||
user = session.get(User, user_id)
|
||||
if user == current_user:
|
||||
@@ -168,7 +168,7 @@ def read_user_by_id(
|
||||
if not current_user.is_superuser:
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail="The user doesn't have enough privileges",
|
||||
detail="用户权限不足",
|
||||
)
|
||||
return user
|
||||
|
||||
@@ -185,20 +185,20 @@ def update_user(
|
||||
user_in: UserUpdate,
|
||||
) -> Any:
|
||||
"""
|
||||
Update a user.
|
||||
更新用户信息。
|
||||
"""
|
||||
|
||||
db_user = session.get(User, user_id)
|
||||
if not db_user:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail="The user with this id does not exist in the system",
|
||||
detail="系统中不存在该ID的用户",
|
||||
)
|
||||
if user_in.email:
|
||||
existing_user = crud.get_user_by_email(session=session, email=user_in.email)
|
||||
if existing_user and existing_user.id != user_id:
|
||||
raise HTTPException(
|
||||
status_code=409, detail="User with this email already exists"
|
||||
status_code=409, detail="该邮箱已被其他用户使用"
|
||||
)
|
||||
|
||||
db_user = crud.update_user(session=session, db_user=db_user, user_in=user_in)
|
||||
@@ -210,17 +210,17 @@ def delete_user(
|
||||
session: SessionDep, current_user: CurrentUser, user_id: uuid.UUID
|
||||
) -> Message:
|
||||
"""
|
||||
Delete a user.
|
||||
删除用户。
|
||||
"""
|
||||
user = session.get(User, user_id)
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail="User not found")
|
||||
raise HTTPException(status_code=404, detail="用户未找到")
|
||||
if user == current_user:
|
||||
raise HTTPException(
|
||||
status_code=403, detail="Super users are not allowed to delete themselves"
|
||||
status_code=403, detail="超级用户不允许删除自己"
|
||||
)
|
||||
statement = delete(Item).where(col(Item.owner_id) == user_id)
|
||||
session.exec(statement) # type: ignore
|
||||
session.delete(user)
|
||||
session.commit()
|
||||
return Message(message="User deleted successfully")
|
||||
return Message(message="用户删除成功")
|
||||
|
||||
@@ -15,7 +15,7 @@ router = APIRouter(prefix="/utils", tags=["utils"])
|
||||
)
|
||||
def test_email(email_to: EmailStr) -> Message:
|
||||
"""
|
||||
Test emails.
|
||||
测试邮件发送。
|
||||
"""
|
||||
email_data = generate_test_email(email_to=email_to)
|
||||
send_email(
|
||||
@@ -23,7 +23,7 @@ def test_email(email_to: EmailStr) -> Message:
|
||||
subject=email_data.subject,
|
||||
html_content=email_data.html_content,
|
||||
)
|
||||
return Message(message="Test email sent")
|
||||
return Message(message="测试邮件已发送")
|
||||
|
||||
|
||||
@router.get("/health-check/")
|
||||
|
||||
@@ -22,7 +22,7 @@ wait_seconds = 1
|
||||
def init(db_engine: Engine) -> None:
|
||||
try:
|
||||
with Session(db_engine) as session:
|
||||
# Try to create session to check if DB is awake
|
||||
# 尝试创建会话以检查数据库是否已就绪
|
||||
session.exec(select(1))
|
||||
except Exception as e:
|
||||
logger.error(e)
|
||||
@@ -30,9 +30,9 @@ def init(db_engine: Engine) -> None:
|
||||
|
||||
|
||||
def main() -> None:
|
||||
logger.info("Initializing service")
|
||||
logger.info("正在初始化服务")
|
||||
init(engine)
|
||||
logger.info("Service finished initializing")
|
||||
logger.info("服务初始化完成")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -25,14 +25,14 @@ def parse_cors(v: Any) -> list[str] | str:
|
||||
|
||||
class Settings(BaseSettings):
|
||||
model_config = SettingsConfigDict(
|
||||
# Use top level .env file (one level above ./backend/)
|
||||
# 使用顶层的 .env 文件(在 ./backend/ 上一级目录)
|
||||
env_file="../.env",
|
||||
env_ignore_empty=True,
|
||||
extra="ignore",
|
||||
)
|
||||
API_V1_STR: str = "/api/v1"
|
||||
SECRET_KEY: str = secrets.token_urlsafe(32)
|
||||
# 60 minutes * 24 hours * 8 days = 8 days
|
||||
# 60 分钟 * 24 小时 * 8 天 = 8 天
|
||||
ACCESS_TOKEN_EXPIRE_MINUTES: int = 60 * 24 * 8
|
||||
FRONTEND_HOST: str = "http://localhost:5173"
|
||||
ENVIRONMENT: Literal["local", "staging", "production"] = "local"
|
||||
@@ -97,8 +97,8 @@ class Settings(BaseSettings):
|
||||
def _check_default_secret(self, var_name: str, value: str | None) -> None:
|
||||
if value == "changethis":
|
||||
message = (
|
||||
f'The value of {var_name} is "changethis", '
|
||||
"for security, please change it, at least for deployments."
|
||||
f'{var_name} 的值为 "changethis",'
|
||||
"出于安全考虑,请修改它,至少在部署时需要修改。"
|
||||
)
|
||||
if self.ENVIRONMENT == "local":
|
||||
warnings.warn(message, stacklevel=1)
|
||||
|
||||
@@ -7,18 +7,17 @@ from app.models import User, UserCreate
|
||||
engine = create_engine(str(settings.SQLALCHEMY_DATABASE_URI))
|
||||
|
||||
|
||||
# make sure all SQLModel models are imported (app.models) before initializing DB
|
||||
# otherwise, SQLModel might fail to initialize relationships properly
|
||||
# for more details: https://github.com/fastapi/full-stack-fastapi-template/issues/28
|
||||
# 确保在初始化数据库之前已导入所有 SQLModel 模型(app.models)
|
||||
# 否则,SQLModel 可能无法正确初始化关系
|
||||
# 更多详情请参考:https://github.com/fastapi/full-stack-fastapi-template/issues/28
|
||||
|
||||
|
||||
def init_db(session: Session) -> None:
|
||||
# Tables should be created with Alembic migrations
|
||||
# But if you don't want to use migrations, create
|
||||
# the tables un-commenting the next lines
|
||||
# 数据库表应该通过 Alembic 迁移创建
|
||||
# 但如果你不想使用迁移,可以取消注释下面的代码来创建表
|
||||
# from sqlmodel import SQLModel
|
||||
|
||||
# This works because the models are already imported and registered from app.models
|
||||
# 这样可以正常工作,因为模型已经从 app.models 导入并注册
|
||||
# SQLModel.metadata.create_all(engine)
|
||||
|
||||
user = session.exec(
|
||||
|
||||
@@ -14,9 +14,9 @@ def init() -> None:
|
||||
|
||||
|
||||
def main() -> None:
|
||||
logger.info("Creating initial data")
|
||||
logger.info("正在创建初始数据")
|
||||
init()
|
||||
logger.info("Initial data created")
|
||||
logger.info("初始数据创建完成")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
+1
-1
@@ -20,7 +20,7 @@ app = FastAPI(
|
||||
generate_unique_id_function=custom_generate_unique_id,
|
||||
)
|
||||
|
||||
# Set all CORS enabled origins
|
||||
# 设置所有允许 CORS 的源
|
||||
if settings.all_cors_origins:
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
|
||||
+13
-13
@@ -4,7 +4,7 @@ from pydantic import EmailStr
|
||||
from sqlmodel import Field, Relationship, SQLModel
|
||||
|
||||
|
||||
# Shared properties
|
||||
# 共享属性
|
||||
class UserBase(SQLModel):
|
||||
email: EmailStr = Field(unique=True, index=True, max_length=255)
|
||||
is_active: bool = True
|
||||
@@ -12,7 +12,7 @@ class UserBase(SQLModel):
|
||||
full_name: str | None = Field(default=None, max_length=255)
|
||||
|
||||
|
||||
# Properties to receive via API on creation
|
||||
# 创建用户时通过 API 接收的属性
|
||||
class UserCreate(UserBase):
|
||||
password: str = Field(min_length=8, max_length=128)
|
||||
|
||||
@@ -23,7 +23,7 @@ class UserRegister(SQLModel):
|
||||
full_name: str | None = Field(default=None, max_length=255)
|
||||
|
||||
|
||||
# Properties to receive via API on update, all are optional
|
||||
# 更新用户时通过 API 接收的属性,所有字段均为可选
|
||||
class UserUpdate(UserBase):
|
||||
email: EmailStr | None = Field(default=None, max_length=255) # type: ignore
|
||||
password: str | None = Field(default=None, min_length=8, max_length=128)
|
||||
@@ -39,14 +39,14 @@ class UpdatePassword(SQLModel):
|
||||
new_password: str = Field(min_length=8, max_length=128)
|
||||
|
||||
|
||||
# Database model, database table inferred from class name
|
||||
# 数据库模型,数据库表名从类名推断
|
||||
class User(UserBase, table=True):
|
||||
id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True)
|
||||
hashed_password: str
|
||||
items: list["Item"] = Relationship(back_populates="owner", cascade_delete=True)
|
||||
|
||||
|
||||
# Properties to return via API, id is always required
|
||||
# 通过 API 返回的属性,id 始终必填
|
||||
class UserPublic(UserBase):
|
||||
id: uuid.UUID
|
||||
|
||||
@@ -56,23 +56,23 @@ class UsersPublic(SQLModel):
|
||||
count: int
|
||||
|
||||
|
||||
# Shared properties
|
||||
# 共享属性
|
||||
class ItemBase(SQLModel):
|
||||
title: str = Field(min_length=1, max_length=255)
|
||||
description: str | None = Field(default=None, max_length=255)
|
||||
|
||||
|
||||
# Properties to receive on item creation
|
||||
# 创建 Item 时接收的属性
|
||||
class ItemCreate(ItemBase):
|
||||
pass
|
||||
|
||||
|
||||
# Properties to receive on item update
|
||||
# 更新 Item 时接收的属性
|
||||
class ItemUpdate(ItemBase):
|
||||
title: str | None = Field(default=None, min_length=1, max_length=255) # type: ignore
|
||||
|
||||
|
||||
# Database model, database table inferred from class name
|
||||
# 数据库模型,数据库表名从类名推断
|
||||
class Item(ItemBase, table=True):
|
||||
id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True)
|
||||
owner_id: uuid.UUID = Field(
|
||||
@@ -81,7 +81,7 @@ class Item(ItemBase, table=True):
|
||||
owner: User | None = Relationship(back_populates="items")
|
||||
|
||||
|
||||
# Properties to return via API, id is always required
|
||||
# 通过 API 返回的属性,id 始终必填
|
||||
class ItemPublic(ItemBase):
|
||||
id: uuid.UUID
|
||||
owner_id: uuid.UUID
|
||||
@@ -92,18 +92,18 @@ class ItemsPublic(SQLModel):
|
||||
count: int
|
||||
|
||||
|
||||
# Generic message
|
||||
# 通用消息
|
||||
class Message(SQLModel):
|
||||
message: str
|
||||
|
||||
|
||||
# JSON payload containing access token
|
||||
# 包含访问令牌的 JSON 载荷
|
||||
class Token(SQLModel):
|
||||
access_token: str
|
||||
token_type: str = "bearer"
|
||||
|
||||
|
||||
# Contents of JWT token
|
||||
# JWT 令牌内容
|
||||
class TokenPayload(SQLModel):
|
||||
sub: str | None = None
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ wait_seconds = 1
|
||||
)
|
||||
def init(db_engine: Engine) -> None:
|
||||
try:
|
||||
# Try to create session to check if DB is awake
|
||||
# 尝试创建会话以检查数据库是否已就绪
|
||||
with Session(db_engine) as session:
|
||||
session.exec(select(1))
|
||||
except Exception as e:
|
||||
@@ -30,9 +30,9 @@ def init(db_engine: Engine) -> None:
|
||||
|
||||
|
||||
def main() -> None:
|
||||
logger.info("Initializing service")
|
||||
logger.info("正在初始化服务")
|
||||
init(engine)
|
||||
logger.info("Service finished initializing")
|
||||
logger.info("服务初始化完成")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -36,7 +36,7 @@ def send_email(
|
||||
subject: str = "",
|
||||
html_content: str = "",
|
||||
) -> None:
|
||||
assert settings.emails_enabled, "no provided configuration for email variables"
|
||||
assert settings.emails_enabled, "未提供邮件相关配置变量"
|
||||
message = emails.Message(
|
||||
subject=subject,
|
||||
html=html_content,
|
||||
|
||||
@@ -3,11 +3,11 @@
|
||||
set -e
|
||||
set -x
|
||||
|
||||
# Let the DB start
|
||||
# 等待数据库启动
|
||||
python app/backend_pre_start.py
|
||||
|
||||
# Run migrations
|
||||
# 运行数据库迁移
|
||||
alembic upgrade head
|
||||
|
||||
# Create initial data in DB
|
||||
# 在数据库中创建初始数据
|
||||
python app/initial_data.py
|
||||
|
||||
Reference in New Issue
Block a user