thinkphp 6 的 paginate() 不支持无限级分类直接关联筛选,应先通过左右值模型安全高效查出目标分类及其子孙 id 集合,再作为 where 条件参与分页,避免 in 超限和 n+1 问题。

ThinkPHP 6 的 paginate() 默认不支持无限级分类关联筛选
直接在 where() 中嵌套子查询或递归查出所有子类 ID 再传入分页,是常见但危险的做法——容易触发 MySQL 的 IN 参数超限(尤其当分类树深、节点多时),且无法利用索引加速。TP6 的 paginate() 底层调用的是 select() + count() 两次查询,而 count() 不会自动识别你在 with() 或闭包中写的分类树逻辑。
真正可行的路径是:先明确目标分类 ID 集合(含自身及全部子孙),再把这个集合作为普通字段条件参与分页。关键在于「怎么安全、高效地拿到这个 ID 集合」。
- 不要用 PHP 递归查全量分类再
array_merge()—— N+1 问题严重,内存易爆 - 推荐用数据库自连接或闭包查询一次性拉出所有子孙 ID:
Db::name('category')->where('lft', '>=', $rootLft)->where('rgt', 'column('id')(需已启用 MPTT 模型) - 若没维护
lft/rgt字段,可用 MySQL 8.0+ 的 CTE 递归查询,但 TP6 原生不支持 CTE 绑定参数,得用Db::query()手写 SQL 并手动绑定
在分页前必须预计算分类 ID 范围,不能塞进 paginate() 闭包里
很多人试图这样写:
$list = Product::where('status', 1)
->where(function ($q) use ($catId) {
$ids = Category::withChildrenIds($catId); // 错!这里每次 count() 和 select() 都会执行一遍
$q->whereIn('category_id', $ids);
})
->paginate(15);
问题在于:TP6 分页会先执行一次 COUNT(*) 查询算总数,再执行一次 SELECT 取数据,而上面的闭包在两次查询中各执行一次 withChildrenIds(),不仅重复查库,还可能导致两次结果不一致(比如中间有分类被删)。
- 务必提前算好
$catIds = Category::withChildrenIds($catId),且确保该方法返回的是整型数组(不是字符串拼接) - 然后直接用于 where:
->whereIn('category_id', $catIds),让 TP6 的查询构造器能正确生成预处理语句 - 如果
$catIds为空数组,whereIn会生成WHERE 1=0,结果为零条,这是预期行为,不用额外判断
分页 URL 中保留分类筛选参数,但别直接透传原始 ID
用户点击第 2 页时,URL 类似 /product/index?cat_id=123&page=2,但如果 123 是父类,真实筛选依据是它的所有子孙 ID 集合,这个集合不应暴露在 URL 中(防篡改、防 SQL 注入、避免链接过长)。
- 把原始
cat_id存进 session 或缓存(如cache('filter_cat_'.$uid, $catId, 3600)),分页时从缓存读取并生成 ID 集合 - 或者用哈希映射:对
$catId和当前时间戳做md5($catId . '_' . date('Ymd')),生成短 token,URL 传 token,后端查表或缓存反查对应分类范围 - 绝对不要在 URL 里拼接一长串 ID,如
?cat_ids=1,2,3,5,8,...—— 超过 2KB 就可能被 Nginx 截断,且无意义暴露数据结构
性能瓶颈往往卡在分类树展开,而不是分页本身
实测发现,当分类层级 > 5、总节点 > 5000 时,纯 PHP 递归展开耗时可超 200ms;而 MPTT 方式查 lft/rgt 通常在 5ms 内。所以优化重心不在 paginate() 参数调优,而在分类维度的预处理。
- 确认你的分类表有
lft、rgt、level字段,并在新增/移动分类时用事务更新(TP6 可封装成模型事件) - 避免在分页回调中用
with('category')加载分类信息——这会让每条商品都查一次分类,应改用with(['category' => function ($q) { $q->field('id,name'); }])限制字段 - 如果分类筛选频繁且固定(如“手机 > iPhone > iPhone 15”),可考虑将路径转成冗余字段
category_path(如"1/5/23"),用like '1/5/23%'查询,配合联合索引加速
最常被忽略的一点:分页总数 total 显示的是匹配分类范围的商品数,但前端「筛选条件栏」里显示的子分类数量,仍需单独查一次该范围内有哪些子分类有商品——这个查询不能复用分页的 count() 结果,必须另起,且建议加缓存。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











