Skip to content

Commit 92df527

Browse files
authored
添加schema响应模式 (#48)
1 parent 859c198 commit 92df527

File tree

10 files changed

+132
-83
lines changed

10 files changed

+132
-83
lines changed

.pre-commit-config.yaml

+2-2
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ repos:
88
- id: check-toml
99

1010
- repo: https://github.com/charliermarsh/ruff-pre-commit
11-
rev: v0.8.2
11+
rev: v0.9.5
1212
hooks:
1313
- id: ruff
1414
args:
@@ -19,7 +19,7 @@ repos:
1919
- id: ruff-format
2020

2121
- repo: https://github.com/pdm-project/pdm
22-
rev: 2.21.0
22+
rev: 2.22.3
2323
hooks:
2424
- id: pdm-export
2525
args:

.ruff.toml

+3-3
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ select = [
1515
"RUF100",
1616
"I002",
1717
"F404",
18-
"TCH",
18+
"TC",
1919
"UP007"
2020
]
2121
preview = true
@@ -25,8 +25,8 @@ lines-between-types = 1
2525
order-by-type = true
2626

2727
[lint.per-file-ignores]
28-
"**/api/v1/*.py" = ["TCH"]
29-
"**/model/*.py" = ["TCH003"]
28+
"**/api/v1/*.py" = ["TC"]
29+
"**/model/*.py" = ["TC003"]
3030
"**/model/__init__.py" = ["F401"]
3131

3232
[format]

backend/app/admin/api/v1/auth/auth.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@
55

66
from backend.app.admin.service.auth_service import auth_service
77
from backend.common.security.jwt import DependsJwtAuth
8-
from backend.common.response.response_schema import response_base, ResponseModel
9-
from backend.app.admin.schema.token import GetSwaggerToken
8+
from backend.common.response.response_schema import response_base, ResponseModel, ResponseSchemaModel
9+
from backend.app.admin.schema.token import GetSwaggerToken, GetLoginToken
1010
from backend.app.admin.schema.user import Auth2
1111

1212
router = APIRouter()
@@ -19,7 +19,7 @@ async def swagger_login(form_data: OAuth2PasswordRequestForm = Depends()) -> Get
1919

2020

2121
@router.post('/login', summary='验证码登录')
22-
async def user_login(request: Request, obj: Auth2) -> ResponseModel:
22+
async def user_login(request: Request, obj: Auth2) -> ResponseSchemaModel[GetLoginToken]:
2323
data = await auth_service.login(request=request, obj=obj)
2424
return response_base.success(data=data)
2525

backend/app/admin/api/v1/auth/captcha.py

+9-8
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@
55
from fastapi_limiter.depends import RateLimiter
66
from starlette.concurrency import run_in_threadpool
77

8-
from backend.common.response.response_schema import ResponseModel, response_base
8+
from backend.app.admin.schema.captcha import GetCaptchaDetail
9+
from backend.common.response.response_schema import ResponseSchemaModel, response_base
910
from backend.core.conf import settings
1011
from backend.database.db import uuid4_str
1112
from backend.database.redis import redis_client
@@ -18,18 +19,18 @@
1819
summary='获取登录验证码',
1920
dependencies=[Depends(RateLimiter(times=5, seconds=10))],
2021
)
21-
async def get_captcha(request: Request) -> ResponseModel:
22+
async def get_captcha(request: Request) -> ResponseSchemaModel[GetCaptchaDetail]:
2223
"""
2324
此接口可能存在性能损耗,尽管是异步接口,但是验证码生成是IO密集型任务,使用线程池尽量减少性能损耗
2425
"""
2526
img_type: str = 'base64'
2627
img, code = await run_in_threadpool(img_captcha, img_byte=img_type)
2728
uuid = uuid4_str()
2829
request.app.state.captcha_uuid = uuid
29-
await redis_client.set(f'{settings.CAPTCHA_LOGIN_REDIS_PREFIX}:{uuid}', code, ex=settings.CAPTCHA_EXPIRATION_TIME)
30-
return response_base.success(
31-
data={
32-
'image_type': img_type,
33-
'image': img,
34-
}
30+
await redis_client.set(
31+
f'{settings.CAPTCHA_LOGIN_REDIS_PREFIX}:{uuid}',
32+
code,
33+
ex=settings.CAPTCHA_LOGIN_EXPIRE_SECONDS,
3534
)
35+
data = GetCaptchaDetail(image_type=img_type, image=img)
36+
return response_base.success(data=data)

backend/app/admin/api/v1/user.py

+13-6
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@
55
from fastapi import APIRouter, Query
66

77
from backend.common.security.jwt import CurrentUser, DependsJwtAuth
8-
from backend.common.pagination import paging_data, DependsPagination
9-
from backend.common.response.response_schema import response_base, ResponseModel
8+
from backend.common.pagination import paging_data, DependsPagination, PageData
9+
from backend.common.response.response_schema import response_base, ResponseModel, ResponseSchemaModel
1010
from backend.database.db import CurrentSession
1111
from backend.app.admin.schema.user import CreateUser, GetUserInfo, ResetPassword, UpdateUser, Avatar
1212
from backend.app.admin.service.user_service import UserService
@@ -30,7 +30,7 @@ async def password_reset(obj: ResetPassword) -> ResponseModel:
3030

3131

3232
@router.get('/{username}', summary='查看用户信息', dependencies=[DependsJwtAuth])
33-
async def get_user(username: str) -> ResponseModel:
33+
async def get_user(username: str) -> ResponseSchemaModel[GetUserInfo]:
3434
current_user = await UserService.get_userinfo(username=username)
3535
data = GetUserInfo(**select_as_dict(current_user))
3636
return response_base.success(data=data)
@@ -52,15 +52,22 @@ async def update_avatar(username: str, avatar: Avatar) -> ResponseModel:
5252
return response_base.fail()
5353

5454

55-
@router.get('', summary='(模糊条件)分页获取所有用户', dependencies=[DependsJwtAuth, DependsPagination])
55+
@router.get(
56+
'',
57+
summary='(模糊条件)分页获取所有用户',
58+
dependencies=[
59+
DependsJwtAuth,
60+
DependsPagination,
61+
],
62+
)
5663
async def get_all_users(
5764
db: CurrentSession,
5865
username: Annotated[str | None, Query()] = None,
5966
phone: Annotated[str | None, Query()] = None,
6067
status: Annotated[int | None, Query()] = None,
61-
) -> ResponseModel:
68+
) -> ResponseSchemaModel[PageData[GetUserInfo]]:
6269
user_select = await UserService.get_select(username=username, phone=phone, status=status)
63-
page_data = await paging_data(db, user_select, GetUserInfo)
70+
page_data = await paging_data(db, user_select)
6471
return response_base.success(data=page_data)
6572

6673

backend/app/admin/schema/captcha.py

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
#!/usr/bin/env python3
2+
# -*- coding: utf-8 -*-
3+
from pydantic import Field
4+
5+
from backend.common.schema import SchemaBase
6+
7+
8+
class GetCaptchaDetail(SchemaBase):
9+
image_type: str = Field(description='图片类型')
10+
image: str = Field(description='图片内容')

backend/common/pagination.py

+66-31
Original file line numberDiff line numberDiff line change
@@ -2,27 +2,25 @@
22
# -*- coding: utf-8 -*-
33
from __future__ import annotations
44

5-
import math
6-
5+
from math import ceil
76
from typing import TYPE_CHECKING, Generic, Sequence, TypeVar
87

98
from fastapi import Depends, Query
109
from fastapi_pagination import pagination_ctx
1110
from fastapi_pagination.bases import AbstractPage, AbstractParams, RawParams
1211
from fastapi_pagination.ext.sqlalchemy import paginate
1312
from fastapi_pagination.links.bases import create_links
14-
from pydantic import BaseModel
13+
from pydantic import BaseModel, Field
1514

1615
if TYPE_CHECKING:
1716
from sqlalchemy import Select
1817
from sqlalchemy.ext.asyncio import AsyncSession
1918

2019
T = TypeVar('T')
21-
DataT = TypeVar('DataT')
2220
SchemaT = TypeVar('SchemaT')
2321

2422

25-
class _Params(BaseModel, AbstractParams):
23+
class _CustomPageParams(BaseModel, AbstractParams):
2624
page: int = Query(1, ge=1, description='Page number')
2725
size: int = Query(20, gt=0, le=100, description='Page size') # 默认 20 条记录
2826

@@ -33,53 +31,90 @@ def to_raw_params(self) -> RawParams:
3331
)
3432

3533

36-
class _Page(AbstractPage[T], Generic[T]):
37-
items: Sequence[T] # 数据
38-
total: int # 总数据数
39-
page: int # 第n页
40-
size: int # 每页数量
41-
total_pages: int # 总页数
42-
links: dict[str, str | None] # 跳转链接
34+
class _Links(BaseModel):
35+
first: str = Field(..., description='首页链接')
36+
last: str = Field(..., description='尾页链接')
37+
self: str = Field(..., description='当前页链接')
38+
next: str | None = Field(None, description='下一页链接')
39+
prev: str | None = Field(None, description='上一页链接')
40+
41+
42+
class _PageDetails(BaseModel):
43+
items: list = Field([], description='当前页数据')
44+
total: int = Field(..., description='总条数')
45+
page: int = Field(..., description='当前页')
46+
size: int = Field(..., description='每页数量')
47+
total_pages: int = Field(..., description='总页数')
48+
links: _Links
49+
4350

44-
__params_type__ = _Params # 使用自定义的Params
51+
class _CustomPage(_PageDetails, AbstractPage[T], Generic[T]):
52+
__params_type__ = _CustomPageParams
4553

4654
@classmethod
4755
def create(
4856
cls,
49-
items: Sequence[T],
57+
items: list,
5058
total: int,
51-
params: _Params,
52-
) -> _Page[T]:
59+
params: _CustomPageParams,
60+
) -> _CustomPage[T]:
5361
page = params.page
5462
size = params.size
55-
total_pages = math.ceil(total / params.size)
56-
links = create_links(**{
57-
'first': {'page': 1, 'size': f'{size}'},
58-
'last': {'page': f'{math.ceil(total / params.size)}', 'size': f'{size}'} if total > 0 else None,
59-
'next': {'page': f'{page + 1}', 'size': f'{size}'} if (page + 1) <= total_pages else None,
60-
'prev': {'page': f'{page - 1}', 'size': f'{size}'} if (page - 1) >= 1 else None,
61-
}).model_dump()
63+
total_pages = ceil(total / params.size)
64+
links = create_links(
65+
first={'page': 1, 'size': size},
66+
last={'page': f'{ceil(total / params.size)}', 'size': size} if total > 0 else {'page': 1, 'size': size},
67+
next={'page': f'{page + 1}', 'size': size} if (page + 1) <= total_pages else None,
68+
prev={'page': f'{page - 1}', 'size': size} if (page - 1) >= 1 else None,
69+
).model_dump()
70+
71+
return cls(
72+
items=items,
73+
total=total,
74+
page=params.page,
75+
size=params.size,
76+
total_pages=total_pages,
77+
links=links, # type: ignore
78+
)
79+
6280

63-
return cls(items=items, total=total, page=params.page, size=params.size, total_pages=total_pages, links=links)
81+
class PageData(_PageDetails, Generic[SchemaT]):
82+
"""
83+
包含 data schema 的统一返回模型,适用于分页接口
6484
85+
E.g. ::
86+
87+
@router.get('/test', response_model=ResponseSchemaModel[PageData[GetApiDetail]])
88+
def test():
89+
return ResponseSchemaModel[PageData[GetApiDetail]](data=GetApiDetail(...))
90+
91+
92+
@router.get('/test')
93+
def test() -> ResponseSchemaModel[PageData[GetApiDetail]]:
94+
return ResponseSchemaModel[PageData[GetApiDetail]](data=GetApiDetail(...))
95+
96+
97+
@router.get('/test')
98+
def test() -> ResponseSchemaModel[PageData[GetApiDetail]]:
99+
res = CustomResponseCode.HTTP_200
100+
return ResponseSchemaModel[PageData[GetApiDetail]](code=res.code, msg=res.msg, data=GetApiDetail(...))
101+
"""
65102

66-
class _PageData(BaseModel, Generic[DataT]):
67-
page_data: DataT | None = None
103+
items: Sequence[SchemaT]
68104

69105

70-
async def paging_data(db: AsyncSession, select: Select, page_data_schema: SchemaT) -> dict:
106+
async def paging_data(db: AsyncSession, select: Select) -> dict:
71107
"""
72108
基于 SQLAlchemy 创建分页数据
73109
74110
:param db:
75111
:param select:
76-
:param page_data_schema:
77112
:return:
78113
"""
79-
_paginate = await paginate(db, select)
80-
page_data = _PageData[_Page[page_data_schema]](page_data=_paginate).model_dump()['page_data']
114+
paginated_data: _CustomPage = await paginate(db, select)
115+
page_data = paginated_data.model_dump()
81116
return page_data
82117

83118

84119
# 分页依赖注入
85-
DependsPagination = Depends(pagination_ctx(_Page))
120+
DependsPagination = Depends(pagination_ctx(_CustomPage))

0 commit comments

Comments
 (0)