option('book_id'); // 获取所有有ISBN的书籍 $books = Book::whereNotNull('isbn') ->where(function ($query) use ($book_id) { if ($book_id) { $query->where('id', $book_id); } })->where('isbn', '!=', '') ->where(function ($query) { // 如果没有封面或者缺少基本信息字段,都需要处理 $query->whereNull('cover_id') ->orWhereNull('publisher') ->orWhereNull('author') ->orWhereNull('publish_year') ->orWhereNull('description') ->orWhere('publisher', '') ->orWhere('author', '') ->orWhere('publish_year', '') ->orWhere('description', ''); })->get(); if ($books->isEmpty()) { $this->info('没有找到需要更新数据的书籍'); return 0; } $this->info("找到 {$books->count()} 本书需要处理"); $this->info("API限制:每秒最多10次请求,预计耗时约 " . ceil($books->count() / 10) . " 秒"); $bar = $this->output->createProgressBar($books->count()); $bar->start(); $successCount = 0; $failCount = 0; $requestCount = 0; $startTime = microtime(true); $lastResetTime = $startTime; foreach ($books as $book) { try { // 每秒重置请求计数器 $currentTime = microtime(true); if ($currentTime - $lastResetTime >= 1.0) { $requestCount = 0; $lastResetTime = $currentTime; } // API频率限制控制:每秒最多10次请求 if ($requestCount >= 10) { $waitTime = 1.0 - ($currentTime - $lastResetTime); if ($waitTime > 0) { usleep(intval($waitTime * 1000000)); $requestCount = 0; $lastResetTime = microtime(true); } } $result = $this->processBook($book, $apiKey); $requestCount++; if ($result) { $successCount++; $this->line("\n✓ 成功处理书籍: {$book->title} (ISBN: {$book->isbn})"); } else { $failCount++; $this->line("\n✗ 处理失败: {$book->title} (ISBN: {$book->isbn})"); } } catch (\Exception $e) { $failCount++; $this->line("\n✗ 处理异常: {$book->title} - {$e->getMessage()}"); // 如果是API相关错误,增加等待时间 if (strpos($e->getMessage(), 'API') !== false || strpos($e->getMessage(), 'HTTP') !== false) { $this->line("检测到API错误,等待2秒后继续..."); sleep(2); $requestCount = 0; $lastResetTime = microtime(true); } } $bar->advance(); } $bar->finish(); $totalTime = microtime(true) - $startTime; $avgTimePerBook = $totalTime / $books->count(); $actualRequestsPerSecond = $books->count() / $totalTime; $this->line(''); $this->info("处理完成!"); $this->info("成功: {$successCount}, 失败: {$failCount}"); $this->info("总耗时: " . round($totalTime, 2) . " 秒"); $this->info("平均每本书耗时: " . round($avgTimePerBook, 2) . " 秒"); $this->info("实际请求频率: " . round($actualRequestsPerSecond, 2) . " 次/秒"); return 0; } /** * 处理单本书籍 * * @param Book $book * @param string $apiKey * @return bool */ private function processBook(Book $book, string $apiKey): bool { $attempt = 0; $maxRetries = 3; while ($attempt < $maxRetries) { try { $attempt++; // 调用ISBN接口 $response = Http::timeout(30)->get('https://api.tanshuapi.com/api/isbn/v2/index', [ 'key' => $apiKey, 'isbn' => $book->isbn ]); if (!$response->successful()) { throw new \Exception("API请求失败: HTTP {$response->status()}"); } $data = $response->json(); if (!$data || $data['code'] !== 1) { // 如果是API密钥错误或其他不可重试的错误,直接返回失败 if (isset($data['code']) && in_array($data['code'], [10001, 10002, 10003])) { $this->error("API返回不可重试错误: " . ($data['msg'] ?? '未知错误')); return false; } throw new \Exception("API返回错误: " . ($data['msg'] ?? '未知错误')); } $bookData = $data['data']; // 更新书籍的基本信息字段 if (!empty($bookData['publisher'])) { $book->publisher = $bookData['publisher']; } if (!empty($bookData['author'])) { $book->author = $bookData['author']; } if (!empty($bookData['pubdate'])) { $book->publish_year = $bookData['pubdate']; } if (!empty($bookData['summary'])) { $book->description = $bookData['summary']; } // 更新书籍的other_data字段(保存完整的API响应数据) $book->other_data = $bookData; // 如果有图片URL,下载图片 if (!empty($bookData['img'])) { $coverId = $this->downloadAndSaveImage($bookData['img'], $book); if ($coverId) { $book->cover_id = $coverId; } } $book->save(); return true; } catch (\Exception $e) { if ($attempt >= $maxRetries) { $this->error("重试 {$maxRetries} 次后仍然失败: {$e->getMessage()}"); return false; } // 指数退避:第1次重试等待1秒,第2次等待2秒,第3次等待4秒 $waitTime = pow(2, $attempt - 1); $this->line("第 {$attempt} 次尝试失败,{$waitTime} 秒后重试: {$e->getMessage()}"); sleep($waitTime); } } return false; } /** * 下载图片并保存到本地 * * @param string $imageUrl * @param Book $book * @return int|null */ private function downloadAndSaveImage(string $imageUrl, Book $book): ?int { try { // 下载图片 $response = Http::timeout(30)->get($imageUrl); if (!$response->successful()) { $this->error("图片下载失败: {$imageUrl}"); return null; } $imageContent = $response->body(); // 获取文件扩展名 $extension = $this->getImageExtension($imageUrl, $response->header('Content-Type')); // 生成文件名 $filename = 'book_cover_' . $book->id . '_' . time() . '.' . $extension; // 定义存储目录 $folder = 'uploads/book_covers'; // 确保目录存在 $fullPath = public_path($folder); if (!file_exists($fullPath)) { mkdir($fullPath, 0755, true); } // 保存文件 $filePath = $folder . '/' . $filename; file_put_contents(public_path($filePath), $imageContent); // 获取文件大小 $fileSize = strlen($imageContent); // 创建uploads记录 $upload = Upload::create([ 'belongs_type' => 'App\\Models\\Book', 'belongs_id' => $book->id, 'original_name' => basename($imageUrl), 'folder' => $folder, 'name' => $filename, 'extension' => $extension, 'size' => $fileSize, 'creator_type' => 'console', 'creator_id' => null, ]); return $upload->id; } catch (\Exception $e) { $this->error("保存图片时出错: {$e->getMessage()}"); return null; } } /** * 获取图片扩展名 * * @param string $url * @param string|null $contentType * @return string */ private function getImageExtension(string $url, ?string $contentType = null): string { // 首先尝试从URL获取扩展名 $pathInfo = pathinfo(parse_url($url, PHP_URL_PATH)); if (!empty($pathInfo['extension'])) { return strtolower($pathInfo['extension']); } // 从Content-Type获取扩展名 if ($contentType) { $mimeToExt = [ 'image/jpeg' => 'jpg', 'image/jpg' => 'jpg', 'image/png' => 'png', 'image/gif' => 'gif', 'image/webp' => 'webp', 'image/bmp' => 'bmp', ]; $contentType = strtolower(trim(explode(';', $contentType)[0])); if (isset($mimeToExt[$contentType])) { return $mimeToExt[$contentType]; } } // 默认返回jpg return 'jpg'; } }