From b52410234577286b50a1ee8b7a7fa686a2b3573a Mon Sep 17 00:00:00 2001
From: zeyan <258785420@qq.com>
Date: Tue, 19 Aug 2025 12:58:53 +0800
Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E6=94=B9=20bug?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../api/controller/apiController/Contract.php | 14 +-
niucloud/app/common.php | 27 +
.../document/DocumentTemplateService.php | 719 +++++++++++++++++-
.../apiService/ContractSignFormService.php | 36 +-
uniapp/common/config.js | 4 +-
5 files changed, 784 insertions(+), 16 deletions(-)
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'