app.py 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186
  1. from flask import Flask, request, jsonify, send_from_directory
  2. import os
  3. import time
  4. # 导入自定义模块
  5. from utils.logger import logger, reset_process_id
  6. from utils.file_utils import get_date_folder, get_safe_filename, allowed_file
  7. from utils.docx_processor import process_word_template, check_variables_in_document, verify_replacement
  8. from scheduler import init_scheduler
  9. # 初始化Flask应用
  10. app = Flask(__name__)
  11. # 启用跨域支持
  12. from flask_cors import CORS
  13. CORS(app)
  14. # 配置常量
  15. app.config['TEMPLATE_FOLDER'] = 'template' # 模板文件目录
  16. app.config['OUTPUT_FOLDER'] = 'outputs' # 处理后文件保存目录
  17. app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024 # 限制上传文件大小为16MB
  18. app.config['DEBUG_MODE'] = False # 调试模式:True启用变量检查和验证,False禁用
  19. app.config['LOG_FOLDER'] = 'logs' # 日志保存目录
  20. # 确保模板、输出和日志目录存在
  21. os.makedirs(app.config['TEMPLATE_FOLDER'], exist_ok=True)
  22. os.makedirs(app.config['OUTPUT_FOLDER'], exist_ok=True)
  23. os.makedirs(app.config['LOG_FOLDER'], exist_ok=True)
  24. # 允许的Word文档扩展名
  25. ALLOWED_EXTENSIONS = {'.doc', '.docx'}
  26. # 添加路由来提供upload.html文件
  27. @app.route('/')
  28. @app.route('/upload.html')
  29. def serve_upload_html():
  30. """
  31. 提供upload.html页面
  32. """
  33. return send_from_directory('.', 'upload.html')
  34. @app.route('/process_file', methods=['POST'])
  35. def process_file():
  36. """
  37. 处理Word文档模板,替换其中的模板变量
  38. 请求参数:
  39. - JSON格式的数据,包含要替换的变量键值对
  40. 返回:
  41. JSON响应,包含处理状态和输出文件名
  42. """
  43. # 重置处理ID,用于日志跟踪
  44. reset_process_id()
  45. # 记录请求开始
  46. start_time = time.time()
  47. logger.info("接收到文档处理请求")
  48. # 检查是否有JSON数据
  49. if not request.is_json:
  50. logger.warning("请求中没有JSON数据")
  51. return jsonify({'error': '请求必须包含JSON数据'}), 400
  52. # 获取JSON数据
  53. data = request.get_json()
  54. if not data:
  55. logger.warning("JSON数据为空")
  56. return jsonify({'error': 'JSON数据为空'}), 400
  57. logger.info(f"接收到的变量数据: {data}")
  58. try:
  59. # 获取模板文件,过滤掉Word临时文件(以~$开头)
  60. template_files = [f for f in os.listdir(app.config['TEMPLATE_FOLDER'])
  61. if f.lower().endswith('.docx') and not f.startswith('~$')]
  62. if not template_files:
  63. logger.error("模板文件夹中没有找到有效的.docx文件")
  64. return jsonify({'error': '没有找到有效的模板文件'}), 500
  65. # 使用第一个模板文件
  66. template_filename = template_files[0]
  67. template_path = os.path.join(app.config['TEMPLATE_FOLDER'], template_filename)
  68. logger.info(f"使用模板文件: {template_filename}")
  69. # 生成唯一的输出文件名
  70. file_name_without_ext = os.path.splitext(template_filename)[0]
  71. extension = os.path.splitext(template_filename)[1]
  72. output_filename = get_safe_filename(file_name_without_ext, extension)
  73. # 获取当天的输出文件夹
  74. output_date_folder = get_date_folder(app.config['OUTPUT_FOLDER'])
  75. # 处理文件(替换变量)
  76. output_path = os.path.join(output_date_folder, output_filename)
  77. # 构建变量字典,为每个键添加{}
  78. variables = {}
  79. for key, value in data.items():
  80. var_key = '{' + key + '}'
  81. variables[var_key] = str(value) if value is not None else ''
  82. logger.info(f"将替换以下变量: {list(variables.keys())}")
  83. # 检查文档中是否包含这些变量(仅在调试模式下执行)
  84. if app.config['DEBUG_MODE']:
  85. check_variables_in_document(template_path, variables)
  86. # 处理文档并替换变量
  87. process_word_template(template_path, output_path, variables)
  88. logger.info(f"变量替换完成")
  89. # 验证替换是否成功(仅在调试模式下执行)
  90. if app.config['DEBUG_MODE']:
  91. verify_replacement(output_path, variables)
  92. # 计算处理时间
  93. process_time = time.time() - start_time
  94. logger.info(f"文档处理完成,耗时: {process_time:.2f}秒")
  95. # 返回成功信息
  96. return jsonify({
  97. 'message': '文档处理成功',
  98. 'output_filename': output_filename
  99. })
  100. except PermissionError as e:
  101. error_msg = f"权限错误: {str(e)}。可能是文件被占用或没有写入权限。"
  102. logger.error(error_msg)
  103. return jsonify({'error': error_msg}), 500
  104. except Exception as e:
  105. error_msg = f"处理文档时出错: {str(e)}"
  106. logger.error(error_msg, exc_info=True)
  107. return jsonify({'error': error_msg}), 500
  108. @app.route('/download/<filename>')
  109. def download_file(filename):
  110. """
  111. 提供处理后文档的下载
  112. 参数:
  113. filename: 要下载的文件名
  114. 返回:
  115. 处理后的文档文件
  116. """
  117. logger.info(f"请求下载文件: {filename}")
  118. try:
  119. # 获取当天的日期文件夹
  120. today_folder = get_date_folder(app.config['OUTPUT_FOLDER'])
  121. # 首先尝试从当天的文件夹中查找
  122. if os.path.exists(os.path.join(today_folder, filename)):
  123. logger.info(f"文件在当天文件夹中找到: {os.path.join(today_folder, filename)}")
  124. return send_from_directory(today_folder, filename)
  125. # 如果当天文件夹中没有,则尝试在输出根目录查找(兼容旧文件)
  126. if os.path.exists(os.path.join(app.config['OUTPUT_FOLDER'], filename)):
  127. logger.info(f"文件在输出根目录中找到: {os.path.join(app.config['OUTPUT_FOLDER'], filename)}")
  128. return send_from_directory(app.config['OUTPUT_FOLDER'], filename)
  129. # 如果还是没找到,尝试在其他日期文件夹中查找
  130. for item in os.listdir(app.config['OUTPUT_FOLDER']):
  131. item_path = os.path.join(app.config['OUTPUT_FOLDER'], item)
  132. if os.path.isdir(item_path):
  133. file_path = os.path.join(item_path, filename)
  134. if os.path.exists(file_path):
  135. logger.info(f"文件在日期文件夹中找到: {file_path}")
  136. return send_from_directory(item_path, filename)
  137. # 如果所有位置都没找到,返回404错误
  138. logger.warning(f"找不到请求的文件: {filename}")
  139. return jsonify({'error': f'找不到文件: {filename}'}), 404
  140. except Exception as e:
  141. error_msg = f"下载文件时出错: {str(e)}"
  142. logger.error(error_msg, exc_info=True)
  143. return jsonify({'error': error_msg}), 500
  144. if __name__ == '__main__':
  145. # 初始化调度器
  146. init_scheduler(app.config['OUTPUT_FOLDER'])
  147. logger.info("Flask应用程序开始运行")
  148. # 使用host='0.0.0.0'使Flask监听所有网络接口,这样局域网内的其他设备可以访问
  149. # port=5000指定端口号
  150. app.run(host='0.0.0.0', port=5000, debug=True)