闭包表通过预存所有祖先-后代关系实现o(1)查询,建表需复合主键(ancestor,descendant)和descendant索引,插入时必须包含depth=0自环及完整层级路径;path字段更新须封装显式方法并重算整链,tp5.1递归查询需限深防循环;佣金结算优先闭包表,低频查询可用path;两者需与用户状态变更强耦合。

闭包表怎么建,为什么比 parent_id 递归快
直接用 parent_id 查三级下线,每次都要 JOIN 多次 users 表,1000 个用户就得发 3000 条 SQL;而闭包表把所有祖先-后代关系提前存好,查某人全部上级只要一条 SQL:SELECT ancestor FROM relation_closure WHERE descendant = ?。
建表语句必须带复合主键和索引:
CREATE TABLE relation_closure ( ancestor INT NOT NULL, descendant INT NOT NULL, depth INT NOT NULL, PRIMARY KEY (ancestor, descendant), INDEX idx_descendant (descendant) );
-
depth = 0表示自己到自己,插入新用户时必须先写这一行 - 用户 A 邀请 B,B 邀请 C,则要插入:
(A,A,0)、(B,B,0)、(C,C,0)、(A,B,1)、(A,C,2)、(B,C,1) - 千万别漏掉
depth字段——它能直接区分一级、二级,避免 PHP 侧再算层级
路径枚举(path 字段)怎么更新才不丢数据
用 path 字段(如 "0-100-205")实现快速 LIKE 查询,但更新逻辑极易出错:模型事件里改 path 会因事务回滚或并发写丢失;手动在注册后立即更新又容易漏掉升级、迁移等场景。
正确做法是封装成独立方法,在业务关键点显式调用:
- 用户注册成功后,立刻执行:
$user->path = $parent ? $parent->path . '-' . $parent->id : '0'; - 用户更换上级(如冻结原上级后指定新推荐人),必须重算整条链路:
rebuildPath($user->id),不能只改当前行 -
path字段开头固定为0,否则WHERE path LIKE '0-100-%'会匹配失败 - 字段长度至少设为
VARCHAR(255),5 级以上分销很容易超长
TP5.1 里怎么写一个安全的递归查询函数
ThinkPHP 5.1 自带的 with() 不适合多级分销——它默认 N+1,查 100 个下线就发起 100 次 SQL;手写递归函数又容易栈溢出或无限循环。真正稳的做法是限制深度 + 提前终止。
下面这个函数控制最多查 3 层,且自动防循环引用:
protected function getDownlines($userId, $maxLevel = 3, $currentLevel = 1, $visited = []) {
if ($currentLevel > $maxLevel || in_array($userId, $visited)) {
return [];
}
$children = Db::name('users')->where('pid', $userId)->select();
$result = [];
foreach ($children as $child) {
$result[] = [
'id' => $child['id'],
'level' => $currentLevel,
'children' => $this->getDownlines($child['id'], $maxLevel, $currentLevel + 1, array_merge($visited, [$userId]))
];
}
return $result;
}
- 必须传入
$visited数组,否则父子互绑(A 的pid是 B,B 的pid又设成 A)会导致死循环 - 别在控制器里直接调这个函数——佣金结算、报表导出等场景要用它,应抽成服务类方法
- 返回结构带
level字段,前端或后续计算可直接用,不用再解析path
佣金结算该用闭包表还是 path 字段
高频结算(比如每笔订单完成都要算)必须用闭包表;低频查询(比如后台看某个用户的下级列表)用 path 更省事。两者不是二选一,而是分工明确。
- 结算时走闭包表:
SELECT u.id, u.level, rc.depth FROM users u JOIN relation_closure rc ON u.id = rc.ancestor WHERE rc.descendant = ? AND rc.depth BETWEEN 1 AND 3 - 查“某人所有二级下线”这种简单需求,
path一行搞定:WHERE path LIKE '0-100-%' AND LENGTH(path) - LENGTH(REPLACE(path, '-', '')) = 3(注意减 1 才是相对级数) - 闭包表插入成本高,但查得快;
path插入快,但修改上级时要批量更新子树——如果业务中换上级频繁,闭包表更稳
真正容易被忽略的是:无论选哪种方案,佣金计算逻辑必须和用户状态强耦合——上级被冻结、降级、注销,都要触发重算,不能只依赖静态路径或关系表快照。











