|
|
@ -652,7 +652,19 @@ class DocumentTemplateService extends BaseAdminService |
|
|
|
|
|
|
|
|
// 准备填充数据 |
|
|
// 准备填充数据 |
|
|
$placeholderConfig = json_decode($template['placeholder_config'], true); |
|
|
$placeholderConfig = json_decode($template['placeholder_config'], true); |
|
|
|
|
|
// 检查是否传递了 use_direct_values 参数,如果是则直接使用传递的值 |
|
|
|
|
|
if (!empty($data['use_direct_values']) && $data['use_direct_values'] === true) { |
|
|
|
|
|
// 直接使用传递的 fill_data 作为填充值,不进行二次处理 |
|
|
|
|
|
$fillValues = $data['fill_data']; |
|
|
|
|
|
Log::info('使用直接填充模式', [ |
|
|
|
|
|
'template_id' => $data['template_id'], |
|
|
|
|
|
'fill_data_count' => count($fillValues), |
|
|
|
|
|
'fill_data_keys' => array_keys($fillValues) |
|
|
|
|
|
]); |
|
|
|
|
|
} else { |
|
|
|
|
|
// 使用原有的配置处理模式 |
|
|
$fillValues = $this->prepareFillData($placeholderConfig, $data['fill_data']); |
|
|
$fillValues = $this->prepareFillData($placeholderConfig, $data['fill_data']); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
// 生成文档 |
|
|
// 生成文档 |
|
|
$templatePath = public_path() . '/upload/' . $template['contract_template']; |
|
|
$templatePath = public_path() . '/upload/' . $template['contract_template']; |
|
|
@ -695,12 +707,14 @@ class DocumentTemplateService extends BaseAdminService |
|
|
throw new \Exception('无法创建临时文档存储目录,请检查系统权限'); |
|
|
throw new \Exception('无法创建临时文档存储目录,请检查系统权限'); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// 预处理:修复被格式化分割的占位符 |
|
|
|
|
|
$fixedTemplatePath = $this->fixBrokenPlaceholders($templatePath); |
|
|
|
|
|
|
|
|
// 使用 PhpWord 模板处理器 |
|
|
// 使用 PhpWord 模板处理器 |
|
|
$templateProcessor = new TemplateProcessor($templatePath); |
|
|
$templateProcessor = new TemplateProcessor($fixedTemplatePath); |
|
|
|
|
|
|
|
|
foreach ($fillValues as $placeholder => $value) { |
|
|
// 智能处理占位符,根据类型使用不同的处理方法 |
|
|
$templateProcessor->setValue($placeholder, $value); |
|
|
$this->processPlaceholders($templateProcessor, $fillValues, $placeholderConfig); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
$templateProcessor->saveAs($fullOutputPath); |
|
|
$templateProcessor->saveAs($fullOutputPath); |
|
|
|
|
|
|
|
|
@ -849,13 +863,37 @@ class DocumentTemplateService extends BaseAdminService |
|
|
$tableName = $config['table_name']; |
|
|
$tableName = $config['table_name']; |
|
|
$fieldName = $config['field_name']; |
|
|
$fieldName = $config['field_name']; |
|
|
|
|
|
|
|
|
// 简单的数据库查询(实际应用中需要更完善的查询逻辑) |
|
|
// 改进的数据库查询逻辑,支持条件查询 |
|
|
$model = \think\facade\Db::connect(); |
|
|
$model = \think\facade\Db::connect(); |
|
|
$result = $model->table($tableName)->field($fieldName)->find(); |
|
|
$query = $model->table($tableName); |
|
|
|
|
|
|
|
|
|
|
|
// 如果有传入的查询条件(比如学员ID),使用条件查询 |
|
|
|
|
|
if (!empty($userFillData) && is_array($userFillData)) { |
|
|
|
|
|
foreach ($userFillData as $key => $value) { |
|
|
|
|
|
// 支持常见的查询字段 |
|
|
|
|
|
if (in_array($key, ['id', 'student_id', 'user_id', 'person_id', 'contract_id'])) { |
|
|
|
|
|
$query->where($key, $value); |
|
|
|
|
|
break; // 只使用第一个匹配的条件 |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
$result = $query->field($fieldName)->find(); |
|
|
|
|
|
|
|
|
|
|
|
Log::info('数据库查询', [ |
|
|
|
|
|
'table' => $tableName, |
|
|
|
|
|
'field' => $fieldName, |
|
|
|
|
|
'result' => $result, |
|
|
|
|
|
'user_data' => $userFillData |
|
|
|
|
|
]); |
|
|
|
|
|
|
|
|
return $result[$fieldName] ?? $config['default_value'] ?? ''; |
|
|
return $result[$fieldName] ?? $config['default_value'] ?? ''; |
|
|
} catch (\Exception $e) { |
|
|
} catch (\Exception $e) { |
|
|
Log::error('数据库查询失败:' . $e->getMessage()); |
|
|
Log::error('数据库查询失败:' . $e->getMessage(), [ |
|
|
|
|
|
'table' => $config['table_name'] ?? '', |
|
|
|
|
|
'field' => $config['field_name'] ?? '', |
|
|
|
|
|
'config' => $config |
|
|
|
|
|
]); |
|
|
return $config['default_value'] ?? ''; |
|
|
return $config['default_value'] ?? ''; |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
@ -1042,6 +1080,130 @@ class DocumentTemplateService extends BaseAdminService |
|
|
return $this->pageQuery($searchModel); |
|
|
return $this->pageQuery($searchModel); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
|
* 修复被Word格式化分割的占位符 |
|
|
|
|
|
* Word在编辑时会在占位符中插入格式化标签,导致{{placeholder}}被分割 |
|
|
|
|
|
* 这个方法会创建一个修复后的临时文件 |
|
|
|
|
|
* |
|
|
|
|
|
* @param string $templatePath 原始模板文件路径 |
|
|
|
|
|
* @return string 修复后的模板文件路径 |
|
|
|
|
|
*/ |
|
|
|
|
|
private function fixBrokenPlaceholders(string $templatePath) |
|
|
|
|
|
{ |
|
|
|
|
|
try { |
|
|
|
|
|
// 创建临时文件 |
|
|
|
|
|
$tempDir = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'niucloud_templates'; |
|
|
|
|
|
if (!is_dir($tempDir)) { |
|
|
|
|
|
mkdir($tempDir, 0755, true); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
$fixedTemplatePath = $tempDir . DIRECTORY_SEPARATOR . 'fixed_' . basename($templatePath); |
|
|
|
|
|
|
|
|
|
|
|
// 复制原文件到临时位置 |
|
|
|
|
|
if (!copy($templatePath, $fixedTemplatePath)) { |
|
|
|
|
|
Log::warning('无法创建临时模板文件,使用原始模板', ['template_path' => $templatePath]); |
|
|
|
|
|
return $templatePath; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// 打开ZIP文件进行修复 |
|
|
|
|
|
$zip = new \ZipArchive(); |
|
|
|
|
|
|
|
|
|
|
|
if ($zip->open($fixedTemplatePath) === TRUE) { |
|
|
|
|
|
$content = $zip->getFromName('word/document.xml'); |
|
|
|
|
|
|
|
|
|
|
|
if ($content !== false) { |
|
|
|
|
|
$originalLength = strlen($content); |
|
|
|
|
|
$fixedContent = $content; |
|
|
|
|
|
|
|
|
|
|
|
// 第一步:修复跨XML标签的占位符(最常见的模式) |
|
|
|
|
|
// 模式1:修复 <w:t>{{</w:t>...<w:t>内容}}</w:t> 这种分割 |
|
|
|
|
|
$pattern1 = '/<w:t[^>]*>\{\{<\/w:t>([^<]*)<w:t[^>]*>([^<]*)\}\}<\/w:t>/'; |
|
|
|
|
|
$fixedContent = preg_replace_callback($pattern1, function($matches) { |
|
|
|
|
|
$placeholder = '{{' . $matches[1] . $matches[2] . '}}'; |
|
|
|
|
|
return '<w:t>' . $placeholder . '</w:t>'; |
|
|
|
|
|
}, $fixedContent); |
|
|
|
|
|
|
|
|
|
|
|
// 模式2:修复 <w:t>{</w:t>...<w:t>{内容}}</w:t> 这种分割 |
|
|
|
|
|
$pattern2 = '/<w:t[^>]*>\{<\/w:t>([^<]*)<w:t[^>]*>\{([^<]*)\}\}<\/w:t>/'; |
|
|
|
|
|
$fixedContent = preg_replace_callback($pattern2, function($matches) { |
|
|
|
|
|
$placeholder = '{{' . $matches[1] . $matches[2] . '}}'; |
|
|
|
|
|
return '<w:t>' . $placeholder . '</w:t>'; |
|
|
|
|
|
}, $fixedContent); |
|
|
|
|
|
|
|
|
|
|
|
// 模式3:修复三段式分割 <w:t>{{</w:t>...<w:t>中间</w:t>...<w:t>}}</w:t> |
|
|
|
|
|
$pattern3 = '/<w:t[^>]*>\{\{<\/w:t>([^<]*)<w:t[^>]*>([^<]*)<\/w:t>([^<]*)<w:t[^>]*>([^<]*)\}\}<\/w:t>/'; |
|
|
|
|
|
$fixedContent = preg_replace_callback($pattern3, function($matches) { |
|
|
|
|
|
$placeholder = '{{' . $matches[1] . $matches[2] . $matches[4] . '}}'; |
|
|
|
|
|
return '<w:t>' . $placeholder . '</w:t>'; |
|
|
|
|
|
}, $fixedContent); |
|
|
|
|
|
|
|
|
|
|
|
// 第二步:处理更复杂的格式化分割(包含rPr标签的) |
|
|
|
|
|
// 匹配包含格式化信息的分割模式 |
|
|
|
|
|
$complexPattern = '/\{\{[^}]*?(?:<\/w:t><\/w:r><w:r[^>]*?>(?:<w:rPr[^>]*?>.*?<\/w:rPr>)?<w:t[^>]*?>)[^}]*?\}\}/s'; |
|
|
|
|
|
$fixedContent = preg_replace_callback($complexPattern, function($matches) { |
|
|
|
|
|
$placeholder = $matches[0]; |
|
|
|
|
|
// 移除所有XML标签,保留纯文本 |
|
|
|
|
|
$cleaned = preg_replace('/<[^>]*?>/', '', $placeholder); |
|
|
|
|
|
// 验证是否为有效占位符 |
|
|
|
|
|
if (!preg_match('/^\{\{[^}]+\}\}$/', $cleaned)) { |
|
|
|
|
|
return $placeholder; |
|
|
|
|
|
} |
|
|
|
|
|
// 保持原有的第一个w:t标签结构 |
|
|
|
|
|
if (preg_match('/^([^<]*<w:t[^>]*>)/', $placeholder, $tagMatch)) { |
|
|
|
|
|
return $tagMatch[1] . $cleaned . '</w:t>'; |
|
|
|
|
|
} |
|
|
|
|
|
return $cleaned; |
|
|
|
|
|
}, $fixedContent); |
|
|
|
|
|
|
|
|
|
|
|
// 第三步:修复任何剩余的基本分割模式 |
|
|
|
|
|
$basicPattern = '/\{\{[^}]*?<[^>]*?>[^}]*?\}\}/'; |
|
|
|
|
|
$fixedContent = preg_replace_callback($basicPattern, function($matches) { |
|
|
|
|
|
$placeholder = $matches[0]; |
|
|
|
|
|
$cleaned = preg_replace('/<[^>]*?>/', '', $placeholder); |
|
|
|
|
|
if (!preg_match('/^\{\{[^}]+\}\}$/', $cleaned)) { |
|
|
|
|
|
return $placeholder; |
|
|
|
|
|
} |
|
|
|
|
|
return $cleaned; |
|
|
|
|
|
}, $fixedContent); |
|
|
|
|
|
|
|
|
|
|
|
// 将修复后的内容写回ZIP文件 |
|
|
|
|
|
if ($zip->addFromString('word/document.xml', $fixedContent)) { |
|
|
|
|
|
$zip->close(); |
|
|
|
|
|
|
|
|
|
|
|
Log::info('占位符修复完成', [ |
|
|
|
|
|
'template_file' => $templatePath, |
|
|
|
|
|
'fixed_file' => $fixedTemplatePath, |
|
|
|
|
|
'original_length' => $originalLength, |
|
|
|
|
|
'fixed_length' => strlen($fixedContent), |
|
|
|
|
|
'size_change' => strlen($fixedContent) - $originalLength |
|
|
|
|
|
]); |
|
|
|
|
|
|
|
|
|
|
|
return $fixedTemplatePath; |
|
|
|
|
|
} else { |
|
|
|
|
|
$zip->close(); |
|
|
|
|
|
Log::warning('无法写入修复后的内容,使用原始模板', ['template_path' => $templatePath]); |
|
|
|
|
|
return $templatePath; |
|
|
|
|
|
} |
|
|
|
|
|
} else { |
|
|
|
|
|
$zip->close(); |
|
|
|
|
|
Log::warning('无法读取document.xml,使用原始模板', ['template_path' => $templatePath]); |
|
|
|
|
|
return $templatePath; |
|
|
|
|
|
} |
|
|
|
|
|
} else { |
|
|
|
|
|
Log::warning('无法打开Word文档,使用原始模板', ['template_path' => $templatePath]); |
|
|
|
|
|
return $templatePath; |
|
|
|
|
|
} |
|
|
|
|
|
} catch (\Exception $e) { |
|
|
|
|
|
Log::error('修复占位符失败', [ |
|
|
|
|
|
'template_path' => $templatePath, |
|
|
|
|
|
'error' => $e->getMessage(), |
|
|
|
|
|
'trace' => $e->getTraceAsString() |
|
|
|
|
|
]); |
|
|
|
|
|
// 修复失败不影响主流程,返回原始模板路径 |
|
|
|
|
|
return $templatePath; |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
/** |
|
|
/** |
|
|
* 格式化文件大小 |
|
|
* 格式化文件大小 |
|
|
* @param int $size |
|
|
* @param int $size |
|
|
@ -1431,4 +1593,545 @@ class DocumentTemplateService extends BaseAdminService |
|
|
throw new \Exception('模板文档更新失败:' . $e->getMessage()); |
|
|
throw new \Exception('模板文档更新失败:' . $e->getMessage()); |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
|
* 使用XML字符串操作生成Word文档 |
|
|
|
|
|
* 此方法直接读取Word模板的XML内容,进行字符串替换,然后保存为新的Word文档 |
|
|
|
|
|
* @param array $data |
|
|
|
|
|
* @return array |
|
|
|
|
|
* @throws \Exception |
|
|
|
|
|
*/ |
|
|
|
|
|
public function generateDocumentByXmlString(array $data) |
|
|
|
|
|
{ |
|
|
|
|
|
$template = $this->contractModel->find($data['template_id']); |
|
|
|
|
|
if (!$template) { |
|
|
|
|
|
throw new \Exception('模板不存在'); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
if (empty($template['placeholder_config'])) { |
|
|
|
|
|
throw new \Exception('模板尚未配置占位符'); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// 创建生成记录 |
|
|
|
|
|
$logData = [ |
|
|
|
|
|
'site_id' => $this->site_id, |
|
|
|
|
|
'template_id' => $data['template_id'], |
|
|
|
|
|
'user_id' => $this->uid, |
|
|
|
|
|
'user_type' => 1, |
|
|
|
|
|
'fill_data' => json_encode($data['fill_data']), |
|
|
|
|
|
'status' => 'pending', |
|
|
|
|
|
'completed_at' => date('Y-m-d H:i:s') |
|
|
|
|
|
]; |
|
|
|
|
|
|
|
|
|
|
|
$log = $this->logModel->create($logData); |
|
|
|
|
|
|
|
|
|
|
|
try { |
|
|
|
|
|
// 更新状态为处理中 |
|
|
|
|
|
$log->status = 'processing'; |
|
|
|
|
|
$log->process_start_time = date('Y-m-d H:i:s'); |
|
|
|
|
|
$log->save(); |
|
|
|
|
|
|
|
|
|
|
|
// 准备填充数据 |
|
|
|
|
|
$placeholderConfig = json_decode($template['placeholder_config'], true); |
|
|
|
|
|
if (!empty($data['use_direct_values']) && $data['use_direct_values'] === true) { |
|
|
|
|
|
$fillValues = $data['fill_data']; |
|
|
|
|
|
Log::info('使用直接填充模式(XML字符串方法)', [ |
|
|
|
|
|
'template_id' => $data['template_id'], |
|
|
|
|
|
'fill_data_count' => count($fillValues), |
|
|
|
|
|
'fill_data_keys' => array_keys($fillValues) |
|
|
|
|
|
]); |
|
|
|
|
|
} else { |
|
|
|
|
|
$fillValues = $this->prepareFillData($placeholderConfig, $data['fill_data']); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// 原始模板路径 |
|
|
|
|
|
$templatePath = public_path() . '/upload/' . $template['contract_template']; |
|
|
|
|
|
$outputFileName = $data['output_filename'] ?: ($template['contract_name'] . '_' . date('YmdHis') . '.docx'); |
|
|
|
|
|
|
|
|
|
|
|
// 生成输出路径 |
|
|
|
|
|
$outputPath = 'generated_documents/' . date('Y/m/') . $outputFileName; |
|
|
|
|
|
$publicOutputPath = public_path() . '/upload/' . $outputPath; |
|
|
|
|
|
$publicOutputDir = dirname($publicOutputPath); |
|
|
|
|
|
|
|
|
|
|
|
// 确保输出目录存在 |
|
|
|
|
|
if (!is_dir($publicOutputDir)) { |
|
|
|
|
|
if (!mkdir($publicOutputDir, 0755, true) && !is_dir($publicOutputDir)) { |
|
|
|
|
|
throw new \Exception('无法创建输出目录:' . $publicOutputDir); |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// 使用XML字符串方法生成文档 |
|
|
|
|
|
$success = $this->processWordDocumentXml($templatePath, $publicOutputPath, $fillValues); |
|
|
|
|
|
|
|
|
|
|
|
if (!$success) { |
|
|
|
|
|
throw new \Exception('XML字符串处理失败'); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// 更新生成记录 |
|
|
|
|
|
$log->status = 'completed'; |
|
|
|
|
|
$log->generated_file_path = $outputPath; |
|
|
|
|
|
$log->generated_file_name = $outputFileName; |
|
|
|
|
|
$log->process_end_time = date('Y-m-d H:i:s'); |
|
|
|
|
|
$log->save(); |
|
|
|
|
|
|
|
|
|
|
|
Log::info('XML字符串方法生成Word文档成功', [ |
|
|
|
|
|
'template_id' => $data['template_id'], |
|
|
|
|
|
'output_path' => $outputPath, |
|
|
|
|
|
'fill_values_count' => count($fillValues) |
|
|
|
|
|
]); |
|
|
|
|
|
|
|
|
|
|
|
return [ |
|
|
|
|
|
'log_id' => $log->id, |
|
|
|
|
|
'file_path' => $outputPath, |
|
|
|
|
|
'file_name' => $outputFileName, |
|
|
|
|
|
'download_url' => url('/upload/' . $outputPath) |
|
|
|
|
|
]; |
|
|
|
|
|
|
|
|
|
|
|
} catch (\Exception $e) { |
|
|
|
|
|
// 更新记录为失败状态 |
|
|
|
|
|
$log->status = 'failed'; |
|
|
|
|
|
$log->error_msg = $e->getMessage(); |
|
|
|
|
|
$log->process_end_time = date('Y-m-d H:i:s'); |
|
|
|
|
|
$log->save(); |
|
|
|
|
|
|
|
|
|
|
|
Log::error('XML字符串方法生成Word文档失败', [ |
|
|
|
|
|
'template_id' => $data['template_id'], |
|
|
|
|
|
'error' => $e->getMessage(), |
|
|
|
|
|
'trace' => $e->getTraceAsString() |
|
|
|
|
|
]); |
|
|
|
|
|
|
|
|
|
|
|
throw new \Exception('文档生成失败:' . $e->getMessage()); |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
|
* 处理Word文档的XML内容进行占位符替换 |
|
|
|
|
|
* @param string $templatePath 模板文件路径 |
|
|
|
|
|
* @param string $outputPath 输出文件路径 |
|
|
|
|
|
* @param array $fillValues 填充值数组 |
|
|
|
|
|
* @return bool |
|
|
|
|
|
* @throws \Exception |
|
|
|
|
|
*/ |
|
|
|
|
|
private function processWordDocumentXml(string $templatePath, string $outputPath, array $fillValues): bool |
|
|
|
|
|
{ |
|
|
|
|
|
try { |
|
|
|
|
|
// 检查模板文件是否存在 |
|
|
|
|
|
if (!file_exists($templatePath)) { |
|
|
|
|
|
throw new \Exception('模板文件不存在:' . $templatePath); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// 创建临时工作目录 |
|
|
|
|
|
$tempDir = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'word_processing_' . uniqid(); |
|
|
|
|
|
if (!mkdir($tempDir, 0755, true)) { |
|
|
|
|
|
throw new \Exception('无法创建临时工作目录'); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
try { |
|
|
|
|
|
// 1. 解压Word文档到临时目录 |
|
|
|
|
|
$zip = new \ZipArchive(); |
|
|
|
|
|
$result = $zip->open($templatePath); |
|
|
|
|
|
|
|
|
|
|
|
if ($result !== TRUE) { |
|
|
|
|
|
throw new \Exception('无法打开Word模板文件,错误代码:' . $result); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
if (!$zip->extractTo($tempDir)) { |
|
|
|
|
|
$zip->close(); |
|
|
|
|
|
throw new \Exception('无法解压Word模板文件'); |
|
|
|
|
|
} |
|
|
|
|
|
$zip->close(); |
|
|
|
|
|
|
|
|
|
|
|
// 2. 读取document.xml文件 |
|
|
|
|
|
$documentXmlPath = $tempDir . DIRECTORY_SEPARATOR . 'word' . DIRECTORY_SEPARATOR . 'document.xml'; |
|
|
|
|
|
if (!file_exists($documentXmlPath)) { |
|
|
|
|
|
throw new \Exception('Word文档缺少document.xml文件'); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
$xmlContent = file_get_contents($documentXmlPath); |
|
|
|
|
|
if ($xmlContent === false) { |
|
|
|
|
|
throw new \Exception('无法读取document.xml文件内容'); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
Log::info('读取XML内容成功', [ |
|
|
|
|
|
'original_length' => strlen($xmlContent), |
|
|
|
|
|
'placeholders_to_replace' => array_keys($fillValues) |
|
|
|
|
|
]); |
|
|
|
|
|
|
|
|
|
|
|
// 3. 进行占位符替换 |
|
|
|
|
|
$modifiedXmlContent = $this->replaceXmlPlaceholders($xmlContent, $fillValues); |
|
|
|
|
|
|
|
|
|
|
|
Log::info('XML占位符替换完成', [ |
|
|
|
|
|
'original_length' => strlen($xmlContent), |
|
|
|
|
|
'modified_length' => strlen($modifiedXmlContent), |
|
|
|
|
|
'size_change' => strlen($modifiedXmlContent) - strlen($xmlContent) |
|
|
|
|
|
]); |
|
|
|
|
|
|
|
|
|
|
|
// 4. 写回修改后的document.xml |
|
|
|
|
|
if (file_put_contents($documentXmlPath, $modifiedXmlContent) === false) { |
|
|
|
|
|
throw new \Exception('无法写入修改后的document.xml'); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// 5. 重新打包为Word文档 |
|
|
|
|
|
$newZip = new \ZipArchive(); |
|
|
|
|
|
$result = $newZip->open($outputPath, \ZipArchive::CREATE | \ZipArchive::OVERWRITE); |
|
|
|
|
|
|
|
|
|
|
|
if ($result !== TRUE) { |
|
|
|
|
|
throw new \Exception('无法创建输出Word文件,错误代码:' . $result); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// 递归添加所有文件到ZIP |
|
|
|
|
|
$this->addDirectoryToZip($newZip, $tempDir, ''); |
|
|
|
|
|
|
|
|
|
|
|
if (!$newZip->close()) { |
|
|
|
|
|
throw new \Exception('无法保存输出Word文件'); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
Log::info('Word文档重新打包成功', [ |
|
|
|
|
|
'output_path' => $outputPath, |
|
|
|
|
|
'file_size' => filesize($outputPath) |
|
|
|
|
|
]); |
|
|
|
|
|
|
|
|
|
|
|
return true; |
|
|
|
|
|
|
|
|
|
|
|
} finally { |
|
|
|
|
|
// 清理临时目录 |
|
|
|
|
|
$this->removeDirectory($tempDir); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
} catch (\Exception $e) { |
|
|
|
|
|
Log::error('处理Word文档XML失败', [ |
|
|
|
|
|
'template_path' => $templatePath, |
|
|
|
|
|
'output_path' => $outputPath, |
|
|
|
|
|
'error' => $e->getMessage(), |
|
|
|
|
|
'trace' => $e->getTraceAsString() |
|
|
|
|
|
]); |
|
|
|
|
|
throw $e; |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
|
* 在XML内容中替换占位符 |
|
|
|
|
|
* @param string $xmlContent 原始XML内容 |
|
|
|
|
|
* @param array $fillValues 替换值数组 |
|
|
|
|
|
* @return string 替换后的XML内容 |
|
|
|
|
|
*/ |
|
|
|
|
|
private function replaceXmlPlaceholders(string $xmlContent, array $fillValues): string |
|
|
|
|
|
{ |
|
|
|
|
|
$modifiedContent = $xmlContent; |
|
|
|
|
|
$replacementCount = 0; |
|
|
|
|
|
|
|
|
|
|
|
foreach ($fillValues as $placeholder => $value) { |
|
|
|
|
|
// 确保占位符格式正确 |
|
|
|
|
|
$searchPattern = '{{' . $placeholder . '}}'; |
|
|
|
|
|
|
|
|
|
|
|
// 转义特殊字符,确保安全的XML替换 |
|
|
|
|
|
$safeValue = htmlspecialchars((string)$value, ENT_XML1, 'UTF-8'); |
|
|
|
|
|
|
|
|
|
|
|
// 进行替换 |
|
|
|
|
|
$beforeLength = strlen($modifiedContent); |
|
|
|
|
|
$modifiedContent = str_replace($searchPattern, $safeValue, $modifiedContent, $count); |
|
|
|
|
|
$afterLength = strlen($modifiedContent); |
|
|
|
|
|
|
|
|
|
|
|
if ($count > 0) { |
|
|
|
|
|
$replacementCount += $count; |
|
|
|
|
|
Log::info('占位符替换成功', [ |
|
|
|
|
|
'placeholder' => $placeholder, |
|
|
|
|
|
'value' => $value, |
|
|
|
|
|
'safe_value' => $safeValue, |
|
|
|
|
|
'replacement_count' => $count, |
|
|
|
|
|
'content_length_change' => $afterLength - $beforeLength |
|
|
|
|
|
]); |
|
|
|
|
|
} else { |
|
|
|
|
|
Log::warning('占位符未找到', [ |
|
|
|
|
|
'placeholder' => $placeholder, |
|
|
|
|
|
'search_pattern' => $searchPattern |
|
|
|
|
|
]); |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
Log::info('所有占位符处理完成', [ |
|
|
|
|
|
'total_replacements' => $replacementCount, |
|
|
|
|
|
'processed_placeholders' => count($fillValues) |
|
|
|
|
|
]); |
|
|
|
|
|
|
|
|
|
|
|
return $modifiedContent; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
|
* 递归添加目录到ZIP文件 |
|
|
|
|
|
* @param \ZipArchive $zip ZIP文件对象 |
|
|
|
|
|
* @param string $sourcePath 源目录路径 |
|
|
|
|
|
* @param string $relativePath ZIP内的相对路径 |
|
|
|
|
|
* @return void |
|
|
|
|
|
*/ |
|
|
|
|
|
private function addDirectoryToZip(\ZipArchive $zip, string $sourcePath, string $relativePath): void |
|
|
|
|
|
{ |
|
|
|
|
|
$iterator = new \RecursiveIteratorIterator( |
|
|
|
|
|
new \RecursiveDirectoryIterator($sourcePath, \RecursiveDirectoryIterator::SKIP_DOTS), |
|
|
|
|
|
\RecursiveIteratorIterator::SELF_FIRST |
|
|
|
|
|
); |
|
|
|
|
|
|
|
|
|
|
|
foreach ($iterator as $file) { |
|
|
|
|
|
$filePath = $file->getRealPath(); |
|
|
|
|
|
$relativeName = $relativePath . substr($filePath, strlen($sourcePath) + 1); |
|
|
|
|
|
|
|
|
|
|
|
// 在Windows系统中统一使用正斜杠 |
|
|
|
|
|
$relativeName = str_replace('\\', '/', $relativeName); |
|
|
|
|
|
|
|
|
|
|
|
if ($file->isDir()) { |
|
|
|
|
|
// 添加目录 |
|
|
|
|
|
$zip->addEmptyDir($relativeName); |
|
|
|
|
|
} else { |
|
|
|
|
|
// 添加文件 |
|
|
|
|
|
$zip->addFile($filePath, $relativeName); |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
|
* 递归删除目录 |
|
|
|
|
|
* @param string $dir 要删除的目录路径 |
|
|
|
|
|
* @return void |
|
|
|
|
|
*/ |
|
|
|
|
|
private function removeDirectory(string $dir): void |
|
|
|
|
|
{ |
|
|
|
|
|
if (!is_dir($dir)) { |
|
|
|
|
|
return; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
$files = array_diff(scandir($dir), ['.', '..']); |
|
|
|
|
|
foreach ($files as $file) { |
|
|
|
|
|
$filePath = $dir . DIRECTORY_SEPARATOR . $file; |
|
|
|
|
|
if (is_dir($filePath)) { |
|
|
|
|
|
$this->removeDirectory($filePath); |
|
|
|
|
|
} else { |
|
|
|
|
|
unlink($filePath); |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
rmdir($dir); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
|
* 智能处理占位符,根据类型使用不同的处理方法 |
|
|
|
|
|
* @param TemplateProcessor $templateProcessor |
|
|
|
|
|
* @param array $fillValues |
|
|
|
|
|
* @param array $placeholderConfig |
|
|
|
|
|
* @return void |
|
|
|
|
|
*/ |
|
|
|
|
|
private function processPlaceholders(TemplateProcessor $templateProcessor, array $fillValues, array $placeholderConfig) |
|
|
|
|
|
{ |
|
|
|
|
|
// 如果没有配置信息,使用简单的文本替换 |
|
|
|
|
|
if (empty($placeholderConfig)) { |
|
|
|
|
|
foreach ($fillValues as $placeholder => $value) { |
|
|
|
|
|
$templateProcessor->setValue($placeholder, $value); |
|
|
|
|
|
} |
|
|
|
|
|
return; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
$placeholderConfig = is_string($placeholderConfig) ? json_decode($placeholderConfig, true) : $placeholderConfig; |
|
|
|
|
|
|
|
|
|
|
|
foreach ($fillValues as $placeholder => $value) { |
|
|
|
|
|
$config = $placeholderConfig[$placeholder] ?? []; |
|
|
|
|
|
$fieldType = $config['field_type'] ?? 'text'; |
|
|
|
|
|
$dataType = $config['data_type'] ?? 'user_input'; |
|
|
|
|
|
|
|
|
|
|
|
Log::info('处理占位符', [ |
|
|
|
|
|
'placeholder' => $placeholder, |
|
|
|
|
|
'value' => $value, |
|
|
|
|
|
'field_type' => $fieldType, |
|
|
|
|
|
'data_type' => $dataType |
|
|
|
|
|
]); |
|
|
|
|
|
|
|
|
|
|
|
try { |
|
|
|
|
|
// 根据字段类型选择处理方式 |
|
|
|
|
|
if ($fieldType === 'image' || $dataType === 'sign_img' || $dataType === 'signature') { |
|
|
|
|
|
// 处理图片类型 |
|
|
|
|
|
$this->setImageValue($templateProcessor, $placeholder, $value); |
|
|
|
|
|
} else { |
|
|
|
|
|
// 处理文本类型 |
|
|
|
|
|
$templateProcessor->setValue($placeholder, $value); |
|
|
|
|
|
} |
|
|
|
|
|
} catch (\Exception $e) { |
|
|
|
|
|
Log::error('占位符处理失败', [ |
|
|
|
|
|
'placeholder' => $placeholder, |
|
|
|
|
|
'value' => $value, |
|
|
|
|
|
'error' => $e->getMessage() |
|
|
|
|
|
]); |
|
|
|
|
|
// 如果图片处理失败,尝试作为文本处理 |
|
|
|
|
|
$templateProcessor->setValue($placeholder, $value); |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
|
* 设置图片占位符的值 |
|
|
|
|
|
* @param TemplateProcessor $templateProcessor |
|
|
|
|
|
* @param string $placeholder |
|
|
|
|
|
* @param mixed $value |
|
|
|
|
|
* @return void |
|
|
|
|
|
*/ |
|
|
|
|
|
private function setImageValue(TemplateProcessor $templateProcessor, string $placeholder, $value) |
|
|
|
|
|
{ |
|
|
|
|
|
if (empty($value)) { |
|
|
|
|
|
// 如果值为空,设置为空文本 |
|
|
|
|
|
$templateProcessor->setValue($placeholder, ''); |
|
|
|
|
|
return; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
$imagePath = null; |
|
|
|
|
|
|
|
|
|
|
|
try { |
|
|
|
|
|
// 判断图片数据类型并处理 |
|
|
|
|
|
if (is_string($value)) { |
|
|
|
|
|
if (str_starts_with($value, 'data:image/')) { |
|
|
|
|
|
// 处理base64图片数据 |
|
|
|
|
|
$imagePath = $this->saveBase64Image($value); |
|
|
|
|
|
} elseif (str_starts_with($value, 'http://') || str_starts_with($value, 'https://')) { |
|
|
|
|
|
// 处理网络图片URL |
|
|
|
|
|
$imagePath = $this->downloadImage($value); |
|
|
|
|
|
} elseif (file_exists($value)) { |
|
|
|
|
|
// 处理本地文件路径 |
|
|
|
|
|
$imagePath = $value; |
|
|
|
|
|
} elseif (file_exists(public_path() . '/' . ltrim($value, '/'))) { |
|
|
|
|
|
// 处理相对路径 |
|
|
|
|
|
$imagePath = public_path() . '/' . ltrim($value, '/'); |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
if ($imagePath && file_exists($imagePath)) { |
|
|
|
|
|
// 验证图片文件 |
|
|
|
|
|
$imageInfo = getimagesize($imagePath); |
|
|
|
|
|
if ($imageInfo === false) { |
|
|
|
|
|
throw new \Exception('无效的图片文件'); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// 设置图片,限制尺寸 |
|
|
|
|
|
$templateProcessor->setImageValue($placeholder, [ |
|
|
|
|
|
'path' => $imagePath, |
|
|
|
|
|
'width' => 100, // 可以根据需要调整 |
|
|
|
|
|
'height' => 100, |
|
|
|
|
|
'ratio' => true |
|
|
|
|
|
]); |
|
|
|
|
|
|
|
|
|
|
|
Log::info('图片占位符设置成功', [ |
|
|
|
|
|
'placeholder' => $placeholder, |
|
|
|
|
|
'image_path' => $imagePath, |
|
|
|
|
|
'image_size' => $imageInfo |
|
|
|
|
|
]); |
|
|
|
|
|
|
|
|
|
|
|
// 如果是临时文件,标记稍后删除 |
|
|
|
|
|
if (str_contains($imagePath, sys_get_temp_dir())) { |
|
|
|
|
|
register_shutdown_function(function() use ($imagePath) { |
|
|
|
|
|
if (file_exists($imagePath)) { |
|
|
|
|
|
@unlink($imagePath); |
|
|
|
|
|
} |
|
|
|
|
|
}); |
|
|
|
|
|
} |
|
|
|
|
|
} else { |
|
|
|
|
|
// 如果无法处理为图片,使用文本替换 |
|
|
|
|
|
$templateProcessor->setValue($placeholder, $value); |
|
|
|
|
|
Log::warning('图片处理失败,使用文本替换', [ |
|
|
|
|
|
'placeholder' => $placeholder, |
|
|
|
|
|
'value' => $value |
|
|
|
|
|
]); |
|
|
|
|
|
} |
|
|
|
|
|
} catch (\Exception $e) { |
|
|
|
|
|
Log::error('图片设置失败', [ |
|
|
|
|
|
'placeholder' => $placeholder, |
|
|
|
|
|
'value' => $value, |
|
|
|
|
|
'error' => $e->getMessage() |
|
|
|
|
|
]); |
|
|
|
|
|
// 如果图片处理失败,使用文本替换 |
|
|
|
|
|
$templateProcessor->setValue($placeholder, $value); |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
|
* 保存base64图片数据到临时文件 |
|
|
|
|
|
* @param string $base64Data |
|
|
|
|
|
* @return string|null |
|
|
|
|
|
*/ |
|
|
|
|
|
private function saveBase64Image(string $base64Data): ?string |
|
|
|
|
|
{ |
|
|
|
|
|
try { |
|
|
|
|
|
// 解析base64数据 |
|
|
|
|
|
if (preg_match('/^data:image\/(\w+);base64,(.+)$/', $base64Data, $matches)) { |
|
|
|
|
|
$imageType = $matches[1]; |
|
|
|
|
|
$imageData = base64_decode($matches[2]); |
|
|
|
|
|
|
|
|
|
|
|
if ($imageData === false) { |
|
|
|
|
|
throw new \Exception('base64解码失败'); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// 创建临时文件 |
|
|
|
|
|
$tempDir = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'niucloud_images'; |
|
|
|
|
|
if (!is_dir($tempDir)) { |
|
|
|
|
|
mkdir($tempDir, 0755, true); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
$fileName = uniqid('img_') . '.' . $imageType; |
|
|
|
|
|
$filePath = $tempDir . DIRECTORY_SEPARATOR . $fileName; |
|
|
|
|
|
|
|
|
|
|
|
if (file_put_contents($filePath, $imageData) === false) { |
|
|
|
|
|
throw new \Exception('保存图片文件失败'); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
return $filePath; |
|
|
|
|
|
} |
|
|
|
|
|
} catch (\Exception $e) { |
|
|
|
|
|
Log::error('保存base64图片失败:' . $e->getMessage()); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
return null; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
|
* 下载网络图片到临时文件 |
|
|
|
|
|
* @param string $url |
|
|
|
|
|
* @return string|null |
|
|
|
|
|
*/ |
|
|
|
|
|
private function downloadImage(string $url): ?string |
|
|
|
|
|
{ |
|
|
|
|
|
try { |
|
|
|
|
|
// 创建临时文件 |
|
|
|
|
|
$tempDir = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'niucloud_images'; |
|
|
|
|
|
if (!is_dir($tempDir)) { |
|
|
|
|
|
mkdir($tempDir, 0755, true); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
$pathInfo = pathinfo(parse_url($url, PHP_URL_PATH)); |
|
|
|
|
|
$extension = $pathInfo['extension'] ?? 'jpg'; |
|
|
|
|
|
$fileName = uniqid('img_') . '.' . $extension; |
|
|
|
|
|
$filePath = $tempDir . DIRECTORY_SEPARATOR . $fileName; |
|
|
|
|
|
|
|
|
|
|
|
// 下载图片 |
|
|
|
|
|
$context = stream_context_create([ |
|
|
|
|
|
'http' => [ |
|
|
|
|
|
'timeout' => 30, |
|
|
|
|
|
'user_agent' => 'Mozilla/5.0 (compatible; Document Generator)', |
|
|
|
|
|
] |
|
|
|
|
|
]); |
|
|
|
|
|
|
|
|
|
|
|
$imageData = file_get_contents($url, false, $context); |
|
|
|
|
|
if ($imageData === false) { |
|
|
|
|
|
throw new \Exception('下载图片失败'); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
if (file_put_contents($filePath, $imageData) === false) { |
|
|
|
|
|
throw new \Exception('保存图片文件失败'); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// 验证是否为有效图片 |
|
|
|
|
|
if (getimagesize($filePath) === false) { |
|
|
|
|
|
unlink($filePath); |
|
|
|
|
|
throw new \Exception('下载的文件不是有效图片'); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
return $filePath; |
|
|
|
|
|
} catch (\Exception $e) { |
|
|
|
|
|
Log::error('下载图片失败:' . $e->getMessage(), ['url' => $url]); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
return null; |
|
|
|
|
|
} |
|
|
} |
|
|
} |