$uid, 'userId' => $uid, 'name' => (string)($user['name'] ?? $user['_name'] ?? ''), 'displayName' => (string)($user['displayName'] ?? $user['_display_name'] ?? $user['name'] ?? $user['_name'] ?? $uid), 'mobile' => (string)($user['mobile'] ?? $user['_mobile'] ?? ''), 'portrait' => (string)($user['portrait'] ?? $user['_portrait'] ?? ''), 'address' => (string)($user['address'] ?? $user['_address'] ?? ''), ]; } function tryProfileUserInfo(array $payload): ?array { try { $result = requireImSuccess(imAdminPost('/admin/user/get_info', $payload), '用户资料加载失败'); return $result ?: null; } catch (Throwable) { return null; } } function loadProfileUserFromDb(string $identity): ?array { $statement = imDb()->prepare( 'select `_uid`, `_name`, `_display_name`, `_mobile`, `_portrait`, `_address`' . ' from `t_user` where `_uid` = ? or `_mobile` = ? or `_name` = ?' . ' order by case when `_uid` = ? then 0 when `_mobile` = ? then 1 when `_name` = ? then 2 else 3 end' . ' limit 1' ); $statement->execute([$identity, $identity, $identity, $identity, $identity, $identity]); $row = $statement->fetch(); return $row ? normalizeProfileUser($row) : null; } function mergeProfileUser(array $primary, ?array $fallback): array { if (!$fallback) { return $primary; } $fallback = normalizeProfileUser($fallback); foreach (['name', 'displayName', 'mobile', 'portrait', 'address'] as $key) { if ((string)($primary[$key] ?? '') === '' && (string)($fallback[$key] ?? '') !== '') { $primary[$key] = (string)$fallback[$key]; } } return $primary; } function persistProfileUserToDb(string $userId, string $displayName, string $portrait): void { if ($portrait !== '') { $statement = imDb()->prepare( 'update `t_user` set `_display_name` = ?, `_portrait` = ?, `_dt` = ? where `_uid` = ? limit 1' ); $statement->execute([$displayName, $portrait, webImNowMs(), $userId]); return; } $statement = imDb()->prepare( 'update `t_user` set `_display_name` = ?, `_dt` = ? where `_uid` = ? limit 1' ); $statement->execute([$displayName, webImNowMs(), $userId]); } function loadProfileUser(string $identity, bool $preferMobile = true): array { $identity = trim($identity); if ($identity === '') { respond(400, '缺少用户ID'); } $dbUser = loadProfileUserFromDb($identity); if ($dbUser) { $fallback = null; if ((string)($dbUser['portrait'] ?? '') === '') { $fallback = tryProfileUserInfo(['userId' => $dbUser['userId']]); if (!$fallback && (string)($dbUser['mobile'] ?? '') !== '') { $fallback = tryProfileUserInfo(['mobile' => $dbUser['mobile']]); } if (!$fallback && (string)($dbUser['name'] ?? '') !== '') { $fallback = tryProfileUserInfo(['name' => $dbUser['name']]); } } return mergeProfileUser($dbUser, $fallback); } $user = null; if ($preferMobile && preg_match('/^\+?\d{5,32}$/', $identity)) { $user = tryProfileUserInfo(['mobile' => $identity]); } if (!$user) { $user = tryProfileUserInfo(['userId' => $identity]); } if (!$user) { $user = tryProfileUserInfo(['name' => $identity]); } if (!$user) { respond(404, '用户资料不存在'); } return normalizeProfileUser($user); } function readProfileJavaSerializedString(string $data, int $offset): ?array { if ($offset + 3 > strlen($data) || ord($data[$offset]) !== 0x74) { return null; } $length = (ord($data[$offset + 1]) << 8) | ord($data[$offset + 2]); $start = $offset + 3; if ($start + $length > strlen($data)) { return null; } return [substr($data, $start, $length), $start + $length]; } function profileSessionUserId(string $sessionData): string { $marker = "\x74\x00\x06userId"; $offset = strpos($sessionData, $marker); if ($offset === false) { return ''; } $decoded = readProfileJavaSerializedString($sessionData, $offset + strlen($marker)); $userId = is_array($decoded) ? (string)$decoded[0] : ''; if (!preg_match('/^[A-Za-z0-9_:@.+-]{1,128}$/', $userId)) { return ''; } return $userId; } function requireProfileUpdateAuth(string $targetUserId): void { $authToken = trim((string)($_SERVER['HTTP_AUTHTOKEN'] ?? $_SERVER['HTTP_AUTH_TOKEN'] ?? '')); if ($authToken === '') { respond(401, '请重新登录后再修改资料'); } $statement = imAppDb()->prepare('select `session_data` from `shiro_session` where `session_id` = ? limit 1'); $statement->execute([$authToken]); $row = $statement->fetch(); if (!$row) { respond(401, '登录状态已失效,请重新登录'); } $sessionUserId = profileSessionUserId((string)$row['session_data']); if ($sessionUserId === '' || $sessionUserId !== $targetUserId) { respond(403, '只能修改自己的资料'); } } function profileUploadedFileExtension(string $fileName, string $mime): string { $extension = strtolower((string)pathinfo($fileName, PATHINFO_EXTENSION)); $extension = preg_replace('/[^a-z0-9]/', '', $extension) ?: ''; if ($extension === 'jpeg') { $extension = 'jpg'; } $allowedExtensions = ['jpg', 'png', 'gif', 'webp', 'bmp', 'ico']; if (in_array($extension, $allowedExtensions, true)) { return $extension; } return match ($mime) { 'image/jpeg' => 'jpg', 'image/png' => 'png', 'image/gif' => 'gif', 'image/webp' => 'webp', 'image/bmp', 'image/x-ms-bmp' => 'bmp', 'image/x-icon', 'image/vnd.microsoft.icon' => 'ico', default => 'jpg', }; } function profilePublicMediaBaseUrl(): string { $isHttps = (string)($_SERVER['HTTPS'] ?? '') !== '' && strtolower((string)$_SERVER['HTTPS']) !== 'off'; $scheme = $isHttps ? 'https' : 'http'; $host = (string)($_SERVER['HTTP_HOST'] ?? $_SERVER['SERVER_NAME'] ?? '127.0.0.1'); $host = trim(preg_replace('/:\d+$/', '', $host) ?: '127.0.0.1'); return $scheme . '://' . $host . ':' . IM_LOCAL_MEDIA_PUBLIC_PORT; } function uploadProfilePortrait(array $file): string { if (($file['error'] ?? UPLOAD_ERR_NO_FILE) !== UPLOAD_ERR_OK) { throw new RuntimeException('头像上传失败'); } if (($file['size'] ?? 0) <= 0 || ($file['size'] ?? 0) > 5 * 1024 * 1024) { throw new RuntimeException('头像不能超过5MB'); } $tmpName = (string)($file['tmp_name'] ?? ''); $mime = mime_content_type($tmpName) ?: ''; if (!str_starts_with($mime, 'image/')) { throw new RuntimeException('头像必须是图片文件'); } if (!is_dir(IM_LOCAL_MEDIA_ROOT) || !is_writable(IM_LOCAL_MEDIA_ROOT)) { throw new RuntimeException('头像存储目录不可写'); } $datePath = 'fs/' . PROFILE_PORTRAIT_MEDIA_TYPE . '/' . date('Y/m/d/H/i/s'); $targetDir = rtrim(IM_LOCAL_MEDIA_ROOT, '/') . '/' . $datePath; if (!is_dir($targetDir) && !mkdir($targetDir, 0775, true)) { throw new RuntimeException('头像存储目录创建失败'); } $extension = profileUploadedFileExtension((string)($file['name'] ?? 'avatar.jpg'), $mime); $targetName = bin2hex(random_bytes(12)) . '.' . $extension; $targetPath = $targetDir . '/' . $targetName; if (!move_uploaded_file($tmpName, $targetPath)) { throw new RuntimeException('头像文件保存失败'); } @chmod($targetPath, 0644); return profilePublicMediaBaseUrl() . '/' . $datePath . '/' . $targetName; } try { $contentType = (string)($_SERVER['CONTENT_TYPE'] ?? ''); $isMultipart = stripos($contentType, 'multipart/form-data') !== false; $payload = $isMultipart ? [] : readJsonPayload(); $action = $isMultipart ? profileRequestValue('action', 'update') : trim((string)($payload['action'] ?? 'get')); $userId = $isMultipart ? profileRequestValue('userId') : trim((string)($payload['userId'] ?? $_GET['userId'] ?? '')); $currentProfile = loadProfileUser($userId); if ($action === 'get') { respond(0, 'success', ['profile' => $currentProfile]); } if ($action !== 'update') { respond(400, '不支持的资料操作'); } requireProfileUpdateAuth((string)$currentProfile['userId']); $displayName = $isMultipart ? profileRequestValue('displayName') : trim((string)($payload['displayName'] ?? '')); if ($displayName === '') { respond(400, '昵称不能为空'); } if (mb_strlen($displayName, 'UTF-8') > 64) { respond(400, '昵称不能超过64个字'); } $portrait = ''; if (!empty($_FILES['portraitFile']) && ($_FILES['portraitFile']['error'] ?? UPLOAD_ERR_NO_FILE) !== UPLOAD_ERR_NO_FILE) { $portrait = uploadProfilePortrait($_FILES['portraitFile']); } $flag = PROFILE_DISPLAY_NAME_FLAG; $userInfo = [ 'userId' => $currentProfile['userId'], 'displayName' => $displayName, ]; if ($portrait !== '') { $flag |= PROFILE_PORTRAIT_FLAG; $userInfo['portrait'] = $portrait; } requireImSuccess( imAdminPost('/admin/user/update', [ 'flag' => $flag, 'userInfo' => $userInfo, ]), '用户资料保存失败' ); persistProfileUserToDb($currentProfile['userId'], $displayName, $portrait); $updatedProfile = loadProfileUser($currentProfile['userId'], false); if ($portrait === '' && (string)$updatedProfile['portrait'] === '' && (string)($currentProfile['portrait'] ?? '') !== '') { $updatedProfile['portrait'] = (string)$currentProfile['portrait']; } if ($portrait !== '' && (string)$updatedProfile['portrait'] === '') { $updatedProfile['portrait'] = $portrait; } if ((string)$updatedProfile['displayName'] === '') { $updatedProfile['displayName'] = $displayName; } respond(0, 'success', ['profile' => $updatedProfile]); } catch (Throwable $error) { respond(500, $error->getMessage() ?: '用户资料服务暂不可用'); }