Browse Source

all

yangrongze
hanyuqing 3 months ago
parent
commit
7515a32da4
  1. 207
      controller/GraphController.py
  2. 181
      python/test1.py
  3. 24
      python/test1222.py
  4. 179
      service/GraphService.py
  5. 108
      util/neo4j_utils.py
  6. 105
      util/redis_utils.py
  7. 15
      vue/src/api/graph.js
  8. 215
      vue/src/system/GraphDemo.vue
  9. 2
      web_main.py

207
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}

181
python/test1.py

@ -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

24
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)
# 在路由中临时加一行打印
print("Total drug names:", get_drug_count()) # 看是否接近 17000+

179
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 "其他" # 所有无法归类的

108
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"

105
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

15
vue/src/api/graph.js

@ -20,4 +20,17 @@ export function getGraph(data) {
method: 'post',
data
});
}
}
export function getDrugTree() {
return request({
url: '/api/drug-tree',
method: 'get'
});
}
export function getCheckTree() {
return request({
url: '/api/check-tree',
method: 'get'
});
}

215
vue/src/system/GraphDemo.vue

@ -144,7 +144,7 @@
<div class="icd10-tree-container">
<div class="icd10-tree-container" style="overflow: scroll;width: 25%">
<div>
<el-radio-group v-model="typeRadio" @change="changeTree">
<el-radio value="Disease">疾病</el-radio>
@ -152,7 +152,7 @@
<el-radio value="Check">检查</el-radio>
</el-radio-group>
</div>
<div v-if="typeRadio === 'Disease'">
<div>
<el-tree
:data="treeData"
:props="treeProps"
@ -161,9 +161,10 @@
>
<template #default="{ node, data }">
<span class="custom-tree-node">
<el-tag size="small" :type="getTagType(data.level)" class="level-tag">
{{ getLevelLabel(data.level) }}
</el-tag>
<!-- <el-tag size="small" :type="getTagType(data.level)" class="level-tag">-->
<!-- -->
<!-- </el-tag>-->
<!-- {{ getLevelLabel(data.level) }}-->
<span class="code">{{ data.code }}</span>
<span class="label">{{ data.label }}</span>
</span>
@ -171,15 +172,33 @@
</el-tree>
</div>
</div>
<div style="width: 75%">
<div style="display: flex; gap: 10px; margin-bottom: 10px;">
<!-- 图例项 -->
<div v-for="(item, index) in legendItems" :key="index" class="legend-item">
<div
:style="{
backgroundColor: item.color,
opacity: visibleCategories.has(item.key) ? 1 : 0.3 //
}"
class="color-block"
@click="toggleCategory(item.key)"
></div>
<span>{{ item.label }}</span>
</div>
</div>
<div ref="graphContainer" class="graph-container" id="container"></div>
</div>
<!-- 图谱容器 -->
<div ref="graphContainer" class="graph-container" id="container"></div>
</div>
</template>
<script>
import {getGraph, getTestGraphData} from "@/api/graph"
import {getCheckTree, getDrugTree, getGraph, getTestGraphData} from "@/api/graph"
import { Graph } from '@antv/g6';
import Menu from "@/components/Menu.vue";
import {a} from "vue-router/dist/devtools-EWN81iOl.mjs";
export default {
name: 'GraphDemo',
@ -224,13 +243,26 @@ export default {
children: 'children',
label: 'title' // el-tree
},
typeRadio:"Disease"
typeRadio:"Disease",
drugTree:[],
diseaseTree:[],
checkTree:[],
legendItems: [
{ key: 'Drug', label: '药品', color: '#91cc75' },
{ key: 'Symptom', label: '症状', color: '#fac858' },
{ key: 'Disease', label: '疾病', color: '#EF4444' },
{ key: 'Check', label: '检查', color: '#59d1d4' },
{ key: 'Other', label: '其他', color: '#336eee' }
],
visibleCategories: new Set(), //
}
},
async mounted() {
this.loadTreeData()
this.visibleCategories = new Set(this.legendItems.map(i => i.key));
await this.loadDiseaseTreeData()
this.treeData=this.diseaseTree
await this.$nextTick();
try {
const response = await getTestGraphData(); // Promise
@ -250,6 +282,7 @@ export default {
}))
const updatedEdges = response.edges.map(edge => ({
...edge,
id: edge.data.relationship.id,
type: this.edgeType,
style: {
endArrow: this.edgeEndArrow,
@ -267,13 +300,17 @@ export default {
edges: updatedEdges
}
this.defaultData = updatedData
setTimeout(() => {
setTimeout(() => {
this.initGraph();
this.loadDrugTreeData()
this.loadCheckTreeData()
this.buildCategoryIndex();
window.addEventListener('resize', this.handleResize);
}, 1000);
} catch (error) {
console.error('加载图谱数据失败:', error);
}
},
beforeUnmount() {
this._graph.destroy()
@ -303,24 +340,124 @@ export default {
edgeFontFamily: 'updateAllEdges',
},
methods: {
buildCategoryIndex() {
const index = {};
const nodes = this._graph.getNodeData() //
nodes.forEach(node => {
console.log(node.data.label)
const category = node.data.label; // label
if(category=='Drug'||category=='Symptom'||
category=='Disease'||category=='Check'){
if (!index[category]) index[category] = [];
index[category].push(node.id);
}else{
if (!index["Other"]) index["Other"] = [];
index["Other"].push(node.id);
}
});
this.categoryToNodeIds = index;
},
//
toggleCategory (key){
if (this.visibleCategories.has(key)) {
this.visibleCategories.delete(key)
} else {
this.visibleCategories.add(key)
}
this.updateGraphFilter()
},
// G6
updateGraphFilter() {
console.log('visibleCategories:', this.visibleCategories);
// Step 1: ID
const visibleNodeIds = new Set();
for (const [category, nodeIds] of Object.entries(this.categoryToNodeIds)) {
if (this.visibleCategories.has(category)) {
nodeIds.forEach(id => visibleNodeIds.add(id));
}
}
// Step 2:
for (const [category, nodeIds] of Object.entries(this.categoryToNodeIds)) {
if (this.visibleCategories.has(category)) {
this._graph.showElement(nodeIds, true);
} else {
this._graph.hideElement(nodeIds, true);
}
}
// Step 3: /
const edges = this._graph.getEdgeData(); //
const edgesToShow = [];
const edgesToHide = [];
edges.forEach(edge => {
const sourceVisible = visibleNodeIds.has(edge.source);
const targetVisible = visibleNodeIds.has(edge.target);
// source target
if (sourceVisible && targetVisible) {
edgesToShow.push(edge.id);
} else {
edgesToHide.push(edge.id);
}
});
if (edgesToShow.length > 0) {
this._graph.showElement(edgesToShow, true);
}
if (edgesToHide.length > 0) {
this._graph.hideElement(edgesToHide, true);
}
},
changeTree(){
console.log(this.typeRadio)
if(this.typeRadio=="Disease"){
this.treeData=this.diseaseTree
}
if(this.typeRadio=="Drug") {
this.treeData=this.drugTree
}
if(this.typeRadio=="Check") {
this.treeData=this.checkTree
}
},
async loadTreeData() {
async loadDiseaseTreeData() {
try {
const res = await fetch('/icd10.json')
if (!res.ok) throw new Error('Failed to load JSON')
this.treeData = await res.json()
this.diseaseTree = await res.json()
console.log(this.treeData)
} catch (error) {
console.error('加载 ICD-10 数据失败:', error)
this.$message.error('加载编码数据失败,请检查文件路径')
}
},
async loadDrugTreeData() {
try {
const res = await getDrugTree()
this.drugTree = res
console.log(this.drugTree)
} catch (error) {
}
},
async loadCheckTreeData() {
try {
const res = await getCheckTree()
this.checkTree = res
console.log(this.checkTree)
} catch (error) {
}
},
async handleNodeClick(data) {
console.log('点击节点:', data)
// code
if(data.type=="Drug"||data.type=="Check"){
const response = await getGraph(data); // Promise
this.formatData(response)
}
if(data.level=="category"||
data.level=="subcategory"||
data.level=="diagnosis"){
@ -328,7 +465,6 @@ export default {
const response = await getGraph(data); // Promise
this.formatData(response)
}
this.$message.info(`已选中: ${data.title}`)
},
buildNodeLabelMap(nodes) {
this._nodeLabelMap = new Map();
@ -425,25 +561,28 @@ export default {
const container = this.$refs.graphContainer;
const width = container.clientWidth || 800;
const height = container.clientHeight || 600;
console.log(width)
console.log(height)
const graph = new Graph({
container,
width,
height,
layout: {
// type: 'force', //
// gravity: 0.3, //
// repulsion: 500, //
// attraction: 20, //
// preventOverlap: true //
type: 'force', //
gravity: 0.3, //
repulsion: 500, //
attraction: 20, //
preventOverlap: true //
// type: 'radial',
// preventOverlap: true,
// unitRadius: 200,
// maxPreventOverlapIteration:100
type: 'force-atlas2',
preventOverlap: true,
kr: 50,
center: [250, 250],
// type: 'force-atlas2',
// preventOverlap: true,
// kr: 1000,
// center: [250, 250],
// barnesHut:true,
},
behaviors: [ 'zoom-canvas', 'drag-element',
'click-select','focus-element', {
@ -466,12 +605,14 @@ export default {
if (label === 'Disease') return '#EF4444'; //
if (label === 'Drug') return '#91cc75'; // 绿
if (label === 'Symptom') return '#fac858'; //
if (label === 'Check') return '#59d1d4'; //
return '#336eee'; //
},
stroke: (d) => {
const label = d.data?.label;
if (label === 'Disease') return '#B91C1C';
if (label === 'Drug') return '#047857';
if (label === 'Check') return '#40999b'; //
if (label === 'Symptom') return '#B45309';
return '#1D4ED8';
},
@ -487,7 +628,7 @@ export default {
state: {
active: {
lineWidth: 2,
shadowColor: '#1890FF',
shadowColor: '#ffffff',
shadowBlur: 10,
opacity: 1
},
@ -511,6 +652,7 @@ export default {
if (targetLabel === 'Disease') return 'rgba(239,68,68,0.5)';
if (targetLabel === 'Drug') return 'rgba(145,204,117,0.5)';
if (targetLabel === 'Symptom') return 'rgba(250,200,88,0.5)';
if (targetLabel === 'Check') return 'rgba(89,209,212,0.5)'; //
return 'rgba(51,110,238,0.5)'; // default
},
// labelFill: (d) => {
@ -827,4 +969,23 @@ button:hover {
border: 1px solid #eee;
background: #fff;
}
.legend-item {
display: flex;
align-items: center;
gap: 5px;
cursor: pointer;
user-select: none;
}
.color-block {
width: 16px;
height: 16px;
border-radius: 4px;
transition: all 0.2s ease;
}
.legend-item:hover {
opacity: 0.8;
}
</style>

2
web_main.py

@ -1,6 +1,6 @@
from app import app
import controller.GraphController
import controller.LoginController
import python.test1 # 👈 关键:导入 test1,触发 @app.get("/getData") 执行
from service.UserService import init_mysql_connection
import os

Loading…
Cancel
Save