diff --git a/niucloud/app/api/controller/apiController/Contract.php b/niucloud/app/api/controller/apiController/Contract.php index 846c0112..b8eede19 100644 --- a/niucloud/app/api/controller/apiController/Contract.php +++ b/niucloud/app/api/controller/apiController/Contract.php @@ -488,15 +488,23 @@ class Contract extends BaseApiService $fillData[$placeholder] = $defaultValue; } - // 调用DocumentTemplateService生成Word文档 + // 调试信息:记录填充数据 + Log::info('准备的填充数据', [ + 'contract_sign_id' => $contract_sign_id, + 'fill_data_count' => count($fillData), + 'fill_data' => $fillData + ]); + + // 调用DocumentTemplateService的新XML字符串方法生成Word文档 $documentService = new \app\service\admin\document\DocumentTemplateService(); $generateData = [ 'template_id' => $contractSign['contract_id'], 'fill_data' => $fillData, - 'output_filename' => $contract['contract_name'] . '_' . $contractSign['student_id'] . '_' . date('YmdHis') . '.docx' + 'output_filename' => $contract['contract_name'] . '_' . $contractSign['student_id'] . '_' . date('YmdHis') . '.docx', + 'use_direct_values' => true // 直接使用已处理的值,不进行二次处理 ]; - $result = $documentService->generateDocument($generateData); + $result = $documentService->generateDocumentByXmlString($generateData); // 更新school_contract_sign表的sign_file字段和status字段 $updateResult = Db::table('school_contract_sign') diff --git a/niucloud/app/common.php b/niucloud/app/common.php index 40e351be..d7b89f99 100644 --- a/niucloud/app/common.php +++ b/niucloud/app/common.php @@ -1672,6 +1672,33 @@ function get_current_week() return date('W'); } +/** + * 获取当前年份(别名函数,兼容配置中的函数名) + * @return string + */ +function current_year() +{ + return get_current_year(); +} + +/** + * 获取当前月份(别名函数,兼容配置中的函数名) + * @return string + */ +function current_month() +{ + return get_current_month(); +} + +/** + * 获取当前日(别名函数,兼容配置中的函数名) + * @return string + */ +function current_day() +{ + return get_current_day(); +} + /** * 获取当前季度 * @return string diff --git a/niucloud/app/service/admin/document/DocumentTemplateService.php b/niucloud/app/service/admin/document/DocumentTemplateService.php index 1b73c87e..ee2adc90 100644 --- a/niucloud/app/service/admin/document/DocumentTemplateService.php +++ b/niucloud/app/service/admin/document/DocumentTemplateService.php @@ -652,7 +652,19 @@ class DocumentTemplateService extends BaseAdminService // 准备填充数据 $placeholderConfig = json_decode($template['placeholder_config'], true); - $fillValues = $this->prepareFillData($placeholderConfig, $data['fill_data']); + // 检查是否传递了 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']); + } // 生成文档 $templatePath = public_path() . '/upload/' . $template['contract_template']; @@ -695,12 +707,14 @@ class DocumentTemplateService extends BaseAdminService throw new \Exception('无法创建临时文档存储目录,请检查系统权限'); } + // 预处理:修复被格式化分割的占位符 + $fixedTemplatePath = $this->fixBrokenPlaceholders($templatePath); + // 使用 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); @@ -849,13 +863,37 @@ class DocumentTemplateService extends BaseAdminService $tableName = $config['table_name']; $fieldName = $config['field_name']; - // 简单的数据库查询(实际应用中需要更完善的查询逻辑) + // 改进的数据库查询逻辑,支持条件查询 $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'] ?? ''; } 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'] ?? ''; } } @@ -1042,6 +1080,130 @@ class DocumentTemplateService extends BaseAdminService 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:修复 {{...内容}} 这种分割 + $pattern1 = '/]*>\{\{<\/w:t>([^<]*)]*>([^<]*)\}\}<\/w:t>/'; + $fixedContent = preg_replace_callback($pattern1, function($matches) { + $placeholder = '{{' . $matches[1] . $matches[2] . '}}'; + return '' . $placeholder . ''; + }, $fixedContent); + + // 模式2:修复 {...{内容}} 这种分割 + $pattern2 = '/]*>\{<\/w:t>([^<]*)]*>\{([^<]*)\}\}<\/w:t>/'; + $fixedContent = preg_replace_callback($pattern2, function($matches) { + $placeholder = '{{' . $matches[1] . $matches[2] . '}}'; + return '' . $placeholder . ''; + }, $fixedContent); + + // 模式3:修复三段式分割 {{...中间...}} + $pattern3 = '/]*>\{\{<\/w:t>([^<]*)]*>([^<]*)<\/w:t>([^<]*)]*>([^<]*)\}\}<\/w:t>/'; + $fixedContent = preg_replace_callback($pattern3, function($matches) { + $placeholder = '{{' . $matches[1] . $matches[2] . $matches[4] . '}}'; + return '' . $placeholder . ''; + }, $fixedContent); + + // 第二步:处理更复杂的格式化分割(包含rPr标签的) + // 匹配包含格式化信息的分割模式 + $complexPattern = '/\{\{[^}]*?(?:<\/w:t><\/w:r>]*?>(?:]*?>.*?<\/w:rPr>)?]*?>)[^}]*?\}\}/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('/^([^<]*]*>)/', $placeholder, $tagMatch)) { + return $tagMatch[1] . $cleaned . ''; + } + 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 @@ -1431,4 +1593,545 @@ class DocumentTemplateService extends BaseAdminService 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; + } } \ No newline at end of file diff --git a/niucloud/app/service/api/apiService/ContractSignFormService.php b/niucloud/app/service/api/apiService/ContractSignFormService.php index 6797ab41..05ad9bc6 100644 --- a/niucloud/app/service/api/apiService/ContractSignFormService.php +++ b/niucloud/app/service/api/apiService/ContractSignFormService.php @@ -636,14 +636,43 @@ class ContractSignFormService extends BaseApiService private function getSignSpecificFormFields($contract_sign_id, $student) { try { - // 从document_data_source_config表获取该签署关系专属的字段配置 + // 先获取签署记录以得到合同ID + $sign_record = Db::table('school_contract_sign') + ->where('id', $contract_sign_id) + ->find(); + + if (!$sign_record) { + Log::error('签署记录不存在', ['contract_sign_id' => $contract_sign_id]); + return []; + } + + $contract_id = $sign_record['contract_id']; + + // 优先获取该签署关系专属的字段配置 $configs = Db::table('school_document_data_source_config') ->where('contract_sign_id', $contract_sign_id) ->select() ->toArray(); + // 如果该签署记录没有专属配置,则获取合同模板的基础配置 if (empty($configs)) { - Log::warning('该签署记录无字段配置', ['contract_sign_id' => $contract_sign_id]); + Log::info('该签署记录无专属配置,使用合同模板基础配置', [ + 'contract_sign_id' => $contract_sign_id, + 'contract_id' => $contract_id + ]); + + $configs = Db::table('school_document_data_source_config') + ->where('contract_id', $contract_id) + ->whereNull('contract_sign_id') + ->select() + ->toArray(); + } + + if (empty($configs)) { + Log::warning('该合同无字段配置', [ + 'contract_sign_id' => $contract_sign_id, + 'contract_id' => $contract_id + ]); return []; } @@ -736,8 +765,9 @@ class ContractSignFormService extends BaseApiService switch ($table_name) { case 'school_student': + case 'students': // 兼容配置中的简化表名 // 学员表:使用CoreFieldMappingService进行字段映射和转义 - $service = new CoreFieldMappingService($config['table_name'], ['id' => $student['id']]); + $service = new CoreFieldMappingService('school_student', ['id' => $student['id']]); // 配置枚举映射 $service->setFieldEnums([ diff --git a/uniapp/common/config.js b/uniapp/common/config.js index 2459f37b..c98a2253 100644 --- a/uniapp/common/config.js +++ b/uniapp/common/config.js @@ -1,6 +1,6 @@ // 环境变量配置 -// const env = 'development' -const env = 'prod' +const env = 'development' +// const env = 'prod' const isMockEnabled = false // 默认禁用Mock优先模式,仅作为回退 const isDebug = false // 默认启用调试模式 const devurl = 'http://localhost:20080/api'