|
|
@@ -0,0 +1,1424 @@
|
|
|
+# 推荐关系系统
|
|
|
+
|
|
|
+## 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 推荐关系建立
|
|
|
+
|
|
|
+当新用户注册并使用推荐码时,系统会建立推荐关系:
|
|
|
+
|
|
|
+```php
|
|
|
+/**
|
|
|
+ * 建立推荐关系
|
|
|
+ */
|
|
|
+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 推荐码生成
|
|
|
+
|
|
|
+系统为每个用户生成唯一的推荐码,用于邀请新用户:
|
|
|
+
|
|
|
+```php
|
|
|
+/**
|
|
|
+ * 生成推荐码
|
|
|
+ */
|
|
|
+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 推荐关系查询
|
|
|
+
|
|
|
+系统提供多种方式查询推荐关系,使用缓存优化查询性能:
|
|
|
+
|
|
|
+```php
|
|
|
+/**
|
|
|
+ * 获取用户的所有上级(包括直接和间接)
|
|
|
+ */
|
|
|
+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)
|
|
|
+
|
|
|
+用户关系缓存表是一个统一的缓存表,用于存储用户之间的所有推荐关系(包括上下级关系)。通过合理设计字段和索引,一张表可以同时满足查询用户上级和下级的需求,减少数据冗余,简化维护。
|
|
|
+
|
|
|
+**表结构设计**:
|
|
|
+
|
|
|
+```sql
|
|
|
+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的所有上级:
|
|
|
+```sql
|
|
|
+SELECT * FROM promotion_user_relation_cache WHERE user_id = 4;
|
|
|
+```
|
|
|
+
|
|
|
+2. 查询用户1的所有下级:
|
|
|
+```sql
|
|
|
+SELECT * FROM promotion_user_relation_cache WHERE related_user_id = 1;
|
|
|
+```
|
|
|
+
|
|
|
+3. 查询用户2的直接下级:
|
|
|
+```sql
|
|
|
+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代下级:
|
|
|
+```sql
|
|
|
+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 缓存表数据维护机制
|
|
|
+
|
|
|
+**初始化机制**:
|
|
|
+- 用户注册并建立推荐关系时,初始化相关缓存数据
|
|
|
+- 系统启动或升级时,可执行全量初始化,确保数据完整性
|
|
|
+
|
|
|
+```php
|
|
|
+/**
|
|
|
+ * 初始化用户的关系缓存
|
|
|
+ */
|
|
|
+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;
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+**更新机制**:
|
|
|
+- 用户变更上级时,同步更新关系缓存
|
|
|
+- 推荐关系变化时,级联更新所有相关用户的缓存
|
|
|
+
|
|
|
+```php
|
|
|
+/**
|
|
|
+ * 更新用户关系缓存
|
|
|
+ */
|
|
|
+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']);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ });
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+**校验与修复**:
|
|
|
+- 定期执行缓存数据校验,检查与实际推荐关系的一致性
|
|
|
+- 发现不一致时自动修复,确保数据准确性
|
|
|
+
|
|
|
+```php
|
|
|
+/**
|
|
|
+ * 验证用户关系缓存
|
|
|
+ */
|
|
|
+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 缓存表查询示例
|
|
|
+
|
|
|
+**查询用户的所有上级**:
|
|
|
+```php
|
|
|
+/**
|
|
|
+ * 获取用户的所有上级
|
|
|
+ */
|
|
|
+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;
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+**查询用户的所有团队成员**:
|
|
|
+```php
|
|
|
+/**
|
|
|
+ * 获取用户的所有团队成员
|
|
|
+ */
|
|
|
+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
|
|
|
+ ];
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+**查询特定代数的团队成员**:
|
|
|
+```php
|
|
|
+/**
|
|
|
+ * 获取用户特定代数的团队成员
|
|
|
+ */
|
|
|
+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;
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+**统计团队规模**:
|
|
|
+```php
|
|
|
+/**
|
|
|
+ * 统计用户的团队规模
|
|
|
+ */
|
|
|
+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 推荐关系修改功能
|
|
|
+
|
|
|
+在只存储直接推荐关系的设计下,系统可以支持修改用户的推荐关系(上级):
|
|
|
+
|
|
|
+```php
|
|
|
+/**
|
|
|
+ * 更新用户的推荐关系
|
|
|
+ */
|
|
|
+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 推荐关系修改记录表
|
|
|
+
|
|
|
+为了记录推荐关系的修改历史,系统使用以下表结构:
|
|
|
+
|
|
|
+```sql
|
|
|
+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 修改限制策略
|
|
|
+
|
|
|
+为了防止频繁修改推荐关系导致系统不稳定,系统实施以下限制策略:
|
|
|
+
|
|
|
+```php
|
|
|
+/**
|
|
|
+ * 验证用户是否可以修改推荐关系
|
|
|
+ */
|
|
|
+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 检测和防止机制
|
|
|
+
|
|
|
+系统通过以下机制检测和防止循环推荐:
|
|
|
+
|
|
|
+```php
|
|
|
+/**
|
|
|
+ * 检查是否形成循环推荐
|
|
|
+ */
|
|
|
+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 直推人数统计
|
|
|
+
|
|
|
+系统实时统计用户的直推人数:
|
|
|
+
|
|
|
+```php
|
|
|
+/**
|
|
|
+ * 统计用户的直推人数
|
|
|
+ */
|
|
|
+public function countDirectReferrals(int $userId): int
|
|
|
+{
|
|
|
+ return $this->referralRepository->countByConditions([
|
|
|
+ 'referrer_id' => $userId,
|
|
|
+ 'level' => 1
|
|
|
+ ]);
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+### 9.2 团队总人数统计
|
|
|
+
|
|
|
+系统实时统计用户的团队总人数(包括直推和间推):
|
|
|
+
|
|
|
+```php
|
|
|
+/**
|
|
|
+ * 统计用户的团队总人数
|
|
|
+ */
|
|
|
+public function countPromotionMembers(int $userId): int
|
|
|
+{
|
|
|
+ return $this->referralRepository->countByConditions([
|
|
|
+ 'referrer_id' => $userId
|
|
|
+ ]);
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+### 9.3 团队活跃度统计
|
|
|
+
|
|
|
+系统支持统计团队的活跃度,用于评估团队质量:
|
|
|
+
|
|
|
+```php
|
|
|
+/**
|
|
|
+ * 统计用户团队的活跃度
|
|
|
+ */
|
|
|
+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 团队树形图
|
|
|
+
|
|
|
+系统支持生成团队的树形结构图,直观展示推荐关系:
|
|
|
+
|
|
|
+```php
|
|
|
+/**
|
|
|
+ * 生成用户的团队树形图
|
|
|
+ */
|
|
|
+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 团队关系图
|
|
|
+
|
|
|
+系统支持生成团队的关系图,展示成员之间的连接:
|
|
|
+
|
|
|
+```php
|
|
|
+/**
|
|
|
+ * 生成用户的团队关系图
|
|
|
+ */
|
|
|
+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_id`、`referrer_id`和`level`字段建立合适的索引
|
|
|
+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. 总结
|
|
|
+
|
|
|
+推荐关系系统是团队模块的核心组成部分,通过建立用户间的直推和间推关系,形成多层级的团队结构,为收益分成和达人等级提供基础数据支持。系统采用只存储直接推荐关系 + 缓存策略的设计,在保持较高查询效率的同时,大幅减少了存储空间,并提高了推荐关系修改的灵活性。
|
|
|
+
|
|
|
+系统实现了推荐码生成、推荐关系修改、防循环推荐、团队统计和可视化等功能,通过缓存机制优化了查询性能,同时保持了数据的一致性。通过与用户系统、达人等级系统和收益分成系统的紧密集成,推荐关系系统为团队模块的其他功能提供了坚实的基础。
|
|
|
+
|
|
|
+这种设计方案特别适合需要支持推荐关系修改的场景,用户可以在特定条件下更换上级,系统会自动处理相关的缓存更新和团队统计数据更新,确保数据的一致性和完整性。
|