contractModel = new Contract(); $this->logModel = new DocumentGenerateLog(); $this->dataSourceModel = new DocumentDataSourceConfig(); } /** * 获取模板列表 * @param array $where * @return array */ public function getPage(array $where = []) { $field = 'id,contract_name,contract_template,contract_status,contract_type,remarks,original_filename,file_size,placeholders,created_at,updated_at'; $order = 'id desc'; $search_model = $this->contractModel->withSearch(["contract_status", "contract_type", "created_at"], $where)->field($field)->order($order); $list = $this->pageQuery($search_model); // 处理数据格式 if (!empty($list['data'])) { foreach ($list['data'] as &$item) { $item['placeholders'] = $item['placeholders'] ? json_decode($item['placeholders'], true) : []; $item['file_size_formatted'] = $this->formatFileSize($item['file_size']); } } return $list; } /** * 获取模板详情 * @param int $id * @return array */ public function getInfo(int $id) { $field = 'id,contract_name,contract_template,contract_content,contract_status,contract_type,remarks,placeholder_config,original_filename,file_size,file_hash,placeholders,created_at,updated_at'; $info = $this->contractModel->field($field)->where([['id', "=", $id]])->findOrEmpty()->toArray(); if (!empty($info)) { $info['placeholder_config'] = $info['placeholder_config'] ? json_decode($info['placeholder_config'], true) : []; $info['placeholders'] = $info['placeholders'] ? json_decode($info['placeholders'], true) : []; $info['file_size_formatted'] = $this->formatFileSize($info['file_size']); } return $info; } /** * 上传Word模板文件 * @param $file * @return array * @throws \Exception */ public function uploadTemplate($file) { // 验证文件类型 $allowedTypes = ['docx', 'doc']; $extension = strtolower($file->getOriginalExtension()); if (!in_array($extension, $allowedTypes)) { throw new \Exception('只支持 .docx 和 .doc 格式的Word文档'); } // 验证文件大小 (最大10MB) $maxSize = 10 * 1024 * 1024; if ($file->getSize() > $maxSize) { throw new \Exception('文件大小不能超过10MB'); } // 生成文件hash防重复 $fileHash = md5_file($file->getRealPath()); // 检查是否已存在相同文件 $existingFile = $this->contractModel->where('file_hash', $fileHash)->find(); if ($existingFile) { throw new \Exception('该文件已经上传过了,模板名称:' . $existingFile['contract_name']); } try { // 保存文件 $savePath = Filesystem::disk('public')->putFile('contract_templates', $file); if (!$savePath) { throw new \Exception('文件保存失败'); } // 解析Word文档内容和占位符 $fullPath = public_path() . '/upload/' . $savePath; $parseResult = $this->parseWordTemplate($fullPath); // 准备保存到数据库的数据 $data = [ 'site_id' => $this->site_id, 'contract_name' => pathinfo($file->getOriginalName(), PATHINFO_FILENAME), 'contract_template' => $savePath, 'contract_content' => $parseResult['content'], 'contract_status' => 'draft', 'contract_type' => 'general', 'original_filename' => $file->getOriginalName(), 'file_size' => $file->getSize(), 'file_hash' => $fileHash, 'placeholders' => json_encode($parseResult['placeholders']) ]; // 保存到数据库 $template = $this->contractModel->create($data); return [ 'id' => $template->id, 'template_name' => $data['contract_name'], 'file_path' => $savePath, 'placeholders' => $parseResult['placeholders'], 'placeholder_count' => count($parseResult['placeholders']) ]; } catch (\Exception $e) { // 如果保存失败,删除已上传的文件 if (isset($savePath)) { Filesystem::disk('public')->delete($savePath); } throw new \Exception('模板上传失败:' . $e->getMessage()); } } /** * 解析Word模板占位符 * @param array $data * @return array * @throws \Exception */ public function parsePlaceholder(array $data) { if (!empty($data['template_id'])) { // 从数据库获取模板信息 $template = $this->contractModel->find($data['template_id']); if (!$template) { throw new \Exception('模板不存在'); } $templatePath = public_path() . '/upload/' . $template['contract_template']; } else if (!empty($data['template_path'])) { $templatePath = $data['template_path']; } else { throw new \Exception('请提供模板文件路径或模板ID'); } if (!file_exists($templatePath)) { throw new \Exception('模板文件不存在'); } return $this->parseWordTemplate($templatePath); } /** * 解析Word模板文件内容 * @param string $filePath * @return array * @throws \Exception */ private function parseWordTemplate(string $filePath) { try { // 读取Word文档 $templateProcessor = new TemplateProcessor($filePath); // 获取文档内容(简化版本,实际可能需要更复杂的解析) $content = $this->extractWordContent($filePath); // 提取占位符 - 匹配 {{...}} 格式 $placeholders = $this->extractPlaceholders($content); return [ 'content' => $content, 'placeholders' => $placeholders ]; } catch (\Exception $e) { Log::error('Word模板解析失败:' . $e->getMessage()); throw new \Exception('Word模板解析失败:' . $e->getMessage()); } } /** * 提取Word文档内容 * @param string $filePath * @return string */ private function extractWordContent(string $filePath) { try { $phpWord = IOFactory::load($filePath); $content = ''; // 遍历所有章节 foreach ($phpWord->getSections() as $section) { foreach ($section->getElements() as $element) { if (method_exists($element, 'getText')) { $content .= $element->getText() . "\n"; } } } return $content; } catch (\Exception $e) { Log::error('提取Word内容失败:' . $e->getMessage()); return ''; } } /** * 从内容中提取占位符 * @param string $content * @return array */ private function extractPlaceholders(string $content) { $placeholders = []; // 匹配 {{变量名}} 格式的占位符 if (preg_match_all('/\{\{([^}]+)\}\}/', $content, $matches)) { foreach ($matches[1] as $placeholder) { $placeholder = trim($placeholder); if (!in_array($placeholder, $placeholders)) { $placeholders[] = $placeholder; } } } return $placeholders; } /** * 保存占位符配置 * @param array $data * @return bool * @throws \Exception */ public function savePlaceholderConfig(array $data) { $template = $this->contractModel->find($data['template_id']); if (!$template) { throw new \Exception('模板不存在'); } // 验证配置数据 $config = $data['placeholder_config']; foreach ($config as $placeholder => $settings) { if ($settings['data_source'] === 'database') { // 验证数据源是否在白名单中 $isAllowed = $this->dataSourceModel ->where('site_id', $this->site_id) ->where('table_name', $settings['table_name']) ->where('field_name', $settings['field_name']) ->where('is_active', 1) ->find(); if (!$isAllowed) { throw new \Exception("数据源 {$settings['table_name']}.{$settings['field_name']} 不在允许的范围内"); } } } // 保存配置 $template->placeholder_config = json_encode($config); $template->contract_status = 'active'; // 配置完成后激活模板 $template->save(); return true; } /** * 获取可用数据源列表 * @return array */ public function getDataSources() { $dataSources = $this->dataSourceModel ->where('site_id', $this->site_id) ->where('is_active', 1) ->order('table_name,sort_order') ->select() ->toArray(); // 按表名分组 $grouped = []; foreach ($dataSources as $item) { $grouped[$item['table_name']]['table_alias'] = $item['table_alias']; $grouped[$item['table_name']]['fields'][] = [ 'field_name' => $item['field_name'], 'field_alias' => $item['field_alias'], 'field_type' => $item['field_type'] ]; } return $grouped; } /** * 生成Word文档 * @param array $data * @return array * @throws \Exception */ public function generateDocument(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' => 'admin', 'fill_data' => json_encode($data['fill_data']), 'status' => 'pending', 'created_at' => time(), 'updated_at' => time() ]; $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); $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; $fullOutputPath = public_path() . '/upload/' . $outputPath; // 确保目录存在 $outputDir = dirname($fullOutputPath); if (!is_dir($outputDir)) { mkdir($outputDir, 0755, true); } // 使用 PhpWord 模板处理器 $templateProcessor = new TemplateProcessor($templatePath); foreach ($fillValues as $placeholder => $value) { $templateProcessor->setValue($placeholder, $value); } $templateProcessor->saveAs($fullOutputPath); // 更新生成记录 $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(); 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(); throw new \Exception('文档生成失败:' . $e->getMessage()); } } /** * 准备填充数据 * @param array $placeholderConfig * @param array $userFillData * @return array */ private function prepareFillData(array $placeholderConfig, array $userFillData) { $fillValues = []; foreach ($placeholderConfig as $placeholder => $config) { $value = ''; if ($config['data_source'] === 'manual') { // 手动填写的数据 $value = $userFillData[$placeholder] ?? $config['default_value'] ?? ''; } else if ($config['data_source'] === 'database') { // 从数据库获取数据 $value = $this->getDataFromDatabase($config, $userFillData); } // 应用处理函数 if (!empty($config['process_function'])) { $value = $this->applyProcessFunction($value, $config['process_function']); } $fillValues[$placeholder] = $value; } return $fillValues; } /** * 从数据库获取数据 * @param array $config * @param array $userFillData * @return string */ private function getDataFromDatabase(array $config, array $userFillData) { try { $tableName = $config['table_name']; $fieldName = $config['field_name']; // 简单的数据库查询(实际应用中需要更完善的查询逻辑) $model = new \think\Model(); $result = $model->table($tableName)->field($fieldName)->find(); return $result[$fieldName] ?? $config['default_value'] ?? ''; } catch (\Exception $e) { Log::error('数据库查询失败:' . $e->getMessage()); return $config['default_value'] ?? ''; } } /** * 应用处理函数 * @param mixed $value * @param string $functionName * @return string */ private function applyProcessFunction($value, string $functionName) { switch ($functionName) { case 'formatDate': return $value ? date('Y年m月d日', strtotime($value)) : ''; case 'formatDateTime': return $value ? date('Y年m月d日 H:i', strtotime($value)) : ''; case 'formatNumber': return is_numeric($value) ? number_format($value, 2) : $value; case 'toUpper': return strtoupper($value); case 'toLower': return strtolower($value); default: return $value; } } /** * 下载生成的文档 * @param int $logId * @return Response * @throws \Exception */ public function downloadDocument(int $logId) { $log = $this->logModel->find($logId); if (!$log) { throw new \Exception('记录不存在'); } if ($log['status'] !== 'completed') { throw new \Exception('文档尚未生成完成'); } $filePath = public_path() . '/upload/' . $log['generated_file_path']; if (!file_exists($filePath)) { throw new \Exception('文件不存在'); } // 更新下载统计 $log->download_count = $log->download_count + 1; $log->last_download_time = date('Y-m-d H:i:s'); $log->save(); // 返回文件下载响应 return download($filePath, $log['generated_file_name']); } /** * 获取生成记录 * @param array $where * @return array */ public function getGenerateLog(array $where = []) { $field = 'id,template_id,user_id,user_type,generated_file_name,status,download_count,created_at,process_start_time,process_end_time'; $order = 'id desc'; $searchModel = $this->logModel ->alias('log') ->join('school_contract template', 'log.template_id = template.id') ->field($field . ',template.contract_name') ->where('log.site_id', $this->site_id) ->order($order); // 添加搜索条件 if (!empty($where['template_id'])) { $searchModel->where('log.template_id', $where['template_id']); } if (!empty($where['status'])) { $searchModel->where('log.status', $where['status']); } return $this->pageQuery($searchModel); } /** * 格式化文件大小 * @param int $size * @return string */ private function formatFileSize(int $size) { $units = ['B', 'KB', 'MB', 'GB']; $index = 0; while ($size >= 1024 && $index < count($units) - 1) { $size /= 1024; $index++; } return round($size, 2) . ' ' . $units[$index]; } /** * 预览模板 * @param int $id * @return array * @throws \Exception */ public function previewTemplate(int $id) { $template = $this->contractModel->find($id); if (!$template) { throw new \Exception('模板不存在'); } return [ 'content' => $template['contract_content'], 'placeholders' => json_decode($template['placeholders'], true) ?: [] ]; } /** * 删除模板 * @param int $id * @return bool * @throws \Exception */ public function delete(int $id) { $template = $this->contractModel->find($id); if (!$template) { throw new \Exception('模板不存在'); } // 删除关联的生成记录和文件 $logs = $this->logModel->where('template_id', $id)->select(); foreach ($logs as $log) { if ($log['generated_file_path']) { $filePath = public_path() . '/upload/' . $log['generated_file_path']; if (file_exists($filePath)) { unlink($filePath); } } } $this->logModel->where('template_id', $id)->delete(); // 删除模板文件 if ($template['contract_template']) { $templatePath = public_path() . '/upload/' . $template['contract_template']; if (file_exists($templatePath)) { unlink($templatePath); } } // 删除数据库记录 return $template->delete(); } /** * 复制模板 * @param int $id * @return array * @throws \Exception */ public function copy(int $id) { $template = $this->contractModel->find($id); if (!$template) { throw new \Exception('模板不存在'); } $newData = $template->toArray(); unset($newData['id']); $newData['contract_name'] = $newData['contract_name'] . '_副本'; $newData['created_at'] = date('Y-m-d H:i:s'); $newData['updated_at'] = date('Y-m-d H:i:s'); $newTemplate = $this->contractModel->create($newData); return ['id' => $newTemplate->id]; } /** * 批量删除生成记录 * @param array $ids * @return bool */ public function batchDeleteLog(array $ids) { $logs = $this->logModel->whereIn('id', $ids)->select(); foreach ($logs as $log) { if ($log['generated_file_path']) { $filePath = public_path() . '/upload/' . $log['generated_file_path']; if (file_exists($filePath)) { unlink($filePath); } } } return $this->logModel->whereIn('id', $ids)->delete(); } }