validator; } /** * @param Request $request * @return mixed|Response * @throws \GuzzleHttp\Exception\GuzzleException */ public function createPost(Request $request) { $client = new Client(); $url = "https://37car.dev.hnxinyouhe.com/wp-json/wp/v2/posts"; $data = [ "title" => "API 创建文章", "content" => "内容来自 API", "status" => "publish" ]; try { $response = $client->post($url, [ RequestOptions::AUTH => ['Borderless', '3Tdw 6O02 DM6f lb2S 7T3l WG8a'], RequestOptions::JSON => $data, RequestOptions::VERIFY => false, // 禁用 SSL 验证 ]); $result = json_decode($response->getBody()->getContents(), true); return $result; } catch (\Exception $e) { throw new \Exception($e->getMessage()); } } /** * @param Request $request * @return mixed|Response * @throws \GuzzleHttp\Exception\GuzzleException */ public function createProduct(string $domain, $id) { $site = WordpressSite::where('site_url',$domain)->first(); //判断是否重复推送 $wpUsed = WordpressUsedCar::where('wp_site_id', $site['id'])->where('used_car_id', $id)->first(); if($wpUsed && ($wpUsed['retry_count']>2 || $wpUsed['status'] ==1)){ $site->last_uid = $id; $site->save(); return '重复推送'; } $client = new Client(); // $url = $domain."/wp-json/wc/v3/products"; $url = $domain."/wp-json/wp/v2/product"; $row = UsedCar::where('id', $id)->first(); $ucImages = explode(",", $row->images); //车辆相册 $images = []; foreach ($ucImages as $ucImage) { $image = $this->uploadImageToWp($domain, $site['api_user'], $site['api_secret'], $ucImage); // $images[] = ['id' => $image['id']]; $images[] = $image['id']; } //$images = array_merge([$images[0]], $images); //车辆详细信息 /*$detils= $this->drawDetils($row); $detils_params_html = []; foreach ($detils as $key => $detil) { if(is_array($detil)){ $detil = implode(",", $detil); } $detils_params_html[] =$key.':'.(string)$detil; } $detils_params_html = implode(PHP_EOL, $detils_params_html);*/ // $detils_params_html = $this->buildVehicleDetailsTable($detils); //语言确定目标网站的语言 $locale = match ($row['locale']) { 'en' => '英文', 'ru' => '俄语', 'ar' => '阿拉伯语', 'fr ' => '法语', default => '英文', }; $spark = new SparkService(); $car_name = $row['product_name']; $car_name_locale = $spark->chat('将车辆名称翻译为'.$locale.':'.$car_name,null,[],'wpSend'); $condition_desc_locale = $spark->chat("下面是车辆的车况介绍将卖家信息去掉,包括但不限于商家名称、集团简介、经销商信息、地址、电话、商家介绍等都去掉\"".$row['condition_desc']."\"",null,[],'wpSend'); $condition_desc_locale = $spark->chat("将下列车况描述翻译为".$locale.":".$condition_desc_locale); $data = [ "title" => $car_name_locale, "Content" => '', "excerpt" => '', "type" => "simple", //单品,不走规格变体 "regular_price" => number_format(($row['price']*$site['exchange_rate']), 2), "sku" => "", //"featured" => true,//是否精选 "description" => $condition_desc_locale, "stock_status" => "instock",//instock现车 // "categories" => [ // ["id" => 16],//used二手车 // ], //"images" => $images, "status" => 'publish', //发布状态 ]; //$data['categories'][] = ["id" => $site['categories']]; //产品分类 $data['product_cat'] = explode(',', $site['categories']); //车辆档案相关参数 $meta = $this->drawMeta($row); //车身颜色 $color = ''; foreach ($meta as $key => $item){ if (!empty($item['key']) && ($item['key'] == 'body_color' || $item['key'] == 'source_place')) { $goal = $this->sparkTransl($spark,$item['value'],$locale); $meta[$key]['value'] = $goal; $color = $item['key'] == 'body_color'?$goal:''; } } if ($site['tpl_type'] == 2) { $meta = []; } //属性:品牌、车体类型、颜色 $brand = $row['crawl_brand']?:$row['product_name']; $brand_locale = $spark->chat('将车辆品牌翻译为'.$locale.':'.$brand,null,[],'wpSend'); //查询品牌id $brand_id = $this->get_or_create_brand_id($site,$domain, $brand_locale); $data['pa_brand'] = [$brand_id]; //查询车身类型 $bodys = $this->get_body($site,$domain); $bady_style = array_column($bodys, 'name'); $style = $spark->chat('根据给出的信息判断车身类型(不要回答多余的文字,只需要返回车身类型,无法判断则返回:Other),车身类型有:'.implode('、',$bady_style).',车辆数据为:'.json_encode($row->toArray(),JSON_UNESCAPED_UNICODE),null,[],'wpSend'); if(!in_array($style,$bady_style)){ $style = 'Other'; } $style_id = 0; foreach ($bodys as $key => $item){ if ($item['name'] == $style) { $style_id = $item['id']; } } $data['pa_body-style'] = [$style_id]; // 翻译车况简介 // $baseInfo = $this->parseCarConfigDetail($row['format_car_info']); $conditionDesc_locale = $spark->chat('将车况描述翻译为'.$locale.':'.$row['condition_desc'],null,[],'wpSend'); //翻译车辆简介描述 $desc = "车辆名称:{$car_name},车辆参数:".json_encode($meta); $vehicleDescription_locale = $spark->chat('根据给出的信息生成'.$locale.'车辆简介描述(要求简短):'.$desc,null,[],'wpSend'); $meta[] = [ 'key' => 'pro-intro-des',//车况介绍 'value' => $vehicleDescription_locale, ]; $meta[] = [ 'key' => 'condition_desc',//产品简介 'value' => $conditionDesc_locale, ]; // $meta[] = [ //列表标签 // 'key' => 'hotpro_mark',//自定义热门产品标签 // 'value' => 'In Stock', //In Stock、HOT SALE // ]; //$data['meta'] = array_values($meta); foreach ($meta as $item){ $data['acf'][$item['key']] = $item['value']; } $data['featured_media'] = $images[0]; $data['acf']['product_gallery'] = $images; try { $response = $client->post($url, [ //RequestOptions::AUTH => ['Borderless', '3Tdw 6O02 DM6f lb2S 7T3l WG8a'], RequestOptions::AUTH => [$site['api_user'], $site['api_secret']], RequestOptions::JSON => $data, RequestOptions::VERIFY => false, // 禁用 SSL 验证 ]); $result = json_decode($response->getBody()->getContents(), true); $wordpressUsedCar = [ 'wp_site_id' => $site['id'], 'used_car_id' => $id, 'wp_post_id' => $result['id'], 'request_payload' => json_encode($data, JSON_UNESCAPED_UNICODE), 'response_body' => $response->getBody()->getContents(), 'status' => 1, 'retry_count' => 1, ]; WordpressUsedCar::create($wordpressUsedCar); $site->last_uid = $id; $site->save(); $data ['result'] = $result; return $data; //return $result; } catch (\Exception $e) { $his = WordpressUsedCar::where('wp_site_id', $site['id'])->where('used_car_id', $id)->first(); if ($his){ $his->retry_count = $his->retry_count + 1; $his->save(); if ($his->retry_count>=3){ $site->last_uid = $id; $site->save(); } }else{ $wordpressUsedCar = [ 'wp_site_id' => $site['id'], 'used_car_id' => $id, 'request_payload' => json_encode($data, JSON_UNESCAPED_UNICODE), // 'response_body' => !empty($response)?$response->getBody()->getContents():$response, 'error_message' => $e->getMessage(), 'status' => 0, 'retry_count' => 1, ]; WordpressUsedCar::create($wordpressUsedCar); } throw new \Exception($e->getMessage()); } } /** * 使用spark lite对话翻译,检查如果没有中文直接返回,有中文则翻译 * @param $spark * @param $str * @param $locale * @return mixed */ public function sparkTransl($spark,$str,$locale) { if(!(preg_match('/[\x{4e00}-\x{9fff}]/u', $str) > 0)){ return $str; }; $str = str_replace(["“", "”"], "", $str); //翻译 $vehicleDescription_locale = $spark->chat('将“'.$str.'”翻译为'.$locale,null,[],'wpSend'); return $vehicleDescription_locale; } /** * 获取meta_data字段,即ACF自定义的字段 * @param $used_detail * @return array * @throws \Exception */ public function drawMeta($used_car){ $fieldMap = [ //中文字段 → ACF 字段名 'plate_time', //上牌时间 'displacement', //排量 'mileage', //里程 'power', //马力 'body_color', //车身颜色 'source_place', //车源地 'price', //价格 ]; $metaData = []; foreach ($fieldMap as $acfKey) { if (!empty($used_car[$acfKey])) { $metaData[] = [ 'key' => $acfKey, 'value' => $used_car[$acfKey], ]; } } return $metaData; } /** * 安全取值 */ public function drawDetils($row) { $baseInfo = $this->parseCarConfigDetail($row['format_car_info']); return [ 'Ref ID' => $row['used_car_id'], 'Model Code' => $row['model_code'] ?? $row['car_name'], 'Model Year' => $row['year'], 'Exterior Color' => value($baseInfo, 'body_color'), 'Engine' => value($baseInfo, 'engine_description'), 'Drivetrain' => value($baseInfo, 'driver_form'), 'Range' => '–', 'Seats' => value($baseInfo, 'seat_count'), 'Dim.(mm)' => value($baseInfo, 'length_width_height'), 'Weight(kg)' => value($baseInfo, 'curb_weight'), 'Steering' => 'Left', 'Body Type' => value($baseInfo, 'car_body_struct'), 'Mlg(km)' => $row['usage_mileage'], 'Fuel' => $row['fuel_form'], 'Transmission' => value($baseInfo, 'gearbox_description'), 'Batt.Cap.(kWh)' => value($baseInfo, 'battery_capacity'), 'Motor Power(kW)' => value($baseInfo, 'motor_power'), 'M³' => value($baseInfo, 'cargo_volume'), 'Doors' => value($baseInfo, 'door_nums'), ]; } /** * 安全取值 */ function value(array $array, string $key, $default = '–') { if (!isset($array[$key]) || $array[$key] === '' || $array[$key] === null) { return $default; } return $array[$key]; } /** * 将 format_car_info 拉平成 key => value * * @param string $json * @return array */ function parseCarConfigDetail(string $json): array { $data = json_decode($json, true); if (!is_array($data)) { return []; } $result = []; foreach ($data as $group) { if (!isset($group['items']) || !is_array($group['items'])) { continue; } foreach ($group['items'] as $item) { if (!isset($item['key'])) { continue; } $result[$item['key']] = $item['value'] ?? ''; } } return $result; } /** * 从 config_detail 中提取配置信息,拉平成 key => value * * @param array $configDetail * @return array */ private function parseConfigDetail(array $configDetail): array { if (!is_array($configDetail)) { return []; } $result = []; foreach ($configDetail as $group) { if (!isset($group['items']) || !is_array($group['items'])) { continue; } foreach ($group['items'] as $item) { if (!isset($item['key'])) { continue; } $key = $item['key']; $value = $item['value'] ?? ''; // 如果值不为空,存储它(如果已存在相同key,保留第一个非空值) if ($value !== '' && $value !== null && (!isset($result[$key]) || $result[$key] === '')) { $result[$key] = $value; } } } return $result; } /** * 从 config_detail 中获取指定配置项的值 * * @param array $configDetail * @param string|array $keys 要查找的key,可以是字符串或数组(多个可能的key) * @param string $default 默认值 * @return string */ private function getConfigValue($configDetail, $keys, $default = ''): string { if (!is_array($configDetail)) { return $default; } $keysToSearch = is_array($keys) ? $keys : [$keys]; foreach ($configDetail as $group) { if (!isset($group['items']) || !is_array($group['items'])) { continue; } foreach ($group['items'] as $item) { if (!isset($item['key'])) { continue; } $itemKey = $item['key']; $itemValue = $item['value'] ?? ''; // 检查是否匹配任何要查找的key foreach ($keysToSearch as $searchKey) { if ($itemKey === $searchKey && $itemValue !== '' && $itemValue !== null) { return (string)$itemValue; } } } } return $default; } /** * 查询wp车身类型 * @param $brand_name * @return mixed * @throws \GuzzleHttp\Exception\GuzzleException */ function get_body($site,$domain) { $client = new Client([ 'base_uri' => $domain . '/wp-json/wp/v2/', RequestOptions::AUTH => [$site['api_user'], $site['api_secret']], // 'auth' => ['your_username', 'your_password'], // 或者使用应用密码 RequestOptions::VERIFY => false, // 禁用 SSL 验证 ]); // 查询车身类型 $response = $client->get('pa_body-style'); $bodys = json_decode($response->getBody(), true); // 返回车身类型 return $bodys; } /** * 查询wp品牌id * @param $brand_name * @return mixed * @throws \GuzzleHttp\Exception\GuzzleException */ function get_or_create_brand_id($site,$domain, $brand_name) { if(empty($brand_name)){ return 0; } $client = new Client([ 'base_uri' => $domain . '/wp-json/wp/v2/', RequestOptions::AUTH => [$site['api_user'], $site['api_secret']], // 'auth' => ['your_username', 'your_password'], // 或者使用应用密码 RequestOptions::VERIFY => false, // 禁用 SSL 验证 ]); // 生成slug $slug = $this->generate_slug($brand_name); // 查询品牌是否已存在 $response = $client->get('pa_brand', [ 'query' => [ 'slug' => $slug // 用品牌名称生成slug查询 ] ]); $brands = json_decode($response->getBody(), true); // 如果品牌存在,返回ID if (!empty($brands)) { return $brands[0]['id']; // 返回第一个匹配的品牌ID } // 品牌不存在,先创建 $response = $client->post('pa_brand', [ 'json' => [ 'name' => $brand_name, 'slug' => $slug ] ]); $brand = json_decode($response->getBody(), true); // 返回创建的品牌ID return $brand['id']; } function generate_slug($brand_name) { // 将品牌名称转成小写并替换掉非字母数字字符 return preg_replace('/[^a-z0-9-]+/', '-', strtolower(trim($brand_name))); } /** * 计算使用时间 * * @param string $licenseDate 上牌时间,格式可能是 YYYY-MM-DD 或其他 * @return string|null */ private function calculateUsageTime($licenseDate): ?string { if (empty($licenseDate)) { return null; } try { // 尝试解析日期 $date = null; if (preg_match('/^(\d{4})-(\d{1,2})-(\d{1,2})/', $licenseDate, $matches)) { $date = new \DateTime($matches[1] . '-' . $matches[2] . '-' . $matches[3]); } elseif (preg_match('/^(\d{4})-(\d{1,2})/', $licenseDate, $matches)) { $date = new \DateTime($matches[1] . '-' . $matches[2] . '-01'); } elseif (is_numeric($licenseDate) && strlen($licenseDate) == 10) { // Unix时间戳 $date = new \DateTime('@' . $licenseDate); } if (!$date) { return null; } $now = new \DateTime(); $diff = $now->diff($date); $years = $diff->y; $months = $diff->m; if ($years > 0) { $months += $years * 12; } if ($months == 0) { // 如果不到1个月,显示天数 $days = $diff->days; if ($days > 0) { return $days == 1 ? "1 day" : "{$days} days"; } return null; } if ($months == 1) { return "1 month"; } else { return "{$months} months"; } } catch (\Exception $e) { Log::error('计算使用时间失败', ['license_date' => $licenseDate, 'error' => $e->getMessage()]); return null; } } /** * 获取内饰颜色 * * @param array $configDetail * @param array $baseInfo * @return string */ private function getInteriorColor($configDetail, $baseInfo): string { $possibleKeys = ['nsys', 'interior_color', 'interiorColor', 'seat_color', 'seatColor', '内饰颜色']; // 优先从 configDetail 中查找 $color = $this->getConfigValue($configDetail, $possibleKeys, ''); if (!empty($color) && $color !== '–') { return $color; } // 如果 configDetail 中没有找到,尝试通过中文字段名查找 if (is_array($configDetail)) { foreach ($configDetail as $group) { if (isset($group['items']) && is_array($group['items'])) { foreach ($group['items'] as $item) { // 检查中文字段名 if (isset($item['name']) && (strpos($item['name'], '内饰') !== false || strpos($item['name'], '座椅') !== false)) { if (!empty($item['value']) && $item['value'] !== '–') { return $item['value']; } } } } } } // 最后尝试从 baseInfo 中查找 foreach ($possibleKeys as $key) { if (!empty($baseInfo[$key]) && $baseInfo[$key] !== '–') { return $baseInfo[$key]; } } return ''; } /** * 将颜色翻译为英文 * * @param string $color 颜色(可能是中文) * @return string */ private function translateColorToEnglish($color): string { if (empty($color)) { return ''; } $colorMap = [ '白色' => 'white', '黑色' => 'black', '红色' => 'red', '蓝色' => 'blue', '绿色' => 'green', '黄色' => 'yellow', '银色' => 'silver', '灰色' => 'gray', '灰色' => 'grey', '棕色' => 'brown', '金色' => 'gold', '橙色' => 'orange', '紫色' => 'purple', '粉色' => 'pink', '米色' => 'beige', '浅色' => 'light-colored', '深色' => 'dark-colored', ]; $colorLower = mb_strtolower(trim($color), 'UTF-8'); // 如果已经是英文,直接返回(转小写) if (preg_match('/^[a-z\s-]+$/i', $color)) { return strtolower($color); } // 查找中文映射 foreach ($colorMap as $cn => $en) { if (strpos($colorLower, $cn) !== false) { return $en; } } // 如果没有找到映射,尝试提取可能的英文部分,否则返回原值 if (preg_match('/[a-z]+/i', $color, $matches)) { return strtolower($matches[0]); } return strtolower($color); } /** * 构建 Vehicle Details 4列表格(2组 Key/Value) * * @param array $items [['label'=>'Ref ID','value'=>'xxx'], ...] * @return string */ public function buildVehicleDetailsTable(array $items): string { $html = ''; // 每行 2 个字段(4 列) $chunks = array_chunk($items, 2,true ); foreach ($chunks as $row) { $html .= ''; foreach ($row as $label => $value) { $label = htmlspecialchars($label); $value = htmlspecialchars((string)($value ?: '–')); $html .= ""; } // 如果是奇数,补空列 if (count($row) === 1) { $html .= ''; } $html .= ''; } $html .= '
{$label}{$value}
'; return $html; } /** * 上传 OSS 图片到 WordPress 媒体库 * * @param string $domain * @param string $imageUrl 阿里云 OSS 图片地址 * @return array * @throws \Exception */ public function uploadImageToWp(string $domain,$api_user, $api_secret, string $imageUrl = "fdesk_test/usedcar_crawl/20251125/ff847a6a2b3114b4c2c230455b74cd2e.jpg"): array { $process = 'image/resize,w_800/quality,Q_50/format,webp'; $processedUrl = (new OssService)->generatePrivateUrlResize($imageUrl,3600,$process); $client = new Client(); // 从原始 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'; // 本地临时文件路径 $tempDir = runtime_path('temp') . '/wp_images/'; if (!is_dir($tempDir)) { mkdir($tempDir, 0755, true); } $tempFilePath = $tempDir . md5($processedUrl) . '.' . $extension; try { // 1. 下载图片到本地临时文件 $imageContent = file_get_contents($processedUrl); if ($imageContent === false) { throw new \Exception('图片下载失败:' . $processedUrl); } // 写入临时文件 $bytesWritten = file_put_contents($tempFilePath, $imageContent); if ($bytesWritten === false) { throw new \Exception('写入临时文件失败:' . $tempFilePath); } // 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); return $result; } catch (\Exception $e) { Log::error('上传图片到WordPress失败', [ 'image_url' => $imageUrl, 'error' => $e->getMessage(), ]); throw new \Exception('上传图片失败:' . $e->getMessage()); } finally { // 3. 删除临时文件 if (file_exists($tempFilePath)) { @unlink($tempFilePath); } } } /** * 上传视频到 WordPress 媒体库 * * @param string $domain * @param string $api_user * @param string $api_secret * @param string $videoUrl 视频URL * @return array * @throws \Exception */ public function uploadVideoToWp(string $domain, string $api_user, string $api_secret, string $videoUrl): array { // 使用 OssService 生成完整的视频URL(不带处理参数) $ossUrl = (new OssService)->generatePrivateUrl($videoUrl, 3600); $client = new Client(); // 从原始 URL 提取文件扩展名 $pathInfo = pathinfo(parse_url($videoUrl, PHP_URL_PATH)); $extension = strtolower($pathInfo['extension'] ?? 'mp4'); $filename = ($pathInfo['filename'] ?? 'video') . '.' . $extension; // 确定Content-Type $mimeTypes = [ 'mp4' => 'video/mp4', 'webm' => 'video/webm', 'ogg' => 'video/ogg', 'mov' => 'video/quicktime', 'avi' => 'video/x-msvideo', 'wmv' => 'video/x-ms-wmv', 'flv' => 'video/x-flv', ]; $contentType = $mimeTypes[$extension] ?? 'video/mp4'; $mediaUrl = $domain . '/wp-json/wp/v2/media'; // 本地临时文件路径 $tempDir = runtime_path('temp') . '/wp_videos/'; if (!is_dir($tempDir)) { mkdir($tempDir, 0755, true); } $tempFilePath = $tempDir . md5($videoUrl) . '.' . $extension; try { // 1. 下载视频到本地临时文件 $videoContent = file_get_contents($ossUrl); if ($videoContent === false) { throw new \Exception('视频下载失败:' . $ossUrl); } // 写入临时文件 $bytesWritten = file_put_contents($tempFilePath, $videoContent); if ($bytesWritten === false) { throw new \Exception('写入临时文件失败:' . $tempFilePath); } // 2. 使用 GuzzleHttp 上传本地文件到 WP 媒体库 $fileHandle = fopen($tempFilePath, 'r'); $safeFilename = 'video_' . time() . '.' . $extension; $multipart = [ [ 'name' => 'file', 'filename' => $safeFilename, 'contents' => $fileHandle, 'headers' => [ 'Content-Type' => $contentType, ], ], ]; try { $response = $client->post($mediaUrl, [ RequestOptions::AUTH => [$api_user, $api_secret], RequestOptions::MULTIPART => $multipart, RequestOptions::VERIFY => false, RequestOptions::TIMEOUT => 300, RequestOptions::HEADERS => [ 'Accept' => 'application/json', ], ]); } catch (\Exception $uploadException) { // GuzzleHttp 上传失败,尝试使用 curl if (is_resource($fileHandle)) { fclose($fileHandle); } // 使用原生 curl 上传 $ch = curl_init(); $cfile = new \CURLFile($tempFilePath, $contentType, $safeFilename); curl_setopt_array($ch, [ CURLOPT_URL => $mediaUrl, CURLOPT_RETURNTRANSFER => true, CURLOPT_POST => true, CURLOPT_POSTFIELDS => ['file' => $cfile], CURLOPT_HTTPAUTH => CURLAUTH_BASIC, CURLOPT_USERPWD => $api_user . ':' . $api_secret, CURLOPT_SSL_VERIFYPEER => false, CURLOPT_TIMEOUT => 300, CURLOPT_HTTPHEADER => [ 'Accept: application/json', 'Content-Type: multipart/form-data', ], ]); $responseBody = curl_exec($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); $curlError = curl_error($ch); curl_close($ch); if ($curlError) { throw new \Exception('Curl上传失败:' . $curlError); } if ($httpCode >= 400) { throw new \Exception('Curl上传失败 HTTP ' . $httpCode . ': ' . $responseBody); } // 创建一个模拟的 Response 对象 $response = new \GuzzleHttp\Psr7\Response($httpCode, [], $responseBody); } // 关闭文件句柄 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'] ?? 'unknown', ]); return $result; } catch (\Exception $e) { Log::error('上传视频到WordPress失败', [ 'video_url' => $videoUrl, 'error' => $e->getMessage(), ]); throw new \Exception('上传视频失败:' . $e->getMessage()); } finally { // 3. 删除临时文件 if (file_exists($tempFilePath)) { @unlink($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 转换后的值 // 解析图片列表 - 优先使用已转换的数组,否则尝试解析原始字符串 $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); } } // 上传图片到WordPress媒体库 $uploadedImages = []; // 存储完整信息 ['id' => xx, 'url' => xx, ...] $uploadedImageIds = []; // 仅存储ID foreach ($images as $index => $imageUrl) { if (!empty($imageUrl)) { try { $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(), ]); } } } // 上传视频到WordPress媒体库 $videoMediaId = null; $videoSourceUrl = null; if (!empty($car->intro_video)) { try { $uploadedVideo = $this->uploadVideoToWp($domain, $site->api_user, $site->api_secret, $car->intro_video); $videoMediaId = $uploadedVideo['id']; $videoSourceUrl = $uploadedVideo['source_url'] ?? $uploadedVideo['url'] ?? ''; } catch (\Exception $e) { Log::error('视频上传失败', ['error' => $e->getMessage()]); // 视频上传失败不影响整体推送,继续执行 } } // 获取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, $videoMediaId, $videoSourceUrl); // 构建产品数据 $data = [ 'title' => $title, 'content' => $content, 'status' => 'publish', 'type' => 'simple', 'regular_price' => number_format($car->price * $site->exchange_rate, 2), ]; // 设置产品分类使用 product_cat 字段) $data['product_cat'] = [15, 16]; // 处理品牌:翻译并获取/创建品牌ID $brand = $car->crawl_brand ?: $car->product_name; $brandEnglish = $this->translateIfNeeded($brand, '英文'); $brandId = $this->get_or_create_brand_id($site, $domain, $brandEnglish); if ($brandId) { $data['pa_brand'] = [$brandId]; } // 设置主图和车辆相册(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; } // 设置ACF车辆参数字段(与旧站点保持一致) $fieldMap = [ 'plate_time' => '上牌时间', 'displacement' => '排量', 'mileage' => '里程', 'power' => '马力', 'body_color' => '车身颜色', 'source_place' => '车源地', 'price' => '价格', ]; if (str_contains($car->mileage, '万') || str_contains($car->mileage, '万公里')) { $car->mileage = str_replace(['万', '公里', '万公里'],'',$car->mileage)*10000; } 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; // } // 视频通过 content 中的 video block 展示,不需要 ACF 字段 try { $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, $videoMediaId = null, $videoSourceUrl = null): string { $content = ''; // 1. 车辆相册标题 $content .= "\n"; $content .= "

Vehicle Image

\n"; $content .= "\n\n"; // 2. 图库 block(使用第6张开始的图片,不足8张时重复使用前5张) $content .= "\n"; $content .= "\n"; $content .= "\n\n"; // 3. Description 标题和段落 $content .= "\n"; $content .= "

Description

\n"; $content .= "\n\n"; // 翻译并分段处理 condition_desc $conditionDesc = $this->translateIfNeeded($car->condition_desc ?? '', '英文'); $paragraphs = preg_split('/\n\n+/', trim($conditionDesc)); foreach ($paragraphs as $para) { $para = trim($para); if (!empty($para)) { $content .= "\n"; $content .= "

" . htmlspecialchars($para) . "

\n"; $content .= "\n\n"; } } // 4. Parameters 标题 $content .= "\n"; $content .= "

Parameters

\n"; $content .= "\n\n"; // Parameters xz-tab block $tabData = $this->buildXzTabData($car); $content .= "\n\n"; // 5. Video section (如果有视频) if ($videoMediaId && $videoSourceUrl) { $content .= "\n"; $content .= "

\n"; $content .= "\n\n"; $content .= "\n"; $content .= "

Video

\n"; $content .= "\n\n"; $content .= "\n"; $content .= "
\n"; $content .= "\n\n"; } // 6. FAQ if ($faqs->count() > 0) { $content .= "\n"; $content .= "

FAQ

\n"; $content .= "\n\n"; $faqQuestions = []; foreach ($faqs as $index => $faq) { $question = $this->translateIfNeeded($faq->question, '英文'); $answer = $this->translateIfNeeded($faq->answer, '英文'); $faqQuestions[] = [ 'id' => 'faq-question-' . $car->id . '-' . $index, 'question' => [$question], 'answer' => [$answer], 'jsonQuestion' => $question, 'jsonAnswer' => $answer, ]; } $faqData = ['questions' => $faqQuestions]; $content .= "\n"; $content .= "
"; foreach ($faqQuestions as $fq) { $content .= "
"; $content .= "" . htmlspecialchars($fq['question'][0]) . " "; $content .= "

" . htmlspecialchars($fq['answer'][0]) . "

"; $content .= "
"; } $content .= "
\n"; $content .= "\n\n"; } return $content; } /** * 构建 xz-tab 数据结构 */ private function buildXzTabData($car): array { // Tab 标题 $tabTitles = [ 'Basic Info', 'Body', 'Transmission', 'Chassis & Steering', 'Active Safety', ]; // Tab 内容数据 $tabContents = [ // Tab 1: 基本信息 $this->buildTabTable([ ['label' => 'Official Guide Price', 'value' => $car->official_guide_price], ['label' => 'Manufacturer', 'value' => $car->manufacturer], ['label' => 'Level', 'value' => $car->level], ['label' => 'Energy Type', 'value' => $car->energy_type], ['label' => 'Listing Time', 'value' => $car->listing_time], ['label' => 'Engine', 'value' => $car->engine], ['label' => 'Electric Motor', 'value' => $car->electric_motor], ['label' => 'NEDC Range (Km)', 'value' => $car->nedc_range], ['label' => 'CLTC Range (Km)', 'value' => $car->cltc_range], ['label' => 'Fast Charge Platform', 'value' => $car->fast_charge_platform], ['label' => 'Charging Time (Hours)', 'value' => $car->charging_time], ['label' => 'Fast Charge Percent (%)', 'value' => $car->fast_charge_percent], ['label' => 'Max Power (Kw)', 'value' => $car->max_power_kw], ['label' => 'Max Torque (N·m)', 'value' => $car->max_torque_nm], ['label' => 'Transmission', 'value' => $car->transmission_desc], ['label' => 'Dimensions (Mm)', 'value' => $car->dimensions], ['label' => 'Body Structure', 'value' => $car->body_structure], ['label' => 'Max Speed (Km/H)', 'value' => $car->max_speed], ['label' => 'Acceleration 100 (S)', 'value' => $car->acceleration_100], ['label' => 'NEDC Fuel Consumption', 'value' => $car->nedc_fuel_consumption], ['label' => 'Power Consumption 100', 'value' => $car->power_consumption_100], ['label' => 'Equivalent Fuel Consumption', 'value' => $car->equivalent_fuel_consumption], ]), // Tab 2: 车身 $this->buildTabTable([ ['label' => 'Body Length (Mm)', 'value' => $car->body_length], ['label' => 'Body Width (Mm)', 'value' => $car->body_width], ['label' => 'Body Height (Mm)', 'value' => $car->body_height], ['label' => 'Wheelbase (Mm)', 'value' => $car->wheelbase], ['label' => 'Body Structure', 'value' => $car->body_structure], ['label' => 'Door Count', 'value' => $car->door_count], ['label' => 'Door Open Type', 'value' => $car->door_open_type], ['label' => 'Seat Count', 'value' => $car->seat_count], ]), // Tab 3: 变速箱 $this->buildTabTable([ ['label' => 'Transmission Description', 'value' => $car->transmission_desc], ['label' => 'Gear Count', 'value' => $car->gear_count], ['label' => 'Transmission Type', 'value' => $car->transmission_type], ]), // Tab 4: 底盘/转向 $this->buildTabTable([ ['label' => 'Drive Mode', 'value' => $car->drive_mode], ['label' => 'Front Suspension', 'value' => $car->front_suspension], ['label' => 'Rear Suspension', 'value' => $car->rear_suspension], ['label' => 'Steering Type', 'value' => $car->steering_type], ]), // Tab 5: 主动安全 $this->buildTabTable([ ['label' => 'Has ABS', 'value' => $car->has_abs], ['label' => 'Has EBD', 'value' => $car->has_ebd], ['label' => 'Has BRA', 'value' => $car->has_bra], ['label' => 'Has TCS', 'value' => $car->has_tcs], ['label' => 'Has ESP', 'value' => $car->has_esp], ['label' => 'Has Active Brake', 'value' => $car->has_active_brake], ['label' => 'Has Lane Assist', 'value' => $car->has_lane_assist], ['label' => 'Has LKA', 'value' => $car->has_lka], ['label' => 'Has Lane Centering', 'value' => $car->has_lane_centering], ['label' => 'Has Fatigue Remind', 'value' => $car->has_fatigue_remind], ]), ]; $tabCount = 5; $data = [ 'name' => 'acf/xz-tab', 'data' => [], 'mode' => 'edit', ]; for ($i = 0; $i < $tabCount; $i++) { $data['data']["xz-tabs_{$i}_tab_title"] = $tabTitles[$i] ?? "Tab {$i}"; $data['data']["_xz-tabs_{$i}_tab_title"] = "field_69ae438a9a253"; $data['data']["xz-tabs_{$i}_tab_content"] = $tabContents[$i] . ' '; $data['data']["_xz-tabs_{$i}_tab_content"] = "field_69ae43959a254"; } $data['data']['xz-tabs'] = $tabCount; $data['data']['_xz-tabs'] = "field_69ae420f515f6"; return $data; } /** * 构建参数表格 HTML */ private function buildTabTable(array $rows): string { $html = '' . "\n"; $html .= '' . "\n"; foreach ($rows as $row) { $value = $row['value'] ?? ''; if (empty($value) && $value !== '0') { continue; } // 翻译中文值 if (is_string($value) && $this->containsChinese($value)) { $value = $this->translateIfNeeded($value, '英文'); } $html .= '' . "\n"; $html .= '' . "\n"; $html .= '' . "\n"; $html .= '' . "\n"; } $html .= '' . "\n"; $html .= '
' . htmlspecialchars($row['label']) . '' . htmlspecialchars($value) . '
' . "\n"; return $html; } /** * 翻译文本(如果包含中文) */ private function translateIfNeeded(string $text, string $targetLang = '英文'): string { if (empty($text)) { return ''; } // 检查是否包含中文 if (!$this->containsChinese($text)) { return $text; } try { $spark = new SparkService(); $prompt = '将以下文本翻译为' . $targetLang . ':' . $text; return $spark->chat($prompt, null, [], 'wpSend'); } catch (\Throwable $e) { Log::warning('翻译失败,使用原文本', ['text' => $text, 'error' => $e->getMessage()]); return $text; } } /** * 检测文本是否包含中文 */ private function containsChinese(string $text): bool { return preg_match('/[\x{4e00}-\x{9fff}]/u', $text) > 0; } }