当前位置: 首页 > news >正文

ThinkPHP 嵌套集合模型(Nested Set Model)适配用户邀请关系

嵌套集合模型通过左右值(lft/rgt)替代传统的“父 ID”,利用深度优先遍历为每个节点标记“左边界值”和“右边界值”,将树形结构转化为二维表结构,从而实现快速的层级查询。

核心操作对比

操作

传统父 ID 方式

嵌套集合方式

查询所有子节点

需要递归查询

单条 SQL(lft/rgt 范围)

查询节点路径

多次关联 / 递归

单条 SQL(lft 当前 rgt)

添加 / 删除节点

简单(仅改父 ID)

需更新相关节点的 lft/rgt 值


用户邀请关系示例

以用户邀请关系为例,需增加根节点 root,每条线数据独立。

数据表结构

CREATE TABLE wg_user_invite ( id int(11) NOT NULL AUTO_INCREMENT COMMENT 'ID', user_id int(11) DEFAULT NULL COMMENT '用户ID', lft int(11) DEFAULT NULL COMMENT '左节点', rgt int(11) DEFAULT NULL COMMENT '右节点', root int(11) DEFAULT NULL COMMENT '根节点', depth int(11) DEFAULT NULL COMMENT '深度', parent_id int(11) DEFAULT NULL COMMENT '邀请人', PRIMARY KEY (id), KEY user_id (user_id) USING BTREE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户邀请人变更日志';

节点操作方法

1. 添加根节点

/** * 新增根节点(无上级的用户). * @param int $userId 用户ID * @return bool */ public function addRootNode($userId) { $exists = $this->where('user_id', $userId)->find(); if ($exists) { return true; } return $this->save([ 'user_id' => $userId, 'lft' => 1, 'rgt' => 2, 'root' => $userId, 'depth' => 0, 'parent_id' => 0, ]); }

2. 添加子节点

/** * 新增子节点(邀请下级). * @param int $parentUserId 邀请人用户ID * @param int $childUserId 被邀请人用户ID * @return bool */ public function addChildNode($childUserId = null, $parentUserId = null) { if (!$parentUserId) { return $this->addRootNode($parentUserId); } $parent = $this->where('user_id', $parentUserId)->find(); if (!$parent) { return false; } $parentRgt = $parent['rgt']; $root = $parent['root']; $depth = $parent['depth'] + 1; // 更新节点lft/rgt $this->where('root', $root)->where('lft', '>', $parentRgt)->inc('lft', 2)->update(); $this->where('root', $root)->where('rgt', '>=', $parentRgt)->inc('rgt', 2)->update(); return $this->save([ 'user_id' => $childUserId, 'lft' => $parentRgt, 'rgt' => $parentRgt + 1, 'root' => $root, 'depth' => $depth, 'parent_id' => $parentUserId, ]); }

3. 删除节点(含所有下级)

/** * 删除节点(含所有下级). * @param int $userId 要删除的用户ID * @return bool */ public function deleteNode($userId) { $node = $this->where('user_id', $userId)->find(); if (!$node) { return true; } $lft = $node['lft']; $rgt = $node['rgt']; $root = $node['root']; $diff = $rgt - $lft + 1; $this->where('root', $root)->where('lft', '>=', $lft)->where('rgt', '<=', $rgt)->delete(); $this->where('root', $root)->where('lft', '>', $rgt)->dec('lft', $diff)->update(); $this->where('root', $root)->where('rgt', '>', $rgt)->dec('rgt', $diff)->update(); return true; }

4. 修改节点父节点

/** * 修改节点的父节点(变更邀请人). * @param int $userId 要修改的用户ID * @param int $newParentId 新的邀请人ID * @return bool */ public function updateParentNode($userId, $newParentId) { $user = $this->where('user_id', $userId)->find(); $newParent = $this->where('user_id', $newParentId)->find(); if (!$user || !$newParent) { return false; } $this->deleteNode($userId); return $this->addChildNode($userId, $newParentId); }

查询操作示例

获取节点深度

/** * 获取指定用户的节点深度. * @param int $userId 用户ID * @return null|int 深度(根节点为0) */ public function getNodeDepth(int $userId): ?int { $node = $this->where('user_id', $userId)->value('depth'); return null === $node ? null : (int) $node; }

获取邀请节点数结构(层级+数量)

/** * 获取指定用户的邀请节点数结构(层级+数量). * @param int $userId 用户ID * @return array 示例:[0 => 1, 1 => 5, 2 => 10] */ public function getInviteNodeCount(int $userId): array { $node = $this->where('user_id', $userId)->find(); if (!$node) { return []; } $lft = $node['lft']; $rgt = $node['rgt']; $root = $node['root']; $baseDepth = $node['depth']; $list = $this->where('root', $root) ->where('lft', '>', $lft) ->where('rgt', '<', $rgt) ->column('depth'); $count = []; $count[0] = 1; foreach ($list as $depth) { $level = $depth - $baseDepth; $count[$level] = ($count[$level] ?? 0) + 1; } ksort($count); return $count; }

获取上级邀请链路(从自己到顶层)

/** * 获取上级邀请链路(从自己到最顶层). * @param int $userId 用户ID * @return array 示例:[1001, 1000, 999] */ public function getParentChain(int $userId): array { $chain = [$userId]; $currentId = $userId; while (true) { $parentId = $this->where('user_id', $currentId)->value('parent_id'); if (!$parentId || 0 == $parentId) { break; } $chain[] = $parentId; $currentId = $parentId; } return $chain; } public function getParentChainV2(int $userId): array { $currentUser = $this->where('user_id', $userId)->find(); if (empty($currentUser)) { return []; } $chain = $this->where('root', $currentUser['root']) ->where('lft', '<', $currentUser['lft']) ->where('rgt', '>', $currentUser['rgt']) ->order('lft', 'asc') ->column('user_id'); $chain[] = $userId; return array_reverse($chain); }

获取所有邀请下级(含所有层级)

/** * 获取所有邀请下级(含所有层级). * @param int $userId 用户ID * @return array 下级用户ID列表 */ public function getAllChildren(int $userId): array { $node = $this->where('user_id', $userId)->find(); if (!$node) { return []; } return $this->where('root', $node['root']) ->where('lft', '>', $node['lft']) ->where('rgt', '<', $node['rgt']) ->column('user_id'); }

获取所有上级(按从近到远顺序)

/** * 获取所有上级(按从近到远顺序). * @param int $userId 用户ID * @return array 上级用户ID列表 */ public function getAllParents(int $userId): array { $chain = $this->getParentChain($userId); array_shift($chain); return $chain; }

获取所有独立的邀请链路(每个根节点一条)

/** * 获取所有独立的邀请链路(每个根节点一条). * @return array 示例:[[999, 1000, 1001], [1002, 1003]] */ public function getAllIndependentChains(): array { $rootNodes = $this->where('parent_id', 0)->column('user_id'); $chains = []; foreach ($rootNodes as $root) { $nodes = $this->where('root', $root)->order('lft')->column('user_id'); $chains[] = $nodes; } return $chains; }

获取指定用户的下级树形结构

/** * 获取指定用户的下级树形结构 * @param int $userId 根用户ID * @param bool $withSelf 是否包含自己(默认包含) * @return array 树形结构数组 */ public function getChildrenTree(int $userId, bool $withSelf = true): array { $currentNode = $this->where('user_id', $userId)->find(); if (!$currentNode) { return []; } $lft = $currentNode['lft']; $rgt = $currentNode['rgt']; $root = $currentNode['root']; $baseDepth = $currentNode['depth']; $nodes = $this->where('root', $root) ->where('lft', '>=', $lft) ->where('rgt', '<=', $rgt) ->field('user_id, depth, parent_id') ->select() ->toArray(); $nodeMap = []; foreach ($nodes as $node) { $nodeMap[$node['user_id']] = [ 'user_id' => $node['user_id'], 'depth' => $node['depth'] - $baseDepth, 'parent_id' => $node['parent_id'], 'children' => [] ]; } foreach ($nodeMap as $id => $node) { $parentId = $node['parent_id']; if ($parentId != 0 && isset($nodeMap[$parentId])) { $nodeMap[$parentId]['children'][] = &$nodeMap[$id]; } } if ($withSelf) { return $nodeMap[$userId]; } else { return $nodeMap[$userId]['children']; } }

获取所有独立邀请树(所有根节点的树形结构)

/** * 获取所有独立邀请树(所有根节点的树形结构) * @return array 所有独立树的列表 */ public function getAllRootTrees(): array { $rootUserIds = $this->where('parent_id', 0)->column('user_id'); $trees = []; foreach ($rootUserIds as $rootId) { $trees[] = $this->getChildrenTree($rootId); } return $trees; }

优缺点总结

优点

  • 查询效率极高:查询子节点、路径、层级结构时,无需递归,单条 SQL 即可完成
  • 适合读多写少场景:如商品分类、网站导航栏等
  • 易于统计:可快速计算某节点下的子节点总数(公式:(rgt - lft - 1) / 2)

缺点

  • 写操作复杂:添加、删除、移动节点时,需要批量更新其他节点的 lft/rgt 值
  • 数据一致性要求高:更新 lft/rgt 时需用事务,否则容易出现数据错乱
  • 不适合高频修改场景:如频繁新增、删除的评论树

总结

  • PHP 嵌套集合模型的核心是通过深度优先遍历给节点标记 lft/rgt 值,将树形结构转化为二维表,实现高效查询
  • 核心优势是查询效率高(无递归),核心劣势是写操作复杂(需批量更新 lft/rgt)
  • 适用场景:读多写少的树形结构(分类、导航),不适用高频修改的场景(评论、动态列表)

相关依赖:
我这里也在网上查询了相关的依赖,说的是适配thinkphp ,但是尝试了一下 安装都报错,也是没办法,后续有时间了再搞。

场景

推荐方案

快速开发、功能全面

使用

lazychaser/laravel-nestedset

兼容老项目、稳定性优先

使用

baum/baum

团队熟悉 TP 语法

尝试国产

think-nestedset

定制化需求多

基于之前的自研代码封装(更灵活)

http://www.jsqmd.com/news/408132/

相关文章:

  • 家庭出游该选哪家?2026年众信旅游推荐与评价,解决行程定制与儿童照看痛点 - 品牌推荐
  • 2026-2-24 BPE Tokenizer
  • 2026年期货量化交易回测框架对比_主流工具的回测精度分析
  • 分析中雅乐石英石制造企业靠谱吗,安徽地区有推荐的合作案例吗? - 工业推荐榜
  • Java驱动:淘宝客多商户优惠券聚合系统
  • 2026年期货量化交易数据管理_历史数据存储与查询实践
  • 好写作AI | 当创意写作遇上AI:大学生也能写出惊艳的小说开头!
  • 探讨工业净水设备选购要点,河南水之流品牌靠谱费用合理 - myqiye
  • VMware vSphere 8.0 Update 3i 下载 - 企业级工作负载平台
  • 2026年期货量化交易风控系统_资金管理与止损策略设计
  • 2026年遗产继承靠谱律师推荐,解决你的继承难题 - 工业品网
  • 2026制造业厂房工业提升门优质品牌推荐榜 - 优质品牌商家
  • VMware ESXi 8.0U3i macOS Unlocker OEM BIOS 2.7 标准版和厂商定制版
  • 2026年2月成都云梯车/高空车/吊车/挖掘机/压路机租赁市场竞争格局深度分析报告 - 2026年企业推荐榜
  • 细聊优质的车位代理销售公司,成都地区有哪些靠谱品牌推荐? - 工业品牌热点
  • 【深度解析】离子净化主机:定义、原理与应用场景全攻略 - 速递信息
  • VMware vCenter Server 8.0U3i 发布 - 集中管理 vSphere 环境
  • 2026年期货量化交易成本控制_手续费和滑点优化实践
  • 无需安装!一键播放 M3U8 格式视频,这个在线工具超好用
  • 2026年正规出国劳务公司公司权威推荐:商务部正规出国劳务公司/怎么办理出国打工/普通人怎么出国打工/选择指南 - 优质品牌商家
  • 2026年ups租赁厂家权威推荐榜:ups不间断电源租赁、ups电源租赁厂家、出租ups电源、出租发电机电话选择指南 - 优质品牌商家
  • 2026乐山临江鳝丝优质门店推荐指南 - 优质品牌商家
  • 华为eNSP综合实验之- 3a认证配置案例及解析(AAA认证)
  • 给 Spring Boot 接口加了幂等保护:Token 机制 + 结果缓存,一个注解搞定
  • 2026智能机器人城市空间场景应用指南:多领域落地技术要求与实施路径全解析
  • 2026年2月三圣乡团建/宝宝宴/寿宴/团建/婚宴场地选择指南暨行业五强解析 - 2026年企业推荐榜
  • 收藏!小白/程序员转行AI工程师必看|3个月从新手到月薪30k+,大模型学习路线不踩坑
  • Kraken IPO 前再出手,拿下 Magna 欲掌控代币「全生命周期」
  • 公司注册流程
  • 2026微型电机行业专题报告:人形机器人灵巧手核心部件技术壁垒与市场前景分析