2026-01-27 15:50:18 +08:00
|
|
|
|
<?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);
|
2026-01-29 11:34:32 +08:00
|
|
|
|
// $images[] = ['id' => $image['id']];
|
|
|
|
|
|
$images[] = $image['id'];
|
2026-01-27 15:50:18 +08:00
|
|
|
|
}
|
2026-01-29 11:34:32 +08:00
|
|
|
|
//$images = array_merge([$images[0]], $images);
|
2026-01-27 15:50:18 +08:00
|
|
|
|
|
|
|
|
|
|
//车辆详细信息
|
|
|
|
|
|
/*$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 = [
|
2026-01-29 11:34:32 +08:00
|
|
|
|
"title" => $car_name_locale,
|
|
|
|
|
|
"Content" => '',
|
|
|
|
|
|
"excerpt" => '',
|
2026-01-27 15:50:18 +08:00
|
|
|
|
"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二手车
|
|
|
|
|
|
// ],
|
2026-01-29 11:34:32 +08:00
|
|
|
|
//"images" => $images,
|
|
|
|
|
|
"status" => 'publish', //发布状态
|
2026-01-27 15:50:18 +08:00
|
|
|
|
];
|
2026-01-29 11:34:32 +08:00
|
|
|
|
//$data['categories'][] = ["id" => $site['categories']];
|
2026-03-11 13:29:10 +08:00
|
|
|
|
//产品分类
|
2026-01-29 11:34:32 +08:00
|
|
|
|
$data['product_cat'] = explode(',', $site['categories']);
|
2026-01-27 15:50:18 +08:00
|
|
|
|
|
|
|
|
|
|
//车辆档案相关参数
|
2026-01-29 11:34:32 +08:00
|
|
|
|
$meta = $this->drawMeta($row);
|
2026-01-27 15:50:18 +08:00
|
|
|
|
|
|
|
|
|
|
//车身颜色
|
|
|
|
|
|
$color = '';
|
2026-01-29 11:34:32 +08:00
|
|
|
|
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:'';
|
2026-01-27 15:50:18 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if ($site['tpl_type'] == 2) {
|
|
|
|
|
|
$meta = [];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-29 11:34:32 +08:00
|
|
|
|
//属性:品牌、车体类型、颜色
|
|
|
|
|
|
$brand = $row['crawl_brand']?:$row['product_name'];
|
|
|
|
|
|
$brand_locale = $spark->chat('将车辆品牌翻译为'.$locale.':'.$brand,null,[],'wpSend');
|
2026-01-27 15:50:18 +08:00
|
|
|
|
|
2026-01-29 11:34:32 +08:00
|
|
|
|
//查询品牌id
|
|
|
|
|
|
$brand_id = $this->get_or_create_brand_id($site,$domain, $brand_locale);
|
|
|
|
|
|
$data['pa_brand'] = [$brand_id];
|
2026-01-27 15:50:18 +08:00
|
|
|
|
|
2026-01-29 11:34:32 +08:00
|
|
|
|
//查询车身类型
|
|
|
|
|
|
$bodys = $this->get_body($site,$domain);
|
|
|
|
|
|
$bady_style = array_column($bodys, 'name');
|
2026-01-27 15:50:18 +08:00
|
|
|
|
|
2026-01-29 11:34:32 +08:00
|
|
|
|
$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];
|
|
|
|
|
|
|
|
|
|
|
|
// 翻译车况简介
|
2026-01-27 15:50:18 +08:00
|
|
|
|
// $baseInfo = $this->parseCarConfigDetail($row['format_car_info']);
|
2026-01-29 11:34:32 +08:00
|
|
|
|
$conditionDesc_locale = $spark->chat('将车况描述翻译为'.$locale.':'.$row['condition_desc'],null,[],'wpSend');
|
2026-01-27 15:50:18 +08:00
|
|
|
|
|
|
|
|
|
|
//翻译车辆简介描述
|
2026-01-29 11:34:32 +08:00
|
|
|
|
$desc = "车辆名称:{$car_name},车辆参数:".json_encode($meta);
|
|
|
|
|
|
$vehicleDescription_locale = $spark->chat('根据给出的信息生成'.$locale.'车辆简介描述(要求简短):'.$desc,null,[],'wpSend');
|
2026-01-27 15:50:18 +08:00
|
|
|
|
|
|
|
|
|
|
$meta[] = [
|
2026-01-29 11:34:32 +08:00
|
|
|
|
'key' => 'pro-intro-des',//车况介绍
|
2026-01-27 15:50:18 +08:00
|
|
|
|
'value' => $vehicleDescription_locale,
|
|
|
|
|
|
];
|
|
|
|
|
|
|
2026-01-29 11:34:32 +08:00
|
|
|
|
$meta[] = [
|
|
|
|
|
|
'key' => 'condition_desc',//产品简介
|
|
|
|
|
|
'value' => $conditionDesc_locale,
|
|
|
|
|
|
];
|
|
|
|
|
|
|
2026-01-27 15:50:18 +08:00
|
|
|
|
// $meta[] = [ //列表标签
|
|
|
|
|
|
// 'key' => 'hotpro_mark',//自定义热门产品标签
|
|
|
|
|
|
// 'value' => 'In Stock', //In Stock、HOT SALE
|
|
|
|
|
|
// ];
|
|
|
|
|
|
|
2026-01-29 11:34:32 +08:00
|
|
|
|
//$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;
|
2026-01-27 15:50:18 +08:00
|
|
|
|
|
|
|
|
|
|
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();
|
|
|
|
|
|
|
2026-01-29 11:34:32 +08:00
|
|
|
|
$data ['result'] = $result;
|
|
|
|
|
|
return $data;
|
|
|
|
|
|
//return $result;
|
2026-01-27 15:50:18 +08:00
|
|
|
|
|
|
|
|
|
|
} 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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
2026-01-29 11:34:32 +08:00
|
|
|
|
* 查询wp车身类型
|
|
|
|
|
|
* @param $brand_name
|
|
|
|
|
|
* @return mixed
|
|
|
|
|
|
* @throws \GuzzleHttp\Exception\GuzzleException
|
2026-01-27 15:50:18 +08:00
|
|
|
|
*/
|
2026-01-29 11:34:32 +08:00
|
|
|
|
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;
|
2026-01-27 15:50:18 +08:00
|
|
|
|
}
|
2026-01-29 11:34:32 +08:00
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 查询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)));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-27 15:50:18 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 计算使用时间
|
|
|
|
|
|
*
|
|
|
|
|
|
* @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
|
|
|
|
|
|
{
|
2026-03-10 18:35:18 +08:00
|
|
|
|
$process = 'image/resize,w_800/quality,Q_50/format,webp';
|
|
|
|
|
|
$processedUrl = (new OssService)->generatePrivateUrlResize($imageUrl,3600,$process);
|
2026-01-27 15:50:18 +08:00
|
|
|
|
|
|
|
|
|
|
$client = new Client();
|
|
|
|
|
|
|
2026-03-10 18:35:18 +08:00
|
|
|
|
// 从原始 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);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-27 15:50:18 +08:00
|
|
|
|
$mediaUrl = $domain . '/wp-json/wp/v2/media';
|
|
|
|
|
|
|
2026-03-10 18:35:18 +08:00
|
|
|
|
// 本地临时文件路径
|
|
|
|
|
|
$tempDir = runtime_path('temp') . '/wp_images/';
|
|
|
|
|
|
if (!is_dir($tempDir)) {
|
|
|
|
|
|
mkdir($tempDir, 0755, true);
|
|
|
|
|
|
}
|
|
|
|
|
|
$tempFilePath = $tempDir . md5($processedUrl) . '.' . $extension;
|
|
|
|
|
|
|
2026-01-27 15:50:18 +08:00
|
|
|
|
try {
|
2026-03-10 18:35:18 +08:00
|
|
|
|
// 1. 下载图片到本地临时文件
|
|
|
|
|
|
$imageContent = file_get_contents($processedUrl);
|
2026-01-27 15:50:18 +08:00
|
|
|
|
if ($imageContent === false) {
|
2026-03-10 18:35:18 +08:00
|
|
|
|
throw new \Exception('图片下载失败:' . $processedUrl);
|
2026-01-27 15:50:18 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-10 18:35:18 +08:00
|
|
|
|
// 写入临时文件
|
|
|
|
|
|
$bytesWritten = file_put_contents($tempFilePath, $imageContent);
|
|
|
|
|
|
if ($bytesWritten === false) {
|
|
|
|
|
|
throw new \Exception('写入临时文件失败:' . $tempFilePath);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 2. 使用 GuzzleHttp 上传本地文件到 WP 媒体库
|
|
|
|
|
|
$fileHandle = fopen($tempFilePath, 'r');
|
2026-01-27 15:50:18 +08:00
|
|
|
|
$response = $client->post($mediaUrl, [
|
|
|
|
|
|
RequestOptions::AUTH => [$api_user, $api_secret],
|
2026-03-10 18:35:18 +08:00
|
|
|
|
RequestOptions::MULTIPART => [
|
|
|
|
|
|
[
|
|
|
|
|
|
'name' => 'file',
|
|
|
|
|
|
'filename' => $filename,
|
|
|
|
|
|
'contents' => $fileHandle,
|
|
|
|
|
|
'headers' => [
|
|
|
|
|
|
'Content-Type' => $contentType,
|
|
|
|
|
|
],
|
|
|
|
|
|
],
|
2026-01-27 15:50:18 +08:00
|
|
|
|
],
|
|
|
|
|
|
RequestOptions::VERIFY => false,
|
2026-03-10 18:35:18 +08:00
|
|
|
|
RequestOptions::TIMEOUT => 30,
|
2026-01-27 15:50:18 +08:00
|
|
|
|
]);
|
2026-03-11 13:29:10 +08:00
|
|
|
|
// 关闭文件句柄
|
|
|
|
|
|
if (is_resource($fileHandle)) {
|
|
|
|
|
|
fclose($fileHandle);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-11 16:23:34 +08:00
|
|
|
|
$responseBody = $response->getBody()->getContents();
|
|
|
|
|
|
$result = json_decode($responseBody, true);
|
|
|
|
|
|
|
|
|
|
|
|
if (empty($result) || !is_array($result)) {
|
|
|
|
|
|
Log::error('WordPress图片上传响应解析失败', [
|
|
|
|
|
|
'image_url' => $imageUrl,
|
|
|
|
|
|
'response' => substr($responseBody, 0, 500),
|
|
|
|
|
|
'status_code' => $response->getStatusCode(),
|
|
|
|
|
|
]);
|
|
|
|
|
|
throw new \Exception('WordPress响应解析失败');
|
|
|
|
|
|
}
|
2026-03-11 13:29:10 +08:00
|
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
|
}
|
2026-01-27 15:50:18 +08:00
|
|
|
|
|
2026-03-10 18:35:18 +08:00
|
|
|
|
// 关闭文件句柄
|
|
|
|
|
|
if (is_resource($fileHandle)) {
|
|
|
|
|
|
fclose($fileHandle);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
$result = json_decode($response->getBody()->getContents(), true);
|
|
|
|
|
|
|
2026-03-11 13:29:10 +08:00
|
|
|
|
Log::info('视频上传成功', [
|
2026-03-10 18:35:18 +08:00
|
|
|
|
'media_id' => $result['id'] ?? 'unknown',
|
2026-03-11 13:29:10 +08:00
|
|
|
|
'source_url' => $result['source_url'] ?? 'unknown',
|
2026-03-10 18:35:18 +08:00
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
|
|
return $result;
|
2026-01-27 15:50:18 +08:00
|
|
|
|
|
|
|
|
|
|
} catch (\Exception $e) {
|
2026-03-11 13:29:10 +08:00
|
|
|
|
Log::error('上传视频到WordPress失败', [
|
|
|
|
|
|
'video_url' => $videoUrl,
|
2026-03-10 18:35:18 +08:00
|
|
|
|
'error' => $e->getMessage(),
|
|
|
|
|
|
]);
|
2026-03-11 13:29:10 +08:00
|
|
|
|
throw new \Exception('上传视频失败:' . $e->getMessage());
|
2026-03-10 18:35:18 +08:00
|
|
|
|
} finally {
|
|
|
|
|
|
// 3. 删除临时文件
|
|
|
|
|
|
if (file_exists($tempFilePath)) {
|
|
|
|
|
|
@unlink($tempFilePath);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-11 14:15:09 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 上传文件(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);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-10 18:35:18 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 创建产品到新站点(使用 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(),
|
|
|
|
|
|
]);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-11 13:29:10 +08:00
|
|
|
|
// 上传视频到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()]);
|
|
|
|
|
|
// 视频上传失败不影响整体推送,继续执行
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-03-10 18:35:18 +08:00
|
|
|
|
|
|
|
|
|
|
// 获取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
|
2026-03-11 13:29:10 +08:00
|
|
|
|
$content = $this->buildGutenbergContent($car, $uploadedImages, $faqs, $videoMediaId, $videoSourceUrl);
|
2026-03-10 18:35:18 +08:00
|
|
|
|
|
|
|
|
|
|
// 构建产品数据
|
|
|
|
|
|
$data = [
|
|
|
|
|
|
'title' => $title,
|
|
|
|
|
|
'content' => $content,
|
|
|
|
|
|
'status' => 'publish',
|
|
|
|
|
|
'type' => 'simple',
|
|
|
|
|
|
'regular_price' => number_format($car->price * $site->exchange_rate, 2),
|
|
|
|
|
|
];
|
|
|
|
|
|
|
2026-03-11 13:29:10 +08:00
|
|
|
|
// 设置产品分类使用 product_cat 字段)
|
|
|
|
|
|
$data['product_cat'] = [15, 16];
|
|
|
|
|
|
|
2026-03-11 13:59:51 +08:00
|
|
|
|
// 处理品牌:翻译并获取/创建品牌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];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-11 14:15:09 +08:00
|
|
|
|
// 处理附件:上传第一个附件到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(),
|
|
|
|
|
|
]);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-10 18:35:18 +08:00
|
|
|
|
// 设置主图和车辆相册(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' => '价格',
|
|
|
|
|
|
];
|
2026-03-11 13:59:51 +08:00
|
|
|
|
if (str_contains($car->mileage, '万') || str_contains($car->mileage, '万公里')) {
|
|
|
|
|
|
$car->mileage = str_replace(['万', '公里', '万公里'],'',$car->mileage)*10000;
|
|
|
|
|
|
}
|
2026-03-11 15:01:54 +08:00
|
|
|
|
$car->power = str_replace('马力','',$car->power);
|
2026-03-10 18:35:18 +08:00
|
|
|
|
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字段)
|
2026-03-11 13:29:10 +08:00
|
|
|
|
// $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 字段
|
2026-03-10 18:35:18 +08:00
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
*/
|
2026-03-11 13:29:10 +08:00
|
|
|
|
private function buildGutenbergContent($car, array $uploadedImages, $faqs, $videoMediaId = null, $videoSourceUrl = null): string
|
2026-03-10 18:35:18 +08:00
|
|
|
|
{
|
|
|
|
|
|
$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, 5); // 第6张开始
|
|
|
|
|
|
$firstFiveImages = array_slice($uploadedImages, 0, 5); // 前5张
|
|
|
|
|
|
|
|
|
|
|
|
// 不足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";
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-11 13:29:10 +08:00
|
|
|
|
// 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
|
2026-03-10 18:35:18 +08:00
|
|
|
|
$tabData = $this->buildXzTabData($car);
|
|
|
|
|
|
$content .= "<!-- wp:acf/xz-tab " . json_encode($tabData, JSON_UNESCAPED_UNICODE) . " /-->\n\n";
|
|
|
|
|
|
|
2026-03-11 13:29:10 +08:00
|
|
|
|
// 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
|
2026-03-10 18:35:18 +08:00
|
|
|
|
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] . ' ';
|
|
|
|
|
|
$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;
|
2026-01-27 15:50:18 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
}
|