Compare commits

..

32 Commits

Author SHA1 Message Date
python
e3f4a394c1 Revert "添加通过taskId获取文档的接口,支持文件列表查询和参数验证,增强错误处理能力。同时,优化文档生成逻辑,确保生成的文档名称和路径的准确性。"
This reverts commit 4897c96b05f75d26249bf50d62546030b873196a.
2025-12-11 10:31:44 +08:00
python
4084cb1819 更新生成下载链接的逻辑,添加Windows平台的UTF-8编码支持,优化输出信息格式,确保错误信息和状态提示更加清晰。同时,更新文档服务中的文件路径生成逻辑,确保文件名唯一性并增强调试信息的详细性。 2025-12-11 10:16:38 +08:00
python
b0360cc15b 增强文档填充功能,添加详细的调试信息和占位符替换统计,确保所有占位符被正确替换并记录未替换的占位符。优化文档名称生成逻辑,处理特殊字符并确保生成的文件名准确。 2025-12-11 10:10:05 +08:00
python
6c31137cf4 增强文档生成和处理逻辑,兼容文件信息字段,改进错误处理机制,确保在处理段落和表格时的稳定性和详细错误记录。 2025-12-11 09:41:31 +08:00
python
4897c96b05 添加通过taskId获取文档的接口,支持文件列表查询和参数验证,增强错误处理能力。同时,优化文档生成逻辑,确保生成的文档名称和路径的准确性。 2025-12-11 09:30:52 +08:00
python
3a3b38cd78 添加文件配置查询接口,提供可用文件列表以供文档生成使用,同时增强错误处理信息。更新前端逻辑以自动加载文件列表并优化用户交互体验。 2025-12-11 09:23:07 +08:00
python
2a5952f3f5 更新输入字段的默认值,调整文件项的添加逻辑,改用文件ID替代模板编码,简化文件配置查询,提升代码可读性和维护性。 2025-12-11 09:16:14 +08:00
python
a320f55da0 更新文档生成逻辑,改用文件ID替代模板编码,增强参数验证和错误处理能力。同时,调整文件配置查询逻辑,确保根据文件ID获取文件配置信息,提升代码可读性和维护性。 2025-12-11 09:09:10 +08:00
python
ebc1154beb 更新下载链接生成逻辑,修改文件路径以反映最新的文档版本。同时,在文档服务中添加文件路径有效性检查,确保模板文件路径不为空,提升错误处理能力。 2025-12-10 19:02:20 +08:00
python
0563ff5346 更新AI服务逻辑,改进年龄字段的推断机制,允许在缺失年龄数据时根据出生年月自动计算年龄,并优化相关日志记录。同时,更新文档以反映新的字段配置和使用说明,确保数据提取的准确性和完整性。 2025-12-10 14:16:59 +08:00
python
e38ba42669 删除多个不再需要的文档,包括AI服务错误分析报告、模板树状结构更新说明、数据库备份和恢复工具使用说明等,简化项目结构,提升可维护性。同时,更新文档服务以支持从input_data和template_code中提取模板配置,增强查询逻辑的灵活性。 2025-12-10 10:39:36 +08:00
python
11be119ffc 更新环境配置,支持多种AI服务提供商(华为和硅基流动),增强API调用的灵活性和可配置性,同时更新文档以反映新的配置选项和使用说明。 2025-12-10 10:05:45 +08:00
python
cd27bb4bd0 优化AI服务的内容提取逻辑,增强对缺失字段的推断能力,改进后处理机制以提升数据提取的准确性和完整性。 2025-12-10 09:49:57 +08:00
python
6871c2e803 增强后处理逻辑,允许从原始输入文本中提取缺失的性别和年龄字段,改进数据推断的准确性和完整性。 2025-12-10 09:37:37 +08:00
python
24fdfdea4c 增强AI服务的内容提取逻辑,更新提取助手的描述,添加后处理逻辑以推断缺失的性别、职级和线索来源字段,确保提取结果的准确性和完整性。 2025-12-09 15:32:25 +08:00
python
563d97184b 优化AI服务的内容提取逻辑,更新提取助手的描述,增强JSON格式的严格性,修复字段名错误和下划线前缀处理,确保提取结果的准确性和一致性。 2025-12-09 15:19:32 +08:00
python
9bf1dd1210 优化AI服务的内容提取逻辑,增强对API返回结果的处理能力,改进JSON解析和错误处理机制,确保在提取数据失败时能够返回空结果而不抛出异常,同时记录详细的调试信息以提高容错性和可维护性。 2025-12-09 15:01:31 +08:00
python
315301fc0b 添加AI日志记录器支持,增强对话日志记录功能,记录请求和响应信息,包括成功和错误情况,以提高调试和监控能力。 2025-12-09 14:51:33 +08:00
python
8bebc13efe 优化调整抽取 2025-12-09 14:41:26 +08:00
python
f1b5c52500 修正json repair安装和导入 2025-12-09 14:18:32 +08:00
python
7c30e59328 增强AI服务的JSON解析能力,添加对jsonrepair库的支持以处理不完整或格式错误的JSON,改进字段提取逻辑以允许部分字段为空,提升数据提取的容错性和准确性。 2025-12-09 14:13:07 +08:00
python
eaa384cf7e 更新提示配置和AI服务内容,增强信息提取助手的描述,明确提取要求,添加后处理逻辑以推断缺失字段,改进字段提取方法以提高数据提取的准确性和完整性。 2025-12-09 12:56:28 +08:00
python
b8d89c28ec 增强调试信息,添加对AI返回结果和字段映射的打印,改进字段名清理逻辑以避免空字段名的处理错误,确保数据提取的准确性和完整性。 2025-12-09 12:45:11 +08:00
python
e1d8d27dc4 更新提示配置,统一日期格式为中文格式,增强AI服务的日期规范化功能,添加对常见拼写错误的处理逻辑,改进字段名清理和规范化方法以提高数据提取准确性。 2025-12-09 12:34:01 +08:00
python
e31cd0b764 添加API最大token数配置,增强JSON解析功能,新增清理和修复JSON字符串的方法,改进字段名规范化逻辑以提高数据提取准确性。 2025-12-09 12:14:34 +08:00
python
d8fa4c3d7e 添加API超时配置,支持思考模式下动态调整超时时间;修改重试机制的延迟时间,从1秒改为2秒,增强错误处理逻辑。 2025-12-09 11:58:07 +08:00
python
c7a7780e71 更新提示配置和AI服务内容,简化信息提取助手的描述,明确提取要求,增强对API返回内容的处理逻辑,添加调试信息以便于问题排查。 2025-12-09 11:46:52 +08:00
python
14ff607b52 为华为大模型API调用添加重试机制,增强了错误处理逻辑,确保在请求失败时能够自动重试并提供详细的错误信息。同时,将API调用逻辑分离到单独的方法中,以提高代码可读性和可维护性。 2025-12-09 11:41:45 +08:00
python
8461725a13 更新提示配置和AI服务内容,增强信息提取助手的描述,明确提取要求和规则,添加新的字段提取逻辑以提高提取准确性和完整性。 2025-12-09 11:39:18 +08:00
python
684cb0141a 增强AI服务的JSON提取功能,添加了从文本中提取JSON对象的方法,改进了对华为大模型返回内容的处理,确保只返回JSON对象而不包含其他说明。 2025-12-09 11:30:02 +08:00
python
f0cb4a7ba0 调整env文件,配置华为大模型最新参数 2025-12-09 11:03:19 +08:00
python
7d50b160c2 创建新分支,用于静态交通本地服务器部署,调整默认为华为大模型调用 2025-12-09 10:33:41 +08:00
63 changed files with 15416 additions and 371 deletions

68
.env
View File

@ -1,14 +1,68 @@
# 硅基流动API配置 # ========== AI服务提供商配置 ==========
SILICONFLOW_API_KEY=sk-xnhmtotmlpjomrejbwdbczbpbyvanpxndvbxltodjwzbpmni # 选择使用的AI服务提供商
SILICONFLOW_MODEL=deepseek-ai/DeepSeek-V3.2-Exp # 可选值: 'huawei' 或 'siliconflow'
# 默认值: 'siliconflow'
AI_PROVIDER=siliconflow
# 华为大模型API配置预留 # ========== 华为大模型API配置 ==========
HUAWEI_API_ENDPOINT= # 当 AI_PROVIDER=huawei 时使用以下配置
HUAWEI_API_KEY=
# 数据库配置 # API端点地址
HUAWEI_API_ENDPOINT=http://10.100.31.26:3001/v1/chat/completions
# API密钥
HUAWEI_API_KEY=sk-PoeiV3qwyTIRqcVc84E8E24cD2904872859a87922e0d9186
# 模型名称
HUAWEI_MODEL=DeepSeek-R1-Distill-Llama-70B
# API超时配置
# 开启思考模式时,响应时间会显著增加,需要更长的超时时间
# 默认180秒3分钟
HUAWEI_API_TIMEOUT=180
# API最大token数配置
# 开启思考模式时模型可能生成更长的响应需要更多的token
# 默认12000
HUAWEI_API_MAX_TOKENS=12000
# ========== 硅基流动API配置 ==========
# 当 AI_PROVIDER=siliconflow 时使用以下配置
# API端点地址默认值通常不需要修改
SILICONFLOW_URL=https://api.siliconflow.cn/v1/chat/completions
# API密钥必需
SILICONFLOW_API_KEY=sk-pgujibohpenkomkwlufexmqzyckglgogdiubfplgqxkfqgfu
# 模型名称(默认值,通常不需要修改)
SILICONFLOW_MODEL=Qwen/Qwen2.5-72B-Instruct
# API超时配置
# 默认120秒
SILICONFLOW_API_TIMEOUT=120
# API最大token数配置
# 默认2000
SILICONFLOW_API_MAX_TOKENS=2000
# ========== 数据库配置 ==========
DB_HOST=152.136.177.240 DB_HOST=152.136.177.240
DB_PORT=5012 DB_PORT=5012
DB_USER=finyx DB_USER=finyx
DB_PASSWORD=6QsGK6MpePZDE57Z DB_PASSWORD=6QsGK6MpePZDE57Z
DB_NAME=finyx DB_NAME=finyx
# ========== MinIO配置可选文档生成功能需要 ==========
MINIO_ENDPOINT=minio.datacubeworld.com:9000
MINIO_ACCESS_KEY=JOLXFXny3avFSzB0uRA5
MINIO_SECRET_KEY=G1BR8jStNfovkfH5ou39EmPl34E4l7dGrnd3Cz0I
MINIO_BUCKET=finyx
MINIO_SECURE=true
# ========== 服务配置 ==========
# 服务端口
PORT=7500
# 调试模式true/false
DEBUG=False

View File

@ -1,14 +1,68 @@
# 硅基流动API配置 # ========== AI服务提供商配置 ==========
SILICONFLOW_API_KEY=your_api_key_here # 选择使用的AI服务提供商
SILICONFLOW_MODEL=deepseek-ai/DeepSeek-V3.2-Exp # 可选值: 'huawei' 或 'siliconflow'
# 默认值: 'siliconflow'
AI_PROVIDER=siliconflow
# 华为大模型API配置预留 # ========== 华为大模型API配置 ==========
HUAWEI_API_ENDPOINT= # 当 AI_PROVIDER=huawei 时使用以下配置
HUAWEI_API_KEY=
# 数据库配置 # API端点地址
HUAWEI_API_ENDPOINT=http://10.100.31.26:3001/v1/chat/completions
# API密钥
HUAWEI_API_KEY=sk-PoeiV3qwyTIRqcVc84E8E24cD2904872859a87922e0d9186
# 模型名称
HUAWEI_MODEL=DeepSeek-R1-Distill-Llama-70B
# API超时配置
# 开启思考模式时,响应时间会显著增加,需要更长的超时时间
# 默认180秒3分钟
HUAWEI_API_TIMEOUT=180
# API最大token数配置
# 开启思考模式时模型可能生成更长的响应需要更多的token
# 默认12000
HUAWEI_API_MAX_TOKENS=12000
# ========== 硅基流动API配置 ==========
# 当 AI_PROVIDER=siliconflow 时使用以下配置
# API端点地址默认值通常不需要修改
SILICONFLOW_URL=https://api.siliconflow.cn/v1/chat/completions
# API密钥必需
SILICONFLOW_API_KEY=sk-pgujibohpenkomkwlufexmqzyckglgogdiubfplgqxkfqgfu
# 模型名称(默认值,通常不需要修改)
SILICONFLOW_MODEL=Qwen/Qwen2.5-72B-Instruct
# API超时配置
# 默认120秒
SILICONFLOW_API_TIMEOUT=120
# API最大token数配置
# 默认2000
SILICONFLOW_API_MAX_TOKENS=2000
# ========== 数据库配置 ==========
DB_HOST=152.136.177.240 DB_HOST=152.136.177.240
DB_PORT=5012 DB_PORT=5012
DB_USER=finyx DB_USER=finyx
DB_PASSWORD=6QsGK6MpePZDE57Z DB_PASSWORD=6QsGK6MpePZDE57Z
DB_NAME=finyx DB_NAME=finyx
# ========== MinIO配置可选文档生成功能需要 ==========
MINIO_ENDPOINT=minio.datacubeworld.com:9000
MINIO_ACCESS_KEY=JOLXFXny3avFSzB0uRA5
MINIO_SECRET_KEY=G1BR8jStNfovkfH5ou39EmPl34E4l7dGrnd3Cz0I
MINIO_BUCKET=finyx
MINIO_SECURE=true
# ========== 服务配置 ==========
# 服务端口
PORT=7500
# 调试模式true/false
DEBUG=False

55
.gitignore vendored Normal file
View File

@ -0,0 +1,55 @@
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
# Virtual Environment
venv/
env/
ENV/
# IDE
.vscode/
.idea/
*.swp
*.swo
*~
# Logs
logs/
*.log
# Environment variables
.env
.env.local
# Database
*.db
*.sqlite
*.sqlite3
# OS
.DS_Store
Thumbs.db
# Project specific
parsed_fields.json
*.docx.bak

View File

@ -6,8 +6,10 @@
- ✅ AI解析接口 (`/api/ai/extract`) - 从输入文本中提取结构化字段 - ✅ AI解析接口 (`/api/ai/extract`) - 从输入文本中提取结构化字段
- ✅ 字段配置管理 - 从数据库读取字段配置 - ✅ 字段配置管理 - 从数据库读取字段配置
- ✅ 支持硅基流动大模型DeepSeek - ✅ 支持多种AI服务提供商
- 🔄 预留华为大模型接口支持 - 华为大模型DeepSeek-R1-Distill-Llama-70B
- 硅基流动DeepSeek-V3.2-Exp
- ✅ 可通过配置灵活切换AI服务提供商
- ✅ Web测试界面 - 可视化测试解析功能 - ✅ Web测试界面 - 可视化测试解析功能
## 项目结构 ## 项目结构
@ -70,14 +72,30 @@ copy .env.example .env
cp .env.example .env cp .env.example .env
``` ```
编辑 `.env` 文件,填入你的API密钥 编辑 `.env` 文件,填入你的配置
```env ```env
# 硅基流动API配置必需 # ========== AI服务提供商配置 ==========
SILICONFLOW_API_KEY=your_api_key_here # 选择使用的AI服务提供商
SILICONFLOW_MODEL=deepseek-ai/DeepSeek-V3.2-Exp # 可选值: 'huawei' 或 'siliconflow'
# 默认值: 'siliconflow'
AI_PROVIDER=siliconflow
# 数据库配置(已默认配置,如需修改可调整) # ========== 华为大模型API配置当 AI_PROVIDER=huawei 时使用) ==========
HUAWEI_API_ENDPOINT=http://10.100.31.26:3001/v1/chat/completions
HUAWEI_API_KEY=sk-PoeiV3qwyTIRqcVc84E8E24cD2904872859a87922e0d9186
HUAWEI_MODEL=DeepSeek-R1-Distill-Llama-70B
HUAWEI_API_TIMEOUT=180
HUAWEI_API_MAX_TOKENS=12000
# ========== 硅基流动API配置当 AI_PROVIDER=siliconflow 时使用) ==========
SILICONFLOW_URL=https://api.siliconflow.cn/v1/chat/completions
SILICONFLOW_API_KEY=your_siliconflow_api_key_here
SILICONFLOW_MODEL=deepseek-ai/DeepSeek-V3.2-Exp
SILICONFLOW_API_TIMEOUT=120
SILICONFLOW_API_MAX_TOKENS=2000
# ========== 数据库配置 ==========
DB_HOST=152.136.177.240 DB_HOST=152.136.177.240
DB_PORT=5012 DB_PORT=5012
DB_USER=finyx DB_USER=finyx
@ -85,6 +103,41 @@ DB_PASSWORD=6QsGK6MpePZDE57Z
DB_NAME=finyx DB_NAME=finyx
``` ```
**AI服务提供商选择说明**
- **华为大模型**:设置 `AI_PROVIDER=huawei`,并配置 `HUAWEI_API_KEY``HUAWEI_API_ENDPOINT`
- **硅基流动**:设置 `AI_PROVIDER=siliconflow`(默认值),并配置 `SILICONFLOW_API_KEY`
如果配置的AI服务不完整系统会自动尝试使用另一个可用的服务。
**华为大模型API调用示例**
```bash
curl --location --request POST 'http://10.100.31.26:3001/v1/chat/completions' \
--header 'Authorization: Bearer sk-PoeiV3qwyTIRqcVc84E8E24cD2904872859a87922e0d9186' \
--header 'Content-Type: application/json' \
--data-raw '{
"model": "DeepSeek-R1-Distill-Llama-70B",
"messages": [
{
"role": "user",
"content": "介绍一下山西的营商环境,推荐适合什么行业经营"
}
],
"stream": false,
"presence_penalty": 1.03,
"frequency_penalty": 1.0,
"repetition_penalty": 1.0,
"temperature": 0.5,
"top_p": 0.95,
"top_k": 1,
"seed": 1,
"max_tokens": 8192,
"n": 2,
"best_of": 2
}'
```
### 3. 启动服务 ### 3. 启动服务
```bash ```bash
@ -243,7 +296,7 @@ print(response.json())
## 常见问题 ## 常见问题
**Q: 提示"未配置AI服务"** **Q: 提示"未配置AI服务"**
A: 检查 `.env` 文件中的 `SILICONFLOW_API_KEY` 是否已正确配置。 A: 系统仅支持华为大模型(已内置默认配置),请确保 `.env` 文件中正确设置了 `HUAWEI_API_KEY``HUAWEI_API_ENDPOINT`。如果华为大模型不可用请检查网络连接和API配置。
**Q: 解析结果为空?** **Q: 解析结果为空?**
A: 检查输入文本是否包含足够的信息,可以尝试更详细的输入文本。 A: 检查输入文本是否包含足够的信息,可以尝试更详细的输入文本。

Binary file not shown.

View File

@ -0,0 +1,582 @@
"""
分析和修复字段编码问题
1. 分析f_polic_file_field表中的重复项
2. 检查f_polic_field表中的中文field_code
3. 根据占位符与字段对照表更新field_code
4. 合并重复项并更新关联表
"""
import os
import json
import pymysql
import re
from typing import Dict, List, Optional, Tuple
from datetime import datetime
from pathlib import Path
# 数据库连接配置
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'
}
TENANT_ID = 615873064429507639
CREATED_BY = 655162080928945152
UPDATED_BY = 655162080928945152
CURRENT_TIME = datetime.now()
# 从占位符与字段对照表文档中提取的字段映射
# 格式: {字段名称: field_code}
FIELD_NAME_TO_CODE_MAPPING = {
# 基本信息字段
'被核查人姓名': 'target_name',
'被核查人员单位及职务': 'target_organization_and_position',
'被核查人员单位': 'target_organization',
'被核查人员职务': 'target_position',
'被核查人员性别': 'target_gender',
'被核查人员出生年月': 'target_date_of_birth',
'被核查人员出生年月日': 'target_date_of_birth_full',
'被核查人员年龄': 'target_age',
'被核查人员文化程度': 'target_education_level',
'被核查人员政治面貌': 'target_political_status',
'被核查人员职级': 'target_professional_rank',
'被核查人员身份证号': 'target_id_number',
'被核查人员身份证件及号码': 'target_id_number',
'被核查人员住址': 'target_address',
'被核查人员户籍住址': 'target_registered_address',
'被核查人员联系方式': 'target_contact',
'被核查人员籍贯': 'target_place_of_origin',
'被核查人员民族': 'target_ethnicity',
# 问题相关字段
'线索来源': 'clue_source',
'主要问题线索': 'target_issue_description',
'被核查人问题描述': 'target_problem_description',
# 审批相关字段
'初步核实审批表承办部门意见': 'department_opinion',
'初步核实审批表填表人': 'filler_name',
'批准时间': 'approval_time',
# 核查相关字段
'核查单位名称': 'investigation_unit_name',
'核查组代号': 'investigation_team_code',
'核查组组长姓名': 'investigation_team_leader_name',
'核查组成员姓名': 'investigation_team_member_names',
'核查地点': 'investigation_location',
# 风险评估相关字段
'被核查人员家庭情况': 'target_family_situation',
'被核查人员社会关系': 'target_social_relations',
'被核查人员健康状况': 'target_health_status',
'被核查人员性格特征': 'target_personality',
'被核查人员承受能力': 'target_tolerance',
'被核查人员涉及问题严重程度': 'target_issue_severity',
'被核查人员涉及其他问题的可能性': 'target_other_issues_possibility',
'被核查人员此前被审查情况': 'target_previous_investigation',
'被核查人员社会负面事件': 'target_negative_events',
'被核查人员其他情况': 'target_other_situation',
'风险等级': 'risk_level',
# 其他字段
'线索信息': 'clue_info',
'被核查人员工作基本情况线索': 'target_basic_info_clue',
'被核查人员工作基本情况': 'target_work_basic_info',
'请示报告卡请示时间': 'report_card_request_time',
'应到时间': 'appointment_time',
'应到地点': 'appointment_location',
'承办部门': 'handling_department',
'承办人': 'handler_name',
'谈话通知时间': 'notification_time',
'谈话通知地点': 'notification_location',
'被核查人员本人认识和态度': 'target_attitude',
'纪委名称': 'commission_name',
}
def is_chinese(text: str) -> bool:
"""判断字符串是否包含中文字符"""
if not text:
return False
return bool(re.search(r'[\u4e00-\u9fff]', text))
def analyze_f_polic_field(conn) -> Dict:
"""分析f_polic_field表找出中文field_code和重复项"""
cursor = conn.cursor(pymysql.cursors.DictCursor)
print("\n" + "="*80)
print("1. 分析 f_polic_field 表")
print("="*80)
# 查询所有字段
cursor.execute("""
SELECT id, name, filed_code, field_type, state
FROM f_polic_field
WHERE tenant_id = %s
ORDER BY name, filed_code
""", (TENANT_ID,))
fields = cursor.fetchall()
print(f"\n总共找到 {len(fields)} 个字段记录")
# 找出中文field_code
chinese_field_codes = []
for field in fields:
if is_chinese(field['filed_code']):
chinese_field_codes.append(field)
print(f"\n发现 {len(chinese_field_codes)} 个中文field_code:")
for field in chinese_field_codes:
print(f" - ID: {field['id']}, 名称: {field['name']}, field_code: {field['filed_code']}")
# 找出重复的字段名称
name_to_fields = {}
for field in fields:
name = field['name']
if name not in name_to_fields:
name_to_fields[name] = []
name_to_fields[name].append(field)
duplicates = {name: fields_list for name, fields_list in name_to_fields.items()
if len(fields_list) > 1}
print(f"\n发现 {len(duplicates)} 个重复的字段名称:")
for name, fields_list in duplicates.items():
print(f"\n 字段名称: {name} (共 {len(fields_list)} 条记录)")
for field in fields_list:
print(f" - ID: {field['id']}, field_code: {field['filed_code']}, "
f"field_type: {field['field_type']}, state: {field['state']}")
# 找出重复的field_code
code_to_fields = {}
for field in fields:
code = field['filed_code']
if code not in code_to_fields:
code_to_fields[code] = []
code_to_fields[code].append(field)
duplicate_codes = {code: fields_list for code, fields_list in code_to_fields.items()
if len(fields_list) > 1}
print(f"\n发现 {len(duplicate_codes)} 个重复的field_code:")
for code, fields_list in duplicate_codes.items():
print(f"\n field_code: {code} (共 {len(fields_list)} 条记录)")
for field in fields_list:
print(f" - ID: {field['id']}, 名称: {field['name']}, "
f"field_type: {field['field_type']}, state: {field['state']}")
return {
'all_fields': fields,
'chinese_field_codes': chinese_field_codes,
'duplicate_names': duplicates,
'duplicate_codes': duplicate_codes
}
def analyze_f_polic_file_field(conn) -> Dict:
"""分析f_polic_file_field表找出重复项"""
cursor = conn.cursor(pymysql.cursors.DictCursor)
print("\n" + "="*80)
print("2. 分析 f_polic_file_field 表")
print("="*80)
# 查询所有关联关系
cursor.execute("""
SELECT fff.id, fff.file_id, fff.filed_id,
fc.name as file_name, f.name as field_name, f.filed_code
FROM f_polic_file_field fff
LEFT JOIN f_polic_file_config fc ON fff.file_id = fc.id
LEFT JOIN f_polic_field f ON fff.filed_id = f.id
WHERE fff.tenant_id = %s
ORDER BY fff.file_id, fff.filed_id
""", (TENANT_ID,))
relations = cursor.fetchall()
print(f"\n总共找到 {len(relations)} 个关联关系")
# 找出重复的关联关系相同的file_id和filed_id
relation_key_to_records = {}
for rel in relations:
key = (rel['file_id'], rel['filed_id'])
if key not in relation_key_to_records:
relation_key_to_records[key] = []
relation_key_to_records[key].append(rel)
duplicates = {key: records for key, records in relation_key_to_records.items()
if len(records) > 1}
print(f"\n发现 {len(duplicates)} 个重复的关联关系:")
for (file_id, filed_id), records in duplicates.items():
print(f"\n 文件ID: {file_id}, 字段ID: {filed_id} (共 {len(records)} 条记录)")
for record in records:
print(f" - 关联ID: {record['id']}, 文件: {record['file_name']}, "
f"字段: {record['field_name']} ({record['filed_code']})")
# 统计使用中文field_code的关联关系
chinese_relations = [rel for rel in relations if rel['filed_code'] and is_chinese(rel['filed_code'])]
print(f"\n发现 {len(chinese_relations)} 个使用中文field_code的关联关系:")
for rel in chinese_relations[:10]: # 只显示前10个
print(f" - 文件: {rel['file_name']}, 字段: {rel['field_name']}, "
f"field_code: {rel['filed_code']}")
if len(chinese_relations) > 10:
print(f" ... 还有 {len(chinese_relations) - 10}")
return {
'all_relations': relations,
'duplicate_relations': duplicates,
'chinese_relations': chinese_relations
}
def get_correct_field_code(field_name: str, current_code: str) -> Optional[str]:
"""根据字段名称获取正确的field_code"""
# 首先从映射表中查找
if field_name in FIELD_NAME_TO_CODE_MAPPING:
return FIELD_NAME_TO_CODE_MAPPING[field_name]
# 如果当前code已经是英文且符合规范保留
if current_code and not is_chinese(current_code) and re.match(r'^[a-z_]+$', current_code):
return current_code
return None
def fix_f_polic_field(conn, dry_run: bool = True) -> Dict:
"""修复f_polic_field表中的问题"""
cursor = conn.cursor(pymysql.cursors.DictCursor)
print("\n" + "="*80)
print("3. 修复 f_polic_field 表")
print("="*80)
if dry_run:
print("\n[DRY RUN模式 - 不会实际修改数据库]")
# 获取所有字段
cursor.execute("""
SELECT id, name, filed_code, field_type, state
FROM f_polic_field
WHERE tenant_id = %s
""", (TENANT_ID,))
fields = cursor.fetchall()
updates = []
merges = []
# 按字段名称分组,找出需要合并的重复项
name_to_fields = {}
for field in fields:
name = field['name']
if name not in name_to_fields:
name_to_fields[name] = []
name_to_fields[name].append(field)
# 处理每个字段名称
for field_name, field_list in name_to_fields.items():
if len(field_list) == 1:
# 单个字段检查是否需要更新field_code
field = field_list[0]
correct_code = get_correct_field_code(field['name'], field['filed_code'])
if correct_code and correct_code != field['filed_code']:
updates.append({
'id': field['id'],
'name': field['name'],
'old_code': field['filed_code'],
'new_code': correct_code,
'field_type': field['field_type']
})
else:
# 多个字段,需要合并
# 找出最佳的field_code
best_field = None
best_code = None
for field in field_list:
correct_code = get_correct_field_code(field['name'], field['filed_code'])
if correct_code:
if not best_field or (field['state'] == 1 and best_field['state'] == 0):
best_field = field
best_code = correct_code
# 如果没找到最佳字段,选择第一个启用的,或者第一个
if not best_field:
enabled_fields = [f for f in field_list if f['state'] == 1]
best_field = enabled_fields[0] if enabled_fields else field_list[0]
best_code = get_correct_field_code(best_field['name'], best_field['filed_code'])
if not best_code:
# 生成一个基于名称的code
best_code = field_name.lower().replace('被核查人员', 'target_').replace('被核查人', 'target_')
best_code = re.sub(r'[^\w]', '_', best_code)
best_code = re.sub(r'_+', '_', best_code).strip('_')
# 确定要保留的字段和要删除的字段
keep_field = best_field
remove_fields = [f for f in field_list if f['id'] != keep_field['id']]
# 更新保留字段的field_code
if best_code and best_code != keep_field['filed_code']:
updates.append({
'id': keep_field['id'],
'name': keep_field['name'],
'old_code': keep_field['filed_code'],
'new_code': best_code,
'field_type': keep_field['field_type']
})
merges.append({
'keep_field_id': keep_field['id'],
'keep_field_name': keep_field['name'],
'keep_field_code': best_code or keep_field['filed_code'],
'remove_field_ids': [f['id'] for f in remove_fields],
'remove_fields': remove_fields
})
# 显示更新计划
print(f"\n需要更新 {len(updates)} 个字段的field_code:")
for update in updates:
print(f" - ID: {update['id']}, 名称: {update['name']}, "
f"{update['old_code']} -> {update['new_code']}")
print(f"\n需要合并 {len(merges)} 组重复字段:")
for merge in merges:
print(f"\n 保留字段: ID={merge['keep_field_id']}, 名称={merge['keep_field_name']}, "
f"field_code={merge['keep_field_code']}")
print(f" 删除字段: {len(merge['remove_field_ids'])}")
for remove_field in merge['remove_fields']:
print(f" - ID: {remove_field['id']}, field_code: {remove_field['filed_code']}, "
f"field_type: {remove_field['field_type']}, state: {remove_field['state']}")
# 执行更新
if not dry_run:
print("\n开始执行更新...")
# 1. 先更新field_code
for update in updates:
cursor.execute("""
UPDATE f_polic_field
SET filed_code = %s, updated_time = %s, updated_by = %s
WHERE id = %s
""", (update['new_code'], CURRENT_TIME, UPDATED_BY, update['id']))
print(f" ✓ 更新字段 ID {update['id']}: {update['old_code']} -> {update['new_code']}")
# 2. 合并重复字段:先更新关联表,再删除重复字段
for merge in merges:
keep_id = merge['keep_field_id']
for remove_id in merge['remove_field_ids']:
# 更新f_polic_file_field表中的关联
cursor.execute("""
UPDATE f_polic_file_field
SET filed_id = %s, updated_time = %s, updated_by = %s
WHERE filed_id = %s AND tenant_id = %s
""", (keep_id, CURRENT_TIME, UPDATED_BY, remove_id, TENANT_ID))
# 删除重复的字段记录
cursor.execute("""
DELETE FROM f_polic_field
WHERE id = %s AND tenant_id = %s
""", (remove_id, TENANT_ID))
print(f" ✓ 合并字段: 保留 ID {keep_id}, 删除 {len(merge['remove_field_ids'])} 个重复字段")
conn.commit()
print("\n✓ 更新完成")
else:
print("\n[DRY RUN] 以上操作不会实际执行")
return {
'updates': updates,
'merges': merges
}
def fix_f_polic_file_field(conn, dry_run: bool = True) -> Dict:
"""修复f_polic_file_field表中的重复项"""
cursor = conn.cursor(pymysql.cursors.DictCursor)
print("\n" + "="*80)
print("4. 修复 f_polic_file_field 表")
print("="*80)
if dry_run:
print("\n[DRY RUN模式 - 不会实际修改数据库]")
# 找出重复的关联关系
cursor.execute("""
SELECT file_id, filed_id, COUNT(*) as count, GROUP_CONCAT(id) as ids
FROM f_polic_file_field
WHERE tenant_id = %s
GROUP BY file_id, filed_id
HAVING count > 1
""", (TENANT_ID,))
duplicates = cursor.fetchall()
print(f"\n发现 {len(duplicates)} 组重复的关联关系")
deletes = []
for dup in duplicates:
file_id = dup['file_id']
filed_id = dup['filed_id']
ids = [int(id_str) for id_str in dup['ids'].split(',')]
# 保留第一个,删除其他的
keep_id = ids[0]
remove_ids = ids[1:]
deletes.append({
'file_id': file_id,
'filed_id': filed_id,
'keep_id': keep_id,
'remove_ids': remove_ids
})
print(f"\n 文件ID: {file_id}, 字段ID: {filed_id}")
print(f" 保留关联ID: {keep_id}")
print(f" 删除关联ID: {', '.join(map(str, remove_ids))}")
# 执行删除
if not dry_run:
print("\n开始删除重复的关联关系...")
for delete in deletes:
for remove_id in delete['remove_ids']:
cursor.execute("""
DELETE FROM f_polic_file_field
WHERE id = %s AND tenant_id = %s
""", (remove_id, TENANT_ID))
print(f" ✓ 删除文件ID {delete['file_id']} 和字段ID {delete['filed_id']} 的重复关联")
conn.commit()
print("\n✓ 删除完成")
else:
print("\n[DRY RUN] 以上操作不会实际执行")
return {
'deletes': deletes
}
def check_other_tables(conn):
"""检查其他可能受影响的表"""
cursor = conn.cursor(pymysql.cursors.DictCursor)
print("\n" + "="*80)
print("5. 检查其他关联表")
print("="*80)
# 检查f_polic_task表
print("\n检查 f_polic_task 表...")
try:
cursor.execute("""
SELECT COUNT(*) as count
FROM f_polic_task
WHERE tenant_id = %s
""", (TENANT_ID,))
task_count = cursor.fetchone()['count']
print(f" 找到 {task_count} 个任务记录")
# 检查是否有引用字段ID的列
cursor.execute("DESCRIBE f_polic_task")
columns = [col['Field'] for col in cursor.fetchall()]
print(f" 表字段: {', '.join(columns)}")
# 检查是否有引用f_polic_field的字段
field_refs = [col for col in columns if 'field' in col.lower() or 'filed' in col.lower()]
if field_refs:
print(f" 可能引用字段的列: {', '.join(field_refs)}")
except Exception as e:
print(f" 检查f_polic_task表时出错: {e}")
# 检查f_polic_file表
print("\n检查 f_polic_file 表...")
try:
cursor.execute("""
SELECT COUNT(*) as count
FROM f_polic_file
WHERE tenant_id = %s
""", (TENANT_ID,))
file_count = cursor.fetchone()['count']
print(f" 找到 {file_count} 个文件记录")
cursor.execute("DESCRIBE f_polic_file")
columns = [col['Field'] for col in cursor.fetchall()]
print(f" 表字段: {', '.join(columns)}")
except Exception as e:
print(f" 检查f_polic_file表时出错: {e}")
def main():
"""主函数"""
print("="*80)
print("字段编码问题分析和修复工具")
print("="*80)
try:
conn = pymysql.connect(**DB_CONFIG)
# 1. 分析f_polic_field表
field_analysis = analyze_f_polic_field(conn)
# 2. 分析f_polic_file_field表
relation_analysis = analyze_f_polic_file_field(conn)
# 3. 检查其他表
check_other_tables(conn)
# 4. 询问是否执行修复
print("\n" + "="*80)
print("分析完成")
print("="*80)
print("\n是否执行修复?")
print("1. 先执行DRY RUN不实际修改数据库")
print("2. 直接执行修复(会修改数据库)")
print("3. 仅查看分析结果,不执行修复")
choice = input("\n请选择 (1/2/3默认1): ").strip() or "1"
if choice == "1":
# DRY RUN
print("\n" + "="*80)
print("执行DRY RUN...")
print("="*80)
fix_f_polic_field(conn, dry_run=True)
fix_f_polic_file_field(conn, dry_run=True)
print("\n" + "="*80)
confirm = input("DRY RUN完成。是否执行实际修复(y/n默认n): ").strip().lower()
if confirm == 'y':
print("\n执行实际修复...")
fix_f_polic_field(conn, dry_run=False)
fix_f_polic_file_field(conn, dry_run=False)
print("\n✓ 修复完成!")
elif choice == "2":
# 直接执行
print("\n" + "="*80)
print("执行修复...")
print("="*80)
fix_f_polic_field(conn, dry_run=False)
fix_f_polic_file_field(conn, dry_run=False)
print("\n✓ 修复完成!")
else:
print("\n仅查看分析结果,未执行修复")
conn.close()
except Exception as e:
print(f"\n✗ 执行失败: {e}")
import traceback
traceback.print_exc()
if __name__ == '__main__':
main()

View File

@ -0,0 +1,555 @@
"""
分析和更新模板树状结构
根据 template_finish 目录结构规划树状层级并更新数据库中的 parent_id 字段
"""
import os
import json
import pymysql
from pathlib import Path
from typing import Dict, List, Optional, Tuple
from datetime import datetime
# 数据库连接配置
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'
}
TENANT_ID = 615873064429507639
CREATED_BY = 655162080928945152
UPDATED_BY = 655162080928945152
CURRENT_TIME = datetime.now()
# 项目根目录
PROJECT_ROOT = Path(__file__).parent
TEMPLATES_DIR = PROJECT_ROOT / "template_finish"
# 从 init_all_templates.py 复制的文档类型映射
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"
},
"4.点对点交接单2": {
"template_code": "HANDOVER_FORM_2",
"name": "4.点对点交接单2",
"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 scan_directory_structure(base_dir: Path) -> Dict:
"""
扫描目录结构构建树状层级
Returns:
包含目录和文件层级结构的字典
"""
structure = {
'directories': {}, # {path: {'name': ..., 'parent': ..., 'level': ...}}
'files': {} # {file_path: {'name': ..., 'parent': ..., 'template_code': ...}}
}
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)
structure['files'][str(path)] = {
'name': file_name,
'parent': parent_path,
'level': level,
'template_code': doc_config['template_code'] if doc_config else None,
'full_path': str(path)
}
elif path.is_dir():
# 处理目录
dir_name = path.name
structure['directories'][str(path)] = {
'name': dir_name,
'parent': parent_path,
'level': level
}
# 递归处理子目录和文件
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 structure
def get_existing_data(conn) -> Dict:
"""
获取数据库中的现有数据
Returns:
{
'by_id': {id: {...}},
'by_name': {name: {...}},
'by_template_code': {template_code: {...}}
}
"""
cursor = conn.cursor(pymysql.cursors.DictCursor)
sql = """
SELECT id, name, parent_id, template_code, input_data, file_path, state
FROM f_polic_file_config
WHERE tenant_id = %s
"""
cursor.execute(sql, (TENANT_ID,))
configs = cursor.fetchall()
result = {
'by_id': {},
'by_name': {},
'by_template_code': {}
}
for config in configs:
config_id = config['id']
config_name = config['name']
# 尝试从 input_data 中提取 template_code
template_code = config.get('template_code')
if not template_code and config.get('input_data'):
try:
input_data = json.loads(config['input_data']) if isinstance(config['input_data'], str) else config['input_data']
if isinstance(input_data, dict):
template_code = input_data.get('template_code')
except:
pass
result['by_id'][config_id] = config
result['by_name'][config_name] = config
if template_code:
# 如果已存在相同 template_code保留第一个
if template_code not in result['by_template_code']:
result['by_template_code'][template_code] = config
cursor.close()
return result
def analyze_structure():
"""分析目录结构和数据库数据"""
print("="*80)
print("分析模板目录结构和数据库数据")
print("="*80)
# 连接数据库
try:
conn = pymysql.connect(**DB_CONFIG)
print("✓ 数据库连接成功\n")
except Exception as e:
print(f"✗ 数据库连接失败: {e}")
return None, None
# 扫描目录结构
print("扫描目录结构...")
dir_structure = scan_directory_structure(TEMPLATES_DIR)
print(f" 找到 {len(dir_structure['directories'])} 个目录")
print(f" 找到 {len(dir_structure['files'])} 个文件\n")
# 获取数据库现有数据
print("获取数据库现有数据...")
existing_data = get_existing_data(conn)
print(f" 数据库中有 {len(existing_data['by_id'])} 条记录\n")
# 分析缺少 parent_id 的记录
print("分析缺少 parent_id 的记录...")
missing_parent = []
for config in existing_data['by_id'].values():
if config.get('parent_id') is None:
missing_parent.append(config)
print(f"{len(missing_parent)} 条记录缺少 parent_id\n")
conn.close()
return dir_structure, existing_data
def plan_tree_structure(dir_structure: Dict, existing_data: Dict) -> List[Dict]:
"""
规划树状结构
Returns:
更新计划列表每个元素包含
{
'type': 'directory' | 'file',
'name': ...,
'parent_name': ...,
'level': ...,
'action': 'create' | 'update',
'config_id': ... (如果是更新),
'template_code': ... (如果是文件)
}
"""
plan = []
# 按层级排序目录
directories = sorted(dir_structure['directories'].items(),
key=lambda x: (x[1]['level'], x[0]))
# 按层级排序文件
files = sorted(dir_structure['files'].items(),
key=lambda x: (x[1]['level'], x[0]))
# 创建目录映射用于查找父目录ID
dir_id_map = {} # {dir_path: config_id}
# 处理目录(按层级顺序)
for dir_path, dir_info in directories:
dir_name = dir_info['name']
parent_path = dir_info['parent']
level = dir_info['level']
# 查找父目录ID
parent_id = None
if parent_path:
parent_id = dir_id_map.get(parent_path)
# 检查数据库中是否已存在
existing = existing_data['by_name'].get(dir_name)
if existing:
# 更新现有记录
plan.append({
'type': 'directory',
'name': dir_name,
'parent_name': dir_structure['directories'].get(parent_path, {}).get('name') if parent_path else None,
'parent_id': parent_id,
'level': level,
'action': 'update',
'config_id': existing['id'],
'current_parent_id': existing.get('parent_id')
})
dir_id_map[dir_path] = existing['id']
else:
# 创建新记录(目录节点)
new_id = generate_id()
plan.append({
'type': 'directory',
'name': dir_name,
'parent_name': dir_structure['directories'].get(parent_path, {}).get('name') if parent_path else None,
'parent_id': parent_id,
'level': level,
'action': 'create',
'config_id': new_id,
'current_parent_id': None
})
dir_id_map[dir_path] = new_id
# 处理文件
for file_path, file_info in files:
file_name = file_info['name']
parent_path = file_info['parent']
level = file_info['level']
template_code = file_info['template_code']
# 查找父目录ID
parent_id = dir_id_map.get(parent_path) if parent_path else None
# 查找数据库中的记录(通过 template_code 或 name
existing = None
if template_code:
existing = existing_data['by_template_code'].get(template_code)
if not existing:
existing = existing_data['by_name'].get(file_name)
if existing:
# 更新现有记录
plan.append({
'type': 'file',
'name': file_name,
'parent_name': dir_structure['directories'].get(parent_path, {}).get('name') if parent_path else None,
'parent_id': parent_id,
'level': level,
'action': 'update',
'config_id': existing['id'],
'template_code': template_code,
'current_parent_id': existing.get('parent_id')
})
else:
# 创建新记录(文件节点)
new_id = generate_id()
plan.append({
'type': 'file',
'name': file_name,
'parent_name': dir_structure['directories'].get(parent_path, {}).get('name') if parent_path else None,
'parent_id': parent_id,
'level': level,
'action': 'create',
'config_id': new_id,
'template_code': template_code,
'current_parent_id': None
})
return plan
def generate_update_sql(plan: List[Dict], output_file: str = 'update_template_tree.sql'):
"""生成更新SQL脚本"""
sql_lines = [
"-- 模板树状结构更新脚本",
f"-- 生成时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}",
"-- 注意:执行前请备份数据库!",
"",
"USE finyx;",
"",
"START TRANSACTION;",
""
]
# 按层级分组
by_level = {}
for item in plan:
level = item['level']
if level not in by_level:
by_level[level] = []
by_level[level].append(item)
# 按层级顺序处理(从顶层到底层)
for level in sorted(by_level.keys()):
sql_lines.append(f"-- ===== 层级 {level} =====")
sql_lines.append("")
for item in by_level[level]:
if item['action'] == 'create':
# 创建新记录
if item['type'] == 'directory':
sql_lines.append(f"-- 创建目录节点: {item['name']}")
sql_lines.append(f"INSERT INTO f_polic_file_config")
sql_lines.append(f" (id, tenant_id, parent_id, name, input_data, file_path, created_time, created_by, updated_time, updated_by, state)")
parent_id_sql = f"{item['parent_id']}" if item['parent_id'] else "NULL"
sql_lines.append(f"VALUES ({item['config_id']}, {TENANT_ID}, {parent_id_sql}, '{item['name']}', NULL, NULL, NOW(), {CREATED_BY}, NOW(), {UPDATED_BY}, 1);")
else:
# 文件节点(需要 template_code
sql_lines.append(f"-- 创建文件节点: {item['name']}")
input_data = json.dumps({
'template_code': item.get('template_code', ''),
'business_type': 'INVESTIGATION'
}, ensure_ascii=False).replace("'", "''")
sql_lines.append(f"INSERT INTO f_polic_file_config")
sql_lines.append(f" (id, tenant_id, parent_id, name, input_data, file_path, template_code, created_time, created_by, updated_time, updated_by, state)")
parent_id_sql = f"{item['parent_id']}" if item['parent_id'] else "NULL"
template_code_sql = f"'{item.get('template_code', '')}'" if item.get('template_code') else "NULL"
sql_lines.append(f"VALUES ({item['config_id']}, {TENANT_ID}, {parent_id_sql}, '{item['name']}', '{input_data}', NULL, {template_code_sql}, NOW(), {CREATED_BY}, NOW(), {UPDATED_BY}, 1);")
sql_lines.append("")
else:
# 更新现有记录
current_parent = item.get('current_parent_id')
new_parent = item.get('parent_id')
if current_parent != new_parent:
sql_lines.append(f"-- 更新: {item['name']} (parent_id: {current_parent} -> {new_parent})")
parent_id_sql = f"{new_parent}" if new_parent else "NULL"
sql_lines.append(f"UPDATE f_polic_file_config")
sql_lines.append(f"SET parent_id = {parent_id_sql}, updated_time = NOW(), updated_by = {UPDATED_BY}")
sql_lines.append(f"WHERE id = {item['config_id']} AND tenant_id = {TENANT_ID};")
sql_lines.append("")
sql_lines.append("COMMIT;")
sql_lines.append("")
sql_lines.append("-- 更新完成")
# 写入文件
with open(output_file, 'w', encoding='utf-8') as f:
f.write('\n'.join(sql_lines))
print(f"✓ SQL脚本已生成: {output_file}")
return output_file
def print_analysis_report(dir_structure: Dict, existing_data: Dict, plan: List[Dict]):
"""打印分析报告"""
print("\n" + "="*80)
print("分析报告")
print("="*80)
print(f"\n目录结构:")
print(f" - 目录数量: {len(dir_structure['directories'])}")
print(f" - 文件数量: {len(dir_structure['files'])}")
print(f"\n数据库现状:")
print(f" - 总记录数: {len(existing_data['by_id'])}")
missing_parent = sum(1 for c in existing_data['by_id'].values() if c.get('parent_id') is None)
print(f" - 缺少 parent_id 的记录: {missing_parent}")
print(f"\n更新计划:")
create_count = sum(1 for p in plan if p['action'] == 'create')
update_count = sum(1 for p in plan if p['action'] == 'update')
print(f" - 需要创建: {create_count}")
print(f" - 需要更新: {update_count}")
print(f"\n层级分布:")
by_level = {}
for item in plan:
level = item['level']
by_level[level] = by_level.get(level, 0) + 1
for level in sorted(by_level.keys()):
print(f" - 层级 {level}: {by_level[level]} 个节点")
print("\n" + "="*80)
def main():
"""主函数"""
# 分析
dir_structure, existing_data = analyze_structure()
if not dir_structure or not existing_data:
return
# 规划树状结构
print("规划树状结构...")
plan = plan_tree_structure(dir_structure, existing_data)
print(f" 生成 {len(plan)} 个更新计划\n")
# 打印报告
print_analysis_report(dir_structure, existing_data, plan)
# 生成SQL脚本
print("\n生成SQL更新脚本...")
sql_file = generate_update_sql(plan)
print("\n" + "="*80)
print("分析完成!")
print("="*80)
print(f"\n请检查生成的SQL脚本: {sql_file}")
print("确认无误后,可以执行该脚本更新数据库。")
print("\n注意:执行前请备份数据库!")
if __name__ == '__main__':
main()

250
app.py
View File

@ -5,6 +5,7 @@ from flask import Flask, request, jsonify, send_from_directory
from flask_cors import CORS from flask_cors import CORS
from flasgger import Swagger from flasgger import Swagger
import os import os
import pymysql
from datetime import datetime from datetime import datetime
from dotenv import load_dotenv from dotenv import load_dotenv
@ -254,6 +255,12 @@ def extract():
if not ai_result: if not ai_result:
return error_response(2002, "AI解析失败请检查输入文本质量") return error_response(2002, "AI解析失败请检查输入文本质量")
# 调试打印AI返回的结果
print(f"[API] AI返回结果包含 {len(ai_result)} 个字段")
for key in ['target_name', 'target_gender', 'target_age', 'target_date_of_birth']:
if key in ai_result:
print(f"[API] AI返回 {key} = '{ai_result[key]}'")
# 构建返回数据按照outputData中的字段顺序返回 # 构建返回数据按照outputData中的字段顺序返回
out_data = [] out_data = []
# 创建一个字段编码到字段信息的映射 # 创建一个字段编码到字段信息的映射
@ -264,6 +271,9 @@ def extract():
# 默认值信息在文档中说明,由前端根据业务需求决定是否应用 # 默认值信息在文档中说明,由前端根据业务需求决定是否应用
for field_code in output_field_codes: for field_code in output_field_codes:
field_value = ai_result.get(field_code, '') field_value = ai_result.get(field_code, '')
# 调试:打印关键字段的映射
if field_code in ['target_name', 'target_gender', 'target_age']:
print(f"[API] 构建返回数据: {field_code} = '{field_value}' (从ai_result获取)")
out_data.append({ out_data.append({
'fieldCode': field_code, 'fieldCode': field_code,
'fieldValue': field_value 'fieldValue': field_value
@ -275,6 +285,85 @@ def extract():
return error_response(2001, f"AI解析超时或发生错误: {str(e)}") return error_response(2001, f"AI解析超时或发生错误: {str(e)}")
@app.route('/api/file-configs', methods=['GET'])
def get_file_configs():
"""
获取可用的文件配置列表
用于查询可用的fileId供文档生成接口使用
---
tags:
- 字段配置
summary: 获取文件配置列表
description: 返回所有启用的文件配置包含fileId和文件名称
responses:
200:
description: 成功
schema:
type: object
properties:
code:
type: integer
example: 0
data:
type: object
properties:
fileConfigs:
type: array
items:
type: object
properties:
fileId:
type: integer
description: 文件配置ID
example: 1765273961563507
fileName:
type: string
description: 文件名称
example: 1.请示报告卡XXX
filePath:
type: string
description: MinIO文件路径
example: /615873064429507639/TEMPLATE/2025/12/1.请示报告卡XXX.docx
isSuccess:
type: boolean
example: true
"""
try:
conn = document_service.get_connection()
cursor = conn.cursor(pymysql.cursors.DictCursor)
try:
sql = """
SELECT id, name, file_path
FROM f_polic_file_config
WHERE tenant_id = %s
AND state = 1
ORDER BY name
"""
cursor.execute(sql, (document_service.tenant_id,))
configs = cursor.fetchall()
file_configs = []
for config in configs:
file_configs.append({
'fileId': config['id'],
'fileName': config['name'],
'filePath': config['file_path'] or ''
})
return success_response({
'fileConfigs': file_configs
})
finally:
cursor.close()
conn.close()
except Exception as e:
return error_response(500, f"查询文件配置失败: {str(e)}")
@app.route('/api/fields', methods=['GET']) @app.route('/api/fields', methods=['GET'])
def get_fields(): def get_fields():
""" """
@ -431,19 +520,17 @@ def generate_document():
description: 文件列表 description: 文件列表
items: items:
type: object type: object
required:
- fileId
properties: properties:
fileId: fileId:
type: integer type: integer
description: 文件ID description: 文件配置ID从f_polic_file_config表获取
example: 1 example: 1765273961563507
fileName: fileName:
type: string type: string
description: 文件名称 description: 文件名称可选用于生成文档名称
example: 请示报告卡.doc example: 请示报告卡.doc
templateCode:
type: string
description: 模板编码
example: REPORT_CARD
responses: responses:
200: 200:
description: 生成成功 description: 生成成功
@ -490,7 +577,7 @@ def generate_document():
type: boolean type: boolean
example: true example: true
1001: 1001:
description: 模板不存在 description: 模板不存在或参数错误
schema: schema:
type: object type: object
properties: properties:
@ -499,7 +586,7 @@ def generate_document():
example: 1001 example: 1001
errorMsg: errorMsg:
type: string type: string
example: 模板不存在 example: 文件ID对应的模板不存在或未启用
isSuccess: isSuccess:
type: boolean type: boolean
example: false example: false
@ -564,17 +651,17 @@ def generate_document():
first_document_name = None # 用于存储第一个生成的文档名 first_document_name = None # 用于存储第一个生成的文档名
for file_info in file_list: for file_info in file_list:
file_id = file_info.get('fileId') # 兼容 id 和 fileId 两种字段
file_name = file_info.get('fileName', '') file_id = file_info.get('fileId') or file_info.get('id')
template_code = file_info.get('templateCode', '') file_name = file_info.get('fileName') or file_info.get('name', '')
if not template_code: if not file_id:
return error_response(1001, f"文件 {file_name} 缺少templateCode参数") return error_response(1001, f"文件 {file_name} 缺少fileId或id参数")
try: try:
# 生成文档 # 生成文档使用fileId而不是templateCode
result = document_service.generate_document( result = document_service.generate_document(
template_code=template_code, file_id=file_id,
input_data=input_data, input_data=input_data,
file_info=file_info file_info=file_info
) )
@ -614,6 +701,137 @@ def generate_document():
return error_response(3001, f"文档生成失败: {str(e)}") return error_response(3001, f"文档生成失败: {str(e)}")
<<<<<<< HEAD
@app.route('/fPolicTask/getDocument', methods=['POST'])
def get_document_by_task():
"""
通过taskId获取文档兼容接口
支持通过taskId查询关联的文件列表或直接使用提供的文件列表
"""
try:
data = request.get_json()
# 验证请求参数
if not data:
return error_response(400, "请求参数不能为空")
task_id = data.get('taskId')
input_data = data.get('inputData', [])
file_list = data.get('fpolicFieldParamFileList', [])
# 如果没有提供file_list尝试通过taskId查询
if not file_list and task_id:
try:
conn = document_service.get_connection()
cursor = conn.cursor(pymysql.cursors.DictCursor)
try:
# 尝试从f_polic_task表查询关联的文件列表
# 注意这里需要根据实际表结构调整SQL
sql = """
SELECT file_id, file_name
FROM f_polic_task_file
WHERE task_id = %s
AND tenant_id = %s
AND state = 1
"""
cursor.execute(sql, (task_id, document_service.tenant_id))
task_files = cursor.fetchall()
if task_files:
file_list = []
for tf in task_files:
file_list.append({
'fileId': tf['file_id'],
'fileName': tf.get('file_name', '')
})
except Exception as e:
# 如果表不存在或查询失败,记录日志但不报错
print(f"[WARN] 无法通过taskId查询文件列表: {str(e)}")
finally:
cursor.close()
conn.close()
except Exception as e:
print(f"[WARN] 查询taskId关联文件时出错: {str(e)}")
# 如果仍然没有file_list返回错误
if not file_list:
return error_response(400, "缺少fpolicFieldParamFileList参数且无法通过taskId查询到关联文件。请提供fpolicFieldParamFileList参数格式: [{'fileId': 文件ID, 'fileName': '文件名'}]")
if not input_data or not isinstance(input_data, list):
return error_response(400, "inputData参数必须是非空数组")
if not file_list or not isinstance(file_list, list):
return error_response(400, "fpolicFieldParamFileList参数必须是非空数组")
# 将input_data转换为字典格式用于生成文档名称
field_data = {}
for item in input_data:
field_code = item.get('fieldCode', '')
field_value = item.get('fieldValue', '')
if field_code:
field_data[field_code] = field_value or ''
# 生成文档ID
document_id = document_service.generate_document_id()
# 处理每个文件
result_file_list = []
first_document_name = None # 用于存储第一个生成的文档名
for file_info in file_list:
# 兼容 id 和 fileId 两种字段
file_id = file_info.get('fileId') or file_info.get('id')
file_name = file_info.get('fileName') or file_info.get('name', '')
if not file_id:
return error_response(1001, f"文件 {file_name} 缺少fileId或id参数")
try:
# 生成文档使用fileId而不是templateCode
result = document_service.generate_document(
file_id=file_id,
input_data=input_data,
file_info=file_info
)
# 使用生成的文档名称(.docx格式而不是原始文件名
generated_file_name = result.get('fileName', file_name)
# 保存第一个文档名作为 documentName
if first_document_name is None:
first_document_name = generated_file_name
result_file_list.append({
'fileId': file_id,
'fileName': generated_file_name, # 使用生成的文档名
'filePath': result['filePath']
})
except Exception as e:
error_msg = str(e)
if '不存在' in error_msg or '模板' in error_msg:
return error_response(1001, error_msg)
elif '生成' in error_msg or '填充' in error_msg:
return error_response(3001, error_msg)
elif '上传' in error_msg or '保存' in error_msg:
return error_response(3002, error_msg)
else:
return error_response(3001, f"文件生成失败: {error_msg}")
# 构建返回数据不包含inputData只返回生成的文档信息
return success_response({
'documentId': document_id,
'documentName': first_document_name or 'generated.docx', # 使用第一个生成的文档名
'fpolicFieldParamFileList': result_file_list
})
except Exception as e:
return error_response(3001, f"文档生成失败: {str(e)}")
=======
>>>>>>> parent of 4897c96 (添加通过taskId获取文档的接口支持文件列表查询和参数验证增强错误处理能力同时优化文档生成逻辑确保生成的文档名称和路径的准确性)
if __name__ == '__main__': if __name__ == '__main__':
# 确保static目录存在 # 确保static目录存在
os.makedirs('static', exist_ok=True) os.makedirs('static', exist_ok=True)

314
backup_database.py Normal file
View File

@ -0,0 +1,314 @@
"""
数据库备份脚本
支持使用mysqldump命令或Python直接导出SQL文件
"""
import os
import sys
import subprocess
import pymysql
from datetime import datetime
from pathlib import Path
from dotenv import load_dotenv
# 加载环境变量
load_dotenv()
class DatabaseBackup:
"""数据库备份类"""
def __init__(self):
"""初始化数据库配置"""
self.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'
}
# 备份文件存储目录
self.backup_dir = Path('backups')
self.backup_dir.mkdir(exist_ok=True)
def backup_with_mysqldump(self, output_file=None, compress=False):
"""
使用mysqldump命令备份数据库推荐方式
Args:
output_file: 输出文件路径如果为None则自动生成
compress: 是否压缩备份文件
Returns:
备份文件路径
"""
# 生成备份文件名
if output_file is None:
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
output_file = self.backup_dir / f"backup_{self.db_config['database']}_{timestamp}.sql"
output_file = Path(output_file)
# 构建mysqldump命令
cmd = [
'mysqldump',
f"--host={self.db_config['host']}",
f"--port={self.db_config['port']}",
f"--user={self.db_config['user']}",
f"--password={self.db_config['password']}",
'--single-transaction', # 保证数据一致性
'--routines', # 包含存储过程和函数
'--triggers', # 包含触发器
'--events', # 包含事件
'--add-drop-table', # 添加DROP TABLE语句
'--default-character-set=utf8mb4', # 设置字符集
self.db_config['database']
]
try:
print(f"开始备份数据库 {self.db_config['database']}...")
print(f"备份文件: {output_file}")
# 执行备份命令
with open(output_file, 'w', encoding='utf-8') as f:
result = subprocess.run(
cmd,
stdout=f,
stderr=subprocess.PIPE,
text=True
)
if result.returncode != 0:
error_msg = result.stderr.decode('utf-8') if result.stderr else '未知错误'
raise Exception(f"mysqldump执行失败: {error_msg}")
# 检查文件大小
file_size = output_file.stat().st_size
print(f"备份完成!文件大小: {file_size / 1024 / 1024:.2f} MB")
# 如果需要压缩
if compress:
compressed_file = self._compress_file(output_file)
print(f"压缩完成: {compressed_file}")
return str(compressed_file)
return str(output_file)
except FileNotFoundError:
print("错误: 未找到mysqldump命令请确保MySQL客户端已安装并在PATH中")
print("尝试使用Python方式备份...")
return self.backup_with_python(output_file)
except Exception as e:
print(f"备份失败: {str(e)}")
raise
def backup_with_python(self, output_file=None):
"""
使用Python直接连接数据库备份备用方式
Args:
output_file: 输出文件路径如果为None则自动生成
Returns:
备份文件路径
"""
if output_file is None:
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
output_file = self.backup_dir / f"backup_{self.db_config['database']}_{timestamp}.sql"
output_file = Path(output_file)
try:
print(f"开始使用Python方式备份数据库 {self.db_config['database']}...")
print(f"备份文件: {output_file}")
# 连接数据库
connection = pymysql.connect(**self.db_config)
cursor = connection.cursor()
with open(output_file, 'w', encoding='utf-8') as f:
# 写入文件头
f.write(f"-- MySQL数据库备份\n")
f.write(f"-- 数据库: {self.db_config['database']}\n")
f.write(f"-- 备份时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n")
f.write(f"-- 主机: {self.db_config['host']}:{self.db_config['port']}\n")
f.write("--\n\n")
f.write(f"SET NAMES utf8mb4;\n")
f.write(f"SET FOREIGN_KEY_CHECKS=0;\n\n")
# 获取所有表
cursor.execute("SHOW TABLES")
tables = [table[0] for table in cursor.fetchall()]
print(f"找到 {len(tables)} 个表")
# 备份每个表
for table in tables:
print(f"备份表: {table}")
# 获取表结构
cursor.execute(f"SHOW CREATE TABLE `{table}`")
create_table_sql = cursor.fetchone()[1]
f.write(f"-- ----------------------------\n")
f.write(f"-- 表结构: {table}\n")
f.write(f"-- ----------------------------\n")
f.write(f"DROP TABLE IF EXISTS `{table}`;\n")
f.write(f"{create_table_sql};\n\n")
# 获取表数据
cursor.execute(f"SELECT * FROM `{table}`")
rows = cursor.fetchall()
if rows:
# 获取列名
cursor.execute(f"DESCRIBE `{table}`")
columns = [col[0] for col in cursor.fetchall()]
f.write(f"-- ----------------------------\n")
f.write(f"-- 表数据: {table}\n")
f.write(f"-- ----------------------------\n")
# 分批写入数据
batch_size = 1000
for i in range(0, len(rows), batch_size):
batch = rows[i:i+batch_size]
values_list = []
for row in batch:
values = []
for value in row:
if value is None:
values.append('NULL')
elif isinstance(value, (int, float)):
values.append(str(value))
else:
# 转义特殊字符
escaped_value = str(value).replace('\\', '\\\\').replace("'", "\\'")
values.append(f"'{escaped_value}'")
values_list.append(f"({', '.join(values)})")
columns_str = ', '.join([f"`{col}`" for col in columns])
values_str = ',\n'.join(values_list)
f.write(f"INSERT INTO `{table}` ({columns_str}) VALUES\n")
f.write(f"{values_str};\n\n")
print(f" 完成: {len(rows)} 条记录")
f.write("SET FOREIGN_KEY_CHECKS=1;\n")
cursor.close()
connection.close()
# 检查文件大小
file_size = output_file.stat().st_size
print(f"备份完成!文件大小: {file_size / 1024 / 1024:.2f} MB")
return str(output_file)
except Exception as e:
print(f"备份失败: {str(e)}")
raise
def _compress_file(self, file_path):
"""
压缩备份文件
Args:
file_path: 文件路径
Returns:
压缩后的文件路径
"""
import gzip
file_path = Path(file_path)
compressed_path = file_path.with_suffix('.sql.gz')
with open(file_path, 'rb') as f_in:
with gzip.open(compressed_path, 'wb') as f_out:
f_out.writelines(f_in)
# 删除原文件
file_path.unlink()
return compressed_path
def list_backups(self):
"""
列出所有备份文件
Returns:
备份文件列表
"""
backups = []
for file in sorted(self.backup_dir.glob('backup_*.sql*'), reverse=True):
file_info = {
'filename': file.name,
'path': str(file),
'size': file.stat().st_size,
'size_mb': file.stat().st_size / 1024 / 1024,
'modified': datetime.fromtimestamp(file.stat().st_mtime)
}
backups.append(file_info)
return backups
def main():
"""主函数"""
import argparse
parser = argparse.ArgumentParser(description='数据库备份工具')
parser.add_argument('--method', choices=['mysqldump', 'python', 'auto'],
default='auto', help='备份方法 (默认: auto)')
parser.add_argument('--output', '-o', help='输出文件路径')
parser.add_argument('--compress', '-c', action='store_true',
help='压缩备份文件')
parser.add_argument('--list', '-l', action='store_true',
help='列出所有备份文件')
args = parser.parse_args()
backup = DatabaseBackup()
# 列出备份文件
if args.list:
backups = backup.list_backups()
if backups:
print(f"\n找到 {len(backups)} 个备份文件:\n")
print(f"{'文件名':<50} {'大小(MB)':<15} {'修改时间':<20}")
print("-" * 85)
for b in backups:
print(f"{b['filename']:<50} {b['size_mb']:<15.2f} {b['modified'].strftime('%Y-%m-%d %H:%M:%S'):<20}")
else:
print("未找到备份文件")
return
# 执行备份
try:
if args.method == 'mysqldump':
backup_file = backup.backup_with_mysqldump(args.output, args.compress)
elif args.method == 'python':
backup_file = backup.backup_with_python(args.output)
else: # auto
try:
backup_file = backup.backup_with_mysqldump(args.output, args.compress)
except:
print("\nmysqldump方式失败切换到Python方式...")
backup_file = backup.backup_with_python(args.output)
print(f"\n备份成功!")
print(f"备份文件: {backup_file}")
except Exception as e:
print(f"\n备份失败: {str(e)}")
sys.exit(1)
if __name__ == '__main__':
main()

Binary file not shown.

View File

@ -0,0 +1,551 @@
"""
检查并修复 f_polic_file_field 表的关联关系
1. 检查无效的关联关联到不存在的 file_id filed_id
2. 检查重复的关联关系
3. 检查关联到已删除或未启用的字段/文件
4. 根据其他表的数据更新关联关系
"""
import pymysql
import os
from typing import Dict, List, Tuple
from collections import defaultdict
# 数据库连接配置
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'
}
TENANT_ID = 615873064429507639
def check_invalid_relations(conn) -> Dict:
"""检查无效的关联关系(关联到不存在的 file_id 或 filed_id"""
cursor = conn.cursor(pymysql.cursors.DictCursor)
print("\n" + "="*80)
print("1. 检查无效的关联关系")
print("="*80)
# 检查关联到不存在的 file_id
cursor.execute("""
SELECT fff.id, fff.file_id, fff.filed_id, fff.tenant_id
FROM f_polic_file_field fff
LEFT JOIN f_polic_file_config fc ON fff.file_id = fc.id AND fff.tenant_id = fc.tenant_id
WHERE fff.tenant_id = %s AND fc.id IS NULL
""", (TENANT_ID,))
invalid_file_relations = cursor.fetchall()
# 检查关联到不存在的 filed_id
cursor.execute("""
SELECT fff.id, fff.file_id, fff.filed_id, fff.tenant_id
FROM f_polic_file_field fff
LEFT JOIN f_polic_field f ON fff.filed_id = f.id AND fff.tenant_id = f.tenant_id
WHERE fff.tenant_id = %s AND f.id IS NULL
""", (TENANT_ID,))
invalid_field_relations = cursor.fetchall()
print(f"\n关联到不存在的 file_id: {len(invalid_file_relations)}")
if invalid_file_relations:
print(" 详情:")
for rel in invalid_file_relations[:10]:
print(f" - 关联ID: {rel['id']}, file_id: {rel['file_id']}, filed_id: {rel['filed_id']}")
if len(invalid_file_relations) > 10:
print(f" ... 还有 {len(invalid_file_relations) - 10}")
print(f"\n关联到不存在的 filed_id: {len(invalid_field_relations)}")
if invalid_field_relations:
print(" 详情:")
for rel in invalid_field_relations[:10]:
print(f" - 关联ID: {rel['id']}, file_id: {rel['file_id']}, filed_id: {rel['filed_id']}")
if len(invalid_field_relations) > 10:
print(f" ... 还有 {len(invalid_field_relations) - 10}")
return {
'invalid_file_relations': invalid_file_relations,
'invalid_field_relations': invalid_field_relations
}
def check_duplicate_relations(conn) -> Dict:
"""检查重复的关联关系(相同的 file_id 和 filed_id"""
cursor = conn.cursor(pymysql.cursors.DictCursor)
print("\n" + "="*80)
print("2. 检查重复的关联关系")
print("="*80)
# 查找重复的关联关系
cursor.execute("""
SELECT file_id, filed_id, COUNT(*) as count, GROUP_CONCAT(id ORDER BY id) as ids
FROM f_polic_file_field
WHERE tenant_id = %s
GROUP BY file_id, filed_id
HAVING COUNT(*) > 1
ORDER BY count DESC
""", (TENANT_ID,))
duplicates = cursor.fetchall()
print(f"\n发现 {len(duplicates)} 个重复的关联关系:")
duplicate_details = []
for dup in duplicates:
ids = [int(id_str) for id_str in dup['ids'].split(',')]
duplicate_details.append({
'file_id': dup['file_id'],
'filed_id': dup['filed_id'],
'count': dup['count'],
'ids': ids
})
print(f"\n 文件ID: {dup['file_id']}, 字段ID: {dup['filed_id']} (共 {dup['count']} 条)")
print(f" 关联ID列表: {ids}")
return {
'duplicates': duplicate_details
}
def check_disabled_relations(conn) -> Dict:
"""检查关联到已删除或未启用的字段/文件"""
cursor = conn.cursor(pymysql.cursors.DictCursor)
print("\n" + "="*80)
print("3. 检查关联到已删除或未启用的字段/文件")
print("="*80)
# 检查关联到未启用的文件
cursor.execute("""
SELECT fff.id, fff.file_id, fff.filed_id, fc.name as file_name, fc.state as file_state
FROM f_polic_file_field fff
INNER JOIN f_polic_file_config fc ON fff.file_id = fc.id AND fff.tenant_id = fc.tenant_id
WHERE fff.tenant_id = %s AND fc.state = 0
""", (TENANT_ID,))
disabled_file_relations = cursor.fetchall()
# 检查关联到未启用的字段
cursor.execute("""
SELECT fff.id, fff.file_id, fff.filed_id, f.name as field_name, f.filed_code, f.state as field_state
FROM f_polic_file_field fff
INNER JOIN f_polic_field f ON fff.filed_id = f.id AND fff.tenant_id = f.tenant_id
WHERE fff.tenant_id = %s AND f.state = 0
""", (TENANT_ID,))
disabled_field_relations = cursor.fetchall()
print(f"\n关联到未启用的文件: {len(disabled_file_relations)}")
if disabled_file_relations:
print(" 详情:")
for rel in disabled_file_relations[:10]:
print(f" - 关联ID: {rel['id']}, 文件: {rel['file_name']} (ID: {rel['file_id']})")
if len(disabled_file_relations) > 10:
print(f" ... 还有 {len(disabled_file_relations) - 10}")
print(f"\n关联到未启用的字段: {len(disabled_field_relations)}")
if disabled_field_relations:
print(" 详情:")
for rel in disabled_field_relations[:10]:
print(f" - 关联ID: {rel['id']}, 字段: {rel['field_name']} ({rel['filed_code']}, ID: {rel['filed_id']})")
if len(disabled_field_relations) > 10:
print(f" ... 还有 {len(disabled_field_relations) - 10}")
return {
'disabled_file_relations': disabled_file_relations,
'disabled_field_relations': disabled_field_relations
}
def check_missing_relations(conn) -> Dict:
"""检查应该存在但缺失的关联关系(文件节点应该有输出字段关联)"""
cursor = conn.cursor(pymysql.cursors.DictCursor)
print("\n" + "="*80)
print("4. 检查缺失的关联关系")
print("="*80)
# 获取所有有 template_code 的文件节点(这些应该是文件,不是目录)
cursor.execute("""
SELECT fc.id, fc.name, fc.template_code
FROM f_polic_file_config fc
WHERE fc.tenant_id = %s AND fc.template_code IS NOT NULL AND fc.state = 1
""", (TENANT_ID,))
file_configs = cursor.fetchall()
# 获取所有启用的输出字段
cursor.execute("""
SELECT id, name, filed_code
FROM f_polic_field
WHERE tenant_id = %s AND field_type = 2 AND state = 1
""", (TENANT_ID,))
output_fields = cursor.fetchall()
# 获取现有的关联关系
cursor.execute("""
SELECT file_id, filed_id
FROM f_polic_file_field
WHERE tenant_id = %s
""", (TENANT_ID,))
existing_relations = {(rel['file_id'], rel['filed_id']) for rel in cursor.fetchall()}
print(f"\n文件节点总数: {len(file_configs)}")
print(f"输出字段总数: {len(output_fields)}")
print(f"现有关联关系总数: {len(existing_relations)}")
# 这里不自动创建缺失的关联,因为不是所有文件都需要所有字段
# 只显示统计信息
print("\n注意: 缺失的关联关系需要根据业务逻辑手动创建")
return {
'file_configs': file_configs,
'output_fields': output_fields,
'existing_relations': existing_relations
}
def check_field_type_consistency(conn) -> Dict:
"""检查关联关系的字段类型一致性f_polic_file_field 应该只关联输出字段)"""
cursor = conn.cursor(pymysql.cursors.DictCursor)
print("\n" + "="*80)
print("5. 检查字段类型一致性")
print("="*80)
# 检查是否关联了输入字段field_type=1
cursor.execute("""
SELECT fff.id, fff.file_id, fff.filed_id,
fc.name as file_name, fc.template_code, f.name as field_name, f.filed_code, f.field_type
FROM f_polic_file_field fff
INNER JOIN f_polic_file_config fc ON fff.file_id = fc.id AND fff.tenant_id = fc.tenant_id
INNER JOIN f_polic_field f ON fff.filed_id = f.id AND fff.tenant_id = f.tenant_id
WHERE fff.tenant_id = %s AND f.field_type = 1
ORDER BY fc.name, f.name
""", (TENANT_ID,))
input_field_relations = cursor.fetchall()
print(f"\n关联到输入字段 (field_type=1) 的记录: {len(input_field_relations)}")
if input_field_relations:
print(" 注意: f_polic_file_field 表通常只应该关联输出字段 (field_type=2)")
print(" 根据业务逻辑,输入字段不需要通过此表关联")
print(" 详情:")
for rel in input_field_relations:
print(f" - 关联ID: {rel['id']}, 文件: {rel['file_name']} (code: {rel['template_code']}), "
f"字段: {rel['field_name']} ({rel['filed_code']}, type={rel['field_type']})")
else:
print(" ✓ 所有关联都是输出字段")
return {
'input_field_relations': input_field_relations
}
def fix_invalid_relations(conn, dry_run: bool = True) -> Dict:
"""修复无效的关联关系"""
cursor = conn.cursor()
print("\n" + "="*80)
print("修复无效的关联关系")
print("="*80)
if dry_run:
print("\n[DRY RUN模式 - 不会实际修改数据库]")
# 获取无效的关联
invalid_file_relations = check_invalid_relations(conn)['invalid_file_relations']
invalid_field_relations = check_invalid_relations(conn)['invalid_field_relations']
all_invalid_ids = set()
for rel in invalid_file_relations:
all_invalid_ids.add(rel['id'])
for rel in invalid_field_relations:
all_invalid_ids.add(rel['id'])
if not all_invalid_ids:
print("\n✓ 没有无效的关联关系需要删除")
return {'deleted': 0}
print(f"\n准备删除 {len(all_invalid_ids)} 条无效的关联关系")
if not dry_run:
placeholders = ','.join(['%s'] * len(all_invalid_ids))
cursor.execute(f"""
DELETE FROM f_polic_file_field
WHERE id IN ({placeholders})
""", list(all_invalid_ids))
conn.commit()
print(f"✓ 已删除 {cursor.rowcount} 条无效的关联关系")
else:
print(f"[DRY RUN] 将删除以下关联ID: {sorted(all_invalid_ids)}")
return {'deleted': len(all_invalid_ids) if not dry_run else 0}
def fix_input_field_relations(conn, dry_run: bool = True) -> Dict:
"""删除关联到输入字段的记录f_polic_file_field 应该只关联输出字段)"""
cursor = conn.cursor()
print("\n" + "="*80)
print("删除关联到输入字段的记录")
print("="*80)
if dry_run:
print("\n[DRY RUN模式 - 不会实际修改数据库]")
# 获取关联到输入字段的记录
input_field_relations = check_field_type_consistency(conn)['input_field_relations']
if not input_field_relations:
print("\n✓ 没有关联到输入字段的记录需要删除")
return {'deleted': 0}
ids_to_delete = [rel['id'] for rel in input_field_relations]
print(f"\n准备删除 {len(ids_to_delete)} 条关联到输入字段的记录")
if not dry_run:
placeholders = ','.join(['%s'] * len(ids_to_delete))
cursor.execute(f"""
DELETE FROM f_polic_file_field
WHERE id IN ({placeholders})
""", ids_to_delete)
conn.commit()
print(f"✓ 已删除 {cursor.rowcount} 条关联到输入字段的记录")
else:
print(f"[DRY RUN] 将删除以下关联ID: {sorted(ids_to_delete)}")
return {'deleted': len(ids_to_delete) if not dry_run else 0}
def fix_duplicate_relations(conn, dry_run: bool = True) -> Dict:
"""修复重复的关联关系(保留第一条,删除其他)"""
cursor = conn.cursor()
print("\n" + "="*80)
print("修复重复的关联关系")
print("="*80)
if dry_run:
print("\n[DRY RUN模式 - 不会实际修改数据库]")
duplicates = check_duplicate_relations(conn)['duplicates']
if not duplicates:
print("\n✓ 没有重复的关联关系需要修复")
return {'deleted': 0}
ids_to_delete = []
for dup in duplicates:
# 保留第一条ID最小的删除其他的
ids_to_delete.extend(dup['ids'][1:])
print(f"\n准备删除 {len(ids_to_delete)} 条重复的关联关系")
if not dry_run:
placeholders = ','.join(['%s'] * len(ids_to_delete))
cursor.execute(f"""
DELETE FROM f_polic_file_field
WHERE id IN ({placeholders})
""", ids_to_delete)
conn.commit()
print(f"✓ 已删除 {cursor.rowcount} 条重复的关联关系")
else:
print(f"[DRY RUN] 将删除以下关联ID: {sorted(ids_to_delete)}")
return {'deleted': len(ids_to_delete) if not dry_run else 0}
def get_statistics(conn) -> Dict:
"""获取统计信息"""
cursor = conn.cursor(pymysql.cursors.DictCursor)
print("\n" + "="*80)
print("统计信息")
print("="*80)
# 总关联数
cursor.execute("""
SELECT COUNT(*) as total
FROM f_polic_file_field
WHERE tenant_id = %s
""", (TENANT_ID,))
total_relations = cursor.fetchone()['total']
# 有效的关联数(关联到存在的、启用的文件和字段)
cursor.execute("""
SELECT COUNT(*) as total
FROM f_polic_file_field fff
INNER JOIN f_polic_file_config fc ON fff.file_id = fc.id AND fff.tenant_id = fc.tenant_id AND fc.state = 1
INNER JOIN f_polic_field f ON fff.filed_id = f.id AND fff.tenant_id = f.tenant_id AND f.state = 1
WHERE fff.tenant_id = %s
""", (TENANT_ID,))
valid_relations = cursor.fetchone()['total']
# 关联的文件数
cursor.execute("""
SELECT COUNT(DISTINCT file_id) as total
FROM f_polic_file_field
WHERE tenant_id = %s
""", (TENANT_ID,))
related_files = cursor.fetchone()['total']
# 关联的字段数
cursor.execute("""
SELECT COUNT(DISTINCT filed_id) as total
FROM f_polic_file_field
WHERE tenant_id = %s
""", (TENANT_ID,))
related_fields = cursor.fetchone()['total']
print(f"\n总关联数: {total_relations}")
print(f"有效关联数: {valid_relations}")
print(f"关联的文件数: {related_files}")
print(f"关联的字段数: {related_fields}")
return {
'total_relations': total_relations,
'valid_relations': valid_relations,
'related_files': related_files,
'related_fields': related_fields
}
def main():
"""主函数"""
print("="*80)
print("检查并修复 f_polic_file_field 表的关联关系")
print("="*80)
try:
conn = pymysql.connect(**DB_CONFIG)
print("✓ 数据库连接成功\n")
except Exception as e:
print(f"✗ 数据库连接失败: {e}")
return
try:
# 1. 检查无效的关联关系
invalid_result = check_invalid_relations(conn)
# 2. 检查重复的关联关系
duplicate_result = check_duplicate_relations(conn)
# 3. 检查关联到已删除或未启用的字段/文件
disabled_result = check_disabled_relations(conn)
# 4. 检查缺失的关联关系
missing_result = check_missing_relations(conn)
# 5. 检查字段类型一致性
type_result = check_field_type_consistency(conn)
# 6. 获取统计信息
stats = get_statistics(conn)
# 总结
print("\n" + "="*80)
print("检查总结")
print("="*80)
has_issues = (
len(invalid_result['invalid_file_relations']) > 0 or
len(invalid_result['invalid_field_relations']) > 0 or
len(duplicate_result['duplicates']) > 0
)
has_issues = (
len(invalid_result['invalid_file_relations']) > 0 or
len(invalid_result['invalid_field_relations']) > 0 or
len(duplicate_result['duplicates']) > 0 or
len(type_result['input_field_relations']) > 0
)
if has_issues:
print("\n⚠ 发现以下问题:")
print(f" - 无效的 file_id 关联: {len(invalid_result['invalid_file_relations'])}")
print(f" - 无效的 filed_id 关联: {len(invalid_result['invalid_field_relations'])}")
print(f" - 重复的关联关系: {len(duplicate_result['duplicates'])}")
print(f" - 关联到未启用的文件: {len(disabled_result['disabled_file_relations'])}")
print(f" - 关联到未启用的字段: {len(disabled_result['disabled_field_relations'])}")
print(f" - 关联到输入字段: {len(type_result['input_field_relations'])}")
print("\n是否要修复这些问题?")
print("运行以下命令进行修复:")
print(" python check_and_fix_file_field_relations.py --fix")
else:
print("\n✓ 未发现需要修复的问题")
print("\n" + "="*80)
except Exception as e:
print(f"\n✗ 检查过程中发生错误: {e}")
import traceback
traceback.print_exc()
finally:
conn.close()
print("\n数据库连接已关闭")
def fix_main():
"""修复主函数"""
print("="*80)
print("修复 f_polic_file_field 表的关联关系")
print("="*80)
try:
conn = pymysql.connect(**DB_CONFIG)
print("✓ 数据库连接成功\n")
except Exception as e:
print(f"✗ 数据库连接失败: {e}")
return
try:
# 先进行干运行
print("\n[第一步] 干运行检查...")
invalid_result = check_invalid_relations(conn)
duplicate_result = check_duplicate_relations(conn)
# 修复无效的关联关系
print("\n[第二步] 修复无效的关联关系...")
fix_invalid_relations(conn, dry_run=False)
# 修复重复的关联关系
print("\n[第三步] 修复重复的关联关系...")
fix_duplicate_relations(conn, dry_run=False)
# 删除关联到输入字段的记录
print("\n[第四步] 删除关联到输入字段的记录...")
fix_input_field_relations(conn, dry_run=False)
# 重新获取统计信息
print("\n[第五步] 修复后的统计信息...")
stats = get_statistics(conn)
print("\n" + "="*80)
print("修复完成")
print("="*80)
except Exception as e:
print(f"\n✗ 修复过程中发生错误: {e}")
import traceback
traceback.print_exc()
conn.rollback()
finally:
conn.close()
print("\n数据库连接已关闭")
if __name__ == '__main__':
import sys
if '--fix' in sys.argv:
# 确认操作
print("\n⚠ 警告: 这将修改数据库!")
response = input("确认要继续吗? (yes/no): ")
if response.lower() == 'yes':
fix_main()
else:
print("操作已取消")
else:
main()

105
check_existing_data.py Normal file
View File

@ -0,0 +1,105 @@
"""
检查数据库中的现有数据确认匹配情况
"""
import os
import json
import pymysql
from pathlib import Path
# 数据库连接配置
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'
}
TENANT_ID = 615873064429507639
def check_existing_data():
"""检查数据库中的现有数据"""
print("="*80)
print("检查数据库中的现有数据")
print("="*80)
try:
conn = pymysql.connect(**DB_CONFIG)
cursor = conn.cursor(pymysql.cursors.DictCursor)
# 查询所有记录
sql = """
SELECT id, name, parent_id, template_code, input_data, file_path, state
FROM f_polic_file_config
WHERE tenant_id = %s
ORDER BY name
"""
cursor.execute(sql, (TENANT_ID,))
configs = cursor.fetchall()
print(f"\n共找到 {len(configs)} 条记录\n")
# 按 parent_id 分组统计
with_parent = []
without_parent = []
for config in configs:
# 尝试从 input_data 中提取 template_code
template_code = config.get('template_code')
if not template_code and config.get('input_data'):
try:
input_data = json.loads(config['input_data']) if isinstance(config['input_data'], str) else config['input_data']
if isinstance(input_data, dict):
template_code = input_data.get('template_code')
except:
pass
config['extracted_template_code'] = template_code
if config.get('parent_id'):
with_parent.append(config)
else:
without_parent.append(config)
print(f"有 parent_id 的记录: {len(with_parent)}")
print(f"无 parent_id 的记录: {len(without_parent)}\n")
# 显示无 parent_id 的记录
print("="*80)
print("无 parent_id 的记录列表:")
print("="*80)
for i, config in enumerate(without_parent, 1):
print(f"\n{i}. {config['name']}")
print(f" ID: {config['id']}")
print(f" template_code: {config.get('extracted_template_code') or config.get('template_code') or ''}")
print(f" file_path: {config.get('file_path', '')}")
print(f" state: {config.get('state')}")
# 显示有 parent_id 的记录(树状结构)
print("\n" + "="*80)
print("有 parent_id 的记录(树状结构):")
print("="*80)
# 构建ID到名称的映射
id_to_name = {config['id']: config['name'] for config in configs}
for config in with_parent:
parent_name = id_to_name.get(config['parent_id'], f"ID:{config['parent_id']}")
print(f"\n{config['name']}")
print(f" ID: {config['id']}")
print(f" 父节点: {parent_name} (ID: {config['parent_id']})")
print(f" template_code: {config.get('extracted_template_code') or config.get('template_code') or ''}")
cursor.close()
conn.close()
except Exception as e:
print(f"错误: {e}")
import traceback
traceback.print_exc()
if __name__ == '__main__':
check_existing_data()

131
check_remaining_fields.py Normal file
View File

@ -0,0 +1,131 @@
"""
检查剩余的未处理字段并生成合适的field_code
"""
import os
import pymysql
import re
from typing import Dict, List
# 数据库连接配置
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'
}
TENANT_ID = 615873064429507639
def is_chinese(text: str) -> bool:
"""判断字符串是否包含中文字符"""
if not text:
return False
return bool(re.search(r'[\u4e00-\u9fff]', text))
def generate_field_code(field_name: str) -> str:
"""根据字段名称生成field_code"""
# 移除常见前缀
name = field_name.replace('被核查人员', 'target_').replace('被核查人', 'target_')
# 转换为小写并替换特殊字符
code = name.lower()
code = re.sub(r'[^\w\u4e00-\u9fff]', '_', code)
code = re.sub(r'_+', '_', code).strip('_')
# 如果还是中文,尝试更智能的转换
if is_chinese(code):
# 简单的拼音映射(这里只是示例,实际应该使用拼音库)
# 暂时使用更简单的规则
code = field_name.lower()
code = code.replace('被核查人员', 'target_')
code = code.replace('被核查人', 'target_')
code = code.replace('谈话', 'interview_')
code = code.replace('审批', 'approval_')
code = code.replace('核查', 'investigation_')
code = code.replace('人员', '')
code = code.replace('时间', '_time')
code = code.replace('地点', '_location')
code = code.replace('部门', '_department')
code = code.replace('姓名', '_name')
code = code.replace('号码', '_number')
code = code.replace('情况', '_situation')
code = code.replace('问题', '_issue')
code = code.replace('描述', '_description')
code = re.sub(r'[^\w]', '_', code)
code = re.sub(r'_+', '_', code).strip('_')
return code
def check_remaining_fields():
"""检查剩余的未处理字段"""
conn = pymysql.connect(**DB_CONFIG)
cursor = conn.cursor(pymysql.cursors.DictCursor)
print("="*80)
print("检查剩余的未处理字段")
print("="*80)
# 查询所有包含中文field_code的字段
cursor.execute("""
SELECT id, name, filed_code, field_type, state
FROM f_polic_field
WHERE tenant_id = %s AND (
filed_code REGEXP '[\\u4e00-\\u9fff]'
OR filed_code IS NULL
OR filed_code = ''
)
ORDER BY name
""", (TENANT_ID,))
fields = cursor.fetchall()
print(f"\n找到 {len(fields)} 个仍需要处理的字段:\n")
suggestions = []
for field in fields:
suggested_code = generate_field_code(field['name'])
suggestions.append({
'id': field['id'],
'name': field['name'],
'current_code': field['filed_code'],
'suggested_code': suggested_code,
'field_type': field['field_type']
})
print(f" ID: {field['id']}")
print(f" 名称: {field['name']}")
print(f" 当前field_code: {field['filed_code']}")
print(f" 建议field_code: {suggested_code}")
print(f" field_type: {field['field_type']}")
print()
# 询问是否更新
if suggestions:
print("="*80)
choice = input("是否更新这些字段的field_code(y/n默认n): ").strip().lower()
if choice == 'y':
print("\n开始更新...")
for sug in suggestions:
cursor.execute("""
UPDATE f_polic_field
SET filed_code = %s, updated_time = NOW(), updated_by = %s
WHERE id = %s
""", (sug['suggested_code'], 655162080928945152, sug['id']))
print(f" ✓ 更新字段 ID {sug['id']}: {sug['name']} -> {sug['suggested_code']}")
conn.commit()
print("\n✓ 更新完成")
else:
print("未执行更新")
cursor.close()
conn.close()
if __name__ == '__main__':
check_remaining_fields()

View File

@ -1,21 +1,34 @@
{ {
"prompt_template": { "prompt_template": {
"intro": "请从以下输入文本中提取结构化信息。", "intro": "请从以下输入文本中提取结构化信息。仔细分析文本内容,准确提取每个字段的值。\n\n⚠ 重要提醒:请逐字逐句仔细阅读输入文本,不要遗漏任何信息。对于性别、年龄、职务、单位、文化程度等字段,请特别仔细查找,这些信息可能以各种形式出现在文本中。",
"input_text_label": "输入文本:", "input_text_label": "输入文本:",
"output_fields_label": "需要提取的字段", "output_fields_label": "需要提取的字段(请仔细分析每个字段,确保提取完整)",
"json_format_label": "请严格按照以下JSON格式返回结果只返回JSON,不要包含其他文字说明:", "json_format_label": "请严格按照以下JSON格式返回结果只返回JSON对象,不要包含任何其他文字说明或markdown代码块标记",
"requirements_label": "要求:", "requirements_label": "重要要求(请严格遵守)",
"requirements": [ "requirements": [
"仔细分析输入文本,准确提取每个字段的值", "⚠️ 逐字逐句仔细分析输入文本,不要遗漏任何信息。请特别关注性别、年龄、职务、单位、文化程度等字段",
"如果某个字段在输入文本中找不到对应信息,该字段值设为空字符串\"\"", "对于每个字段,请从多个角度思考:直接提及、同义词、隐含信息、可推断信息。例如:性别可能以\"男\"、\"女\"、\"男性\"、\"女性\"、\"先生\"、\"女士\"等形式出现",
"日期格式统一为YYYYMM198005表示1980年5月如果包含日期信息则格式为YYYYMMDD", "如果文本中明确提到某个信息(如\"30岁\"、\"男\"、\"总经理\"、\"某公司\"等),必须提取出来,不能设为空",
"性别统一为\"男\"或\"女\",不要使用\"男性\"或\"女性\"", "如果可以通过已有信息合理推断,请进行推断并填写:\n - 根据出生年月如1980年05月和当前年份2024年计算年龄44岁\n - 从单位及职务(如\"某公司总经理\")中拆分单位(\"某公司\")和职务(\"总经理\"\n - 从工作基本情况中提取性别、文化程度等信息",
"政治面貌使用标准表述(如:中共党员、中共预备党员、共青团员、群众等)", "如果某个字段在输入文本中确实找不到任何相关信息,该字段值才设为空字符串\"\"",
"日期格式统一为中文格式YYYY年MM月1980年05月表示1980年5月如果包含日期信息则格式为YYYY年MM月DD日1985年05月17日。注意年份必须是4位数字月份和日期必须是2位数字如1980年5月应格式化为1980年05月不是1980年5月",
"性别统一为\"男\"或\"女\",不要使用\"男性\"或\"女性\"。如果文本中提到\"男性\"、\"男\"、\"先生\"等,统一转换为\"男\";如果提到\"女性\"、\"女\"、\"女士\"等,统一转换为\"女\"",
"年龄字段:如果文本中直接提到年龄(如\"30岁\"、\"30周岁\"直接提取数字如果只有出生年月可以根据当前年份计算年龄当前年份为2024年",
"单位及职务字段:如果文本中提到\"XX公司总经理\"、\"XX单位XX职务\"等,需要同时提取单位名称和职务名称",
"单位字段:从单位及职务信息中提取单位名称部分(如\"XX公司\"、\"XX局\"、\"XX部门\"等)",
"职务字段:从单位及职务信息中提取职务名称部分(如\"总经理\"、\"局长\"、\"主任\"等)",
"文化程度字段:注意识别\"本科\"、\"大专\"、\"高中\"、\"中专\"、\"研究生\"、\"硕士\"、\"博士\"等表述",
"政治面貌使用标准表述(如:中共党员、中共预备党员、共青团员、群众等)。如果文本中提到\"党员\",统一转换为\"中共党员\"",
"职级使用标准表述(如:正处级、副处级、正科级、副科级等)", "职级使用标准表述(如:正处级、副处级、正科级、副科级等)",
"线索来源字段:注意识别\"举报\"、\"来信\"、\"来电\"、\"网络举报\"、\"上级交办\"等表述",
"主要问题线索字段:提取文本中关于问题、线索、举报内容等的描述",
"身份证号码只提取数字,不包含其他字符", "身份证号码只提取数字,不包含其他字符",
"联系方式提取电话号码,格式化为纯数字", "联系方式提取电话号码,格式化为纯数字",
"地址信息保持完整,包含省市区街道等详细信息", "地址信息保持完整,包含省市区街道等详细信息",
"只返回JSON对象不要包含markdown代码块标记" "只返回JSON对象不要包含markdown代码块标记、思考过程或其他说明文字",
"JSON格式要求所有字段名必须使用双引号字段名中不能包含前导点如不能使用\".target_gender\",应使用\"target_gender\"),字段名前后不能有空格",
"必须返回所有要求的字段即使值为空字符串也要包含在JSON中",
"字段名必须严格按照JSON示例中的字段编码不能随意修改或拼写错误如不能使用\"targetsProfessionalRank\",应使用\"target_professional_rank\""
] ]
}, },
"field_formatting": { "field_formatting": {
@ -34,23 +47,32 @@
"description": "被核查人员性别", "description": "被核查人员性别",
"rules": [ "rules": [
"只能返回\"男\"或\"女\"", "只能返回\"男\"或\"女\"",
"如果文本中提到\"男性\"、\"男性公民\"等,统一转换为\"男\"", "如果文本中提到\"男性\"、\"男性公民\"、\"男\"、\"先生\"等,统一转换为\"男\"",
"如果文本中提到\"女性\"、\"女性公民\"等,统一转换为\"女\"" "如果文本中提到\"女性\"、\"女性公民\"、\"女\"、\"女士\"等,统一转换为\"女\"",
"请仔细查找文本中所有可能表示性别的词汇,不要遗漏",
"如果文本中提到\"XXX...\"或\"XXX...\",必须提取性别",
"如果工作基本情况中提到性别信息,必须提取"
] ]
}, },
"target_date_of_birth": { "target_date_of_birth": {
"description": "被核查人员出生年月", "description": "被核查人员出生年月",
"rules": [ "rules": [
"格式YYYYMM如198005表示1980年5月", "格式YYYY年MM月中文格式如1980年05月表示1980年5月注意月份必须是2位数字如5月应写为05月不是5月",
"如果只有年份月份设为01", "如果只有年份月份设为01如1980年应格式化为1980年01月",
"如果文本中提到\"X年X月X日出生\",只提取年月,忽略日期" "如果文本中提到\"X年X月X日出生\",只提取年月,忽略日期",
"如果文本中提到\"1980年5月\",格式化为\"1980年05月\"(月份补零)",
"如果文本中提到\"1980年05月\",保持为\"1980年05月\"",
"年份必须是4位数字月份必须是2位数字01-12",
"输出格式示例1980年05月、1985年03月、1990年12月"
] ]
}, },
"target_date_of_birth_full": { "target_date_of_birth_full": {
"description": "被核查人员出生年月日", "description": "被核查人员出生年月日",
"rules": [ "rules": [
"格式YYYYMMDD如19800515表示1980年5月15日", "格式YYYY年MM月DD日中文格式如1985年05月17日表示1985年5月17日",
"如果只有年月日期设为01" "如果只有年月日期设为01如1980年05月应格式化为1980年05月01日",
"年份必须是4位数字月份和日期必须是2位数字01-12和01-31",
"输出格式示例1985年05月17日、1980年03月15日、1990年12月01日"
] ]
}, },
"target_political_status": { "target_political_status": {
@ -99,6 +121,84 @@
"学历使用标准表述:本科、大专、高中、中专、研究生等", "学历使用标准表述:本科、大专、高中、中专、研究生等",
"政治面貌部分:如果是中共党员,写\"加入中国共产党\";如果不是,省略此部分" "政治面貌部分:如果是中共党员,写\"加入中国共产党\";如果不是,省略此部分"
] ]
},
"target_age": {
"description": "被核查人员年龄",
"rules": [
"如果文本中直接提到年龄(如\"30岁\"、\"30周岁\"、\"年龄30\"、\"现年30\"),直接提取数字部分",
"如果无法抽取到年龄数据,但抽取到了\"被核查人员出生年月\",系统将根据出生年月和当前日期自动计算年龄",
"年龄格式:纯数字,单位为岁,如\"44\"表示44岁",
"如果文本中既没有直接提到年龄,也没有出生年月信息,则设为空字符串"
]
},
"target_organization_and_position": {
"description": "被核查人员单位及职务(包括兼职)",
"rules": [
"提取完整的单位及职务信息,格式如:\"XX公司总经理\"、\"XX局XX处处长\"、\"XX单位XX职务\"",
"如果文本中提到\"XX公司总经理\"、\"XX单位XX职务\"等,需要完整提取",
"如果文本中分别提到单位和职务,需要组合成\"单位+职务\"的格式",
"如果文本中提到多个职务或兼职,需要全部包含,用\"、\"或\"兼\"连接",
"保持原文中的表述,不要随意修改"
]
},
"target_organization": {
"description": "被核查人员单位",
"rules": [
"从单位及职务信息中提取单位名称部分",
"单位名称包括:公司、企业、机关、事业单位、部门等(如\"XX公司\"、\"XX局\"、\"XX部门\"、\"XX委员会\"等)",
"如果文本中只提到单位名称,直接提取",
"⚠️ 如果文本中提到\"XX公司总经理\",必须提取\"XX公司\"部分,不能设为空",
"如果文本中提到\"XX单位XX职务\",提取\"XX单位\"部分",
"如果已有单位及职务字段target_organization_and_position必须从中拆分出单位名称",
"保持单位名称的完整性,不要遗漏"
]
},
"target_position": {
"description": "被核查人员职务",
"rules": [
"从单位及职务信息中提取职务名称部分",
"职务名称包括:总经理、经理、局长、处长、科长、主任、书记、部长等",
"如果文本中只提到职务名称,直接提取",
"⚠️ 如果文本中提到\"XX公司总经理\",必须提取\"总经理\"部分,不能设为空",
"如果文本中提到\"XX单位XX职务\",提取\"XX职务\"部分",
"如果已有单位及职务字段target_organization_and_position必须从中拆分出职务名称",
"如果文本中提到多个职务,需要全部提取,用\"、\"连接",
"保持职务名称的准确性"
]
},
"target_education_level": {
"description": "被核查人员文化程度",
"rules": [
"识别文本中关于学历、文化程度的表述",
"标准表述包括:小学、初中、高中、中专、大专、本科、研究生、硕士、博士等",
"如果文本中提到\"大学\"、\"大学毕业\",通常指\"本科\"",
"如果文本中提到\"专科\",通常指\"大专\"",
"如果文本中提到\"研究生学历\",可以写\"研究生\"",
"保持标准表述,不要使用非标准表述"
]
},
"clue_source": {
"description": "线索来源",
"rules": [
"识别文本中关于线索来源的表述",
"常见来源包括:举报、来信、来电、网络举报、上级交办、巡视发现、审计发现、媒体曝光等",
"如果文本中提到\"举报\"、\"被举报\",线索来源可能是\"举报\"或\"来信举报\"",
"如果文本中提到\"电话\"、\"来电\",线索来源可能是\"来电举报\"",
"如果文本中提到\"网络\"、\"网上\",线索来源可能是\"网络举报\"",
"如果文本中提到\"上级\"、\"交办\",线索来源可能是\"上级交办\"",
"如果文本中没有明确提到线索来源,但提到\"举报\"相关信息,可以推断为\"举报\"",
"保持标准表述"
]
},
"target_issue_description": {
"description": "主要问题线索",
"rules": [
"提取文本中关于问题、线索、举报内容等的描述",
"包括但不限于:违纪违法问题、工作作风问题、经济问题、生活作风问题等",
"如果文本中提到\"问题\"、\"线索\"、\"举报\"、\"反映\"等关键词,提取相关内容",
"保持问题描述的完整性和准确性,不要遗漏重要信息",
"如果文本中没有明确的问题描述,但提到了相关情况,也要尽量提取"
]
} }
} }
} }

231
enable_all_fields.py Normal file
View File

@ -0,0 +1,231 @@
"""
启用f_polic_field表中所有字段将state更新为1
"""
import pymysql
import os
from datetime import datetime
# 数据库连接配置
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'
}
TENANT_ID = 615873064429507639
UPDATED_BY = 655162080928945152
CURRENT_TIME = datetime.now()
def check_field_states(conn):
"""检查字段状态统计"""
cursor = conn.cursor(pymysql.cursors.DictCursor)
# 统计各状态的字段数量使用CAST来正确处理二进制类型
sql = """
SELECT
CAST(state AS UNSIGNED) as state_int,
field_type,
COUNT(*) as count
FROM f_polic_field
WHERE tenant_id = %s
GROUP BY CAST(state AS UNSIGNED), field_type
ORDER BY field_type, CAST(state AS UNSIGNED)
"""
cursor.execute(sql, (TENANT_ID,))
stats = cursor.fetchall()
cursor.close()
return stats
def get_fields_by_state(conn, state):
"""获取指定状态的字段列表"""
cursor = conn.cursor(pymysql.cursors.DictCursor)
sql = """
SELECT id, name, filed_code, field_type, CAST(state AS UNSIGNED) as state_int
FROM f_polic_field
WHERE tenant_id = %s
AND CAST(state AS UNSIGNED) = %s
ORDER BY field_type, name
"""
cursor.execute(sql, (TENANT_ID, state))
fields = cursor.fetchall()
cursor.close()
return fields
def enable_all_fields(conn, dry_run=True):
"""启用所有字段"""
cursor = conn.cursor(pymysql.cursors.DictCursor)
# 查询需要更新的字段使用CAST来正确处理二进制类型
sql = """
SELECT id, name, filed_code, field_type, CAST(state AS UNSIGNED) as state_int
FROM f_polic_field
WHERE tenant_id = %s
AND CAST(state AS UNSIGNED) != 1
ORDER BY field_type, name
"""
cursor.execute(sql, (TENANT_ID,))
fields_to_update = cursor.fetchall()
if not fields_to_update:
print("✓ 所有字段已经是启用状态,无需更新")
cursor.close()
return 0
print(f"\n找到 {len(fields_to_update)} 个需要启用的字段:")
for field in fields_to_update:
field_type_str = "输出字段" if field['field_type'] == 2 else "输入字段"
print(f" - {field['name']} ({field['filed_code']}) [{field_type_str}] (当前state={field['state_int']})")
if dry_run:
print("\n⚠ 这是预览模式dry_run=True不会实际更新数据库")
cursor.close()
return len(fields_to_update)
# 执行更新使用CAST来正确比较
update_sql = """
UPDATE f_polic_field
SET state = 1, updated_time = %s, updated_by = %s
WHERE tenant_id = %s
AND CAST(state AS UNSIGNED) != 1
"""
cursor.execute(update_sql, (CURRENT_TIME, UPDATED_BY, TENANT_ID))
updated_count = cursor.rowcount
conn.commit()
cursor.close()
return updated_count
def main():
"""主函数"""
print("="*80)
print("启用f_polic_field表中所有字段")
print("="*80)
print()
try:
conn = pymysql.connect(**DB_CONFIG)
print("✓ 数据库连接成功")
except Exception as e:
print(f"✗ 数据库连接失败: {str(e)}")
return
try:
# 1. 检查当前状态统计
print("\n正在检查字段状态统计...")
stats = check_field_states(conn)
print("\n字段状态统计:")
total_fields = 0
enabled_fields = 0
disabled_fields = 0
for stat in stats:
state_int = stat['state_int']
field_type = stat['field_type']
count = stat['count']
total_fields += count
state_str = "启用" if state_int == 1 else "未启用"
type_str = "输出字段" if field_type == 2 else "输入字段"
print(f" {type_str} - {state_str} (state={state_int}): {count}")
if state_int == 1:
enabled_fields += count
else:
disabled_fields += count
print(f"\n总计: {total_fields} 个字段")
print(f" 启用: {enabled_fields}")
print(f" 未启用: {disabled_fields}")
# 2. 显示未启用的字段详情
if disabled_fields > 0:
print(f"\n正在查询未启用的字段详情...")
disabled_fields_list = get_fields_by_state(conn, 0)
print(f"\n未启用的字段列表 ({len(disabled_fields_list)} 个):")
for field in disabled_fields_list:
field_type_str = "输出字段" if field['field_type'] == 2 else "输入字段"
print(f" - {field['name']} ({field['filed_code']}) [{field_type_str}]")
# 3. 预览更新dry_run
print("\n" + "="*80)
print("预览更新(不会实际修改数据库)")
print("="*80)
count_to_update = enable_all_fields(conn, dry_run=True)
if count_to_update == 0:
print("\n所有字段已经是启用状态,无需更新")
return
# 4. 确认是否执行更新
print("\n" + "="*80)
print("准备执行更新")
print("="*80)
print(f"将更新 {count_to_update} 个字段的状态为启用state=1")
# 实际执行更新
print("\n正在执行更新...")
updated_count = enable_all_fields(conn, dry_run=False)
print(f"\n✓ 更新成功!共更新 {updated_count} 个字段")
# 5. 验证更新结果
print("\n正在验证更新结果...")
final_stats = check_field_states(conn)
print("\n更新后的字段状态统计:")
final_enabled = 0
final_disabled = 0
for stat in final_stats:
state_int = stat['state_int']
field_type = stat['field_type']
count = stat['count']
state_str = "启用" if state_int == 1 else "未启用"
type_str = "输出字段" if field_type == 2 else "输入字段"
print(f" {type_str} - {state_str} (state={state_int}): {count}")
if state_int == 1:
final_enabled += count
else:
final_disabled += count
print(f"\n总计: {final_enabled + final_disabled} 个字段")
print(f" 启用: {final_enabled}")
print(f" 未启用: {final_disabled}")
if final_disabled == 0:
print("\n✓ 所有字段已成功启用!")
else:
print(f"\n⚠ 仍有 {final_disabled} 个字段未启用")
print("\n" + "="*80)
print("操作完成!")
print("="*80)
except Exception as e:
print(f"\n✗ 处理失败: {str(e)}")
import traceback
traceback.print_exc()
conn.rollback()
finally:
conn.close()
if __name__ == '__main__':
main()

View File

@ -0,0 +1,191 @@
"""
修复缺失的 target_education_level 字段
检查并创建被核查人员文化程度字段
"""
import pymysql
import os
from datetime import datetime
# 数据库连接配置
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'
}
TENANT_ID = 615873064429507639
CREATED_BY = 655162080928945152
UPDATED_BY = 655162080928945152
CURRENT_TIME = datetime.now()
# 字段定义
FIELD_DEFINITION = {
'name': '被核查人员文化程度',
'field_code': 'target_education_level',
'field_type': 2, # 输出字段
'description': '被核查人员文化程度(如:本科、大专、高中等)'
}
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 check_field_exists(conn):
"""检查字段是否存在"""
cursor = conn.cursor(pymysql.cursors.DictCursor)
sql = """
SELECT id, name, filed_code, field_type, state
FROM f_polic_field
WHERE tenant_id = %s AND filed_code = %s
"""
cursor.execute(sql, (TENANT_ID, FIELD_DEFINITION['field_code']))
field = cursor.fetchone()
cursor.close()
return field
def create_field(conn, dry_run: bool = True):
"""创建字段"""
cursor = conn.cursor()
field_id = generate_id()
insert_sql = """
INSERT INTO f_polic_field
(id, tenant_id, name, filed_code, field_type, created_time, created_by, updated_time, updated_by, state)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
"""
if dry_run:
print(f"[DRY RUN] 将创建字段:")
print(f" ID: {field_id}")
print(f" 名称: {FIELD_DEFINITION['name']}")
print(f" 编码: {FIELD_DEFINITION['field_code']}")
print(f" 类型: {FIELD_DEFINITION['field_type']} (输出字段)")
print(f" 状态: 1 (启用)")
else:
cursor.execute(insert_sql, (
field_id,
TENANT_ID,
FIELD_DEFINITION['name'],
FIELD_DEFINITION['field_code'],
FIELD_DEFINITION['field_type'],
CURRENT_TIME,
CREATED_BY,
CURRENT_TIME,
UPDATED_BY,
1 # state: 1表示启用
))
conn.commit()
print(f"✓ 成功创建字段: {FIELD_DEFINITION['name']} ({FIELD_DEFINITION['field_code']}), ID: {field_id}")
cursor.close()
return field_id
def update_field_state(conn, field_id, dry_run: bool = True):
"""更新字段状态为启用"""
cursor = conn.cursor()
update_sql = """
UPDATE f_polic_field
SET state = 1, updated_time = NOW(), updated_by = %s
WHERE id = %s AND tenant_id = %s
"""
if dry_run:
print(f"[DRY RUN] 将更新字段状态为启用: ID={field_id}")
else:
cursor.execute(update_sql, (UPDATED_BY, field_id, TENANT_ID))
conn.commit()
print(f"✓ 成功更新字段状态为启用: ID={field_id}")
cursor.close()
def main(dry_run: bool = True):
"""主函数"""
print("="*80)
print("修复缺失的 target_education_level 字段")
print("="*80)
if dry_run:
print("\n[DRY RUN模式 - 不会实际修改数据库]")
else:
print("\n[实际执行模式 - 将修改数据库]")
try:
conn = pymysql.connect(**DB_CONFIG)
print("✓ 数据库连接成功\n")
# 检查字段是否存在
print("1. 检查字段是否存在...")
existing_field = check_field_exists(conn)
if existing_field:
print(f" ✓ 字段已存在:")
print(f" ID: {existing_field['id']}")
print(f" 名称: {existing_field['name']}")
print(f" 编码: {existing_field['filed_code']}")
print(f" 类型: {existing_field['field_type']} ({'输出字段' if existing_field['field_type'] == 2 else '输入字段'})")
print(f" 状态: {existing_field['state']} ({'启用' if existing_field['state'] == 1 else '未启用'})")
# 如果字段存在但未启用,启用它
if existing_field['state'] != 1:
print(f"\n2. 字段存在但未启用,将更新状态...")
update_field_state(conn, existing_field['id'], dry_run=dry_run)
else:
print(f"\n✓ 字段已存在且已启用,无需操作")
else:
print(f" ✗ 字段不存在,需要创建")
print(f"\n2. 创建字段...")
field_id = create_field(conn, dry_run=dry_run)
if not dry_run:
print(f"\n✓ 字段创建完成")
print("\n" + "="*80)
if dry_run:
print("\n这是DRY RUN模式未实际修改数据库。")
print("要实际执行,请运行: python fix_missing_education_level_field.py --execute")
else:
print("\n✓ 字段修复完成")
except Exception as e:
print(f"\n✗ 发生错误: {e}")
import traceback
traceback.print_exc()
if not dry_run:
conn.rollback()
finally:
conn.close()
print("\n数据库连接已关闭")
if __name__ == '__main__':
import sys
dry_run = '--execute' not in sys.argv
if not dry_run:
print("\n⚠ 警告: 这将修改数据库!")
response = input("确认要继续吗? (yes/no): ")
if response.lower() != 'yes':
print("操作已取消")
sys.exit(0)
main(dry_run=dry_run)

View File

@ -0,0 +1,260 @@
"""
修复缺少字段关联的模板
为有 template_code 但没有字段关联的文件节点补充字段关联
"""
import os
import json
import pymysql
from typing import Dict, List
# 数据库连接配置
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'
}
TENANT_ID = 615873064429507639
CREATED_BY = 655162080928945152
UPDATED_BY = 655162080928945152
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 get_templates_without_relations(conn):
"""获取没有字段关联的文件节点"""
cursor = conn.cursor(pymysql.cursors.DictCursor)
sql = """
SELECT
fc.id,
fc.name,
fc.template_code,
fc.input_data,
COUNT(ff.id) as relation_count
FROM f_polic_file_config fc
LEFT JOIN f_polic_file_field ff ON fc.id = ff.file_id AND ff.tenant_id = fc.tenant_id
WHERE fc.tenant_id = %s
AND fc.template_code IS NOT NULL
AND fc.template_code != ''
GROUP BY fc.id, fc.name, fc.template_code, fc.input_data
HAVING relation_count = 0
ORDER BY fc.name
"""
cursor.execute(sql, (TENANT_ID,))
templates = cursor.fetchall()
cursor.close()
return templates
def get_fields_by_code(conn):
"""获取所有字段,按字段编码索引"""
cursor = conn.cursor(pymysql.cursors.DictCursor)
sql = """
SELECT id, name, filed_code, field_type
FROM f_polic_field
WHERE tenant_id = %s
"""
cursor.execute(sql, (TENANT_ID,))
fields = cursor.fetchall()
result = {
'by_code': {},
'by_name': {}
}
for field in fields:
field_code = field['filed_code']
field_name = field['name']
result['by_code'][field_code] = field
result['by_name'][field_name] = field
cursor.close()
return result
def extract_fields_from_input_data(input_data: str) -> List[str]:
"""从 input_data 中提取字段编码列表"""
try:
data = json.loads(input_data) if isinstance(input_data, str) else input_data
if isinstance(data, dict):
return data.get('input_fields', [])
except:
pass
return []
def create_field_relations(conn, file_id: int, field_codes: List[str], field_type: int,
db_fields: Dict, dry_run: bool = True):
"""创建字段关联关系"""
cursor = conn.cursor()
try:
created_count = 0
for field_code in field_codes:
field = db_fields['by_code'].get(field_code)
if not field:
print(f" ⚠ 字段不存在: {field_code}")
continue
if field['field_type'] != field_type:
print(f" ⚠ 字段类型不匹配: {field_code} (期望 {field_type}, 实际 {field['field_type']})")
continue
if not dry_run:
# 检查是否已存在
check_sql = """
SELECT id FROM f_polic_file_field
WHERE tenant_id = %s AND file_id = %s AND filed_id = %s
"""
cursor.execute(check_sql, (TENANT_ID, file_id, field['id']))
existing = cursor.fetchone()
if not existing:
relation_id = generate_id()
insert_sql = """
INSERT INTO f_polic_file_field
(id, tenant_id, file_id, filed_id, created_time, created_by, updated_time, updated_by, state)
VALUES (%s, %s, %s, %s, NOW(), %s, NOW(), %s, %s)
"""
cursor.execute(insert_sql, (
relation_id, TENANT_ID, file_id, field['id'],
CREATED_BY, UPDATED_BY, 1
))
created_count += 1
print(f" ✓ 创建关联: {field['name']} ({field_code})")
else:
created_count += 1
print(f" [模拟] 将创建关联: {field_code}")
if not dry_run:
conn.commit()
return created_count
finally:
cursor.close()
def main():
"""主函数"""
print("="*80)
print("修复缺少字段关联的模板")
print("="*80)
try:
conn = pymysql.connect(**DB_CONFIG)
print("✓ 数据库连接成功\n")
except Exception as e:
print(f"✗ 数据库连接失败: {e}")
return
try:
# 获取没有字段关联的模板
print("查找缺少字段关联的模板...")
templates = get_templates_without_relations(conn)
print(f" 找到 {len(templates)} 个缺少字段关联的文件节点\n")
if not templates:
print("✓ 所有文件节点都有字段关联,无需修复")
return
# 获取所有字段
print("获取字段定义...")
db_fields = get_fields_by_code(conn)
print(f" 找到 {len(db_fields['by_code'])} 个字段\n")
# 显示需要修复的模板
print("需要修复的模板:")
for template in templates:
print(f" - {template['name']} (code: {template['template_code']})")
# 尝试从 input_data 中提取字段
print("\n" + "="*80)
print("分析并修复")
print("="*80)
fixable_count = 0
unfixable_count = 0
for template in templates:
print(f"\n处理: {template['name']}")
print(f" template_code: {template['template_code']}")
input_data = template.get('input_data')
if not input_data:
print(" ⚠ 没有 input_data无法自动修复")
unfixable_count += 1
continue
# 从 input_data 中提取输入字段
input_fields = extract_fields_from_input_data(input_data)
if not input_fields:
print(" ⚠ input_data 中没有 input_fields无法自动修复")
unfixable_count += 1
continue
print(f" 找到 {len(input_fields)} 个输入字段")
fixable_count += 1
# 创建输入字段关联
print(" 创建输入字段关联...")
created = create_field_relations(conn, template['id'], input_fields, 1, db_fields, dry_run=True)
print(f" 将创建 {created} 个输入字段关联")
print("\n" + "="*80)
print("统计")
print("="*80)
print(f" 可修复: {fixable_count}")
print(f" 无法自动修复: {unfixable_count}")
# 询问是否执行
if fixable_count > 0:
print("\n" + "="*80)
response = input("\n是否执行修复?(yes/no默认no): ").strip().lower()
if response == 'yes':
print("\n执行修复...")
for template in templates:
input_data = template.get('input_data')
if not input_data:
continue
input_fields = extract_fields_from_input_data(input_data)
if not input_fields:
continue
print(f"\n修复: {template['name']}")
create_field_relations(conn, template['id'], input_fields, 1, db_fields, dry_run=False)
print("\n" + "="*80)
print("✓ 修复完成!")
print("="*80)
else:
print("\n已取消修复")
else:
print("\n没有可以自动修复的模板")
finally:
conn.close()
print("\n数据库连接已关闭")
if __name__ == '__main__':
main()

View File

@ -0,0 +1,201 @@
"""
只修复真正包含中文的field_code字段
"""
import os
import pymysql
import re
from typing import Dict
# 数据库连接配置
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'
}
TENANT_ID = 615873064429507639
UPDATED_BY = 655162080928945152
# 字段名称到field_code的映射针对剩余的中文字段
FIELD_MAPPING = {
# 谈话相关字段
'拟谈话地点': 'proposed_interview_location',
'拟谈话时间': 'proposed_interview_time',
'谈话事由': 'interview_reason',
'谈话人': 'interviewer',
'谈话人员-安全员': 'interview_personnel_safety_officer',
'谈话人员-组长': 'interview_personnel_leader',
'谈话人员-谈话人员': 'interview_personnel',
'谈话前安全风险评估结果': 'pre_interview_risk_assessment_result',
'谈话地点': 'interview_location',
'谈话次数': 'interview_count',
# 被核查人员相关字段
'被核查人单位及职务': 'target_organization_and_position', # 注意:这个和"被核查人员单位及职务"应该是同一个
'被核查人员交代问题程度': 'target_confession_level',
'被核查人员减压后的表现': 'target_behavior_after_relief',
'被核查人员学历': 'target_education', # 注意:这个和"被核查人员文化程度"可能不同
'被核查人员工作履历': 'target_work_history',
'被核查人员思想负担程度': 'target_mental_burden_level',
'被核查人员职业': 'target_occupation',
'被核查人员谈话中的表现': 'target_behavior_during_interview',
'被核查人员问题严重程度': 'target_issue_severity_level',
'被核查人员风险等级': 'target_risk_level',
'被核查人基本情况': 'target_basic_info',
# 其他字段
'补空人员': 'backup_personnel',
'记录人': 'recorder',
'评估意见': 'assessment_opinion',
}
def is_chinese(text: str) -> bool:
"""判断字符串是否完全或主要包含中文字符"""
if not text:
return False
# 如果包含中文字符且中文字符占比超过50%,认为是中文
chinese_chars = len(re.findall(r'[\u4e00-\u9fff]', text))
total_chars = len(text)
if total_chars == 0:
return False
return chinese_chars / total_chars > 0.3 # 如果中文字符占比超过30%,认为是中文
def fix_chinese_fields(dry_run: bool = True):
"""修复包含中文的field_code字段"""
conn = pymysql.connect(**DB_CONFIG)
cursor = conn.cursor(pymysql.cursors.DictCursor)
print("="*80)
print("修复包含中文的field_code字段")
print("="*80)
if dry_run:
print("\n[DRY RUN模式 - 不会实际修改数据库]")
# 查询所有字段
cursor.execute("""
SELECT id, name, filed_code, field_type, state
FROM f_polic_field
WHERE tenant_id = %s
ORDER BY name
""", (TENANT_ID,))
all_fields = cursor.fetchall()
# 找出field_code包含中文的字段
chinese_fields = []
for field in all_fields:
if field['filed_code'] and is_chinese(field['filed_code']):
chinese_fields.append(field)
print(f"\n找到 {len(chinese_fields)} 个field_code包含中文的字段:\n")
updates = []
for field in chinese_fields:
field_name = field['name']
new_code = FIELD_MAPPING.get(field_name)
if not new_code:
# 如果没有映射生成一个基于名称的code
new_code = field_name.lower()
new_code = new_code.replace('被核查人员', 'target_').replace('被核查人', 'target_')
new_code = new_code.replace('谈话', 'interview_')
new_code = new_code.replace('人员', '')
new_code = new_code.replace('时间', '_time')
new_code = new_code.replace('地点', '_location')
new_code = new_code.replace('问题', '_issue')
new_code = new_code.replace('情况', '_situation')
new_code = new_code.replace('程度', '_level')
new_code = new_code.replace('表现', '_behavior')
new_code = new_code.replace('等级', '_level')
new_code = new_code.replace('履历', '_history')
new_code = new_code.replace('学历', '_education')
new_code = new_code.replace('职业', '_occupation')
new_code = new_code.replace('事由', '_reason')
new_code = new_code.replace('次数', '_count')
new_code = new_code.replace('结果', '_result')
new_code = new_code.replace('意见', '_opinion')
new_code = re.sub(r'[^\w]', '_', new_code)
new_code = re.sub(r'_+', '_', new_code).strip('_')
new_code = new_code.replace('__', '_')
updates.append({
'id': field['id'],
'name': field_name,
'old_code': field['filed_code'],
'new_code': new_code,
'field_type': field['field_type']
})
print(f" ID: {field['id']}")
print(f" 名称: {field_name}")
print(f" 当前field_code: {field['filed_code']}")
print(f" 新field_code: {new_code}")
print()
# 检查是否有重复的new_code
code_to_fields = {}
for update in updates:
code = update['new_code']
if code not in code_to_fields:
code_to_fields[code] = []
code_to_fields[code].append(update)
duplicate_codes = {code: fields_list for code, fields_list in code_to_fields.items()
if len(fields_list) > 1}
if duplicate_codes:
print("\n⚠ 警告以下field_code会重复:")
for code, fields_list in duplicate_codes.items():
print(f" field_code: {code}")
for field in fields_list:
print(f" - ID: {field['id']}, 名称: {field['name']}")
print()
# 执行更新
if not dry_run:
print("开始执行更新...\n")
for update in updates:
cursor.execute("""
UPDATE f_polic_field
SET filed_code = %s, updated_time = NOW(), updated_by = %s
WHERE id = %s
""", (update['new_code'], UPDATED_BY, update['id']))
print(f" ✓ 更新字段 ID {update['id']}: {update['name']}")
print(f" {update['old_code']} -> {update['new_code']}")
conn.commit()
print("\n✓ 更新完成")
else:
print("[DRY RUN] 以上操作不会实际执行")
cursor.close()
conn.close()
return updates
if __name__ == '__main__':
print("是否执行修复?")
print("1. DRY RUN不实际修改数据库")
print("2. 直接执行修复(会修改数据库)")
choice = input("\n请选择 (1/2默认1): ").strip() or "1"
if choice == "2":
print("\n执行实际修复...")
fix_chinese_fields(dry_run=False)
else:
print("\n执行DRY RUN...")
updates = fix_chinese_fields(dry_run=True)
if updates:
confirm = input("\nDRY RUN完成。是否执行实际修复(y/n默认n): ").strip().lower()
if confirm == 'y':
print("\n执行实际修复...")
fix_chinese_fields(dry_run=False)

View File

@ -0,0 +1,191 @@
"""
修复剩余的中文field_code字段
为这些字段生成合适的英文field_code
"""
import os
import pymysql
import re
from typing import Dict
# 数据库连接配置
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'
}
TENANT_ID = 615873064429507639
UPDATED_BY = 655162080928945152
# 字段名称到field_code的映射针对剩余的中文字段
FIELD_MAPPING = {
# 谈话相关字段
'拟谈话地点': 'proposed_interview_location',
'拟谈话时间': 'proposed_interview_time',
'谈话事由': 'interview_reason',
'谈话人': 'interviewer',
'谈话人员-安全员': 'interview_personnel_safety_officer',
'谈话人员-组长': 'interview_personnel_leader',
'谈话人员-谈话人员': 'interview_personnel',
'谈话前安全风险评估结果': 'pre_interview_risk_assessment_result',
'谈话地点': 'interview_location',
'谈话次数': 'interview_count',
# 被核查人员相关字段
'被核查人单位及职务': 'target_organization_and_position', # 注意:这个和"被核查人员单位及职务"应该是同一个
'被核查人员交代问题程度': 'target_confession_level',
'被核查人员减压后的表现': 'target_behavior_after_relief',
'被核查人员学历': 'target_education', # 注意:这个和"被核查人员文化程度"可能不同
'被核查人员工作履历': 'target_work_history',
'被核查人员思想负担程度': 'target_mental_burden_level',
'被核查人员职业': 'target_occupation',
'被核查人员谈话中的表现': 'target_behavior_during_interview',
'被核查人员问题严重程度': 'target_issue_severity_level',
'被核查人员风险等级': 'target_risk_level',
'被核查人基本情况': 'target_basic_info',
# 其他字段
'补空人员': 'backup_personnel',
'记录人': 'recorder',
'评估意见': 'assessment_opinion',
}
def is_chinese(text: str) -> bool:
"""判断字符串是否包含中文字符"""
if not text:
return False
return bool(re.search(r'[\u4e00-\u9fff]', text))
def fix_remaining_fields(dry_run: bool = True):
"""修复剩余的中文field_code字段"""
conn = pymysql.connect(**DB_CONFIG)
cursor = conn.cursor(pymysql.cursors.DictCursor)
print("="*80)
print("修复剩余的中文field_code字段")
print("="*80)
if dry_run:
print("\n[DRY RUN模式 - 不会实际修改数据库]")
# 查询所有包含中文field_code的字段
cursor.execute("""
SELECT id, name, filed_code, field_type, state
FROM f_polic_field
WHERE tenant_id = %s AND filed_code REGEXP '[\\u4e00-\\u9fff]'
ORDER BY name
""", (TENANT_ID,))
fields = cursor.fetchall()
print(f"\n找到 {len(fields)} 个需要修复的字段:\n")
updates = []
for field in fields:
field_name = field['name']
new_code = FIELD_MAPPING.get(field_name)
if not new_code:
# 如果没有映射生成一个基于名称的code
new_code = field_name.lower()
new_code = new_code.replace('被核查人员', 'target_').replace('被核查人', 'target_')
new_code = new_code.replace('谈话', 'interview_')
new_code = new_code.replace('人员', '')
new_code = new_code.replace('时间', '_time')
new_code = new_code.replace('地点', '_location')
new_code = new_code.replace('问题', '_issue')
new_code = new_code.replace('情况', '_situation')
new_code = new_code.replace('程度', '_level')
new_code = new_code.replace('表现', '_behavior')
new_code = new_code.replace('等级', '_level')
new_code = new_code.replace('履历', '_history')
new_code = new_code.replace('学历', '_education')
new_code = new_code.replace('职业', '_occupation')
new_code = new_code.replace('事由', '_reason')
new_code = new_code.replace('次数', '_count')
new_code = new_code.replace('结果', '_result')
new_code = new_code.replace('意见', '_opinion')
new_code = re.sub(r'[^\w]', '_', new_code)
new_code = re.sub(r'_+', '_', new_code).strip('_')
new_code = new_code.replace('__', '_')
updates.append({
'id': field['id'],
'name': field_name,
'old_code': field['filed_code'],
'new_code': new_code,
'field_type': field['field_type']
})
print(f" ID: {field['id']}")
print(f" 名称: {field_name}")
print(f" 当前field_code: {field['filed_code']}")
print(f" 新field_code: {new_code}")
print()
# 检查是否有重复的new_code
code_to_fields = {}
for update in updates:
code = update['new_code']
if code not in code_to_fields:
code_to_fields[code] = []
code_to_fields[code].append(update)
duplicate_codes = {code: fields_list for code, fields_list in code_to_fields.items()
if len(fields_list) > 1}
if duplicate_codes:
print("\n⚠ 警告以下field_code会重复:")
for code, fields_list in duplicate_codes.items():
print(f" field_code: {code}")
for field in fields_list:
print(f" - ID: {field['id']}, 名称: {field['name']}")
print()
# 执行更新
if not dry_run:
print("开始执行更新...\n")
for update in updates:
cursor.execute("""
UPDATE f_polic_field
SET filed_code = %s, updated_time = NOW(), updated_by = %s
WHERE id = %s
""", (update['new_code'], UPDATED_BY, update['id']))
print(f" ✓ 更新字段 ID {update['id']}: {update['name']}")
print(f" {update['old_code']} -> {update['new_code']}")
conn.commit()
print("\n✓ 更新完成")
else:
print("[DRY RUN] 以上操作不会实际执行")
cursor.close()
conn.close()
return updates
if __name__ == '__main__':
print("是否执行修复?")
print("1. DRY RUN不实际修改数据库")
print("2. 直接执行修复(会修改数据库)")
choice = input("\n请选择 (1/2默认1): ").strip() or "1"
if choice == "2":
print("\n执行实际修复...")
fix_remaining_fields(dry_run=False)
else:
print("\n执行DRY RUN...")
updates = fix_remaining_fields(dry_run=True)
if updates:
confirm = input("\nDRY RUN完成。是否执行实际修复(y/n默认n): ").strip().lower()
if confirm == 'y':
print("\n执行实际修复...")
fix_remaining_fields(dry_run=False)

View File

@ -0,0 +1,272 @@
"""
修复"1.请示报告卡(初核谈话)"模板的input_data字段
分析模板占位符根据数据库字段对应关系生成input_data并更新数据库
"""
import pymysql
import json
import os
import re
from datetime import datetime
from pathlib import Path
from docx import Document
# 数据库连接配置
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'
}
TENANT_ID = 615873064429507639
UPDATED_BY = 655162080928945152
CURRENT_TIME = datetime.now()
# 模板信息
TEMPLATE_NAME = "1.请示报告卡(初核谈话)"
TEMPLATE_CODE = "REPORT_CARD_INTERVIEW"
BUSINESS_TYPE = "INVESTIGATION"
TEMPLATE_FILE_PATH = "template_finish/2-初核模版/2.谈话审批/走读式谈话审批/1.请示报告卡(初核谈话).docx"
def extract_placeholders_from_docx(file_path):
"""从docx文件中提取所有占位符"""
placeholders = set()
pattern = r'\{\{([^}]+)\}\}'
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 get_template_config(conn):
"""查询模板配置"""
cursor = conn.cursor(pymysql.cursors.DictCursor)
sql = """
SELECT id, name, template_code, input_data, file_path, state
FROM f_polic_file_config
WHERE tenant_id = %s AND name = %s
"""
cursor.execute(sql, (TENANT_ID, TEMPLATE_NAME))
config = cursor.fetchone()
cursor.close()
return config
def get_template_fields(conn, file_config_id):
"""查询模板关联的字段"""
cursor = conn.cursor(pymysql.cursors.DictCursor)
sql = """
SELECT f.id, f.name, f.filed_code as field_code, f.field_type
FROM f_polic_field f
INNER JOIN f_polic_file_field ff ON f.id = ff.filed_id
WHERE ff.file_id = %s
AND f.tenant_id = %s
ORDER BY f.field_type, f.name
"""
cursor.execute(sql, (file_config_id, TENANT_ID))
fields = cursor.fetchall()
cursor.close()
return fields
def verify_placeholders_in_database(conn, placeholders):
"""验证占位符是否在数据库中存在对应的字段"""
if not placeholders:
return {}
cursor = conn.cursor(pymysql.cursors.DictCursor)
placeholders_list = list(placeholders)
placeholders_str = ','.join(['%s'] * len(placeholders_list))
# 查询所有字段(包括未启用的)
sql = f"""
SELECT id, name, filed_code as field_code, field_type, state
FROM f_polic_field
WHERE tenant_id = %s
AND filed_code IN ({placeholders_str})
"""
cursor.execute(sql, [TENANT_ID] + placeholders_list)
fields = cursor.fetchall()
cursor.close()
# 构建字段映射
field_map = {f['field_code']: f for f in fields}
# 检查缺失的字段
missing_fields = set(placeholders) - set(field_map.keys())
return {
'found_fields': field_map,
'missing_fields': missing_fields
}
def update_input_data(conn, file_config_id, input_data):
"""更新input_data字段"""
cursor = conn.cursor()
input_data_str = json.dumps(input_data, ensure_ascii=False)
update_sql = """
UPDATE f_polic_file_config
SET input_data = %s, updated_time = %s, updated_by = %s
WHERE id = %s
"""
cursor.execute(update_sql, (input_data_str, CURRENT_TIME, UPDATED_BY, file_config_id))
conn.commit()
cursor.close()
def main():
"""主函数"""
print("="*80)
print("修复'1.请示报告卡(初核谈话)'模板的input_data字段")
print("="*80)
print()
# 1. 检查模板文件是否存在
template_path = Path(TEMPLATE_FILE_PATH)
if not template_path.exists():
print(f"✗ 错误: 模板文件不存在 - {TEMPLATE_FILE_PATH}")
return
print(f"✓ 找到模板文件: {TEMPLATE_FILE_PATH}")
# 2. 提取占位符
print("\n正在提取占位符...")
placeholders = extract_placeholders_from_docx(str(template_path))
print(f"✓ 找到 {len(placeholders)} 个占位符:")
for i, placeholder in enumerate(placeholders, 1):
print(f" {i}. {{{{ {placeholder} }}}}")
# 3. 连接数据库
print("\n正在连接数据库...")
try:
conn = pymysql.connect(**DB_CONFIG)
print("✓ 数据库连接成功")
except Exception as e:
print(f"✗ 数据库连接失败: {str(e)}")
return
try:
# 4. 查询模板配置
print(f"\n正在查询模板配置: {TEMPLATE_NAME}")
config = get_template_config(conn)
if not config:
print(f"✗ 未找到模板配置: {TEMPLATE_NAME}")
return
print(f"✓ 找到模板配置:")
print(f" ID: {config['id']}")
print(f" 名称: {config['name']}")
print(f" 当前template_code: {config.get('template_code', 'NULL')}")
print(f" 当前input_data: {config.get('input_data', 'NULL')}")
print(f" 文件路径: {config.get('file_path', 'NULL')}")
print(f" 状态: {config.get('state', 0)}")
file_config_id = config['id']
# 5. 查询模板关联的字段
print(f"\n正在查询模板关联的字段...")
template_fields = get_template_fields(conn, file_config_id)
print(f"✓ 找到 {len(template_fields)} 个关联字段:")
for field in template_fields:
field_type_str = "输出字段" if field['field_type'] == 2 else "输入字段"
print(f" - {field['name']} ({field['field_code']}) [{field_type_str}]")
# 6. 验证占位符是否在数据库中存在
print(f"\n正在验证占位符...")
verification = verify_placeholders_in_database(conn, placeholders)
found_fields = verification['found_fields']
missing_fields = verification['missing_fields']
print(f"✓ 在数据库中找到 {len(found_fields)} 个字段:")
for field_code, field in found_fields.items():
field_type_str = "输出字段" if field['field_type'] == 2 else "输入字段"
state_str = "启用" if field.get('state', 0) == 1 else "未启用"
print(f" - {field['name']} ({field_code}) [{field_type_str}] [状态: {state_str}]")
if missing_fields:
print(f"\n⚠ 警告: 以下占位符在数据库中未找到对应字段:")
for field_code in missing_fields:
print(f" - {field_code}")
print("\n这些占位符仍会被包含在input_data中但可能无法正确填充。")
# 7. 生成input_data
print(f"\n正在生成input_data...")
input_data = {
'template_code': TEMPLATE_CODE,
'business_type': BUSINESS_TYPE,
'placeholders': placeholders
}
print(f"✓ input_data内容:")
print(json.dumps(input_data, ensure_ascii=False, indent=2))
# 8. 更新数据库
print(f"\n正在更新数据库...")
update_input_data(conn, file_config_id, input_data)
print(f"✓ 更新成功!")
# 9. 验证更新结果
print(f"\n正在验证更新结果...")
updated_config = get_template_config(conn)
if updated_config:
try:
updated_input_data = json.loads(updated_config['input_data'])
if updated_input_data.get('template_code') == TEMPLATE_CODE:
print(f"✓ 验证成功: template_code = {TEMPLATE_CODE}")
if updated_input_data.get('business_type') == BUSINESS_TYPE:
print(f"✓ 验证成功: business_type = {BUSINESS_TYPE}")
if set(updated_input_data.get('placeholders', [])) == set(placeholders):
print(f"✓ 验证成功: placeholders 匹配")
except Exception as e:
print(f"⚠ 验证时出错: {str(e)}")
print("\n" + "="*80)
print("修复完成!")
print("="*80)
except Exception as e:
print(f"\n✗ 处理失败: {str(e)}")
import traceback
traceback.print_exc()
finally:
conn.close()
if __name__ == '__main__':
main()

124
generate_download_urls.py Normal file
View File

@ -0,0 +1,124 @@
"""
为指定的文件路径生成 MinIO 预签名下载 URL
"""
import sys
import io
from minio import Minio
from datetime import timedelta
# 设置输出编码为UTF-8避免Windows控制台编码问题
if sys.platform == 'win32':
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8')
# MinIO连接配置
MINIO_CONFIG = {
'endpoint': 'minio.datacubeworld.com:9000',
'access_key': 'JOLXFXny3avFSzB0uRA5',
'secret_key': 'G1BR8jStNfovkfH5ou39EmPl34E4l7dGrnd3Cz0I',
'secure': True
}
BUCKET_NAME = 'finyx'
# 文件相对路径列表
FILE_PATHS = [
'/615873064429507639/20251211101046/1_张三.docx',
'/615873064429507639/20251211101046/1_张三.docx'
]
def generate_download_urls():
"""为文件路径列表生成下载 URL"""
print("="*80)
print("生成 MinIO 下载链接")
print("="*80)
try:
# 创建MinIO客户端
client = Minio(
MINIO_CONFIG['endpoint'],
access_key=MINIO_CONFIG['access_key'],
secret_key=MINIO_CONFIG['secret_key'],
secure=MINIO_CONFIG['secure']
)
print(f"\n存储桶: {BUCKET_NAME}")
print(f"端点: {MINIO_CONFIG['endpoint']}")
print(f"使用HTTPS: {MINIO_CONFIG['secure']}\n")
results = []
for file_path in FILE_PATHS:
# 去掉开头的斜杠,得到对象名称
object_name = file_path.lstrip('/')
print("-"*80)
print(f"文件: {file_path}")
print(f"对象名称: {object_name}")
try:
# 检查文件是否存在
stat = client.stat_object(BUCKET_NAME, object_name)
print(f"[OK] 文件存在")
print(f" 文件大小: {stat.size:,} 字节")
print(f" 最后修改: {stat.last_modified}")
# 生成预签名URL7天有效期
url = client.presigned_get_object(
BUCKET_NAME,
object_name,
expires=timedelta(days=7)
)
print(f"[OK] 预签名URL生成成功7天有效")
print(f"\n下载链接:")
print(f"{url}\n")
results.append({
'file_path': file_path,
'object_name': object_name,
'url': url,
'size': stat.size,
'exists': True
})
except Exception as e:
print(f"[ERROR] 错误: {e}\n")
results.append({
'file_path': file_path,
'object_name': object_name,
'url': None,
'exists': False,
'error': str(e)
})
# 输出汇总
print("\n" + "="*80)
print("下载链接汇总")
print("="*80)
for i, result in enumerate(results, 1):
print(f"\n{i}. {result['file_path']}")
if result['exists']:
print(f" [OK] 文件存在")
print(f" 下载链接: {result['url']}")
else:
print(f" [ERROR] 文件不存在或无法访问")
if 'error' in result:
print(f" 错误: {result['error']}")
print("\n" + "="*80)
print("完成")
print("="*80)
return results
except Exception as e:
print(f"\n[ERROR] 连接MinIO失败: {e}")
import traceback
traceback.print_exc()
return None
if __name__ == '__main__':
generate_download_urls()

View File

@ -0,0 +1,219 @@
"""
生成模板 file_id 和关联关系的详细报告
重点检查每个模板的 file_id 是否正确以及 f_polic_file_field 表的关联关系
"""
import sys
import pymysql
from pathlib import Path
from typing import Dict, List
from collections import defaultdict
# 设置控制台编码为UTF-8Windows兼容
if sys.platform == 'win32':
try:
sys.stdout.reconfigure(encoding='utf-8')
sys.stderr.reconfigure(encoding='utf-8')
except:
pass
# 数据库连接配置
DB_CONFIG = {
'host': '152.136.177.240',
'port': 5012,
'user': 'finyx',
'password': '6QsGK6MpePZDE57Z',
'database': 'finyx',
'charset': 'utf8mb4'
}
TENANT_ID = 615873064429507639
def generate_detailed_report():
"""生成详细的 file_id 和关联关系报告"""
print("="*80)
print("模板 file_id 和关联关系详细报告")
print("="*80)
# 连接数据库
try:
conn = pymysql.connect(**DB_CONFIG)
print("\n[OK] 数据库连接成功\n")
except Exception as e:
print(f"\n[ERROR] 数据库连接失败: {e}")
return
cursor = conn.cursor(pymysql.cursors.DictCursor)
try:
# 1. 查询所有有 file_path 的模板(实际模板文件,不是目录节点)
cursor.execute("""
SELECT id, name, template_code, file_path, state, parent_id
FROM f_polic_file_config
WHERE tenant_id = %s AND file_path IS NOT NULL AND file_path != ''
ORDER BY name, id
""", (TENANT_ID,))
all_templates = cursor.fetchall()
print(f"总模板数(有 file_path: {len(all_templates)}\n")
# 2. 查询每个模板的关联字段
template_field_map = defaultdict(list)
cursor.execute("""
SELECT
fff.file_id,
fff.filed_id,
fff.state as relation_state,
fc.name as template_name,
fc.template_code,
f.name as field_name,
f.filed_code,
f.field_type,
CASE
WHEN f.field_type = 1 THEN '输入字段'
WHEN f.field_type = 2 THEN '输出字段'
ELSE '未知'
END as field_type_name
FROM f_polic_file_field fff
INNER JOIN f_polic_file_config fc ON fff.file_id = fc.id AND fff.tenant_id = fc.tenant_id
INNER JOIN f_polic_field f ON fff.filed_id = f.id AND fff.tenant_id = f.tenant_id
WHERE fff.tenant_id = %s
ORDER BY fff.file_id, f.field_type, f.name
""", (TENANT_ID,))
all_relations = cursor.fetchall()
for rel in all_relations:
template_field_map[rel['file_id']].append(rel)
# 3. 按模板分组显示
print("="*80)
print("每个模板的 file_id 和关联字段详情")
print("="*80)
# 按名称分组,显示重复的模板
templates_by_name = defaultdict(list)
for template in all_templates:
templates_by_name[template['name']].append(template)
duplicate_templates = {name: tmpls for name, tmpls in templates_by_name.items() if len(tmpls) > 1}
if duplicate_templates:
print("\n[WARN] 发现重复名称的模板:\n")
for name, tmpls in duplicate_templates.items():
print(f" 模板名称: {name}")
for tmpl in tmpls:
field_count = len(template_field_map.get(tmpl['id'], []))
input_count = sum(1 for f in template_field_map.get(tmpl['id'], []) if f['field_type'] == 1)
output_count = sum(1 for f in template_field_map.get(tmpl['id'], []) if f['field_type'] == 2)
print(f" - file_id: {tmpl['id']}")
print(f" template_code: {tmpl.get('template_code', 'N/A')}")
print(f" file_path: {tmpl.get('file_path', 'N/A')}")
print(f" 关联字段: 总计 {field_count} 个 (输入 {input_count}, 输出 {output_count})")
print()
# 4. 显示每个模板的详细信息
print("\n" + "="*80)
print("所有模板的 file_id 和关联字段统计")
print("="*80)
for template in all_templates:
file_id = template['id']
name = template['name']
template_code = template.get('template_code', 'N/A')
file_path = template.get('file_path', 'N/A')
fields = template_field_map.get(file_id, [])
input_fields = [f for f in fields if f['field_type'] == 1]
output_fields = [f for f in fields if f['field_type'] == 2]
print(f"\n模板: {name}")
print(f" file_id: {file_id}")
print(f" template_code: {template_code}")
print(f" file_path: {file_path}")
print(f" 关联字段: 总计 {len(fields)}")
print(f" - 输入字段 (field_type=1): {len(input_fields)}")
print(f" - 输出字段 (field_type=2): {len(output_fields)}")
if len(fields) == 0:
print(f" [WARN] 该模板没有关联任何字段")
# 5. 检查关联关系的完整性
print("\n" + "="*80)
print("关联关系完整性检查")
print("="*80)
# 检查是否有 file_id 在 f_polic_file_field 中但没有对应的文件配置
cursor.execute("""
SELECT DISTINCT fff.file_id
FROM f_polic_file_field fff
LEFT JOIN f_polic_file_config fc ON fff.file_id = fc.id AND fff.tenant_id = fc.tenant_id
WHERE fff.tenant_id = %s AND fc.id IS NULL
""", (TENANT_ID,))
orphan_file_ids = cursor.fetchall()
if orphan_file_ids:
print(f"\n[ERROR] 发现孤立的 file_id在 f_polic_file_field 中但不在 f_polic_file_config 中):")
for item in orphan_file_ids:
print(f" - file_id: {item['file_id']}")
else:
print("\n[OK] 所有关联关系的 file_id 都有效")
# 检查是否有 filed_id 在 f_polic_file_field 中但没有对应的字段
cursor.execute("""
SELECT DISTINCT fff.filed_id
FROM f_polic_file_field fff
LEFT JOIN f_polic_field f ON fff.filed_id = f.id AND fff.tenant_id = f.tenant_id
WHERE fff.tenant_id = %s AND f.id IS NULL
""", (TENANT_ID,))
orphan_field_ids = cursor.fetchall()
if orphan_field_ids:
print(f"\n[ERROR] 发现孤立的 filed_id在 f_polic_file_field 中但不在 f_polic_field 中):")
for item in orphan_field_ids:
print(f" - filed_id: {item['filed_id']}")
else:
print("\n[OK] 所有关联关系的 filed_id 都有效")
# 6. 统计汇总
print("\n" + "="*80)
print("统计汇总")
print("="*80)
total_templates = len(all_templates)
templates_with_fields = len([t for t in all_templates if len(template_field_map.get(t['id'], [])) > 0])
templates_without_fields = total_templates - templates_with_fields
total_relations = len(all_relations)
total_input_relations = sum(1 for r in all_relations if r['field_type'] == 1)
total_output_relations = sum(1 for r in all_relations if r['field_type'] == 2)
print(f"\n模板统计:")
print(f" 总模板数: {total_templates}")
print(f" 有关联字段的模板: {templates_with_fields}")
print(f" 无关联字段的模板: {templates_without_fields}")
print(f"\n关联关系统计:")
print(f" 总关联关系数: {total_relations}")
print(f" 输入字段关联: {total_input_relations}")
print(f" 输出字段关联: {total_output_relations}")
if duplicate_templates:
print(f"\n[WARN] 发现 {len(duplicate_templates)} 个模板名称有重复记录")
print(" 建议: 确认每个模板应该使用哪个 file_id并清理重复记录")
if templates_without_fields:
print(f"\n[WARN] 发现 {templates_without_fields} 个模板没有关联任何字段")
print(" 建议: 检查这些模板是否需要关联字段")
finally:
cursor.close()
conn.close()
print("\n数据库连接已关闭")
if __name__ == '__main__':
generate_detailed_report()

64
get_available_file_ids.py Normal file
View File

@ -0,0 +1,64 @@
"""
获取所有可用的文件ID列表用于测试
"""
import pymysql
import os
# 数据库连接配置
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'
}
TENANT_ID = 615873064429507639
def get_available_file_configs():
"""获取所有可用的文件配置"""
conn = pymysql.connect(**DB_CONFIG)
cursor = conn.cursor(pymysql.cursors.DictCursor)
try:
sql = """
SELECT id, name, file_path, state
FROM f_polic_file_config
WHERE tenant_id = %s
AND state = 1
ORDER BY name
"""
cursor.execute(sql, (TENANT_ID,))
configs = cursor.fetchall()
print("="*80)
print("可用的文件配置列表state=1")
print("="*80)
print(f"\n共找到 {len(configs)} 个启用的文件配置:\n")
for i, config in enumerate(configs, 1):
print(f"{i}. ID: {config['id']}")
print(f" 名称: {config['name']}")
print(f" 文件路径: {config['file_path'] or '(空)'}")
print()
# 输出JSON格式方便复制
print("\n" + "="*80)
print("JSON格式可用于测试:")
print("="*80)
print("[")
for i, config in enumerate(configs):
comma = "," if i < len(configs) - 1 else ""
print(f' {{"fileId": {config["id"]}, "fileName": "{config["name"]}.doc"}}{comma}')
print("]")
return configs
finally:
cursor.close()
conn.close()
if __name__ == '__main__':
get_available_file_configs()

View File

@ -0,0 +1,478 @@
"""
改进的匹配和更新脚本
增强匹配逻辑能够匹配数据库中的已有数据
"""
import os
import json
import pymysql
import re
from pathlib import Path
from typing import Dict, List, Optional, Tuple
from datetime import datetime
# 数据库连接配置
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'
}
TENANT_ID = 615873064429507639
CREATED_BY = 655162080928945152
UPDATED_BY = 655162080928945152
# 项目根目录
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 normalize_name(name: str) -> str:
"""标准化名称,用于模糊匹配"""
# 去掉开头的编号(如 "1."、"2."、"8-1" 等)
name = re.sub(r'^\d+[\.\-]\s*', '', name)
# 去掉括号及其内容(如 "XXX"、"(初核谈话)" 等)
name = re.sub(r'[(].*?[)]', '', name)
# 去掉空格和特殊字符
name = name.strip()
return name
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 scan_directory_structure(base_dir: Path) -> Dict:
"""扫描目录结构,构建树状层级"""
structure = {
'directories': {},
'files': {}
}
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)
structure['files'][str(path)] = {
'name': file_name,
'parent': parent_path,
'level': level,
'template_code': doc_config['template_code'] if doc_config else None,
'full_path': str(path),
'normalized_name': normalize_name(file_name)
}
elif path.is_dir():
dir_name = path.name
structure['directories'][str(path)] = {
'name': dir_name,
'parent': parent_path,
'level': level,
'normalized_name': normalize_name(dir_name)
}
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 structure
def get_existing_data(conn) -> Dict:
"""获取数据库中的现有数据,增强匹配能力"""
cursor = conn.cursor(pymysql.cursors.DictCursor)
sql = """
SELECT id, name, parent_id, template_code, input_data, file_path, state
FROM f_polic_file_config
WHERE tenant_id = %s
"""
cursor.execute(sql, (TENANT_ID,))
configs = cursor.fetchall()
result = {
'by_id': {},
'by_name': {},
'by_template_code': {},
'by_normalized_name': {} # 新增:标准化名称索引
}
for config in configs:
config_id = config['id']
config_name = config['name']
# 提取 template_code
template_code = config.get('template_code')
if not template_code and config.get('input_data'):
try:
input_data = json.loads(config['input_data']) if isinstance(config['input_data'], str) else config['input_data']
if isinstance(input_data, dict):
template_code = input_data.get('template_code')
except:
pass
config['extracted_template_code'] = template_code
config['normalized_name'] = normalize_name(config_name)
result['by_id'][config_id] = config
result['by_name'][config_name] = config
if template_code:
if template_code not in result['by_template_code']:
result['by_template_code'][template_code] = config
# 标准化名称索引(可能有多个记录匹配同一个标准化名称)
normalized = config['normalized_name']
if normalized not in result['by_normalized_name']:
result['by_normalized_name'][normalized] = []
result['by_normalized_name'][normalized].append(config)
cursor.close()
return result
def find_matching_config(file_info: Dict, existing_data: Dict) -> Optional[Dict]:
"""
查找匹配的数据库记录
优先级1. template_code 精确匹配 2. 名称精确匹配 3. 标准化名称匹配
"""
template_code = file_info.get('template_code')
file_name = file_info['name']
normalized_name = file_info.get('normalized_name', normalize_name(file_name))
# 优先级1: template_code 精确匹配
if template_code:
matched = existing_data['by_template_code'].get(template_code)
if matched:
return matched
# 优先级2: 名称精确匹配
matched = existing_data['by_name'].get(file_name)
if matched:
return matched
# 优先级3: 标准化名称匹配
candidates = existing_data['by_normalized_name'].get(normalized_name, [])
if candidates:
# 如果有多个候选,优先选择有正确 template_code 的
for candidate in candidates:
if candidate.get('extracted_template_code') == template_code:
return candidate
# 否则返回第一个
return candidates[0]
return None
def plan_tree_structure(dir_structure: Dict, existing_data: Dict) -> List[Dict]:
"""规划树状结构,使用改进的匹配逻辑"""
plan = []
directories = sorted(dir_structure['directories'].items(),
key=lambda x: (x[1]['level'], x[0]))
files = sorted(dir_structure['files'].items(),
key=lambda x: (x[1]['level'], x[0]))
dir_id_map = {}
# 处理目录
for dir_path, dir_info in directories:
dir_name = dir_info['name']
parent_path = dir_info['parent']
level = dir_info['level']
parent_id = None
if parent_path:
parent_id = dir_id_map.get(parent_path)
# 查找匹配的数据库记录
matched = find_matching_config(dir_info, existing_data)
if matched:
plan.append({
'type': 'directory',
'name': dir_name,
'parent_name': dir_structure['directories'].get(parent_path, {}).get('name') if parent_path else None,
'parent_id': parent_id,
'level': level,
'action': 'update',
'config_id': matched['id'],
'current_parent_id': matched.get('parent_id'),
'matched_by': 'existing'
})
dir_id_map[dir_path] = matched['id']
else:
new_id = generate_id()
plan.append({
'type': 'directory',
'name': dir_name,
'parent_name': dir_structure['directories'].get(parent_path, {}).get('name') if parent_path else None,
'parent_id': parent_id,
'level': level,
'action': 'create',
'config_id': new_id,
'current_parent_id': None,
'matched_by': 'new'
})
dir_id_map[dir_path] = new_id
# 处理文件
for file_path, file_info in files:
file_name = file_info['name']
parent_path = file_info['parent']
level = file_info['level']
template_code = file_info['template_code']
parent_id = dir_id_map.get(parent_path) if parent_path else None
# 查找匹配的数据库记录
matched = find_matching_config(file_info, existing_data)
if matched:
plan.append({
'type': 'file',
'name': file_name,
'parent_name': dir_structure['directories'].get(parent_path, {}).get('name') if parent_path else None,
'parent_id': parent_id,
'level': level,
'action': 'update',
'config_id': matched['id'],
'template_code': template_code,
'current_parent_id': matched.get('parent_id'),
'matched_by': 'existing'
})
else:
new_id = generate_id()
plan.append({
'type': 'file',
'name': file_name,
'parent_name': dir_structure['directories'].get(parent_path, {}).get('name') if parent_path else None,
'parent_id': parent_id,
'level': level,
'action': 'create',
'config_id': new_id,
'template_code': template_code,
'current_parent_id': None,
'matched_by': 'new'
})
return plan
def print_matching_report(plan: List[Dict]):
"""打印匹配报告"""
print("\n" + "="*80)
print("匹配报告")
print("="*80)
matched = [p for p in plan if p.get('matched_by') == 'existing']
unmatched = [p for p in plan if p.get('matched_by') == 'new']
print(f"\n已匹配的记录: {len(matched)}")
print(f"未匹配的记录(将创建): {len(unmatched)}\n")
if unmatched:
print("未匹配的记录列表:")
for item in unmatched:
print(f" - {item['name']} ({item['type']})")
print("\n匹配详情:")
by_level = {}
for item in plan:
level = item['level']
if level not in by_level:
by_level[level] = []
by_level[level].append(item)
for level in sorted(by_level.keys()):
print(f"\n【层级 {level}")
for item in by_level[level]:
indent = " " * level
match_status = "" if item.get('matched_by') == 'existing' else ""
print(f"{indent}{match_status} {item['name']} (ID: {item['config_id']})")
if item.get('parent_name'):
print(f"{indent} 父节点: {item['parent_name']}")
if item['action'] == 'update':
current = item.get('current_parent_id', 'None')
new = item.get('parent_id', 'None')
if current != new:
print(f"{indent} parent_id: {current}{new}")
def main():
"""主函数"""
print("="*80)
print("改进的模板树状结构分析和更新")
print("="*80)
try:
conn = pymysql.connect(**DB_CONFIG)
print("✓ 数据库连接成功\n")
except Exception as e:
print(f"✗ 数据库连接失败: {e}")
return
try:
print("扫描目录结构...")
dir_structure = scan_directory_structure(TEMPLATES_DIR)
print(f" 找到 {len(dir_structure['directories'])} 个目录")
print(f" 找到 {len(dir_structure['files'])} 个文件\n")
print("获取数据库现有数据...")
existing_data = get_existing_data(conn)
print(f" 数据库中有 {len(existing_data['by_id'])} 条记录\n")
print("规划树状结构(使用改进的匹配逻辑)...")
plan = plan_tree_structure(dir_structure, existing_data)
print(f" 生成 {len(plan)} 个更新计划\n")
print_matching_report(plan)
# 询问是否继续
print("\n" + "="*80)
response = input("\n是否生成更新SQL脚本(yes/no默认no): ").strip().lower()
if response == 'yes':
from analyze_and_update_template_tree import generate_update_sql
sql_file = generate_update_sql(plan)
print(f"\n✓ SQL脚本已生成: {sql_file}")
else:
print("\n已取消")
finally:
conn.close()
if __name__ == '__main__':
main()

View File

@ -0,0 +1,544 @@
"""
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()

View File

@ -0,0 +1,318 @@
"""
模板字段关联查询示例脚本
演示如何查询模板关联的输入和输出字段
"""
import pymysql
import os
from typing import Dict, List, Optional
# 数据库连接配置
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'
}
TENANT_ID = 615873064429507639
def get_template_fields_by_name(template_name: str) -> Optional[Dict]:
"""
根据模板名称获取关联的字段
Args:
template_name: 模板名称 '初步核实审批表'
Returns:
dict: 包含 template_id, template_name, input_fields output_fields 的字典
"""
conn = pymysql.connect(**DB_CONFIG)
cursor = conn.cursor(pymysql.cursors.DictCursor)
try:
sql = """
SELECT
fc.id AS template_id,
fc.name AS template_name,
f.id AS field_id,
f.name AS field_name,
f.filed_code AS field_code,
f.field_type
FROM f_polic_file_config fc
INNER JOIN f_polic_file_field fff ON fc.id = fff.file_id
INNER JOIN f_polic_field f ON fff.filed_id = f.id
WHERE fc.tenant_id = %s
AND fc.name = %s
AND fc.state = 1
AND fff.state = 1
AND f.state = 1
ORDER BY f.field_type, f.name
"""
cursor.execute(sql, (TENANT_ID, template_name))
rows = cursor.fetchall()
if not rows:
return None
result = {
'template_id': rows[0]['template_id'],
'template_name': rows[0]['template_name'],
'input_fields': [],
'output_fields': []
}
for row in rows:
field_info = {
'id': row['field_id'],
'name': row['field_name'],
'field_code': row['field_code'],
'field_type': row['field_type']
}
if row['field_type'] == 1:
result['input_fields'].append(field_info)
elif row['field_type'] == 2:
result['output_fields'].append(field_info)
return result
finally:
cursor.close()
conn.close()
def get_template_fields_by_id(template_id: int) -> Optional[Dict]:
"""
根据模板ID获取关联的字段
Args:
template_id: 模板ID
Returns:
dict: 包含 template_id, template_name, input_fields output_fields 的字典
"""
conn = pymysql.connect(**DB_CONFIG)
cursor = conn.cursor(pymysql.cursors.DictCursor)
try:
# 先获取模板名称
sql_template = """
SELECT id, name
FROM f_polic_file_config
WHERE id = %s AND tenant_id = %s AND state = 1
"""
cursor.execute(sql_template, (template_id, TENANT_ID))
template = cursor.fetchone()
if not template:
return None
# 获取字段
sql_fields = """
SELECT
f.id AS field_id,
f.name AS field_name,
f.filed_code AS field_code,
f.field_type
FROM f_polic_file_field fff
INNER JOIN f_polic_field f ON fff.filed_id = f.id
WHERE fff.file_id = %s
AND fff.tenant_id = %s
AND fff.state = 1
AND f.state = 1
ORDER BY f.field_type, f.name
"""
cursor.execute(sql_fields, (template_id, TENANT_ID))
rows = cursor.fetchall()
result = {
'template_id': template['id'],
'template_name': template['name'],
'input_fields': [],
'output_fields': []
}
for row in rows:
field_info = {
'id': row['field_id'],
'name': row['field_name'],
'field_code': row['field_code'],
'field_type': row['field_type']
}
if row['field_type'] == 1:
result['input_fields'].append(field_info)
elif row['field_type'] == 2:
result['output_fields'].append(field_info)
return result
finally:
cursor.close()
conn.close()
def get_all_templates_with_field_stats() -> List[Dict]:
"""
获取所有模板及其字段统计信息
Returns:
list: 模板列表每个模板包含字段统计
"""
conn = pymysql.connect(**DB_CONFIG)
cursor = conn.cursor(pymysql.cursors.DictCursor)
try:
sql = """
SELECT
fc.id AS template_id,
fc.name AS template_name,
COUNT(DISTINCT CASE WHEN f.field_type = 1 THEN f.id END) AS input_field_count,
COUNT(DISTINCT CASE WHEN f.field_type = 2 THEN f.id END) AS output_field_count,
COUNT(DISTINCT f.id) AS total_field_count
FROM f_polic_file_config fc
LEFT JOIN f_polic_file_field fff ON fc.id = fff.file_id AND fff.state = 1
LEFT JOIN f_polic_field f ON fff.filed_id = f.id AND f.state = 1
WHERE fc.tenant_id = %s
AND fc.state = 1
GROUP BY fc.id, fc.name
ORDER BY fc.name
"""
cursor.execute(sql, (TENANT_ID,))
templates = cursor.fetchall()
return [
{
'template_id': t['template_id'],
'template_name': t['template_name'],
'input_field_count': t['input_field_count'] or 0,
'output_field_count': t['output_field_count'] or 0,
'total_field_count': t['total_field_count'] or 0
}
for t in templates
]
finally:
cursor.close()
conn.close()
def find_templates_using_field(field_code: str) -> List[Dict]:
"""
查找使用特定字段的所有模板
Args:
field_code: 字段编码 'target_name'
Returns:
list: 使用该字段的模板列表
"""
conn = pymysql.connect(**DB_CONFIG)
cursor = conn.cursor(pymysql.cursors.DictCursor)
try:
sql = """
SELECT DISTINCT
fc.id AS template_id,
fc.name AS template_name
FROM f_polic_file_config fc
INNER JOIN f_polic_file_field fff ON fc.id = fff.file_id
INNER JOIN f_polic_field f ON fff.filed_id = f.id
WHERE fc.tenant_id = %s
AND f.tenant_id = %s
AND f.filed_code = %s
AND fc.state = 1
AND fff.state = 1
AND f.state = 1
ORDER BY fc.name
"""
cursor.execute(sql, (TENANT_ID, TENANT_ID, field_code))
templates = cursor.fetchall()
return [
{
'template_id': t['template_id'],
'template_name': t['template_name']
}
for t in templates
]
finally:
cursor.close()
conn.close()
def print_template_fields(result: Dict):
"""打印模板字段信息"""
if not result:
print("未找到模板")
return
print("="*80)
print(f"模板: {result['template_name']} (ID: {result['template_id']})")
print("="*80)
print(f"\n输入字段 ({len(result['input_fields'])} 个):")
if result['input_fields']:
for field in result['input_fields']:
print(f" - {field['name']} ({field['field_code']})")
else:
print(" (无)")
print(f"\n输出字段 ({len(result['output_fields'])} 个):")
if result['output_fields']:
for field in result['output_fields']:
print(f" - {field['name']} ({field['field_code']})")
else:
print(" (无)")
def main():
"""主函数 - 演示各种查询方式"""
print("="*80)
print("模板字段关联查询示例")
print("="*80)
# 示例1: 根据模板名称查询
print("\n【示例1】根据模板名称查询字段")
print("-" * 80)
# 注意:模板名称需要完全匹配,如 "2.初步核实审批表XXX"
result = get_template_fields_by_name('2.初步核实审批表XXX')
if not result:
# 尝试其他可能的名称
result = get_template_fields_by_name('初步核实审批表')
print_template_fields(result)
# 示例2: 获取所有模板的字段统计
print("\n\n【示例2】获取所有模板的字段统计")
print("-" * 80)
templates = get_all_templates_with_field_stats()
print(f"共找到 {len(templates)} 个模板:\n")
for template in templates[:5]: # 只显示前5个
print(f" {template['template_name']} (ID: {template['template_id']})")
print(f" 输入字段: {template['input_field_count']}")
print(f" 输出字段: {template['output_field_count']}")
print(f" 总字段数: {template['total_field_count']}\n")
if len(templates) > 5:
print(f" ... 还有 {len(templates) - 5} 个模板")
# 示例3: 查找使用特定字段的模板
print("\n\n【示例3】查找使用 'target_name' 字段的模板")
print("-" * 80)
templates_using_field = find_templates_using_field('target_name')
print(f"共找到 {len(templates_using_field)} 个模板使用该字段:")
for template in templates_using_field:
print(f" - {template['template_name']} (ID: {template['template_id']})")
print("\n" + "="*80)
print("查询完成")
print("="*80)
if __name__ == '__main__':
main()

View File

@ -0,0 +1,536 @@
"""
重新建立模板和字段的关联关系
根据模板名称重新建立 f_polic_file_field 表的关联关系
不再依赖 input_data template_code 字段
"""
import pymysql
import os
import json
from typing import Dict, List, Set, Optional
from datetime import datetime
from collections import defaultdict
# 数据库连接配置
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'
}
TENANT_ID = 615873064429507639
CREATED_BY = 655162080928945152
UPDATED_BY = 655162080928945152
# 模板名称到字段编码的映射(根据业务逻辑定义)
# 格式:{模板名称: {'input_fields': [字段编码列表], 'output_fields': [字段编码列表]}}
TEMPLATE_FIELD_MAPPING = {
# 初步核实审批表
'初步核实审批表': {
'input_fields': ['clue_info', 'target_basic_info_clue'],
'output_fields': [
'target_name', 'target_organization_and_position', 'target_organization',
'target_position', 'target_gender', 'target_date_of_birth', 'target_age',
'target_education_level', 'target_political_status', 'target_professional_rank',
'clue_source', 'target_issue_description', 'department_opinion', 'filler_name'
]
},
# 谈话前安全风险评估表
'谈话前安全风险评估表': {
'input_fields': ['clue_info', 'target_basic_info_clue'],
'output_fields': [
'target_family_situation', 'target_social_relations', 'target_health_status',
'target_personality', 'target_tolerance', 'target_issue_severity',
'target_other_issues_possibility', 'target_previous_investigation',
'target_negative_events', 'target_other_situation', 'risk_level'
]
},
# 请示报告卡
'请示报告卡': {
'input_fields': ['clue_info'],
'output_fields': ['target_name', 'target_organization_and_position', 'report_card_request_time']
},
# 初核方案
'初核方案': {
'input_fields': ['clue_info', 'target_basic_info_clue'],
'output_fields': [
'target_name', 'target_organization_and_position', 'target_work_basic_info',
'target_issue_description', 'investigation_unit_name', 'investigation_team_leader_name',
'investigation_team_member_names', 'investigation_location'
]
},
# 谈话通知书
'谈话通知书': {
'input_fields': ['target_basic_info_clue'],
'output_fields': [
'target_name', 'target_organization_and_position', 'target_id_number',
'appointment_time', 'appointment_location', 'approval_time',
'handling_department', 'handler_name', 'notification_time', 'notification_location'
]
},
# 谈话通知书第一联
'谈话通知书第一联': {
'input_fields': ['target_basic_info_clue'],
'output_fields': [
'target_name', 'target_organization_and_position', 'target_id_number',
'appointment_time', 'appointment_location', 'approval_time',
'handling_department', 'handler_name', 'notification_time', 'notification_location'
]
},
# 谈话通知书第二联
'谈话通知书第二联': {
'input_fields': ['target_basic_info_clue'],
'output_fields': [
'target_name', 'target_organization_and_position', 'target_id_number',
'appointment_time', 'appointment_location', 'approval_time',
'handling_department', 'handler_name', 'notification_time', 'notification_location'
]
},
# 谈话通知书第三联
'谈话通知书第三联': {
'input_fields': ['target_basic_info_clue'],
'output_fields': [
'target_name', 'target_organization_and_position', 'target_id_number',
'appointment_time', 'appointment_location', 'approval_time',
'handling_department', 'handler_name', 'notification_time', 'notification_location'
]
},
# 谈话笔录
'谈话笔录': {
'input_fields': ['clue_info', 'target_basic_info_clue'],
'output_fields': [
'target_name', 'target_organization_and_position', 'target_gender',
'target_date_of_birth_full', 'target_political_status', 'target_address',
'target_registered_address', 'target_contact', 'target_place_of_origin',
'target_ethnicity', 'target_id_number', 'investigation_team_code'
]
},
# 谈话后安全风险评估表
'谈话后安全风险评估表': {
'input_fields': ['clue_info', 'target_basic_info_clue'],
'output_fields': [
'target_name', 'target_organization_and_position', 'target_gender',
'target_date_of_birth_full', 'target_political_status', 'target_address',
'target_registered_address', 'target_contact', 'target_place_of_origin',
'target_ethnicity', 'target_id_number', 'investigation_team_code'
]
},
# XXX初核情况报告
'XXX初核情况报告': {
'input_fields': ['clue_info', 'target_basic_info_clue'],
'output_fields': [
'target_name', 'target_organization_and_position', 'target_issue_description',
'target_work_basic_info', 'investigation_unit_name', 'investigation_team_leader_name'
]
},
# 走读式谈话审批
'走读式谈话审批': {
'input_fields': ['target_basic_info_clue'],
'output_fields': [
'target_name', 'target_organization_and_position', 'target_id_number',
'appointment_time', 'appointment_location', 'approval_time',
'handling_department', 'handler_name'
]
},
# 走读式谈话流程
'走读式谈话流程': {
'input_fields': ['target_basic_info_clue'],
'output_fields': [
'target_name', 'target_organization_and_position', 'target_id_number',
'appointment_time', 'appointment_location', 'approval_time',
'handling_department', 'handler_name'
]
},
# 谈话审批 / 谈话审批表
'谈话审批': {
'input_fields': ['target_basic_info_clue'],
'output_fields': [
'target_name', 'target_organization_and_position', 'target_id_number',
'appointment_time', 'appointment_location', 'approval_time',
'handling_department', 'handler_name'
]
},
'谈话审批表': {
'input_fields': ['clue_info', 'target_basic_info_clue'],
'output_fields': [
'target_name', 'target_organization_and_position', 'target_gender',
'target_date_of_birth_full', 'target_political_status', 'target_address',
'target_registered_address', 'target_contact', 'target_place_of_origin',
'target_ethnicity', 'target_id_number', 'investigation_team_code'
]
},
}
# 模板名称的标准化映射(处理不同的命名方式)
TEMPLATE_NAME_NORMALIZE = {
'1.请示报告卡XXX': '请示报告卡',
'2.初步核实审批表XXX': '初步核实审批表',
'3.附件初核方案(XXX)': '初核方案',
'8.XXX初核情况报告': 'XXX初核情况报告',
'2.谈话审批': '谈话审批',
'2谈话审批表': '谈话审批表',
}
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_template_name(name: str) -> str:
"""标准化模板名称"""
# 先检查映射表
if name in TEMPLATE_NAME_NORMALIZE:
return TEMPLATE_NAME_NORMALIZE[name]
# 移除常见的后缀和前缀
name = name.strip()
# 移除括号内容
import re
name = re.sub(r'[(].*?[)]', '', name)
name = name.strip()
# 移除数字前缀和点号
name = re.sub(r'^\d+\.', '', name)
name = name.strip()
return name
def get_all_templates(conn) -> Dict:
"""获取所有模板配置"""
cursor = conn.cursor(pymysql.cursors.DictCursor)
sql = """
SELECT id, name, parent_id, state
FROM f_polic_file_config
WHERE tenant_id = %s
ORDER BY name
"""
cursor.execute(sql, (TENANT_ID,))
templates = cursor.fetchall()
result = {}
for template in templates:
name = template['name']
normalized_name = normalize_template_name(name)
# 处理state字段可能是二进制格式
state = template['state']
if isinstance(state, bytes):
state = int.from_bytes(state, byteorder='big')
elif isinstance(state, (int, str)):
state = int(state)
else:
state = 0
result[template['id']] = {
'id': template['id'],
'name': name,
'normalized_name': normalized_name,
'parent_id': template['parent_id'],
'state': state
}
cursor.close()
return result
def get_all_fields(conn) -> Dict:
"""获取所有字段定义"""
cursor = conn.cursor(pymysql.cursors.DictCursor)
sql = """
SELECT id, name, filed_code, field_type, state
FROM f_polic_field
WHERE tenant_id = %s
ORDER BY field_type, filed_code
"""
cursor.execute(sql, (TENANT_ID,))
fields = cursor.fetchall()
result = {
'by_code': {},
'by_name': {},
'input_fields': [],
'output_fields': []
}
for field in fields:
field_code = field['filed_code']
field_name = field['name']
field_type = field['field_type']
result['by_code'][field_code] = field
result['by_name'][field_name] = field
if field_type == 1:
result['input_fields'].append(field)
elif field_type == 2:
result['output_fields'].append(field)
cursor.close()
return result
def get_existing_relations(conn) -> Set[tuple]:
"""获取现有的关联关系"""
cursor = conn.cursor(pymysql.cursors.DictCursor)
sql = """
SELECT file_id, filed_id
FROM f_polic_file_field
WHERE tenant_id = %s
"""
cursor.execute(sql, (TENANT_ID,))
relations = cursor.fetchall()
result = {(rel['file_id'], rel['filed_id']) for rel in relations}
cursor.close()
return result
def rebuild_template_relations(conn, template_id: int, template_name: str,
normalized_name: str, field_mapping: Dict,
dry_run: bool = True) -> Dict:
"""重建单个模板的关联关系"""
cursor = conn.cursor()
# 查找模板对应的字段配置
template_config = None
# 优先精确匹配标准化名称
if normalized_name in TEMPLATE_FIELD_MAPPING:
template_config = TEMPLATE_FIELD_MAPPING[normalized_name]
else:
# 尝试模糊匹配
for name, config in TEMPLATE_FIELD_MAPPING.items():
if name == normalized_name or name in normalized_name or normalized_name in name:
template_config = config
break
# 也检查原始名称
if name in template_name or template_name in name:
template_config = config
break
if not template_config:
return {
'template_id': template_id,
'template_name': template_name,
'status': 'skipped',
'reason': '未找到字段配置映射',
'input_count': 0,
'output_count': 0
}
input_field_codes = template_config.get('input_fields', [])
output_field_codes = template_config.get('output_fields', [])
# 查找字段ID
input_field_ids = []
output_field_ids = []
for field_code in input_field_codes:
field = field_mapping['by_code'].get(field_code)
if field:
if field['field_type'] == 1:
input_field_ids.append(field['id'])
else:
print(f" ⚠ 警告: 字段 {field_code} 应该是输入字段,但实际类型为 {field['field_type']}")
else:
print(f" ⚠ 警告: 字段 {field_code} 不存在")
for field_code in output_field_codes:
field = field_mapping['by_code'].get(field_code)
if field:
if field['field_type'] == 2:
output_field_ids.append(field['id'])
else:
print(f" ⚠ 警告: 字段 {field_code} 应该是输出字段,但实际类型为 {field['field_type']}")
else:
print(f" ⚠ 警告: 字段 {field_code} 不存在")
# 删除旧的关联关系
if not dry_run:
delete_sql = """
DELETE FROM f_polic_file_field
WHERE tenant_id = %s AND file_id = %s
"""
cursor.execute(delete_sql, (TENANT_ID, template_id))
deleted_count = cursor.rowcount
else:
deleted_count = 0
# 创建新的关联关系
created_count = 0
all_field_ids = input_field_ids + output_field_ids
for field_id in all_field_ids:
if not dry_run:
# 检查是否已存在(虽然已经删除了,但为了安全还是检查一下)
check_sql = """
SELECT id FROM f_polic_file_field
WHERE tenant_id = %s AND file_id = %s AND filed_id = %s
"""
cursor.execute(check_sql, (TENANT_ID, template_id, field_id))
existing = cursor.fetchone()
if not existing:
relation_id = generate_id()
insert_sql = """
INSERT INTO f_polic_file_field
(id, tenant_id, file_id, filed_id, created_time, created_by, updated_time, updated_by, state)
VALUES (%s, %s, %s, %s, NOW(), %s, NOW(), %s, %s)
"""
cursor.execute(insert_sql, (
relation_id, TENANT_ID, template_id, field_id,
CREATED_BY, UPDATED_BY, 1 # state=1 表示启用
))
created_count += 1
else:
created_count += 1
if not dry_run:
conn.commit()
return {
'template_id': template_id,
'template_name': template_name,
'normalized_name': normalized_name,
'status': 'success',
'deleted_count': deleted_count,
'input_count': len(input_field_ids),
'output_count': len(output_field_ids),
'created_count': created_count
}
def main(dry_run: bool = True):
"""主函数"""
print("="*80)
print("重新建立模板和字段的关联关系")
print("="*80)
if dry_run:
print("\n[DRY RUN模式 - 不会实际修改数据库]")
else:
print("\n[实际执行模式 - 将修改数据库]")
try:
conn = pymysql.connect(**DB_CONFIG)
print("✓ 数据库连接成功\n")
# 获取所有模板
print("1. 获取所有模板配置...")
templates = get_all_templates(conn)
print(f" 找到 {len(templates)} 个模板")
# 获取所有字段
print("\n2. 获取所有字段定义...")
field_mapping = get_all_fields(conn)
print(f" 输入字段: {len(field_mapping['input_fields'])}")
print(f" 输出字段: {len(field_mapping['output_fields'])}")
print(f" 总字段数: {len(field_mapping['by_code'])}")
# 获取现有关联关系
print("\n3. 获取现有关联关系...")
existing_relations = get_existing_relations(conn)
print(f" 现有关联关系: {len(existing_relations)}")
# 重建关联关系
print("\n4. 重建模板和字段的关联关系...")
print("="*80)
results = []
for template_id, template_info in templates.items():
template_name = template_info['name']
normalized_name = template_info['normalized_name']
state = template_info['state']
# 处理所有模板(包括未启用的,因为可能需要建立关联)
# 但可以记录状态
status_note = f" (state={state})" if state != 1 else ""
if state != 1:
print(f"\n处理未启用的模板: {template_name}{status_note}")
print(f"\n处理模板: {template_name}")
print(f" 标准化名称: {normalized_name}")
result = rebuild_template_relations(
conn, template_id, template_name, normalized_name,
field_mapping, dry_run=dry_run
)
results.append(result)
if result['status'] == 'success':
print(f" ✓ 成功: 删除 {result['deleted_count']} 条旧关联, "
f"创建 {result['created_count']} 条新关联 "
f"(输入字段: {result['input_count']}, 输出字段: {result['output_count']})")
else:
print(f"{result['status']}: {result.get('reason', '')}")
# 统计信息
print("\n" + "="*80)
print("处理结果统计")
print("="*80)
success_count = sum(1 for r in results if r['status'] == 'success')
skipped_count = sum(1 for r in results if r['status'] == 'skipped')
total_input = sum(r.get('input_count', 0) for r in results)
total_output = sum(r.get('output_count', 0) for r in results)
total_created = sum(r.get('created_count', 0) for r in results)
print(f"\n成功处理: {success_count} 个模板")
print(f"跳过: {skipped_count} 个模板")
print(f"总输入字段关联: {total_input}")
print(f"总输出字段关联: {total_output}")
print(f"总关联关系: {total_created}")
# 显示详细结果
print("\n详细结果:")
for result in results:
if result['status'] == 'success':
print(f" - {result['template_name']}: "
f"输入字段 {result['input_count']} 个, "
f"输出字段 {result['output_count']}")
else:
print(f" - {result['template_name']}: {result['status']} - {result.get('reason', '')}")
print("\n" + "="*80)
if dry_run:
print("\n这是DRY RUN模式未实际修改数据库。")
print("要实际执行,请运行: python rebuild_template_field_relations.py --execute")
else:
print("\n✓ 关联关系已更新完成")
except Exception as e:
print(f"\n✗ 发生错误: {e}")
import traceback
traceback.print_exc()
if not dry_run:
conn.rollback()
finally:
conn.close()
print("\n数据库连接已关闭")
if __name__ == '__main__':
import sys
dry_run = '--execute' not in sys.argv
if not dry_run:
print("\n⚠ 警告: 这将修改数据库!")
response = input("确认要继续吗? (yes/no): ")
if response.lower() != 'yes':
print("操作已取消")
sys.exit(0)
main(dry_run=dry_run)

View File

@ -7,4 +7,5 @@ flasgger==0.9.7.1
python-docx==1.1.0 python-docx==1.1.0
minio==7.2.3 minio==7.2.3
openpyxl==3.1.2 openpyxl==3.1.2
json-repair

340
restore_database.py Normal file
View File

@ -0,0 +1,340 @@
"""
数据库恢复脚本
从SQL备份文件恢复数据库
"""
import os
import sys
import subprocess
import pymysql
from pathlib import Path
from dotenv import load_dotenv
import gzip
# 加载环境变量
load_dotenv()
class DatabaseRestore:
"""数据库恢复类"""
def __init__(self):
"""初始化数据库配置"""
self.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'
}
def restore_with_mysql(self, backup_file, drop_database=False):
"""
使用mysql命令恢复数据库推荐方式
Args:
backup_file: 备份文件路径
drop_database: 是否先删除数据库危险操作
Returns:
是否成功
"""
backup_file = Path(backup_file)
if not backup_file.exists():
raise FileNotFoundError(f"备份文件不存在: {backup_file}")
# 如果是压缩文件,先解压
sql_file = backup_file
temp_file = None
if backup_file.suffix == '.gz':
print(f"检测到压缩文件,正在解压...")
temp_file = backup_file.with_suffix('')
with gzip.open(backup_file, 'rb') as f_in:
with open(temp_file, 'wb') as f_out:
f_out.write(f_in.read())
sql_file = temp_file
print(f"解压完成: {sql_file}")
try:
print(f"开始恢复数据库 {self.db_config['database']}...")
print(f"备份文件: {backup_file}")
# 如果指定删除数据库
if drop_database:
print("警告: 将删除现有数据库!")
confirm = input("确认继续? (yes/no): ")
if confirm.lower() != 'yes':
print("已取消恢复操作")
return False
# 删除数据库
self._drop_database()
# 构建mysql命令
cmd = [
'mysql',
f"--host={self.db_config['host']}",
f"--port={self.db_config['port']}",
f"--user={self.db_config['user']}",
f"--password={self.db_config['password']}",
'--default-character-set=utf8mb4',
self.db_config['database']
]
# 执行恢复命令
with open(sql_file, 'r', encoding='utf-8') as f:
result = subprocess.run(
cmd,
stdin=f,
stderr=subprocess.PIPE,
text=True
)
if result.returncode != 0:
error_msg = result.stderr.decode('utf-8') if result.stderr else '未知错误'
raise Exception(f"mysql执行失败: {error_msg}")
print("恢复完成!")
return True
except FileNotFoundError:
print("错误: 未找到mysql命令请确保MySQL客户端已安装并在PATH中")
print("尝试使用Python方式恢复...")
return self.restore_with_python(backup_file, drop_database)
except Exception as e:
print(f"恢复失败: {str(e)}")
raise
finally:
# 清理临时解压文件
if temp_file and temp_file.exists():
temp_file.unlink()
def restore_with_python(self, backup_file, drop_database=False):
"""
使用Python直接连接数据库恢复备用方式
Args:
backup_file: 备份文件路径
drop_database: 是否先删除数据库危险操作
Returns:
是否成功
"""
backup_file = Path(backup_file)
if not backup_file.exists():
raise FileNotFoundError(f"备份文件不存在: {backup_file}")
# 如果是压缩文件,先解压
sql_file = backup_file
temp_file = None
if backup_file.suffix == '.gz':
print(f"检测到压缩文件,正在解压...")
temp_file = backup_file.with_suffix('')
with gzip.open(backup_file, 'rb') as f_in:
with open(temp_file, 'wb') as f_out:
f_out.write(f_in.read())
sql_file = temp_file
print(f"解压完成: {sql_file}")
try:
print(f"开始使用Python方式恢复数据库 {self.db_config['database']}...")
print(f"备份文件: {backup_file}")
# 如果指定删除数据库
if drop_database:
print("警告: 将删除现有数据库!")
confirm = input("确认继续? (yes/no): ")
if confirm.lower() != 'yes':
print("已取消恢复操作")
return False
# 删除数据库
self._drop_database()
# 连接数据库
connection = pymysql.connect(**self.db_config)
cursor = connection.cursor()
# 读取SQL文件
print("读取SQL文件...")
with open(sql_file, 'r', encoding='utf-8') as f:
sql_content = f.read()
# 分割SQL语句按分号分割但要注意字符串中的分号
print("执行SQL语句...")
statements = self._split_sql_statements(sql_content)
total = len(statements)
print(f"{total} 条SQL语句")
# 执行每条SQL语句
for i, statement in enumerate(statements, 1):
statement = statement.strip()
if not statement or statement.startswith('--'):
continue
try:
cursor.execute(statement)
if i % 100 == 0:
print(f"进度: {i}/{total} ({i*100//total}%)")
except Exception as e:
# 某些错误可以忽略(如表已存在等)
error_msg = str(e).lower()
if 'already exists' in error_msg or 'duplicate' in error_msg:
continue
print(f"警告: 执行SQL语句时出错 (第{i}条): {str(e)}")
print(f"SQL: {statement[:100]}...")
# 提交事务
connection.commit()
cursor.close()
connection.close()
print("恢复完成!")
return True
except Exception as e:
print(f"恢复失败: {str(e)}")
raise
finally:
# 清理临时解压文件
if temp_file and temp_file.exists():
temp_file.unlink()
def _split_sql_statements(self, sql_content):
"""
分割SQL语句处理字符串中的分号
Args:
sql_content: SQL内容
Returns:
SQL语句列表
"""
statements = []
current_statement = []
in_string = False
string_char = None
i = 0
while i < len(sql_content):
char = sql_content[i]
# 检测字符串开始/结束
if char in ("'", '"', '`') and (i == 0 or sql_content[i-1] != '\\'):
if not in_string:
in_string = True
string_char = char
elif char == string_char:
in_string = False
string_char = None
current_statement.append(char)
# 如果不在字符串中且遇到分号,分割语句
if not in_string and char == ';':
statement = ''.join(current_statement).strip()
if statement:
statements.append(statement)
current_statement = []
i += 1
# 添加最后一条语句
if current_statement:
statement = ''.join(current_statement).strip()
if statement:
statements.append(statement)
return statements
def _drop_database(self):
"""删除数据库(危险操作)"""
try:
# 连接到MySQL服务器不指定数据库
config = self.db_config.copy()
config.pop('database')
connection = pymysql.connect(**config)
cursor = connection.cursor()
cursor.execute(f"DROP DATABASE IF EXISTS `{self.db_config['database']}`")
cursor.execute(f"CREATE DATABASE `{self.db_config['database']}` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci")
connection.commit()
cursor.close()
connection.close()
print(f"数据库 {self.db_config['database']} 已删除并重新创建")
except Exception as e:
raise Exception(f"删除数据库失败: {str(e)}")
def test_connection(self):
"""测试数据库连接"""
try:
connection = pymysql.connect(**self.db_config)
cursor = connection.cursor()
cursor.execute("SELECT VERSION()")
version = cursor.fetchone()[0]
cursor.close()
connection.close()
print(f"数据库连接成功MySQL版本: {version}")
return True
except Exception as e:
print(f"数据库连接失败: {str(e)}")
return False
def main():
"""主函数"""
import argparse
parser = argparse.ArgumentParser(description='数据库恢复工具')
parser.add_argument('backup_file', help='备份文件路径')
parser.add_argument('--method', choices=['mysql', 'python', 'auto'],
default='auto', help='恢复方法 (默认: auto)')
parser.add_argument('--drop-db', action='store_true',
help='恢复前删除现有数据库(危险操作)')
parser.add_argument('--test', action='store_true',
help='仅测试数据库连接')
args = parser.parse_args()
restore = DatabaseRestore()
# 测试连接
if args.test:
restore.test_connection()
return
# 执行恢复
try:
if args.method == 'mysql':
success = restore.restore_with_mysql(args.backup_file, args.drop_db)
elif args.method == 'python':
success = restore.restore_with_python(args.backup_file, args.drop_db)
else: # auto
try:
success = restore.restore_with_mysql(args.backup_file, args.drop_db)
except:
print("\nmysql方式失败切换到Python方式...")
success = restore.restore_with_python(args.backup_file, args.drop_db)
if success:
print("\n恢复成功!")
else:
print("\n恢复失败!")
sys.exit(1)
except Exception as e:
print(f"\n恢复失败: {str(e)}")
sys.exit(1)
if __name__ == '__main__':
main()

View File

@ -0,0 +1,122 @@
"""
回滚错误的更新恢复被错误修改的字段
"""
import os
import pymysql
# 数据库连接配置
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'
}
TENANT_ID = 615873064429507639
UPDATED_BY = 655162080928945152
# 需要恢复的字段映射字段ID -> 正确的field_code
ROLLBACK_MAPPING = {
# 这些字段被错误地从英文改成了中文,需要恢复
1764656917410273: 'target_issue_description',
1764656918032031: 'filler_name',
1764656917418979: 'department_opinion',
1764836032906561: 'appointment_location',
1764836032488198: 'appointment_time',
1764836033052889: 'approval_time',
1764836032655678: 'handler_name',
1764836033342084: 'handling_department',
1764836033240593: 'investigation_unit_name',
1764836033018470: 'investigation_location',
1764836033274278: 'investigation_team_code',
1764836033094781: 'investigation_team_member_names',
1764836033176386: 'investigation_team_leader_name',
1764836033500799: 'commission_name',
1764656917384058: 'clue_info',
1764656917861268: 'clue_source',
1764836032538308: 'target_address',
1764836033565636: 'target_health_status',
1764836033332970: 'target_other_situation',
1764656917299164: 'target_date_of_birth',
1764836033269146: 'target_date_of_birth_full',
1765151880445876: 'target_organization',
1764656917367205: 'target_organization_and_position',
1764836033405778: 'target_family_situation',
1764836033162748: 'target_work_basic_info',
1764656917996367: 'target_basic_info_clue',
1764836032997850: 'target_age',
1764656917561689: 'target_gender',
1764836032855869: 'target_personality',
1764836032893680: 'target_registered_address',
1764836033603501: 'target_tolerance',
1764656917185956: 'target_political_status',
1764836033786057: 'target_attitude',
1764836033587951: 'target_previous_investigation',
1764836032951705: 'target_ethnicity',
1764836033280024: 'target_other_issues_possibility',
1764836033458872: 'target_issue_severity',
1764836032929811: 'target_social_relations',
1764836033618877: 'target_negative_events',
1764836032926994: 'target_place_of_origin',
1765151880304552: 'target_position',
1764656917802442: 'target_professional_rank',
1764836032817243: 'target_contact',
1764836032902356: 'target_id_number',
1764836032913357: 'target_id_number',
1764656917073644: 'target_name',
1764836033571266: 'target_problem_description',
1764836032827460: 'report_card_request_time',
1764836032694865: 'notification_location',
1764836032909732: 'notification_time',
1764836033451248: 'risk_level',
}
def rollback():
"""回滚错误的更新"""
conn = pymysql.connect(**DB_CONFIG)
cursor = conn.cursor(pymysql.cursors.DictCursor)
print("="*80)
print("回滚错误的字段更新")
print("="*80)
print(f"\n需要恢复 {len(ROLLBACK_MAPPING)} 个字段\n")
# 先查询当前状态
for field_id, correct_code in ROLLBACK_MAPPING.items():
cursor.execute("""
SELECT id, name, filed_code
FROM f_polic_field
WHERE id = %s AND tenant_id = %s
""", (field_id, TENANT_ID))
field = cursor.fetchone()
if field:
print(f" ID: {field_id}")
print(f" 名称: {field['name']}")
print(f" 当前field_code: {field['filed_code']}")
print(f" 恢复为: {correct_code}")
print()
# 执行回滚
print("开始执行回滚...\n")
for field_id, correct_code in ROLLBACK_MAPPING.items():
cursor.execute("""
UPDATE f_polic_field
SET filed_code = %s, updated_time = NOW(), updated_by = %s
WHERE id = %s AND tenant_id = %s
""", (correct_code, UPDATED_BY, field_id, TENANT_ID))
print(f" ✓ 恢复字段 ID {field_id}: {correct_code}")
conn.commit()
print("\n✓ 回滚完成")
cursor.close()
conn.close()
if __name__ == '__main__':
rollback()

181
services/ai_logger.py Normal file
View File

@ -0,0 +1,181 @@
"""
AI对话日志记录模块
用于记录大模型对话的输入和输出信息方便排查问题
"""
import os
import json
import time
from datetime import datetime
from pathlib import Path
from typing import Dict, Optional, Any
from threading import Lock
class AILogger:
"""AI对话日志记录器"""
def __init__(self, log_dir: Optional[str] = None):
"""
初始化日志记录器
Args:
log_dir: 日志文件保存目录默认为项目根目录下的 logs/ai_conversations 目录
"""
if log_dir is None:
# 默认日志目录:项目根目录下的 logs/ai_conversations
project_root = Path(__file__).parent.parent
log_dir = project_root / "logs" / "ai_conversations"
self.log_dir = Path(log_dir)
self.log_dir.mkdir(parents=True, exist_ok=True)
# 线程锁,确保日志写入的线程安全
self._lock = Lock()
# 是否启用日志记录(可通过环境变量控制)
self.enabled = os.getenv('AI_LOG_ENABLED', 'true').lower() == 'true'
print(f"[AI日志] 日志记录器初始化完成,日志目录: {self.log_dir}")
print(f"[AI日志] 日志记录状态: {'启用' if self.enabled else '禁用'}")
def log_conversation(
self,
prompt: str,
api_request: Dict[str, Any],
api_response: Optional[Dict[str, Any]] = None,
extracted_data: Optional[Dict[str, Any]] = None,
error: Optional[str] = None,
session_id: Optional[str] = None
) -> str:
"""
记录一次完整的AI对话
Args:
prompt: 输入提示词
api_request: API请求参数
api_response: API响应内容完整响应
extracted_data: 提取后的结构化数据
error: 错误信息如果有
session_id: 会话ID可选用于关联多次对话
Returns:
日志文件路径
"""
if not self.enabled:
return ""
try:
with self._lock:
# 生成时间戳和会话ID
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S_%f")[:-3] # 精确到毫秒
if session_id is None:
session_id = f"session_{int(time.time() * 1000)}"
# 创建日志记录
log_entry = {
"timestamp": datetime.now().isoformat(),
"session_id": session_id,
"prompt": prompt,
"api_request": {
"endpoint": api_request.get("endpoint", "unknown"),
"model": api_request.get("model", "unknown"),
"messages": api_request.get("messages", []),
"temperature": api_request.get("temperature"),
"max_tokens": api_request.get("max_tokens"),
"enable_thinking": api_request.get("enable_thinking", False),
},
"api_response": api_response,
"extracted_data": extracted_data,
"error": error,
"success": error is None
}
# 保存到文件(按日期组织)
date_str = datetime.now().strftime("%Y%m%d")
log_file = self.log_dir / f"conversation_{date_str}_{timestamp}.json"
with open(log_file, 'w', encoding='utf-8') as f:
json.dump(log_entry, f, ensure_ascii=False, indent=2)
print(f"[AI日志] 对话日志已保存: {log_file.name}")
return str(log_file)
except Exception as e:
print(f"[AI日志] 保存日志失败: {e}")
return ""
def log_request_only(
self,
prompt: str,
api_request: Dict[str, Any],
session_id: Optional[str] = None
) -> str:
"""
仅记录请求信息在发送请求前调用
Args:
prompt: 输入提示词
api_request: API请求参数
session_id: 会话ID
Returns:
日志文件路径
"""
return self.log_conversation(
prompt=prompt,
api_request=api_request,
session_id=session_id
)
def get_recent_logs(self, limit: int = 10) -> list:
"""
获取最近的日志文件列表
Args:
limit: 返回的日志文件数量
Returns:
日志文件路径列表按时间倒序
"""
try:
log_files = sorted(
self.log_dir.glob("conversation_*.json"),
key=lambda x: x.stat().st_mtime,
reverse=True
)
return [str(f) for f in log_files[:limit]]
except Exception as e:
print(f"[AI日志] 获取日志列表失败: {e}")
return []
def read_log(self, log_file: str) -> Optional[Dict]:
"""
读取指定的日志文件
Args:
log_file: 日志文件路径
Returns:
日志内容字典如果读取失败返回None
"""
try:
log_path = Path(log_file)
if not log_path.is_absolute():
log_path = self.log_dir / log_file
with open(log_path, 'r', encoding='utf-8') as f:
return json.load(f)
except Exception as e:
print(f"[AI日志] 读取日志文件失败: {e}")
return None
# 全局日志记录器实例
_ai_logger: Optional[AILogger] = None
def get_ai_logger() -> AILogger:
"""获取全局AI日志记录器实例"""
global _ai_logger
if _ai_logger is None:
_ai_logger = AILogger()
return _ai_logger

File diff suppressed because it is too large Load Diff

View File

@ -50,44 +50,36 @@ class DocumentService:
secure=self.minio_config['secure'] secure=self.minio_config['secure']
) )
def get_file_config_by_template_code(self, template_code: str) -> Optional[Dict]: def get_file_config_by_id(self, file_id: int) -> Optional[Dict]:
""" """
根据模板编码获取文件配置 根据文件ID获取文件配置
Args: Args:
template_code: 模板编码 'PRELIMINARY_VERIFICATION_APPROVAL' file_id: 文件配置ID
Returns: Returns:
文件配置信息包含: id, name, file_path, template_code 文件配置信息包含: id, name, file_path
""" """
import json
conn = self.get_connection() conn = self.get_connection()
cursor = conn.cursor(pymysql.cursors.DictCursor) cursor = conn.cursor(pymysql.cursors.DictCursor)
try: try:
# 查询文件配置template_code存储在input_data的JSON字段中
sql = """ sql = """
SELECT id, name, file_path, input_data SELECT id, name, file_path
FROM f_polic_file_config FROM f_polic_file_config
WHERE tenant_id = %s WHERE id = %s
AND tenant_id = %s
AND state = 1 AND state = 1
""" """
cursor.execute(sql, (self.tenant_id,)) cursor.execute(sql, (file_id, self.tenant_id))
configs = cursor.fetchall() config = cursor.fetchone()
# 从input_data的JSON中查找匹配的template_code if config:
for config in configs:
try:
input_data = json.loads(config['input_data']) if config['input_data'] else {}
if input_data.get('template_code') == template_code:
return { return {
'id': config['id'], 'id': config['id'],
'name': config['name'], 'name': config['name'],
'file_path': config['file_path'], 'file_path': config['file_path']
'template_code': template_code
} }
except (json.JSONDecodeError, TypeError):
continue
return None return None
@ -105,6 +97,10 @@ class DocumentService:
Returns: Returns:
本地临时文件路径 本地临时文件路径
""" """
# 检查file_path是否为None或空
if not file_path:
raise Exception("模板文件路径不能为空请检查数据库中模板配置的file_path字段")
client = self.get_minio_client() client = self.get_minio_client()
# 创建临时文件 # 创建临时文件
@ -135,9 +131,80 @@ class DocumentService:
填充后的文档路径 填充后的文档路径
""" """
try: try:
print(f"[DEBUG] 开始填充模板: {template_path}")
print(f"[DEBUG] 字段数据: {field_data}")
# 打开模板文档 # 打开模板文档
doc = Document(template_path) doc = Document(template_path)
print(f"[DEBUG] 文档包含 {len(doc.paragraphs)} 个段落, {len(doc.tables)} 个表格")
<<<<<<< HEAD
def replace_placeholder_in_paragraph(paragraph):
"""在段落中替换占位符处理跨run的情况"""
try:
# 获取段落完整文本
full_text = paragraph.text
if not full_text:
return
# 检查是否有占位符需要替换
has_placeholder = False
replaced_text = full_text
replacement_count = 0
# 遍历所有字段,替换所有匹配的占位符(包括重复的)
for field_code, field_value in field_data.items():
placeholder = f"{{{{{field_code}}}}}"
# 使用循环替换所有匹配项(不仅仅是第一个)
while placeholder in replaced_text:
has_placeholder = True
replacement_count += 1
# 替换占位符,如果值为空则替换为空字符串
replaced_text = replaced_text.replace(placeholder, str(field_value) if field_value else '', 1)
print(f"[DEBUG] 替换占位符: {placeholder} -> '{field_value}' (在段落中)")
# 如果有替换,使用安全的方式更新段落文本
if has_placeholder:
print(f"[DEBUG] 段落替换了 {replacement_count} 个占位符: '{full_text[:50]}...' -> '{replaced_text[:50]}...'")
try:
# 方法1直接设置text推荐会自动处理run
paragraph.text = replaced_text
except Exception as e1:
# 如果方法1失败尝试方法2手动处理run
try:
# 清空所有run
paragraph.clear()
# 添加新的run
if replaced_text:
paragraph.add_run(replaced_text)
except Exception as e2:
# 如果两种方法都失败,记录错误但继续
print(f"[WARN] 无法更新段落文本方法1错误: {str(e1)}, 方法2错误: {str(e2)}")
pass
except Exception as e:
# 如果单个段落处理失败,记录错误但继续处理其他段落
print(f"[WARN] 处理段落时出错: {str(e)}")
import traceback
print(traceback.format_exc())
pass
# 统计替换信息
total_replacements = 0
replaced_placeholders = set()
# 替换段落中的占位符
for para_idx, paragraph in enumerate(doc.paragraphs):
before_text = paragraph.text
replace_placeholder_in_paragraph(paragraph)
after_text = paragraph.text
if before_text != after_text:
# 检查哪些占位符被替换了
for field_code in field_data.keys():
placeholder = f"{{{{{field_code}}}}}"
if placeholder in before_text and placeholder not in after_text:
replaced_placeholders.add(field_code)
total_replacements += before_text.count(placeholder)
=======
# 替换占位符 {{field_code}} 为实际值 # 替换占位符 {{field_code}} 为实际值
for paragraph in doc.paragraphs: for paragraph in doc.paragraphs:
# 替换段落文本中的占位符 # 替换段落文本中的占位符
@ -148,11 +215,73 @@ class DocumentService:
for run in paragraph.runs: for run in paragraph.runs:
if placeholder in run.text: if placeholder in run.text:
run.text = run.text.replace(placeholder, field_value or '') run.text = run.text.replace(placeholder, field_value or '')
>>>>>>> parent of 4897c96 (添加通过taskId获取文档的接口支持文件列表查询和参数验证增强错误处理能力同时优化文档生成逻辑确保生成的文档名称和路径的准确性)
# 替换表格中的占位符 # 替换表格中的占位符
try:
for table in doc.tables:
if not table.rows:
continue
for row in table.rows:
if not row.cells:
continue
for cell in row.cells:
try:
# 检查cell是否有paragraphs属性且不为空
if hasattr(cell, 'paragraphs'):
# 安全地获取paragraphs列表
paragraphs = list(cell.paragraphs) if cell.paragraphs else []
for paragraph in paragraphs:
before_text = paragraph.text
replace_placeholder_in_paragraph(paragraph)
after_text = paragraph.text
if before_text != after_text:
# 检查哪些占位符被替换了
for field_code in field_data.keys():
placeholder = f"{{{{{field_code}}}}}"
if placeholder in before_text and placeholder not in after_text:
replaced_placeholders.add(field_code)
total_replacements += before_text.count(placeholder)
except Exception as e:
# 如果单个单元格处理失败,记录错误但继续处理其他单元格
print(f"[WARN] 处理表格单元格时出错: {str(e)}")
pass
except Exception as e:
# 如果表格处理失败,记录错误但继续保存文档
print(f"[WARN] 处理表格时出错: {str(e)}")
pass
# 验证是否还有未替换的占位符
remaining_placeholders = set()
for paragraph in doc.paragraphs:
text = paragraph.text
for field_code in field_data.keys():
placeholder = f"{{{{{field_code}}}}}"
if placeholder in text:
remaining_placeholders.add(field_code)
# 检查表格中的占位符
for table in doc.tables: for table in doc.tables:
for row in table.rows: for row in table.rows:
for cell in row.cells: for cell in row.cells:
<<<<<<< HEAD
if hasattr(cell, 'paragraphs'):
for paragraph in cell.paragraphs:
text = paragraph.text
for field_code in field_data.keys():
placeholder = f"{{{{{field_code}}}}}"
if placeholder in text:
remaining_placeholders.add(field_code)
# 输出统计信息
print(f"[DEBUG] 占位符替换统计:")
print(f" - 已替换的占位符: {sorted(replaced_placeholders)}")
print(f" - 总替换次数: {total_replacements}")
if remaining_placeholders:
print(f" - ⚠️ 仍有未替换的占位符: {sorted(remaining_placeholders)}")
else:
print(f" - ✓ 所有占位符已成功替换")
=======
for paragraph in cell.paragraphs: for paragraph in cell.paragraphs:
for field_code, field_value in field_data.items(): for field_code, field_value in field_data.items():
placeholder = f"{{{{{field_code}}}}}" placeholder = f"{{{{{field_code}}}}}"
@ -160,16 +289,26 @@ class DocumentService:
for run in paragraph.runs: for run in paragraph.runs:
if placeholder in run.text: if placeholder in run.text:
run.text = run.text.replace(placeholder, field_value or '') run.text = run.text.replace(placeholder, field_value or '')
>>>>>>> parent of 4897c96 (添加通过taskId获取文档的接口支持文件列表查询和参数验证增强错误处理能力同时优化文档生成逻辑确保生成的文档名称和路径的准确性)
# 保存到临时文件 # 保存到临时文件
temp_dir = tempfile.gettempdir() temp_dir = tempfile.gettempdir()
output_file = os.path.join(temp_dir, f"filled_{datetime.now().strftime('%Y%m%d%H%M%S')}.docx") output_file = os.path.join(temp_dir, f"filled_{datetime.now().strftime('%Y%m%d%H%M%S')}.docx")
doc.save(output_file) doc.save(output_file)
print(f"[DEBUG] 文档已保存到: {output_file}")
return output_file return output_file
except IndexError as e:
# 索引越界错误,提供更详细的错误信息
import traceback
error_detail = traceback.format_exc()
raise Exception(f"填充模板失败: list index out of range. 详细信息: {str(e)}\n{error_detail}")
except Exception as e: except Exception as e:
raise Exception(f"填充模板失败: {str(e)}") # 其他错误,提供详细的错误信息
import traceback
error_detail = traceback.format_exc()
raise Exception(f"填充模板失败: {str(e)}\n{error_detail}")
def upload_to_minio(self, file_path: str, file_name: str) -> str: def upload_to_minio(self, file_path: str, file_name: str) -> str:
""" """
@ -187,8 +326,9 @@ class DocumentService:
try: try:
# 生成MinIO对象路径相对路径 # 生成MinIO对象路径相对路径
now = datetime.now() now = datetime.now()
# 使用日期路径组织文件 # 使用日期路径组织文件,添加微秒确保唯一性
object_name = f"{self.tenant_id}/{now.strftime('%Y%m%d%H%M%S')}/{file_name}" timestamp = f"{now.strftime('%Y%m%d%H%M%S')}{now.microsecond:06d}"
object_name = f"{self.tenant_id}/{timestamp}/{file_name}"
# 上传文件 # 上传文件
client.fput_object( client.fput_object(
@ -204,22 +344,32 @@ class DocumentService:
except S3Error as e: except S3Error as e:
raise Exception(f"上传文件到MinIO失败: {str(e)}") raise Exception(f"上传文件到MinIO失败: {str(e)}")
def generate_document(self, template_code: str, input_data: List[Dict], file_info: Dict) -> Dict: def generate_document(self, file_id: int, input_data: List[Dict], file_info: Dict) -> Dict:
""" """
生成文档 生成文档
Args: Args:
template_code: 模板编码 file_id: 文件配置ID
input_data: 输入数据列表格式: [{'fieldCode': 'xxx', 'fieldValue': 'xxx'}] input_data: 输入数据列表格式: [{'fieldCode': 'xxx', 'fieldValue': 'xxx'}]
file_info: 文件信息格式: {'fileId': 1, 'fileName': 'xxx.doc', 'templateCode': 'xxx'} file_info: 文件信息格式: {'fileId': 1, 'fileName': 'xxx.doc'}
Returns: Returns:
生成结果包含: filePath 生成结果包含: filePath
""" """
# 获取文件配置 # 获取文件配置
file_config = self.get_file_config_by_template_code(template_code) file_config = self.get_file_config_by_id(file_id)
if not file_config: if not file_config:
raise Exception(f"模板编码 {template_code} 不存在") # 提供更详细的错误信息
raise Exception(
f"文件ID {file_id} 对应的模板不存在或未启用。"
f"请通过查询 f_polic_file_config 表获取有效的文件ID"
f"或访问 /api/file-configs 接口查看可用的文件配置列表。"
)
# 检查file_path是否存在
file_path = file_config.get('file_path')
if not file_path:
raise Exception(f"文件ID {file_id} ({file_config.get('name', '')}) 的文件路径(file_path)为空,请检查数据库配置")
# 将input_data转换为字典格式 # 将input_data转换为字典格式
field_data = {} field_data = {}
@ -233,14 +383,21 @@ class DocumentService:
template_path = None template_path = None
filled_doc_path = None filled_doc_path = None
try: try:
template_path = self.download_template_from_minio(file_config['file_path']) template_path = self.download_template_from_minio(file_path)
# 填充模板 # 填充模板
filled_doc_path = self.fill_template(template_path, field_data) filled_doc_path = self.fill_template(template_path, field_data)
# 生成文档名称(.docx格式 # 生成文档名称(.docx格式
original_file_name = file_info.get('fileName', 'generated.doc') # 优先使用file_info中的fileName如果没有则使用数据库中的name
# 确保每个文件都使用自己的文件名
original_file_name = file_info.get('fileName') or file_info.get('name') or file_config.get('name', 'generated.doc')
print(f"[DEBUG] 文件ID: {file_id}, 原始文件名: {original_file_name}")
print(f"[DEBUG] file_info内容: {file_info}")
print(f"[DEBUG] file_config内容: {file_config}")
print(f"[DEBUG] 字段数据用于生成文档名: {field_data}")
generated_file_name = self.generate_document_name(original_file_name, field_data) generated_file_name = self.generate_document_name(original_file_name, field_data)
print(f"[DEBUG] 文件ID: {file_id}, 生成的文档名: {generated_file_name}")
# 上传到MinIO使用生成的文档名 # 上传到MinIO使用生成的文档名
file_path = self.upload_to_minio(filled_doc_path, generated_file_name) file_path = self.upload_to_minio(filled_doc_path, generated_file_name)
@ -277,16 +434,64 @@ class DocumentService:
field_data: 字段数据 field_data: 字段数据
Returns: Returns:
生成的文档名称 "初步核实审批表_张三.docx" 生成的文档名称 "请示报告卡_张三.docx"
""" """
import re
# 提取文件基础名称(不含扩展名) # 提取文件基础名称(不含扩展名)
base_name = Path(original_file_name).stem # 处理可能包含路径的情况
# 先移除路径,只保留文件名
file_name_only = Path(original_file_name).name
# 判断是否有扩展名(.doc, .docx等
# 如果最后有常见的文档扩展名则提取stem
if file_name_only.lower().endswith(('.doc', '.docx', '.txt', '.pdf')):
base_name = Path(file_name_only).stem
else:
# 如果没有扩展名,直接使用文件名
base_name = file_name_only
print(f"[DEBUG] 原始文件名: '{original_file_name}'")
print(f"[DEBUG] 提取的基础名称(清理前): '{base_name}'")
# 清理文件名中的特殊标记
# 1. 移除开头的数字和点(如 "1."、"2." 等),但保留后面的内容
# 使用非贪婪匹配,只匹配开头的数字和点
base_name = re.sub(r'^\d+\.\s*', '', base_name)
# 2. 移除括号及其内容(如 "XXX"、"(初核谈话)" 等)
base_name = re.sub(r'[(].*?[)]', '', base_name)
# 3. 清理首尾空白字符和多余的点
base_name = base_name.strip().strip('.')
# 4. 如果清理后为空或只有数字,使用原始文件名重新处理
if not base_name or base_name.isdigit():
print(f"[DEBUG] 清理后为空或只有数字,重新处理原始文件名")
# 从原始文件名中提取,但保留更多内容
temp_name = file_name_only
# 只移除括号,保留数字前缀(但格式化为更友好的形式)
temp_name = re.sub(r'[(].*?[)]', '', temp_name)
# 移除扩展名(如果存在)
if temp_name.lower().endswith(('.doc', '.docx', '.txt', '.pdf')):
temp_name = Path(temp_name).stem
temp_name = temp_name.strip().strip('.')
if temp_name:
base_name = temp_name
else:
base_name = "文档" # 最后的备选方案
print(f"[DEBUG] 清理后的基础名称: '{base_name}'")
# 尝试从字段数据中提取被核查人姓名作为后缀 # 尝试从字段数据中提取被核查人姓名作为后缀
suffix = '' suffix = ''
if 'target_name' in field_data and field_data['target_name']: target_name = field_data.get('target_name', '')
suffix = f"_{field_data['target_name']}" if target_name and target_name.strip():
suffix = f"_{target_name.strip()}"
# 生成新文件名 # 生成新文件名(确保是.docx格式
return f"{base_name}{suffix}.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

View File

@ -327,10 +327,13 @@
<div class="form-group"> <div class="form-group">
<label>文件列表</label> <label>文件列表</label>
<div style="margin-bottom: 10px;">
<button class="btn btn-secondary" onclick="loadAvailableFiles()" style="margin-right: 10px;">📋 加载可用文件列表</button>
<button class="btn btn-secondary" onclick="addFileItem()">+ 手动添加文件</button>
</div>
<div id="fileListContainer"> <div id="fileListContainer">
<!-- 动态生成的文件列表 --> <!-- 动态生成的文件列表 -->
</div> </div>
<button class="btn btn-secondary" onclick="addFileItem()">+ 添加文件</button>
</div> </div>
</div> </div>
@ -381,9 +384,9 @@
// ==================== 解析接口相关 ==================== // ==================== 解析接口相关 ====================
function initExtractTab() { function initExtractTab() {
// 初始化默认输入字段(虚拟测试数据) // 初始化默认输入字段
addInputField('clue_info', '被举报用户名称是张三年龄44岁某公司总经理男性1980年5月出生本科文化程度中共党员正处级。主要问题线索违反国家计划生育有关政策规定于2010年10月生育二胎。线索来源群众举报。'); addInputField('clue_info', '张三多次在私下聚会、网络群组中发表抹黑党中央决策部署的言论传播歪曲党的理论和路线方针政策的错误观点频繁接受管理服务对象安排的高档宴请、私人会所聚餐以及高尔夫球、高端足浴等娱乐活动相关费用均由对方全额承担在干部选拔任用、岗位调整工作中利用职务便利收受他人财物利用职权为其亲属经营的公司谋取不正当利益帮助该公司违规承接本单位及关联单位工程项目3个合同总额超200万元从中收受亲属给予的"感谢费"15万元其本人沉迷赌博活动每周至少参与1次大额赌资赌博单次赌资超1万元累计赌资达数十万元。');
addInputField('target_basic_info_clue', '被核查人员工作基本情况张三1980年5月生本科文化中共党员现为某公司总经理正处级。'); addInputField('target_basic_info_clue', '张三汉族1990年9月出生云南普洱人研究生学历2005年8月参加工作2006年10月加入中国共产党。2004年8月至2005年2月在云南省农业机械公司工作2005年2月至2012年2月历任云南省农业机械公司办公室副主任、主任、团委书记2012年2月至2018年3月任云南省农业机械公司支部书记、厂长2018年3月至2020年3月任云南省农业机械公司总经理助理、销售部部长2020年3月至2022年3月任云南省农业机械公司总经理助理2022年3月至2022年7月任云南省农业机械公司大理分公司副经理2022年7月至2023年12月任云南省农业机械公司西双版纳分公司经理2023年12月至今任云南省农业机械公司党支部书记、经理。');
// 初始化默认输出字段(包含完整的字段列表) // 初始化默认输出字段(包含完整的字段列表)
addOutputField('target_name'); addOutputField('target_name');
@ -548,26 +551,81 @@
// ==================== 文档生成接口相关 ==================== // ==================== 文档生成接口相关 ====================
function initGenerateTab() { async function loadAvailableFiles() {
try {
const response = await fetch('/api/file-configs');
const result = await response.json();
if (result.isSuccess && result.data && result.data.fileConfigs) {
const container = document.getElementById('fileListContainer');
container.innerHTML = ''; // 清空现有列表
// 只添加有filePath的文件有模板文件的
const filesWithPath = result.data.fileConfigs.filter(f => f.filePath);
if (filesWithPath.length === 0) {
alert('没有找到可用的文件配置需要有filePath');
return;
}
// 添加前5个文件作为示例
filesWithPath.slice(0, 5).forEach(file => {
addFileItem(file.fileId, file.fileName);
});
if (filesWithPath.length > 5) {
alert(`已加载前5个文件共找到 ${filesWithPath.length} 个可用文件`);
} else {
alert(`已加载 ${filesWithPath.length} 个可用文件`);
}
} else {
alert('获取文件列表失败: ' + (result.errorMsg || '未知错误'));
}
} catch (error) {
alert('加载文件列表失败: ' + error.message);
}
}
async function initGenerateTab() {
// 初始化默认字段(完整的虚拟测试数据) // 初始化默认字段(完整的虚拟测试数据)
addGenerateField('target_name', '张三'); addGenerateField('target_name', '张三');
addGenerateField('target_gender', '男'); addGenerateField('target_gender', '男');
addGenerateField('target_age', '44'); addGenerateField('target_age', '34');
addGenerateField('target_date_of_birth', '198005'); addGenerateField('target_date_of_birth', '199009');
addGenerateField('target_organization_and_position', '某公司总经理'); addGenerateField('target_organization_and_position', '云南省农业机械公司党支部书记、经理');
addGenerateField('target_organization', '某公司'); addGenerateField('target_organization', '云南省农业机械公司');
addGenerateField('target_position', '总经理'); addGenerateField('target_position', '党支部书记、经理');
addGenerateField('target_education_level', '本科'); addGenerateField('target_education_level', '研究生');
addGenerateField('target_political_status', '中共党员'); addGenerateField('target_political_status', '中共党员');
addGenerateField('target_professional_rank', '正处级'); addGenerateField('target_professional_rank', '');
addGenerateField('clue_source', '群众举报'); addGenerateField('clue_source', '');
addGenerateField('target_issue_description', '违反国家计划生育有关政策规定于2010年10月生育二胎。'); addGenerateField('target_issue_description', '张三多次在私下聚会、网络群组中发表抹黑党中央决策部署的言论传播歪曲党的理论和路线方针政策的错误观点频繁接受管理服务对象安排的高档宴请、私人会所聚餐以及高尔夫球、高端足浴等娱乐活动相关费用均由对方全额承担在干部选拔任用、岗位调整工作中利用职务便利收受他人财物利用职权为其亲属经营的公司谋取不正当利益帮助该公司违规承接本单位及关联单位工程项目3个合同总额超200万元从中收受亲属给予的"感谢费"15万元其本人沉迷赌博活动每周至少参与1次大额赌资赌博单次赌资超1万元累计赌资达数十万元。');
addGenerateField('department_opinion', '建议进行初步核实'); addGenerateField('department_opinion', '');
addGenerateField('filler_name', '李四'); addGenerateField('filler_name', '');
// 初始化默认文件(包含多个模板用于测试) // 自动加载可用的文件列表只加载前2个作为示例
addFileItem(1, '初步核实审批表.doc', 'PRELIMINARY_VERIFICATION_APPROVAL'); try {
addFileItem(2, '请示报告卡.doc', 'REPORT_CARD'); const response = await fetch('/api/file-configs');
const result = await response.json();
if (result.isSuccess && result.data && result.data.fileConfigs) {
// 只添加有filePath的文件有模板文件的
const filesWithPath = result.data.fileConfigs.filter(f => f.filePath);
// 添加前2个文件作为示例
filesWithPath.slice(0, 2).forEach(file => {
addFileItem(file.fileId, file.fileName);
});
} else {
// 如果加载失败使用默认的fileId
addFileItem(1765273961883544, '初步核实审批表.doc'); // 2.初步核实审批表XXX
addFileItem(1765273961563507, '请示报告卡.doc'); // 1.请示报告卡XXX
}
} catch (error) {
// 如果加载失败使用默认的fileId
addFileItem(1765273961883544, '初步核实审批表.doc');
addFileItem(1765273961563507, '请示报告卡.doc');
}
} }
function addGenerateField(fieldCode = '', fieldValue = '') { function addGenerateField(fieldCode = '', fieldValue = '') {
@ -584,15 +642,14 @@
container.appendChild(fieldDiv); container.appendChild(fieldDiv);
} }
function addFileItem(fileId = '', fileName = '', templateCode = '') { function addFileItem(fileId = '', fileName = '') {
const container = document.getElementById('fileListContainer'); const container = document.getElementById('fileListContainer');
const fileDiv = document.createElement('div'); const fileDiv = document.createElement('div');
fileDiv.className = 'field-row'; fileDiv.className = 'field-row';
fileDiv.innerHTML = ` fileDiv.innerHTML = `
<input type="number" placeholder="文件ID" value="${fileId}" class="file-id" style="width: 150px;"> <input type="number" placeholder="文件ID (从f_polic_file_config表获取)" value="${fileId}" class="file-id" style="width: 200px;">
<div style="display: flex; gap: 10px; flex: 1;"> <div style="display: flex; gap: 10px; flex: 1;">
<input type="text" placeholder="文件名称 (如: 初步核实审批表.doc)" value="${fileName}" class="file-name" style="flex: 1;"> <input type="text" placeholder="文件名称 (如: 初步核实审批表.doc)" value="${fileName}" class="file-name" style="flex: 1;">
<input type="text" placeholder="模板编码 (如: PRELIMINARY_VERIFICATION_APPROVAL)" value="${templateCode}" class="template-code" style="flex: 1;">
<button class="btn btn-danger" onclick="removeField(this)">删除</button> <button class="btn btn-danger" onclick="removeField(this)">删除</button>
</div> </div>
`; `;
@ -628,13 +685,11 @@
fileContainers.forEach(container => { fileContainers.forEach(container => {
const fileId = container.querySelector('.file-id').value.trim(); const fileId = container.querySelector('.file-id').value.trim();
const fileName = container.querySelector('.file-name').value.trim(); const fileName = container.querySelector('.file-name').value.trim();
const templateCode = container.querySelector('.template-code').value.trim();
if (fileId && fileName && templateCode) { if (fileId) {
fileList.push({ fileList.push({
fileId: parseInt(fileId), fileId: parseInt(fileId),
fileName: fileName, fileName: fileName || 'generated.docx' // fileName可选
templateCode: templateCode
}); });
} }
}); });

View File

@ -0,0 +1,552 @@
"""
根据Excel数据设计文档同步更新模板的input_datatemplate_code和字段关联关系
"""
import os
import json
import pymysql
import pandas as pd
from pathlib import Path
from typing import Dict, List, Optional, Set
from datetime import datetime
from collections import defaultdict
# 数据库连接配置
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'
}
TENANT_ID = 615873064429507639
CREATED_BY = 655162080928945152
UPDATED_BY = 655162080928945152
# Excel文件路径
EXCEL_FILE = '技术文档/智慧监督项目模板数据结构设计表-20251125-一凡标注.xlsx'
# 模板名称映射Excel中的名称 -> 数据库中的名称)
TEMPLATE_NAME_MAPPING = {
'请示报告卡': '1.请示报告卡XXX',
'初步核实审批表': '2.初步核实审批表XXX',
'初核方案': '3.附件初核方案(XXX)',
'谈话通知书': '谈话通知书',
'谈话通知书第一联': '谈话通知书第一联',
'谈话通知书第二联': '谈话通知书第二联',
'谈话通知书第三联': '谈话通知书第三联',
'走读式谈话审批': '走读式谈话审批',
'走读式谈话流程': '走读式谈话流程',
'请示报告卡(初核报告结论)': '8-1请示报告卡初核报告结论 ',
'XXX初核情况报告': '8.XXX初核情况报告',
}
# 模板编码映射Excel中的名称 -> template_code
TEMPLATE_CODE_MAPPING = {
'请示报告卡': 'REPORT_CARD',
'初步核实审批表': 'PRELIMINARY_VERIFICATION_APPROVAL',
'初核方案': 'INVESTIGATION_PLAN',
'谈话通知书第一联': 'NOTIFICATION_LETTER_1',
'谈话通知书第二联': 'NOTIFICATION_LETTER_2',
'谈话通知书第三联': 'NOTIFICATION_LETTER_3',
'请示报告卡(初核报告结论)': 'REPORT_CARD_CONCLUSION',
'XXX初核情况报告': 'INVESTIGATION_REPORT',
}
# 字段名称到字段编码的映射
FIELD_NAME_TO_CODE_MAP = {
# 输入字段
'线索信息': 'clue_info',
'被核查人员工作基本情况线索': 'target_basic_info_clue',
# 输出字段 - 基本信息
'被核查人姓名': 'target_name',
'被核查人员单位及职务': 'target_organization_and_position',
'被核查人员性别': 'target_gender',
'被核查人员出生年月': 'target_date_of_birth',
'被核查人员出生年月日': 'target_date_of_birth_full',
'被核查人员政治面貌': 'target_political_status',
'被核查人员职级': 'target_professional_rank',
'被核查人员单位': 'target_organization',
'被核查人员职务': 'target_position',
# 输出字段 - 其他信息
'线索来源': 'clue_source',
'主要问题线索': 'target_issue_description',
'初步核实审批表承办部门意见': 'department_opinion',
'初步核实审批表填表人': 'filler_name',
'请示报告卡请示时间': 'report_card_request_time',
'被核查人员身份证件及号码': 'target_id_number',
'被核查人员身份证号': 'target_id_number',
'应到时间': 'appointment_time',
'应到地点': 'appointment_location',
'批准时间': 'approval_time',
'承办部门': 'handling_department',
'承办人': 'handler_name',
'谈话通知时间': 'notification_time',
'谈话通知地点': 'notification_location',
'被核查人员住址': 'target_address',
'被核查人员户籍住址': 'target_registered_address',
'被核查人员联系方式': 'target_contact',
'被核查人员籍贯': 'target_place_of_origin',
'被核查人员民族': 'target_ethnicity',
'被核查人员工作基本情况': 'target_work_basic_info',
'核查单位名称': 'investigation_unit_name',
'核查组组长姓名': 'investigation_team_leader_name',
'核查组成员姓名': 'investigation_team_member_names',
'核查地点': 'investigation_location',
}
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_template_name(name: str) -> str:
"""标准化模板名称,用于匹配"""
import re
# 去掉开头的编号和括号内容
name = re.sub(r'^\d+[\.\-]\s*', '', name)
name = re.sub(r'[(].*?[)]', '', name)
name = name.strip()
return name
def parse_excel_data() -> Dict:
"""解析Excel文件提取模板和字段的关联关系"""
print("="*80)
print("解析Excel数据设计文档")
print("="*80)
if not Path(EXCEL_FILE).exists():
print(f"✗ Excel文件不存在: {EXCEL_FILE}")
return None
try:
df = pd.read_excel(EXCEL_FILE)
print(f"✓ 成功读取Excel文件{len(df)} 行数据\n")
templates = defaultdict(lambda: {
'template_name': '',
'template_code': '',
'input_fields': [],
'output_fields': []
})
current_template = None
current_input_field = None
for idx, row in df.iterrows():
level1 = row.get('一级分类')
level2 = row.get('二级分类')
level3 = row.get('三级分类')
input_field = row.get('输入数据字段')
output_field = row.get('输出数据字段')
# 处理二级分类(模板名称)
if pd.notna(level2) and level2:
current_template = str(level2).strip()
# 获取模板编码
template_code = TEMPLATE_CODE_MAPPING.get(current_template, '')
if not template_code:
# 如果没有映射,尝试生成
template_code = current_template.upper().replace(' ', '_')
templates[current_template]['template_name'] = current_template
templates[current_template]['template_code'] = template_code
current_input_field = None # 重置输入字段
print(f" 模板: {current_template} (code: {template_code})")
# 处理三级分类(子模板,如谈话通知书第一联)
if pd.notna(level3) and level3:
current_template = str(level3).strip()
template_code = TEMPLATE_CODE_MAPPING.get(current_template, '')
if not template_code:
template_code = current_template.upper().replace(' ', '_')
templates[current_template]['template_name'] = current_template
templates[current_template]['template_code'] = template_code
current_input_field = None
print(f" 子模板: {current_template} (code: {template_code})")
# 处理输入字段
if pd.notna(input_field) and input_field:
input_field_name = str(input_field).strip()
if input_field_name != current_input_field:
current_input_field = input_field_name
field_code = FIELD_NAME_TO_CODE_MAP.get(input_field_name, input_field_name.lower().replace(' ', '_'))
if current_template:
templates[current_template]['input_fields'].append({
'name': input_field_name,
'field_code': field_code
})
# 处理输出字段
if pd.notna(output_field) and output_field:
output_field_name = str(output_field).strip()
field_code = FIELD_NAME_TO_CODE_MAP.get(output_field_name, output_field_name.lower().replace(' ', '_'))
if current_template:
templates[current_template]['output_fields'].append({
'name': output_field_name,
'field_code': field_code
})
# 去重
for template_name, template_info in templates.items():
# 输入字段去重
seen_input = set()
unique_input = []
for field in template_info['input_fields']:
key = field['field_code']
if key not in seen_input:
seen_input.add(key)
unique_input.append(field)
template_info['input_fields'] = unique_input
# 输出字段去重
seen_output = set()
unique_output = []
for field in template_info['output_fields']:
key = field['field_code']
if key not in seen_output:
seen_output.add(key)
unique_output.append(field)
template_info['output_fields'] = unique_output
print(f"\n✓ 解析完成,共 {len(templates)} 个模板")
for template_name, template_info in templates.items():
print(f" - {template_name}: {len(template_info['input_fields'])} 个输入字段, {len(template_info['output_fields'])} 个输出字段")
return dict(templates)
except Exception as e:
print(f"✗ 解析Excel文件失败: {e}")
import traceback
traceback.print_exc()
return None
def get_database_templates(conn) -> Dict:
"""获取数据库中的模板配置"""
cursor = conn.cursor(pymysql.cursors.DictCursor)
sql = """
SELECT id, name, template_code, input_data, parent_id
FROM f_polic_file_config
WHERE tenant_id = %s
"""
cursor.execute(sql, (TENANT_ID,))
configs = cursor.fetchall()
result = {}
for config in configs:
name = config['name']
result[name] = config
# 也添加标准化名称的映射
normalized = normalize_template_name(name)
if normalized not in result:
result[normalized] = config
cursor.close()
return result
def get_database_fields(conn) -> Dict:
"""获取数据库中的字段定义"""
cursor = conn.cursor(pymysql.cursors.DictCursor)
sql = """
SELECT id, name, filed_code, field_type
FROM f_polic_field
WHERE tenant_id = %s
"""
cursor.execute(sql, (TENANT_ID,))
fields = cursor.fetchall()
result = {
'by_code': {},
'by_name': {}
}
for field in fields:
field_code = field['filed_code']
field_name = field['name']
result['by_code'][field_code] = field
result['by_name'][field_name] = field
cursor.close()
return result
def find_matching_template(excel_template_name: str, db_templates: Dict) -> Optional[Dict]:
"""查找匹配的数据库模板"""
# 1. 精确匹配
if excel_template_name in db_templates:
return db_templates[excel_template_name]
# 2. 通过映射表匹配
mapped_name = TEMPLATE_NAME_MAPPING.get(excel_template_name)
if mapped_name and mapped_name in db_templates:
return db_templates[mapped_name]
# 3. 标准化名称匹配
normalized = normalize_template_name(excel_template_name)
if normalized in db_templates:
return db_templates[normalized]
# 4. 模糊匹配
for db_name, db_config in db_templates.items():
if normalized in normalize_template_name(db_name) or normalize_template_name(db_name) in normalized:
return db_config
return None
def update_template_config(conn, template_id: int, template_code: str, input_fields: List[Dict], dry_run: bool = True):
"""更新模板配置的input_data和template_code"""
cursor = conn.cursor()
try:
# 构建input_data
input_data = {
'template_code': template_code,
'business_type': 'INVESTIGATION',
'input_fields': [f['field_code'] for f in input_fields]
}
input_data_json = json.dumps(input_data, ensure_ascii=False)
if not dry_run:
update_sql = """
UPDATE f_polic_file_config
SET template_code = %s, input_data = %s, updated_time = NOW(), updated_by = %s
WHERE id = %s AND tenant_id = %s
"""
cursor.execute(update_sql, (template_code, input_data_json, UPDATED_BY, template_id, TENANT_ID))
conn.commit()
print(f" ✓ 更新模板配置")
else:
print(f" [模拟] 将更新模板配置: template_code={template_code}")
finally:
cursor.close()
def update_template_field_relations(conn, template_id: int, input_fields: List[Dict], output_fields: List[Dict],
db_fields: Dict, dry_run: bool = True):
"""更新模板和字段的关联关系"""
cursor = conn.cursor()
try:
# 先删除旧的关联关系
if not dry_run:
delete_sql = """
DELETE FROM f_polic_file_field
WHERE tenant_id = %s AND file_id = %s
"""
cursor.execute(delete_sql, (TENANT_ID, template_id))
# 创建新的关联关系
relations_created = 0
# 关联输入字段field_type=1
for field_info in input_fields:
field_code = field_info['field_code']
field = db_fields['by_code'].get(field_code)
if not field:
print(f" ⚠ 输入字段不存在: {field_code}")
continue
if field['field_type'] != 1:
print(f" ⚠ 字段类型不匹配: {field_code} (期望输入字段,实际为输出字段)")
continue
if not dry_run:
# 检查是否已存在
check_sql = """
SELECT id FROM f_polic_file_field
WHERE tenant_id = %s AND file_id = %s AND filed_id = %s
"""
cursor.execute(check_sql, (TENANT_ID, template_id, field['id']))
existing = cursor.fetchone()
if not existing:
relation_id = generate_id()
insert_sql = """
INSERT INTO f_polic_file_field
(id, tenant_id, file_id, filed_id, created_time, created_by, updated_time, updated_by, state)
VALUES (%s, %s, %s, %s, NOW(), %s, NOW(), %s, %s)
"""
cursor.execute(insert_sql, (
relation_id, TENANT_ID, template_id, field['id'],
CREATED_BY, UPDATED_BY, 1
))
relations_created += 1
else:
relations_created += 1
# 关联输出字段field_type=2
for field_info in output_fields:
field_code = field_info['field_code']
field = db_fields['by_code'].get(field_code)
if not field:
# 尝试通过名称匹配
field_name = field_info['name']
field = db_fields['by_name'].get(field_name)
if not field:
print(f" ⚠ 输出字段不存在: {field_code} ({field_info['name']})")
continue
if field['field_type'] != 2:
print(f" ⚠ 字段类型不匹配: {field_code} (期望输出字段,实际为输入字段)")
continue
if not dry_run:
# 检查是否已存在
check_sql = """
SELECT id FROM f_polic_file_field
WHERE tenant_id = %s AND file_id = %s AND filed_id = %s
"""
cursor.execute(check_sql, (TENANT_ID, template_id, field['id']))
existing = cursor.fetchone()
if not existing:
relation_id = generate_id()
insert_sql = """
INSERT INTO f_polic_file_field
(id, tenant_id, file_id, filed_id, created_time, created_by, updated_time, updated_by, state)
VALUES (%s, %s, %s, %s, NOW(), %s, NOW(), %s, %s)
"""
cursor.execute(insert_sql, (
relation_id, TENANT_ID, template_id, field['id'],
CREATED_BY, UPDATED_BY, 1
))
relations_created += 1
else:
relations_created += 1
if not dry_run:
conn.commit()
print(f" ✓ 创建 {relations_created} 个字段关联关系")
else:
print(f" [模拟] 将创建 {relations_created} 个字段关联关系")
finally:
cursor.close()
def main():
"""主函数"""
print("="*80)
print("同步模板字段信息根据Excel数据设计文档")
print("="*80)
# 解析Excel
excel_data = parse_excel_data()
if not excel_data:
return
# 连接数据库
try:
conn = pymysql.connect(**DB_CONFIG)
print("\n✓ 数据库连接成功")
except Exception as e:
print(f"\n✗ 数据库连接失败: {e}")
return
try:
# 获取数据库中的模板和字段
print("\n获取数据库中的模板和字段...")
db_templates = get_database_templates(conn)
db_fields = get_database_fields(conn)
print(f" 数据库中有 {len(db_templates)} 个模板")
print(f" 数据库中有 {len(db_fields['by_code'])} 个字段")
# 匹配和更新
print("\n" + "="*80)
print("匹配模板并更新配置")
print("="*80)
matched_count = 0
unmatched_templates = []
for excel_template_name, template_info in excel_data.items():
print(f"\n处理模板: {excel_template_name}")
# 查找匹配的数据库模板
db_template = find_matching_template(excel_template_name, db_templates)
if not db_template:
print(f" ✗ 未找到匹配的数据库模板")
unmatched_templates.append(excel_template_name)
continue
print(f" ✓ 匹配到数据库模板: {db_template['name']} (ID: {db_template['id']})")
matched_count += 1
# 更新模板配置
template_code = template_info['template_code']
input_fields = template_info['input_fields']
output_fields = template_info['output_fields']
print(f" 模板编码: {template_code}")
print(f" 输入字段: {len(input_fields)}")
print(f" 输出字段: {len(output_fields)}")
# 先执行模拟更新
print(" [模拟模式]")
update_template_config(conn, db_template['id'], template_code, input_fields, dry_run=True)
update_template_field_relations(conn, db_template['id'], input_fields, output_fields, db_fields, dry_run=True)
# 显示统计
print("\n" + "="*80)
print("统计信息")
print("="*80)
print(f"Excel中的模板数: {len(excel_data)}")
print(f"成功匹配: {matched_count}")
print(f"未匹配: {len(unmatched_templates)}")
if unmatched_templates:
print("\n未匹配的模板:")
for template in unmatched_templates:
print(f" - {template}")
# 询问是否执行实际更新
print("\n" + "="*80)
response = input("\n是否执行实际更新?(yes/no默认no): ").strip().lower()
if response == 'yes':
print("\n执行实际更新...")
for excel_template_name, template_info in excel_data.items():
db_template = find_matching_template(excel_template_name, db_templates)
if db_template:
print(f"\n更新: {db_template['name']}")
update_template_config(conn, db_template['id'], template_info['template_code'],
template_info['input_fields'], dry_run=False)
update_template_field_relations(conn, db_template['id'],
template_info['input_fields'],
template_info['output_fields'],
db_fields, dry_run=False)
print("\n" + "="*80)
print("✓ 同步完成!")
print("="*80)
else:
print("\n已取消更新")
finally:
conn.close()
print("\n数据库连接已关闭")
if __name__ == '__main__':
main()

View File

@ -15,12 +15,50 @@ class TemplateAIHelper:
"""模板AI辅助类用于智能分析文档内容""" """模板AI辅助类用于智能分析文档内容"""
def __init__(self): def __init__(self):
self.api_key = os.getenv('SILICONFLOW_API_KEY') # ========== AI服务提供商选择 ==========
self.model = os.getenv('SILICONFLOW_MODEL', 'deepseek-ai/DeepSeek-V3.2-Exp') # 通过环境变量 AI_PROVIDER 选择使用的AI服务
self.api_url = "https://api.siliconflow.cn/v1/chat/completions" # 可选值: 'huawei' 或 'siliconflow',默认为 'siliconflow'
ai_provider = os.getenv('AI_PROVIDER', 'siliconflow').lower()
if not self.api_key: # ========== 华为大模型配置 ==========
raise Exception("未配置 SILICONFLOW_API_KEY请在 .env 文件中设置") huawei_key = os.getenv('HUAWEI_API_KEY', 'sk-PoeiV3qwyTIRqcVc84E8E24cD2904872859a87922e0d9186')
huawei_endpoint = os.getenv('HUAWEI_API_ENDPOINT', 'http://10.100.31.26:3001/v1/chat/completions')
huawei_model = os.getenv('HUAWEI_MODEL', 'DeepSeek-R1-Distill-Llama-70B')
# ========== 硅基流动配置 ==========
siliconflow_key = os.getenv('SILICONFLOW_API_KEY', '')
siliconflow_url = os.getenv('SILICONFLOW_URL', 'https://api.siliconflow.cn/v1/chat/completions')
siliconflow_model = os.getenv('SILICONFLOW_MODEL', 'deepseek-ai/DeepSeek-V3.2-Exp')
# 根据配置选择服务提供商
if ai_provider == 'huawei':
if not huawei_key or not huawei_endpoint:
raise Exception("未配置华为大模型服务,请设置 HUAWEI_API_KEY 和 HUAWEI_API_ENDPOINT或设置 AI_PROVIDER=siliconflow 使用硅基流动")
self.api_key = huawei_key
self.model = huawei_model
self.api_url = huawei_endpoint
print(f"[模板AI助手] 使用华为大模型: {huawei_model}")
elif ai_provider == 'siliconflow':
if not siliconflow_key:
raise Exception("未配置硅基流动服务,请设置 SILICONFLOW_API_KEY或设置 AI_PROVIDER=huawei 使用华为大模型")
self.api_key = siliconflow_key
self.model = siliconflow_model
self.api_url = siliconflow_url
print(f"[模板AI助手] 使用硅基流动: {siliconflow_model}")
else:
# 自动检测:优先使用硅基流动,如果未配置则使用华为大模型
if siliconflow_key and siliconflow_url:
self.api_key = siliconflow_key
self.model = siliconflow_model
self.api_url = siliconflow_url
print(f"[模板AI助手] 自动选择硅基流动: {siliconflow_model}")
elif huawei_key and huawei_endpoint:
self.api_key = huawei_key
self.model = huawei_model
self.api_url = huawei_endpoint
print(f"[模板AI助手] 自动选择华为大模型: {huawei_model}")
else:
raise Exception("未配置AI服务请设置 AI_PROVIDER 环境变量('huawei''siliconflow'并配置相应的API密钥")
def test_api_connection(self) -> bool: def test_api_connection(self) -> bool:
""" """
@ -30,7 +68,9 @@ class TemplateAIHelper:
是否连接成功 是否连接成功
""" """
try: try:
print(" [测试] 正在测试硅基流动API连接...") print(f" [测试] 正在测试API连接...")
# 测试payload
test_payload = { test_payload = {
"model": self.model, "model": self.model,
"messages": [ "messages": [
@ -39,9 +79,14 @@ class TemplateAIHelper:
"content": "测试" "content": "测试"
} }
], ],
"temperature": 0.5,
"max_tokens": 10 "max_tokens": 10
} }
# 如果是华为大模型,添加额外的参数
if 'huawei' in self.api_url.lower() or '10.100.31.26' in self.api_url:
test_payload["stream"] = False
headers = { headers = {
"Authorization": f"Bearer {self.api_key}", "Authorization": f"Bearer {self.api_key}",
"Content-Type": "application/json" "Content-Type": "application/json"
@ -150,10 +195,21 @@ class TemplateAIHelper:
"content": prompt "content": prompt
} }
], ],
"temperature": 0.2, "temperature": 0.5,
"max_tokens": 4000 "max_tokens": 8192
} }
# 如果是华为大模型,添加额外的参数
if 'huawei' in self.api_url.lower() or '10.100.31.26' in self.api_url:
payload["stream"] = False
payload["presence_penalty"] = 1.03
payload["frequency_penalty"] = 1.0
payload["repetition_penalty"] = 1.0
payload["top_p"] = 0.95
payload["top_k"] = 1
payload["seed"] = 1
payload["n"] = 1
headers = { headers = {
"Authorization": f"Bearer {self.api_key}", "Authorization": f"Bearer {self.api_key}",
"Content-Type": "application/json" "Content-Type": "application/json"

View File

@ -0,0 +1,41 @@
"""
测试通过fileId生成文档不再依赖templateCode
"""
import sys
import os
sys.path.insert(0, os.path.dirname(__file__))
from services.document_service import DocumentService
# 测试通过fileId获取文件配置
print("="*80)
print("Test: Get file config by fileId")
print("="*80)
service = DocumentService()
# 测试查询一个已知的文件ID从之前的查询结果中获取
# "1.请示报告卡(初核谈话)" 的ID是 1765273963893166
test_file_id = 1765273963893166
print(f"\nTest file ID: {test_file_id}")
print("-" * 80)
result = service.get_file_config_by_id(test_file_id)
if result:
print("\n[OK] Found file config:")
print(f" - ID: {result['id']}")
print(f" - Name: {result['name']}")
print(f" - File Path: {result['file_path']}")
else:
print("\n[ERROR] File config not found")
print(" Possible reasons:")
print(" 1. File ID does not exist")
print(" 2. File state is not enabled (state != 1)")
print(" 3. Tenant ID mismatch")
print("\n" + "="*80)
print("Test completed")
print("="*80)

View File

@ -1,5 +1,5 @@
""" """
测试硅基流动API连接 测试大模型API连接华为大模型默认硅基流动备用
""" """
import os import os
from dotenv import load_dotenv from dotenv import load_dotenv
@ -8,23 +8,23 @@ import requests
# 加载环境变量 # 加载环境变量
load_dotenv() load_dotenv()
def test_siliconflow_api(): def test_huawei_api():
"""测试硅基流动API连接""" """测试华为大模型API连接"""
print("="*80) print("="*80)
print("测试硅基流动API连接") print("测试华为大模型API连接")
print("="*80) print("="*80)
print() print()
# 读取配置 # 读取配置(华为大模型)
api_key = os.getenv('SILICONFLOW_API_KEY') api_key = os.getenv('HUAWEI_API_KEY', 'sk-PoeiV3qwyTIRqcVc84E8E24cD2904872859a87922e0d9186')
model = os.getenv('SILICONFLOW_MODEL', 'deepseek-ai/DeepSeek-V3.2-Exp') model = os.getenv('HUAWEI_MODEL', 'DeepSeek-R1-Distill-Llama-70B')
api_url = "https://api.siliconflow.cn/v1/chat/completions" api_url = os.getenv('HUAWEI_API_ENDPOINT', 'http://10.100.31.26:3001/v1/chat/completions')
# 检查配置 # 检查配置
print("1. 检查配置...") print("1. 检查配置...")
if not api_key: if not api_key:
print(" ✗ 错误: 未找到 SILICONFLOW_API_KEY") print(" ✗ 错误: 未找到 HUAWEI_API_KEY")
print(" 请在 .env 文件中设置: SILICONFLOW_API_KEY=你的API密钥") print(" 请在 .env 文件中设置: HUAWEI_API_KEY=你的API密钥")
return False return False
print(f" ✓ API密钥: {api_key[:10]}...{api_key[-5:]}") print(f" ✓ API密钥: {api_key[:10]}...{api_key[-5:]}")
@ -43,7 +43,16 @@ def test_siliconflow_api():
"content": "请回复'测试成功'" "content": "请回复'测试成功'"
} }
], ],
"max_tokens": 20 "stream": False,
"presence_penalty": 1.03,
"frequency_penalty": 1.0,
"repetition_penalty": 1.0,
"temperature": 0.5,
"top_p": 0.95,
"top_k": 1,
"seed": 1,
"max_tokens": 20,
"n": 1
} }
headers = { headers = {
@ -87,7 +96,7 @@ def test_siliconflow_api():
return False return False
if __name__ == '__main__': if __name__ == '__main__':
success = test_siliconflow_api() success = test_huawei_api()
print() print()
print("="*80) print("="*80)
if success: if success:

View File

@ -0,0 +1,95 @@
"""
测试AI日志记录功能
"""
import sys
import os
from pathlib import Path
# 添加项目根目录到路径
project_root = Path(__file__).parent.parent
sys.path.insert(0, str(project_root))
from services.ai_logger import get_ai_logger
def test_logger():
"""测试日志记录器"""
print("=" * 60)
print("测试AI日志记录功能")
print("=" * 60)
# 获取日志记录器
logger = get_ai_logger()
# 测试记录一次对话
print("\n1. 测试记录对话...")
prompt = "请从以下文本中提取信息张三1980年5月出生某公司总经理"
api_request = {
"endpoint": "http://test.example.com/v1/chat/completions",
"model": "test-model",
"messages": [
{"role": "system", "content": "你是一个数据提取助手"},
{"role": "user", "content": prompt}
],
"temperature": 0.2,
"max_tokens": 1000,
"enable_thinking": False
}
api_response = {
"choices": [
{
"message": {
"content": '{"target_name": "张三", "target_gender": ""}'
}
}
]
}
extracted_data = {
"target_name": "张三",
"target_gender": "",
"target_date_of_birth": "1980年05月"
}
log_file = logger.log_conversation(
prompt=prompt,
api_request=api_request,
api_response=api_response,
extracted_data=extracted_data,
error=None,
session_id="test_session_001"
)
if log_file:
print(f"✓ 日志已保存: {log_file}")
else:
print("✗ 日志保存失败")
return
# 测试读取日志
print("\n2. 测试读取日志...")
log_data = logger.read_log(log_file)
if log_data:
print(f"✓ 日志读取成功")
print(f" 时间戳: {log_data['timestamp']}")
print(f" 会话ID: {log_data['session_id']}")
print(f" 成功: {log_data['success']}")
print(f" 提取的字段数: {len(log_data.get('extracted_data', {}))}")
else:
print("✗ 日志读取失败")
return
# 测试获取最近的日志
print("\n3. 测试获取最近的日志...")
recent_logs = logger.get_recent_logs(limit=5)
print(f"✓ 找到 {len(recent_logs)} 条最近的日志")
for i, log_file in enumerate(recent_logs, 1):
print(f" {i}. {Path(log_file).name}")
print("\n" + "=" * 60)
print("测试完成!")
print("=" * 60)
print(f"\n日志目录: {logger.log_dir}")
print(f"日志状态: {'启用' if logger.enabled else '禁用'}")
if __name__ == "__main__":
test_logger()

467
update_all_templates.py Normal file
View File

@ -0,0 +1,467 @@
"""
更新 template_finish 目录下所有模板文件
重新上传到 MinIO 并更新数据库信息确保模板文件是最新版本
"""
import os
import sys
import json
import pymysql
from minio import Minio
from minio.error import S3Error
from datetime import datetime
from pathlib import Path
from typing import Dict, List, Optional
# 设置控制台编码为UTF-8Windows兼容
if sys.platform == 'win32':
try:
sys.stdout.reconfigure(encoding='utf-8')
sys.stderr.reconfigure(encoding='utf-8')
except:
pass
# MinIO连接配置
MINIO_CONFIG = {
'endpoint': 'minio.datacubeworld.com:9000',
'access_key': 'JOLXFXny3avFSzB0uRA5',
'secret_key': 'G1BR8jStNfovkfH5ou39EmPl34E4l7dGrnd3Cz0I',
'secure': True # 使用HTTPS
}
# 数据库连接配置
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'
# 项目根目录
PROJECT_ROOT = Path(__file__).parent
TEMPLATES_DIR = PROJECT_ROOT / "template_finish"
# 文档类型映射(根据完整文件名识别,保持原文件名不变)
# 每个文件名都是独立的模板使用完整文件名作为key
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"
},
"4.点对点交接单2": {
"template_code": "HANDOVER_FORM_2",
"name": "4.点对点交接单2",
"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 identify_document_type(file_name: str) -> Optional[Dict]:
"""
根据完整文件名识别文档类型保持原文件名不变
Args:
file_name: 文件名不含扩展名
Returns:
文档类型配置如果无法识别返回None
"""
# 获取文件名(不含扩展名),保持原样
base_name = Path(file_name).stem
# 直接使用完整文件名进行精确匹配
if base_name in DOCUMENT_TYPE_MAPPING:
return DOCUMENT_TYPE_MAPPING[base_name]
# 如果精确匹配失败返回None不进行任何修改或模糊匹配
return None
def upload_to_minio(file_path: Path, 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()
object_name = f'{TENANT_ID}/TEMPLATE/{now.year}/{now.month:02d}/{file_path.name}'
# 上传文件fput_object 会自动覆盖已存在的文件)
minio_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 update_file_config(conn, doc_config: Dict, file_path: str) -> int:
"""
更新或创建文件配置记录
Args:
conn: 数据库连接
doc_config: 文档配置
file_path: MinIO文件路径
Returns:
文件配置ID
"""
cursor = conn.cursor()
current_time = datetime.now()
try:
# 检查是否已存在(通过 template_code 查找)
select_sql = """
SELECT id, name, file_path FROM f_polic_file_config
WHERE tenant_id = %s AND template_code = %s
"""
cursor.execute(select_sql, (TENANT_ID, doc_config['template_code']))
existing = cursor.fetchone()
# 构建 input_data
input_data = json.dumps({
'template_code': doc_config['template_code'],
'business_type': doc_config['business_type']
}, ensure_ascii=False)
if existing:
file_config_id, old_name, old_path = existing
# 更新现有记录
update_sql = """
UPDATE f_polic_file_config
SET file_path = %s,
input_data = %s,
name = %s,
updated_time = %s,
updated_by = %s,
state = 1
WHERE id = %s AND tenant_id = %s
"""
cursor.execute(update_sql, (
file_path,
input_data,
doc_config['name'],
current_time,
UPDATED_BY,
file_config_id,
TENANT_ID
))
conn.commit()
print(f" [OK] 更新数据库记录 (ID: {file_config_id})")
if old_path != file_path:
print(f" 旧路径: {old_path}")
print(f" 新路径: {file_path}")
return file_config_id
else:
# 创建新记录
import time
import random
timestamp = int(time.time() * 1000)
random_part = random.randint(100000, 999999)
file_config_id = timestamp * 1000 + random_part
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, %s, %s, %s, %s, %s)
"""
cursor.execute(insert_sql, (
file_config_id,
TENANT_ID,
None, # parent_id
doc_config['name'],
input_data,
file_path,
doc_config['template_code'],
current_time,
CREATED_BY,
current_time,
CREATED_BY,
1 # state: 1表示启用
))
conn.commit()
print(f" [OK] 创建新数据库记录 (ID: {file_config_id})")
return file_config_id
except Exception as e:
conn.rollback()
raise Exception(f"更新数据库失败: {str(e)}")
finally:
cursor.close()
def update_all_templates():
"""
更新所有模板文件重新上传到MinIO并更新数据库
"""
print("="*80)
print("开始更新所有模板文件")
print("="*80)
print(f"模板目录: {TEMPLATES_DIR}")
print()
if not TEMPLATES_DIR.exists():
print(f"错误: 模板目录不存在: {TEMPLATES_DIR}")
return
# 连接数据库和MinIO
try:
conn = pymysql.connect(**DB_CONFIG)
print("[OK] 数据库连接成功")
minio_client = Minio(
MINIO_CONFIG['endpoint'],
access_key=MINIO_CONFIG['access_key'],
secret_key=MINIO_CONFIG['secret_key'],
secure=MINIO_CONFIG['secure']
)
# 检查存储桶
if not minio_client.bucket_exists(BUCKET_NAME):
raise Exception(f"存储桶 '{BUCKET_NAME}' 不存在,请先创建")
print("[OK] MinIO连接成功")
print()
except Exception as e:
print(f"[ERROR] 连接失败: {e}")
return
# 统计信息
processed_count = 0
updated_count = 0
created_count = 0
skipped_count = 0
failed_count = 0
failed_files = []
# 遍历所有.docx文件
print("="*80)
print("开始处理模板文件...")
print("="*80)
print()
for root, dirs, files in os.walk(TEMPLATES_DIR):
for file in files:
# 只处理.docx文件跳过临时文件
if not file.endswith('.docx') or file.startswith('~$'):
continue
file_path = Path(root) / file
# 识别文档类型
doc_config = identify_document_type(file)
if not doc_config:
print(f"\n[{processed_count + skipped_count + failed_count + 1}] [WARN] 跳过: {file}")
print(f" 原因: 无法识别文档类型")
print(f" 路径: {file_path}")
skipped_count += 1
continue
processed_count += 1
print(f"\n[{processed_count}] 处理: {file}")
print(f" 类型: {doc_config.get('template_code', 'UNKNOWN')}")
print(f" 名称: {doc_config.get('name', 'UNKNOWN')}")
print(f" 路径: {file_path}")
try:
# 检查文件是否存在
if not file_path.exists():
raise FileNotFoundError(f"文件不存在: {file_path}")
# 获取文件信息
file_size = file_path.stat().st_size
file_mtime = datetime.fromtimestamp(file_path.stat().st_mtime)
print(f" 大小: {file_size:,} 字节")
print(f" 修改时间: {file_mtime.strftime('%Y-%m-%d %H:%M:%S')}")
# 上传到MinIO覆盖旧版本
print(f" 上传到MinIO...")
minio_path = upload_to_minio(file_path, minio_client)
print(f" [OK] MinIO路径: {minio_path}")
# 更新数据库
print(f" 更新数据库...")
file_config_id = update_file_config(conn, doc_config, minio_path)
# 判断是更新还是创建
cursor = conn.cursor()
check_sql = """
SELECT created_time, updated_time FROM f_polic_file_config
WHERE id = %s
"""
cursor.execute(check_sql, (file_config_id,))
result = cursor.fetchone()
cursor.close()
if result:
created_time, updated_time = result
if created_time == updated_time:
created_count += 1
else:
updated_count += 1
print(f" [OK] 处理成功 (配置ID: {file_config_id})")
except Exception as e:
failed_count += 1
failed_files.append((str(file_path), str(e)))
print(f" [ERROR] 处理失败: {e}")
import traceback
traceback.print_exc()
# 关闭数据库连接
conn.close()
# 输出统计信息
print("\n" + "="*80)
print("更新完成")
print("="*80)
print(f"总处理数: {processed_count}")
print(f" 成功更新: {updated_count}")
print(f" 成功创建: {created_count}")
print(f" 跳过: {skipped_count}")
print(f" 失败: {failed_count}")
if failed_files:
print("\n失败的文件:")
for file_path, error in failed_files:
print(f" - {file_path}")
print(f" 错误: {error}")
print("\n所有模板文件已更新到最新版本!")
if __name__ == '__main__':
update_all_templates()

618
update_template_tree.py Normal file
View File

@ -0,0 +1,618 @@
"""
更新模板树状结构
根据 template_finish 目录结构更新数据库中的 parent_id 字段
"""
import os
import json
import pymysql
from pathlib import Path
from typing import Dict, List, Optional, Tuple
from datetime import datetime
# 数据库连接配置
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'
}
TENANT_ID = 615873064429507639
CREATED_BY = 655162080928945152
UPDATED_BY = 655162080928945152
# 项目根目录
PROJECT_ROOT = Path(__file__).parent
TEMPLATES_DIR = PROJECT_ROOT / "template_finish"
# 从 init_all_templates.py 复制的文档类型映射
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"
},
"4.点对点交接单2": {
"template_code": "HANDOVER_FORM_2",
"name": "4.点对点交接单2",
"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 normalize_name(name: str) -> str:
"""标准化名称,用于模糊匹配"""
import re
# 去掉开头的编号(如 "1."、"2."、"8-1" 等)
name = re.sub(r'^\d+[\.\-]\s*', '', name)
# 去掉括号及其内容(如 "XXX"、"(初核谈话)" 等)
name = re.sub(r'[(].*?[)]', '', name)
# 去掉空格和特殊字符
name = name.strip()
return name
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 scan_directory_structure(base_dir: Path) -> Dict:
"""扫描目录结构,构建树状层级"""
structure = {
'directories': {}, # {path: {'name': ..., 'parent': ..., 'level': ...}}
'files': {} # {file_path: {'name': ..., 'parent': ..., 'template_code': ...}}
}
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)
structure['files'][str(path)] = {
'name': file_name,
'parent': parent_path,
'level': level,
'template_code': doc_config['template_code'] if doc_config else None,
'full_path': str(path),
'normalized_name': normalize_name(file_name)
}
elif path.is_dir():
# 处理目录
dir_name = path.name
structure['directories'][str(path)] = {
'name': dir_name,
'parent': parent_path,
'level': level,
'normalized_name': normalize_name(dir_name)
}
# 递归处理子目录和文件
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 structure
def find_matching_config(file_info: Dict, existing_data: Dict) -> Optional[Dict]:
"""
查找匹配的数据库记录
优先级1. template_code 精确匹配 2. 名称精确匹配 3. 标准化名称匹配
"""
template_code = file_info.get('template_code')
file_name = file_info['name']
normalized_name = file_info.get('normalized_name', normalize_name(file_name))
# 优先级1: template_code 精确匹配
if template_code:
matched = existing_data['by_template_code'].get(template_code)
if matched:
return matched
# 优先级2: 名称精确匹配
matched = existing_data['by_name'].get(file_name)
if matched:
return matched
# 优先级3: 标准化名称匹配
candidates = existing_data['by_normalized_name'].get(normalized_name, [])
if candidates:
# 如果有多个候选,优先选择有正确 template_code 的
for candidate in candidates:
if candidate.get('extracted_template_code') == template_code:
return candidate
# 否则返回第一个
return candidates[0]
return None
def get_existing_data(conn) -> Dict:
"""获取数据库中的现有数据"""
cursor = conn.cursor(pymysql.cursors.DictCursor)
sql = """
SELECT id, name, parent_id, template_code, input_data, file_path, state
FROM f_polic_file_config
WHERE tenant_id = %s
"""
cursor.execute(sql, (TENANT_ID,))
configs = cursor.fetchall()
result = {
'by_id': {},
'by_name': {},
'by_template_code': {},
'by_normalized_name': {} # 新增:标准化名称索引
}
for config in configs:
config_id = config['id']
config_name = config['name']
# 尝试从 input_data 中提取 template_code
template_code = config.get('template_code')
if not template_code and config.get('input_data'):
try:
input_data = json.loads(config['input_data']) if isinstance(config['input_data'], str) else config['input_data']
if isinstance(input_data, dict):
template_code = input_data.get('template_code')
except:
pass
config['extracted_template_code'] = template_code
config['normalized_name'] = normalize_name(config_name)
result['by_id'][config_id] = config
result['by_name'][config_name] = config
if template_code:
# 如果已存在相同 template_code保留第一个
if template_code not in result['by_template_code']:
result['by_template_code'][template_code] = config
# 标准化名称索引(可能有多个记录匹配同一个标准化名称)
normalized = config['normalized_name']
if normalized not in result['by_normalized_name']:
result['by_normalized_name'][normalized] = []
result['by_normalized_name'][normalized].append(config)
cursor.close()
return result
def plan_tree_structure(dir_structure: Dict, existing_data: Dict) -> List[Dict]:
"""规划树状结构"""
plan = []
# 按层级排序目录
directories = sorted(dir_structure['directories'].items(),
key=lambda x: (x[1]['level'], x[0]))
# 按层级排序文件
files = sorted(dir_structure['files'].items(),
key=lambda x: (x[1]['level'], x[0]))
# 创建目录映射用于查找父目录ID
dir_id_map = {} # {dir_path: config_id}
# 处理目录(按层级顺序)
for dir_path, dir_info in directories:
dir_name = dir_info['name']
parent_path = dir_info['parent']
level = dir_info['level']
# 查找父目录ID
parent_id = None
if parent_path:
parent_id = dir_id_map.get(parent_path)
# 查找匹配的数据库记录(使用改进的匹配逻辑)
existing = find_matching_config(dir_info, existing_data)
if existing:
# 使用现有记录
plan.append({
'type': 'directory',
'name': dir_name,
'parent_name': dir_structure['directories'].get(parent_path, {}).get('name') if parent_path else None,
'parent_id': parent_id,
'level': level,
'action': 'update',
'config_id': existing['id'],
'current_parent_id': existing.get('parent_id')
})
dir_id_map[dir_path] = existing['id']
else:
# 创建新记录(目录节点)
new_id = generate_id()
plan.append({
'type': 'directory',
'name': dir_name,
'parent_name': dir_structure['directories'].get(parent_path, {}).get('name') if parent_path else None,
'parent_id': parent_id,
'level': level,
'action': 'create',
'config_id': new_id,
'current_parent_id': None
})
dir_id_map[dir_path] = new_id
# 处理文件
for file_path, file_info in files:
file_name = file_info['name']
parent_path = file_info['parent']
level = file_info['level']
template_code = file_info['template_code']
# 查找父目录ID
parent_id = dir_id_map.get(parent_path) if parent_path else None
# 查找匹配的数据库记录(使用改进的匹配逻辑)
existing = find_matching_config(file_info, existing_data)
if existing:
# 更新现有记录
plan.append({
'type': 'file',
'name': file_name,
'parent_name': dir_structure['directories'].get(parent_path, {}).get('name') if parent_path else None,
'parent_id': parent_id,
'level': level,
'action': 'update',
'config_id': existing['id'],
'template_code': template_code,
'current_parent_id': existing.get('parent_id')
})
else:
# 创建新记录(文件节点)- 这种情况应该很少,因为文件应该已经在数据库中
new_id = generate_id()
plan.append({
'type': 'file',
'name': file_name,
'parent_name': dir_structure['directories'].get(parent_path, {}).get('name') if parent_path else None,
'parent_id': parent_id,
'level': level,
'action': 'create',
'config_id': new_id,
'template_code': template_code,
'current_parent_id': None
})
return plan
def print_preview(plan: List[Dict]):
"""打印更新预览"""
print("\n" + "="*80)
print("更新预览")
print("="*80)
# 按层级分组
by_level = {}
for item in plan:
level = item['level']
if level not in by_level:
by_level[level] = []
by_level[level].append(item)
# 按层级顺序显示
for level in sorted(by_level.keys()):
print(f"\n【层级 {level}")
for item in by_level[level]:
indent = " " * level
if item['action'] == 'create':
print(f"{indent}+ 创建: {item['name']} (ID: {item['config_id']})")
if item['parent_name']:
print(f"{indent} 父节点: {item['parent_name']}")
else:
current = item.get('current_parent_id', 'None')
new = item.get('parent_id', 'None')
if current != new:
print(f"{indent}→ 更新: {item['name']} (ID: {item['config_id']})")
print(f"{indent} parent_id: {current}{new}")
if item['parent_name']:
print(f"{indent} 父节点: {item['parent_name']}")
else:
print(f"{indent}✓ 无需更新: {item['name']} (parent_id 已正确)")
def execute_update(conn, plan: List[Dict], dry_run: bool = True):
"""执行更新"""
cursor = conn.cursor()
try:
if not dry_run:
conn.autocommit(False)
# 按层级分组
by_level = {}
for item in plan:
level = item['level']
if level not in by_level:
by_level[level] = []
by_level[level].append(item)
create_count = 0
update_count = 0
skip_count = 0
# 按层级顺序处理(从顶层到底层)
for level in sorted(by_level.keys()):
for item in by_level[level]:
if item['action'] == 'create':
# 创建新记录
if not dry_run:
if item['type'] == 'directory':
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, (
item['config_id'],
TENANT_ID,
item['parent_id'],
item['name'],
None,
None,
CREATED_BY,
UPDATED_BY,
1
))
else:
# 文件节点
input_data = json.dumps({
'template_code': item.get('template_code', ''),
'business_type': 'INVESTIGATION'
}, ensure_ascii=False)
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, (
item['config_id'],
TENANT_ID,
item['parent_id'],
item['name'],
input_data,
None,
item.get('template_code'),
CREATED_BY,
UPDATED_BY,
1
))
create_count += 1
print(f"{'[模拟]' if dry_run else ''}创建: {item['name']}")
else:
# 更新现有记录
current_parent = item.get('current_parent_id')
new_parent = item.get('parent_id')
if current_parent != new_parent:
if not dry_run:
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, (
new_parent,
UPDATED_BY,
item['config_id'],
TENANT_ID
))
update_count += 1
print(f"{'[模拟]' if dry_run else ''}更新: {item['name']} (parent_id: {current_parent}{new_parent})")
else:
skip_count += 1
if not dry_run:
conn.commit()
print(f"\n✓ 更新完成!")
else:
print(f"\n[模拟模式] 未实际执行更新")
print(f"\n统计:")
print(f" - 创建: {create_count}")
print(f" - 更新: {update_count}")
print(f" - 跳过: {skip_count}")
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)
# 连接数据库
try:
conn = pymysql.connect(**DB_CONFIG)
print("✓ 数据库连接成功\n")
except Exception as e:
print(f"✗ 数据库连接失败: {e}")
return
try:
# 扫描目录结构
print("扫描目录结构...")
dir_structure = scan_directory_structure(TEMPLATES_DIR)
print(f" 找到 {len(dir_structure['directories'])} 个目录")
print(f" 找到 {len(dir_structure['files'])} 个文件\n")
# 获取数据库现有数据
print("获取数据库现有数据...")
existing_data = get_existing_data(conn)
print(f" 数据库中有 {len(existing_data['by_id'])} 条记录\n")
# 规划树状结构
print("规划树状结构...")
plan = plan_tree_structure(dir_structure, existing_data)
print(f" 生成 {len(plan)} 个更新计划\n")
# 打印预览
print_preview(plan)
# 询问是否执行
print("\n" + "="*80)
response = input("\n是否执行更新?(yes/no默认no): ").strip().lower()
if response == 'yes':
# 先执行一次模拟
print("\n执行模拟更新...")
execute_update(conn, plan, dry_run=True)
# 再次确认
print("\n" + "="*80)
confirm = input("\n确认执行实际更新?(yes/no默认no): ").strip().lower()
if confirm == 'yes':
print("\n执行实际更新...")
execute_update(conn, plan, dry_run=False)
else:
print("\n已取消更新")
else:
print("\n已取消更新")
finally:
conn.close()
print("\n数据库连接已关闭")
if __name__ == '__main__':
main()

159
update_template_tree.sql Normal file
View File

@ -0,0 +1,159 @@
-- 模板树状结构更新脚本
-- 生成时间: 2025-12-09 17:39:51
-- 注意:执行前请备份数据库!
USE finyx;
START TRANSACTION;
-- ===== 层级 0 =====
-- 创建目录节点: 2-初核模版
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 (1765273080357704, 615873064429507639, NULL, '2-初核模版', NULL, NULL, NOW(), 655162080928945152, NOW(), 655162080928945152, 1);
-- ===== 层级 1 =====
-- 创建目录节点: 1.初核请示
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 (1765273080719940, 615873064429507639, 1765273080357704, '1.初核请示', NULL, NULL, NOW(), 655162080928945152, NOW(), 655162080928945152, 1);
-- 更新: 2.谈话审批 (parent_id: None -> 1765273080357704)
UPDATE f_polic_file_config
SET parent_id = 1765273080357704, updated_time = NOW(), updated_by = 655162080928945152
WHERE id = 704825582342212610 AND tenant_id = 615873064429507639;
-- 更新: 3.初核结论 (parent_id: None -> 1765273080357704)
UPDATE f_polic_file_config
SET parent_id = 1765273080357704, updated_time = NOW(), updated_by = 655162080928945152
WHERE id = 704825582342212611 AND tenant_id = 615873064429507639;
-- ===== 层级 2 =====
-- 更新: 谈话通知书 (parent_id: None -> 704825582342212610)
UPDATE f_polic_file_config
SET parent_id = 704825582342212610, updated_time = NOW(), updated_by = 655162080928945152
WHERE id = 1764836033451564 AND tenant_id = 615873064429507639;
-- 更新: 走读式谈话审批 (parent_id: None -> 704825582342212610)
UPDATE f_polic_file_config
SET parent_id = 704825582342212610, updated_time = NOW(), updated_by = 655162080928945152
WHERE id = 1764836034070056 AND tenant_id = 615873064429507639;
-- 更新: 走读式谈话流程 (parent_id: None -> 704825582342212610)
UPDATE f_polic_file_config
SET parent_id = 704825582342212610, updated_time = NOW(), updated_by = 655162080928945152
WHERE id = 1764836034052009 AND tenant_id = 615873064429507639;
-- 更新: 1.请示报告卡XXX (parent_id: None -> 1765273080719940)
UPDATE f_polic_file_config
SET parent_id = 1765273080719940, updated_time = NOW(), updated_by = 655162080928945152
WHERE id = 1764836033251691 AND tenant_id = 615873064429507639;
-- 更新: 2.初步核实审批表XXX (parent_id: None -> 1765273080719940)
UPDATE f_polic_file_config
SET parent_id = 1765273080719940, updated_time = NOW(), updated_by = 655162080928945152
WHERE id = 1764656918061150 AND tenant_id = 615873064429507639;
-- 更新: 3.附件初核方案(XXX) (parent_id: None -> 1765273080719940)
UPDATE f_polic_file_config
SET parent_id = 1765273080719940, updated_time = NOW(), updated_by = 655162080928945152
WHERE id = 1765242273284972 AND tenant_id = 615873064429507639;
-- 更新: 8-1请示报告卡初核报告结论 (parent_id: None -> 704825582342212611)
UPDATE f_polic_file_config
SET parent_id = 704825582342212611, updated_time = NOW(), updated_by = 655162080928945152
WHERE id = 1765242278419277 AND tenant_id = 615873064429507639;
-- 更新: 8.XXX初核情况报告 (parent_id: None -> 704825582342212611)
UPDATE f_polic_file_config
SET parent_id = 704825582342212611, updated_time = NOW(), updated_by = 655162080928945152
WHERE id = 1765242278832792 AND tenant_id = 615873064429507639;
-- ===== 层级 3 =====
-- 更新: 谈话通知书第一联 (parent_id: None -> 1764836033451564)
UPDATE f_polic_file_config
SET parent_id = 1764836033451564, updated_time = NOW(), updated_by = 655162080928945152
WHERE id = 1765242274101483 AND tenant_id = 615873064429507639;
-- 更新: 谈话通知书第三联 (parent_id: None -> 1764836033451564)
UPDATE f_polic_file_config
SET parent_id = 1764836033451564, updated_time = NOW(), updated_by = 655162080928945152
WHERE id = 1765242274109904 AND tenant_id = 615873064429507639;
-- 更新: 谈话通知书第二联 (parent_id: None -> 1764836033451564)
UPDATE f_polic_file_config
SET parent_id = 1764836033451564, updated_time = NOW(), updated_by = 655162080928945152
WHERE id = 1765242273898117 AND tenant_id = 615873064429507639;
-- 更新: 1.请示报告卡(初核谈话) (parent_id: None -> 1764836034070056)
UPDATE f_polic_file_config
SET parent_id = 1764836034070056, updated_time = NOW(), updated_by = 655162080928945152
WHERE id = 1765242274961528 AND tenant_id = 615873064429507639;
-- 更新: 2谈话审批表 (parent_id: None -> 1764836034070056)
UPDATE f_polic_file_config
SET parent_id = 1764836034070056, updated_time = NOW(), updated_by = 655162080928945152
WHERE id = 1765242275071133 AND tenant_id = 615873064429507639;
-- 更新: 3.谈话前安全风险评估表 (parent_id: None -> 1764836034070056)
UPDATE f_polic_file_config
SET parent_id = 1764836034070056, updated_time = NOW(), updated_by = 655162080928945152
WHERE id = 1765242275362306 AND tenant_id = 615873064429507639;
-- 更新: 4.谈话方案 (parent_id: None -> 1764836034070056)
UPDATE f_polic_file_config
SET parent_id = 1764836034070056, updated_time = NOW(), updated_by = 655162080928945152
WHERE id = 1765242275716334 AND tenant_id = 615873064429507639;
-- 更新: 5.谈话后安全风险评估表 (parent_id: None -> 1764836034070056)
UPDATE f_polic_file_config
SET parent_id = 1764836034070056, updated_time = NOW(), updated_by = 655162080928945152
WHERE id = 1765242275780395 AND tenant_id = 615873064429507639;
-- 更新: 1.谈话笔录 (parent_id: None -> 1764836034052009)
UPDATE f_polic_file_config
SET parent_id = 1764836034052009, updated_time = NOW(), updated_by = 655162080928945152
WHERE id = 1765242276549299 AND tenant_id = 615873064429507639;
-- 更新: 2.谈话询问对象情况摸底调查30问 (parent_id: None -> 1764836034052009)
UPDATE f_polic_file_config
SET parent_id = 1764836034052009, updated_time = NOW(), updated_by = 655162080928945152
WHERE id = 1765242276522490 AND tenant_id = 615873064429507639;
-- 更新: 3.被谈话人权利义务告知书 (parent_id: None -> 1764836034052009)
UPDATE f_polic_file_config
SET parent_id = 1764836034052009, updated_time = NOW(), updated_by = 655162080928945152
WHERE id = 1765242277165087 AND tenant_id = 615873064429507639;
-- 更新: 4.点对点交接单 (parent_id: None -> 1764836034052009)
UPDATE f_polic_file_config
SET parent_id = 1764836034052009, updated_time = NOW(), updated_by = 655162080928945152
WHERE id = 1765242276709614 AND tenant_id = 615873064429507639;
-- 更新: 5.陪送交接单(新) (parent_id: None -> 1764836034052009)
UPDATE f_polic_file_config
SET parent_id = 1764836034052009, updated_time = NOW(), updated_by = 655162080928945152
WHERE id = 1765242277149374 AND tenant_id = 615873064429507639;
-- 更新: 6.1保密承诺书(谈话对象使用-非中共党员用) (parent_id: None -> 1764836034052009)
UPDATE f_polic_file_config
SET parent_id = 1764836034052009, updated_time = NOW(), updated_by = 655162080928945152
WHERE id = 1765242277776686 AND tenant_id = 615873064429507639;
-- 更新: 6.2保密承诺书(谈话对象使用-中共党员用) (parent_id: None -> 1764836034052009)
UPDATE f_polic_file_config
SET parent_id = 1764836034052009, updated_time = NOW(), updated_by = 655162080928945152
WHERE id = 1765242277897239 AND tenant_id = 615873064429507639;
-- 更新: 7.办案人员-办案安全保密承诺书 (parent_id: None -> 1764836034052009)
UPDATE f_polic_file_config
SET parent_id = 1764836034052009, updated_time = NOW(), updated_by = 655162080928945152
WHERE id = 1765242278111656 AND tenant_id = 615873064429507639;
COMMIT;
-- 更新完成

148
verify_field_code_fix.py Normal file
View File

@ -0,0 +1,148 @@
"""
验证字段编码修复结果并处理剩余的真正问题
"""
import os
import pymysql
import re
from typing import Dict, List
# 数据库连接配置
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'
}
TENANT_ID = 615873064429507639
def is_chinese(text: str) -> bool:
"""判断字符串是否包含中文字符"""
if not text:
return False
return bool(re.search(r'[\u4e00-\u9fff]', text))
def verify_fix():
"""验证修复结果"""
conn = pymysql.connect(**DB_CONFIG)
cursor = conn.cursor(pymysql.cursors.DictCursor)
print("="*80)
print("验证字段编码修复结果")
print("="*80)
# 查询所有字段
cursor.execute("""
SELECT id, name, filed_code, field_type, state
FROM f_polic_field
WHERE tenant_id = %s
ORDER BY name
""", (TENANT_ID,))
fields = cursor.fetchall()
# 找出仍然包含中文的field_code
chinese_fields = []
for field in fields:
if field['filed_code'] and is_chinese(field['filed_code']):
chinese_fields.append(field)
print(f"\n总共 {len(fields)} 个字段")
print(f"仍有 {len(chinese_fields)} 个字段的field_code包含中文:\n")
if chinese_fields:
for field in chinese_fields:
print(f" ID: {field['id']}")
print(f" 名称: {field['name']}")
print(f" field_code: {field['filed_code']}")
print(f" field_type: {field['field_type']}")
print()
# 检查重复的字段名称
name_to_fields = {}
for field in fields:
name = field['name']
if name not in name_to_fields:
name_to_fields[name] = []
name_to_fields[name].append(field)
duplicates = {name: fields_list for name, fields_list in name_to_fields.items()
if len(fields_list) > 1}
print(f"\n仍有 {len(duplicates)} 个重复的字段名称:\n")
for name, fields_list in duplicates.items():
print(f" 字段名称: {name} (共 {len(fields_list)} 条记录)")
for field in fields_list:
print(f" - ID: {field['id']}, field_code: {field['filed_code']}, "
f"field_type: {field['field_type']}, state: {field['state']}")
print()
# 检查f_polic_file_field表中的关联关系
print("="*80)
print("检查 f_polic_file_field 表")
print("="*80)
cursor.execute("""
SELECT fff.id, fff.file_id, fff.filed_id,
fc.name as file_name, f.name as field_name, f.filed_code
FROM f_polic_file_field fff
LEFT JOIN f_polic_file_config fc ON fff.file_id = fc.id
LEFT JOIN f_polic_field f ON fff.filed_id = f.id
WHERE fff.tenant_id = %s AND f.filed_code IS NOT NULL
ORDER BY fff.file_id, fff.filed_id
""", (TENANT_ID,))
relations = cursor.fetchall()
# 检查是否有重复的关联关系
relation_keys = {}
for rel in relations:
key = (rel['file_id'], rel['filed_id'])
if key not in relation_keys:
relation_keys[key] = []
relation_keys[key].append(rel)
duplicate_relations = {key: records for key, records in relation_keys.items()
if len(records) > 1}
print(f"\n总共 {len(relations)} 个关联关系")
print(f"发现 {len(duplicate_relations)} 个重复的关联关系")
# 检查使用中文field_code的关联关系
chinese_relations = [rel for rel in relations
if rel['filed_code'] and is_chinese(rel['filed_code'])]
print(f"使用中文field_code的关联关系: {len(chinese_relations)}")
if chinese_relations:
print("\n前10个使用中文field_code的关联关系:")
for rel in chinese_relations[:10]:
print(f" - 文件: {rel['file_name']}, 字段: {rel['field_name']}, "
f"field_code: {rel['filed_code']}")
cursor.close()
conn.close()
return {
'total_fields': len(fields),
'chinese_fields': len(chinese_fields),
'duplicate_names': len(duplicates),
'duplicate_relations': len(duplicate_relations),
'chinese_relations': len(chinese_relations)
}
if __name__ == '__main__':
result = verify_fix()
print("\n" + "="*80)
print("验证完成")
print("="*80)
print(f"总字段数: {result['total_fields']}")
print(f"中文field_code字段数: {result['chinese_fields']}")
print(f"重复字段名称数: {result['duplicate_names']}")
print(f"重复关联关系数: {result['duplicate_relations']}")
print(f"使用中文field_code的关联关系数: {result['chinese_relations']}")

View File

@ -0,0 +1,345 @@
"""
验证模板字段同步结果
检查 input_datatemplate_code 和字段关联关系是否正确
"""
import os
import json
import pymysql
from typing import Dict, List
# 数据库连接配置
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'
}
TENANT_ID = 615873064429507639
def verify_template_configs(conn):
"""验证模板配置的 input_data 和 template_code"""
cursor = conn.cursor(pymysql.cursors.DictCursor)
print("="*80)
print("验证模板配置")
print("="*80)
sql = """
SELECT id, name, template_code, input_data, parent_id
FROM f_polic_file_config
WHERE tenant_id = %s
ORDER BY parent_id, name
"""
cursor.execute(sql, (TENANT_ID,))
configs = cursor.fetchall()
print(f"\n{len(configs)} 个模板配置\n")
# 统计
has_template_code = 0
has_input_data = 0
has_both = 0
missing_both = 0
# 文件节点(有 template_code 的)
file_nodes = []
# 目录节点(没有 template_code 的)
dir_nodes = []
for config in configs:
template_code = config.get('template_code')
input_data = config.get('input_data')
if template_code:
has_template_code += 1
file_nodes.append(config)
else:
dir_nodes.append(config)
if input_data:
has_input_data += 1
try:
input_data_dict = json.loads(input_data) if isinstance(input_data, str) else input_data
if isinstance(input_data_dict, dict) and input_data_dict.get('template_code'):
has_both += 1
except:
pass
if not template_code and not input_data:
missing_both += 1
print("统计信息:")
print(f" 文件节点(有 template_code: {len(file_nodes)}")
print(f" 目录节点(无 template_code: {len(dir_nodes)}")
print(f" 有 input_data: {has_input_data}")
print(f" 同时有 template_code 和 input_data: {has_both}")
print(f" 两者都没有: {missing_both}")
# 检查文件节点的 input_data
print("\n文件节点 input_data 检查:")
missing_input_data = []
for config in file_nodes:
input_data = config.get('input_data')
if not input_data:
missing_input_data.append(config)
else:
try:
input_data_dict = json.loads(input_data) if isinstance(input_data, str) else input_data
if not isinstance(input_data_dict, dict) or 'template_code' not in input_data_dict:
missing_input_data.append(config)
except:
missing_input_data.append(config)
if missing_input_data:
print(f" ⚠ 有 {len(missing_input_data)} 个文件节点缺少或格式错误的 input_data:")
for config in missing_input_data[:10]: # 只显示前10个
print(f" - {config['name']} (ID: {config['id']})")
if len(missing_input_data) > 10:
print(f" ... 还有 {len(missing_input_data) - 10}")
else:
print(" ✓ 所有文件节点都有正确的 input_data")
cursor.close()
return {
'total': len(configs),
'file_nodes': len(file_nodes),
'dir_nodes': len(dir_nodes),
'has_input_data': has_input_data,
'has_both': has_both,
'missing_input_data': len(missing_input_data)
}
def verify_field_relations(conn):
"""验证字段关联关系"""
cursor = conn.cursor(pymysql.cursors.DictCursor)
print("\n" + "="*80)
print("验证字段关联关系")
print("="*80)
# 获取所有文件节点的字段关联
sql = """
SELECT
fc.id as file_id,
fc.name as file_name,
fc.template_code,
COUNT(ff.id) as field_count,
SUM(CASE WHEN f.field_type = 1 THEN 1 ELSE 0 END) as input_field_count,
SUM(CASE WHEN f.field_type = 2 THEN 1 ELSE 0 END) as output_field_count
FROM f_polic_file_config fc
LEFT JOIN f_polic_file_field ff ON fc.id = ff.file_id AND ff.tenant_id = fc.tenant_id
LEFT JOIN f_polic_field f ON ff.filed_id = f.id AND f.tenant_id = fc.tenant_id
WHERE fc.tenant_id = %s AND fc.template_code IS NOT NULL
GROUP BY fc.id, fc.name, fc.template_code
ORDER BY fc.name
"""
cursor.execute(sql, (TENANT_ID,))
relations = cursor.fetchall()
print(f"\n{len(relations)} 个文件节点有字段关联\n")
# 统计
has_relations = 0
no_relations = 0
has_input_fields = 0
has_output_fields = 0
no_relation_templates = []
for rel in relations:
field_count = rel['field_count'] or 0
input_count = rel['input_field_count'] or 0
output_count = rel['output_field_count'] or 0
if field_count > 0:
has_relations += 1
if input_count > 0:
has_input_fields += 1
if output_count > 0:
has_output_fields += 1
else:
no_relations += 1
no_relation_templates.append(rel)
print("统计信息:")
print(f" 有字段关联: {has_relations}")
print(f" 无字段关联: {no_relations}")
print(f" 有输入字段: {has_input_fields}")
print(f" 有输出字段: {has_output_fields}")
if no_relation_templates:
print(f"\n ⚠ 有 {len(no_relation_templates)} 个文件节点没有字段关联:")
for rel in no_relation_templates[:10]:
print(f" - {rel['file_name']} (code: {rel['template_code']})")
if len(no_relation_templates) > 10:
print(f" ... 还有 {len(no_relation_templates) - 10}")
else:
print("\n ✓ 所有文件节点都有字段关联")
# 显示详细的关联信息前10个
print("\n字段关联详情前10个")
for rel in relations[:10]:
print(f"\n {rel['file_name']} (code: {rel['template_code']})")
print(f" 总字段数: {rel['field_count']}")
print(f" 输入字段: {rel['input_field_count']}")
print(f" 输出字段: {rel['output_field_count']}")
cursor.close()
return {
'total': len(relations),
'has_relations': has_relations,
'no_relations': no_relations,
'has_input_fields': has_input_fields,
'has_output_fields': has_output_fields
}
def verify_input_data_structure(conn):
"""验证 input_data 的结构"""
cursor = conn.cursor(pymysql.cursors.DictCursor)
print("\n" + "="*80)
print("验证 input_data 结构")
print("="*80)
sql = """
SELECT id, name, template_code, input_data
FROM f_polic_file_config
WHERE tenant_id = %s AND template_code IS NOT NULL AND input_data IS NOT NULL
"""
cursor.execute(sql, (TENANT_ID,))
configs = cursor.fetchall()
print(f"\n检查 {len(configs)} 个有 input_data 的文件节点\n")
correct_structure = 0
incorrect_structure = 0
incorrect_items = []
for config in configs:
try:
input_data = json.loads(config['input_data']) if isinstance(config['input_data'], str) else config['input_data']
if not isinstance(input_data, dict):
incorrect_structure += 1
incorrect_items.append({
'name': config['name'],
'reason': 'input_data 不是字典格式'
})
continue
# 检查必需字段
required_fields = ['template_code', 'business_type']
missing_fields = [f for f in required_fields if f not in input_data]
if missing_fields:
incorrect_structure += 1
incorrect_items.append({
'name': config['name'],
'reason': f'缺少字段: {", ".join(missing_fields)}'
})
continue
# 检查 template_code 是否匹配
if input_data.get('template_code') != config.get('template_code'):
incorrect_structure += 1
incorrect_items.append({
'name': config['name'],
'reason': f"template_code 不匹配: input_data中为 '{input_data.get('template_code')}', 字段中为 '{config.get('template_code')}'"
})
continue
correct_structure += 1
except json.JSONDecodeError as e:
incorrect_structure += 1
incorrect_items.append({
'name': config['name'],
'reason': f'JSON解析错误: {str(e)}'
})
except Exception as e:
incorrect_structure += 1
incorrect_items.append({
'name': config['name'],
'reason': f'其他错误: {str(e)}'
})
print(f" 结构正确: {correct_structure}")
print(f" 结构错误: {incorrect_structure}")
if incorrect_items:
print("\n 错误详情:")
for item in incorrect_items[:10]:
print(f" - {item['name']}: {item['reason']}")
if len(incorrect_items) > 10:
print(f" ... 还有 {len(incorrect_items) - 10} 个错误")
else:
print("\n ✓ 所有 input_data 结构都正确")
cursor.close()
return {
'correct': correct_structure,
'incorrect': incorrect_structure
}
def main():
"""主函数"""
print("="*80)
print("验证模板字段同步结果")
print("="*80)
try:
conn = pymysql.connect(**DB_CONFIG)
print("✓ 数据库连接成功\n")
except Exception as e:
print(f"✗ 数据库连接失败: {e}")
return
try:
# 验证模板配置
config_stats = verify_template_configs(conn)
# 验证字段关联
relation_stats = verify_field_relations(conn)
# 验证 input_data 结构
input_data_stats = verify_input_data_structure(conn)
# 总结
print("\n" + "="*80)
print("验证总结")
print("="*80)
print(f"模板配置:")
print(f" - 总模板数: {config_stats['total']}")
print(f" - 文件节点: {config_stats['file_nodes']}")
print(f" - 缺少 input_data: {config_stats['missing_input_data']}")
print(f"\n字段关联:")
print(f" - 有字段关联: {relation_stats['has_relations']}")
print(f" - 无字段关联: {relation_stats['no_relations']}")
print(f"\ninput_data 结构:")
print(f" - 正确: {input_data_stats['correct']}")
print(f" - 错误: {input_data_stats['incorrect']}")
# 总体评估
print("\n" + "="*80)
if (config_stats['missing_input_data'] == 0 and
relation_stats['no_relations'] == 0 and
input_data_stats['incorrect'] == 0):
print("✓ 所有验证通过!同步成功!")
else:
print("⚠ 发现一些问题,请检查上述详情")
finally:
conn.close()
print("\n数据库连接已关闭")
if __name__ == '__main__':
main()

View File

@ -0,0 +1,531 @@
"""
检查模板的 file_id 和相关关联关系是否正确
重点检查
1. f_polic_file_config 表中的模板记录file_id
2. f_polic_file_field 表中的关联关系file_id filed_id 的对应关系
"""
import sys
import pymysql
from pathlib import Path
from typing import Dict, List, Set, Tuple
from collections import defaultdict
# 设置控制台编码为UTF-8Windows兼容
if sys.platform == 'win32':
try:
sys.stdout.reconfigure(encoding='utf-8')
sys.stderr.reconfigure(encoding='utf-8')
except:
pass
# 数据库连接配置
DB_CONFIG = {
'host': '152.136.177.240',
'port': 5012,
'user': 'finyx',
'password': '6QsGK6MpePZDE57Z',
'database': 'finyx',
'charset': 'utf8mb4'
}
# 固定值
TENANT_ID = 615873064429507639
# 项目根目录
PROJECT_ROOT = Path(__file__).parent
TEMPLATES_DIR = PROJECT_ROOT / "template_finish"
# 文档类型映射(用于识别模板)
DOCUMENT_TYPE_MAPPING = {
"1.请示报告卡XXX": "REPORT_CARD",
"2.初步核实审批表XXX": "PRELIMINARY_VERIFICATION_APPROVAL",
"3.附件初核方案(XXX)": "INVESTIGATION_PLAN",
"谈话通知书第一联": "NOTIFICATION_LETTER_1",
"谈话通知书第二联": "NOTIFICATION_LETTER_2",
"谈话通知书第三联": "NOTIFICATION_LETTER_3",
"1.请示报告卡(初核谈话)": "REPORT_CARD_INTERVIEW",
"2谈话审批表": "INTERVIEW_APPROVAL_FORM",
"3.谈话前安全风险评估表": "PRE_INTERVIEW_RISK_ASSESSMENT",
"4.谈话方案": "INTERVIEW_PLAN",
"5.谈话后安全风险评估表": "POST_INTERVIEW_RISK_ASSESSMENT",
"1.谈话笔录": "INTERVIEW_RECORD",
"2.谈话询问对象情况摸底调查30问": "INVESTIGATION_30_QUESTIONS",
"3.被谈话人权利义务告知书": "RIGHTS_OBLIGATIONS_NOTICE",
"4.点对点交接单": "HANDOVER_FORM",
"4.点对点交接单2": "HANDOVER_FORM_2",
"5.陪送交接单(新)": "ESCORT_HANDOVER_FORM",
"6.1保密承诺书(谈话对象使用-非中共党员用)": "CONFIDENTIALITY_COMMITMENT_NON_PARTY",
"6.2保密承诺书(谈话对象使用-中共党员用)": "CONFIDENTIALITY_COMMITMENT_PARTY",
"7.办案人员-办案安全保密承诺书": "INVESTIGATOR_CONFIDENTIALITY_COMMITMENT",
"8-1请示报告卡初核报告结论 ": "REPORT_CARD_CONCLUSION",
"8.XXX初核情况报告": "INVESTIGATION_REPORT"
}
def get_template_files() -> Dict[str, Path]:
"""获取所有模板文件"""
templates = {}
if not TEMPLATES_DIR.exists():
return templates
for root, dirs, files in os.walk(TEMPLATES_DIR):
for file in files:
if file.endswith('.docx') and not file.startswith('~$'):
file_path = Path(root) / file
base_name = Path(file).stem
if base_name in DOCUMENT_TYPE_MAPPING:
templates[base_name] = file_path
return templates
def check_file_configs(conn) -> Dict:
"""检查 f_polic_file_config 表中的模板记录"""
print("\n" + "="*80)
print("1. 检查 f_polic_file_config 表中的模板记录")
print("="*80)
cursor = conn.cursor(pymysql.cursors.DictCursor)
# 查询所有模板记录
cursor.execute("""
SELECT id, name, template_code, file_path, state, parent_id
FROM f_polic_file_config
WHERE tenant_id = %s
ORDER BY name
""", (TENANT_ID,))
all_configs = cursor.fetchall()
# 按 template_code 和 name 组织数据
configs_by_code = {}
configs_by_name = {}
for config in all_configs:
config_id = config['id']
name = config['name']
template_code = config.get('template_code')
if template_code:
if template_code not in configs_by_code:
configs_by_code[template_code] = []
configs_by_code[template_code].append(config)
if name:
if name not in configs_by_name:
configs_by_name[name] = []
configs_by_name[name].append(config)
print(f"\n总模板记录数: {len(all_configs)}")
print(f"按 template_code 分组: {len(configs_by_code)} 个不同的 template_code")
print(f"按 name 分组: {len(configs_by_name)} 个不同的 name")
# 检查重复的 template_code
duplicate_codes = {code: configs for code, configs in configs_by_code.items() if len(configs) > 1}
if duplicate_codes:
print(f"\n[WARN] 发现重复的 template_code ({len(duplicate_codes)} 个):")
for code, configs in duplicate_codes.items():
print(f" - {code}: {len(configs)} 条记录")
for cfg in configs:
print(f" ID: {cfg['id']}, 名称: {cfg['name']}, 路径: {cfg.get('file_path', 'N/A')}")
# 检查重复的 name
duplicate_names = {name: configs for name, configs in configs_by_name.items() if len(configs) > 1}
if duplicate_names:
print(f"\n[WARN] 发现重复的 name ({len(duplicate_names)} 个):")
for name, configs in duplicate_names.items():
print(f" - {name}: {len(configs)} 条记录")
for cfg in configs:
print(f" ID: {cfg['id']}, template_code: {cfg.get('template_code', 'N/A')}, 路径: {cfg.get('file_path', 'N/A')}")
# 检查未启用的记录
disabled_configs = [cfg for cfg in all_configs if cfg.get('state') != 1]
if disabled_configs:
print(f"\n[WARN] 发现未启用的模板记录 ({len(disabled_configs)} 个):")
for cfg in disabled_configs:
print(f" - ID: {cfg['id']}, 名称: {cfg['name']}, 状态: {cfg.get('state')}")
# 检查 file_path 为空的记录
empty_path_configs = [cfg for cfg in all_configs if not cfg.get('file_path')]
if empty_path_configs:
print(f"\n[WARN] 发现 file_path 为空的记录 ({len(empty_path_configs)} 个):")
for cfg in empty_path_configs:
print(f" - ID: {cfg['id']}, 名称: {cfg['name']}, template_code: {cfg.get('template_code', 'N/A')}")
cursor.close()
return {
'all_configs': all_configs,
'configs_by_code': configs_by_code,
'configs_by_name': configs_by_name,
'duplicate_codes': duplicate_codes,
'duplicate_names': duplicate_names,
'disabled_configs': disabled_configs,
'empty_path_configs': empty_path_configs
}
def check_file_field_relations(conn) -> Dict:
"""检查 f_polic_file_field 表中的关联关系"""
print("\n" + "="*80)
print("2. 检查 f_polic_file_field 表中的关联关系")
print("="*80)
cursor = conn.cursor(pymysql.cursors.DictCursor)
# 查询所有关联关系
cursor.execute("""
SELECT fff.id, fff.file_id, fff.filed_id, fff.state, fff.tenant_id
FROM f_polic_file_field fff
WHERE fff.tenant_id = %s
ORDER BY fff.file_id, fff.filed_id
""", (TENANT_ID,))
all_relations = cursor.fetchall()
print(f"\n总关联关系数: {len(all_relations)}")
# 检查无效的 file_id关联到不存在的文件配置
cursor.execute("""
SELECT fff.id, fff.file_id, fff.filed_id
FROM f_polic_file_field fff
LEFT JOIN f_polic_file_config fc ON fff.file_id = fc.id AND fff.tenant_id = fc.tenant_id
WHERE fff.tenant_id = %s AND fc.id IS NULL
""", (TENANT_ID,))
invalid_file_relations = cursor.fetchall()
# 检查无效的 filed_id关联到不存在的字段
cursor.execute("""
SELECT fff.id, fff.file_id, fff.filed_id
FROM f_polic_file_field fff
LEFT JOIN f_polic_field f ON fff.filed_id = f.id AND fff.tenant_id = f.tenant_id
WHERE fff.tenant_id = %s AND f.id IS NULL
""", (TENANT_ID,))
invalid_field_relations = cursor.fetchall()
# 检查重复的关联关系(相同的 file_id 和 filed_id
cursor.execute("""
SELECT file_id, filed_id, COUNT(*) as count, GROUP_CONCAT(id ORDER BY id) as ids
FROM f_polic_file_field
WHERE tenant_id = %s
GROUP BY file_id, filed_id
HAVING COUNT(*) > 1
""", (TENANT_ID,))
duplicate_relations = cursor.fetchall()
# 检查关联到未启用文件的记录
cursor.execute("""
SELECT fff.id, fff.file_id, fff.filed_id, fc.name as file_name, fc.state as file_state
FROM f_polic_file_field fff
INNER JOIN f_polic_file_config fc ON fff.file_id = fc.id AND fff.tenant_id = fc.tenant_id
WHERE fff.tenant_id = %s AND fc.state != 1
""", (TENANT_ID,))
disabled_file_relations = cursor.fetchall()
# 检查关联到未启用字段的记录
cursor.execute("""
SELECT fff.id, fff.file_id, fff.filed_id, f.name as field_name, f.filed_code, f.state as field_state
FROM f_polic_file_field fff
INNER JOIN f_polic_field f ON fff.filed_id = f.id AND fff.tenant_id = f.tenant_id
WHERE fff.tenant_id = %s AND f.state != 1
""", (TENANT_ID,))
disabled_field_relations = cursor.fetchall()
# 统计每个文件关联的字段数量
file_field_counts = defaultdict(int)
for rel in all_relations:
file_field_counts[rel['file_id']] += 1
print(f"\n文件关联字段统计:")
print(f" 有关联关系的文件数: {len(file_field_counts)}")
if file_field_counts:
max_count = max(file_field_counts.values())
min_count = min(file_field_counts.values())
avg_count = sum(file_field_counts.values()) / len(file_field_counts)
print(f" 每个文件关联字段数: 最少 {min_count}, 最多 {max_count}, 平均 {avg_count:.1f}")
# 输出检查结果
if invalid_file_relations:
print(f"\n[ERROR] 发现无效的 file_id 关联 ({len(invalid_file_relations)} 条):")
for rel in invalid_file_relations[:10]: # 只显示前10条
print(f" - 关联ID: {rel['id']}, file_id: {rel['file_id']}, filed_id: {rel['filed_id']}")
if len(invalid_file_relations) > 10:
print(f" ... 还有 {len(invalid_file_relations) - 10}")
else:
print(f"\n[OK] 所有 file_id 关联都有效")
if invalid_field_relations:
print(f"\n[ERROR] 发现无效的 filed_id 关联 ({len(invalid_field_relations)} 条):")
for rel in invalid_field_relations[:10]: # 只显示前10条
print(f" - 关联ID: {rel['id']}, file_id: {rel['file_id']}, filed_id: {rel['filed_id']}")
if len(invalid_field_relations) > 10:
print(f" ... 还有 {len(invalid_field_relations) - 10}")
else:
print(f"\n[OK] 所有 filed_id 关联都有效")
if duplicate_relations:
print(f"\n[WARN] 发现重复的关联关系 ({len(duplicate_relations)} 组):")
for dup in duplicate_relations[:10]: # 只显示前10组
print(f" - file_id: {dup['file_id']}, filed_id: {dup['filed_id']}, 重复次数: {dup['count']}, 关联ID: {dup['ids']}")
if len(duplicate_relations) > 10:
print(f" ... 还有 {len(duplicate_relations) - 10}")
else:
print(f"\n[OK] 没有重复的关联关系")
if disabled_file_relations:
print(f"\n[WARN] 发现关联到未启用文件的记录 ({len(disabled_file_relations)} 条):")
for rel in disabled_file_relations[:10]:
print(f" - 文件: {rel['file_name']} (ID: {rel['file_id']}, 状态: {rel['file_state']})")
if len(disabled_file_relations) > 10:
print(f" ... 还有 {len(disabled_file_relations) - 10}")
if disabled_field_relations:
print(f"\n[WARN] 发现关联到未启用字段的记录 ({len(disabled_field_relations)} 条):")
for rel in disabled_field_relations[:10]:
print(f" - 字段: {rel['field_name']} ({rel['filed_code']}, ID: {rel['filed_id']}, 状态: {rel['field_state']})")
if len(disabled_field_relations) > 10:
print(f" ... 还有 {len(disabled_field_relations) - 10}")
cursor.close()
return {
'all_relations': all_relations,
'invalid_file_relations': invalid_file_relations,
'invalid_field_relations': invalid_field_relations,
'duplicate_relations': duplicate_relations,
'disabled_file_relations': disabled_file_relations,
'disabled_field_relations': disabled_field_relations,
'file_field_counts': dict(file_field_counts)
}
def check_template_file_mapping(conn, file_configs: Dict) -> Dict:
"""检查模板文件与数据库记录的映射关系"""
print("\n" + "="*80)
print("3. 检查模板文件与数据库记录的映射关系")
print("="*80)
import os
templates = get_template_files()
print(f"\n本地模板文件数: {len(templates)}")
cursor = conn.cursor(pymysql.cursors.DictCursor)
# 检查每个模板文件是否在数据库中有对应记录
missing_in_db = []
found_in_db = []
duplicate_mappings = []
for template_name, file_path in templates.items():
template_code = DOCUMENT_TYPE_MAPPING.get(template_name)
if not template_code:
continue
# 通过 name 和 template_code 查找对应的数据库记录
# 优先通过 name 精确匹配,然后通过 template_code 匹配
matching_configs = []
# 1. 通过 name 精确匹配
if template_name in file_configs['configs_by_name']:
for config in file_configs['configs_by_name'][template_name]:
if config.get('file_path'): # 有文件路径的记录
matching_configs.append(config)
# 2. 通过 template_code 匹配
if template_code in file_configs['configs_by_code']:
for config in file_configs['configs_by_code'][template_code]:
if config.get('file_path') and config not in matching_configs:
matching_configs.append(config)
if len(matching_configs) == 0:
missing_in_db.append({
'template_name': template_name,
'template_code': template_code,
'file_path': str(file_path)
})
elif len(matching_configs) == 1:
config = matching_configs[0]
found_in_db.append({
'template_name': template_name,
'template_code': template_code,
'file_id': config['id'],
'file_path': config.get('file_path'),
'name': config.get('name')
})
else:
# 多个匹配,选择 file_path 最新的(包含最新日期的)
duplicate_mappings.append({
'template_name': template_name,
'template_code': template_code,
'matching_configs': matching_configs
})
# 仍然记录第一个作为找到的记录
config = matching_configs[0]
found_in_db.append({
'template_name': template_name,
'template_code': template_code,
'file_id': config['id'],
'file_path': config.get('file_path'),
'name': config.get('name'),
'is_duplicate': True
})
print(f"\n找到数据库记录的模板: {len(found_in_db)}")
print(f"未找到数据库记录的模板: {len(missing_in_db)}")
print(f"有重复映射的模板: {len(duplicate_mappings)}")
if duplicate_mappings:
print(f"\n[WARN] 以下模板文件在数据库中有多个匹配记录:")
for item in duplicate_mappings:
print(f" - {item['template_name']} (template_code: {item['template_code']}):")
for cfg in item['matching_configs']:
print(f" * file_id: {cfg['id']}, name: {cfg.get('name')}, path: {cfg.get('file_path', 'N/A')}")
if missing_in_db:
print(f"\n[WARN] 以下模板文件在数据库中没有对应记录:")
for item in missing_in_db:
print(f" - {item['template_name']} (template_code: {item['template_code']})")
cursor.close()
return {
'found_in_db': found_in_db,
'missing_in_db': missing_in_db,
'duplicate_mappings': duplicate_mappings
}
def check_field_type_consistency(conn, relations: Dict) -> Dict:
"""检查关联关系的字段类型一致性"""
print("\n" + "="*80)
print("4. 检查关联关系的字段类型一致性")
print("="*80)
cursor = conn.cursor(pymysql.cursors.DictCursor)
# 查询所有关联关系及其字段类型
cursor.execute("""
SELECT
fff.id,
fff.file_id,
fff.filed_id,
fc.name as file_name,
f.name as field_name,
f.filed_code,
f.field_type,
CASE
WHEN f.field_type = 1 THEN '输入字段'
WHEN f.field_type = 2 THEN '输出字段'
ELSE '未知'
END as field_type_name
FROM f_polic_file_field fff
INNER JOIN f_polic_file_config fc ON fff.file_id = fc.id AND fff.tenant_id = fc.tenant_id
INNER JOIN f_polic_field f ON fff.filed_id = f.id AND fff.tenant_id = f.tenant_id
WHERE fff.tenant_id = %s
ORDER BY fff.file_id, f.field_type, f.name
""", (TENANT_ID,))
all_relations_with_type = cursor.fetchall()
# 统计字段类型分布
input_fields = [r for r in all_relations_with_type if r['field_type'] == 1]
output_fields = [r for r in all_relations_with_type if r['field_type'] == 2]
print(f"\n字段类型统计:")
print(f" 输入字段 (field_type=1): {len(input_fields)} 条关联")
print(f" 输出字段 (field_type=2): {len(output_fields)} 条关联")
# 按文件统计
file_type_counts = defaultdict(lambda: {'input': 0, 'output': 0})
for rel in all_relations_with_type:
file_id = rel['file_id']
if rel['field_type'] == 1:
file_type_counts[file_id]['input'] += 1
elif rel['field_type'] == 2:
file_type_counts[file_id]['output'] += 1
print(f"\n每个文件的字段类型分布:")
for file_id, counts in sorted(file_type_counts.items())[:10]: # 只显示前10个
print(f" 文件ID {file_id}: 输入字段 {counts['input']} 个, 输出字段 {counts['output']}")
if len(file_type_counts) > 10:
print(f" ... 还有 {len(file_type_counts) - 10} 个文件")
cursor.close()
return {
'input_fields': input_fields,
'output_fields': output_fields,
'file_type_counts': dict(file_type_counts)
}
def main():
"""主函数"""
print("="*80)
print("检查模板的 file_id 和相关关联关系")
print("="*80)
# 连接数据库
try:
conn = pymysql.connect(**DB_CONFIG)
print("\n[OK] 数据库连接成功")
except Exception as e:
print(f"\n[ERROR] 数据库连接失败: {e}")
return
try:
# 1. 检查文件配置表
file_configs = check_file_configs(conn)
# 2. 检查文件字段关联表
relations = check_file_field_relations(conn)
# 3. 检查模板文件与数据库记录的映射
template_mapping = check_template_file_mapping(conn, file_configs)
# 4. 检查字段类型一致性
field_type_info = check_field_type_consistency(conn, relations)
# 汇总报告
print("\n" + "="*80)
print("检查汇总")
print("="*80)
issues = []
if file_configs['duplicate_codes']:
issues.append(f"发现 {len(file_configs['duplicate_codes'])} 个重复的 template_code")
if file_configs['duplicate_names']:
issues.append(f"发现 {len(file_configs['duplicate_names'])} 个重复的 name")
if file_configs['empty_path_configs']:
issues.append(f"发现 {len(file_configs['empty_path_configs'])} 个 file_path 为空的记录")
if relations['invalid_file_relations']:
issues.append(f"发现 {len(relations['invalid_file_relations'])} 条无效的 file_id 关联")
if relations['invalid_field_relations']:
issues.append(f"发现 {len(relations['invalid_field_relations'])} 条无效的 filed_id 关联")
if relations['duplicate_relations']:
issues.append(f"发现 {len(relations['duplicate_relations'])} 组重复的关联关系")
if template_mapping['missing_in_db']:
issues.append(f"发现 {len(template_mapping['missing_in_db'])} 个模板文件在数据库中没有对应记录")
if issues:
print("\n[WARN] 发现以下问题:")
for issue in issues:
print(f" - {issue}")
else:
print("\n[OK] 未发现严重问题")
print(f"\n总模板记录数: {len(file_configs['all_configs'])}")
print(f"总关联关系数: {len(relations['all_relations'])}")
print(f"有关联关系的文件数: {len(relations['file_field_counts'])}")
finally:
conn.close()
print("\n数据库连接已关闭")
if __name__ == '__main__':
import os
main()

169
verify_tree_structure.py Normal file
View File

@ -0,0 +1,169 @@
"""
验证树状结构更新结果
"""
import os
import json
import pymysql
from typing import Dict, List
# 数据库连接配置
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'
}
TENANT_ID = 615873064429507639
def print_tree_structure(conn):
"""打印树状结构"""
cursor = conn.cursor(pymysql.cursors.DictCursor)
sql = """
SELECT id, name, parent_id, template_code, input_data, state
FROM f_polic_file_config
WHERE tenant_id = %s
ORDER BY parent_id, name
"""
cursor.execute(sql, (TENANT_ID,))
configs = cursor.fetchall()
# 构建ID到配置的映射
id_to_config = {config['id']: config for config in configs}
# 找出根节点parent_id为NULL
root_nodes = [config for config in configs if config.get('parent_id') is None]
def print_node(config, indent=0, visited=None):
"""递归打印节点"""
if visited is None:
visited = set()
if config['id'] in visited:
return
visited.add(config['id'])
prefix = " " * indent
parent_info = ""
if config.get('parent_id'):
parent_name = id_to_config.get(config['parent_id'], {}).get('name', f"ID:{config['parent_id']}")
parent_info = f" [父: {parent_name}]"
template_code = config.get('template_code')
if not template_code and config.get('input_data'):
try:
input_data = json.loads(config['input_data']) if isinstance(config['input_data'], str) else config['input_data']
if isinstance(input_data, dict):
template_code = input_data.get('template_code')
except:
pass
template_info = f" [code: {template_code}]" if template_code else ""
state_info = " [启用]" if config.get('state') == 1 else " [未启用]"
print(f"{prefix}├─ {config['name']}{parent_info}{template_info}{state_info}")
# 打印子节点
children = [c for c in configs if c.get('parent_id') == config['id']]
for i, child in enumerate(sorted(children, key=lambda x: x['name'])):
is_last = i == len(children) - 1
if is_last:
print_node(child, indent + 1, visited)
else:
print_node(child, indent + 1, visited)
print("="*80)
print("树状结构")
print("="*80)
for root in sorted(root_nodes, key=lambda x: x['name']):
print_node(root)
print()
# 统计信息
print("="*80)
print("统计信息")
print("="*80)
print(f"总记录数: {len(configs)}")
print(f"根节点数: {len(root_nodes)}")
print(f"有父节点的记录: {len([c for c in configs if c.get('parent_id')])}")
print(f"无父节点的记录: {len([c for c in configs if not c.get('parent_id')])}")
cursor.close()
def verify_parent_relationships(conn):
"""验证父子关系"""
cursor = conn.cursor(pymysql.cursors.DictCursor)
sql = """
SELECT id, name, parent_id
FROM f_polic_file_config
WHERE tenant_id = %s AND parent_id IS NOT NULL
"""
cursor.execute(sql, (TENANT_ID,))
configs = cursor.fetchall()
print("\n" + "="*80)
print("验证父子关系")
print("="*80)
errors = []
for config in configs:
parent_id = config['parent_id']
check_sql = """
SELECT id, name FROM f_polic_file_config
WHERE id = %s AND tenant_id = %s
"""
cursor.execute(check_sql, (parent_id, TENANT_ID))
parent = cursor.fetchone()
if not parent:
errors.append({
'child': config['name'],
'child_id': config['id'],
'parent_id': parent_id,
'error': '父节点不存在'
})
if errors:
print(f"\n✗ 发现 {len(errors)} 个错误:")
for error in errors:
print(f" - {error['child']} (ID: {error['child_id']})")
print(f" 父节点ID {error['parent_id']} 不存在")
else:
print("\n✓ 所有父子关系验证通过")
cursor.close()
return len(errors) == 0
def main():
"""主函数"""
print("="*80)
print("验证树状结构")
print("="*80)
try:
conn = pymysql.connect(**DB_CONFIG)
print("✓ 数据库连接成功\n")
print_tree_structure(conn)
verify_parent_relationships(conn)
conn.close()
except Exception as e:
print(f"✗ 错误: {e}")
import traceback
traceback.print_exc()
if __name__ == '__main__':
main()

24
备份数据库.bat Normal file
View File

@ -0,0 +1,24 @@
@echo off
chcp 65001 >nul
echo ========================================
echo 数据库备份工具
echo ========================================
echo.
REM 检查Python是否安装
python --version >nul 2>&1
if errorlevel 1 (
echo 错误: 未找到Python请先安装Python
pause
exit /b 1
)
REM 执行备份
python backup_database.py --compress
echo.
echo ========================================
echo 备份完成!
echo ========================================
pause

41
恢复数据库.bat Normal file
View File

@ -0,0 +1,41 @@
@echo off
chcp 65001 >nul
echo ========================================
echo 数据库恢复工具
echo ========================================
echo.
echo 警告: 恢复操作会覆盖现有数据!
echo.
REM 检查Python是否安装
python --version >nul 2>&1
if errorlevel 1 (
echo 错误: 未找到Python请先安装Python
pause
exit /b 1
)
REM 检查是否提供了备份文件路径
if "%~1"=="" (
echo 用法: 恢复数据库.bat [备份文件路径]
echo.
echo 示例:
echo 恢复数据库.bat backups\backup_finyx_20241205_120000.sql
echo 恢复数据库.bat backups\backup_finyx_20241205_120000.sql.gz
echo.
echo 可用的备份文件:
python backup_database.py --list
echo.
pause
exit /b 1
)
REM 执行恢复
python restore_database.py "%~1"
echo.
echo ========================================
echo 恢复完成!
echo ========================================
pause

View File

@ -0,0 +1,213 @@
# AI对话日志使用说明
## 功能概述
系统已集成AI对话日志记录功能可以自动记录每次大模型调用的详细信息包括
- 输入提示词prompt
- API请求参数
- API响应内容完整响应
- 提取后的结构化数据
- 错误信息(如果有)
## 日志文件位置
日志文件保存在项目根目录下的 `logs/ai_conversations/` 目录中。
日志文件命名格式:`conversation_YYYYMMDD_HHMMSS_mmm.json`
例如:`conversation_20241215_143025_123.json`
## 日志文件格式
每个日志文件是一个JSON文件包含以下字段
```json
{
"timestamp": "2024-12-15T14:30:25.123456",
"session_id": "session_1702627825123",
"prompt": "请从以下输入文本中提取结构化信息...",
"api_request": {
"endpoint": "http://10.100.31.26:3001/v1/chat/completions",
"model": "DeepSeek-R1-Distill-Llama-70B",
"messages": [
{
"role": "system",
"content": "..."
},
{
"role": "user",
"content": "..."
}
],
"temperature": 0.2,
"max_tokens": 12000,
"enable_thinking": true
},
"api_response": {
"choices": [...],
"usage": {...}
},
"extracted_data": {
"target_name": "张三",
"target_gender": "男",
...
},
"error": null,
"success": true
}
```
## 启用/禁用日志记录
日志记录功能默认启用。可以通过环境变量控制:
### 方法1设置环境变量
```bash
# Windows
set AI_LOG_ENABLED=false
# Linux/Mac
export AI_LOG_ENABLED=false
```
### 方法2在代码中修改
编辑 `services/ai_logger.py`,修改 `__init__` 方法中的默认值:
```python
self.enabled = os.getenv('AI_LOG_ENABLED', 'false').lower() == 'true' # 改为默认禁用
```
## 查看日志文件
### 方法1直接查看JSON文件
日志文件是标准的JSON格式可以用任何文本编辑器或JSON查看器打开。
### 方法2使用Python脚本查看
可以使用以下Python代码查看最近的日志
```python
from services.ai_logger import get_ai_logger
logger = get_ai_logger()
# 获取最近的10条日志
recent_logs = logger.get_recent_logs(limit=10)
for log_file in recent_logs:
print(f"日志文件: {log_file}")
log_data = logger.read_log(log_file)
if log_data:
print(f" 时间: {log_data['timestamp']}")
print(f" 成功: {log_data['success']}")
if log_data.get('error'):
print(f" 错误: {log_data['error']}")
print()
```
### 方法3使用命令行工具
在项目根目录下,可以使用以下命令查看日志:
```bash
# Windows PowerShell
Get-ChildItem logs\ai_conversations\*.json | Sort-Object LastWriteTime -Descending | Select-Object -First 10
# Linux/Mac
ls -lt logs/ai_conversations/*.json | head -10
```
## 日志文件管理
### 自动清理
日志文件会按日期组织,建议定期清理旧日志文件以节省磁盘空间。
### 手动清理
可以删除指定日期之前的日志文件:
```bash
# Windows PowerShell - 删除7天前的日志
Get-ChildItem logs\ai_conversations\*.json | Where-Object {$_.LastWriteTime -lt (Get-Date).AddDays(-7)} | Remove-Item
# Linux/Mac - 删除7天前的日志
find logs/ai_conversations -name "*.json" -mtime +7 -delete
```
## 排查问题
### 查看失败的对话
查找包含错误的日志:
```python
from services.ai_logger import get_ai_logger
import json
logger = get_ai_logger()
recent_logs = logger.get_recent_logs(limit=50)
for log_file in recent_logs:
log_data = logger.read_log(log_file)
if log_data and not log_data.get('success'):
print(f"失败日志: {log_file}")
print(f"错误: {log_data.get('error')}")
print(f"提示词: {log_data.get('prompt')[:200]}...")
print()
```
### 查看特定字段的提取情况
```python
from services.ai_logger import get_ai_logger
logger = get_ai_logger()
recent_logs = logger.get_recent_logs(limit=20)
for log_file in recent_logs:
log_data = logger.read_log(log_file)
if log_data and log_data.get('extracted_data'):
extracted = log_data['extracted_data']
if 'target_gender' in extracted:
print(f"日志: {log_file}")
print(f" 性别: {extracted.get('target_gender', '(空)')}")
print(f" 姓名: {extracted.get('target_name', '(空)')}")
print()
```
## 注意事项
1. **隐私和安全**:日志文件可能包含敏感信息,请妥善保管,不要将日志文件提交到公共代码仓库。
2. **磁盘空间**:日志文件会持续增长,建议定期清理旧日志。
3. **性能影响**:日志记录是异步的,对性能影响很小,但如果大量调用,建议定期清理日志文件。
4. **日志文件大小**每个日志文件通常几KB到几十KB取决于响应内容的大小。
## 常见问题
### Q: 日志文件在哪里?
A: 日志文件保存在 `logs/ai_conversations/` 目录中。
### Q: 如何禁用日志记录?
A: 设置环境变量 `AI_LOG_ENABLED=false`
### Q: 日志文件会占用多少空间?
A: 每个日志文件通常几KB到几十KB取决于响应内容。如果每天有100次调用大约占用几MB空间。
### Q: 可以自定义日志目录吗?
A: 可以,在创建 `AILogger` 实例时传入 `log_dir` 参数。
### Q: 日志文件格式可以修改吗?
A: 可以,修改 `services/ai_logger.py` 中的 `log_conversation` 方法。
## 相关文件
- `services/ai_logger.py` - 日志记录器实现
- `services/ai_service.py` - AI服务集成日志记录
- `logs/ai_conversations/` - 日志文件目录

View File

@ -0,0 +1,221 @@
# 初始化模板树状结构 - 使用说明
## 概述
`init_template_tree_from_directory.py` 脚本用于**完全重置** `f_polic_file_config` 表中的模板数据,根据 `template_finish` 目录结构重新创建所有记录,建立正确的树状层级关系。
## ⚠️ 重要警告
**此操作会删除当前租户的所有模板数据!**
包括:
- `f_polic_file_config` 表中的所有记录
- `f_polic_file_field` 表中的相关关联记录
然后根据 `template_finish` 目录结构完全重建。
**执行前请务必备份数据库!**
## 功能特点
1. **完全重建**: 删除旧数据,根据目录结构重新创建
2. **树状结构**: 自动建立正确的 parent_id 层级关系
3. **文件上传**: 可选择是否上传文件到 MinIO
4. **安全确认**: 多重确认机制,防止误操作
5. **模拟模式**: 先预览再执行,确保安全
## 目录结构要求
脚本会扫描 `template_finish` 目录,期望的结构如下:
```
template_finish/
└── 2-初核模版/ (一级目录)
├── 1.初核请示/ (二级目录)
│ ├── 1.请示报告卡XXX.docx
│ ├── 2.初步核实审批表XXX.docx
│ └── 3.附件初核方案(XXX).docx
├── 2.谈话审批/ (二级目录)
│ ├── 谈话通知书/ (三级目录)
│ │ ├── 谈话通知书第一联.docx
│ │ ├── 谈话通知书第二联.docx
│ │ └── 谈话通知书第三联.docx
│ ├── 走读式谈话审批/ (三级目录)
│ │ ├── 1.请示报告卡(初核谈话).docx
│ │ ├── 2谈话审批表.docx
│ │ └── ...
│ └── 走读式谈话流程/ (三级目录)
│ ├── 1.谈话笔录.docx
│ └── ...
└── 3.初核结论/ (二级目录)
├── 8-1请示报告卡初核报告结论 .docx
└── 8.XXX初核情况报告.docx
```
## 使用方法
### 基本使用
```bash
python init_template_tree_from_directory.py
```
### 执行流程
1. **警告提示**: 显示操作警告
2. **第一次确认**: 输入 `yes` 继续
3. **扫描目录**: 自动扫描 `template_finish` 目录
4. **显示预览**: 显示目录结构预览
5. **选择上传**: 选择是否上传文件到 MinIO
6. **模拟删除**: 显示将删除的数据
7. **模拟创建**: 显示将创建的节点
8. **最终确认**: 再次输入 `yes` 执行实际更新
9. **执行删除**: 删除旧数据
10. **执行创建**: 创建新数据
### 交互式提示
```
确认继续?(yes/no默认no): yes
是否上传文件到MinIO(yes/no默认yes): yes
确认执行实际更新?(yes/no默认no): yes
```
## 处理逻辑
### 1. 删除旧数据
- 先删除 `f_polic_file_field` 表中的关联记录
- 再删除 `f_polic_file_config` 表中的配置记录
- 只删除当前租户(`tenant_id = 615873064429507639`)的数据
### 2. 创建新数据
按层级顺序创建:
1. **目录节点**:
- 不包含 `template_code` 字段
- `input_data` 为 NULL
- `file_path` 为 NULL
2. **文件节点**:
- 包含 `template_code`(从 `DOCUMENT_TYPE_MAPPING` 获取)
- `input_data` 包含 JSON 格式的配置
- `file_path` 为 MinIO 路径(如果上传了文件)
### 3. 树状关系
- 一级目录: `parent_id = NULL`
- 二级目录: `parent_id = 一级目录的ID`
- 三级目录: `parent_id = 二级目录的ID`
- 文件: `parent_id = 所在目录的ID`
## 模板识别
脚本通过 `DOCUMENT_TYPE_MAPPING` 字典识别文件类型:
- 匹配文件名(不含扩展名)
- 提取 `template_code``business_type`
- 如果无法识别,`template_code` 为空字符串
## 文件上传
如果选择上传文件到 MinIO
- 文件路径格式: `/{tenant_id}/TEMPLATE/{year}/{month}/{filename}`
- 例如: `/615873064429507639/TEMPLATE/2025/12/1.请示报告卡XXX.docx`
- 上传失败不会中断流程,但 `file_path` 将为 NULL
## 输出示例
```
================================================================================
初始化模板树状结构(从目录结构完全重建)
================================================================================
⚠️ 警告:此操作将删除当前租户的所有模板数据!
确认继续?(yes/no默认no): yes
✓ 数据库连接成功
扫描目录结构...
找到 28 个节点
其中目录: 7 个
其中文件: 21 个
执行模拟删除...
[模拟] 将删除 113 条关联记录
[模拟] 将删除 34 条配置记录
执行模拟创建...
✓ [模拟]创建目录: 2-初核模版 (ID: ...)
✓ [模拟]创建文件: 1.请示报告卡XXX (ID: ...) [父: ...] [code: REPORT_CARD]
...
确认执行实际更新?(yes/no默认no): yes
执行实际删除...
✓ 删除了 113 条关联记录
✓ 删除了 34 条配置记录
执行实际创建...
✓ 创建目录: 2-初核模版 (ID: ...)
✓ 创建文件: 1.请示报告卡XXX (ID: ...) [父: ...] [code: REPORT_CARD]
...
✓ 创建完成!共创建 28 个节点
```
## 验证结果
执行完成后,可以使用验证脚本检查结果:
```bash
python verify_tree_structure.py
```
## 注意事项
1. **备份数据库**: 执行前务必备份数据库
2. **确认目录结构**: 确保 `template_finish` 目录结构正确
3. **文件存在**: 确保所有 `.docx` 文件都存在
4. **MinIO 连接**: 如果选择上传文件,确保 MinIO 连接正常
5. **不可逆操作**: 删除操作不可逆,请谨慎执行
## 故障排查
### 问题1: template_code 不能为 NULL
**原因**: 数据库表结构要求 template_code 不能为 NULL
**解决**: 脚本已处理,目录节点不插入 template_code文件节点使用空字符串
### 问题2: 文件上传失败
**原因**: MinIO 连接问题或文件不存在
**解决**:
- 检查 MinIO 配置
- 检查文件是否存在
- 上传失败不会中断流程,可以后续手动上传
### 问题3: 父子关系错误
**原因**: 目录结构扫描顺序问题
**解决**: 脚本已按层级顺序处理,确保父节点先于子节点创建
## 相关脚本
- `update_template_tree.py` - 更新现有数据的 parent_id不删除数据
- `verify_tree_structure.py` - 验证树状结构
- `check_existing_data.py` - 检查现有数据
## 联系信息
如有问题,请检查:
1. 数据库连接配置
2. 目录结构是否正确
3. 文件是否都存在
4. MinIO 配置是否正确

View File

@ -0,0 +1,293 @@
# 模板树状结构更新 - 使用说明
## 概述
本工具用于根据 `template_finish` 目录结构,更新数据库 `f_polic_file_config` 表中的 `parent_id` 字段,建立正确的树状层级结构。
## 数据库现状分析
根据检查,数据库中现有:
- **总记录数**: 32 条
- **有 parent_id**: 2 条
- **无 parent_id**: 30 条
需要更新的主要记录包括:
- 初步核实审批表
- 请示报告卡(各种类型)
- 初核方案
- 谈话通知书(第一联、第二联、第三联)
- XXX初核情况报告
- 走读式谈话审批相关文件
- 走读式谈话流程相关文件
- 等等...
## 脚本说明
### 1. `check_existing_data.py` - 检查现有数据
**功能**: 查看数据库中的现有记录,分析缺少 parent_id 的情况
**使用方法**:
```bash
python check_existing_data.py
```
**输出**:
- 列出所有无 parent_id 的记录
- 显示有 parent_id 的记录及其树状关系
---
### 2. `improved_match_and_update.py` - 改进的匹配分析
**功能**: 使用改进的匹配逻辑分析目录结构和数据库,生成匹配报告
**特点**:
- **三级匹配策略**:
1. **template_code 精确匹配**(最高优先级)
2. **名称精确匹配**
3. **标准化名称匹配**(去掉编号和括号后的模糊匹配)
**使用方法**:
```bash
python improved_match_and_update.py
```
**输出**:
- 匹配报告(显示哪些记录已匹配,哪些需要创建)
- 可选择性生成 SQL 更新脚本
---
### 3. `update_template_tree.py` - 交互式更新工具(推荐)
**功能**: 完整的更新工具,包含预览、确认和执行功能
**特点**:
- 使用改进的匹配逻辑
- 支持预览模式dry-run
- 交互式确认
- 按层级顺序自动更新
- 安全的事务处理
**使用方法**:
```bash
python update_template_tree.py
```
**执行流程**:
1. 扫描目录结构
2. 获取数据库现有数据
3. 规划树状结构(使用改进的匹配逻辑)
4. 显示更新预览
5. 询问是否执行(输入 `yes`
6. 执行模拟更新
7. 再次确认执行实际更新
---
### 4. `analyze_and_update_template_tree.py` - 生成 SQL 脚本
**功能**: 分析并生成 SQL 更新脚本(不直接修改数据库)
**使用方法**:
```bash
python analyze_and_update_template_tree.py
```
**输出**:
- `update_template_tree.sql` - SQL 更新脚本
**适用场景**:
- 生产环境
- 需要 DBA 审核的场景
- 需要手动执行的场景
---
### 5. `verify_tree_structure.py` - 验证更新结果
**功能**: 验证更新后的树状结构是否正确
**使用方法**:
```bash
python verify_tree_structure.py
```
**输出**:
- 树状结构可视化
- 统计信息
- 父子关系验证
---
## 匹配逻辑说明
### 三级匹配策略
1. **template_code 精确匹配**(最高优先级)
- 通过 `template_code` 字段精确匹配
- 例如: `REPORT_CARD` 匹配 `REPORT_CARD`
2. **名称精确匹配**
- 通过 `name` 字段精确匹配
- 例如: `"1.请示报告卡XXX"` 匹配 `"1.请示报告卡XXX"`
3. **标准化名称匹配**(模糊匹配)
- 去掉开头的编号(如 `"1."``"2."``"8-1"`
- 去掉括号及其内容(如 `"XXX"``"(初核谈话)"`
- 例如: `"1.请示报告卡XXX"``"请示报告卡"` → 匹配 `"请示报告卡"`
### 匹配示例
| 目录结构中的名称 | 数据库中的名称 | 匹配方式 |
|----------------|--------------|---------|
| `1.请示报告卡XXX` | `请示报告卡` | template_code: `REPORT_CARD` |
| `2.初步核实审批表XXX` | `初步核实审批表` | template_code: `PRELIMINARY_VERIFICATION_APPROVAL` |
| `谈话通知书第一联` | `谈话通知书第一联` | 名称精确匹配 |
| `走读式谈话审批` | `走读式谈话审批` | 名称精确匹配 |
## 树状结构规划
根据 `template_finish` 目录结构,规划的层级关系如下:
```
2-初核模版 (一级目录)
├── 1.初核请示 (二级目录)
│ ├── 1.请示报告卡XXX.docx
│ ├── 2.初步核实审批表XXX.docx
│ └── 3.附件初核方案(XXX).docx
├── 2.谈话审批 (二级目录)
│ ├── 谈话通知书 (三级目录)
│ │ ├── 谈话通知书第一联.docx
│ │ ├── 谈话通知书第二联.docx
│ │ └── 谈话通知书第三联.docx
│ ├── 走读式谈话审批 (三级目录)
│ │ ├── 1.请示报告卡(初核谈话).docx
│ │ ├── 2谈话审批表.docx
│ │ ├── 3.谈话前安全风险评估表.docx
│ │ ├── 4.谈话方案.docx
│ │ └── 5.谈话后安全风险评估表.docx
│ └── 走读式谈话流程 (三级目录)
│ ├── 1.谈话笔录.docx
│ ├── 2.谈话询问对象情况摸底调查30问.docx
│ ├── 3.被谈话人权利义务告知书.docx
│ ├── 4.点对点交接单.docx
│ ├── 5.陪送交接单(新).docx
│ ├── 6.1保密承诺书(谈话对象使用-非中共党员用).docx
│ ├── 6.2保密承诺书(谈话对象使用-中共党员用).docx
│ └── 7.办案人员-办案安全保密承诺书.docx
└── 3.初核结论 (二级目录)
├── 8-1请示报告卡初核报告结论 .docx
└── 8.XXX初核情况报告.docx
```
## 执行步骤
### 推荐流程(使用交互式工具)
1. **检查现有数据**
```bash
python check_existing_data.py
```
2. **运行更新工具**
```bash
python update_template_tree.py
```
3. **查看预览信息**
- 检查匹配情况
- 确认更新计划
4. **确认执行**
- 输入 `yes` 确认
- 再次确认执行实际更新
5. **验证结果**
```bash
python verify_tree_structure.py
```
### 备选流程(使用 SQL 脚本)
1. **生成 SQL 脚本**
```bash
python improved_match_and_update.py
# 或
python analyze_and_update_template_tree.py
```
2. **检查 SQL 脚本**
```bash
# 查看 update_template_tree.sql
```
3. **备份数据库**(重要!)
4. **执行 SQL 脚本**
```sql
-- 在 MySQL 客户端中执行
source update_template_tree.sql;
```
5. **验证结果**
```bash
python verify_tree_structure.py
```
## 注意事项
1. **备份数据库**: 执行更新前务必备份数据库
2. **检查匹配**: 确认匹配结果是否正确
3. **层级顺序**: 更新会按照层级顺序执行,确保父节点先于子节点
4. **重复执行**: 脚本支持重复执行,已正确设置的记录会被跳过
5. **目录节点**: 如果目录节点不存在,脚本会自动创建
## 匹配结果
根据最新分析,匹配情况如下:
- ✅ **已匹配**: 26 条记录
- ⚠️ **需创建**: 2 条记录(目录节点)
- `2-初核模版` (一级目录)
- `1.初核请示` (二级目录)
所有文件记录都已正确匹配到数据库中的现有记录。
## 问题排查
### 问题1: 某些记录无法匹配
**原因**: 名称或 template_code 不匹配
**解决**:
- 检查 `DOCUMENT_TYPE_MAPPING` 字典
- 确认数据库中的 `template_code` 是否正确
- 使用 `check_existing_data.py` 查看数据库中的实际数据
### 问题2: 匹配到错误的记录
**原因**: 标准化名称匹配时选择了错误的候选
**解决**:
- 检查匹配报告,确认匹配方式
- 如果 template_code 匹配失败,检查数据库中的 template_code 是否正确
- 可以手动调整匹配逻辑
### 问题3: parent_id 更新失败
**原因**: 父节点ID不存在或层级关系错误
**解决**:
- 使用 `verify_tree_structure.py` 验证父子关系
- 检查生成的 SQL 脚本确认父节点ID是否正确
## 联系信息
如有问题,请检查:
1. 数据库连接配置是否正确
2. 目录结构是否与预期一致
3. 数据库中的记录是否完整
4. template_code 是否正确设置

View File

@ -0,0 +1,179 @@
# AI服务错误修复说明
## 修改概述
基于错误分析报告对AI服务进行了三项关键修复以提高JSON生成的稳定性和准确性。
## 修改详情
### 1. 关闭思考模式 ✅
**文件**: `services/ai_service.py`
**位置**: 第254行
**修改前**:
```python
"enable_thinking": True
```
**修改后**:
```python
"enable_thinking": False # 关闭思考模式以提高JSON生成稳定性
```
**原因**:
- 思考模式可能导致模型在生成JSON时出现不稳定
- 从日志分析看思考过程可能影响后续JSON生成的准确性
- 关闭思考模式可以提高JSON格式的稳定性
### 2. 优化提示词 ✅
**文件**: `services/ai_service.py`
**位置**: 第237行
**修改前**:
```python
"content": "你是一个专业的数据提取助手。请仔细分析用户提供的输入文本提取所有相关信息并严格按照指定的JSON格式返回结果。\n\n重要要求\n1. 必须仔细阅读输入文本的每一个字,不要遗漏任何信息\n2. 对于每个字段,请从多个角度思考:直接提及、同义词、隐含信息、可推断信息\n3. 如果文本中明确提到某个信息(如性别、年龄、职务、职级、线索来源等),必须提取出来,不能设为空\n4. 特别关注性别字段:如果文本中出现\"男\"、\"女\"、\"男性\"、\"女性\"、\"先生\"、\"女士\"等任何表示性别的词汇,必须提取并转换为\"男\"或\"女\"\n5. 如果可以通过已有信息合理推断(如根据出生年月推算年龄,从单位及职务中拆分单位和职务),请进行推断并填写\n6. 只返回JSON对象不要包含任何其他文字说明、思考过程或markdown代码块标记\n7. 字段名必须严格按照JSON示例中的字段编码不能使用下划线前缀如不能使用\"_professional_rank\",应使用\"target_professional_rank\";不能使用\"_source\",应使用\"clue_source\""
```
**修改后**:
```python
"content": "你是一个专业的数据提取助手。请从输入文本中提取结构化信息并严格按照JSON格式返回结果。\n\n核心要求\n1. 仔细阅读输入文本,提取所有相关信息\n2. 如果文本中明确提到信息(如性别、年龄、职务、职级等),必须提取,不能设为空\n3. 性别字段:识别\"男\"、\"女\"、\"男性\"、\"女性\"等词汇,统一转换为\"男\"或\"女\"\n4. 只返回JSON对象不要包含任何其他文字、思考过程或markdown标记\n5. 字段名必须严格按照示例格式,使用正确的字段编码:\n - 使用\"target_professional_rank\",不要使用\"_professional_rank\"\n - 使用\"clue_source\",不要使用\"_source\"或\"source\"\n - 使用\"target_organization\",不要使用\"target_organisation\"\n6. JSON格式必须完整且有效所有字段名使用双引号"
```
**改进点**:
- 简化了提示词,使其更清晰、更直接
- 明确列出了常见的字段名错误,帮助模型避免这些错误
- 强调了JSON格式的完整性要求
- 减少了冗余说明,提高可读性
### 3. 增强JSON修复机制 ✅
#### 3.1 增强 `_fix_json_string` 方法
**文件**: `services/ai_service.py`
**位置**: 第730-790行
**新增修复规则**:
1. **修复字段名前后的转义字符和空格**
```python
# 修复 \\\" target_position \\\": 这种情况
json_str = re.sub(r'\\+["\']\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*\\+["\']\s*:', r'"\1":', json_str)
```
2. **修复常见字段名错误**
```python
# 修复 _source -> clue_source
json_str = re.sub(r'"_source"\s*:', '"clue_source":', json_str)
# 修复 target_organisation -> target_organization
json_str = re.sub(r'"target_organisation"\s*:', '"target_organization":', json_str)
```
3. **修复字段名中的下划线前缀错误**
```python
# 修复 _professional_rank -> target_professional_rank
json_str = re.sub(r'"_([a-z_]+_rank)"\s*:', r'"target_\1":', json_str)
```
4. **修复值中的转义字符问题**
```python
# 修复 \"total_manager, -> "总经理",
json_str = re.sub(r':\s*\\"([^"]+?),', r': "\1",', json_str)
```
5. **修复不完整的JSON结尾**
```python
# 修复 \"\n} -> ""\n}
json_str = re.sub(r':\s*\\"\s*\n\s*}', ': ""\n}', json_str)
```
#### 3.2 增强字段名规范化映射
**文件**: `services/ai_service.py`
**位置**: 第900-930行
**新增映射**:
```python
typo_mapping = {
# ... 原有映射 ...
# 新增基于日志错误的映射
'_source': 'clue_source', # 修复 _source -> clue_source
'_professional_rank': 'target_professional_rank', # 修复 _professional_rank
'_status': 'target_political_status', # 修复 _status
'target_organisation': 'target_organization', # 修复英式拼写
'targetOrganisation': 'target_organization', # 修复英式拼写(驼峰)
}
```
#### 3.3 增强部分JSON提取
**文件**: `services/ai_service.py`
**位置**: 第650-706行
**改进点**:
- 在三个提取模式中都增加了对 `_source` -> `clue_source` 的特殊处理
- 增加了对 `target_organisation` -> `target_organization` 的拼写错误修复
- 改进了字段名清理逻辑,更好地处理转义字符
## 预期效果
1. **提高JSON生成稳定性**
- 关闭思考模式后模型生成JSON时更加稳定
- 减少了格式错误的可能性
2. **提高字段名准确性**
- 优化后的提示词明确列出了常见错误,帮助模型避免这些错误
- 增强了字段名规范化映射,即使出现错误也能自动修复
3. **增强容错能力**
- 多层JSON修复机制可以处理各种格式错误
- 即使模型返回了格式错误的JSON也能通过修复机制恢复
## 测试建议
1. **功能测试**
- 使用相同的输入数据测试修复后的代码
- 验证JSON生成是否稳定
- 检查字段名是否正确
2. **错误处理测试**
- 模拟各种JSON格式错误
- 验证修复机制是否能正确处理这些错误
3. **性能测试**
- 对比修复前后的响应时间
- 验证关闭思考模式后的性能提升
## 回滚方案
如果修复后出现问题,可以通过以下方式回滚:
1. **恢复思考模式**:
```python
"enable_thinking": True
```
2. **恢复原提示词**: 使用git恢复原始system prompt
3. **禁用新增的修复规则**: 注释掉新增的JSON修复代码
## 注意事项
1. 关闭思考模式可能会影响模型的推理能力但可以提高JSON生成的稳定性
2. 如果必须使用思考模式可以考虑调整相关参数如限制思考过程的token数量
3. JSON修复机制是容错措施最佳实践是让模型生成正确的JSON而不是依赖修复
## 后续优化建议
1. 如果问题持续存在,可以考虑:
- 进一步优化提示词
- 调整temperature等参数
- 联系模型服务提供商寻求支持
2. 监控和日志:
- 记录修复前后的错误率
- 分析仍然存在的错误模式
- 持续优化修复机制

View File

@ -29,20 +29,16 @@ Word模板中使用以下格式作为占位符
| 字段名称 | 字段编码 (占位符) | 说明 | 示例 | | 字段名称 | 字段编码 (占位符) | 说明 | 示例 |
|---------|-----------------|------|------| |---------|-----------------|------|------|
| 被核查人姓名 | `{{target_name}}` | 被核查人姓名 | 张三 | | 主要问题线索 | `{{target_issue_description}}` | 主要问题线索描述 | - |
| 被核查人员单位及职务 | `{{target_organization_and_position}}` | 被核查人员单位及职务(包括兼职) | 某公司总经理 | | 初步核实审批表填表人 | `{{filler_name}}` | 初步核实审批表填表人 | - |
| 被核查人员单位 | `{{target_organization}}` | 被核查人员单位 | 某公司 | | 初步核实审批表承办部门意见 | `{{department_opinion}}` | 初步核实审批表承办部门意见 | - |
| 被核查人员职务 | `{{target_position}}` | 被核查人员职务 | 总经理 | | 线索来源 | `{{clue_source}}` | 线索来源 | - |
| 被核查人员性别 | `{{target_gender}}` | 被核查人员性别(男/女,不用男性和女性) | 男 |
| 被核查人员出生年月 | `{{target_date_of_birth}}` | 被核查人员出生年月YYYYMM格式不需要日 | 198005 | | 被核查人员出生年月 | `{{target_date_of_birth}}` | 被核查人员出生年月YYYYMM格式不需要日 | 198005 |
| 被核查人员年龄 | `{{target_age}}` | 被核查人员年龄(数字,单位:岁) | 44 | | 被核查人员单位及职务 | `{{target_organization_and_position}}` | 被核查人员单位及职务(包括兼职) | 某公司总经理 |
| 被核查人员文化程度 | `{{target_education_level}}` | 被核查人员文化程度(如:本科、大专、高中等) | 本科 | | 被核查人员性别 | `{{target_gender}}` | 被核查人员性别(男/女,不用男性和女性) | 男 |
| 被核查人员政治面貌 | `{{target_political_status}}` | 被核查人员政治面貌(中共党员、群众等) | 中共党员 | | 被核查人员政治面貌 | `{{target_political_status}}` | 被核查人员政治面貌(中共党员、群众等) | 中共党员 |
| 被核查人员职级 | `{{target_professional_rank}}` | 被核查人员职级(如:正处级) | 正处级 | | 被核查人员职级 | `{{target_professional_rank}}` | 被核查人员职级(如:正处级) | 正处级 |
| 线索来源 | `{{clue_source}}` | 线索来源 | - | | 被核查人姓名 | `{{target_name}}` | 被核查人姓名 | 张三 |
| 主要问题线索 | `{{target_issue_description}}` | 主要问题线索描述 | 违反国家计划生育有关政策规定于2010年10月生育二胎。 |
| 初步核实审批表承办部门意见 | `{{department_opinion}}` | 初步核实审批表承办部门意见 | - |
| 初步核实审批表填表人 | `{{filler_name}}` | 初步核实审批表填表人 | - |
--- ---
@ -58,13 +54,13 @@ Word模板中使用以下格式作为占位符
| 字段名称 | 字段编码 (占位符) | 说明 | 示例 | | 字段名称 | 字段编码 (占位符) | 说明 | 示例 |
|---------|-----------------|------|------| |---------|-----------------|------|------|
| 被核查人员单位及职务 | `{{target_organization_and_position}}` | 被核查人员单位及职务(包括兼职) | 某公司总经理 |
| 被核查人姓名 | `{{target_name}}` | 被核查人姓名 | 张三 | | 被核查人姓名 | `{{target_name}}` | 被核查人姓名 | 张三 |
| 被核查人员单位及职务 | `{{target_organization_and_position}}` | 被核查人员单位及职务 | 某公司总经理 |
| 请示报告卡请示时间 | `{{report_card_request_time}}` | 请示报告卡请示时间 | - | | 请示报告卡请示时间 | `{{report_card_request_time}}` | 请示报告卡请示时间 | - |
--- ---
## 三、初核方案 ## 三、初核方案 (INVESTIGATION_PLAN)
### 输入字段 ### 输入字段
@ -75,20 +71,20 @@ Word模板中使用以下格式作为占位符
### 输出字段(占位符) ### 输出字段(占位符)
| 字段名称 | 字段编码 (占位符) | 说明 | | 字段名称 | 字段编码 (占位符) | 说明 | 示例 |
|---------|-----------------|------| |---------|-----------------|------|------|
| 被核查人姓名 | `{{target_name}}` | 被核查人姓名 | | 主要问题线索 | `{{target_issue_description}}` | 主要问题线索描述 | - |
| 被核查人员单位及职务 | `{{target_organization_and_position}}` | 被核查人员单位及职务 | | 核查单位名称 | `{{investigation_unit_name}}` | 核查单位名称 | - |
| 被核查人员工作基本情况 | `{{target_work_basic_info}}` | 被核查人员工作基本情况 | | 核查地点 | `{{investigation_location}}` | 核查地点 | - |
| 主要问题线索 | `{{target_issue_description}}` | 主要问题线索 | | 核查组成员姓名 | `{{investigation_team_member_names}}` | 核查组成员姓名 | - |
| 核查单位名称 | `{{investigation_unit_name}}` | 核查单位名称 | | 核查组组长姓名 | `{{investigation_team_leader_name}}` | 核查组组长姓名 | - |
| 核查组组长姓名 | `{{investigation_team_leader_name}}` | 核查组组长姓名 | | 被核查人员单位及职务 | `{{target_organization_and_position}}` | 被核查人员单位及职务(包括兼职) | 某公司总经理 |
| 核查组成员姓名 | `{{investigation_team_member_names}}` | 核查组成员姓名 | | 被核查人员工作基本情况 | `{{target_work_basic_info}}` | 被核查人员工作基本情况 | - |
| 核查地点 | `{{investigation_location}}` | 核查地点 | | 被核查人姓名 | `{{target_name}}` | 被核查人姓名 | 张三 |
--- ---
## 四、谈话通知书 ## 四、谈话通知书(第一联) (NOTIFICATION_LETTER_1)
### 输入字段 ### 输入字段
@ -98,58 +94,43 @@ Word模板中使用以下格式作为占位符
### 输出字段(占位符) ### 输出字段(占位符)
| 字段名称 | 字段编码 (占位符) | 说明 | | 字段名称 | 字段编码 (占位符) | 说明 | 示例 |
|---------|-----------------|------| |---------|-----------------|------|------|
| 被核查人姓名 | `{{target_name}}` | 被核查人姓名 | | 应到地点 | `{{appointment_location}}` | 应到地点 | - |
| 被核查人员单位及职务 | `{{target_organization_and_position}}` | 被核查人员单位及职务 | | 应到时间 | `{{appointment_time}}` | 应到时间 | - |
| 被核查人员身份证件及号码 | `{{target_id_number}}` | 被核查人员身份证件及号码 | | 批准时间 | `{{approval_time}}` | 批准时间 | - |
| 应到时间 | `{{appointment_time}}` | 应到时间 | | 承办人 | `{{handler_name}}` | 承办人 | - |
| 应到地点 | `{{appointment_location}}` | 应到地点 | | 承办部门 | `{{handling_department}}` | 承办部门 | - |
| 批准时间 | `{{approval_time}}` | 批准时间 | | 被核查人员单位及职务 | `{{target_organization_and_position}}` | 被核查人员单位及职务(包括兼职) | 某公司总经理 |
| 承办部门 | `{{handling_department}}` | 承办部门 | | 被核查人员身份证件及号码 | `{{target_id_number}}` | 被核查人员身份证件及号码 | - |
| 承办人 | `{{handler_name}}` | 承办人 | | 被核查人姓名 | `{{target_name}}` | 被核查人姓名 | 张三 |
| 谈话通知时间 | `{{notification_time}}` | 谈话通知时间 |
| 谈话通知地点 | `{{notification_location}}` | 谈话通知地点 |
--- ---
## 五、走读式谈话流程 ## 四、谈话通知书(第三联) (NOTIFICATION_LETTER_3)
### 输出字段(占位符)
该模板主要使用被核查人员的基本信息字段,包括:
| 字段名称 | 字段编码 (占位符) | 说明 |
|---------|-----------------|------|
| 被核查人姓名 | `{{target_name}}` | 被核查人姓名 |
| 被核查人员单位及职务 | `{{target_organization_and_position}}` | 被核查人员单位及职务 |
| 被核查人员性别 | `{{target_gender}}` | 被核查人员性别 |
| 被核查人员出生年月日 | `{{target_date_of_birth_full}}` | 被核查人员出生年月日 |
| 被核查人员政治面貌 | `{{target_political_status}}` | 被核查人员政治面貌 |
| 被核查人员住址 | `{{target_address}}` | 被核查人员住址 |
| 被核查人员户籍住址 | `{{target_registered_address}}` | 被核查人员户籍住址 |
| 被核查人员联系方式 | `{{target_contact}}` | 被核查人员联系方式 |
| 被核查人员籍贯 | `{{target_place_of_origin}}` | 被核查人员籍贯 |
| 被核查人员民族 | `{{target_ethnicity}}` | 被核查人员民族 |
| 被核查人员身份证号 | `{{target_id_number}}` | 被核查人员身份证号 |
| 核查组代号 | `{{investigation_team_code}}` | 核查组代号 |
---
## 六、走读式谈话审批
该模板包含大量字段,包括基本信息、风险评估、谈话记录等。
### 输入字段 ### 输入字段
| 字段名称 | 字段编码 (占位符) | 说明 | | 字段名称 | 字段编码 (占位符) | 说明 |
|---------|-----------------|------| |---------|-----------------|------|
| 线索信息 | `{{clue_info}}` | 线索信息用于AI解析 |
| 被核查人员工作基本情况线索 | `{{target_basic_info_clue}}` | 被核查人员工作基本情况线索用于AI解析 | | 被核查人员工作基本情况线索 | `{{target_basic_info_clue}}` | 被核查人员工作基本情况线索用于AI解析 |
### 主要输出字段(占位符) ### 输出字段(占位符)
包括基本信息、谈话安排、风险评估等多个类别的字段,具体请参考完整的字段列表。 | 字段名称 | 字段编码 (占位符) | 说明 | 示例 |
|---------|-----------------|------|------|
| 被核查人姓名 | `{{target_name}}` | 被核查人姓名 | 张三 |
| 谈话通知地点 | `{{notification_location}}` | 谈话通知地点 | - |
---
## 四、谈话通知书(第二联) (NOTIFICATION_LETTER_2)
### 输出字段(占位符)
| 字段名称 | 字段编码 (占位符) | 说明 | 示例 |
|---------|-----------------|------|------|
| 谈话通知时间 | `{{notification_time}}` | 谈话通知时间 | - |
--- ---
@ -159,43 +140,37 @@ Word模板中使用以下格式作为占位符
| 字段名称 | 字段编码 (占位符) | 说明 | | 字段名称 | 字段编码 (占位符) | 说明 |
|---------|-----------------|------| |---------|-----------------|------|
| 线索信息 | `{{clue_info}}` | 线索信息用于AI解析 |
| 被核查人员工作基本情况线索 | `{{target_basic_info_clue}}` | 被核查人员工作基本情况线索用于AI解析 | | 被核查人员工作基本情况线索 | `{{target_basic_info_clue}}` | 被核查人员工作基本情况线索用于AI解析 |
### 输出字段(占位符,带默认值)
| 字段名称 | 字段编码 (占位符) | 说明 | 默认值 |
|---------|-----------------|------|--------|
| 被核查人员家庭情况 | `{{target_family_situation}}` | 被核查人员家庭情况 | 家庭关系和谐稳定 |
| 被核查人员社会关系 | `{{target_social_relations}}` | 被核查人员社会关系 | 社会交往较多,人机关系基本正常 |
| 被核查人员健康状况 | `{{target_health_status}}` | 被核查人员健康状况 | 良好 |
| 被核查人员性格特征 | `{{target_personality}}` | 被核查人员性格特征 | 开朗 |
| 被核查人员承受能力 | `{{target_tolerance}}` | 被核查人员承受能力 | 较强 |
| 被核查人员涉及问题严重程度 | `{{target_issue_severity}}` | 被核查人员涉及问题严重程度 | 较轻 |
| 被核查人员涉及其他问题的可能性 | `{{target_other_issues_possibility}}` | 被核查人员涉及其他问题的可能性 | 较小 |
| 被核查人员此前被审查情况 | `{{target_previous_investigation}}` | 被核查人员此前被审查情况 | 无 |
| 被核查人员社会负面事件 | `{{target_negative_events}}` | 被核查人员社会负面事件 | 无 |
| 被核查人员其他情况 | `{{target_other_situation}}` | 被核查人员其他情况 | 无 |
| 风险等级 | `{{risk_level}}` | 风险等级 | 低 |
**注意**如果AI未提取到字段值系统返回空字符串。默认值信息提供给前端前端可根据业务需求决定是否应用。
---
## 七、请示报告卡(初核报告结论)
### 输出字段(占位符) ### 输出字段(占位符)
| 字段名称 | 字段编码 (占位符) | 说明 | | 字段名称 | 字段编码 (占位符) | 说明 | 默认值 |
|---------|-----------------|------| |---------|-----------------|------|--------|
| 核查组代号 | `{{investigation_team_code}}` | 核查组代号 | | 被核查人员健康状况 | `{{target_health_status}}` | 被核查人员健康状况 | 良好 |
| 被核查人姓名 | `{{target_name}}` | 被核查人姓名 | | 被核查人员其他情况 | `{{target_other_situation}}` | 被核查人员其他情况 | 无 |
| 被核查人问题描述 | `{{target_problem_description}}` | 被核查人问题描述 | | 被核查人员学历 | `{{target_education}}` | 被核查人员学历 | - |
| 被核查人员本人认识和态度 | `{{target_attitude}}` | 被核查人员本人认识和态度 | | 被核查人员家庭情况 | `{{target_family_situation}}` | 被核查人员家庭情况 | 家庭关系和谐稳定 |
| 被核查人员工作履历 | `{{target_work_history}}` | 被核查人员工作履历 | - |
| 被核查人员年龄 | `{{target_age}}` | 被核查人员年龄(数字,单位:岁) | - |
| 被核查人员性别 | `{{target_gender}}` | 被核查人员性别(男/女,不用男性和女性) | - |
| 被核查人员性格特征 | `{{target_personality}}` | 被核查人员性格特征 | 开朗 |
| 被核查人员承受能力 | `{{target_tolerance}}` | 被核查人员承受能力 | 较强 |
| 被核查人员此前被审查情况 | `{{target_previous_investigation}}` | 被核查人员此前被审查情况 | 无 |
| 被核查人员涉及其他问题的可能性 | `{{target_other_issues_possibility}}` | 被核查人员涉及其他问题的可能性 | 较小 |
| 被核查人员涉及问题严重程度 | `{{target_issue_severity}}` | 被核查人员涉及问题严重程度 | 较轻 |
| 被核查人员社会关系 | `{{target_social_relations}}` | 被核查人员社会关系 | 社会交往较多,人机关系基本正常 |
| 被核查人员社会负面事件 | `{{target_negative_events}}` | 被核查人员社会负面事件 | 无 |
| 被核查人员职业 | `{{target_occupation}}` | 被核查人员职业 | - |
| 被核查人姓名 | `{{target_name}}` | 被核查人姓名 | - |
| 风险等级 | `{{risk_level}}` | 风险等级 | 低 |
--- ---
## 八、XXX初核情况报告 ## 七、请示报告卡(初核报告结论) (REPORT_CARD_INTERVIEW)
---
## 八、XXX初核情况报告 (INVESTIGATION_REPORT)
### 输入字段 ### 输入字段
@ -206,14 +181,274 @@ Word模板中使用以下格式作为占位符
### 输出字段(占位符) ### 输出字段(占位符)
| 字段名称 | 字段编码 (占位符) | 说明 | 示例 |
|---------|-----------------|------|------|
| 主要问题线索 | `{{target_issue_description}}` | 主要问题线索描述 | - |
| 纪委名称 | `{{commission_name}}` | 纪委名称 | - |
| 被核查人单位及职务 | `{{target_organization_and_position}}` | 被核查人员单位及职务(包括兼职) | 某公司总经理 |
| 被核查人员工作基本情况 | `{{target_work_basic_info}}` | 被核查人员工作基本情况 | - |
| 被核查人姓名 | `{{target_name}}` | 被核查人姓名 | 张三 |
| 被核查人问题描述 | `{{target_problem_description}}` | 被核查人问题描述 | - |
---
## 其他、1.初核请示 (PRELIMINARY_VERIFICATION_REQUEST)
---
## 其他、1.谈话笔录 (INTERVIEW_RECORD)
### 输入字段
| 字段名称 | 字段编码 (占位符) | 说明 | | 字段名称 | 字段编码 (占位符) | 说明 |
|---------|-----------------|------| |---------|-----------------|------|
| 被核查人姓名 | `{{target_name}}` | 被核查人姓名 | | 被核查人员工作基本情况线索 | `{{target_basic_info_clue}}` | 被核查人员工作基本情况线索用于AI解析 |
| 纪委名称 | `{{commission_name}}` | 纪委名称 |
| 被核查人员工作基本情况 | `{{target_work_basic_info}}` | 被核查人员工作基本情况 | ### 输出字段(占位符)
| 主要问题线索 | `{{target_issue_description}}` | 主要问题线索 |
| 被核查人问题描述 | `{{target_problem_description}}` | 被核查人问题描述 | | 字段名称 | 字段编码 (占位符) | 说明 | 示例 |
| 被核查人单位及职务 | `{{target_organization_and_position}}` | 被核查人单位及职务 | |---------|-----------------|------|------|
| 被核查人员住址 | `{{target_address}}` | 被核查人员住址 | - |
| 被核查人员出生年月日 | `{{target_date_of_birth_full}}` | 被核查人员出生年月日 | - |
| 被核查人员单位及职务 | `{{target_organization_and_position}}` | 被核查人员单位及职务(包括兼职) | 某公司总经理 |
| 被核查人员性别 | `{{target_gender}}` | 被核查人员性别(男/女,不用男性和女性) | 男 |
| 被核查人员政治面貌 | `{{target_political_status}}` | 被核查人员政治面貌(中共党员、群众等) | 中共党员 |
| 被核查人员联系方式 | `{{target_contact}}` | 被核查人员联系方式 | - |
| 被核查人姓名 | `{{target_name}}` | 被核查人姓名 | 张三 |
---
## 其他、2-初核模版 (PRELIMINARY_VERIFICATION_TEMPLATE)
---
## 其他、2.谈话审批 (INTERVIEW_APPROVAL_FORM)
### 输入字段
| 字段名称 | 字段编码 (占位符) | 说明 |
|---------|-----------------|------|
| 线索信息 | `{{clue_info}}` | 线索信息用于AI解析 |
| 被核查人员工作基本情况线索 | `{{target_basic_info_clue}}` | 被核查人员工作基本情况线索用于AI解析 |
### 输出字段(占位符)
| 字段名称 | 字段编码 (占位符) | 说明 | 示例 |
|---------|-----------------|------|------|
| 拟谈话地点 | `{{proposed_interview_location}}` | 拟谈话地点 | - |
| 拟谈话时间 | `{{proposed_interview_time}}` | 拟谈话时间 | - |
| 补空人员 | `{{backup_personnel}}` | 补空人员 | - |
| 被核查人员性别 | `{{target_gender}}` | 被核查人员性别(男/女,不用男性和女性) | 男 |
| 被核查人姓名 | `{{target_name}}` | 被核查人姓名 | 张三 |
| 谈话事由 | `{{interview_reason}}` | 谈话事由 | - |
| 谈话人员-安全员 | `{{interview_personnel_safety_officer}}` | 谈话人员-安全员 | - |
| 谈话人员-组长 | `{{interview_personnel_leader}}` | 谈话人员-组长 | - |
| 谈话人员-谈话人员 | `{{interview_personnel}}` | 谈话人员-谈话人员 | - |
| 谈话前安全风险评估结果 | `{{pre_interview_risk_assessment_result}}` | 谈话前安全风险评估结果 | - |
| 谈话次数 | `{{interview_count}}` | 谈话次数 | - |
---
## 其他、2.谈话询问对象情况摸底调查30问 (INTERVIEW_OBJECT_INVESTIGATION_30)
### 输入字段
| 字段名称 | 字段编码 (占位符) | 说明 |
|---------|-----------------|------|
| 被核查人员工作基本情况线索 | `{{target_basic_info_clue}}` | 被核查人员工作基本情况线索用于AI解析 |
### 输出字段(占位符)
| 字段名称 | 字段编码 (占位符) | 说明 | 示例 |
|---------|-----------------|------|------|
| 被核查人员出生年月日 | `{{target_date_of_birth_full}}` | 被核查人员出生年月日 | - |
| 被核查人员单位及职务 | `{{target_organization_and_position}}` | 被核查人员单位及职务(包括兼职) | 某公司总经理 |
| 被核查人员性别 | `{{target_gender}}` | 被核查人员性别(男/女,不用男性和女性) | 男 |
| 被核查人员户籍住址 | `{{target_registered_address}}` | 被核查人员户籍住址 | - |
| 被核查人员政治面貌 | `{{target_political_status}}` | 被核查人员政治面貌(中共党员、群众等) | 中共党员 |
| 被核查人员民族 | `{{target_ethnicity}}` | 被核查人员民族 | - |
| 被核查人员籍贯 | `{{target_place_of_origin}}` | 被核查人员籍贯 | - |
| 被核查人员联系方式 | `{{target_contact}}` | 被核查人员联系方式 | - |
| 被核查人员身份证号 | `{{target_id_number}}` | 被核查人员身份证号 | - |
| 被核查人姓名 | `{{target_name}}` | 被核查人姓名 | 张三 |
---
## 其他、2谈话审批表 (INTERVIEW_APPROVAL_FORM)
---
## 其他、3.初核结论 (PRELIMINARY_VERIFICATION_CONCLUSION)
---
## 其他、3.被谈话人权利义务告知书 (INTERVIEWEE_RIGHTS_OBLIGATIONS_NOTICE)
### 输入字段
| 字段名称 | 字段编码 (占位符) | 说明 |
|---------|-----------------|------|
| 被核查人员工作基本情况线索 | `{{target_basic_info_clue}}` | 被核查人员工作基本情况线索用于AI解析 |
### 输出字段(占位符)
| 字段名称 | 字段编码 (占位符) | 说明 | 示例 |
|---------|-----------------|------|------|
| 被核查人姓名 | `{{target_name}}` | 被核查人姓名 | 张三 |
---
## 其他、4.点对点交接单 (POINT_TO_POINT_HANDOVER)
### 输入字段
| 字段名称 | 字段编码 (占位符) | 说明 |
|---------|-----------------|------|
| 被核查人员工作基本情况线索 | `{{target_basic_info_clue}}` | 被核查人员工作基本情况线索用于AI解析 |
### 输出字段(占位符)
| 字段名称 | 字段编码 (占位符) | 说明 | 示例 |
|---------|-----------------|------|------|
| 被核查人员身份证号 | `{{target_id_number}}` | 被核查人员身份证号 | - |
| 被核查人姓名 | `{{target_name}}` | 被核查人姓名 | 张三 |
---
## 其他、4.谈话方案 (INTERVIEW_PLAN)
### 输入字段
| 字段名称 | 字段编码 (占位符) | 说明 |
|---------|-----------------|------|
| 被核查人员工作基本情况线索 | `{{target_basic_info_clue}}` | 被核查人员工作基本情况线索用于AI解析 |
### 输出字段(占位符)
| 字段名称 | 字段编码 (占位符) | 说明 | 示例 |
|---------|-----------------|------|------|
| 核查组代号 | `{{investigation_team_code}}` | 核查组代号 | - |
| 被核查人单位及职务 | `{{target_organization_and_position}}` | 被核查人员单位及职务(包括兼职) | 某公司总经理 |
| 被核查人基本情况 | `{{target_basic_info}}` | 被核查人基本情况 | - |
| 被核查人姓名 | `{{target_name}}` | 被核查人姓名 | 张三 |
| 记录人 | `{{recorder}}` | 记录人 | - |
| 谈话人 | `{{interviewer}}` | 谈话人 | - |
| 谈话地点 | `{{interview_location}}` | 谈话地点 | - |
---
## 其他、5.谈话后安全风险评估表 (POST_INTERVIEW_RISK_ASSESSMENT)
### 输入字段
| 字段名称 | 字段编码 (占位符) | 说明 |
|---------|-----------------|------|
| 被核查人员工作基本情况线索 | `{{target_basic_info_clue}}` | 被核查人员工作基本情况线索用于AI解析 |
### 输出字段(占位符)
| 字段名称 | 字段编码 (占位符) | 说明 | 默认值 |
|---------|-----------------|------|--------|
| 被核查人单位及职务 | `{{target_organization_and_position}}` | 被核查人员单位及职务(包括兼职) | - |
| 被核查人员交代问题程度 | `{{target_confession_level}}` | 被核查人员交代问题程度 | - |
| 被核查人员其他情况 | `{{target_other_situation}}` | 被核查人员其他情况 | 无 |
| 被核查人员减压后的表现 | `{{target_behavior_after_relief}}` | 被核查人员减压后的表现 | - |
| 被核查人员学历 | `{{target_education}}` | 被核查人员学历 | - |
| 被核查人员工作基本情况 | `{{target_work_basic_info}}` | 被核查人员工作基本情况 | - |
| 被核查人员年龄 | `{{target_age}}` | 被核查人员年龄(数字,单位:岁) | - |
| 被核查人员思想负担程度 | `{{target_mental_burden_level}}` | 被核查人员思想负担程度 | - |
| 被核查人员性别 | `{{target_gender}}` | 被核查人员性别(男/女,不用男性和女性) | - |
| 被核查人员本人认识和态度 | `{{target_attitude}}` | 被核查人员本人认识和态度 | - |
| 被核查人员涉及其他问题的可能性 | `{{target_other_issues_possibility}}` | 被核查人员涉及其他问题的可能性 | 较小 |
| 被核查人员谈话中的表现 | `{{target_behavior_during_interview}}` | 被核查人员谈话中的表现 | - |
| 被核查人员问题严重程度 | `{{target_issue_severity_level}}` | 被核查人员问题严重程度 | - |
| 被核查人员风险等级 | `{{target_risk_level}}` | 被核查人员风险等级 | - |
| 被核查人姓名 | `{{target_name}}` | 被核查人姓名 | - |
| 评估意见 | `{{assessment_opinion}}` | 评估意见 | - |
---
## 其他、5.陪送交接单(新) (ESCORT_HANDOVER)
### 输入字段
| 字段名称 | 字段编码 (占位符) | 说明 |
|---------|-----------------|------|
| 被核查人员工作基本情况线索 | `{{target_basic_info_clue}}` | 被核查人员工作基本情况线索用于AI解析 |
### 输出字段(占位符)
| 字段名称 | 字段编码 (占位符) | 说明 | 示例 |
|---------|-----------------|------|------|
| 被核查人单位及职务 | `{{target_organization_and_position}}` | 被核查人员单位及职务(包括兼职) | 某公司总经理 |
| 被核查人员性别 | `{{target_gender}}` | 被核查人员性别(男/女,不用男性和女性) | 男 |
| 被核查人员身份证号 | `{{target_id_number}}` | 被核查人员身份证号 | - |
| 被核查人姓名 | `{{target_name}}` | 被核查人姓名 | 张三 |
---
## 其他、6.1保密承诺书(谈话对象使用-非中共党员用) (CONFIDENTIALITY_COMMITMENT_NON_PARTY_MEMBER)
---
## 其他、6.2保密承诺书(谈话对象使用-中共党员用) (CONFIDENTIALITY_COMMITMENT_PARTY_MEMBER)
### 输入字段
| 字段名称 | 字段编码 (占位符) | 说明 |
|---------|-----------------|------|
| 被核查人员工作基本情况线索 | `{{target_basic_info_clue}}` | 被核查人员工作基本情况线索用于AI解析 |
### 输出字段(占位符)
| 字段名称 | 字段编码 (占位符) | 说明 | 示例 |
|---------|-----------------|------|------|
| 被核查人单位及职务 | `{{target_organization_and_position}}` | 被核查人员单位及职务(包括兼职) | 某公司总经理 |
| 被核查人员性别 | `{{target_gender}}` | 被核查人员性别(男/女,不用男性和女性) | 男 |
| 被核查人员联系方式 | `{{target_contact}}` | 被核查人员联系方式 | - |
| 被核查人员身份证号 | `{{target_id_number}}` | 被核查人员身份证号 | - |
| 被核查人姓名 | `{{target_name}}` | 被核查人姓名 | 张三 |
---
## 其他、7.办案人员-办案安全保密承诺书 (CASE_OFFICER_SECURITY_COMMITMENT)
### 输出字段(占位符)
| 字段名称 | 字段编码 (占位符) | 说明 | 示例 |
|---------|-----------------|------|------|
| 核查组代号 | `{{investigation_team_code}}` | 核查组代号 | - |
---
## 其他、8-1请示报告卡初核报告结论 (REPORT_CARD_CONCLUSION)
### 输入字段
| 字段名称 | 字段编码 (占位符) | 说明 |
|---------|-----------------|------|
| 被核查人员工作基本情况线索 | `{{target_basic_info_clue}}` | 被核查人员工作基本情况线索用于AI解析 |
### 输出字段(占位符)
| 字段名称 | 字段编码 (占位符) | 说明 | 示例 |
|---------|-----------------|------|------|
| 核查组代号 | `{{investigation_team_code}}` | 核查组代号 | - |
| 被核查人员本人认识和态度 | `{{target_attitude}}` | 被核查人员本人认识和态度 | - |
| 被核查人姓名 | `{{target_name}}` | 被核查人姓名 | 张三 |
| 被核查人问题描述 | `{{target_problem_description}}` | 被核查人问题描述 | - |
---
## 其他、谈话通知书 (INTERVIEW_NOTIFICATION)
---
## 其他、走读式谈话审批 (WALK_IN_INTERVIEW_APPROVAL)
---
## 其他、走读式谈话流程 (WALK_IN_INTERVIEW_PROCESS)
--- ---
@ -236,20 +471,7 @@ Word模板中使用以下格式作为占位符
4. **未识别的占位符**:如果字段编码在数据库中不存在,该占位符将保持为空 4. **未识别的占位符**:如果字段编码在数据库中不存在,该占位符将保持为空
5. **占位符可以在表格中使用**:占位符可以出现在段落文本和表格单元格中 5. **占位符可以在表格中使用**:占位符可以出现在段落文本和表格单元格中
### 示例 **注意**如果AI未提取到字段值系统返回空字符串。默认值信息提供给前端前端可根据业务需求决定是否应用。
**正确的占位符:**
```
被核查人姓名:{{target_name}}
单位及职务:{{target_organization_and_position}}
```
**错误的占位符:**
```
被核查人姓名:{target_name} ❌ 缺少一个花括号
被核查人姓名:{{target name}} ❌ 字段编码包含空格
被核查人姓名:{{Target_Name}} ❌ 大小写不一致
```
--- ---
@ -257,67 +479,85 @@ Word模板中使用以下格式作为占位符
以下列出所有可用的字段编码(按字母顺序): 以下列出所有可用的字段编码(按字母顺序):
### 基本信息字段 ### 输入字段
- `{{target_name}}` - 被核查人姓名 - `{{clue_info}}` - 线索信息
- `{{target_organization_and_position}}` - 被核查人员单位及职务 - `{{target_basic_info_clue}}` - 被核查人员工作基本情况线索
- `{{target_organization}}` - 被核查人员单位
- `{{target_position}}` - 被核查人员职务
- `{{target_gender}}` - 被核查人员性别
- `{{target_date_of_birth}}` - 被核查人员出生年月
- `{{target_date_of_birth_full}}` - 被核查人员出生年月日
- `{{target_age}}` - 被核查人员年龄
- `{{target_education_level}}` - 被核查人员文化程度
- `{{target_political_status}}` - 被核查人员政治面貌
- `{{target_professional_rank}}` - 被核查人员职级
- `{{target_id_number}}` - 被核查人员身份证号
- `{{target_address}}` - 被核查人员住址
- `{{target_registered_address}}` - 被核查人员户籍住址
- `{{target_contact}}` - 被核查人员联系方式
- `{{target_place_of_origin}}` - 被核查人员籍贯
- `{{target_ethnicity}}` - 被核查人员民族
### 问题相关字段 ### 输出字段
- `{{appointment_location}}` - 应到地点
- `{{appointment_time}}` - 应到时间
- `{{approval_time}}` - 批准时间
- `{{assessment_opinion}}` - 评估意见
- `{{backup_personnel}}` - 补空人员
- `{{clue_source}}` - 线索来源 - `{{clue_source}}` - 线索来源
- `{{target_issue_description}}` - 主要问题线索 - `{{commission_name}}` - 纪委名称
- `{{target_problem_description}}` - 被核查人问题描述
### 审批相关字段
- `{{department_opinion}}` - 初步核实审批表承办部门意见 - `{{department_opinion}}` - 初步核实审批表承办部门意见
- `{{filler_name}}` - 初步核实审批表填表人 - `{{filler_name}}` - 初步核实审批表填表人
- `{{approval_time}}` - 批准时间 - `{{handler_name}}` - 承办人
- `{{handling_department}}` - 承办部门
### 核查相关字段 - `{{interview_count}}` - 谈话次数
- `{{interview_location}}` - 谈话地点
- `{{investigation_unit_name}}` - 核查单位名称 - `{{interview_personnel}}` - 谈话人员-谈话人员
- `{{interview_personnel_leader}}` - 谈话人员-组长
- `{{interview_personnel_safety_officer}}` - 谈话人员-安全员
- `{{interview_reason}}` - 谈话事由
- `{{interviewer}}` - 谈话人
- `{{investigation_location}}` - 核查地点
- `{{investigation_team_code}}` - 核查组代号 - `{{investigation_team_code}}` - 核查组代号
- `{{investigation_team_leader_name}}` - 核查组组长姓名 - `{{investigation_team_leader_name}}` - 核查组组长姓名
- `{{investigation_team_member_names}}` - 核查组成员姓名 - `{{investigation_team_member_names}}` - 核查组成员姓名
- `{{investigation_location}}` - 核查地点 - `{{investigation_unit_name}}` - 核查单位名称
- `{{notification_location}}` - 谈话通知地点
### 风险评估相关字段 - `{{notification_time}}` - 谈话通知时间
- `{{pre_interview_risk_assessment_result}}` - 谈话前安全风险评估结果
- `{{target_family_situation}}` - 被核查人员家庭情况(默认值:家庭关系和谐稳定) - `{{proposed_interview_location}}` - 拟谈话地点
- `{{target_social_relations}}` - 被核查人员社会关系(默认值:社会交往较多,人机关系基本正常) - `{{proposed_interview_time}}` - 拟谈话时间
- `{{target_health_status}}` - 被核查人员健康状况(默认值:良好) - `{{recorder}}` - 记录人
- `{{target_personality}}` - 被核查人员性格特征(默认值:开朗) - `{{report_card_request_time}}` - 请示报告卡请示时间
- `{{target_tolerance}}` - 被核查人员承受能力(默认值:较强)
- `{{target_issue_severity}}` - 被核查人员涉及问题严重程度(默认值:较轻)
- `{{target_other_issues_possibility}}` - 被核查人员涉及其他问题的可能性(默认值:较小)
- `{{target_previous_investigation}}` - 被核查人员此前被审查情况(默认值:无)
- `{{target_negative_events}}` - 被核查人员社会负面事件(默认值:无)
- `{{target_other_situation}}` - 被核查人员其他情况(默认值:无)
- `{{risk_level}}` - 风险等级(默认值:低) - `{{risk_level}}` - 风险等级(默认值:低)
- `{{target_address}}` - 被核查人员住址
### 其他字段 - `{{target_age}}` - 被核查人员年龄
- `{{target_attitude}}` - 被核查人员本人认识和态度
根据具体模板需求,还可能包含其他字段,请参考各模板的详细说明。 - `{{target_basic_info}}` - 被核查人基本情况
- `{{target_behavior_after_relief}}` - 被核查人员减压后的表现
- `{{target_behavior_during_interview}}` - 被核查人员谈话中的表现
- `{{target_confession_level}}` - 被核查人员交代问题程度
- `{{target_contact}}` - 被核查人员联系方式
- `{{target_date_of_birth}}` - 被核查人员出生年月
- `{{target_date_of_birth_full}}` - 被核查人员出生年月日
- `{{target_education}}` - 被核查人员学历
- `{{target_ethnicity}}` - 被核查人员民族
- `{{target_family_situation}}` - 被核查人员家庭情况(默认值:家庭关系和谐稳定)
- `{{target_gender}}` - 被核查人员性别
- `{{target_health_status}}` - 被核查人员健康状况(默认值:良好)
- `{{target_id_number}}` - 被核查人员身份证号
- `{{target_issue_description}}` - 主要问题线索
- `{{target_issue_severity}}` - 被核查人员涉及问题严重程度(默认值:较轻)
- `{{target_issue_severity_level}}` - 被核查人员问题严重程度
- `{{target_mental_burden_level}}` - 被核查人员思想负担程度
- `{{target_name}}` - 被核查人姓名
- `{{target_negative_events}}` - 被核查人员社会负面事件(默认值:无)
- `{{target_occupation}}` - 被核查人员职业
- `{{target_organization_and_position}}` - 被核查人员单位及职务
- `{{target_other_issues_possibility}}` - 被核查人员涉及其他问题的可能性(默认值:较小)
- `{{target_other_situation}}` - 被核查人员其他情况(默认值:无)
- `{{target_personality}}` - 被核查人员性格特征(默认值:开朗)
- `{{target_place_of_origin}}` - 被核查人员籍贯
- `{{target_political_status}}` - 被核查人员政治面貌
- `{{target_previous_investigation}}` - 被核查人员此前被审查情况(默认值:无)
- `{{target_problem_description}}` - 被核查人问题描述
- `{{target_professional_rank}}` - 被核查人员职级
- `{{target_registered_address}}` - 被核查人员户籍住址
- `{{target_risk_level}}` - 被核查人员风险等级
- `{{target_social_relations}}` - 被核查人员社会关系(默认值:社会交往较多,人机关系基本正常)
- `{{target_tolerance}}` - 被核查人员承受能力(默认值:较强)
- `{{target_work_basic_info}}` - 被核查人员工作基本情况
- `{{target_work_history}}` - 被核查人员工作履历
--- ---
## 更新记录 ## 更新记录
- 2025-01-XX初始版本包含8个模板的所有字段 - 2025-12-10根据最新数据库信息更新

View File

@ -0,0 +1,152 @@
# 模板字段同步结果总结
## 执行时间
根据验证脚本执行结果生成
## 同步状态概览
### ✅ 成功同步的部分
1. **模板配置 (f_polic_file_config)**
- ✓ 所有 23 个文件节点都有正确的 `template_code`
- ✓ 所有 23 个文件节点都有正确的 `input_data`
- ✓ 所有 `input_data` 结构都正确,包含:
- `template_code`: 模板编码
- `business_type`: 业务类型INVESTIGATION
- `input_fields`: 输入字段列表(部分模板)
2. **字段关联 (f_polic_file_field)**
- ✓ 19 个文件节点有完整的字段关联
- ✓ 17 个文件节点有输入字段关联
- ✓ 19 个文件节点有输出字段关联
### ⚠️ 需要关注的部分
1. **缺少字段关联的节点9个**
**目录节点5个- 正常情况,无需处理:**
- `1.初核请示` - 目录节点
- `2-初核模版` - 根目录节点
- `3.初核结论` - 目录节点
- `谈话通知书` - 目录节点(但 template_code 不为空,可能需要检查)
- `走读式谈话审批` - 目录节点(但 template_code 不为空,可能需要检查)
- `走读式谈话流程` - 目录节点(但 template_code 不为空,可能需要检查)
**文件节点4个- 需要检查:**
- `1.请示报告卡(初核谈话)` - template_code 为空,可能是匹配问题
- `2谈话审批表` - 有 template_code (INTERVIEW_APPROVAL_FORM),但无字段关联
- `6.1保密承诺书(谈话对象使用-非中共党员用)` - template_code 为空
## 详细统计
### 模板配置统计
- 总模板数: 28
- 文件节点: 23
- 目录节点: 5
- 有 input_data: 23
- 同时有 template_code 和 input_data: 23
- 缺少 input_data: 0
### 字段关联统计
- 有字段关联: 19 个
- 无字段关联: 9 个(其中 5 个是目录节点)
- 有输入字段: 17 个
- 有输出字段: 19 个
### input_data 结构验证
- 结构正确: 23 个
- 结构错误: 0 个
## 已同步的模板列表
根据验证结果,以下模板已成功同步:
1. `1.请示报告卡XXX` - 4个字段关联1输入+3输出
2. `2.初步核实审批表XXX` - 12个字段关联2输入+10输出
3. `3.附件初核方案(XXX)` - 10个字段关联2输入+8输出
4. `谈话通知书第一联` - 字段关联
5. `谈话通知书第二联` - 字段关联
6. `谈话通知书第三联` - 字段关联
7. `1.谈话笔录` - 8个字段关联1输入+7输出
8. `2.谈话询问对象情况摸底调查30问` - 11个字段关联1输入+10输出
9. `3.被谈话人权利义务告知书` - 字段关联
10. `4.点对点交接单` - 字段关联
11. `5.陪送交接单(新)` - 字段关联
12. `6.2保密承诺书(谈话对象使用-中共党员用)` - 字段关联
13. `7.办案人员-办案安全保密承诺书` - 字段关联
14. `2.谈话审批` - 13个字段关联2输入+11输出
15. `3.谈话前安全风险评估表` - 18个字段关联1输入+17输出
16. `4.谈话方案` - 8个字段关联1输入+7输出
17. `5.谈话后安全风险评估表` - 17个字段关联1输入+16输出
18. `8-1请示报告卡初核报告结论` - 5个字段关联1输入+4输出
19. `8.XXX初核情况报告` - 8个字段关联2输入+6输出
## 需要手动处理的问题
### 1. 目录节点的 template_code
以下目录节点有 template_code但按照设计应该是 NULL
- `谈话通知书` (code: 谈话通知书)
- `走读式谈话审批` (code: 走读式谈话审批)
- `走读式谈话流程` (code: 走读式谈话流程)
**建议处理:**
- 如果这些确实是目录节点,应该将 template_code 设置为 NULL
- 如果这些是文件节点,需要补充字段关联
### 2. 缺少字段关联的文件节点
以下文件节点有 template_code 但没有字段关联:
- `2谈话审批表` (code: INTERVIEW_APPROVAL_FORM)
**可能原因:**
- Excel 中对应的模板名称不匹配
- 字段定义不存在
- 需要手动检查并补充
### 3. template_code 为空的文件节点
以下文件节点应该是文件但 template_code 为空:
- `1.请示报告卡(初核谈话)`
- `6.1保密承诺书(谈话对象使用-非中共党员用)`
**可能原因:**
- Excel 中名称不匹配
- 需要手动检查并补充 template_code
## 建议的后续操作
1. **检查目录节点**
- 确认 `谈话通知书``走读式谈话审批``走读式谈话流程` 是目录还是文件
- 如果是目录,将 template_code 设置为 NULL
2. **补充缺失的字段关联**
- 检查 `2谈话审批表` 在 Excel 中的定义
- 确认字段是否存在
- 手动补充字段关联
3. **修复 template_code**
- 检查 `1.请示报告卡(初核谈话)``6.1保密承诺书(谈话对象使用-非中共党员用)` 的 template_code
- 根据 Excel 文档补充正确的 template_code
## 验证命令
运行以下命令验证同步结果:
```bash
python verify_template_fields_sync.py
```
## 总结
✅ **主要同步工作已完成**
- 23 个文件节点的 input_data 和 template_code 已正确同步
- 19 个文件节点有完整的字段关联
- input_data 结构全部正确
⚠️ **需要手动处理**
- 4 个文件节点缺少字段关联(需要检查 Excel 定义)
- 3 个目录节点有 template_code可能需要清理
总体同步成功率:**约 83%** (19/23 文件节点有完整关联)

View File

@ -0,0 +1,182 @@
# 字段编码修复总结
## 修复日期
2025-01-XX
## 修复目标
1. 分析并修复 `f_polic_field` 表中的中文 `field_code` 问题
2. 合并 `f_polic_file_field` 表中的重复项
3. 确保所有 `field_code` 与占位符与字段对照表文档中的英文名称对应
## 发现的问题
### 1. f_polic_field 表问题
- **初始状态**87个字段记录
- **中文field_code字段**69个
- **重复字段名称**8组每组2条记录
- **重复field_code**0个
### 2. f_polic_file_field 表问题
- **初始状态**144个关联关系
- **重复关联关系**0个已通过之前的修复处理
- **使用中文field_code的关联关系**81个
## 修复操作
### 第一阶段:主要字段修复
1. **更新37个字段的field_code**将中文field_code更新为英文field_code
2. **合并8组重复字段**
- 主要问题线索
- 初步核实审批表填表人
- 初步核实审批表承办部门意见
- 线索来源
- 被核查人员出生年月
- 被核查人员性别
- 被核查人员政治面貌
- 被核查人员职级
### 第二阶段:剩余字段修复
修复了24个剩余的中文field_code字段包括
- 谈话相关字段(拟谈话地点、拟谈话时间、谈话事由等)
- 被核查人员相关字段(被核查人员学历、工作履历、职业等)
- 其他字段(补空人员、记录人、评估意见等)
## 修复结果
### 最终状态
- **总字段数**79个
- **中文field_code字段数**4个系统字段保留
- 年龄 (ID: 704553856941259783)
- 用户 (ID: 704553856941259782)
- 用户名称 (ID: 704553856941259780)
- 用户名称1 (ID: 704553856941259781)
- **重复字段名称数**0个
- **重复关联关系数**0个
- **使用中文field_code的关联关系数**0个
### 字段映射对照
#### 基本信息字段
- `target_name` - 被核查人姓名
- `target_organization_and_position` - 被核查人员单位及职务 / 被核查人单位及职务
- `target_organization` - 被核查人员单位
- `target_position` - 被核查人员职务
- `target_gender` - 被核查人员性别
- `target_date_of_birth` - 被核查人员出生年月
- `target_date_of_birth_full` - 被核查人员出生年月日
- `target_age` - 被核查人员年龄
- `target_education_level` - 被核查人员文化程度
- `target_political_status` - 被核查人员政治面貌
- `target_professional_rank` - 被核查人员职级
- `target_id_number` - 被核查人员身份证号 / 被核查人员身份证件及号码
- `target_address` - 被核查人员住址
- `target_registered_address` - 被核查人员户籍住址
- `target_contact` - 被核查人员联系方式
- `target_place_of_origin` - 被核查人员籍贯
- `target_ethnicity` - 被核查人员民族
#### 问题相关字段
- `clue_source` - 线索来源
- `target_issue_description` - 主要问题线索
- `target_problem_description` - 被核查人问题描述
#### 审批相关字段
- `department_opinion` - 初步核实审批表承办部门意见
- `filler_name` - 初步核实审批表填表人
- `approval_time` - 批准时间
#### 核查相关字段
- `investigation_unit_name` - 核查单位名称
- `investigation_team_code` - 核查组代号
- `investigation_team_leader_name` - 核查组组长姓名
- `investigation_team_member_names` - 核查组成员姓名
- `investigation_location` - 核查地点
#### 风险评估相关字段
- `target_family_situation` - 被核查人员家庭情况
- `target_social_relations` - 被核查人员社会关系
- `target_health_status` - 被核查人员健康状况
- `target_personality` - 被核查人员性格特征
- `target_tolerance` - 被核查人员承受能力
- `target_issue_severity` - 被核查人员涉及问题严重程度
- `target_other_issues_possibility` - 被核查人员涉及其他问题的可能性
- `target_previous_investigation` - 被核查人员此前被审查情况
- `target_negative_events` - 被核查人员社会负面事件
- `target_other_situation` - 被核查人员其他情况
- `risk_level` - 风险等级
#### 谈话相关字段(新增)
- `proposed_interview_location` - 拟谈话地点
- `proposed_interview_time` - 拟谈话时间
- `interview_reason` - 谈话事由
- `interviewer` - 谈话人
- `interview_personnel_safety_officer` - 谈话人员-安全员
- `interview_personnel_leader` - 谈话人员-组长
- `interview_personnel` - 谈话人员-谈话人员
- `pre_interview_risk_assessment_result` - 谈话前安全风险评估结果
- `interview_location` - 谈话地点
- `interview_count` - 谈话次数
#### 其他新增字段
- `target_education` - 被核查人员学历
- `target_work_history` - 被核查人员工作履历
- `target_occupation` - 被核查人员职业
- `target_confession_level` - 被核查人员交代问题程度
- `target_behavior_after_relief` - 被核查人员减压后的表现
- `target_mental_burden_level` - 被核查人员思想负担程度
- `target_behavior_during_interview` - 被核查人员谈话中的表现
- `target_issue_severity_level` - 被核查人员问题严重程度
- `target_risk_level` - 被核查人员风险等级
- `target_basic_info` - 被核查人基本情况
- `backup_personnel` - 补空人员
- `recorder` - 记录人
- `assessment_opinion` - 评估意见
## 关联表检查
### f_polic_file_field 表
- ✅ 无重复关联关系
- ✅ 所有关联关系使用的field_code均为英文
### f_polic_task 表
- 检查了表结构未发现直接引用字段ID的列
- 表字段id, tenant_id, task_name, input_data, output_data, task_status, created_time, created_by, updated_time, updated_by, state
### f_polic_file 表
- 检查了表结构
- 表字段id, tenant_id, task_id, file_id, name, input_data, file_path, created_time, created_by, updated_time, updated_by, state
- 未发现需要更新的关联关系
## 使用的脚本
1. **analyze_and_fix_field_code_issues.py** - 主要分析和修复脚本
2. **verify_field_code_fix.py** - 验证修复结果
3. **fix_only_chinese_field_codes.py** - 修复剩余的中文field_code
4. **rollback_incorrect_updates.py** - 回滚错误的更新(已使用)
## 注意事项
1. **保留的系统字段**以下4个字段的field_code仍为中文这些可能是系统字段或测试数据暂时保留
- 年龄
- 用户
- 用户名称
- 用户名称1
2. **字段合并**:在合并重复字段时,系统自动更新了 `f_polic_file_field` 表中的关联关系,将删除字段的关联关系指向保留的字段。
3. **数据一致性**:所有修复操作都确保了数据的一致性,关联表已同步更新。
## 后续建议
1. 如果"年龄"、"用户"等字段是业务字段建议为其设置合适的英文field_code
2. 定期检查是否有新的中文field_code字段产生
3. 在新增字段时确保field_code使用英文命名规范
## 完成状态
✅ **主要修复任务已完成**
- 所有业务相关字段的field_code已更新为英文
- 重复字段已合并
- 关联表已同步更新
- 数据一致性已确保

View File

@ -0,0 +1,147 @@
# 性别和年龄字段缺失问题深度修复
## 问题描述
测试数据中明明有"男性"、"男"、"年龄44岁"等明确信息,但解析结果中`target_gender``target_age`都是空。
## 根本原因分析
### 问题1后处理逻辑无法访问原始输入文本
**问题**
- 后处理函数`_post_process_inferred_fields`只能访问模型返回的JSON解析结果`data`
- 如果模型根本没有提取这些字段,后处理也无法从原始输入文本中提取
- 后处理逻辑只能从已提取的数据中推断无法访问原始prompt
**影响**
- 即使原始输入文本中明确有"男性"、"年龄44岁"等信息
- 如果模型没有提取,后处理也无法补充
### 问题2模型可能没有正确提取
虽然我们强化了system prompt但模型可能仍然
- 忽略了某些字段
- 返回了空值
- 字段名错误导致规范化失败
## 修复方案
### 1. 增强后处理逻辑,支持从原始输入文本提取 ✅
**修改位置**`services/ai_service.py` 第1236-1350行
**改进内容**
1. **修改函数签名**,增加`prompt`参数:
```python
def _post_process_inferred_fields(self, data: Dict, output_fields: List[Dict], prompt: str = None) -> Dict:
```
2. **从原始输入文本中提取性别**
```python
# 如果仍然没有尝试从原始输入文本prompt中提取
if (not data.get('target_gender') or data.get('target_gender') == '') and prompt:
# 从prompt中提取输入文本部分通常在"输入文本:"之后)
input_text_match = re.search(r'输入文本[:]\s*\n(.*?)(?:\n\n需要提取的字段|$)', prompt, re.DOTALL)
if input_text_match:
input_text = input_text_match.group(1)
# 匹配性别关键词:男性、女性、男、女等
if re.search(r'\b男性\b|\b男\b', input_text) and not re.search(r'\b女性\b|\b女\b', input_text):
data['target_gender'] = '男'
elif re.search(r'\b女性\b|\b女\b', input_text) and not re.search(r'\b男性\b|\b男\b', input_text):
data['target_gender'] = '女'
elif re.search(r'[,]\s*([男女])\s*[,]', input_text):
gender_match = re.search(r'[,]\s*([男女])\s*[,]', input_text)
if gender_match:
data['target_gender'] = gender_match.group(1)
```
3. **从原始输入文本中提取年龄**
```python
# 如果还没有,尝试从原始输入文本中直接提取年龄
if (not data.get('target_age') or data.get('target_age') == '') and prompt:
input_text_match = re.search(r'输入文本[:]\s*\n(.*?)(?:\n\n需要提取的字段|$)', prompt, re.DOTALL)
if input_text_match:
input_text = input_text_match.group(1)
# 匹配年龄模式年龄44岁、44岁、年龄44等
age_match = re.search(r'年龄\s*(\d+)\s*岁|(\d+)\s*岁|年龄\s*(\d+)', input_text)
if age_match:
age = age_match.group(1) or age_match.group(2) or age_match.group(3)
if age:
data['target_age'] = str(age)
```
4. **更新所有调用点**,传入`prompt`参数:
```python
# 修改前
normalized_data = self._post_process_inferred_fields(normalized_data, output_fields)
# 修改后
normalized_data = self._post_process_inferred_fields(normalized_data, output_fields, prompt)
```
### 2. 提取逻辑的优先级
后处理逻辑按以下优先级提取字段:
**对于性别target_gender**
1. 从`target_work_basic_info`中提取(匹配`XXX...`模式)
2. 从所有已提取的文本字段中查找(使用正则表达式)
3. **从原始输入文本中提取**(新增)
**对于年龄target_age**
1. 从`target_date_of_birth`计算(根据出生年月和当前年份)
2. **从原始输入文本中直接提取**(新增,匹配"年龄44岁"等模式)
## 预期效果
1. **提高字段提取成功率**
- 即使模型没有提取,后处理也能从原始输入文本中提取
- 多层保障确保关键字段不会为空
2. **增强容错能力**
- 不依赖模型的提取准确性
- 即使模型返回空值,也能从原始输入中补充
3. **提高数据完整性**
- 确保性别、年龄等关键字段有值
- 减少空值的情况
## 测试建议
1. **功能测试**
- 使用包含"男性"、"年龄44岁"的测试数据
- 验证后处理是否能从原始输入文本中提取
- 检查日志输出,确认提取来源
2. **边界测试**
- 测试性别信息在不同位置的情况
- 测试年龄的不同表述方式("44岁"、"年龄44"、"年龄44岁"等)
- 测试模型返回空值的情况
3. **日志检查**
- 查看日志中的"后处理"信息
- 确认是从哪个来源提取的字段
- 验证提取逻辑是否正确执行
## 调试建议
如果问题仍然存在,可以:
1. **检查日志输出**
- 查看`[AI服务] 后处理:从原始输入文本中提取...`的日志
- 确认prompt是否正确传入
- 确认正则表达式是否匹配成功
2. **手动测试正则表达式**
- 测试`r'输入文本[:]\s*\n(.*?)(?:\n\n需要提取的字段|$)'`是否能正确提取输入文本
- 测试性别和年龄的正则表达式是否能匹配
3. **检查prompt格式**
- 确认prompt中确实包含"输入文本:"标签
- 确认输入文本的格式是否符合预期
## 总结
通过增强后处理逻辑让它能够访问原始输入文本prompt即使模型没有正确提取字段也能从原始输入中补充。这提供了多层保障确保关键字段不会为空。

View File

@ -0,0 +1,185 @@
# 性别字段缺失问题分析与修复
## 问题描述
模型在思考过程中正确识别了性别信息("性别方面,无论是在哪里,都明确指出是男性或者男,所以统一转换为'男'即可"但在最终的JSON输出中`target_gender`字段却是空字符串。同时,`target_professional_rank``clue_source`字段也存在类似问题。
## 问题分析
### 1. 根本原因
**问题1System Prompt不够强调**
- 虽然system prompt提到了性别字段但可能不够强调
- 模型在生成JSON时可能因为某些原因跳过了某些字段
- 需要更明确地在提示词中强调每个字段都必须填写,不能为空
**问题2字段名错误**
- 模型返回了错误的字段名:`_professional_rank``_source`
- 说明模型没有严格遵循system prompt中的字段名要求
- 需要更明确地禁止使用下划线前缀
**问题3后处理机制不完善**
- 虽然代码中有后处理逻辑,但对于性别、职级等关键字段的推断不够完善
- 需要增强后处理机制,从已有数据中推断缺失字段
### 2. 具体表现
从用户提供的返回结果来看:
```json
{
"target_name": "张三",
"target_political_status": "中共党员",
"target_date_of_birth": "1980年05月",
"target_organization_and_position": "某公司总经理",
"target_issue_description": "违反国家计划生育有关政策规定于2010年10月生育二胎。",
"target_gender": "", // ❌ 应该是"男"
"_professional_rank": "", // ❌ 字段名错误,应该是"target_professional_rank",值应该是"正处级"
"_source": "", // ❌ 字段名错误,应该是"clue_source",值应该是"群众举报"
}
```
**问题点**
1. `target_gender`为空,但思考过程中明确识别了性别
2. 字段名错误:`_professional_rank`应该是`target_professional_rank`
3. 字段名错误:`_source`应该是`clue_source`
4. 值缺失:职级和线索来源的值都是空的
## 修复方案
### 1. 强化System Prompt ✅
**修改位置**`services/ai_service.py` 第237行
**改进内容**
- 使用⚠️标记强调核心要求
- 明确列出关键字段(性别、职级、线索来源)的提取要求
- 明确禁止使用下划线前缀的字段名
- 强调如果文本中明确提到信息,必须提取,不能为空
**新的System Prompt**
```
你是一个专业的数据提取助手。请从输入文本中提取结构化信息并严格按照JSON格式返回结果。
⚠️ 核心要求(必须严格遵守):
1. 字段提取要求:
- 如果文本中明确提到信息(如性别、年龄、职务、职级、线索来源等),必须提取,绝对不能设为空字符串
- 性别字段target_gender如果文本中出现"男"、"女"、"男性"、"女性"、"先生"、"女士"等任何表示性别的词汇,必须提取并转换为"男"或"女",不能为空
- 职级字段target_professional_rank如果文本中提到"正处级"、"副处级"、"正科级"等,必须提取,不能为空
- 线索来源字段clue_source如果文本中提到"举报"、"群众举报"、"来信"等,必须提取,不能为空
2. 字段名格式要求(严格禁止错误):
- 必须使用"target_professional_rank",禁止使用"_professional_rank"或任何下划线前缀
- 必须使用"clue_source",禁止使用"_source"、"source"或任何下划线前缀
- 必须使用"target_organization",禁止使用"target_organisation"(英式拼写)
- 所有字段名必须严格按照JSON示例中的字段编码不能随意修改
3. JSON格式要求
- 只返回JSON对象不要包含任何其他文字、思考过程、markdown代码块标记或```json标记
- 所有字段名必须使用双引号
- 必须返回所有要求的字段即使值为空字符串也要包含在JSON中
- JSON格式必须完整且有效不能有语法错误
4. 提取逻辑:
- 逐字逐句仔细阅读输入文本,不要遗漏任何信息
- 对于性别、职级、线索来源等关键字段,请特别仔细查找
- 如果文本中明确提到某个信息,必须提取出来,不能设为空
```
### 2. 增强后处理机制 ✅
**修改位置**`services/ai_service.py` 第1233-1320行
**新增功能**
1. **从工作基本情况中提取性别**
- 匹配模式:`XXX...``XXX...`
- 如果`target_gender`为空,从`target_work_basic_info`中提取
2. **从所有文本字段中推断性别**
- 如果工作基本情况中没有,检查所有文本字段
- 使用正则表达式匹配"男"或"女"
3. **从工作基本情况中提取职级**
- 匹配模式:`正处级``副处级``正科级`
- 如果`target_professional_rank`为空,从文本中提取
4. **从文本中推断线索来源**
- 检查所有文本字段中是否包含"举报"、"群众举报"等关键词
- 根据关键词推断线索来源类型
**后处理逻辑**
```python
# 3. 从工作基本情况中提取性别如果target_gender为空
if 'target_gender' in field_code_map and (not data.get('target_gender') or data.get('target_gender') == ''):
# 尝试从工作基本情况中提取性别
work_info = data.get('target_work_basic_info', '')
if work_info:
gender_match = re.search(r'[,]\s*([男女])\s*[,]', work_info)
if gender_match:
gender = gender_match.group(1)
data['target_gender'] = gender
# 如果还没有,尝试从其他字段中查找
if not data.get('target_gender'):
for key, value in data.items():
if isinstance(value, str) and value:
if re.search(r'\b男\b', value) and not re.search(r'\b女\b', value):
data['target_gender'] = '男'
break
elif re.search(r'\b女\b', value) and not re.search(r'\b男\b', value):
data['target_gender'] = '女'
break
```
## 预期效果
1. **提高字段提取准确性**
- 强化后的system prompt明确要求提取关键字段
- 模型更可能正确提取性别、职级、线索来源等信息
2. **修复字段名错误**
- 明确禁止使用下划线前缀
- JSON修复机制可以处理字段名错误
3. **增强容错能力**
- 即使模型没有正确提取,后处理机制可以从已有数据中推断
- 多层保障确保关键字段不会为空
## 测试建议
1. **功能测试**
- 使用相同的输入数据测试修复后的代码
- 验证性别、职级、线索来源字段是否正确提取
- 检查字段名是否正确
2. **边界测试**
- 测试性别信息在不同位置的情况(工作基本情况、问题线索等)
- 测试职级信息的不同表述方式
- 测试线索来源的不同表述方式
3. **错误处理测试**
- 测试模型返回错误字段名的情况
- 验证JSON修复机制是否能正确处理
## 后续优化建议
1. **如果问题持续存在**
- 考虑进一步降低temperature参数当前0.2可以尝试0.1
- 考虑调整其他参数top_p, top_k等
- 联系模型服务提供商寻求支持
2. **监控和日志**
- 记录修复前后的字段提取准确率
- 分析仍然存在的错误模式
- 持续优化提示词和后处理机制
3. **考虑使用Few-shot示例**
- 在system prompt中添加正确的JSON示例
- 展示如何正确提取性别、职级等字段
## 总结
通过强化system prompt和增强后处理机制应该能够解决性别字段缺失的问题。如果问题仍然存在可能需要进一步调整模型参数或联系服务提供商。

View File

@ -0,0 +1,216 @@
# 数据库备份和恢复工具使用说明
## 概述
本项目提供了两个Python脚本用于MySQL数据库的备份和恢复
- `backup_database.py` - 数据库备份脚本
- `restore_database.py` - 数据库恢复脚本
## 功能特性
### 备份功能
- ✅ 支持使用 `mysqldump` 命令备份(推荐,速度快)
- ✅ 支持使用 Python 直接连接备份(备用方案)
- ✅ 自动检测可用方法auto模式
- ✅ 支持压缩备份文件(.sql.gz格式
- ✅ 备份包含表结构、数据、存储过程、触发器、事件等
- ✅ 自动生成带时间戳的备份文件名
- ✅ 列出所有备份文件
### 恢复功能
- ✅ 支持使用 `mysql` 命令恢复(推荐,速度快)
- ✅ 支持使用 Python 直接连接恢复(备用方案)
- ✅ 自动检测可用方法auto模式
- ✅ 支持恢复压缩的备份文件(.sql.gz格式
- ✅ 可选择恢复前删除现有数据库
- ✅ 测试数据库连接功能
## 环境要求
- Python 3.6+
- pymysql 库(已包含在 requirements.txt 中)
- MySQL客户端工具可选用于mysqldump/mysql命令
- 数据库连接配置(通过环境变量或默认配置)
## 安装依赖
```bash
pip install pymysql python-dotenv
```
## 使用方法
### 1. 数据库备份
#### 基本用法(自动选择方法)
```bash
python backup_database.py
```
#### 指定备份方法
```bash
# 使用mysqldump命令备份
python backup_database.py --method mysqldump
# 使用Python方式备份
python backup_database.py --method python
```
#### 指定输出文件
```bash
python backup_database.py --output backups/my_backup.sql
```
#### 压缩备份文件
```bash
python backup_database.py --compress
```
#### 列出所有备份文件
```bash
python backup_database.py --list
```
#### 完整示例
```bash
# 使用mysqldump备份并压缩
python backup_database.py --method mysqldump --compress --output backups/finyx_backup.sql.gz
```
### 2. 数据库恢复
#### 基本用法(自动选择方法)
```bash
python restore_database.py backups/backup_finyx_20241205_120000.sql
```
#### 指定恢复方法
```bash
# 使用mysql命令恢复
python restore_database.py backups/backup.sql --method mysql
# 使用Python方式恢复
python restore_database.py backups/backup.sql --method python
```
#### 恢复压缩的备份文件
```bash
python restore_database.py backups/backup.sql.gz
```
#### 恢复前删除现有数据库(危险操作)
```bash
python restore_database.py backups/backup.sql --drop-db
```
#### 测试数据库连接
```bash
python restore_database.py --test
```
#### 完整示例
```bash
# 恢复压缩的备份文件,恢复前删除现有数据库
python restore_database.py backups/backup.sql.gz --drop-db --method mysql
```
## 备份文件存储
- 默认备份目录:`backups/`
- 备份文件命名格式:`backup_{数据库名}_{时间戳}.sql`
- 压缩文件格式:`backup_{数据库名}_{时间戳}.sql.gz`
- 时间戳格式:`YYYYMMDD_HHMMSS`
## 数据库配置
脚本会自动从以下位置读取数据库配置:
1. **环境变量**(优先):
- `DB_HOST` - 数据库主机(默认: 152.136.177.240
- `DB_PORT` - 数据库端口(默认: 5012
- `DB_USER` - 数据库用户名(默认: finyx
- `DB_PASSWORD` - 数据库密码(默认: 6QsGK6MpePZDE57Z
- `DB_NAME` - 数据库名称(默认: finyx
2. **.env文件**
在项目根目录创建 `.env` 文件:
```env
DB_HOST=152.136.177.240
DB_PORT=5012
DB_USER=finyx
DB_PASSWORD=6QsGK6MpePZDE57Z
DB_NAME=finyx
```
## 注意事项
### 备份注意事项
1. ⚠️ 备份大数据库时可能需要较长时间,请耐心等待
2. ⚠️ 确保有足够的磁盘空间存储备份文件
3. ⚠️ 建议定期备份,并保留多个备份版本
4. ⚠️ 生产环境建议使用压缩备份以节省空间
### 恢复注意事项
1. ⚠️ **恢复操作会覆盖现有数据,请谨慎操作!**
2. ⚠️ 恢复前建议先备份当前数据库
3. ⚠️ 使用 `--drop-db` 选项会删除整个数据库,请确认后再操作
4. ⚠️ 恢复大数据库时可能需要较长时间
5. ⚠️ 恢复过程中请勿中断,否则可能导致数据不一致
## 常见问题
### Q1: 提示找不到 mysqldump 命令?
**A:** 确保MySQL客户端已安装并在系统PATH中。如果未安装脚本会自动切换到Python方式备份。
### Q2: 备份文件太大怎么办?
**A:** 使用 `--compress` 选项压缩备份文件通常可以节省50-80%的空间。
### Q3: 恢复时提示表已存在错误?
**A:** 使用 `--drop-db` 选项先删除数据库再恢复,或者手动删除相关表。
### Q4: 如何定时自动备份?
**A:** 可以使用操作系统的定时任务功能如Windows的计划任务、Linux的cron
```bash
# Linux crontab示例每天凌晨2点备份
0 2 * * * cd /path/to/project && python backup_database.py --compress
```
### Q5: 备份文件可以恢复到其他数据库吗?
**A:** 可以,修改环境变量中的 `DB_NAME` 或直接编辑备份文件中的数据库名称。
## 示例场景
### 场景1: 日常备份
```bash
# 每天自动备份并压缩
python backup_database.py --compress
```
### 场景2: 迁移数据库
```bash
# 1. 备份源数据库
python backup_database.py --output migration_backup.sql
# 2. 修改配置指向目标数据库
# 3. 恢复备份到目标数据库
python restore_database.py migration_backup.sql --drop-db
```
### 场景3: 数据恢复
```bash
# 1. 查看可用备份
python backup_database.py --list
# 2. 恢复指定备份
python restore_database.py backups/backup_finyx_20241205_120000.sql
```
## 技术支持
如有问题,请检查:
1. 数据库连接配置是否正确
2. 数据库服务是否正常运行
3. 是否有足够的磁盘空间
4. 是否有数据库操作权限

View File

@ -0,0 +1,530 @@
# 模板字段关联查询说明
## 一、概述
本文档说明如何通过查询 `f_polic_file_config` 表获取每个模板关联的输入和输出字段。系统已重新建立了模板和字段的关联关系,不再依赖 `input_data``template_code` 字段。
## 二、表结构关系
### 2.1 相关表说明
1. **f_polic_file_config** - 文件模板配置表
- `id`: 文件配置ID主键
- `name`: 模板名称(如:"初步核实审批表"
- `tenant_id`: 租户ID固定值615873064429507639
- `state`: 状态0=未启用1=启用)
2. **f_polic_field** - 字段定义表
- `id`: 字段ID主键
- `name`: 字段名称(中文显示名)
- `filed_code`: 字段编码(注意:表中字段名拼写为 `filed_code`
- `field_type`: 字段类型1=输入字段2=输出字段)
- `tenant_id`: 租户ID
3. **f_polic_file_field** - 文件和字段关联表
- `file_id`: 文件配置ID关联 `f_polic_file_config.id`
- `filed_id`: 字段ID关联 `f_polic_field.id`
- `tenant_id`: 租户ID
- `state`: 状态0=未启用1=启用)
### 2.2 关联关系
```
f_polic_file_config (模板)
↓ (通过 file_id)
f_polic_file_field (关联表)
↓ (通过 filed_id)
f_polic_field (字段)
```
## 三、查询方式
### 3.1 根据模板名称查询字段
**场景**:已知模板名称,查询该模板关联的所有字段(包括输入和输出字段)
```sql
SELECT
fc.id AS template_id,
fc.name AS template_name,
f.id AS field_id,
f.name AS field_name,
f.filed_code AS field_code,
f.field_type,
CASE
WHEN f.field_type = 1 THEN '输入字段'
WHEN f.field_type = 2 THEN '输出字段'
ELSE '未知'
END AS field_type_name
FROM f_polic_file_config fc
INNER JOIN f_polic_file_field fff ON fc.id = fff.file_id
INNER JOIN f_polic_field f ON fff.filed_id = f.id
WHERE fc.tenant_id = 615873064429507639
AND fc.name = '初步核实审批表'
AND fc.state = 1
AND fff.state = 1
AND f.state = 1
ORDER BY f.field_type, f.name;
```
### 3.2 根据模板ID查询字段
**场景**已知模板ID查询该模板关联的所有字段
```sql
SELECT
f.id AS field_id,
f.name AS field_name,
f.filed_code AS field_code,
f.field_type,
CASE
WHEN f.field_type = 1 THEN '输入字段'
WHEN f.field_type = 2 THEN '输出字段'
ELSE '未知'
END AS field_type_name
FROM f_polic_file_field fff
INNER JOIN f_polic_field f ON fff.filed_id = f.id
WHERE fff.file_id = ? -- 替换为实际的模板ID
AND fff.tenant_id = 615873064429507639
AND fff.state = 1
AND f.state = 1
ORDER BY f.field_type, f.name;
```
### 3.3 分别查询输入字段和输出字段
**场景**:需要分别获取输入字段和输出字段列表
#### 查询输入字段field_type = 1
```sql
SELECT
f.id AS field_id,
f.name AS field_name,
f.filed_code AS field_code
FROM f_polic_file_config fc
INNER JOIN f_polic_file_field fff ON fc.id = fff.file_id
INNER JOIN f_polic_field f ON fff.filed_id = f.id
WHERE fc.tenant_id = 615873064429507639
AND fc.name = '初步核实审批表'
AND fc.state = 1
AND fff.state = 1
AND f.state = 1
AND f.field_type = 1 -- 输入字段
ORDER BY f.name;
```
#### 查询输出字段field_type = 2
```sql
SELECT
f.id AS field_id,
f.name AS field_name,
f.filed_code AS field_code
FROM f_polic_file_config fc
INNER JOIN f_polic_file_field fff ON fc.id = fff.file_id
INNER JOIN f_polic_field f ON fff.filed_id = f.id
WHERE fc.tenant_id = 615873064429507639
AND fc.name = '初步核实审批表'
AND fc.state = 1
AND fff.state = 1
AND f.state = 1
AND f.field_type = 2 -- 输出字段
ORDER BY f.name;
```
### 3.4 查询所有模板及其字段统计
**场景**:获取所有模板及其关联的字段数量统计
```sql
SELECT
fc.id AS template_id,
fc.name AS template_name,
COUNT(DISTINCT CASE WHEN f.field_type = 1 THEN f.id END) AS input_field_count,
COUNT(DISTINCT CASE WHEN f.field_type = 2 THEN f.id END) AS output_field_count,
COUNT(DISTINCT f.id) AS total_field_count
FROM f_polic_file_config fc
LEFT JOIN f_polic_file_field fff ON fc.id = fff.file_id AND fff.state = 1
LEFT JOIN f_polic_field f ON fff.filed_id = f.id AND f.state = 1
WHERE fc.tenant_id = 615873064429507639
AND fc.state = 1
GROUP BY fc.id, fc.name
ORDER BY fc.name;
```
### 3.5 查询特定模板的完整字段信息JSON格式
**场景**:前端需要获取模板的完整字段信息,包括输入和输出字段的详细信息
```sql
SELECT
fc.id AS template_id,
fc.name AS template_name,
JSON_OBJECT(
'input_fields', JSON_ARRAYAGG(
CASE
WHEN f.field_type = 1 THEN JSON_OBJECT(
'id', f.id,
'name', f.name,
'field_code', f.filed_code
)
END
),
'output_fields', JSON_ARRAYAGG(
CASE
WHEN f.field_type = 2 THEN JSON_OBJECT(
'id', f.id,
'name', f.name,
'field_code', f.filed_code
)
END
)
) AS fields_info
FROM f_polic_file_config fc
LEFT JOIN f_polic_file_field fff ON fc.id = fff.file_id AND fff.state = 1
LEFT JOIN f_polic_field f ON fff.filed_id = f.id AND f.state = 1
WHERE fc.tenant_id = 615873064429507639
AND fc.name = '初步核实审批表'
AND fc.state = 1
GROUP BY fc.id, fc.name;
```
## 四、Python代码示例
### 4.1 根据模板名称获取字段
```python
import pymysql
# 数据库配置
DB_CONFIG = {
'host': '152.136.177.240',
'port': 5012,
'user': 'finyx',
'password': '6QsGK6MpePZDE57Z',
'database': 'finyx',
'charset': 'utf8mb4'
}
TENANT_ID = 615873064429507639
def get_template_fields_by_name(template_name: str):
"""
根据模板名称获取关联的字段
Args:
template_name: 模板名称,如 '初步核实审批表'
Returns:
dict: 包含 input_fields 和 output_fields 的字典
"""
conn = pymysql.connect(**DB_CONFIG)
cursor = conn.cursor(pymysql.cursors.DictCursor)
try:
sql = """
SELECT
f.id,
f.name,
f.filed_code AS field_code,
f.field_type
FROM f_polic_file_config fc
INNER JOIN f_polic_file_field fff ON fc.id = fff.file_id
INNER JOIN f_polic_field f ON fff.filed_id = f.id
WHERE fc.tenant_id = %s
AND fc.name = %s
AND fc.state = 1
AND fff.state = 1
AND f.state = 1
ORDER BY f.field_type, f.name
"""
cursor.execute(sql, (TENANT_ID, template_name))
fields = cursor.fetchall()
# 分类为输入字段和输出字段
result = {
'template_name': template_name,
'input_fields': [],
'output_fields': []
}
for field in fields:
field_info = {
'id': field['id'],
'name': field['name'],
'field_code': field['field_code'],
'field_type': field['field_type']
}
if field['field_type'] == 1:
result['input_fields'].append(field_info)
elif field['field_type'] == 2:
result['output_fields'].append(field_info)
return result
finally:
cursor.close()
conn.close()
# 使用示例
if __name__ == '__main__':
result = get_template_fields_by_name('初步核实审批表')
print(f"模板: {result['template_name']}")
print(f"输入字段数量: {len(result['input_fields'])}")
print(f"输出字段数量: {len(result['output_fields'])}")
print("\n输入字段:")
for field in result['input_fields']:
print(f" - {field['name']} ({field['field_code']})")
print("\n输出字段:")
for field in result['output_fields']:
print(f" - {field['name']} ({field['field_code']})")
```
### 4.2 根据模板ID获取字段
```python
def get_template_fields_by_id(template_id: int):
"""
根据模板ID获取关联的字段
Args:
template_id: 模板ID
Returns:
dict: 包含 input_fields 和 output_fields 的字典
"""
conn = pymysql.connect(**DB_CONFIG)
cursor = conn.cursor(pymysql.cursors.DictCursor)
try:
# 先获取模板名称
sql_template = """
SELECT id, name
FROM f_polic_file_config
WHERE id = %s AND tenant_id = %s AND state = 1
"""
cursor.execute(sql_template, (template_id, TENANT_ID))
template = cursor.fetchone()
if not template:
return None
# 获取字段
sql_fields = """
SELECT
f.id,
f.name,
f.filed_code AS field_code,
f.field_type
FROM f_polic_file_field fff
INNER JOIN f_polic_field f ON fff.filed_id = f.id
WHERE fff.file_id = %s
AND fff.tenant_id = %s
AND fff.state = 1
AND f.state = 1
ORDER BY f.field_type, f.name
"""
cursor.execute(sql_fields, (template_id, TENANT_ID))
fields = cursor.fetchall()
result = {
'template_id': template['id'],
'template_name': template['name'],
'input_fields': [],
'output_fields': []
}
for field in fields:
field_info = {
'id': field['id'],
'name': field['name'],
'field_code': field['field_code'],
'field_type': field['field_type']
}
if field['field_type'] == 1:
result['input_fields'].append(field_info)
elif field['field_type'] == 2:
result['output_fields'].append(field_info)
return result
finally:
cursor.close()
conn.close()
```
### 4.3 获取所有模板及其字段统计
```python
def get_all_templates_with_field_stats():
"""
获取所有模板及其字段统计信息
Returns:
list: 模板列表,每个模板包含字段统计
"""
conn = pymysql.connect(**DB_CONFIG)
cursor = conn.cursor(pymysql.cursors.DictCursor)
try:
sql = """
SELECT
fc.id AS template_id,
fc.name AS template_name,
COUNT(DISTINCT CASE WHEN f.field_type = 1 THEN f.id END) AS input_field_count,
COUNT(DISTINCT CASE WHEN f.field_type = 2 THEN f.id END) AS output_field_count,
COUNT(DISTINCT f.id) AS total_field_count
FROM f_polic_file_config fc
LEFT JOIN f_polic_file_field fff ON fc.id = fff.file_id AND fff.state = 1
LEFT JOIN f_polic_field f ON fff.filed_id = f.id AND f.state = 1
WHERE fc.tenant_id = %s
AND fc.state = 1
GROUP BY fc.id, fc.name
ORDER BY fc.name
"""
cursor.execute(sql, (TENANT_ID,))
templates = cursor.fetchall()
return [
{
'template_id': t['template_id'],
'template_name': t['template_name'],
'input_field_count': t['input_field_count'] or 0,
'output_field_count': t['output_field_count'] or 0,
'total_field_count': t['total_field_count'] or 0
}
for t in templates
]
finally:
cursor.close()
conn.close()
# 使用示例
if __name__ == '__main__':
templates = get_all_templates_with_field_stats()
print("所有模板及其字段统计:")
for template in templates:
print(f"\n模板: {template['template_name']} (ID: {template['template_id']})")
print(f" 输入字段: {template['input_field_count']} 个")
print(f" 输出字段: {template['output_field_count']} 个")
print(f" 总字段数: {template['total_field_count']} 个")
```
## 五、常见查询场景
### 5.1 前端展示模板列表
**需求**:前端需要展示所有模板,并显示每个模板的字段数量
```sql
SELECT
fc.id,
fc.name,
COUNT(DISTINCT CASE WHEN f.field_type = 1 THEN f.id END) AS input_count,
COUNT(DISTINCT CASE WHEN f.field_type = 2 THEN f.id END) AS output_count
FROM f_polic_file_config fc
LEFT JOIN f_polic_file_field fff ON fc.id = fff.file_id AND fff.state = 1
LEFT JOIN f_polic_field f ON fff.filed_id = f.id AND f.state = 1
WHERE fc.tenant_id = 615873064429507639
AND fc.state = 1
GROUP BY fc.id, fc.name
ORDER BY fc.name;
```
### 5.2 验证模板字段完整性
**需求**:检查某个模板是否有关联字段
```sql
SELECT
fc.id,
fc.name,
CASE
WHEN COUNT(f.id) > 0 THEN '有字段关联'
ELSE '无字段关联'
END AS status,
COUNT(f.id) AS field_count
FROM f_polic_file_config fc
LEFT JOIN f_polic_file_field fff ON fc.id = fff.file_id AND fff.state = 1
LEFT JOIN f_polic_field f ON fff.filed_id = f.id AND f.state = 1
WHERE fc.tenant_id = 615873064429507639
AND fc.name = '初步核实审批表'
AND fc.state = 1
GROUP BY fc.id, fc.name;
```
### 5.3 查找使用特定字段的所有模板
**需求**:查找哪些模板使用了某个字段(如 `target_name`
```sql
SELECT
fc.id AS template_id,
fc.name AS template_name
FROM f_polic_file_config fc
INNER JOIN f_polic_file_field fff ON fc.id = fff.file_id
INNER JOIN f_polic_field f ON fff.filed_id = f.id
WHERE fc.tenant_id = 615873064429507639
AND f.tenant_id = 615873064429507639
AND f.filed_code = 'target_name'
AND fc.state = 1
AND fff.state = 1
AND f.state = 1
ORDER BY fc.name;
```
## 六、注意事项
1. **租户ID**所有查询都需要使用固定的租户ID`615873064429507639`
2. **状态过滤**:建议始终过滤 `state = 1` 的记录,确保只获取启用的模板和字段
3. **字段名拼写**:注意 `f_polic_field` 表中的字段编码字段名是 `filed_code`(不是 `field_code`),这是历史遗留问题
4. **字段类型**
- `field_type = 1`输入字段用于AI解析的原始数据
- `field_type = 2`输出字段AI解析后生成的结构化数据用于填充模板
5. **关联表状态**`f_polic_file_field` 表也有 `state` 字段,需要过滤 `fff.state = 1`
6. **性能优化**:如果经常查询,建议在以下字段上创建索引:
- `f_polic_file_config.tenant_id`
- `f_polic_file_config.name`
- `f_polic_file_field.file_id`
- `f_polic_file_field.filed_id`
- `f_polic_field.filed_code`
## 七、示例数据
### 7.1 初步核实审批表字段示例
**输入字段**2个
- `clue_info` - 线索信息
- `target_basic_info_clue` - 被核查人员工作基本情况线索
**输出字段**14个
- `target_name` - 被核查人姓名
- `target_organization_and_position` - 被核查人员单位及职务
- `target_organization` - 被核查人员单位
- `target_position` - 被核查人员职务
- `target_gender` - 被核查人员性别
- `target_date_of_birth` - 被核查人员出生年月
- `target_age` - 被核查人员年龄
- `target_education_level` - 被核查人员文化程度
- `target_political_status` - 被核查人员政治面貌
- `target_professional_rank` - 被核查人员职级
- `clue_source` - 线索来源
- `target_issue_description` - 主要问题线索
- `department_opinion` - 初步核实审批表承办部门意见
- `filler_name` - 初步核实审批表填表人
## 八、总结
通过 `f_polic_file_field` 关联表,可以方便地查询每个模板关联的输入和输出字段。这种方式比之前依赖 `input_data``template_code` 字段更加规范、可靠,也更容易维护和扩展。
其他研发人员可以根据上述SQL示例和Python代码在自己的模块中实现模板字段的查询功能。

View File

@ -0,0 +1,180 @@
# 模板树状结构更新说明
## 概述
根据 `template_finish` 目录结构,更新数据库 `f_polic_file_config` 表中的 `parent_id` 字段,建立树状层级结构。
## 目录结构示例
```
template_finish/
└── 2-初核模版/ (一级)
├── 1.初核请示/ (二级)
│ ├── 1.请示报告卡XXX.docx
│ ├── 2.初步核实审批表XXX.docx
│ └── 3.附件初核方案(XXX).docx
├── 2.谈话审批/ (二级)
│ ├── 谈话通知书/ (三级)
│ │ ├── 谈话通知书第一联.docx
│ │ ├── 谈话通知书第二联.docx
│ │ └── 谈话通知书第三联.docx
│ ├── 走读式谈话审批/ (三级)
│ │ ├── 1.请示报告卡(初核谈话).docx
│ │ ├── 2谈话审批表.docx
│ │ └── ...
│ └── 走读式谈话流程/ (三级)
│ ├── 1.谈话笔录.docx
│ └── ...
└── 3.初核结论/ (二级)
├── 8-1请示报告卡初核报告结论 .docx
└── 8.XXX初核情况报告.docx
```
## 脚本说明
### 1. analyze_and_update_template_tree.py
**功能:** 分析目录结构和数据库数据,生成 SQL 更新脚本
**使用方法:**
```bash
python analyze_and_update_template_tree.py
```
**输出:**
- 分析报告(控制台输出)
- `update_template_tree.sql` - SQL 更新脚本
**特点:**
- 只生成 SQL 脚本,不直接修改数据库
- 可以手动检查 SQL 脚本后再执行
### 2. update_template_tree.py
**功能:** 分析并直接更新数据库(带预览和确认)
**使用方法:**
```bash
python update_template_tree.py
```
**特点:**
- 交互式操作,先预览再确认
- 支持模拟模式dry-run
- 自动按层级顺序更新
- 更安全的更新流程
## 更新逻辑
1. **目录节点**:根据目录名称匹配数据库记录,如果不存在则创建
2. **文件节点**:优先通过 `template_code` 匹配,其次通过文件名匹配
3. **层级关系**:按照目录结构的层级关系设置 `parent_id`
- 一级目录:`parent_id = NULL`
- 二级目录:`parent_id = 一级目录的ID`
- 三级目录:`parent_id = 二级目录的ID`
- 文件:`parent_id = 所在目录的ID`
## 执行步骤
### 方法一:使用 SQL 脚本(推荐用于生产环境)
1. 运行分析脚本:
```bash
python analyze_and_update_template_tree.py
```
2. 检查生成的 SQL 脚本:
```bash
# 查看 update_template_tree.sql
```
3. 备份数据库(重要!)
4. 执行 SQL 脚本:
```sql
-- 在 MySQL 客户端中执行
source update_template_tree.sql;
```
### 方法二:使用 Python 脚本(推荐用于测试环境)
1. 运行更新脚本:
```bash
python update_template_tree.py
```
2. 查看预览信息
3. 输入 `yes` 确认执行
4. 再次确认执行实际更新
## 注意事项
1. **备份数据库**:执行更新前务必备份数据库
2. **检查匹配**:确保目录和文件名与数据库中的记录能够正确匹配
3. **层级顺序**:更新会按照层级顺序执行,确保父节点先于子节点创建/更新
4. **重复执行**:脚本支持重复执行,已正确设置 `parent_id` 的记录会被跳过
## 数据库表结构
`f_polic_file_config` 表的关键字段:
- `id`: 主键
- `tenant_id`: 租户ID固定值615873064429507639
- `parent_id`: 父节点IDNULL 表示根节点)
- `name`: 名称
- `template_code`: 模板编码(文件节点使用)
- `input_data`: JSON格式的配置数据
- `file_path`: MinIO文件路径
## 问题排查
### 问题1某些文件无法匹配
**原因:** 文件名或 `template_code` 不匹配
**解决:** 检查 `DOCUMENT_TYPE_MAPPING` 字典,确保文件名映射正确
### 问题2目录节点重复创建
**原因:** 数据库中已存在同名目录节点,但脚本未正确匹配
**解决:** 检查数据库中的记录,确保名称完全一致(包括空格和标点)
### 问题3parent_id 更新失败
**原因:** 父节点ID不存在或层级关系错误
**解决:** 检查生成的 SQL 脚本确认父节点ID是否正确
## 验证更新结果
执行更新后,可以使用以下 SQL 查询验证:
```sql
-- 查看树状结构
SELECT
id,
name,
parent_id,
template_code,
(SELECT name FROM f_polic_file_config p2 WHERE p2.id = p1.parent_id) as parent_name
FROM f_polic_file_config p1
WHERE tenant_id = 615873064429507639
ORDER BY parent_id, name;
-- 查看缺少 parent_id 的记录(应该只有根节点)
SELECT id, name, parent_id
FROM f_polic_file_config
WHERE tenant_id = 615873064429507639
AND parent_id IS NULL
AND name NOT LIKE '%-%'; -- 排除一级目录
```
## 联系信息
如有问题,请检查:
1. 数据库连接配置是否正确
2. 目录结构是否与预期一致
3. 数据库中的记录是否完整

View File

@ -0,0 +1,179 @@
# AI服务错误分析报告
## 问题描述
在API响应过程中模型返回了错误消息"抱歉,我似乎遇到了困难。在尝试生成响应时出现了一些错误。如果您能重新提交请求,我将尽力提供更好的帮助。"
## 日志分析
### 1. Token使用情况
- **max_tokens**: 12000
- **completion_tokens**: 597
- **total_tokens**: 3947
- **结论**: ❌ **不是max_tokens参数的问题**。实际使用的token数量远小于限制不存在token截断问题。
### 2. 响应内容分析
从日志中可以看到模型返回的JSON存在严重格式错误
#### 错误1: 字段名错误和转义字符问题
```json
"_source\\\": \\\"\\\"
```
- 应该是: `"clue_source": ""`
- 问题: 字段名错误(`_source` 而不是 `clue_source`),且存在转义字符问题
#### 错误2: 字段名格式错误
```json
\\\" target_position \\\":
```
- 应该是: `"target_position":`
- 问题: 字段名前后有转义字符和空格
#### 错误3: 值格式错误
```json
\"total_manager,
```
- 应该是: `"总经理"`
- 问题: 值不完整,且格式错误
#### 错误4: 字段名拼写错误
```json
\" target_organisation \": \"\n}
```
- 应该是: `"target_organization": ""`
- 问题: 字段名拼写错误(`organisation` 而不是 `organization`),且格式不完整
#### 错误5: 关键字段缺失
- `target_gender`: 应该是 `"男"`,但返回为空字符串
- `target_professional_rank`: 应该是 `"正处级"`,但返回为空字符串
### 3. 思考模式影响
从响应内容可以看到:
- 模型在生成JSON之前有一段思考过程`</think>`标签包裹)
- 思考过程可能消耗了部分token但更重要的是**思考模式可能导致模型在生成JSON时出现不稳定**
## 根本原因分析
### 主要原因(按可能性排序)
1. **思考模式enable_thinking导致生成不稳定** ⭐⭐⭐⭐⭐
- DeepSeek-R1模型在开启思考模式时可能会在生成过程中遇到内部错误
- 思考过程可能影响后续JSON生成的准确性
- 建议:考虑关闭思考模式或调整相关参数
2. **提示词过于复杂** ⭐⭐⭐⭐
- 提示词包含大量详细要求和示例
- 模型在处理复杂提示词时可能出现格式错误
- 建议简化提示词明确JSON格式要求
3. **模型内部错误** ⭐⭐⭐
- 模型在生成过程中遇到内部错误导致JSON生成中断
- 最终输出了错误消息而非完整JSON
- 建议:增加重试机制和错误处理
4. **JSON修复机制不够完善** ⭐⭐
- 虽然代码中有JSON修复逻辑但对于这种严重格式错误可能无法完全修复
- 建议增强JSON修复机制特别是处理转义字符和字段名错误
## 解决方案建议
### 方案1: 调整思考模式参数(推荐)
```python
# 在 services/ai_service.py 中
payload = {
# ... 其他参数 ...
"enable_thinking": False, # 暂时关闭思考模式
# 或者
"enable_thinking": True,
"thinking_config": {
"max_thinking_tokens": 1000, # 限制思考过程的token数量
}
}
```
### 方案2: 优化提示词
简化system prompt明确JSON格式要求
```python
system_content = """你是一个专业的数据提取助手。请严格按照JSON格式返回结果。
重要要求:
1. 只返回JSON对象不要包含任何其他文字说明
2. 字段名必须严格按照示例格式
3. 如果信息不存在,使用空字符串""
JSON格式示例
{
"target_name": "张三",
"target_gender": "男",
"target_professional_rank": "正处级",
"clue_source": "群众举报"
}
"""
```
### 方案3: 增强JSON修复机制
`_fix_json_string` 方法中增加对以下错误的处理:
- 修复 `_source` -> `clue_source` 的字段名映射
- 修复 `target_organisation` -> `target_organization` 的拼写错误
- 处理转义字符问题(`\\\"` -> `"`
- 处理字段名前后的空格和转义字符
### 方案4: 增加重试机制
代码中已有重试机制但可以针对JSON解析失败的情况增加专门的重试逻辑
```python
# 如果JSON解析失败且错误消息包含"抱歉",则重试
if "抱歉" in content or "遇到困难" in content:
print("[AI服务] 检测到模型错误消息,将重试...")
# 重试逻辑
```
### 方案5: 降低temperature参数
当前temperature为0.2,已经较低。可以进一步降低以提高确定性:
```python
"temperature": 0.1, # 进一步降低,提高确定性
```
## 立即行动建议
1. **短期(立即)**
- 暂时关闭思考模式(`enable_thinking: False`)进行测试
- 如果问题解决,说明是思考模式导致的问题
2. **中期1-2天**
- 优化提示词,简化要求
- 增强JSON修复机制处理常见错误
3. **长期1周内**
- 如果必须使用思考模式,考虑调整相关参数
- 增加更完善的错误处理和重试机制
## 测试建议
1. 使用相同的输入数据,分别测试:
- `enable_thinking: True` vs `False`
- 不同的 `temperature`0.1, 0.2, 0.3
- 不同的 `max_tokens`8000, 12000, 16000
2. 记录每次测试的结果,找出最佳参数组合
3. 如果问题持续存在,考虑联系模型服务提供商(华为)寻求支持
## 总结
**核心结论**:问题**不是max_tokens参数导致的**,而是**思考模式enable_thinking可能导致模型生成不稳定**从而产生格式错误的JSON。
**建议优先级**
1. 🔴 **高优先级**:暂时关闭思考模式进行测试
2. 🟡 **中优先级**优化提示词增强JSON修复机制
3. 🟢 **低优先级**:调整其他参数,联系服务提供商