|
|
<?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';
|
|
|
}
|
|
|
}
|