Files
useds/app/common/service/UsedCarDetailService.php
T
2026-03-16 13:07:28 +08:00

1066 lines
36 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\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;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\GuzzleException;
use QL\QueryList;
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'])) {
$data['images'] = implode(',', $data['images']);
}
} 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'])) {
$data['intro_images'] = implode(',', $data['intro_images']);
}
} 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('该车源已收录');
}
}
// 如果提供了 crawl_car_id,自动抓取懂车帝车辆参数
if (!empty($data['crawl_car_id'])) {
try {
$params = $this->crawlParams($data['crawl_car_id']);
if(str_contains($params['official_guide_price'],'万')){
$params['official_guide_price'] = str_replace('万','',$params['official_guide_price'])*10000;
}
// 合并抓取的数据,手动提交的字段优先级更高
$data = array_merge($params, $data);
} catch (\Throwable $e) {
// 抓取失败不阻断创建,记录日志
Log::warning('抓取车辆参数失败', [
'crawl_car_id' => $data['crawl_car_id'],
'error' => $e->getMessage(),
]);
}
}
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,
]);
// 更新车辆状态
$result = UsedCarDetail::where('id', $carId)->update(['status' => $auditStatus]);
// 审核通过后,将推送任务发送到队列
$pushQueued = false;
if ($auditStatus == 1) {
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;
}
}
// 返回审核结果
return [
'audit_success' => true,
'push_queued' => $pushQueued,
'message' => $pushQueued ? '审核通过,WordPress推送任务已加入队列' : '审核通过',
];
}
/**
* 获取审核记录列表
*/
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) {
if(!is_numeric($item->created_at)){
$created_at = $item->created_at->timestamp;
}else{
$created_at = $item->created_at;
}
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 ?? '',
'created_at' => date('Y-m-d H:i:s', $created_at),
];
});
}
/**
* 获取审核统计数据
*/
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(),
];
}
/**
* 抓取懂车帝车辆参数
*
* @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 = [];
// 从 car_info 直接获取的字段,清理掉"万"等字符
$officialPrice = $carInfo['official_price'] ?? '';
$result['official_guide_price'] = $this->convertPriceToYuan($officialPrice);
// 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 '';
}
/**
* 抓取懂车帝二手车数据并保存
*
* @param string $url 懂车帝二手车链接
* @param int $userId 用户ID
* @return array 返回创建的车辆数据
* @throws \Exception
*/
public function crawl(string $url, int $userId): array
{
// 检查该用户是否已抓取过此URL的车辆
$existingCar = UsedCarDetail::where('user_id', $userId)
->where('source_url', $url)
->first();
if ($existingCar) {
throw new \Exception('该车辆信息已存在,无需重复抓取');
}
// 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,
'status' => 3, // 待推送状态
'images' => $imagesValue,
'power' => $info['horsepower'] ?? '',
];
// 4. 合并详细参数,过滤掉数组类型的值
$params = array_filter($params, function($value) {
return !is_array($value);
});
$data = array_merge($data, $params);
return $data;
// 5. 创建记录
/*$car = UsedCarDetail::create($data);
// 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;
// 5. 创建记录
$car = UsedCarDetail::create($data);
// 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();
}
/**
* 生成车辆编码
*/
private function generateCode(): string
{
return 'ST' . date('mdHis') . rand(1000, 9999);
}
/**
* 将价格(万)转换为元
*/
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;
}
/**
* 重新推送单辆失败的车到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;
}
}