'中文', 'en' => '英语', 'ru' => '俄语', 'es' => '西班牙语', 'fr' => '法语', ]; /** * 货币符号映射 */ private const CURRENCIES = [ 'CNY' => '¥', 'USD' => '$', 'RUB' => '₽', 'AED' => 'د.إ', 'EUR' => '€', ]; /** * 生成二手车PDF * * @param int $usedCarId 二手车ID * @param int $userId 用户ID * @param float $price 价格(用于PDF显示) * @param string $lang 语言代码 * @param string $currency 货币代码 * @param string $tradeTerm 贸易条款 * @return string PDF文件名 * @throws \Exception */ public function generateUsedCarPdf(int $usedCarId, int $userId, float $price, string $lang = 'zh', string $currency = 'CNY', string $tradeTerm = ''): string { // 验证参数 if (!isset(self::LANGUAGES[$lang])) { throw new \Exception('不支持的语言: ' . $lang); } if (!isset(self::CURRENCIES[$currency])) { throw new \Exception('不支持的货币: ' . $currency); } // 获取车辆信息 $car = UsedCar::with('user')->findOrFail($usedCarId); // 检查权限 if ($car->user_id !== $userId) { throw new \Exception('无权生成此车辆的PDF'); } // 获取用户信息 $user = $car->user ?? User::findOrFail($userId); // 准备PDF数据 $pdfData = $this->preparePdfData($car, $price, $lang, $currency, $tradeTerm, $user); // 生成PDF $pdfFileName = $this->renderPdf($pdfData, $car, $user, $lang); // 更新车辆记录 $car->pdf_src = $pdfFileName; $car->save(); // 记录PDF生成日志 UsedCarPdfLog::create([ 'user_id' => $userId, 'used_car_id' => $usedCarId, 'price_snapshot' => (int)($price * 100), // 转换为分 'pdf_path' => $pdfFileName, ]); // 清理临时文件 $this->cleanupTempFiles(); return $pdfFileName; } /** * 准备PDF数据 */ private function preparePdfData(UsedCar $car, float $price, string $lang, string $currency, string $tradeTerm, User $user): array { // 获取字段翻译 $field = $this->getFieldTranslations($lang); $field['currency'] = self::CURRENCIES[$currency]; // 获取名片翻译 $contactField = $this->getContactTranslations($lang); // 构建车辆名称 $productName = trim(($car->crawl_brand ?? '') . ' ' . ($car->crawl_series ?? '') . ' ' . ($car->crawl_year ?? '') . '款 ' . ($car->crawl_title ?? $car->product_name)); // 处理图片 - 下载到本地 $images = []; $imageDir = runtime_path('pdf/images/'); if (!is_dir($imageDir)) { mkdir($imageDir, 0755, true); } if (!empty($car->images)) { $imageUrls = explode(',', $car->images); $crawlService = new UsedCarCrawlService(); $downloadedImages = $crawlService->downloadImages($imageUrls, $imageDir); foreach ($downloadedImages as $localPath) { if ($localPath && file_exists($localPath)) { $images[] = $localPath; } } } // 处理附件PDF $attachments = []; if (!empty($car->attachment)) { $attachmentData = json_decode($car->attachment, true); if (is_array($attachmentData)) { $attachmentDir = runtime_path('pdf/attachment/'); if (!is_dir($attachmentDir)) { mkdir($attachmentDir, 0755, true); } foreach ($attachmentData as $attachment) { $src = $attachment['src'] ?? ''; if (str_ends_with(strtolower($src), '.pdf')) { // 下载PDF附件 $localPath = $this->downloadAttachment($src, $attachmentDir); if ($localPath && file_exists($localPath)) { $attachments[] = $localPath; } } } } } // 处理社交二维码 $socialCode = ''; if (!empty($user->social_code)) { $socialCodePath = $this->downloadSocialCode($user->social_code); if ($socialCodePath && file_exists($socialCodePath)) { $socialCode = $socialCodePath; } } return [ 'car' => [ 'product_name' => $productName, 'price' => $price, 'trade' => $tradeTerm, 'plate_time' => $car->plate_time ?? '', 'displacement' => $car->displacement ?? '', 'mileage' => $car->mileage ?? 0, 'power' => $car->power ?? '', 'body_color' => $car->body_color ?? '', 'condition_desc' => $car->condition_desc ?? '', 'images' => $images, 'attachment' => $attachments, 'created_at' => $car->created_at ? $car->created_at->format('Y-m-d H:i:s') : '', ], 'user' => [ 'name' => $user->name ?? '', 'name_en' => $user->name_en ?? $user->name ?? '', 'email' => $user->email ?? '', 'phone' => $user->phone ?? '', 'whatsapp' => $user->whatsapp ?? '', 'social' => !empty($user->social_code), 'social_code' => $socialCode, ], 'field' => $field, 'contact_field' => $contactField, 'lang' => $lang, ]; } /** * 渲染PDF */ private function renderPdf(array $pdfData, UsedCar $car, User $user, string $lang): string { // 创建临时目录 $tempDir = runtime_path('pdf/temp/'); if (!is_dir($tempDir)) { mkdir($tempDir, 0755, true); } // 初始化mPDF $mpdf = new Mpdf([ 'mode' => 'utf-8', 'format' => 'A4-L', // 横向 'margin_top' => 10, 'margin_bottom' => 10, 'margin_left' => 10, 'margin_right' => 10, 'tempDir' => $tempDir, 'default_font' => 'sun-exta', ]); // 设置水印 $watermarkText = $pdfData['user']['name_en'] ?? $pdfData['user']['name']; if (!empty($watermarkText)) { $mpdf->SetWatermarkText($watermarkText, 0.08); $mpdf->showWatermarkText = true; } // 渲染车辆详情页面 ob_start(); $carTpl = runtime_path('pdf/templates/usedCar.stub'); extract([ 'car' => $pdfData['car'], 'field' => $pdfData['field'], ]); include $carTpl; $carHtml = ob_get_clean(); $mpdf->WriteHTML($carHtml); // 追加附件PDF foreach ($pdfData['car']['attachment'] as $attachmentPdf) { $this->appendPdf($mpdf, $attachmentPdf); } // 添加名片页面 $mpdf->AddPage(); ob_start(); $cardTpl = runtime_path('pdf/templates/contactCard.stub'); extract([ 'user' => $pdfData['user'], 'contactField' => $pdfData['contact_field'], ]); include $cardTpl; $cardHtml = ob_get_clean(); $mpdf->WriteHTML($cardHtml); // 生成PDF文件 $pdfDir = runtime_path('pdf/'); if (!is_dir($pdfDir)) { mkdir($pdfDir, 0755, true); } $pdfFileName = 'usedcar-' . $car->code . '-' . $car->id . '-' . $lang . '-' . substr(time(), -6) . '.pdf'; $pdfPath = $pdfDir . '/' . $pdfFileName; $mpdf->Output($pdfPath, \Mpdf\Output\Destination::FILE); return $pdfFileName; } /** * 追加PDF到现有文档 */ private function appendPdf(Mpdf $mpdf, string $pdfPath): void { if (!file_exists($pdfPath)) { return; } try { $pageCount = $mpdf->SetSourceFile($pdfPath); for ($i = 1; $i <= $pageCount; $i++) { $tplId = $mpdf->ImportPage($i); $size = $mpdf->getTemplateSize($tplId); $mpdf->AddPageByArray([ 'orientation' => $size['orientation'], 'sheet-size' => [$size['width'], $size['height']] ]); $mpdf->UseTemplate($tplId); } } catch (\Exception $e) { error_log('追加PDF失败: ' . $e->getMessage()); } } /** * 下载附件 */ private function downloadAttachment(string $url, string $saveDir): ?string { $filename = md5($url) . '.pdf'; $savePath = $saveDir . $filename; if (file_exists($savePath)) { return $savePath; } try { $content = file_get_contents($url); if ($content !== false) { file_put_contents($savePath, $content); return $savePath; } } catch (\Exception $e) { error_log('下载附件失败: ' . $e->getMessage()); } return null; } /** * 下载社交二维码 */ private function downloadSocialCode(string $url): ?string { $saveDir = runtime_path('pdf/qrcode/'); if (!is_dir($saveDir)) { mkdir($saveDir, 0755, true); } $filename = md5($url) . '.jpg'; $savePath = $saveDir . $filename; if (file_exists($savePath)) { return $savePath; } try { $content = file_get_contents($url); if ($content !== false) { file_put_contents($savePath, $content); return $savePath; } } catch (\Exception $e) { error_log('下载二维码失败: ' . $e->getMessage()); } return null; } /** * 清理临时文件 */ private function cleanupTempFiles(): void { // 清理图片 $imageDir = runtime_path('pdf/images/'); if (is_dir($imageDir)) { array_map('unlink', glob($imageDir . '*.jpg') ?: []); array_map('unlink', glob($imageDir . '*.png') ?: []); } // 清理附件 $attachmentDir = runtime_path('pdf/attachment/'); if (is_dir($attachmentDir)) { array_map('unlink', glob($attachmentDir . '*.pdf') ?: []); } // 清理二维码 $qrcodeDir = runtime_path('pdf/qrcode/'); if (is_dir($qrcodeDir)) { array_map('unlink', glob($qrcodeDir . '*.jpg') ?: []); array_map('unlink', glob($qrcodeDir . '*.png') ?: []); } } /** * 获取字段翻译 */ private function getFieldTranslations(string $lang): array { $translations = [ 'zh' => [ 'price' => '车价', 'source_place' => '车源地', 'plate_time' => '上牌时间', 'displacement' => '排量', 'mileage' => '里程', 'power' => '马力', 'body_color' => '车身颜色', 'created_at' => '创建时间', 'condition_desc' => '车况介绍', 'images' => '产品图', 'trade' => '贸易条款', ], 'en' => [ 'price' => 'Price', 'source_place' => 'Vehicle Location', 'plate_time' => 'Registration Date', 'displacement' => 'Engine Displacement', 'mileage' => 'Mileage', 'power' => 'Horsepower', 'body_color' => 'Body Color', 'created_at' => 'Created At', 'condition_desc' => 'Vehicle Condition', 'images' => 'Product Image', 'trade' => 'Trade Terms', ], 'ru' => [ 'price' => 'Цена', 'source_place' => 'Местонахождение автомобиля', 'plate_time' => 'Дата регистрации', 'displacement' => 'Объём двигателя', 'mileage' => 'Пробег', 'power' => 'Мощность', 'body_color' => 'Цвет кузова', 'created_at' => 'Дата создания', 'condition_desc' => 'Состояние автомобиля', 'images' => 'Фотографии', 'trade' => 'Условия торговли', ], 'es' => [ 'price' => 'Precio', 'source_place' => 'Ubicación del vehículo', 'plate_time' => 'Fecha de matriculación', 'displacement' => 'Cilindrada', 'mileage' => 'Kilometraje', 'power' => 'Potencia', 'body_color' => 'Color de la carrocería', 'created_at' => 'Fecha de creación', 'condition_desc' => 'Estado del vehículo', 'images' => 'Imagen del producto', 'trade' => 'Condiciones de comercio', ], 'fr' => [ 'price' => 'Prix', 'source_place' => 'Emplacement du véhicule', 'plate_time' => 'Date d\'immatriculation', 'displacement' => 'Cylindrée', 'mileage' => 'Kilométrage', 'power' => 'Puissance', 'body_color' => 'Couleur de la carrosserie', 'created_at' => 'Date de création', 'condition_desc' => 'État du véhicule', 'images' => 'Image du produit', 'trade' => 'Conditions commerciales', ], ]; return $translations[$lang] ?? $translations['zh']; } /** * 获取名片翻译 */ private function getContactTranslations(string $lang): array { $translations = [ 'zh' => [ 'title' => '联系人名片', 'name' => '姓名', 'email' => '邮箱', 'phone' => '电话', 'code' => '二维码', 'left' => '扫码即可联系本人', 'right' => '本资料随PDF自动生成', ], 'en' => [ 'title' => 'Contact Name Card', 'name' => 'Name', 'email' => 'Email', 'phone' => 'Phone', 'code' => 'QR Code', 'left' => 'Scan the code to contact me', 'right' => 'This information is automatically generated with the PDF', ], 'ru' => [ 'title' => 'Контактная карточка', 'name' => 'Имя', 'email' => 'Электронная почта', 'phone' => 'Телефон', 'code' => 'QR-код', 'left' => 'Код позволяет связаться с самим собой.', 'right' => 'Эта информация автоматически генерируется с помощью PDF', ], 'es' => [ 'title' => 'Tarjeta de visita de contacto', 'name' => 'Nombre', 'email' => 'Correo electrónico', 'phone' => 'Teléfono', 'code' => 'Código QR', 'left' => 'Escanee el código para contactarme.', 'right' => 'Esta información se genera automáticamente con PDF', ], 'fr' => [ 'title' => 'Carte de visite de contact', 'name' => 'Nom', 'email' => 'E-mail', 'phone' => 'Téléphone', 'code' => 'Code QR', 'left' => 'Scannez le code pour me contacter', 'right' => 'Ces informations sont générées automatiquement avec le PDF', ], ]; return $translations[$lang] ?? $translations['zh']; } /** * 获取PDF文件路径 */ public function getPdfPath(string $pdfFileName): string { return runtime_path('pdf/' . $pdfFileName); } /** * 检查PDF文件是否存在 */ public function pdfExists(string $pdfFileName): bool { return file_exists($this->getPdfPath($pdfFileName)); } }