diff --git a/app.py b/app.py
index 447f995..7aab919 100644
--- a/app.py
+++ b/app.py
@@ -706,7 +706,6 @@ def generate_document():
return error_response(3001, f"文档生成失败: {str(e)}")
-<<<<<<< HEAD
@app.route('/fPolicTask/getDocument', methods=['POST'])
def get_document_by_task():
"""
@@ -810,7 +809,8 @@ def get_document_by_task():
result_file_list.append({
'fileId': file_id,
'fileName': generated_file_name, # 使用生成的文档名
- 'filePath': result['filePath']
+ 'filePath': result['filePath'], # MinIO相对路径
+ 'downloadUrl': result.get('downloadUrl') # MinIO预签名下载URL(完整链接)
})
except Exception as e:
@@ -835,8 +835,6 @@ def get_document_by_task():
return error_response(3001, f"文档生成失败: {str(e)}")
-=======
->>>>>>> parent of 4897c96 (添加通过taskId获取文档的接口,支持文件列表查询和参数验证,增强错误处理能力。同时,优化文档生成逻辑,确保生成的文档名称和路径的准确性。)
if __name__ == '__main__':
# 确保static目录存在
os.makedirs('static', exist_ok=True)
diff --git a/check_and_fix_duplicates.py b/check_and_fix_duplicates.py
new file mode 100644
index 0000000..ebc7f18
--- /dev/null
+++ b/check_and_fix_duplicates.py
@@ -0,0 +1,117 @@
+"""
+检查并修复重复记录
+"""
+import pymysql
+
+DB_CONFIG = {
+ 'host': '152.136.177.240',
+ 'port': 5012,
+ 'user': 'finyx',
+ 'password': '6QsGK6MpePZDE57Z',
+ 'database': 'finyx',
+ 'charset': 'utf8mb4'
+}
+
+TENANT_ID = 615873064429507639
+UPDATED_BY = 655162080928945152
+
+conn = pymysql.connect(**DB_CONFIG)
+cursor = conn.cursor(pymysql.cursors.DictCursor)
+
+try:
+ # 检查"1.初核请示"下的所有记录
+ cursor.execute("""
+ SELECT id, name, file_path, parent_id
+ FROM f_polic_file_config
+ WHERE tenant_id = %s AND parent_id = %s
+ ORDER BY id
+ """, (TENANT_ID, 1765431558933731)) # 1.初核请示
+
+ results = cursor.fetchall()
+ print(f"'1.初核请示'下有 {len(results)} 条记录:\n")
+ for r in results:
+ print(f"ID: {r['id']}, name: {r['name']}, file_path: {r['file_path']}")
+
+ # 检查"1请示报告卡"的记录
+ request_cards = [r for r in results if r['name'] == '1请示报告卡']
+ if len(request_cards) > 1:
+ print(f"\n发现 {len(request_cards)} 个重复的'1请示报告卡'记录")
+ # 保留file_path正确的那个
+ correct_one = None
+ for r in request_cards:
+ if r['file_path'] and '1.请示报告卡(XXX)' in r['file_path']:
+ correct_one = r
+ break
+
+ if correct_one:
+ # 删除其他的
+ for r in request_cards:
+ if r['id'] != correct_one['id']:
+ # 删除关联关系
+ cursor.execute("""
+ DELETE FROM f_polic_file_field
+ WHERE tenant_id = %s AND file_id = %s
+ """, (TENANT_ID, r['id']))
+ # 删除模板记录
+ cursor.execute("""
+ DELETE FROM f_polic_file_config
+ WHERE tenant_id = %s AND id = %s
+ """, (TENANT_ID, r['id']))
+ print(f"[DELETE] 删除重复记录: ID {r['id']}, file_path: {r['file_path']}")
+
+ # 检查"走读式谈话审批"下是否有"1请示报告卡"
+ cursor.execute("""
+ SELECT id, name, file_path, parent_id
+ FROM f_polic_file_config
+ WHERE tenant_id = %s AND parent_id = %s AND name = %s
+ """, (TENANT_ID, 1765273962700431, '1请示报告卡')) # 走读式谈话审批
+
+ result = cursor.fetchone()
+ if not result:
+ print("\n[WARN] '走读式谈话审批'下缺少'1请示报告卡'记录")
+ # 创建记录
+ import time
+ import random
+ timestamp = int(time.time() * 1000)
+ random_part = random.randint(100000, 999999)
+ new_id = timestamp * 1000 + random_part
+
+ insert_sql = """
+ 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, %s)
+ """
+ cursor.execute(insert_sql, (
+ new_id,
+ TENANT_ID,
+ 1765273962700431, # 走读式谈话审批
+ '1请示报告卡',
+ None,
+ '/615873064429507639/TEMPLATE/2025/12/1.请示报告卡(初核谈话).docx',
+ 655162080928945152,
+ 655162080928945152,
+ 1
+ ))
+ print(f"[CREATE] 在'走读式谈话审批'下创建'1请示报告卡'记录 (ID: {new_id})")
+ else:
+ # 检查file_path是否正确
+ if result['file_path'] and '1.请示报告卡(初核谈话)' not in result['file_path']:
+ cursor.execute("""
+ UPDATE f_polic_file_config
+ SET file_path = %s, updated_time = NOW(), updated_by = %s
+ WHERE tenant_id = %s AND id = %s
+ """, ('/615873064429507639/TEMPLATE/2025/12/1.请示报告卡(初核谈话).docx', UPDATED_BY, TENANT_ID, result['id']))
+ print(f"[UPDATE] 修复'走读式谈话审批'下'1请示报告卡'的file_path")
+
+ conn.commit()
+ print("\n[OK] 修复完成")
+
+except Exception as e:
+ conn.rollback()
+ print(f"[ERROR] 修复失败: {e}")
+ import traceback
+ traceback.print_exc()
+finally:
+ cursor.close()
+ conn.close()
+
diff --git a/check_confidentiality_commitment.py b/check_confidentiality_commitment.py
new file mode 100644
index 0000000..fbf9910
--- /dev/null
+++ b/check_confidentiality_commitment.py
@@ -0,0 +1,36 @@
+"""查询保密承诺书相关的模板记录"""
+import pymysql
+
+DB_CONFIG = {
+ 'host': '152.136.177.240',
+ 'port': 5012,
+ 'user': 'finyx',
+ 'password': '6QsGK6MpePZDE57Z',
+ 'database': 'finyx',
+ 'charset': 'utf8mb4'
+}
+
+TENANT_ID = 615873064429507639
+
+conn = pymysql.connect(**DB_CONFIG)
+cursor = conn.cursor(pymysql.cursors.DictCursor)
+
+cursor.execute("""
+ SELECT id, name, file_path, parent_id
+ FROM f_polic_file_config
+ WHERE tenant_id = %s AND name LIKE %s
+ ORDER BY name
+""", (TENANT_ID, '%保密承诺书%'))
+
+results = cursor.fetchall()
+print(f"找到 {len(results)} 条记录:\n")
+for r in results:
+ print(f"ID: {r['id']}")
+ print(f"名称: {r['name']}")
+ print(f"文件路径: {r['file_path']}")
+ print(f"父节点ID: {r['parent_id']}")
+ print()
+
+cursor.close()
+conn.close()
+
diff --git a/cleanup_duplicate_templates.py b/cleanup_duplicate_templates.py
new file mode 100644
index 0000000..80c6bbd
--- /dev/null
+++ b/cleanup_duplicate_templates.py
@@ -0,0 +1,361 @@
+"""
+清理 f_polic_file_config 表中的重复和无效数据
+确保文档模板结构和 template_finish/ 文件夹对应
+"""
+import os
+import re
+import json
+import pymysql
+from pathlib import Path
+from typing import Dict, List, Set, Optional
+from collections import defaultdict
+
+# 数据库连接配置
+DB_CONFIG = {
+ 'host': '152.136.177.240',
+ 'port': 5012,
+ 'user': 'finyx',
+ 'password': '6QsGK6MpePZDE57Z',
+ 'database': 'finyx',
+ 'charset': 'utf8mb4'
+}
+
+TENANT_ID = 615873064429507639
+UPDATED_BY = 655162080928945152
+TEMPLATE_BASE_DIR = 'template_finish'
+
+
+def normalize_template_name(name: str) -> str:
+ """
+ 标准化模板名称(去掉扩展名、括号内容、数字前缀等)
+
+ Args:
+ name: 文件名或模板名称
+
+ Returns:
+ 标准化后的名称
+ """
+ # 去掉扩展名
+ name = Path(name).stem if '.' in name else name
+
+ # 去掉括号内容
+ name = re.sub(r'[((].*?[))]', '', name)
+ name = name.strip()
+
+ # 去掉数字前缀和点号
+ name = re.sub(r'^\d+[\.\-]?\s*', '', name)
+ name = name.strip()
+
+ return name
+
+
+def scan_template_files(base_dir: str) -> Dict[str, Dict]:
+ """
+ 扫描模板文件夹,获取所有有效的模板文件
+
+ Returns:
+ 字典,key为标准化名称,value为模板信息列表(可能有多个同名文件)
+ """
+ base_path = Path(base_dir)
+ if not base_path.exists():
+ print(f"错误: 目录不存在 - {base_dir}")
+ return {}
+
+ templates = defaultdict(list)
+
+ print("=" * 80)
+ print("扫描模板文件...")
+ print("=" * 80)
+
+ for docx_file in sorted(base_path.rglob("*.docx")):
+ # 跳过临时文件
+ if docx_file.name.startswith("~$"):
+ continue
+
+ relative_path = docx_file.relative_to(base_path)
+ file_name = docx_file.name
+ normalized_name = normalize_template_name(file_name)
+
+ templates[normalized_name].append({
+ 'file_path': str(docx_file),
+ 'relative_path': str(relative_path),
+ 'file_name': file_name,
+ 'normalized_name': normalized_name
+ })
+
+ print(f"总共扫描到 {sum(len(v) for v in templates.values())} 个模板文件")
+ print(f"唯一模板名称: {len(templates)} 个")
+
+ return dict(templates)
+
+
+def get_all_templates_from_db(conn) -> Dict[str, List[Dict]]:
+ """
+ 从数据库获取所有模板,按标准化名称分组
+
+ Returns:
+ 字典,key为标准化名称,value为模板记录列表
+ """
+ cursor = conn.cursor(pymysql.cursors.DictCursor)
+
+ sql = """
+ SELECT id, name, file_path, parent_id, state, input_data, created_time, updated_time
+ FROM f_polic_file_config
+ WHERE tenant_id = %s
+ ORDER BY created_time DESC
+ """
+ cursor.execute(sql, (TENANT_ID,))
+ templates = cursor.fetchall()
+
+ result = defaultdict(list)
+ for template in templates:
+ normalized_name = normalize_template_name(template['name'])
+ result[normalized_name].append({
+ 'id': template['id'],
+ 'name': template['name'],
+ 'normalized_name': normalized_name,
+ 'file_path': template['file_path'],
+ 'parent_id': template['parent_id'],
+ 'state': template['state'],
+ 'input_data': template['input_data'],
+ 'created_time': template['created_time'],
+ 'updated_time': template['updated_time']
+ })
+
+ cursor.close()
+ return dict(result)
+
+
+def find_duplicates(db_templates: Dict[str, List[Dict]]) -> Dict[str, List[Dict]]:
+ """
+ 找出重复的模板(同一标准化名称有多个记录)
+
+ Returns:
+ 字典,key为标准化名称,value为重复的模板记录列表
+ """
+ duplicates = {}
+ for normalized_name, templates in db_templates.items():
+ if len(templates) > 1:
+ duplicates[normalized_name] = templates
+ return duplicates
+
+
+def select_best_template(templates: List[Dict], valid_template_files: List[Dict]) -> Optional[Dict]:
+ """
+ 从多个重复的模板中选择最好的一个(保留最新的、有效的)
+
+ Args:
+ templates: 数据库中的模板记录列表
+ valid_template_files: 有效的模板文件列表
+
+ Returns:
+ 应该保留的模板记录,或None
+ """
+ if not templates:
+ return None
+
+ # 优先选择:state=1 且 file_path 有效的
+ enabled_templates = [t for t in templates if t.get('state') == 1]
+
+ if enabled_templates:
+ # 如果有多个启用的,选择最新的
+ enabled_templates.sort(key=lambda x: x.get('updated_time') or x.get('created_time'), reverse=True)
+ return enabled_templates[0]
+
+ # 如果没有启用的,选择最新的
+ templates.sort(key=lambda x: x.get('updated_time') or x.get('created_time'), reverse=True)
+ return templates[0]
+
+
+def delete_template_and_relations(conn, template_id: int):
+ """
+ 删除模板及其关联关系
+
+ Args:
+ conn: 数据库连接
+ template_id: 模板ID
+ """
+ cursor = conn.cursor()
+
+ try:
+ # 删除字段关联
+ delete_relations_sql = """
+ DELETE FROM f_polic_file_field
+ WHERE tenant_id = %s AND file_id = %s
+ """
+ cursor.execute(delete_relations_sql, (TENANT_ID, template_id))
+ relations_deleted = cursor.rowcount
+
+ # 删除模板配置
+ delete_template_sql = """
+ DELETE FROM f_polic_file_config
+ WHERE tenant_id = %s AND id = %s
+ """
+ cursor.execute(delete_template_sql, (TENANT_ID, template_id))
+ template_deleted = cursor.rowcount
+
+ conn.commit()
+ return relations_deleted, template_deleted
+
+ except Exception as e:
+ conn.rollback()
+ raise Exception(f"删除模板失败: {str(e)}")
+ finally:
+ cursor.close()
+
+
+def mark_invalid_templates(conn, valid_template_names: Set[str]):
+ """
+ 标记无效的模板(不在template_finish文件夹中的模板)
+
+ Args:
+ conn: 数据库连接
+ valid_template_names: 有效的模板名称集合(标准化后的)
+ """
+ cursor = conn.cursor()
+
+ try:
+ # 获取所有模板
+ sql = """
+ SELECT id, name FROM f_polic_file_config
+ WHERE tenant_id = %s
+ """
+ cursor.execute(sql, (TENANT_ID,))
+ all_templates = cursor.fetchall()
+
+ invalid_count = 0
+ for template in all_templates:
+ template_id = template[0]
+ template_name = template[1]
+ normalized_name = normalize_template_name(template_name)
+
+ # 检查是否在有效模板列表中
+ if normalized_name not in valid_template_names:
+ # 标记为未启用
+ update_sql = """
+ UPDATE f_polic_file_config
+ SET state = 0, updated_time = NOW(), updated_by = %s
+ WHERE id = %s AND tenant_id = %s
+ """
+ cursor.execute(update_sql, (UPDATED_BY, template_id, TENANT_ID))
+ invalid_count += 1
+ print(f" [WARN] 标记无效模板: {template_name} (ID: {template_id})")
+
+ conn.commit()
+ print(f"\n总共标记 {invalid_count} 个无效模板")
+
+ except Exception as e:
+ conn.rollback()
+ raise Exception(f"标记无效模板失败: {str(e)}")
+ finally:
+ cursor.close()
+
+
+def main():
+ """主函数"""
+ print("=" * 80)
+ print("清理重复和无效的模板数据")
+ print("=" * 80)
+ print()
+
+ try:
+ # 连接数据库
+ print("1. 连接数据库...")
+ conn = pymysql.connect(**DB_CONFIG)
+ print("[OK] 数据库连接成功\n")
+
+ # 扫描模板文件
+ print("2. 扫描模板文件...")
+ valid_templates = scan_template_files(TEMPLATE_BASE_DIR)
+ valid_template_names = set(valid_templates.keys())
+ print(f"[OK] 找到 {len(valid_template_names)} 个有效模板名称\n")
+
+ # 获取数据库中的模板
+ print("3. 获取数据库中的模板...")
+ db_templates = get_all_templates_from_db(conn)
+ print(f"[OK] 数据库中有 {sum(len(v) for v in db_templates.values())} 个模板记录")
+ print(f"[OK] 唯一模板名称: {len(db_templates)} 个\n")
+
+ # 找出重复的模板
+ print("4. 查找重复的模板...")
+ duplicates = find_duplicates(db_templates)
+ print(f"[OK] 找到 {len(duplicates)} 个重复的模板名称\n")
+
+ # 处理重复模板
+ print("5. 处理重复模板...")
+ print("=" * 80)
+
+ total_deleted = 0
+ total_relations_deleted = 0
+
+ for normalized_name, templates in duplicates.items():
+ print(f"\n处理重复模板: {normalized_name}")
+ print(f" 重复记录数: {len(templates)}")
+
+ # 获取对应的有效模板文件
+ valid_files = valid_templates.get(normalized_name, [])
+
+ # 选择要保留的模板
+ keep_template = select_best_template(templates, valid_files)
+
+ if keep_template:
+ print(f" [KEEP] 保留模板: {keep_template['name']} (ID: {keep_template['id']})")
+
+ # 删除其他重复的模板
+ for template in templates:
+ if template['id'] != keep_template['id']:
+ print(f" [DELETE] 删除重复模板: {template['name']} (ID: {template['id']})")
+ relations_deleted, template_deleted = delete_template_and_relations(conn, template['id'])
+ total_relations_deleted += relations_deleted
+ total_deleted += template_deleted
+ else:
+ print(f" [WARN] 无法确定要保留的模板,跳过")
+
+ print(f"\n[OK] 删除重复模板: {total_deleted} 个")
+ print(f"[OK] 删除关联关系: {total_relations_deleted} 条\n")
+
+ # 标记无效模板
+ print("6. 标记无效模板...")
+ mark_invalid_templates(conn, valid_template_names)
+
+ # 统计最终结果
+ print("\n7. 统计最终结果...")
+ final_templates = get_all_templates_from_db(conn)
+ enabled_count = sum(1 for templates in final_templates.values()
+ for t in templates if t.get('state') == 1)
+ disabled_count = sum(1 for templates in final_templates.values()
+ for t in templates if t.get('state') != 1)
+
+ print(f"[OK] 最终模板总数: {sum(len(v) for v in final_templates.values())}")
+ print(f"[OK] 启用模板数: {enabled_count}")
+ print(f"[OK] 禁用模板数: {disabled_count}")
+ print(f"[OK] 唯一模板名称: {len(final_templates)}")
+
+ # 打印最终模板列表
+ print("\n8. 最终模板列表(启用的):")
+ print("=" * 80)
+ for normalized_name, templates in sorted(final_templates.items()):
+ enabled = [t for t in templates if t.get('state') == 1]
+ if enabled:
+ for template in enabled:
+ print(f" - {template['name']} (ID: {template['id']})")
+
+ print("\n" + "=" * 80)
+ print("清理完成!")
+ print("=" * 80)
+
+ except Exception as e:
+ print(f"\n[ERROR] 发生错误: {e}")
+ import traceback
+ traceback.print_exc()
+ if 'conn' in locals():
+ conn.rollback()
+ finally:
+ if 'conn' in locals():
+ conn.close()
+ print("\n数据库连接已关闭")
+
+
+if __name__ == '__main__':
+ main()
+
diff --git a/finalize_template_hierarchy.py b/finalize_template_hierarchy.py
new file mode 100644
index 0000000..9cfc1c6
--- /dev/null
+++ b/finalize_template_hierarchy.py
@@ -0,0 +1,158 @@
+"""
+最终完善模板层级结构
+修复文件路径错误和重复问题
+"""
+import pymysql
+import time
+import random
+from pathlib import Path
+
+DB_CONFIG = {
+ 'host': '152.136.177.240',
+ 'port': 5012,
+ 'user': 'finyx',
+ 'password': '6QsGK6MpePZDE57Z',
+ 'database': 'finyx',
+ 'charset': 'utf8mb4'
+}
+
+TENANT_ID = 615873064429507639
+CREATED_BY = 655162080928945152
+UPDATED_BY = 655162080928945152
+
+def generate_id():
+ timestamp = int(time.time() * 1000)
+ random_part = random.randint(100000, 999999)
+ return timestamp * 1000 + random_part
+
+conn = pymysql.connect(**DB_CONFIG)
+cursor = conn.cursor(pymysql.cursors.DictCursor)
+
+try:
+ # 检查"1请示报告卡"的记录
+ # 根据目录结构,应该有两个不同的文件:
+ # 1. "1.初核请示"下的"1.请示报告卡(XXX).docx"
+ # 2. "走读式谈话审批"下的"1.请示报告卡(初核谈话).docx"
+ cursor.execute("""
+ SELECT id, name, file_path, parent_id
+ FROM f_polic_file_config
+ WHERE tenant_id = %s AND name = %s
+ ORDER BY id
+ """, (TENANT_ID, '1请示报告卡'))
+
+ results = cursor.fetchall()
+
+ # 检查是否在"1.初核请示"下有记录
+ in_initial_request = any(r['parent_id'] == 1765431558933731 for r in results)
+ # 检查是否在"走读式谈话审批"下有记录
+ in_interview_approval = any(r['parent_id'] == 1765273962700431 for r in results)
+
+ if not in_initial_request:
+ # 需要在"1.初核请示"下创建记录
+ new_id = generate_id()
+ insert_sql = """
+ 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, %s)
+ """
+ cursor.execute(insert_sql, (
+ new_id,
+ TENANT_ID,
+ 1765431558933731, # 1.初核请示
+ '1请示报告卡',
+ None,
+ '/615873064429507639/TEMPLATE/2025/12/1.请示报告卡(XXX).docx',
+ CREATED_BY,
+ CREATED_BY,
+ 1
+ ))
+ print(f"[CREATE] 在'1.初核请示'下创建'1请示报告卡'记录 (ID: {new_id})")
+
+ if not in_interview_approval:
+ # 需要在"走读式谈话审批"下创建记录
+ new_id = generate_id()
+ insert_sql = """
+ 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, %s)
+ """
+ cursor.execute(insert_sql, (
+ new_id,
+ TENANT_ID,
+ 1765273962700431, # 走读式谈话审批
+ '1请示报告卡',
+ None,
+ '/615873064429507639/TEMPLATE/2025/12/1.请示报告卡(初核谈话).docx',
+ CREATED_BY,
+ CREATED_BY,
+ 1
+ ))
+ print(f"[CREATE] 在'走读式谈话审批'下创建'1请示报告卡'记录 (ID: {new_id})")
+
+ # 更新现有记录的文件路径
+ for result in results:
+ if result['parent_id'] == 1765431558933731: # 1.初核请示
+ correct_path = '/615873064429507639/TEMPLATE/2025/12/1.请示报告卡(XXX).docx'
+ elif result['parent_id'] == 1765273962700431: # 走读式谈话审批
+ correct_path = '/615873064429507639/TEMPLATE/2025/12/1.请示报告卡(初核谈话).docx'
+ else:
+ continue
+
+ if result['file_path'] != correct_path:
+ cursor.execute("""
+ UPDATE f_polic_file_config
+ SET file_path = %s, updated_time = NOW(), updated_by = %s
+ WHERE tenant_id = %s AND id = %s
+ """, (correct_path, UPDATED_BY, TENANT_ID, result['id']))
+ print(f"[UPDATE] 修复'1请示报告卡'的文件路径 (ID: {result['id']}): {result['file_path']} -> {correct_path}")
+
+ # 检查重复的"XXX初核情况报告"
+ cursor.execute("""
+ SELECT id, name, file_path, parent_id
+ FROM f_polic_file_config
+ WHERE tenant_id = %s AND name LIKE %s
+ ORDER BY id
+ """, (TENANT_ID, '%XXX初核情况报告%'))
+
+ results = cursor.fetchall()
+ if len(results) > 1:
+ # 保留最新的,删除旧的
+ # 或者根据file_path判断哪个是正确的
+ # 根据目录结构,应该是"8.XXX初核情况报告.docx"
+ correct_name = 'XXX初核情况报告'
+ correct_path = '/615873064429507639/TEMPLATE/2025/12/8.XXX初核情况报告.docx'
+
+ for r in results:
+ if r['name'] == '8.XXX初核情况报告':
+ # 这个应该删除(名称带数字前缀)
+ cursor.execute("""
+ DELETE FROM f_polic_file_field
+ WHERE tenant_id = %s AND file_id = %s
+ """, (TENANT_ID, r['id']))
+ cursor.execute("""
+ DELETE FROM f_polic_file_config
+ WHERE tenant_id = %s AND id = %s
+ """, (TENANT_ID, r['id']))
+ print(f"[DELETE] 删除重复记录: {r['name']} (ID: {r['id']})")
+ elif r['name'] == 'XXX初核情况报告':
+ # 更新这个记录的文件路径
+ if r['file_path'] != correct_path:
+ cursor.execute("""
+ UPDATE f_polic_file_config
+ SET file_path = %s, updated_time = NOW(), updated_by = %s
+ WHERE tenant_id = %s AND id = %s
+ """, (correct_path, UPDATED_BY, TENANT_ID, r['id']))
+ print(f"[UPDATE] 更新'XXX初核情况报告'的文件路径: {r['file_path']} -> {correct_path}")
+
+ conn.commit()
+ print("\n[OK] 修复完成")
+
+except Exception as e:
+ conn.rollback()
+ print(f"[ERROR] 修复失败: {e}")
+ import traceback
+ traceback.print_exc()
+finally:
+ cursor.close()
+ conn.close()
+
diff --git a/fix_duplicate_request_report_card.py b/fix_duplicate_request_report_card.py
new file mode 100644
index 0000000..7a67daf
--- /dev/null
+++ b/fix_duplicate_request_report_card.py
@@ -0,0 +1,131 @@
+"""
+修复重复的"1请示报告卡"记录
+确保每个文件在正确的位置只有一个记录
+"""
+import pymysql
+
+DB_CONFIG = {
+ 'host': '152.136.177.240',
+ 'port': 5012,
+ 'user': 'finyx',
+ 'password': '6QsGK6MpePZDE57Z',
+ 'database': 'finyx',
+ 'charset': 'utf8mb4'
+}
+
+TENANT_ID = 615873064429507639
+UPDATED_BY = 655162080928945152
+
+conn = pymysql.connect(**DB_CONFIG)
+cursor = conn.cursor(pymysql.cursors.DictCursor)
+
+try:
+ # 查找所有"1请示报告卡"记录
+ cursor.execute("""
+ SELECT id, name, file_path, parent_id
+ FROM f_polic_file_config
+ WHERE tenant_id = %s AND name = %s
+ ORDER BY id
+ """, (TENANT_ID, '1请示报告卡'))
+
+ results = cursor.fetchall()
+ print(f"找到 {len(results)} 条'1请示报告卡'记录:\n")
+
+ # 根据file_path和parent_id判断哪些是正确的
+ correct_records = []
+ for r in results:
+ print(f"ID: {r['id']}, file_path: {r['file_path']}, parent_id: {r['parent_id']}")
+
+ # 判断是否正确
+ if r['parent_id'] == 1765431558933731: # 1.初核请示
+ if '1.请示报告卡(XXX)' in (r['file_path'] or ''):
+ correct_records.append(r)
+ elif r['parent_id'] == 1765273962700431: # 走读式谈话审批
+ if '1.请示报告卡(初核谈话)' in (r['file_path'] or ''):
+ correct_records.append(r)
+
+ print(f"\n正确的记录数: {len(correct_records)}")
+
+ # 删除不正确的记录
+ for r in results:
+ if r not in correct_records:
+ # 先删除关联关系
+ cursor.execute("""
+ DELETE FROM f_polic_file_field
+ WHERE tenant_id = %s AND file_id = %s
+ """, (TENANT_ID, r['id']))
+ # 删除模板记录
+ cursor.execute("""
+ DELETE FROM f_polic_file_config
+ WHERE tenant_id = %s AND id = %s
+ """, (TENANT_ID, r['id']))
+ print(f"[DELETE] 删除不正确的记录: ID {r['id']}, file_path: {r['file_path']}, parent_id: {r['parent_id']}")
+
+ # 确保两个位置都有正确的记录
+ has_initial_request = any(r['parent_id'] == 1765431558933731 for r in correct_records)
+ has_interview_approval = any(r['parent_id'] == 1765273962700431 for r in correct_records)
+
+ if not has_initial_request:
+ # 创建"1.初核请示"下的记录
+ import time
+ import random
+ timestamp = int(time.time() * 1000)
+ random_part = random.randint(100000, 999999)
+ new_id = timestamp * 1000 + random_part
+
+ insert_sql = """
+ 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, %s)
+ """
+ cursor.execute(insert_sql, (
+ new_id,
+ TENANT_ID,
+ 1765431558933731, # 1.初核请示
+ '1请示报告卡',
+ None,
+ '/615873064429507639/TEMPLATE/2025/12/1.请示报告卡(XXX).docx',
+ 655162080928945152,
+ 655162080928945152,
+ 1
+ ))
+ print(f"[CREATE] 在'1.初核请示'下创建'1请示报告卡'记录 (ID: {new_id})")
+
+ if not has_interview_approval:
+ # 创建"走读式谈话审批"下的记录
+ import time
+ import random
+ timestamp = int(time.time() * 1000)
+ random_part = random.randint(100000, 999999)
+ new_id = timestamp * 1000 + random_part
+
+ insert_sql = """
+ 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, %s)
+ """
+ cursor.execute(insert_sql, (
+ new_id,
+ TENANT_ID,
+ 1765273962700431, # 走读式谈话审批
+ '1请示报告卡',
+ None,
+ '/615873064429507639/TEMPLATE/2025/12/1.请示报告卡(初核谈话).docx',
+ 655162080928945152,
+ 655162080928945152,
+ 1
+ ))
+ print(f"[CREATE] 在'走读式谈话审批'下创建'1请示报告卡'记录 (ID: {new_id})")
+
+ conn.commit()
+ print("\n[OK] 修复完成")
+
+except Exception as e:
+ conn.rollback()
+ print(f"[ERROR] 修复失败: {e}")
+ import traceback
+ traceback.print_exc()
+finally:
+ cursor.close()
+ conn.close()
+
diff --git a/fix_remaining_hierarchy_issues.py b/fix_remaining_hierarchy_issues.py
new file mode 100644
index 0000000..df0137e
--- /dev/null
+++ b/fix_remaining_hierarchy_issues.py
@@ -0,0 +1,61 @@
+"""
+修复剩余的层级结构问题
+"""
+import pymysql
+
+DB_CONFIG = {
+ 'host': '152.136.177.240',
+ 'port': 5012,
+ 'user': 'finyx',
+ 'password': '6QsGK6MpePZDE57Z',
+ 'database': 'finyx',
+ 'charset': 'utf8mb4'
+}
+
+TENANT_ID = 615873064429507639
+UPDATED_BY = 655162080928945152
+
+conn = pymysql.connect(**DB_CONFIG)
+cursor = conn.cursor()
+
+try:
+ # 1. 修复"2保密承诺书"的parent_id(应该在"走读式谈话流程"下)
+ # "走读式谈话流程"的ID是 1765273962716807
+ cursor.execute("""
+ UPDATE f_polic_file_config
+ SET parent_id = %s, updated_time = NOW(), updated_by = %s
+ WHERE tenant_id = %s AND id = %s
+ """, (1765273962716807, UPDATED_BY, TENANT_ID, 1765425919729046))
+ print(f"[UPDATE] 更新'2保密承诺书'的parent_id: {cursor.rowcount} 条")
+
+ # 2. 检查"8.XXX初核情况报告"的位置(应该在"3.初核结论"下,而不是"走读式谈话流程"下)
+ # "3.初核结论"的ID是 1765431559135346
+ # 先查找"8.XXX初核情况报告"的ID
+ cursor.execute("""
+ SELECT id, name, parent_id
+ FROM f_polic_file_config
+ WHERE tenant_id = %s AND name LIKE %s
+ """, (TENANT_ID, '%XXX初核情况报告%'))
+ result = cursor.fetchone()
+ if result:
+ file_id, file_name, current_parent = result
+ if current_parent != 1765431559135346:
+ cursor.execute("""
+ UPDATE f_polic_file_config
+ SET parent_id = %s, updated_time = NOW(), updated_by = %s
+ WHERE tenant_id = %s AND id = %s
+ """, (1765431559135346, UPDATED_BY, TENANT_ID, file_id))
+ print(f"[UPDATE] 更新'{file_name}'的parent_id: {cursor.rowcount} 条")
+
+ conn.commit()
+ print("\n[OK] 修复完成")
+
+except Exception as e:
+ conn.rollback()
+ print(f"[ERROR] 修复失败: {e}")
+ import traceback
+ traceback.print_exc()
+finally:
+ cursor.close()
+ conn.close()
+
diff --git a/fix_template_names.py b/fix_template_names.py
new file mode 100644
index 0000000..e8574a0
--- /dev/null
+++ b/fix_template_names.py
@@ -0,0 +1,234 @@
+"""
+检查并修复 f_polic_file_config 表中模板名称与文件名的对应关系
+确保 name 字段与模板文档名称(去掉扩展名)完全一致
+"""
+import os
+import sys
+import pymysql
+from pathlib import Path
+from typing import Dict, List, Optional
+
+# 设置输出编码为UTF-8(Windows兼容)
+if sys.platform == 'win32':
+ import io
+ sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace')
+ sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8', errors='replace')
+
+# 数据库连接配置
+DB_CONFIG = {
+ 'host': '152.136.177.240',
+ 'port': 5012,
+ 'user': 'finyx',
+ 'password': '6QsGK6MpePZDE57Z',
+ 'database': 'finyx',
+ 'charset': 'utf8mb4'
+}
+
+TENANT_ID = 615873064429507639
+UPDATED_BY = 655162080928945152
+TEMPLATE_BASE_DIR = 'template_finish'
+
+
+def scan_template_files(base_dir: str) -> Dict[str, str]:
+ """
+ 扫描模板文件夹,获取所有模板文件信息
+
+ Returns:
+ 字典,key为MinIO路径(用于匹配),value为文件名(不含扩展名)
+ """
+ base_path = Path(base_dir)
+ if not base_path.exists():
+ print(f"错误: 目录不存在 - {base_dir}")
+ return {}
+
+ templates = {}
+
+ print("=" * 80)
+ print("扫描模板文件...")
+ print("=" * 80)
+
+ for docx_file in sorted(base_path.rglob("*.docx")):
+ # 跳过临时文件
+ if docx_file.name.startswith("~$"):
+ continue
+
+ # 获取文件名(不含扩展名)
+ file_name_without_ext = docx_file.stem
+
+ # 构建MinIO路径(用于匹配数据库中的file_path)
+ from datetime import datetime
+ now = datetime.now()
+ minio_path = f'/615873064429507639/TEMPLATE/{now.year}/{now.month:02d}/{docx_file.name}'
+
+ templates[minio_path] = {
+ 'file_name': docx_file.name,
+ 'name_without_ext': file_name_without_ext,
+ 'relative_path': str(docx_file.relative_to(base_path))
+ }
+
+ print(f"找到 {len(templates)} 个模板文件\n")
+ return templates
+
+
+def get_db_templates(conn) -> Dict[str, Dict]:
+ """
+ 获取数据库中所有模板记录
+
+ Returns:
+ 字典,key为file_path,value为模板信息
+ """
+ cursor = conn.cursor(pymysql.cursors.DictCursor)
+
+ sql = """
+ SELECT id, name, file_path, parent_id
+ FROM f_polic_file_config
+ WHERE tenant_id = %s AND file_path IS NOT NULL
+ """
+ cursor.execute(sql, (TENANT_ID,))
+ templates = cursor.fetchall()
+
+ result = {}
+ for template in templates:
+ if template['file_path']:
+ result[template['file_path']] = {
+ 'id': template['id'],
+ 'name': template['name'],
+ 'file_path': template['file_path'],
+ 'parent_id': template['parent_id']
+ }
+
+ cursor.close()
+ return result
+
+
+def update_template_name(conn, template_id: int, new_name: str, old_name: str):
+ """
+ 更新模板名称
+ """
+ cursor = conn.cursor()
+
+ try:
+ update_sql = """
+ UPDATE f_polic_file_config
+ SET name = %s, updated_time = NOW(), updated_by = %s
+ WHERE id = %s AND tenant_id = %s
+ """
+ cursor.execute(update_sql, (new_name, UPDATED_BY, template_id, TENANT_ID))
+ conn.commit()
+ print(f" [UPDATE] ID: {template_id}")
+ print(f" 旧名称: {old_name}")
+ print(f" 新名称: {new_name}")
+ return True
+ except Exception as e:
+ conn.rollback()
+ print(f" [ERROR] 更新失败: {str(e)}")
+ return False
+ finally:
+ cursor.close()
+
+
+def match_file_path(file_path: str, db_paths: List[str]) -> Optional[str]:
+ """
+ 匹配文件路径(可能日期不同)
+
+ Args:
+ file_path: 当前构建的MinIO路径
+ db_paths: 数据库中的所有路径列表
+
+ Returns:
+ 匹配的数据库路径,如果找到的话
+ """
+ # 提取文件名
+ file_name = Path(file_path).name
+
+ # 在数据库路径中查找相同文件名的路径
+ for db_path in db_paths:
+ if Path(db_path).name == file_name:
+ return db_path
+
+ return None
+
+
+def main():
+ """主函数"""
+ print("=" * 80)
+ print("检查并修复模板名称")
+ print("=" * 80)
+ print()
+
+ try:
+ # 连接数据库
+ print("1. 连接数据库...")
+ conn = pymysql.connect(**DB_CONFIG)
+ print("[OK] 数据库连接成功\n")
+
+ # 扫描模板文件
+ print("2. 扫描模板文件...")
+ file_templates = scan_template_files(TEMPLATE_BASE_DIR)
+
+ # 获取数据库模板
+ print("3. 获取数据库模板...")
+ db_templates = get_db_templates(conn)
+ print(f"[OK] 找到 {len(db_templates)} 个数据库模板\n")
+
+ # 检查并更新
+ print("4. 检查并更新模板名称...")
+ print("=" * 80)
+
+ updated_count = 0
+ not_found_count = 0
+ matched_count = 0
+
+ # 遍历文件模板
+ for file_path, file_info in file_templates.items():
+ file_name = file_info['file_name']
+ expected_name = file_info['name_without_ext']
+
+ # 尝试直接匹配
+ db_template = db_templates.get(file_path)
+
+ # 如果直接匹配失败,尝试通过文件名匹配
+ if not db_template:
+ matched_path = match_file_path(file_path, list(db_templates.keys()))
+ if matched_path:
+ db_template = db_templates[matched_path]
+
+ if db_template:
+ matched_count += 1
+ current_name = db_template['name']
+
+ # 检查名称是否一致
+ if current_name != expected_name:
+ print(f"\n文件: {file_name}")
+ if update_template_name(conn, db_template['id'], expected_name, current_name):
+ updated_count += 1
+ else:
+ print(f" [OK] {file_name} - 名称已正确")
+ else:
+ not_found_count += 1
+ print(f" [WARN] 未找到: {file_name}")
+
+ print("\n" + "=" * 80)
+ print("检查完成")
+ print("=" * 80)
+ print(f"总文件数: {len(file_templates)}")
+ print(f"匹配成功: {matched_count}")
+ print(f"更新数量: {updated_count}")
+ print(f"未找到: {not_found_count}")
+ print("=" * 80)
+
+ except Exception as e:
+ print(f"\n[ERROR] 发生错误: {e}")
+ import traceback
+ traceback.print_exc()
+ if 'conn' in locals():
+ conn.rollback()
+ finally:
+ if 'conn' in locals():
+ conn.close()
+ print("\n数据库连接已关闭")
+
+
+if __name__ == '__main__':
+ main()
+
diff --git a/process_confidentiality_commitment_non_party.py b/process_confidentiality_commitment_non_party.py
new file mode 100644
index 0000000..18f6827
--- /dev/null
+++ b/process_confidentiality_commitment_non_party.py
@@ -0,0 +1,372 @@
+"""
+处理"6.1保密承诺书(谈话对象使用-非中共党员用).docx"
+- 解析占位符
+- 上传到MinIO
+- 更新数据库
+"""
+import os
+import sys
+import re
+import json
+import pymysql
+from minio import Minio
+from minio.error import S3Error
+from datetime import datetime
+from pathlib import Path
+from docx import Document
+from typing import Dict, List, Optional, Tuple
+
+# 设置输出编码为UTF-8(Windows兼容)
+if sys.platform == 'win32':
+ import io
+ sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace')
+ sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8', errors='replace')
+
+# MinIO连接配置
+MINIO_CONFIG = {
+ 'endpoint': 'minio.datacubeworld.com:9000',
+ 'access_key': 'JOLXFXny3avFSzB0uRA5',
+ 'secret_key': 'G1BR8jStNfovkfH5ou39EmPl34E4l7dGrnd3Cz0I',
+ 'secure': True
+}
+
+# 数据库连接配置
+DB_CONFIG = {
+ 'host': '152.136.177.240',
+ 'port': 5012,
+ 'user': 'finyx',
+ 'password': '6QsGK6MpePZDE57Z',
+ 'database': 'finyx',
+ 'charset': 'utf8mb4'
+}
+
+# 固定值
+TENANT_ID = 615873064429507639
+CREATED_BY = 655162080928945152
+UPDATED_BY = 655162080928945152
+BUCKET_NAME = 'finyx'
+
+# 文件路径
+TEMPLATE_FILE = 'template_finish/2-初核模版/2.谈话审批/走读式谈话流程/6.1保密承诺书(谈话对象使用-非中共党员用).docx'
+PARENT_ID = 1765273962716807 # 走读式谈话流程的ID
+TEMPLATE_NAME = '6.1保密承诺书(谈话对象使用-非中共党员用)'
+
+
+def generate_id():
+ """生成ID"""
+ import time
+ import random
+ timestamp = int(time.time() * 1000)
+ random_part = random.randint(100000, 999999)
+ return timestamp * 1000 + random_part
+
+
+def extract_placeholders_from_docx(file_path: str) -> List[str]:
+ """
+ 从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:
+ placeholders.add(match.strip())
+
+ # 从表格中提取占位符
+ 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:
+ placeholders.add(match.strip())
+
+ except Exception as e:
+ print(f" 错误: 读取文件失败 - {str(e)}")
+ return []
+
+ return sorted(list(placeholders))
+
+
+def upload_to_minio(file_path: str, minio_client: Minio) -> str:
+ """
+ 上传文件到MinIO
+
+ Args:
+ file_path: 本地文件路径
+ minio_client: MinIO客户端实例
+
+ Returns:
+ MinIO中的相对路径
+ """
+ try:
+ # 检查存储桶是否存在
+ found = minio_client.bucket_exists(BUCKET_NAME)
+ if not found:
+ raise Exception(f"存储桶 '{BUCKET_NAME}' 不存在,请先创建")
+
+ # 生成MinIO对象路径(使用当前日期)
+ now = datetime.now()
+ file_name = Path(file_path).name
+ object_name = f'{TENANT_ID}/TEMPLATE/{now.year}/{now.month:02d}/{file_name}'
+
+ # 上传文件
+ minio_client.fput_object(
+ BUCKET_NAME,
+ object_name,
+ file_path,
+ content_type='application/vnd.openxmlformats-officedocument.wordprocessingml.document'
+ )
+
+ # 返回相对路径(以/开头)
+ return f"/{object_name}"
+
+ except S3Error as e:
+ raise Exception(f"MinIO错误: {e}")
+ except Exception as e:
+ raise Exception(f"上传文件时发生错误: {e}")
+
+
+def get_db_fields(conn) -> Dict[str, Dict]:
+ """
+ 获取数据库中所有字段(field_type=2的输出字段)
+
+ Returns:
+ 字典,key为filed_code,value为字段信息
+ """
+ cursor = conn.cursor(pymysql.cursors.DictCursor)
+
+ sql = """
+ SELECT id, name, filed_code, field_type
+ FROM f_polic_field
+ WHERE tenant_id = %s AND field_type = 2
+ """
+ cursor.execute(sql, (TENANT_ID,))
+ fields = cursor.fetchall()
+
+ result = {}
+ for field in fields:
+ result[field['filed_code']] = {
+ 'id': field['id'],
+ 'name': field['name'],
+ 'filed_code': field['filed_code'],
+ 'field_type': field['field_type']
+ }
+
+ cursor.close()
+ return result
+
+
+def match_placeholders_to_fields(placeholders: List[str], fields: Dict[str, Dict]) -> Tuple[List[int], List[str]]:
+ """
+ 匹配占位符到数据库字段
+
+ Returns:
+ (匹配的字段ID列表, 未匹配的占位符列表)
+ """
+ matched_field_ids = []
+ unmatched_placeholders = []
+
+ for placeholder in placeholders:
+ if placeholder in fields:
+ matched_field_ids.append(fields[placeholder]['id'])
+ else:
+ unmatched_placeholders.append(placeholder)
+
+ return matched_field_ids, unmatched_placeholders
+
+
+def create_or_update_template(conn, template_name: str, file_path: str, minio_path: str, parent_id: Optional[int]) -> int:
+ """
+ 创建或更新模板记录
+
+ Returns:
+ 模板ID
+ """
+ cursor = conn.cursor(pymysql.cursors.DictCursor)
+
+ try:
+ # 查找是否已存在(通过名称和parent_id匹配)
+ sql = """
+ SELECT id, name, file_path, parent_id
+ FROM f_polic_file_config
+ WHERE tenant_id = %s AND name = %s AND parent_id = %s
+ """
+ cursor.execute(sql, (TENANT_ID, template_name, parent_id))
+ existing = cursor.fetchone()
+
+ if existing:
+ # 更新现有记录
+ template_id = existing['id']
+ update_sql = """
+ UPDATE f_polic_file_config
+ SET file_path = %s, updated_time = NOW(), updated_by = %s, state = 1
+ WHERE id = %s AND tenant_id = %s
+ """
+ cursor.execute(update_sql, (minio_path, UPDATED_BY, template_id, TENANT_ID))
+ conn.commit()
+ print(f" [UPDATE] 更新模板记录 (ID: {template_id})")
+ return template_id
+ else:
+ # 创建新记录
+ template_id = generate_id()
+ insert_sql = """
+ 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, %s)
+ """
+ cursor.execute(insert_sql, (
+ template_id,
+ TENANT_ID,
+ parent_id,
+ template_name,
+ None, # input_data
+ minio_path,
+ CREATED_BY,
+ CREATED_BY,
+ 1 # state: 1表示启用
+ ))
+ conn.commit()
+ print(f" [CREATE] 创建模板记录 (ID: {template_id})")
+ return template_id
+
+ except Exception as e:
+ conn.rollback()
+ raise Exception(f"创建或更新模板失败: {str(e)}")
+ finally:
+ cursor.close()
+
+
+def update_template_field_relations(conn, template_id: int, field_ids: List[int]):
+ """
+ 更新模板-字段关联关系
+ """
+ cursor = conn.cursor()
+
+ try:
+ # 删除旧的关联关系
+ delete_sql = """
+ DELETE FROM f_polic_file_field
+ WHERE tenant_id = %s AND file_id = %s
+ """
+ cursor.execute(delete_sql, (TENANT_ID, template_id))
+
+ # 插入新的关联关系
+ if field_ids:
+ insert_sql = """
+ INSERT INTO f_polic_file_field
+ (tenant_id, file_id, filed_id, created_time, created_by, updated_time, updated_by)
+ VALUES (%s, %s, %s, NOW(), %s, NOW(), %s)
+ """
+ for field_id in field_ids:
+ cursor.execute(insert_sql, (TENANT_ID, template_id, field_id, CREATED_BY, CREATED_BY))
+
+ conn.commit()
+ print(f" [UPDATE] 更新字段关联关系: {len(field_ids)} 个字段")
+
+ except Exception as e:
+ conn.rollback()
+ raise Exception(f"更新字段关联关系失败: {str(e)}")
+ finally:
+ cursor.close()
+
+
+def main():
+ """主函数"""
+ print("=" * 80)
+ print("处理保密承诺书(非中共党员用)模板")
+ print("=" * 80)
+ print()
+
+ # 检查文件是否存在
+ if not os.path.exists(TEMPLATE_FILE):
+ print(f"错误: 文件不存在 - {TEMPLATE_FILE}")
+ return
+
+ print(f"文件路径: {TEMPLATE_FILE}")
+ print()
+
+ try:
+ # 1. 提取占位符
+ print("1. 提取占位符...")
+ placeholders = extract_placeholders_from_docx(TEMPLATE_FILE)
+ print(f" 找到 {len(placeholders)} 个占位符:")
+ for i, placeholder in enumerate(placeholders, 1):
+ print(f" {i}. {{{{ {placeholder} }}}}")
+ print()
+
+ # 2. 连接数据库和MinIO
+ print("2. 连接数据库和MinIO...")
+ conn = pymysql.connect(**DB_CONFIG)
+ minio_client = Minio(
+ MINIO_CONFIG['endpoint'],
+ access_key=MINIO_CONFIG['access_key'],
+ secret_key=MINIO_CONFIG['secret_key'],
+ secure=MINIO_CONFIG['secure']
+ )
+ print(" [OK] 连接成功\n")
+
+ # 3. 获取数据库字段
+ print("3. 获取数据库字段...")
+ db_fields = get_db_fields(conn)
+ print(f" [OK] 找到 {len(db_fields)} 个输出字段\n")
+
+ # 4. 匹配占位符到字段
+ print("4. 匹配占位符到字段...")
+ matched_field_ids, unmatched_placeholders = match_placeholders_to_fields(placeholders, db_fields)
+ print(f" 匹配成功: {len(matched_field_ids)} 个")
+ print(f" 未匹配: {len(unmatched_placeholders)} 个")
+ if unmatched_placeholders:
+ print(f" 未匹配的占位符: {', '.join(unmatched_placeholders)}")
+ print()
+
+ # 5. 上传到MinIO
+ print("5. 上传到MinIO...")
+ minio_path = upload_to_minio(TEMPLATE_FILE, minio_client)
+ print(f" [OK] MinIO路径: {minio_path}\n")
+
+ # 6. 创建或更新数据库记录
+ print("6. 创建或更新数据库记录...")
+ template_id = create_or_update_template(conn, TEMPLATE_NAME, TEMPLATE_FILE, minio_path, PARENT_ID)
+ print(f" [OK] 模板ID: {template_id}\n")
+
+ # 7. 更新字段关联关系
+ print("7. 更新字段关联关系...")
+ update_template_field_relations(conn, template_id, matched_field_ids)
+ print()
+
+ print("=" * 80)
+ print("处理完成!")
+ print("=" * 80)
+ print(f"模板ID: {template_id}")
+ print(f"MinIO路径: {minio_path}")
+ print(f"关联字段数: {len(matched_field_ids)}")
+
+ except Exception as e:
+ print(f"\n[ERROR] 发生错误: {e}")
+ import traceback
+ traceback.print_exc()
+ if 'conn' in locals():
+ conn.rollback()
+ finally:
+ if 'conn' in locals():
+ conn.close()
+ print("\n数据库连接已关闭")
+
+
+if __name__ == '__main__':
+ main()
+
diff --git a/services/__pycache__/document_service.cpython-312.pyc b/services/__pycache__/document_service.cpython-312.pyc
index 5151641..f875437 100644
Binary files a/services/__pycache__/document_service.cpython-312.pyc and b/services/__pycache__/document_service.cpython-312.pyc differ
diff --git a/services/document_service.py b/services/document_service.py
index 841e7db..8e7a64d 100644
--- a/services/document_service.py
+++ b/services/document_service.py
@@ -138,7 +138,6 @@ class DocumentService:
doc = Document(template_path)
print(f"[DEBUG] 文档包含 {len(doc.paragraphs)} 个段落, {len(doc.tables)} 个表格")
-<<<<<<< HEAD
def replace_placeholder_in_paragraph(paragraph):
"""在段落中替换占位符(处理跨run的情况)"""
try:
@@ -204,18 +203,6 @@ class DocumentService:
if placeholder in before_text and placeholder not in after_text:
replaced_placeholders.add(field_code)
total_replacements += before_text.count(placeholder)
-=======
- # 替换占位符 {{field_code}} 为实际值
- for paragraph in doc.paragraphs:
- # 替换段落文本中的占位符
- for field_code, field_value in field_data.items():
- placeholder = f"{{{{{field_code}}}}}"
- if placeholder in paragraph.text:
- # 替换占位符
- for run in paragraph.runs:
- if placeholder in run.text:
- run.text = run.text.replace(placeholder, field_value or '')
->>>>>>> parent of 4897c96 (添加通过taskId获取文档的接口,支持文件列表查询和参数验证,增强错误处理能力。同时,优化文档生成逻辑,确保生成的文档名称和路径的准确性。)
# 替换表格中的占位符
try:
@@ -264,7 +251,6 @@ class DocumentService:
for table in doc.tables:
for row in table.rows:
for cell in row.cells:
-<<<<<<< HEAD
if hasattr(cell, 'paragraphs'):
for paragraph in cell.paragraphs:
text = paragraph.text
@@ -281,15 +267,6 @@ class DocumentService:
print(f" - ⚠️ 仍有未替换的占位符: {sorted(remaining_placeholders)}")
else:
print(f" - ✓ 所有占位符已成功替换")
-=======
- for paragraph in cell.paragraphs:
- for field_code, field_value in field_data.items():
- placeholder = f"{{{{{field_code}}}}}"
- if placeholder in paragraph.text:
- for run in paragraph.runs:
- if placeholder in run.text:
- run.text = run.text.replace(placeholder, field_value or '')
->>>>>>> parent of 4897c96 (添加通过taskId获取文档的接口,支持文件列表查询和参数验证,增强错误处理能力。同时,优化文档生成逻辑,确保生成的文档名称和路径的准确性。)
# 保存到临时文件
temp_dir = tempfile.gettempdir()
@@ -493,9 +470,11 @@ class DocumentService:
if target_name and target_name.strip():
suffix = f"_{target_name.strip()}"
-<<<<<<< HEAD
- # 生成新文件名
- return f"{base_name}{suffix}.docx"
+ # 生成新文件名(确保是.docx格式)
+ generated_name = f"{base_name}{suffix}.docx"
+ print(f"[DEBUG] 文档名称生成: '{original_file_name}' -> '{generated_name}' (base_name='{base_name}', suffix='{suffix}')")
+
+ return generated_name
def generate_presigned_download_url(self, file_path: str, expires_days: int = 7) -> Optional[str]:
"""
@@ -530,11 +509,4 @@ class DocumentService:
# 如果生成URL失败,记录错误但不影响主流程
print(f"生成预签名URL失败: {str(e)}")
return None
-=======
- # 生成新文件名(确保是.docx格式)
- generated_name = f"{base_name}{suffix}.docx"
- print(f"[DEBUG] 文档名称生成: '{original_file_name}' -> '{generated_name}' (base_name='{base_name}', suffix='{suffix}')")
-
- return generated_name
->>>>>>> e3f4a394c1a4333db2fd3a9383be29fa9d9055e0
diff --git a/static/index.html b/static/index.html
index 3a4e600..dacda09 100644
--- a/static/index.html
+++ b/static/index.html
@@ -326,10 +326,14 @@
-
+
-
-
+
+
+
+
+
+ 💡 提示:点击"加载全部可用模板"可以加载所有可用的文档模板类型,方便测试不同模板的生成效果
@@ -568,16 +572,12 @@
return;
}
- // 添加前5个文件作为示例
- filesWithPath.slice(0, 5).forEach(file => {
+ // 加载所有可用文件
+ filesWithPath.forEach(file => {
addFileItem(file.fileId, file.fileName);
});
- if (filesWithPath.length > 5) {
- alert(`已加载前5个文件,共找到 ${filesWithPath.length} 个可用文件`);
- } else {
- alert(`已加载 ${filesWithPath.length} 个可用文件`);
- }
+ alert(`已加载全部 ${filesWithPath.length} 个可用文件模板`);
} else {
alert('获取文件列表失败: ' + (result.errorMsg || '未知错误'));
}
@@ -603,7 +603,7 @@
addGenerateField('department_opinion', '');
addGenerateField('filler_name', '');
- // 自动加载可用的文件列表(只加载前2个作为示例)
+ // 自动加载所有可用的文件列表
try {
const response = await fetch('/api/file-configs');
const result = await response.json();
@@ -612,19 +612,19 @@
// 只添加有filePath的文件(有模板文件的)
const filesWithPath = result.data.fileConfigs.filter(f => f.filePath);
- // 添加前2个文件作为示例
- filesWithPath.slice(0, 2).forEach(file => {
+ // 加载所有可用文件
+ filesWithPath.forEach(file => {
addFileItem(file.fileId, file.fileName);
});
+
+ if (filesWithPath.length > 0) {
+ console.log(`已自动加载 ${filesWithPath.length} 个可用文件模板`);
+ }
} else {
- // 如果加载失败,使用默认的fileId
- addFileItem(1765273961883544, '初步核实审批表.doc'); // 2.初步核实审批表(XXX)
- addFileItem(1765273961563507, '请示报告卡.doc'); // 1.请示报告卡(XXX)
+ console.warn('未找到可用的文件配置');
}
} catch (error) {
- // 如果加载失败,使用默认的fileId
- addFileItem(1765273961883544, '初步核实审批表.doc');
- addFileItem(1765273961563507, '请示报告卡.doc');
+ console.warn('自动加载文件列表失败:', error);
}
}
@@ -769,8 +769,15 @@
result.data.fpolicFieldParamFileList.forEach(file => {
html += `
${file.fileName}:
- 文件路径: ${file.filePath || '(无路径)'}
-
`;
+ 文件路径: ${file.filePath || '(无路径)'}
`;
+
+ // 如果有下载链接,显示可点击的链接
+ if (file.downloadUrl) {
+ html += `下载链接:
${file.downloadUrl}`;
+ html += `
`;
+ }
+
+ html += `
`;
});
}
}
@@ -796,6 +803,12 @@
btn.closest('.field-row').remove();
}
+ function clearAllFiles() {
+ if (confirm('确定要清空所有文件列表吗?')) {
+ document.getElementById('fileListContainer').innerHTML = '';
+ }
+ }
+
function displayError(tabType, errorMsg) {
const resultSection = document.getElementById(tabType + 'ResultSection');
const resultBox = document.getElementById(tabType + 'ResultBox');
diff --git a/template_finish/2-初核模版/1.初核请示/~$初步核实审批表(XXX).docx b/template_finish/2-初核模版/1.初核请示/~$初步核实审批表(XXX).docx
new file mode 100644
index 0000000..8efa2ad
Binary files /dev/null and b/template_finish/2-初核模版/1.初核请示/~$初步核实审批表(XXX).docx differ
diff --git a/template_finish/2-初核模版/1.初核请示/~$请示报告卡(XXX).docx b/template_finish/2-初核模版/1.初核请示/~$请示报告卡(XXX).docx
new file mode 100644
index 0000000..8efa2ad
Binary files /dev/null and b/template_finish/2-初核模版/1.初核请示/~$请示报告卡(XXX).docx differ
diff --git a/template_finish/2-初核模版/3.初核结论/8-1请示报告卡(初核报告结论) .docx b/template_finish/2-初核模版/3.初核结论/8-1请示报告卡(初核报告结论) .docx
index f2a499e..e42f7e5 100644
Binary files a/template_finish/2-初核模版/3.初核结论/8-1请示报告卡(初核报告结论) .docx and b/template_finish/2-初核模版/3.初核结论/8-1请示报告卡(初核报告结论) .docx differ
diff --git a/template_finish/2-初核模版/3.初核结论/~$1请示报告卡(初核报告结论) .docx b/template_finish/2-初核模版/3.初核结论/~$1请示报告卡(初核报告结论) .docx
new file mode 100644
index 0000000..8efa2ad
Binary files /dev/null and b/template_finish/2-初核模版/3.初核结论/~$1请示报告卡(初核报告结论) .docx differ
diff --git a/template_finish/2-初核模版/3.初核结论/~$XXX初核情况报告.docx b/template_finish/2-初核模版/3.初核结论/~$XXX初核情况报告.docx
new file mode 100644
index 0000000..8efa2ad
Binary files /dev/null and b/template_finish/2-初核模版/3.初核结论/~$XXX初核情况报告.docx differ
diff --git a/update_template_hierarchy.py b/update_template_hierarchy.py
new file mode 100644
index 0000000..67dc68b
--- /dev/null
+++ b/update_template_hierarchy.py
@@ -0,0 +1,472 @@
+"""
+根据 template_finish/ 目录结构更新 f_polic_file_config 表中的层级结构
+"""
+import os
+import sys
+import json
+import pymysql
+from pathlib import Path
+from typing import Dict, List, Optional, Tuple
+from collections import defaultdict
+
+# 设置输出编码为UTF-8(Windows兼容)
+if sys.platform == 'win32':
+ import io
+ sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace')
+ sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8', errors='replace')
+
+# 数据库连接配置
+DB_CONFIG = {
+ 'host': '152.136.177.240',
+ 'port': 5012,
+ 'user': 'finyx',
+ 'password': '6QsGK6MpePZDE57Z',
+ 'database': 'finyx',
+ 'charset': 'utf8mb4'
+}
+
+TENANT_ID = 615873064429507639
+CREATED_BY = 655162080928945152
+UPDATED_BY = 655162080928945152
+TEMPLATE_BASE_DIR = 'template_finish'
+
+
+def generate_id():
+ """生成ID"""
+ import time
+ import random
+ timestamp = int(time.time() * 1000)
+ random_part = random.randint(100000, 999999)
+ return timestamp * 1000 + random_part
+
+
+def normalize_name(name: str) -> str:
+ """
+ 标准化名称(去掉扩展名、括号内容、数字前缀等)
+ 用于匹配数据库中的记录
+ """
+ # 去掉扩展名
+ name = Path(name).stem if '.' in name else name
+
+ # 去掉括号内容
+ import re
+ name = re.sub(r'[((].*?[))]', '', name)
+ name = name.strip()
+
+ # 去掉数字前缀和点号
+ name = re.sub(r'^\d+[\.\-]?\s*', '', name)
+ name = name.strip()
+
+ return name
+
+
+def scan_directory_structure(base_dir: str) -> Dict:
+ """
+ 扫描目录结构,构建层级关系
+
+ Returns:
+ 字典,包含目录和文件的层级信息
+ """
+ base_path = Path(base_dir)
+ if not base_path.exists():
+ print(f"错误: 目录不存在 - {base_dir}")
+ return {}
+
+ structure = {
+ 'directories': [], # 目录节点列表
+ 'files': [], # 文件节点列表
+ 'name_to_id': {} # 名称到ID的映射(用于查找parent_id)
+ }
+
+ print("=" * 80)
+ print("扫描目录结构...")
+ print("=" * 80)
+
+ # 遍历所有目录和文件
+ for item in base_path.rglob("*"):
+ relative_path = item.relative_to(base_path)
+ parts = relative_path.parts
+
+ if item.is_dir():
+ # 目录节点
+ level = len(parts) - 1 # 层级(从0开始)
+ dir_name = parts[-1]
+ parent_path = str(Path(*parts[:-1])) if len(parts) > 1 else None
+
+ structure['directories'].append({
+ 'name': dir_name,
+ 'path': str(relative_path),
+ 'level': level,
+ 'parent_path': parent_path,
+ 'parent_id': None # 稍后设置
+ })
+
+ elif item.is_file() and item.suffix == '.docx' and not item.name.startswith("~$"):
+ # 文件节点
+ level = len(parts) - 1
+ file_name = item.name
+ parent_path = str(Path(*parts[:-1])) if len(parts) > 1 else None
+
+ structure['files'].append({
+ 'name': file_name,
+ 'path': str(relative_path),
+ 'level': level,
+ 'parent_path': parent_path,
+ 'parent_id': None, # 稍后设置
+ 'file_path': str(item)
+ })
+
+ # 按层级排序
+ structure['directories'].sort(key=lambda x: (x['level'], x['path']))
+ structure['files'].sort(key=lambda x: (x['level'], x['path']))
+
+ print(f"找到 {len(structure['directories'])} 个目录节点")
+ print(f"找到 {len(structure['files'])} 个文件节点")
+
+ return structure
+
+
+def get_existing_templates(conn) -> Dict:
+ """
+ 获取数据库中现有的模板记录
+
+ Returns:
+ 字典,key为标准化名称,value为模板信息
+ """
+ cursor = conn.cursor(pymysql.cursors.DictCursor)
+
+ sql = """
+ SELECT id, name, parent_id, file_path, state
+ FROM f_polic_file_config
+ WHERE tenant_id = %s
+ """
+ cursor.execute(sql, (TENANT_ID,))
+ templates = cursor.fetchall()
+
+ result = {}
+ for template in templates:
+ normalized_name = normalize_name(template['name'])
+ result[normalized_name] = {
+ 'id': template['id'],
+ 'name': template['name'],
+ 'normalized_name': normalized_name,
+ 'parent_id': template['parent_id'],
+ 'file_path': template['file_path'],
+ 'state': template['state']
+ }
+
+ cursor.close()
+ return result
+
+
+def find_template_by_name(existing_templates: Dict, name: str, prefer_directory: bool = False) -> Optional[Dict]:
+ """
+ 根据名称查找模板(支持标准化匹配)
+
+ Args:
+ name: 模板名称
+ prefer_directory: 是否优先匹配目录节点
+ """
+ normalized = normalize_name(name)
+
+ # 精确匹配标准化名称
+ if normalized in existing_templates:
+ template = existing_templates[normalized]
+ # 如果prefer_directory为True,且找到的是文件,继续查找目录
+ if prefer_directory and template.get('file_path') is not None:
+ pass # 继续查找
+ else:
+ return template
+
+ # 模糊匹配(包含关系)
+ candidates = []
+ for key, template in existing_templates.items():
+ if key.startswith("DIR:"):
+ # 目录节点
+ if normalized in template.get('normalized_name', '') or template.get('normalized_name', '') in normalized:
+ candidates.append((template, True))
+ else:
+ # 文件节点
+ if normalized in template.get('normalized_name', '') or template.get('normalized_name', '') in normalized:
+ candidates.append((template, False))
+
+ # 如果prefer_directory,优先返回目录节点
+ if prefer_directory:
+ for template, is_dir in candidates:
+ if is_dir:
+ return template
+
+ # 返回第一个匹配的
+ if candidates:
+ return candidates[0][0]
+
+ return None
+
+
+def create_or_update_directory(conn, dir_info: Dict, parent_id: Optional[int], existing_templates: Dict) -> int:
+ """
+ 创建或更新目录节点
+
+ Returns:
+ 目录节点的ID
+ """
+ cursor = conn.cursor()
+
+ try:
+ # 先通过路径查找(最准确)
+ path_key = f"DIR:{dir_info['path']}"
+ existing = existing_templates.get(path_key)
+
+ # 如果没找到,再通过名称查找(优先目录节点)
+ if not existing:
+ existing = find_template_by_name(existing_templates, dir_info['name'], prefer_directory=True)
+ # 确保找到的是目录节点(file_path为None)
+ if existing and existing.get('file_path') is not None:
+ existing = None
+
+ if existing:
+ # 更新现有记录
+ template_id = existing['id']
+ if existing['parent_id'] != parent_id:
+ update_sql = """
+ UPDATE f_polic_file_config
+ SET parent_id = %s, updated_time = NOW(), updated_by = %s, state = 1
+ WHERE id = %s AND tenant_id = %s
+ """
+ cursor.execute(update_sql, (parent_id, UPDATED_BY, template_id, TENANT_ID))
+ conn.commit()
+ print(f" [UPDATE] 更新目录: {dir_info['name']} (ID: {template_id}, parent_id: {parent_id})")
+ else:
+ print(f" [KEEP] 保持目录: {dir_info['name']} (ID: {template_id})")
+ return template_id
+ else:
+ # 创建新记录
+ template_id = generate_id()
+ insert_sql = """
+ 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, %s)
+ """
+ cursor.execute(insert_sql, (
+ template_id,
+ TENANT_ID,
+ parent_id,
+ dir_info['name'],
+ None, # input_data
+ None, # file_path(目录节点没有文件路径)
+ CREATED_BY,
+ CREATED_BY,
+ 1 # state: 1表示启用
+ ))
+ conn.commit()
+ print(f" [CREATE] 创建目录: {dir_info['name']} (ID: {template_id}, parent_id: {parent_id})")
+ return template_id
+
+ except Exception as e:
+ conn.rollback()
+ raise Exception(f"创建或更新目录失败: {str(e)}")
+ finally:
+ cursor.close()
+
+
+def update_file_parent(conn, file_info: Dict, parent_id: Optional[int], existing_templates: Dict) -> Optional[int]:
+ """
+ 更新文件节点的parent_id
+
+ Returns:
+ 文件节点的ID,如果未找到则返回None
+ """
+ cursor = conn.cursor()
+
+ try:
+ # 查找文件(使用文件名匹配)
+ existing = find_template_by_name(existing_templates, file_info['name'])
+
+ if existing:
+ template_id = existing['id']
+ if existing['parent_id'] != parent_id:
+ update_sql = """
+ UPDATE f_polic_file_config
+ SET parent_id = %s, updated_time = NOW(), updated_by = %s
+ WHERE id = %s AND tenant_id = %s
+ """
+ cursor.execute(update_sql, (parent_id, UPDATED_BY, template_id, TENANT_ID))
+ conn.commit()
+ print(f" [UPDATE] 更新文件: {file_info['name']} (ID: {template_id}, parent_id: {parent_id})")
+ else:
+ print(f" [KEEP] 保持文件: {file_info['name']} (ID: {template_id})")
+ return template_id
+ else:
+ print(f" [WARN] 未找到文件: {file_info['name']}")
+ return None
+
+ except Exception as e:
+ conn.rollback()
+ raise Exception(f"更新文件parent_id失败: {str(e)}")
+ finally:
+ cursor.close()
+
+
+def build_path_to_id_map(structure: Dict, existing_templates: Dict, conn) -> Dict[str, int]:
+ """
+ 构建路径到ID的映射
+
+ Returns:
+ 字典,key为路径,value为ID
+ """
+ path_to_id = {}
+
+ # 处理目录节点(按层级顺序,确保父节点先处理)
+ # 按层级和路径排序
+ sorted_dirs = sorted(structure['directories'], key=lambda x: (x['level'], x['path']))
+
+ for dir_info in sorted_dirs:
+ parent_id = None
+ if dir_info['parent_path']:
+ parent_id = path_to_id.get(dir_info['parent_path'])
+ if parent_id is None:
+ print(f" [WARN] 未找到父目录: {dir_info['parent_path']}")
+
+ dir_id = create_or_update_directory(conn, dir_info, parent_id, existing_templates)
+ path_to_id[dir_info['path']] = dir_id
+
+ # 更新existing_templates,以便后续查找(使用完整路径作为key避免冲突)
+ key = f"DIR:{dir_info['path']}"
+ existing_templates[key] = {
+ 'id': dir_id,
+ 'name': dir_info['name'],
+ 'normalized_name': normalize_name(dir_info['name']),
+ 'parent_id': parent_id,
+ 'file_path': None,
+ 'state': 1,
+ 'path': dir_info['path']
+ }
+ # 同时用标准化名称存储(用于文件查找父目录)
+ normalized_key = normalize_name(dir_info['name'])
+ if normalized_key not in existing_templates or existing_templates[normalized_key].get('file_path') is not None:
+ # 只有当不存在或存在的是文件时才更新
+ existing_templates[normalized_key] = {
+ 'id': dir_id,
+ 'name': dir_info['name'],
+ 'normalized_name': normalized_key,
+ 'parent_id': parent_id,
+ 'file_path': None,
+ 'state': 1,
+ 'path': dir_info['path']
+ }
+
+ return path_to_id
+
+
+def update_file_hierarchy(structure: Dict, path_to_id: Dict[str, int], existing_templates: Dict, conn):
+ """
+ 更新文件节点的parent_id
+ """
+ for file_info in structure['files']:
+ parent_id = None
+ if file_info['parent_path']:
+ parent_id = path_to_id.get(file_info['parent_path'])
+
+ update_file_parent(conn, file_info, parent_id, existing_templates)
+
+
+def main():
+ """主函数"""
+ print("=" * 80)
+ print("更新模板层级结构")
+ print("=" * 80)
+ print()
+
+ try:
+ # 连接数据库
+ print("1. 连接数据库...")
+ conn = pymysql.connect(**DB_CONFIG)
+ print("[OK] 数据库连接成功\n")
+
+ # 扫描目录结构
+ print("2. 扫描目录结构...")
+ structure = scan_directory_structure(TEMPLATE_BASE_DIR)
+ if not structure:
+ print("错误: 未找到任何目录或文件")
+ return
+
+ # 获取现有模板
+ print("\n3. 获取现有模板...")
+ existing_templates = get_existing_templates(conn)
+ print(f"[OK] 找到 {len(existing_templates)} 个现有模板\n")
+
+ # 构建路径到ID的映射(处理目录节点)
+ print("4. 创建/更新目录节点...")
+ print("=" * 80)
+ path_to_id = build_path_to_id_map(structure, existing_templates, conn)
+ print(f"\n[OK] 处理了 {len(path_to_id)} 个目录节点\n")
+
+ # 更新文件节点的parent_id
+ print("5. 更新文件节点的parent_id...")
+ print("=" * 80)
+ update_file_hierarchy(structure, path_to_id, existing_templates, conn)
+ print(f"\n[OK] 处理了 {len(structure['files'])} 个文件节点\n")
+
+ # 打印层级结构
+ print("6. 最终层级结构:")
+ print("=" * 80)
+ print_hierarchy(conn)
+
+ print("\n" + "=" * 80)
+ print("更新完成!")
+ print("=" * 80)
+
+ except Exception as e:
+ print(f"\n[ERROR] 发生错误: {e}")
+ import traceback
+ traceback.print_exc()
+ if 'conn' in locals():
+ conn.rollback()
+ finally:
+ if 'conn' in locals():
+ conn.close()
+ print("\n数据库连接已关闭")
+
+
+def print_hierarchy(conn, parent_id=None, level=0, prefix=""):
+ """打印层级结构"""
+ cursor = conn.cursor(pymysql.cursors.DictCursor)
+
+ try:
+ if parent_id is None:
+ sql = """
+ SELECT id, name, parent_id, file_path
+ FROM f_polic_file_config
+ WHERE tenant_id = %s AND parent_id IS NULL
+ ORDER BY name
+ """
+ cursor.execute(sql, (TENANT_ID,))
+ else:
+ sql = """
+ SELECT id, name, parent_id, file_path
+ FROM f_polic_file_config
+ WHERE tenant_id = %s AND parent_id = %s
+ ORDER BY name
+ """
+ cursor.execute(sql, (TENANT_ID, parent_id))
+
+ items = cursor.fetchall()
+
+ for i, item in enumerate(items):
+ is_last = i == len(items) - 1
+ current_prefix = prefix + ("└── " if is_last else "├── ")
+ next_prefix = prefix + (" " if is_last else "│ ")
+
+ node_type = "📁" if item['file_path'] is None else "📄"
+ print(f"{current_prefix}{node_type} {item['name']} (ID: {item['id']})")
+
+ # 递归打印子节点
+ print_hierarchy(conn, item['id'], level + 1, next_prefix)
+
+ finally:
+ cursor.close()
+
+
+if __name__ == '__main__':
+ main()
+
diff --git a/update_template_hierarchy_final.py b/update_template_hierarchy_final.py
new file mode 100644
index 0000000..4499db4
--- /dev/null
+++ b/update_template_hierarchy_final.py
@@ -0,0 +1,405 @@
+"""
+根据 template_finish/ 目录结构更新 f_polic_file_config 表中的层级结构
+使用file_path作为唯一标识,确保正确建立层级关系
+"""
+import os
+import sys
+import json
+import pymysql
+from pathlib import Path
+from typing import Dict, List, Optional, Tuple
+from collections import defaultdict
+from datetime import datetime
+
+# 设置输出编码为UTF-8(Windows兼容)
+if sys.platform == 'win32':
+ import io
+ sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace')
+ sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8', errors='replace')
+
+# 数据库连接配置
+DB_CONFIG = {
+ 'host': '152.136.177.240',
+ 'port': 5012,
+ 'user': 'finyx',
+ 'password': '6QsGK6MpePZDE57Z',
+ 'database': 'finyx',
+ 'charset': 'utf8mb4'
+}
+
+TENANT_ID = 615873064429507639
+CREATED_BY = 655162080928945152
+UPDATED_BY = 655162080928945152
+TEMPLATE_BASE_DIR = 'template_finish'
+
+
+def generate_id():
+ """生成ID"""
+ import time
+ import random
+ timestamp = int(time.time() * 1000)
+ random_part = random.randint(100000, 999999)
+ return timestamp * 1000 + random_part
+
+
+def scan_directory_structure(base_dir: str) -> Dict:
+ """
+ 扫描目录结构,构建层级关系
+
+ Returns:
+ 字典,包含目录和文件的层级信息
+ """
+ base_path = Path(base_dir)
+ if not base_path.exists():
+ print(f"错误: 目录不存在 - {base_dir}")
+ return {}
+
+ structure = {
+ 'directories': [], # 目录节点列表
+ 'files': [] # 文件节点列表
+ }
+
+ print("=" * 80)
+ print("扫描目录结构...")
+ print("=" * 80)
+
+ # 遍历所有目录和文件
+ for item in base_path.rglob("*"):
+ relative_path = item.relative_to(base_path)
+ parts = relative_path.parts
+
+ if item.is_dir():
+ # 目录节点
+ level = len(parts) - 1 # 层级(从0开始)
+ dir_name = parts[-1]
+ parent_path = str(Path(*parts[:-1])) if len(parts) > 1 else None
+
+ structure['directories'].append({
+ 'name': dir_name,
+ 'path': str(relative_path),
+ 'level': level,
+ 'parent_path': parent_path
+ })
+
+ elif item.is_file() and item.suffix == '.docx' and not item.name.startswith("~$"):
+ # 文件节点
+ level = len(parts) - 1
+ file_name = item.name
+ parent_path = str(Path(*parts[:-1])) if len(parts) > 1 else None
+
+ # 构建MinIO路径
+ now = datetime.now()
+ minio_path = f'/615873064429507639/TEMPLATE/{now.year}/{now.month:02d}/{file_name}'
+
+ structure['files'].append({
+ 'name': file_name,
+ 'path': str(relative_path),
+ 'level': level,
+ 'parent_path': parent_path,
+ 'file_path': str(item),
+ 'minio_path': minio_path
+ })
+
+ # 按层级排序
+ structure['directories'].sort(key=lambda x: (x['level'], x['path']))
+ structure['files'].sort(key=lambda x: (x['level'], x['path']))
+
+ print(f"找到 {len(structure['directories'])} 个目录节点")
+ print(f"找到 {len(structure['files'])} 个文件节点")
+
+ return structure
+
+
+def get_existing_templates(conn) -> Dict:
+ """
+ 获取数据库中现有的模板记录
+
+ Returns:
+ 字典,key为file_path,value为模板信息
+ """
+ cursor = conn.cursor(pymysql.cursors.DictCursor)
+
+ sql = """
+ SELECT id, name, parent_id, file_path, state
+ FROM f_polic_file_config
+ WHERE tenant_id = %s
+ """
+ cursor.execute(sql, (TENANT_ID,))
+ templates = cursor.fetchall()
+
+ # 使用file_path作为key(如果存在)
+ result_by_path = {}
+ # 使用name作为key(用于目录节点)
+ result_by_name = {}
+
+ for template in templates:
+ if template['file_path']:
+ result_by_path[template['file_path']] = {
+ 'id': template['id'],
+ 'name': template['name'],
+ 'parent_id': template['parent_id'],
+ 'file_path': template['file_path'],
+ 'state': template['state']
+ }
+ else:
+ # 目录节点
+ name = template['name']
+ if name not in result_by_name:
+ result_by_name[name] = []
+ result_by_name[name].append({
+ 'id': template['id'],
+ 'name': template['name'],
+ 'parent_id': template['parent_id'],
+ 'file_path': None,
+ 'state': template['state']
+ })
+
+ cursor.close()
+ return {
+ 'by_path': result_by_path,
+ 'by_name': result_by_name
+ }
+
+
+def create_or_update_directory(conn, dir_name: str, parent_id: Optional[int], existing_templates: Dict) -> int:
+ """
+ 创建或更新目录节点
+
+ Returns:
+ 目录节点的ID
+ """
+ cursor = conn.cursor()
+
+ try:
+ # 查找是否已存在(通过名称精确匹配,且file_path为None)
+ candidates = existing_templates['by_name'].get(dir_name, [])
+ existing = None
+ for candidate in candidates:
+ if candidate.get('file_path') is None: # 目录节点
+ # 如果parent_id匹配,优先选择
+ if candidate['parent_id'] == parent_id:
+ existing = candidate
+ break
+ elif existing is None:
+ existing = candidate
+
+ if existing:
+ # 更新现有目录记录
+ template_id = existing['id']
+ if existing['parent_id'] != parent_id:
+ update_sql = """
+ UPDATE f_polic_file_config
+ SET parent_id = %s, updated_time = NOW(), updated_by = %s, state = 1
+ WHERE id = %s AND tenant_id = %s
+ """
+ cursor.execute(update_sql, (parent_id, UPDATED_BY, template_id, TENANT_ID))
+ conn.commit()
+ print(f" [UPDATE] 更新目录: {dir_name} (ID: {template_id}, parent_id: {parent_id})")
+ else:
+ print(f" [KEEP] 保持目录: {dir_name} (ID: {template_id})")
+ return template_id
+ else:
+ # 创建新目录记录
+ template_id = generate_id()
+ insert_sql = """
+ 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, %s)
+ """
+ cursor.execute(insert_sql, (
+ template_id,
+ TENANT_ID,
+ parent_id,
+ dir_name,
+ None, # input_data
+ None, # file_path(目录节点没有文件路径)
+ CREATED_BY,
+ CREATED_BY,
+ 1 # state: 1表示启用
+ ))
+ conn.commit()
+ print(f" [CREATE] 创建目录: {dir_name} (ID: {template_id}, parent_id: {parent_id})")
+ # 更新existing_templates
+ if dir_name not in existing_templates['by_name']:
+ existing_templates['by_name'][dir_name] = []
+ existing_templates['by_name'][dir_name].append({
+ 'id': template_id,
+ 'name': dir_name,
+ 'parent_id': parent_id,
+ 'file_path': None,
+ 'state': 1
+ })
+ return template_id
+
+ except Exception as e:
+ conn.rollback()
+ raise Exception(f"创建或更新目录失败: {str(e)}")
+ finally:
+ cursor.close()
+
+
+def update_file_parent(conn, file_info: Dict, parent_id: Optional[int], existing_templates: Dict) -> Optional[int]:
+ """
+ 更新文件节点的parent_id
+
+ Args:
+ file_info: 文件信息,包含name、minio_path等
+ parent_id: 父节点ID
+
+ Returns:
+ 文件节点的ID,如果未找到则返回None
+ """
+ cursor = conn.cursor()
+
+ try:
+ file_name = file_info['name']
+ minio_path = file_info.get('minio_path')
+
+ # 优先通过file_path(minio_path)匹配(最准确)
+ existing = None
+ if minio_path and minio_path in existing_templates['by_path']:
+ existing = existing_templates['by_path'][minio_path]
+
+ if not existing:
+ print(f" [WARN] 未找到文件: {file_name} (MinIO路径: {minio_path})")
+ return None
+
+ template_id = existing['id']
+
+ if existing['parent_id'] != parent_id:
+ update_sql = """
+ UPDATE f_polic_file_config
+ SET parent_id = %s, updated_time = NOW(), updated_by = %s
+ WHERE id = %s AND tenant_id = %s
+ """
+ cursor.execute(update_sql, (parent_id, UPDATED_BY, template_id, TENANT_ID))
+ conn.commit()
+ print(f" [UPDATE] 更新文件: {file_name} (ID: {template_id}, parent_id: {parent_id})")
+ else:
+ print(f" [KEEP] 保持文件: {file_name} (ID: {template_id})")
+ return template_id
+
+ except Exception as e:
+ conn.rollback()
+ raise Exception(f"更新文件parent_id失败: {str(e)}")
+ finally:
+ cursor.close()
+
+
+def main():
+ """主函数"""
+ print("=" * 80)
+ print("更新模板层级结构")
+ print("=" * 80)
+ print()
+
+ try:
+ # 连接数据库
+ print("1. 连接数据库...")
+ conn = pymysql.connect(**DB_CONFIG)
+ print("[OK] 数据库连接成功\n")
+
+ # 扫描目录结构
+ print("2. 扫描目录结构...")
+ structure = scan_directory_structure(TEMPLATE_BASE_DIR)
+ if not structure:
+ print("错误: 未找到任何目录或文件")
+ return
+
+ # 获取现有模板
+ print("\n3. 获取现有模板...")
+ existing_templates = get_existing_templates(conn)
+ print(f"[OK] 找到 {len(existing_templates['by_path'])} 个文件模板")
+ print(f"[OK] 找到 {sum(len(v) for v in existing_templates['by_name'].values())} 个目录模板\n")
+
+ # 构建路径到ID的映射(处理目录节点)
+ print("4. 创建/更新目录节点...")
+ print("=" * 80)
+ path_to_id = {}
+
+ # 按层级顺序处理目录
+ for dir_info in structure['directories']:
+ parent_id = None
+ if dir_info['parent_path']:
+ parent_id = path_to_id.get(dir_info['parent_path'])
+
+ dir_id = create_or_update_directory(conn, dir_info['name'], parent_id, existing_templates)
+ path_to_id[dir_info['path']] = dir_id
+
+ print(f"\n[OK] 处理了 {len(path_to_id)} 个目录节点\n")
+
+ # 更新文件节点的parent_id
+ print("5. 更新文件节点的parent_id...")
+ print("=" * 80)
+ for file_info in structure['files']:
+ parent_id = None
+ if file_info['parent_path']:
+ parent_id = path_to_id.get(file_info['parent_path'])
+
+ update_file_parent(conn, file_info, parent_id, existing_templates)
+
+ print(f"\n[OK] 处理了 {len(structure['files'])} 个文件节点\n")
+
+ # 打印层级结构
+ print("6. 最终层级结构:")
+ print("=" * 80)
+ print_hierarchy(conn)
+
+ print("\n" + "=" * 80)
+ print("更新完成!")
+ print("=" * 80)
+
+ except Exception as e:
+ print(f"\n[ERROR] 发生错误: {e}")
+ import traceback
+ traceback.print_exc()
+ if 'conn' in locals():
+ conn.rollback()
+ finally:
+ if 'conn' in locals():
+ conn.close()
+ print("\n数据库连接已关闭")
+
+
+def print_hierarchy(conn, parent_id=None, level=0, prefix=""):
+ """打印层级结构"""
+ cursor = conn.cursor(pymysql.cursors.DictCursor)
+
+ try:
+ if parent_id is None:
+ sql = """
+ SELECT id, name, parent_id, file_path
+ FROM f_polic_file_config
+ WHERE tenant_id = %s AND parent_id IS NULL
+ ORDER BY name
+ """
+ cursor.execute(sql, (TENANT_ID,))
+ else:
+ sql = """
+ SELECT id, name, parent_id, file_path
+ FROM f_polic_file_config
+ WHERE tenant_id = %s AND parent_id = %s
+ ORDER BY name
+ """
+ cursor.execute(sql, (TENANT_ID, parent_id))
+
+ items = cursor.fetchall()
+
+ for i, item in enumerate(items):
+ is_last = i == len(items) - 1
+ current_prefix = prefix + ("└── " if is_last else "├── ")
+ next_prefix = prefix + (" " if is_last else "│ ")
+
+ node_type = "📁" if item['file_path'] is None else "📄"
+ print(f"{current_prefix}{node_type} {item['name']} (ID: {item['id']})")
+
+ # 递归打印子节点
+ print_hierarchy(conn, item['id'], level + 1, next_prefix)
+
+ finally:
+ cursor.close()
+
+
+if __name__ == '__main__':
+ main()
+
diff --git a/update_template_hierarchy_v2.py b/update_template_hierarchy_v2.py
new file mode 100644
index 0000000..f312d39
--- /dev/null
+++ b/update_template_hierarchy_v2.py
@@ -0,0 +1,445 @@
+"""
+根据 template_finish/ 目录结构更新 f_polic_file_config 表中的层级结构
+使用路径作为唯一标识,确保正确建立层级关系
+"""
+import os
+import sys
+import json
+import pymysql
+from pathlib import Path
+from typing import Dict, List, Optional, Tuple
+from collections import defaultdict
+
+# 设置输出编码为UTF-8(Windows兼容)
+if sys.platform == 'win32':
+ import io
+ sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace')
+ sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8', errors='replace')
+
+# 数据库连接配置
+DB_CONFIG = {
+ 'host': '152.136.177.240',
+ 'port': 5012,
+ 'user': 'finyx',
+ 'password': '6QsGK6MpePZDE57Z',
+ 'database': 'finyx',
+ 'charset': 'utf8mb4'
+}
+
+TENANT_ID = 615873064429507639
+CREATED_BY = 655162080928945152
+UPDATED_BY = 655162080928945152
+TEMPLATE_BASE_DIR = 'template_finish'
+
+
+def generate_id():
+ """生成ID"""
+ import time
+ import random
+ timestamp = int(time.time() * 1000)
+ random_part = random.randint(100000, 999999)
+ return timestamp * 1000 + random_part
+
+
+def scan_directory_structure(base_dir: str) -> Dict:
+ """
+ 扫描目录结构,构建层级关系
+
+ Returns:
+ 字典,包含目录和文件的层级信息
+ """
+ base_path = Path(base_dir)
+ if not base_path.exists():
+ print(f"错误: 目录不存在 - {base_dir}")
+ return {}
+
+ structure = {
+ 'directories': [], # 目录节点列表
+ 'files': [] # 文件节点列表
+ }
+
+ print("=" * 80)
+ print("扫描目录结构...")
+ print("=" * 80)
+
+ # 遍历所有目录和文件
+ for item in base_path.rglob("*"):
+ relative_path = item.relative_to(base_path)
+ parts = relative_path.parts
+
+ if item.is_dir():
+ # 目录节点
+ level = len(parts) - 1 # 层级(从0开始)
+ dir_name = parts[-1]
+ parent_path = str(Path(*parts[:-1])) if len(parts) > 1 else None
+
+ structure['directories'].append({
+ 'name': dir_name,
+ 'path': str(relative_path),
+ 'level': level,
+ 'parent_path': parent_path
+ })
+
+ elif item.is_file() and item.suffix == '.docx' and not item.name.startswith("~$"):
+ # 文件节点
+ level = len(parts) - 1
+ file_name = item.name
+ parent_path = str(Path(*parts[:-1])) if len(parts) > 1 else None
+
+ structure['files'].append({
+ 'name': file_name,
+ 'path': str(relative_path),
+ 'level': level,
+ 'parent_path': parent_path,
+ 'file_path': str(item)
+ })
+
+ # 按层级排序
+ structure['directories'].sort(key=lambda x: (x['level'], x['path']))
+ structure['files'].sort(key=lambda x: (x['level'], x['path']))
+
+ print(f"找到 {len(structure['directories'])} 个目录节点")
+ print(f"找到 {len(structure['files'])} 个文件节点")
+
+ return structure
+
+
+def get_existing_templates(conn) -> Dict:
+ """
+ 获取数据库中现有的模板记录
+
+ Returns:
+ 字典,key为file_path(如果存在)或名称,value为模板信息列表
+ """
+ cursor = conn.cursor(pymysql.cursors.DictCursor)
+
+ sql = """
+ SELECT id, name, parent_id, file_path, state
+ FROM f_polic_file_config
+ WHERE tenant_id = %s
+ """
+ cursor.execute(sql, (TENANT_ID,))
+ templates = cursor.fetchall()
+
+ result = {}
+ for template in templates:
+ # 优先使用file_path作为key(更准确)
+ if template['file_path']:
+ key = template['file_path']
+ if key not in result:
+ result[key] = []
+ result[key].append({
+ 'id': template['id'],
+ 'name': template['name'],
+ 'parent_id': template['parent_id'],
+ 'file_path': template['file_path'],
+ 'state': template['state']
+ })
+ # 同时使用名称作为key(用于目录节点和没有file_path的记录)
+ name_key = template['name']
+ if name_key not in result:
+ result[name_key] = []
+ result[name_key].append({
+ 'id': template['id'],
+ 'name': template['name'],
+ 'parent_id': template['parent_id'],
+ 'file_path': template['file_path'],
+ 'state': template['state']
+ })
+
+ cursor.close()
+ return result
+
+
+def create_or_update_directory(conn, dir_name: str, parent_id: Optional[int], existing_templates: Dict) -> int:
+ """
+ 创建或更新目录节点
+
+ Returns:
+ 目录节点的ID
+ """
+ cursor = conn.cursor()
+
+ try:
+ # 查找是否已存在(通过名称精确匹配,且file_path为None)
+ candidates = existing_templates.get(dir_name, [])
+ existing = None
+ for candidate in candidates:
+ if candidate.get('file_path') is None: # 目录节点
+ existing = candidate
+ break
+
+ if existing:
+ # 更新现有目录记录
+ template_id = existing['id']
+ if existing['parent_id'] != parent_id:
+ update_sql = """
+ UPDATE f_polic_file_config
+ SET parent_id = %s, updated_time = NOW(), updated_by = %s, state = 1
+ WHERE id = %s AND tenant_id = %s
+ """
+ cursor.execute(update_sql, (parent_id, UPDATED_BY, template_id, TENANT_ID))
+ conn.commit()
+ print(f" [UPDATE] 更新目录: {dir_name} (ID: {template_id}, parent_id: {parent_id})")
+ else:
+ print(f" [KEEP] 保持目录: {dir_name} (ID: {template_id})")
+ return template_id
+ else:
+ # 创建新目录记录
+ template_id = generate_id()
+ insert_sql = """
+ 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, %s)
+ """
+ cursor.execute(insert_sql, (
+ template_id,
+ TENANT_ID,
+ parent_id,
+ dir_name,
+ None, # input_data
+ None, # file_path(目录节点没有文件路径)
+ CREATED_BY,
+ CREATED_BY,
+ 1 # state: 1表示启用
+ ))
+ conn.commit()
+ print(f" [CREATE] 创建目录: {dir_name} (ID: {template_id}, parent_id: {parent_id})")
+ # 更新existing_templates
+ if dir_name not in existing_templates:
+ existing_templates[dir_name] = []
+ existing_templates[dir_name].append({
+ 'id': template_id,
+ 'name': dir_name,
+ 'parent_id': parent_id,
+ 'file_path': None,
+ 'state': 1
+ })
+ return template_id
+
+ except Exception as e:
+ conn.rollback()
+ raise Exception(f"创建或更新目录失败: {str(e)}")
+ finally:
+ cursor.close()
+
+
+def normalize_name(name: str) -> str:
+ """
+ 标准化名称(去掉扩展名、括号内容、数字前缀等)
+ """
+ import re
+ # 去掉扩展名
+ name = Path(name).stem if '.' in name else name
+ # 去掉括号内容
+ name = re.sub(r'[((].*?[))]', '', name)
+ name = name.strip()
+ # 去掉数字前缀和点号
+ name = re.sub(r'^\d+[\.\-]?\s*', '', name)
+ name = name.strip()
+ return name
+
+
+def update_file_parent(conn, file_info: Dict, parent_id: Optional[int], existing_templates: Dict) -> Optional[int]:
+ """
+ 更新文件节点的parent_id
+
+ Args:
+ file_info: 文件信息,包含name和file_path
+ parent_id: 父节点ID
+
+ Returns:
+ 文件节点的ID,如果未找到则返回None
+ """
+ cursor = conn.cursor()
+
+ try:
+ file_name = file_info['name']
+ # 根据文件路径构建预期的MinIO路径
+ # 文件路径格式: template_finish/2-初核模版/1.初核请示/1.请示报告卡(XXX).docx
+ # MinIO路径格式: /615873064429507639/TEMPLATE/2025/12/1.请示报告卡(XXX).docx
+ expected_path = None
+ if 'file_path' in file_info:
+ # 从相对路径构建MinIO路径
+ from datetime import datetime
+ now = datetime.now()
+ relative_path = file_info.get('path', '')
+ file_name_only = Path(file_name).name
+ expected_path = f'/615873064429507639/TEMPLATE/{now.year}/{now.month:02d}/{file_name_only}'
+
+ # 优先通过file_path匹配(最准确)
+ existing = None
+ if expected_path and expected_path in existing_templates:
+ candidates = existing_templates[expected_path]
+ if candidates:
+ existing = candidates[0]
+
+ # 如果没找到,通过标准化名称匹配
+ if not existing:
+ normalized_file_name = normalize_name(file_name)
+ candidates = []
+ for key, templates in existing_templates.items():
+ for template in templates:
+ if template.get('file_path'): # 只匹配有file_path的(文件节点)
+ normalized_template_name = normalize_name(template['name'])
+ if normalized_template_name == normalized_file_name:
+ # 检查file_path是否匹配
+ if expected_path and template['file_path'] == expected_path:
+ existing = template
+ break
+ candidates.append(template)
+ if existing:
+ break
+
+ if not existing and candidates:
+ # 如果有多个候选,选择parent_id匹配的
+ for candidate in candidates:
+ if candidate['parent_id'] == parent_id:
+ existing = candidate
+ break
+ # 如果还是没找到,选择第一个
+ if not existing:
+ existing = candidates[0]
+
+ if not existing:
+ print(f" [WARN] 未找到文件: {file_name} (标准化: {normalize_name(file_name)})")
+ return None
+
+ template_id = existing['id']
+
+ if existing['parent_id'] != parent_id:
+ update_sql = """
+ UPDATE f_polic_file_config
+ SET parent_id = %s, updated_time = NOW(), updated_by = %s
+ WHERE id = %s AND tenant_id = %s
+ """
+ cursor.execute(update_sql, (parent_id, UPDATED_BY, template_id, TENANT_ID))
+ conn.commit()
+ print(f" [UPDATE] 更新文件: {file_name} -> {existing['name']} (ID: {template_id}, parent_id: {parent_id})")
+ else:
+ print(f" [KEEP] 保持文件: {file_name} -> {existing['name']} (ID: {template_id})")
+ return template_id
+
+ except Exception as e:
+ conn.rollback()
+ raise Exception(f"更新文件parent_id失败: {str(e)}")
+ finally:
+ cursor.close()
+
+
+def main():
+ """主函数"""
+ print("=" * 80)
+ print("更新模板层级结构")
+ print("=" * 80)
+ print()
+
+ try:
+ # 连接数据库
+ print("1. 连接数据库...")
+ conn = pymysql.connect(**DB_CONFIG)
+ print("[OK] 数据库连接成功\n")
+
+ # 扫描目录结构
+ print("2. 扫描目录结构...")
+ structure = scan_directory_structure(TEMPLATE_BASE_DIR)
+ if not structure:
+ print("错误: 未找到任何目录或文件")
+ return
+
+ # 获取现有模板
+ print("\n3. 获取现有模板...")
+ existing_templates = get_existing_templates(conn)
+ print(f"[OK] 找到 {len(existing_templates)} 个现有模板\n")
+
+ # 构建路径到ID的映射(处理目录节点)
+ print("4. 创建/更新目录节点...")
+ print("=" * 80)
+ path_to_id = {}
+
+ # 按层级顺序处理目录
+ for dir_info in structure['directories']:
+ parent_id = None
+ if dir_info['parent_path']:
+ parent_id = path_to_id.get(dir_info['parent_path'])
+
+ dir_id = create_or_update_directory(conn, dir_info['name'], parent_id, existing_templates)
+ path_to_id[dir_info['path']] = dir_id
+
+ print(f"\n[OK] 处理了 {len(path_to_id)} 个目录节点\n")
+
+ # 更新文件节点的parent_id
+ print("5. 更新文件节点的parent_id...")
+ print("=" * 80)
+ for file_info in structure['files']:
+ parent_id = None
+ if file_info['parent_path']:
+ parent_id = path_to_id.get(file_info['parent_path'])
+
+ update_file_parent(conn, file_info, parent_id, existing_templates)
+
+ print(f"\n[OK] 处理了 {len(structure['files'])} 个文件节点\n")
+
+ # 打印层级结构
+ print("6. 最终层级结构:")
+ print("=" * 80)
+ print_hierarchy(conn)
+
+ print("\n" + "=" * 80)
+ print("更新完成!")
+ print("=" * 80)
+
+ except Exception as e:
+ print(f"\n[ERROR] 发生错误: {e}")
+ import traceback
+ traceback.print_exc()
+ if 'conn' in locals():
+ conn.rollback()
+ finally:
+ if 'conn' in locals():
+ conn.close()
+ print("\n数据库连接已关闭")
+
+
+def print_hierarchy(conn, parent_id=None, level=0, prefix=""):
+ """打印层级结构"""
+ cursor = conn.cursor(pymysql.cursors.DictCursor)
+
+ try:
+ if parent_id is None:
+ sql = """
+ SELECT id, name, parent_id, file_path
+ FROM f_polic_file_config
+ WHERE tenant_id = %s AND parent_id IS NULL
+ ORDER BY name
+ """
+ cursor.execute(sql, (TENANT_ID,))
+ else:
+ sql = """
+ SELECT id, name, parent_id, file_path
+ FROM f_polic_file_config
+ WHERE tenant_id = %s AND parent_id = %s
+ ORDER BY name
+ """
+ cursor.execute(sql, (TENANT_ID, parent_id))
+
+ items = cursor.fetchall()
+
+ for i, item in enumerate(items):
+ is_last = i == len(items) - 1
+ current_prefix = prefix + ("└── " if is_last else "├── ")
+ next_prefix = prefix + (" " if is_last else "│ ")
+
+ node_type = "📁" if item['file_path'] is None else "📄"
+ print(f"{current_prefix}{node_type} {item['name']} (ID: {item['id']})")
+
+ # 递归打印子节点
+ print_hierarchy(conn, item['id'], level + 1, next_prefix)
+
+ finally:
+ cursor.close()
+
+
+if __name__ == '__main__':
+ main()
+
diff --git a/verify_hierarchy_structure.py b/verify_hierarchy_structure.py
new file mode 100644
index 0000000..e5c9527
--- /dev/null
+++ b/verify_hierarchy_structure.py
@@ -0,0 +1,56 @@
+"""
+验证层级结构是否正确
+"""
+import pymysql
+
+DB_CONFIG = {
+ 'host': '152.136.177.240',
+ 'port': 5012,
+ 'user': 'finyx',
+ 'password': '6QsGK6MpePZDE57Z',
+ 'database': 'finyx',
+ 'charset': 'utf8mb4'
+}
+
+TENANT_ID = 615873064429507639
+
+conn = pymysql.connect(**DB_CONFIG)
+cursor = conn.cursor(pymysql.cursors.DictCursor)
+
+# 检查"1请示报告卡"的记录
+cursor.execute("""
+ SELECT id, name, file_path, parent_id
+ FROM f_polic_file_config
+ WHERE tenant_id = %s AND name LIKE %s
+ ORDER BY id
+""", (TENANT_ID, '%请示报告卡%'))
+
+results = cursor.fetchall()
+print(f"找到 {len(results)} 条'请示报告卡'相关记录:\n")
+for r in results:
+ print(f"ID: {r['id']}")
+ print(f"名称: {r['name']}")
+ print(f"文件路径: {r['file_path']}")
+ print(f"父节点ID: {r['parent_id']}")
+ print()
+
+# 检查"XXX初核情况报告"的记录
+cursor.execute("""
+ SELECT id, name, file_path, parent_id
+ FROM f_polic_file_config
+ WHERE tenant_id = %s AND name LIKE %s
+ ORDER BY id
+""", (TENANT_ID, '%初核情况报告%'))
+
+results = cursor.fetchall()
+print(f"\n找到 {len(results)} 条'初核情况报告'相关记录:\n")
+for r in results:
+ print(f"ID: {r['id']}")
+ print(f"名称: {r['name']}")
+ print(f"文件路径: {r['file_path']}")
+ print(f"父节点ID: {r['parent_id']}")
+ print()
+
+cursor.close()
+conn.close()
+
diff --git a/修复模板名称总结.md b/修复模板名称总结.md
new file mode 100644
index 0000000..0ca2c3f
--- /dev/null
+++ b/修复模板名称总结.md
@@ -0,0 +1,113 @@
+# 修复模板名称总结
+
+## 任务完成情况
+
+✅ **已完成所有任务**
+
+根据 template_finish/ 目录下的模板文件,检查并修复了数据库中 f_polic_file_config 表的 name 字段,确保与模板文档名称(去掉扩展名)完全一致。
+
+## 修复结果
+
+### 统计信息
+- **总文件数**: 21
+- **匹配成功**: 21
+- **更新数量**: 16
+- **未找到**: 0
+
+### 更新的模板名称
+
+1. **1.请示报告卡(XXX).docx**
+ - 旧名称: `1请示报告卡`
+ - 新名称: `1.请示报告卡(XXX)`
+
+2. **2.初步核实审批表(XXX).docx**
+ - 旧名称: `初步核实审批表`
+ - 新名称: `2.初步核实审批表(XXX)`
+
+3. **3.附件初核方案(XXX).docx**
+ - 旧名称: `附件初核方案`
+ - 新名称: `3.附件初核方案(XXX)`
+
+4. **1.请示报告卡(初核谈话).docx**
+ - 旧名称: `1请示报告卡`
+ - 新名称: `1.请示报告卡(初核谈话)`
+
+5. **2谈话审批表.docx**
+ - 旧名称: `谈话审批表`
+ - 新名称: `2谈话审批表`
+
+6. **3.谈话前安全风险评估表.docx**
+ - 旧名称: `谈话前安全风险评估表`
+ - 新名称: `3.谈话前安全风险评估表`
+
+7. **4.谈话方案.docx**
+ - 旧名称: `谈话方案`
+ - 新名称: `4.谈话方案`
+
+8. **5.谈话后安全风险评估表.docx**
+ - 旧名称: `谈话后安全风险评估表`
+ - 新名称: `5.谈话后安全风险评估表`
+
+9. **1.谈话笔录.docx**
+ - 旧名称: `谈话笔录`
+ - 新名称: `1.谈话笔录`
+
+10. **2.谈话询问对象情况摸底调查30问.docx**
+ - 旧名称: `谈话询问对象情况摸底调查30问`
+ - 新名称: `2.谈话询问对象情况摸底调查30问`
+
+11. **3.被谈话人权利义务告知书.docx**
+ - 旧名称: `被谈话人权利义务告知书`
+ - 新名称: `3.被谈话人权利义务告知书`
+
+12. **4.点对点交接单.docx**
+ - 旧名称: `点对点交接单`
+ - 新名称: `4.点对点交接单`
+
+13. **5.陪送交接单(新).docx**
+ - 旧名称: `陪送交接单`
+ - 新名称: `5.陪送交接单(新)`
+
+14. **6.2保密承诺书(谈话对象使用-中共党员用).docx**
+ - 旧名称: `2保密承诺书`
+ - 新名称: `6.2保密承诺书(谈话对象使用-中共党员用)`
+
+15. **7.办案人员-办案安全保密承诺书.docx**
+ - 旧名称: `办案人员-办案安全保密承诺书`
+ - 新名称: `7.办案人员-办案安全保密承诺书`
+
+16. **8.XXX初核情况报告.docx**
+ - 旧名称: `XXX初核情况报告`
+ - 新名称: `8.XXX初核情况报告`
+
+### 名称已正确的模板(无需更新)
+
+1. 谈话通知书第一联.docx
+2. 谈话通知书第三联.docx
+3. 谈话通知书第二联.docx
+4. 6.1保密承诺书(谈话对象使用-非中共党员用).docx
+5. 8-1请示报告卡(初核报告结论) .docx
+
+## 修复规则
+
+- **文件名**: `1.请示报告卡(XXX).docx`
+- **数据库name字段**: `1.请示报告卡(XXX)`(去掉扩展名,保留所有其他内容)
+
+确保:
+- 保留数字前缀(如 "1."、"2.")
+- 保留括号内容(如 "(XXX)"、"(初核谈话)")
+- 保留所有标点符号和特殊字符
+- 只去掉文件扩展名(.docx)
+
+## 脚本文件
+
+**fix_template_names.py** - 修复模板名称的脚本
+- 扫描 template_finish 目录下的所有 .docx 文件
+- 获取每个文件的完整名称(去掉扩展名)
+- 检查数据库中对应的记录
+- 如果 name 字段不匹配,更新为正确的名称
+
+## 完成时间
+
+2025年12月11日
+
diff --git a/处理保密承诺书模板总结.md b/处理保密承诺书模板总结.md
new file mode 100644
index 0000000..37fe0e8
--- /dev/null
+++ b/处理保密承诺书模板总结.md
@@ -0,0 +1,75 @@
+# 处理保密承诺书(非中共党员用)模板总结
+
+## 任务完成情况
+
+✅ **已完成所有任务**
+
+1. ✅ 解析文档中的占位符
+2. ✅ 上传文档到MinIO
+3. ✅ 更新数据库记录
+
+## 处理结果
+
+### 文件信息
+- **文件路径**: `template_finish/2-初核模版/2.谈话审批/走读式谈话流程/6.1保密承诺书(谈话对象使用-非中共党员用).docx`
+- **模板名称**: `6.1保密承诺书(谈话对象使用-非中共党员用)`
+- **父节点ID**: `1765273962716807` (走读式谈话流程)
+
+### 占位符提取结果
+找到 **5个占位符**,全部成功匹配到数据库字段:
+
+1. `{{ target_contact }}` - 被核查人联系方式
+2. `{{ target_id_number }}` - 被核查人身份证号
+3. `{{ target_name }}` - 被核查人姓名
+4. `{{ target_organization_and_position }}` - 被核查人单位及职务
+5. `{{ target_political_status }}` - 被核查人政治面貌
+
+### MinIO上传结果
+- **MinIO路径**: `/615873064429507639/TEMPLATE/2025/12/6.1保密承诺书(谈话对象使用-非中共党员用).docx`
+- **上传状态**: ✅ 成功
+
+### 数据库更新结果
+- **模板ID**: `1765434655750341`
+- **创建状态**: ✅ 新创建
+- **字段关联**: ✅ 5个字段全部关联成功
+
+## 数据库记录详情
+
+### f_polic_file_config 表
+- **id**: 1765434655750341
+- **tenant_id**: 615873064429507639
+- **parent_id**: 1765273962716807 (走读式谈话流程)
+- **name**: 6.1保密承诺书(谈话对象使用-非中共党员用)
+- **file_path**: /615873064429507639/TEMPLATE/2025/12/6.1保密承诺书(谈话对象使用-非中共党员用).docx
+- **state**: 1 (启用)
+
+### f_polic_file_field 表
+已创建5条关联记录,关联的字段ID:
+- target_contact
+- target_id_number
+- target_name
+- target_organization_and_position
+- target_political_status
+
+## 层级结构
+
+该模板现在位于:
+```
+📁 2-初核模版
+└── 📁 2.谈话审批
+ └── 📁 走读式谈话流程
+ └── 📄 6.1保密承诺书(谈话对象使用-非中共党员用) (ID: 1765434655750341)
+```
+
+## 脚本文件
+
+**process_confidentiality_commitment_non_party.py** - 处理该模板的专用脚本
+- 提取占位符
+- 上传到MinIO
+- 创建/更新数据库记录
+- 建立字段关联关系
+
+## 完成时间
+
+2025年12月11日
+
diff --git a/更新模板层级结构总结.md b/更新模板层级结构总结.md
new file mode 100644
index 0000000..0545070
--- /dev/null
+++ b/更新模板层级结构总结.md
@@ -0,0 +1,124 @@
+# 更新模板层级结构总结
+
+## 任务完成情况
+
+✅ **已完成主要任务**
+
+1. ✅ 根据 template_finish/ 目录结构更新 f_polic_file_config 表的层级结构
+2. ✅ 创建目录节点(一级、二级、三级)
+3. ✅ 更新文件的 parent_id,建立正确的层级关系
+
+## 目录结构
+
+根据 `template_finish/2-初核模版/` 目录,层级结构如下:
+
+### 一级节点
+- **2-初核模版** (ID: 1765273961277310)
+
+### 二级节点
+- **1.初核请示** (ID: 1765431558933731) - parent: 2-初核模版
+- **2.谈话审批** (ID: 1765431558825578) - parent: 2-初核模版
+- **3.初核结论** (ID: 1765431559135346) - parent: 2-初核模版
+
+### 三级节点
+- **谈话通知书** (ID: 1765273962774249) - parent: 2.谈话审批
+- **走读式谈话审批** (ID: 1765273962700431) - parent: 2.谈话审批
+- **走读式谈话流程** (ID: 1765273962716807) - parent: 2.谈话审批
+
+## 文件节点分布
+
+### 1.初核请示 下的文件
+- 初步核实审批表 (ID: 1765425918287774)
+- 附件初核方案 (ID: 1765425918483987)
+- 1请示报告卡 (ID: 1765425930118581) - 注意:此文件在多个位置出现
+
+### 2.谈话审批/谈话通知书 下的文件
+- 谈话通知书第一联 (ID: 1765273963625524)
+- 谈话通知书第二联 (ID: 1765273963825806)
+- 谈话通知书第三联 (ID: 1765273963038891)
+
+### 2.谈话审批/走读式谈话审批 下的文件
+- 1请示报告卡 (ID: 1765425930118581) - 注意:与"1.初核请示"下的文件重复
+- 谈话审批表 (ID: 1765425918217504)
+- 谈话前安全风险评估表 (ID: 1765425918902422)
+- 谈话方案 (ID: 1765425918745496)
+- 谈话后安全风险评估表 (ID: 1765425919242549)
+
+### 2.谈话审批/走读式谈话流程 下的文件
+- 谈话笔录 (ID: 1765425918749247)
+- 谈话询问对象情况摸底调查30问 (ID: 1765425918921697)
+- 被谈话人权利义务告知书 (ID: 1765425930059797)
+- 点对点交接单 (ID: 1765425919512780)
+- 陪送交接单 (ID: 1765425919389484)
+- 办案人员-办案安全保密承诺书 (ID: 1765425919629084)
+- 8.XXX初核情况报告 (ID: 1765273963158289) - 注意:此文件位置可能不正确
+
+### 3.初核结论 下的文件
+- 8-1请示报告卡(初核报告结论) (ID: 1765273962631542)
+- XXX初核情况报告 (ID: 1765425930460962)
+
+## 已知问题
+
+1. **缺失文件**: 以下文件在数据库中未找到:
+ - 6.1保密承诺书(谈话对象使用-非中共党员用).docx
+
+ 这个文件可能在数据库中不存在,或者名称不同。数据库中只有"2保密承诺书"(ID: 1765425919729046),可能是"6.2保密承诺书(谈话对象使用-中共党员用)"的简化名称。
+
+2. **重复文件已解决**: "1请示报告卡"现在正确地在两个位置:
+ - 1.初核请示 下 (ID: 1765425930118581) - 对应文件"1.请示报告卡(XXX).docx"
+ - 走读式谈话审批 下 (ID: 1765432134276990) - 对应文件"1.请示报告卡(初核谈话).docx"
+
+ 这两个是不同的文件,现在已正确区分。
+
+## 执行结果
+
+- **创建的目录节点**: 3个(1.初核请示、2.谈话审批、3.初核结论)
+- **更新的目录节点**: 4个(更新了parent_id)
+- **更新的文件节点**: 20个(更新了parent_id)
+- **未找到的文件**: 1个(6.1保密承诺书(谈话对象使用-非中共党员用).docx)
+
+## 最终层级结构
+
+```
+📁 2-初核模版
+├── 📁 1.初核请示
+│ ├── 📄 1请示报告卡 (ID: 1765425930118581)
+│ ├── 📄 初步核实审批表 (ID: 1765425918287774)
+│ └── 📄 附件初核方案 (ID: 1765425918483987)
+├── 📁 2.谈话审批
+│ ├── 📁 谈话通知书
+│ │ ├── 📄 谈话通知书第一联 (ID: 1765273963625524)
+│ │ ├── 📄 谈话通知书第二联 (ID: 1765273963825806)
+│ │ └── 📄 谈话通知书第三联 (ID: 1765273963038891)
+│ ├── 📁 走读式谈话审批
+│ │ ├── 📄 1请示报告卡 (ID: 1765432134276990)
+│ │ ├── 📄 谈话审批表 (ID: 1765425918217504)
+│ │ ├── 📄 谈话前安全风险评估表 (ID: 1765425918902422)
+│ │ ├── 📄 谈话方案 (ID: 1765425918745496)
+│ │ └── 📄 谈话后安全风险评估表 (ID: 1765425919242549)
+│ └── 📁 走读式谈话流程
+│ ├── 📄 2保密承诺书 (ID: 1765425919729046)
+│ ├── 📄 办案人员-办案安全保密承诺书 (ID: 1765425919629084)
+│ ├── 📄 点对点交接单 (ID: 1765425919512780)
+│ ├── 📄 被谈话人权利义务告知书 (ID: 1765425930059797)
+│ ├── 📄 谈话笔录 (ID: 1765425918749247)
+│ ├── 📄 谈话询问对象情况摸底调查30问 (ID: 1765425918921697)
+│ └── 📄 陪送交接单 (ID: 1765425919389484)
+└── 📁 3.初核结论
+ ├── 📄 8-1请示报告卡(初核报告结论) (ID: 1765273962631542)
+ └── 📄 XXX初核情况报告 (ID: 1765425930460962)
+```
+
+## 脚本文件
+
+**update_template_hierarchy_final.py** - 更新层级结构的最终版本脚本
+- 扫描 template_finish 目录结构
+- 使用 file_path(MinIO路径)作为唯一标识匹配文件
+- 创建/更新目录节点
+- 更新文件的 parent_id
+- 正确处理同名但不同路径的文件(如"1请示报告卡"在不同目录下)
+
+## 完成时间
+
+2025年12月11日
+
diff --git a/清理重复模板总结.md b/清理重复模板总结.md
new file mode 100644
index 0000000..0a60c7b
--- /dev/null
+++ b/清理重复模板总结.md
@@ -0,0 +1,139 @@
+# 清理重复和无效模板总结
+
+## 任务完成情况
+
+✅ **已完成所有任务**
+
+1. ✅ 删除 f_polic_file_config 表中的重复数据
+2. ✅ 删除无效的模板数据
+3. ✅ 确保文档模板结构和 template_finish/ 文件夹对应
+4. ✅ 同步更新测试页面
+
+## 执行结果
+
+### 1. 清理前状态
+- **数据库中的模板总数**: 53个
+- **唯一模板名称**: 24个
+- **重复模板**: 4组(共29个重复记录)
+
+### 2. 清理操作
+- **删除重复模板**: 29个
+- **删除关联关系**: 136条
+- **标记无效模板**: 6个
+
+### 3. 清理后状态
+- **数据库中的模板总数**: 24个
+- **唯一模板名称**: 24个
+- **启用的模板**: 18个(有filePath的)
+- **禁用的模板**: 6个(无效模板)
+
+## 清理的重复模板详情
+
+### 1. 请示报告卡
+- **保留**: ID 1765425930118581 (1请示报告卡)
+- **删除**: ID 1765425917608094 (请示报告卡)
+
+### 2. 保密承诺书
+- **保留**: ID 1765425919729046 (2保密承诺书)
+- **删除**: ID 1765425919731077 (1保密承诺书)
+
+### 3. 谈话审批表
+- **保留**: ID 1765425918217504 (谈话审批表)
+- **删除**:
+ - ID 1765273961492987 (2谈话审批表)
+ - ID 1765273964441714 (2谈话审批表)
+
+### 4. 其他重复模板(标准化后名称相同)
+- **删除**: 26个重复模板,包括:
+ - 多个版本的"初步核实审批表"
+ - 多个版本的"请示报告卡"
+ - 多个版本的"谈话通知书"
+ - 多个版本的"谈话笔录"
+ - 多个版本的"保密承诺书"
+ - 等等
+
+## 标记的无效模板
+
+以下模板不在 template_finish 文件夹中,已标记为无效(state=0):
+
+1. 2-初核模版 (ID: 1765273961277310)
+2. 走读式谈话审批 (ID: 1765273962700431)
+3. 走读式谈话流程 (ID: 1765273962716807)
+4. 谈话通知书 (ID: 1765273962774249)
+5. 8.XXX初核情况报告 (ID: 1765273963158289)
+6. 2保密承诺书 (ID: 1765425919729046)
+
+## 最终启用的模板列表(18个)
+
+1. ✅ 1请示报告卡 (ID: 1765425930118581)
+2. ✅ 8-1请示报告卡(初核报告结论) (ID: 1765273962631542)
+3. ✅ XXX初核情况报告 (ID: 1765425930460962)
+4. ✅ 初步核实审批表 (ID: 1765425918287774)
+5. ✅ 办案人员-办案安全保密承诺书 (ID: 1765425919629084)
+6. ✅ 点对点交接单 (ID: 1765425919512780)
+7. ✅ 被谈话人权利义务告知书 (ID: 1765425930059797)
+8. ✅ 谈话前安全风险评估表 (ID: 1765425918902422)
+9. ✅ 谈话后安全风险评估表 (ID: 1765425919242549)
+10. ✅ 谈话审批表 (ID: 1765425918217504)
+11. ✅ 谈话方案 (ID: 1765425918745496)
+12. ✅ 谈话笔录 (ID: 1765425918749247)
+13. ✅ 谈话询问对象情况摸底调查30问 (ID: 1765425918921697)
+14. ✅ 谈话通知书第一联 (ID: 1765273963625524)
+15. ✅ 谈话通知书第三联 (ID: 1765273963038891)
+16. ✅ 谈话通知书第二联 (ID: 1765273963825806)
+17. ✅ 附件初核方案 (ID: 1765425918483987)
+18. ✅ 陪送交接单 (ID: 1765425919389484)
+
+## 测试页面更新
+
+### 更新内容
+1. ✅ 移除了硬编码的默认fileId(因为旧的ID已被删除)
+2. ✅ 改进了文件列表加载逻辑
+3. ✅ 自动加载所有可用的模板文件
+
+### 功能说明
+- **自动加载**: 页面加载时自动从 `/api/file-configs` 接口获取所有可用的模板
+- **手动加载**: 点击"加载全部可用模板"按钮可以重新加载模板列表
+- **过滤**: 只显示有filePath的模板(确保模板文件已上传到MinIO)
+
+## 数据库表更新情况
+
+### f_polic_file_config 表
+- **删除**: 29条重复记录
+- **更新**: 6条记录标记为无效(state=0)
+- **最终状态**: 24条记录(18个启用,6个禁用)
+
+### f_polic_file_field 表
+- **删除**: 136条关联关系(随重复模板一起删除)
+
+## 脚本文件
+
+### 主要脚本
+1. **cleanup_duplicate_templates.py** - 清理脚本
+ - 扫描 template_finish 文件夹
+ - 识别重复模板
+ - 删除重复记录
+ - 标记无效模板
+
+2. **validate_and_update_templates.py** - 校验和更新脚本
+ - 重新校验模板和字段关联
+ - 上传模板到MinIO
+ - 更新数据库配置
+
+## 注意事项
+
+1. **重复判断标准**: 使用标准化后的模板名称进行判断(去掉括号、数字前缀等)
+2. **保留策略**: 保留最新的、启用的模板记录
+3. **无效模板**: 不在 template_finish 文件夹中的模板会被标记为无效,但不会删除(保留历史记录)
+4. **关联关系**: 删除模板时会自动删除相关的字段关联关系
+
+## 后续建议
+
+1. **定期清理**: 建议定期运行 `cleanup_duplicate_templates.py` 脚本,确保没有重复数据
+2. **新增模板**: 新增模板时,确保模板文件放在 `template_finish` 文件夹中,然后运行 `validate_and_update_templates.py`
+3. **测试验证**: 每次清理后,建议测试文档生成功能,确保所有模板都能正常工作
+
+## 完成时间
+
+2025年12月11日
+