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

1066 lines
36 KiB
PHP
Raw Normal View History

2026-03-09 17:46:17 +08:00
<?php
namespace app\common\service;
use app\common\validate\UsedCarDetailValidate;
use app\common\traits\crud\CrudServiceTrait;
use app\common\model\UsedCarDetail;
use app\common\model\User;
use support\exception\BusinessException;
use support\Log;
use support\Request;
use support\Redis;
2026-03-10 11:54:58 +08:00
use GuzzleHttp\Client;
use GuzzleHttp\Exception\GuzzleException;
use QL\QueryList;
2026-03-09 17:46:17 +08:00
class UsedCarDetailService
{
use CrudServiceTrait;
/**
* 指定当前要操作的模型
*/
protected string $modelClass = UsedCarDetail::class;
/**
* 验证器
*/
protected $validator = UsedCarDetailValidate::class;
/**
* 获取验证器
*/
public function getValidator()
{
return new $this->validator;
}
/**
* 构建查询配置
*/
protected function buildQueryConfig(Request $request, string $scene = 'default'): array
{
$p = $request->all();
$config = [];
$config['model'] = $this->modelClass;
switch ($scene) {
case 'default':
$config['where']['status'] = 1;
$config['order']['sort'] = 'desc';
$config['order']['id'] = 'desc';
break;
case 'index':
// 审核状态筛选
if (isset($p['status'])) {
$config['where']['status'] = $p['status'];
}
// 我的车辆
if (isset($p['scene']) && $p['scene'] == 'self') {
$config['where']['user_id'] = $request->user['id'];
unset($config['where']['status']);
}
// 关键字搜索
if (isset($p['keyword']) && $p['keyword']) {
$k = '%' . $p['keyword'] . '%';
foreach (['product_name', 'code', 'vin', 'manufacturer'] as $col) {
$config['orWhere'][] = [$col, 'like', $k];
}
}
// 品牌筛选
if (isset($p['crawl_brand_id']) && $p['crawl_brand_id']) {
$config['where']['crawl_brand_id'] = $p['crawl_brand_id'];
}
// 车系筛选
if (isset($p['crawl_series_id']) && $p['crawl_series_id']) {
$config['where']['crawl_series_id'] = $p['crawl_series_id'];
}
// 价格区间
if (isset($p['min_price']) && $p['min_price']) {
$config['where'][] = ['price', '>=', $p['min_price']];
}
if (isset($p['max_price']) && $p['max_price']) {
$config['where'][] = ['price', '<=', $p['max_price']];
}
$config['order']['sort'] = 'desc';
$config['order']['id'] = 'desc';
break;
case 'show':
$config['where']['id'] = $p['id'];
// 关联FAQ
$config['with'] = ['faqs'];
break;
case 'store':
break;
case 'update':
$config['where']['id'] = $p['id'];
break;
case 'destroy':
$config['where']['id'] = $p['id'];
break;
}
return $config;
}
/**
* 创建前数据处理
*/
public function beforeServiceStore($data, Request $request): array
{
if (empty($data['product_name'])) {
throw new BusinessException('产品名称不能为空');
}
// 设置用户信息
$data['user_id'] = $request->user['id'];
$user = User::where('id', $request->user['id'])->first();
$data['role_wp'] = $user['role_wp'] ?? 1;
// 默认待审核状态
if (!isset($data['status'])) {
$data['status'] = 0;
}
// 生成车辆编号
$data['code'] = $this->getCode($request->user['id']);
// 处理图片数据
if (!empty($data['images'])) {
if (is_array($data['images'])) {
2026-03-10 11:05:30 +08:00
$data['images'] = implode(',', $data['images']);
2026-03-09 17:46:17 +08:00
}
} else {
$data['images'] = '';
}
// 处理附件数据
if (!empty($data['attachment'])) {
if (is_array($data['attachment'])) {
$data['attachment'] = json_encode($data['attachment']);
}
} else {
$data['attachment'] = '';
}
// 处理介绍图片
if (!empty($data['intro_images'])) {
if (is_array($data['intro_images'])) {
2026-03-10 11:36:24 +08:00
$data['intro_images'] = implode(',', $data['intro_images']);
2026-03-09 17:46:17 +08:00
}
} else {
$data['intro_images'] = '';
}
// 检查来源URL是否重复
if (!empty($data['source_url'])) {
$exists = UsedCarDetail::where('source_url', $data['source_url']);
$exists = $exists->where('user_id', $data['user_id']);
if ($exists->exists()) {
throw new BusinessException('该车源已收录');
}
}
2026-03-10 11:54:58 +08:00
// 如果提供了 crawl_car_id,自动抓取懂车帝车辆参数
if (!empty($data['crawl_car_id'])) {
try {
$params = $this->crawlParams($data['crawl_car_id']);
2026-03-12 09:39:06 +08:00
if(str_contains($params['official_guide_price'],'万')){
$params['official_guide_price'] = str_replace('万','',$params['official_guide_price'])*10000;
}
2026-03-10 11:54:58 +08:00
// 合并抓取的数据,手动提交的字段优先级更高
$data = array_merge($params, $data);
} catch (\Throwable $e) {
// 抓取失败不阻断创建,记录日志
Log::warning('抓取车辆参数失败', [
'crawl_car_id' => $data['crawl_car_id'],
'error' => $e->getMessage(),
]);
}
}
2026-03-09 17:46:17 +08:00
return $data;
}
/**
* 更新前数据处理
*/
public function beforeServiceUpdate($data, Request $request): array
{
if (empty($data['product_name'])) {
throw new BusinessException('产品名称不能为空');
}
// 获取车辆信息
$carId = $data['id'] ?? null;
if ($carId) {
$car = UsedCarDetail::find($carId);
if (!$car) {
throw new BusinessException('车辆不存在');
}
// 审核通过的车辆禁止修改
if ($car->status == 1) {
throw new BusinessException('审核通过的车辆禁止修改');
}
// 审核拒绝的车辆修改后状态改为待审核
if ($car->status == 2) {
$data['status'] = 0;
}
}
// 处理附件数据
if (isset($data['attachment'])) {
if (is_array($data['attachment'])) {
$data['attachment'] = json_encode($data['attachment']);
}
}
// 处理介绍图片
if (isset($data['intro_images'])) {
if (is_array($data['intro_images'])) {
$data['intro_images'] = json_encode($data['intro_images']);
}
}
return $data;
}
/**
* 更新状态
*/
public function serviceUpdateStatus(Request $request, array $data)
{
if (empty($data['id'])) {
throw new BusinessException('id不能为空');
}
if (!isset($data['status'])) {
throw new BusinessException('status错误');
}
return UsedCarDetail::where('id', $data['id'])->update(['status' => $data['status']]);
}
/**
* 保存FAQ
*/
public function saveFaqs(int $carId, array $faqs)
{
// 删除旧的FAQ
\app\common\model\UsedCarFaq::where('car_id', $carId)->delete();
// 创建新的FAQ
if (!empty($faqs)) {
foreach ($faqs as $faq) {
\app\common\model\UsedCarFaq::create([
'car_id' => $carId,
'question' => $faq['question'] ?? '',
'answer' => $faq['answer'] ?? '',
'sort' => $faq['sort'] ?? 0,
'status' => $faq['status'] ?? 1,
]);
}
}
}
/**
* 生成车辆编号
*/
public function getCode(int $userId): string
{
$userCount = Redis::get('used_car_detail_count_' . $userId, 0);
$todayStart = strtotime('today');
$count = UsedCarDetail::where('user_id', $userId)
->where('created_at', '>=', $todayStart)
->count();
$count = $count < $userCount ? $userCount + 1 : $count + 1;
$countCode = str_pad($count, 3, '0', STR_PAD_LEFT);
$userCode = str_pad($userId, 3, '0', STR_PAD_LEFT);
Redis::set('used_car_detail_count_' . $userId, $count, 86400);
$user = User::where('id', $userId)->first();
$nameInitials = $this->getNameInitials($user['name'] ?? '');
return 'UCD' . $nameInitials . $userCode . date('Ymd') . $countCode;
}
/**
* 获取中文名拼音首字母
*/
private function getNameInitials(string $name): string
{
$initials = '';
$len = mb_strlen($name);
for ($i = 0; $i < $len; $i++) {
$char = mb_substr($name, $i, 1);
$initials .= $this->getFirstChar($char);
}
return strtoupper($initials);
}
/**
* 获取单个字符的首字母
*/
private function getFirstChar(string $char): string
{
$s = iconv('UTF-8', 'GB2312//IGNORE', $char);
$asc = ord($s[0]);
if ($asc >= 65 && $asc <= 90) {
return $char;
}
if ($s === false || strlen($s) < 2) {
return '';
}
$code = ord($s[0]) * 256 + ord($s[1]) - 65536;
$map = [
[-20319, -20284, 'A'],
[-20283, -19776, 'B'],
[-19775, -19219, 'C'],
[-19218, -18711, 'D'],
[-18710, -18527, 'E'],
[-18526, -18240, 'F'],
[-18239, -17923, 'G'],
[-17922, -17418, 'H'],
[-17417, -16475, 'J'],
[-16474, -16213, 'K'],
[-16212, -15641, 'L'],
[-15640, -15166, 'M'],
[-15165, -14923, 'N'],
[-14922, -14915, 'O'],
[-14914, -14631, 'P'],
[-14630, -14150, 'Q'],
[-14149, -14091, 'R'],
[-14090, -13319, 'S'],
[-13318, -12839, 'T'],
[-12838, -12557, 'W'],
[-12556, -11848, 'X'],
[-11847, -11056, 'Y'],
[-11055, -10247, 'Z']
];
foreach ($map as [$start, $end, $letter]) {
if ($code >= $start && $code <= $end) {
return $letter;
}
}
return '';
}
/**
* 提交审核
*/
public function submitAudit(Request $request, int $carId)
{
$car = UsedCarDetail::find($carId);
if (!$car) {
throw new BusinessException('车辆不存在');
}
// 检查权限:只能提交自己创建的车辆
if ($car->user_id != $request->user['id']) {
throw new BusinessException('无权操作');
}
// 只有待审核状态才能提交
if ($car->status != 0) {
throw new BusinessException('当前状态不允许提交审核');
}
return UsedCarDetail::where('id', $carId)->update(['status' => 0]);
}
/**
* 审核车辆
*/
public function audit(Request $request, array $data)
{
$carId = $data['car_id'] ?? null;
$auditStatus = $data['audit_status'] ?? null;
$auditReason = $data['audit_reason'] ?? '';
if (empty($carId)) {
throw new BusinessException('车辆ID不能为空');
}
if (!isset($auditStatus) || !in_array($auditStatus, [1, 2])) {
throw new BusinessException('审核状态错误');
}
$car = UsedCarDetail::find($carId);
if (!$car) {
throw new BusinessException('车辆不存在');
}
$beforeStatus = $car->status;
// 创建审核记录
\app\common\model\UsedCarDetailAudit::create([
'car_id' => $carId,
'auditor_id' => $request->user['id'],
'audit_status' => $auditStatus,
'audit_reason' => $auditReason,
'before_status' => $beforeStatus,
'after_status' => $auditStatus,
]);
// 更新车辆状态
2026-03-10 18:35:18 +08:00
$result = UsedCarDetail::where('id', $carId)->update(['status' => $auditStatus]);
2026-03-11 16:01:18 +08:00
// 审核通过后,将推送任务发送到队列
$pushQueued = false;
2026-03-10 18:35:18 +08:00
if ($auditStatus == 1) {
2026-03-11 16:01:18 +08:00
try {
// 发送到 Redis 队列异步处理
\Webman\RedisQueue\Redis::send('wordpress-push', [
'car_id' => $carId,
]);
$pushQueued = true;
Log::info('WordPress推送任务已加入队列', ['car_id' => $carId]);
} catch (\Throwable $e) {
Log::error('WordPress推送任务加入队列失败', [
'car_id' => $carId,
'error' => $e->getMessage(),
]);
$pushQueued = false;
2026-03-10 18:35:18 +08:00
}
}
2026-03-11 16:01:18 +08:00
// 返回审核结果
2026-03-10 18:35:18 +08:00
return [
'audit_success' => true,
2026-03-11 16:01:18 +08:00
'push_queued' => $pushQueued,
'message' => $pushQueued ? '审核通过,WordPress推送任务已加入队列' : '审核通过',
2026-03-10 18:35:18 +08:00
];
}
2026-03-09 17:46:17 +08:00
/**
* 获取审核记录列表
*/
public function getAuditRecords(int $carId)
{
return \app\common\model\UsedCarDetailAudit::where('car_id', $carId)
->with(['auditor:id,name'])
->orderBy('created_at', 'desc')
->get()
->map(function ($item) {
2026-03-12 09:46:17 +08:00
if(!is_numeric($item->created_at)){
2026-03-12 09:52:52 +08:00
$created_at = $item->created_at->timestamp;
}else{
$created_at = $item->created_at;
2026-03-12 09:46:17 +08:00
}
2026-03-09 17:46:17 +08:00
return [
'id' => $item->id,
'audit_status' => $item->audit_status,
'audit_status_text' => $item->audit_status_text,
'audit_reason' => $item->audit_reason,
'before_status' => $item->before_status,
'after_status' => $item->after_status,
'auditor_name' => $item->auditor->name ?? '',
2026-03-12 09:52:52 +08:00
'created_at' => date('Y-m-d H:i:s', $created_at),
2026-03-09 17:46:17 +08:00
];
});
}
/**
* 获取审核统计数据
*/
public function getAuditStats(Request $request)
{
$query = UsedCarDetail::query();
// 如果不是管理员,只能看到自己的数据
if ($request->user['role_wp'] != 3) {
$query->where('user_id', $request->user['id']);
}
return [
'pending' => (clone $query)->where('status', 0)->count(),
'approved' => (clone $query)->where('status', 1)->count(),
'rejected' => (clone $query)->where('status', 2)->count(),
'total' => $query->count(),
];
}
2026-03-10 11:54:58 +08:00
/**
* 抓取懂车帝车辆参数
*
* @param string $crawlCarId 车型ID
* @return array 格式化后的参数数据
* @throws BusinessException
*/
public function crawlParams(string $crawlCarId): array
{
$url = 'https://www.dongchedi.com/auto/params-carIds-' . $crawlCarId;
// 请求头配置
$headers = [
'User-Agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
'Accept-Language' => 'zh-CN,zh;q=0.9',
'Referer' => 'https://www.dongchedi.com/',
];
try {
$client = new Client();
$response = $client->request('GET', $url, [
'headers' => $headers,
'timeout' => 10,
'verify' => false, // 禁用SSL验证
]);
$html = (string)$response->getBody();
if (empty($html)) {
throw new BusinessException('页面内容为空');
}
// 解析HTML获取JSON数据
$ql = QueryList::html($html);
$jsonText = $ql->find('#__NEXT_DATA__')->text();
if (empty($jsonText)) {
throw new BusinessException('未找到 __NEXT_DATA__ JSON');
}
$raw = json_decode($jsonText, true);
if (json_last_error() !== JSON_ERROR_NONE) {
throw new BusinessException('JSON解析失败: ' . json_last_error_msg());
}
// 提取数据
$properties = $raw['props']['pageProps']['rawData']['properties'] ?? [];
$carInfo = $raw['props']['pageProps']['rawData']['car_info'][0] ?? [];
if (empty($properties) || empty($carInfo)) {
throw new BusinessException('数据结构异常,无法提取参数');
}
// 构建值映射表和分组
$valuesMap = $this->buildValuesMap($carInfo);
$groups = $this->buildCarSpecGroups($properties, $valuesMap);
// 映射到数据库字段
return $this->mapParamsToDbFields($groups, $carInfo);
} catch (GuzzleException $e) {
Log::error('抓取懂车帝参数失败', [
'crawl_car_id' => $crawlCarId,
'error' => $e->getMessage(),
]);
throw new BusinessException('抓取参数失败: ' . $e->getMessage());
} catch (\Throwable $e) {
Log::error('解析懂车帝参数失败', [
'crawl_car_id' => $crawlCarId,
'error' => $e->getMessage(),
]);
throw new BusinessException('解析参数失败: ' . $e->getMessage());
}
}
/**
* 构建值映射表
*/
private function buildValuesMap(array $carInfo): array
{
$valuesMap = [];
// 顶层字段
foreach ($carInfo as $k => $v) {
if ($k === 'info' || is_array($v)) {
continue;
}
$valuesMap[$k] = ['value' => $v];
}
// info中的配置项
if (!empty($carInfo['info']) && is_array($carInfo['info'])) {
foreach ($carInfo['info'] as $k => $v) {
if (is_array($v) && array_key_exists('value', $v)) {
$valuesMap[$k] = $v;
}
}
}
return $valuesMap;
}
/**
* 构建车辆规格分组
*/
private function buildCarSpecGroups(array $properties, array $valuesMap): array
{
$groups = [];
$currentKey = null;
foreach ($properties as $item) {
$type = (int)($item['type'] ?? 1);
$key = $item['key'] ?? '';
$text = $item['text'] ?? '';
$subList = $item['sub_list'] ?? null;
// 分类字段:type = 0 / 2
if (in_array($type, [0, 2], true)) {
$currentKey = $key ?: ($text ?: ('group_' . count($groups)));
if (!isset($groups[$currentKey])) {
$groups[$currentKey] = [
'group_key' => $currentKey,
'group_text' => $text,
'group_type' => $type,
'items' => [],
];
}
continue;
}
// 普通字段:挂到最近的分类下
if ($currentKey === null) {
$currentKey = '_default';
$groups[$currentKey] = [
'group_key' => '_default',
'group_text' => '其他',
'group_type' => -1,
'items' => [],
];
}
$value = '';
if ($key && isset($valuesMap[$key])) {
$value = trim((string)($valuesMap[$key]['value'] ?? ''));
}
$node = [
'key' => $key,
'text' => $text,
'value' => $value,
];
if (is_array($subList) && !empty($subList)) {
$children = [];
foreach ($subList as $sub) {
$subKey = $sub['key'] ?? '';
$subText = $sub['text'] ?? '';
$subValue = '';
if ($subKey && isset($valuesMap[$subKey])) {
$subValue = trim((string)($valuesMap[$subKey]['value'] ?? ''));
}
$children[] = [
'key' => $subKey,
'text' => $subText,
'value' => $subValue,
];
}
$node['children'] = $children;
}
$groups[$currentKey]['items'][] = $node;
}
return array_values($groups);
}
/**
* 从分组数据中获取字段值
*/
private function getFieldValue($groups, string $field): ?string
{
if (is_string($groups)) {
$groups = json_decode($groups, true);
if (json_last_error() !== JSON_ERROR_NONE) {
return null;
}
}
if (!is_array($groups)) {
return null;
}
foreach ($groups as $group) {
if (isset($group['items']) && is_array($group['items'])) {
foreach ($group['items'] as $item) {
if (($item['key'] ?? null) === $field) {
return $item['value'] ?? null;
}
if (!empty($item['children']) && is_array($item['children'])) {
foreach ($item['children'] as $child) {
if (($child['key'] ?? null) === $field) {
return $child['value'] ?? null;
}
}
}
}
}
}
return null;
}
/**
* 解析尺寸字符串 (如 "4890x1890x1680" 转为长宽高)
*/
private function parseDimensions(string $dimensionStr): array
{
$result = ['body_length' => '', 'body_width' => '', 'body_height' => ''];
if (empty($dimensionStr)) {
return $result;
}
$parts = explode('x', strtolower(str_replace(['×', 'X', 'X'], 'x', $dimensionStr)));
if (count($parts) >= 3) {
$result['body_length'] = trim($parts[0]);
$result['body_width'] = trim($parts[1]);
$result['body_height'] = trim($parts[2]);
}
return $result;
}
/**
* 转换安全配置值 (●→标配, ○→选装, -→空)
*/
private function convertSafetyValue(?string $value): string
{
if ($value === '●') {
return '标配';
} elseif ($value === '○') {
return '选装';
} elseif ($value === '-') {
return '';
}
return $value ?? '';
}
/**
* 映射参数到数据库字段
* DongChedi 字段 → 数据库字段
*/
private function mapParamsToDbFields(array $groups, array $carInfo): array
{
$result = [];
2026-03-11 15:01:54 +08:00
// 从 car_info 直接获取的字段,清理掉"万"等字符
$officialPrice = $carInfo['official_price'] ?? '';
$result['official_guide_price'] = $this->convertPriceToYuan($officialPrice);
2026-03-10 11:54:58 +08:00
// 1. 基本信息
$result['manufacturer'] = $this->getFieldValue($groups, 'sub_brand_name') ?? '';
$result['level'] = $this->getFieldValue($groups, 'jb') ?? '';
$result['energy_type'] = $this->getFieldValue($groups, 'fuel_form') ?? '';
$result['listing_time'] = $this->getFieldValue($groups, 'market_time') ?? '';
$result['engine'] = $this->getFieldValue($groups, 'engine_description') ?? '';
$result['electric_motor'] = $this->getFieldValue($groups, 'electric_description') ?? '';
$result['nedc_range'] = $this->parseNumeric($this->getFieldValue($groups, 'nedc_range'));
$result['cltc_range'] = $this->parseNumeric($this->getFieldValue($groups, 'cltc_range'));
$result['fast_charge_platform'] = $this->getFieldValue($groups, 'fast_charge_platform') ?? '';
$result['charging_time'] = $this->parseDecimal($this->getFieldValue($groups, 'charging_time'));
$result['fast_charge_percent'] = $this->parseNumeric($this->getFieldValue($groups, 'fast_charge_percent'));
$result['max_power_kw'] = $this->parseNumeric($this->getFieldValue($groups, 'energy_elect_max_power'));
$result['max_torque_nm'] = $this->parseNumeric($this->getFieldValue($groups, 'energy_elect_max_torque'));
$result['transmission_desc'] = $this->getFieldValue($groups, 'gearbox_description') ?? '';
$result['dimensions'] = $this->getFieldValue($groups, 'length_width_height') ?? '';
$result['body_structure'] = $this->getFieldValue($groups, 'car_body_struct') ?? '';
$result['max_speed'] = $this->parseNumeric($this->getFieldValue($groups, 'max_speed'));
$result['acceleration_100'] = $this->parseDecimal($this->getFieldValue($groups, 'acceleration_time'));
$result['nedc_fuel_consumption'] = $this->parseDecimal($this->getFieldValue($groups, 'fuel_comprehensive'));
$result['power_consumption_100'] = $this->parseDecimal($this->getFieldValue($groups, 'power_consumption_100'));
$result['equivalent_fuel_consumption'] = $this->parseDecimal($this->getFieldValue($groups, 'equivalent_fuel_consumption'));
$result['maintenance_cost_60k'] = $this->parseDecimal($this->getFieldValue($groups, 'maintain_cost'));
// 2. 车身信息 (解析尺寸)
$dimensionStr = $this->getFieldValue($groups, 'length_width_height');
$dimensions = $this->parseDimensions($dimensionStr ?? '');
$result['body_length'] = $dimensions['body_length'];
$result['body_width'] = $dimensions['body_width'];
$result['body_height'] = $dimensions['body_height'];
$result['wheelbase'] = $this->parseNumeric($this->getFieldValue($groups, 'wheelbase'));
$result['door_count'] = $this->parseNumeric($this->getFieldValue($groups, 'door_nums'));
$result['door_open_type'] = $this->getFieldValue($groups, 'door_open_way') ?? '';
$result['seat_count'] = $this->parseNumeric($this->getFieldValue($groups, 'seat_count'));
// 3. 变速箱
$result['gear_count'] = $this->parseNumeric($this->getFieldValue($groups, 'stalls'));
$result['transmission_type'] = $this->getFieldValue($groups, 'gearbox_type') ?? '';
// 4. 底盘/转向
$result['drive_mode'] = $this->getFieldValue($groups, 'driver_form') ?? '';
$result['front_suspension'] = $this->getFieldValue($groups, 'front_suspension_form') ?? '';
$result['rear_suspension'] = $this->getFieldValue($groups, 'rear_suspension_form') ?? '';
$result['steering_type'] = $this->getFieldValue($groups, 'power_steering_type') ?? '';
// 5. 主动安全配置
$result['has_abs'] = $this->convertSafetyValue($this->getFieldValue($groups, 'abs_anti_lock'));
$result['has_ebd'] = $this->convertSafetyValue($this->getFieldValue($groups, 'brake_force'));
$result['has_bra'] = $this->convertSafetyValue($this->getFieldValue($groups, 'brake_assist'));
$result['has_tcs'] = $this->convertSafetyValue($this->getFieldValue($groups, 'traction_control'));
$result['has_esp'] = $this->convertSafetyValue($this->getFieldValue($groups, 'body_stability_system'));
$result['has_active_brake'] = $this->convertSafetyValue($this->getFieldValue($groups, 'active_brake'));
$result['has_lane_assist'] = $this->convertSafetyValue($this->getFieldValue($groups, 'line_support'));
$result['has_lka'] = $this->convertSafetyValue($this->getFieldValue($groups, 'lane_keeping_assist'));
$result['has_lane_centering'] = $this->convertSafetyValue($this->getFieldValue($groups, 'lane_center'));
$result['has_fatigue_remind'] = $this->convertSafetyValue($this->getFieldValue($groups, 'fatigue_driving_warning'));
// 6. 车轮/制动
$result['front_brake_type'] = $this->getFieldValue($groups, 'front_brake_type') ?? '';
$result['rear_brake_type'] = $this->getFieldValue($groups, 'rear_brake_type') ?? '';
$result['parking_brake_type'] = $this->getFieldValue($groups, 'park_brake_type') ?? '';
$result['front_tire_spec'] = $this->getFieldValue($groups, 'front_tire_size') ?? '';
$result['rear_tire_spec'] = $this->getFieldValue($groups, 'rear_tire_size') ?? '';
// 确保空字符串不被转为null
foreach ($result as $key => $value) {
if ($value === null) {
$result[$key] = '';
}
}
return $result;
}
/**
* 解析数值字段
*/
private function parseNumeric(?string $value): string
{
if ($value === null || $value === '') {
return '';
}
// 提取数字部分
if (preg_match('/\d+/', $value, $matches)) {
return $matches[0];
}
return '';
}
/**
* 解析小数
*/
private function parseDecimal(?string $value): string
{
if ($value === null || $value === '') {
return '';
}
// 提取数字和小数点
if (preg_match('/[\d.]+/', $value, $matches)) {
return $matches[0];
}
return '';
}
2026-03-11 15:01:54 +08:00
/**
* 抓取懂车帝二手车数据并保存
*
* @param string $url 懂车帝二手车链接
* @param int $userId 用户ID
* @return array 返回创建的车辆数据
* @throws \Exception
*/
2026-03-12 14:21:12 +08:00
public function crawl(string $url, int $userId): array
2026-03-11 15:01:54 +08:00
{
2026-03-11 17:16:16 +08:00
// 检查该用户是否已抓取过此URL的车辆
$existingCar = UsedCarDetail::where('user_id', $userId)
->where('source_url', $url)
->first();
if ($existingCar) {
throw new \Exception('该车辆信息已存在,无需重复抓取');
}
2026-03-11 15:01:54 +08:00
// 1. 抓取二手车基本信息
$usedCarCrawl = new \app\common\utils\crawl\service\UsedCarCrawlSingle();
$info = $usedCarCrawl->crawlCar($url);
// 2. 如果有 car_id,抓取详细参数
$params = [];
if (!empty($info['car_id'])) {
try {
$params = $this->crawlParams($info['car_id']);
} catch (\Throwable $e) {
Log::error('抓取车辆参数失败', [
'car_id' => $info['car_id'],
'error' => $e->getMessage(),
]);
// 参数抓取失败不影响主流程
}
}
// 3. 构建数据库记录数据
// 处理图片:如果是数组,转为逗号分隔字符串
$imagesValue = $info['image'] ?? '';
if (is_array($imagesValue)) {
$imagesValue = implode(',', array_filter($imagesValue));
}
$data = [
'code' => $this->generateCode(),
'user_id' => $userId,
'product_name' => $info['title'] ?? $info['car_name'] ?? '',
'price' => $this->convertPriceToYuan($info['price'] ?? 0),
'source_place' => $info['source_place'] ?? '',
'plate_time' => $info['plate_time'] ?? '',
'displacement' => $info['capacity'] ?? '',
'mileage' => $this->convertMileageToKm($info['usage_mileage'] ?? 0),
'body_color' => $info['body_color'] ?? '',
'condition_desc' => $info['condition_desc'] ?? '',
'source_url' => $url,
'crawl_title' => $info['title'] ?? '',
'crawl_brand' => $info['brand_name'] ?? '',
'crawl_brand_id' => $info['brand_id'] ?? 0,
'crawl_series' => $info['series_name'] ?? '',
'crawl_series_id' => $info['series_id'] ?? 0,
'crawl_year' => $info['year'] ?? '',
'crawl_msrp' => $info['new_price'] ?? '',
'crawl_car_id' => $info['car_id'] ?? 0,
2026-03-11 16:01:18 +08:00
'status' => 3, // 待推送状态
2026-03-11 15:01:54 +08:00
'images' => $imagesValue,
'power' => $info['horsepower'] ?? '',
];
// 4. 合并详细参数,过滤掉数组类型的值
$params = array_filter($params, function($value) {
return !is_array($value);
});
$data = array_merge($data, $params);
2026-03-12 14:21:12 +08:00
return $data;
// 5. 创建记录
/*$car = UsedCarDetail::create($data);
2026-03-11 15:01:54 +08:00
2026-03-12 14:21:12 +08:00
// 6. 将推送任务发送到队列
try {
\Webman\RedisQueue\Redis::send('wordpress-push', [
'car_id' => $car->id,
]);
Log::info('WordPress推送任务已加入队列', ['car_id' => $car->id]);
} catch (\Throwable $e) {
Log::error('WordPress推送任务加入队列失败', [
'car_id' => $car->id,
'error' => $e->getMessage(),
]);
}
return $car->toArray();*/
}
/**
* 保存懂车帝二手车数据
*
* @param string $url 懂车帝二手车链接
* @param int $userId 用户ID
* @return array 返回创建的车辆数据
* @throws \Exception
*/
public function crawlStore(array $data, int $userId): array
{
$data['user_id'] = $userId;
2026-03-11 15:01:54 +08:00
// 5. 创建记录
$car = UsedCarDetail::create($data);
2026-03-11 16:01:18 +08:00
// 6. 将推送任务发送到队列
try {
\Webman\RedisQueue\Redis::send('wordpress-push', [
'car_id' => $car->id,
]);
Log::info('WordPress推送任务已加入队列', ['car_id' => $car->id]);
} catch (\Throwable $e) {
Log::error('WordPress推送任务加入队列失败', [
'car_id' => $car->id,
'error' => $e->getMessage(),
]);
}
2026-03-11 15:01:54 +08:00
return $car->toArray();
}
/**
* 生成车辆编码
*/
private function generateCode(): string
{
2026-03-12 15:45:26 +08:00
return 'ST' . date('mdHis') . rand(1000, 9999);
2026-03-11 15:01:54 +08:00
}
/**
* 将价格(万)转换为元
*/
private function convertPriceToYuan($price): float
{
if (empty($price)) {
return 0;
}
// 如果是数字,直接乘以10000
if (is_numeric($price)) {
return floatval($price) * 10000;
}
// 如果是字符串,移除"万"字后乘以10000
$price = str_replace('万', '', $price);
return floatval($price) * 10000;
}
/**
* 将里程(万公里)转换为公里
*/
private function convertMileageToKm($mileage): float
{
if (empty($mileage)) {
return 0;
}
// 如果是数字,直接乘以10000
if (is_numeric($mileage)) {
return floatval($mileage) * 10000;
}
// 如果是字符串,移除"万公里"后乘以10000
$mileage = str_replace(['万公里', '万'], '', $mileage);
return floatval($mileage) * 10000;
}
2026-03-11 17:16:16 +08:00
/**
* 重新推送单辆失败的车到WordPress
*
* @param int $carId 车辆ID
* @return int 成功加入队列返回1,否则返回0
*/
public function retryPush(int $carId): int
{
$car = UsedCarDetail::where('id', $carId)
->where('status', 5) // 只处理推送失败的
->first();
if (!$car) {
return 0;
}
// 更新状态为待推送
$car->update(['status' => 3]);
// 加入队列
\Webman\RedisQueue\Redis::send('wordpress-push', ['car_id' => $carId]);
Log::info('车辆重新加入推送队列', ['car_id' => $carId]);
return 1;
}
2026-03-09 17:46:17 +08:00
}