正确姿势是直接用 paginate() 链式调用,避免先 select() 导致全表加载;需透传搜索参数到 query 选项,关联查询须用 JOIN 或子查询确保 COUNT 准确,模板中用 {$list->render()} 输出分页HTML。

直接用 paginate() 链式调用,别先 select()
ThinkPHP 模型分页最常踩的坑,就是写成 UserModel::where('status', 1)->select()->paginate(10)。这会导致全表数据先查进 PHP 内存,再在数组里切片——总数不准、翻页卡顿、JOIN 失效,完全失去数据库层分页意义。
正确姿势是让 paginate() 接管整个查询链,它会自动生成两条 SQL:COUNT(*) 算总数 + 带 LIMIT 的主查询。
UserModel::where('status', 1)->order('id desc')->paginate(15)- 每页 15 条,自动读
$_GET['page'],当前页码不传也行 - 如果接口走 POST 或 API,必须显式传页码:
->paginate(15, false, ['page' => input('p/d', 1)]) - 返回对象直接
assign给模板,变量名比如list,不用解包
带搜索参数翻页不丢关键词?透传 query 是关键
点“下一页”后 keyword 消失、筛选条件重置——这不是 bug,是默认行为:框架只自动保留 page 参数,其他 GET 字段不会带上。
解决办法是手动把需要透传的参数塞进 query 配置项:
立即学习“PHP免费学习笔记(深入)”;
- 安全通用写法:
->paginate(10, false, ['query' => request()->param()]) - 精准控制(推荐):
->paginate(10, false, ['query' => ['keyword' => $keyword, 'type' => $type]]),避免意外透传token或sign - 如果用
input('get.'),记得过滤键名,防止 URL 注入(比如用户传?page=1&_sql=xxx)
关联查询分页出错?with() 不行,得手写 JOIN 或子查询
写 UserModel::with('orders')->paginate(10) 表面能跑,但 $list->total() 返回的是用户数,不是「有订单的用户」数;更严重的是,with 补 null 后,分页切片和实际结果集错位,第 2 页可能漏掉本该出现的数据。
根本原因是 paginate() 默认对主表 COUNT,而关联结果集需要对 JOIN 后的结果 COUNT。
- 简单 1:1 关联(如用户 + 最新订单):用
Db::table()显式 JOIN,确保 COUNT 和 SELECT 同源 - 示例:
Db::name('user')->alias('u')->join('order o', 'u.id = o.user_id AND o.is_latest = 1')->where('u.status', 1)->paginate(15) - 1:N 场景(如每个用户带多条订单明细):
paginate()完全不适用,必须改用子查询兜底,或前端分页 + 接口拉取 - 字段索引要检查:JOIN 字段(如
user_id)没加索引,COUNT 查询会变慢
模板里怎么取数据和分页 HTML?别用 {$page},用 {$list->render()}
控制器 assign 的变量名是 list,模板里不是用 {$page} 或 {$list->show()},那是老版本写法,现在统一用 render() 方法输出完整 HTML。
- 数据循环:
{volist name="list" id="item"}{$item.name}{/volist} - 分页输出:
{$list->render()}—— 注意不是{$page},也不是{$list->show()} - 总条数:
{$list->total()};当前页:{$list->currentPage()};总页数:{$list->lastPage()} - 样式定制不要硬覆盖 CSS,优先复制
vendor/topthink/framework/src/view/paginate.tpl到app/view/paginate.tpl,再在config/app.php中配置'template' => app_path() . 'view/paginate.tpl'
query 不传和 with() 直接链式分页,这两个点几乎必踩。



















