diff --git a/controller/OperationController.py b/controller/OperationController.py
new file mode 100644
index 0000000..af300ad
--- /dev/null
+++ b/controller/OperationController.py
@@ -0,0 +1,193 @@
+import json
+import traceback
+from app import app
+from robyn import jsonify, Response
+from service.OperationService import OperationService
+from urllib.parse import unquote
+
+# 实例化业务逻辑对象
+operation_service = OperationService()
+
+def create_response(status_code, data_dict):
+ """
+ 统一响应格式封装,强制使用 UTF-8 防止中文乱码。
+ """
+ return Response(
+ status_code=status_code,
+ description=jsonify(data_dict),
+ headers={"Content-Type": "application/json; charset=utf-8"}
+ )
+
+def parse_request_body(req):
+ """
+ 通用请求体解析器:解决解析 bytes、字符串或多重序列化的情况。
+ """
+ body = req.body
+ try:
+ if isinstance(body, (bytes, bytearray)):
+ body = json.loads(body.decode('utf-8'))
+ if isinstance(body, str):
+ body = json.loads(body)
+ return body if isinstance(body, dict) else {}
+ except Exception as e:
+ print(f"Request Body Parse Error: {e}")
+ return {}
+
+# --- 1. 获取全量动态标签 ---
+@app.get("/api/kg/labels")
+def get_labels(req):
+ try:
+ labels = operation_service.get_all_labels()
+ return create_response(200, {"code": 200, "data": labels, "msg": "success"})
+ except Exception as e:
+ traceback.print_exc()
+ return create_response(500, {"code": 500, "msg": f"获取标签失败: {str(e)}"})
+
+# --- 2. 输入联想建议 ---
+@app.get("/api/kg/node/suggest")
+def suggest_node(req):
+ try:
+ query_data = getattr(req, "queries", getattr(req, "query_params", {}))
+ raw_keyword = query_data.get("keyword", "")
+ keyword = raw_keyword[0] if isinstance(raw_keyword, list) and raw_keyword else str(raw_keyword)
+ clean_keyword = unquote(keyword).strip()
+
+ suggestions = operation_service.suggest_nodes(clean_keyword)
+ return create_response(200, {"code": 200, "data": suggestions, "msg": "success"})
+ except Exception as e:
+ traceback.print_exc()
+ return create_response(500, {"code": 500, "msg": str(e)})
+
+# --- 3. 获取分页节点列表 ---
+@app.get("/api/kg/nodes")
+def get_nodes(req):
+ try:
+ query_data = getattr(req, "queries", getattr(req, "query_params", {}))
+ page = int(query_data.get("page", "1"))
+ page_size = int(query_data.get("pageSize", "20"))
+
+ res_data = operation_service.get_nodes_subset(page, page_size)
+ return create_response(200, {"code": 200, "data": res_data, "msg": "success"})
+ except Exception as e:
+ traceback.print_exc()
+ return create_response(500, {"code": 500, "msg": f"获取节点列表失败: {str(e)}"})
+
+# --- 4. 获取分页关系列表 ---
+@app.get("/api/kg/relationships")
+def get_relationships(req):
+ try:
+ query_data = getattr(req, "queries", getattr(req, "query_params", {}))
+ page = int(query_data.get("page", "1"))
+ page_size = int(query_data.get("pageSize", "20"))
+
+ res_data = operation_service.get_relationships_subset(page, page_size)
+ return create_response(200, {"code": 200, "data": res_data, "msg": "success"})
+ except Exception as e:
+ traceback.print_exc()
+ return create_response(500, {"code": 500, "msg": f"获取关系列表失败: {str(e)}"})
+
+# --- 5. 新增节点 ---
+@app.post("/api/kg/node/add")
+def add_node(req):
+ try:
+ body = parse_request_body(req)
+ # 增加 strip() 防止空格导致的匹配失败
+ label = str(body.get("label", "Drug")).strip()
+ name = str(body.get("name", "")).strip()
+
+ if not name:
+ return create_response(200, {"code": 400, "msg": "添加失败:节点名称不能为空"})
+
+ result = operation_service.add_node(label, name)
+ return create_response(200, {"code": 200 if result["success"] else 400, "msg": result["msg"]})
+ except Exception as e:
+ traceback.print_exc()
+ return create_response(200, {"code": 500, "msg": f"服务器内部错误: {str(e)}"})
+
+# --- 6. 修改节点 ---
+@app.post("/api/kg/node/update")
+def update_node(req):
+ try:
+ body = parse_request_body(req)
+ node_id = body.get("id")
+ name = str(body.get("name", "")).strip()
+ label = str(body.get("label", "")).strip()
+
+ if not node_id or not name:
+ return create_response(200, {"code": 400, "msg": "修改失败:参数缺失(ID或Name)"})
+
+ result = operation_service.update_node(node_id, name, label)
+ return create_response(200, {"code": 200 if result["success"] else 400, "msg": result["msg"]})
+ except Exception as e:
+ traceback.print_exc()
+ return create_response(200, {"code": 500, "msg": str(e)})
+
+# --- 7. 新增关系 ---
+@app.post("/api/kg/rel/add")
+def add_relationship(req):
+ try:
+ body = parse_request_body(req)
+ # 强制格式化所有字符串,彻底杜绝数据类型导致的 MATCH 失败
+ source = str(body.get("source", "")).strip()
+ target = str(body.get("target", "")).strip()
+ rel_type = str(body.get("type", "")).strip()
+ rel_label = str(body.get("label", "")).strip()
+
+ if not all([source, target, rel_type]):
+ return create_response(200, {"code": 400, "msg": "添加失败:起始点、结束点和类型不能为空"})
+
+ result = operation_service.add_relationship(source, target, rel_type, rel_label)
+ return create_response(200, {"code": 200 if result["success"] else 400, "msg": result["msg"]})
+ except Exception as e:
+ traceback.print_exc()
+ return create_response(200, {"code": 500, "msg": f"新增关系异常: {str(e)}"})
+
+# --- 8. 修改关系 ---
+@app.post("/api/kg/rel/update")
+def update_rel(req):
+ try:
+ body = parse_request_body(req)
+ rel_id = body.get("id")
+ source = str(body.get("source", "")).strip()
+ target = str(body.get("target", "")).strip()
+ rel_type = str(body.get("type", "")).strip()
+ rel_label = str(body.get("label", "")).strip()
+
+ if not all([rel_id, source, target, rel_type]):
+ return create_response(200, {"code": 400, "msg": "修改失败:必要参数(ID/Source/Target/Type)缺失"})
+
+ result = operation_service.update_relationship(rel_id, source, target, rel_type, rel_label)
+ return create_response(200, {"code": 200 if result["success"] else 400, "msg": result["msg"]})
+ except Exception as e:
+ traceback.print_exc()
+ return create_response(200, {"code": 500, "msg": str(e)})
+
+# --- 9. 删除节点 ---
+@app.post("/api/kg/node/delete")
+def delete_node(req):
+ try:
+ body = parse_request_body(req)
+ node_id = body.get("id")
+ if not node_id:
+ return create_response(200, {"code": 400, "msg": "删除失败:节点ID不能为空"})
+
+ operation_service.delete_node(node_id)
+ return create_response(200, {"code": 200, "msg": "节点及其关联关系已成功删除"})
+ except Exception as e:
+ traceback.print_exc()
+ return create_response(200, {"code": 500, "msg": str(e)})
+
+# --- 10. 删除关系 ---
+@app.post("/api/kg/rel/delete")
+def delete_rel(req):
+ try:
+ body = parse_request_body(req)
+ rel_id = body.get("id")
+ if not rel_id:
+ return create_response(200, {"code": 400, "msg": "删除失败:关系ID不能为空"})
+
+ operation_service.delete_relationship(rel_id)
+ return create_response(200, {"code": 200, "msg": "关系已成功从数据库移除"})
+ except Exception as e:
+ traceback.print_exc()
+ return create_response(200, {"code": 500, "msg": str(e)})
\ No newline at end of file
diff --git a/service/OperationService.py b/service/OperationService.py
new file mode 100644
index 0000000..4f59609
--- /dev/null
+++ b/service/OperationService.py
@@ -0,0 +1,203 @@
+from util.neo4j_utils import neo4j_client
+from urllib.parse import unquote
+import traceback
+
+class OperationService:
+ def __init__(self):
+ self.db = neo4j_client
+
+ # --- 1. 节点查询:去掉强制命名的逻辑 ---
+ def get_nodes_subset(self, page: int = 1, page_size: int = 20):
+ skip_val = int((page - 1) * page_size)
+ limit_val = int(page_size)
+ # 去掉 Cypher 中的排序干扰,确保 ID 稳定
+ cypher = """
+ MATCH (n)
+ WITH count(n) AS total_count
+ MATCH (n)
+ RETURN elementId(n) AS id, labels(n) AS labels, n.name AS name, n.nodeId AS nodeId, total_count AS total
+ ORDER BY toInteger(n.nodeId) DESC
+ SKIP $skip LIMIT $limit
+ """
+ try:
+ params = {"skip": skip_val, "limit": limit_val}
+ raw_data = self.db.execute_read(cypher, params)
+ if not raw_data: return {"items": [], "total": 0}
+ items = []
+ for item in raw_data:
+ items.append({
+ "id": item["id"],
+ "labels": item["labels"],
+ # 修复:不再强转为“未命名”,保留真实状态
+ "name": item.get("name"),
+ "nodeId": item.get("nodeId") or 0
+ })
+ return {"items": items, "total": raw_data[0]['total']}
+ except Exception as e:
+ print(f"Service Error (Nodes): {e}")
+ return {"items": [], "total": 0}
+
+ # --- 2. 关系查询:去掉 coalesce 强制命名 ---
+ def get_relationships_subset(self, page: int = 1, page_size: int = 20):
+ skip_val = int((page - 1) * page_size)
+ limit_val = int(page_size)
+ # 修复:移除 coalesce(..., "未知节点")
+ cypher = """
+ MATCH (a)-[r]->(b)
+ WITH count(r) AS total_count
+ MATCH (a)-[r]->(b)
+ RETURN elementId(r) as id,
+ type(r) as type,
+ r.label as label,
+ a.name as source,
+ b.name as target,
+ coalesce(r.createTime, 0) as createTime,
+ total_count
+ ORDER BY createTime DESC
+ SKIP $skip LIMIT $limit
+ """
+ try:
+ params = {"skip": skip_val, "limit": limit_val}
+ raw_data = self.db.execute_read(cypher, params)
+ if not raw_data: return {"items": [], "total": 0}
+ items = []
+ for row in raw_data:
+ items.append({
+ "id": row["id"],
+ "type": row["type"],
+ "label": row["label"] if row["label"] is not None else "",
+ "source": row["source"],
+ "target": row["target"]
+ })
+ return {"items": items, "total": raw_data[0]['total_count']}
+ except Exception as e:
+ print(f"Service Error (Rels): {e}")
+ return {"items": [], "total": 0}
+
+ # --- 3. 联想建议:排除“未命名”干扰 ---
+ def suggest_nodes(self, keyword: str):
+ if not keyword: return []
+ try:
+ kw = unquote(str(keyword)).strip()
+ # 增加过滤:不返回名为“未命名”的建议
+ cypher = """
+ MATCH (n)
+ WHERE n.name CONTAINS $kw AND n.name <> '未命名'
+ RETURN DISTINCT n.name as name LIMIT 15
+ """
+ results = self.db.execute_read(cypher, {"kw": kw})
+ db_suggestions = [row["name"] for row in results if row["name"]]
+ suffix_suggestions = [f"{kw}片", f"{kw}胶囊", f"{kw}注射液"]
+ final_res = list(dict.fromkeys(db_suggestions + suffix_suggestions))
+ return final_res[:15]
+ except:
+ return []
+
+ # --- 4. 节点管理 ---
+ def add_node(self, label: str, name: str):
+ try:
+ nm = str(name).strip()
+ if not nm: return {"success": False, "msg": "名称不能为空"}
+
+ check_cypher = "MATCH (n) WHERE n.name = $name RETURN n LIMIT 1"
+ existing = self.db.execute_read(check_cypher, {"name": nm})
+ if existing:
+ return {"success": False, "msg": f"添加失败:已存在名为 '{nm}' 的节点"}
+
+ create_cypher = f"CREATE (n:`{label}` {{name: $name, nodeId: timestamp()}}) RETURN elementId(n) as id"
+ self.db.execute_write(create_cypher, {"name": nm})
+ return {"success": True, "msg": "添加成功"}
+ except Exception as e:
+ return {"success": False, "msg": f"写入失败: {str(e)}"}
+
+ def update_node(self, node_id: str, name: str, label: str):
+ try:
+ nm = str(name).strip()
+ if not nm: return {"success": False, "msg": "名称不能为空"}
+
+ check_name = "MATCH (n) WHERE n.name = $name AND elementId(n) <> $id RETURN n LIMIT 1"
+ existing = self.db.execute_read(check_name, {"name": nm, "id": node_id})
+ if existing:
+ return {"success": False, "msg": f"修改失败:库中已有其他名为 '{nm}' 的节点"}
+
+ cypher = f"""
+ MATCH (n) WHERE elementId(n) = $id
+ SET n.name = $name, n.nodeId = timestamp()
+ WITH n
+ REMOVE n:Drug:Disease:Symptom:Entity
+ WITH n
+ SET n:`{label}`
+ RETURN n
+ """
+ result = self.db.execute_write(cypher, {"id": node_id, "name": nm})
+ return {"success": True, "msg": "节点修改成功"} if result else {"success": False, "msg": "找不到该节点"}
+ except Exception as e:
+ return {"success": False, "msg": str(e)}
+
+ def delete_node(self, node_id: str):
+ cypher = "MATCH (n) WHERE elementId(n) = $id DETACH DELETE n"
+ return self.db.execute_write(cypher, {"id": node_id})
+
+ def get_all_labels(self):
+ cypher = "CALL db.labels()"
+ try:
+ results = self.db.execute_read(cypher)
+ labels = [list(row.values())[0] for row in results]
+ # 排除掉 Neo4j 默认的一些内部标签(如果有)
+ return labels if labels else ["Drug", "Disease", "Symptom"]
+ except:
+ return ["Drug", "Disease", "Symptom"]
+
+ # --- 5. 关系管理 ---
+ def add_relationship(self, source_name: str, target_name: str, rel_type: str, rel_label: str):
+ try:
+ s, t, l = str(source_name).strip(), str(target_name).strip(), str(rel_label).strip()
+
+ check_nodes = """
+ OPTIONAL MATCH (a) WHERE a.name = $s
+ OPTIONAL MATCH (b) WHERE b.name = $t
+ RETURN a IS NOT NULL as hasA, b IS NOT NULL as hasB
+ """
+ exists = self.db.execute_read(check_nodes, {"s": s, "t": t})
+
+ if not exists or not exists[0]['hasA'] or not exists[0]['hasB']:
+ err_msg = "添加失败: "
+ if not exists[0]['hasA']: err_msg += f"起始节点'{s}'不存在; "
+ if not exists[0]['hasB']: err_msg += f"结束节点'{t}'不存在"
+ return {"success": False, "msg": err_msg}
+
+ cypher = f"""
+ MATCH (a {{name: $s}}), (b {{name: $t}})
+ MERGE (a)-[r:`{rel_type}`]->(b)
+ ON CREATE SET r.label = $l, r.createTime = timestamp()
+ ON MATCH SET r.label = $l, r.updateTime = timestamp()
+ RETURN r
+ """
+ self.db.execute_write(cypher, {"s": s, "t": t, "l": l})
+ return {"success": True, "msg": "操作成功"}
+
+ except Exception as e:
+ traceback.print_exc()
+ return {"success": False, "msg": f"数据库写入异常: {str(e)}"}
+
+ def update_relationship(self, rel_id: str, source_name: str, target_name: str, rel_type: str, rel_label: str):
+ try:
+ s, t, l = str(source_name).strip(), str(target_name).strip(), str(rel_label).strip()
+ find_old = "MATCH (a)-[r]->(b) WHERE elementId(r) = $id RETURN type(r) as type, a.name as s, b.name as t"
+ old = self.db.execute_read(find_old, {"id": rel_id})
+ if not old: return {"success": False, "msg": "修改失败:原关系不存在"}
+
+ if old[0]['s'] != s or old[0]['t'] != t or old[0]['type'] != rel_type:
+ self.delete_relationship(rel_id)
+ return self.add_relationship(s, t, rel_type, l)
+ else:
+ update_cypher = "MATCH ()-[r]->() WHERE elementId(r) = $id SET r.label = $l, r.updateTime = timestamp() RETURN r"
+ self.db.execute_write(update_cypher, {"id": rel_id, "l": l})
+ return {"success": True, "msg": "修改成功"}
+ except Exception as e:
+ traceback.print_exc()
+ return {"success": False, "msg": f"修改异常: {str(e)}"}
+
+ def delete_relationship(self, rel_id: str):
+ cypher = "MATCH ()-[r]->() WHERE elementId(r) = $id DELETE r"
+ return self.db.execute_write(cypher, {"id": rel_id})
\ No newline at end of file
diff --git a/vue/.npmrc b/vue/.npmrc
new file mode 100644
index 0000000..a82f373
--- /dev/null
+++ b/vue/.npmrc
@@ -0,0 +1 @@
+overrides=entities@3.19.0
diff --git a/vue/src/router/index.js b/vue/src/router/index.js
index a410e6a..60b8bda 100644
--- a/vue/src/router/index.js
+++ b/vue/src/router/index.js
@@ -7,6 +7,8 @@ import Display from '../system/GraphDemo.vue'
import Builder from '../system/GraphBuilder.vue'
import Style from '../system/GraphStyle.vue'
import QA from '../system/GraphQA.vue'
+import KGData from '../system/KGData.vue'
+
const routes = [
{
path: '/',
@@ -59,7 +61,12 @@ const routes = [
path: '/kg-qa',
name: 'QA',
component: QA
- }
+ },
+ {
+ path: '/kg-data',
+ name: 'KGData',
+ component: KGData
+ },
]
const router = createRouter({
diff --git a/vue/src/system/KGData.vue b/vue/src/system/KGData.vue
new file mode 100644
index 0000000..b5da636
--- /dev/null
+++ b/vue/src/system/KGData.vue
@@ -0,0 +1,344 @@
+
+
+
+
+
+
+ 新增节点
+
+
+
+
+
+
+
+ {{ scope.row.name || 'N/A' }}
+
+
+
+
+
+
+ {{ label }}
+
+
+
+
+ 编辑
+ 删除
+
+
+
+
+
+
+
+
+
+
+ 新增关系
+
+
+
+
+
+ {{ scope.row.source || 'N/A' }}
+
+
+
+
+
+
+
+
+
+ {{ scope.row.target || 'N/A' }}
+
+
+
+
+
+
+
+ 编辑
+ 删除
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 取消
+ 确认
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 取消
+ 确认
+
+
+
+
+
+
\ No newline at end of file
diff --git a/web_main.py b/web_main.py
index f121dbc..4ada4f4 100644
--- a/web_main.py
+++ b/web_main.py
@@ -3,6 +3,10 @@ import controller
from service.UserService import init_mysql_connection
import os
+# 开启全局跨域支持,允许所有来源访问
+# 这将解决浏览器报 "CORS error" 或请求被拦截的问题
+ALLOW_CORS(app, ["*"])
+
# 添加静态文件服务
current_dir = os.path.dirname(os.path.abspath(__file__))
resource_dir = os.path.join(current_dir, "resource")
@@ -11,4 +15,7 @@ if os.path.exists(resource_dir):
print(f"静态资源目录已配置: {resource_dir}")
if __name__ == "__main__":
- init_mysql_connection() and app.start(host="0.0.0.0", port=8088)
\ No newline at end of file
+ # 启动服务
+ # 确保 init_mysql_connection 返回 True 或者去掉 and 逻辑以保证 start 执行
+ init_mysql_connection()
+ app.start(host="0.0.0.0", port=8088)
\ No newline at end of file