ThinkPHP分页过滤条件丢失的根源是paginate()默认只保留page参数,需通过config的query选项显式透传category_id、keyword等参数;总数不准因count与主查询条件不一致或缓存导致;深分页性能差源于MySQL LIMIT OFFSET机制缺陷,应改用游标分页。

ThinkPHP分页时过滤条件丢失的典型表现
点“下一页”后搜索关键词消失、分类ID清空、排序还原成默认值,甚至返回空数据——这不是前端跳转问题,而是 paginate() 默认只认 page 参数,其他如 category_id、keyword、sort 全被丢弃。框架不会自动把 URL 里所有 GET 参数塞进下一页链接,必须显式告诉它哪些要透传。
query 参数必须手动配置才能保留过滤状态
paginate() 的第三个参数是 config 数组,其中 query 是唯一能控制分页链接携带哪些额外参数的入口。漏配或配错,翻页即失联。
- 安全写法:
->paginate(15, false, ['query' => request()->param()])—— 透传全部合法 GET 参数,但需确保没带敏感字段(如token) - 精准写法:
->paginate(15, false, ['query' => ['category_id' => $cid, 'keyword' => $kw]])—— 只传业务需要的键,避免污染 - 别用
$_GET直接拼,它不做过滤;request()->param()或input('get.')才带类型校验 - 如果用了
input('category_id/d')做整型过滤,query里也得用同样方式取值,否则传字符串"1"可能被数据库当字符处理
分页总数不准?大概率是 count 查询被缓存或条件不一致
总条数 $list->total() 和当前页数据不匹配,常见于:后台新增商品后前台仍显示“共 23 条”,或筛选后总页数没变。根本原因是 paginate() 内部先发一条 COUNT(*),再发主查询,两者 WHERE 条件必须完全一致,且 COUNT 不该被缓存。
- 确认
count()没走缓存:在paginate()前加->cache(false)强制禁用,例如Db::name('product')->where($where)->cache(false)->paginate(10) - 避免手写
count()+select()分两步:一旦 WHERE 条件用变量拼过一次,第二次可能因作用域或引用变化导致不一致 - 多表 JOIN 场景下,
COUNT(*)易出错,建议改用COUNT(DISTINCT t1.id)并确保关联字段有索引
深分页性能崩了?不是 paginate 写法问题,是 MySQL LIMIT OFFSET 本身缺陷
第 1000 页开始明显卡顿,不是 PHP 或 ThinkPHP 的锅,是 MySQL 执行 LIMIT 99990, 10 时被迫扫描并丢弃前 99990 行。TP8 的 page(10000, 10) 会原样生成这个 SQL,毫无优化空间。
立即学习“PHP免费学习笔记(深入)”;
- 真要支持万级页码,必须放弃 offset 分页,改用游标(cursor-based):记录上一页最后一条的
create_time和id,下一页查WHERE create_time - 游标要求排序字段严格唯一、有索引,且不能用
SELECT *,得明确字段列表 - TP 不原生支持游标分页,需手写查询 + 自定义分页 HTML,
render()不能直接用 - 对普通后台管理页,更现实的解法是限制最大页码(如
max_page = 200),并在 UI 上隐藏“尾页”按钮
paginate() 就完事,关键逻辑全在 WHERE 条件的一致性、query 参数的显式透传、count 查询的实时性,以及对 MySQL 分页机制边界的清醒认知。最容易被忽略的是:你以为在翻页,其实数据库在反复做无用功。



















