519 lines
17 KiB
PHP
519 lines
17 KiB
PHP
<?php
|
|
|
|
namespace app\Service;
|
|
|
|
use app\Model\UsedCar;
|
|
use app\Model\UsedCarPdfLog;
|
|
use app\Model\User;
|
|
use Mpdf\Mpdf;
|
|
|
|
/**
|
|
* PDF生成服务
|
|
* 用于生成二手车相关的PDF文档
|
|
*/
|
|
class PdfService
|
|
{
|
|
/**
|
|
* 语言映射
|
|
*/
|
|
private const LANGUAGES = [
|
|
'zh' => '中文',
|
|
'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));
|
|
}
|
|
}
|