diff --git a/controller/GraphController.py b/controller/GraphController.py new file mode 100644 index 0000000..c17579c --- /dev/null +++ b/controller/GraphController.py @@ -0,0 +1,207 @@ +import json +import sys +from datetime import datetime +from app import app + +from robyn import Robyn, jsonify, Response +from typing import Optional, List, Any, Dict + +from service.GraphService import build_g6_subgraph_by_props, get_drug_names_from_neo4j, get_group_key, \ + get_check_names_from_neo4j +from util.neo4j_utils import Neo4jUtil +from util.neo4j_utils import neo4j_client +from util.redis_utils import set as redis_set, get as redis_get # 使用你已有的模块级 Redis 工 + +# 缓存键 +DRUG_TREE_KEY = "cache:drug_tree" +CHECK_TREE_KEY = "cache:check_tree" + +# ======================== +# 🔥 启动时预加载数据(在 app 启动前执行) +# ======================== +def preload_data(): + print("🚀 正在预加载 Drug 和 Check 树...") + + try: + # --- Drug Tree --- + names = get_drug_names_from_neo4j() + groups = {} + for name in names: + key = get_group_key(name) + groups.setdefault(key, []).append(name) + + alphabet = [chr(i) for i in range(ord('A'), ord('Z') + 1)] + all_keys = alphabet + ["0-9", "其他"] + + tree_data = [] + for key in all_keys: + if key in groups: + children = [{"label": name, "type": "Drug"} for name in sorted(groups[key])] + tree_data.append({"label": key, "type": "Drug", "children": children}) + + redis_set(DRUG_TREE_KEY, json.dumps(tree_data, ensure_ascii=False), ex=3600) + + # --- Check Tree --- + names = get_check_names_from_neo4j() + groups = {} + for name in names: + key = get_group_key(name) + groups.setdefault(key, []).append(name) + + tree_data = [] + for key in all_keys: + if key in groups: + children = [{"label": name, "type": "Check"} for name in sorted(groups[key])] + tree_data.append({"label": key, "type": "Check", "children": children}) + + redis_set(CHECK_TREE_KEY, json.dumps(tree_data, ensure_ascii=False), ex=3600) + + print("✅ 预加载完成!数据已写入 Redis 缓存。") + except Exception as e: + print(f"❌ 预加载失败: {e}", file=sys.stderr) + # 可选:是否允许启动失败?这里选择继续启动(接口会返回错误) + # 或者 sys.exit(1) 强制退出 + +# 执行预加载(在 app 创建前) +preload_data() +@app.get("/api/getData") +def get_data(): + try: + graph_data = build_g6_subgraph_by_props( + neo4j_client, + node_label="Disease", + node_properties={"name": "霍乱"}, + direction="both", + rel_type=None + ) + + return Response( + status_code=200, + description=jsonify(graph_data), + headers={"Content-Type": "text/plain; charset=utf-8"} + ) + except Exception as e: + return jsonify({"error": str(e)}), 500 +@app.post("/api/getGraph") +def get_graph(req): + try: + + # 1. 获取 JSON body(自动解析为 dict) + body = req.json() + # 2. 提取 label 字段(即疾病名称) + disease_name = body.get("label") + if not disease_name: + return jsonify({"error": "Missing 'label' in request body"}), 400 + graph_data = build_g6_subgraph_by_props( + neo4j_client, + node_label=body.get("type"), + node_properties={"name": disease_name}, + direction="both", + rel_type=None + ) + return Response( + status_code=200, + description=jsonify(graph_data), + headers={"Content-Type": "text/plain; charset=utf-8"} + ) + except Exception as e: + return jsonify({"error": str(e)}), 500 +@app.get("/api/drug-tree") +def get_drug_tree(): + return Response( + status_code=200, + description=redis_get(DRUG_TREE_KEY), + headers={"Content-Type": "application/json; charset=utf-8"} + ) + # try: + # names = get_drug_names_from_neo4j() + # print(f"[Step 1] Loaded {len(names)} names") + # + # groups = {} + # for name in names: + # key = get_group_key(name) + # groups.setdefault(key, []).append(name) + # print(f"[Step 2] Grouped into {len(groups)} groups") + # + # # ✅ 顺序:A-Z, 0-9, 其他 + # alphabet = [chr(i) for i in range(ord('A'), ord('Z') + 1)] + # all_keys = alphabet + ["0-9", "其他"] + # + # tree_data = [] + # total_children = 0 + # for key in all_keys: + # if key in groups: + # children = [{"label": name,"type":"Drug"} for name in sorted(groups[key])] + # total_children += len(children) + # tree_data.append({"label": key,"type":"Drug", "children": children}) + # + # print(f"[Step 3] Final tree: {len(tree_data)} groups, {total_children} drugs") + # + # json_str = json.dumps(tree_data, ensure_ascii=False) + # print(f"[Step 4] JSON size: {len(json_str)} chars") + # + # return Response( + # status_code=200, + # description=json_str, + # headers={"Content-Type": "application/json; charset=utf-8"} + # ) + # except Exception as e: + # print(f"[ERROR] {str(e)}") + # return Response( + # status_code=500, + # description=json.dumps({"error": str(e)}, ensure_ascii=False), + # headers={"Content-Type": "application/json; charset=utf-8"} + # ) +@app.get("/api/check-tree") +def get_check_tree(): + return Response( + status_code=200, + description=redis_get(CHECK_TREE_KEY), + headers={"Content-Type": "application/json; charset=utf-8"} + ) + # try: + # names = get_check_names_from_neo4j() + # print(f"[Step 1] Loaded {len(names)} names") + # + # groups = {} + # for name in names: + # key = get_group_key(name) + # groups.setdefault(key, []).append(name) + # print(f"[Step 2] Grouped into {len(groups)} groups") + # + # # ✅ 顺序:A-Z, 0-9, 其他 + # alphabet = [chr(i) for i in range(ord('A'), ord('Z') + 1)] + # all_keys = alphabet + ["0-9", "其他"] + # + # tree_data = [] + # total_children = 0 + # for key in all_keys: + # if key in groups: + # children = [{"label": name,"type":"Check"} for name in sorted(groups[key])] + # total_children += len(children) + # tree_data.append({"label": key,"type":"Check", "children": children}) + # + # print(f"[Step 3] Final tree: {len(tree_data)} groups, {total_children} checks") + # + # json_str = json.dumps(tree_data, ensure_ascii=False) + # print(f"[Step 4] JSON size: {len(json_str)} chars") + # + # return Response( + # status_code=200, + # description=json_str, + # headers={"Content-Type": "application/json; charset=utf-8"} + # ) + # except Exception as e: + # print(f"[ERROR] {str(e)}") + # return Response( + # status_code=500, + # description=json.dumps({"error": str(e)}, ensure_ascii=False), + # headers={"Content-Type": "application/json; charset=utf-8"} + # ) + + +@app.get("/health") +def health(): + print(redis_get(DRUG_TREE_KEY)) + print(redis_get(CHECK_TREE_KEY)) + return {"status": "ok", "drug_cached": redis_get(DRUG_TREE_KEY) is not None} \ No newline at end of file diff --git a/python/test1.py b/python/test1.py deleted file mode 100644 index 56d4ca3..0000000 --- a/python/test1.py +++ /dev/null @@ -1,181 +0,0 @@ -from datetime import datetime -from app import app - -from robyn import Robyn, jsonify, Response -from typing import Optional, List, Any, Dict -from util.neo4j_utils import Neo4jUtil -from util.neo4j_utils import neo4j_client -def convert_node_to_g6_v5(neo4j_node: dict) -> dict: - node_id = neo4j_node.get("id") - if node_id is None: - raise ValueError("节点必须包含 'id' 字段") - - data = {k: v for k, v in neo4j_node.items() if k != "id"} - if "name" not in data and "label" not in data: - data["name"] = str(node_id) - - return { - "id": node_id, - "data": data, - "states": [], - "combo": None - } - - -def build_g6_graph_data_from_results( - nodes: List[Dict[str, Any]], - relationships: List[Dict[str, Any]] -) -> dict: - """ - 通用方法:根据节点列表和关系列表构建 G6 v5 图数据 - - Args: - nodes: 节点列表,每个节点需含 "id" - relationships: 关系列表,每个关系需含: - - source: {"id": ..., ...} - - target: {"id": ..., ...} - - relationship: {"type": str, "properties": dict} 或直接扁平化字段 - - Returns: - {"nodes": [...], "edges": [...]} - """ - g6_node_map = {} - - # 处理显式传入的节点 - for node in nodes: - node_id = node.get("id") - if node_id: - g6_node_map[node_id] = convert_node_to_g6_v5(node) - - g6_edges = [] - for rel in relationships: - source_node = rel.get("source") - target_node = rel.get("target") - if not source_node or not target_node: - continue - - source_id = source_node.get("id") - target_id = target_node.get("id") - if not source_id or not target_id: - continue - - # 确保 source/target 节点也加入图中(即使未在 nodes 中显式提供) - if source_id not in g6_node_map: - g6_node_map[source_id] = convert_node_to_g6_v5(source_node) - if target_id not in g6_node_map: - g6_node_map[target_id] = convert_node_to_g6_v5(target_node) - - # 构建 edge data - edge_data = {} - rel_type_str = rel.get("type") or rel.get("relationship") # 兼容不同结构 - if rel_type_str: - edge_data["relationship"] = rel_type_str - - # 尝试从 relProps 或 properties 或顶层提取关系属性 - rel_props = ( - rel.get("relProps") or - rel.get("properties") or - {k: v for k, v in rel.items() if k not in ("source", "target", "type", "relationship")} - ) - if isinstance(rel_props, dict): - edge_data.update(rel_props) - - g6_edge = { - "source": source_id, - "target": target_id, - "type": "line", - "data": edge_data, - "states": [] - } - g6_edges.append(g6_edge) - - return { - "nodes": list(g6_node_map.values()), - "edges": g6_edges - } -def build_g6_subgraph_by_props( - neo4j_util: Neo4jUtil, - node_properties: Dict[str, Any], - node_label: Optional[str] = None, - direction: str = "both", - rel_type: Optional[str] = None -) -> dict: - neighbor_list = neo4j_util.find_neighbors_with_relationships( - node_label=node_label, - node_properties=node_properties, - direction=direction, - rel_type=rel_type - ) - - # 提取所有唯一节点 - node_dict = {} - for item in neighbor_list: - for key in ["source", "target"]: - n = item[key] - nid = n.get("id") - if nid and nid not in node_dict: - node_dict[nid] = n - - # 如果没找到关系,但中心节点存在,也要包含它 - if not neighbor_list: - center_nodes = neo4j_util.find_nodes_with_element_id(node_label, node_properties) - if center_nodes: - n = center_nodes[0] - node_dict[n["id"]] = n - - nodes = list(node_dict.values()) - relationships = neighbor_list # 结构已兼容 - - return build_g6_graph_data_from_results(nodes, relationships) -print("✅ test1 已导入,路由应已注册") -@app.get("/api/getData") -def get_data(): - try: - graph_data = build_g6_subgraph_by_props( - neo4j_client, - node_label="Disease", - node_properties={"name": "埃尔托生物型霍乱"}, - direction="both", - rel_type=None - ) - - return Response( - status_code=200, - description=jsonify(graph_data), - headers={"Content-Type": "text/plain; charset=utf-8"} - ) - except Exception as e: - return jsonify({"error": str(e)}), 500 - - -@app.post("/api/getGraph") -def get_graph(req): - try: - - # 1. 获取 JSON body(自动解析为 dict) - body = req.json() - # 2. 提取 label 字段(即疾病名称) - disease_name = body.get("label") - if not disease_name: - return jsonify({"error": "Missing 'label' in request body"}), 400 - code = body.get("code") - level = body.get("level") - print("sssssssssss") - print(body.get("type")) - - print(code) - print(level) - graph_data = build_g6_subgraph_by_props( - neo4j_client, - node_label=body.get("type"), - node_properties={"name": disease_name}, - direction="both", - rel_type=None - ) - return Response( - status_code=200, - description=jsonify(graph_data), - headers={"Content-Type": "text/plain; charset=utf-8"} - ) - except Exception as e: - return jsonify({"error": str(e)}), 500 \ No newline at end of file diff --git a/python/test1222.py b/python/test1222.py index 7abd5c7..3cede4d 100644 --- a/python/test1222.py +++ b/python/test1222.py @@ -1,20 +1,10 @@ -from neo4j import GraphDatabase -from pypinyin import lazy_pinyin +from util.neo4j_utils import neo4j_client -URI = "bolt://localhost:7687" -AUTH = ("neo4j", "12345678") -with GraphDatabase.driver(URI, auth=AUTH) as driver: - with driver.session() as session: - result = session.run("MATCH (d:Drug) WHERE d.name IS NOT NULL RETURN d.name AS name") - names = [record["name"] for record in result] +def get_drug_count(): + cypher = "MATCH (d:Drug) WHERE d.name IS NOT NULL RETURN count(d) AS total" + res = neo4j_client.execute_read(cypher) + return res[0]["total"] -# 按拼音首字母 A-Z 排序(支持中英文混合) -def sort_key(name): - # 将每个字转为拼音,取首字母(如 "阿司匹林" → ['a', 'si', 'pi', 'lin'] → 拼成 "asipilin") - return ''.join(lazy_pinyin(name)) - -sorted_names = sorted(names, key=sort_key) - -for name in sorted_names: - print(name) \ No newline at end of file +# 在路由中临时加一行打印 +print("Total drug names:", get_drug_count()) # 看是否接近 17000+ \ No newline at end of file diff --git a/service/GraphService.py b/service/GraphService.py index b8a00ee..541dbfb 100644 --- a/service/GraphService.py +++ b/service/GraphService.py @@ -1,111 +1,174 @@ from typing import Dict, List, Any, Optional +from pypinyin import lazy_pinyin, Style +from util.neo4j_utils import Neo4jUtil, neo4j_client - -def convert_node_to_g6_v5(neo4j_node: Dict[str, Any]) -> Dict[str, Any]: - """ - 将 Neo4j 节点(含 id 和 props)转换为 G6 v5 节点格式。 - - :param neo4j_node: 来自 Neo4jUtil 的节点字典,必须包含 'id' 字段 - :return: G6 v5 节点对象 - """ +def convert_node_to_g6_v5(neo4j_node: dict) -> dict: node_id = neo4j_node.get("id") if node_id is None: raise ValueError("节点必须包含 'id' 字段") - # 构建 data:排除 'id',保留其他属性 data = {k: v for k, v in neo4j_node.items() if k != "id"} - - # 确保有显示用的 name 或 label(G6 默认使用 data.name) if "name" not in data and "label" not in data: data["name"] = str(node_id) - g6_node = { + return { "id": node_id, "data": data, - # 可选字段(前端可覆盖): - # "type": "circle", - # "style": {"size": 32, "fill": "violet"}, "states": [], "combo": None } - return g6_node - -def build_g6_graph_data( - neo4j_util: 'Neo4jUtil', - node_label: str, - rel_type: Optional[str] = None -) -> Dict[str, List[Dict[str, Any]]]: - """ - 构建 G6 v5 兼容的图数据结构(nodes + edges)。 - :param neo4j_util: 已连接的 Neo4jUtil 实例 - :param node_label: 中心节点标签(用于获取初始节点集) - :param rel_type: 关系类型(可选,若为 None 则获取所有关系) - :return: {'nodes': [...], 'edges': [...]} +def build_g6_graph_data_from_results( + nodes: List[Dict[str, Any]], + relationships: List[Dict[str, Any]] +) -> dict: """ - # 1. 获取中心节点(指定标签的所有节点) - center_nodes = neo4j_util.find_all_nodes(node_label) + 通用方法:根据节点列表和关系列表构建 G6 v5 图数据 - # 2. 获取所有相关关系(可按类型过滤) - relationships = neo4j_util.find_all_relationships(rel_type=rel_type) + Args: + nodes: 节点列表,每个节点需含 "id" + relationships: 关系列表,每个关系需含: + - source: {"id": ..., ...} + - target: {"id": ..., ...} + - relationship: {"type": str, "properties": dict} 或直接扁平化字段 - # 3. 使用字典去重节点(key: elementId) - g6_node_map: Dict[str, Dict[str, Any]] = {} + Returns: + {"nodes": [...], "edges": [...]} + """ + g6_node_map = {} - # 添加中心节点 - for node in center_nodes: + # 处理显式传入的节点 + for node in nodes: node_id = node.get("id") if node_id: g6_node_map[node_id] = convert_node_to_g6_v5(node) - # 4. 处理关系,提取 source/target 节点并构建边 - g6_edges: List[Dict[str, Any]] = [] - + g6_edges = [] for rel in relationships: source_node = rel.get("source") target_node = rel.get("target") - if not source_node or not target_node: continue source_id = source_node.get("id") target_id = target_node.get("id") - if not source_id or not target_id: continue - # 自动加入 source 和 target 节点(去重) - g6_node_map[source_id] = convert_node_to_g6_v5(source_node) - g6_node_map[target_id] = convert_node_to_g6_v5(target_node) + # 确保 source/target 节点也加入图中(即使未在 nodes 中显式提供) + if source_id not in g6_node_map: + g6_node_map[source_id] = convert_node_to_g6_v5(source_node) + if target_id not in g6_node_map: + g6_node_map[target_id] = convert_node_to_g6_v5(target_node) - # 构建 G6 边对象 + # 构建 edge data edge_data = {} - - # 关系类型 - rel_type_str = rel.get("type") + rel_type_str = rel.get("type") or rel.get("relationship") # 兼容不同结构 if rel_type_str: edge_data["relationship"] = rel_type_str - # 合并关系属性 - rel_props = rel.get("relProps") or {} - edge_data.update(rel_props) + # 尝试从 relProps 或 properties 或顶层提取关系属性 + rel_props = ( + rel.get("relProps") or + rel.get("properties") or + {k: v for k, v in rel.items() if k not in ("source", "target", "type", "relationship")} + ) + if isinstance(rel_props, dict): + edge_data.update(rel_props) g6_edge = { "source": source_id, "target": target_id, - "type": "line", # G6 默认边类型 + "type": "line", "data": edge_data, - # "style": {}, # 可由前端统一配置 "states": [] } g6_edges.append(g6_edge) - # 5. 组装结果 - result = { + return { "nodes": list(g6_node_map.values()), "edges": g6_edges } - return result - - +def build_g6_subgraph_by_props( + neo4j_util: Neo4jUtil, + node_properties: Dict[str, Any], + node_label: Optional[str] = None, + direction: str = "both", + rel_type: Optional[str] = None +) -> dict: + neighbor_list = neo4j_util.find_neighbors_with_relationships( + node_label=node_label, + node_properties=node_properties, + direction=direction, + rel_type=rel_type + ) + + # 提取所有唯一节点 + node_dict = {} + for item in neighbor_list: + for key in ["source", "target"]: + n = item[key] + nid = n.get("id") + if nid and nid not in node_dict: + node_dict[nid] = n + + # 如果没找到关系,但中心节点存在,也要包含它 + if not neighbor_list: + center_nodes = neo4j_util.find_nodes_with_element_id(node_label, node_properties) + if center_nodes: + n = center_nodes[0] + node_dict[n["id"]] = n + + nodes = list(node_dict.values()) + relationships = neighbor_list # 结构已兼容 + + return build_g6_graph_data_from_results(nodes, relationships) + + +def get_drug_names_from_neo4j(): + """安全获取全部 Drug.name,支持大数据量""" + cypher = "MATCH (d:Drug) WHERE d.name IS NOT NULL RETURN d.name AS name" + results = neo4j_client.execute_read(cypher) + names = [] + for record in results: + name = record.get("name") + if name is not None: # 再次过滤 None + names.append(name) + print(f"[DEBUG] Loaded {len(names)} drug names from Neo4j") # 打印实际数量 + return names + +def get_check_names_from_neo4j(): + """安全获取全部 Drug.name,支持大数据量""" + cypher = "MATCH (d:Check) WHERE d.name IS NOT NULL RETURN d.name AS name" + results = neo4j_client.execute_read(cypher) + names = [] + for record in results: + name = record.get("name") + if name is not None: # 再次过滤 None + names.append(name) + print(f"[DEBUG] Loaded {len(names)} check names from Neo4j") # 打印实际数量 + return names +def get_group_key(name: str) -> str: + if not name or not isinstance(name, str): + return "其他" + + name = name.strip() + if not name: + return "其他" + + for char in name: + if char.isdigit(): + return "0-9" + if char.isalpha() and char.isascii(): + return char.upper() + if '\u4e00' <= char <= '\u9fff': # 中文 + try: + first_letter = lazy_pinyin(char, style=Style.FIRST_LETTER)[0].upper() + if 'A' <= first_letter <= 'Z': + return first_letter + except Exception: + continue + # 其他字符:跳过 + + return "其他" # 所有无法归类的 \ No newline at end of file diff --git a/util/neo4j_utils.py b/util/neo4j_utils.py index 5b33163..96b81df 100644 --- a/util/neo4j_utils.py +++ b/util/neo4j_utils.py @@ -261,37 +261,24 @@ class Neo4jUtil: self, node_label: Optional[str], node_properties: Dict[str, Any], - direction: str = "both", # 可选: "out", "in", "both" + direction: str = "both", rel_type: Optional[str] = None, + limit: int = 500, # 👈 新增参数,默认 1000 ) -> List[Dict[str, Any]]: """ - 查询指定节点的所有邻居节点及其关系(包括入边、出边或双向) - - Args: - node_label (Optional[str]): 节点标签,若为 None 则匹配任意标签的节点 - node_properties (Dict[str, Any]): 节点匹配属性(必须能唯一或有效定位节点) - direction (str): 关系方向,"out" 表示 (n)-[r]->(m),"in" 表示 (n)<-[r]-(m),"both" 表示无向 - rel_type (Optional[str]): 可选的关系类型过滤 - - Returns: - List[Dict]: 每项包含 source(原节点)、target(邻居)、relationship 信息 + 查询指定节点的邻居(最多返回 limit 条) """ if not node_properties: raise ValueError("node_properties 不能为空,用于定位起始节点") - # 构建起始节点匹配条件 where_clause, params = self._build_where_conditions("n", node_properties, "node") - - # 构建关系类型过滤 rel_filter = f":`{rel_type}`" if rel_type else "" - # ✅ 动态构建节点模式:支持 node_label=None if node_label is not None: node_pattern = f"(n:`{node_label}`)" else: node_pattern = "(n)" - # 构建完整 MATCH 模式 if direction == "out": pattern = f"{node_pattern}-[r{rel_filter}]->(m)" elif direction == "in": @@ -301,6 +288,7 @@ class Neo4jUtil: else: raise ValueError("direction 必须是 'out', 'in' 或 'both'") + # ✅ 添加 LIMIT cypher = f""" MATCH {pattern} WHERE {where_clause} @@ -314,12 +302,13 @@ class Neo4jUtil: elementId(r) AS relId, type(r) AS relType, r{{.*}} AS relProps + LIMIT $limit """ + params["limit"] = limit # 注入 limit 参数(安全) raw_results = self.execute_read(cypher, params) neighbors = [] - for row in raw_results: source = dict(row["sourceProps"]) source.update({"id": row["sourceId"], "label": row["sourceLabel"]}) @@ -330,7 +319,7 @@ class Neo4jUtil: relationship = { "id": row["relId"], "type": row["relType"], - "properties": row["relProps"] + "properties": dict(row["relProps"]) if row["relProps"] else {} } neighbors.append({ @@ -340,6 +329,89 @@ class Neo4jUtil: }) return neighbors + # def find_neighbors_with_relationships( + # self, + # node_label: Optional[str], + # node_properties: Dict[str, Any], + # direction: str = "both", # 可选: "out", "in", "both" + # rel_type: Optional[str] = None, + # ) -> List[Dict[str, Any]]: + # """ + # 查询指定节点的所有邻居节点及其关系(包括入边、出边或双向) + # + # Args: + # node_label (Optional[str]): 节点标签,若为 None 则匹配任意标签的节点 + # node_properties (Dict[str, Any]): 节点匹配属性(必须能唯一或有效定位节点) + # direction (str): 关系方向,"out" 表示 (n)-[r]->(m),"in" 表示 (n)<-[r]-(m),"both" 表示无向 + # rel_type (Optional[str]): 可选的关系类型过滤 + # + # Returns: + # List[Dict]: 每项包含 source(原节点)、target(邻居)、relationship 信息 + # """ + # if not node_properties: + # raise ValueError("node_properties 不能为空,用于定位起始节点") + # + # # 构建起始节点匹配条件 + # where_clause, params = self._build_where_conditions("n", node_properties, "node") + # + # # 构建关系类型过滤 + # rel_filter = f":`{rel_type}`" if rel_type else "" + # + # # ✅ 动态构建节点模式:支持 node_label=None + # if node_label is not None: + # node_pattern = f"(n:`{node_label}`)" + # else: + # node_pattern = "(n)" + # + # # 构建完整 MATCH 模式 + # if direction == "out": + # pattern = f"{node_pattern}-[r{rel_filter}]->(m)" + # elif direction == "in": + # pattern = f"{node_pattern}<[r{rel_filter}]-(m)" + # elif direction == "both": + # pattern = f"{node_pattern}-[r{rel_filter}]-(m)" + # else: + # raise ValueError("direction 必须是 'out', 'in' 或 'both'") + # + # cypher = f""" + # MATCH {pattern} + # WHERE {where_clause} + # RETURN + # elementId(n) AS sourceId, + # head(labels(n)) AS sourceLabel, + # n{{.*}} AS sourceProps, + # elementId(m) AS targetId, + # head(labels(m)) AS targetLabel, + # m{{.*}} AS targetProps, + # elementId(r) AS relId, + # type(r) AS relType, + # r{{.*}} AS relProps + # """ + # + # raw_results = self.execute_read(cypher, params) + # + # neighbors = [] + # + # for row in raw_results: + # source = dict(row["sourceProps"]) + # source.update({"id": row["sourceId"], "label": row["sourceLabel"]}) + # + # target = dict(row["targetProps"]) + # target.update({"id": row["targetId"], "label": row["targetLabel"]}) + # + # relationship = { + # "id": row["relId"], + # "type": row["relType"], + # "properties": row["relProps"] + # } + # + # neighbors.append({ + # "source": source, + # "target": target, + # "relationship": relationship + # }) + # + # return neighbors def delete_all_relationships_by_node_label(self, node_label: str): """删除某标签节点的所有关系(保留节点)""" cypher = f"MATCH (n:`{node_label}`)-[r]-() DELETE r" diff --git a/util/redis_utils.py b/util/redis_utils.py new file mode 100644 index 0000000..8ab0956 --- /dev/null +++ b/util/redis_utils.py @@ -0,0 +1,105 @@ +# redis_utils.py +import redis +from typing import Any, Optional, Union, List, Dict +import os + +# 从环境变量读取配置(可选,更安全) +REDIS_HOST = os.getenv("REDIS_HOST", "localhost") +REDIS_PORT = int(os.getenv("REDIS_PORT", 6379)) +REDIS_DB = int(os.getenv("REDIS_DB", 0)) +REDIS_PASSWORD = os.getenv("REDIS_PASSWORD", None) + +# 创建全局唯一的 Redis 客户端(模块加载时初始化一次) +_client = redis.Redis( + host=REDIS_HOST, + port=REDIS_PORT, + db=REDIS_DB, + password=REDIS_PASSWORD, + decode_responses=True, + socket_connect_timeout=5, + retry_on_timeout=True, + health_check_interval=30 +) + +# ========== 封装常用方法(直接操作 _client) ========== +def set(key: str, value: Any, ex: Optional[int] = None) -> bool: + """设置字符串值""" + return _client.set(key, value, ex=ex) + + +def get(key: str) -> Optional[str]: + """获取字符串值""" + return _client.get(key) + + +def delete(*keys: str) -> int: + """删除 key""" + return _client.delete(*keys) + + +def exists(key: str) -> bool: + """检查 key 是否存在""" + return _client.exists(key) == 1 + + +def hset(name: str, key: str, value: Any) -> int: + """Hash 设置字段""" + return _client.hset(name, key, value) + + +def hget(name: str, key: str) -> Optional[str]: + """Hash 获取字段""" + return _client.hget(name, key) + + +def hgetall(name: str) -> Dict[str, str]: + """获取整个 Hash""" + return _client.hgetall(name) + + +def lpush(name: str, *values: Any) -> int: + """List 左插入""" + return _client.lpush(name, *values) + + +def rpop(name: str) -> Optional[str]: + """List 右弹出""" + return _client.rpop(name) + + +def sadd(name: str, *values: Any) -> int: + """Set 添加成员""" + return _client.sadd(name, *values) + + +def smembers(name: str) -> set: + """获取 Set 所有成员""" + return _client.smembers(name) + + +def keys(pattern: str = "*") -> List[str]: + """查找匹配的 key(慎用)""" + return _client.keys(pattern) + + +def ttl(key: str) -> int: + """获取 key 剩余生存时间(秒)""" + return _client.ttl(key) + + +def expire(key: str, seconds: int) -> bool: + """设置 key 过期时间""" + return _client.expire(key, seconds) + + +def ping() -> bool: + """测试 Redis 连接""" + try: + return _client.ping() + except Exception: + return False + + +# 可选:提供原始 client(谨慎使用) +def get_raw_client(): + return _client \ No newline at end of file diff --git a/vue/src/api/graph.js b/vue/src/api/graph.js index c4759a6..e048c2f 100644 --- a/vue/src/api/graph.js +++ b/vue/src/api/graph.js @@ -20,4 +20,17 @@ export function getGraph(data) { method: 'post', data }); -} \ No newline at end of file +} + +export function getDrugTree() { + return request({ + url: '/api/drug-tree', + method: 'get' + }); +} +export function getCheckTree() { + return request({ + url: '/api/check-tree', + method: 'get' + }); +} diff --git a/vue/src/system/GraphDemo.vue b/vue/src/system/GraphDemo.vue index e9161c8..a952f8d 100644 --- a/vue/src/system/GraphDemo.vue +++ b/vue/src/system/GraphDemo.vue @@ -144,7 +144,7 @@ -
+
疾病 @@ -152,7 +152,7 @@ 检查
-
+