各位同学,大家好!欢迎来到今天的PHP进阶实战讲座。
别急着把椅子放下,我知道你们脑子里可能已经在想昨晚的球赛或者今晚的外卖了。但咱们今天要聊的东西,比外卖还重要,比球赛还刺激。它关乎金钱,关乎权力,关乎你如何让你的用户像病毒一样在互联网上疯长。
我们要讲的主题是:PHP如何实现用户邀请码裂变系统并统计推广层级关系。
听着,这不仅仅是一个“注册送优惠券”的功能,这是一个商业帝国的基石。这就是传说中的“传销”——哦不,我是说“裂变营销”。
第一部分:这玩意儿到底是个什么鬼?
想象一下,你开发了一个很棒的App,比如“摸鱼神器”或者“猫咪拍照”。你把它推给朋友A。
朋友A注册了,他变成了你的用户。
这时候,你的系统给他一个神秘代码(比如 INV-8848)。朋友A把这个代码发到朋友圈,说:“快来用这个神器,我带你们赚钱!”
朋友B扫了码,注册了,而且系统自动把B的“上家”设为A。
朋友B又拉了C,C又拉了D。
如果你把所有用户的关系画出来,你会发现什么?
一张巨大的树。
你站在树根,A是第一层,B是第二层,C是第三层。这棵树越长越大,你的利益也就源源不断。
在数据库的世界里,这种结构叫什么?自连接表,或者叫树形结构。在程序员的世界里,这叫“递归”。
第二部分:数据库设计——地基要打牢
在写代码之前,咱们先得把地基打好。就像盖房子不能直接在烂泥塘上动土一样。
我们需要一张 users 表。除了常规的 id, username, password,我们需要两个核心字段:
invite_code: 邀请码,用户分享出去的“武器”。parent_id: 父级ID,记录这个用户是谁拉来的。如果没有,就是0,代表他是“根节点”。
SQL 建表脚本:
CREATE TABLE users (
id INT AUTO_INCREMENT PRIMARY KEY COMMENT '用户ID',
username VARCHAR(50) NOT NULL COMMENT '用户名',
email VARCHAR(100) NOT NULL COMMENT '邮箱',
password VARCHAR(255) NOT NULL COMMENT '密码',
invite_code CHAR(12) NOT NULL UNIQUE COMMENT '邀请码',
parent_id INT DEFAULT 0 COMMENT '推广人ID,0代表无推广人',
level INT DEFAULT 1 COMMENT '推广层级,1代表一级推广人',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '注册时间',
INDEX idx_parent_id (parent_id),
INDEX idx_invite_code (invite_code)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户邀请裂变表';
这里有个技术点:
我在 invite_code 上加了 UNIQUE 约束,防止一个用户有两个邀请码。在 parent_id 上加了索引,为什么?因为当你查询某个用户下面有多少子节点时,如果不用索引,数据库就要像大海捞针一样扫描整张表,那就是性能灾难。
第三部分:生成邀请码——听起来简单,其实不然
很多新手同学上来就写个 rand(1000, 9999),这太low了。数字太容易混淆了,比如 1 和 l,0 和 O。而且,如果邀请码太短,重复的概率就高。
我们要生成一个看起来很高大上的字符串。最经典的方法是用 62进制(包含大小写字母和数字)。
PHP 实现:
<?php
class InviteCodeGenerator
{
// 字符集:0-9, a-z, A-Z
private static $chars = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
// 映射表,用于将ID转换为62进制字符串,并反之
private static $map = [];
public function __construct()
{
// 初始化映射表:0->'0', 1->'1', ... 61->'z', 62->'A' ...
// 实际生产环境中,为了性能,可以缓存这个映射表,不要每次实例化都生成
for ($i = 0; $i < 62; $i++) {
self::$map[self::$chars[$i]] = $i;
}
}
/**
* 根据用户ID生成唯一的邀请码
* @param int $userId
* @return string
*/
public static function encode(int $userId): string
{
$code = '';
$base = 62;
$num = $userId; // 这里假设ID是自增的数字
while ($num > 0) {
$remainder = $num % $base;
$code = self::$chars[$remainder] . $code;
$num = floor($num / $base);
}
// 如果用户ID太小(比如0, 1, 2),生成的code可能不够长,补全
return str_pad($code, 8, '0', STR_PAD_LEFT); // 固定8位,不够补0
}
/**
* 将邀请码还原为用户ID
* @param string $code
* @return int|null
*/
public static function decode(string $code): ?int
{
$code = strtoupper(str_replace('O', '0', $code)); // 容错处理
$code = str_replace('I', '1', $code);
$code = str_replace('L', '1', $code);
$len = strlen($code);
$id = 0;
$base = 62;
for ($i = 0; $i < $len; $i++) {
$char = $code[$i];
// 简单的校验,防止非法字符
if (!isset(self::$map[$char])) {
return null;
}
$id = $id * $base + self::$map[$char];
}
return $id;
}
}
// 测试一下
$userId = 12345678;
$code = InviteCodeGenerator::encode($userId);
echo "用户ID {$userId} 的邀请码是: {$code}n";
$decodedId = InviteCodeGenerator::decode($code);
echo "解码邀请码 {$code} 得到ID: {$decodedId}n";
专家点评:
看懂了吗?这就是数学的力量。用62进制编码,12位数字的ID可以编码成8个字符的邀请码。既保证了唯一性,又缩短了长度,方便用户复制粘贴。
第四部分:注册流程——裂变的开始
当用户B扫描A的邀请码进行注册时,这是整个系统的核心时刻。这里面有几个关键点:
- 验证邀请码是否存在。
- 获取邀请人的ID。
- 设置当前用户的
parent_id。 - 计算层级(如果A已经是二级推广人,B就是三级)。
- 开启事务!这是重中之重。如果注册成功但层级更新失败,或者反之,你的数据就不一致了。
PHP 代码示例:
class ReferralService
{
private $pdo;
public function __construct(PDO $pdo)
{
$this->pdo = $pdo;
}
/**
* 用户注册并绑定邀请关系
*/
public function registerWithReferral(string $username, string $password, string $inviteCodeInput)
{
$this->pdo->beginTransaction();
try {
// 1. 查找邀请人
$stmt = $this->pdo->prepare("SELECT id, level FROM users WHERE invite_code = ?");
$stmt->execute([$inviteCodeInput]);
$referrer = $stmt->fetch(PDO::FETCH_ASSOC);
$parentId = 0;
$level = 1;
if ($referrer) {
$parentId = $referrer['id'];
$level = $referrer['level'] + 1;
}
// 2. 生成新用户的邀请码
// 这里需要查最大ID,或者用雪花算法,为了演示简单,我们假设查询最后插入ID
// 实际生产中,最好用 UUID 或 雪花算法
$newCode = InviteCodeGenerator::encode(time() * rand(1, 100)); // 简单演示,实际最好查库获取当前最大ID
// 3. 插入新用户
$insertStmt = $this->pdo->prepare("INSERT INTO users (username, password, invite_code, parent_id, level) VALUES (?, ?, ?, ?, ?)");
$insertStmt->execute([$username, password_hash($password, PASSWORD_DEFAULT), $newCode, $parentId, $level]);
$newUserId = $this->pdo->lastInsertId();
// 4. 更新邀请人的“下级人数”(可选,用于统计)
$updateReferrer = $this->pdo->prepare("UPDATE users SET child_count = child_count + 1 WHERE id = ?");
$updateReferrer->execute([$parentId]);
$this->pdo->commit();
return ['status' => 'success', 'user_id' => $newUserId, 'parent_id' => $parentId];
} catch (Exception $e) {
$this->pdo->rollBack();
return ['status' => 'error', 'message' => $e->getMessage()];
}
}
}
场景演练:
假设你(ID=1)有一个邀请码 ABC123。
你的朋友小明(ID=2)用了你的码注册。系统发现 parent_id=1,level=2。小明就是你的“下线”。
小明的朋友小红(ID=3)用了小明的码注册。系统发现 parent_id=2,level=3。小红是“下线的下线”,你是她的“上上级”。
第五部分:统计推广层级关系——递归的艺术
这是最头疼的部分。你怎么知道某个用户邀请了多少人?怎么知道这个用户是第几级推广人?怎么知道这个用户在整个树里占据多大的面积?
这就是树遍历。
方法一:递归——优雅但危险
递归是理解树结构的最好方式,就像俄罗斯套娃,一层套一层。
class TreeAnalyzer
{
private $pdo;
public function __construct(PDO $pdo)
{
$this->pdo = $pdo;
}
/**
* 获取某用户的直接下级列表
*/
public function getChildren($parentId)
{
$stmt = $this->pdo->prepare("SELECT * FROM users WHERE parent_id = ? ORDER BY id ASC");
$stmt->execute([$parentId]);
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
/**
* 递归打印树结构(DFS深度优先)
* @param int $userId
* @param int $depth 当前深度(层级)
*/
public function printTreeRecursive($userId, $depth = 1)
{
// 查询用户信息
$stmt = $this->pdo->prepare("SELECT id, username, level FROM users WHERE id = ?");
$stmt->execute([$userId]);
$user = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$user) return;
// 打印当前节点
echo str_repeat('-', $depth) . " ID:{$user['id']} | 用户:{$user['username']} | 级别:{$user['level']}<br>";
// 递归调用:找他的孩子
$children = $this->getChildren($userId);
foreach ($children as $child) {
$this->printTreeRecursive($child['id'], $depth + 1);
}
}
}
Bug警告:
递归有个死敌叫栈溢出。如果你的树结构有几千层,或者某个用户有上万个直接下级,PHP的递归深度限制(默认是100)就会直接报错,服务器崩给你看。所以,递归虽然好看,但在高并发大流量下要慎用。
方法二:迭代——使用 SplStack——安全且高效
为了解决递归溢出问题,我们使用栈。这就像你在一个房间里把箱子堆起来,只要箱子没堆到天花板(栈溢出),你就能一直往上堆。
PHP 自带了一个非常强大的数据结构 SplStack。
class TreeAnalyzerIterative
{
private $pdo;
public function __construct(PDO $pdo)
{
$this->pdo = $pdo;
}
/**
* 使用栈迭代打印树结构(DFS)
*/
public function printTreeIterative($rootId)
{
// 创建栈,并压入根节点
$stack = new SplStack();
$stack->push(['id' => $rootId, 'depth' => 1]);
while (!$stack->isEmpty()) {
// 弹出栈顶元素
$node = $stack->pop();
$currentUserId = $node['id'];
$currentDepth = $node['depth'];
// 查询数据
$stmt = $this->pdo->prepare("SELECT id, username, level FROM users WHERE id = ?");
$stmt->execute([$currentUserId]);
$user = $stmt->fetch(PDO::FETCH_ASSOC);
if ($user) {
echo str_repeat('-', $currentDepth) . " ID:{$user['id']} | 用户:{$user['username']} | 级别:{$user['level']}<br>";
// 关键步骤:先把所有孩子压入栈
// 注意:因为栈是后进先出(LIFO),如果我们先压 A 再压 B,最后弹出的是 B
// 为了保持层级顺序,我们需要先压最大的ID,或者倒序处理
$children = $this->getChildren($currentUserId);
// 倒序遍历,保证ID小的先出栈(虽然对打印顺序要求不高,但逻辑上更符合直觉)
for ($i = count($children) - 1; $i >= 0; $i--) {
$stack->push([
'id' => $children[$i]['id'],
'depth' => $currentDepth + 1
]);
}
}
}
}
// 辅助方法,同上
private function getChildren($parentId) {
$stmt = $this->pdo->prepare("SELECT * FROM users WHERE parent_id = ? ORDER BY id ASC");
$stmt->execute([$parentId]);
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
}
专家点评:
看到没?这就是工业级代码。无论树有多深,只要内存够,SplStack 就能搞定。它把“函数调用栈”换成了“内存栈”,性能更高,更可控。
第六部分:计算层级关系与返佣——利益分配
有了树,接下来就是分钱。假设规则是:
- 直接下线(一级)注册,邀请人得10元。
- 二级下线注册,邀请人得5元。
- 三级下线注册,邀请人得2元。
这需要我们在注册的时候,或者后台手动触发的时候,把这笔钱算清楚。这涉及到路径查找。
我们需要找到从“根节点”到“当前用户”的所有中间人。
实现思路:
遍历树,找到当前用户ID,然后反向追溯 parent_id,直到 parent_id 为 0。
PHP 代码示例:
class CommissionCalculator
{
private $pdo;
public function __construct(PDO $pdo)
{
$this->pdo = $pdo;
}
/**
* 获取某用户的推荐人链(从上到下)
* @param int $userId
* @return array
*/
public function getReferrerChain($userId)
{
$chain = [];
$currentId = $userId;
while ($currentId > 0) {
$stmt = $this->pdo->prepare("SELECT id, username, parent_id FROM users WHERE id = ?");
$stmt->execute([$currentId]);
$user = $stmt->fetch(PDO::FETCH_ASSOC);
if ($user) {
$chain[] = $user; // 添加当前用户到链中
$currentId = $user['parent_id']; // 继续找上一级
} else {
break;
}
}
return $chain; // 返回的是从当前用户往上追溯的数组
}
/**
* 计算并发放佣金
* 假设规则:1级返10%,2级返5%,3级返2%
*/
public function calculateEarnings($userId)
{
$chain = $this->getReferrerChain($userId);
if (empty($chain)) {
return [];
}
$earnings = [];
// 从第2个元素开始遍历(第1个是当前用户自己)
for ($i = 1; $i < count($chain); $i++) {
$referrer = $chain[$i];
$level = $i + 1; // 链中的第1个推荐人,就是二级推广人(因为链里包含了当前用户)
// 根据层级判断返佣比例
$rate = 0;
if ($level == 2) $rate = 0.10;
elseif ($level == 3) $rate = 0.05;
elseif ($level == 4) $rate = 0.02;
else $rate = 0;
if ($rate > 0) {
$earnings[] = [
'referrer_id' => $referrer['id'],
'referrer_name' => $referrer['username'],
'level' => $level,
'amount' => 100 * $rate, // 假设注册费100
'reason' => "推广佣金"
];
}
}
return $earnings;
}
}
深度剖析:
这里有个逻辑陷阱,要注意。
如果A拉了B,B拉了C,C拉了D。
D的链是:[D, C, B, A]。
C是D的第1个推荐人,但在层级统计里,C是第2级推广人(D是C的1级下级,C是B的1级下级…)。
所以上面的代码里 level = i + 1 是对的。链里的第1个推荐人,是层级 1 + 1 = 2。
第七部分:性能优化与常见陷阱——那些年我们踩过的坑
好,现在代码有了,数据库有了,逻辑通了。但是,如果这一天,你的网站火得一塌糊涂,来了100万个用户,问题来了。
1. 无限层级与深度限制
如果你的规则允许无限拉人(A拉B,B拉C…拉到第100层),那么递归算法或者栈遍历虽然不报错了,但你会发现内存被吃光了。
对策: 在数据库层面限制 level,比如 level <= 5。超过5层的邀请无效,或者自动降级处理。
2. 树形数据的查询优化
如果你要做“排行榜”,要展示“谁的粉丝最多”或者“谁的层级最高”,你不能每次都去跑 SELECT * FROM users WHERE parent_id = ? 然后统计。
对策:
- 广度优先(BFS)层级缓存: 在用户表里加一个字段
total_downline_count。每次有人注册,更新所有上级的total_downline_count。这叫“计数器更新”。虽然写操作多了点,但查询是O(1)的,快如闪电。 - Redis 树形结构: 使用 Redis 的 ZSet 或者 Hash 结构来存储树。Redis 的速度比磁盘上的 MySQL 快几十倍,专门用来存这种热点数据。
3. 邀请码冲突
如果你用时间戳+随机数生成邀请码,在高并发下,两个人可能生成一样的。或者,因为系统崩溃重启,时间戳冲突。
对策:
永远不要只用时间戳。一定要结合 INSERT ... ON DUPLICATE KEY UPDATE 或者数据库的 LAST_INSERT_ID。
或者,使用数据库的主键ID作为唯一的种子源,正如我在第三部分演示的那样。
4. 循环引用(死循环)
比如A邀请了B,B又悄悄偷偷去修改数据库把B的 parent_id 改成了A。这会导致树变成了一个圈,无限死循环下去。
对策:
在应用层做判断。注册时检查 parent_id 是否等于 current_user_id,如果是,直接报错拦截。
第八部分:完整实战演练
让我们把以上所有的代码封装在一个类里,模拟一个完整的请求生命周期。
<?php
// 假设这是你的PDO连接
$pdo = new PDO('mysql:host=localhost;dbname=referral_db', 'root', '');
// 1. 初始化服务
$referralService = new ReferralService($pdo);
$treeAnalyzer = new TreeAnalyzerIterative($pdo);
// --- 模拟数据 ---
// 假设用户 A (ID=1) 已经存在
$parentId = 1;
$parentInfo = $treeAnalyzer->getChildren($parentId);
echo "==== 用户 A (ID:{$parentId}) 的推广树结构 ====n";
$treeAnalyzer->printTreeIterative($parentId);
echo "<br><br>";
// --- 模拟用户 B 注册 ---
$code = InviteCodeGenerator::encode($parentId); // 用A的ID生成邀请码
echo "==== 用户 B 注册 ====n";
echo "邀请码: {$code}n";
$result = $referralService->registerWithReferral('UserB', 'password123', $code);
if ($result['status'] === 'success') {
$newUserId = $result['user_id'];
echo "注册成功!新用户ID: {$newUserId}, 父级ID: {$result['parent_id']}<br><br>";
} else {
echo "注册失败: " . $result['message'] . "<br><br>";
}
// 再次查看树
echo "==== 注册后 A 的推广树结构 ====n";
$treeAnalyzer->printTreeIterative($parentId);
echo "<br><br>";
// --- 模拟用户 C 注册 (由 B 邀请) ---
// 先获取 B 的邀请码(实际开发中由系统自动生成或从DB查)
$codeC = InviteCodeGenerator::encode($newUserId);
echo "==== 用户 C 注册 (由 B 邀请) ====n";
echo "邀请码: {$codeC}n";
$resultC = $referralService->registerWithReferral('UserC', 'password456', $codeC);
if ($resultC['status'] === 'success') {
$newUserIdC = $resultC['user_id'];
echo "注册成功!新用户ID: {$newUserIdC}, 父级ID: {$resultC['parent_id']}<br><br>";
}
// 最终树结构
echo "==== 最终 A 的推广树结构 ====n";
$treeAnalyzer->printTreeIterative($parentId);
结语
好了,同学们。我们今天用PHP构建了一个完整的邀请裂变系统。
从简单的 invite_code 生成,到复杂的 parent_id 层级关联;从优雅的递归遍历到稳健的 SplStack 迭代;从利益的分配到性能的瓶颈。
你们学到了什么?
- 数据结构是灵魂: 树形结构是自连接表的核心,理解它,你就理解了层级。
- PHP 不仅仅有
echo:SplStack,PDO, 类型提示,这些都是现代 PHP 的利器。 - 安全与性能: 事务保护数据一致性,索引和缓存解决性能问题。
记住,裂变营销不仅仅是拉人头,更是一套精密的数据逻辑。当你看着你的树状图越来越茂盛时,不要只顾着数钱,要记得检查你的递归深度是不是要爆了,你的数据库索引是不是需要优化了。
现在,拿起你们的键盘,去创造属于你们的商业帝国吧!