From fb7fb985adec91b63a2fe59c05d2d9a4a24dac6f Mon Sep 17 00:00:00 2001 From: python Date: Mon, 15 Dec 2025 16:23:37 +0800 Subject: [PATCH] =?UTF-8?q?=E9=87=8D=E5=81=9A=E8=B0=88=E8=AF=9D=E5=AE=A1?= =?UTF-8?q?=E6=89=B9=E8=A1=A8=E5=B9=B6=E4=B8=94=E4=BF=AE=E6=94=B9=E6=B5=8B?= =?UTF-8?q?=E8=AF=95=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app.py | 340 ++++++++++++++++ static/template_field_manager.html | 378 ++++++++++++++++++ .../走读式谈话审批/2谈话审批表-重新制作表格.docx | Bin 0 -> 15347 bytes test_template_placeholder_replacement.py | 49 ++- 4 files changed, 756 insertions(+), 11 deletions(-) create mode 100644 template_finish/2-初核模版/2.谈话审批/走读式谈话审批/2谈话审批表-重新制作表格.docx diff --git a/app.py b/app.py index 95dec91..43ed814 100644 --- a/app.py +++ b/app.py @@ -9,9 +9,13 @@ import pymysql import json import tempfile import zipfile +import re from datetime import datetime from dotenv import load_dotenv from flask import send_file +from docx import Document +from minio import Minio +from werkzeug.utils import secure_filename from services.ai_service import AIService from services.field_service import FieldService @@ -1680,6 +1684,342 @@ def restore_database(): return error_response(500, f"恢复数据库失败: {str(e)}") +def get_minio_client(): + """获取 MinIO 客户端""" + minio_endpoint = os.getenv('MINIO_ENDPOINT') + minio_access_key = os.getenv('MINIO_ACCESS_KEY') + minio_secret_key = os.getenv('MINIO_SECRET_KEY') + minio_secure = os.getenv('MINIO_SECURE', 'false').lower() == 'true' + + if not all([minio_endpoint, minio_access_key, minio_secret_key]): + raise ValueError("MinIO配置不完整") + + return Minio( + minio_endpoint, + access_key=minio_access_key, + secret_key=minio_secret_key, + secure=minio_secure + ) + + +def extract_placeholders_from_docx(file_path: str) -> list: + """ + 从docx文件中提取所有占位符 + + Args: + file_path: docx文件路径 + + Returns: + 占位符列表,格式: ['field_code1', 'field_code2', ...] + """ + placeholders = set() + pattern = r'\{\{([^}]+)\}\}' # 匹配 {{field_code}} 格式 + + try: + doc = Document(file_path) + + # 从段落中提取占位符 + for paragraph in doc.paragraphs: + text = paragraph.text + matches = re.findall(pattern, text) + for match in matches: + cleaned = match.strip() + # 过滤掉不完整的占位符(包含 { 或 } 的) + if cleaned and '{' not in cleaned and '}' not in cleaned: + placeholders.add(cleaned) + + # 从表格中提取占位符 + for table in doc.tables: + for row in table.rows: + for cell in row.cells: + for paragraph in cell.paragraphs: + text = paragraph.text + matches = re.findall(pattern, text) + for match in matches: + cleaned = match.strip() + # 过滤掉不完整的占位符(包含 { 或 } 的) + if cleaned and '{' not in cleaned and '}' not in cleaned: + placeholders.add(cleaned) + + except Exception as e: + raise Exception(f"读取文件失败: {str(e)}") + + return sorted(list(placeholders)) + + +@app.route('/api/template/upload', methods=['POST']) +def upload_template(): + """ + 上传新模板文件 + 1. 接收上传的 Word 文档 + 2. 提取占位符 + 3. 匹配数据库中的字段 + 4. 返回未匹配的占位符列表,等待用户输入字段名称 + """ + try: + # 检查是否有文件 + if 'file' not in request.files: + return error_response(400, "未找到上传的文件") + + file = request.files['file'] + tenant_id = request.form.get('tenant_id', type=int) + + if not tenant_id: + return error_response(400, "缺少 tenant_id 参数") + + if file.filename == '': + return error_response(400, "文件名为空") + + # 检查文件扩展名 + if not file.filename.lower().endswith('.docx'): + return error_response(400, "只支持 .docx 格式的文件") + + # 保存临时文件 + temp_dir = tempfile.mkdtemp() + temp_file_path = os.path.join(temp_dir, secure_filename(file.filename)) + file.save(temp_file_path) + + try: + # 提取占位符 + placeholders = extract_placeholders_from_docx(temp_file_path) + + # 查询数据库中的字段 + conn = pymysql.connect( + host=os.getenv('DB_HOST'), + port=int(os.getenv('DB_PORT', 3306)), + user=os.getenv('DB_USER'), + password=os.getenv('DB_PASSWORD'), + database=os.getenv('DB_NAME'), + charset='utf8mb4' + ) + + cursor = conn.cursor(pymysql.cursors.DictCursor) + + # 查询所有字段 + cursor.execute(""" + SELECT id, name, filed_code, field_type, state + FROM f_polic_field + WHERE tenant_id = %s + """, (tenant_id,)) + + fields = cursor.fetchall() + cursor.close() + conn.close() + + # 构建字段映射 + field_map = {} + for field in fields: + state = field['state'] + if isinstance(state, bytes): + state = int.from_bytes(state, byteorder='big') if len(state) == 1 else 1 + + if state == 1: # 只使用启用的字段 + field_map[field['filed_code']] = { + 'id': field['id'], + 'name': field['name'], + 'field_type': field['field_type'] + } + + # 匹配占位符 + matched_fields = [] + unmatched_placeholders = [] + + for placeholder in placeholders: + if placeholder in field_map: + matched_fields.append({ + 'placeholder': placeholder, + 'field_id': field_map[placeholder]['id'], + 'field_name': field_map[placeholder]['name'], + 'field_type': field_map[placeholder]['field_type'] + }) + else: + unmatched_placeholders.append(placeholder) + + # 返回结果 + return success_response({ + 'filename': file.filename, + 'placeholders': placeholders, + 'matched_fields': matched_fields, + 'unmatched_placeholders': unmatched_placeholders, + 'temp_file_path': temp_file_path # 临时保存路径,用于后续保存 + }, "模板解析成功") + + except Exception as e: + # 清理临时文件 + try: + os.remove(temp_file_path) + os.rmdir(temp_dir) + except: + pass + raise e + + except Exception as e: + return error_response(500, f"上传模板失败: {str(e)}") + + +@app.route('/api/template/save', methods=['POST']) +def save_template(): + """ + 保存模板 + 1. 接收模板信息(文件名、tenant_id、字段关联关系、新字段信息) + 2. 创建新字段(如果有) + 3. 上传文件到 MinIO + 4. 保存模板到数据库 + 5. 保存字段关联关系 + """ + try: + data = request.get_json() + + tenant_id = data.get('tenant_id') + filename = data.get('filename') + temp_file_path = data.get('temp_file_path') + field_relations = data.get('field_relations', []) # [{field_id, field_type}, ...] + new_fields = data.get('new_fields', []) # [{placeholder, name, field_type}, ...] + template_name = data.get('template_name') # 用户输入的模板名称 + + if not all([tenant_id, filename, temp_file_path]): + return error_response(400, "缺少必要参数") + + if not os.path.exists(temp_file_path): + return error_response(400, "临时文件不存在") + + # 连接数据库 + conn = pymysql.connect( + host=os.getenv('DB_HOST'), + port=int(os.getenv('DB_PORT', 3306)), + user=os.getenv('DB_USER'), + password=os.getenv('DB_PASSWORD'), + database=os.getenv('DB_NAME'), + charset='utf8mb4' + ) + + cursor = conn.cursor() + created_by = 655162080928945152 # 默认创建者ID + updated_by = 655162080928945152 + + try: + # 1. 创建新字段(如果有) + new_field_map = {} # placeholder -> field_id + if new_fields: + for new_field in new_fields: + placeholder = new_field.get('placeholder') + field_name = new_field.get('name') + field_type = new_field.get('field_type', 2) # 默认为输出字段 + + if not placeholder or not field_name: + continue + + # 生成字段ID + field_id = generate_id() + + # 插入新字段 + cursor.execute(""" + INSERT INTO f_polic_field + (id, tenant_id, name, filed_code, field_type, created_time, created_by, updated_time, updated_by, state) + VALUES (%s, %s, %s, %s, %s, NOW(), %s, NOW(), %s, 1) + """, (field_id, tenant_id, field_name, placeholder, field_type, created_by, updated_by)) + + new_field_map[placeholder] = field_id + + # 2. 上传文件到 MinIO + minio_client = get_minio_client() + bucket_name = os.getenv('MINIO_BUCKET', 'finyx') + + # 确保存储桶存在 + if not minio_client.bucket_exists(bucket_name): + minio_client.make_bucket(bucket_name) + + # 生成 MinIO 路径 + now = datetime.now() + object_name = f'{tenant_id}/TEMPLATE/{now.year}/{now.month:02d}/{filename}' + + minio_client.fput_object( + bucket_name, + object_name, + temp_file_path, + content_type='application/vnd.openxmlformats-officedocument.wordprocessingml.document' + ) + + minio_path = f"/{object_name}" + + # 3. 保存模板到数据库 + template_id = generate_id() + template_name_final = template_name or filename.replace('.docx', '') + + cursor.execute(""" + INSERT INTO f_polic_file_config + (id, tenant_id, parent_id, name, input_data, file_path, created_time, created_by, updated_time, updated_by, state) + VALUES (%s, %s, %s, %s, %s, %s, NOW(), %s, NOW(), %s, 1) + """, ( + template_id, + tenant_id, + None, # parent_id + template_name_final, + json.dumps({'template_name': template_name_final}, ensure_ascii=False), + minio_path, + created_by, + updated_by + )) + + # 4. 保存字段关联关系 + all_field_ids = [] + + # 添加用户选择的字段关联 + for relation in field_relations: + field_id = relation.get('field_id') + if field_id: + all_field_ids.append(field_id) + + # 添加新创建的字段关联 + for placeholder, field_id in new_field_map.items(): + if field_id not in all_field_ids: + all_field_ids.append(field_id) + + # 添加必需的输入字段 + cursor.execute(""" + SELECT id FROM f_polic_field + WHERE tenant_id = %s AND filed_code IN ('clue_info', 'target_basic_info_clue') AND state = 1 + """, (tenant_id,)) + required_fields = cursor.fetchall() + for row in required_fields: + if row[0] not in all_field_ids: + all_field_ids.append(row[0]) + + # 插入关联关系 + for field_id in all_field_ids: + cursor.execute(""" + INSERT INTO f_polic_file_field + (tenant_id, file_id, filed_id, created_time, created_by, updated_time, updated_by, state) + VALUES (%s, %s, %s, NOW(), %s, NOW(), %s, 1) + """, (tenant_id, template_id, field_id, created_by, updated_by)) + + conn.commit() + + # 清理临时文件 + try: + os.remove(temp_file_path) + os.rmdir(os.path.dirname(temp_file_path)) + except: + pass + + return success_response({ + 'template_id': template_id, + 'template_name': template_name_final, + 'file_path': minio_path, + 'field_count': len(all_field_ids) + }, "模板保存成功") + + except Exception as e: + conn.rollback() + raise e + finally: + cursor.close() + conn.close() + + except Exception as e: + return error_response(500, f"保存模板失败: {str(e)}") + + if __name__ == '__main__': # 确保static目录存在 os.makedirs('static', exist_ok=True) diff --git a/static/template_field_manager.html b/static/template_field_manager.html index 3c796ca..f9df275 100644 --- a/static/template_field_manager.html +++ b/static/template_field_manager.html @@ -412,6 +412,26 @@ + +
+

上传新模板

+
+ +
+ + +
+ + 支持 .docx 格式,系统将自动扫描占位符并匹配字段 + +
+ + + +
+