You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1218 lines
42 KiB
1218 lines
42 KiB
<?php
|
|
// +----------------------------------------------------------------------
|
|
// | Niucloud-admin 企业快速开发的多应用管理平台
|
|
// +----------------------------------------------------------------------
|
|
// | 官方网址:https://www.niucloud.com
|
|
// +----------------------------------------------------------------------
|
|
// | niucloud团队 版权所有 开源版本可自由商用
|
|
// +----------------------------------------------------------------------
|
|
// | Author: Niucloud Team
|
|
// +----------------------------------------------------------------------
|
|
|
|
namespace app\service\admin\document;
|
|
|
|
use app\model\contract\Contract;
|
|
use app\model\document\DocumentGenerateLog;
|
|
use app\model\document\DocumentDataSourceConfig;
|
|
use core\base\BaseAdminService;
|
|
use PhpOffice\PhpWord\IOFactory;
|
|
use PhpOffice\PhpWord\PhpWord;
|
|
use PhpOffice\PhpWord\TemplateProcessor;
|
|
use think\facade\Filesystem;
|
|
use think\facade\Log;
|
|
use think\Response;
|
|
|
|
/**
|
|
* Word文档模板解析服务层
|
|
* Class DocumentTemplateService
|
|
* @package app\service\admin\document
|
|
*/
|
|
class DocumentTemplateService extends BaseAdminService
|
|
{
|
|
protected $contractModel;
|
|
protected $logModel;
|
|
protected $dataSourceModel;
|
|
|
|
public function __construct()
|
|
{
|
|
parent::__construct();
|
|
$this->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']);
|
|
|
|
// 获取数据源配置信息,从placeholder_config字段获取
|
|
$dataSourceConfigs = [];
|
|
if (!empty($info['placeholder_config'])) {
|
|
// 转换placeholder_config格式为前端需要的data_source_configs格式
|
|
foreach ($info['placeholder_config'] as $placeholder => $config) {
|
|
$dataSourceConfigs[] = [
|
|
'id' => 0,
|
|
'placeholder' => $placeholder,
|
|
'data_type' => $config['data_type'] ?? 'user_input',
|
|
'table_name' => $config['table_name'] ?? '',
|
|
'field_name' => $config['field_name'] ?? '',
|
|
'system_function' => $config['system_function'] ?? '',
|
|
'user_input_value' => $config['user_input_value'] ?? '',
|
|
'sign_party' => $config['sign_party'] ?? '',
|
|
'field_type' => $config['field_type'] ?? 'text',
|
|
'is_required' => $config['is_required'] ?? 0,
|
|
'default_value' => $config['default_value'] ?? ''
|
|
];
|
|
}
|
|
}
|
|
|
|
$info['data_source_configs'] = $dataSourceConfigs;
|
|
|
|
// 如果没有数据源配置,但有占位符,则创建默认配置
|
|
if (empty($dataSourceConfigs) && !empty($info['placeholders'])) {
|
|
$defaultConfigs = [];
|
|
foreach ($info['placeholders'] as $placeholder) {
|
|
$defaultConfigs[] = [
|
|
'id' => 0,
|
|
'placeholder' => $placeholder,
|
|
'data_type' => 'user_input',
|
|
'table_name' => '',
|
|
'field_name' => '',
|
|
'system_function' => '',
|
|
'user_input_value' => '',
|
|
'sign_party' => '',
|
|
'field_type' => 'text',
|
|
'is_required' => 0,
|
|
'default_value' => ''
|
|
];
|
|
}
|
|
$info['data_source_configs'] = $defaultConfigs;
|
|
}
|
|
}
|
|
|
|
return $info;
|
|
}
|
|
|
|
/**
|
|
* 保存数据源配置
|
|
* @param int $contractId 合同ID
|
|
* @param array $configs 配置数据
|
|
* @return bool
|
|
* @throws \Exception
|
|
*/
|
|
public function saveDataSourceConfig(int $contractId, array $configs): bool
|
|
{
|
|
// 验证合同是否存在
|
|
$contract = $this->contractModel->find($contractId);
|
|
if (!$contract) {
|
|
throw new \Exception('合同不存在');
|
|
}
|
|
|
|
// 开启事务
|
|
\think\facade\Db::startTrans();
|
|
try {
|
|
// 删除现有配置
|
|
$this->dataSourceModel->where('contract_id', $contractId)->delete();
|
|
|
|
// 批量插入新配置
|
|
if (!empty($configs)) {
|
|
$insertData = [];
|
|
foreach ($configs as $config) {
|
|
// 验证必需字段
|
|
if (empty($config['placeholder'])) {
|
|
throw new \Exception('占位符不能为空');
|
|
}
|
|
|
|
$insertData[] = [
|
|
'contract_id' => $contractId,
|
|
'placeholder' => $config['placeholder'],
|
|
'table_name' => $config['table_name'] ?? '',
|
|
'field_name' => $config['field_name'] ?? '',
|
|
'field_type' => $config['field_type'] ?? 'string',
|
|
'is_required' => $config['is_required'] ?? 0,
|
|
'default_value' => $config['default_value'] ?? '',
|
|
'created_at' => date('Y-m-d H:i:s')
|
|
];
|
|
}
|
|
|
|
$result = $this->dataSourceModel->insertAll($insertData);
|
|
if (!$result) {
|
|
throw new \Exception('保存配置失败');
|
|
}
|
|
}
|
|
|
|
// 提交事务
|
|
\think\facade\Db::commit();
|
|
return true;
|
|
|
|
} catch (\Exception $e) {
|
|
// 回滚事务
|
|
\think\facade\Db::rollback();
|
|
throw $e;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 上传Word模板文件
|
|
* @param $file
|
|
* @param array $data
|
|
* @return array
|
|
* @throws \Exception
|
|
*/
|
|
public function uploadTemplate($file, array $data = [])
|
|
{
|
|
// 验证文件类型
|
|
$allowedTypes = ['docx', 'doc'];
|
|
$extension = strtolower($file->getOriginalExtension());
|
|
|
|
if (!in_array($extension, $allowedTypes)) {
|
|
throw new \Exception('只支持 .docx 和 .doc 格式的Word文档');
|
|
}
|
|
|
|
// 获取文件信息
|
|
$fileSize = $file->getSize();
|
|
$realPath = $file->getRealPath();
|
|
|
|
// 验证文件大小 (最大10MB)
|
|
$maxSize = 10 * 1024 * 1024;
|
|
if ($fileSize > $maxSize) {
|
|
throw new \Exception('文件大小不能超过10MB');
|
|
}
|
|
|
|
// 生成文件hash防重复
|
|
$fileHash = md5_file($realPath);
|
|
|
|
try {
|
|
// 生成保存路径
|
|
$uploadDir = 'contract_templates/' . date('Ymd');
|
|
$uploadPath = public_path() . '/upload/' . $uploadDir;
|
|
|
|
// 确保目录存在
|
|
if (!is_dir($uploadPath)) {
|
|
mkdir($uploadPath, 0777, true);
|
|
}
|
|
|
|
// 生成文件名
|
|
$fileName = md5(time() . $file->getOriginalName()) . '.' . $extension;
|
|
$fullPath = $uploadPath . '/' . $fileName;
|
|
$savePath = $uploadDir . '/' . $fileName;
|
|
|
|
// 移动文件到目标位置
|
|
if (!move_uploaded_file($realPath, $fullPath)) {
|
|
throw new \Exception('文件保存失败');
|
|
}
|
|
|
|
// 解析Word文档内容和占位符
|
|
$parseResult = $this->parseWordTemplate($fullPath);
|
|
|
|
// 准备保存到数据库的数据
|
|
$saveData = [
|
|
'contract_name' => !empty($data['contract_name']) ? $data['contract_name'] : pathinfo($file->getOriginalName(), PATHINFO_FILENAME),
|
|
'contract_template' => $savePath,
|
|
'contract_content' => $parseResult['content'],
|
|
'contract_status' => 'draft',
|
|
'contract_type' => !empty($data['contract_type']) ? $data['contract_type'] : 'general',
|
|
'original_filename' => $file->getOriginalName(),
|
|
'file_size' => $fileSize,
|
|
'file_hash' => $fileHash,
|
|
'placeholders' => json_encode($parseResult['placeholders']),
|
|
'remarks' => !empty($data['remarks']) ? $data['remarks'] : '',
|
|
'created_at' => date('Y-m-d H:i:s'),
|
|
'updated_at' => date('Y-m-d H:i:s')
|
|
];
|
|
|
|
// 保存到数据库
|
|
$template = $this->contractModel->create($saveData);
|
|
|
|
return [
|
|
'id' => $template->id,
|
|
'template_name' => $saveData['contract_name'],
|
|
'file_path' => $savePath,
|
|
'placeholders' => $parseResult['placeholders'],
|
|
'placeholder_count' => count($parseResult['placeholders'])
|
|
];
|
|
|
|
} catch (\Exception $e) {
|
|
// 如果保存失败,删除已上传的文件
|
|
if (isset($fullPath) && file_exists($fullPath)) {
|
|
unlink($fullPath);
|
|
}
|
|
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(int $templateId, array $configs)
|
|
{
|
|
$template = $this->contractModel->find($templateId);
|
|
if (!$template) {
|
|
throw new \Exception('模板不存在');
|
|
}
|
|
|
|
// 转换配置数据格式以支持三种数据类型:database, system, user_input
|
|
$configData = [];
|
|
foreach ($configs as $config) {
|
|
$placeholder = $config['placeholder'];
|
|
$dataType = $config['data_type'] ?? 'user_input';
|
|
|
|
$configData[$placeholder] = [
|
|
'data_type' => $dataType,
|
|
'table_name' => $config['table_name'] ?? '',
|
|
'field_name' => $config['field_name'] ?? '',
|
|
'system_function' => $config['system_function'] ?? '',
|
|
'user_input_value' => $config['user_input_value'] ?? '',
|
|
'sign_party' => $config['sign_party'] ?? '',
|
|
'field_type' => $config['field_type'] ?? 'text',
|
|
'is_required' => $config['is_required'] ?? 0,
|
|
'default_value' => $config['default_value'] ?? ''
|
|
];
|
|
}
|
|
|
|
// 开启事务
|
|
\think\facade\Db::startTrans();
|
|
try {
|
|
// 1. 保存配置到合同表的placeholder_config字段(保持兼容性)
|
|
$template->placeholder_config = json_encode($configData);
|
|
$template->updated_at = date('Y-m-d H:i:s');
|
|
$template->save();
|
|
|
|
// 2. 同时保存到独立的数据源配置表(用户期望的表)
|
|
$this->saveConfigToDataSourceTable($templateId, $configData);
|
|
|
|
\think\facade\Db::commit();
|
|
return true;
|
|
} catch (\Exception $e) {
|
|
\think\facade\Db::rollback();
|
|
throw $e;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 保存配置到数据源配置表
|
|
* @param int $contractId 合同ID
|
|
* @param array $config 配置数据
|
|
* @return void
|
|
* @throws \Exception
|
|
*/
|
|
private function saveConfigToDataSourceTable(int $contractId, array $config): void
|
|
{
|
|
// 删除现有配置
|
|
$this->dataSourceModel->where('contract_id', $contractId)->delete();
|
|
|
|
// 转换配置格式并保存
|
|
if (!empty($config)) {
|
|
$insertData = [];
|
|
foreach ($config as $placeholder => $settings) {
|
|
$insertData[] = [
|
|
'contract_id' => $contractId,
|
|
'placeholder' => $placeholder,
|
|
'data_type' => $settings['data_type'] ?? 'user_input',
|
|
'table_name' => $settings['table_name'] ?? '',
|
|
'field_name' => $settings['field_name'] ?? '',
|
|
'system_function' => $settings['system_function'] ?? '',
|
|
'sign_party' => $settings['sign_party'] ?? '',
|
|
'field_type' => $settings['field_type'] ?? 'text',
|
|
'is_required' => $settings['is_required'] ?? 0,
|
|
'default_value' => $settings['default_value'] ?? '',
|
|
'created_at' => date('Y-m-d H:i:s')
|
|
];
|
|
}
|
|
|
|
if (!empty($insertData)) {
|
|
$this->dataSourceModel->insertAll($insertData);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 获取可用数据源列表
|
|
* @return array
|
|
*/
|
|
public function getDataSources()
|
|
{
|
|
return [
|
|
'tables' => $this->getAvailableTables(),
|
|
'system_functions' => $this->getSystemFunctions()
|
|
];
|
|
}
|
|
|
|
/**
|
|
* 获取可用数据表配置
|
|
* @return array
|
|
*/
|
|
public function getAvailableTables()
|
|
{
|
|
return [
|
|
'school_student' => [
|
|
'label' => '学员表',
|
|
'fields' => [
|
|
'id' => '学员ID',
|
|
'name' => '学员姓名',
|
|
'gender' => '性别',
|
|
'age' => '年龄',
|
|
'birthday' => '生日',
|
|
'emergency_contact' => '紧急联系人',
|
|
'contact_phone' => '联系人电话',
|
|
'status' => '学员状态',
|
|
'trial_class_count' => '体验课次数',
|
|
'created_at' => '创建时间',
|
|
'updated_at' => '修改时间'
|
|
]
|
|
],
|
|
'school_customer_resources' => [
|
|
'label' => '客户资源表',
|
|
'fields' => [
|
|
'id' => '编号',
|
|
'name' => '姓名',
|
|
'phone_number' => '联系电话',
|
|
'gender' => '性别',
|
|
'age' => '年龄',
|
|
'source_channel' => '来源渠道',
|
|
'source' => '来源',
|
|
'consultant' => '顾问',
|
|
'demand' => '需求',
|
|
'purchasing_power' => '购买力',
|
|
'initial_intent' => '客户初步意向度',
|
|
'trial_class_count' => '体验课次数',
|
|
'created_at' => '创建时间'
|
|
]
|
|
],
|
|
'school_order_table' => [
|
|
'label' => '订单表',
|
|
'fields' => [
|
|
'id' => '订单编号',
|
|
'payment_id' => '支付编号',
|
|
'order_type' => '订单类型',
|
|
'order_status' => '订单状态',
|
|
'payment_type' => '付款类型',
|
|
'order_amount' => '订单金额',
|
|
'discount_amount' => '优惠金额',
|
|
'payment_time' => '支付时间',
|
|
'created_at' => '创建时间',
|
|
'remark' => '订单备注'
|
|
]
|
|
],
|
|
'school_course' => [
|
|
'label' => '课程表',
|
|
'fields' => [
|
|
'id' => '课程编号',
|
|
'course_name' => '课程名称',
|
|
'course_type' => '课程类型',
|
|
'duration' => '课程时长',
|
|
'session_count' => '课时数量',
|
|
'single_session_count' => '单次消课数量',
|
|
'gift_session_count' => '赠送课时数量',
|
|
'price' => '课程价格',
|
|
'internal_reminder' => '内部提醒课时',
|
|
'customer_reminder' => '客户提醒课时',
|
|
'status' => '课程状态',
|
|
'created_at' => '创建时间'
|
|
]
|
|
],
|
|
'school_personnel' => [
|
|
'label' => '人员表',
|
|
'fields' => [
|
|
'id' => 'ID',
|
|
'name' => '姓名',
|
|
'gender' => '性别',
|
|
'phone' => '电话',
|
|
'email' => '邮箱',
|
|
'wx' => '微信号',
|
|
'address' => '家庭住址',
|
|
'education' => '学历',
|
|
'employee_number' => '员工编号',
|
|
'account_type' => '账号类型',
|
|
'status' => '状态',
|
|
'join_time' => '入职时间',
|
|
'create_time' => '创建时间'
|
|
]
|
|
]
|
|
];
|
|
}
|
|
|
|
/**
|
|
* 获取系统函数配置
|
|
* @return array
|
|
*/
|
|
public function getSystemFunctions()
|
|
{
|
|
return [
|
|
'current_date' => '当前日期',
|
|
'current_time' => '当前时间',
|
|
'current_datetime' => '当前日期时间',
|
|
'current_year' => '当前年份',
|
|
'current_month' => '当前月份',
|
|
'current_day' => '当前日',
|
|
'random_number' => '随机编号',
|
|
'contract_generate_time' => '合同生成时间',
|
|
'system_name' => '系统名称',
|
|
'current_user' => '当前用户',
|
|
'current_campus' => '当前校区',
|
|
// 签名占位符
|
|
'employee_signature' => '员工签名位置',
|
|
'student_signature' => '学员签名位置'
|
|
];
|
|
}
|
|
|
|
/**
|
|
* 生成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 = Db::connect();
|
|
$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();
|
|
}
|
|
|
|
/**
|
|
* 编辑模板
|
|
* @param int $id
|
|
* @param array $data
|
|
* @return bool
|
|
* @throws \Exception
|
|
*/
|
|
public function update(int $id, array $data)
|
|
{
|
|
$template = $this->contractModel->find($id);
|
|
if (!$template) {
|
|
throw new \Exception('模板不存在');
|
|
}
|
|
|
|
// 更新基本信息
|
|
if (isset($data['contract_name'])) {
|
|
$template->contract_name = $data['contract_name'];
|
|
}
|
|
if (isset($data['contract_type'])) {
|
|
$template->contract_type = $data['contract_type'];
|
|
}
|
|
if (isset($data['remarks'])) {
|
|
$template->remarks = $data['remarks'];
|
|
}
|
|
|
|
$template->updated_at = date('Y-m-d H:i:s');
|
|
|
|
return $template->save();
|
|
}
|
|
|
|
/**
|
|
* 更新模板状态
|
|
* @param int $id
|
|
* @param string $status
|
|
* @return bool
|
|
* @throws \Exception
|
|
*/
|
|
public function updateStatus(int $id, string $status)
|
|
{
|
|
$template = $this->contractModel->find($id);
|
|
if (!$template) {
|
|
throw new \Exception('模板不存在');
|
|
}
|
|
|
|
$template->contract_status = $status;
|
|
$template->updated_at = date('Y-m-d H:i:s');
|
|
|
|
return $template->save();
|
|
}
|
|
|
|
/**
|
|
* 重新识别占位符
|
|
* @param int $id 模板ID
|
|
* @return array
|
|
* @throws \Exception
|
|
*/
|
|
public function reidentifyPlaceholders(int $id)
|
|
{
|
|
$template = $this->contractModel->find($id);
|
|
if (!$template) {
|
|
throw new \Exception('模板不存在');
|
|
}
|
|
|
|
// 检查模板文件路径是否存在
|
|
if (empty($template['contract_template'])) {
|
|
throw new \Exception('模板未上传Word文档,无法识别占位符');
|
|
}
|
|
|
|
// 检查模板文件是否存在
|
|
$templatePath = public_path() . '/upload/' . $template['contract_template'];
|
|
if (!file_exists($templatePath)) {
|
|
throw new \Exception('模板文件不存在:' . $template['contract_template']);
|
|
}
|
|
|
|
// 检查是否为文件而不是目录
|
|
if (is_dir($templatePath)) {
|
|
throw new \Exception('模板路径指向的是目录而不是文件:' . $template['contract_template']);
|
|
}
|
|
|
|
try {
|
|
// 重新解析Word文档内容和占位符
|
|
$parseResult = $this->parseWordTemplate($templatePath);
|
|
|
|
// 更新数据库中的占位符列表
|
|
$template->placeholders = json_encode($parseResult['placeholders']);
|
|
$template->contract_content = $parseResult['content'];
|
|
$template->updated_at = date('Y-m-d H:i:s');
|
|
$template->save();
|
|
|
|
// 获取现有的占位符配置
|
|
$existingConfig = [];
|
|
if ($template['placeholder_config']) {
|
|
$existingConfig = json_decode($template['placeholder_config'], true) ?: [];
|
|
}
|
|
|
|
// 为新的占位符创建默认配置,保留现有配置
|
|
$newConfig = [];
|
|
foreach ($parseResult['placeholders'] as $placeholder) {
|
|
if (isset($existingConfig[$placeholder])) {
|
|
// 保留现有配置
|
|
$newConfig[$placeholder] = $existingConfig[$placeholder];
|
|
} else {
|
|
// 为新占位符创建默认配置
|
|
$newConfig[$placeholder] = [
|
|
'data_type' => 'user_input',
|
|
'table_name' => '',
|
|
'field_name' => '',
|
|
'system_function' => '',
|
|
'user_input_value' => '',
|
|
'sign_party' => '',
|
|
'field_type' => 'text',
|
|
'is_required' => 0,
|
|
'default_value' => ''
|
|
];
|
|
}
|
|
}
|
|
|
|
// 更新占位符配置
|
|
if (!empty($newConfig)) {
|
|
$template->placeholder_config = json_encode($newConfig);
|
|
$template->save();
|
|
|
|
// 同步更新数据源配置表
|
|
$this->saveConfigToDataSourceTable($id, $newConfig);
|
|
}
|
|
|
|
return [
|
|
'placeholders' => $parseResult['placeholders'],
|
|
'placeholder_count' => count($parseResult['placeholders']),
|
|
'new_placeholders' => array_diff($parseResult['placeholders'], array_keys($existingConfig)),
|
|
'removed_placeholders' => array_diff(array_keys($existingConfig), $parseResult['placeholders'])
|
|
];
|
|
|
|
} catch (\Exception $e) {
|
|
Log::error('重新识别占位符失败:' . $e->getMessage());
|
|
throw new \Exception('重新识别占位符失败:' . $e->getMessage());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 更新模板Word文档
|
|
* @param int $id 模板ID
|
|
* @param $file 上传的文件
|
|
* @return array
|
|
* @throws \Exception
|
|
*/
|
|
public function updateTemplateFile(int $id, $file)
|
|
{
|
|
$template = $this->contractModel->find($id);
|
|
if (!$template) {
|
|
throw new \Exception('模板不存在');
|
|
}
|
|
|
|
// 验证文件类型
|
|
$allowedTypes = ['docx', 'doc'];
|
|
$extension = strtolower($file->getOriginalExtension());
|
|
|
|
if (!in_array($extension, $allowedTypes)) {
|
|
throw new \Exception('只支持 .docx 和 .doc 格式的Word文档');
|
|
}
|
|
|
|
// 获取文件信息
|
|
$fileSize = $file->getSize();
|
|
$realPath = $file->getRealPath();
|
|
|
|
// 验证文件大小 (ORM大10MB)
|
|
$maxSize = 10 * 1024 * 1024;
|
|
if ($fileSize > $maxSize) {
|
|
throw new \Exception('文件大小不能超过10MB');
|
|
}
|
|
|
|
// 生成文件hash防重复
|
|
$fileHash = md5_file($realPath);
|
|
|
|
try {
|
|
// 删除旧文件
|
|
if ($template['contract_template']) {
|
|
$oldFilePath = public_path() . '/upload/' . $template['contract_template'];
|
|
if (file_exists($oldFilePath) && is_file($oldFilePath)) {
|
|
unlink($oldFilePath);
|
|
}
|
|
}
|
|
|
|
// 生成保存路径
|
|
$uploadDir = 'contract_templates/' . date('Ymd');
|
|
$uploadPath = public_path() . '/upload/' . $uploadDir;
|
|
|
|
// 确保目录存在
|
|
if (!is_dir($uploadPath)) {
|
|
mkdir($uploadPath, 0777, true);
|
|
}
|
|
|
|
// 生成文件名
|
|
$fileName = md5(time() . $file->getOriginalName()) . '.' . $extension;
|
|
$fullPath = $uploadPath . '/' . $fileName;
|
|
$savePath = $uploadDir . '/' . $fileName;
|
|
|
|
// 移动文件到目标位置
|
|
if (!move_uploaded_file($realPath, $fullPath)) {
|
|
throw new \Exception('文件保存失败');
|
|
}
|
|
|
|
// 解析Word文档内容和占位符
|
|
$parseResult = $this->parseWordTemplate($fullPath);
|
|
|
|
// 更新数据库记录
|
|
$template->contract_template = $savePath;
|
|
$template->contract_content = $parseResult['content'];
|
|
$template->original_filename = $file->getOriginalName();
|
|
$template->file_size = $fileSize;
|
|
$template->file_hash = $fileHash;
|
|
$template->placeholders = json_encode($parseResult['placeholders']);
|
|
$template->updated_at = date('Y-m-d H:i:s');
|
|
$template->save();
|
|
|
|
// 获取现有的占位符配置
|
|
$existingConfig = [];
|
|
if ($template['placeholder_config']) {
|
|
$existingConfig = json_decode($template['placeholder_config'], true) ?: [];
|
|
}
|
|
|
|
// 为新的占位符创建默认配置,保留现有配置
|
|
$newConfig = [];
|
|
foreach ($parseResult['placeholders'] as $placeholder) {
|
|
if (isset($existingConfig[$placeholder])) {
|
|
// 保留现有配置
|
|
$newConfig[$placeholder] = $existingConfig[$placeholder];
|
|
} else {
|
|
// 为新占位符创建默认配置
|
|
$newConfig[$placeholder] = [
|
|
'data_type' => 'user_input',
|
|
'table_name' => '',
|
|
'field_name' => '',
|
|
'system_function' => '',
|
|
'user_input_value' => '',
|
|
'sign_party' => '',
|
|
'field_type' => 'text',
|
|
'is_required' => 0,
|
|
'default_value' => ''
|
|
];
|
|
}
|
|
}
|
|
|
|
// 更新占位符配置
|
|
if (!empty($newConfig)) {
|
|
$template->placeholder_config = json_encode($newConfig);
|
|
$template->save();
|
|
|
|
// 同步更新数据源配置表
|
|
$this->saveConfigToDataSourceTable($id, $newConfig);
|
|
}
|
|
|
|
return [
|
|
'id' => $template->id,
|
|
'template_name' => $template->contract_name,
|
|
'file_path' => $savePath,
|
|
'placeholders' => $parseResult['placeholders'],
|
|
'placeholder_count' => count($parseResult['placeholders']),
|
|
'new_placeholders' => array_diff($parseResult['placeholders'], array_keys($existingConfig)),
|
|
'removed_placeholders' => array_diff(array_keys($existingConfig), $parseResult['placeholders'])
|
|
];
|
|
|
|
} catch (\Exception $e) {
|
|
// 如果保存失败,删除已上传的文件
|
|
if (isset($fullPath) && file_exists($fullPath)) {
|
|
unlink($fullPath);
|
|
}
|
|
throw new \Exception('模板文档更新失败:' . $e->getMessage());
|
|
}
|
|
}
|
|
}
|