ShareRelations.php 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152
  1. <?php
  2. namespace App\Services\Sharing;
  3. use Carbon\Carbon;
  4. use App\Models\User;
  5. use Illuminate\Support\Facades\DB;
  6. class ShareRelations
  7. {
  8. public function initialize(bool $write = false): array
  9. {
  10. if (!$write) return $this->inspect(false);
  11. return DB::transaction(function () {
  12. $this->lock();
  13. return $this->inspect(true);
  14. });
  15. }
  16. private function inspect(bool $write): array
  17. {
  18. $users = DB::table('users')->get(['id','user_code','agent_user_code','from','created_at'])->keyBy('id');
  19. $existing = DB::table('share_members')->get()->keyBy('user_id');
  20. $assigned = 0;
  21. foreach ($users as $id=>$user) {
  22. if ((int)$user->from === 2) continue;
  23. $old=(string)($user->user_code ?? '');
  24. $stable=(string)($existing[$id]->invitation_code ?? '');
  25. if ($old === '' || $old === '0') {
  26. $assigned++;
  27. if (!$write) continue;
  28. $code=($stable !== '' && $stable !== '0') ? $stable : User::generateUserCode();
  29. if (DB::table('users')->where('user_code',$code)->where('id','<>',$id)->exists()) throw new \RuntimeException('固化邀请码已被其他会员占用,请人工核对:'.$id);
  30. DB::table('users')->where('id',$id)->update(['user_code'=>$code]);
  31. $user->user_code=$code;
  32. }
  33. if ($write && $existing->has($id) && ($stable === '' || $stable === '0')) {
  34. DB::table('share_members')->where('user_id',$id)->update(['invitation_code'=>(string)$user->user_code]);
  35. $existing[$id]->invitation_code=(string)$user->user_code;
  36. }
  37. }
  38. $codes = [];
  39. foreach ($users as $u) if ((string)$u->user_code !== '' && (string)$u->user_code !== '0') $codes[strtoupper(trim((string)$u->user_code))][] = (int)$u->id;
  40. $parents = []; $statuses = []; $errors = [];
  41. foreach ($users as $u) {
  42. $id = (int)$u->id;
  43. if ($existing->has($id)) {
  44. $parents[$id] = $existing[$id]->parent_user_id === null ? null : (int)$existing[$id]->parent_user_id;
  45. $statuses[$id] = $existing[$id]->binding_status;
  46. continue;
  47. }
  48. $code = strtoupper(trim((string)($u->agent_user_code ?? '')));
  49. $matches = $codes[$code] ?? [];
  50. $parents[$id] = null;
  51. $statuses[$id] = 'root';
  52. if ((int)$u->from === 2 || $code === '') continue;
  53. if ($code === '0' || count($matches) === 0) $statuses[$id] = 'missing';
  54. elseif (count($matches) > 1) $statuses[$id] = 'ambiguous';
  55. elseif ((int)$users[$matches[0]]->from === 2) $statuses[$id] = 'missing';
  56. else { $parents[$id] = $matches[0]; $statuses[$id] = 'matched'; }
  57. }
  58. // Only actual cycle edges are removed. Confirmed children outside the cycle keep their identity.
  59. $done = [];
  60. foreach (array_keys($parents) as $id) {
  61. $path = []; $at = $id;
  62. while ($at !== null && isset($parents[$at]) && !isset($done[$at])) {
  63. if (isset($path[$at])) {
  64. $cycle = array_slice(array_keys($path), $path[$at]);
  65. foreach ($cycle as $member) {
  66. if ($existing->has($member)) throw new \RuntimeException('已固化的推荐关系存在循环,禁止自动改绑:' . $member);
  67. $parents[$member] = null; $statuses[$member] = 'cycle';
  68. }
  69. break;
  70. }
  71. $path[$at] = count($path); $at = $parents[$at];
  72. }
  73. foreach ($path as $member => $_) $done[$member] = true;
  74. }
  75. foreach ($statuses as $id => $status) if (in_array($status,['missing','ambiguous','cycle'],true)) $errors[] = ['user_id'=>$id,'status'=>$status];
  76. $result = ['users'=>$users->count(),'created'=>0,'existing'=>$existing->count(),'invitation_codes_assigned'=>$assigned,'errors'=>$errors,'written'=>$write];
  77. if (!$write) { $result['created'] = $users->count() - $existing->count(); return $result; }
  78. $now = Carbon::now(ShareDefaults::TIMEZONE)->format('Y-m-d H:i:s');
  79. $initial = DB::table('share_system_state')->where('key','initialized_at')->value('value') === null;
  80. foreach ($users as $u) {
  81. $id = (int)$u->id;
  82. if ($existing->has($id)) continue;
  83. DB::table('share_members')->insert(['user_id'=>$id,'parent_user_id'=>$parents[$id],
  84. 'invitation_code'=>(string)($u->user_code ?? ''),'binding_status'=>$statuses[$id],
  85. 'bound_at'=>$initial ? '1970-01-01 00:00:00' : ($u->created_at ?: $now), 'created_at'=>$now,'updated_at'=>$now]);
  86. $result['created']++;
  87. }
  88. $members = DB::table('share_members')->get()->keyBy('user_id');
  89. foreach ($members as $member) {
  90. $id = (int)$member->user_id; $at = $id; $depth = 0; $seen = [];
  91. $bound = '1970-01-01 00:00:00'; $rows = [];
  92. while ($at !== null && $members->has($at)) {
  93. if (isset($seen[$at])) throw new \RuntimeException('推荐关系循环');
  94. $seen[$at] = true;
  95. $rows[] = ['ancestor_user_id'=>$at,'descendant_user_id'=>$id,'depth'=>$depth,'bound_at'=>$bound];
  96. $edge = $members[$at];
  97. if ($edge->parent_user_id === null) break;
  98. $bound = max($bound,(string)$edge->bound_at);
  99. $at = (int)$edge->parent_user_id; $depth++;
  100. }
  101. foreach (array_chunk($rows,500) as $chunk) DB::table('share_tree')->insertOrIgnore($chunk);
  102. DB::table('share_accounts')->insertOrIgnore(['user_id'=>$id,'created_at'=>$now,'updated_at'=>$now]);
  103. }
  104. DB::table('share_system_state')->updateOrInsert(['key'=>'initialized_at'],['value'=>$now,'created_at'=>$now,'updated_at'=>$now]);
  105. return $result;
  106. }
  107. public function bind(int $userId, ?int $parentId, string $code, string $status = 'matched'): void
  108. {
  109. DB::transaction(function () use ($userId,$parentId,$code,$status) {
  110. $this->lock();
  111. $now = Carbon::now(ShareDefaults::TIMEZONE)->format('Y-m-d H:i:s');
  112. $member = DB::table('share_members')->where('user_id',$userId)->lockForUpdate()->first();
  113. if ($member && $member->parent_user_id !== null) {
  114. if ((int)$member->parent_user_id !== $parentId) throw new \RuntimeException('上级关系已固定,不能改绑');
  115. return;
  116. }
  117. if ($parentId !== null) {
  118. $parent = DB::table('share_members')->where('user_id',$parentId)->first();
  119. if (!$parent) throw new \RuntimeException('推广会员关系尚未初始化');
  120. if ($parentId === $userId || DB::table('share_tree')->where('ancestor_user_id',$userId)->where('descendant_user_id',$parentId)->exists()) throw new \RuntimeException('不能绑定自己或自己的下级');
  121. $user = DB::table('users')->where('id',$parentId)->first();
  122. if (!$user || (int)$user->from === 2) throw new \RuntimeException('试玩会员不能作为推广人');
  123. }
  124. if (!$member) {
  125. DB::table('share_members')->insert(['user_id'=>$userId,'parent_user_id'=>null,'invitation_code'=>$code,'binding_status'=>'root','bound_at'=>$now,'created_at'=>$now,'updated_at'=>$now]);
  126. DB::table('share_tree')->insertOrIgnore(['ancestor_user_id'=>$userId,'descendant_user_id'=>$userId,'depth'=>0,'bound_at'=>'1970-01-01 00:00:00']);
  127. DB::table('share_accounts')->insertOrIgnore(['user_id'=>$userId,'created_at'=>$now,'updated_at'=>$now]);
  128. }
  129. if ($parentId === null) return;
  130. $ancestors = DB::table('share_tree')->where('descendant_user_id',$parentId)->get();
  131. $descendants = DB::table('share_tree')->where('ancestor_user_id',$userId)->get();
  132. foreach ($ancestors as $ancestor) {
  133. $rows = [];
  134. foreach ($descendants as $descendant) $rows[] = ['ancestor_user_id'=>$ancestor->ancestor_user_id,
  135. 'descendant_user_id'=>$descendant->descendant_user_id,'depth'=>$ancestor->depth+1+$descendant->depth,
  136. 'bound_at'=>max($now,(string)$ancestor->bound_at,(string)$descendant->bound_at)];
  137. foreach (array_chunk($rows,500) as $chunk) DB::table('share_tree')->insertOrIgnore($chunk);
  138. }
  139. DB::table('share_members')->where('user_id',$userId)->update(['parent_user_id'=>$parentId,'binding_status'=>$status,'bound_at'=>$now,'updated_at'=>$now]);
  140. });
  141. }
  142. private function lock(): void
  143. {
  144. if (!DB::table('share_system_state')->where('key','relations_lock')->lockForUpdate()->first()) throw new \RuntimeException('分享关系表尚未初始化,请执行迁移');
  145. }
  146. }