ソースを参照

修复bug。支持tag多标签。统一graphall接口。

maxiaolong 4 日 前
コミット
600c85fc5d

ファイルの差分が大きいため隠しています
+ 0 - 3
.cursor/pending_tasks.json


+ 172 - 426
app/api/business_domain/routes.py

@@ -2,6 +2,7 @@
 Business Domain API 路由模块
 提供业务领域相关的 RESTful API 接口
 """
+
 import io
 import json
 import time
@@ -11,6 +12,7 @@ import urllib.parse
 from flask import request, jsonify, current_app, send_file
 from minio import Minio
 from minio.error import S3Error
+
 from app.api.business_domain import bp
 from app.models.result import success, failed
 from app.services.neo4j_driver import neo4j_driver
@@ -24,379 +26,253 @@ from app.core.business_domain import (
     business_domain_graph_all,
     business_domain_search_list,
     business_domain_compose,
-    business_domain_label_list
+    business_domain_label_list,
 )
 
 logger = logging.getLogger("app")
 
 
+# ----------------------- MinIO helpers -----------------------
 def get_minio_client():
     """获取 MinIO 客户端实例"""
     return Minio(
-        current_app.config['MINIO_HOST'],
-        access_key=current_app.config['MINIO_USER'],
-        secret_key=current_app.config['MINIO_PASSWORD'],
-        secure=current_app.config['MINIO_SECURE']
+        current_app.config["MINIO_HOST"],
+        access_key=current_app.config["MINIO_USER"],
+        secret_key=current_app.config["MINIO_PASSWORD"],
+        secure=current_app.config["MINIO_SECURE"],
     )
 
 
 def get_minio_config():
     """获取 MinIO 配置"""
     return {
-        'MINIO_BUCKET': current_app.config['MINIO_BUCKET'],
-        'PREFIX': current_app.config.get(
-            'BUSINESS_DOMAIN_PREFIX', 'business_domain'
+        "MINIO_BUCKET": current_app.config["MINIO_BUCKET"],
+        "PREFIX": current_app.config.get(
+            "BUSINESS_DOMAIN_PREFIX", "business_domain"
         ),
-        'ALLOWED_EXTENSIONS': current_app.config['ALLOWED_EXTENSIONS']
+        "ALLOWED_EXTENSIONS": current_app.config["ALLOWED_EXTENSIONS"],
     }
 
 
 def allowed_file(filename):
     """检查文件扩展名是否允许"""
-    if '.' not in filename:
+    if "." not in filename:
         return False
-    ext = filename.rsplit('.', 1)[1].lower()
-    return ext in get_minio_config()['ALLOWED_EXTENSIONS']
+    ext = filename.rsplit(".", 1)[1].lower()
+    return ext in get_minio_config()["ALLOWED_EXTENSIONS"]
 
 
-@bp.route('/list', methods=['POST'])
+# ----------------------- Business Domain APIs -----------------------
+@bp.route("/list", methods=["POST"])
 def bd_list():
-    """
-    获取业务领域列表
-    
-    请求参数 (JSON):
-        - current: 当前页码,默认1
-        - size: 每页大小,默认10
-        - name_en: 英文名称过滤条件(可选)
-        - name_zh: 中文名称过滤条件(可选)
-        - type: 类型过滤条件,默认'all'表示不过滤(可选)
-        - category: 分类过滤条件(可选)
-        - tag: 标签过滤条件(可选)
-        
-    返回:
-        - success: 是否成功
-        - message: 消息
-        - data: 
-            - records: 业务领域列表
-            - total: 总数量
-            - size: 每页大小
-            - current: 当前页码
-    """
+    """获取业务领域列表"""
     try:
-        # 获取分页和筛选参数
         if not request.json:
-            return jsonify(failed('请求数据不能为空'))
-        
-        page = int(request.json.get('current', 1))
-        page_size = int(request.json.get('size', 10))
-        name_en_filter = request.json.get('name_en')
-        name_zh_filter = request.json.get('name_zh')
-        type_filter = request.json.get('type', 'all')
-        category_filter = request.json.get('category')
-        tag_filter = request.json.get('tag')
-        
-        # 调用业务逻辑查询业务领域列表
+            return jsonify(failed("请求数据不能为空"))
+
+        page = int(request.json.get("current", 1))
+        page_size = int(request.json.get("size", 10))
+        name_en_filter = request.json.get("name_en")
+        name_zh_filter = request.json.get("name_zh")
+        type_filter = request.json.get("type", "all")
+        category_filter = request.json.get("category")
+        tag_filter = request.json.get("tag")
+
         domains, total_count = business_domain_list(
-            page, 
-            page_size, 
-            name_en_filter, 
-            name_zh_filter, 
-            type_filter, 
-            category_filter, 
-            tag_filter
+            page,
+            page_size,
+            name_en_filter,
+            name_zh_filter,
+            type_filter,
+            category_filter,
+            tag_filter,
+        )
+
+        return jsonify(
+            success(
+                {
+                    "records": domains,
+                    "total": total_count,
+                    "size": page_size,
+                    "current": page,
+                }
+            )
         )
-        
-        # 返回结果
-        return jsonify(success({
-            "records": domains,
-            "total": total_count,
-            "size": page_size,
-            "current": page
-        }))
     except Exception as e:
         logger.error(f"获取业务领域列表失败: {str(e)}")
         return jsonify(failed("获取业务领域列表失败", error=str(e)))
 
 
-@bp.route('/detail', methods=['POST'])
+@bp.route("/detail", methods=["POST"])
 def bd_detail():
-    """
-    获取业务领域详情
-    
-    请求参数 (JSON):
-        - id: 业务领域节点ID(必填)
-        
-    返回:
-        - success: 是否成功
-        - message: 消息
-        - data: 业务领域详情
-    """
+    """获取业务领域详情"""
     try:
-        # 获取参数
         if not request.json:
-            return jsonify(failed('请求数据不能为空'))
-        
-        domain_id = request.json.get('id')
-        
+            return jsonify(failed("请求数据不能为空"))
+
+        domain_id = request.json.get("id")
         if domain_id is None:
             return jsonify(failed("业务领域ID不能为空"))
-        
-        # 确保传入的ID为整数
+
         try:
             domain_id = int(domain_id)
         except (ValueError, TypeError):
             return jsonify(failed(f"业务领域ID必须为整数, 收到的是: {domain_id}"))
-        
-        # 调用业务逻辑查询业务领域详情
+
         domain_data = get_business_domain_by_id(domain_id)
-        
         if not domain_data:
             return jsonify(failed("业务领域不存在"))
-        
+
         return jsonify(success(domain_data))
     except Exception as e:
         logger.error(f"获取业务领域详情失败: {str(e)}")
         return jsonify(failed("获取业务领域详情失败", error=str(e)))
 
 
-@bp.route('/delete', methods=['POST'])
+@bp.route("/delete", methods=["POST"])
 def bd_delete():
-    """
-    删除业务领域
-    
-    请求参数 (JSON):
-        - id: 业务领域节点ID(必填)
-        
-    返回:
-        - success: 是否成功
-        - message: 消息
-        - data: 删除结果
-    """
+    """删除业务领域"""
     try:
-        # 获取参数
         if not request.json:
             return jsonify(failed("请求数据不能为空"))
-        
-        domain_id = request.json.get('id')
+
+        domain_id = request.json.get("id")
         if domain_id is None:
             return jsonify(failed("业务领域ID不能为空"))
-        
-        # 调用业务逻辑删除业务领域
+
         result = delete_business_domain(domain_id)
-        
         if result:
             return jsonify(success({"message": "业务领域删除成功"}))
-        else:
-            return jsonify(failed("业务领域删除失败"))
+        return jsonify(failed("业务领域删除失败"))
     except Exception as e:
         logger.error(f"删除业务领域失败: {str(e)}")
         return jsonify(failed("删除业务领域失败", error=str(e)))
 
 
-@bp.route('/save', methods=['POST'])
+@bp.route("/save", methods=["POST"])
 def bd_save():
-    """
-    保存业务领域(新建或更新)
-
-    请求参数 (JSON):
-        - id: 业务领域节点ID(可选,有则更新,无则新建)
-        - name_zh: 中文名称(新建时必填)
-        - name_en: 英文名称(新建时必填)
-        - describe: 描述(可选)
-        - type: 类型(可选)
-        - category: 分类(可选)
-        - tag: 标签ID(可选)
-        - data_source: 数据源ID(可选)
-        - 其他属性字段...
-
-    返回:
-        - success: 是否成功
-        - message: 消息
-        - data: 保存后的业务领域数据
-    """
+    """保存业务领域(新建或更新)"""
     try:
-        # 获取保存数据
         data = request.json
-
         if not data:
             return jsonify(failed("请求数据不能为空"))
 
-        # 新建时校验必填字段
         if not data.get("id"):
             if not data.get("name_zh") or not data.get("name_en"):
                 return jsonify(failed("新建时 name_zh 和 name_en 为必填项"))
 
-        # 调用业务逻辑保存业务领域
         saved_data = save_business_domain(data)
-
         return jsonify(success(saved_data))
     except Exception as e:
         logger.error(f"保存业务领域失败: {str(e)}")
         return jsonify(failed("保存业务领域失败", error=str(e)))
 
 
-@bp.route('/update', methods=['POST'])
+@bp.route("/update", methods=["POST"])
 def bd_update():
-    """
-    更新业务领域
-
-    请求参数 (JSON):
-        - id: 业务领域节点ID(必填)
-        - name_zh: 中文名称(可选)
-        - name_en: 英文名称(可选)
-        - describe: 描述(可选)
-        - tag: 标签ID(可选)
-        - data_source: 数据源ID(可选)
-        - 其他属性字段...
-
-    返回:
-        - success: 是否成功
-        - message: 消息
-        - data: 更新后的业务领域数据
-    """
+    """更新业务领域"""
     try:
-        # 获取更新数据
         data = request.json
-
         if not data or "id" not in data:
             return jsonify(failed("参数不完整"))
 
-        # 调用业务逻辑更新业务领域
         updated_data = update_business_domain(data)
-
         return jsonify(success(updated_data))
     except Exception as e:
         logger.error(f"更新业务领域失败: {str(e)}")
         return jsonify(failed("更新业务领域失败", error=str(e)))
 
 
-@bp.route('/upload', methods=['POST'])
+@bp.route("/upload", methods=["POST"])
 def bd_upload():
-    """
-    上传业务领域相关文件
-
-    请求参数 (multipart/form-data):
-        - file: 上传的文件(必填)
-
-    返回:
-        - success: 是否成功
-        - message: 消息
-        - data:
-            - filename: 原始文件名
-            - size: 文件大小(字节)
-            - type: 文件类型
-            - url: 文件存储路径
-    """
+    """上传业务领域相关文件"""
+    response = None
     try:
-        # 检查请求中是否有文件
-        if 'file' not in request.files:
+        if "file" not in request.files:
             return jsonify(failed("没有找到上传的文件"))
 
-        file = request.files['file']
-
-        # 检查文件名
-        if file.filename == '':
+        file = request.files["file"]
+        if file.filename == "":
             return jsonify(failed("未选择文件"))
-
-        # 检查文件类型
         if not allowed_file(file.filename):
             return jsonify(failed("不支持的文件类型"))
 
-        # 获取 MinIO 配置
         minio_client = get_minio_client()
         config = get_minio_config()
 
-        # 读取文件内容
         file_content = file.read()
         file_size = len(file_content)
-        filename = file.filename or ''
-        file_type = filename.rsplit('.', 1)[1].lower()
-
-        # 提取文件名(不包含扩展名)
-        filename_without_ext = filename.rsplit('.', 1)[0]
-
-        # 生成紧凑的时间戳 (yyyyMMddHHmmss)
+        filename = file.filename or ""
+        file_type = filename.rsplit(".", 1)[1].lower()
+        filename_without_ext = filename.rsplit(".", 1)[0]
         timestamp = time.strftime("%Y%m%d%H%M%S", time.localtime())
 
-        # 生成唯一文件名
-        prefix = config['PREFIX']
         object_name = (
-            f"{prefix}/{filename_without_ext}_{timestamp}.{file_type}"
+            f"{config['PREFIX']}/"
+            f"{filename_without_ext}_{timestamp}.{file_type}"
         )
 
-        # 上传文件到 MinIO
         minio_client.put_object(
-            config['MINIO_BUCKET'],
+            config["MINIO_BUCKET"],
             object_name,
             io.BytesIO(file_content),
             file_size,
-            content_type=f"application/{file_type}"
+            content_type=f"application/{file_type}",
         )
 
         logger.info(f"文件上传成功: {object_name}, 大小: {file_size}")
 
-        # 返回结果
-        return jsonify(success({
-            "filename": file.filename,
-            "size": file_size,
-            "type": file_type,
-            "url": object_name
-        }))
+        return jsonify(
+            success(
+                {
+                    "filename": file.filename,
+                    "size": file_size,
+                    "type": file_type,
+                    "url": object_name,
+                }
+            )
+        )
     except Exception as e:
         logger.error(f"文件上传失败: {str(e)}")
         return jsonify(failed("文件上传失败", error=str(e)))
+    finally:
+        if response:
+            response.close()
+            response.release_conn()
 
 
-@bp.route('/download', methods=['GET'])
+@bp.route("/download", methods=["GET"])
 def bd_download():
-    """
-    下载业务领域相关文件
-
-    请求参数 (URL Query):
-        - url: 文件存储路径(必填)
-
-    返回:
-        - 文件流(作为附件下载)
-    """
+    """下载业务领域相关文件"""
     response = None
     try:
-        # 获取文件路径参数
-        object_name = request.args.get('url')
+        object_name = request.args.get("url")
         if not object_name:
             return jsonify(failed("文件路径不能为空"))
 
-        # URL解码,处理特殊字符
         object_name = urllib.parse.unquote(object_name)
-
-        # 记录下载请求信息,便于调试
         logger.info(f"下载文件请求: {object_name}")
 
-        # 获取 MinIO 配置
         minio_client = get_minio_client()
         config = get_minio_config()
 
-        # 获取文件
         try:
             response = minio_client.get_object(
-                config['MINIO_BUCKET'], object_name
+                config["MINIO_BUCKET"], object_name
             )
             file_data = response.read()
         except S3Error as e:
             logger.error(f"MinIO获取文件失败: {str(e)}")
             return jsonify(failed(f"文件获取失败: {str(e)}"))
 
-        # 获取文件名
-        file_name = object_name.split('/')[-1]
-
-        # 直接从内存返回文件,不创建临时文件
+        file_name = object_name.split("/")[-1]
         file_stream = io.BytesIO(file_data)
 
-        # 返回文件
         return send_file(
             file_stream,
             as_attachment=True,
             download_name=file_name,
-            mimetype="application/octet-stream"
+            mimetype="application/octet-stream",
         )
     except Exception as e:
         logger.error(f"文件下载失败: {str(e)}")
@@ -407,119 +283,68 @@ def bd_download():
             response.release_conn()
 
 
-@bp.route('/graphall', methods=['POST'])
+@bp.route("/graphall", methods=["POST"])
 def bd_graph_all():
-    """
-    获取业务领域完整关系图谱
-
-    请求参数 (JSON):
-        - id: 业务领域节点ID(必填)
-        - meta: 是否包含元数据节点,默认True(可选)
-
-    返回:
-        - success: 是否成功
-        - message: 消息
-        - data:
-            - nodes: 节点列表
-            - lines: 关系列表
-    """
+    """获取业务领域完整关系图谱"""
     try:
-        # 获取参数
         if not request.json:
-            return jsonify(failed('请求数据不能为空'))
-
-        domain_id = request.json.get('id')
-        include_meta = request.json.get('meta', True)
+            return jsonify(failed("请求数据不能为空"))
 
+        domain_id = request.json.get("id")
+        include_meta = request.json.get("meta", True)
         if domain_id is None:
             return jsonify(failed("业务领域ID不能为空"))
 
-        # 确保传入的ID为整数
         try:
             domain_id = int(domain_id)
         except (ValueError, TypeError):
-            return jsonify(failed(
-                f"业务领域ID必须为整数, 收到的是: {domain_id}"
-            ))
+            return jsonify(failed(f"业务领域ID必须为整数, 收到的是: {domain_id}"))
 
-        # 调用业务逻辑获取完整图谱
         graph_data = business_domain_graph_all(domain_id, include_meta)
-
         return jsonify(success(graph_data))
     except Exception as e:
         logger.error(f"获取业务领域图谱失败: {str(e)}")
         return jsonify(failed("获取业务领域图谱失败", error=str(e)))
 
 
-@bp.route('/ddlparse', methods=['POST'])
+@bp.route("/ddlparse", methods=["POST"])
 def bd_ddl_parse():
-    """
-    解析DDL语句,用于业务领域创建
-
-    请求参数:
-        - file: SQL文件(multipart/form-data,可选)
-        - sql: SQL内容(JSON,可选)
-        至少提供其中一种方式
-
-    返回:
-        - success: 是否成功
-        - message: 消息
-        - data: 解析后的DDL列表,包含表信息和字段信息
-    """
+    """解析DDL语句,用于业务领域创建"""
     try:
-        # 获取参数 - 支持两种方式:上传文件或JSON
-        sql_content = ''
+        sql_content = ""
 
-        # 检查是否有文件上传
-        if 'file' in request.files:
-            file = request.files['file']
-            # 检查文件是否存在且文件名不为空
+        if "file" in request.files:
+            file = request.files["file"]
             if file and file.filename:
-                # 检查是否是SQL文件
-                if not file.filename.lower().endswith('.sql'):
+                if not file.filename.lower().endswith(".sql"):
                     return jsonify(failed("只接受SQL文件"))
-
-                # 读取文件内容
-                sql_content = file.read().decode('utf-8')
-                logger.info(
-                    f"从上传的文件中读取SQL内容,文件名: {file.filename}"
-                )
-        # 如果没有文件上传,检查是否有JSON输入
+                sql_content = file.read().decode("utf-8")
+                logger.info(f"从上传的文件中读取SQL内容,文件名: {file.filename}")
         elif request.is_json and request.json:
-            sql_content = request.json.get('sql', '')
+            sql_content = request.json.get("sql", "")
 
-        # 如果两种方式都没有提供SQL内容,则返回错误
         if not sql_content:
-            return jsonify(failed(
-                "SQL内容不能为空,请上传SQL文件或提供SQL内容"
-            ))
+            return jsonify(failed("SQL内容不能为空,请上传SQL文件或提供SQL内容"))
 
         parser = DDLParser()
-        # 提取创建表的DDL语句
         ddl_list = parser.parse_ddl(sql_content)
-
         if not ddl_list:
             return jsonify(failed("未找到有效的CREATE TABLE语句"))
 
-        # 处理表的存在状态
         if isinstance(ddl_list, list):
-            # 新格式:数组格式
-            # 获取所有表名
             table_names = []
             for table_item in ddl_list:
-                if isinstance(table_item, dict) and 'table_info' in table_item:
-                    table_name = table_item['table_info'].get('name_en')
+                if isinstance(table_item, dict) and "table_info" in table_item:
+                    table_name = table_item["table_info"].get("name_en")
                     if table_name:
                         table_names.append(table_name)
 
-            # 首先为所有表设置默认的exist状态
             for table_item in ddl_list:
                 if isinstance(table_item, dict):
                     table_item["exist"] = False
 
             if table_names:
                 try:
-                    # 查询业务领域是否存在
                     with neo4j_driver.get_session() as session:
                         table_query = """
                         UNWIND $names AS name
@@ -530,30 +355,26 @@ def bd_ddl_parse():
                             table_query, names=table_names
                         )
 
-                        # 创建存在状态映射
                         exist_map = {}
                         for record in table_results:
-                            table_name = record["name"]
+                            t_name = record["name"]
                             exists = record["exists"]
-                            exist_map[table_name] = exists
+                            exist_map[t_name] = exists
 
-                        # 更新存在的表的状态
                         for table_item in ddl_list:
-                            if (isinstance(table_item, dict)
-                                    and 'table_info' in table_item):
-                                info = table_item['table_info']
-                                t_name = info.get('name_en')
+                            if (
+                                isinstance(table_item, dict)
+                                and "table_info" in table_item
+                            ):
+                                info = table_item["table_info"]
+                                t_name = info.get("name_en")
                                 if t_name and t_name in exist_map:
                                     table_item["exist"] = exist_map[t_name]
                 except Exception as e:
                     logger.error(f"检查业务领域存在状态失败: {str(e)}")
-                    # 如果查询失败,所有表保持默认的False状态
 
         elif isinstance(ddl_list, dict):
-            # 兼容旧格式:字典格式(以表名为key)
             table_names = list(ddl_list.keys())
-
-            # 首先为所有表设置默认的exist状态
             for table_name in table_names:
                 if isinstance(ddl_list[table_name], dict):
                     ddl_list[table_name]["exist"] = False
@@ -565,7 +386,6 @@ def bd_ddl_parse():
 
             if table_names:
                 try:
-                    # 查询业务领域是否存在
                     with neo4j_driver.get_session() as session:
                         table_query = """
                         UNWIND $names AS name
@@ -576,24 +396,21 @@ def bd_ddl_parse():
                             table_query, names=table_names
                         )
 
-                        # 更新存在的表的状态
                         for record in table_results:
-                            table_name = record["name"]
+                            t_name = record["name"]
                             exists = record["exists"]
                             is_valid = (
-                                table_name in ddl_list
-                                and isinstance(ddl_list[table_name], dict)
+                                t_name in ddl_list
+                                and isinstance(ddl_list[t_name], dict)
                             )
                             if is_valid:
-                                ddl_list[table_name]["exist"] = exists
+                                ddl_list[t_name]["exist"] = exists
                 except Exception as e:
                     logger.error(f"检查业务领域存在状态失败: {str(e)}")
-                    # 如果查询失败,所有表保持默认的False状态
 
         logger.debug(
             f"识别到的DDL语句: {json.dumps(ddl_list, ensure_ascii=False)}"
         )
-
         return jsonify(success(ddl_list))
     except Exception as e:
         logger.error(f"解析DDL语句失败: {str(e)}")
@@ -601,58 +418,30 @@ def bd_ddl_parse():
         return jsonify(failed("解析DDL语句失败", error=str(e)))
 
 
-@bp.route('/search', methods=['POST'])
+@bp.route("/search", methods=["POST"])
 def bd_search():
-    """
-    搜索业务领域关联的元数据
-
-    请求参数 (JSON):
-        - id: 业务领域节点ID(必填)
-        - current: 当前页码,默认1
-        - size: 每页大小,默认10
-        - name_en: 英文名称过滤条件(可选)
-        - name_zh: 中文名称过滤条件(可选)
-        - category: 分类过滤条件(可选)
-        - tag: 标签过滤条件(可选)
-
-    返回:
-        - success: 是否成功
-        - message: 消息
-        - data:
-            - records: 元数据列表
-            - total: 总数量
-            - size: 每页大小
-            - current: 当前页码
-    """
+    """搜索业务领域关联的元数据"""
     try:
-        # 获取分页和筛选参数
         if not request.json:
-            return jsonify(failed('请求数据不能为空'))
+            return jsonify(failed("请求数据不能为空"))
 
-        page = int(request.json.get('current', 1))
-        page_size = int(request.json.get('size', 10))
-        domain_id = request.json.get('id')
+        page = int(request.json.get("current", 1))
+        page_size = int(request.json.get("size", 10))
+        domain_id = request.json.get("id")
 
-        name_en_filter = request.json.get('name_en')
-        name_zh_filter = request.json.get('name_zh')
-        category_filter = request.json.get('category')
-        tag_filter = request.json.get('tag')
+        name_en_filter = request.json.get("name_en")
+        name_zh_filter = request.json.get("name_zh")
+        category_filter = request.json.get("category")
+        tag_filter = request.json.get("tag")
 
         if domain_id is None:
             return jsonify(failed("业务领域ID不能为空"))
 
-        # 确保传入的ID为整数
         try:
             domain_id = int(domain_id)
         except (ValueError, TypeError):
-            return jsonify(failed(
-                f"业务领域ID必须为整数, 收到的是: {domain_id}"
-            ))
-
-        # 记录请求信息
-        logger.info(f"获取业务领域关联元数据请求,ID: {domain_id}")
+            return jsonify(failed(f"业务领域ID必须为整数, 收到的是: {domain_id}"))
 
-        # 调用业务逻辑查询关联元数据
         metadata_list, total_count = business_domain_search_list(
             domain_id,
             page,
@@ -660,121 +449,78 @@ def bd_search():
             name_en_filter,
             name_zh_filter,
             category_filter,
-            tag_filter
+            tag_filter,
         )
 
-        # 返回结果
-        return jsonify(success({
-            "records": metadata_list,
-            "total": total_count,
-            "size": page_size,
-            "current": page
-        }))
+        return jsonify(
+            success(
+                {
+                    "records": metadata_list,
+                    "total": total_count,
+                    "size": page_size,
+                    "current": page,
+                }
+            )
+        )
     except Exception as e:
         logger.error(f"业务领域关联元数据搜索失败: {str(e)}")
         return jsonify(failed("业务领域关联元数据搜索失败", error=str(e)))
 
 
-@bp.route('/compose', methods=['POST'])
+@bp.route("/compose", methods=["POST"])
 def bd_compose():
-    """
-    从已有业务领域中组合创建新的业务领域
-
-    请求参数 (JSON):
-        - name_zh: 中文名称(必填)
-        - name_en: 英文名称(可选,不提供则自动翻译)
-        - id_list: 关联的业务领域和元数据列表(必填)
-            格式: [{"domain_id": 123, "metaData": [{"id": 456}, ...]}]
-        - describe: 描述(可选)
-        - type: 类型(可选)
-        - category: 分类(可选)
-        - tag: 标签ID(可选)
-        - data_source: 数据源ID(可选)
-
-    返回:
-        - success: 是否成功
-        - message: 消息
-        - data: 创建后的业务领域数据
-    """
+    """从已有业务领域中组合创建新的业务领域"""
     try:
-        # 获取请求数据
         data = request.json
-
         if not data:
             return jsonify(failed("请求数据不能为空"))
 
-        # 校验必填字段
         if not data.get("name_zh"):
             return jsonify(failed("name_zh 为必填项"))
-
         if not data.get("id_list"):
             return jsonify(failed("id_list 为必填项"))
 
-        # 调用业务逻辑组合创建业务领域
         result_data = business_domain_compose(data)
-
-        # 构建响应数据
-        response_data = {
-            "business_domain": result_data
-        }
-
+        response_data = {"business_domain": result_data}
         return jsonify(success(response_data))
     except Exception as e:
         logger.error(f"组合创建业务领域失败: {str(e)}")
         return jsonify(failed("组合创建业务领域失败", error=str(e)))
 
 
-@bp.route('/labellist', methods=['POST'])
+@bp.route("/labellist", methods=["POST"])
 def bd_label_list():
-    """
-    获取数据标签列表(用于业务领域关联)
-
-    请求参数 (JSON):
-        - current: 当前页码,默认1
-        - size: 每页大小,默认10
-        - name_en: 英文名称过滤条件(可选)
-        - name_zh: 中文名称过滤条件(可选)
-        - category: 分类过滤条件(可选)
-        - group: 分组过滤条件(可选)
-
-    返回:
-        - success: 是否成功
-        - message: 消息
-        - data:
-            - records: 标签列表
-            - total: 总数量
-            - size: 每页大小
-            - current: 当前页码
-    """
+    """获取数据标签列表(用于业务领域关联)"""
     try:
-        # 获取分页和筛选参数
         if not request.json:
-            return jsonify(failed('请求数据不能为空'))
+            return jsonify(failed("请求数据不能为空"))
 
-        page = int(request.json.get('current', 1))
-        page_size = int(request.json.get('size', 10))
-        name_en_filter = request.json.get('name_en')
-        name_zh_filter = request.json.get('name_zh')
-        category_filter = request.json.get('category')
-        group_filter = request.json.get('group')
+        page = int(request.json.get("current", 1))
+        page_size = int(request.json.get("size", 10))
+        name_en_filter = request.json.get("name_en")
+        name_zh_filter = request.json.get("name_zh")
+        category_filter = request.json.get("category")
+        group_filter = request.json.get("group")
 
-        # 调用业务逻辑查询标签列表
         labels, total_count = business_domain_label_list(
             page,
             page_size,
             name_en_filter,
             name_zh_filter,
             category_filter,
-            group_filter
+            group_filter,
         )
 
-        # 返回结果
-        return jsonify(success({
-            "records": labels,
-            "total": total_count,
-            "size": page_size,
-            "current": page
-        }))
+        return jsonify(
+            success(
+                {
+                    "records": labels,
+                    "total": total_count,
+                    "size": page_size,
+                    "current": page,
+                }
+            )
+        )
     except Exception as e:
         logger.error(f"获取标签列表失败: {str(e)}")
         return jsonify(failed("获取标签列表失败", error=str(e)))

+ 32 - 14
app/api/data_flow/routes.py

@@ -1,4 +1,4 @@
-from flask import request, jsonify
+from flask import request
 from app.api.data_flow import bp
 from app.core.data_flow.dataflows import DataFlowService
 import logging
@@ -9,6 +9,7 @@ from app.core.graph.graph_operations import MyEncoder
 
 logger = logging.getLogger(__name__)
 
+
 @bp.route('/get-dataflows-list', methods=['GET'])
 def get_dataflows():
     """获取数据流列表"""
@@ -16,8 +17,12 @@ def get_dataflows():
         page = request.args.get('page', 1, type=int)
         page_size = request.args.get('page_size', 10, type=int)
         search = request.args.get('search', '')
-        
-        result = DataFlowService.get_dataflows(page=page, page_size=page_size, search=search)
+
+        result = DataFlowService.get_dataflows(
+            page=page,
+            page_size=page_size,
+            search=search,
+        )
         res = success(result, "success")
         return json.dumps(res, ensure_ascii=False, cls=MyEncoder)
     except Exception as e:
@@ -25,6 +30,7 @@ def get_dataflows():
         res = failed(f'获取数据流列表失败: {str(e)}')
         return json.dumps(res, ensure_ascii=False, cls=MyEncoder)
 
+
 @bp.route('/get-dataflow/<int:dataflow_id>', methods=['GET'])
 def get_dataflow(dataflow_id):
     """根据ID获取数据流详情"""
@@ -41,6 +47,7 @@ def get_dataflow(dataflow_id):
         res = failed(f'获取数据流详情失败: {str(e)}')
         return json.dumps(res, ensure_ascii=False, cls=MyEncoder)
 
+
 @bp.route('/add-dataflow', methods=['POST'])
 def create_dataflow():
     """创建新的数据流"""
@@ -49,7 +56,7 @@ def create_dataflow():
         if not data:
             res = failed("请求数据不能为空", code=400)
             return json.dumps(res, ensure_ascii=False, cls=MyEncoder)
-            
+
         result = DataFlowService.create_dataflow(data)
         res = success(result, "数据流创建成功")
         return json.dumps(res, ensure_ascii=False, cls=MyEncoder)
@@ -62,6 +69,7 @@ def create_dataflow():
         res = failed(f'创建数据流失败: {str(e)}')
         return json.dumps(res, ensure_ascii=False, cls=MyEncoder)
 
+
 @bp.route('/update-dataflow/<int:dataflow_id>', methods=['PUT'])
 def update_dataflow(dataflow_id):
     """更新数据流"""
@@ -70,7 +78,7 @@ def update_dataflow(dataflow_id):
         if not data:
             res = failed("请求数据不能为空", code=400)
             return json.dumps(res, ensure_ascii=False, cls=MyEncoder)
-            
+
         result = DataFlowService.update_dataflow(dataflow_id, data)
         if result:
             res = success(result, "数据流更新成功")
@@ -83,6 +91,7 @@ def update_dataflow(dataflow_id):
         res = failed(f'更新数据流失败: {str(e)}')
         return json.dumps(res, ensure_ascii=False, cls=MyEncoder)
 
+
 @bp.route('/delete-dataflow/<int:dataflow_id>', methods=['DELETE'])
 def delete_dataflow(dataflow_id):
     """删除数据流"""
@@ -99,6 +108,7 @@ def delete_dataflow(dataflow_id):
         res = failed(f'删除数据流失败: {str(e)}')
         return json.dumps(res, ensure_ascii=False, cls=MyEncoder)
 
+
 @bp.route('/execute-dataflow/<int:dataflow_id>', methods=['POST'])
 def execute_dataflow(dataflow_id):
     """执行数据流"""
@@ -112,6 +122,7 @@ def execute_dataflow(dataflow_id):
         res = failed(f'执行数据流失败: {str(e)}')
         return json.dumps(res, ensure_ascii=False, cls=MyEncoder)
 
+
 @bp.route('/get-dataflow-status/<int:dataflow_id>', methods=['GET'])
 def get_dataflow_status(dataflow_id):
     """获取数据流执行状态"""
@@ -124,14 +135,19 @@ def get_dataflow_status(dataflow_id):
         res = failed(f'获取数据流状态失败: {str(e)}')
         return json.dumps(res, ensure_ascii=False, cls=MyEncoder)
 
+
 @bp.route('/get-dataflow-logs/<int:dataflow_id>', methods=['GET'])
 def get_dataflow_logs(dataflow_id):
     """获取数据流执行日志"""
     try:
         page = request.args.get('page', 1, type=int)
         page_size = request.args.get('page_size', 50, type=int)
-        
-        result = DataFlowService.get_dataflow_logs(dataflow_id, page=page, page_size=page_size)
+
+        result = DataFlowService.get_dataflow_logs(
+            dataflow_id,
+            page=page,
+            page_size=page_size,
+        )
         res = success(result, "success")
         return json.dumps(res, ensure_ascii=False, cls=MyEncoder)
     except Exception as e:
@@ -139,6 +155,7 @@ def get_dataflow_logs(dataflow_id):
         res = failed(f'获取数据流日志失败: {str(e)}')
         return json.dumps(res, ensure_ascii=False, cls=MyEncoder)
 
+
 @bp.route('/create-script', methods=['POST'])
 def create_script():
     """使用Deepseek模型生成脚本"""
@@ -147,20 +164,20 @@ def create_script():
         if not json_data:
             res = failed("请求数据不能为空", code=400)
             return json.dumps(res, ensure_ascii=False, cls=MyEncoder)
-        
+
         # 记录接收到的数据用于调试
         logger.info(f"create_script接收到的数据: {json_data}")
         logger.info(f"json_data类型: {type(json_data)}")
-        
+
         # 直接使用前端提交的json_data作为request_data参数
         script_content = DataFlowService.create_script(json_data)
-        
+
         result_data = {
             'script_content': script_content,
             'format': 'txt',
             'generated_at': datetime.now().isoformat()
         }
-        
+
         res = success(result_data, "脚本生成成功")
         return json.dumps(res, ensure_ascii=False, cls=MyEncoder)
     except ValueError as ve:
@@ -172,18 +189,19 @@ def create_script():
         res = failed(f'脚本生成失败: {str(e)}')
         return json.dumps(res, ensure_ascii=False, cls=MyEncoder)
 
+
 @bp.route('/get-BD-list', methods=['GET'])
 def get_business_domain_list():
     """获取BusinessDomain节点列表"""
     try:
         logger.info("接收到获取BusinessDomain列表请求")
-        
+
         # 调用服务层函数获取BusinessDomain列表
         bd_list = DataFlowService.get_business_domain_list()
-        
+
         res = success(bd_list, "操作成功")
         return json.dumps(res, ensure_ascii=False, cls=MyEncoder)
     except Exception as e:
         logger.error(f"获取BusinessDomain列表失败: {str(e)}")
         res = failed(f'获取BusinessDomain列表失败: {str(e)}', 500, {})
-        return json.dumps(res, ensure_ascii=False, cls=MyEncoder) 
+        return json.dumps(res, ensure_ascii=False, cls=MyEncoder)

ファイルの差分が大きいため隠しています
+ 365 - 316
app/core/business_domain/business_domain.py


+ 107 - 0
docs/api_interface_labellist.md

@@ -0,0 +1,107 @@
+# /labellist 接口说明(DataLabel 列表查询)
+
+本文档描述数据接口模块提供的 DataLabel 列表查询接口,便于前端接入与调试。
+
+## 基本信息
+- **URL**:`/labellist`(最终完整路径取决于 `data_interface` 蓝图注册前缀,例如 `/api/data_interface/labellist`)
+- **方法**:`POST`
+- **内容类型**:`application/json`
+- **返回格式**:`application/json`
+
+## 请求参数(JSON Body)
+| 字段 | 类型 | 必填 | 说明 |
+| --- | --- | --- | --- |
+| current | int | 否 | 页码,默认 1 |
+| size | int | 否 | 每页条数,默认 10 |
+| name_en | str | 否 | 标签英文名模糊匹配 |
+| name_zh | str | 否 | 标签中文名模糊匹配 |
+| category_filter | dict/list/str | 否 | 分类过滤。支持:<br>- `dict`:键为属性名,值为匹配内容,如 `{ "category": "质量", "scope": "公共" }`<br>- `list`:元素为 `{ "field": "...", "value": "..." }` 或单键值对,如 `[{"field":"category","value":"质量"},{"group":"公共"}]`<br>- `str`:等同于按 `category` 字段模糊匹配 |
+| group | str | 否 | 分组名模糊匹配 |
+
+说明:
+- 所有字符串匹配均使用 Cypher `CONTAINS`(大小写敏感视 Neo4j 配置而定)。
+- `category_filter` 会按提供的多个条件叠加 `AND` 过滤。
+
+## 响应字段
+成功时(`code=200`,`message="success"`):
+```json
+{
+  "code": 200,
+  "message": "success",
+  "data": {
+    "records": [
+      {
+        "id": 123,
+        "name_zh": "示例标签",
+        "name_en": "sample_label",
+        "category": "质量",
+        "group": "公共",
+        "describe": null,
+        "scope": null,
+        "number": 4
+      }
+    ],
+    "total": 57,
+    "size": 10,
+    "current": 1
+  }
+}
+```
+
+失败时(例如参数错误或 Neo4j 异常):
+```json
+{
+  "code": 500,
+  "message": "错误描述",
+  "data": {}
+}
+```
+
+## 请求示例
+```http
+POST /labellist
+Content-Type: application/json
+
+{
+  "current": 1,
+  "size": 10,
+  "name_zh": "标签",
+  "category_filter": [
+    {"field": "category", "value": "质量"},
+    {"field": "scope", "value": "公共"}
+  ],
+  "group": "模型"
+}
+```
+
+## 返回示例
+```json
+{
+  "code": 200,
+  "message": "success",
+  "data": {
+    "records": [
+      {
+        "id": 321,
+        "name_zh": "标签-质量",
+        "name_en": "label_quality",
+        "category": "质量",
+        "group": "模型",
+        "describe": null,
+        "scope": null,
+        "number": 2
+      }
+    ],
+    "total": 5,
+    "size": 10,
+    "current": 1
+  }
+}
+```
+
+## 前端对接提示
+- 必须以 `POST + JSON` 调用;若使用 Fetch/axios,设置 `headers: { "Content-Type": "application/json" }`。
+- 分页字段 `current`、`size` 需为整数,未传时后端使用默认值。
+- `category_filter` 支持多条件 AND 过滤,请确保字段名为合法的 Neo4j 属性名(只含字母、数字、下划线,且非数字开头)。
+- 返回的 `number` 字段表示该标签入度+出度关系数量,可用于前端展示关联数。
+

+ 82 - 0
docs/api_meta_node_graph.md

@@ -0,0 +1,82 @@
+# /node/graph 接口前端操作指南(元数据图谱查询)
+
+## 基本信息
+- **URL**:`/node/graph`(实际完整路径取决于 meta_data 蓝图前缀,例如 `/api/meta_data/node/graph`)
+- **方法**:`POST`
+- **请求体**:`application/json`
+- **返回格式**:`application/json`
+
+## 功能概述
+提交一个 `nodeId`,返回:
+- `node`:该节点的属性信息(即便没有任何关联关系也会返回)。
+-.`related_nodes`:与该节点存在关系的其他节点(包含属性)。
+- `relationships`:关系列表,含 `source`、`target`、`type`、`id`。
+
+## 请求参数
+| 字段 | 类型 | 必填 | 说明 |
+| --- | --- | --- | --- |
+| nodeId | int | 是 | Neo4j 节点 ID,必须为整数 |
+
+错误时返回:
+- `code=500`,`message` 为错误描述(如 `nodeId 必须为整数`)。
+
+## 响应结构
+成功(`code=200`,`message="success"`)示例:
+```json
+{
+  "code": 200,
+  "message": "success",
+  "data": {
+    "node": {
+      "id": 123,
+      "name_zh": "示例节点",
+      "name_en": "sample_node",
+      "...": "其他属性"
+    },
+    "related_nodes": [
+      {
+        "id": 456,
+        "name_zh": "关联节点A",
+        "...": "其他属性"
+      }
+    ],
+    "relationships": [
+      {
+        "id": 789,
+        "source": 123,
+        "target": 456,
+        "type": "REL_TYPE"
+      }
+    ]
+  }
+}
+```
+
+当节点存在但无任何关系时:
+```json
+{
+  "code": 200,
+  "message": "success",
+  "data": {
+    "node": { "id": 123, "...": "节点属性" },
+    "related_nodes": [],
+    "relationships": []
+  }
+}
+```
+
+## 调用示例
+```http
+POST /node/graph
+Content-Type: application/json
+
+{ "nodeId": 123 }
+```
+
+## 前端接入提示
+- 使用 `POST` 且 `Content-Type: application/json`。
+- `nodeId` 必须为整数;非整数后端会返回错误。
+- 前端可直接用 `data.node` 展示当前节点属性;`data.related_nodes` 渲染侧边列表或标签;`data.relationships` 可用于绘制边。
+- 关系的 `source` 和 `target` 均为 Neo4j 节点 ID,可与 `node` 与 `related_nodes` 对应。
+- 若需区分箭头方向或类型,请使用 `type` 字段做样式映射。
+

+ 4 - 3
scripts/auto_execute_tasks.py

@@ -79,15 +79,16 @@ CHAT_INPUT_POS: Optional[Tuple[int, int]] = None
 # 数据库操作
 # ============================================================================
 def get_db_connection():
-    """获取数据库连接"""
+    """获取数据库连接(使用 production 环境配置)"""
     try:
         import psycopg2
         import sys
 
         sys.path.insert(0, str(WORKSPACE_ROOT))
-        from app.config.config import config, current_env
+        from app.config.config import config
 
-        app_config = config.get(current_env, config['default'])
+        # 强制使用 production 环境的数据库配置
+        app_config = config['production']
         db_uri = app_config.SQLALCHEMY_DATABASE_URI
         return psycopg2.connect(db_uri)
 

+ 12 - 12
scripts/start_task_scheduler.bat

@@ -33,9 +33,9 @@ if not exist "scripts\auto_execute_tasks.py" (
     exit /b 1
 )
 
-REM 检查数据库配置是否存在
-if not exist "mcp-servers\task-manager\config.json" (
-    echo [错误] 未找到数据库配置: mcp-servers\task-manager\config.json
+REM 检查项目配置文件是否存在
+if not exist "app\config\config.py" (
+    echo [错误] 未找到项目配置文件: app\config\config.py
     pause
     exit /b 1
 )
@@ -47,13 +47,13 @@ echo [信息] 当前目录: %cd%
 echo.
 echo 请选择运行模式:
 echo.
-echo   1. 前台运行(可以看到实时日志,按 Ctrl+C 停止)
-echo   2. 后台运行(无窗口,日志输出到 logs\auto_execute.log)
-echo   3. 执行一次(只检查一次 pending 任务)
-echo   4. 前台运行 + 启用自动 Chat
-echo   5. 后台运行 + 启用自动 Chat
+echo   1. 前台运行
+echo   2. 后台运行
+echo   3. 单次执行
+echo   4. 前台运行 + 启用自动Chat
+echo   5. 后台运行 + 启用自动Chat
 echo   6. 查看服务状态
-echo   7. 停止服务
+echo   7. stop_service
 echo   0. 退出
 echo.
 
@@ -74,7 +74,7 @@ exit /b 1
 
 :run_foreground
 echo.
-echo [启动] 前台运行模式(检查间隔: 5分钟)
+echo [启动] 前台运行模式,检查间隔: 5分钟
 echo [提示] 按 Ctrl+C 可停止服务
 echo.
 python scripts\auto_execute_tasks.py --interval 300
@@ -83,7 +83,7 @@ goto :exit
 
 :run_background
 echo.
-echo [启动] 后台运行模式(检查间隔: 5分钟)
+echo [启动] 后台运行模式,检查间隔: 5分钟
 echo [信息] 日志输出到: logs\auto_execute.log
 start /B "" python scripts\auto_execute_tasks.py --interval 300 > logs\auto_execute.log 2>&1
 echo.
@@ -147,7 +147,7 @@ powershell -Command "$processes = Get-WmiObject Win32_Process | Where-Object { $
 
 echo.
 echo ========================================================
-echo                   最近日志(最后 20 行)
+echo                   最近日志 - 最后 20 行
 echo ========================================================
 echo.
 

+ 20 - 0
test_final.json

@@ -0,0 +1,20 @@
+{
+    "code":  200,
+    "data":  {
+                 "current":  1,
+                 "records":  [
+                                 {
+                                     "create_time":  "2025-11-28 14:52:12",
+                                     "data_type":  "CHAR(20)",
+                                     "id":  2259,
+                                     "name_en":  "HISKSDM",
+                                     "name_zh":  "HIS科室代码",
+                                     "status":  true,
+                                     "tag":  ""
+                                 }
+                             ],
+                 "size":  1,
+                 "total":  220
+             },
+    "message":  "操作成功"
+}

+ 22 - 0
test_fixed.json

@@ -0,0 +1,22 @@
+{
+    "code":  200,
+    "data":  {
+                 "current":  1,
+                 "records":  [
+                                 {
+                                     "create_time":  "2025-11-28 14:52:12",
+                                     "data_type":  "CHAR(20)",
+                                     "id":  2259,
+                                     "name_en":  "HISKSDM",
+                                     "name_zh":  "HIS科室代码",
+                                     "status":  true,
+                                     "tag":  [
+
+                                             ]
+                                 }
+                             ],
+                 "size":  10,
+                 "total":  1
+             },
+    "message":  "操作成功"
+}

+ 20 - 0
test_notexist.json

@@ -0,0 +1,20 @@
+{
+    "code":  200,
+    "data":  {
+                 "current":  1,
+                 "records":  [
+                                 {
+                                     "create_time":  "2025-11-28 14:52:12",
+                                     "data_type":  "CHAR(20)",
+                                     "id":  2259,
+                                     "name_en":  "HISKSDM",
+                                     "name_zh":  "HIS科室代码",
+                                     "status":  true,
+                                     "tag":  ""
+                                 }
+                             ],
+                 "size":  1,
+                 "total":  220
+             },
+    "message":  "操作成功"
+}

+ 22 - 0
test_result.json

@@ -0,0 +1,22 @@
+{
+    "code":  200,
+    "data":  {
+                 "current":  1,
+                 "records":  [
+                                 {
+                                     "create_time":  "2025-11-28 14:52:12",
+                                     "data_type":  "CHAR(20)",
+                                     "id":  2259,
+                                     "name_en":  "HISKSDM",
+                                     "name_zh":  "HIS科室代码",
+                                     "status":  true,
+                                     "tag":  [
+
+                                             ]
+                                 }
+                             ],
+                 "size":  1,
+                 "total":  220
+             },
+    "message":  "操作成功"
+}

BIN
test_result.txt


+ 22 - 0
test_result2.json

@@ -0,0 +1,22 @@
+{
+    "code":  200,
+    "data":  {
+                 "current":  1,
+                 "records":  [
+                                 {
+                                     "create_time":  "2025-11-28 14:52:12",
+                                     "data_type":  "CHAR(20)",
+                                     "id":  2259,
+                                     "name_en":  "HISKSDM",
+                                     "name_zh":  "HIS科室代码",
+                                     "status":  true,
+                                     "tag":  [
+
+                                             ]
+                                 }
+                             ],
+                 "size":  1,
+                 "total":  220
+             },
+    "message":  "操作成功"
+}

+ 20 - 0
test_result3.json

@@ -0,0 +1,20 @@
+{
+    "code":  200,
+    "data":  {
+                 "current":  1,
+                 "records":  [
+                                 {
+                                     "create_time":  "2025-11-28 14:52:12",
+                                     "data_type":  "CHAR(20)",
+                                     "id":  2259,
+                                     "name_en":  "HISKSDM",
+                                     "name_zh":  "HIS科室代码",
+                                     "status":  true,
+                                     "tag":  ""
+                                 }
+                             ],
+                 "size":  1,
+                 "total":  220
+             },
+    "message":  "操作成功"
+}

+ 22 - 0
test_result_new.json

@@ -0,0 +1,22 @@
+{
+    "code":  200,
+    "data":  {
+                 "current":  1,
+                 "records":  [
+                                 {
+                                     "create_time":  "2025-11-28 14:52:12",
+                                     "data_type":  "CHAR(20)",
+                                     "id":  2259,
+                                     "name_en":  "HISKSDM",
+                                     "name_zh":  "HIS科室代码",
+                                     "status":  true,
+                                     "tag":  [
+
+                                             ]
+                                 }
+                             ],
+                 "size":  1,
+                 "total":  220
+             },
+    "message":  "操作成功"
+}

この差分においてかなりの量のファイルが変更されているため、一部のファイルを表示していません