|
|
<?php
|
|
|
|
|
|
namespace App\Services;
|
|
|
|
|
|
use Illuminate\Support\Facades\DB;
|
|
|
use LightnCandy\LightnCandy;
|
|
|
use Illuminate\Support\Facades\Storage;
|
|
|
|
|
|
/**
|
|
|
* 打印模板服务
|
|
|
*/
|
|
|
class PrintTemplateService
|
|
|
{
|
|
|
/**
|
|
|
* 根据ID获取模板
|
|
|
*/
|
|
|
public function getTemplateById(int $id)
|
|
|
{
|
|
|
return DB::table('print_templates')
|
|
|
->where('id', $id)
|
|
|
->where('status', 1)
|
|
|
->first();
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* 获取模板
|
|
|
*/
|
|
|
public function getTemplate(string $templateCode)
|
|
|
{
|
|
|
return DB::table('print_templates')
|
|
|
->where('template_code', $templateCode)
|
|
|
->where('status', 1)
|
|
|
->first();
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* 获取默认模板
|
|
|
*/
|
|
|
public function getDefaultTemplate(string $templateType)
|
|
|
{
|
|
|
return DB::table('print_templates')
|
|
|
->where('template_type', $templateType)
|
|
|
->where('is_default', 1)
|
|
|
->where('status', 1)
|
|
|
->first();
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* 获取模板列表(仅返回基本字段)
|
|
|
*/
|
|
|
public function getTemplateList(?string $templateType = null)
|
|
|
{
|
|
|
$query = DB::table('print_templates')
|
|
|
->where('status', 1)
|
|
|
->select([
|
|
|
'id',
|
|
|
'template_code',
|
|
|
'template_name',
|
|
|
'template_type',
|
|
|
'is_default',
|
|
|
'status',
|
|
|
'created_at',
|
|
|
'updated_at'
|
|
|
]);
|
|
|
|
|
|
if ($templateType) {
|
|
|
$query->where('template_type', $templateType);
|
|
|
}
|
|
|
|
|
|
return $query
|
|
|
->orderBy('created_at', 'desc')
|
|
|
->get();
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* 渲染模板(使用 LightnCandy)
|
|
|
*/
|
|
|
public function renderTemplate(string $templateCode, array $data): string
|
|
|
{
|
|
|
$template = $this->getTemplate($templateCode);
|
|
|
|
|
|
if (!$template) {
|
|
|
throw new \Exception("模板不存在:{$templateCode}");
|
|
|
}
|
|
|
|
|
|
// 编译 Handlebars 模板为 PHP 代码
|
|
|
$phpCode = LightnCandy::compile($template->html_template, [
|
|
|
'flags' => LightnCandy::FLAG_ERROR_EXCEPTION |
|
|
|
LightnCandy::FLAG_HANDLEBARSJS |
|
|
|
LightnCandy::FLAG_RUNTIMEPARTIAL |
|
|
|
LightnCandy::FLAG_NOESCAPE |
|
|
|
LightnCandy::FLAG_BESTPERFORMANCE |
|
|
|
LightnCandy::FLAG_SPVARS,
|
|
|
'helpers' => $this->getHelpers(),
|
|
|
]);
|
|
|
|
|
|
// 执行编译后的 PHP 代码
|
|
|
$renderer = eval($phpCode);
|
|
|
$html = $renderer($data);
|
|
|
|
|
|
// 注入 CSS 样式
|
|
|
if ($template->css_style) {
|
|
|
$html = $this->injectStyles($html, $template->css_style);
|
|
|
}
|
|
|
|
|
|
return $html;
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* 获取自定义 Helper
|
|
|
*/
|
|
|
private function getHelpers(): array
|
|
|
{
|
|
|
return [
|
|
|
// 日期格式化
|
|
|
'formatDate' => function($args) {
|
|
|
$value = $args[0] ?? '';
|
|
|
$format = $args[1] ?? 'Y-m-d H:i:s';
|
|
|
if (!$value) return '';
|
|
|
return date($format, strtotime($value));
|
|
|
},
|
|
|
// 金额格式化
|
|
|
'formatMoney' => function($args) {
|
|
|
return number_format($args[0] ?? 0, 2);
|
|
|
},
|
|
|
// 等于 - Block Helper(支持 {{#eq}}...{{else}}...{{/eq}})
|
|
|
'eq' => function($a, $b, $options) {
|
|
|
$isEqual = strval($a) === strval($b);
|
|
|
|
|
|
if (is_array($options) && isset($options['fn'])) {
|
|
|
// Block helper 模式
|
|
|
return $isEqual ? $options['fn']() : (isset($options['inverse']) ? $options['inverse']() : '');
|
|
|
}
|
|
|
// 简单模式(子表达式)
|
|
|
return $isEqual ? '1' : '';
|
|
|
},
|
|
|
// 不等于 - Block Helper
|
|
|
'ne' => function($a, $b, $options) {
|
|
|
$notEqual = strval($a) !== strval($b);
|
|
|
|
|
|
if ($options && isset($options['fn'])) {
|
|
|
return $notEqual ? $options['fn']() : (isset($options['inverse']) ? $options['inverse']() : '');
|
|
|
}
|
|
|
return $notEqual ? '1' : '';
|
|
|
},
|
|
|
// 大于 - Block Helper
|
|
|
'gt' => function($a, $b, $options) {
|
|
|
$result = $a > $b;
|
|
|
if ($options && isset($options['fn'])) {
|
|
|
return $result ? $options['fn']() : (isset($options['inverse']) ? $options['inverse']() : '');
|
|
|
}
|
|
|
return $result ? '1' : '';
|
|
|
},
|
|
|
// 小于 - Block Helper
|
|
|
'lt' => function($a, $b, $options) {
|
|
|
$result = $a < $b;
|
|
|
if ($options && isset($options['fn'])) {
|
|
|
return $result ? $options['fn']() : (isset($options['inverse']) ? $options['inverse']() : '');
|
|
|
}
|
|
|
return $result ? '1' : '';
|
|
|
},
|
|
|
// 大于等于 - Block Helper
|
|
|
'gte' => function($a, $b, $options) {
|
|
|
$result = $a >= $b;
|
|
|
if ($options && isset($options['fn'])) {
|
|
|
return $result ? $options['fn']() : (isset($options['inverse']) ? $options['inverse']() : '');
|
|
|
}
|
|
|
return $result ? '1' : '';
|
|
|
},
|
|
|
// 小于等于 - Block Helper
|
|
|
'lte' => function($a, $b, $options) {
|
|
|
$result = $a <= $b;
|
|
|
if ($options && isset($options['fn'])) {
|
|
|
return $result ? $options['fn']() : (isset($options['inverse']) ? $options['inverse']() : '');
|
|
|
}
|
|
|
return $result ? '1' : '';
|
|
|
},
|
|
|
// 返回值的 helper(用于子表达式 {{#if (eqVal ...)}})
|
|
|
'eqVal' => function($args) {
|
|
|
$a = $args[0] ?? null;
|
|
|
$b = $args[1] ?? null;
|
|
|
return strval($a) === strval($b);
|
|
|
},
|
|
|
'neVal' => function($args) {
|
|
|
$a = $args[0] ?? null;
|
|
|
$b = $args[1] ?? null;
|
|
|
return strval($a) !== strval($b);
|
|
|
},
|
|
|
'gtVal' => function($args) {
|
|
|
return ($args[0] ?? 0) > ($args[1] ?? 0);
|
|
|
},
|
|
|
'ltVal' => function($args) {
|
|
|
return ($args[0] ?? 0) < ($args[1] ?? 0);
|
|
|
},
|
|
|
'gteVal' => function($args) {
|
|
|
return ($args[0] ?? 0) >= ($args[1] ?? 0);
|
|
|
},
|
|
|
'lteVal' => function($args) {
|
|
|
return ($args[0] ?? 0) <= ($args[1] ?? 0);
|
|
|
},
|
|
|
// 数组长度
|
|
|
'length' => function($args) {
|
|
|
return is_array($args[0]) ? count($args[0]) : 0;
|
|
|
},
|
|
|
// 加
|
|
|
'add' => function($args) {
|
|
|
return ($args[0] ?? 0) + ($args[1] ?? 0);
|
|
|
},
|
|
|
// 减
|
|
|
'sub' => function($args) {
|
|
|
return ($args[0] ?? 0) - ($args[1] ?? 0);
|
|
|
},
|
|
|
// 默认值
|
|
|
'default' => function($args) {
|
|
|
return $args[0] ?? $args[1] ?? '';
|
|
|
},
|
|
|
];
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* 注入样式
|
|
|
*/
|
|
|
private function injectStyles(string $html, string $css): string
|
|
|
{
|
|
|
$styleTag = sprintf('<style type="text/css">%s</style>', $css);
|
|
|
|
|
|
// 尝试插入到 <head> 中
|
|
|
if (preg_match('/<head\b[^>]*>/i', $html, $matches)) {
|
|
|
return str_replace($matches[0], $matches[0] . $styleTag, $html);
|
|
|
}
|
|
|
|
|
|
// 如果没有 head 标签,插入到 <html> 之后
|
|
|
if (preg_match('/<html\b[^>]*>/i', $html, $matches)) {
|
|
|
return str_replace($matches[0], $matches[0] . $styleTag, $html);
|
|
|
}
|
|
|
|
|
|
// 都没有,插入到最前面
|
|
|
return $styleTag . $html;
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* 替换条形码占位符
|
|
|
*/
|
|
|
private function replaceBarcodes(string $html): string
|
|
|
{
|
|
|
$generator = new \Picqer\Barcode\BarcodeGeneratorHTML();
|
|
|
|
|
|
// 匹配 <svg data-barcode="..."> 格式的条形码占位符
|
|
|
return preg_replace_callback(
|
|
|
'/<svg\s+data-barcode=["\']([^"\']+)["\'](?:\s+data-format=["\']([^"\']+)["\'])?(?:\s+data-width=["\']([^"\']+)["\'])?(?:\s+data-height=["\']([^"\']+)["\'])?\s*><\/svg>/i',
|
|
|
function ($matches) use ($generator) {
|
|
|
$code = $matches[1];
|
|
|
$format = $matches[2] ?? 'CODE128';
|
|
|
$width = intval($matches[3] ?? 2);
|
|
|
$height = intval($matches[4] ?? 50);
|
|
|
|
|
|
// 映射格式名称
|
|
|
$formatMap = [
|
|
|
'CODE128' => $generator::TYPE_CODE_128,
|
|
|
'CODE39' => $generator::TYPE_CODE_39,
|
|
|
'EAN13' => $generator::TYPE_EAN_13,
|
|
|
'UPC_A' => $generator::TYPE_UPC_A,
|
|
|
];
|
|
|
|
|
|
$type = $formatMap[$format] ?? $generator::TYPE_CODE_128;
|
|
|
|
|
|
// 生成条形码 HTML
|
|
|
$barcodeHtml = $generator->getBarcode($code, $type, $width, $height);
|
|
|
|
|
|
// 包装条形码,在下方添加编号显示
|
|
|
return sprintf(
|
|
|
'<div style="display:inline-block;">%s<div style="text-align:center;font-size:11px;margin-top:2px;">%s</div></div>',
|
|
|
$barcodeHtml,
|
|
|
htmlspecialchars($code)
|
|
|
);
|
|
|
},
|
|
|
$html
|
|
|
);
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* 生成 PDF(使用 SnappyPdf)
|
|
|
*/
|
|
|
public function generatePDF(string $templateCode, array $data, ?string $filePath = null)
|
|
|
{
|
|
|
$html = $this->renderTemplate($templateCode, $data);
|
|
|
|
|
|
// 替换条形码占位符
|
|
|
$html = $this->replaceBarcodes($html);
|
|
|
|
|
|
// 确保 HTML 是有效的 UTF-8
|
|
|
$html = mb_convert_encoding($html, 'UTF-8', 'UTF-8,GBK,GB2312');
|
|
|
|
|
|
// 获取模板的页面设置
|
|
|
$template = $this->getTemplate($templateCode);
|
|
|
$pageSettings = $template->page_settings ? json_decode($template->page_settings, true) : [];
|
|
|
|
|
|
$pdf = \Barryvdh\Snappy\Facades\SnappyPdf::loadHTML($html)
|
|
|
->setOption('encoding', 'UTF-8')
|
|
|
->setOption('enable-local-file-access', true)
|
|
|
->setOption('no-outline', true)
|
|
|
->setOption('disable-smart-shrinking', true);
|
|
|
|
|
|
// 应用页面设置
|
|
|
if (!empty($pageSettings['paper_size'])) {
|
|
|
$pdf->setOption('page-size', $pageSettings['paper_size']);
|
|
|
}
|
|
|
if (!empty($pageSettings['orientation'])) {
|
|
|
$pdf->setOption('orientation', $pageSettings['orientation']);
|
|
|
}
|
|
|
if (!empty($pageSettings['margin_top'])) {
|
|
|
$pdf->setOption('margin-top', $pageSettings['margin_top']);
|
|
|
}
|
|
|
if (!empty($pageSettings['margin_bottom'])) {
|
|
|
$pdf->setOption('margin-bottom', $pageSettings['margin_bottom']);
|
|
|
}
|
|
|
if (!empty($pageSettings['margin_left'])) {
|
|
|
$pdf->setOption('margin-left', $pageSettings['margin_left']);
|
|
|
}
|
|
|
if (!empty($pageSettings['margin_right'])) {
|
|
|
$pdf->setOption('margin-right', $pageSettings['margin_right']);
|
|
|
}
|
|
|
|
|
|
if ($filePath) {
|
|
|
// 确保目录存在
|
|
|
$directory = dirname($filePath);
|
|
|
if (!file_exists($directory)) {
|
|
|
mkdir($directory, 0755, true);
|
|
|
}
|
|
|
return $pdf->save($filePath);
|
|
|
}
|
|
|
|
|
|
return $pdf;
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* 验证模板语法
|
|
|
*/
|
|
|
public function validateTemplate(string $htmlTemplate): array
|
|
|
{
|
|
|
try {
|
|
|
LightnCandy::compile($htmlTemplate, [
|
|
|
'flags' => LightnCandy::FLAG_ERROR_EXCEPTION,
|
|
|
]);
|
|
|
return ['valid' => true, 'message' => '模板语法正确'];
|
|
|
} catch (\Exception $e) {
|
|
|
return ['valid' => false, 'message' => $e->getMessage()];
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* 保存模板
|
|
|
*/
|
|
|
public function saveTemplate(array $data): int
|
|
|
{
|
|
|
$templateId = $data['id'] ?? null;
|
|
|
|
|
|
$templateData = [
|
|
|
'template_code' => $data['template_code'],
|
|
|
'template_name' => $data['template_name'],
|
|
|
'template_type' => $data['template_type'],
|
|
|
'html_template' => $data['html_template'],
|
|
|
'css_style' => $data['css_style'] ?? '',
|
|
|
'data_mapping' => isset($data['data_mapping']) ? json_encode($data['data_mapping']) : null,
|
|
|
'page_settings' => isset($data['page_settings']) ? json_encode($data['page_settings']) : null,
|
|
|
'example_json' => $data['example_json'] ?? '',
|
|
|
'is_default' => $data['is_default'] ?? false,
|
|
|
'status' => $data['status'] ?? true,
|
|
|
'updated_at' => now(),
|
|
|
];
|
|
|
|
|
|
if ($templateId) {
|
|
|
// 更新
|
|
|
DB::table('print_templates')->where('id', $templateId)->update($templateData);
|
|
|
return $templateId;
|
|
|
} else {
|
|
|
// 新增
|
|
|
$templateData['created_at'] = now();
|
|
|
return DB::table('print_templates')->insertGetId($templateData);
|
|
|
}
|
|
|
}
|
|
|
}
|