重做谈话审批表并且修改测试代码

This commit is contained in:
python 2025-12-15 16:23:37 +08:00
parent 557c9ae351
commit fb7fb985ad
4 changed files with 756 additions and 11 deletions

340
app.py
View File

@ -9,9 +9,13 @@ import pymysql
import json import json
import tempfile import tempfile
import zipfile import zipfile
import re
from datetime import datetime from datetime import datetime
from dotenv import load_dotenv from dotenv import load_dotenv
from flask import send_file 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.ai_service import AIService
from services.field_service import FieldService from services.field_service import FieldService
@ -1680,6 +1684,342 @@ def restore_database():
return error_response(500, f"恢复数据库失败: {str(e)}") 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__': if __name__ == '__main__':
# 确保static目录存在 # 确保static目录存在
os.makedirs('static', exist_ok=True) os.makedirs('static', exist_ok=True)

View File

@ -412,6 +412,26 @@
</div> </div>
</div> </div>
<!-- 上传新模板区域 -->
<div class="section">
<h2>上传新模板</h2>
<div class="form-group">
<label>上传 Word 模板文件:</label>
<div style="display: flex; gap: 10px; align-items: center;">
<input type="file" id="templateFileInput" accept=".docx" style="flex: 1; padding: 8px;">
<button class="btn btn-primary" onclick="uploadTemplate()" id="uploadBtn" disabled>上传并解析</button>
</div>
<small style="color: #666; margin-top: 5px; display: block;">
支持 .docx 格式,系统将自动扫描占位符并匹配字段
</small>
</div>
<!-- 上传结果展示区域 -->
<div id="uploadResultArea" style="display: none; margin-top: 20px;">
<div id="uploadResultContent"></div>
</div>
</div>
<!-- 字段管理区域 --> <!-- 字段管理区域 -->
<div class="section" id="fieldManagementSection" style="display: none;"> <div class="section" id="fieldManagementSection" style="display: none;">
<h2>字段管理</h2> <h2>字段管理</h2>
@ -586,6 +606,23 @@
console.warn('清除缓存失败:', e); console.warn('清除缓存失败:', e);
} }
// 监听文件选择和租户选择
const fileInput = document.getElementById('templateFileInput');
if (fileInput) {
fileInput.addEventListener('change', function() {
const uploadBtn = document.getElementById('uploadBtn');
if (uploadBtn) {
uploadBtn.disabled = !this.files.length || !currentTenantId;
}
});
}
// 监听租户选择变化,更新上传按钮状态
const tenantSelect = document.getElementById('tenantSelect');
if (tenantSelect) {
// 注意:这里会在 loadTenantIds 中重新设置 onchange所以这里只设置初始状态
}
loadTenantIds(); loadTenantIds();
}; };
@ -1419,6 +1456,347 @@
// 初始化搜索功能 // 初始化搜索功能
setupSearch(); setupSearch();
// 上传模板文件
async function uploadTemplate() {
const fileInput = document.getElementById('templateFileInput');
const file = fileInput.files[0];
if (!file) {
showMessage('请选择要上传的文件', 'error');
return;
}
if (!currentTenantId) {
showMessage('请先选择租户ID', 'error');
return;
}
if (!file.name.toLowerCase().endsWith('.docx')) {
showMessage('只支持 .docx 格式的文件', 'error');
return;
}
const formData = new FormData();
formData.append('file', file);
formData.append('tenant_id', currentTenantId);
try {
showMessage('正在上传并解析模板...', 'info');
const uploadBtn = document.getElementById('uploadBtn');
uploadBtn.disabled = true;
uploadBtn.textContent = '上传中...';
const response = await fetch('/api/template/upload', {
method: 'POST',
body: formData
});
const result = await response.json();
if (result.isSuccess) {
displayUploadResult(result.data);
showMessage('模板解析成功', 'success');
} else {
showMessage('上传失败: ' + result.errorMsg, 'error');
}
} catch (error) {
console.error('上传失败:', error);
showMessage('上传失败: ' + error.message, 'error');
} finally {
const uploadBtn = document.getElementById('uploadBtn');
uploadBtn.disabled = false;
uploadBtn.textContent = '上传并解析';
}
}
// 显示上传结果
function displayUploadResult(data) {
const resultArea = document.getElementById('uploadResultArea');
const resultContent = document.getElementById('uploadResultContent');
let html = `
<div style="background: #f0f7ff; padding: 20px; border-radius: 6px; margin-bottom: 20px;">
<h3 style="margin-top: 0;">模板信息</h3>
<p><strong>文件名:</strong>${data.filename}</p>
<p><strong>占位符总数:</strong>${data.placeholders.length}</p>
<p><strong>已匹配字段:</strong>${data.matched_fields.length}</p>
<p><strong>未匹配占位符:</strong>${data.unmatched_placeholders.length}</p>
</div>
`;
// 显示已匹配的字段
if (data.matched_fields.length > 0) {
html += `
<div style="margin-bottom: 20px;">
<h3>已匹配的字段</h3>
<table style="width: 100%; border-collapse: collapse;">
<thead>
<tr style="background: #f5f5f5;">
<th style="padding: 10px; border: 1px solid #ddd;">占位符</th>
<th style="padding: 10px; border: 1px solid #ddd;">字段名称</th>
<th style="padding: 10px; border: 1px solid #ddd;">字段类型</th>
<th style="padding: 10px; border: 1px solid #ddd;">操作</th>
</tr>
</thead>
<tbody>
`;
data.matched_fields.forEach(field => {
html += `
<tr>
<td style="padding: 10px; border: 1px solid #ddd;"><code>${field.placeholder}</code></td>
<td style="padding: 10px; border: 1px solid #ddd;">${field.field_name}</td>
<td style="padding: 10px; border: 1px solid #ddd;">${field.field_type === 1 ? '输入字段' : '输出字段'}</td>
<td style="padding: 10px; border: 1px solid #ddd;">
<button class="btn btn-secondary" onclick="removeMatchedField('${field.placeholder}')" style="padding: 5px 10px; font-size: 12px;">移除</button>
</td>
</tr>
`;
});
html += `
</tbody>
</table>
</div>
`;
}
// 显示未匹配的占位符
if (data.unmatched_placeholders.length > 0) {
html += `
<div style="margin-bottom: 20px;">
<h3>未匹配的占位符(需要创建新字段)</h3>
<p style="color: #666; margin-bottom: 10px;">以下占位符在数据库中不存在,请为它们创建新字段:</p>
<button class="btn btn-primary" onclick="showUnmatchedFieldsModal(${JSON.stringify(data.unmatched_placeholders).replace(/"/g, '&quot;')})">
为未匹配占位符创建字段
</button>
</div>
`;
}
// 添加模板名称输入和保存按钮
html += `
<div style="margin-top: 20px; padding: 20px; background: #fff; border: 1px solid #ddd; border-radius: 6px;">
<h3>保存模板</h3>
<div class="form-group">
<label for="templateNameInput">模板名称:</label>
<input type="text" id="templateNameInput" value="${data.filename.replace('.docx', '')}" style="width: 100%; padding: 8px; margin-top: 5px;">
</div>
<div class="btn-group" style="margin-top: 15px;">
<button class="btn btn-primary" onclick="saveUploadedTemplate(${JSON.stringify(data).replace(/"/g, '&quot;')})">保存模板</button>
<button class="btn btn-secondary" onclick="cancelUpload()">取消</button>
</div>
</div>
`;
resultContent.innerHTML = html;
resultArea.style.display = 'block';
// 保存上传数据到全局变量
window.uploadedTemplateData = data;
}
// 显示未匹配字段输入弹窗
function showUnmatchedFieldsModal(placeholders) {
let html = `
<div class="modal" id="unmatchedFieldsModal" style="display: block;">
<div class="modal-content" style="max-width: 800px;">
<div class="modal-header">
<h2>为未匹配占位符创建字段</h2>
<span class="close" onclick="closeUnmatchedFieldsModal()">&times;</span>
</div>
<div style="max-height: 60vh; overflow-y: auto;">
<p style="color: #666; margin-bottom: 15px;">请为以下占位符输入字段名称和类型:</p>
<table style="width: 100%; border-collapse: collapse;">
<thead>
<tr style="background: #f5f5f5;">
<th style="padding: 10px; border: 1px solid #ddd;">占位符</th>
<th style="padding: 10px; border: 1px solid #ddd;">字段名称</th>
<th style="padding: 10px; border: 1px solid #ddd;">字段类型</th>
</tr>
</thead>
<tbody>
`;
placeholders.forEach(placeholder => {
html += `
<tr>
<td style="padding: 10px; border: 1px solid #ddd;"><code>${placeholder}</code></td>
<td style="padding: 10px; border: 1px solid #ddd;">
<input type="text" class="new-field-name" data-placeholder="${placeholder}"
placeholder="请输入字段名称" style="width: 100%; padding: 5px;" required>
</td>
<td style="padding: 10px; border: 1px solid #ddd;">
<select class="new-field-type" data-placeholder="${placeholder}" style="width: 100%; padding: 5px;">
<option value="2">输出字段</option>
<option value="1">输入字段</option>
</select>
</td>
</tr>
`;
});
html += `
</tbody>
</table>
</div>
<div class="btn-group" style="margin-top: 20px;">
<button class="btn btn-primary" onclick="confirmNewFields()">确认创建</button>
<button class="btn btn-secondary" onclick="closeUnmatchedFieldsModal()">取消</button>
</div>
</div>
</div>
`;
// 创建并显示模态框
const modal = document.createElement('div');
modal.innerHTML = html;
document.body.appendChild(modal);
}
// 确认创建新字段
function confirmNewFields() {
const nameInputs = document.querySelectorAll('.new-field-name');
const typeSelects = document.querySelectorAll('.new-field-type');
const newFields = [];
let hasError = false;
nameInputs.forEach(input => {
const placeholder = input.dataset.placeholder;
const name = input.value.trim();
const typeSelect = document.querySelector(`.new-field-type[data-placeholder="${placeholder}"]`);
const type = typeSelect ? parseInt(typeSelect.value) : 2;
if (!name) {
hasError = true;
input.style.borderColor = '#f44336';
} else {
input.style.borderColor = '#ddd';
newFields.push({
placeholder: placeholder,
name: name,
field_type: type
});
}
});
if (hasError) {
showMessage('请填写所有字段名称', 'error');
return;
}
// 将新字段添加到上传数据中
if (window.uploadedTemplateData) {
window.uploadedTemplateData.new_fields = newFields;
// 更新显示
const unmatchedPlaceholders = window.uploadedTemplateData.unmatched_placeholders.filter(
p => !newFields.find(f => f.placeholder === p)
);
window.uploadedTemplateData.unmatched_placeholders = unmatchedPlaceholders;
// 重新显示结果
displayUploadResult(window.uploadedTemplateData);
}
closeUnmatchedFieldsModal();
showMessage('新字段信息已保存,请继续保存模板', 'success');
}
// 关闭未匹配字段弹窗
function closeUnmatchedFieldsModal() {
const modal = document.getElementById('unmatchedFieldsModal');
if (modal) {
modal.remove();
}
}
// 保存上传的模板
async function saveUploadedTemplate(data) {
const templateName = document.getElementById('templateNameInput').value.trim();
if (!templateName) {
showMessage('请输入模板名称', 'error');
return;
}
if (!currentTenantId) {
showMessage('请先选择租户ID', 'error');
return;
}
// 构建字段关联关系
const fieldRelations = data.matched_fields.map(field => ({
field_id: field.field_id,
field_type: field.field_type
}));
// 添加新字段信息
const newFields = data.new_fields || [];
const saveData = {
tenant_id: parseInt(currentTenantId),
filename: data.filename,
temp_file_path: data.temp_file_path,
template_name: templateName,
field_relations: fieldRelations,
new_fields: newFields
};
try {
showMessage('正在保存模板...', 'info');
const response = await fetch('/api/template/save', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(saveData)
});
const result = await response.json();
if (result.isSuccess) {
showMessage('模板保存成功!', 'success');
// 清空上传区域
cancelUpload();
// 刷新数据
if (currentTenantId) {
await loadData();
}
} else {
showMessage('保存失败: ' + result.errorMsg, 'error');
}
} catch (error) {
console.error('保存失败:', error);
showMessage('保存失败: ' + error.message, 'error');
}
}
// 取消上传
function cancelUpload() {
const resultArea = document.getElementById('uploadResultArea');
resultArea.style.display = 'none';
const fileInput = document.getElementById('templateFileInput');
fileInput.value = '';
window.uploadedTemplateData = null;
}
// 移除已匹配的字段
function removeMatchedField(placeholder) {
if (window.uploadedTemplateData) {
window.uploadedTemplateData.matched_fields = window.uploadedTemplateData.matched_fields.filter(
f => f.placeholder !== placeholder
);
window.uploadedTemplateData.unmatched_placeholders.push(placeholder);
displayUploadResult(window.uploadedTemplateData);
}
}
</script> </script>
</body> </body>
</html> </html>

View File

@ -34,7 +34,10 @@ def extract_placeholders_from_docx(file_path: str) -> set:
matches = re.findall(pattern, text) matches = re.findall(pattern, text)
for match in matches: for match in matches:
# 清理占位符:去除首尾空格,并将中间的空格/换行符替换为下划线
cleaned = match.strip() cleaned = match.strip()
# 将中间的空格、换行符、制表符等空白字符替换为下划线
cleaned = re.sub(r'\s+', '_', cleaned)
# 过滤掉不完整的占位符(包含 { 或 } 的) # 过滤掉不完整的占位符(包含 { 或 } 的)
if cleaned and '{' not in cleaned and '}' not in cleaned: if cleaned and '{' not in cleaned and '}' not in cleaned:
placeholders.add(cleaned) placeholders.add(cleaned)
@ -50,7 +53,10 @@ def extract_placeholders_from_docx(file_path: str) -> set:
matches = re.findall(pattern, cell_text) matches = re.findall(pattern, cell_text)
for match in matches: for match in matches:
# 清理占位符:去除首尾空格,并将中间的空格/换行符替换为下划线
cleaned = match.strip() cleaned = match.strip()
# 将中间的空格、换行符、制表符等空白字符替换为下划线
cleaned = re.sub(r'\s+', '_', cleaned)
# 过滤掉不完整的占位符(包含 { 或 } 的) # 过滤掉不完整的占位符(包含 { 或 } 的)
if cleaned and '{' not in cleaned and '}' not in cleaned: if cleaned and '{' not in cleaned and '}' not in cleaned:
placeholders.add(cleaned) placeholders.add(cleaned)
@ -199,9 +205,22 @@ def test_template_replacement(template_path: str, output_dir: str):
# 执行替换 # 执行替换
final_text = full_text final_text = full_text
for field_code, field_value in field_data.items(): for field_code, field_value in field_data.items():
placeholder = f"{{{{{field_code}}}}}"
replacement_value = str(field_value) if field_value else '' replacement_value = str(field_value) if field_value else ''
# 尝试多种格式的占位符替换(处理空格问题)
# 标准格式
placeholder = f"{{{{{field_code}}}}}"
final_text = final_text.replace(placeholder, replacement_value) final_text = final_text.replace(placeholder, replacement_value)
# 带空格的格式(空格替换为下划线后的字段名)
placeholder_with_spaces = f"{{{{ {field_code.replace('_', ' ')} }}}}"
if placeholder_with_spaces in final_text:
final_text = final_text.replace(placeholder_with_spaces, replacement_value)
# 正则表达式匹配(处理各种空格情况)
placeholder_pattern_variants = [
re.compile(re.escape(f"{{{{ {field_code.replace('_', ' ')} }}}}")),
re.compile(re.escape(f"{{{{{field_code.replace('_', ' ')}}}}}")),
]
for variant_pattern in placeholder_pattern_variants:
final_text = variant_pattern.sub(replacement_value, final_text)
# 替换段落文本(保持格式) # 替换段落文本(保持格式)
if len(paragraph.runs) == 1: if len(paragraph.runs) == 1:
@ -234,9 +253,22 @@ def test_template_replacement(template_path: str, output_dir: str):
# 执行替换 # 执行替换
final_text = full_text final_text = full_text
for field_code, field_value in field_data.items(): for field_code, field_value in field_data.items():
placeholder = f"{{{{{field_code}}}}}"
replacement_value = str(field_value) if field_value else '' replacement_value = str(field_value) if field_value else ''
# 尝试多种格式的占位符替换(处理空格问题)
# 标准格式
placeholder = f"{{{{{field_code}}}}}"
final_text = final_text.replace(placeholder, replacement_value) final_text = final_text.replace(placeholder, replacement_value)
# 带空格的格式(空格替换为下划线后的字段名)
placeholder_with_spaces = f"{{{{ {field_code.replace('_', ' ')} }}}}"
if placeholder_with_spaces in final_text:
final_text = final_text.replace(placeholder_with_spaces, replacement_value)
# 正则表达式匹配(处理各种空格情况)
placeholder_pattern_variants = [
re.compile(re.escape(f"{{{{ {field_code.replace('_', ' ')} }}}}")),
re.compile(re.escape(f"{{{{{field_code.replace('_', ' ')}}}}}")),
]
for variant_pattern in placeholder_pattern_variants:
final_text = variant_pattern.sub(replacement_value, final_text)
# 替换段落文本 # 替换段落文本
if len(paragraph.runs) == 1: if len(paragraph.runs) == 1:
@ -303,8 +335,7 @@ def main():
project_root = Path(__file__).parent project_root = Path(__file__).parent
# 模板文件路径 # 模板文件路径
template1_path = project_root / "template_finish" / "2-初核模版" / "2.谈话审批" / "走读式谈话审批" / "2谈话审批表.docx" template_path = project_root / "template_finish" / "2-初核模版" / "2.谈话审批" / "走读式谈话审批" / "2谈话审批表-重新制作表格.docx"
template2_path = project_root / "template_finish" / "2-初核模版" / "3.初核结论" / "8-1请示报告卡初核报告结论 .docx"
# 输出目录 # 输出目录
output_dir = project_root / "output_temp" output_dir = project_root / "output_temp"
@ -313,18 +344,14 @@ def main():
print("模板占位符识别和替换测试") print("模板占位符识别和替换测试")
print("="*80) print("="*80)
# 测试第一个模板 # 测试模板
success1 = test_template_replacement(str(template1_path), str(output_dir)) success = test_template_replacement(str(template_path), str(output_dir))
# 测试第二个模板
success2 = test_template_replacement(str(template2_path), str(output_dir))
# 总结 # 总结
print(f"\n{'='*80}") print(f"\n{'='*80}")
print("测试总结") print("测试总结")
print(f"{'='*80}") print(f"{'='*80}")
print(f"模板1 (2谈话审批表.docx): {'[成功]' if success1 else '[失败]'}") print(f"模板 (2谈话审批表-重新制作表格.docx): {'[成功]' if success else '[失败]'}")
print(f"模板2 (8-1请示报告卡初核报告结论.docx): {'[成功]' if success2 else '[失败]'}")
print(f"\n输出目录: {output_dir}") print(f"\n输出目录: {output_dir}")
print(f"{'='*80}\n") print(f"{'='*80}\n")