1. DRF 列表接口为什么总在过滤排序分页上翻车
DRF(Django REST Framework)的列表接口看起来简单,真正落地时最容易被三件事卡住:过滤条件写错位置、排序字段没暴露、分页类挂错视图。更麻烦的是,当接口还要走统一鉴权通道时,很多人会把鉴权和业务参数混在一起调,结果 401 和 400 交替出现,排查半天不知道是哪一层的问题。
这篇聚焦 DRF 视图层,目标很明确:在 TaoToken 统一 Key/API 通道打通鉴权之后,把filter_backends过滤、ordering排序、PageNumberPagination分页串成一条可复制的链路,最后用 curl 验证一个带鉴权的列表接口能一次跑通。适合已经写过基础 DRF 视图、但列表接口参数一多就乱的朋友。
核心检索词先摆出来:DRF 过滤排序分页配置、filter_backends用法、PageNumberPagination自定义、DRF 列表接口鉴权。这几个词基本覆盖了从视图到响应体的全过程。
我试过把过滤、排序、分页拆成三个文件维护,好处是视图类干净,坏处是新人接手时找不到配置在哪。所以下面会给出一套「视图内声明 + 独立 pagination 文件」的折中结构,既能一眼看到用了哪些 backend,又不会让视图膨胀。
先说清楚三者的职责边界。过滤负责「查哪些行」,排序负责「按什么顺序排」,分页负责「一次返回多少行」。它们都作用在QuerySet上,最终由序列化器转成响应体。顺序上,DRF 会先执行filter_queryset,再交给分页器切分,排序则通过OrderingFilter注入到 QuerySet 的order_by。理解这个顺序,后面排查「为什么过滤后分页数量不对」就有方向了。
还有一个高频误区:以为SearchFilter和DjangoFilterBackend可以随便混用。实际上SearchFilter是模糊匹配,查询参数固定为search;DjangoFilterBackend是精准匹配,查询参数是字段名。两者可以共存,但search_fields和filterset_fields要分别声明,否则会出现「传了参数却没生效」的假象。
下面从环境准备开始,一步步把配置补齐。
2. TaoToken 统一 Key 前置配置与鉴权通道打通
在写视图之前,先把鉴权通道打通。TaoToken 在这里的角色是统一 Key/API 通道,让 DRF 接口在调用上游模型或内部服务时不用每个视图单独维护一套凭证。官网入口是 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,API 基址是 https://taotoken.net/api 。
第一步是拿到 Key。进入控制台创建 API Key,路径在 console:https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_content=console&utm_campaign=rewrite 。创建后复制保存,后面所有请求都靠它。如果你还没决定用哪种接入方式,可以先在模型对话页试一下返回是否符合预期:https://taotoken.net/models?utm_source=taotoken_aicg_blog_end&utm_content=models&utm_campaign=rewrite 。
第二步是把 Key 写进 Django 配置。推荐用环境变量,不要硬编码。在settings.py里加一段:
import os TAOTOKEN_API_KEY = os.environ.get("TAOTOKEN_API_KEY", "") TAOTOKEN_BASE_URL = "https://taotoken.net/api"然后在项目根目录的.env或启动脚本里设置:
export TAOTOKEN_API_KEY="sk-你的Key"第三步是确认鉴权头格式。TaoToken 的 API 通道使用标准的Authorization: Bearer <Key>头。如果你在 DRF 里封装了一个调用上游的客户端,可以这样写:
import requests from django.conf import settings def call_taotoken(path, payload): headers = { "Authorization": f"Bearer {settings.TAOTOKEN_API_KEY}", "Content-Type": "application/json", } resp = requests.post( f"{settings.TAOTOKEN_BASE_URL}{path}", json=payload, headers=headers, timeout=30, ) resp.raise_for_status() return resp.json()这里要注意,raise_for_status()会把 401 直接抛出来,方便你在视图层统一捕获。如果你用的是 Claude Code 这类编码工具,接入文档在 doc:https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite ,里面有 Base URL、Key、Model ID 三件套的完整说明。
第四步是验证 Key 是否可用。在终端里跑一条最小请求:
curl -s -X POST https://taotoken.net/api/chat/completions \ -H "Authorization: Bearer $TAOTOKEN_API_KEY" \ -H "Content-Type: application/json" \ -d '{"model":"gpt-4o-mini","messages":[{"role":"user","content":"ping"}]}'如果返回里有choices字段,说明通道通了。如果返回 401,先检查 Key 有没有多余空格,再检查环境变量有没有被当前 shell 读到。这一步过了,再往下写 DRF 视图,否则后面报错你分不清是鉴权问题还是过滤问题。
长期做编码或 Agent 任务的话,可以考虑 Coding Plan:https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding-plan&utm_campaign=rewrite ,它更适合持续性的调用场景。API Keys 管理页在:https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api-keys&utm_campaign=rewrite 。
3. 可复制的 settings 片段与 ViewSet 完整配置
这一节是全文的核心,直接给可复制的配置。先看settings.py需要加的部分:
INSTALLED_APPS = [ # ... 其他 app "rest_framework", "django_filters", ] REST_FRAMEWORK = { "DEFAULT_AUTHENTICATION_CLASSES": [ "rest_framework.authentication.TokenAuthentication", ], "DEFAULT_PERMISSION_CLASSES": [ "rest_framework.permissions.IsAuthenticated", ], "DEFAULT_FILTER_BACKENDS": [ "django_filters.rest_framework.DjangoFilterBackend", "rest_framework.filters.OrderingFilter", "rest_framework.filters.SearchFilter", ], "DEFAULT_PAGINATION_CLASS": "api.pagination.StandardPagination", "PAGE_SIZE": 10, }注意DEFAULT_FILTER_BACKENDS的顺序会影响执行顺序,一般把精准过滤放前面,排序和搜索放后面。DEFAULT_PAGINATION_CLASS指向我们自定义的分页类,路径要和实际文件一致。
接着建一个api/pagination.py:
from rest_framework.pagination import PageNumberPagination class StandardPagination(PageNumberPagination): page_size = 10 page_query_param = "page" page_size_query_param = "size" max_page_size = 100这里四个参数的含义:page_size是默认每页条数;page_query_param是页码参数名,默认就是page;page_size_query_param允许客户端用size覆盖每页条数;max_page_size是上限,防止有人传size=100000把库拖垮。
然后是模型和序列化器,用一个Book举例:
# api/models.py from django.db import models class Book(models.Model): name = models.CharField(max_length=100) price = models.DecimalField(max_digits=8, decimal_places=2) stock = models.IntegerField(default=0) created_at = models.DateTimeField(auto_now_add=True) def __str__(self): return self.name# api/serializers.py from rest_framework import serializers from .models import Book class BookSerializer(serializers.ModelSerializer): class Meta: model = Book fields = ["id", "name", "price", "stock", "created_at"]最后是 ViewSet,把过滤、排序、分页三件套挂上:
# api/views.py from rest_framework import viewsets from django_filters.rest_framework import DjangoFilterBackend from rest_framework.filters import OrderingFilter, SearchFilter from .models import Book from .serializers import BookSerializer from .pagination import StandardPagination class BookViewSet(viewsets.ModelViewSet): queryset = Book.objects.all() serializer_class = BookSerializer pagination_class = StandardPagination filter_backends = [DjangoFilterBackend, OrderingFilter, SearchFilter] filterset_fields = ["name", "stock"] search_fields = ["name"] ordering_fields = ["id", "price", "created_at"] ordering = ["-created_at"]逐行解释关键点。filterset_fields里的字段支持精准匹配,请求时用?name=xxx&stock=5。search_fields支持模糊匹配,请求时用?search=西,会走icontains。ordering_fields是白名单,只有列在这里的字段才能被?ordering=使用,这是安全边界,别偷懒写成__all__。ordering = ["-created_at"]是默认排序,客户端不传ordering时生效。
路由注册:
# api/urls.py from rest_framework.routers import DefaultRouter from .views import BookViewSet router = DefaultRouter() router.register(r"books", BookViewSet, basename="book") urlpatterns = router.urls到这里配置就齐了。如果你用的是 Cline MCP 或 Codex 这类工具做辅助开发,记得把 Base URL、Key、Model ID 三件套对齐,Base URL 用https://taotoken.net/api,Key 用控制台生成的,Model ID 按文档填。三件套缺一个都会在调用时报错。
4. 验证请求与成功响应:curl 跑通带鉴权列表接口
配置写完必须验证,不然你不知道是配置生效了还是碰巧没报错。先准备一条鉴权 Token。如果你用的是 DRF 的TokenAuthentication,可以这样生成:
python manage.py drf_create_token your_username拿到 Token 后,先跑一条最基础的列表请求:
curl -s -X GET "http://127.0.0.1:8000/api/books/" \ -H "Authorization: Token 你的Token"预期返回结构里应该有count、next、previous、results四个字段。count是总数,results是当前页数据。如果返回 401,说明鉴权头没带对;如果返回 403,说明权限类拦住了。
接着验证过滤。精准过滤用字段名:
curl -s -X GET "http://127.0.0.1:8000/api/books/?name=西游记&stock=5" \ -H "Authorization: Token 你的Token"模糊搜索用search:
curl -s -X GET "http://127.0.0.1:8000/api/books/?search=西" \ -H "Authorization: Token 你的Token"验证排序。正序直接写字段名,倒序加-:
curl -s -X GET "http://127.0.0.1:8000/api/books/?ordering=-price" \ -H "Authorization: Token 你的Token"多字段排序用逗号分隔,第一个字段优先级最高:
curl -s -X GET "http://127.0.0.1:8000/api/books/?ordering=-stock,price" \ -H "Authorization: Token 你的Token"验证分页。默认每页 10 条,可以用size覆盖:
curl -s -X GET "http://127.0.0.1:8000/api/books/?page=2&size=5" \ -H "Authorization: Token 你的Token"组合起来就是完整链路:
curl -s -X GET "http://127.0.0.1:8000/api/books/?search=西&ordering=-price&page=1&size=3" \ -H "Authorization: Token 你的Token"成功响应的样子大致是:
{ "count": 12, "next": "http://127.0.0.1:8000/api/books/?page=2&size=3", "previous": null, "results": [ {"id": 3, "name": "西游记", "price": "59.90", "stock": 8, "created_at": "2024-05-01T10:00:00Z"}, {"id": 7, "name": "西厢记", "price": "39.90", "stock": 5, "created_at": "2024-05-02T10:00:00Z"} ] }看到count和results同时出现,说明过滤、排序、分页三层都生效了。如果count是过滤后的数量,但results条数不对,检查size有没有超过max_page_size。如果next链接里的参数丢了search,那是分页器没保留查询参数,需要在分页类里确认page_size_query_param配置正确。
5. 本篇常见报错排查:401、local proxy failed、reading choices、OAuth
这一节按真实报错来对照,方便你快速定位。
401 Unauthorized。最常见的原因是鉴权头格式不对。DRF 的TokenAuthentication要求Authorization: Token <token>,不是Bearer。如果你同时接了 TaoToken 的 API 通道,注意区分:调上游用Bearer,调自己的 DRF 接口用Token。两者混用就会 401。排查时先print(request.headers)看实际发出去的头部。
local proxy failed。这个报错通常出现在本地开发环境配置了转发但目标不可达时。检查TAOTOKEN_BASE_URL是不是写成了https://taotoken.net/api,末尾不要多加斜杠。另外确认本机网络能正常访问该地址,可以用curl -I https://taotoken.net/api看返回码。
reading choices。这个报错一般出现在解析响应体时,代码期望choices字段但实际返回结构不同。比如你调的是模型对话接口,返回里应该有choices;如果调的是别的路径,结构可能不一样。排查时先把原始响应print(resp.text)打出来,别直接.json()["choices"]。
OAuth 相关报错。如果你在 DRF 里用了 OAuth 认证类,但 Token 是普通 Token,会报认证失败。确认DEFAULT_AUTHENTICATION_CLASSES里只保留你实际使用的认证方式。混用多种认证类时,DRF 会依次尝试,第一个成功的就返回,所以顺序也有影响。
过滤参数不生效。检查filter_backends有没有包含对应的 backend。DjangoFilterBackend对应filterset_fields,SearchFilter对应search_fields,OrderingFilter对应ordering_fields。三者缺一不可。另外确认视图继承的是GenericAPIView或其子类,普通APIView不会自动应用filter_backends。
分页不生效。确认pagination_class挂在了视图上,或者settings.py里配了DEFAULT_PAGINATION_CLASS。如果两者都配了,视图上的优先级更高。还要确认返回的是QuerySet而不是列表,分页器只对QuerySet生效。
排序字段报错。如果传了ordering_fields之外的字段,OrderingFilter会忽略它而不是报错,这容易让人以为排序没生效。排查时先确认字段在白名单里,再看数据库里该字段有没有索引,大数据量下没索引的排序会很慢。
如果你在接入过程中遇到鉴权通道的问题,可以对照接入文档:https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite 。需要重新生成 Key 就去 API Keys 页:https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api-keys&utm_campaign=rewrite 。
6. 从 QuerySet 到响应体的稳定链路与后续动作
把这条链路跑通之后,你会发现 DRF 列表接口的复杂度其实集中在「配置位置」和「参数命名」上。过滤、排序、分页各自有独立的配置项,混在一起就容易乱。我的习惯是:settings.py里只放全局默认值,视图里显式声明filter_backends和pagination_class,分页类单独放一个文件。这样新人接手时,打开视图就能看到这个接口支持哪些查询能力。
还有一个实用技巧:给filterset_fields和ordering_fields加注释,写清楚每个字段对应的业务含义。比如stock是库存,created_at是上架时间。半年后你自己回来看,也能快速想起为什么这个字段要暴露给客户端。
如果你接下来要做更复杂的过滤,比如范围查询、日期区间,可以上django-filter的FilterSet类,把filterset_fields换成filterset_class。如果要做大数据量分页,把PageNumberPagination换成CursorPagination,它只支持上一页下一页,但查询速度更快,适合手机端下拉加载。
验证模型返回是否符合预期,可以去模型对话页试:https://taotoken.net/models?utm_source=taotoken_aicg_blog_end&utm_content=models&utm_campaign=rewrite 。长期做编码或 Agent 任务,Coding Plan 更合适:https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding-plan&utm_campaign=rewrite 。控制台入口:https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_content=console&utm_campaign=rewrite 。
最后留一个检查清单,每次加新接口时过一遍:视图是否继承GenericAPIView子类;filter_backends是否包含需要的 backend;filterset_fields、search_fields、ordering_fields是否都声明了;pagination_class是否挂上;鉴权头格式是否正确。这五条过了,列表接口基本不会出问题。