文章目录
项目介绍
使用python3.12+fastapi实现一个api框架
项目目录结构
├── app
│ ├── api
├── core
│ └── exception.py
│ └── middleware.py
│ └── register.py
├── logs
├── utils
│ └── common.py
│ └── response.py
├── .env
├── config.py
├── main.py
└── requirements.txt
编写 main.py
import logging
from fastapi import FastAPI
from core.register import (
register_exception,
register_middleware,
register_router,
)
# 关闭 Uvicorn HTTP 请求日志记录
uvicorn_access_logger = logging.getLogger("uvicorn.access")
uvicorn_access_logger.handlers = []
uvicorn_access_logger.propagate = False
# uvicorn 主日志处理器
uvicorn_logger = logging.getLogger("uvicorn")
def create_app() -> FastAPI:
# 创建fastapi对象
_app = FastAPI()
# 全局异常捕捉处理
register_exception(_app)
# 注册中间件
register_middleware(_app)
# 引入应用中的路由
register_router(_app)
# 控制台日志
uvicorn_logger.info(f"服务已启动,共加载了 {len(_app.routes)} 条路由!")
return _app
app = create_app()
app目录
api.py(api示例)
from fastapi import APIRouter
from config import settings
from utils.response import RestfulResponse
router = APIRouter(prefix="/api", tags=["接口服务"])
@router.post("/test", summary="test")
async def test():
return RestfulResponse.success()
core目录
exception.py
from fastapi.responses import JSONResponse
from starlette.exceptions import HTTPException
from fastapi.exceptions import RequestValidationError
from fastapi import status as fastapi_status
from fastapi import Request, FastAPI
from fastapi.encoders import jsonable_encoder
from typing import Optional, Dict, Any, Union
from loguru import logger
from utils import common
class CustomException(Exception):
"""
自定义异常类,用于统一处理业务异常
"""
def __init__(
self,
message: str,
*,
errcode: int = 400,
code: int = 400,
error: int = 400,
status_code: int = fastapi_status.HTTP_200_OK,
desc: Optional[str] = None,
):
"""
自定义异常初始化
:param message: 错误提示信息
:param errcode: 错误码
:param code: 兼容旧版错误码
:param error: 兼容旧版错误码
:param status_code: HTTP状态码
:param desc: 详细描述信息(仅日志记录,不返回给前端)
"""
self.message = message
self.status_code = status_code
# 如果状态码不是200,则使用状态码作为错误码
if self.status_code != fastapi_status.HTTP_200_OK:
self.errcode = self.status_code
self.code = self.status_code
self.error = self.status_code
else:
self.errcode = errcode
self.code = code
self.error = error
self.desc = desc
super().__init__(self.message)
def create_error_response(status_code: int, message: str, errcode: int) -> JSONResponse:
"""
创建统一的错误响应
"""
return JSONResponse(
status_code=status_code,
content={
"message": message,
"errcode": errcode,
"code": errcode,
"error": errcode
}
)
def refactoring_exception(app: FastAPI):
"""
注册全局异常处理器
"""
@app.exception_handler(CustomException)
async def custom_exception_handler(request: Request, exc: CustomException):
"""
处理自定义业务异常
"""
if exc.desc:
logger.warning(f"业务异常: {exc.message}, 详情: {exc.desc}")
return JSONResponse(
status_code=exc.status_code,
content={"message": exc.message, "errcode": exc.errcode, "code": exc.code, "error": exc.error},
)
@app.exception_handler(HTTPException)
async def http_exception_handler(request: Request, exc: HTTPException):
"""
处理HTTP异常
"""
# logger.warning(f"HTTP异常: {exc.detail}, 状态码: {exc.status_code}")
return create_error_response(
status_code=exc.status_code,
message=exc.detail,
errcode=exc.status_code
)
@app.exception_handler(RequestValidationError)
async def validation_exception_handler(request: Request, exc: RequestValidationError):
"""
处理请求参数验证异常
"""
# 获取错误信息
errors = exc.errors()
if not errors:
error_msg = "请求参数验证失败"
missing_field = "未知字段"
else:
first_error = errors[0]
loc = first_error.get("loc", [])
missing_field = loc[-1] if loc else "未知字段"
error_msg = first_error.get("msg", "参数验证错误")
# 错误类型映射关系
error_types = {
"Field required": f"请求错误,缺少必填参数:{missing_field}",
"Input should be a valid string": f"参数:{missing_field},输入类型错误,应该为字符串!",
"Input should be a valid integer": f"参数:{missing_field},输入类型错误,应该为整数!",
"JSON decode error": "body参数不是一个标准的json格式"
}
for error_type, message in error_types.items():
if common.is_string_contained(error_msg, error_type):
error_msg = message
break
return JSONResponse(
status_code=fastapi_status.HTTP_200_OK,
content=jsonable_encoder({
"errcode": 400,
"code": 400,
"error": 400,
"message": error_msg
}),
)
@app.exception_handler(ValueError)
async def value_exception_handler(request: Request, exc: ValueError):
"""
处理值异常
"""
error_message = str(exc)
logger.error(f"值异常: {error_message}")
return create_error_response(
status_code=fastapi_status.HTTP_200_OK,
message=error_message,
errcode=400
)
@app.exception_handler(Exception)
async def all_exception_handler(request: Request, exc: Exception):
"""
处理所有未捕获的异常
"""
error_message = str(exc)
logger.exception(f"未捕获异常: {error_message}")
return create_error_response(
status_code=fastapi_status.HTTP_500_INTERNAL_SERVER_ERROR,
message="服务器错误!",
errcode=500
)
middleware.py
import sys
import time
import json
from pathlib import Path
from loguru import logger
from fastapi import FastAPI, Request
from starlette.concurrency import iterate_in_threadpool
from config import settings
# 记录请求日志中间件
def register_request_log_middleware(app: FastAPI):
# 禁用全局 logger 的默认处理器
logger.remove()
# 添加控制台输出
if settings.system.LOG_CONSOLE_OUT:
logger.add(sys.stdout, level="DEBUG")
# 配置日志输出到文件
log_path = Path(settings.BASE_PATH) / "logs" / "api_info_{time:YYYY_MM_DD}.log"
logger.add(
log_path,
format="{time:YYYY-MM-DD HH:mm:ss.SSS} | {level} | {message}",
level="INFO",
enqueue=True,
rotation="00:00",
retention="7 days",
mode="a",
backtrace=True,
diagnose=True
)
@app.middleware("http")
async def request_log_middleware(request: Request, call_next):
start_time = time.time()
# 获取请求数据,优化判断逻辑
content_type = request.headers.get("content-type", "")
# 获取URL查询参数
query_params = dict(request.query_params)
# 获取请求体数据
if "multipart/form-data" in content_type:
request_body_data = {}
else:
try:
request_body = await request.body()
request_body_data = json.loads(request_body.decode("utf-8")) if request_body else None
except Exception:
request_body_data = None
# 合并请求数据
request_data = {
"query_params": query_params,
"body": request_body_data
}
# 调用下一个中间件或路由
try:
response = await call_next(request)
# 计算处理时间
process_time = time.time() - start_time
# 获取响应数据
response_body = b""
async for chunk in response.body_iterator:
response_body += chunk
# 解析返回值为纯json字符串
try:
response_text = json.loads(response_body.decode("utf-8"))
except (json.JSONDecodeError, UnicodeDecodeError):
response_text = str(response_body)
# 记录日志
log_data = {
"method": request.method,
"path": request.url.path,
"status_code": response.status_code,
"request_data": request_data,
"response_body": response_text,
"time": f"{process_time:.2f}s"
}
logger.info(json.dumps(log_data))
# 重新设置响应体,确保其能正确返回
response.body_iterator = iterate_in_threadpool([response_body])
return response
except Exception as e:
process_time = time.time() - start_time
logger.error(f"请求处理异常: {str(e)}, 路径: {request.url.path}, 完整URL: {request.url}, 耗时: {process_time:.2f}s")
raise
register.py
import os
import sys
import importlib
from typing import List, Optional
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from loguru import logger
from config import settings
from utils import common
from core.exception import refactoring_exception
def register_middleware(app: FastAPI) -> None:
"""
注册中间件
Args:
app: FastAPI应用实例
"""
# 中间件列表
middlewares: List[Optional[str]] = [
"core.middleware.register_request_log_middleware" if settings.system.REQUEST_LOG_RECORD else None,
]
# 过滤掉None值并导入中间件
valid_middlewares = [m for m in middlewares if m]
common.import_modules(valid_middlewares, "中间件", app=app)
# 注册CORS中间件
if settings.system.CORS_ORIGIN_ENABLE:
app.add_middleware(
CORSMiddleware,
allow_origins=settings.system.ALLOW_ORIGINS,
allow_credentials=settings.system.ALLOW_CREDENTIALS,
allow_methods=settings.system.ALLOW_METHODS,
allow_headers=settings.system.ALLOW_HEADERS,
)
def register_router(app: FastAPI) -> None:
"""
动态加载 api 目录下各个文件,注册路由
Args:
app: FastAPI应用实例
"""
sys.path.append(settings.router.ROUTERS_PATH)
# 路由主入口文件名(不含扩展名)
api_file = settings.router.ROUTERS_FILE.rsplit('.', 1)[0]
try:
# 导入 api.py 模块
module_views = importlib.import_module(api_file)
# 获取 router 属性
router = getattr(module_views, "router", None)
if router:
app.include_router(router)
else:
logger.warning(f"{api_file}.py 中没有 router 属性")
except ImportError as e:
logger.error(f"模块 {api_file}.py 导入失败: {str(e)}")
except Exception as e:
logger.error(f"路由加载失败: {str(e)}")
def register_exception(app: FastAPI) -> None:
"""
注册全局异常处理
Args:
app: FastAPI应用实例
"""
refactoring_exception(app)
utils
common.py
import importlib
from loguru import logger
def is_string_contained(full_string: str, substring: str) -> bool:
"""
判断一个字符串是否是另一个字符串的子串
:param full_string: 完整的字符串
:param substring: 需要查找的子串
:return:
"""
return substring in full_string
def import_modules(modules: list, desc: str, **kwargs):
"""
通过反射执行方法
:param modules:
:param desc:
:param kwargs:
:return:
"""
for module in modules:
if not module:
continue
try:
module_pag = importlib.import_module(module[0: module.rindex(".")])
getattr(module_pag, module[module.rindex(".") + 1:])(**kwargs)
except ModuleNotFoundError as e:
logger.error(f"AttributeError:导入{desc}失败,模块:{module},详细报错信息:{e}")
except AttributeError as e:
logger.error(f"ModuleNotFoundError:导入{desc}失败,模块方法:{module},详细报错信息:{e}")
response.py
from pydantic import BaseModel, Field
from typing import Generic, TypeVar
from fastapi import status as fastapi_status
from fastapi.responses import ORJSONResponse
DataT = TypeVar("DataT")
class Status:
HTTP_SUCCESS = 200 # OK 请求成功
HTTP_ERROR = 400 # BAD_REQUEST 因客户端错误的原因请求失败
HTTP_401 = 401 # UNAUTHORIZED: 未授权
HTTP_403 = 403 # FORBIDDEN: 禁止访问
HTTP_404 = 404 # NOT_FOUND: 未找到
HTTP_405 = 405 # METHOD_NOT_ALLOWED: 方法不允许
HTTP_408 = 408 # REQUEST_TIMEOUT: 请求超时
HTTP_500 = 500 # INTERNAL_SERVER_ERROR: 服务器内部错误
HTTP_502 = 502 # BAD_GATEWAY: 错误的网关
HTTP_503 = 503 # SERVICE_UNAVAILABLE: 服务不可用
class ResponseSchema(BaseModel, Generic[DataT]):
"""
默认响应模型
"""
errcode: int = Field(Status.HTTP_SUCCESS, description="响应状态码(响应体内)")
message: str = Field("success", description="响应结果描述")
data: DataT = Field(None, description="响应结果数据")
class PageResponseSchema(ResponseSchema):
"""
带有分页的响应模型
"""
total: int = Field(0, description="总数据量")
page: int = Field(1, description="当前页数")
limit: int = Field(10, description="每页多少条数据")
class ErrorResponseSchema(BaseModel, Generic[DataT]):
"""
默认请求失败响应模型
"""
errcode: int = Field(Status.HTTP_ERROR, description="响应状态码(响应体内)")
message: str = Field("请求失败,请联系管理员", description="响应结果描述")
data: DataT = Field(None, description="响应结果数据")
ResponseSchemaT = TypeVar("ResponseSchemaT", bound=ResponseSchema)
class RestfulResponse:
"""
响应体
"""
@staticmethod
def success(
message: str = "success",
*,
errcode: int = 0,
data: DataT = None,
status_code: int = fastapi_status.HTTP_200_OK,
**kwargs,
) -> ORJSONResponse:
"""
成功响应
:param message: 响应结果描述
:param errcode: 业务响应体状态码 0:成功
:param data: 响应结果数据
:param status_code: HTTP 响应状态码
:param kwargs: 额外参数
:return:
"""
content = ResponseSchema(errcode=errcode, message=message, data=data)
content = content.model_dump() | kwargs
return ORJSONResponse(content=content, status_code=status_code)
@staticmethod
def error(
message: str,
*,
errcode: int = Status.HTTP_ERROR,
data: DataT = None,
status_code: int = fastapi_status.HTTP_200_OK,
**kwargs,
) -> ORJSONResponse:
"""
失败响应
:param message: 响应结果描述
:param errcode: 业务响应体状态码
:param data: 响应结果数据
:param status_code: HTTP 响应状态码
:return:
"""
content = ErrorResponseSchema(errcode=errcode, message=message, data=data)
content = content.model_dump() | kwargs
return ORJSONResponse(content=content, status_code=status_code)
.env
# -----------------------------------------------
# system 配置项
# LOG_CONSOLE_OUT: 是否将日志打印在控制台
# REQUEST_LOG_RECORD: 是否将日志写入文件
# -----------------------------------------------
LOG_CONSOLE_OUT=False
REQUEST_LOG_RECORD=True
config.py
from pathlib import Path
from pydantic_settings import BaseSettings, PydanticBaseSettingsSource, SettingsConfigDict
# 项目根目录
_BASE_PATH: Path = Path(__file__).resolve().parent
class Settings(BaseSettings):
model_config = SettingsConfigDict(
env_file=str(_BASE_PATH / ".env"), env_file_encoding="utf-8", extra="ignore"
)
@classmethod
def settings_customise_sources(
cls,
settings_cls: type[BaseSettings],
init_settings: PydanticBaseSettingsSource,
env_settings: PydanticBaseSettingsSource,
dotenv_settings: PydanticBaseSettingsSource,
file_secret_settings: PydanticBaseSettingsSource,
) -> tuple[PydanticBaseSettingsSource, ...]:
return init_settings, env_settings, dotenv_settings
class SystemSettings(Settings):
# 是否启用跨域
CORS_ORIGIN_ENABLE: bool = True
# 只允许访问的域名列表,* 代表所有
ALLOW_ORIGINS: list[str] = ["*"]
# 是否支持携带 cookie
ALLOW_CREDENTIALS: bool = True
# 设置允许跨域的http方法,比如 get、post、put等。
ALLOW_METHODS: list[str] = ["*"]
# 允许携带的headers,可以用来鉴别来源等作用。
ALLOW_HEADERS: list[str] = ["*"]
# 是否将日志打印到控制台
LOG_CONSOLE_OUT: bool = True
# 是否将日志写入文件
REQUEST_LOG_RECORD: bool = False
# 飞书机器人配置
BOT_WEBHOOK: str
BOT_SECRET: str
BOT_TEMPLATE_ID: str
class RouterSettings(Settings):
"""
路由配置
主要为系统默认配置,一般情况下不涉及改变
"""
# 应用路由文件目录
ROUTERS_PATH: str = str(_BASE_PATH / "app")
# 路由的主入口文件
ROUTERS_FILE: str = "api.py"
class GlobalSettings(BaseSettings):
"""
全局统一配置入口
"""
BASE_PATH: Path = _BASE_PATH
# 系统基础配置
system: SystemSettings = SystemSettings()
# 系统路由
router: RouterSettings = RouterSettings()
settings = GlobalSettings()
requirements.txt
fastapi==0.115.6
gunicorn==23.0.0
uvicorn[standard]==0.34.0
pydantic==2.10.5
pydantic_core==2.27.2
pydantic-settings==2.7.1
python-dotenv==1.0.1
python-multipart==0.0.20
loguru==0.7.3
httpx==0.28.1
pycryptodome==3.21.0
numpy==2.3.3
requests==2.32.5
aiohttp==3.13.2
orjson==3.11.4
启动服务(windows)
uvicorn main:app --host 0.0.0.0 --port 10088
584

被折叠的 条评论
为什么被折叠?



