推荐关系系统.md 46 KB

推荐关系系统

1. 概述

推荐关系系统是团队模块的核心组成部分,负责管理用户之间的推荐关系、团队结构和推荐码。该系统通过建立用户间的直推和间推关系,形成多层级的团队结构,为收益分成和达人等级提供基础数据支持。

2. 推荐关系类型

2.1 直推关系

直推关系是指用户A直接推荐用户B注册,则用户A是用户B的直推上级,用户B是用户A的直推下级。

用户A ──直推──> 用户B

直推关系具有以下特点:

  • 一个用户只能有一个直推上级
  • 一个用户可以有多个直推下级
  • 直推关系不可更改,一旦建立永久有效
  • 直推上级可获得下级收益的固定比例分成(通常为5%)

2.2 间推关系

间推关系是指通过直推关系形成的间接推荐关系。例如,用户A推荐用户B,用户B推荐用户C,则用户A是用户C的间推上级,用户C是用户A的间推下级。

用户A ──直推──> 用户B ──直推──> 用户C
用户A ──间推──> 用户C

间推关系具有以下特点:

  • 一个用户可以有多个间推上级
  • 一个用户可以有多个间推下级
  • 间推关系通过直推关系自动建立
  • 间推上级可获得下级收益的部分分成,比例取决于达人等级

2.3 团队结构

用户的所有直推和间推下级构成该用户的团队。团队结构是一个多层级的树状结构,用户在其中可以同时扮演上级和下级的角色。

            用户A
           /     \
        用户B     用户C
       /    \        \
    用户D   用户E     用户F

在上图中:

  • 用户A的直推下级:用户B、用户C
  • 用户A的间推下级:用户D、用户E、用户F
  • 用户A的团队成员:用户B、用户C、用户D、用户E、用户F

3. 数据结构设计

3.1 用户推荐关系表 (promotion_user_referrals)

用户推荐关系表采用简化设计,只存储用户与其直接上级(直推关系)的关系,间接关系通过缓存或实时计算获取。

字段名 类型 说明
id bigint 主键ID
user_id bigint 用户ID
referrer_id bigint 直接推荐人ID
created_at timestamp 创建时间
updated_at timestamp 更新时间

示例数据:

id | user_id | referrer_id | created_at
---|---------|------------|------------
1  | 2       | 1          | 2023-05-01 10:00:00
2  | 3       | 2          | 2023-05-01 11:00:00
3  | 4       | 3          | 2023-05-01 12:00:00

在上面的示例中:

  • 用户1直推了用户2
  • 用户2直推了用户3
  • 用户3直推了用户4

通过这种链式关系,可以计算出间接推荐关系:

  • 用户1间接推荐了用户3(通过用户2)
  • 用户1间接推荐了用户4(通过用户2和用户3)
  • 用户2间接推荐了用户4(通过用户3)

3.2 推荐码表 (promotion_referral_codes)

推荐码表存储用户的推荐码信息,用于邀请新用户注册。

字段名 类型 说明
id bigint 主键ID
user_id bigint 用户ID
code varchar 推荐码
usage_count int 使用次数
status tinyint 状态(1=有效,0=无效)
expire_time timestamp 过期时间,NULL表示永不过期
created_at timestamp 创建时间
updated_at timestamp 更新时间

4. 核心功能

4.1 推荐关系建立

当新用户注册并使用推荐码时,系统会建立推荐关系:

/**
 * 建立推荐关系
 */
public function createReferralRelation(int $userId, int $referrerId): bool
{
    return DB::transaction(function () use ($userId, $referrerId) {
        // 验证用户和推荐人是否存在
        if (!$this->userRepository->exists($userId) || !$this->userRepository->exists($referrerId)) {
            throw new UserNotFoundException("用户或推荐人不存在");
        }

        // 验证是否已存在推荐关系
        if ($this->referralRepository->hasReferrer($userId)) {
            throw new ReferralException("用户已有推荐人,不能重复设置");
        }

        // 验证是否形成循环推荐
        if ($this->checkCircularReferral($userId, $referrerId)) {
            throw new ReferralException("不能形成循环推荐关系");
        }

        // 创建直推关系
        $this->referralRepository->create([
            'user_id' => $userId,
            'referrer_id' => $referrerId
        ]);

        // 更新推荐人的直推人数
        $this->talentRepository->incrementDirectCount($referrerId);

        // 清除相关缓存
        $this->clearReferralCaches($userId, null, $referrerId);

        // 更新推荐人的团队人数
        $this->updatePromotionCounts($referrerId);

        // 触发推荐关系创建事件
        event(new ReferralCreatedEvent($userId, $referrerId));

        // 检查并更新达人等级
        $this->talentService->checkAndUpdateTalentLevel($referrerId);

        return true;
    });
}

/**
 * 更新用户及其所有上级的团队人数
 */
private function updatePromotionCounts(int $userId): void
{
    // 更新当前用户的团队人数
    $directCount = $this->countDirectReferrals($userId);
    $promotionCount = $this->calculatePromotionCount($userId);
    $this->talentRepository->updateCounts($userId, $directCount, $promotionCount);

    // 递归更新所有上级的团队人数
    $directReferrer = $this->referralRepository->getDirectReferrer($userId);
    if ($directReferrer) {
        $this->updatePromotionCounts($directReferrer->referrer_id);
    }
}

/**
 * 计算用户的团队总人数
 */
private function calculatePromotionCount(int $userId): int
{
    // 获取所有团队成员
    $allMembers = $this->getAllPromotionMembers($userId);
    return count($allMembers);
}

4.2 推荐码生成

系统为每个用户生成唯一的推荐码,用于邀请新用户:

/**
 * 生成推荐码
 */
public function generateReferralCode(int $userId): string
{
    // 检查用户是否已有推荐码
    $existingCode = $this->referralCodeRepository->getByUserId($userId);
    if ($existingCode) {
        return $existingCode->code;
    }

    // 生成唯一推荐码
    $code = $this->generateUniqueCode();

    // 保存推荐码
    $this->referralCodeRepository->create([
        'user_id' => $userId,
        'code' => $code,
        'usage_count' => 0,
        'status' => 1
    ]);

    return $code;
}

/**
 * 生成唯一推荐码
 */
private function generateUniqueCode(): string
{
    $characters = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ';
    $length = 6;

    do {
        $code = '';
        for ($i = 0; $i < $length; $i++) {
            $code .= $characters[random_int(0, strlen($characters) - 1)];
        }
    } while ($this->referralCodeRepository->codeExists($code));

    return $code;
}

4.3 推荐关系查询

系统提供多种方式查询推荐关系,使用缓存优化查询性能:

/**
 * 获取用户的所有上级(包括直接和间接)
 */
public function getAllReferrers(int $userId): array
{
    // 尝试从缓存获取
    $cacheKey = "promotion:user:{$userId}:all_referrers";
    $cachedReferrers = Redis::get($cacheKey);

    if ($cachedReferrers !== null) {
        return json_decode($cachedReferrers, true);
    }

    // 缓存不存在,计算所有上级
    $allReferrers = [];
    $this->calculateAllReferrers($userId, $allReferrers);

    // 缓存结果
    Redis::setex($cacheKey, 86400, json_encode($allReferrers)); // 缓存1天

    return $allReferrers;
}

/**
 * 递归计算用户的所有上级
 */
private function calculateAllReferrers(int $userId, array &$allReferrers, int $level = 0, int $maxLevel = 20): void
{
    // 限制最大层级
    if ($level >= $maxLevel) {
        return;
    }

    // 获取直接上级
    $directReferrer = $this->referralRepository->getDirectReferrer($userId);
    if (!$directReferrer) {
        return;
    }

    $referrerId = $directReferrer->referrer_id;

    // 添加到结果集
    $allReferrers[] = [
        'user_id' => $referrerId,
        'level' => $level == 0 ? 1 : 2, // 1=直推, 2=间推
        'created_at' => $directReferrer->created_at
    ];

    // 递归获取上级的上级
    $this->calculateAllReferrers($referrerId, $allReferrers, $level + 1, $maxLevel);
}

/**
 * 获取用户的所有团队成员(包括直接和间接)
 */
public function getAllPromotionMembers(int $userId, int $page = 1, int $pageSize = 20): array
{
    // 尝试从缓存获取
    $cacheKey = "promotion:user:{$userId}:all_members";
    $cachedMembers = Redis::get($cacheKey);

    $allMembers = [];
    if ($cachedMembers !== null) {
        $allMembers = json_decode($cachedMembers, true);
    } else {
        // 缓存不存在,计算所有下级
        $this->calculateAllPromotionMembers($userId, $allMembers);

        // 缓存结果
        Redis::setex($cacheKey, 86400, json_encode($allMembers)); // 缓存1天
    }

    // 计算总数
    $total = count($allMembers);

    // 分页
    $offset = ($page - 1) * $pageSize;
    $pagedMembers = array_slice($allMembers, $offset, $pageSize);

    // 获取成员详细信息
    $members = [];
    foreach ($pagedMembers as $member) {
        $userInfo = $this->userRepository->find($member['user_id']);
        if ($userInfo) {
            $members[] = [
                'user_id' => $userInfo->id,
                'username' => $userInfo->username,
                'avatar' => $userInfo->avatar,
                'level' => $member['level'],
                'created_at' => $member['created_at']
            ];
        }
    }

    // 返回结果
    return [
        'total' => $total,
        'page' => $page,
        'page_size' => $pageSize,
        'total_pages' => ceil($total / $pageSize),
        'members' => $members
    ];
}

/**
 * 递归计算用户的所有团队成员
 */
private function calculateAllPromotionMembers(int $userId, array &$allMembers, int $level = 0, int $maxLevel = 20): void
{
    // 限制最大层级
    if ($level >= $maxLevel) {
        return;
    }

    // 获取直接下级
    $directMembers = $this->referralRepository->getDirectMembers($userId);

    foreach ($directMembers as $member) {
        $memberId = $member->user_id;

        // 添加到结果集
        $allMembers[] = [
            'user_id' => $memberId,
            'level' => $level == 0 ? 1 : 2, // 1=直推, 2=间推
            'created_at' => $member->created_at
        ];

        // 递归获取下级的下级
        $this->calculateAllPromotionMembers($memberId, $allMembers, $level + 1, $maxLevel);
    }
}

/**
 * 清除推荐关系相关的缓存
 */
private function clearReferralCaches(int $userId, ?int $oldReferrerId, int $newReferrerId): void
{
    // 清除用户自身的缓存
    Redis::del("promotion:user:{$userId}:all_referrers");

    // 清除旧推荐人相关的缓存
    if ($oldReferrerId) {
        Redis::del("promotion:user:{$oldReferrerId}:all_members");

        // 获取旧推荐人的所有上级,清除他们的团队成员缓存
        $oldUpperReferrers = $this->getAllReferrers($oldReferrerId);
        foreach ($oldUpperReferrers as $referrer) {
            Redis::del("promotion:user:{$referrer['user_id']}:all_members");
        }
    }

    // 清除新推荐人相关的缓存
    Redis::del("promotion:user:{$newReferrerId}:all_members");

    // 获取新推荐人的所有上级,清除他们的团队成员缓存
    $newUpperReferrers = $this->getAllReferrers($newReferrerId);
    foreach ($newUpperReferrers as $referrer) {
        Redis::del("promotion:user:{$referrer['user_id']}:all_members");
    }

    // 获取用户的所有下级,清除他们的上级缓存
    $allMembers = $this->getAllPromotionMembers($userId);
    foreach ($allMembers['members'] as $member) {
        Redis::del("promotion:user:{$member['user_id']}:all_referrers");
    }
}

4.4 推荐码验证与使用记录

用户注册时,系统验证推荐码并获取推荐人ID。验证过程包括:

  1. 查询推荐码是否存在
  2. 验证推荐码的有效性(状态是否为有效)
  3. 检查推荐码是否已过期
  4. 更新推荐码的使用次数
  5. 记录邀请码使用情况(无论成功与否)
  6. 返回推荐码对应的推荐人ID

验证成功后,系统将使用返回的推荐人ID建立推荐关系。

系统会详细记录每次邀请码的使用情况,包括:

  1. 使用的邀请码:记录用户输入的邀请码
  2. 邀请码所有者:记录邀请码属于哪个用户
  3. 使用者信息:记录使用邀请码的用户ID、IP地址和用户代理
  4. 使用结果:记录使用是否成功,以及失败原因
  5. 使用时间:记录邀请码使用的时间

邀请码使用记录可用于:

  1. 邀请统计:统计用户成功邀请的人数
  2. 防止刷邀请:检测同一IP地址短时间内多次使用邀请码的情况
  3. 邀请追踪:追踪邀请链路,分析用户邀请行为
  4. 问题排查:当用户反馈邀请码问题时,可查询使用记录进行排查

4.5 邀请奖励发放

当用户成功邀请他人注册或被邀请用户达成特定条件(如升级、完成任务等)时,系统会发放邀请奖励。奖励发放流程包括:

  1. 验证用户关系:确认邀请人和被邀请人的推荐关系存在
  2. 获取奖励配置:根据奖励来源(注册、升级、任务等)获取对应的奖励配置
  3. 检查重复发放:确保同一来源的奖励不会重复发放(注册奖励除外)
  4. 创建奖励记录:在邀请奖励记录表中创建奖励记录
  5. 发放实际奖励:根据奖励类型(物品、货币、经验等)调用相应服务发放实际奖励
  6. 更新奖励状态:将奖励记录状态更新为已发放
  7. 触发奖励事件:触发邀请奖励发放事件,供其他模块响应

系统支持多种奖励类型:

  • 物品奖励:向用户背包添加指定物品
  • 货币奖励:向用户账户添加指定类型的货币
  • 经验奖励:为用户增加经验值

4.6 邀请奖励查询

系统提供多种方式查询邀请奖励记录,支持按用户、被邀请用户、奖励类型、来源和状态进行筛选:

  1. 获取用户的邀请奖励记录:查询用户获得的所有邀请奖励,支持分页和多种筛选条件
  2. 获取被邀请用户产生的奖励记录:查询特定被邀请用户为他人产生的所有奖励记录
  3. 统计用户邀请奖励总量:统计用户获得的各类型奖励总量和邀请总人数

查询结果包含详细的奖励信息,如奖励类型、数量、来源、状态等,以及相关用户的基本信息(用户名、头像等)。

4.7 邀请码使用记录查询

系统提供多种方式查询邀请码的使用记录,支持按邀请码、邀请码所有者、使用者和使用结果等条件进行筛选:

  1. 查询特定邀请码的使用记录:查询某个邀请码的所有使用记录,包括成功和失败的记录
  2. 查询用户使用的邀请码记录:查询特定用户使用过的所有邀请码记录
  3. 查询邀请码所有者的邀请记录:查询特定用户的邀请码被他人使用的记录
  4. 查询特定状态的邀请码使用记录:查询成功、失败或被撤销的邀请码使用记录
  5. 统计邀请码所有者的邀请人数:统计用户成功邀请的总人数
  6. 查询特定时间段的邀请记录:查询某个时间段内的邀请码使用情况
  7. 查询特定IP地址的邀请记录:查询来自特定IP地址的邀请码使用记录,用于防止刷邀请

查询结果包含详细的邀请码使用信息,如邀请码、使用者、使用时间、使用结果等,以及相关用户的基本信息。

5. 推荐关系存储策略

5.1 直接关系存储 + 缓存策略

团队模块采用只存储直接推荐关系,通过缓存或实时计算处理间接关系的策略。这种设计有以下优点:

  1. 数据结构简化:只需存储直接推荐关系,大幅减少数据表的记录数量
  2. 存储空间优化:减少了数据库存储空间的使用
  3. 关系修改灵活:修改推荐关系更加简单,只需更新一条记录
  4. 缓存加速查询:通过缓存加速查询,保持查询性能
  5. 按需计算:间接关系按需计算,减少不必要的计算

5.2 存储空间与查询效率的权衡

直接关系存储 + 缓存策略在存储空间和查询效率之间取得了良好的平衡:

存储方式 存储空间 查询效率 维护成本 修改灵活性
仅存储直推关系(无缓存) 低(需递归查询)
仅存储直推关系(有缓存) 低 + 缓存空间 高(缓存命中)/ 中(缓存未命中)
扁平化存储 高(一次查询)
直推关系 + 缓存表 高(一次查询)

对于团队模块,直接关系存储 + 缓存策略是最佳选择,因为:

  • 在保持较高查询效率的同时,大幅减少了存储空间
  • 提高了推荐关系修改的灵活性,支持用户更换上级
  • 缓存机制可以有效减轻递归查询的性能问题
  • 缓存策略可以根据业务需求灵活调整

5.3 缓存策略

为了提高查询效率,系统采用多层缓存策略:

5.3.1 内存缓存(Redis)

  1. 缓存内容

    • 用户的所有上级(包括直接和间接)
    • 用户的所有下级(包括直接和间接)
  2. 缓存键设计

    // 用户的所有上级
    key: promotion:user:{userId}:all_referrers
    value: [referrerId1, referrerId2, ...]
    
    // 用户的所有下级
    key: promotion:user:{userId}:all_members
    value: [memberId1, memberId2, ...]
    
  3. 缓存更新策略

    • 懒加载更新:当查询用户的上级或下级时,如果缓存不存在,则计算并缓存
    • 主动更新:当推荐关系发生变化时,主动更新相关用户的缓存
    • 定期刷新:定期任务刷新活跃用户的缓存,确保数据一致性
  4. 缓存过期时间

    • 设置合理的缓存过期时间,如1天
    • 对于活跃用户和大型团队的领导者,可以设置更长的缓存时间

5.3.2 数据库缓存表

为了进一步提高查询效率,特别是在Redis缓存失效或需要复杂查询时,系统引入了用户关系缓存表:

5.3.2.1 用户关系缓存表 (promotion_user_relation_cache)

用户关系缓存表是一个统一的缓存表,用于存储用户之间的所有推荐关系(包括上下级关系)。通过合理设计字段和索引,一张表可以同时满足查询用户上级和下级的需求,减少数据冗余,简化维护。

表结构设计

CREATE TABLE `promotion_user_relation_cache` (
  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
  `user_id` bigint(20) NOT NULL COMMENT '用户ID',
  `related_user_id` bigint(20) NOT NULL COMMENT '关联用户ID(上级)',
  `level` tinyint(3) unsigned NOT NULL COMMENT '关系层级:1直接,2间接',
  `path` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '关系路径,格式:1,2,3',
  `depth` tinyint(3) unsigned NOT NULL COMMENT '层级深度,从1开始',
  `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  PRIMARY KEY (`id`),
  UNIQUE KEY `idx_user_relation` (`user_id`,`related_user_id`),
  KEY `idx_user_id` (`user_id`),
  KEY `idx_related_user_id` (`related_user_id`),
  KEY `idx_level` (`level`),
  KEY `idx_depth` (`depth`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户关系缓存表';

字段说明

字段名 类型 说明
id bigint 主键ID
user_id bigint 用户ID(下级)
related_user_id bigint 关联用户ID(上级)
level tinyint 关系层级:1直接,2间接
path varchar 关系路径,格式:1,2,3
depth tinyint 层级深度,从1开始
created_at timestamp 创建时间
updated_at timestamp 更新时间

主要功能

  • 快速查询用户的所有上级(直推和间推)
  • 快速查询用户的所有下级(直推和间推)
  • 支持按层级、深度等条件进行复杂查询
  • 为收益分成系统和团队统计提供高效的数据支持

示例数据

id | user_id | related_user_id | level | path      | depth
---|---------|----------------|-------|-----------|------
1  | 4       | 3              | 1     | 3         | 1
2  | 4       | 2              | 2     | 3,2       | 2
3  | 4       | 1              | 2     | 3,2,1     | 3
4  | 3       | 2              | 1     | 2         | 1
5  | 3       | 1              | 2     | 2,1       | 2
6  | 2       | 1              | 1     | 1         | 1

在上面的示例中:

  • 用户4的直接上级是用户3(level=1)
  • 用户4的间接上级是用户2和用户1(level=2)
  • 用户3的直接上级是用户2(level=1)
  • 用户3的间接上级是用户1(level=2)
  • 用户2的直接上级是用户1(level=1)

查询示例

  1. 查询用户4的所有上级:

    SELECT * FROM promotion_user_relation_cache WHERE user_id = 4;
    
  2. 查询用户1的所有下级:

    SELECT * FROM promotion_user_relation_cache WHERE related_user_id = 1;
    
  3. 查询用户2的直接下级:

    SELECT u.* FROM users u
    JOIN promotion_user_relation_cache r ON u.id = r.user_id
    WHERE r.related_user_id = 2 AND r.level = 1;
    
  4. 查询用户1的第2代下级:

    SELECT u.* FROM users u
    JOIN promotion_user_relation_cache r ON u.id = r.user_id
    WHERE r.related_user_id = 1 AND r.depth = 2;
    
5.3.2.2 缓存表优势与应用场景

缓存表优势

  • 数据统一性:单一表存储所有关系,减少数据冗余,简化维护
  • 持久化存储:不受内存限制,数据持久保存
  • 复杂查询支持:支持SQL的各种复杂查询条件和聚合函数
  • 关联查询能力:可与其他表进行关联查询,扩展数据分析能力
  • 减轻内存压力:减少对Redis等内存缓存的依赖,降低缓存失效的影响
  • 高效统计分析:支持团队规模、层级分布等复杂统计分析
  • 存储空间优化:相比维护两张独立的缓存表,减少了约50%的存储空间

应用场景

  • 收益分成计算:快速查找用户的所有上级,计算分成
  • 团队规模统计:快速统计用户的团队规模,评定达人等级
  • 层级分布分析:分析团队的层级分布,了解团队结构
  • 特定代数查询:查询用户的特定代数下级,如20代以内的团队成员
  • 团队关系可视化:为团队树形图和关系图提供数据支持
  • 双向关系查询:同时支持上级和下级的高效查询
5.3.2.3 缓存表数据维护机制

初始化机制

  • 用户注册并建立推荐关系时,初始化相关缓存数据
  • 系统启动或升级时,可执行全量初始化,确保数据完整性

    /**
    * 初始化用户的关系缓存
    */
    public function initializeUserRelationCache(int $userId): int
    {
    // 删除用户现有缓存
    $this->relationCacheRepository->deleteByConditions([
        'user_id' => $userId
    ]);
    
    // 获取直接上级
    $directReferrer = $this->referralRepository->getDirectReferrer($userId);
    if (!$directReferrer) {
        return 0;
    }
    
    $count = 0;
    $referrerId = $directReferrer->referrer_id;
    
    // 添加直接上级缓存
    $this->relationCacheRepository->create([
        'user_id' => $userId,
        'related_user_id' => $referrerId,
        'level' => 1,
        'path' => (string)$referrerId,
        'depth' => 1
    ]);
    $count++;
    
    // 获取上级的所有上级
    $upperReferrers = $this->relationCacheRepository->findByConditions([
        'user_id' => $referrerId
    ]);
    
    // 添加间接上级缓存
    foreach ($upperReferrers as $upperReferrer) {
        $this->relationCacheRepository->create([
            'user_id' => $userId,
            'related_user_id' => $upperReferrer->related_user_id,
            'level' => 2,
            'path' => $referrerId . ',' . $upperReferrer->path,
            'depth' => $upperReferrer->depth + 1
        ]);
        $count++;
    }
    
    return $count;
    }
    

更新机制

  • 用户变更上级时,同步更新关系缓存
  • 推荐关系变化时,级联更新所有相关用户的缓存

    /**
    * 更新用户关系缓存
    */
    public function updateUserRelationCache(int $userId, int $oldReferrerId, int $newReferrerId): void
    {
    DB::transaction(function () use ($userId, $oldReferrerId, $newReferrerId) {
        // 1. 删除用户及其所有下级与旧上级及其所有上级的关系缓存
        if ($oldReferrerId) {
            // 获取用户的所有下级ID
            $downlineIds = $this->getDownlineIds($userId);
            $downlineIds[] = $userId; // 包括用户自身
    
            // 获取旧上级的所有上级ID
            $upperReferrerIds = $this->getUpperReferrerIds($oldReferrerId);
            $upperReferrerIds[] = $oldReferrerId; // 包括直接上级
    
            // 删除关系缓存
            foreach ($downlineIds as $downlineId) {
                foreach ($upperReferrerIds as $upperReferrerId) {
                    $this->relationCacheRepository->deleteByConditions([
                        'user_id' => $downlineId,
                        'related_user_id' => $upperReferrerId
                    ]);
                }
            }
        }
    
        // 2. 为用户及其所有下级创建与新上级及其所有上级的关系缓存
        if ($newReferrerId) {
            // 初始化用户的关系缓存
            $this->initializeUserRelationCache($userId);
    
            // 获取用户的所有下级
            $downlines = $this->getDownlines($userId);
    
            // 更新所有下级的关系缓存
            foreach ($downlines as $downline) {
                $this->initializeUserRelationCache($downline['user_id']);
            }
        }
    });
    }
    

校验与修复

  • 定期执行缓存数据校验,检查与实际推荐关系的一致性
  • 发现不一致时自动修复,确保数据准确性

    /**
    * 验证用户关系缓存
    */
    private function validateUserRelationCache(int $userId): bool
    {
    // 获取直接上级
    $directReferrer = $this->referralRepository->getDirectReferrer($userId);
    
    if (!$directReferrer) {
        // 用户没有上级,缓存应该为空
        $cacheCount = $this->relationCacheRepository->countByConditions([
            'user_id' => $userId
        ]);
        return $cacheCount === 0;
    }
    
    // 获取缓存的直接上级
    $cachedDirectReferrer = $this->relationCacheRepository->findByConditions([
        'user_id' => $userId,
        'level' => 1
    ])->first();
    
    if (!$cachedDirectReferrer || $cachedDirectReferrer->related_user_id != $directReferrer->referrer_id) {
        return false;
    }
    
    // 验证间接上级
    // 递归计算实际的间接上级
    $actualIndirectReferrers = $this->calculateIndirectReferrers($directReferrer->referrer_id);
    
    // 获取缓存的间接上级
    $cachedIndirectReferrers = $this->relationCacheRepository->findByConditions([
        'user_id' => $userId,
        'level' => 2
    ]);
    
    // 比较间接上级数量
    if (count($actualIndirectReferrers) != count($cachedIndirectReferrers)) {
        return false;
    }
    
    // 比较间接上级ID
    $actualReferrerIds = collect($actualIndirectReferrers)->pluck('referrer_id')->toArray();
    $cachedReferrerIds = $cachedIndirectReferrers->pluck('related_user_id')->toArray();
    
    sort($actualReferrerIds);
    sort($cachedReferrerIds);
    
    return $actualReferrerIds == $cachedReferrerIds;
    }
    

性能优化

  • 批量处理大规模数据更新
  • 异步处理非关键路径的缓存更新
  • 分片处理大型团队的缓存维护
  • 定期优化表和索引,保持查询性能
5.3.2.4 缓存表查询示例

查询用户的所有上级

/**
 * 获取用户的所有上级
 */
public function getAllReferrers(int $userId, int $level = 0, int $maxDepth = 20): array
{
    $conditions = ['user_id' => $userId];

    if ($level > 0) {
        $conditions['level'] = $level;
    }

    if ($maxDepth > 0) {
        $conditions['depth'] = ['<=', $maxDepth];
    }

    $relations = $this->relationCacheRepository->findByConditions($conditions);

    $referrers = [];
    foreach ($relations as $relation) {
        $referrers[] = [
            'user_id' => $relation->related_user_id,
            'level' => $relation->level,
            'depth' => $relation->depth,
            'path' => $relation->path
        ];
    }

    return $referrers;
}

查询用户的所有团队成员

/**
 * 获取用户的所有团队成员
 */
public function getAllPromotionMembers(int $userId, int $level = 0, int $maxDepth = 20, int $page = 1, int $pageSize = 20): array
{
    $conditions = ['related_user_id' => $userId];

    if ($level > 0) {
        $conditions['level'] = $level;
    }

    if ($maxDepth > 0) {
        $conditions['depth'] = ['<=', $maxDepth];
    }

    // 获取总数
    $total = $this->relationCacheRepository->countByConditions($conditions);

    // 分页查询
    $offset = ($page - 1) * $pageSize;
    $relations = $this->relationCacheRepository->findByConditionsPaginated($conditions, $offset, $pageSize);

    // 获取成员详细信息
    $members = [];
    foreach ($relations as $relation) {
        $userInfo = $this->userRepository->find($relation->user_id);
        if ($userInfo) {
            $members[] = [
                'user_id' => $userInfo->id,
                'username' => $userInfo->username,
                'avatar' => $userInfo->avatar,
                'level' => $relation->level,
                'depth' => $relation->depth,
                'path' => $relation->path,
                'created_at' => $userInfo->created_at
            ];
        }
    }

    return [
        'total' => $total,
        'page' => $page,
        'page_size' => $pageSize,
        'total_pages' => ceil($total / $pageSize),
        'members' => $members
    ];
}

查询特定代数的团队成员

/**
 * 获取用户特定代数的团队成员
 */
public function getPromotionMembersByDepth(int $userId, int $depth): array
{
    $relations = $this->relationCacheRepository->findByConditions([
        'related_user_id' => $userId,
        'depth' => $depth
    ]);

    $members = [];
    foreach ($relations as $relation) {
        $userInfo = $this->userRepository->find($relation->user_id);
        if ($userInfo) {
            $members[] = [
                'user_id' => $userInfo->id,
                'username' => $userInfo->username,
                'level' => $relation->level,
                'depth' => $relation->depth,
                'path' => $relation->path
            ];
        }
    }

    return $members;
}

统计团队规模

/**
 * 统计用户的团队规模
 */
public function countPromotionMembers(int $userId, int $level = 0): int
{
    $conditions = ['related_user_id' => $userId];

    if ($level > 0) {
        $conditions['level'] = $level;
    }

    return $this->relationCacheRepository->countByConditions($conditions);
}

5.4 关系建立流程

新用户注册并使用推荐码时,系统按以下流程建立推荐关系:

  1. 验证推荐码,获取直推推荐人ID
  2. 创建用户与直推推荐人的直推关系
  3. 清除相关缓存,包括用户自身、推荐人及其上级的缓存
  4. 更新推荐人及其上级的团队统计数据
  5. 触发推荐关系创建事件

这种方式简化了关系建立流程,同时通过缓存机制保持了查询效率。

6. 推荐码系统

6.1 推荐码生成算法

推荐码生成采用随机字符串算法,确保唯一性和安全性:

  1. 字符集:使用数字和大写字母(0-9, A-Z)组成的36个字符
  2. 长度:默认6位,可根据需求调整
  3. 唯一性检查:生成后检查数据库中是否已存在,如存在则重新生成
  4. 安全性:使用密码学安全的随机数生成器,避免可预测性

6.2 推荐码使用流程

推荐码的使用流程如下:

  1. 用户注册时输入推荐码
  2. 系统验证推荐码的有效性
  3. 获取推荐码对应的推荐人ID
  4. 建立用户与推荐人的推荐关系
  5. 更新推荐码的使用次数

6.3 推荐链接和二维码

除了纯文本推荐码外,系统还支持推荐链接和二维码:

  1. 推荐链接:格式为https://example.com/register?code=ABCDEF
  2. 推荐二维码:包含推荐链接的二维码图片

用户可以分享推荐链接或二维码,新用户通过链接注册时自动填入推荐码。

7. 推荐关系修改

7.1 推荐关系修改功能

在只存储直接推荐关系的设计下,系统可以支持修改用户的推荐关系(上级):

/**
 * 更新用户的推荐关系
 */
public function updateReferralRelation(int $userId, int $newReferrerId): bool
{
    return DB::transaction(function () use ($userId, $newReferrerId) {
        // 验证用户和新推荐人是否存在
        if (!$this->userRepository->exists($userId) || !$this->userRepository->exists($newReferrerId)) {
            throw new UserNotFoundException("用户或新推荐人不存在");
        }

        // 验证是否形成循环推荐
        if ($this->checkCircularReferral($userId, $newReferrerId)) {
            throw new ReferralException("不能形成循环推荐关系");
        }

        // 获取旧的直接推荐人
        $oldReferrer = $this->referralRepository->getDirectReferrer($userId);
        $oldReferrerId = $oldReferrer ? $oldReferrer->referrer_id : null;

        // 如果新旧推荐人相同,无需更新
        if ($oldReferrerId == $newReferrerId) {
            return true;
        }

        // 更新直接推荐关系
        if ($oldReferrerId) {
            $this->referralRepository->deleteDirectReferral($userId);
        }

        $this->referralRepository->createDirectReferral($userId, $newReferrerId);

        // 清除相关缓存
        $this->clearReferralCaches($userId, $oldReferrerId, $newReferrerId);

        // 更新旧推荐人的直推人数和团队人数
        if ($oldReferrerId) {
            $this->updatePromotionCounts($oldReferrerId);
            $this->talentService->checkAndUpdateTalentLevel($oldReferrerId);
        }

        // 更新新推荐人的直推人数和团队人数
        $this->updatePromotionCounts($newReferrerId);
        $this->talentService->checkAndUpdateTalentLevel($newReferrerId);

        // 记录修改历史
        $this->referralChangeRepository->create([
            'user_id' => $userId,
            'old_referrer_id' => $oldReferrerId,
            'new_referrer_id' => $newReferrerId,
            'change_time' => now(),
            'change_reason' => request('reason', ''),
            'changed_by' => Auth::id() ?? 0
        ]);

        // 触发推荐关系更新事件
        event(new ReferralUpdatedEvent($userId, $oldReferrerId, $newReferrerId));

        return true;
    });
}

7.2 推荐关系修改记录表

为了记录推荐关系的修改历史,系统使用以下表结构:

CREATE TABLE `promotion_referral_changes` (
  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
  `user_id` bigint(20) NOT NULL COMMENT '用户ID',
  `old_referrer_id` bigint(20) DEFAULT NULL COMMENT '旧推荐人ID',
  `new_referrer_id` bigint(20) NOT NULL COMMENT '新推荐人ID',
  `change_time` timestamp NOT NULL COMMENT '修改时间',
  `change_reason` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '修改原因',
  `changed_by` bigint(20) NOT NULL COMMENT '操作人ID',
  `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  PRIMARY KEY (`id`),
  KEY `idx_user_id` (`user_id`),
  KEY `idx_change_time` (`change_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='推荐关系修改记录表';

7.3 修改限制策略

为了防止频繁修改推荐关系导致系统不稳定,系统实施以下限制策略:

/**
 * 验证用户是否可以修改推荐关系
 */
private function validateReferralChangeEligibility(int $userId): void
{
    // 检查修改次数限制
    $changeCount = $this->referralChangeRepository->countRecentChanges($userId, 180); // 180天内
    if ($changeCount >= 1) {
        throw new ReferralException("180天内只能修改一次推荐关系");
    }

    // 检查注册时间限制
    $user = $this->userRepository->find($userId);
    if ($user->created_at->diffInDays(now()) < 30) {
        throw new ReferralException("注册30天内不能修改推荐关系");
    }

    // 检查团队规模限制
    $promotionCount = $this->calculatePromotionCount($userId);
    if ($promotionCount > 100) {
        throw new ReferralException("团队规模超过100人不能修改推荐关系");
    }
}

8. 防循环推荐机制

8.1 循环推荐问题

循环推荐是指用户A推荐用户B,用户B又推荐用户A的情况,或者形成更长的循环链。这种情况会导致:

  1. 无限循环的推荐关系
  2. 收益分成的循环计算
  3. 团队统计数据的错误

8.2 检测和防止机制

系统通过以下机制检测和防止循环推荐:

/**
 * 检查是否形成循环推荐
 */
private function checkCircularReferral(int $userId, int $referrerId): bool
{
    // 如果用户ID和推荐人ID相同,直接形成循环
    if ($userId == $referrerId) {
        return true;
    }

    // 获取用户的所有下级
    $allMembers = $this->getAllPromotionMembers($userId);

    // 检查新推荐人是否在用户的下级中
    foreach ($allMembers['members'] as $member) {
        if ($member['user_id'] == $referrerId) {
            return true;
        }
    }

    return false;
}

此外,系统还实施以下规则:

  1. 修改限制:限制用户修改推荐关系的频率和条件
  2. 层级限制:间推关系最多支持20层,防止过深的推荐链
  3. 状态验证:只有状态正常的用户才能成为推荐人

9. 团队统计

9.1 直推人数统计

系统实时统计用户的直推人数:

/**
 * 统计用户的直推人数
 */
public function countDirectReferrals(int $userId): int
{
    return $this->referralRepository->countByConditions([
        'referrer_id' => $userId,
        'level' => 1
    ]);
}

9.2 团队总人数统计

系统实时统计用户的团队总人数(包括直推和间推):

/**
 * 统计用户的团队总人数
 */
public function countPromotionMembers(int $userId): int
{
    return $this->referralRepository->countByConditions([
        'referrer_id' => $userId
    ]);
}

9.3 团队活跃度统计

系统支持统计团队的活跃度,用于评估团队质量:

/**
 * 统计用户团队的活跃度
 */
public function calculatePromotionActivity(int $userId, int $days = 7): array
{
    // 获取团队成员ID列表
    $memberIds = $this->referralRepository->getPromotionMemberIds($userId);

    // 统计活跃用户数
    $activeCount = $this->userActivityRepository->countActiveUsers($memberIds, $days);

    // 计算活跃率
    $totalCount = count($memberIds);
    $activeRate = $totalCount > 0 ? $activeCount / $totalCount : 0;

    return [
        'total_members' => $totalCount,
        'active_members' => $activeCount,
        'active_rate' => $activeRate,
        'days' => $days
    ];
}

10. 推荐关系可视化

10.1 团队树形图

系统支持生成团队的树形结构图,直观展示推荐关系:

/**
 * 生成用户的团队树形图
 */
public function generatePromotionTree(int $userId, int $maxDepth = 3): array
{
    // 获取用户信息
    $user = $this->userRepository->find($userId);
    if (!$user) {
        return [];
    }

    // 构建根节点
    $root = [
        'id' => $user->id,
        'name' => $user->username,
        'avatar' => $user->avatar,
        'children' => []
    ];

    // 递归构建团队树
    $this->buildPromotionTreeNode($root, 1, $maxDepth);

    return $root;
}

/**
 * 递归构建团队树节点
 */
private function buildPromotionTreeNode(array &$node, int $currentDepth, int $maxDepth): void
{
    // 达到最大深度,停止递归
    if ($currentDepth >= $maxDepth) {
        return;
    }

    // 获取直推成员
    $directMembers = $this->referralRepository->findByConditions([
        'referrer_id' => $node['id'],
        'level' => 1
    ]);

    // 构建子节点
    foreach ($directMembers as $member) {
        $user = $this->userRepository->find($member->user_id);
        if ($user) {
            $childNode = [
                'id' => $user->id,
                'name' => $user->username,
                'avatar' => $user->avatar,
                'children' => []
            ];

            // 递归构建子节点
            $this->buildPromotionTreeNode($childNode, $currentDepth + 1, $maxDepth);

            // 添加到父节点
            $node['children'][] = $childNode;
        }
    }
}

10.2 团队关系图

系统支持生成团队的关系图,展示成员之间的连接:

/**
 * 生成用户的团队关系图
 */
public function generatePromotionGraph(int $userId, int $maxMembers = 50): array
{
    // 获取团队成员
    $members = $this->getPromotionMembers($userId, 0, 1, $maxMembers)['members'];

    // 构建节点
    $nodes = [];
    $nodes[] = [
        'id' => $userId,
        'name' => $this->userRepository->find($userId)->username,
        'type' => 'root'
    ];

    foreach ($members as $member) {
        $nodes[] = [
            'id' => $member['user_id'],
            'name' => $member['username'],
            'type' => $member['level'] == 1 ? 'direct' : 'indirect'
        ];
    }

    // 构建边
    $edges = [];
    foreach ($members as $member) {
        $edges[] = [
            'source' => $member['level'] == 1 ? $userId : $this->getDirectReferrerId($member['user_id']),
            'target' => $member['user_id'],
            'type' => $member['level'] == 1 ? 'direct' : 'indirect'
        ];
    }

    return [
        'nodes' => $nodes,
        'edges' => $edges
    ];
}

11. 与其他系统的集成

11.1 与用户系统的集成

推荐关系系统与用户系统紧密集成:

  1. 用户注册:用户注册时处理推荐码,建立推荐关系
  2. 用户状态:用户状态变更(如封禁、注销)会影响推荐关系
  3. 用户信息:推荐关系展示需要用户的基本信息(昵称、头像等)

11.2 与达人等级系统的集成

推荐关系系统为达人等级系统提供基础数据:

  1. 团队规模:直推人数和团队总人数用于计算达人等级
  2. 团队活跃度:团队活跃度可作为达人等级的附加条件
  3. 等级变更:推荐关系变化可能触发达人等级变更

11.3 与收益分成系统的集成

推荐关系系统为收益分成系统提供关系数据:

  1. 分成对象:确定哪些上级可以获得分成
  2. 分成比例:根据推荐关系类型(直推/间推)确定基础分成比例
  3. 分成计算:提供推荐链路,用于计算多级分成

12. 性能优化

12.1 查询优化

针对推荐关系的高频查询,系统采用以下优化措施:

  1. 索引优化:为user_idreferrer_idlevel字段建立合适的索引
  2. 复合索引:为常用的查询条件组合建立复合索引
  3. 查询缓存:缓存频繁查询的结果,如用户的直推人数和团队总人数
  4. 分页查询:大数据量查询采用分页机制,避免一次加载过多数据

12.2 缓存策略

系统采用多级缓存策略提高性能:

  1. 本地缓存:缓存单次请求中重复使用的数据
  2. 分布式缓存:使用Redis缓存跨请求的数据
  3. 缓存更新:推荐关系变更时主动更新缓存
  4. 缓存失效:设置合理的缓存过期时间,平衡实时性和性能

12.3 批量操作

对于大规模数据操作,系统采用批量处理方式:

  1. 批量插入:一次插入多条推荐关系记录
  2. 批量更新:一次更新多个用户的团队统计数据
  3. 异步处理:耗时操作放入队列异步处理
  4. 定时任务:定期执行统计和维护任务,避免实时计算

13. 安全性考虑

13.1 防刷机制

防止恶意用户刷取推荐关系:

  1. IP限制:限制同一IP短时间内的注册次数
  2. 设备限制:限制同一设备短时间内的注册次数
  3. 验证码:注册时使用验证码防止自动注册
  4. 人工审核:对异常推荐关系进行人工审核

13.2 数据验证

严格验证输入数据,确保数据安全:

  1. 用户验证:验证用户和推荐人的存在性和有效性
  2. 关系验证:验证推荐关系的合法性,防止循环推荐
  3. 推荐码验证:验证推荐码的有效性和状态
  4. 参数验证:验证API参数的合法性和范围

13.3 权限控制

实施严格的权限控制,保护用户数据:

  1. 用户权限:用户只能查看自己的团队数据
  2. 管理权限:只有管理员可以查看和修改所有推荐关系
  3. 操作日志:记录关键操作的日志,便于审计和追踪
  4. 数据脱敏:展示团队数据时对敏感信息进行脱敏

14. 总结

推荐关系系统是团队模块的核心组成部分,通过建立用户间的直推和间推关系,形成多层级的团队结构,为收益分成和达人等级提供基础数据支持。系统采用只存储直接推荐关系 + 缓存策略的设计,在保持较高查询效率的同时,大幅减少了存储空间,并提高了推荐关系修改的灵活性。

系统实现了推荐码生成、推荐关系修改、防循环推荐、团队统计和可视化等功能,通过缓存机制优化了查询性能,同时保持了数据的一致性。通过与用户系统、达人等级系统和收益分成系统的紧密集成,推荐关系系统为团队模块的其他功能提供了坚实的基础。

这种设计方案特别适合需要支持推荐关系修改的场景,用户可以在特定条件下更换上级,系统会自动处理相关的缓存更新和团队统计数据更新,确保数据的一致性和完整性。