123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234 |
- from flask import Flask, request, jsonify, send_from_directory
- import os
- import time
- from dotenv import load_dotenv
- # 加载环境变量
- load_dotenv()
- # 导入自定义模块
- from utils.logger import logger, reset_process_id
- from utils.file_utils import get_date_folder, get_safe_filename, allowed_file
- from utils.docx_processor import process_word_template, check_variables_in_document, verify_replacement
- from scheduler import init_scheduler
- # 初始化Flask应用
- app = Flask(__name__)
- # 启用跨域支持
- from flask_cors import CORS
- CORS(app)
- # 配置常量
- app.config['TEMPLATE_FOLDER'] = os.getenv('TEMPLATE_FOLDER', 'template') # 模板文件目录
- app.config['OUTPUT_FOLDER'] = os.getenv('OUTPUT_FOLDER', 'outputs') # 处理后文件保存目录
- app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024 # 限制上传文件大小为16MB
- app.config['DEBUG_MODE'] = os.getenv('DEBUG_MODE', 'False').lower() == 'true' # 调试模式:True启用变量检查和验证,False禁用
- app.config['LOG_FOLDER'] = os.getenv('LOG_FOLDER', 'logs') # 日志保存目录
- # 确保模板、输出和日志目录存在
- os.makedirs(app.config['TEMPLATE_FOLDER'], exist_ok=True)
- os.makedirs(app.config['OUTPUT_FOLDER'], exist_ok=True)
- os.makedirs(app.config['LOG_FOLDER'], exist_ok=True)
- # 允许的Word文档扩展名
- ALLOWED_EXTENSIONS = {'.doc', '.docx'}
- # 添加路由来提供upload.html文件
- @app.route('/')
- @app.route('/upload.html')
- def serve_upload_html():
- """
- 提供upload.html页面
- """
- return send_from_directory('.', 'upload.html')
- @app.route('/process_file', methods=['POST'])
- def process_file():
- """
- 处理Word文档模板,替换其中的模板变量
-
- 请求参数:
- - JSON格式的数据,包含要替换的变量键值对
-
- 返回:
- JSON响应,包含处理状态和输出文件名
- """
- # 重置处理ID,用于日志跟踪
- reset_process_id()
-
- # 记录请求开始
- start_time = time.time()
- logger.info("接收到文档处理请求")
-
- # 检查是否有JSON数据
- if not request.is_json:
- logger.warning("请求中没有JSON数据")
- return jsonify({'error': '请求必须包含JSON数据'}), 400
-
- # 获取JSON数据
- data = request.get_json()
- if not data:
- logger.warning("JSON数据为空")
- return jsonify({'error': 'JSON数据为空'}), 400
-
- logger.info(f"接收到的变量数据: {data}")
-
- try:
- # 获取模板文件,过滤掉Word临时文件(以~$开头)
- template_files = [f for f in os.listdir(app.config['TEMPLATE_FOLDER'])
- if f.lower().endswith('.docx') and not f.startswith('~$')]
-
- if not template_files:
- logger.error("模板文件夹中没有找到有效的.docx文件")
- return jsonify({'error': '没有找到有效的模板文件'}), 500
-
- # 使用第一个模板文件
- template_filename = template_files[0]
- template_path = os.path.join(app.config['TEMPLATE_FOLDER'], template_filename)
- logger.info(f"使用模板文件: {template_filename}")
-
- # 生成唯一的输出文件名
- file_name_without_ext = os.path.splitext(template_filename)[0]
- extension = os.path.splitext(template_filename)[1]
- output_filename = get_safe_filename(file_name_without_ext, extension)
-
- # 获取当天的输出文件夹
- output_date_folder = get_date_folder(app.config['OUTPUT_FOLDER'])
-
- # 处理文件(替换变量)
- output_path = os.path.join(output_date_folder, output_filename)
-
- # 构建变量字典,为每个键添加{}
- variables = {}
- for key, value in data.items():
- var_key = '{' + key + '}'
- variables[var_key] = str(value) if value is not None else ''
-
- logger.info(f"将替换以下变量: {list(variables.keys())}")
-
- # 检查文档中是否包含这些变量(仅在调试模式下执行)
- if app.config['DEBUG_MODE']:
- check_variables_in_document(template_path, variables)
-
- # 处理文档并替换变量
- process_word_template(template_path, output_path, variables)
-
- logger.info(f"变量替换完成")
-
- # 验证替换是否成功(仅在调试模式下执行)
- if app.config['DEBUG_MODE']:
- verify_replacement(output_path, variables)
-
- # 计算处理时间
- process_time = time.time() - start_time
- logger.info(f"文档处理完成,耗时: {process_time:.2f}秒")
-
- # 返回成功信息
- return jsonify({
- 'message': '文档处理成功',
- 'output_filename': output_filename
- })
- except PermissionError as e:
- error_msg = f"权限错误: {str(e)}。可能是文件被占用或没有写入权限。"
- logger.error(error_msg)
- return jsonify({'error': error_msg}), 500
- except Exception as e:
- error_msg = f"处理文档时出错: {str(e)}"
- logger.error(error_msg, exc_info=True)
- return jsonify({'error': error_msg}), 500
- @app.route('/download/<filename>')
- def download_file(filename):
- """
- 提供处理后文档的下载或预览
-
- 参数:
- filename: 要下载的文件名
-
- 返回:
- 处理后的文档文件
- """
- logger.info(f"请求下载/预览文件: {filename}")
- return serve_file(filename, False)
- @app.route('/download-attachment/<filename>')
- def download_file_attachment(filename):
- """
- 提供处理后文档的强制下载
-
- 参数:
- filename: 要下载的文件名
-
- 返回:
- 处理后的文档文件(作为附件)
- """
- logger.info(f"请求强制下载文件: {filename}")
- return serve_file(filename, True)
- def serve_file(filename, as_attachment):
- """
- 通用文件服务函数
-
- 参数:
- filename: 要提供的文件名
- as_attachment: 是否作为附件下载
-
- 返回:
- 文件响应
- """
- try:
- # 获取当天的日期文件夹
- today_folder = get_date_folder(app.config['OUTPUT_FOLDER'])
-
- # 首先尝试从当天的文件夹中查找
- if os.path.exists(os.path.join(today_folder, filename)):
- logger.info(f"文件在当天文件夹中找到: {os.path.join(today_folder, filename)}")
- return send_from_directory(
- today_folder,
- filename,
- mimetype='application/vnd.openxmlformats-officedocument.wordprocessingml.document',
- as_attachment=as_attachment
- )
-
- # 如果当天文件夹中没有,则尝试在输出根目录查找(兼容旧文件)
- if os.path.exists(os.path.join(app.config['OUTPUT_FOLDER'], filename)):
- logger.info(f"文件在输出根目录中找到: {os.path.join(app.config['OUTPUT_FOLDER'], filename)}")
- return send_from_directory(
- app.config['OUTPUT_FOLDER'],
- filename,
- mimetype='application/vnd.openxmlformats-officedocument.wordprocessingml.document',
- as_attachment=as_attachment
- )
-
- # 如果还是没找到,尝试在其他日期文件夹中查找
- for item in os.listdir(app.config['OUTPUT_FOLDER']):
- item_path = os.path.join(app.config['OUTPUT_FOLDER'], item)
- if os.path.isdir(item_path):
- file_path = os.path.join(item_path, filename)
- if os.path.exists(file_path):
- logger.info(f"文件在日期文件夹中找到: {file_path}")
- return send_from_directory(
- item_path,
- filename,
- mimetype='application/vnd.openxmlformats-officedocument.wordprocessingml.document',
- as_attachment=as_attachment
- )
-
- # 如果所有位置都没找到,返回404错误
- logger.warning(f"找不到请求的文件: {filename}")
- return jsonify({'error': f'找不到文件: {filename}'}), 404
- except Exception as e:
- error_msg = f"下载文件时出错: {str(e)}"
- logger.error(error_msg, exc_info=True)
- return jsonify({'error': error_msg}), 500
- if __name__ == '__main__':
- # 初始化调度器
- init_scheduler(app.config['OUTPUT_FOLDER'])
- logger.info("Flask应用程序开始运行")
- # 使用host='0.0.0.0'使Flask监听所有网络接口,这样局域网内的其他设备可以访问
- # 从环境变量获取端口号,默认为5000
- port = int(os.getenv('PORT', 5000))
- debug = os.getenv('FLASK_DEBUG', '0') == '1'
- app.run(host='0.0.0.0', port=port, debug=debug)
|