| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152 |
- <?php
- namespace App\Services\Sharing;
- use Carbon\Carbon;
- use App\Models\User;
- use Illuminate\Support\Facades\DB;
- class ShareRelations
- {
- public function initialize(bool $write = false): array
- {
- if (!$write) return $this->inspect(false);
- return DB::transaction(function () {
- $this->lock();
- return $this->inspect(true);
- });
- }
- private function inspect(bool $write): array
- {
- $users = DB::table('users')->get(['id','user_code','agent_user_code','from','created_at'])->keyBy('id');
- $existing = DB::table('share_members')->get()->keyBy('user_id');
- $assigned = 0;
- foreach ($users as $id=>$user) {
- if ((int)$user->from === 2) continue;
- $old=(string)($user->user_code ?? '');
- $stable=(string)($existing[$id]->invitation_code ?? '');
- if ($old === '' || $old === '0') {
- $assigned++;
- if (!$write) continue;
- $code=($stable !== '' && $stable !== '0') ? $stable : User::generateUserCode();
- if (DB::table('users')->where('user_code',$code)->where('id','<>',$id)->exists()) throw new \RuntimeException('固化邀请码已被其他会员占用,请人工核对:'.$id);
- DB::table('users')->where('id',$id)->update(['user_code'=>$code]);
- $user->user_code=$code;
- }
- if ($write && $existing->has($id) && ($stable === '' || $stable === '0')) {
- DB::table('share_members')->where('user_id',$id)->update(['invitation_code'=>(string)$user->user_code]);
- $existing[$id]->invitation_code=(string)$user->user_code;
- }
- }
- $codes = [];
- foreach ($users as $u) if ((string)$u->user_code !== '' && (string)$u->user_code !== '0') $codes[strtoupper(trim((string)$u->user_code))][] = (int)$u->id;
- $parents = []; $statuses = []; $errors = [];
- foreach ($users as $u) {
- $id = (int)$u->id;
- if ($existing->has($id)) {
- $parents[$id] = $existing[$id]->parent_user_id === null ? null : (int)$existing[$id]->parent_user_id;
- $statuses[$id] = $existing[$id]->binding_status;
- continue;
- }
- $code = strtoupper(trim((string)($u->agent_user_code ?? '')));
- $matches = $codes[$code] ?? [];
- $parents[$id] = null;
- $statuses[$id] = 'root';
- if ((int)$u->from === 2 || $code === '') continue;
- if ($code === '0' || count($matches) === 0) $statuses[$id] = 'missing';
- elseif (count($matches) > 1) $statuses[$id] = 'ambiguous';
- elseif ((int)$users[$matches[0]]->from === 2) $statuses[$id] = 'missing';
- else { $parents[$id] = $matches[0]; $statuses[$id] = 'matched'; }
- }
- // Only actual cycle edges are removed. Confirmed children outside the cycle keep their identity.
- $done = [];
- foreach (array_keys($parents) as $id) {
- $path = []; $at = $id;
- while ($at !== null && isset($parents[$at]) && !isset($done[$at])) {
- if (isset($path[$at])) {
- $cycle = array_slice(array_keys($path), $path[$at]);
- foreach ($cycle as $member) {
- if ($existing->has($member)) throw new \RuntimeException('已固化的推荐关系存在循环,禁止自动改绑:' . $member);
- $parents[$member] = null; $statuses[$member] = 'cycle';
- }
- break;
- }
- $path[$at] = count($path); $at = $parents[$at];
- }
- foreach ($path as $member => $_) $done[$member] = true;
- }
- foreach ($statuses as $id => $status) if (in_array($status,['missing','ambiguous','cycle'],true)) $errors[] = ['user_id'=>$id,'status'=>$status];
- $result = ['users'=>$users->count(),'created'=>0,'existing'=>$existing->count(),'invitation_codes_assigned'=>$assigned,'errors'=>$errors,'written'=>$write];
- if (!$write) { $result['created'] = $users->count() - $existing->count(); return $result; }
- $now = Carbon::now(ShareDefaults::TIMEZONE)->format('Y-m-d H:i:s');
- $initial = DB::table('share_system_state')->where('key','initialized_at')->value('value') === null;
- foreach ($users as $u) {
- $id = (int)$u->id;
- if ($existing->has($id)) continue;
- DB::table('share_members')->insert(['user_id'=>$id,'parent_user_id'=>$parents[$id],
- 'invitation_code'=>(string)($u->user_code ?? ''),'binding_status'=>$statuses[$id],
- 'bound_at'=>$initial ? '1970-01-01 00:00:00' : ($u->created_at ?: $now), 'created_at'=>$now,'updated_at'=>$now]);
- $result['created']++;
- }
- $members = DB::table('share_members')->get()->keyBy('user_id');
- foreach ($members as $member) {
- $id = (int)$member->user_id; $at = $id; $depth = 0; $seen = [];
- $bound = '1970-01-01 00:00:00'; $rows = [];
- while ($at !== null && $members->has($at)) {
- if (isset($seen[$at])) throw new \RuntimeException('推荐关系循环');
- $seen[$at] = true;
- $rows[] = ['ancestor_user_id'=>$at,'descendant_user_id'=>$id,'depth'=>$depth,'bound_at'=>$bound];
- $edge = $members[$at];
- if ($edge->parent_user_id === null) break;
- $bound = max($bound,(string)$edge->bound_at);
- $at = (int)$edge->parent_user_id; $depth++;
- }
- foreach (array_chunk($rows,500) as $chunk) DB::table('share_tree')->insertOrIgnore($chunk);
- DB::table('share_accounts')->insertOrIgnore(['user_id'=>$id,'created_at'=>$now,'updated_at'=>$now]);
- }
- DB::table('share_system_state')->updateOrInsert(['key'=>'initialized_at'],['value'=>$now,'created_at'=>$now,'updated_at'=>$now]);
- return $result;
- }
- public function bind(int $userId, ?int $parentId, string $code, string $status = 'matched'): void
- {
- DB::transaction(function () use ($userId,$parentId,$code,$status) {
- $this->lock();
- $now = Carbon::now(ShareDefaults::TIMEZONE)->format('Y-m-d H:i:s');
- $member = DB::table('share_members')->where('user_id',$userId)->lockForUpdate()->first();
- if ($member && $member->parent_user_id !== null) {
- if ((int)$member->parent_user_id !== $parentId) throw new \RuntimeException('上级关系已固定,不能改绑');
- return;
- }
- if ($parentId !== null) {
- $parent = DB::table('share_members')->where('user_id',$parentId)->first();
- if (!$parent) throw new \RuntimeException('推广会员关系尚未初始化');
- if ($parentId === $userId || DB::table('share_tree')->where('ancestor_user_id',$userId)->where('descendant_user_id',$parentId)->exists()) throw new \RuntimeException('不能绑定自己或自己的下级');
- $user = DB::table('users')->where('id',$parentId)->first();
- if (!$user || (int)$user->from === 2) throw new \RuntimeException('试玩会员不能作为推广人');
- }
- if (!$member) {
- 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]);
- DB::table('share_tree')->insertOrIgnore(['ancestor_user_id'=>$userId,'descendant_user_id'=>$userId,'depth'=>0,'bound_at'=>'1970-01-01 00:00:00']);
- DB::table('share_accounts')->insertOrIgnore(['user_id'=>$userId,'created_at'=>$now,'updated_at'=>$now]);
- }
- if ($parentId === null) return;
- $ancestors = DB::table('share_tree')->where('descendant_user_id',$parentId)->get();
- $descendants = DB::table('share_tree')->where('ancestor_user_id',$userId)->get();
- foreach ($ancestors as $ancestor) {
- $rows = [];
- foreach ($descendants as $descendant) $rows[] = ['ancestor_user_id'=>$ancestor->ancestor_user_id,
- 'descendant_user_id'=>$descendant->descendant_user_id,'depth'=>$ancestor->depth+1+$descendant->depth,
- 'bound_at'=>max($now,(string)$ancestor->bound_at,(string)$descendant->bound_at)];
- foreach (array_chunk($rows,500) as $chunk) DB::table('share_tree')->insertOrIgnore($chunk);
- }
- DB::table('share_members')->where('user_id',$userId)->update(['parent_user_id'=>$parentId,'binding_status'=>$status,'bound_at'=>$now,'updated_at'=>$now]);
- });
- }
- private function lock(): void
- {
- if (!DB::table('share_system_state')->where('key','relations_lock')->lockForUpdate()->first()) throw new \RuntimeException('分享关系表尚未初始化,请执行迁移');
- }
- }
|