545 lines
19 KiB
Python
545 lines
19 KiB
Python
"""
|
||
从 template_finish 目录初始化模板树状结构
|
||
删除旧数据,根据目录结构完全重建
|
||
"""
|
||
import os
|
||
import json
|
||
import pymysql
|
||
from pathlib import Path
|
||
from typing import Dict, List, Optional, Tuple
|
||
from datetime import datetime
|
||
from minio import Minio
|
||
from minio.error import S3Error
|
||
|
||
# 数据库连接配置
|
||
DB_CONFIG = {
|
||
'host': os.getenv('DB_HOST', '152.136.177.240'),
|
||
'port': int(os.getenv('DB_PORT', 5012)),
|
||
'user': os.getenv('DB_USER', 'finyx'),
|
||
'password': os.getenv('DB_PASSWORD', '6QsGK6MpePZDE57Z'),
|
||
'database': os.getenv('DB_NAME', 'finyx'),
|
||
'charset': 'utf8mb4'
|
||
}
|
||
|
||
# MinIO连接配置
|
||
MINIO_CONFIG = {
|
||
'endpoint': 'minio.datacubeworld.com:9000',
|
||
'access_key': 'JOLXFXny3avFSzB0uRA5',
|
||
'secret_key': 'G1BR8jStNfovkfH5ou39EmPl34E4l7dGrnd3Cz0I',
|
||
'secure': True
|
||
}
|
||
|
||
TENANT_ID = 615873064429507639
|
||
CREATED_BY = 655162080928945152
|
||
UPDATED_BY = 655162080928945152
|
||
BUCKET_NAME = 'finyx'
|
||
|
||
# 项目根目录
|
||
PROJECT_ROOT = Path(__file__).parent
|
||
TEMPLATES_DIR = PROJECT_ROOT / "template_finish"
|
||
|
||
# 文档类型映射
|
||
DOCUMENT_TYPE_MAPPING = {
|
||
"1.请示报告卡(XXX)": {
|
||
"template_code": "REPORT_CARD",
|
||
"name": "1.请示报告卡(XXX)",
|
||
"business_type": "INVESTIGATION"
|
||
},
|
||
"2.初步核实审批表(XXX)": {
|
||
"template_code": "PRELIMINARY_VERIFICATION_APPROVAL",
|
||
"name": "2.初步核实审批表(XXX)",
|
||
"business_type": "INVESTIGATION"
|
||
},
|
||
"3.附件初核方案(XXX)": {
|
||
"template_code": "INVESTIGATION_PLAN",
|
||
"name": "3.附件初核方案(XXX)",
|
||
"business_type": "INVESTIGATION"
|
||
},
|
||
"谈话通知书第一联": {
|
||
"template_code": "NOTIFICATION_LETTER_1",
|
||
"name": "谈话通知书第一联",
|
||
"business_type": "INVESTIGATION"
|
||
},
|
||
"谈话通知书第二联": {
|
||
"template_code": "NOTIFICATION_LETTER_2",
|
||
"name": "谈话通知书第二联",
|
||
"business_type": "INVESTIGATION"
|
||
},
|
||
"谈话通知书第三联": {
|
||
"template_code": "NOTIFICATION_LETTER_3",
|
||
"name": "谈话通知书第三联",
|
||
"business_type": "INVESTIGATION"
|
||
},
|
||
"1.请示报告卡(初核谈话)": {
|
||
"template_code": "REPORT_CARD_INTERVIEW",
|
||
"name": "1.请示报告卡(初核谈话)",
|
||
"business_type": "INVESTIGATION"
|
||
},
|
||
"2谈话审批表": {
|
||
"template_code": "INTERVIEW_APPROVAL_FORM",
|
||
"name": "2谈话审批表",
|
||
"business_type": "INVESTIGATION"
|
||
},
|
||
"3.谈话前安全风险评估表": {
|
||
"template_code": "PRE_INTERVIEW_RISK_ASSESSMENT",
|
||
"name": "3.谈话前安全风险评估表",
|
||
"business_type": "INVESTIGATION"
|
||
},
|
||
"4.谈话方案": {
|
||
"template_code": "INTERVIEW_PLAN",
|
||
"name": "4.谈话方案",
|
||
"business_type": "INVESTIGATION"
|
||
},
|
||
"5.谈话后安全风险评估表": {
|
||
"template_code": "POST_INTERVIEW_RISK_ASSESSMENT",
|
||
"name": "5.谈话后安全风险评估表",
|
||
"business_type": "INVESTIGATION"
|
||
},
|
||
"1.谈话笔录": {
|
||
"template_code": "INTERVIEW_RECORD",
|
||
"name": "1.谈话笔录",
|
||
"business_type": "INVESTIGATION"
|
||
},
|
||
"2.谈话询问对象情况摸底调查30问": {
|
||
"template_code": "INVESTIGATION_30_QUESTIONS",
|
||
"name": "2.谈话询问对象情况摸底调查30问",
|
||
"business_type": "INVESTIGATION"
|
||
},
|
||
"3.被谈话人权利义务告知书": {
|
||
"template_code": "RIGHTS_OBLIGATIONS_NOTICE",
|
||
"name": "3.被谈话人权利义务告知书",
|
||
"business_type": "INVESTIGATION"
|
||
},
|
||
"4.点对点交接单": {
|
||
"template_code": "HANDOVER_FORM",
|
||
"name": "4.点对点交接单",
|
||
"business_type": "INVESTIGATION"
|
||
},
|
||
"5.陪送交接单(新)": {
|
||
"template_code": "ESCORT_HANDOVER_FORM",
|
||
"name": "5.陪送交接单(新)",
|
||
"business_type": "INVESTIGATION"
|
||
},
|
||
"6.1保密承诺书(谈话对象使用-非中共党员用)": {
|
||
"template_code": "CONFIDENTIALITY_COMMITMENT_NON_PARTY",
|
||
"name": "6.1保密承诺书(谈话对象使用-非中共党员用)",
|
||
"business_type": "INVESTIGATION"
|
||
},
|
||
"6.2保密承诺书(谈话对象使用-中共党员用)": {
|
||
"template_code": "CONFIDENTIALITY_COMMITMENT_PARTY",
|
||
"name": "6.2保密承诺书(谈话对象使用-中共党员用)",
|
||
"business_type": "INVESTIGATION"
|
||
},
|
||
"7.办案人员-办案安全保密承诺书": {
|
||
"template_code": "INVESTIGATOR_CONFIDENTIALITY_COMMITMENT",
|
||
"name": "7.办案人员-办案安全保密承诺书",
|
||
"business_type": "INVESTIGATION"
|
||
},
|
||
"8-1请示报告卡(初核报告结论) ": {
|
||
"template_code": "REPORT_CARD_CONCLUSION",
|
||
"name": "8-1请示报告卡(初核报告结论) ",
|
||
"business_type": "INVESTIGATION"
|
||
},
|
||
"8.XXX初核情况报告": {
|
||
"template_code": "INVESTIGATION_REPORT",
|
||
"name": "8.XXX初核情况报告",
|
||
"business_type": "INVESTIGATION"
|
||
}
|
||
}
|
||
|
||
|
||
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 identify_document_type(file_name: str) -> Optional[Dict]:
|
||
"""根据完整文件名识别文档类型"""
|
||
base_name = Path(file_name).stem
|
||
if base_name in DOCUMENT_TYPE_MAPPING:
|
||
return DOCUMENT_TYPE_MAPPING[base_name]
|
||
return None
|
||
|
||
|
||
def upload_to_minio(file_path: Path) -> str:
|
||
"""上传文件到MinIO"""
|
||
try:
|
||
client = Minio(
|
||
MINIO_CONFIG['endpoint'],
|
||
access_key=MINIO_CONFIG['access_key'],
|
||
secret_key=MINIO_CONFIG['secret_key'],
|
||
secure=MINIO_CONFIG['secure']
|
||
)
|
||
|
||
found = client.bucket_exists(BUCKET_NAME)
|
||
if not found:
|
||
raise Exception(f"存储桶 '{BUCKET_NAME}' 不存在,请先创建")
|
||
|
||
now = datetime.now()
|
||
object_name = f'{TENANT_ID}/TEMPLATE/{now.year}/{now.month:02d}/{file_path.name}'
|
||
|
||
client.fput_object(
|
||
BUCKET_NAME,
|
||
object_name,
|
||
str(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 scan_directory_structure(base_dir: Path) -> List[Dict]:
|
||
"""
|
||
扫描目录结构,返回按层级排序的节点列表
|
||
每个节点包含:type, name, path, parent_path, level, template_code, file_path
|
||
"""
|
||
nodes = []
|
||
|
||
def process_path(path: Path, parent_path: Optional[str] = None, level: int = 0):
|
||
"""递归处理路径"""
|
||
if path.is_file() and path.suffix == '.docx':
|
||
file_name = path.stem
|
||
doc_config = identify_document_type(file_name)
|
||
|
||
nodes.append({
|
||
'type': 'file',
|
||
'name': file_name,
|
||
'path': str(path),
|
||
'parent_path': parent_path,
|
||
'level': level,
|
||
'template_code': doc_config['template_code'] if doc_config else None,
|
||
'doc_config': doc_config,
|
||
'file_path': path
|
||
})
|
||
elif path.is_dir():
|
||
dir_name = path.name
|
||
nodes.append({
|
||
'type': 'directory',
|
||
'name': dir_name,
|
||
'path': str(path),
|
||
'parent_path': parent_path,
|
||
'level': level,
|
||
'template_code': None,
|
||
'doc_config': None,
|
||
'file_path': None
|
||
})
|
||
|
||
for child in sorted(path.iterdir()):
|
||
if child.name != '__pycache__':
|
||
process_path(child, str(path), level + 1)
|
||
|
||
if TEMPLATES_DIR.exists():
|
||
for item in sorted(TEMPLATES_DIR.iterdir()):
|
||
if item.name != '__pycache__':
|
||
process_path(item, None, 0)
|
||
|
||
# 按层级排序
|
||
return sorted(nodes, key=lambda x: (x['level'], x['path']))
|
||
|
||
|
||
def delete_old_data(conn, dry_run: bool = True):
|
||
"""删除旧数据"""
|
||
cursor = conn.cursor()
|
||
|
||
try:
|
||
print("\n" + "="*80)
|
||
print("删除旧数据")
|
||
print("="*80)
|
||
|
||
# 1. 先删除关联表 f_polic_file_field
|
||
print("\n1. 删除 f_polic_file_field 关联记录...")
|
||
|
||
if not dry_run:
|
||
# 先获取所有相关的 file_id
|
||
select_file_ids_sql = """
|
||
SELECT id FROM f_polic_file_config
|
||
WHERE tenant_id = %s
|
||
"""
|
||
cursor.execute(select_file_ids_sql, (TENANT_ID,))
|
||
file_ids = [row[0] for row in cursor.fetchall()]
|
||
|
||
if file_ids:
|
||
# 使用占位符构建SQL
|
||
placeholders = ','.join(['%s'] * len(file_ids))
|
||
delete_file_field_sql = f"""
|
||
DELETE FROM f_polic_file_field
|
||
WHERE tenant_id = %s AND file_id IN ({placeholders})
|
||
"""
|
||
cursor.execute(delete_file_field_sql, [TENANT_ID] + file_ids)
|
||
deleted_count = cursor.rowcount
|
||
print(f" ✓ 删除了 {deleted_count} 条关联记录")
|
||
else:
|
||
print(" ✓ 没有需要删除的关联记录")
|
||
else:
|
||
# 模拟模式:只统计
|
||
count_sql = """
|
||
SELECT COUNT(*) FROM f_polic_file_field
|
||
WHERE tenant_id = %s AND file_id IN (
|
||
SELECT id FROM f_polic_file_config WHERE tenant_id = %s
|
||
)
|
||
"""
|
||
cursor.execute(count_sql, (TENANT_ID, TENANT_ID))
|
||
count = cursor.fetchone()[0]
|
||
print(f" [模拟] 将删除 {count} 条关联记录")
|
||
|
||
# 2. 删除 f_polic_file_config 记录
|
||
print("\n2. 删除 f_polic_file_config 记录...")
|
||
delete_config_sql = """
|
||
DELETE FROM f_polic_file_config
|
||
WHERE tenant_id = %s
|
||
"""
|
||
|
||
if not dry_run:
|
||
cursor.execute(delete_config_sql, (TENANT_ID,))
|
||
deleted_count = cursor.rowcount
|
||
print(f" ✓ 删除了 {deleted_count} 条配置记录")
|
||
conn.commit()
|
||
else:
|
||
count_sql = "SELECT COUNT(*) FROM f_polic_file_config WHERE tenant_id = %s"
|
||
cursor.execute(count_sql, (TENANT_ID,))
|
||
count = cursor.fetchone()[0]
|
||
print(f" [模拟] 将删除 {count} 条配置记录")
|
||
|
||
return True
|
||
|
||
except Exception as e:
|
||
if not dry_run:
|
||
conn.rollback()
|
||
print(f" ✗ 删除失败: {e}")
|
||
raise
|
||
finally:
|
||
cursor.close()
|
||
|
||
|
||
def create_tree_structure(conn, nodes: List[Dict], upload_files: bool = True, dry_run: bool = True):
|
||
"""创建树状结构"""
|
||
cursor = conn.cursor()
|
||
|
||
try:
|
||
if not dry_run:
|
||
conn.autocommit(False)
|
||
|
||
print("\n" + "="*80)
|
||
print("创建树状结构")
|
||
print("="*80)
|
||
|
||
# 创建路径到ID的映射
|
||
path_to_id = {}
|
||
created_count = 0
|
||
updated_count = 0
|
||
|
||
# 按层级顺序处理
|
||
for node in nodes:
|
||
node_path = node['path']
|
||
node_name = node['name']
|
||
parent_path = node['parent_path']
|
||
level = node['level']
|
||
|
||
# 获取父节点ID
|
||
parent_id = path_to_id.get(parent_path) if parent_path else None
|
||
|
||
if node['type'] == 'directory':
|
||
# 创建目录节点
|
||
node_id = generate_id()
|
||
path_to_id[node_path] = node_id
|
||
|
||
if not dry_run:
|
||
# 目录节点不包含 template_code 字段
|
||
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, (
|
||
node_id,
|
||
TENANT_ID,
|
||
parent_id,
|
||
node_name,
|
||
None,
|
||
None,
|
||
CREATED_BY,
|
||
UPDATED_BY,
|
||
1
|
||
))
|
||
|
||
indent = " " * level
|
||
parent_info = f" [父: {path_to_id.get(parent_path, 'None')}]" if parent_path else ""
|
||
print(f"{indent}✓ {'[模拟]' if dry_run else ''}创建目录: {node_name} (ID: {node_id}){parent_info}")
|
||
created_count += 1
|
||
|
||
else:
|
||
# 创建文件节点
|
||
node_id = generate_id()
|
||
path_to_id[node_path] = node_id
|
||
|
||
doc_config = node.get('doc_config')
|
||
template_code = node.get('template_code')
|
||
file_path_obj = node.get('file_path')
|
||
|
||
# 上传文件到MinIO(如果需要)
|
||
minio_path = None
|
||
if upload_files and file_path_obj and file_path_obj.exists():
|
||
try:
|
||
if not dry_run:
|
||
minio_path = upload_to_minio(file_path_obj)
|
||
else:
|
||
minio_path = f"/{TENANT_ID}/TEMPLATE/2025/12/{file_path_obj.name}"
|
||
print(f" {'[模拟]' if dry_run else ''}上传文件: {file_path_obj.name} → {minio_path}")
|
||
except Exception as e:
|
||
print(f" ⚠ 上传文件失败: {e}")
|
||
# 继续执行,使用None作为路径
|
||
|
||
# 构建 input_data
|
||
input_data = None
|
||
if doc_config:
|
||
input_data = json.dumps({
|
||
'template_code': doc_config['template_code'],
|
||
'business_type': doc_config['business_type']
|
||
}, ensure_ascii=False)
|
||
|
||
if not dry_run:
|
||
# 如果 template_code 为 None,使用空字符串
|
||
template_code_value = template_code if template_code else ''
|
||
|
||
insert_sql = """
|
||
INSERT INTO f_polic_file_config
|
||
(id, tenant_id, parent_id, name, input_data, file_path, template_code,
|
||
created_time, created_by, updated_time, updated_by, state)
|
||
VALUES (%s, %s, %s, %s, %s, %s, %s, NOW(), %s, NOW(), %s, %s)
|
||
"""
|
||
cursor.execute(insert_sql, (
|
||
node_id,
|
||
TENANT_ID,
|
||
parent_id,
|
||
node_name,
|
||
input_data,
|
||
minio_path,
|
||
template_code_value,
|
||
CREATED_BY,
|
||
UPDATED_BY,
|
||
1
|
||
))
|
||
|
||
indent = " " * level
|
||
parent_info = f" [父: {path_to_id.get(parent_path, 'None')}]" if parent_path else ""
|
||
template_info = f" [code: {template_code}]" if template_code else ""
|
||
print(f"{indent}✓ {'[模拟]' if dry_run else ''}创建文件: {node_name} (ID: {node_id}){parent_info}{template_info}")
|
||
created_count += 1
|
||
|
||
if not dry_run:
|
||
conn.commit()
|
||
print(f"\n✓ 创建完成!共创建 {created_count} 个节点")
|
||
else:
|
||
print(f"\n[模拟模式] 将创建 {created_count} 个节点")
|
||
|
||
return path_to_id
|
||
|
||
except Exception as e:
|
||
if not dry_run:
|
||
conn.rollback()
|
||
print(f"\n✗ 创建失败: {e}")
|
||
import traceback
|
||
traceback.print_exc()
|
||
raise
|
||
finally:
|
||
cursor.close()
|
||
|
||
|
||
def main():
|
||
"""主函数"""
|
||
print("="*80)
|
||
print("初始化模板树状结构(从目录结构完全重建)")
|
||
print("="*80)
|
||
print("\n⚠️ 警告:此操作将删除当前租户的所有模板数据!")
|
||
print(" 包括:")
|
||
print(" - f_polic_file_config 表中的所有记录")
|
||
print(" - f_polic_file_field 表中的相关关联记录")
|
||
print(" 然后根据 template_finish 目录结构完全重建")
|
||
|
||
# 确认
|
||
print("\n" + "="*80)
|
||
confirm1 = input("\n确认继续?(yes/no,默认no): ").strip().lower()
|
||
if confirm1 != 'yes':
|
||
print("已取消")
|
||
return
|
||
|
||
# 连接数据库
|
||
try:
|
||
conn = pymysql.connect(**DB_CONFIG)
|
||
print("✓ 数据库连接成功")
|
||
except Exception as e:
|
||
print(f"✗ 数据库连接失败: {e}")
|
||
return
|
||
|
||
try:
|
||
# 扫描目录结构
|
||
print("\n扫描目录结构...")
|
||
nodes = scan_directory_structure(TEMPLATES_DIR)
|
||
print(f" 找到 {len(nodes)} 个节点")
|
||
print(f" 其中目录: {len([n for n in nodes if n['type'] == 'directory'])} 个")
|
||
print(f" 其中文件: {len([n for n in nodes if n['type'] == 'file'])} 个")
|
||
|
||
# 显示预览
|
||
print("\n目录结构预览:")
|
||
for node in nodes[:10]: # 只显示前10个
|
||
indent = " " * node['level']
|
||
type_icon = "📁" if node['type'] == 'directory' else "📄"
|
||
print(f"{indent}{type_icon} {node['name']}")
|
||
if len(nodes) > 10:
|
||
print(f" ... 还有 {len(nodes) - 10} 个节点")
|
||
|
||
# 询问是否上传文件
|
||
print("\n" + "="*80)
|
||
upload_files = input("\n是否上传文件到MinIO?(yes/no,默认yes): ").strip().lower()
|
||
upload_files = upload_files != 'no'
|
||
|
||
# 先执行模拟删除
|
||
print("\n执行模拟删除...")
|
||
delete_old_data(conn, dry_run=True)
|
||
|
||
# 再执行模拟创建
|
||
print("\n执行模拟创建...")
|
||
create_tree_structure(conn, nodes, upload_files=upload_files, dry_run=True)
|
||
|
||
# 最终确认
|
||
print("\n" + "="*80)
|
||
confirm2 = input("\n确认执行实际更新?(yes/no,默认no): ").strip().lower()
|
||
if confirm2 != 'yes':
|
||
print("已取消")
|
||
return
|
||
|
||
# 执行实际删除
|
||
print("\n执行实际删除...")
|
||
delete_old_data(conn, dry_run=False)
|
||
|
||
# 执行实际创建
|
||
print("\n执行实际创建...")
|
||
create_tree_structure(conn, nodes, upload_files=upload_files, dry_run=False)
|
||
|
||
print("\n" + "="*80)
|
||
print("初始化完成!")
|
||
print("="*80)
|
||
|
||
except Exception as e:
|
||
print(f"\n✗ 初始化失败: {e}")
|
||
import traceback
|
||
traceback.print_exc()
|
||
finally:
|
||
conn.close()
|
||
print("\n数据库连接已关闭")
|
||
|
||
|
||
if __name__ == '__main__':
|
||
main()
|
||
|