1587 lines
56 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
智慧监督AI文书写作服务 - 主应用
"""
from flask import Flask, request, jsonify, send_from_directory
from flask_cors import CORS
from flasgger import Swagger
import os
import pymysql
import json
import tempfile
import zipfile
from datetime import datetime
from dotenv import load_dotenv
from flask import send_file
from services.ai_service import AIService
from services.field_service import FieldService
from services.document_service import DocumentService
from utils.response import success_response, error_response
# 加载环境变量
load_dotenv()
app = Flask(__name__)
CORS(app) # 允许跨域请求
# 配置Swagger
swagger_config = {
"headers": [],
"specs": [
{
"endpoint": "apispec",
"route": "/apispec.json",
"rule_filter": lambda rule: True,
"model_filter": lambda tag: True,
}
],
"static_url_path": "/flasgger_static",
"swagger_ui": True,
"specs_route": "/api-docs"
}
swagger_template = {
"swagger": "2.0",
"info": {
"title": "智慧监督AI文书写作服务 API",
"description": "基于大模型的智能文书生成服务,支持从非结构化文本中提取结构化字段数据",
"version": "1.0.0",
"contact": {
"name": "API支持"
}
},
"basePath": "/",
"schemes": ["http", "https"],
"tags": [
{
"name": "AI解析",
"description": "AI字段提取相关接口"
},
{
"name": "文档生成",
"description": "文档生成相关接口"
},
{
"name": "字段配置",
"description": "字段配置查询接口"
}
]
}
swagger = Swagger(app, config=swagger_config, template=swagger_template)
# 初始化服务
ai_service = AIService()
field_service = FieldService()
document_service = DocumentService()
@app.route('/')
def index():
"""返回测试页面"""
return send_from_directory('static', 'index.html')
@app.route('/ai/extract', methods=['POST'])
@app.route('/api/ai/extract', methods=['POST']) # 保留旧路径以兼容
def extract():
"""
AI字段提取接口
从输入的非结构化文本中提取结构化字段数据
---
tags:
- AI解析
summary: 从输入数据中提取结构化字段
description: 使用AI大模型从输入文本中提取结构化字段根据fieldCode从数据库查询字段配置
consumes:
- application/json
produces:
- application/json
parameters:
- in: body
name: body
description: 请求参数
required: true
schema:
type: object
required:
- inputData
- outputData
properties:
inputData:
type: array
description: 输入数据列表
items:
type: object
properties:
fieldCode:
type: string
description: 字段编码
example: clue_info
fieldValue:
type: string
description: 字段值(原始文本)
example: 被举报用户名称是张三年龄30岁某公司总经理
outputData:
type: array
description: 需要提取的输出字段列表
items:
type: object
properties:
fieldCode:
type: string
description: 字段编码
example: userName
responses:
200:
description: 解析成功
schema:
type: object
properties:
code:
type: integer
description: 响应码0表示成功
example: 0
data:
type: object
properties:
outData:
type: array
description: 提取的字段列表
items:
type: object
properties:
fieldCode:
type: string
description: 字段编码
example: userName
fieldValue:
type: string
description: 提取的字段值
example: 张三
msg:
type: string
description: 响应消息
example: ok
isSuccess:
type: boolean
description: 是否成功
example: true
timestamp:
type: string
description: 时间戳
errorMsg:
type: string
description: 错误信息(成功时为空)
400:
description: 请求参数错误
schema:
type: object
properties:
code:
type: integer
example: 400
errorMsg:
type: string
example: 请求参数不能为空
isSuccess:
type: boolean
example: false
2001:
description: AI解析超时或发生错误
schema:
type: object
properties:
code:
type: integer
example: 2001
errorMsg:
type: string
example: AI解析超时或发生错误
isSuccess:
type: boolean
example: false
2002:
description: AI解析失败
schema:
type: object
properties:
code:
type: integer
example: 2002
errorMsg:
type: string
example: AI解析失败请检查输入文本质量
isSuccess:
type: boolean
example: false
"""
try:
data = request.get_json()
# 验证请求参数
if not data:
return error_response(400, "请求参数不能为空")
input_data = data.get('inputData', [])
output_data = data.get('outputData', [])
if not input_data or not isinstance(input_data, list):
return error_response(400, "inputData参数必须是非空数组")
if not output_data or not isinstance(output_data, list):
return error_response(400, "outputData参数必须是非空数组")
# 提取outputData中的fieldCode列表
output_field_codes = []
for item in output_data:
if isinstance(item, dict) and 'fieldCode' in item:
output_field_codes.append(item['fieldCode'])
elif isinstance(item, str):
output_field_codes.append(item)
if not output_field_codes:
return error_response(400, "outputData中必须包含至少一个fieldCode")
# 根据fieldCode从数据库查询输出字段配置
output_fields = field_service.get_output_fields_by_field_codes(output_field_codes)
if not output_fields:
return error_response(2002, f"未找到字段编码 {output_field_codes} 对应的字段配置")
# 构建AI提示词不再需要business_type
prompt = field_service.build_extract_prompt(input_data, output_fields)
# 调用AI服务进行解析
ai_result = ai_service.extract_fields(prompt, output_fields)
if not ai_result:
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中的字段顺序返回
out_data = []
# 创建一个字段编码到字段信息的映射
field_map = {field['field_code']: field for field in output_fields}
# 按照outputData的顺序构建返回数据
# 注意如果AI未提取到值返回空字符串不自动应用默认值
# 默认值信息在文档中说明,由前端根据业务需求决定是否应用
for field_code in output_field_codes:
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({
'fieldCode': field_code,
'fieldValue': field_value
})
return success_response({'outData': out_data})
except Exception as 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 state = 1
ORDER BY name
"""
cursor.execute(sql)
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'])
def get_fields():
"""
获取字段配置接口
获取指定业务类型的输入和输出字段配置
---
tags:
- 字段配置
summary: 获取字段配置
description: 获取指定业务类型的输入字段和输出字段配置,用于测试页面展示
produces:
- application/json
parameters:
- in: query
name: businessType
type: string
required: false
default: INVESTIGATION
description: 业务类型
example: INVESTIGATION
responses:
200:
description: 获取成功
schema:
type: object
properties:
code:
type: integer
description: 响应码0表示成功
example: 0
data:
type: object
properties:
fields:
type: object
properties:
input_fields:
type: array
description: 输入字段列表
items:
type: object
properties:
id:
type: integer
description: 字段ID
name:
type: string
description: 字段名称
example: 线索信息
field_code:
type: string
description: 字段编码
example: clue_info
field_type:
type: integer
description: 字段类型1=输入字段2=输出字段)
example: 1
output_fields:
type: array
description: 输出字段列表
items:
type: object
properties:
id:
type: integer
description: 字段ID
name:
type: string
description: 字段名称
example: 被核查人姓名
field_code:
type: string
description: 字段编码
example: target_name
field_type:
type: integer
description: 字段类型1=输入字段2=输出字段)
example: 2
msg:
type: string
description: 响应消息
example: ok
isSuccess:
type: boolean
description: 是否成功
example: true
500:
description: 服务器错误
schema:
type: object
properties:
code:
type: integer
example: 500
errorMsg:
type: string
example: 获取字段配置失败
isSuccess:
type: boolean
example: false
"""
try:
business_type = request.args.get('businessType', 'INVESTIGATION')
fields = field_service.get_fields_by_business_type(business_type)
return success_response({'fields': fields})
except Exception as e:
return error_response(500, f"获取字段配置失败: {str(e)}")
@app.route('/ai/generate-document', methods=['POST'])
@app.route('/api/ai/generate-document', methods=['POST']) # 保留旧路径以兼容
def generate_document():
"""
文档生成接口
根据输入数据填充Word模板并生成文档
---
tags:
- 文档生成
summary: 生成填充后的文档
description: 根据输入数据填充Word模板上传到MinIO并返回文件路径
consumes:
- application/json
produces:
- application/json
parameters:
- in: body
name: body
description: 请求参数
required: true
schema:
type: object
required:
- inputData
- fpolicFieldParamFileList
properties:
inputData:
type: array
description: 输入数据列表
items:
type: object
properties:
fieldCode:
type: string
description: 字段编码
example: userName
fieldValue:
type: string
description: 字段值
example: 张三
fpolicFieldParamFileList:
type: array
description: 文件列表
items:
type: object
required:
- fileId
properties:
fileId:
type: integer
description: 文件配置ID从f_polic_file_config表获取
example: 1765273961563507
fileName:
type: string
description: 文件名称(可选,用于生成文档名称)
example: 请示报告卡.doc
responses:
200:
description: 生成成功
schema:
type: object
properties:
code:
type: integer
description: 响应码0表示成功
example: 0
data:
type: object
properties:
documentId:
type: string
description: 文档ID
example: DOC202411260001
documentName:
type: string
description: 文档名称(第一个生成的文档名称)
example: 初步核实审批表_张三.docx
fpolicFieldParamFileList:
type: array
description: 生成的文档列表(数量与请求一致)
items:
type: object
properties:
fileId:
type: integer
description: 文件ID与请求中的fileId一致
example: 1
fileName:
type: string
description: 实际生成的文档名称(.docx格式与请求中的fileName可能不同
example: 初步核实审批表_张三.docx
filePath:
type: string
description: MinIO相对路径指向生成的文档文件
example: /615873064429507639/20251205090700/初步核实审批表_张三.docx
downloadUrl:
type: string
description: MinIO预签名下载URL完整链接7天有效可直接下载
example: https://minio.datacubeworld.com:9000/finyx/615873064429507639/20251205090700/初步核实审批表_张三.docx?X-Amz-Algorithm=...
msg:
type: string
example: ok
isSuccess:
type: boolean
example: true
1001:
description: 模板不存在或参数错误
schema:
type: object
properties:
code:
type: integer
example: 1001
errorMsg:
type: string
example: 文件ID对应的模板不存在或未启用
isSuccess:
type: boolean
example: false
3001:
description: 文件生成失败
schema:
type: object
properties:
code:
type: integer
example: 3001
errorMsg:
type: string
example: 文件生成失败
isSuccess:
type: boolean
example: false
3002:
description: 文件保存失败
schema:
type: object
properties:
code:
type: integer
example: 3002
errorMsg:
type: string
example: 文件保存失败
isSuccess:
type: boolean
example: false
"""
try:
data = request.get_json()
# 验证请求参数
if not data:
return error_response(400, "请求参数不能为空")
input_data = data.get('inputData', [])
file_list = data.get('fpolicFieldParamFileList', [])
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'], # MinIO相对路径
'downloadUrl': result.get('downloadUrl') # MinIO预签名下载URL完整链接
})
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)}")
@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 state = 1
"""
cursor.execute(sql, (task_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'], # MinIO相对路径
'downloadUrl': result.get('downloadUrl') # MinIO预签名下载URL完整链接
})
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)}")
@app.route('/template-field-manager')
def template_field_manager():
"""返回模板字段关联管理页面"""
return send_from_directory('static', 'template_field_manager.html')
def generate_id():
"""生成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:
conn = document_service.get_connection()
cursor = conn.cursor(pymysql.cursors.DictCursor)
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("""
SELECT id, name, template_code
FROM f_polic_file_config
WHERE tenant_id = %s AND state = 1
ORDER BY name
""", (tenant_id,))
templates = cursor.fetchall()
# 获取指定 tenant_id 下所有启用的输入字段
cursor.execute("""
SELECT id, name, filed_code, field_type
FROM f_polic_field
WHERE tenant_id = %s AND field_type = 1 AND state = 1
ORDER BY name
""", (tenant_id,))
input_fields = cursor.fetchall()
# 获取指定 tenant_id 下所有启用的输出字段
cursor.execute("""
SELECT id, name, filed_code, field_type
FROM f_polic_field
WHERE tenant_id = %s AND field_type = 2 AND state = 1
ORDER BY name
""", (tenant_id,))
output_fields = cursor.fetchall()
# 获取指定 tenant_id 下现有的关联关系
cursor.execute("""
SELECT fff.file_id, fff.filed_id
FROM f_polic_file_field fff
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()
# 构建关联关系映射 (file_id -> list of filed_id)
relation_map = {}
for rel in relations:
file_id = rel['file_id']
filed_id = rel['filed_id']
if file_id not in relation_map:
relation_map[file_id] = []
relation_map[file_id].append(filed_id)
return success_response({
'tenant_id': tenant_id,
'templates': templates,
'input_fields': input_fields,
'output_fields': output_fields,
'relations': relation_map
})
finally:
cursor.close()
conn.close()
except Exception as e:
return error_response(500, f"获取关联关系失败: {str(e)}")
@app.route('/api/template-field-relations', methods=['POST'])
def save_template_field_relations():
"""
保存模板和字段的关联关系
请求体格式: {
"tenant_id": 123,
"template_id": 123,
"input_field_ids": [1, 2, 3],
"output_field_ids": [4, 5, 6]
}
"""
try:
data = request.get_json()
if not data:
return error_response(400, "请求参数不能为空")
tenant_id = data.get('tenant_id')
template_id = data.get('template_id')
input_field_ids = data.get('input_field_ids', [])
output_field_ids = data.get('output_field_ids', [])
if not tenant_id:
return error_response(400, "tenant_id参数不能为空")
if not 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()
cursor = conn.cursor()
try:
# 验证模板是否存在且属于该 tenant_id
cursor.execute("""
SELECT id FROM f_polic_file_config
WHERE id = %s AND tenant_id = %s AND state = 1
""", (template_id, tenant_id))
if not cursor.fetchone():
return error_response(400, f"模板ID {template_id} 不存在或不属于该租户")
# 合并所有字段ID
all_field_ids = set(input_field_ids + output_field_ids)
# 验证字段是否存在且属于该 tenant_id
if all_field_ids:
placeholders = ','.join(['%s'] * len(all_field_ids))
cursor.execute(f"""
SELECT id FROM f_polic_field
WHERE id IN ({placeholders}) AND tenant_id = %s AND state = 1
""", list(all_field_ids) + [tenant_id])
existing_field_ids = {row[0] for row in cursor.fetchall()}
invalid_field_ids = all_field_ids - existing_field_ids
if invalid_field_ids:
return error_response(400, f"字段ID {list(invalid_field_ids)} 不存在或不属于该租户")
# 删除该模板的所有现有关联关系(仅限该 tenant_id
cursor.execute("""
DELETE FROM f_polic_file_field
WHERE file_id = %s AND tenant_id = %s
""", (template_id, tenant_id))
# 插入新的关联关系
current_time = datetime.now()
created_by = 655162080928945152 # 默认创建者ID
if all_field_ids:
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, 1)
"""
for field_id in all_field_ids:
cursor.execute(insert_sql, (
tenant_id,
template_id,
field_id,
current_time,
created_by,
current_time,
created_by
))
conn.commit()
return success_response({
'template_id': template_id,
'input_field_count': len(input_field_ids),
'output_field_count': len(output_field_ids),
'total_field_count': len(all_field_ids)
}, "保存成功")
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/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__':
# 确保static目录存在
os.makedirs('static', exist_ok=True)
port = int(os.getenv('PORT', 7500))
debug = os.getenv('DEBUG', 'False').lower() == 'true'
print(f"服务启动在 http://localhost:{port}")
print(f"测试页面: http://localhost:{port}/")
print(f"模板字段管理页面: http://localhost:{port}/template-field-manager")
print(f"Swagger API文档: http://localhost:{port}/api-docs")
app.run(host='0.0.0.0', port=port, debug=debug)