From 37cd2171b1ff35d7111a1fab1598e1d7cf3b4805 Mon Sep 17 00:00:00 2001 From: wonder Date: Wed, 15 Apr 2026 14:24:39 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=A2=9E=E5=8A=A0=E5=AF=B9neo4j?= =?UTF-8?q?=E7=9A=84=E5=A2=9E=E6=94=B9=E6=9F=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .claude/settings.local.json | 9 + CLAUDE.md | 82 +++ backend/docs/docs.go | 398 ++++++++++++ backend/docs/swagger.json | 1012 ++++++++++++++++++++--------- backend/docs/swagger.yaml | 359 ++++++++-- backend/handlers/crud_handler.go | 202 ++++++ backend/handlers/graph_handler.go | 8 +- backend/main.go | 19 + backend/models/requests.go | 47 ++ backend/services/neo4j_service.go | 352 ++++++++++ backend/services/service.go | 8 + 11 files changed, 2148 insertions(+), 348 deletions(-) create mode 100644 .claude/settings.local.json create mode 100644 CLAUDE.md create mode 100644 backend/handlers/crud_handler.go create mode 100644 backend/models/requests.go diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..7d76490 --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,9 @@ +{ + "permissions": { + "allow": [ + "Bash(curl -X POST \"http://localhost:3001/api/nodes\" -H \"Content-Type: application/json\" -d \"{\\\\\"id\\\\\":\\\\\"test_node_1\\\\\",\\\\\"label\\\\\":\\\\\"测试节点\\\\\",\\\\\"type\\\\\":\\\\\"测试类型\\\\\",\\\\\"x\\\\\":100,\\\\\"y\\\\\":200}\" -s)", + "Bash(curl -X PUT \"http://localhost:3001/api/nodes/test_node_1\" -H \"Content-Type: application/json\" -d \"{\\\\\"label\\\\\":\\\\\"更新后的测试节点\\\\\",\\\\\"x\\\\\":150}\" -s)", + "Bash(curl:*)" + ] + } +} diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..b940fc2 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,82 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +This is a knowledge graph visualization system with a Go backend providing REST APIs and a React frontend using AntV G6 for interactive graph rendering. The system stores graph data in Neo4j and provides nodes, edges, and relationship visualization. + +## Architecture + +### Backend (`/backend`) +- **Entry point**: `main.go` - Sets up Gin router, initializes Neo4j connection, registers handlers +- **Handlers** (`/handlers`): HTTP request handlers for `/api/graph`, `/api/search`, `/api/nodes/:id`, `/api/nodes/:id/neighbors` +- **Services** (`/services`): Business logic layer. `Neo4jService` queries Neo4j and converts results to domain models +- **Models** (`/models`): Domain models (`Node`, `Edge`, `GraphData`) and API response types +- **Neo4j client** (`/neo4j`): Connection wrapper using `github.com/neo4j/neo4j-go-driver/v5` +- **Config** (`/config`): Configuration loading from `.env` with CORS and server settings + +**Important**: Neo4j connection config is in `backend/config/.env` which contains credentials. The service queries Neo4j by executing Cypher queries and converting results to JSON. + +### Frontend (`/frontend`) +- **React 19 + TypeScript + Vite** build system +- **State management**: Zustand stores (`graphStore.ts`, `layoutStore.ts`) +- **Graph visualization**: AntV G6 v5 for force-directed layout and interactive graphs +- **Styling**: Tailwind CSS v4 + HeroUI components +- **API client**: Axios (`services/graphApi.ts`) + +**Key frontend components**: +- `KnowledgeGraph.tsx` - Main page that wires up search, graph view, and node panel +- `GraphView/index.tsx` - G6 graph rendering with node selection, highlighting +- `NodePanel/index.tsx` - Sidebar showing selected node details +- `SearchBar/index.tsx` - Search input with node autocomplete + +## Common Commands + +### Backend +```bash +cd backend +go run main.go # Start dev server on :3001 +swag init -g main.go # Regenerate Swagger docs +``` + +### Frontend +```bash +cd frontend +npm run dev # Start dev server on :5173 +npm run build # Production build +npm run lint # Lint code +``` + +### API Endpoints +- `GET /health` - Health check +- `GET /api/graph` - Get all graph data (nodes + edges) +- `GET /api/graph/stats` - Graph statistics +- `GET /api/search?q=query` - Search nodes by label, ID, or type +- `GET /api/nodes/:id` - Get node by ID +- `GET /api/nodes/:id/neighbors` - Get node neighbors and related edges +- `GET /swagger/*` - Swagger UI documentation + +## Data Flow + +1. Frontend fetches graph data via `/api/graph` +2. Graph data is stored in `graphStore` (Zustand) +3. `GraphView` renders with G6 force-directed layout +4. Node clicks highlight connected nodes and trigger `NodePanel` updates +5. Search queries `/api/search` and selects matching nodes + +## Neo4j Queries + +The backend uses these Cypher query patterns: +- All nodes: `MATCH (n) RETURN n` +- All edges: `MATCH (a)-[r]->(b) RETURN a.id AS source, b.id AS target, r` +- Node by ID: `MATCH (n {id: $id}) RETURN n` +- Search: `MATCH (n) WHERE toLower(n.label) CONTAINS toLower($query) OR ... RETURN n` +- Neighbors: `MATCH ({id: $id})-[r]-(m) RETURN m, r, startNode(r).id AS source, endNode(r).id AS target` + +## File Conventions + +- Backend handlers follow `*_handler.go` naming +- Swagger annotations in handlers with `@Summary`, `@Router`, etc. +- Frontend types in `types/graph.ts` shared between components +- Graph styling utilities in `utils/graphStyle.ts` diff --git a/backend/docs/docs.go b/backend/docs/docs.go index 1ecd82e..ffe91fb 100644 --- a/backend/docs/docs.go +++ b/backend/docs/docs.go @@ -15,6 +15,99 @@ const docTemplate = `{ "host": "{{.Host}}", "basePath": "{{.BasePath}}", "paths": { + "/api/edges": { + "post": { + "description": "在知识图谱中创建一个新边(关系)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "summary": "创建边", + "parameters": [ + { + "description": "边创建请求", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/models.CreateEdgeRequest" + } + } + ], + "responses": { + "200": { + "description": "创建成功", + "schema": { + "$ref": "#/definitions/models.EdgeResponse" + } + }, + "400": { + "description": "请求错误", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + }, + "404": { + "description": "源节点或目标节点未找到", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + }, + "409": { + "description": "边已存在", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + }, + "500": { + "description": "内部错误", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + } + } + } + }, + "/api/edges/{id}": { + "delete": { + "description": "从知识图谱中删除指定的边", + "produces": [ + "application/json" + ], + "summary": "删除边", + "parameters": [ + { + "type": "string", + "description": "边ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "删除成功", + "schema": { + "$ref": "#/definitions/models.DeleteResponse" + } + }, + "404": { + "description": "边未找到", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + }, + "500": { + "description": "内部错误", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + } + } + } + }, "/api/graph": { "get": { "description": "获取知识图谱的全部节点和边数据", @@ -62,6 +155,55 @@ const docTemplate = `{ } } }, + "/api/nodes": { + "post": { + "description": "在知识图谱中创建一个新节点", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "summary": "创建节点", + "parameters": [ + { + "description": "节点创建请求", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/models.CreateNodeRequest" + } + } + ], + "responses": { + "200": { + "description": "创建成功", + "schema": { + "$ref": "#/definitions/models.Node" + } + }, + "400": { + "description": "请求错误", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + }, + "409": { + "description": "节点已存在", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + }, + "500": { + "description": "内部错误", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + } + } + } + }, "/api/nodes/{id}": { "get": { "description": "根据节点 ID 获取知识图谱中的节点详细信息", @@ -104,6 +246,96 @@ const docTemplate = `{ } } } + }, + "put": { + "description": "更新知识图谱中指定节点的信息", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "summary": "更新节点", + "parameters": [ + { + "type": "string", + "description": "节点ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "节点更新请求", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/models.UpdateNodeRequest" + } + } + ], + "responses": { + "200": { + "description": "更新成功", + "schema": { + "$ref": "#/definitions/models.Node" + } + }, + "400": { + "description": "请求错误", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + }, + "404": { + "description": "节点未找到", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + }, + "500": { + "description": "内部错误", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + } + } + }, + "delete": { + "description": "从知识图谱中删除指定节点及其相关的所有边", + "produces": [ + "application/json" + ], + "summary": "删除节点", + "parameters": [ + { + "type": "string", + "description": "节点ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "删除成功", + "schema": { + "$ref": "#/definitions/models.DeleteResponse" + } + }, + "404": { + "description": "节点未找到", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + }, + "500": { + "description": "内部错误", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + } + } } }, "/api/nodes/{id}/neighbors": { @@ -190,6 +422,107 @@ const docTemplate = `{ } }, "definitions": { + "models.CreateEdgeRequest": { + "type": "object", + "required": [ + "id", + "label", + "source", + "target" + ], + "properties": { + "id": { + "description": "边的唯一标识", + "type": "string", + "example": "edge_123" + }, + "label": { + "description": "边的显示标签", + "type": "string", + "example": "包含" + }, + "properties": { + "description": "自定义属性", + "type": "object", + "additionalProperties": true + }, + "source": { + "description": "源节点ID", + "type": "string", + "example": "node_1" + }, + "target": { + "description": "目标节点ID", + "type": "string", + "example": "node_2" + }, + "type": { + "description": "边的类型(可选,默认为关系类型)", + "type": "string", + "example": "CONTAINS" + } + } + }, + "models.CreateNodeRequest": { + "type": "object", + "required": [ + "id", + "label", + "type" + ], + "properties": { + "id": { + "description": "节点唯一标识", + "type": "string", + "example": "node_123" + }, + "label": { + "description": "节点显示标签", + "type": "string", + "example": "人工智能" + }, + "properties": { + "description": "自定义属性", + "type": "object", + "additionalProperties": true + }, + "type": { + "description": "节点类型", + "type": "string", + "example": "概念" + }, + "x": { + "description": "X坐标(可选)", + "type": "number", + "example": 100 + }, + "y": { + "description": "Y坐标(可选)", + "type": "number", + "example": 200 + } + } + }, + "models.DeleteResponse": { + "type": "object", + "properties": { + "id": { + "description": "被删除的实体ID(可选)", + "type": "string", + "example": "node_123" + }, + "message": { + "description": "消息", + "type": "string", + "example": "节点已成功删除" + }, + "success": { + "description": "是否成功", + "type": "boolean", + "example": true + } + } + }, "models.Edge": { "type": "object", "properties": { @@ -218,6 +551,41 @@ const docTemplate = `{ } } }, + "models.EdgeResponse": { + "type": "object", + "properties": { + "id": { + "description": "边的ID", + "type": "string", + "example": "edge_123" + }, + "label": { + "description": "边的显示标签", + "type": "string", + "example": "包含" + }, + "properties": { + "description": "自定义属性", + "type": "object", + "additionalProperties": true + }, + "source": { + "description": "源节点ID", + "type": "string", + "example": "node_1" + }, + "target": { + "description": "目标节点ID", + "type": "string", + "example": "node_2" + }, + "type": { + "description": "边的类型", + "type": "string", + "example": "CONTAINS" + } + } + }, "models.ErrorResponse": { "type": "object", "properties": { @@ -325,6 +693,36 @@ const docTemplate = `{ } } } + }, + "models.UpdateNodeRequest": { + "type": "object", + "properties": { + "label": { + "description": "节点显示标签", + "type": "string", + "example": "机器学习" + }, + "properties": { + "description": "自定义属性(更新)", + "type": "object", + "additionalProperties": true + }, + "type": { + "description": "节点类型", + "type": "string", + "example": "工具" + }, + "x": { + "description": "X坐标", + "type": "number", + "example": 150 + }, + "y": { + "description": "Y坐标", + "type": "number", + "example": 250 + } + } } } }` diff --git a/backend/docs/swagger.json b/backend/docs/swagger.json index ac93659..4cc1b73 100644 --- a/backend/docs/swagger.json +++ b/backend/docs/swagger.json @@ -1,314 +1,722 @@ { - "swagger": "2.0", - "info": { - "description": "知识图谱后端服务,提供图数据查询、节点搜索等功能", - "title": "知识图谱 API", - "contact": {}, - "version": "1.0.0" - }, - "host": "localhost:3001", - "basePath": "/", - "paths": { - "/api/graph": { - "get": { - "description": "获取知识图谱的全部节点和边数据", - "produces": ["application/json"], - "summary": "获取图数据", - "responses": { - "200": { - "description": "成功", - "schema": { - "$ref": "#/definitions/models.GraphData" - } - }, - "500": { - "description": "内部错误", - "schema": { - "$ref": "#/definitions/models.ErrorResponse" - } - } - } - } + "swagger": "2.0", + "info": { + "description": "知识图谱后端服务,提供图数据查询、节点搜索等功能", + "title": "知识图谱 API", + "contact": {}, + "version": "1.0.0" }, - "/api/graph/stats": { - "get": { - "description": "获取知识图谱的节点数、边数等统计信息", - "produces": ["application/json"], - "summary": "获取图统计信息", - "responses": { - "200": { - "description": "成功", - "schema": { - "type": "object", - "additionalProperties": true + "host": "localhost:3001", + "basePath": "/", + "paths": { + "/api/edges": { + "post": { + "description": "在知识图谱中创建一个新边(关系)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "summary": "创建边", + "parameters": [ + { + "description": "边创建请求", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/models.CreateEdgeRequest" + } + } + ], + "responses": { + "200": { + "description": "创建成功", + "schema": { + "$ref": "#/definitions/models.EdgeResponse" + } + }, + "400": { + "description": "请求错误", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + }, + "404": { + "description": "源节点或目标节点未找到", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + }, + "409": { + "description": "边已存在", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + }, + "500": { + "description": "内部错误", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + } + } } - }, - "500": { - "description": "内部错误", - "schema": { - "$ref": "#/definitions/models.ErrorResponse" + }, + "/api/edges/{id}": { + "delete": { + "description": "从知识图谱中删除指定的边", + "produces": [ + "application/json" + ], + "summary": "删除边", + "parameters": [ + { + "type": "string", + "description": "边ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "删除成功", + "schema": { + "$ref": "#/definitions/models.DeleteResponse" + } + }, + "404": { + "description": "边未找到", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + }, + "500": { + "description": "内部错误", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + } + } + } + }, + "/api/graph": { + "get": { + "description": "获取知识图谱的全部节点和边数据", + "produces": [ + "application/json" + ], + "summary": "获取图数据", + "responses": { + "200": { + "description": "成功", + "schema": { + "$ref": "#/definitions/models.GraphData" + } + }, + "500": { + "description": "内部错误", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + } + } + } + }, + "/api/graph/stats": { + "get": { + "description": "获取知识图谱的节点数、边数等统计信息", + "produces": [ + "application/json" + ], + "summary": "获取图统计信息", + "responses": { + "200": { + "description": "成功", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "500": { + "description": "内部错误", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + } + } + } + }, + "/api/nodes": { + "post": { + "description": "在知识图谱中创建一个新节点", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "summary": "创建节点", + "parameters": [ + { + "description": "节点创建请求", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/models.CreateNodeRequest" + } + } + ], + "responses": { + "200": { + "description": "创建成功", + "schema": { + "$ref": "#/definitions/models.Node" + } + }, + "400": { + "description": "请求错误", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + }, + "409": { + "description": "节点已存在", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + }, + "500": { + "description": "内部错误", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + } + } + } + }, + "/api/nodes/{id}": { + "get": { + "description": "根据节点 ID 获取知识图谱中的节点详细信息", + "produces": [ + "application/json" + ], + "summary": "获取节点详情", + "parameters": [ + { + "type": "string", + "description": "节点 ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "成功", + "schema": { + "$ref": "#/definitions/models.NodeResponse" + } + }, + "400": { + "description": "请求错误", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + }, + "404": { + "description": "节点未找到", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + }, + "500": { + "description": "内部错误", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + } + } + }, + "put": { + "description": "更新知识图谱中指定节点的信息", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "summary": "更新节点", + "parameters": [ + { + "type": "string", + "description": "节点ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "节点更新请求", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/models.UpdateNodeRequest" + } + } + ], + "responses": { + "200": { + "description": "更新成功", + "schema": { + "$ref": "#/definitions/models.Node" + } + }, + "400": { + "description": "请求错误", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + }, + "404": { + "description": "节点未找到", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + }, + "500": { + "description": "内部错误", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + } + } + }, + "delete": { + "description": "从知识图谱中删除指定节点及其相关的所有边", + "produces": [ + "application/json" + ], + "summary": "删除节点", + "parameters": [ + { + "type": "string", + "description": "节点ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "删除成功", + "schema": { + "$ref": "#/definitions/models.DeleteResponse" + } + }, + "404": { + "description": "节点未找到", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + }, + "500": { + "description": "内部错误", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + } + } + } + }, + "/api/nodes/{id}/neighbors": { + "get": { + "description": "根据节点 ID 获取其所有邻居节点及关联边", + "produces": [ + "application/json" + ], + "summary": "获取节点邻居", + "parameters": [ + { + "type": "string", + "description": "节点 ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "成功", + "schema": { + "$ref": "#/definitions/models.NeighborResponse" + } + }, + "400": { + "description": "请求错误", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + }, + "404": { + "description": "节点未找到", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + }, + "500": { + "description": "内部错误", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + } + } + } + }, + "/api/search": { + "get": { + "description": "根据关键词搜索知识图谱中的节点", + "produces": [ + "application/json" + ], + "summary": "搜索节点", + "parameters": [ + { + "type": "string", + "description": "搜索关键词", + "name": "q", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "成功", + "schema": { + "$ref": "#/definitions/models.SearchResponse" + } + }, + "400": { + "description": "请求错误", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + }, + "500": { + "description": "内部错误", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + } + } } - } } - } }, - "/api/nodes/{id}": { - "get": { - "description": "根据节点 ID 获取知识图谱中的节点详细信息", - "produces": ["application/json"], - "summary": "获取节点详情", - "parameters": [ - { - "type": "string", - "description": "节点 ID", - "name": "id", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "成功", - "schema": { - "$ref": "#/definitions/models.NodeResponse" + "definitions": { + "models.CreateEdgeRequest": { + "type": "object", + "required": [ + "id", + "label", + "source", + "target" + ], + "properties": { + "id": { + "description": "边的唯一标识", + "type": "string", + "example": "edge_123" + }, + "label": { + "description": "边的显示标签", + "type": "string", + "example": "包含" + }, + "properties": { + "description": "自定义属性", + "type": "object", + "additionalProperties": true + }, + "source": { + "description": "源节点ID", + "type": "string", + "example": "node_1" + }, + "target": { + "description": "目标节点ID", + "type": "string", + "example": "node_2" + }, + "type": { + "description": "边的类型(可选,默认为关系类型)", + "type": "string", + "example": "CONTAINS" + } } - }, - "400": { - "description": "请求错误", - "schema": { - "$ref": "#/definitions/models.ErrorResponse" + }, + "models.CreateNodeRequest": { + "type": "object", + "required": [ + "id", + "label", + "type" + ], + "properties": { + "id": { + "description": "节点唯一标识", + "type": "string", + "example": "node_123" + }, + "label": { + "description": "节点显示标签", + "type": "string", + "example": "人工智能" + }, + "properties": { + "description": "自定义属性", + "type": "object", + "additionalProperties": true + }, + "type": { + "description": "节点类型", + "type": "string", + "example": "概念" + }, + "x": { + "description": "X坐标(可选)", + "type": "number", + "example": 100 + }, + "y": { + "description": "Y坐标(可选)", + "type": "number", + "example": 200 + } } - }, - "404": { - "description": "节点未找到", - "schema": { - "$ref": "#/definitions/models.ErrorResponse" + }, + "models.DeleteResponse": { + "type": "object", + "properties": { + "id": { + "description": "被删除的实体ID(可选)", + "type": "string", + "example": "node_123" + }, + "message": { + "description": "消息", + "type": "string", + "example": "节点已成功删除" + }, + "success": { + "description": "是否成功", + "type": "boolean", + "example": true + } } - }, - "500": { - "description": "内部错误", - "schema": { - "$ref": "#/definitions/models.ErrorResponse" + }, + "models.Edge": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "label": { + "type": "string" + }, + "properties": { + "type": "object", + "additionalProperties": true + }, + "source": { + "type": "string" + }, + "style": { + "type": "object", + "additionalProperties": true + }, + "target": { + "type": "string" + }, + "type": { + "type": "string" + } + } + }, + "models.EdgeResponse": { + "type": "object", + "properties": { + "id": { + "description": "边的ID", + "type": "string", + "example": "edge_123" + }, + "label": { + "description": "边的显示标签", + "type": "string", + "example": "包含" + }, + "properties": { + "description": "自定义属性", + "type": "object", + "additionalProperties": true + }, + "source": { + "description": "源节点ID", + "type": "string", + "example": "node_1" + }, + "target": { + "description": "目标节点ID", + "type": "string", + "example": "node_2" + }, + "type": { + "description": "边的类型", + "type": "string", + "example": "CONTAINS" + } + } + }, + "models.ErrorResponse": { + "type": "object", + "properties": { + "error": { + "type": "string" + }, + "message": { + "type": "string" + } + } + }, + "models.GraphData": { + "type": "object", + "properties": { + "edges": { + "type": "array", + "items": { + "$ref": "#/definitions/models.Edge" + } + }, + "nodes": { + "type": "array", + "items": { + "$ref": "#/definitions/models.Node" + } + } + } + }, + "models.NeighborResponse": { + "type": "object", + "properties": { + "edges": { + "type": "array", + "items": { + "$ref": "#/definitions/models.Edge" + } + }, + "nodes": { + "type": "array", + "items": { + "$ref": "#/definitions/models.Node" + } + } + } + }, + "models.Node": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "label": { + "type": "string" + }, + "properties": { + "type": "object", + "additionalProperties": true + }, + "style": { + "type": "object", + "additionalProperties": true + }, + "type": { + "type": "string" + }, + "x": { + "type": "number" + }, + "y": { + "type": "number" + } + } + }, + "models.NodeResponse": { + "type": "object", + "properties": { + "id": { + "type": "string", + "example": "1" + }, + "label": { + "type": "string", + "example": "人工智能" + }, + "properties": { + "type": "object", + "additionalProperties": true + }, + "type": { + "type": "string", + "example": "概念" + } + } + }, + "models.SearchResponse": { + "type": "object", + "properties": { + "count": { + "type": "integer" + }, + "results": { + "type": "array", + "items": { + "$ref": "#/definitions/models.Node" + } + } + } + }, + "models.UpdateNodeRequest": { + "type": "object", + "properties": { + "label": { + "description": "节点显示标签", + "type": "string", + "example": "机器学习" + }, + "properties": { + "description": "自定义属性(更新)", + "type": "object", + "additionalProperties": true + }, + "type": { + "description": "节点类型", + "type": "string", + "example": "工具" + }, + "x": { + "description": "X坐标", + "type": "number", + "example": 150 + }, + "y": { + "description": "Y坐标", + "type": "number", + "example": 250 + } } - } } - } - }, - "/api/nodes/{id}/neighbors": { - "get": { - "description": "根据节点 ID 获取其所有邻居节点及关联边", - "produces": ["application/json"], - "summary": "获取节点邻居", - "parameters": [ - { - "type": "string", - "description": "节点 ID", - "name": "id", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "成功", - "schema": { - "$ref": "#/definitions/models.NeighborResponse" - } - }, - "400": { - "description": "请求错误", - "schema": { - "$ref": "#/definitions/models.ErrorResponse" - } - }, - "404": { - "description": "节点未找到", - "schema": { - "$ref": "#/definitions/models.ErrorResponse" - } - }, - "500": { - "description": "内部错误", - "schema": { - "$ref": "#/definitions/models.ErrorResponse" - } - } - } - } - }, - "/api/search": { - "get": { - "description": "根据关键词搜索知识图谱中的节点", - "produces": ["application/json"], - "summary": "搜索节点", - "parameters": [ - { - "type": "string", - "description": "搜索关键词", - "name": "q", - "in": "query", - "required": true - } - ], - "responses": { - "200": { - "description": "成功", - "schema": { - "$ref": "#/definitions/models.SearchResponse" - } - }, - "400": { - "description": "请求错误", - "schema": { - "$ref": "#/definitions/models.ErrorResponse" - } - }, - "500": { - "description": "内部错误", - "schema": { - "$ref": "#/definitions/models.ErrorResponse" - } - } - } - } } - }, - "definitions": { - "models.Edge": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "label": { - "type": "string" - }, - "properties": { - "type": "object", - "additionalProperties": true - }, - "source": { - "type": "string" - }, - "style": { - "type": "object", - "additionalProperties": true - }, - "target": { - "type": "string" - }, - "type": { - "type": "string" - } - } - }, - "models.ErrorResponse": { - "type": "object", - "properties": { - "error": { - "type": "string" - }, - "message": { - "type": "string" - } - } - }, - "models.GraphData": { - "type": "object", - "properties": { - "edges": { - "type": "array", - "items": { - "$ref": "#/definitions/models.Edge" - } - }, - "nodes": { - "type": "array", - "items": { - "$ref": "#/definitions/models.Node" - } - } - } - }, - "models.NeighborResponse": { - "type": "object", - "properties": { - "edges": { - "type": "array", - "items": { - "$ref": "#/definitions/models.Edge" - } - }, - "nodes": { - "type": "array", - "items": { - "$ref": "#/definitions/models.Node" - } - } - } - }, - "models.Node": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "label": { - "type": "string" - }, - "properties": { - "type": "object", - "additionalProperties": true - }, - "style": { - "type": "object", - "additionalProperties": true - }, - "type": { - "type": "string" - }, - "x": { - "type": "number" - }, - "y": { - "type": "number" - } - } - }, - "models.NodeResponse": { - "type": "object", - "properties": { - "id": { - "type": "string", - "example": "1" - }, - "label": { - "type": "string", - "example": "人工智能" - }, - "properties": { - "type": "object", - "additionalProperties": true - }, - "type": { - "type": "string", - "example": "概念" - } - } - }, - "models.SearchResponse": { - "type": "object", - "properties": { - "count": { - "type": "integer" - }, - "results": { - "type": "array", - "items": { - "$ref": "#/definitions/models.Node" - } - } - } - } - } -} +} \ No newline at end of file diff --git a/backend/docs/swagger.yaml b/backend/docs/swagger.yaml index 6879bb1..5a8e458 100644 --- a/backend/docs/swagger.yaml +++ b/backend/docs/swagger.yaml @@ -1,5 +1,83 @@ basePath: / definitions: + models.CreateEdgeRequest: + properties: + id: + description: 边的唯一标识 + example: edge_123 + type: string + label: + description: 边的显示标签 + example: 包含 + type: string + properties: + additionalProperties: true + description: 自定义属性 + type: object + source: + description: 源节点ID + example: node_1 + type: string + target: + description: 目标节点ID + example: node_2 + type: string + type: + description: 边的类型(可选,默认为关系类型) + example: CONTAINS + type: string + required: + - id + - label + - source + - target + type: object + models.CreateNodeRequest: + properties: + id: + description: 节点唯一标识 + example: node_123 + type: string + label: + description: 节点显示标签 + example: 人工智能 + type: string + properties: + additionalProperties: true + description: 自定义属性 + type: object + type: + description: 节点类型 + example: 概念 + type: string + x: + description: X坐标(可选) + example: 100 + type: number + "y": + description: Y坐标(可选) + example: 200 + type: number + required: + - id + - label + - type + type: object + models.DeleteResponse: + properties: + id: + description: 被删除的实体ID(可选) + example: node_123 + type: string + message: + description: 消息 + example: 节点已成功删除 + type: string + success: + description: 是否成功 + example: true + type: boolean + type: object models.Edge: properties: id: @@ -19,6 +97,33 @@ definitions: type: type: string type: object + models.EdgeResponse: + properties: + id: + description: 边的ID + example: edge_123 + type: string + label: + description: 边的显示标签 + example: 包含 + type: string + properties: + additionalProperties: true + description: 自定义属性 + type: object + source: + description: 源节点ID + example: node_1 + type: string + target: + description: 目标节点ID + example: node_2 + type: string + type: + description: 边的类型 + example: CONTAINS + type: string + type: object models.ErrorResponse: properties: error: @@ -30,22 +135,22 @@ definitions: properties: edges: items: - $ref: "#/definitions/models.Edge" + $ref: '#/definitions/models.Edge' type: array nodes: items: - $ref: "#/definitions/models.Node" + $ref: '#/definitions/models.Node' type: array type: object models.NeighborResponse: properties: edges: items: - $ref: "#/definitions/models.Edge" + $ref: '#/definitions/models.Edge' type: array nodes: items: - $ref: "#/definitions/models.Node" + $ref: '#/definitions/models.Node' type: array type: object models.Node: @@ -88,9 +193,32 @@ definitions: type: integer results: items: - $ref: "#/definitions/models.Node" + $ref: '#/definitions/models.Node' type: array type: object + models.UpdateNodeRequest: + properties: + label: + description: 节点显示标签 + example: 机器学习 + type: string + properties: + additionalProperties: true + description: 自定义属性(更新) + type: object + type: + description: 节点类型 + example: 工具 + type: string + x: + description: X坐标 + example: 150 + type: number + "y": + description: Y坐标 + example: 250 + type: number + type: object host: localhost:3001 info: contact: {} @@ -98,26 +226,87 @@ info: title: 知识图谱 API version: 1.0.0 paths: + /api/edges: + post: + consumes: + - application/json + description: 在知识图谱中创建一个新边(关系) + parameters: + - description: 边创建请求 + in: body + name: request + required: true + schema: + $ref: '#/definitions/models.CreateEdgeRequest' + produces: + - application/json + responses: + "200": + description: 创建成功 + schema: + $ref: '#/definitions/models.EdgeResponse' + "400": + description: 请求错误 + schema: + $ref: '#/definitions/models.ErrorResponse' + "404": + description: 源节点或目标节点未找到 + schema: + $ref: '#/definitions/models.ErrorResponse' + "409": + description: 边已存在 + schema: + $ref: '#/definitions/models.ErrorResponse' + "500": + description: 内部错误 + schema: + $ref: '#/definitions/models.ErrorResponse' + summary: 创建边 + /api/edges/{id}: + delete: + description: 从知识图谱中删除指定的边 + parameters: + - description: 边ID + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: 删除成功 + schema: + $ref: '#/definitions/models.DeleteResponse' + "404": + description: 边未找到 + schema: + $ref: '#/definitions/models.ErrorResponse' + "500": + description: 内部错误 + schema: + $ref: '#/definitions/models.ErrorResponse' + summary: 删除边 /api/graph: get: description: 获取知识图谱的全部节点和边数据 produces: - - application/json + - application/json responses: "200": description: 成功 schema: - $ref: "#/definitions/models.GraphData" + $ref: '#/definitions/models.GraphData' "500": description: 内部错误 schema: - $ref: "#/definitions/models.ErrorResponse" + $ref: '#/definitions/models.ErrorResponse' summary: 获取图数据 /api/graph/stats: get: description: 获取知识图谱的节点数、边数等统计信息 produces: - - application/json + - application/json responses: "200": description: 成功 @@ -127,89 +316,181 @@ paths: "500": description: 内部错误 schema: - $ref: "#/definitions/models.ErrorResponse" + $ref: '#/definitions/models.ErrorResponse' summary: 获取图统计信息 + /api/nodes: + post: + consumes: + - application/json + description: 在知识图谱中创建一个新节点 + parameters: + - description: 节点创建请求 + in: body + name: request + required: true + schema: + $ref: '#/definitions/models.CreateNodeRequest' + produces: + - application/json + responses: + "200": + description: 创建成功 + schema: + $ref: '#/definitions/models.Node' + "400": + description: 请求错误 + schema: + $ref: '#/definitions/models.ErrorResponse' + "409": + description: 节点已存在 + schema: + $ref: '#/definitions/models.ErrorResponse' + "500": + description: 内部错误 + schema: + $ref: '#/definitions/models.ErrorResponse' + summary: 创建节点 /api/nodes/{id}: + delete: + description: 从知识图谱中删除指定节点及其相关的所有边 + parameters: + - description: 节点ID + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: 删除成功 + schema: + $ref: '#/definitions/models.DeleteResponse' + "404": + description: 节点未找到 + schema: + $ref: '#/definitions/models.ErrorResponse' + "500": + description: 内部错误 + schema: + $ref: '#/definitions/models.ErrorResponse' + summary: 删除节点 get: description: 根据节点 ID 获取知识图谱中的节点详细信息 parameters: - - description: 节点 ID - in: path - name: id - required: true - type: string + - description: 节点 ID + in: path + name: id + required: true + type: string produces: - - application/json + - application/json responses: "200": description: 成功 schema: - $ref: "#/definitions/models.NodeResponse" + $ref: '#/definitions/models.NodeResponse' "400": description: 请求错误 schema: - $ref: "#/definitions/models.ErrorResponse" + $ref: '#/definitions/models.ErrorResponse' "404": description: 节点未找到 schema: - $ref: "#/definitions/models.ErrorResponse" + $ref: '#/definitions/models.ErrorResponse' "500": description: 内部错误 schema: - $ref: "#/definitions/models.ErrorResponse" + $ref: '#/definitions/models.ErrorResponse' summary: 获取节点详情 + put: + consumes: + - application/json + description: 更新知识图谱中指定节点的信息 + parameters: + - description: 节点ID + in: path + name: id + required: true + type: string + - description: 节点更新请求 + in: body + name: request + required: true + schema: + $ref: '#/definitions/models.UpdateNodeRequest' + produces: + - application/json + responses: + "200": + description: 更新成功 + schema: + $ref: '#/definitions/models.Node' + "400": + description: 请求错误 + schema: + $ref: '#/definitions/models.ErrorResponse' + "404": + description: 节点未找到 + schema: + $ref: '#/definitions/models.ErrorResponse' + "500": + description: 内部错误 + schema: + $ref: '#/definitions/models.ErrorResponse' + summary: 更新节点 /api/nodes/{id}/neighbors: get: description: 根据节点 ID 获取其所有邻居节点及关联边 parameters: - - description: 节点 ID - in: path - name: id - required: true - type: string + - description: 节点 ID + in: path + name: id + required: true + type: string produces: - - application/json + - application/json responses: "200": description: 成功 schema: - $ref: "#/definitions/models.NeighborResponse" + $ref: '#/definitions/models.NeighborResponse' "400": description: 请求错误 schema: - $ref: "#/definitions/models.ErrorResponse" + $ref: '#/definitions/models.ErrorResponse' "404": description: 节点未找到 schema: - $ref: "#/definitions/models.ErrorResponse" + $ref: '#/definitions/models.ErrorResponse' "500": description: 内部错误 schema: - $ref: "#/definitions/models.ErrorResponse" + $ref: '#/definitions/models.ErrorResponse' summary: 获取节点邻居 /api/search: get: description: 根据关键词搜索知识图谱中的节点 parameters: - - description: 搜索关键词 - in: query - name: q - required: true - type: string + - description: 搜索关键词 + in: query + name: q + required: true + type: string produces: - - application/json + - application/json responses: "200": description: 成功 schema: - $ref: "#/definitions/models.SearchResponse" + $ref: '#/definitions/models.SearchResponse' "400": description: 请求错误 schema: - $ref: "#/definitions/models.ErrorResponse" + $ref: '#/definitions/models.ErrorResponse' "500": description: 内部错误 schema: - $ref: "#/definitions/models.ErrorResponse" + $ref: '#/definitions/models.ErrorResponse' summary: 搜索节点 swagger: "2.0" diff --git a/backend/handlers/crud_handler.go b/backend/handlers/crud_handler.go new file mode 100644 index 0000000..54fa0bc --- /dev/null +++ b/backend/handlers/crud_handler.go @@ -0,0 +1,202 @@ +package handlers + +import ( + "net/http" + + "github.com/gin-gonic/gin" + "knowledge-graph-backend/models" + "knowledge-graph-backend/services" +) + +// NodeCRUDHandler 节点的增删改处理器 +type NodeCRUDHandler struct { + service services.GraphService +} + +// NewNodeCRUDHandler 创建节点CRUD处理器 +func NewNodeCRUDHandler(service services.GraphService) *NodeCRUDHandler { + return &NodeCRUDHandler{ + service: service, + } +} + +// EdgeCRUDHandler 边的增删改处理器 +type EdgeCRUDHandler struct { + service services.GraphService +} + +// NewEdgeCRUDHandler 创建边CRUD处理器 +func NewEdgeCRUDHandler(service services.GraphService) *EdgeCRUDHandler { + return &EdgeCRUDHandler{ + service: service, + } +} + +// CreateNode 创建新节点 +// @Summary 创建节点 +// @Description 在知识图谱中创建一个新节点 +// @Accept json +// @Produce json +// @Param request body models.CreateNodeRequest true "节点创建请求" +// @Success 200 {object} models.Node "创建成功" +// @Failure 400 {object} models.ErrorResponse "请求错误" +// @Failure 409 {object} models.ErrorResponse "节点已存在" +// @Failure 500 {object} models.ErrorResponse "内部错误" +// @Router /api/nodes [post] +func (h *NodeCRUDHandler) CreateNode(c *gin.Context) { + var req models.CreateNodeRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, models.ErrorResponse{ + Error: "invalid_request", + Message: err.Error(), + }) + return + } + + node, err := h.service.CreateNode(req) + if err != nil { + c.JSON(http.StatusConflict, models.ErrorResponse{ + Error: "create_failed", + Message: err.Error(), + }) + return + } + + c.JSON(http.StatusOK, node) +} + +// UpdateNode 更新节点 +// @Summary 更新节点 +// @Description 更新知识图谱中指定节点的信息 +// @Accept json +// @Produce json +// @Param id path string true "节点ID" +// @Param request body models.UpdateNodeRequest true "节点更新请求" +// @Success 200 {object} models.Node "更新成功" +// @Failure 400 {object} models.ErrorResponse "请求错误" +// @Failure 404 {object} models.ErrorResponse "节点未找到" +// @Failure 500 {object} models.ErrorResponse "内部错误" +// @Router /api/nodes/{id} [put] +func (h *NodeCRUDHandler) UpdateNode(c *gin.Context) { + id := c.Param("id") + + var req models.UpdateNodeRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, models.ErrorResponse{ + Error: "invalid_request", + Message: err.Error(), + }) + return + } + + node, err := h.service.UpdateNode(id, req) + if err != nil { + c.JSON(http.StatusNotFound, models.ErrorResponse{ + Error: "update_failed", + Message: err.Error(), + }) + return + } + + c.JSON(http.StatusOK, node) +} + +// DeleteNode 删除节点 +// @Summary 删除节点 +// @Description 从知识图谱中删除指定节点及其相关的所有边 +// @Produce json +// @Param id path string true "节点ID" +// @Success 200 {object} models.DeleteResponse "删除成功" +// @Failure 404 {object} models.ErrorResponse "节点未找到" +// @Failure 500 {object} models.ErrorResponse "内部错误" +// @Router /api/nodes/{id} [delete] +func (h *NodeCRUDHandler) DeleteNode(c *gin.Context) { + id := c.Param("id") + + err := h.service.DeleteNode(id) + if err != nil { + c.JSON(http.StatusNotFound, models.ErrorResponse{ + Error: "delete_failed", + Message: err.Error(), + }) + return + } + + c.JSON(http.StatusOK, models.DeleteResponse{ + Success: true, + Message: "节点已成功删除", + ID: id, + }) +} + +// CreateEdge 创建新边 +// @Summary 创建边 +// @Description 在知识图谱中创建一个新边(关系) +// @Accept json +// @Produce json +// @Param request body models.CreateEdgeRequest true "边创建请求" +// @Success 200 {object} models.EdgeResponse "创建成功" +// @Failure 400 {object} models.ErrorResponse "请求错误" +// @Failure 404 {object} models.ErrorResponse "源节点或目标节点未找到" +// @Failure 409 {object} models.ErrorResponse "边已存在" +// @Failure 500 {object} models.ErrorResponse "内部错误" +// @Router /api/edges [post] +func (h *EdgeCRUDHandler) CreateEdge(c *gin.Context) { + var req models.CreateEdgeRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, models.ErrorResponse{ + Error: "invalid_request", + Message: err.Error(), + }) + return + } + + edge, err := h.service.CreateEdge(req) + if err != nil { + c.JSON(http.StatusConflict, models.ErrorResponse{ + Error: "create_failed", + Message: err.Error(), + }) + return + } + + // 转换为响应格式 + response := models.EdgeResponse{ + ID: edge.ID, + Source: edge.Source, + Target: edge.Target, + Label: edge.Label, + Type: edge.Type, + Properties: edge.Properties, + } + + c.JSON(http.StatusOK, response) +} + +// DeleteEdge 删除边 +// @Summary 删除边 +// @Description 从知识图谱中删除指定的边 +// @Produce json +// @Param id path string true "边ID" +// @Success 200 {object} models.DeleteResponse "删除成功" +// @Failure 404 {object} models.ErrorResponse "边未找到" +// @Failure 500 {object} models.ErrorResponse "内部错误" +// @Router /api/edges/{id} [delete] +func (h *EdgeCRUDHandler) DeleteEdge(c *gin.Context) { + edgeID := c.Param("id") + + err := h.service.DeleteEdge(edgeID) + if err != nil { + c.JSON(http.StatusNotFound, models.ErrorResponse{ + Error: "delete_failed", + Message: err.Error(), + }) + return + } + + c.JSON(http.StatusOK, models.DeleteResponse{ + Success: true, + Message: "边已成功删除", + ID: edgeID, + }) +} diff --git a/backend/handlers/graph_handler.go b/backend/handlers/graph_handler.go index b427931..c33ca86 100644 --- a/backend/handlers/graph_handler.go +++ b/backend/handlers/graph_handler.go @@ -36,11 +36,5 @@ func (h *GraphHandler) GetGraphData(c *gin.Context) { // @Router /api/graph/stats [get] func (h *GraphHandler) GetStats(c *gin.Context) { stats := h.service.GetStats() - c.JSON(http.StatusOK, gin.H{ - "stats": stats, - "timestamp": gin.H{ - "totalNodes": stats["totalNodes"], - "totalEdges": stats["totalEdges"], - }, - }) + c.JSON(http.StatusOK, stats) } diff --git a/backend/main.go b/backend/main.go index 6b58861..c85e1c9 100644 --- a/backend/main.go +++ b/backend/main.go @@ -37,6 +37,10 @@ func main() { searchHandler := handlers.NewSearchHandler(graphService) nodeHandler := handlers.NewNodeHandler(graphService) + // 增删改处理器 + nodeCRUDHandler := handlers.NewNodeCRUDHandler(graphService) + edgeCRUDHandler := handlers.NewEdgeCRUDHandler(graphService) + router := gin.Default() router.Use(cors.New(cfg.CORS.ToGinConfig())) @@ -64,8 +68,18 @@ func main() { api.GET("/search", searchHandler.SearchNodes) + // 节点查询路由 api.GET("/nodes/:id", nodeHandler.GetNodeByID) api.GET("/nodes/:id/neighbors", nodeHandler.GetNeighbors) + + // 节点增删改路由 + api.POST("/nodes", nodeCRUDHandler.CreateNode) + api.PUT("/nodes/:id", nodeCRUDHandler.UpdateNode) + api.DELETE("/nodes/:id", nodeCRUDHandler.DeleteNode) + + // 边增删改路由 + api.POST("/edges", edgeCRUDHandler.CreateEdge) + api.DELETE("/edges/:id", edgeCRUDHandler.DeleteEdge) } log.Println("Knowledge Graph API Server running on http://localhost:" + cfg.Server.Port) @@ -77,6 +91,11 @@ func main() { log.Println(" GET /api/search?q=query - Search nodes") log.Println(" GET /api/nodes/:id - Get node by ID") log.Println(" GET /api/nodes/:id/neighbors - Get node neighbors") + log.Println(" POST /api/nodes - Create new node") + log.Println(" PUT /api/nodes/:id - Update node") + log.Println(" DELETE /api/nodes/:id - Delete node") + log.Println(" POST /api/edges - Create new edge") + log.Println(" DELETE /api/edges/:id - Delete edge") if err := router.Run(":" + cfg.Server.Port); err != nil { log.Fatalf("Failed to start server: %v", err) diff --git a/backend/models/requests.go b/backend/models/requests.go new file mode 100644 index 0000000..0ae7443 --- /dev/null +++ b/backend/models/requests.go @@ -0,0 +1,47 @@ +package models + +// CreateNodeRequest 创建节点的请求结构 +type CreateNodeRequest struct { + ID string `json:"id" binding:"required" example:"node_123"` // 节点唯一标识 + Label string `json:"label" binding:"required" example:"人工智能"` // 节点显示标签 + Type string `json:"type" binding:"required" example:"概念"` // 节点类型 + X float64 `json:"x" example:"100"` // X坐标(可选) + Y float64 `json:"y" example:"200"` // Y坐标(可选) + Properties map[string]interface{} `json:"properties,omitempty"` // 自定义属性 +} + +// UpdateNodeRequest 更新节点的请求结构 +type UpdateNodeRequest struct { + Label string `json:"label,omitempty" example:"机器学习"` // 节点显示标签 + Type string `json:"type,omitempty" example:"工具"` // 节点类型 + X *float64 `json:"x,omitempty" example:"150"` // X坐标 + Y *float64 `json:"y,omitempty" example:"250"` // Y坐标 + Properties map[string]interface{} `json:"properties,omitempty"` // 自定义属性(更新) +} + +// DeleteResponse 删除操作的响应结构 +type DeleteResponse struct { + Success bool `json:"success" example:"true"` // 是否成功 + Message string `json:"message" example:"节点已成功删除"` // 消息 + ID string `json:"id,omitempty" example:"node_123"` // 被删除的实体ID(可选) +} + +// CreateEdgeRequest 创建边的请求结构 +type CreateEdgeRequest struct { + ID string `json:"id" binding:"required" example:"edge_123"` // 边的唯一标识 + Source string `json:"source" binding:"required" example:"node_1"` // 源节点ID + Target string `json:"target" binding:"required" example:"node_2"` // 目标节点ID + Label string `json:"label" binding:"required" example:"包含"` // 边的显示标签 + Type string `json:"type" example:"CONTAINS"` // 边的类型(可选,默认为关系类型) + Properties map[string]interface{} `json:"properties,omitempty"` // 自定义属性 +} + +// EdgeResponse 边的响应结构 +type EdgeResponse struct { + ID string `json:"id" example:"edge_123"` // 边的ID + Source string `json:"source" example:"node_1"` // 源节点ID + Target string `json:"target" example:"node_2"` // 目标节点ID + Label string `json:"label" example:"包含"` // 边的显示标签 + Type string `json:"type" example:"CONTAINS"` // 边的类型 + Properties map[string]interface{} `json:"properties,omitempty"` // 自定义属性 +} diff --git a/backend/services/neo4j_service.go b/backend/services/neo4j_service.go index 8d173c3..8e3f698 100644 --- a/backend/services/neo4j_service.go +++ b/backend/services/neo4j_service.go @@ -3,6 +3,7 @@ package services import ( "context" "fmt" + "strings" "github.com/neo4j/neo4j-go-driver/v5/neo4j" @@ -299,3 +300,354 @@ func getInt(record *neo4j.Record, key string) int { } return 0 } + +// CreateNode 创建新节点 +func (s *Neo4jService) CreateNode(req models.CreateNodeRequest) (models.Node, error) { + ctx := context.Background() + + // 检查节点是否已存在 + checkQuery := "MATCH (n {id: $id}) RETURN n" + checkResult, err := neo4j.ExecuteQuery(ctx, s.driver, checkQuery, + map[string]any{"id": req.ID}, neo4j.EagerResultTransformer) + if err != nil { + return models.Node{}, fmt.Errorf("error checking node existence: %w", err) + } + if len(checkResult.Records) > 0 { + return models.Node{}, fmt.Errorf("node with id %s already exists", req.ID) + } + + // 构建创建属性 + props := map[string]any{ + "id": req.ID, + "label": req.Label, + } + + if req.Type != "" { + props["type"] = req.Type + } + if req.X != 0 { + props["x"] = req.X + } + if req.Y != 0 { + props["y"] = req.Y + } + + // 合并自定义属性(避免覆盖系统属性) + if req.Properties != nil { + for k, v := range req.Properties { + if k != "id" && k != "label" && k != "type" && k != "x" && k != "y" { + props[k] = v + } + } + } + + // 执行创建 + nodeType := "Node" + if req.Type != "" { + nodeType = req.Type + } + + query := fmt.Sprintf(` + CREATE (n:%s) + SET n = $props + RETURN n`, nodeType) + + result, err := neo4j.ExecuteQuery(ctx, s.driver, query, + map[string]any{"props": props}, neo4j.EagerResultTransformer) + if err != nil { + fmt.Printf("Error creating node: %v\n", err) + return models.Node{}, fmt.Errorf("failed to create node: %w", err) + } + + if len(result.Records) == 0 { + return models.Node{}, fmt.Errorf("failed to create node: no result returned") + } + + v, _ := result.Records[0].Get("n") + if n, ok := v.(neo4j.Node); ok { + return neo4jNodeToModel(n), nil + } + + return models.Node{}, fmt.Errorf("unexpected result type while creating node") +} + +// CreateNodeV2 创建新节点(改进版) +func (s *Neo4jService) CreateNodeV2(req models.CreateNodeRequest) (models.Node, error) { + ctx := context.Background() + + // 先检查节点是否已存在 + checkQuery := "MATCH (n {id: $id}) RETURN n" + checkResult, err := neo4j.ExecuteQuery(ctx, s.driver, checkQuery, + map[string]any{"id": req.ID}, neo4j.EagerResultTransformer) + if err != nil { + return models.Node{}, err + } + if len(checkResult.Records) > 0 { + return models.Node{}, fmt.Errorf("node with id %s already exists", req.ID) + } + + // 构建创建属性 + props := map[string]any{ + "id": req.ID, + "label": req.Label, + } + + if req.Type != "" { + props["type"] = req.Type + } + if req.X != 0 { + props["x"] = req.X + } + if req.Y != 0 { + props["y"] = req.Y + } + + // 合并自定义属性(避免覆盖系统属性) + if req.Properties != nil { + for k, v := range req.Properties { + if k != "id" && k != "label" && k != "type" && k != "x" && k != "y" { + props[k] = v + } + } + } + + // 执行创建 + nodeType := "Node" + if req.Type != "" { + nodeType = req.Type + } + + query := fmt.Sprintf(` + CREATE (n:%s) + SET n = $props + RETURN n`, nodeType) + + result, err := neo4j.ExecuteQuery(ctx, s.driver, query, + map[string]any{"props": props}, neo4j.EagerResultTransformer) + if err != nil { + fmt.Printf("Error creating node: %v\n", err) + return models.Node{}, err + } + + if len(result.Records) == 0 { + return models.Node{}, fmt.Errorf("failed to create node") + } + + v, _ := result.Records[0].Get("n") + if n, ok := v.(neo4j.Node); ok { + return neo4jNodeToModel(n), nil + } + + return models.Node{}, fmt.Errorf("unexpected result type while creating node") +} + +// UpdateNode 更新节点 +func (s *Neo4jService) UpdateNode(id string, req models.UpdateNodeRequest) (models.Node, error) { + ctx := context.Background() + + // 检查节点是否存在 + _, exists := s.GetNodeByID(id) + if !exists { + return models.Node{}, fmt.Errorf("node with id %s not found", id) + } + + // 构建更新语句 + setClauses := []string{} + params := map[string]any{"id": id} + + if req.Label != "" { + setClauses = append(setClauses, "n.label = $label") + params["label"] = req.Label + } + + if req.Type != "" { + setClauses = append(setClauses, "n.type = $type") + params["type"] = req.Type + } + + if req.X != nil { + setClauses = append(setClauses, "n.x = $x") + params["x"] = *req.X + } + if req.Y != nil { + setClauses = append(setClauses, "n.y = $y") + params["y"] = *req.Y + } + + query := `MATCH (n {id: $id})` + + // 处理自定义属性(不管是否有标准字段更新) + if req.Properties != nil && len(req.Properties) > 0 { + for key, value := range req.Properties { + // 跳过系统属性,避免冲突 + if key != "id" && key != "label" && key != "type" && key != "x" && key != "y" { + setClauses = append(setClauses, fmt.Sprintf("n.%s = $%s", key, key)) + params[key] = value + } + } + } + + if len(setClauses) > 0 { + query += ` SET ` + fmt.Sprintf("%s", strings.Join(setClauses, ", ")) + } + query += ` RETURN n` + + result, err := neo4j.ExecuteQuery(ctx, s.driver, query, params, neo4j.EagerResultTransformer) + if err != nil { + fmt.Printf("Error updating node: %v\n", err) + return models.Node{}, err + } + + if len(result.Records) == 0 { + return models.Node{}, fmt.Errorf("failed to update node") + } + + v, _ := result.Records[0].Get("n") + if n, ok := v.(neo4j.Node); ok { + return neo4jNodeToModel(n), nil + } + + return models.Node{}, fmt.Errorf("unexpected result type while updating node") +} + +// DeleteNode 删除节点 +func (s *Neo4jService) DeleteNode(id string) error { + ctx := context.Background() + + // 检查节点是否存在 + _, exists := s.GetNodeByID(id) + if !exists { + return fmt.Errorf("node with id %s not found", id) + } + + // 先删除与该节点相关的所有关系 + deleteRelationsQuery := ` + MATCH (n {id: $id})-[r]-(m) + DELETE r` + + _, err := neo4j.ExecuteQuery(ctx, s.driver, deleteRelationsQuery, + map[string]any{"id": id}, neo4j.EagerResultTransformer) + if err != nil { + fmt.Printf("Error deleting relations: %v\n", err) + // 继续尝试删除节点 + } + + // 删除节点 + deleteNodeQuery := ` + MATCH (n {id: $id}) + DELETE n` + + _, err = neo4j.ExecuteQuery(ctx, s.driver, deleteNodeQuery, + map[string]any{"id": id}, neo4j.EagerResultTransformer) + if err != nil { + fmt.Printf("Error deleting node: %v\n", err) + return err + } + + return nil +} + +// CreateEdge 创建新边 +func (s *Neo4jService) CreateEdge(req models.CreateEdgeRequest) (models.Edge, error) { + ctx := context.Background() + + // 检查源节点和目标节点是否存在 + _, sourceExists := s.GetNodeByID(req.Source) + if !sourceExists { + return models.Edge{}, fmt.Errorf("source node with id %s not found", req.Source) + } + _, targetExists := s.GetNodeByID(req.Target) + if !targetExists { + return models.Edge{}, fmt.Errorf("target node with id %s not found", req.Target) + } + + // 构建创建属性 + + // 构建创建属性 + props := map[string]any{ + "id": req.ID, + "label": req.Label, + } + + // 合并自定义属性 + if req.Properties != nil { + for k, v := range req.Properties { + if k != "id" && k != "label" { + props[k] = v + } + } + } + + // 确定边类型 + relationType := "RELATED_TO" + if req.Type != "" { + relationType = strings.ToUpper(req.Type) + } + + // 执行创建 + query := fmt.Sprintf(` + MATCH (a {id: $source}), (b {id: $target}) + CREATE (a)-[r:%s]->(b) + SET r = $props + RETURN r`, relationType) + + params := map[string]any{ + "source": req.Source, + "target": req.Target, + "props": props, + } + + result, err := neo4j.ExecuteQuery(ctx, s.driver, query, params, neo4j.EagerResultTransformer) + if err != nil { + fmt.Printf("Error creating edge: %v\n", err) + return models.Edge{}, err + } + + if len(result.Records) == 0 { + return models.Edge{}, fmt.Errorf("failed to create edge") + } + + v, _ := result.Records[0].Get("r") + if r, ok := v.(neo4j.Relationship); ok { + edge := neo4jRelToModel(r) + edge.Source = req.Source + edge.Target = req.Target + return edge, nil + } + + return models.Edge{}, fmt.Errorf("unexpected result type while creating edge") +} + +// DeleteEdge 删除边 +func (s *Neo4jService) DeleteEdge(edgeID string) error { + ctx := context.Background() + + // 检查边是否存在 + checkQuery := ` + MATCH ()-[r]-() + WHERE r.id = $id + RETURN r` + checkResult, err := neo4j.ExecuteQuery(ctx, s.driver, checkQuery, + map[string]any{"id": edgeID}, neo4j.EagerResultTransformer) + if err != nil { + return err + } + if len(checkResult.Records) == 0 { + return fmt.Errorf("edge with id %s not found", edgeID) + } + + // 删除边 + deleteQuery := ` + MATCH ()-[r]-() + WHERE r.id = $id + DELETE r` + + _, err = neo4j.ExecuteQuery(ctx, s.driver, deleteQuery, + map[string]any{"id": edgeID}, neo4j.EagerResultTransformer) + if err != nil { + fmt.Printf("Error deleting edge: %v\n", err) + return err + } + + return nil +} diff --git a/backend/services/service.go b/backend/services/service.go index 60513be..23644b5 100644 --- a/backend/services/service.go +++ b/backend/services/service.go @@ -8,4 +8,12 @@ type GraphService interface { SearchNodes(query string) []models.Node GetNeighbors(nodeID string) (models.NeighborResponse, bool) GetStats() map[string]int + + // 新增增删改接口 + CreateNode(req models.CreateNodeRequest) (models.Node, error) + UpdateNode(id string, req models.UpdateNodeRequest) (models.Node, error) + DeleteNode(id string) error + + CreateEdge(req models.CreateEdgeRequest) (models.Edge, error) + DeleteEdge(id string) error }