MemberSuperiorService.php 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117
  1. <?php
  2. namespace App\Services;
  3. use App\Models\Agent\Agent;
  4. use App\Models\User;
  5. use App\Services\Sharing\ShareRelations;
  6. use Illuminate\Support\Facades\DB;
  7. use Illuminate\Support\Facades\Schema;
  8. /**
  9. * 后台会员列表:给没有上级(无合营代理、无邀请人)的会员补绑上级。
  10. * type=agent:agent_username 为代理账号(agents.username),写 users.agent_id;
  11. * type=member:superior_id 为上级会员的会员 ID(users.member_id),写 users.agent_user_code 并挂到分享关系树。
  12. * 上级只有两种:会员邀请码,或合营代理(绑定域名注册)。入股代理不发展会员,不能当作上级。
  13. * 已有合营代理、能对上的邀请人,或分享关系树里已有上级的会员不能改绑。
  14. */
  15. class MemberSuperiorService
  16. {
  17. public const TYPE_AGENT = 'agent';
  18. public const TYPE_MEMBER = 'member';
  19. public function bind(string $memberId, string $type, string $target): array
  20. {
  21. return DB::transaction(function () use ($memberId, $type, $target) {
  22. // 与注册、分享关系绑定同一把锁,先锁关系再锁会员,避免死锁和并发重复绑定
  23. ShareRelations::lock();
  24. $user = User::query()->where('member_id', $memberId)->lockForUpdate()->first();
  25. if (!$user) throw new \RuntimeException('会员不存在');
  26. if ((int) $user->from === 2) throw new \RuntimeException('试玩会员不能绑定上级');
  27. if ($this->hasSuperior($user)) throw new \RuntimeException('会员已有上级,不能修改');
  28. if ($type === self::TYPE_AGENT) {
  29. $agent = Agent::query()->where('username', $target)->lockForUpdate()->first();
  30. if (!$agent) throw new \RuntimeException('代理不存在');
  31. if (!$agent->isJoint()) throw new \RuntimeException('只能绑定合营代理');
  32. if ((int) $agent->status !== Agent::STATUS_ENABLED) throw new \RuntimeException('代理已禁用');
  33. $user->agent_id = $agent->id;
  34. $user->agent_user_code = null;
  35. $user->save();
  36. return ['member_id' => (string) $user->member_id, 'type' => $type, 'agent_id' => (int) $agent->id, 'account' => (string) $agent->username];
  37. }
  38. if ($type !== self::TYPE_MEMBER) throw new \RuntimeException('绑定类型错误');
  39. $parent = User::query()->where('member_id', $target)->first();
  40. if (!$parent) throw new \RuntimeException('上级会员不存在');
  41. if ((int) $parent->id === (int) $user->id) throw new \RuntimeException('不能绑定自己');
  42. if ((int) $parent->from === 2) throw new \RuntimeException('试玩会员不能作为上级');
  43. $parentCode = $this->code($parent);
  44. if ($parentCode === '') throw new \RuntimeException('上级会员没有邀请码');
  45. $ownCode = $this->code($user);
  46. if ($ownCode === '') throw new \RuntimeException('会员邀请码为空,请先人工核对');
  47. // 校验上级关系已初始化、不能绑定自己的下级(成环),并写入分享关系树
  48. (new ShareRelations())->bind((int) $user->id, (int) $parent->id, $ownCode);
  49. $user->agent_id = null;
  50. $user->agent_user_code = $parentCode;
  51. $user->save();
  52. return ['member_id' => (string) $user->member_id, 'type' => $type, 'superior_member_id' => (string) $parent->member_id,
  53. 'account' => (string) ($parent->account ?: $parent->first_name)];
  54. });
  55. }
  56. public function hasSuperior(User $user): bool
  57. {
  58. return self::hasJointAgent($user) || self::hasMemberSuperior($user);
  59. }
  60. public static function hasJointAgent(User $user): bool
  61. {
  62. return $user->agent_id !== null && Agent::query()->whereKey($user->agent_id)
  63. ->where('type', Agent::TYPE_JOINT)->exists();
  64. }
  65. public static function hasMemberSuperior(User $user): bool
  66. {
  67. return array_key_exists($user->id, self::memberParentIds([$user]));
  68. }
  69. /** 已固化的上级优先。历史邀请码匹配异常时记录空值,保留绑定限制但不展示上级。 */
  70. public static function memberParentIds(iterable $users): array
  71. {
  72. $userIds = $legacyCodes = [];
  73. foreach ($users as $user) {
  74. $userIds[] = $user->id;
  75. $code = trim((string) $user->agent_user_code);
  76. if ($code !== '' && $code !== '0') $legacyCodes[$user->id] = $code;
  77. }
  78. if ($userIds === []) return [];
  79. $fixedParents = Schema::hasTable('share_members')
  80. ? DB::table('share_members')->whereIn('user_id', $userIds)->whereNotNull('parent_user_id')
  81. ->pluck('parent_user_id', 'user_id')->all()
  82. : [];
  83. $legacyCodes = array_diff_key($legacyCodes, $fixedParents);
  84. if ($legacyCodes === []) return $fixedParents;
  85. $codes = array_unique(array_merge($legacyCodes, array_map('strtoupper', $legacyCodes)));
  86. $matches = User::query()->whereIn('user_code', $codes)->get(['id', 'user_code', 'from'])
  87. ->groupBy(fn (User $inviter) => strtoupper(trim((string) $inviter->user_code)));
  88. foreach ($legacyCodes as $userId => $code) {
  89. $candidates = $matches->get(strtoupper($code));
  90. if (!$candidates) continue;
  91. $inviter = $candidates->count() === 1 ? $candidates->first() : null;
  92. $fixedParents[$userId] = $inviter && (int) $inviter->id !== (int) $userId && (int) $inviter->from !== 2
  93. ? $inviter->id : null;
  94. }
  95. return $fixedParents;
  96. }
  97. /** 会员自己的邀请码:分享关系表固化的优先,其次 users.user_code。 */
  98. private function code(User $user): string
  99. {
  100. $code = trim((string) DB::table('share_members')->where('user_id', $user->id)->value('invitation_code'));
  101. if ($code === '' || $code === '0') $code = trim((string) ($user->user_code ?? ''));
  102. return $code === '0' ? '' : $code;
  103. }
  104. }