diff --git a/app/common/controller/UploadController.php b/app/common/controller/UploadController.php index 90921c0..b10cf93 100644 --- a/app/common/controller/UploadController.php +++ b/app/common/controller/UploadController.php @@ -89,12 +89,10 @@ class UploadController $originalName = $file->getUploadName(); $ossservice = new OssService(); - $mimes = $ossservice->getMimeTypes(['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'txt', 'zip', 'rar', 'MP4', 'MOV', 'M4V', 'WEBM', 'MKV', 'FLV', 'WMV', 'AVI', 'TS']); $result = Storage::adapter('oss') ->path(config('oss.oss_main_dir') . $sceneUrl . date('Ymd')) - ->size(1024*1024*50) - ->extYes($mimes) + ->size(1024*1024*100) ->upload($file); diff --git a/app/common/logic/AuthLogic.php b/app/common/logic/AuthLogic.php index f324b4a..f145f3c 100644 --- a/app/common/logic/AuthLogic.php +++ b/app/common/logic/AuthLogic.php @@ -14,7 +14,6 @@ use app\common\utils\Jwt; use support\Redis; use support\exception\BusinessException; use support\Log; -use function Symfony\Component\String\b; class AuthLogic { diff --git a/app/common/model/UsedCarDetail.php b/app/common/model/UsedCarDetail.php index 3a5ff3c..8688fa6 100644 --- a/app/common/model/UsedCarDetail.php +++ b/app/common/model/UsedCarDetail.php @@ -51,9 +51,9 @@ class UsedCarDetail extends Model * JSON 类型字段自动转换 */ protected $casts = [ - 'images' => 'array', - 'attachment' => 'array', - 'intro_images' => 'array', +// 'images' => 'array', +// 'attachment' => 'array', +// 'intro_images' => 'array', ]; /** diff --git a/app/common/service/UsedCarDetailService.php b/app/common/service/UsedCarDetailService.php index 4b93076..6bdc4fe 100644 --- a/app/common/service/UsedCarDetailService.php +++ b/app/common/service/UsedCarDetailService.php @@ -421,7 +421,70 @@ class UsedCarDetailService ]); // 更新车辆状态 - return UsedCarDetail::where('id', $carId)->update(['status' => $auditStatus]); + $result = UsedCarDetail::where('id', $carId)->update(['status' => $auditStatus]); + + // 审核通过后自动推送到WordPress + $pushResult = ['success' => true, 'message' => '']; + if ($auditStatus == 1) { + $pushResult = $this->pushToWordpressAfterAudit($carId); + + // 推送失败,将车辆状态修改为3 + if (!$pushResult['success']) { + UsedCarDetail::where('id', $carId)->update(['status' => 3]); + } + } + + // 返回审核结果和推送结果 + return [ + 'audit_success' => true, + 'push_success' => $pushResult['success'], + 'push_message' => $pushResult['message'], + 'message' => $pushResult['success'] ? '审核通过并推送成功' : '审核通过,但推送失败:' . $pushResult['message'], + ]; + } + + /** + * 审核通过后推送车辆到WordPress + * @return array ['success' => bool, 'message' => string] + */ + private function pushToWordpressAfterAudit(int $carId): array + { + try { + // 获取启用的WordPress站点 + $site = \app\common\model\WordpressSite::where('status', 1)->first(); + if (!$site) { + $msg = '没有启用的WordPress站点'; + Log::warning('WordPress推送失败:' . $msg, ['car_id' => $carId]); + return ['success' => false, 'message' => $msg]; + } + + // 检查是否已推送过 + $existing = \app\common\model\WordpressUsedCar::where('wp_site_id', $site->id) + ->where('used_car_id', $carId) + ->where('status', 1) + ->first(); + if ($existing) { + $msg = '车辆已推送,跳过'; + Log::info('WordPress推送:' . $msg, ['car_id' => $carId]); + return ['success' => true, 'message' => $msg]; + } + + // 调用推送服务 + $wpService = new \app\common\service\WordpressSiteService(); + $wpService->createProductForNewSite($site, $carId); + + Log::info('WordPress推送成功', ['car_id' => $carId, 'site_id' => $site->id]); + return ['success' => true, 'message' => '']; + + } catch (\Throwable $e) { + $msg = $e->getMessage(); + Log::error('WordPress推送失败', [ + 'car_id' => $carId, + 'error' => $msg, + 'trace' => $e->getTraceAsString(), + ]); + return ['success' => false, 'message' => $msg]; + } } /** diff --git a/app/common/service/WordpressSiteService.php b/app/common/service/WordpressSiteService.php index b091057..1ebac77 100644 --- a/app/common/service/WordpressSiteService.php +++ b/app/common/service/WordpressSiteService.php @@ -767,38 +767,653 @@ class WordpressSiteService */ public function uploadImageToWp(string $domain,$api_user, $api_secret, string $imageUrl = "fdesk_test/usedcar_crawl/20251125/ff847a6a2b3114b4c2c230455b74cd2e.jpg"): array { - $process = 'image/resize,w_900/quality,Q_50/format,webp'; - $imageUrl = (new OssService)->generatePrivateUrlResize($imageUrl,3600,$process); - + $process = 'image/resize,w_800/quality,Q_50/format,webp'; + $processedUrl = (new OssService)->generatePrivateUrlResize($imageUrl,3600,$process); $client = new Client(); - $filename = basename(parse_url($imageUrl, PHP_URL_PATH)); + // 从原始 URL 提取文件扩展名 + $pathInfo = pathinfo(parse_url($imageUrl, PHP_URL_PATH)); + $extension = strtolower($pathInfo['extension'] ?? 'jpg'); + + // 根据处理后的格式确定文件名和 Content-Type + if (strpos($processedUrl, 'webp') !== false || strpos($process, 'webp') !== false) { + $filename = ($pathInfo['filename'] ?? 'image') . '.webp'; + $contentType = 'image/webp'; + } else { + $filename = ($pathInfo['filename'] ?? 'image') . '.' . $extension; + $contentType = 'image/' . ($extension === 'jpg' ? 'jpeg' : $extension); + } + $mediaUrl = $domain . '/wp-json/wp/v2/media'; - try { - // 1. 拉取 OSS 图片内容 - $imageContent = file_get_contents($imageUrl); - if ($imageContent === false) { - throw new \Exception('图片下载失败:' . $imageUrl); - } + // 本地临时文件路径 + $tempDir = runtime_path('temp') . '/wp_images/'; + if (!is_dir($tempDir)) { + mkdir($tempDir, 0755, true); + } + $tempFilePath = $tempDir . md5($processedUrl) . '.' . $extension; - // 2. 上传到 WP 媒体库 - $response = $client->post($mediaUrl, [ - RequestOptions::AUTH => [$api_user, $api_secret], - RequestOptions::HEADERS => [ - 'Content-Disposition' => 'attachment; filename="' . $filename . '"', - 'Content-Type' => 'image/jpeg', // 如有 png/webp 可自行判断 - ], - RequestOptions::BODY => $imageContent, - RequestOptions::VERIFY => false, + try { + Log::info('开始上传图片到WordPress', [ + 'original_url' => $imageUrl, + 'processed_url' => $processedUrl, + 'filename' => $filename, + 'content_type' => $contentType, + 'temp_file' => $tempFilePath, ]); - return json_decode($response->getBody()->getContents(), true); + // 1. 下载图片到本地临时文件 + $imageContent = file_get_contents($processedUrl); + if ($imageContent === false) { + throw new \Exception('图片下载失败:' . $processedUrl); + } + + $imageSize = strlen($imageContent); + Log::info('图片下载成功', ['size' => $imageSize, 'temp_file' => $tempFilePath]); + + // 写入临时文件 + $bytesWritten = file_put_contents($tempFilePath, $imageContent); + if ($bytesWritten === false) { + throw new \Exception('写入临时文件失败:' . $tempFilePath); + } + + Log::info('临时文件创建成功', ['file' => $tempFilePath, 'size' => $bytesWritten]); + + // 2. 使用 GuzzleHttp 上传本地文件到 WP 媒体库 + $fileHandle = fopen($tempFilePath, 'r'); + $response = $client->post($mediaUrl, [ + RequestOptions::AUTH => [$api_user, $api_secret], + RequestOptions::MULTIPART => [ + [ + 'name' => 'file', + 'filename' => $filename, + 'contents' => $fileHandle, + 'headers' => [ + 'Content-Type' => $contentType, + ], + ], + ], + RequestOptions::VERIFY => false, + RequestOptions::TIMEOUT => 30, + ]); + + // 关闭文件句柄 + if (is_resource($fileHandle)) { + fclose($fileHandle); + } + + $result = json_decode($response->getBody()->getContents(), true); + + Log::info('图片上传成功', [ + 'media_id' => $result['id'] ?? 'unknown', + 'source_url' => $result['source_url'] ?? 'not set', + 'url' => $result['url'] ?? 'not set', + 'link' => $result['link'] ?? 'not set', + 'guid' => $result['guid']['rendered'] ?? 'not set', + 'all_keys' => array_keys($result), + ]); + + return $result; } catch (\Exception $e) { - throw new \Exception('上传图片失败:' .$domain . $imageUrl . "?????". $e->getMessage()); + Log::error('上传图片到WordPress失败', [ + 'domain' => $domain, + 'image_url' => $imageUrl, + 'processed_url' => $processedUrl ?? 'N/A', + 'temp_file' => $tempFilePath ?? 'N/A', + 'error' => $e->getMessage(), + 'trace' => $e->getTraceAsString(), + ]); + throw new \Exception('上传图片失败:' . $e->getMessage()); + } finally { + // 3. 删除临时文件 + if (file_exists($tempFilePath)) { + @unlink($tempFilePath); + Log::info('临时文件已删除', ['file' => $tempFilePath]); + } } } + /** + * 创建产品到新站点(使用 Gutenberg Blocks 结构) + * + * @param \app\common\model\WordpressSite $site WordPress站点 + * @param int $carId 车辆ID + * @return array + * @throws \Exception + */ + public function createProductForNewSite($site, int $carId): array + { + // 获取车辆数据 + $car = \app\common\model\UsedCarDetail::find($carId); + if (!$car) { + throw new \Exception('车辆不存在'); + } + + // 检查是否重复推送 + $wpUsed = WordpressUsedCar::where('wp_site_id', $site->id) + ->where('used_car_id', $carId) + ->first(); + if ($wpUsed && ($wpUsed->retry_count > 2 || $wpUsed->status == 1)) { + return ['message' => '重复推送,跳过']; + } + + $domain = $site->site_url; + $client = new Client(); + $url = $domain . '/wp-json/wp/v2/product'; + + // 调试:检查 images 字段的原始值和转换后的值 + $imagesRaw = $car->getAttribute('images'); // 获取原始值,不经过 casts + $imagesCast = $car->images; // 经过 casts 转换后的值 + + Log::info('检查 images 字段', [ + 'car_id' => $carId, + 'images_raw' => $imagesRaw, + 'images_cast' => $imagesCast, + 'images_raw_type' => gettype($imagesRaw), + 'images_cast_type' => gettype($imagesCast), + 'images_cast_is_array' => is_array($imagesCast), + ]); + + // 解析图片列表 - 优先使用已转换的数组,否则尝试解析原始字符串 + $images = []; + if (!empty($imagesCast) && is_array($imagesCast)) { + $images = $imagesCast; + } elseif (!empty($imagesRaw)) { + // 尝试 JSON 解析 + $decoded = json_decode($imagesRaw, true); + if (is_array($decoded)) { + $images = $decoded; + } else { + // 尝试逗号分割 + $images = explode(',', $imagesRaw); + } + } + + Log::info('开始处理车辆图片', [ + 'car_id' => $carId, + 'images_final' => $images, + 'images_count' => count($images), + ]); + + // 上传图片到WordPress媒体库 + $uploadedImages = []; // 存储完整信息 ['id' => xx, 'url' => xx, ...] + $uploadedImageIds = []; // 仅存储ID + + foreach ($images as $index => $imageUrl) { + if (!empty($imageUrl)) { + try { + Log::info('正在上传图片', ['index' => $index, 'url' => $imageUrl]); + $uploadedImg = $this->uploadImageToWp($domain, $site->api_user, $site->api_secret, $imageUrl); + $uploadedImages[] = [ + 'id' => $uploadedImg['id'], + 'url' => $uploadedImg['source_url'] ?? $uploadedImg['url'] ?? '', + ]; + $uploadedImageIds[] = $uploadedImg['id']; + Log::info('图片上传成功', ['index' => $index, 'media_id' => $uploadedImg['id'], 'url' => $uploadedImages[count($uploadedImages)-1]['url']]); + } catch (\Exception $e) { + Log::error('图片上传失败', [ + 'index' => $index, + 'url' => $imageUrl, + 'error' => $e->getMessage(), + ]); + } + } + } + + Log::info('图片上传完成', [ + 'total' => count($images), + 'success' => count($uploadedImages), + 'uploaded_ids' => $uploadedImageIds, + 'uploaded_details' => $uploadedImages, + ]); + + // 获取FAQ数据 + $faqs = \app\common\model\UsedCarFaq::where('car_id', $carId) + ->where('status', 1) + ->orderBy('sort') + ->get(); + + // 翻译标题 + $title = $this->translateIfNeeded($car->product_name, '英文'); + + // 构建Gutenberg Blocks格式content + $content = $this->buildGutenbergContent($car, $uploadedImages, $faqs); + + // 构建产品数据 + $data = [ + 'title' => $title, + 'content' => $content, + 'status' => 'publish', + 'type' => 'simple', + 'regular_price' => number_format($car->price * $site->exchange_rate, 2), + ]; + + // 设置主图和车辆相册(ACF字段) + if (!empty($uploadedImages)) { + $allImageIds = array_map(function($img) { + return $img['id']; + }, $uploadedImages); + + $data['featured_media'] = $uploadedImages[0]['id']; + + // 车辆相册:前5张图片 + $productGalleryImages = array_slice($uploadedImages, 0, 5); + $productGalleryIds = array_map(function($img) { + return $img['id']; + }, $productGalleryImages); + $data['acf']['product_gallery'] = $productGalleryIds; + + Log::info('设置ACF车辆相册', [ + 'featured_media' => $data['featured_media'], + 'total_images' => count($uploadedImages), + 'product_gallery_count' => count($data['acf']['product_gallery']), + 'product_gallery_ids' => $data['acf']['product_gallery'], + ]); + } + + // 设置ACF车辆参数字段(与旧站点保持一致) + $fieldMap = [ + 'plate_time' => '上牌时间', + 'displacement' => '排量', + 'mileage' => '里程', + 'power' => '马力', + 'body_color' => '车身颜色', + 'source_place' => '车源地', + 'price' => '价格', + ]; + + foreach ($fieldMap as $acfKey => $label) { + if (!empty($car->$acfKey)) { + $value = $car->$acfKey; + // 翻译颜色和车源地 + if ($acfKey === 'body_color') { + $value = $this->translateColorToEnglish($value); + } elseif ($acfKey === 'source_place' && $this->containsChinese($value)) { + $value = $this->translateIfNeeded($value, '英文'); + } + $data['acf'][$acfKey] = $value; + } + } + + // 翻译并设置车况介绍和产品简介(ACF字段) + $conditionDesc = $car->condition_desc ?? ''; + if (!empty($conditionDesc)) { + // 产品简介(完整翻译) + $data['acf']['condition_desc'] = $this->translateIfNeeded($conditionDesc, '英文'); + + // 车况介绍(简短翻译,去掉商家信息) + $cleanDesc = preg_replace('/[^\x{4e00}-\x{9fff}\w\s.,!?()-]/u', ' ', $conditionDesc); + $vehicleDesc = $this->translateIfNeeded($cleanDesc, '英文'); + $data['acf']['pro-intro-des'] = $vehicleDesc; + } + + Log::info('设置ACF参数字段', [ + 'acf_fields' => array_keys($data['acf'] ?? []), + 'acf_values' => $data['acf'] ?? [], + ]); + + try { + Log::info('准备推送产品到WordPress', [ + 'car_id' => $carId, + 'featured_media' => $data['featured_media'] ?? 'not set', + 'acf_product_gallery_count' => count($data['acf']['product_gallery'] ?? []), + 'acf_fields_count' => count($data['acf'] ?? []), + 'content_length' => strlen($data['content'] ?? ''), + ]); + + $response = $client->post($url, [ + RequestOptions::AUTH => [$site->api_user, $site->api_secret], + RequestOptions::JSON => $data, + RequestOptions::VERIFY => false, + ]); + + $result = json_decode($response->getBody()->getContents(), true); + + // 记录推送成功 + if ($wpUsed) { + $wpUsed->update([ + 'wp_post_id' => $result['id'], + 'request_payload' => json_encode($data, JSON_UNESCAPED_UNICODE), + 'response_body' => $response->getBody()->getContents(), + 'status' => 1, + 'retry_count' => $wpUsed->retry_count + 1, + 'pushed_at' => time(), + ]); + } else { + WordpressUsedCar::create([ + 'wp_site_id' => $site->id, + 'used_car_id' => $carId, + 'wp_post_id' => $result['id'], + 'request_payload' => json_encode($data, JSON_UNESCAPED_UNICODE), + 'response_body' => $response->getBody()->getContents(), + 'status' => 1, + 'retry_count' => 1, + 'pushed_at' => time(), + ]); + } + + $site->last_uid = $carId; + $site->save(); + + return array_merge($data, ['result' => $result]); + + } catch (\Exception $e) { + // 记录推送失败 + if ($wpUsed) { + $wpUsed->update([ + 'retry_count' => $wpUsed->retry_count + 1, + 'error_message' => $e->getMessage(), + ]); + if ($wpUsed->retry_count >= 3) { + $site->last_uid = $carId; + $site->save(); + } + } else { + WordpressUsedCar::create([ + 'wp_site_id' => $site->id, + 'used_car_id' => $carId, + 'request_payload' => json_encode($data, JSON_UNESCAPED_UNICODE), + 'error_message' => $e->getMessage(), + 'status' => 0, + 'retry_count' => 1, + ]); + } + throw new \Exception('WordPress推送失败:' . $e->getMessage()); + } + } + + /** + * 构建 Gutenberg Blocks 格式 content + */ + private function buildGutenbergContent($car, array $uploadedImages, $faqs): string + { + $content = ''; + + // 1. 车辆相册标题 + $content .= "\n"; + $content .= "
" . htmlspecialchars($para) . "
\n"; + $content .= "\n\n"; + } + } + + // 4. Parameters xz-tab block + $tabData = $this->buildXzTabData($car); + $content .= "\n\n"; + + // 5. FAQ + if ($faqs->count() > 0) { + $content .= "\n"; + $content .= "" . htmlspecialchars($fq['answer'][0]) . "
"; + $content .= "| ' . htmlspecialchars($row['label']) . ' | ' . "\n"; + $html .= '' . htmlspecialchars($value) . ' | ' . "\n"; + $html .= '