瀏覽代碼

first commit

xbx 1 月之前
當前提交
4d569b1f9c
共有 37 個文件被更改,包括 3251 次插入0 次删除
  1. 78 0
      README.md
  2. 二進制
      __pycache__/scheduler.cpython-313.pyc
  3. 186 0
      app.py
  4. 79 0
      check_template.py
  5. 1737 0
      logs/word_processor.log
  6. 二進制
      outputs/2025-06-26/采购合同模版2_1750910184.docx
  7. 二進制
      outputs/2025-06-26/采购合同模版2_1750910710.docx
  8. 二進制
      outputs/2025-06-26/采购合同模版2_1750910757.docx
  9. 二進制
      outputs/2025-06-26/采购合同模版2_1750911316.docx
  10. 二進制
      outputs/2025-06-26/采购合同模版2_1750911320.docx
  11. 二進制
      outputs/2025-06-26/采购合同模版2_1750911884.docx
  12. 二進制
      outputs/2025-06-26/采购合同模版2_1750912050.docx
  13. 二進制
      outputs/2025-06-26/采购合同模版2_1750912279.docx
  14. 二進制
      outputs/2025-06-26/采购合同模版2_1750912840.docx
  15. 二進制
      outputs/2025-06-26/采购合同模版2_1750915960.docx
  16. 二進制
      outputs/2025-06-26/采购合同模版2_1750916537.docx
  17. 二進制
      outputs/2025-06-26/采购合同模版2_1750916959.docx
  18. 二進制
      outputs/2025-06-26/采购合同模版2_1750917260.docx
  19. 二進制
      outputs/2025-06-26/采购合同模版2_1750917527.docx
  20. 二進制
      outputs/2025-06-26/采购合同模版2_1750917703.docx
  21. 二進制
      outputs/2025-06-26/采购合同模版2_1750917965.docx
  22. 二進制
      outputs/2025-06-26/采购合同模版2_1750917967.docx
  23. 二進制
      outputs/2025-06-26/采购合同模版2_1750917997.docx
  24. 二進制
      outputs/2025-06-26/采购合同模版2_1750917999.docx
  25. 二進制
      outputs/采购合同模版2_1750844904.docx
  26. 4 0
      requirements.txt
  27. 44 0
      scheduler.py
  28. 二進制
      template/采购合同模版2.docx
  29. 448 0
      upload.html
  30. 3 0
      utils/__init__.py
  31. 二進制
      utils/__pycache__/__init__.cpython-313.pyc
  32. 二進制
      utils/__pycache__/docx_processor.cpython-313.pyc
  33. 二進制
      utils/__pycache__/file_utils.cpython-313.pyc
  34. 二進制
      utils/__pycache__/logger.cpython-313.pyc
  35. 368 0
      utils/docx_processor.py
  36. 96 0
      utils/file_utils.py
  37. 208 0
      utils/logger.py

+ 78 - 0
README.md

@@ -0,0 +1,78 @@
+# Word文档上传系统
+
+这是一个简单的Word文档上传系统,包含前端HTML页面和Flask后端API。
+
+## 功能特点
+
+- 前端使用纯HTML和JavaScript (Axios)实现
+- 后端使用Flask提供API
+- 仅支持上传Word文档(.doc和.docx格式)
+- 支持跨域请求
+- 文件大小限制为16MB
+- 防止页面刷新问题
+
+## 安装步骤
+
+1. 安装Python依赖:
+```
+pip install -r requirements.txt
+```
+
+2. 运行Flask服务器:
+```
+python app.py
+```
+
+3. 在浏览器中打开`upload.html`文件
+   - **重要**: 请直接在浏览器中打开HTML文件(使用file://协议),而不是通过Flask服务器访问
+   - 例如: `file:///D:/mycode/pytest/upload.html`
+
+## 使用说明
+
+1. 点击"选择文件"按钮,选择一个Word文档(.doc或.docx)
+2. 点击"上传文档"按钮
+3. 上传成功后,将显示成功消息
+
+## 常见问题解决
+
+### 页面自动刷新问题
+如果上传后页面仍然自动刷新,可能是由以下原因导致:
+
+1. 通过Flask服务器访问HTML文件(应该直接在浏览器中打开HTML文件)
+2. 浏览器插件干扰
+3. 网络请求错误
+
+当前版本已经添加了防止页面刷新的代码:
+- 使用`<form onsubmit="return false;">`阻止表单默认提交
+- 添加`e.preventDefault()`防止点击事件导致页面刷新
+- 添加调试信息帮助排查问题
+
+## 目录结构
+
+- `app.py` - Flask后端API
+- `upload.html` - 前端HTML页面
+- `requirements.txt` - 项目依赖
+- `uploads/` - 上传文件保存目录(自动创建)
+
+## API说明
+
+### POST /upload
+上传Word文档
+
+**请求参数**:
+- `file`: 文件对象,必须是.doc或.docx格式
+
+**响应**:
+```json
+{
+  "message": "Word文档上传成功",
+  "filename": "文件名.docx",
+  "size": 12345
+}
+```
+
+### GET /uploads/<filename>
+获取已上传的文件
+
+**响应**:
+- 返回请求的文件 

二進制
__pycache__/scheduler.cpython-313.pyc


+ 186 - 0
app.py

@@ -0,0 +1,186 @@
+from flask import Flask, request, jsonify, send_from_directory
+import os
+import time
+
+# 导入自定义模块
+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'] = 'template'  # 模板文件目录
+app.config['OUTPUT_FOLDER'] = 'outputs'  # 处理后文件保存目录
+app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024  # 限制上传文件大小为16MB
+app.config['DEBUG_MODE'] = False  # 调试模式:True启用变量检查和验证,False禁用
+app.config['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}")
+    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)
+        
+        # 如果当天文件夹中没有,则尝试在输出根目录查找(兼容旧文件)
+        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)
+        
+        # 如果还是没找到,尝试在其他日期文件夹中查找
+        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)
+        
+        # 如果所有位置都没找到,返回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监听所有网络接口,这样局域网内的其他设备可以访问
+    # port=5000指定端口号
+    app.run(host='0.0.0.0', port=5000, debug=True) 

+ 79 - 0
check_template.py

@@ -0,0 +1,79 @@
+"""
+检查模板文件中的变量
+"""
+
+import os
+from docx import Document
+from utils.logger import logger
+
+def check_template_variables(template_path):
+    """
+    检查模板文件中的所有变量
+    
+    参数:
+        template_path: 模板文件路径
+        
+    返回:
+        set: 找到的变量集合
+    """
+    logger.info(f"开始检查模板文件: {template_path}")
+    
+    if not os.path.exists(template_path):
+        logger.error(f"模板文件不存在: {template_path}")
+        return set()
+    
+    doc = Document(template_path)
+    
+    # 收集文档中的所有文本
+    all_text = []
+    
+    # 检查段落中的文本
+    for i, paragraph in enumerate(doc.paragraphs):
+        if paragraph.text.strip():
+            all_text.append(paragraph.text)
+            logger.info(f"段落 {i}: {paragraph.text}")
+    
+    # 检查表格中的文本
+    for t_idx, table in enumerate(doc.tables):
+        for r_idx, row in enumerate(table.rows):
+            for c_idx, cell in enumerate(row.cells):
+                for p_idx, paragraph in enumerate(cell.paragraphs):
+                    if paragraph.text.strip():
+                        all_text.append(paragraph.text)
+                        logger.info(f"表格 {t_idx}, 行 {r_idx}, 列 {c_idx}, 段落 {p_idx}: {paragraph.text}")
+    
+    # 查找所有变量
+    variables = set()
+    for text in all_text:
+        # 查找形如 {xxx} 的模式
+        start = 0
+        while True:
+            start = text.find('{', start)
+            if start == -1:
+                break
+            end = text.find('}', start)
+            if end == -1:
+                break
+            potential_var = text[start:end+1]
+            variables.add(potential_var)
+            start = end + 1
+    
+    logger.info(f"在模板中找到以下变量: {variables}")
+    return variables
+
+if __name__ == "__main__":
+    template_folder = 'template'
+    template_files = [f for f in os.listdir(template_folder) 
+                    if f.lower().endswith('.docx') and not f.startswith('~$')]
+    
+    if not template_files:
+        logger.error("模板文件夹中没有找到有效的.docx文件")
+    else:
+        # 使用第一个模板文件
+        template_filename = template_files[0]
+        template_path = os.path.join(template_folder, template_filename)
+        variables = check_template_variables(template_path)
+        
+        print("\n在模板中找到以下变量:")
+        for var in sorted(variables):
+            print(f"- {var}") 

File diff suppressed because it is too large
+ 1737 - 0
logs/word_processor.log


二進制
outputs/2025-06-26/采购合同模版2_1750910184.docx


二進制
outputs/2025-06-26/采购合同模版2_1750910710.docx


二進制
outputs/2025-06-26/采购合同模版2_1750910757.docx


二進制
outputs/2025-06-26/采购合同模版2_1750911316.docx


二進制
outputs/2025-06-26/采购合同模版2_1750911320.docx


二進制
outputs/2025-06-26/采购合同模版2_1750911884.docx


二進制
outputs/2025-06-26/采购合同模版2_1750912050.docx


二進制
outputs/2025-06-26/采购合同模版2_1750912279.docx


二進制
outputs/2025-06-26/采购合同模版2_1750912840.docx


二進制
outputs/2025-06-26/采购合同模版2_1750915960.docx


二進制
outputs/2025-06-26/采购合同模版2_1750916537.docx


二進制
outputs/2025-06-26/采购合同模版2_1750916959.docx


二進制
outputs/2025-06-26/采购合同模版2_1750917260.docx


二進制
outputs/2025-06-26/采购合同模版2_1750917527.docx


二進制
outputs/2025-06-26/采购合同模版2_1750917703.docx


二進制
outputs/2025-06-26/采购合同模版2_1750917965.docx


二進制
outputs/2025-06-26/采购合同模版2_1750917967.docx


二進制
outputs/2025-06-26/采购合同模版2_1750917997.docx


二進制
outputs/2025-06-26/采购合同模版2_1750917999.docx


二進制
outputs/采购合同模版2_1750844904.docx


+ 4 - 0
requirements.txt

@@ -0,0 +1,4 @@
+Flask==2.3.3
+flask-cors==4.0.0
+python-docx==0.8.11
+APScheduler==3.10.4 

+ 44 - 0
scheduler.py

@@ -0,0 +1,44 @@
+"""
+定时任务调度模块
+提供定时任务的调度和管理功能
+"""
+
+from apscheduler.schedulers.background import BackgroundScheduler
+from apscheduler.triggers.cron import CronTrigger
+from utils.logger import logger
+from utils.file_utils import clean_old_files
+
+def init_scheduler(output_folder):
+    """
+    初始化调度器,添加定时清理任务
+    
+    参数:
+        output_folder: 输出文件根目录
+    
+    返回:
+        scheduler: 已启动的调度器对象
+    """
+    logger.info("初始化定时任务调度器")
+    scheduler = BackgroundScheduler()
+    
+    # 每天凌晨2点执行清理任务
+    scheduler.add_job(
+        lambda: clean_old_files(output_folder),
+        trigger=CronTrigger(hour=2, minute=0),
+        id='clean_old_files_job',
+        name='清理前一天的输出文件',
+        replace_existing=True
+    )
+    
+    scheduler.start()
+    logger.info("定时清理任务已启动,将在每天凌晨2:00执行")
+    
+    # 添加关闭时的清理
+    import atexit
+    def shutdown_handler():
+        scheduler.shutdown()
+        logger.info("应用程序关闭,调度器已停止")
+    
+    atexit.register(shutdown_handler)
+    
+    return scheduler 

二進制
template/采购合同模版2.docx


+ 448 - 0
upload.html

@@ -0,0 +1,448 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+  <head>
+    <meta charset="UTF-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Word模板替换</title>
+    <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
+    <style>
+      /* 全局样式 */
+      body {
+        font-family: Arial, sans-serif;
+        max-width: 800px;
+        margin: 0 auto;
+        padding: 20px;
+        background-color: #f5f5f5;
+      }
+      .container {
+        background-color: white;
+        padding: 20px;
+        border-radius: 8px;
+        box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
+      }
+      h1 {
+        color: #333;
+        text-align: center;
+      }
+
+      /* 表单样式 */
+      .form {
+        margin-top: 20px;
+      }
+      .form-group {
+        margin-bottom: 15px;
+      }
+      label {
+        display: block;
+        margin-bottom: 5px;
+        font-weight: bold;
+      }
+      .text-input {
+        width: 100%;
+        padding: 8px 10px;
+        border: 1px solid #ddd;
+        border-radius: 4px;
+        box-sizing: border-box;
+      }
+      textarea.text-input {
+        resize: vertical;
+        min-height: 60px;
+        font-family: Arial, sans-serif;
+      }
+
+      /* 按钮样式 */
+      .btn {
+        background-color: #4caf50;
+        color: white;
+        padding: 12px 20px;
+        border: none;
+        border-radius: 4px;
+        cursor: pointer;
+        font-size: 16px;
+        display: block;
+        width: 100%;
+        margin-top: 20px;
+      }
+      .btn:hover {
+        background-color: #45a049;
+      }
+      .btn:disabled {
+        background-color: #cccccc;
+        cursor: not-allowed;
+      }
+
+      /* 状态消息样式 */
+      .status {
+        margin-top: 20px;
+        padding: 15px;
+        border-radius: 4px;
+        display: none;
+      }
+      .success {
+        background-color: #dff0d8;
+        color: #3c763d;
+        border: 1px solid #d6e9c6;
+      }
+      .error {
+        background-color: #f2dede;
+        color: #a94442;
+        border: 1px solid #ebccd1;
+      }
+
+      /* 下载链接样式 */
+      .download-link {
+        display: inline-block;
+        margin-top: 15px;
+        padding: 8px 15px;
+        background-color: #337ab7;
+        color: white;
+        text-decoration: none;
+        border-radius: 4px;
+      }
+      .download-link:hover {
+        background-color: #286090;
+      }
+
+      /* 字段操作样式 */
+      .field-actions {
+        margin-top: 20px;
+        text-align: center;
+      }
+      .add-field-btn {
+        background-color: #5bc0de;
+        color: white;
+        padding: 8px 15px;
+        border: none;
+        border-radius: 4px;
+        cursor: pointer;
+        margin-right: 10px;
+      }
+      .add-field-btn:hover {
+        background-color: #46b8da;
+      }
+      .field-row {
+        display: flex;
+        margin-bottom: 10px;
+        align-items: flex-start;
+      }
+      .field-key {
+        flex: 1;
+        margin-right: 10px;
+      }
+      .field-value {
+        flex: 2;
+        margin-right: 10px;
+      }
+      .field-label {
+        width: 100px;
+        text-align: left;
+        color: #666;
+        font-size: 14px;
+      }
+      .remove-field-btn {
+        background-color: #d9534f;
+        color: white;
+        border: none;
+        border-radius: 4px;
+        cursor: pointer;
+        padding: 5px 10px;
+      }
+      .remove-field-btn:hover {
+        background-color: #c9302c;
+      }
+    </style>
+  </head>
+  <body>
+    <div class="container">
+      <h1>Word模板替换系统</h1>
+
+      <div class="form">
+        <h3>输入模板变量</h3>
+        <p>系统将使用固定模板,根据下方输入的字段自动替换模板中的变量。</p>
+        <p>
+          请填写以下字段,系统将自动替换模板中对应的变量(如{name}、{address}等)。
+        </p>
+
+        <div id="fieldsContainer">
+          <!-- 动态字段将在这里添加 -->
+          <div class="field-row">
+            <input
+              type="text"
+              class="text-input field-key"
+              placeholder="字段名"
+              value="person1"
+              readonly
+            />
+            <input
+              type="text"
+              class="text-input field-value"
+              placeholder="甲方"
+              value="杭州遁地科技"
+            />
+            <span class="field-label">甲方</span>
+            <button
+              class="remove-field-btn"
+              onclick="removeField(this)"
+              style="visibility: hidden"
+            >
+              删除
+            </button>
+          </div>
+          <div class="field-row">
+            <input
+              type="text"
+              class="text-input field-key"
+              placeholder="字段名"
+              value="person2"
+              readonly
+            />
+            <input
+              type="text"
+              class="text-input field-value"
+              placeholder="乙方"
+              value="杭州飞天科技"
+            />
+            <span class="field-label">乙方</span>
+            <button
+              class="remove-field-btn"
+              onclick="removeField(this)"
+              style="visibility: hidden"
+            >
+              删除
+            </button>
+          </div>
+          <div class="field-row">
+            <input
+              type="text"
+              class="text-input field-key"
+              placeholder="字段名"
+              value="name"
+              readonly
+            />
+            <input
+              type="text"
+              class="text-input field-value"
+              placeholder="货物名称"
+            />
+            <span class="field-label">货物名称</span>
+            <button
+              class="remove-field-btn"
+              onclick="removeField(this)"
+              style="visibility: hidden"
+            >
+              删除
+            </button>
+          </div>
+          <div class="field-row">
+            <input
+              type="text"
+              class="text-input field-key"
+              placeholder="字段名"
+              value="address"
+              readonly
+            />
+            <input
+              type="text"
+              class="text-input field-value"
+              placeholder="请输入原产地"
+            />
+            <span class="field-label">原产地</span>
+            <button
+              class="remove-field-btn"
+              onclick="removeField(this)"
+              style="visibility: hidden"
+            >
+              删除
+            </button>
+          </div>
+          <div class="field-row">
+            <input
+              type="text"
+              class="text-input field-key"
+              placeholder="字段名"
+              value="address1"
+              readonly
+            />
+            <input
+              type="text"
+              class="text-input field-value"
+              placeholder="请输入目的地口岸"
+            />
+            <span class="field-label">目的地口岸</span>
+            <button
+              class="remove-field-btn"
+              onclick="removeField(this)"
+              style="visibility: hidden"
+            >
+              删除
+            </button>
+          </div>
+          <div class="field-row">
+            <input
+              type="text"
+              class="text-input field-key"
+              placeholder="字段名"
+              value="addType"
+              readonly
+            />
+            <input
+              type="text"
+              class="text-input field-value"
+              placeholder="请输入交货方式"
+            />
+            <span class="field-label">交货方式</span>
+            <button
+              class="remove-field-btn"
+              onclick="removeField(this)"
+              style="visibility: hidden"
+            >
+              删除
+            </button>
+          </div>
+
+          <div class="field-row" style="height: 300px">
+            <input
+              type="text"
+              class="text-input field-key"
+              placeholder="字段名"
+              value="productGuide"
+              readonly
+            />
+            <textarea
+              class="text-input field-value"
+              placeholder="请输入产品规格"
+              rows="15"
+            >
+我是一瓶番茄酱+沙拉酱
+我是一瓶番茄酱+沙拉酱
+我是一瓶番茄酱+沙拉酱
+我是一瓶番茄酱+沙拉酱
+我是一瓶番茄酱+沙拉酱
+我是一瓶番茄酱+沙拉酱
+我是一瓶番茄酱+沙拉酱
+我是一瓶番茄酱+沙拉酱
+我是一瓶番茄酱+沙拉酱
+我是一瓶番茄酱+沙拉酱
+我是一瓶番茄酱+沙拉酱
+我是一瓶番茄酱+沙拉酱
+我是一瓶番茄酱+沙拉酱
+我是一瓶番茄酱+沙拉酱
+我是一瓶番茄酱+沙拉酱
+
+
+</textarea
+            >
+            <span class="field-label">产品规格</span>
+            <button
+              class="remove-field-btn"
+              onclick="removeField(this)"
+              style="visibility: hidden"
+            >
+              删除
+            </button>
+          </div>
+        </div>
+
+        <div class="field-actions">
+          <!-- 移除添加字段按钮 -->
+        </div>
+
+        <button id="generateBtn" class="btn">生成文档</button>
+      </div>
+
+      <!-- 状态消息区域 -->
+      <div id="statusMessage" class="status"></div>
+
+      <!-- 下载区域 -->
+      <div id="downloadSection" style="display: none; margin-top: 20px">
+        <p>文档已成功生成,点击下面的链接下载:</p>
+        <a id="downloadLink" href="#" class="download-link" download
+          >下载文档</a
+        >
+      </div>
+    </div>
+
+    <script>
+      // DOM元素引用
+      const generateBtn = document.getElementById("generateBtn");
+      const statusMessage = document.getElementById("statusMessage");
+      const downloadSection = document.getElementById("downloadSection");
+      const downloadLink = document.getElementById("downloadLink");
+      const fieldsContainer = document.getElementById("fieldsContainer");
+      
+      // 获取当前页面的主机名和端口,用于API请求
+      const apiBaseUrl = `${window.location.protocol}//${window.location.hostname}:5000`;
+      
+      // 收集所有字段
+      function collectFields() {
+        const data = {};
+        const fieldRows = fieldsContainer.querySelectorAll(".field-row");
+
+        fieldRows.forEach((row) => {
+          const key = row.querySelector(".field-key").value.trim();
+          const value = row.querySelector(".field-value").value.trim();
+
+          if (key) {
+            data[key] = value;
+          }
+        });
+
+        return data;
+      }
+
+      // 生成文档
+      generateBtn.addEventListener("click", function (e) {
+        e.preventDefault();
+
+        // 收集字段数据
+        const data = collectFields();
+
+        // 更新UI状态为处理中
+        generateBtn.disabled = true;
+        generateBtn.textContent = "处理中...";
+        statusMessage.className = "status";
+        statusMessage.style.display = "none";
+
+        // 发送请求到Flask后端
+        axios
+          .post(`${apiBaseUrl}/process_file`, data, {
+            headers: {
+              "Content-Type": "application/json",
+            },
+          })
+          .then((response) => {
+            // 处理成功响应
+            statusMessage.className = "status success";
+            statusMessage.style.display = "block";
+            statusMessage.textContent = `成功: ${response.data.message}`;
+            console.log("成功:", response.data);
+
+            // 更新下载链接
+            downloadLink.href = `${apiBaseUrl}/download/${response.data.output_filename}`;
+
+            // 显示下载部分
+            downloadSection.style.display = "block";
+          })
+          .catch((error) => {
+            // 处理错误响应
+            statusMessage.className = "status error";
+            statusMessage.style.display = "block";
+
+            if (error.response) {
+              statusMessage.textContent = `错误: ${
+                error.response.data.error || "处理失败"
+              }`;
+            } else {
+              statusMessage.textContent = "错误: 服务器连接失败";
+            }
+            console.error("处理错误:", error);
+          })
+          .finally(() => {
+            // 恢复按钮状态
+            generateBtn.disabled = false;
+            generateBtn.textContent = "生成文档";
+          });
+      });
+    </script>
+  </body>
+</html>

+ 3 - 0
utils/__init__.py

@@ -0,0 +1,3 @@
+"""
+工具包初始化文件
+""" 

二進制
utils/__pycache__/__init__.cpython-313.pyc


二進制
utils/__pycache__/docx_processor.cpython-313.pyc


二進制
utils/__pycache__/file_utils.cpython-313.pyc


二進制
utils/__pycache__/logger.cpython-313.pyc


+ 368 - 0
utils/docx_processor.py

@@ -0,0 +1,368 @@
+"""
+Word文档处理模块
+提供Word文档的处理和变量替换功能
+"""
+
+import os
+import time
+from docx import Document
+from docx.oxml.ns import qn  # 导入qn函数用于处理中文字体
+from .logger import logger
+
+def check_variables_in_document(docx_path, variables):
+    """
+    检查文档中是否包含指定的变量,并输出详细信息用于调试
+    
+    参数:
+        docx_path: Word文档路径
+        variables: 要检查的变量字典 {变量名: 替换值}
+    """
+    logger.debug(f"检查文档变量: {docx_path}")
+    doc = Document(docx_path)
+    
+    # 收集文档中的所有文本
+    all_text = []
+    
+    # 检查段落中的变量(简化输出)
+    for i, paragraph in enumerate(doc.paragraphs):
+        all_text.append(paragraph.text)
+        if paragraph.text.strip():  # 只记录非空段落
+            logger.debug(f"段落 {i}: {paragraph.text}")
+    
+    # 检查表格中的变量(简化输出)
+    for t_idx, table in enumerate(doc.tables):
+        for r_idx, row in enumerate(table.rows):
+            for c_idx, cell in enumerate(row.cells):
+                for p_idx, paragraph in enumerate(cell.paragraphs):
+                    all_text.append(paragraph.text)
+                    if paragraph.text.strip():  # 只记录非空段落
+                        logger.debug(f"表格 {t_idx}, 行 {r_idx}, 列 {c_idx}, 段落 {p_idx}: {paragraph.text}")
+    
+    # 检查是否找到所有变量
+    for var_name in variables.keys():
+        found = False
+        for text in all_text:
+            if var_name in text:
+                found = True
+                logger.info(f"变量 '{var_name}' 在文档中找到!")
+                break
+        
+        if not found:
+            logger.warning(f"变量 '{var_name}' 在文档中未找到! 将被替换为空字符串。")
+            
+    # 检查文档中可能存在但未提供的变量
+    potential_vars = set()
+    for text in all_text:
+        # 查找形如 {xxx} 的模式
+        start = 0
+        while True:
+            start = text.find('{', start)
+            if start == -1:
+                break
+            end = text.find('}', start)
+            if end == -1:
+                break
+            potential_var = text[start:end+1]
+            potential_vars.add(potential_var)
+            start = end + 1
+    
+    # 检查是否有未提供的变量
+    for var in potential_vars:
+        if var not in variables:
+            logger.warning(f"文档中存在变量 '{var}',但未提供替换值!")
+
+def verify_replacement(docx_path, variables):
+    """
+    验证变量是否已被成功替换
+    
+    参数:
+        docx_path: 处理后的Word文档路径
+        variables: 要验证的变量字典 {变量名: 替换值}
+    """
+    logger.debug(f"验证变量替换: {docx_path}")
+    doc = Document(docx_path)
+    
+    # 收集文档中的所有文本
+    all_text = []
+    
+    # 检查段落
+    for paragraph in doc.paragraphs:
+        all_text.append(paragraph.text)
+    
+    # 检查表格
+    for table in doc.tables:
+        for row in table.rows:
+            for cell in row.cells:
+                for paragraph in cell.paragraphs:
+                    all_text.append(paragraph.text)
+    
+    # 检查是否所有变量都已被替换
+    replacement_failed = False
+    for var_name in variables.keys():
+        for text in all_text:
+            if var_name in text:
+                logger.warning(f"变量 '{var_name}' 在处理后的文档中仍然存在! 替换可能失败。")
+                replacement_failed = True
+                break
+    
+    if not replacement_failed:
+        logger.info("所有变量都已成功替换")
+
+def replace_text_in_paragraph(paragraph, variables):
+    """
+    在段落中替换变量,同时保留文本格式
+    
+    此方法通过以下步骤工作:
+    1. 收集段落中的所有runs(文本片段)
+    2. 清空段落
+    3. 处理每个run中的变量
+    4. 创建新的run,保留原始格式
+    5. 将处理后的文本添加回段落
+    
+    参数:
+        paragraph: 要处理的段落对象
+        variables: 变量替换字典 {变量名: 替换值}
+    """
+    # 检查段落是否包含任何变量
+    contains_variable = False
+    found_variables = []
+    
+    # 记录原始段落文本
+    original_text = paragraph.text
+    
+    for var_name in variables.keys():
+        if var_name in original_text:
+            contains_variable = True
+            found_variables.append(var_name)
+    
+    if not contains_variable:
+        return
+    
+    logger.info(f"在段落中找到变量: {found_variables}")
+    logger.info(f"原始段落文本: {original_text}")
+    
+    # 存储原始的运行对象
+    runs = [run for run in paragraph.runs]
+    paragraph.clear()
+    
+    # 对每个运行进行处理
+    for i, run in enumerate(runs):
+        text = run.text
+        original_run_text = text
+        
+        # 替换所有变量
+        for var_name, var_value in variables.items():
+            if var_name in text:
+                logger.info(f"在run {i}中替换变量 '{var_name}' 为 '{var_value}'")
+                logger.info(f"替换前文本: '{text}'")
+                text = text.replace(var_name, var_value)
+                logger.info(f"替换后文本: '{text}'")
+        
+        # 如果文本没有变化,记录一下
+        if original_run_text == text:
+            logger.debug(f"Run {i} 文本未变化: '{text}'")
+        
+        # 创建新的运行,保留原始格式
+        new_run = paragraph.add_run(text)
+        
+        # 复制格式
+        new_run.bold = run.bold
+        new_run.italic = run.italic
+        new_run.underline = run.underline
+        new_run.font.name = run.font.name
+        new_run.font.size = run.font.size
+        
+        # 专门处理中文字体,确保东亚字体(如仿宋、宋体等)能够正确保留
+        if hasattr(run._element, 'rPr') and run._element.rPr is not None:
+            # 检查是否有rFonts元素
+            rfonts = run._element.rPr.xpath('./w:rFonts')
+            if rfonts and hasattr(rfonts[0], 'get'):
+                # 获取东亚字体属性
+                east_asia_font = rfonts[0].get(qn('w:eastAsia'))
+                if east_asia_font:
+                    # 设置新run的东亚字体
+                    new_run._element.rPr.rFonts.set(qn('w:eastAsia'), east_asia_font)
+                    logger.debug(f"设置东亚字体: {east_asia_font}")
+        
+        if run.font.color.rgb is not None:
+            new_run.font.color.rgb = run.font.color.rgb
+    
+    # 记录处理后的段落文本
+    logger.info(f"处理后段落文本: {paragraph.text}")
+
+def process_word_template(template_path, output_path, variables):
+    """
+    处理Word文档,替换其中的模板变量
+    
+    参数:
+        template_path: 模板文档路径
+        output_path: 输出文档路径
+        variables: 变量替换字典 {变量名: 替换值}
+    """
+    # 记录处理开始
+    start_time = time.time()
+    logger.info(f"开始处理文档: {os.path.basename(template_path)}")
+    logger.info(f"需要替换的变量: {list(variables.keys())}")
+    
+    # 只处理docx文件
+    if not template_path.lower().endswith('.docx'):
+        logger.error("只支持.docx格式的文件")
+        raise ValueError("只支持.docx格式的文件")
+    
+    doc = Document(template_path)
+    
+    # 统计替换次数
+    replacement_count = 0
+    
+    # 处理段落中的变量
+    logger.info("开始处理文档段落...")
+    paragraph_count = 0
+    for i, paragraph in enumerate(doc.paragraphs):
+        has_var = any(var_name in paragraph.text for var_name in variables.keys())
+        if has_var:
+            paragraph_count += 1
+            replace_text_in_paragraph_improved(paragraph, variables)
+            replacement_count += 1
+    
+    # 处理表格中的变量
+    logger.info("开始处理文档表格...")
+    table_cell_count = 0
+    for t_idx, table in enumerate(doc.tables):
+        for r_idx, row in enumerate(table.rows):
+            for c_idx, cell in enumerate(row.cells):
+                for p_idx, paragraph in enumerate(cell.paragraphs):
+                    has_var = any(var_name in paragraph.text for var_name in variables.keys())
+                    if has_var:
+                        table_cell_count += 1
+                        replace_text_in_paragraph_improved(paragraph, variables)
+                        replacement_count += 1
+    
+    logger.info(f"处理完成: 共处理了 {paragraph_count} 个段落和 {table_cell_count} 个表格单元格")
+    
+    # 保存生成的文档
+    try:
+        doc.save(output_path)
+        logger.info(f"文档已保存: {os.path.basename(output_path)}")
+    except PermissionError:
+        # 如果文件被占用,尝试使用新的文件名
+        dir_name = os.path.dirname(output_path)
+        base_name = os.path.basename(output_path)
+        new_output_path = os.path.join(dir_name, f"new_{base_name}")
+        logger.warning(f"文件被占用,尝试保存到新位置: {os.path.basename(new_output_path)}")
+        doc.save(new_output_path)
+        # 重命名原始路径,以便后续代码能正确引用
+        os.rename(new_output_path, output_path)
+        logger.info(f"文件已重命名为: {os.path.basename(output_path)}")
+    
+    # 记录处理时间
+    process_time = time.time() - start_time
+    logger.info(f"文档处理完成,耗时: {process_time:.2f}秒")
+
+def replace_text_in_paragraph_improved(paragraph, variables):
+    """
+    改进的段落变量替换方法,处理变量可能被分割在多个runs的情况
+    
+    参数:
+        paragraph: 要处理的段落对象
+        variables: 变量替换字典 {变量名: 替换值}
+    """
+    # 记录原始段落文本
+    original_text = paragraph.text
+    
+    # 检查段落是否包含任何变量
+    found_variables = []
+    for var_name in variables.keys():
+        if var_name in original_text:
+            found_variables.append(var_name)
+    
+    if not found_variables:
+        return
+    
+    logger.info(f"发现需要替换的变量: {found_variables}")
+    logger.info(f"原始文本: {original_text}")
+    
+    # 尝试使用原始的替换方法
+    try:
+        # 先尝试使用原始替换方法
+        replace_text_in_paragraph(paragraph, variables)
+        
+        # 检查是否所有变量都被替换
+        all_replaced = True
+        for var_name in found_variables:
+            if var_name in paragraph.text:
+                all_replaced = False
+                logger.warning(f"变量 '{var_name}' 未被替换,尝试使用备用方法")
+                break
+        
+        if all_replaced:
+            logger.info("所有变量已成功替换")
+            return
+    except Exception as e:
+        logger.warning(f"原始替换方法失败: {str(e)},尝试使用备用方法")
+    
+    # 如果原始方法失败或未替换所有变量,使用备用方法
+    logger.info("使用备用替换方法")
+    
+    # 记录原始格式信息
+    original_runs = []
+    for i, run in enumerate(paragraph.runs):
+        original_runs.append({
+            'text': run.text,
+            'bold': run.bold,
+            'italic': run.italic,
+            'underline': run.underline,
+            'font_name': run.font.name,
+            'font_size': run.font.size,
+            'font_color': run.font.color.rgb,
+            'east_asia_font': None
+        })
+        
+        # 获取东亚字体信息
+        if hasattr(run._element, 'rPr') and run._element.rPr is not None:
+            rfonts = run._element.rPr.xpath('./w:rFonts')
+            if rfonts and hasattr(rfonts[0], 'get'):
+                east_asia_font = rfonts[0].get(qn('w:eastAsia'))
+                if east_asia_font:
+                    original_runs[-1]['east_asia_font'] = east_asia_font
+    
+    # 创建一个新的段落文本,替换所有变量
+    new_text = original_text
+    for var_name, var_value in variables.items():
+        if var_name in new_text:
+            logger.info(f"替换变量 '{var_name}' 为 '{var_value}'")
+            new_text = new_text.replace(var_name, var_value)
+    
+    # 如果文本没有变化,不需要进一步处理
+    if new_text == original_text:
+        logger.info("文本未变化,跳过处理")
+        return
+    
+    # 记录处理后的段落文本
+    logger.info(f"替换后文本: {new_text}")
+    
+    # 清空段落
+    paragraph.clear()
+    
+    # 使用最简单的方法:使用第一个run的格式,但不应用下划线
+    # 这样可以确保文本不会全部带有下划线
+    default_format = original_runs[0] if original_runs else None
+    
+    if default_format:
+        new_run = paragraph.add_run(new_text)
+        new_run.bold = default_format['bold']
+        new_run.italic = default_format['italic']
+        new_run.underline = False  # 明确设置为不使用下划线
+        new_run.font.name = default_format['font_name']
+        
+        if default_format['font_size'] is not None:
+            new_run.font.size = default_format['font_size']
+            
+        if default_format['font_color'] is not None:
+            new_run.font.color.rgb = default_format['font_color']
+        
+        # 设置东亚字体
+        if default_format['east_asia_font']:
+            new_run._element.rPr.rFonts.set(qn('w:eastAsia'), default_format['east_asia_font'])
+    else:
+        # 如果没有原始格式信息,直接添加文本
+        paragraph.add_run(new_text) 

+ 96 - 0
utils/file_utils.py

@@ -0,0 +1,96 @@
+"""
+文件处理工具模块
+提供文件操作相关的功能
+"""
+
+import os
+import time
+import datetime
+import shutil
+from .logger import logger
+
+def get_date_folder(base_folder):
+    """
+    获取当前日期对应的文件夹路径,如果不存在则创建
+    
+    参数:
+        base_folder: 基础文件夹路径
+        
+    返回:
+        str: 日期文件夹路径
+    """
+    today = datetime.datetime.now().strftime('%Y-%m-%d')
+    date_folder = os.path.join(base_folder, today)
+    os.makedirs(date_folder, exist_ok=True)
+    return date_folder
+
+def get_safe_filename(base_name, extension):
+    """
+    生成一个带时间戳的安全文件名,避免文件名冲突
+    
+    参数:
+        base_name: 原始文件名基础部分
+        extension: 文件扩展名
+        
+    返回:
+        str: 安全的文件名
+    """
+    timestamp = int(time.time())
+    return f"{base_name}_{timestamp}{extension}"
+
+def clean_old_files(output_folder):
+    """
+    清理前一天的输出文件
+    每天凌晨2点自动运行
+    
+    参数:
+        output_folder: 输出文件根目录
+    """
+    logger.info("开始执行文件清理任务")
+    try:
+        # 获取前一天的日期
+        yesterday = (datetime.datetime.now() - datetime.timedelta(days=1)).strftime('%Y-%m-%d')
+        yesterday_folder = os.path.join(output_folder, yesterday)
+        
+        # 如果文件夹存在,则删除
+        if os.path.exists(yesterday_folder) and os.path.isdir(yesterday_folder):
+            # 记录删除前的文件数量和大小
+            files_count = 0
+            total_size = 0
+            for root, dirs, files in os.walk(yesterday_folder):
+                files_count += len(files)
+                for f in files:
+                    file_path = os.path.join(root, f)
+                    total_size += os.path.getsize(file_path)
+            
+            # 删除文件夹
+            shutil.rmtree(yesterday_folder)
+            logger.info(f"已清理前一天({yesterday})的输出文件夹,共删除 {files_count} 个文件,总大小 {total_size/1024/1024:.2f} MB")
+        else:
+            logger.info(f"前一天({yesterday})的输出文件夹不存在,无需清理")
+    except Exception as e:
+        logger.error(f"清理文件时出错: {str(e)}", exc_info=True)
+
+def allowed_file(file, allowed_extensions):
+    """
+    检查上传的文件是否为允许的类型
+    
+    参数:
+        file: 上传的文件对象
+        allowed_extensions: 允许的文件扩展名集合
+        
+    返回:
+        bool: 文件类型是否允许
+    """
+    # 检查文件扩展名
+    extension = os.path.splitext(file.filename)[1].lower() if file.filename else ''
+    if extension not in allowed_extensions:
+        return False
+    
+    # 检查MIME类型
+    content_type = file.content_type
+    allowed_mime_types = {
+        'application/msword',  # .doc
+        'application/vnd.openxmlformats-officedocument.wordprocessingml.document'  # .docx
+    }
+    return content_type in allowed_mime_types 

+ 208 - 0
utils/logger.py

@@ -0,0 +1,208 @@
+"""
+日志配置和功能模块
+提供应用程序的日志记录功能
+"""
+
+import os
+import logging
+import gzip
+import shutil
+import glob
+from logging.handlers import RotatingFileHandler, TimedRotatingFileHandler
+import time
+from typing import Optional, IO, Any
+
+# 添加处理ID过滤器
+class ProcessIdFilter(logging.Filter):
+    def __init__(self):
+        super().__init__()
+        self.process_id = None
+        
+    def filter(self, record):
+        if not hasattr(record, 'process_id'):
+            if self.process_id is None:
+                # 生成唯一的处理ID
+                self.process_id = int(time.time() * 1000) % 10000
+            record.process_id = f"PROC-{self.process_id:04d}"
+        return True
+
+# 自定义日志处理器,支持压缩
+class CompressedRotatingFileHandler(RotatingFileHandler):
+    def __init__(self, filename, mode='a', maxBytes=0, backupCount=0, encoding=None, delay=False):
+        super().__init__(filename, mode, maxBytes, backupCount, encoding, delay)
+        
+    def doRollover(self):
+        """
+        执行日志滚动时,压缩旧的日志文件
+        """
+        # 关闭流
+        if self.stream:
+            self.stream.close()
+            self.stream = None  # type: ignore
+            
+        if self.backupCount > 0:
+            # 移动旧的日志文件
+            for i in range(self.backupCount - 1, 0, -1):
+                sfn = f"{self.baseFilename}.{i}"
+                dfn = f"{self.baseFilename}.{i + 1}"
+                if os.path.exists(sfn):
+                    if os.path.exists(dfn):
+                        os.remove(dfn)
+                    os.rename(sfn, dfn)
+            
+            dfn = f"{self.baseFilename}.1"
+            if os.path.exists(dfn):
+                os.remove(dfn)
+                
+            # 压缩当前日志文件
+            try:
+                with open(self.baseFilename, 'rb') as f_in:
+                    with gzip.open(f"{dfn}.gz", 'wb') as f_out:
+                        shutil.copyfileobj(f_in, f_out)
+            except Exception:
+                # 如果压缩失败,回退到普通复制
+                shutil.copy2(self.baseFilename, dfn)
+                
+        # 重新打开流
+        if not self.delay:
+            self.stream = self._open()
+
+# 自定义TimedRotatingFileHandler,支持压缩
+class CompressedTimedRotatingFileHandler(TimedRotatingFileHandler):
+    def __init__(self, filename, when='h', interval=1, backupCount=0, encoding=None, delay=False, utc=False, atTime=None):
+        super().__init__(filename, when, interval, backupCount, encoding, delay, utc, atTime)
+        
+    def doRollover(self):
+        """
+        执行日志滚动时,压缩旧的日志文件
+        """
+        # 执行标准的滚动
+        super().doRollover()
+        
+        # 查找最新创建的备份文件并压缩
+        backup_files = glob.glob(f"{self.baseFilename}.*")
+        for backup_file in backup_files:
+            if not backup_file.endswith('.gz') and os.path.isfile(backup_file):
+                try:
+                    with open(backup_file, 'rb') as f_in:
+                        with gzip.open(f"{backup_file}.gz", 'wb') as f_out:
+                            shutil.copyfileobj(f_in, f_out)
+                    os.remove(backup_file)  # 删除原始未压缩文件
+                except Exception:
+                    # 压缩失败时不做处理,保留原始文件
+                    pass
+
+def clean_old_logs(log_folder, max_days=30):
+    """
+    清理超过指定天数的日志文件
+    
+    参数:
+        log_folder: 日志目录
+        max_days: 保留的最大天数
+    """
+    try:
+        current_time = time.time()
+        max_age = max_days * 86400  # 转换为秒
+        
+        for file in os.listdir(log_folder):
+            file_path = os.path.join(log_folder, file)
+            if os.path.isfile(file_path) and file.startswith('word_processor.log.'):
+                file_age = current_time - os.path.getmtime(file_path)
+                if file_age > max_age:
+                    os.remove(file_path)
+    except Exception as e:
+        # 清理过程中的错误不应影响主程序
+        pass
+
+def setup_logger(log_folder='logs', log_level=logging.INFO, max_size_mb=10, backup_count=30):
+    """
+    配置日志记录器
+    
+    参数:
+        log_folder: 日志保存目录
+        log_level: 日志级别
+        max_size_mb: 单个日志文件的最大大小(MB)
+        backup_count: 保留的备份文件数量
+        
+    返回:
+        logger: 配置好的日志记录器
+    """
+    # 确保日志目录存在
+    os.makedirs(log_folder, exist_ok=True)
+    
+    # 创建日志记录器
+    logger = logging.getLogger('word_processor')
+    logger.setLevel(log_level)
+    
+    # 如果已经有处理器,则不再添加
+    if logger.handlers:
+        return logger
+    
+    # 日志格式 - 优化格式,添加处理ID以便跟踪单次操作
+    log_format = logging.Formatter(
+        '%(asctime)s [%(levelname)s] [%(process_id)s] %(message)s',
+        datefmt='%Y-%m-%d %H:%M:%S'
+    )
+    
+    # 创建处理ID过滤器实例
+    process_id_filter = ProcessIdFilter()
+    
+    # 文件处理器 - 同时基于大小和时间滚动
+    log_file = os.path.join(log_folder, 'word_processor.log')
+    
+    # 基于大小的处理器
+    size_handler = CompressedRotatingFileHandler(
+        log_file,
+        maxBytes=max_size_mb * 1024 * 1024,  # 转换为字节
+        backupCount=5,  # 保留5个基于大小的备份
+        encoding='utf-8'
+    )
+    size_handler.setFormatter(log_format)
+    size_handler.setLevel(log_level)
+    size_handler.addFilter(process_id_filter)
+    
+    # 基于时间的处理器
+    time_handler = CompressedTimedRotatingFileHandler(
+        log_file,
+        when='midnight',
+        interval=1,
+        backupCount=backup_count,  # 保留指定天数的日志
+        encoding='utf-8'
+    )
+    time_handler.setFormatter(log_format)
+    time_handler.setLevel(log_level)
+    time_handler.addFilter(process_id_filter)
+    
+    # 控制台处理器
+    console_handler = logging.StreamHandler()
+    console_handler.setFormatter(log_format)
+    console_handler.setLevel(log_level)
+    console_handler.addFilter(process_id_filter)
+    
+    # 添加处理器到记录器
+    logger.addHandler(size_handler)
+    logger.addHandler(time_handler)
+    logger.addHandler(console_handler)
+    
+    # 清理旧日志
+    clean_old_logs(log_folder, backup_count)
+    
+    return logger
+
+# 初始化日志记录器 - 可以根据环境设置不同的级别
+# 生产环境推荐 logging.INFO 或 logging.WARNING
+# 开发环境可以使用 logging.DEBUG
+import os
+env = os.environ.get('FLASK_ENV', 'production')
+log_level = logging.INFO if env == 'production' else logging.DEBUG
+max_size_mb = 10  # 10MB
+backup_count = 30  # 30天
+
+logger = setup_logger(log_level=log_level, max_size_mb=max_size_mb, backup_count=backup_count)
+
+# 重置处理ID,用于跟踪新的请求
+def reset_process_id():
+    for handler in logger.handlers:
+        for filter_obj in handler.filters:
+            if isinstance(filter_obj, ProcessIdFilter):
+                filter_obj.process_id = None