Compare commits
32 Commits
master
...
jingtai_lo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e3f4a394c1 | ||
|
|
4084cb1819 | ||
|
|
b0360cc15b | ||
|
|
6c31137cf4 | ||
|
|
4897c96b05 | ||
|
|
3a3b38cd78 | ||
|
|
2a5952f3f5 | ||
|
|
a320f55da0 | ||
|
|
ebc1154beb | ||
|
|
0563ff5346 | ||
|
|
e38ba42669 | ||
|
|
11be119ffc | ||
|
|
cd27bb4bd0 | ||
|
|
6871c2e803 | ||
|
|
24fdfdea4c | ||
|
|
563d97184b | ||
|
|
9bf1dd1210 | ||
|
|
315301fc0b | ||
|
|
8bebc13efe | ||
|
|
f1b5c52500 | ||
|
|
7c30e59328 | ||
|
|
eaa384cf7e | ||
|
|
b8d89c28ec | ||
|
|
e1d8d27dc4 | ||
|
|
e31cd0b764 | ||
|
|
d8fa4c3d7e | ||
|
|
c7a7780e71 | ||
|
|
14ff607b52 | ||
|
|
8461725a13 | ||
|
|
684cb0141a | ||
|
|
f0cb4a7ba0 | ||
|
|
7d50b160c2 |
68
.env
68
.env
@ -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
|
||||||
|
|||||||
68
.env.example
68
.env.example
@ -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
55
.gitignore
vendored
Normal 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
|
||||||
|
|
||||||
69
README.md
69
README.md
@ -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.
582
analyze_and_fix_field_code_issues.py
Normal file
582
analyze_and_fix_field_code_issues.py
Normal 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()
|
||||||
|
|
||||||
555
analyze_and_update_template_tree.py
Normal file
555
analyze_and_update_template_tree.py
Normal 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
250
app.py
@ -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
314
backup_database.py
Normal 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()
|
||||||
|
|
||||||
BIN
backups/backup_finyx_20251209_170604.zip
Normal file
BIN
backups/backup_finyx_20251209_170604.zip
Normal file
Binary file not shown.
551
check_and_fix_file_field_relations.py
Normal file
551
check_and_fix_file_field_relations.py
Normal 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
105
check_existing_data.py
Normal 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
131
check_remaining_fields.py
Normal 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()
|
||||||
|
|
||||||
@ -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": [
|
||||||
"仔细分析输入文本,准确提取每个字段的值",
|
"⚠️ 逐字逐句仔细分析输入文本,不要遗漏任何信息。请特别关注性别、年龄、职务、单位、文化程度等字段",
|
||||||
"如果某个字段在输入文本中找不到对应信息,该字段值设为空字符串\"\"",
|
"对于每个字段,请从多个角度思考:直接提及、同义词、隐含信息、可推断信息。例如:性别可能以\"男\"、\"女\"、\"男性\"、\"女性\"、\"先生\"、\"女士\"等形式出现",
|
||||||
"日期格式统一为YYYYMM(如:198005表示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
231
enable_all_fields.py
Normal 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()
|
||||||
|
|
||||||
191
fix_missing_education_level_field.py
Normal file
191
fix_missing_education_level_field.py
Normal 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)
|
||||||
|
|
||||||
260
fix_missing_field_relations.py
Normal file
260
fix_missing_field_relations.py
Normal 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()
|
||||||
|
|
||||||
201
fix_only_chinese_field_codes.py
Normal file
201
fix_only_chinese_field_codes.py
Normal 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)
|
||||||
|
|
||||||
191
fix_remaining_chinese_fields.py
Normal file
191
fix_remaining_chinese_fields.py
Normal 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)
|
||||||
|
|
||||||
272
fix_report_card_interview_input_data.py
Normal file
272
fix_report_card_interview_input_data.py
Normal 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
124
generate_download_urls.py
Normal 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}")
|
||||||
|
|
||||||
|
# 生成预签名URL(7天有效期)
|
||||||
|
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()
|
||||||
|
|
||||||
219
generate_template_file_id_report.py
Normal file
219
generate_template_file_id_report.py
Normal 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-8(Windows兼容)
|
||||||
|
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
64
get_available_file_ids.py
Normal 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()
|
||||||
|
|
||||||
478
improved_match_and_update.py
Normal file
478
improved_match_and_update.py
Normal 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()
|
||||||
|
|
||||||
544
init_template_tree_from_directory.py
Normal file
544
init_template_tree_from_directory.py
Normal 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()
|
||||||
|
|
||||||
318
query_template_fields_example.py
Normal file
318
query_template_fields_example.py
Normal 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()
|
||||||
|
|
||||||
536
rebuild_template_field_relations.py
Normal file
536
rebuild_template_field_relations.py
Normal 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)
|
||||||
|
|
||||||
@ -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
340
restore_database.py
Normal 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()
|
||||||
|
|
||||||
122
rollback_incorrect_updates.py
Normal file
122
rollback_incorrect_updates.py
Normal 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()
|
||||||
|
|
||||||
Binary file not shown.
Binary file not shown.
181
services/ai_logger.py
Normal file
181
services/ai_logger.py
Normal 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
@ -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
|
||||||
|
|
||||||
|
|||||||
@ -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
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
552
sync_template_fields_from_excel.py
Normal file
552
sync_template_fields_from_excel.py
Normal file
@ -0,0 +1,552 @@
|
|||||||
|
"""
|
||||||
|
根据Excel数据设计文档同步更新模板的input_data、template_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()
|
||||||
|
|
||||||
@ -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"
|
||||||
|
|||||||
41
test_document_generation_by_file_id.py
Normal file
41
test_document_generation_by_file_id.py
Normal 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)
|
||||||
|
|
||||||
@ -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:
|
||||||
|
|||||||
95
test_scripts/test_ai_logger.py
Normal file
95
test_scripts/test_ai_logger.py
Normal 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
467
update_all_templates.py
Normal 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-8(Windows兼容)
|
||||||
|
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
618
update_template_tree.py
Normal 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
159
update_template_tree.sql
Normal 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
148
verify_field_code_fix.py
Normal 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']}")
|
||||||
|
|
||||||
345
verify_template_fields_sync.py
Normal file
345
verify_template_fields_sync.py
Normal file
@ -0,0 +1,345 @@
|
|||||||
|
"""
|
||||||
|
验证模板字段同步结果
|
||||||
|
检查 input_data、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
|
||||||
|
|
||||||
|
|
||||||
|
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()
|
||||||
|
|
||||||
531
verify_template_file_id_relations.py
Normal file
531
verify_template_file_id_relations.py
Normal 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-8(Windows兼容)
|
||||||
|
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
169
verify_tree_structure.py
Normal 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
24
备份数据库.bat
Normal 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
41
恢复数据库.bat
Normal 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
|
||||||
|
|
||||||
213
技术文档/AI对话日志使用说明.md
Normal file
213
技术文档/AI对话日志使用说明.md
Normal 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/` - 日志文件目录
|
||||||
|
|
||||||
221
技术文档/README_初始化模板树状结构.md
Normal file
221
技术文档/README_初始化模板树状结构.md
Normal 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 配置是否正确
|
||||||
|
|
||||||
293
技术文档/README_模板树状结构更新.md
Normal file
293
技术文档/README_模板树状结构更新.md
Normal 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 是否正确设置
|
||||||
|
|
||||||
179
技术文档/修改说明.md
Normal file
179
技术文档/修改说明.md
Normal 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. 监控和日志:
|
||||||
|
- 记录修复前后的错误率
|
||||||
|
- 分析仍然存在的错误模式
|
||||||
|
- 持续优化修复机制
|
||||||
|
|
||||||
@ -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:根据最新数据库信息更新
|
||||||
|
|
||||||
|
|||||||
152
技术文档/同步结果总结.md
Normal file
152
技术文档/同步结果总结.md
Normal 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 文件节点有完整关联)
|
||||||
|
|
||||||
182
技术文档/字段编码修复总结.md
Normal file
182
技术文档/字段编码修复总结.md
Normal 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已更新为英文
|
||||||
|
- 重复字段已合并
|
||||||
|
- 关联表已同步更新
|
||||||
|
- 数据一致性已确保
|
||||||
|
|
||||||
147
技术文档/性别和年龄字段缺失问题深度修复.md
Normal file
147
技术文档/性别和年龄字段缺失问题深度修复.md
Normal 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),即使模型没有正确提取字段,也能从原始输入中补充。这提供了多层保障,确保关键字段不会为空。
|
||||||
|
|
||||||
185
技术文档/性别字段缺失问题分析与修复.md
Normal file
185
技术文档/性别字段缺失问题分析与修复.md
Normal file
@ -0,0 +1,185 @@
|
|||||||
|
# 性别字段缺失问题分析与修复
|
||||||
|
|
||||||
|
## 问题描述
|
||||||
|
|
||||||
|
模型在思考过程中正确识别了性别信息("性别方面,无论是在哪里,都明确指出是男性或者男,所以统一转换为'男'即可"),但在最终的JSON输出中,`target_gender`字段却是空字符串。同时,`target_professional_rank`和`clue_source`字段也存在类似问题。
|
||||||
|
|
||||||
|
## 问题分析
|
||||||
|
|
||||||
|
### 1. 根本原因
|
||||||
|
|
||||||
|
**问题1:System 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和增强后处理机制,应该能够解决性别字段缺失的问题。如果问题仍然存在,可能需要进一步调整模型参数或联系服务提供商。
|
||||||
|
|
||||||
216
技术文档/数据库备份恢复说明.md
Normal file
216
技术文档/数据库备份恢复说明.md
Normal 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. 是否有数据库操作权限
|
||||||
|
|
||||||
530
技术文档/模板字段关联查询说明.md
Normal file
530
技术文档/模板字段关联查询说明.md
Normal 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代码,在自己的模块中实现模板字段的查询功能。
|
||||||
|
|
||||||
180
技术文档/模板树状结构更新说明.md
Normal file
180
技术文档/模板树状结构更新说明.md
Normal 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`: 父节点ID(NULL 表示根节点)
|
||||||
|
- `name`: 名称
|
||||||
|
- `template_code`: 模板编码(文件节点使用)
|
||||||
|
- `input_data`: JSON格式的配置数据
|
||||||
|
- `file_path`: MinIO文件路径
|
||||||
|
|
||||||
|
## 问题排查
|
||||||
|
|
||||||
|
### 问题1:某些文件无法匹配
|
||||||
|
|
||||||
|
**原因:** 文件名或 `template_code` 不匹配
|
||||||
|
|
||||||
|
**解决:** 检查 `DOCUMENT_TYPE_MAPPING` 字典,确保文件名映射正确
|
||||||
|
|
||||||
|
### 问题2:目录节点重复创建
|
||||||
|
|
||||||
|
**原因:** 数据库中已存在同名目录节点,但脚本未正确匹配
|
||||||
|
|
||||||
|
**解决:** 检查数据库中的记录,确保名称完全一致(包括空格和标点)
|
||||||
|
|
||||||
|
### 问题3:parent_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. 数据库中的记录是否完整
|
||||||
|
|
||||||
179
技术文档/错误分析报告.md
Normal file
179
技术文档/错误分析报告.md
Normal 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. 🟢 **低优先级**:调整其他参数,联系服务提供商
|
||||||
|
|
||||||
Loading…
x
Reference in New Issue
Block a user