Files
useds/app/common/service/WordpressSiteService.php
T
2026-01-29 11:34:32 +08:00

804 lines
25 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
namespace app\common\service;
use app\common\model\UsedCar;
use app\common\model\UsedcarCrawlResults;
use app\common\model\UsedcarCrawlResultsImages;
use app\common\model\WordpressUsedCar;
use app\common\validate\WordpressSiteValidate;
use app\common\traits\crud\CrudServiceTrait;
use app\common\model\WordpressSite;
use GuzzleHttp\Client;
use GuzzleHttp\RequestOptions;
use support\Log;
use support\Request;
use support\Response;
class WordpressSiteService
{
use CrudServiceTrait;
/**
* 指定当前要操作的模型
*/
protected string $modelClass = WordpressSite::class;
/**
* 验证器
*/
protected $validator = WordpressSiteValidate::class;
/**
* 获取验证器
*/
public function getValidator()
{
return new $this->validator;
}
/**
* @param Request $request
* @return mixed|Response
* @throws \GuzzleHttp\Exception\GuzzleException
*/
public function createPost(Request $request)
{
$client = new Client();
$url = "https://37car.dev.hnxinyouhe.com/wp-json/wp/v2/posts";
$data = [
"title" => "API 创建文章",
"content" => "内容来自 API",
"status" => "publish"
];
try {
$response = $client->post($url, [
RequestOptions::AUTH => ['Borderless', '3Tdw 6O02 DM6f lb2S 7T3l WG8a'],
RequestOptions::JSON => $data,
RequestOptions::VERIFY => false, // 禁用 SSL 验证
]);
$result = json_decode($response->getBody()->getContents(), true);
return $result;
} catch (\Exception $e) {
throw new \Exception($e->getMessage());
}
}
/**
* @param Request $request
* @return mixed|Response
* @throws \GuzzleHttp\Exception\GuzzleException
*/
public function createProduct(string $domain, $id)
{
$site = WordpressSite::where('site_url',$domain)->first();
//判断是否重复推送
$wpUsed = WordpressUsedCar::where('wp_site_id', $site['id'])->where('used_car_id', $id)->first();
if($wpUsed && ($wpUsed['retry_count']>2 || $wpUsed['status'] ==1)){
$site->last_uid = $id;
$site->save();
return '重复推送';
}
$client = new Client();
// $url = $domain."/wp-json/wc/v3/products";
$url = $domain."/wp-json/wp/v2/product";
$row = UsedCar::where('id', $id)->first();
$ucImages = explode(",", $row->images);
//车辆相册
$images = [];
foreach ($ucImages as $ucImage) {
$image = $this->uploadImageToWp($domain, $site['api_user'], $site['api_secret'], $ucImage);
// $images[] = ['id' => $image['id']];
$images[] = $image['id'];
}
//$images = array_merge([$images[0]], $images);
//车辆详细信息
/*$detils= $this->drawDetils($row);
$detils_params_html = [];
foreach ($detils as $key => $detil) {
if(is_array($detil)){
$detil = implode(",", $detil);
}
$detils_params_html[] =$key.':'.(string)$detil;
}
$detils_params_html = implode(PHP_EOL, $detils_params_html);*/
// $detils_params_html = $this->buildVehicleDetailsTable($detils);
//语言确定目标网站的语言
$locale = match ($row['locale']) {
'en' => '英文',
'ru' => '俄语',
'ar' => '阿拉伯语',
'fr ' => '法语',
default => '英文',
};
$spark = new SparkService();
$car_name = $row['product_name'];
$car_name_locale = $spark->chat('将车辆名称翻译为'.$locale.':'.$car_name,null,[],'wpSend');
$condition_desc_locale = $spark->chat("下面是车辆的车况介绍将卖家信息去掉,包括但不限于商家名称、集团简介、经销商信息、地址、电话、商家介绍等都去掉\"".$row['condition_desc']."\"",null,[],'wpSend');
$condition_desc_locale = $spark->chat("将下列车况描述翻译为".$locale.":".$condition_desc_locale);
$data = [
"title" => $car_name_locale,
"Content" => '',
"excerpt" => '',
"type" => "simple", //单品,不走规格变体
"regular_price" => number_format(($row['price']*$site['exchange_rate']), 2),
"sku" => "",
//"featured" => true,//是否精选
"description" => $condition_desc_locale,
"stock_status" => "instock",//instock现车
// "categories" => [
// ["id" => 16],//used二手车
// ],
//"images" => $images,
"status" => 'publish', //发布状态
];
//$data['categories'][] = ["id" => $site['categories']];
$data['product_cat'] = explode(',', $site['categories']);
//车辆档案相关参数
$meta = $this->drawMeta($row);
//车身颜色
$color = '';
foreach ($meta as $key => $item){
if (!empty($item['key']) && ($item['key'] == 'body_color' || $item['key'] == 'source_place')) {
$goal = $this->sparkTransl($spark,$item['value'],$locale);
$meta[$key]['value'] = $goal;
$color = $item['key'] == 'body_color'?$goal:'';
}
}
if ($site['tpl_type'] == 2) {
$meta = [];
}
//属性:品牌、车体类型、颜色
$brand = $row['crawl_brand']?:$row['product_name'];
$brand_locale = $spark->chat('将车辆品牌翻译为'.$locale.':'.$brand,null,[],'wpSend');
//查询品牌id
$brand_id = $this->get_or_create_brand_id($site,$domain, $brand_locale);
$data['pa_brand'] = [$brand_id];
//查询车身类型
$bodys = $this->get_body($site,$domain);
$bady_style = array_column($bodys, 'name');
$style = $spark->chat('根据给出的信息判断车身类型(不要回答多余的文字,只需要返回车身类型,无法判断则返回:Other),车身类型有:'.implode('、',$bady_style).',车辆数据为:'.json_encode($row->toArray(),JSON_UNESCAPED_UNICODE),null,[],'wpSend');
if(!in_array($style,$bady_style)){
$style = 'Other';
}
$style_id = 0;
foreach ($bodys as $key => $item){
if ($item['name'] == $style) {
$style_id = $item['id'];
}
}
$data['pa_body-style'] = [$style_id];
// 翻译车况简介
// $baseInfo = $this->parseCarConfigDetail($row['format_car_info']);
$conditionDesc_locale = $spark->chat('将车况描述翻译为'.$locale.':'.$row['condition_desc'],null,[],'wpSend');
//翻译车辆简介描述
$desc = "车辆名称:{$car_name},车辆参数:".json_encode($meta);
$vehicleDescription_locale = $spark->chat('根据给出的信息生成'.$locale.'车辆简介描述(要求简短):'.$desc,null,[],'wpSend');
$meta[] = [
'key' => 'pro-intro-des',//车况介绍
'value' => $vehicleDescription_locale,
];
$meta[] = [
'key' => 'condition_desc',//产品简介
'value' => $conditionDesc_locale,
];
// $meta[] = [ //列表标签
// 'key' => 'hotpro_mark',//自定义热门产品标签
// 'value' => 'In Stock', //In Stock、HOT SALE
// ];
//$data['meta'] = array_values($meta);
foreach ($meta as $item){
$data['acf'][$item['key']] = $item['value'];
}
$data['featured_media'] = $images[0];
$data['acf']['product_gallery'] = $images;
try {
$response = $client->post($url, [
//RequestOptions::AUTH => ['Borderless', '3Tdw 6O02 DM6f lb2S 7T3l WG8a'],
RequestOptions::AUTH => [$site['api_user'], $site['api_secret']],
RequestOptions::JSON => $data,
RequestOptions::VERIFY => false, // 禁用 SSL 验证
]);
$result = json_decode($response->getBody()->getContents(), true);
$wordpressUsedCar = [
'wp_site_id' => $site['id'],
'used_car_id' => $id,
'wp_post_id' => $result['id'],
'request_payload' => json_encode($data, JSON_UNESCAPED_UNICODE),
'response_body' => $response->getBody()->getContents(),
'status' => 1,
'retry_count' => 1,
];
WordpressUsedCar::create($wordpressUsedCar);
$site->last_uid = $id;
$site->save();
$data ['result'] = $result;
return $data;
//return $result;
} catch (\Exception $e) {
$his = WordpressUsedCar::where('wp_site_id', $site['id'])->where('used_car_id', $id)->first();
if ($his){
$his->retry_count = $his->retry_count + 1;
$his->save();
if ($his->retry_count>=3){
$site->last_uid = $id;
$site->save();
}
}else{
$wordpressUsedCar = [
'wp_site_id' => $site['id'],
'used_car_id' => $id,
'request_payload' => json_encode($data, JSON_UNESCAPED_UNICODE),
// 'response_body' => !empty($response)?$response->getBody()->getContents():$response,
'error_message' => $e->getMessage(),
'status' => 0,
'retry_count' => 1,
];
WordpressUsedCar::create($wordpressUsedCar);
}
throw new \Exception($e->getMessage());
}
}
/**
* 使用spark lite对话翻译,检查如果没有中文直接返回,有中文则翻译
* @param $spark
* @param $str
* @param $locale
* @return mixed
*/
public function sparkTransl($spark,$str,$locale)
{
if(!(preg_match('/[\x{4e00}-\x{9fff}]/u', $str) > 0)){
return $str;
};
$str = str_replace(["“", "”"], "", $str);
//翻译
$vehicleDescription_locale = $spark->chat('将“'.$str.'”翻译为'.$locale,null,[],'wpSend');
return $vehicleDescription_locale;
}
/**
* 获取meta_data字段,即ACF自定义的字段
* @param $used_detail
* @return array
* @throws \Exception
*/
public function drawMeta($used_car){
$fieldMap = [ //中文字段 → ACF 字段名
'plate_time', //上牌时间
'displacement', //排量
'mileage', //里程
'power', //马力
'body_color', //车身颜色
'source_place', //车源地
'price', //价格
];
$metaData = [];
foreach ($fieldMap as $acfKey) {
if (!empty($used_car[$acfKey])) {
$metaData[] = [
'key' => $acfKey,
'value' => $used_car[$acfKey],
];
}
}
return $metaData;
}
/**
* 安全取值
*/
public function drawDetils($row)
{
$baseInfo = $this->parseCarConfigDetail($row['format_car_info']);
return [
'Ref ID' => $row['used_car_id'],
'Model Code' => $row['model_code'] ?? $row['car_name'],
'Model Year' => $row['year'],
'Exterior Color' => value($baseInfo, 'body_color'),
'Engine' => value($baseInfo, 'engine_description'),
'Drivetrain' => value($baseInfo, 'driver_form'),
'Range' => '–',
'Seats' => value($baseInfo, 'seat_count'),
'Dim.(mm)' => value($baseInfo, 'length_width_height'),
'Weight(kg)' => value($baseInfo, 'curb_weight'),
'Steering' => 'Left',
'Body Type' => value($baseInfo, 'car_body_struct'),
'Mlg(km)' => $row['usage_mileage'],
'Fuel' => $row['fuel_form'],
'Transmission' => value($baseInfo, 'gearbox_description'),
'Batt.Cap.(kWh)' => value($baseInfo, 'battery_capacity'),
'Motor Power(kW)' => value($baseInfo, 'motor_power'),
'M³' => value($baseInfo, 'cargo_volume'),
'Doors' => value($baseInfo, 'door_nums'),
];
}
/**
* 安全取值
*/
function value(array $array, string $key, $default = '–')
{
if (!isset($array[$key]) || $array[$key] === '' || $array[$key] === null) {
return $default;
}
return $array[$key];
}
/**
* 将 format_car_info 拉平成 key => value
*
* @param string $json
* @return array
*/
function parseCarConfigDetail(string $json): array
{
$data = json_decode($json, true);
if (!is_array($data)) {
return [];
}
$result = [];
foreach ($data as $group) {
if (!isset($group['items']) || !is_array($group['items'])) {
continue;
}
foreach ($group['items'] as $item) {
if (!isset($item['key'])) {
continue;
}
$result[$item['key']] = $item['value'] ?? '';
}
}
return $result;
}
/**
* 从 config_detail 中提取配置信息,拉平成 key => value
*
* @param array $configDetail
* @return array
*/
private function parseConfigDetail(array $configDetail): array
{
if (!is_array($configDetail)) {
return [];
}
$result = [];
foreach ($configDetail as $group) {
if (!isset($group['items']) || !is_array($group['items'])) {
continue;
}
foreach ($group['items'] as $item) {
if (!isset($item['key'])) {
continue;
}
$key = $item['key'];
$value = $item['value'] ?? '';
// 如果值不为空,存储它(如果已存在相同key,保留第一个非空值)
if ($value !== '' && $value !== null && (!isset($result[$key]) || $result[$key] === '')) {
$result[$key] = $value;
}
}
}
return $result;
}
/**
* 从 config_detail 中获取指定配置项的值
*
* @param array $configDetail
* @param string|array $keys 要查找的key,可以是字符串或数组(多个可能的key)
* @param string $default 默认值
* @return string
*/
private function getConfigValue($configDetail, $keys, $default = ''): string
{
if (!is_array($configDetail)) {
return $default;
}
$keysToSearch = is_array($keys) ? $keys : [$keys];
foreach ($configDetail as $group) {
if (!isset($group['items']) || !is_array($group['items'])) {
continue;
}
foreach ($group['items'] as $item) {
if (!isset($item['key'])) {
continue;
}
$itemKey = $item['key'];
$itemValue = $item['value'] ?? '';
// 检查是否匹配任何要查找的key
foreach ($keysToSearch as $searchKey) {
if ($itemKey === $searchKey && $itemValue !== '' && $itemValue !== null) {
return (string)$itemValue;
}
}
}
}
return $default;
}
/**
* 查询wp车身类型
* @param $brand_name
* @return mixed
* @throws \GuzzleHttp\Exception\GuzzleException
*/
function get_body($site,$domain) {
$client = new Client([
'base_uri' => $domain . '/wp-json/wp/v2/',
RequestOptions::AUTH => [$site['api_user'], $site['api_secret']],
// 'auth' => ['your_username', 'your_password'], // 或者使用应用密码
RequestOptions::VERIFY => false, // 禁用 SSL 验证
]);
// 查询车身类型
$response = $client->get('pa_body-style');
$bodys = json_decode($response->getBody(), true);
// 返回车身类型
return $bodys;
}
/**
* 查询wp品牌id
* @param $brand_name
* @return mixed
* @throws \GuzzleHttp\Exception\GuzzleException
*/
function get_or_create_brand_id($site,$domain, $brand_name) {
if(empty($brand_name)){
return 0;
}
$client = new Client([
'base_uri' => $domain . '/wp-json/wp/v2/',
RequestOptions::AUTH => [$site['api_user'], $site['api_secret']],
// 'auth' => ['your_username', 'your_password'], // 或者使用应用密码
RequestOptions::VERIFY => false, // 禁用 SSL 验证
]);
// 生成slug
$slug = $this->generate_slug($brand_name);
// 查询品牌是否已存在
$response = $client->get('pa_brand', [
'query' => [
'slug' => $slug // 用品牌名称生成slug查询
]
]);
$brands = json_decode($response->getBody(), true);
// 如果品牌存在,返回ID
if (!empty($brands)) {
return $brands[0]['id']; // 返回第一个匹配的品牌ID
}
// 品牌不存在,先创建
$response = $client->post('pa_brand', [
'json' => [
'name' => $brand_name,
'slug' => $slug
]
]);
$brand = json_decode($response->getBody(), true);
// 返回创建的品牌ID
return $brand['id'];
}
function generate_slug($brand_name) {
// 将品牌名称转成小写并替换掉非字母数字字符
return preg_replace('/[^a-z0-9-]+/', '-', strtolower(trim($brand_name)));
}
/**
* 计算使用时间
*
* @param string $licenseDate 上牌时间,格式可能是 YYYY-MM-DD 或其他
* @return string|null
*/
private function calculateUsageTime($licenseDate): ?string
{
if (empty($licenseDate)) {
return null;
}
try {
// 尝试解析日期
$date = null;
if (preg_match('/^(\d{4})-(\d{1,2})-(\d{1,2})/', $licenseDate, $matches)) {
$date = new \DateTime($matches[1] . '-' . $matches[2] . '-' . $matches[3]);
} elseif (preg_match('/^(\d{4})-(\d{1,2})/', $licenseDate, $matches)) {
$date = new \DateTime($matches[1] . '-' . $matches[2] . '-01');
} elseif (is_numeric($licenseDate) && strlen($licenseDate) == 10) {
// Unix时间戳
$date = new \DateTime('@' . $licenseDate);
}
if (!$date) {
return null;
}
$now = new \DateTime();
$diff = $now->diff($date);
$years = $diff->y;
$months = $diff->m;
if ($years > 0) {
$months += $years * 12;
}
if ($months == 0) {
// 如果不到1个月,显示天数
$days = $diff->days;
if ($days > 0) {
return $days == 1 ? "1 day" : "{$days} days";
}
return null;
}
if ($months == 1) {
return "1 month";
} else {
return "{$months} months";
}
} catch (\Exception $e) {
Log::error('计算使用时间失败', ['license_date' => $licenseDate, 'error' => $e->getMessage()]);
return null;
}
}
/**
* 获取内饰颜色
*
* @param array $configDetail
* @param array $baseInfo
* @return string
*/
private function getInteriorColor($configDetail, $baseInfo): string
{
$possibleKeys = ['nsys', 'interior_color', 'interiorColor', 'seat_color', 'seatColor', '内饰颜色'];
// 优先从 configDetail 中查找
$color = $this->getConfigValue($configDetail, $possibleKeys, '');
if (!empty($color) && $color !== '–') {
return $color;
}
// 如果 configDetail 中没有找到,尝试通过中文字段名查找
if (is_array($configDetail)) {
foreach ($configDetail as $group) {
if (isset($group['items']) && is_array($group['items'])) {
foreach ($group['items'] as $item) {
// 检查中文字段名
if (isset($item['name']) && (strpos($item['name'], '内饰') !== false || strpos($item['name'], '座椅') !== false)) {
if (!empty($item['value']) && $item['value'] !== '–') {
return $item['value'];
}
}
}
}
}
}
// 最后尝试从 baseInfo 中查找
foreach ($possibleKeys as $key) {
if (!empty($baseInfo[$key]) && $baseInfo[$key] !== '–') {
return $baseInfo[$key];
}
}
return '';
}
/**
* 将颜色翻译为英文
*
* @param string $color 颜色(可能是中文)
* @return string
*/
private function translateColorToEnglish($color): string
{
if (empty($color)) {
return '';
}
$colorMap = [
'白色' => 'white',
'黑色' => 'black',
'红色' => 'red',
'蓝色' => 'blue',
'绿色' => 'green',
'黄色' => 'yellow',
'银色' => 'silver',
'灰色' => 'gray',
'灰色' => 'grey',
'棕色' => 'brown',
'金色' => 'gold',
'橙色' => 'orange',
'紫色' => 'purple',
'粉色' => 'pink',
'米色' => 'beige',
'浅色' => 'light-colored',
'深色' => 'dark-colored',
];
$colorLower = mb_strtolower(trim($color), 'UTF-8');
// 如果已经是英文,直接返回(转小写)
if (preg_match('/^[a-z\s-]+$/i', $color)) {
return strtolower($color);
}
// 查找中文映射
foreach ($colorMap as $cn => $en) {
if (strpos($colorLower, $cn) !== false) {
return $en;
}
}
// 如果没有找到映射,尝试提取可能的英文部分,否则返回原值
if (preg_match('/[a-z]+/i', $color, $matches)) {
return strtolower($matches[0]);
}
return strtolower($color);
}
/**
* 构建 Vehicle Details 4列表格(2组 Key/Value)
*
* @param array $items [['label'=>'Ref ID','value'=>'xxx'], ...]
* @return string
*/
public function buildVehicleDetailsTable(array $items): string
{
$html = '<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
{
$process = 'image/resize,w_900/quality,Q_50/format,webp';
$imageUrl = (new OssService)->generatePrivateUrlResize($imageUrl,3600,$process);
$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());
}
}
}