Files
useds/app/common/service/WordpressSiteService.php
T

1419 lines
50 KiB
PHP
Raw Normal View History

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']];
$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
Log::info('开始上传图片到WordPress', [
'original_url' => $imageUrl,
'processed_url' => $processedUrl,
'filename' => $filename,
'content_type' => $contentType,
'temp_file' => $tempFilePath,
]);
// 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
$imageSize = strlen($imageContent);
Log::info('图片下载成功', ['size' => $imageSize, 'temp_file' => $tempFilePath]);
// 写入临时文件
$bytesWritten = file_put_contents($tempFilePath, $imageContent);
if ($bytesWritten === false) {
throw new \Exception('写入临时文件失败:' . $tempFilePath);
}
Log::info('临时文件创建成功', ['file' => $tempFilePath, 'size' => $bytesWritten]);
// 2. 使用 GuzzleHttp 上传本地文件到 WP 媒体库
$fileHandle = fopen($tempFilePath, 'r');
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-10 18:35:18 +08:00
// 关闭文件句柄
if (is_resource($fileHandle)) {
fclose($fileHandle);
}
$result = json_decode($response->getBody()->getContents(), true);
Log::info('图片上传成功', [
'media_id' => $result['id'] ?? 'unknown',
'source_url' => $result['source_url'] ?? 'not set',
'url' => $result['url'] ?? 'not set',
'link' => $result['link'] ?? 'not set',
'guid' => $result['guid']['rendered'] ?? 'not set',
'all_keys' => array_keys($result),
]);
return $result;
2026-01-27 15:50:18 +08:00
} catch (\Exception $e) {
2026-03-10 18:35:18 +08:00
Log::error('上传图片到WordPress失败', [
'domain' => $domain,
'image_url' => $imageUrl,
'processed_url' => $processedUrl ?? 'N/A',
'temp_file' => $tempFilePath ?? 'N/A',
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
]);
throw new \Exception('上传图片失败:' . $e->getMessage());
} finally {
// 3. 删除临时文件
if (file_exists($tempFilePath)) {
@unlink($tempFilePath);
Log::info('临时文件已删除', ['file' => $tempFilePath]);
}
}
}
/**
* 创建产品到新站点(使用 Gutenberg Blocks 结构)
*
* @param \app\common\model\WordpressSite $site WordPress站点
* @param int $carId 车辆ID
* @return array
* @throws \Exception
*/
public function createProductForNewSite($site, int $carId): array
{
// 获取车辆数据
$car = \app\common\model\UsedCarDetail::find($carId);
if (!$car) {
throw new \Exception('车辆不存在');
}
// 检查是否重复推送
$wpUsed = WordpressUsedCar::where('wp_site_id', $site->id)
->where('used_car_id', $carId)
->first();
if ($wpUsed && ($wpUsed->retry_count > 2 || $wpUsed->status == 1)) {
return ['message' => '重复推送,跳过'];
}
$domain = $site->site_url;
$client = new Client();
$url = $domain . '/wp-json/wp/v2/product';
// 调试:检查 images 字段的原始值和转换后的值
$imagesRaw = $car->getAttribute('images'); // 获取原始值,不经过 casts
$imagesCast = $car->images; // 经过 casts 转换后的值
Log::info('检查 images 字段', [
'car_id' => $carId,
'images_raw' => $imagesRaw,
'images_cast' => $imagesCast,
'images_raw_type' => gettype($imagesRaw),
'images_cast_type' => gettype($imagesCast),
'images_cast_is_array' => is_array($imagesCast),
]);
// 解析图片列表 - 优先使用已转换的数组,否则尝试解析原始字符串
$images = [];
if (!empty($imagesCast) && is_array($imagesCast)) {
$images = $imagesCast;
} elseif (!empty($imagesRaw)) {
// 尝试 JSON 解析
$decoded = json_decode($imagesRaw, true);
if (is_array($decoded)) {
$images = $decoded;
} else {
// 尝试逗号分割
$images = explode(',', $imagesRaw);
}
}
Log::info('开始处理车辆图片', [
'car_id' => $carId,
'images_final' => $images,
'images_count' => count($images),
]);
// 上传图片到WordPress媒体库
$uploadedImages = []; // 存储完整信息 ['id' => xx, 'url' => xx, ...]
$uploadedImageIds = []; // 仅存储ID
foreach ($images as $index => $imageUrl) {
if (!empty($imageUrl)) {
try {
Log::info('正在上传图片', ['index' => $index, 'url' => $imageUrl]);
$uploadedImg = $this->uploadImageToWp($domain, $site->api_user, $site->api_secret, $imageUrl);
$uploadedImages[] = [
'id' => $uploadedImg['id'],
'url' => $uploadedImg['source_url'] ?? $uploadedImg['url'] ?? '',
];
$uploadedImageIds[] = $uploadedImg['id'];
Log::info('图片上传成功', ['index' => $index, 'media_id' => $uploadedImg['id'], 'url' => $uploadedImages[count($uploadedImages)-1]['url']]);
} catch (\Exception $e) {
Log::error('图片上传失败', [
'index' => $index,
'url' => $imageUrl,
'error' => $e->getMessage(),
]);
}
}
}
Log::info('图片上传完成', [
'total' => count($images),
'success' => count($uploadedImages),
'uploaded_ids' => $uploadedImageIds,
'uploaded_details' => $uploadedImages,
]);
// 获取FAQ数据
$faqs = \app\common\model\UsedCarFaq::where('car_id', $carId)
->where('status', 1)
->orderBy('sort')
->get();
// 翻译标题
$title = $this->translateIfNeeded($car->product_name, '英文');
// 构建Gutenberg Blocks格式content
$content = $this->buildGutenbergContent($car, $uploadedImages, $faqs);
// 构建产品数据
$data = [
'title' => $title,
'content' => $content,
'status' => 'publish',
'type' => 'simple',
'regular_price' => number_format($car->price * $site->exchange_rate, 2),
];
// 设置主图和车辆相册(ACF字段)
if (!empty($uploadedImages)) {
$allImageIds = array_map(function($img) {
return $img['id'];
}, $uploadedImages);
$data['featured_media'] = $uploadedImages[0]['id'];
// 车辆相册:前5张图片
$productGalleryImages = array_slice($uploadedImages, 0, 5);
$productGalleryIds = array_map(function($img) {
return $img['id'];
}, $productGalleryImages);
$data['acf']['product_gallery'] = $productGalleryIds;
Log::info('设置ACF车辆相册', [
'featured_media' => $data['featured_media'],
'total_images' => count($uploadedImages),
'product_gallery_count' => count($data['acf']['product_gallery']),
'product_gallery_ids' => $data['acf']['product_gallery'],
]);
}
// 设置ACF车辆参数字段(与旧站点保持一致)
$fieldMap = [
'plate_time' => '上牌时间',
'displacement' => '排量',
'mileage' => '里程',
'power' => '马力',
'body_color' => '车身颜色',
'source_place' => '车源地',
'price' => '价格',
];
foreach ($fieldMap as $acfKey => $label) {
if (!empty($car->$acfKey)) {
$value = $car->$acfKey;
// 翻译颜色和车源地
if ($acfKey === 'body_color') {
$value = $this->translateColorToEnglish($value);
} elseif ($acfKey === 'source_place' && $this->containsChinese($value)) {
$value = $this->translateIfNeeded($value, '英文');
}
$data['acf'][$acfKey] = $value;
}
}
// 翻译并设置车况介绍和产品简介(ACF字段)
$conditionDesc = $car->condition_desc ?? '';
if (!empty($conditionDesc)) {
// 产品简介(完整翻译)
$data['acf']['condition_desc'] = $this->translateIfNeeded($conditionDesc, '英文');
// 车况介绍(简短翻译,去掉商家信息)
$cleanDesc = preg_replace('/[^\x{4e00}-\x{9fff}\w\s.,!?()-]/u', ' ', $conditionDesc);
$vehicleDesc = $this->translateIfNeeded($cleanDesc, '英文');
$data['acf']['pro-intro-des'] = $vehicleDesc;
2026-01-27 15:50:18 +08:00
}
2026-03-10 18:35:18 +08:00
Log::info('设置ACF参数字段', [
'acf_fields' => array_keys($data['acf'] ?? []),
'acf_values' => $data['acf'] ?? [],
]);
try {
Log::info('准备推送产品到WordPress', [
'car_id' => $carId,
'featured_media' => $data['featured_media'] ?? 'not set',
'acf_product_gallery_count' => count($data['acf']['product_gallery'] ?? []),
'acf_fields_count' => count($data['acf'] ?? []),
'content_length' => strlen($data['content'] ?? ''),
]);
$response = $client->post($url, [
RequestOptions::AUTH => [$site->api_user, $site->api_secret],
RequestOptions::JSON => $data,
RequestOptions::VERIFY => false,
]);
$result = json_decode($response->getBody()->getContents(), true);
// 记录推送成功
if ($wpUsed) {
$wpUsed->update([
'wp_post_id' => $result['id'],
'request_payload' => json_encode($data, JSON_UNESCAPED_UNICODE),
'response_body' => $response->getBody()->getContents(),
'status' => 1,
'retry_count' => $wpUsed->retry_count + 1,
'pushed_at' => time(),
]);
} else {
WordpressUsedCar::create([
'wp_site_id' => $site->id,
'used_car_id' => $carId,
'wp_post_id' => $result['id'],
'request_payload' => json_encode($data, JSON_UNESCAPED_UNICODE),
'response_body' => $response->getBody()->getContents(),
'status' => 1,
'retry_count' => 1,
'pushed_at' => time(),
]);
}
$site->last_uid = $carId;
$site->save();
return array_merge($data, ['result' => $result]);
} catch (\Exception $e) {
// 记录推送失败
if ($wpUsed) {
$wpUsed->update([
'retry_count' => $wpUsed->retry_count + 1,
'error_message' => $e->getMessage(),
]);
if ($wpUsed->retry_count >= 3) {
$site->last_uid = $carId;
$site->save();
}
} else {
WordpressUsedCar::create([
'wp_site_id' => $site->id,
'used_car_id' => $carId,
'request_payload' => json_encode($data, JSON_UNESCAPED_UNICODE),
'error_message' => $e->getMessage(),
'status' => 0,
'retry_count' => 1,
]);
}
throw new \Exception('WordPress推送失败:' . $e->getMessage());
}
}
/**
* 构建 Gutenberg Blocks 格式 content
*/
private function buildGutenbergContent($car, array $uploadedImages, $faqs): string
{
$content = '';
// 1. 车辆相册标题
$content .= "<!-- 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张
Log::info('构建图库block', [
'total_images' => count($uploadedImages),
'gallery_start_from' => 6,
'gallery_before_fill' => count($galleryImages),
'first_five_count' => count($firstFiveImages),
'gallery_ids_before' => array_column($galleryImages, 'id'),
]);
// 不足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);
}
Log::info('图库图片准备完成', [
'final_count' => count($galleryImages),
'image_ids' => array_column($galleryImages, 'id'),
]);
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 xz-tab block
$tabData = $this->buildXzTabData($car);
$content .= "<!-- wp:acf/xz-tab " . json_encode($tabData, JSON_UNESCAPED_UNICODE) . " /-->\n\n";
// 5. 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";
$translatedCount = 0;
foreach ($rows as $row) {
$value = $row['value'] ?? '';
if (empty($value) && $value !== '0') {
continue;
}
$originalValue = $value;
// 翻译中文值
if (is_string($value) && $this->containsChinese($value)) {
$value = $this->translateIfNeeded($value, '英文');
$translatedCount++;
Log::info('翻译参数表格字段值', [
'label' => $row['label'],
'original' => $originalValue,
'translated' => $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";
if ($translatedCount > 0) {
Log::info('参数表格翻译完成', ['translated_count' => $translatedCount]);
}
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
}
}