imImport = $imImport ?? static function (int $id, string $name): void { AdminLogic::importDoctorAccountToIm($id, $name); }; } public static function identity(array $config, string $subject): string { return json_encode([$config['issuer'], $config['application_id'], $subject], JSON_THROW_ON_ERROR); } public static function validateContext(array $context, string $subject, string $applicationId): void { if (($context['applicationId'] ?? '') !== $applicationId || ($context['localIdentityKey'] ?? '') !== $subject || ($context['employee']['oidcSubject'] ?? '') !== $subject || ($context['employee']['status'] ?? '') !== 'active') { throw new RuntimeException('当前统一账号未获得甄养堂访问权限'); } } public function resolve(IamOidcClient $client, array $config, string $subject): int { $context = $client->provisioning($subject); self::validateContext($context, $subject, $config['application_id']); if (($context['shouldCreateLocalAccount'] ?? null) !== true) { return IamLoginService::boundAdminId($context, $subject, $config['application_id']); } if (($context['externalAccountId'] ?? null) !== '') { throw new RuntimeException('统一账号绑定状态不一致'); } $id = $this->createOrResume(self::identity($config, $subject), $context); // Creation is durable before the remote call. On timeout retry this SAME account, never another one. $client->bind($subject, $id); $confirmed = IamLoginService::boundAdminId($client->provisioning($subject), $subject, $config['application_id']); if ($confirmed !== $id) { throw new RuntimeException('统一账号绑定冲突,请联系管理员'); } // Match AdminLogic::add's best-effort doctor IM initialization only after central confirmation. // importDoctorAccountToIm catches/logs provider failures; it does not grant permissions. ($this->imImport)($id, (string) Db::name('admin')->where('id', $id)->value('name')); return $id; } public function createOrResume(string $identity, array $context): int { $key = hash('sha256', $identity); try { return Db::transaction(function () use ($key, $identity, $context): int { $row = Db::name('iam_local_identity')->where('identity_key', $key)->lock(true)->find(); if ($row) { return $this->existing($row, $identity); } // Unique PK serializes concurrent workers, including workers on different servers. Db::name('iam_local_identity')->insert(['identity_key' => $key, 'identity_value' => $identity, 'admin_id' => null, 'create_time' => time()]); $role = Db::name('system_role')->where('id', 2)->whereNull('delete_time')->lock(true)->find(); if (!$role || ($role['name'] ?? '') !== '医助' || (isset($role['disable']) && (int) $role['disable'] !== 0)) { throw new RuntimeException('默认医助角色未就绪,请联系管理员'); } // Random local login name; never adopt an existing similarly named local account. $account = 'iam_' . bin2hex(random_bytes(12)); if (Db::name('admin')->where('account', $account)->find()) { throw new RuntimeException('新账号标识冲突,请重新登录'); } $name = (string) ($context['employee']['displayName'] ?? '统一账号用户'); $id = (int) Db::name('admin')->insertGetId([ 'account' => $account, 'name' => mb_substr($name !== '' ? $name : '统一账号用户', 0, 20), 'password' => create_password(bin2hex(random_bytes(32)), Config::get('project.unique_identification')), 'avatar' => (string) Config::get('project.default_image.admin_avatar', ''), 'root' => 0, 'disable' => 0, 'is_paw' => 0, 'multipoint_login' => 0, 'gender' => 1, 'enable_image_consult' => 1, 'enable_video_consult' => 1, 'enable_charge' => 0, 'create_time' => time(), 'update_time' => time(), ]); Db::name('admin_role')->insert(['admin_id' => $id, 'role_id' => 2]); Db::name('iam_local_identity')->where('identity_key', $key)->update(['admin_id' => $id]); return $id; }); } catch (\Throwable $error) { // A concurrent insert may have won while our transaction rolled back. Only the exact ledger is reusable. $row = Db::name('iam_local_identity')->where('identity_key', $key)->find(); if ($row) { return $this->existing($row, $identity); } throw $error; } } private function existing(array $row, string $identity): int { if (!hash_equals((string) $row['identity_value'], $identity) || (int) $row['admin_id'] <= 0) { throw new RuntimeException('统一账号本地开户状态异常'); } $admin = Db::name('admin')->where('id', (int) $row['admin_id'])->find(); if (!$admin || (int) $admin['disable'] !== 0 || !empty($admin['delete_time'])) { throw new RuntimeException('甄养堂账号已停用,请联系管理员'); } // Existing permissions and password/WeCom requirements are never rewritten. return (int) $row['admin_id']; } }