Files
x-server-mobile/backend/app/service/PdfService.php
T
2026-03-06 13:51:25 +08:00

522 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();
// 保存车辆信息用于文件名(extract会覆盖$car变量)
$carCode = $car->code;
$carId = $car->id;
$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-' . $carCode . '-' . $carId . '-' . $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));
}
}