app.py 8.7 KB

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