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': // 默认只显示审核通过的数据 $config['where']['status'] = 1; // 审核状态筛选 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']); $params['official_guide_price'] = str_replace('万','',$params['official_guide_price']); // 合并抓取的数据,手动提交的字段优先级更高 $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]); // 审核通过后自动推送到WordPress $pushResult = ['success' => true, 'message' => '']; if ($auditStatus == 1) { $pushResult = $this->pushToWordpressAfterAudit($carId); // 推送失败,将车辆状态修改为3 if (!$pushResult['success']) { UsedCarDetail::where('id', $carId)->update(['status' => 3]); } } // 返回审核结果和推送结果 return [ 'audit_success' => true, 'push_success' => $pushResult['success'], 'push_message' => $pushResult['message'], 'message' => $pushResult['success'] ? '审核通过并推送成功' : '审核通过,但推送失败:' . $pushResult['message'], ]; } /** * 审核通过后推送车辆到WordPress * @return array ['success' => bool, 'message' => string] */ private function pushToWordpressAfterAudit(int $carId): array { try { // 获取启用的WordPress站点 $site = \app\common\model\WordpressSite::where('status', 1)->first(); if (!$site) { $msg = '没有启用的WordPress站点'; Log::warning('WordPress推送失败:' . $msg, ['car_id' => $carId]); return ['success' => false, 'message' => $msg]; } // 检查是否已推送过 $existing = \app\common\model\WordpressUsedCar::where('wp_site_id', $site->id) ->where('used_car_id', $carId) ->where('status', 1) ->first(); if ($existing) { $msg = '车辆已推送,跳过'; Log::info('WordPress推送:' . $msg, ['car_id' => $carId]); return ['success' => true, 'message' => $msg]; } // 调用推送服务 $wpService = new \app\common\service\WordpressSiteService(); $wpService->createProductForNewSite($site, $carId); Log::info('WordPress推送成功', ['car_id' => $carId, 'site_id' => $site->id]); return ['success' => true, 'message' => '']; } catch (\Throwable $e) { $msg = $e->getMessage(); Log::error('WordPress推送失败', [ 'car_id' => $carId, 'error' => $msg, 'trace' => $e->getTraceAsString(), ]); return ['success' => false, 'message' => $msg]; } } /** * 获取审核记录列表 */ 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) { 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', $item->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 直接获取的字段 $result['official_guide_price'] = $carInfo['official_price'] ?? ''; // 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 ''; } }