更新.env文件以反映新的MinIO和AI服务配置,同时删除无用的文档和模板文件,增强了系统的灵活性和可维护性。添加了数据库备份和恢复功能,支持通过API管理租户ID和字段关联,优化了前端模板字段管理界面。
This commit is contained in:
parent
1d0f7a5bfe
commit
665612d2bf
324
.cursorrules
Normal file
324
.cursorrules
Normal file
@ -0,0 +1,324 @@
|
|||||||
|
# 智慧监督AI文书写作服务 - AI开发手册
|
||||||
|
|
||||||
|
## 项目背景
|
||||||
|
|
||||||
|
本项目是一个基于大模型的智能文书生成服务,主要功能包括:
|
||||||
|
- 从非结构化文本中提取结构化字段数据(使用AI大模型)
|
||||||
|
- 根据字段数据填充Word模板生成正式文书
|
||||||
|
- 支持多种业务类型的文书模板管理
|
||||||
|
- 文档存储和下载管理(MinIO对象存储)
|
||||||
|
|
||||||
|
核心业务流程:
|
||||||
|
1. 接收输入的非结构化文本数据
|
||||||
|
2. 使用AI大模型提取结构化字段
|
||||||
|
3. 根据字段数据填充Word模板
|
||||||
|
4. 生成文档并上传到MinIO
|
||||||
|
5. 返回文档下载链接
|
||||||
|
|
||||||
|
## 技术栈与编码标准
|
||||||
|
|
||||||
|
### 核心技术栈
|
||||||
|
- **Python版本**: Python 3.8+
|
||||||
|
- **Web框架**: Flask 3.0.0
|
||||||
|
- **数据库**: MySQL (使用PyMySQL 1.1.2)
|
||||||
|
- **文档处理**: python-docx 1.1.0
|
||||||
|
- **对象存储**: MinIO 7.2.3
|
||||||
|
- **AI服务**:
|
||||||
|
- 华为大模型 (DeepSeek-R1-Distill-Llama-70B)
|
||||||
|
- 硅基流动 (DeepSeek-V3.2-Exp)
|
||||||
|
- **其他依赖**: flask-cors, flasgger, python-dotenv, requests, openpyxl, json-repair
|
||||||
|
|
||||||
|
### 编码规范
|
||||||
|
- **代码风格**: 遵循PEP 8规范
|
||||||
|
- **命名规范**:
|
||||||
|
- 类名使用大驼峰命名(PascalCase):`AIService`, `DocumentService`
|
||||||
|
- 函数和变量使用小写下划线命名(snake_case):`get_connection`, `field_data`
|
||||||
|
- 常量使用全大写下划线命名:`AI_PROVIDER`, `DB_HOST`
|
||||||
|
- **注释要求**:
|
||||||
|
- 所有类和方法必须有docstring(使用三引号)
|
||||||
|
- 复杂逻辑必须添加行内注释
|
||||||
|
- 使用中文注释(项目统一使用中文)
|
||||||
|
- **类型提示**: 建议使用类型提示(typing模块),提高代码可读性
|
||||||
|
- **异常处理**: 必须使用try-except捕获异常,并提供有意义的错误信息
|
||||||
|
|
||||||
|
### 文件编码
|
||||||
|
- 所有Python文件使用UTF-8编码
|
||||||
|
- 文件开头不需要BOM标记
|
||||||
|
|
||||||
|
## 项目文件结构
|
||||||
|
|
||||||
|
```
|
||||||
|
.
|
||||||
|
├── app.py # Flask主应用,定义所有API路由
|
||||||
|
├── requirements.txt # Python依赖列表
|
||||||
|
├── .env.example # 环境变量配置示例
|
||||||
|
├── .cursorrules # AI开发手册(本文件)
|
||||||
|
├── services/ # 服务层(业务逻辑)
|
||||||
|
│ ├── __init__.py
|
||||||
|
│ ├── ai_service.py # AI服务:封装大模型调用逻辑
|
||||||
|
│ ├── document_service.py # 文档服务:Word模板填充、MinIO上传
|
||||||
|
│ ├── field_service.py # 字段服务:数据库字段配置查询
|
||||||
|
│ └── ai_logger.py # AI日志记录器
|
||||||
|
├── utils/ # 工具类
|
||||||
|
│ ├── __init__.py
|
||||||
|
│ └── response.py # 统一API响应格式工具
|
||||||
|
├── config/ # 配置文件
|
||||||
|
│ ├── prompt_config.json # AI提示词配置
|
||||||
|
│ └── field_defaults.json # 字段默认值配置
|
||||||
|
├── static/ # 静态文件
|
||||||
|
│ ├── index.html # 测试页面
|
||||||
|
│ └── template_field_manager.html # 模板字段管理页面
|
||||||
|
├── template/ # Word模板文件目录
|
||||||
|
├── template_finish/ # 已完成的模板文件
|
||||||
|
├── test_scripts/ # 测试脚本
|
||||||
|
└── 技术文档/ # 技术文档目录
|
||||||
|
```
|
||||||
|
|
||||||
|
## 架构约束与最佳实践
|
||||||
|
|
||||||
|
### 1. 分层架构
|
||||||
|
项目采用分层架构,严格遵循以下层次:
|
||||||
|
|
||||||
|
- **路由层** (`app.py`): 只负责接收HTTP请求、参数验证、调用服务层、返回响应
|
||||||
|
- **服务层** (`services/`): 包含所有业务逻辑,服务类之间可以相互调用
|
||||||
|
- **工具层** (`utils/`): 提供通用工具函数,不包含业务逻辑
|
||||||
|
- **数据层**: 数据库操作封装在服务层中,不单独抽象
|
||||||
|
|
||||||
|
**重要原则**:
|
||||||
|
- 路由层不包含业务逻辑,只做参数验证和响应格式化
|
||||||
|
- 服务层方法应该是可测试的,不依赖Flask的request对象
|
||||||
|
- 数据库连接在使用后必须关闭(使用try-finally确保)
|
||||||
|
|
||||||
|
### 2. 服务层设计规范
|
||||||
|
|
||||||
|
#### AI服务 (`services/ai_service.py`)
|
||||||
|
- 负责所有AI大模型调用
|
||||||
|
- 支持多种AI服务提供商(华为、硅基流动),通过环境变量切换
|
||||||
|
- 必须处理JSON解析失败的情况,提供多种修复机制
|
||||||
|
- 字段名规范化:将AI返回的各种字段名格式映射到正确的字段编码
|
||||||
|
- 日期格式规范化:统一转换为中文格式(YYYY年MM月 或 YYYY年MM月DD日)
|
||||||
|
- 后处理:从已有信息推断缺失字段(如从出生年月计算年龄)
|
||||||
|
|
||||||
|
#### 文档服务 (`services/document_service.py`)
|
||||||
|
- 负责Word模板下载、填充、上传
|
||||||
|
- 占位符格式:`{{field_code}}`(双大括号)
|
||||||
|
- 必须处理表格中的占位符(使用索引访问,避免迭代器bug)
|
||||||
|
- 提供XML备用方案处理特殊表格结构
|
||||||
|
- 文档名称生成:从原始文件名提取基础名称,添加被核查人姓名后缀
|
||||||
|
- MinIO路径格式:`/{tenant_id}/{timestamp}/{file_name}`
|
||||||
|
|
||||||
|
#### 字段服务 (`services/field_service.py`)
|
||||||
|
- 负责从数据库查询字段配置
|
||||||
|
- 构建AI提示词:根据输入数据和输出字段配置生成提示词
|
||||||
|
- 支持从配置文件加载提示词模板和字段默认值
|
||||||
|
|
||||||
|
### 3. 数据库设计规范
|
||||||
|
|
||||||
|
#### 主要数据表
|
||||||
|
- `f_polic_field`: 字段配置表
|
||||||
|
- `id`: 主键
|
||||||
|
- `name`: 字段名称
|
||||||
|
- `filed_code`: 字段编码(注意:数据库字段名是filed_code,不是field_code)
|
||||||
|
- `field_type`: 字段类型(1=输入字段,2=输出字段)
|
||||||
|
- `state`: 状态(1=启用,0=禁用)
|
||||||
|
|
||||||
|
- `f_polic_file_config`: 文件配置表(模板配置)
|
||||||
|
- `id`: 主键(作为fileId使用)
|
||||||
|
- `name`: 文件名称
|
||||||
|
- `file_path`: MinIO中的文件路径
|
||||||
|
- `input_data`: JSON格式的输入数据配置
|
||||||
|
- `state`: 状态(1=启用,0=禁用)
|
||||||
|
|
||||||
|
- `f_polic_file_field`: 文件字段关联表
|
||||||
|
- `file_id`: 文件配置ID
|
||||||
|
- `filed_id`: 字段ID
|
||||||
|
- `state`: 状态(1=启用,0=禁用)
|
||||||
|
|
||||||
|
#### 数据库操作规范
|
||||||
|
- 所有数据库操作必须使用参数化查询,防止SQL注入
|
||||||
|
- 使用`pymysql.cursors.DictCursor`获取字典格式结果
|
||||||
|
- 数据库连接必须在使用后关闭(try-finally模式)
|
||||||
|
- 事务操作必须正确处理回滚
|
||||||
|
|
||||||
|
### 4. API设计规范
|
||||||
|
|
||||||
|
#### 统一响应格式
|
||||||
|
所有API必须使用`utils/response.py`中的工具函数:
|
||||||
|
|
||||||
|
**成功响应**:
|
||||||
|
```python
|
||||||
|
return success_response(data={'key': 'value'}, msg="ok")
|
||||||
|
```
|
||||||
|
|
||||||
|
**错误响应**:
|
||||||
|
```python
|
||||||
|
return error_response(code=400, error_msg="错误信息")
|
||||||
|
```
|
||||||
|
|
||||||
|
**响应结构**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 0, // 0表示成功,其他值表示错误码
|
||||||
|
"data": {}, // 响应数据
|
||||||
|
"msg": "ok", // 响应消息
|
||||||
|
"timestamp": "1234567890", // 时间戳(毫秒)
|
||||||
|
"errorMsg": "", // 错误信息(成功时为空)
|
||||||
|
"isSuccess": true // 是否成功
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### API路由规范
|
||||||
|
- 使用`@app.route`装饰器定义路由
|
||||||
|
- 支持Swagger文档(使用flasgger)
|
||||||
|
- 路由路径使用小写字母和连字符:`/api/ai/extract`
|
||||||
|
- 保留旧路径以兼容:`/ai/extract` 和 `/api/ai/extract` 同时支持
|
||||||
|
|
||||||
|
#### 错误码规范
|
||||||
|
- `0`: 成功
|
||||||
|
- `400`: 请求参数错误
|
||||||
|
- `500`: 服务器内部错误
|
||||||
|
- `1001`: 模板不存在
|
||||||
|
- `2001`: AI解析超时
|
||||||
|
- `2002`: AI解析失败
|
||||||
|
- `3001`: 文件生成失败
|
||||||
|
- `3002`: 文件保存失败
|
||||||
|
|
||||||
|
### 5. 环境变量配置
|
||||||
|
|
||||||
|
所有配置通过环境变量管理,使用`.env`文件(不要提交到版本控制):
|
||||||
|
|
||||||
|
**必需配置**:
|
||||||
|
- `DB_HOST`: 数据库主机
|
||||||
|
- `DB_PORT`: 数据库端口
|
||||||
|
- `DB_USER`: 数据库用户名
|
||||||
|
- `DB_PASSWORD`: 数据库密码
|
||||||
|
- `DB_NAME`: 数据库名称
|
||||||
|
- `MINIO_ENDPOINT`: MinIO服务地址
|
||||||
|
- `MINIO_ACCESS_KEY`: MinIO访问密钥
|
||||||
|
- `MINIO_SECRET_KEY`: MinIO密钥
|
||||||
|
- `MINIO_BUCKET`: MinIO存储桶名称
|
||||||
|
- `MINIO_SECURE`: 是否使用HTTPS(true/false)
|
||||||
|
|
||||||
|
**AI服务配置**:
|
||||||
|
- `AI_PROVIDER`: AI服务提供商('huawei' 或 'siliconflow')
|
||||||
|
- `HUAWEI_API_ENDPOINT`: 华为API地址
|
||||||
|
- `HUAWEI_API_KEY`: 华为API密钥
|
||||||
|
- `HUAWEI_MODEL`: 华为模型名称
|
||||||
|
- `HUAWEI_API_TIMEOUT`: 超时时间(秒)
|
||||||
|
- `HUAWEI_API_MAX_TOKENS`: 最大token数
|
||||||
|
- `SILICONFLOW_URL`: 硅基流动API地址
|
||||||
|
- `SILICONFLOW_API_KEY`: 硅基流动API密钥
|
||||||
|
- `SILICONFLOW_MODEL`: 硅基流动模型名称
|
||||||
|
- `SILICONFLOW_API_TIMEOUT`: 超时时间(秒)
|
||||||
|
- `SILICONFLOW_API_MAX_TOKENS`: 最大token数
|
||||||
|
|
||||||
|
**可选配置**:
|
||||||
|
- `PORT`: 服务端口(默认7500)
|
||||||
|
- `DEBUG`: 调试模式(true/false,默认false)
|
||||||
|
- `TENANT_ID`: 租户ID(用于MinIO路径)
|
||||||
|
|
||||||
|
### 6. 错误处理规范
|
||||||
|
|
||||||
|
- 所有可能失败的操作必须使用try-except捕获异常
|
||||||
|
- 异常信息要详细,包含上下文信息
|
||||||
|
- 数据库操作异常必须回滚事务
|
||||||
|
- 文件操作异常必须清理临时文件
|
||||||
|
- 对外暴露的错误信息要友好,不泄露内部实现细节
|
||||||
|
|
||||||
|
**示例**:
|
||||||
|
```python
|
||||||
|
try:
|
||||||
|
# 业务逻辑
|
||||||
|
result = some_operation()
|
||||||
|
return success_response(data=result)
|
||||||
|
except ValueError as e:
|
||||||
|
return error_response(400, f"参数错误: {str(e)}")
|
||||||
|
except Exception as e:
|
||||||
|
# 记录详细错误日志
|
||||||
|
print(f"[ERROR] 操作失败: {str(e)}")
|
||||||
|
import traceback
|
||||||
|
print(traceback.format_exc())
|
||||||
|
return error_response(500, "服务器内部错误")
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7. 日志规范
|
||||||
|
|
||||||
|
- 使用`print`输出日志(项目当前使用print,不是logging模块)
|
||||||
|
- 日志格式:`[级别] 消息内容`
|
||||||
|
- 日志级别:
|
||||||
|
- `[DEBUG]`: 调试信息(详细的执行过程)
|
||||||
|
- `[INFO]`: 一般信息(正常流程)
|
||||||
|
- `[WARN]`: 警告信息(不影响功能但需要注意)
|
||||||
|
- `[ERROR]`: 错误信息(功能失败)
|
||||||
|
- 关键操作必须记录日志:AI调用、文件生成、数据库操作
|
||||||
|
|
||||||
|
### 8. 代码质量要求
|
||||||
|
|
||||||
|
- **可读性**: 代码要清晰易懂,变量名要有意义
|
||||||
|
- **可维护性**: 避免重复代码,提取公共方法
|
||||||
|
- **可测试性**: 服务层方法应该是纯函数,便于单元测试
|
||||||
|
- **健壮性**: 处理边界情况,避免崩溃
|
||||||
|
- **性能**: 数据库查询要优化,避免N+1查询
|
||||||
|
|
||||||
|
### 9. 特殊注意事项
|
||||||
|
|
||||||
|
#### Word模板处理
|
||||||
|
- 使用`python-docx`库处理Word文档
|
||||||
|
- 占位符格式:`{{field_code}}`(双大括号)
|
||||||
|
- 表格访问必须使用索引方式,避免迭代器导致的IndexError
|
||||||
|
- 处理跨run的占位符时,需要合并runs并保持格式
|
||||||
|
- 某些复杂表格结构可能导致访问失败,提供XML备用方案
|
||||||
|
|
||||||
|
#### AI字段提取
|
||||||
|
- AI返回的JSON可能格式不正确,需要多种修复机制
|
||||||
|
- 字段名可能不规范(如`_source`、`target_organisation`),需要规范化映射
|
||||||
|
- 日期格式需要统一转换为中文格式
|
||||||
|
- 缺失字段需要从已有信息推断(如从出生年月计算年龄)
|
||||||
|
|
||||||
|
#### MinIO文件管理
|
||||||
|
- 文件路径使用相对路径(以`/`开头)
|
||||||
|
- 上传文件时自动生成时间戳路径
|
||||||
|
- 支持生成预签名下载URL(7天有效期)
|
||||||
|
- 临时文件使用后必须清理
|
||||||
|
|
||||||
|
## 开发工作流
|
||||||
|
|
||||||
|
1. **添加新功能**:
|
||||||
|
- 在服务层添加业务逻辑方法
|
||||||
|
- 在路由层添加API端点
|
||||||
|
- 更新Swagger文档注释
|
||||||
|
- 测试功能是否正常
|
||||||
|
|
||||||
|
2. **修改现有功能**:
|
||||||
|
- 理解现有代码逻辑
|
||||||
|
- 保持API接口兼容性(如需要修改,考虑版本控制)
|
||||||
|
- 更新相关文档
|
||||||
|
|
||||||
|
3. **调试问题**:
|
||||||
|
- 查看日志输出(使用[DEBUG]级别)
|
||||||
|
- 检查数据库数据是否正确
|
||||||
|
- 验证环境变量配置
|
||||||
|
- 测试AI服务是否可用
|
||||||
|
|
||||||
|
## 常见问题处理
|
||||||
|
|
||||||
|
1. **AI解析失败**: 检查输入文本质量,查看AI日志,尝试修复JSON格式
|
||||||
|
2. **模板填充失败**: 检查占位符格式是否正确,查看表格结构是否异常
|
||||||
|
3. **MinIO上传失败**: 检查网络连接,验证MinIO配置和权限
|
||||||
|
4. **数据库连接失败**: 检查数据库配置和网络连接
|
||||||
|
|
||||||
|
## 代码生成指导
|
||||||
|
|
||||||
|
当AI需要生成代码时,请遵循以下原则:
|
||||||
|
|
||||||
|
1. **保持一致性**: 遵循项目现有的代码风格和架构模式
|
||||||
|
2. **错误处理**: 所有可能失败的操作都要有异常处理
|
||||||
|
3. **日志记录**: 关键操作要记录日志
|
||||||
|
4. **参数验证**: API接口要验证输入参数
|
||||||
|
5. **资源清理**: 文件、数据库连接等资源要正确清理
|
||||||
|
6. **中文注释**: 使用中文编写注释和文档字符串
|
||||||
|
7. **类型提示**: 建议使用类型提示提高代码可读性
|
||||||
|
|
||||||
|
## 更新日志
|
||||||
|
|
||||||
|
- 2025-12-13: 创建初始版本
|
||||||
84
.env
84
.env
@ -1,26 +1,68 @@
|
|||||||
# MinIO配置
|
# ========== AI服务提供商配置 ==========
|
||||||
MINIO_ENDPOINT=10.100.31.21:9000
|
# 选择使用的AI服务提供商
|
||||||
MINIO_ACCESS_KEY=minio_PC8dcY
|
# 可选值: 'huawei' 或 'siliconflow'
|
||||||
MINIO_SECRET_KEY=minio_7k7RNJ
|
# 默认值: 'siliconflow'
|
||||||
MINIO_BUCKET=finyx
|
|
||||||
MINIO_SECURE=false # 重要:新服务器使用HTTP,必须是false
|
|
||||||
|
|
||||||
# 其他配置
|
|
||||||
AI_PROVIDER=siliconflow
|
AI_PROVIDER=siliconflow
|
||||||
|
|
||||||
|
# ========== 华为大模型API配置 ==========
|
||||||
|
# 当 AI_PROVIDER=huawei 时使用以下配置
|
||||||
|
|
||||||
|
# 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_NAME=finyx
|
|
||||||
DB_PASSWORD=6QsGK6MpePZDE57Z
|
|
||||||
DB_PORT=5012
|
DB_PORT=5012
|
||||||
DB_USER=finyx
|
DB_USER=finyx
|
||||||
DEBUG=False
|
DB_PASSWORD=6QsGK6MpePZDE57Z
|
||||||
HUAWEI_API_ENDPOINT=http://10.100.31.26:3001/v1/chat/completions
|
DB_NAME=finyx
|
||||||
HUAWEI_API_KEY=sk-PoeiV3qwyTIRqcVc84E8E24cD2904872859a87922e0d9186
|
|
||||||
HUAWEI_API_MAX_TOKENS=12000
|
# ========== MinIO配置(可选,文档生成功能需要) ==========
|
||||||
HUAWEI_API_TIMEOUT=180
|
MINIO_ENDPOINT=minio.datacubeworld.com:9000
|
||||||
HUAWEI_MODEL=DeepSeek-R1-Distill-Llama-70B
|
MINIO_ACCESS_KEY=JOLXFXny3avFSzB0uRA5
|
||||||
|
MINIO_SECRET_KEY=G1BR8jStNfovkfH5ou39EmPl34E4l7dGrnd3Cz0I
|
||||||
|
MINIO_BUCKET=finyx
|
||||||
|
MINIO_SECURE=true
|
||||||
|
|
||||||
|
# ========== 服务配置 ==========
|
||||||
|
# 服务端口
|
||||||
PORT=7500
|
PORT=7500
|
||||||
SILICONFLOW_API_KEY=sk-pgujibohpenkomkwlufexmqzyckglgogdiubfplgqxkfqgfu
|
|
||||||
SILICONFLOW_API_MAX_TOKENS=2000
|
# 调试模式(true/false)
|
||||||
SILICONFLOW_API_TIMEOUT=120
|
DEBUG=False
|
||||||
SILICONFLOW_MODEL=Qwen/Qwen2.5-72B-Instruct
|
|
||||||
SILICONFLOW_URL=https://api.siliconflow.cn/v1/chat/completions
|
|
||||||
|
|||||||
644
app.py
644
app.py
@ -6,8 +6,12 @@ from flask_cors import CORS
|
|||||||
from flasgger import Swagger
|
from flasgger import Swagger
|
||||||
import os
|
import os
|
||||||
import pymysql
|
import pymysql
|
||||||
|
import json
|
||||||
|
import tempfile
|
||||||
|
import zipfile
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
|
from flask import send_file
|
||||||
|
|
||||||
from services.ai_service import AIService
|
from services.ai_service import AIService
|
||||||
from services.field_service import FieldService
|
from services.field_service import FieldService
|
||||||
@ -839,54 +843,104 @@ def template_field_manager():
|
|||||||
return send_from_directory('static', 'template_field_manager.html')
|
return send_from_directory('static', 'template_field_manager.html')
|
||||||
|
|
||||||
|
|
||||||
@app.route('/api/template-field-relations', methods=['GET'])
|
def generate_id():
|
||||||
def get_template_field_relations():
|
"""生成ID(使用时间戳+随机数的方式,模拟雪花算法)"""
|
||||||
|
import time
|
||||||
|
import random
|
||||||
|
timestamp = int(time.time() * 1000)
|
||||||
|
random_part = random.randint(100000, 999999)
|
||||||
|
return timestamp * 1000 + random_part
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/api/tenant-ids', methods=['GET'])
|
||||||
|
def get_tenant_ids():
|
||||||
"""
|
"""
|
||||||
获取所有模板和字段的关联关系
|
获取数据库中所有已存在的 tenant_id
|
||||||
用于模板字段关联管理页面
|
用于模板字段关联管理页面选择租户
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
conn = document_service.get_connection()
|
conn = document_service.get_connection()
|
||||||
cursor = conn.cursor(pymysql.cursors.DictCursor)
|
cursor = conn.cursor(pymysql.cursors.DictCursor)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# 获取所有启用的模板
|
# 从 f_polic_file_config 表中获取所有不同的 tenant_id
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT DISTINCT tenant_id
|
||||||
|
FROM f_polic_file_config
|
||||||
|
WHERE tenant_id IS NOT NULL
|
||||||
|
ORDER BY tenant_id
|
||||||
|
""")
|
||||||
|
tenant_ids = [row['tenant_id'] for row in cursor.fetchall()]
|
||||||
|
|
||||||
|
return success_response({'tenant_ids': tenant_ids})
|
||||||
|
|
||||||
|
finally:
|
||||||
|
cursor.close()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return error_response(500, f"获取租户ID列表失败: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/api/template-field-relations', methods=['GET'])
|
||||||
|
def get_template_field_relations():
|
||||||
|
"""
|
||||||
|
获取指定 tenant_id 下的所有模板和字段的关联关系
|
||||||
|
用于模板字段关联管理页面
|
||||||
|
查询参数: tenant_id (必填)
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
tenant_id = request.args.get('tenant_id')
|
||||||
|
|
||||||
|
if not tenant_id:
|
||||||
|
return error_response(400, "tenant_id参数不能为空")
|
||||||
|
|
||||||
|
try:
|
||||||
|
tenant_id = int(tenant_id)
|
||||||
|
except ValueError:
|
||||||
|
return error_response(400, "tenant_id必须是数字")
|
||||||
|
|
||||||
|
conn = document_service.get_connection()
|
||||||
|
cursor = conn.cursor(pymysql.cursors.DictCursor)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 获取指定 tenant_id 下所有启用的模板
|
||||||
cursor.execute("""
|
cursor.execute("""
|
||||||
SELECT id, name, template_code
|
SELECT id, name, template_code
|
||||||
FROM f_polic_file_config
|
FROM f_polic_file_config
|
||||||
WHERE state = 1
|
WHERE tenant_id = %s AND state = 1
|
||||||
ORDER BY name
|
ORDER BY name
|
||||||
""")
|
""", (tenant_id,))
|
||||||
templates = cursor.fetchall()
|
templates = cursor.fetchall()
|
||||||
|
|
||||||
# 获取所有启用的输入字段
|
# 获取指定 tenant_id 下所有启用的输入字段
|
||||||
cursor.execute("""
|
cursor.execute("""
|
||||||
SELECT id, name, filed_code, field_type
|
SELECT id, name, filed_code, field_type
|
||||||
FROM f_polic_field
|
FROM f_polic_field
|
||||||
WHERE field_type = 1 AND state = 1
|
WHERE tenant_id = %s AND field_type = 1 AND state = 1
|
||||||
ORDER BY name
|
ORDER BY name
|
||||||
""")
|
""", (tenant_id,))
|
||||||
input_fields = cursor.fetchall()
|
input_fields = cursor.fetchall()
|
||||||
|
|
||||||
# 获取所有启用的输出字段
|
# 获取指定 tenant_id 下所有启用的输出字段
|
||||||
cursor.execute("""
|
cursor.execute("""
|
||||||
SELECT id, name, filed_code, field_type
|
SELECT id, name, filed_code, field_type
|
||||||
FROM f_polic_field
|
FROM f_polic_field
|
||||||
WHERE field_type = 2 AND state = 1
|
WHERE tenant_id = %s AND field_type = 2 AND state = 1
|
||||||
ORDER BY name
|
ORDER BY name
|
||||||
""")
|
""", (tenant_id,))
|
||||||
output_fields = cursor.fetchall()
|
output_fields = cursor.fetchall()
|
||||||
|
|
||||||
# 获取现有的关联关系
|
# 获取指定 tenant_id 下现有的关联关系
|
||||||
cursor.execute("""
|
cursor.execute("""
|
||||||
SELECT file_id, filed_id
|
SELECT fff.file_id, fff.filed_id
|
||||||
FROM f_polic_file_field
|
FROM f_polic_file_field fff
|
||||||
WHERE state = 1
|
INNER JOIN f_polic_file_config fc ON fff.file_id = fc.id
|
||||||
""")
|
WHERE fff.tenant_id = %s AND fff.state = 1
|
||||||
|
""", (tenant_id,))
|
||||||
relations = cursor.fetchall()
|
relations = cursor.fetchall()
|
||||||
|
|
||||||
# 构建关联关系映射 (file_id -> list of filed_id)
|
# 构建关联关系映射 (file_id -> list of filed_id)
|
||||||
# 注意:JSON不支持set,所以转换为list
|
|
||||||
relation_map = {}
|
relation_map = {}
|
||||||
for rel in relations:
|
for rel in relations:
|
||||||
file_id = rel['file_id']
|
file_id = rel['file_id']
|
||||||
@ -896,6 +950,7 @@ def get_template_field_relations():
|
|||||||
relation_map[file_id].append(filed_id)
|
relation_map[file_id].append(filed_id)
|
||||||
|
|
||||||
return success_response({
|
return success_response({
|
||||||
|
'tenant_id': tenant_id,
|
||||||
'templates': templates,
|
'templates': templates,
|
||||||
'input_fields': input_fields,
|
'input_fields': input_fields,
|
||||||
'output_fields': output_fields,
|
'output_fields': output_fields,
|
||||||
@ -915,6 +970,7 @@ def save_template_field_relations():
|
|||||||
"""
|
"""
|
||||||
保存模板和字段的关联关系
|
保存模板和字段的关联关系
|
||||||
请求体格式: {
|
请求体格式: {
|
||||||
|
"tenant_id": 123,
|
||||||
"template_id": 123,
|
"template_id": 123,
|
||||||
"input_field_ids": [1, 2, 3],
|
"input_field_ids": [1, 2, 3],
|
||||||
"output_field_ids": [4, 5, 6]
|
"output_field_ids": [4, 5, 6]
|
||||||
@ -926,56 +982,59 @@ def save_template_field_relations():
|
|||||||
if not data:
|
if not data:
|
||||||
return error_response(400, "请求参数不能为空")
|
return error_response(400, "请求参数不能为空")
|
||||||
|
|
||||||
|
tenant_id = data.get('tenant_id')
|
||||||
template_id = data.get('template_id')
|
template_id = data.get('template_id')
|
||||||
input_field_ids = data.get('input_field_ids', [])
|
input_field_ids = data.get('input_field_ids', [])
|
||||||
output_field_ids = data.get('output_field_ids', [])
|
output_field_ids = data.get('output_field_ids', [])
|
||||||
|
|
||||||
|
if not tenant_id:
|
||||||
|
return error_response(400, "tenant_id参数不能为空")
|
||||||
if not template_id:
|
if not template_id:
|
||||||
return error_response(400, "template_id参数不能为空")
|
return error_response(400, "template_id参数不能为空")
|
||||||
|
|
||||||
|
try:
|
||||||
|
tenant_id = int(tenant_id)
|
||||||
|
except ValueError:
|
||||||
|
return error_response(400, "tenant_id必须是数字")
|
||||||
|
|
||||||
conn = document_service.get_connection()
|
conn = document_service.get_connection()
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# 验证模板是否存在
|
# 验证模板是否存在且属于该 tenant_id
|
||||||
cursor.execute("""
|
cursor.execute("""
|
||||||
SELECT id FROM f_polic_file_config
|
SELECT id FROM f_polic_file_config
|
||||||
WHERE id = %s AND state = 1
|
WHERE id = %s AND tenant_id = %s AND state = 1
|
||||||
""", (template_id,))
|
""", (template_id, tenant_id))
|
||||||
if not cursor.fetchone():
|
if not cursor.fetchone():
|
||||||
return error_response(400, f"模板ID {template_id} 不存在或未启用")
|
return error_response(400, f"模板ID {template_id} 不存在或不属于该租户")
|
||||||
|
|
||||||
# 合并所有字段ID
|
# 合并所有字段ID
|
||||||
all_field_ids = set(input_field_ids + output_field_ids)
|
all_field_ids = set(input_field_ids + output_field_ids)
|
||||||
|
|
||||||
# 验证字段是否存在
|
# 验证字段是否存在且属于该 tenant_id
|
||||||
if all_field_ids:
|
if all_field_ids:
|
||||||
placeholders = ','.join(['%s'] * len(all_field_ids))
|
placeholders = ','.join(['%s'] * len(all_field_ids))
|
||||||
cursor.execute(f"""
|
cursor.execute(f"""
|
||||||
SELECT id FROM f_polic_field
|
SELECT id FROM f_polic_field
|
||||||
WHERE id IN ({placeholders}) AND state = 1
|
WHERE id IN ({placeholders}) AND tenant_id = %s AND state = 1
|
||||||
""", list(all_field_ids))
|
""", list(all_field_ids) + [tenant_id])
|
||||||
existing_field_ids = {row[0] for row in cursor.fetchall()}
|
existing_field_ids = {row[0] for row in cursor.fetchall()}
|
||||||
invalid_field_ids = all_field_ids - existing_field_ids
|
invalid_field_ids = all_field_ids - existing_field_ids
|
||||||
if invalid_field_ids:
|
if invalid_field_ids:
|
||||||
return error_response(400, f"字段ID {list(invalid_field_ids)} 不存在或未启用")
|
return error_response(400, f"字段ID {list(invalid_field_ids)} 不存在或不属于该租户")
|
||||||
|
|
||||||
# 删除该模板的所有现有关联关系
|
# 删除该模板的所有现有关联关系(仅限该 tenant_id)
|
||||||
cursor.execute("""
|
cursor.execute("""
|
||||||
DELETE FROM f_polic_file_field
|
DELETE FROM f_polic_file_field
|
||||||
WHERE file_id = %s
|
WHERE file_id = %s AND tenant_id = %s
|
||||||
""", (template_id,))
|
""", (template_id, tenant_id))
|
||||||
|
|
||||||
# 插入新的关联关系
|
# 插入新的关联关系
|
||||||
current_time = datetime.now()
|
current_time = datetime.now()
|
||||||
created_by = 655162080928945152 # 默认创建者ID
|
created_by = 655162080928945152 # 默认创建者ID
|
||||||
|
|
||||||
# 从环境变量读取tenant_id(如果数据库表需要),如果不需要可以设置为NULL
|
|
||||||
tenant_id = os.getenv('TENANT_ID')
|
|
||||||
|
|
||||||
if all_field_ids:
|
if all_field_ids:
|
||||||
# 如果tenant_id是必填字段,从环境变量读取;如果可以为NULL,则使用NULL
|
|
||||||
if tenant_id:
|
|
||||||
insert_sql = """
|
insert_sql = """
|
||||||
INSERT INTO f_polic_file_field
|
INSERT INTO f_polic_file_field
|
||||||
(tenant_id, file_id, filed_id, created_time, created_by, updated_time, updated_by, state)
|
(tenant_id, file_id, filed_id, created_time, created_by, updated_time, updated_by, state)
|
||||||
@ -991,22 +1050,6 @@ def save_template_field_relations():
|
|||||||
current_time,
|
current_time,
|
||||||
created_by
|
created_by
|
||||||
))
|
))
|
||||||
else:
|
|
||||||
# 如果tenant_id可以为NULL,使用NULL
|
|
||||||
insert_sql = """
|
|
||||||
INSERT INTO f_polic_file_field
|
|
||||||
(file_id, filed_id, created_time, created_by, updated_time, updated_by, state)
|
|
||||||
VALUES (%s, %s, %s, %s, %s, %s, 1)
|
|
||||||
"""
|
|
||||||
for field_id in all_field_ids:
|
|
||||||
cursor.execute(insert_sql, (
|
|
||||||
template_id,
|
|
||||||
field_id,
|
|
||||||
current_time,
|
|
||||||
created_by,
|
|
||||||
current_time,
|
|
||||||
created_by
|
|
||||||
))
|
|
||||||
|
|
||||||
conn.commit()
|
conn.commit()
|
||||||
|
|
||||||
@ -1028,6 +1071,505 @@ def save_template_field_relations():
|
|||||||
return error_response(500, f"保存关联关系失败: {str(e)}")
|
return error_response(500, f"保存关联关系失败: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
|
# 字段管理 API
|
||||||
|
@app.route('/api/fields', methods=['GET'])
|
||||||
|
def get_fields():
|
||||||
|
"""
|
||||||
|
获取字段列表
|
||||||
|
查询参数: tenant_id (必填), field_type (可选: 1=输入字段, 2=输出字段)
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
tenant_id = request.args.get('tenant_id')
|
||||||
|
field_type = request.args.get('field_type')
|
||||||
|
|
||||||
|
if not tenant_id:
|
||||||
|
return error_response(400, "tenant_id参数不能为空")
|
||||||
|
|
||||||
|
try:
|
||||||
|
tenant_id = int(tenant_id)
|
||||||
|
except ValueError:
|
||||||
|
return error_response(400, "tenant_id必须是数字")
|
||||||
|
|
||||||
|
conn = document_service.get_connection()
|
||||||
|
cursor = conn.cursor(pymysql.cursors.DictCursor)
|
||||||
|
|
||||||
|
try:
|
||||||
|
if field_type:
|
||||||
|
try:
|
||||||
|
field_type = int(field_type)
|
||||||
|
except ValueError:
|
||||||
|
return error_response(400, "field_type必须是数字")
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT id, name, filed_code, field_type, state
|
||||||
|
FROM f_polic_field
|
||||||
|
WHERE tenant_id = %s AND field_type = %s
|
||||||
|
ORDER BY field_type, name
|
||||||
|
""", (tenant_id, field_type))
|
||||||
|
else:
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT id, name, filed_code, field_type, state
|
||||||
|
FROM f_polic_field
|
||||||
|
WHERE tenant_id = %s
|
||||||
|
ORDER BY field_type, name
|
||||||
|
""", (tenant_id,))
|
||||||
|
|
||||||
|
fields = cursor.fetchall()
|
||||||
|
return success_response({'fields': fields})
|
||||||
|
|
||||||
|
finally:
|
||||||
|
cursor.close()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return error_response(500, f"获取字段列表失败: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/api/fields', methods=['POST'])
|
||||||
|
def create_field():
|
||||||
|
"""
|
||||||
|
创建新字段
|
||||||
|
请求体格式: {
|
||||||
|
"tenant_id": 123,
|
||||||
|
"name": "字段名称",
|
||||||
|
"filed_code": "field_code",
|
||||||
|
"field_type": 1 // 1=输入字段, 2=输出字段
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
data = request.get_json()
|
||||||
|
|
||||||
|
if not data:
|
||||||
|
return error_response(400, "请求参数不能为空")
|
||||||
|
|
||||||
|
tenant_id = data.get('tenant_id')
|
||||||
|
name = data.get('name')
|
||||||
|
filed_code = data.get('filed_code')
|
||||||
|
field_type = data.get('field_type')
|
||||||
|
|
||||||
|
if not all([tenant_id, name, filed_code, field_type]):
|
||||||
|
return error_response(400, "tenant_id, name, filed_code, field_type 参数不能为空")
|
||||||
|
|
||||||
|
try:
|
||||||
|
tenant_id = int(tenant_id)
|
||||||
|
field_type = int(field_type)
|
||||||
|
except ValueError:
|
||||||
|
return error_response(400, "tenant_id 和 field_type 必须是数字")
|
||||||
|
|
||||||
|
if field_type not in [1, 2]:
|
||||||
|
return error_response(400, "field_type 必须是 1(输入字段)或 2(输出字段)")
|
||||||
|
|
||||||
|
conn = document_service.get_connection()
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 检查字段编码是否已存在(同一 tenant_id 下)
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT id FROM f_polic_field
|
||||||
|
WHERE tenant_id = %s AND filed_code = %s
|
||||||
|
""", (tenant_id, filed_code))
|
||||||
|
if cursor.fetchone():
|
||||||
|
return error_response(400, f"字段编码 {filed_code} 已存在")
|
||||||
|
|
||||||
|
# 创建新字段
|
||||||
|
field_id = generate_id()
|
||||||
|
current_time = datetime.now()
|
||||||
|
created_by = 655162080928945152
|
||||||
|
|
||||||
|
cursor.execute("""
|
||||||
|
INSERT INTO f_polic_field
|
||||||
|
(id, tenant_id, name, filed_code, field_type, created_time, created_by, updated_time, updated_by, state)
|
||||||
|
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, 1)
|
||||||
|
""", (field_id, tenant_id, name, filed_code, field_type, current_time, created_by, current_time, created_by))
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
return success_response({'field_id': field_id}, "字段创建成功")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
conn.rollback()
|
||||||
|
raise e
|
||||||
|
finally:
|
||||||
|
cursor.close()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return error_response(500, f"创建字段失败: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/api/fields/<int:field_id>', methods=['PUT'])
|
||||||
|
def update_field(field_id):
|
||||||
|
"""
|
||||||
|
更新字段
|
||||||
|
请求体格式: {
|
||||||
|
"tenant_id": 123,
|
||||||
|
"name": "字段名称",
|
||||||
|
"filed_code": "field_code",
|
||||||
|
"field_type": 1,
|
||||||
|
"state": 1 // 0=未启用, 1=启用
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
data = request.get_json()
|
||||||
|
|
||||||
|
if not data:
|
||||||
|
return error_response(400, "请求参数不能为空")
|
||||||
|
|
||||||
|
tenant_id = data.get('tenant_id')
|
||||||
|
name = data.get('name')
|
||||||
|
filed_code = data.get('filed_code')
|
||||||
|
field_type = data.get('field_type')
|
||||||
|
state = data.get('state')
|
||||||
|
|
||||||
|
if not tenant_id:
|
||||||
|
return error_response(400, "tenant_id参数不能为空")
|
||||||
|
|
||||||
|
try:
|
||||||
|
tenant_id = int(tenant_id)
|
||||||
|
except ValueError:
|
||||||
|
return error_response(400, "tenant_id必须是数字")
|
||||||
|
|
||||||
|
conn = document_service.get_connection()
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 验证字段是否存在且属于该 tenant_id
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT id FROM f_polic_field
|
||||||
|
WHERE id = %s AND tenant_id = %s
|
||||||
|
""", (field_id, tenant_id))
|
||||||
|
if not cursor.fetchone():
|
||||||
|
return error_response(404, "字段不存在或不属于该租户")
|
||||||
|
|
||||||
|
# 如果更新 filed_code,检查是否与其他字段冲突
|
||||||
|
if filed_code:
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT id FROM f_polic_field
|
||||||
|
WHERE tenant_id = %s AND filed_code = %s AND id != %s
|
||||||
|
""", (tenant_id, filed_code, field_id))
|
||||||
|
if cursor.fetchone():
|
||||||
|
return error_response(400, f"字段编码 {filed_code} 已被其他字段使用")
|
||||||
|
|
||||||
|
# 构建更新语句
|
||||||
|
update_fields = []
|
||||||
|
update_values = []
|
||||||
|
|
||||||
|
if name is not None:
|
||||||
|
update_fields.append("name = %s")
|
||||||
|
update_values.append(name)
|
||||||
|
if filed_code is not None:
|
||||||
|
update_fields.append("filed_code = %s")
|
||||||
|
update_values.append(filed_code)
|
||||||
|
if field_type is not None:
|
||||||
|
if field_type not in [1, 2]:
|
||||||
|
return error_response(400, "field_type 必须是 1(输入字段)或 2(输出字段)")
|
||||||
|
update_fields.append("field_type = %s")
|
||||||
|
update_values.append(field_type)
|
||||||
|
if state is not None:
|
||||||
|
update_fields.append("state = %s")
|
||||||
|
update_values.append(state)
|
||||||
|
|
||||||
|
if not update_fields:
|
||||||
|
return error_response(400, "没有需要更新的字段")
|
||||||
|
|
||||||
|
update_fields.append("updated_time = %s")
|
||||||
|
update_values.append(datetime.now())
|
||||||
|
update_fields.append("updated_by = %s")
|
||||||
|
update_values.append(655162080928945152)
|
||||||
|
|
||||||
|
update_values.append(field_id)
|
||||||
|
update_values.append(tenant_id)
|
||||||
|
|
||||||
|
update_sql = f"""
|
||||||
|
UPDATE f_polic_field
|
||||||
|
SET {', '.join(update_fields)}
|
||||||
|
WHERE id = %s AND tenant_id = %s
|
||||||
|
"""
|
||||||
|
|
||||||
|
cursor.execute(update_sql, update_values)
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
return success_response({'field_id': field_id}, "字段更新成功")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
conn.rollback()
|
||||||
|
raise e
|
||||||
|
finally:
|
||||||
|
cursor.close()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return error_response(500, f"更新字段失败: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/api/fields/<int:field_id>', methods=['DELETE'])
|
||||||
|
def delete_field(field_id):
|
||||||
|
"""
|
||||||
|
删除字段(软删除,将 state 设置为 0)
|
||||||
|
查询参数: tenant_id (必填)
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
tenant_id = request.args.get('tenant_id')
|
||||||
|
|
||||||
|
if not tenant_id:
|
||||||
|
return error_response(400, "tenant_id参数不能为空")
|
||||||
|
|
||||||
|
try:
|
||||||
|
tenant_id = int(tenant_id)
|
||||||
|
except ValueError:
|
||||||
|
return error_response(400, "tenant_id必须是数字")
|
||||||
|
|
||||||
|
conn = document_service.get_connection()
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 验证字段是否存在且属于该 tenant_id
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT id FROM f_polic_field
|
||||||
|
WHERE id = %s AND tenant_id = %s
|
||||||
|
""", (field_id, tenant_id))
|
||||||
|
if not cursor.fetchone():
|
||||||
|
return error_response(404, "字段不存在或不属于该租户")
|
||||||
|
|
||||||
|
# 检查字段是否被模板关联
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT COUNT(*) as count
|
||||||
|
FROM f_polic_file_field
|
||||||
|
WHERE filed_id = %s AND tenant_id = %s AND state = 1
|
||||||
|
""", (field_id, tenant_id))
|
||||||
|
result = cursor.fetchone()
|
||||||
|
if result and result[0] > 0:
|
||||||
|
return error_response(400, f"字段正在被 {result[0]} 个模板使用,无法删除")
|
||||||
|
|
||||||
|
# 软删除字段
|
||||||
|
cursor.execute("""
|
||||||
|
UPDATE f_polic_field
|
||||||
|
SET state = 0, updated_time = %s, updated_by = %s
|
||||||
|
WHERE id = %s AND tenant_id = %s
|
||||||
|
""", (datetime.now(), 655162080928945152, field_id, tenant_id))
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
return success_response({'field_id': field_id}, "字段删除成功")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
conn.rollback()
|
||||||
|
raise e
|
||||||
|
finally:
|
||||||
|
cursor.close()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return error_response(500, f"删除字段失败: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
|
# 数据库备份和恢复 API
|
||||||
|
@app.route('/api/database/backup', methods=['POST'])
|
||||||
|
def backup_database():
|
||||||
|
"""
|
||||||
|
备份数据库相关表格
|
||||||
|
请求体格式: {
|
||||||
|
"tenant_id": 123 // 可选,如果提供则只备份该 tenant_id 的数据
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
data = request.get_json() or {}
|
||||||
|
tenant_id = data.get('tenant_id')
|
||||||
|
|
||||||
|
conn = document_service.get_connection()
|
||||||
|
cursor = conn.cursor(pymysql.cursors.DictCursor)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 要备份的表
|
||||||
|
tables = ['f_polic_file_config', 'f_polic_field', 'f_polic_file_field']
|
||||||
|
backup_data = {}
|
||||||
|
|
||||||
|
for table in tables:
|
||||||
|
if tenant_id:
|
||||||
|
# 根据表结构决定如何过滤 tenant_id
|
||||||
|
if table == 'f_polic_file_config':
|
||||||
|
cursor.execute(f"SELECT * FROM {table} WHERE tenant_id = %s", (tenant_id,))
|
||||||
|
elif table == 'f_polic_field':
|
||||||
|
cursor.execute(f"SELECT * FROM {table} WHERE tenant_id = %s", (tenant_id,))
|
||||||
|
elif table == 'f_polic_file_field':
|
||||||
|
cursor.execute(f"SELECT * FROM {table} WHERE tenant_id = %s", (tenant_id,))
|
||||||
|
else:
|
||||||
|
cursor.execute(f"SELECT * FROM {table}")
|
||||||
|
|
||||||
|
backup_data[table] = cursor.fetchall()
|
||||||
|
|
||||||
|
# 创建临时文件保存备份数据
|
||||||
|
temp_file = tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False, encoding='utf-8')
|
||||||
|
json.dump({
|
||||||
|
'backup_time': datetime.now().isoformat(),
|
||||||
|
'tenant_id': tenant_id,
|
||||||
|
'tables': backup_data
|
||||||
|
}, temp_file, ensure_ascii=False, indent=2, default=str)
|
||||||
|
temp_file.close()
|
||||||
|
|
||||||
|
return send_file(
|
||||||
|
temp_file.name,
|
||||||
|
as_attachment=True,
|
||||||
|
download_name=f'db_backup_{datetime.now().strftime("%Y%m%d_%H%M%S")}.json',
|
||||||
|
mimetype='application/json'
|
||||||
|
)
|
||||||
|
|
||||||
|
finally:
|
||||||
|
cursor.close()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return error_response(500, f"备份数据库失败: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/api/database/restore', methods=['POST'])
|
||||||
|
def restore_database():
|
||||||
|
"""
|
||||||
|
恢复数据库相关表格
|
||||||
|
通过文件上传: file (multipart/form-data)
|
||||||
|
查询参数: tenant_id (必填,恢复数据到该 tenant_id)
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# 从查询参数获取 tenant_id
|
||||||
|
tenant_id = request.args.get('tenant_id')
|
||||||
|
if not tenant_id:
|
||||||
|
# 尝试从表单数据获取
|
||||||
|
tenant_id = request.form.get('tenant_id')
|
||||||
|
|
||||||
|
if not tenant_id:
|
||||||
|
return error_response(400, "tenant_id参数不能为空(可通过查询参数或表单数据提供)")
|
||||||
|
|
||||||
|
try:
|
||||||
|
tenant_id = int(tenant_id)
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
return error_response(400, "tenant_id必须是数字")
|
||||||
|
|
||||||
|
backup_data = None
|
||||||
|
|
||||||
|
# 检查是否有文件上传
|
||||||
|
if 'file' in request.files:
|
||||||
|
file = request.files['file']
|
||||||
|
if file.filename:
|
||||||
|
try:
|
||||||
|
backup_data = json.load(file)
|
||||||
|
except json.JSONDecodeError as e:
|
||||||
|
return error_response(400, f"备份文件格式错误: {str(e)}")
|
||||||
|
else:
|
||||||
|
return error_response(400, "请上传备份文件")
|
||||||
|
|
||||||
|
if not backup_data:
|
||||||
|
return error_response(400, "备份数据不能为空")
|
||||||
|
|
||||||
|
conn = document_service.get_connection()
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 验证备份数据格式
|
||||||
|
if 'tables' not in backup_data:
|
||||||
|
return error_response(400, "备份数据格式错误:缺少 tables 字段")
|
||||||
|
|
||||||
|
tables_data = backup_data['tables']
|
||||||
|
required_tables = ['f_polic_file_config', 'f_polic_field', 'f_polic_file_field']
|
||||||
|
|
||||||
|
for table in required_tables:
|
||||||
|
if table not in tables_data:
|
||||||
|
return error_response(400, f"备份数据格式错误:缺少表 {table} 的数据")
|
||||||
|
|
||||||
|
# 开始恢复(注意:这里只恢复指定 tenant_id 的数据)
|
||||||
|
# 先删除该 tenant_id 的现有数据
|
||||||
|
cursor.execute("DELETE FROM f_polic_file_field WHERE tenant_id = %s", (tenant_id,))
|
||||||
|
cursor.execute("DELETE FROM f_polic_field WHERE tenant_id = %s", (tenant_id,))
|
||||||
|
cursor.execute("DELETE FROM f_polic_file_config WHERE tenant_id = %s", (tenant_id,))
|
||||||
|
|
||||||
|
# 恢复 f_polic_file_config
|
||||||
|
if tables_data['f_polic_file_config']:
|
||||||
|
insert_sql = """
|
||||||
|
INSERT INTO f_polic_file_config
|
||||||
|
(id, tenant_id, parent_id, name, template_code, input_data, file_path,
|
||||||
|
created_time, created_by, updated_time, updated_by, state)
|
||||||
|
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
|
||||||
|
"""
|
||||||
|
for row in tables_data['f_polic_file_config']:
|
||||||
|
# 确保 tenant_id 正确
|
||||||
|
row['tenant_id'] = tenant_id
|
||||||
|
cursor.execute(insert_sql, (
|
||||||
|
row.get('id'),
|
||||||
|
row.get('tenant_id'),
|
||||||
|
row.get('parent_id'),
|
||||||
|
row.get('name'),
|
||||||
|
row.get('template_code'),
|
||||||
|
row.get('input_data'),
|
||||||
|
row.get('file_path'),
|
||||||
|
row.get('created_time'),
|
||||||
|
row.get('created_by'),
|
||||||
|
row.get('updated_time'),
|
||||||
|
row.get('updated_by'),
|
||||||
|
row.get('state', 1)
|
||||||
|
))
|
||||||
|
|
||||||
|
# 恢复 f_polic_field
|
||||||
|
if tables_data['f_polic_field']:
|
||||||
|
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)
|
||||||
|
"""
|
||||||
|
for row in tables_data['f_polic_field']:
|
||||||
|
# 确保 tenant_id 正确
|
||||||
|
row['tenant_id'] = tenant_id
|
||||||
|
cursor.execute(insert_sql, (
|
||||||
|
row.get('id'),
|
||||||
|
row.get('tenant_id'),
|
||||||
|
row.get('name'),
|
||||||
|
row.get('filed_code'),
|
||||||
|
row.get('field_type'),
|
||||||
|
row.get('created_time'),
|
||||||
|
row.get('created_by'),
|
||||||
|
row.get('updated_time'),
|
||||||
|
row.get('updated_by'),
|
||||||
|
row.get('state', 1)
|
||||||
|
))
|
||||||
|
|
||||||
|
# 恢复 f_polic_file_field
|
||||||
|
if tables_data['f_polic_file_field']:
|
||||||
|
insert_sql = """
|
||||||
|
INSERT INTO f_polic_file_field
|
||||||
|
(tenant_id, file_id, filed_id,
|
||||||
|
created_time, created_by, updated_time, updated_by, state)
|
||||||
|
VALUES (%s, %s, %s, %s, %s, %s, %s, %s)
|
||||||
|
"""
|
||||||
|
for row in tables_data['f_polic_file_field']:
|
||||||
|
# 确保 tenant_id 正确
|
||||||
|
row['tenant_id'] = tenant_id
|
||||||
|
cursor.execute(insert_sql, (
|
||||||
|
row.get('tenant_id'),
|
||||||
|
row.get('file_id'),
|
||||||
|
row.get('filed_id'),
|
||||||
|
row.get('created_time'),
|
||||||
|
row.get('created_by'),
|
||||||
|
row.get('updated_time'),
|
||||||
|
row.get('updated_by'),
|
||||||
|
row.get('state', 1)
|
||||||
|
))
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
return success_response({
|
||||||
|
'tenant_id': tenant_id,
|
||||||
|
'restored_tables': required_tables
|
||||||
|
}, "数据库恢复成功")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
conn.rollback()
|
||||||
|
raise e
|
||||||
|
finally:
|
||||||
|
cursor.close()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return error_response(500, f"恢复数据库失败: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
# 确保static目录存在
|
# 确保static目录存在
|
||||||
os.makedirs('static', exist_ok=True)
|
os.makedirs('static', exist_ok=True)
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
flask==3.0.0
|
flask==3.0.0
|
||||||
flask-cors==4.0.0
|
flask-cors==4.0.0
|
||||||
pymysql==1.1.2
|
pymysql==1.1.2
|
||||||
|
cryptography>=41.0.0
|
||||||
python-dotenv==1.0.0
|
python-dotenv==1.0.0
|
||||||
requests==2.31.0
|
requests==2.31.0
|
||||||
flasgger==0.9.7.1
|
flasgger==0.9.7.1
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user