Files
useds/app/common/service/WordpressSiteService.php
T
2026-03-12 16:22:59 +08:00

1744 lines
62 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
namespace app\common\service;
use app\common\model\UsedCar;
use app\common\model\UsedcarCrawlResults;
use app\common\model\UsedcarCrawlResultsImages;
use app\common\model\WordpressUsedCar;
use app\common\validate\WordpressSiteValidate;
use app\common\traits\crud\CrudServiceTrait;
use app\common\model\WordpressSite;
use GuzzleHttp\Client;
use GuzzleHttp\RequestOptions;
use support\Log;
use support\Request;
use support\Response;
class WordpressSiteService
{
use CrudServiceTrait;
/**
* 指定当前要操作的模型
*/
protected string $modelClass = WordpressSite::class;
/**
* 验证器
*/
protected $validator = WordpressSiteValidate::class;
/**
* 获取验证器
*/
public function getValidator()
{
return new $this->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;
}
// 获取API凭证,支持模型对象和数组
$apiUser = is_object($site) ? $site->api_user : ($site['api_user'] ?? null);
$apiSecret = is_object($site) ? $site->api_secret : ($site['api_secret'] ?? null);
if (empty($apiUser) || empty($apiSecret)) {
Log::error('WordPress站点API凭证为空', [
'site_type' => is_object($site) ? get_class($site) : 'array',
]);
return 0;
}
$client = new Client([
'base_uri' => $domain . '/wp-json/wp/v2/',
RequestOptions::AUTH => [$apiUser, $apiSecret],
RequestOptions::VERIFY => false, // 禁用 SSL 验证
]);
// 生成slug
$slug = $this->generate_slug($brand_name);
try {
// 查询品牌是否已存在
$response = $client->get('pa_brand', [
'query' => [
'slug' => $slug
]
]);
$brands = json_decode($response->getBody()->getContents(), true);
// 如果品牌存在,返回ID
if (!empty($brands) && isset($brands[0]['id'])) {
return $brands[0]['id'];
}
} catch (\Exception $e) {
Log::error('查询品牌失败', [
'brand_name' => $brand_name,
'slug' => $slug,
'error' => $e->getMessage(),
]);
}
// 品牌不存在,先创建
try {
$response = $client->post('pa_brand', [
'json' => [
'name' => $brand_name,
'slug' => $slug
]
]);
$responseBody = $response->getBody()->getContents();
$brand = json_decode($responseBody, true);
if (empty($brand) || !isset($brand['id'])) {
Log::error('创建品牌失败,响应无效', [
'brand_name' => $brand_name,
'slug' => $slug,
'response' => $responseBody,
'status_code' => $response->getStatusCode(),
]);
return 0;
}
// 返回创建的品牌ID
return $brand['id'];
} catch (\Exception $e) {
Log::error('创建品牌失败', [
'brand_name' => $brand_name,
'slug' => $slug,
'error' => $e->getMessage(),
]);
return 0;
}
}
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 = '<table class="vehicle-details"><tbody>';
// 每行 2 个字段(4 列)
$chunks = array_chunk($items, 2,true );
foreach ($chunks as $row) {
$html .= '<tr>';
foreach ($row as $label => $value) {
$label = htmlspecialchars($label);
$value = htmlspecialchars((string)($value ?: '–'));
$html .= "<th>{$label}</th><td>{$value}</td>";
}
// 如果是奇数,补空列
if (count($row) === 1) {
$html .= '<th></th><td></td>';
}
$html .= '</tr>';
}
$html .= '</tbody></table>';
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
{
// 使用原始图片格式,不做webp转换,避免"File Modified"错误
$process = 'image/resize,w_800/quality,Q_50';
try {
$processedUrl = (new OssService)->generatePrivateUrlResize($imageUrl,3600,$process);
} catch (\Exception $e) {
Log::error('生成OSS URL失败', [
'image_url' => $imageUrl,
'error' => $e->getMessage(),
]);
throw new \Exception('生成图片URL失败: ' . $e->getMessage());
}
$client = new Client();
// 从原始 URL 提取文件扩展名
$pathInfo = pathinfo(parse_url($imageUrl, PHP_URL_PATH));
$extension = strtolower($pathInfo['extension'] ?? 'jpg');
// 确定文件名和 Content-Type(使用原始格式,不做webp转换)
$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) {
Log::error('从OSS下载图片失败', [
'original_url' => $imageUrl,
'processed_url' => $processedUrl,
]);
throw new \Exception('图片下载失败:' . $processedUrl);
}
// 检查下载的内容大小
if (strlen($imageContent) < 100) {
Log::error('下载的图片内容过小', [
'original_url' => $imageUrl,
'processed_url' => $processedUrl,
'size' => strlen($imageContent),
'content' => $imageContent,
]);
throw new \Exception('下载的图片内容无效');
}
// 写入临时文件
$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);
}
$responseBody = $response->getBody()->getContents();
$statusCode = $response->getStatusCode();
// 检查HTTP状态码
if ($statusCode !== 201 && $statusCode !== 200) {
Log::error('WordPress图片上传失败,状态码异常', [
'image_url' => $imageUrl,
'status_code' => $statusCode,
'response' => substr($responseBody, 0, 1000),
]);
throw new \Exception("WordPress返回状态码: {$statusCode}");
}
$result = json_decode($responseBody, true);
if (empty($result) || !is_array($result)) {
Log::error('WordPress图片上传响应解析失败', [
'image_url' => $imageUrl,
'response' => substr($responseBody, 0, 1000),
'status_code' => $statusCode,
'json_error' => json_last_error_msg(),
]);
throw new \Exception('WordPress响应解析失败: ' . json_last_error_msg());
}
if (!isset($result['id'])) {
Log::error('WordPress图片上传响应缺少ID', [
'image_url' => $imageUrl,
'response' => $responseBody,
]);
throw new \Exception('WordPress响应缺少媒体ID');
}
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);
}
}
}
/**
* 上传文件(PDF等附件)到 WordPress 媒体库
*
* @param string $domain
* @param string $api_user
* @param string $api_secret
* @param string $fileUrl 文件URL
* @return array
* @throws \Exception
*/
public function uploadFileToWp(string $domain, string $api_user, string $api_secret, string $fileUrl): array
{
// 使用 OssService 生成完整的文件URL
$ossUrl = (new OssService)->generatePrivateUrl($fileUrl, 3600);
$client = new Client();
// 从原始 URL 提取文件扩展名
$pathInfo = pathinfo(parse_url($fileUrl, PHP_URL_PATH));
$extension = strtolower($pathInfo['extension'] ?? 'pdf');
$filename = ($pathInfo['filename'] ?? 'file') . '.' . $extension;
// 确定Content-Type
$mimeTypes = [
'pdf' => 'application/pdf',
'doc' => 'application/msword',
'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'xls' => 'application/vnd.ms-excel',
'xlsx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'zip' => 'application/zip',
'rar' => 'application/x-rar-compressed',
];
$contentType = $mimeTypes[$extension] ?? 'application/octet-stream';
$mediaUrl = $domain . '/wp-json/wp/v2/media';
// 本地临时文件路径
$tempDir = runtime_path('temp') . '/wp_files/';
if (!is_dir($tempDir)) {
mkdir($tempDir, 0755, true);
}
$tempFilePath = $tempDir . md5($fileUrl) . '.' . $extension;
try {
// 1. 下载文件到本地临时文件
$fileContent = file_get_contents($ossUrl);
if ($fileContent === false) {
throw new \Exception('文件下载失败:' . $ossUrl);
}
// 写入临时文件
$bytesWritten = file_put_contents($tempFilePath, $fileContent);
if ($bytesWritten === false) {
throw new \Exception('写入临时文件失败:' . $tempFilePath);
}
// 2. 使用 GuzzleHttp 上传本地文件到 WP 媒体库
$fileHandle = fopen($tempFilePath, 'r');
$multipart = [
[
'name' => 'file',
'filename' => $filename,
'contents' => $fileHandle,
'headers' => [
'Content-Type' => $contentType,
],
],
];
$response = $client->post($mediaUrl, [
RequestOptions::AUTH => [$api_user, $api_secret],
RequestOptions::MULTIPART => $multipart,
RequestOptions::VERIFY => false,
RequestOptions::TIMEOUT => 60,
]);
// 关闭文件句柄
if (is_resource($fileHandle)) {
fclose($fileHandle);
}
$result = json_decode($response->getBody()->getContents(), true);
return $result;
} catch (\Exception $e) {
Log::error('上传文件到WordPress失败', [
'file_url' => $fileUrl,
'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'];
} 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()]);
// 视频上传失败不影响整体推送,继续执行
}
}
// 检查是否有成功上传的图片
if (empty($uploadedImages)) {
Log::warning('没有成功上传任何图片', [
'car_id' => $carId,
'original_images_count' => count($images),
]);
// 继续创建产品,但没有图片
}
// 获取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];
}
// 处理附件:上传第一个附件到WordPress,使用ACF字段存储
if (!empty($car->attachment)) {
$attachments = json_decode($car->attachment, true);
if (is_array($attachments) && count($attachments) > 0) {
$firstAttachment = $attachments[0];
$attachmentSrc = $firstAttachment['src'] ?? '';
if (!empty($attachmentSrc)) {
try {
$uploadedFile = $this->uploadFileToWp($domain, $site->api_user, $site->api_secret, $attachmentSrc);
// ACF attachment字段直接存储媒体ID
$data['acf']['attachment'] = $uploadedFile['id'];
} catch (\Exception $e) {
Log::error('附件上传失败', [
'attachment_src' => $attachmentSrc,
'error' => $e->getMessage(),
]);
}
}
}
}
// 设置主图和车辆相册(ACF字段)
if (!empty($uploadedImages)) {
$allImageIds = array_map(function($img) {
return $img['id'];
}, $uploadedImages);
$data['featured_media'] = $uploadedImages[0]['id'];
// 车辆相册:前8张图片
$productGalleryImages = array_slice($uploadedImages, 0, 8);
$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;
}
$car->power = str_replace('马力','',$car->power);
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;
}
}
$data['acf']['price'] = '$'.number_format($car->price * $site->exchange_rate, 2);
$data['acf']['source_num'] = $car->code;
// 翻译并设置车况介绍和产品简介(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 .= "<!-- wp:heading -->\n";
$content .= "<h2 class=\"wp-block-heading\" id=\"vehicle-image\">Vehicle Image</h2>\n";
$content .= "<!-- /wp:heading -->\n\n";
// 2. 图库 block(使用第6张开始的图片,不足8张时重复使用前5张)
$content .= "<!-- wp:gallery {\"columns\":4,\"linkTo\":\"none\"} -->\n";
$content .= "<figure class=\"wp-block-gallery has-nested-images columns-4 is-cropped\">\n";
$galleryImages = array_slice($uploadedImages, 8); // 第9张开始
$firstFiveImages = array_slice($uploadedImages, 0, 8); // 前8张
// 不足8张时,重复使用前5张图片
if (count($galleryImages) > 0 && count($galleryImages) < 8) {
$repeatSource = $firstFiveImages; // 重复使用前5张
while (count($galleryImages) < 8) {
$galleryImages = array_merge($galleryImages, $repeatSource);
}
$galleryImages = array_slice($galleryImages, 0, 8);
}
foreach ($galleryImages as $img) {
$imgId = $img['id'];
$imgUrl = $img['url'] ?? '';
// 构建 Gutenberg image block,包含完整的 URL
$blockAttrs = json_encode([
"id" => $imgId,
"sizeSlug" => "large",
"linkDestination" => "none",
"url" => $imgUrl,
], JSON_UNESCAPED_SLASHES);
$content .= "<!-- wp:image {$blockAttrs} -->\n";
$content .= "<figure class=\"wp-block-image size-large\"><img src=\"{$imgUrl}\" alt=\"\" class=\"wp-image-{$imgId}\"/></figure>\n";
$content .= "<!-- /wp:image -->\n";
}
$content .= "</figure>\n";
$content .= "<!-- /wp:gallery -->\n\n";
// 3. Description 标题和段落
$content .= "<!-- wp:heading -->\n";
$content .= "<h2 class=\"wp-block-heading\" id=\"Description\">Description</h2>\n";
$content .= "<!-- /wp:heading -->\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 .= "<!-- wp:paragraph -->\n";
$content .= "<p>" . htmlspecialchars($para) . "</p>\n";
$content .= "<!-- /wp:paragraph -->\n\n";
}
}
// 4. Parameters 标题
$content .= "<!-- wp:heading -->\n";
$content .= "<h2 class=\"wp-block-heading\" id=\"Parameters\">Parameters</h2>\n";
$content .= "<!-- /wp:heading -->\n\n";
// Parameters xz-tab block
$tabData = $this->buildXzTabData($car);
$content .= "<!-- wp:acf/xz-tab " . json_encode($tabData, JSON_UNESCAPED_UNICODE) . " /-->\n\n";
// 5. Video section (如果有视频)
if ($videoMediaId && $videoSourceUrl) {
$content .= "<!-- wp:paragraph -->\n";
$content .= "<p></p>\n";
$content .= "<!-- /wp:paragraph -->\n\n";
$content .= "<!-- wp:heading -->\n";
$content .= "<h2 class=\"wp-block-heading\" id=\"Video\">Video</h2>\n";
$content .= "<!-- /wp:heading -->\n\n";
$content .= "<!-- wp:video {\"id\":" . $videoMediaId . "} -->\n";
$content .= "<figure class=\"wp-block-video\"><video controls src=\"" . htmlspecialchars($videoSourceUrl) . "\"></video></figure>\n";
$content .= "<!-- /wp:video -->\n\n";
}
// 6. FAQ
if ($faqs->count() > 0) {
$content .= "<!-- wp:heading -->\n";
$content .= "<h2 class=\"wp-block-heading\" id=\"FAQ\">FAQ</h2>\n";
$content .= "<!-- /wp:heading -->\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 .= "<!-- wp:yoast/faq-block " . json_encode($faqData, JSON_UNESCAPED_UNICODE) . " -->\n";
$content .= "<div class=\"schema-faq wp-block-yoast-faq-block\">";
foreach ($faqQuestions as $fq) {
$content .= "<div class=\"schema-faq-section\" id=\"" . $fq['id'] . "\">";
$content .= "<strong class=\"schema-faq-question\">" . htmlspecialchars($fq['question'][0]) . "</strong> ";
$content .= "<p class=\"schema-faq-answer\">" . htmlspecialchars($fq['answer'][0]) . "</p> ";
$content .= "</div> ";
}
$content .= "</div>\n";
$content .= "<!-- /wp:yoast/faq-block -->\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] . '&nbsp;';
$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 = '<table class="spec-table">' . "\n";
$html .= '<tbody>' . "\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 .= '<tr>' . "\n";
$html .= '<td class="spec-name">' . htmlspecialchars($row['label']) . '</td>' . "\n";
$html .= '<td>' . htmlspecialchars($value) . '</td>' . "\n";
$html .= '</tr>' . "\n";
}
$html .= '</tbody>' . "\n";
$html .= '</table>' . "\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;
}
}