Skip to content

28. FastAPI 项目结构 ​

项目结构要表达职责和依赖方向 ​

第 27 篇按业务分组,本篇决定项目级文件放哪里。目录的目标是让新人能快速找到入口、配置、业务规则和测试,而不是追求层数。

推荐的中小型结构 ​

text
fastapi-project/
├── app/
│   ├── __init__.py
│   ├── main.py
│   ├── api/
│   │   ├── __init__.py
│   │   ├── router.py
│   │   ├── deps.py
│   │   └── users.py
│   ├── core/
│   │   ├── __init__.py
│   │   ├── config.py
│   │   └── security.py
│   ├── db/
│   │   ├── __init__.py
│   │   ├── session.py
│   │   └── models.py
│   ├── middleware/
│   │   ├── __init__.py
│   │   └── request_context.py
│   ├── schemas/
│   │   ├── __init__.py
│   │   └── user.py
│   ├── services/
│   │   ├── __init__.py
│   │   └── user_service.py
│   └── utils/
│       ├── __init__.py
│       └── text.py
├── tests/
├── migrations/
├── scripts/
├── .env.example
├── .gitignore
├── pyproject.toml
├── uv.lock
└── README.md

这套结构的核心不是“文件夹越多越好”,而是把因不同原因变化的代码分开:URL 和 HTTP 参数变化时找 api,全局请求响应处理变化时找 middleware,路由拦截规则变化时找 api/deps.py,业务规则变化时找 services,数据库结构变化时找 db 和 migrations,接口输入输出变化时找 schemas,环境配置变化时找 core/config.py。utils 只收纳难以归入具体职责、却又确实通用的工具代码,不是默认落脚点。

它是一种适合中小型项目的参考结构,不是 FastAPI 强制规定的模板。下面逐项说明每个目录和文件的职责。

项目根目录与 app:划分项目边界 ​

路径干什么用为什么这样设计
fastapi-project/整个项目的根目录,包含应用代码、测试、迁移、依赖声明和项目说明。安装依赖、启动应用、执行测试通常都在这里进行。固定工作目录后,.env、数据库相对路径和 app.main 等导入路径都有统一基准,减少“在某个目录能运行,换个目录就失败”的问题。
app/存放应用运行时需要的 Python 代码,包括入口、路由、业务逻辑、数据库模型和配置。把应用代码与测试、脚本、项目配置分开,代码边界更清楚,也便于使用 from app.xxx import ... 做明确的包导入。
app/__init__.py将 app 标记为 Python 包,使 app.main、app.api.users 等模块可以按包路径导入。学习阶段通常可以为空。让包边界明确。不要在应用导入时就自动创建应用、连接数据库或注册大量模块,否则一个简单的 import app 也可能产生副作用和循环导入。
app/main.py应用组装入口:创建 FastAPI 实例或应用工厂,注册总路由、中间件、异常处理器和 lifespan,最终暴露 app 给 Uvicorn。入口只负责“把各部件装起来”,不应堆满用户注册、订单计算和 SQL。这样启动方式统一,业务修改也不会不断改动入口文件。

app/main.py 中常见的内容可以概括为:

python
from fastapi import FastAPI
from app.api.router import router

app = FastAPI(title="学习项目")
app.include_router(router, prefix="/api/v1")

这里只展示组装关系。随着项目增长,可以再加入中间件和 lifespan,但不需要把每个业务路由都直接写到 main.py。

app/api/:只处理 HTTP 接口层 ​

路径干什么用为什么这样设计
app/api/存放所有 HTTP 接口相关代码,包括路径、请求方法、参数声明、状态码、响应模型和路由依赖。它是外部请求进入应用的边界。把 HTTP 细节集中在这里,业务层就不必依赖 Request、Response 等 Web 概念。
app/api/__init__.py将 api 标记为包,通常保持为空或只做很少的公开导出。仅仅导入这个包不应该自动注册路由。路由是否生效应由 include_router 显式决定,避免隐藏副作用。
app/api/router.py汇总各业务路由,例如把 users.py、products.py 中的 router 注册到一个总 router 上。main.py 只需注册总路由,不必了解每个业务文件。新增业务时主要修改路由聚合处,入口保持稳定。
app/api/deps.py存放面向 HTTP 的共享依赖,例如获取当前用户、校验租户请求头或要求管理员身份。FastAPI 常用 Depends 承担路由“拦截器”的职责。依赖与路由分开后,可以按总路由、业务路由或单个接口复用,覆盖范围也更明确。
app/api/users.py定义用户相关接口,例如创建用户、查询用户、修改资料。它声明输入 Schema、输出 Schema 和所需依赖,然后调用用户 Service。路由负责“接收 HTTP 请求并返回 HTTP 响应”,Service 负责“业务上应该怎样做”。这样 URL 调整不会带动业务规则重写,业务规则也能脱离 HTTP 单独测试。

例如,users.py 适合出现 APIRouter、@router.post()、Depends、response_model;不适合出现大段密码校验规则、复杂事务流程或散落的 SQL。业务继续增多时,可以新增 products.py、orders.py,而不是把所有接口塞回 users.py 或 main.py。

router.py 与 users.py 的职责也不同:前者是路由目录和装配点,后者是某个业务的具体接口实现。总前缀、业务前缀和具体操作路径会逐层拼接,不要重复书写。

app/core/:放应用级基础设施,不放业务杂物 ​

路径干什么用为什么这样设计
app/core/存放配置、安全算法等应用级、较低层的基础设施代码。这里的 core 不是“所有核心代码都往里放”的意思。多个模块都可能使用配置和安全工具。让它们保持独立,可以避免基础设施反过来依赖路由或业务服务。
app/core/__init__.py将 core 标记为包,通常保持为空。避免导入配置包时顺带初始化认证、数据库等无关功能。
app/core/config.py使用 pydantic-settings 定义和读取环境变量,例如应用名、调试开关、数据库地址,并提供统一的 get_settings()。配置只有一个权威入口,便于类型检查、校验和区分开发、测试、生产环境;业务代码不再到处调用 os.getenv() 或硬编码连接信息。
app/core/security.py放通用的安全操作,例如密码哈希与校验、令牌签名与验证。这些是与具体 HTTP 路由无关的通用能力,可以被登录、改密码等多个业务复用。

要注意,security.py 不等于“所有权限代码”。密码怎样哈希属于通用安全能力;“用户是否有权查看这笔订单”包含业务规则,更适合放在对应业务 Service 或明确的授权模块中。core 也不应逐渐变成塞满无关函数的 utils.py。

app/db/:管理数据库连接与持久化模型 ​

路径干什么用为什么这样设计
app/db/集中存放 SQLAlchemy Engine、Session 工厂、数据库依赖和 ORM 模型。数据库资源有明确归属,路由和业务代码不必各自创建连接,也不会把连接参数散落到项目中。
app/db/__init__.py将 db 标记为包,通常保持为空或只暴露少量稳定接口。不建议在这里导入所有模型并自动建表,否则导入顺序更难控制,也容易产生副作用。
app/db/session.py创建 Engine 和 SessionLocal,并提供 get_db() 之类的 FastAPI 依赖,在请求结束时关闭 Session。Engine 管理连接池,通常按进程复用;Session 表示一次工作单元,应按请求创建和关闭。两者生命周期不同,不能都做成随意共享的全局对象。
app/db/models.py定义 ORM 模型,例如 User 对应的表名、列、默认值、索引和关系。模型面向数据库,而不是面向前端。表结构集中定义后,查询、迁移和关系维护都有统一依据。

session.py 中不要每次请求都重新创建 Engine,这会破坏连接池复用;也不要创建一个全局 Session 给所有请求共享,因为并发请求会相互污染事务状态。

models.py 里的字段变化不会自动修改已有数据库。模型说明“代码期望表是什么样”,migrations/ 中的迁移则描述“现有数据库怎样逐步变成那个样子”。模型较多后,再把单文件拆成 models/user.py、models/order.py 等文件,不必第一天就创建大量空文件。

app/middleware/:放中间件实现,在 main.py 注册 ​

中间件适合处理围绕整个 HTTP 请求响应过程的公共逻辑,例如请求编号、访问日志、耗时统计和统一响应头。项目结构中要把实现位置与注册位置分开:自研中间件放在 app/middleware/,应用创建时在 app/main.py 显式注册。

路径干什么用为什么这样设计
app/middleware/存放自研中间件,按职责拆成请求上下文、访问日志等模块。中间件不再堆进入口文件,又能与业务路由和 Service 保持边界。
app/middleware/__init__.py将 middleware 标记为包,通常保持为空。不要在导入包时自动注册中间件或导入 main.py,否则容易产生副作用和循环导入。
app/middleware/request_context.py创建请求编号、写入 request.state,并给响应补充对应响应头。请求上下文是一个独立横切职责,有明确文件可供中间件注册和测试使用。

例如,app/middleware/request_context.py 可以只保存中间件函数:

python
from uuid import uuid4
from fastapi import Request

async def request_context(request: Request, call_next):
    request_id = uuid4().hex
    request.state.request_id = request_id
    response = await call_next(request)
    response.headers["X-Request-ID"] = request_id
    return response

然后在 app/main.py 中注册:

python
from fastapi import FastAPI
from app.api.router import router
from app.middleware.request_context import request_context

app = FastAPI(title="学习项目")
app.middleware("http")(request_context)
app.include_router(router, prefix="/api/v1")

app.middleware("http")(request_context) 是把已有函数交给装饰器注册,效果等同于在函数定义处使用 @app.middleware("http")。这样中间件模块不需要反向导入 main.py 中的应用对象;仅仅导入中间件文件也不会让它自动生效。

CORSMiddleware、GZipMiddleware 等现成中间件通常直接在 main.py 注册即可,不必为了“每种代码都有一个文件”再包装一层。自研中间件很少时,也可以先使用单个 app/middleware.py,增多后再拆成同名包。多层中间件的执行顺序、请求体处理等机制见第 25 篇,本篇只约束代码落点。

“拦截器”写在哪里:FastAPI 通常用依赖实现 ​

FastAPI 没有名为 Interceptor 的统一内置组件。从其他框架迁移“拦截器”时,不要一律新建 interceptors/ 目录,而要先判断它拦截的范围和目的:

要解决的问题推荐机制代码位置
所有 HTTP 请求都要增加请求编号、日志或公共响应头中间件app/middleware/,在 main.py 注册
一组路由或某个接口需要登录、租户检查、管理员身份Depends 依赖共享依赖放 app/api/deps.py
校验令牌、哈希密码等具体算法普通函数app/core/security.py
当前用户能否取消这笔订单、能否执行某个业务动作业务规则对应 Service 或明确的授权模块

例如,共享的路由依赖放在 app/api/deps.py:

python
from typing import Annotated
from fastapi import Header, HTTPException

def require_tenant(
    x_tenant_id: Annotated[str | None, Header()] = None,
) -> str:
    if not x_tenant_id:
        raise HTTPException(status_code=400, detail="缺少 X-Tenant-ID")
    return x_tenant_id

如果整个业务总路由都需要这项检查,在 app/api/router.py 挂载:

python
from fastapi import APIRouter, Depends
from app.api.deps import require_tenant
from app.api.users import router as users_router

router = APIRouter(dependencies=[Depends(require_tenant)])
router.include_router(users_router, prefix="/users")

只需要拦截一个接口时,把 Depends(require_tenant) 声明在该路径操作上即可,不要改成全局中间件再维护一串排除路径。路由需要依赖返回的当前用户或租户值时,应把它声明为参数依赖,而不是只放进 dependencies=[...]。只被单个业务使用的依赖也可以留在该业务路由附近,deps.py 主要承载跨路由共享的 HTTP 依赖;get_db() 仍可留在 db/session.py,不必为了集中而全部搬走。

app/schemas/:定义对外输入输出契约 ​

路径干什么用为什么这样设计
app/schemas/存放 Pydantic Schema,描述接口接收什么数据、返回什么数据,以及校验和序列化规则。API 契约与数据库表结构是两种不同职责。分开后,接口字段可以独立演进,也能避免把数据库内部字段直接暴露出去。
app/schemas/__init__.py将 schemas 标记为包,通常保持为空。让导入路径明确,同时避免导入一个 Schema 时连带加载大量无关模块。
app/schemas/user.py定义用户相关 Schema,例如 UserCreate、UserRead、UserUpdate,分别服务于创建、响应和更新。不同操作所需字段并不相同,拆分后能精确控制客户端可以提交什么、服务端允许返回什么。

不要把 ORM 的 User 模型直接当成所有接口的输入输出。例如:

  • 数据库模型可能包含 password_hash,但 UserRead 不应返回它。
  • 创建用户时需要客户端提交明文密码,但响应中不能原样返回密码。
  • id、created_at 可以由服务端或数据库生成,不应因为模型里有这些列,就要求客户端创建时提交。
  • 更新资料时往往只允许修改部分字段,不能直接把客户端提交的所有键无筛选地写到模型上。

因此,Model 主要回答“数据怎样存储”,Schema 主要回答“外部怎样传入和看到数据”。FastAPI 的 response_model 还可以对响应字段做最后一道筛选。

app/services/:承载业务规则与用例流程 ​

路径干什么用为什么这样设计
app/services/存放注册、下单、取消订单等业务用例,以及重复数据检查、状态流转、事务编排等业务规则。业务规则不绑定某个 URL 后,可以被 HTTP 接口、命令行脚本或测试复用,也更容易定位规则修改的位置。
app/services/__init__.py将 services 标记为包,通常保持为空。避免导入 Service 包时自动执行其他业务逻辑。
app/services/user_service.py实现用户相关业务,例如检查邮箱是否已存在、调用密码哈希函数、创建用户对象,并在一个明确的业务边界内提交事务。用户路由保持简洁,复杂规则有独立位置;测试可以直接构造 Session 调用 Service,不必每次模拟完整 HTTP 请求。

Service 通常接收已经构造好的参数和当前请求的 Session,而不是自己读取全局 Session,也不应直接返回 JSONResponse。它关注的是“业务是否允许、需要完成哪些写入”,HTTP 状态码和响应格式仍由 API 层决定。

不过,Service 也不是为了分层而做的机械转发。如果项目只有一个简单查询,额外包一层只执行同一行查询的函数未必有价值。出现重复规则、多个写入步骤、多个调用入口或独立测试需求时,Service 边界的收益会更明显。

app/utils/:只放真正通用的工具代码 ​

Python 中所谓“工具类”通常不需要写成只有静态方法的 StringUtils、DateUtils 类。优先使用按职责命名的模块和普通函数,例如 app/utils/text.py;只有确实需要封装状态和统一接口时才定义类,而且要先判断它是否已有更明确的归属。

路径干什么用为什么这样设计
app/utils/存放不依赖具体路由、数据库和业务规则的通用辅助代码。给真正可复用且无明确业务归属的函数一个位置,同时避免它们散落复制。
app/utils/__init__.py将 utils 标记为包,通常保持为空。不要在导入包时把所有工具互相导入,否则容易形成循环依赖和隐藏副作用。
app/utils/text.py放通用文本处理函数,例如折叠连续空白,而不是用户昵称的完整业务校验。按能力命名文件比不断扩充一个 utils.py 更容易查找,也能减少杂物式堆积。

例如,app/utils/text.py 可以保持很简单:

python
def collapse_whitespace(value: str) -> str:
    return " ".join(value.split())

不要因为“多个地方都会调用”就把代码一律放进 utils。调用次数多不等于没有职责归属:

代码更合适的位置
密码哈希、令牌签名与验证app/core/security.py
获取当前用户、校验请求头等 HTTP 依赖app/api/deps.py
请求编号、访问日志等请求响应包装app/middleware/
邮箱查重、注册流程、订单金额和状态规则对应的 app/services/ 模块
Engine、Session 和 ORM 查询相关辅助代码app/db/ 或明确的数据访问模块

只在用户模块中使用的小函数,可以先留在用户模块附近,出现真实复用需求后再提取。工具模块一旦需要依赖 Request、某个 ORM 模型或具体 Service,通常说明它已经不是通用工具,应按实际职责重新定位。也不要同时维护 utils/、helpers/、common/ 等多个含义相近的杂物目录。

tests/、migrations/、scripts/:三种非请求代码分开管理 ​

路径干什么用为什么这样设计
tests/存放接口测试、Service 测试以及 pytest 配置和夹具,例如 test_users.py、conftest.py。测试可以使用独立配置和独立数据库。测试验证应用行为,但不属于线上请求处理代码。独立目录便于统一发现和执行,也能防止测试工具混入业务模块。
migrations/存放 Alembic 等迁移工具生成的环境配置和版本脚本,记录建表、加列、改索引等数据库结构变化。数据库结构需要按版本、可审查地应用到不同环境,不能只依赖启动时 create_all。迁移文件通常进入版本控制,并与对应模型变更一起审核。
scripts/存放需要人工明确执行的一次性或运维脚本,例如历史数据导入、数据修复、初始化管理员。这些任务与 Web 应用生命周期不同。独立存放可以避免它们在 import app、启动 worker 或处理普通请求时被意外执行。

三者不能混用:测试不应成为正式迁移手段,迁移不应夹带不可重复的大数据修复,临时脚本也不应通过复制一份业务代码来绕过 Service。脚本需要业务能力时,应尽量复用现有 Service 和数据库配置。

根目录文件:环境、依赖与协作约定 ​

路径干什么用为什么这样设计
.env.example列出需要配置的环境变量名和无敏感示例值,例如数据库主机、端口、应用环境。该文件可以提交到版本控制。新人能据此创建自己的 .env,又不会在示例中泄露真实密码、私钥或令牌。真正的 .env 保存本机或部署环境配置,不应提交。
.gitignore排除虚拟环境、Python 缓存、真实 .env、测试缓存、本地上传文件等不应共享的内容。密钥不应进入仓库,自动生成或本机特有的文件也不应制造无意义变更。依赖锁文件不属于应忽略的临时产物。
pyproject.toml声明项目元数据、直接依赖、Python 版本要求,也可集中放置 pytest 等工具配置。它表达“项目直接需要什么”,使用统一标准文件后,安装、构建和工具配置都有稳定入口。
uv.lock由 uv 生成,锁定直接依赖和间接依赖的解析结果及版本。通常不手工编辑,并随应用项目提交。让开发、测试和部署能够复现同一套依赖解析结果,降低“我机器上可以运行”的差异。更新依赖时通过 uv 命令重新生成。
README.md说明项目用途、环境要求、安装步骤、变量配置、启动命令、测试和迁移方式。它是协作者的项目入口。常用操作有统一说明,就不必靠口头传递,也不应把真实环境密钥写进去。

pyproject.toml 与 uv.lock 不是二选一:前者主要记录项目声明,后者记录本次完整解析结果。.env.example 与 .env 也不是重复文件:前者是可公开的模板,后者是具体环境的实际配置。

用一次“创建用户”请求串起所有层 ​

以 POST /api/v1/users 为例,各文件大致这样协作:

  1. Uvicorn 从 app/main.py 找到 app,应用已经注册中间件和 app/api/router.py 中的总路由。请求先经过 request_context,获得本次请求编号。
  2. 总路由找到匹配的路径操作,并按挂载范围执行 app/api/deps.py 中的依赖,再把请求交给 app/api/users.py。
  3. 路由使用 app/schemas/user.py 的 UserCreate 校验请求体,并通过 app/db/session.py 的依赖获得本次请求的 Session。
  4. 路由调用 app/services/user_service.py。Service 检查用户名或邮箱是否重复,调用 app/core/security.py 对密码做哈希。
  5. Service 使用 app/db/models.py 中的 User 模型组织要保存的数据,通过 Session 完成事务写入。
  6. 路由取得业务结果,使用 UserRead 作为 response_model,只返回允许公开的字段,不包含密码或密码哈希。响应返回阶段再经过中间件,补充 X-Request-ID。
  7. 数据库依赖按自身生命周期关闭本次 Session,连接按连接池规则归还或管理。

这条链路可以概括为:

text
HTTP 请求
  → middleware/request_context.py 建立请求上下文
  → api/deps.py                  按挂载范围执行路由依赖
  → api/users.py                 解析 HTTP 输入、选择用例
  → services/user_service.py     执行业务规则和事务
  → db/models.py + Session       映射并保存数据
  → 数据库

数据库结果
  → Service 返回业务结果
  → schemas/user.py              筛选并序列化公开响应
  → middleware/request_context.py 处理返回阶段公共逻辑
  → HTTP 响应

这样拆分后,修改通常有明确落点:改 URL 找 API,改全局请求上下文找 Middleware,改路由拦截规则找 Dependency,改重复邮箱规则找 Service,改响应字段找 Schema,改表字段找 Model 并新增迁移,改数据库连接参数找配置和 Session。不是每次变化都要修改所有层,例如只调整内部业务规则时,公开 Schema 可能完全不变;utils 也只有被某层实际调用时才进入链路,不是请求必须经过的一层。

依赖方向为什么重要 ​

请求流向与代码依赖有关,但不是简单地把所有文件互相导入。这个结构建议遵守以下方向:

  • main.py 是组装入口,可以导入 API 总路由、中间件和必要的应用级配置。
  • 中间件由 main.py 导入和注册,不反向导入 main.py,也不依赖具体路由函数或业务用例。
  • API 层可以导入 Schema、Service,以及 db.session 提供的请求依赖。
  • api/deps.py 可以使用 core.security、Service 或数据库依赖,但不要导入挂载它的 router.py,避免形成 router → deps → router 循环。
  • Service 可以使用 ORM Model、传入的 Session、core.security 和真正通用的工具函数,不依赖具体路由函数。
  • db、schemas、core、utils 等较低层模块不反向导入 API、Service 或中间件;数据库连接代码可以读取配置,但配置模块不应导入业务路由。
  • 另一个业务需要用户能力时,调用用户 Service 或明确的领域接口,不要调用用户的 HTTP 路由函数。

这些约束是为了避免 models → router → models 之类的循环,也让替换 HTTP 入口或测试业务逻辑时不必启动整个 Web 应用。__init__.py 中大量互相导入,往往会重新制造已经通过目录拆分避免的循环依赖。

目录什么时候应该调整 ​

结构应该随真实复杂度演进,而不是永远不变:

  • 很小的练习项目只有几个接口时,可以先使用少量文件,不必创建一整套空目录;没有实际中间件或通用工具时,也不必预先创建 middleware/、utils/。
  • models.py、Schema 或 Service 单文件过大时,可以拆成同名包下的多个业务文件。
  • 用户、商品、订单各自包含完整路由、模型和业务规则时,可以改为按业务包组织,例如把用户的 router、schemas、service、models 放在一个用户包内。
  • 按业务包组织后,不必再维护一套内容重复的全局 schemas/、services/;共享基础设施仍可保留在 core/、db/。
  • 目录模块化不等于必须拆微服务。只有进程边界、独立部署等需求真实存在时,才需要承担网络调用和分布式事务等成本。

判断结构是否合适,可以看两个实际问题:新人能否快速找到某段职责,修改一个常见需求时是否需要无规律地跨很多文件。如果答案是否定的,就应调整结构,而不是继续增加目录层数。

配置集中管理 ​

安装 uv add pydantic-settings,app/core/config.py:

python
from functools import lru_cache
from pydantic_settings import BaseSettings, SettingsConfigDict

class Settings(BaseSettings):
    model_config = SettingsConfigDict(
        env_file=".env", env_file_encoding="utf-8", extra="ignore",
    )
    app_name: str = "学习项目"
    debug: bool = False
    database_url: str = "sqlite:///./study.db"

@lru_cache
def get_settings():
    return Settings()

BaseSettings 负责把环境变量转换成声明的类型并校验,get_settings() 则给其他模块提供统一入口。@lru_cache 让同一进程复用一个 Settings 实例,避免每次访问都重新读取和解析 .env;如果测试在运行中修改了环境变量,要注意已有缓存不会自动变化,应清理缓存或为测试构造独立配置。

这里 SQLite 只是结构示例的默认值,不替代第 20 篇 MySQL 配置。.env 路径相对于工作目录;统一从项目根目录启动。真实密钥使用必填字段或外部密钥系统,不提供可误用于生产的固定默认密钥。

生命周期管理共享资源 ​

共享 HTTP 客户端、连接池适合在应用 lifespan 初始化并在退出时关闭。每请求数据库 Session 则由依赖管理,不能把一个 Session 放到 app.state 让所有请求共享。

迁移脚本放 migrations,离线导入放 scripts。不要在多 worker 的模块导入时自动跑迁移或百万条初始化,多个进程可能同时执行。

版本控制与运行约定 ​

.gitignore 至少忽略 .venv/、__pycache__/、.env、临时上传和测试缓存。.env.example 只列变量名与无敏感示例;依赖声明和锁文件进入版本控制。

启动入口统一为 uv run uvicorn app.main:app --reload。测试、迁移、部署也在项目根目录执行,减少随意修改 sys.path 带来的隐性依赖。

练习:画出各目录依赖方向,检查是否出现 models → router → models 或 router → deps → router 循环;将一项配置从散落常量迁入 Settings;把入口文件中的请求编号中间件移到 app/middleware/ 并显式注册;分别判断“获取当前用户、密码哈希、邮箱查重、折叠空白”应放在 api/deps.py、core/security.py、Service 还是 utils/。