Skip to content

分页、错误与版本演进 ​

本站纵览 | 练习答案

使用稳定复合游标避免同一时间戳下丢项:

python
from django.db.models import Q
from django.contrib.auth.decorators import login_required
from django.http import JsonResponse
from django.utils.dateparse import parse_datetime
from .api import serialize_ticket
from .models import Ticket


@login_required
def ticket_page(request):
    rows = Ticket.objects.filter(assignee=request.user).order_by("-created_at", "-id")
    created = parse_datetime(request.GET.get("before", ""))
    before_id = request.GET.get("before_id")
    if created and before_id and before_id.isdigit():
        rows = rows.filter(
            Q(created_at__lt=created)
            | Q(created_at=created, id__lt=int(before_id))
        )
    items = list(rows[:21])
    return JsonResponse({"results": [serialize_ticket(row) for row in items[:20]], "has_more": len(items) > 20})

深水边界 ​

游标是协议,不应直接暴露可任意拼接的 SQL。签名或编码游标,校验版本、排序和租户。错误响应区分语法错误、校验失败、未认证、无权限、冲突和暂时不可用;不要全部返回 200,也不向客户端泄露堆栈。

版本演进优先做兼容新增。字段改义比字段改名更危险;先记录调用方、兼容窗口和观测指标,再删除旧合同。

验收 ​

并发插入时翻页无既有项重复;非法游标返回稳定 400;错误码与 HTTP 状态一致;旧客户端在兼容窗口仍可运行。

练习(先独立完成) ​

  1. 将两个游标字段编码为带签名 token。
  2. 为 409 冲突设计错误体和重试提示。
  3. 增加 v2 字段时保持 v1 响应测试。

完成后查看练习答案。

深度加餐:分页是在有限预算里分发结果 ​

offset 方便理解却会随深页变慢;游标分页更稳定,但客户端必须保存位置。

先做一个可观察实验 ​

python
from django.core.paginator import Paginator
from django.http import JsonResponse

def list_tickets(request):
    page_number = max(int(request.GET.get('page', 1)), 1)
    paginator = Paginator(
        Ticket.objects.order_by('-created_at', '-pk'),
        per_page=20,
    )
    page = paginator.get_page(page_number)
    return JsonResponse({
        'data': list(page.object_list.values('id', 'title')),
        'meta': {'page': page.number, 'has_next': page.has_next()},
    })

预期:排序包含唯一 pk,翻页边界稳定;非法页码回退到可定义的页,而不是 500。

原理下潜与工程边界 ​

offset 分页需要跳过前 N 行;seek/cursor 分页使用上一页最后的排序键,适合持续增长的数据。错误响应应统一 code、message、details,并通过版本路径或 Accept 头演进。

排序字段被修改会让游标失效;count() 可能很贵;不要把数据库异常直接暴露给 API 客户端。版本并存期间要记录每个版本的使用量。

反例排错路线 ​

记录 page、cursor、query time 和返回条数;深页慢时看 EXPLAIN,空页和重复项通常先查排序是否不唯一。

练习加码(仍不提供答案) ​

加码:实现基于 created_at + pk 的 cursor 分页,拒绝篡改后的 cursor,并补齐过期版本的 410 响应。

验收时请补充:正常路径、权限或失败路径、以及一条能证明事务/资源/配置边界没有被绕过的测试。

继续查阅:Django 官方文档(外部资料已收录于证据账本) · Django 安全清单(外部资料已收录于证据账本)

课程合同:分页、错误与版本 ​

学习目标 ​

学完本篇,读者应能把「游标分页、冲突和弃用字段让旧客户端继续工作」拆成输入、可信边界、Django 机制和可复查证据;同时能说明 Django 6.0 与 5.2 LTS 的差异不会改变业务不变量。

心智模型 ​

请求/数据输入 → 边界检查 → Django 核心机制 → 可观察输出 → 恢复动作。配置、数据库提交和外部副作用分别拥有自己的生命周期;没有证据的“应该可以”不算完成。

本地机制图 ​

分页、错误与版本机制图

图中的“输入”是不可信的 HTTP、表单、任务或配置;“边界”负责身份、格式、权限、大小、超时与版本;“机制”才是 URL、View、ORM、事务、缓存、Tasks 或 ASGI;“证据”是测试、响应、日志、查询计划和发布记录。

最小实验 ​

在全新虚拟环境中运行:

bash
python -m django --version
python manage.py check
python manage.py test

实验只验证一个机制:输入一条有效值,再输入一条空值、重复值或无权限值。每次实验都记录 Django 版本、数据库、命令和退出码。

完整场景 ​

把实验接到 tickets 工单流:请求携带用户身份和工单 ID,经过当前专题负责的边界,完成一次读或写,并留下响应状态、数据库变化、日志字段和清理动作。完整场景必须能第二次执行,或明确说明幂等键、事务回滚、缓存失效和任务重试策略。

运行输出 ​

成功路径应出现:

text
Django 6.0.x
System check identified no issues (0 silenced).
OK

失败路径要保留真实信号,例如 400(输入无效)、403(权限/CSRF)、404(资源不存在)、405(方法不允许)或数据库约束异常;不要把失败改写成含糊的 200。

失败边界 ​

至少验证:空输入、重复提交、匿名用户、越权对象、并发写入、下游不可用、超时和版本不兼容。记录“谁拒绝、何时拒绝、状态是否回滚、重试是否安全”。开发模式的 traceback 不得进入生产响应,缓存不得跨用户复用敏感结果。

原理与排错 ​

先确认版本和 settings,再用 manage.py check 定位启动问题;接着确认 URL/任务入口、输入解析和权限;最后检查 SQL、事务提交、响应和副作用。若结果异常,用最小复现打开 SQL/请求 ID/任务 ID 观测,沿「复现 → 缩小 → 观测 → 修复 → 回归」闭环,不靠重装依赖猜测。

练习(正文不泄答案) ​

  1. 为上述完整场景补一条正常路径和两条失败路径测试,并记录完整输出。
  2. 画出本篇机制图,标出身份 owner、数据提交点、缓存/任务副作用和恢复动作。
  3. 故意制造一次重复、越权或超时,说明应返回的状态、日志字段和是否可重试。
  4. 写一条 Django 5.2 LTS 兼容验收标准,再说明 Django 6.0 的新能力是否需要条件分支。

验收标准:命令可重复、输出可复查、正文没有练习答案;完成后再打开本阶段答案册。

答案册 ​

本阶段答案册提供参考实现、关键推理、失败边界和替代方案;先独立完成练习再对照。

下一篇 ​

下一篇是07-middleware-sessions-cache/01-middleware-session-cache-signals,继续沿同一条输入—边界—机制—证据链前进。

用心记录每一步成长