PHP如何实现用户邀请码裂变系统并统计推广层级关系

各位同学,大家好!欢迎来到今天的PHP进阶实战讲座。

别急着把椅子放下,我知道你们脑子里可能已经在想昨晚的球赛或者今晚的外卖了。但咱们今天要聊的东西,比外卖还重要,比球赛还刺激。它关乎金钱,关乎权力,关乎你如何让你的用户像病毒一样在互联网上疯长。

我们要讲的主题是:PHP如何实现用户邀请码裂变系统并统计推广层级关系。

听着,这不仅仅是一个“注册送优惠券”的功能,这是一个商业帝国的基石。这就是传说中的“传销”——哦不,我是说“裂变营销”。

第一部分:这玩意儿到底是个什么鬼?

想象一下,你开发了一个很棒的App,比如“摸鱼神器”或者“猫咪拍照”。你把它推给朋友A。
朋友A注册了,他变成了你的用户。
这时候,你的系统给他一个神秘代码(比如 INV-8848)。朋友A把这个代码发到朋友圈,说:“快来用这个神器,我带你们赚钱!”
朋友B扫了码,注册了,而且系统自动把B的“上家”设为A。
朋友B又拉了C,C又拉了D。

如果你把所有用户的关系画出来,你会发现什么?
一张巨大的
你站在树根,A是第一层,B是第二层,C是第三层。这棵树越长越大,你的利益也就源源不断。

在数据库的世界里,这种结构叫什么?自连接表,或者叫树形结构。在程序员的世界里,这叫“递归”。

第二部分:数据库设计——地基要打牢

在写代码之前,咱们先得把地基打好。就像盖房子不能直接在烂泥塘上动土一样。

我们需要一张 users 表。除了常规的 id, username, password,我们需要两个核心字段:

  1. invite_code: 邀请码,用户分享出去的“武器”。
  2. 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了。数字太容易混淆了,比如 1l0O。而且,如果邀请码太短,重复的概率就高。

我们要生成一个看起来很高大上的字符串。最经典的方法是用 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的邀请码进行注册时,这是整个系统的核心时刻。这里面有几个关键点:

  1. 验证邀请码是否存在
  2. 获取邀请人的ID
  3. 设置当前用户的 parent_id
  4. 计算层级(如果A已经是二级推广人,B就是三级)。
  5. 开启事务!这是重中之重。如果注册成功但层级更新失败,或者反之,你的数据就不一致了。

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=1level=2。小明就是你的“下线”。
小明的朋友小红(ID=3)用了小明的码注册。系统发现 parent_id=2level=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 就能搞定。它把“函数调用栈”换成了“内存栈”,性能更高,更可控。

第六部分:计算层级关系与返佣——利益分配

有了树,接下来就是分钱。假设规则是:

  1. 直接下线(一级)注册,邀请人得10元。
  2. 二级下线注册,邀请人得5元。
  3. 三级下线注册,邀请人得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 迭代;从利益的分配到性能的瓶颈。

你们学到了什么?

  1. 数据结构是灵魂: 树形结构是自连接表的核心,理解它,你就理解了层级。
  2. PHP 不仅仅有 echo SplStack, PDO, 类型提示,这些都是现代 PHP 的利器。
  3. 安全与性能: 事务保护数据一致性,索引和缓存解决性能问题。

记住,裂变营销不仅仅是拉人头,更是一套精密的数据逻辑。当你看着你的树状图越来越茂盛时,不要只顾着数钱,要记得检查你的递归深度是不是要爆了,你的数据库索引是不是需要优化了。

现在,拿起你们的键盘,去创造属于你们的商业帝国吧!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注