You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Laravel Eloquent 如何统计无限层级分类下的关联文章总数?

无限层级分类文章总数统计实现方案

原实现的核心局限

  • 原countPosts方法仅遍历当前层级的直接子分类,未递归合并深层子分类的文章ID,最多只能统计到“当前分类+直接子分类”两级的文章数,层级更深的分类数据全部遗漏
  • 递归过程中,子节点的统计结果没有向上传递,每次统计都重新从直接层级取数据,重复计算且覆盖不完整
  • 递归查找子节点时反复遍历全量分类集合,数据量较大时性能损耗明显
  • 模型中withChildren方法硬编码嵌套7层关联加载,不属于真正的无限层级实现,还会和现有全量加载逻辑触发重复查询

具体改造方案

1. 预构建分类映射,优化递归查找效率

在内存中提前按parent_id对所有分类做分组映射,查找某分类的直接子节点时直接从映射数组取,将递归的时间复杂度从O(n²)降至O(n)。
在当前类中新增映射属性:

/** @var array<int, \Illuminate\Support\Collection> 按parent_id分组的分类映射 */
protected array $categoryMap;

2. 递归过程中同步汇总统计数据,沿树结构向上传递

不再依赖单独的countPosts方法做两级统计,而是在每个节点生成时,就把自身关联的文章ID、以及所有子节点上传的汇总文章ID合并去重,存到节点属性中,供上层父节点直接合并使用。该逻辑完全不依赖预设层级数,不管分类树有多少层,都能完整覆盖所有节点的统计。
修改buildMenu初始化逻辑:

private function buildMenu(): void
{
    $this->menu = new Collection();
    $allCategories = PostCategory::with('posts')->orderBy('order')->get();
    // 预构建分类映射:key为parent_id,value为对应父级下的直接子分类集合
    $this->categoryMap = $allCategories->groupBy('parent_id')->toArray();

    $topLevelCategories = $this->categoryMap[null] ?? [];
    foreach ($topLevelCategories as $postCategory) {
        $subChildren = $this->getChildren($postCategory, 1);
        
        // 顶级分类统计:自身文章 + 所有子节点汇总的文章ID
        $currentPostIds = $postCategory->posts->pluck('id')->toArray();
        foreach ($subChildren as $child) {
            $currentPostIds = array_merge($currentPostIds, $child->allPostIds);
        }
        $totalCount = count(array_unique($currentPostIds));

        $this->menu->push(new MenuItem([
            'count'     => $totalCount,
            'children'  => $subChildren,
            'offset'    => 0,
            'category'  => null,
        ]));
    }

    $this->menu->prepend(new MenuItem([
        'count'     => $this->getAllPostCount(),
        'children'  => collect([]),
        'offset'    => 0,
        'category'  => null,
    ]));
}

修改递归生成子节点的getChildren方法:

private function getChildren(PostCategory $parentCategory, int $offset): Collection
{
    $children = collect([]);
    // 直接从预构建映射取当前分类的直接子分类
    $directChildren = $this->categoryMap[$parentCategory->id] ?? [];

    foreach ($directChildren as $postCategory) {
        // 递归生成所有深层子节点
        $subChildren = $this->getChildren($postCategory, $offset + 1);

        // 汇总当前节点的所有关联文章ID:自身文章 + 所有子节点上传的汇总ID
        $currentPostIds = $postCategory->posts->pluck('id')->toArray();
        foreach ($subChildren as $child) {
            $currentPostIds = array_merge($currentPostIds, $child->allPostIds);
        }
        $uniquePostIds = array_unique($currentPostIds);

        $children->push(new MenuItem([
            'count'     => count($uniquePostIds),
            'children'  => $subChildren,
            'offset'    => $offset,
            'category'  => $postCategory,
            // 存储当前节点汇总的全量文章ID,供上层父节点合并
            'allPostIds' => $uniquePostIds,
        ]));
    }

    return $children;
}

3. 冗余逻辑清理

  • 删除原countPosts方法,避免和新的递归统计逻辑重复
  • 移除模型中硬编码嵌套层级的withChildren方法,当前逻辑已经在内存中全量加载所有分类,不需要额外通过关联递归查询,可减少不必要的数据库查询开销

方案特性

  • 统计逻辑沿着树结构从最深层叶子节点开始,每往上一层就合并当前层节点的文章ID和下层传上来的汇总ID,全程只需要一次递归遍历,不需要重复深入深层节点
  • 全程做文章ID去重,就算同一篇文章被绑定到多个子分类下,统计时也不会重复计数
  • 适配任意层级深度的分类树,不需要提前预设层级数
  • 若存在分类量、文章量极大的场景,可将存储在MenuItem中的allPostIds调整为全局静态映射(按节点ID索引对应ID集合),避免重复存储造成的内存浪费

内容的提问来源于stack exchange,提问作者Miner

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.10.22 08:54:04