Files
useds/app/common/service/WordpressSiteService.php
T
2026-01-27 15:50:18 +08:00

868 lines
28 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
namespace app\common\service;
use app\common\model\UsedCar;
use app\common\model\UsedcarCrawlResults;
use app\common\model\UsedcarCrawlResultsImages;
use app\common\model\WordpressUsedCar;
use app\common\validate\WordpressSiteValidate;
use app\common\traits\crud\CrudServiceTrait;
use app\common\model\WordpressSite;
use GuzzleHttp\Client;
use GuzzleHttp\RequestOptions;
use support\Log;
use support\Request;
use support\Response;
class WordpressSiteService
{
use CrudServiceTrait;
/**
* 指定当前要操作的模型
*/
protected string $modelClass = WordpressSite::class;
/**
* 验证器
*/
protected $validator = WordpressSiteValidate::class;
/**
* 获取验证器
*/
public function getValidator()
{
return new $this->validator;
}
/**
* @param Request $request
* @return mixed|Response
* @throws \GuzzleHttp\Exception\GuzzleException
*/
public function createPost(Request $request)
{
$client = new Client();
$url = "https://37car.dev.hnxinyouhe.com/wp-json/wp/v2/posts";
$data = [
"title" => "API 创建文章",
"content" => "内容来自 API",
"status" => "publish"
];
try {
$response = $client->post($url, [
RequestOptions::AUTH => ['Borderless', '3Tdw 6O02 DM6f lb2S 7T3l WG8a'],
RequestOptions::JSON => $data,
RequestOptions::VERIFY => false, // 禁用 SSL 验证
]);
$result = json_decode($response->getBody()->getContents(), true);
return $result;
} catch (\Exception $e) {
throw new \Exception($e->getMessage());
}
}
/**
* @param Request $request
* @return mixed|Response
* @throws \GuzzleHttp\Exception\GuzzleException
*/
public function createProduct(string $domain, $id)
{
$site = WordpressSite::where('site_url',$domain)->first();
//判断是否重复推送
$wpUsed = WordpressUsedCar::where('wp_site_id', $site['id'])->where('used_car_id', $id)->first();
if($wpUsed && ($wpUsed['retry_count']>2 || $wpUsed['status'] ==1)){
$site->last_uid = $id;
$site->save();
return '重复推送';
}
$client = new Client();
// $url = $domain."/wp-json/wc/v3/products";
$url = $domain."/wp-json/wp/v2/product";
$row = UsedCar::where('id', $id)->first();
$ucImages = explode(",", $row->images);
//车辆相册
$images = [];
foreach ($ucImages as $ucImage) {
$image = $this->uploadImageToWp($domain, $site['api_user'], $site['api_secret'], $ucImage);
$images[] = ['id' => $image['id']];
}
$images = array_merge([$images[0]], $images);
//车辆详细信息
/*$detils= $this->drawDetils($row);
$detils_params_html = [];
foreach ($detils as $key => $detil) {
if(is_array($detil)){
$detil = implode(",", $detil);
}
$detils_params_html[] =$key.':'.(string)$detil;
}
$detils_params_html = implode(PHP_EOL, $detils_params_html);*/
// $detils_params_html = $this->buildVehicleDetailsTable($detils);
//语言确定目标网站的语言
$locale = match ($row['locale']) {
'en' => '英文',
'ru' => '俄语',
'ar' => '阿拉伯语',
'fr ' => '法语',
default => '英文',
};
$spark = new SparkService();
$car_name = $row['product_name'];
$car_name_locale = $spark->chat('将车辆名称翻译为'.$locale.':'.$car_name,null,[],'wpSend');
$condition_desc_locale = $spark->chat("下面是车辆的车况介绍将卖家信息去掉,包括但不限于商家名称、集团简介、经销商信息、地址、电话、商家介绍等都去掉\"".$row['condition_desc']."\"",null,[],'wpSend');
$condition_desc_locale = $spark->chat("将下列车况描述翻译为".$locale.":".$condition_desc_locale);
$data = [
"name" => $car_name_locale,
"type" => "simple", //单品,不走规格变体
"regular_price" => number_format(($row['price']*$site['exchange_rate']), 2),
"sku" => "",
//"featured" => true,//是否精选
"description" => $condition_desc_locale,
"stock_status" => "instock",//instock现车
// "categories" => [
// ["id" => 16],//used二手车
// ],
"images" => $images,
];
$data['categories'][] = ["id" => $site['categories']];
//车辆档案相关参数
$meta = $this->drawMeta($row['used_detail']);
//车身颜色
$color = '';
foreach ($meta as $item){
if (!empty($item['key']) && $item['key'] == 'color') {
$color = $item['value'];
$color = $this->sparkTransl($spark,$color,$locale);
}
}
if ($site['tpl_type'] == 2) {
$meta = [];
}
//$configDetail = json_decode($row['config_detail'], true);
//车身级别
/*$body_style = '';
foreach ($configDetail[0]['items'] as $item){
if (!empty($item['key']) && $item['key'] == 'jb') {
$body_style = $item['value'];
$body_style = $this->sparkTransl($spark,$body_style,$locale);
}
}*/
//产品设置相关参数
$meta[] = [
'key' => 'pro-intro-txt',//产品参数简介
'value' => "<ul>
<li><strong>Engine Size</strong>{$row['displacement']}</li>
<li><strong>Engine Power</strong>{$row['power']}</li>
<li><strong>Mileage(KM)</strong>{$row['mileage']}</li>
<li><strong>Color</strong>{$color}</li>
</ul>",
];
// Engine Size
//Engine Power
//Mileage
//Color
// <li><strong>Year</strong>{$row['year']}</li>
// <li><strong>Body Style</strong>{$body_style}</li>
//属性:品牌、车体类型、颜色
$brand_locale = $spark->chat('将车辆品牌翻译为'.$locale.':'.$row['brand_name'],null,[],'wpSend');
$data['attributes'][] = [ //品牌
"id" => "1",
"visible" => true,
"variation" => false,
"options" => [$brand_locale]
];
/*$data['attributes'][] = [ //车体类型
"id" => "2",
"visible" => true,
"variation" => false,
"options" => [$body_style]
];*/
$data['attributes'][] = [ //颜色
"id" => "4",
"visible" => true,
"variation" => false,
"options" => [$color]
];
// 构建车辆简介描述
// $baseInfo = $this->parseCarConfigDetail($row['format_car_info']);
// $vehicleDescription = $this->buildVehicleDescription($row, $baseInfo, $meta, $configDetail);
//翻译车辆简介描述
$desc = "车辆名称:{$car_name},车辆参数:".json_encode($meta)."车辆简介:{$row['condition_desc']}";
$vehicleDescription_locale = $spark->chat('根据给出的信息生成'.$locale.'车辆简介描述:'.$desc,null,[],'wpSend');
$meta[] = [
'key' => 'pro-intro-des',//产品简介
'value' => $vehicleDescription_locale,
];
// $meta[] = [ //列表标签
// 'key' => 'hotpro_mark',//自定义热门产品标签
// 'value' => 'In Stock', //In Stock、HOT SALE
// ];
$data['meta_data'] = array_values($meta);
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();
return $result;
} catch (\Exception $e) {
$his = WordpressUsedCar::where('wp_site_id', $site['id'])->where('used_car_id', $id)->first();
var_dump($his);
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;
}
/**
* 构建车辆简介描述
*
* @param array $row 车辆基本信息
* @param array $baseInfo 车辆配置信息
* @param array $meta meta_data数组
* @param array $configDetail 配置详情
* @return string
*/
public function buildVehicleDescription($row, $baseInfo, $meta, $configDetail): string
{
$parts = [];
// 1. 品牌 + 系列 + 年份 + 车型名称
$brandName = trim($row['brand_name'] ?? '');
$seriesName = trim($row['series_name'] ?? '');
$year = trim($row['year'] ?? '');
$carName = trim($row['car_name'] ?? '');
$modelParts = [];
if ($brandName) $modelParts[] = $brandName;
if ($seriesName) $modelParts[] = $seriesName;
if ($year) $modelParts[] = $year;
// 尝试从car_name中提取版本信息(如 "Champion Edition", "Champion PRO")
$version = '';
$carNameWithoutVersion = $carName;
if ($carName) {
// 如果car_name包含版本信息,提取出来
if (preg_match('/\s+([A-Z][a-zA-Z\s]+(?:Edition|PRO|PLUS|MAX|PRO|Limited))$/i', $carName, $matches)) {
$version = trim($matches[1]);
$carNameWithoutVersion = trim(str_replace($version, '', $carName));
}
$modelParts[] = $carNameWithoutVersion;
if ($version) {
$modelParts[] = $version;
}
}
// 2. 引擎和变速箱信息(优先从 config_detail 中获取)
$engine = $this->getConfigValue($configDetail, ['fdj', 'engine', 'engine_description', '发动机'], '');
if (empty($engine)) {
$engine = $this->value($baseInfo, 'engine_description', '');
}
$transmission = $this->getConfigValue($configDetail, ['bsx', 'gearbox', 'gearbox_description', 'transmission', '变速箱'], '');
if (empty($transmission)) {
$transmission = $this->value($baseInfo, 'gearbox_description', '');
}
if ($engine || $transmission) {
$engineParts = [];
if ($engine && $engine !== '–') $engineParts[] = $engine;
if ($transmission && $transmission !== '–') $engineParts[] = $transmission;
if (!empty($engineParts)) {
$modelParts[] = implode(' ', $engineParts);
}
}
if (!empty($modelParts)) {
$parts[] = implode(' ', $modelParts);
}
// 3. 座位数(优先从 config_detail 中获取)
$seatCount = $this->getConfigValue($configDetail, ['zws', 'seat_count', 'seat', 'seats', '座位数'], '');
if (empty($seatCount) || $seatCount === '–') {
$seatCount = $this->value($baseInfo, 'seat_count', '');
}
if ($seatCount && $seatCount !== '–' && is_numeric($seatCount)) {
$parts[] = "{$seatCount}-seater";
}
// 4. 外观颜色(优先从 config_detail 中获取)
$exteriorColor = $this->getConfigValue($configDetail, ['cys', 'body_color', 'color', 'exterior_color', '车身颜色'], '');
// 如果 config_detail 中没有,尝试从 baseInfo 中获取
if (empty($exteriorColor) || $exteriorColor === '–') {
$exteriorColor = $this->value($baseInfo, 'body_color', '');
}
// 如果 baseInfo 中也没有,尝试从 meta 中获取
if (empty($exteriorColor) || $exteriorColor === '–') {
foreach ($meta as $item) {
if (!empty($item['key']) && $item['key'] == 'color') {
$exteriorColor = $item['value'] ?? '';
break;
}
}
}
if ($exteriorColor && $exteriorColor !== '–') {
// 翻译颜色为英文(如果必要)
$exteriorColorEn = $this->translateColorToEnglish($exteriorColor);
$parts[] = "{$exteriorColorEn} exterior";
}
// 5. 内饰颜色(尝试从config_detail中查找)
$interiorColor = $this->getInteriorColor($configDetail, $baseInfo);
if ($interiorColor && trim($interiorColor) !== '') {
$interiorColorEn = $this->translateColorToEnglish($interiorColor);
// 如果翻译后仍然是原始的中文值(说明未找到映射),使用light-colored作为默认值
if ($interiorColorEn === mb_strtolower($interiorColor, 'UTF-8') && !preg_match('/^[a-z\s-]+$/i', $interiorColor)) {
$interiorColorEn = 'light-colored';
}
if ($interiorColorEn) {
$parts[] = "{$interiorColorEn} interior";
}
}
// 6. 使用时间(从上牌时间计算)
$licenseDate = '';
foreach ($meta as $item) {
if (!empty($item['key']) && $item['key'] == 'license_date') {
$licenseDate = $item['value'] ?? '';
break;
}
}
if ($licenseDate) {
$usageTime = $this->calculateUsageTime($licenseDate);
if ($usageTime) {
$parts[] = "used for {$usageTime}";
}
}
// 7. 行驶里程
$mileage = $row['usage_mileage'] ?? '';
if ($mileage && is_numeric($mileage)) {
$formattedMileage = number_format((float)$mileage, 0, '.', ',');
$parts[] = "traveled {$formattedMileage} kilometers";
}
return implode(', ', $parts) . '.';
}
/**
* 计算使用时间
*
* @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
{
$imageUrl = (new OssService)->generatePrivateUrl($imageUrl);
$client = new Client();
$filename = basename(parse_url($imageUrl, PHP_URL_PATH));
$mediaUrl = $domain . '/wp-json/wp/v2/media';
try {
// 1. 拉取 OSS 图片内容
$imageContent = file_get_contents($imageUrl);
if ($imageContent === false) {
throw new \Exception('图片下载失败:' . $imageUrl);
}
// 2. 上传到 WP 媒体库
$response = $client->post($mediaUrl, [
RequestOptions::AUTH => [$api_user, $api_secret],
RequestOptions::HEADERS => [
'Content-Disposition' => 'attachment; filename="' . $filename . '"',
'Content-Type' => 'image/jpeg', // 如有 png/webp 可自行判断
],
RequestOptions::BODY => $imageContent,
RequestOptions::VERIFY => false,
]);
return json_decode($response->getBody()->getContents(), true);
} catch (\Exception $e) {
throw new \Exception('上传图片失败:' .$domain . $imageUrl . "?????". $e->getMessage());
}
}
}