You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

332 lines
11 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<?php
namespace App\Console\Commands;
use App\Models\Book;
use App\Models\Config;
use App\Models\Upload;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
class UpdateBookIsbnData extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'book:update-isbn-data {--book_id=} ';
/**
* The console command description.
*
* @var string
*/
protected $description = '从ISBN接口获取书籍数据更新出版社、作者、出版年份、简介等信息并下载封面图片';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
$apiKey = Config::getValueByKey('book_key');
$book_id = $this->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';
}
}