From c30ce8f0dd27cbce562d762b7559910e46f1cb75 Mon Sep 17 00:00:00 2001 From: ivamp Date: Sat, 7 Dec 2024 02:44:32 +0800 Subject: [PATCH] update --- cmd/wire_gen.go | 26 +- docs/docs.go | 1456 ++++++++++++++++- docs/swagger.json | 1456 ++++++++++++++++- docs/swagger.yaml | 909 +++++++++- go.sum | 1 + internal/api/http/controller/collection.go | 168 ++ internal/api/http/controller/document.go | 476 ++++++ internal/api/http/controller/workspace.go | 240 +++ internal/api/http/provider.go | 20 +- internal/api/http/v1/test.go | 56 - internal/base/server/error.go | 1 + internal/dao/block_chunks.gen.go | 8 +- internal/dao/collections.gen.go | 8 +- internal/dao/document_blocks.gen.go | 14 +- internal/dao/documents.gen.go | 8 +- internal/dao/workspace_members.gen.go | 8 +- internal/dao/workspaces.gen.go | 8 +- .../2_add_parent_id_to_documents.sql | 2 +- .../3_add_order_to_document_blocks.sql | 10 + internal/entity/Document.go | 11 +- internal/entity/Model.go | 2 +- internal/entity/Workspace.go | 4 +- internal/pkg/validator/validator.go | 6 +- internal/router/api.go | 69 +- internal/services/auth/auth.go | 10 +- internal/services/collection/collection.go | 89 +- internal/services/document/collection.go | 105 -- internal/services/document/document.go | 256 +++ internal/services/document/provider.go | 2 +- internal/services/provider.go | 23 +- internal/services/workspace/workspace.go | 27 +- internal/types/dto/base.go | 10 + internal/types/dto/collection.go | 22 + internal/types/dto/document.go | 71 + internal/types/dto/dto.go | 36 +- internal/types/dto/params.go | 27 + internal/types/dto/user_response.go | 2 +- internal/types/dto/workspace.go | 29 + internal/types/errs/model.go | 7 +- internal/types/user/user.go | 12 +- 40 files changed, 5288 insertions(+), 407 deletions(-) create mode 100644 internal/api/http/controller/collection.go create mode 100644 internal/api/http/controller/document.go create mode 100644 internal/api/http/controller/workspace.go delete mode 100644 internal/api/http/v1/test.go create mode 100644 internal/database/migrations/3_add_order_to_document_blocks.sql delete mode 100644 internal/services/document/collection.go create mode 100644 internal/services/document/document.go create mode 100644 internal/types/dto/base.go create mode 100644 internal/types/dto/collection.go create mode 100644 internal/types/dto/document.go create mode 100644 internal/types/dto/params.go create mode 100644 internal/types/dto/workspace.go diff --git a/cmd/wire_gen.go b/cmd/wire_gen.go index 9ec1a9d..95e3afc 100644 --- a/cmd/wire_gen.go +++ b/cmd/wire_gen.go @@ -13,7 +13,7 @@ import ( "leafdev.top/Leaf/leaf-library-3/internal/api/grpc/interceptor" "leafdev.top/Leaf/leaf-library-3/internal/api/grpc/v1/documents" "leafdev.top/Leaf/leaf-library-3/internal/api/http" - "leafdev.top/Leaf/leaf-library-3/internal/api/http/v1" + "leafdev.top/Leaf/leaf-library-3/internal/api/http/controller" "leafdev.top/Leaf/leaf-library-3/internal/base" "leafdev.top/Leaf/leaf-library-3/internal/base/conf" "leafdev.top/Leaf/leaf-library-3/internal/base/logger" @@ -27,8 +27,11 @@ import ( "leafdev.top/Leaf/leaf-library-3/internal/router" "leafdev.top/Leaf/leaf-library-3/internal/services" "leafdev.top/Leaf/leaf-library-3/internal/services/auth" + "leafdev.top/Leaf/leaf-library-3/internal/services/collection" + "leafdev.top/Leaf/leaf-library-3/internal/services/document" "leafdev.top/Leaf/leaf-library-3/internal/services/jwks" "leafdev.top/Leaf/leaf-library-3/internal/services/stream" + "leafdev.top/Leaf/leaf-library-3/internal/services/workspace" ) // Injectors from wire.go: @@ -36,24 +39,29 @@ import ( func CreateApp() (*base.Application, error) { config := conf.NewConfig() loggerLogger := logger.NewZapLogger(config) + db := orm.NewGORM(config, loggerLogger) + query := dao.NewQuery(db) + service := workspace.NewService(config, query) jwksJWKS := jwks.NewJWKS(config, loggerLogger) - service := auth.NewService(config, jwksJWKS, loggerLogger) - userController := v1.NewUserController(service) - handlers := http.NewHandler(userController) - middleware := http.NewMiddleware(config, loggerLogger, service) + authService := auth.NewService(config, jwksJWKS, loggerLogger) + workspaceController := controller.NewWorkspaceController(service, authService) + collectionService := collection.NewService(config, query) + collectionController := controller.NewCollectionController(collectionService, service, authService) + documentService := document.NewService(config, query) + documentController := controller.NewDocumentController(documentService, collectionService, service, authService) + handlers := http.NewHandler(workspaceController, collectionController, documentController) + middleware := http.NewMiddleware(config, loggerLogger, authService) routerApi := router.NewApiRoute(handlers, middleware) swaggerRouter := router.NewSwaggerRoute() httpServer := server.NewHTTPServer(config, routerApi, swaggerRouter, middleware, loggerLogger) - db := orm.NewGORM(config, loggerLogger) - query := dao.NewQuery(db) handler := documents.NewHandler(query) - interceptorAuth := interceptor.NewAuth(service, loggerLogger, config) + interceptorAuth := interceptor.NewAuth(authService, loggerLogger, config) interceptorLogger := interceptor.NewLogger(loggerLogger) grpcInterceptor := grpc.NewInterceptor(interceptorAuth, interceptorLogger) grpcHandlers := grpc.NewHandler(handler, grpcInterceptor) apiApi := api.NewApi(grpcHandlers, handlers) streamService := stream.NewService(config) - servicesService := services.NewService(loggerLogger, jwksJWKS, service, streamService) + servicesService := services.NewService(loggerLogger, jwksJWKS, authService, streamService, service, collectionService, documentService) redisRedis := redis.NewRedis(config) batchBatch := batch.NewBatch(loggerLogger) s3S3 := s3.NewS3(config) diff --git a/docs/docs.go b/docs/docs.go index 47cd9cf..a8bd92a 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -15,14 +15,9 @@ const docTemplate = `{ "host": "{{.Host}}", "basePath": "{{.BasePath}}", "paths": { - "/api/v1/ping": { - "get": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "description": "测试接口,将会返回当前用户的信息", + "/collections": { + "post": { + "description": "在工作空间下创建新的集合", "consumes": [ "application/json" ], @@ -30,23 +25,33 @@ const docTemplate = `{ "application/json" ], "tags": [ - "ping" + "Collection" + ], + "summary": "创建集合", + "parameters": [ + { + "description": "创建集合请求", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.CreateCollectionRequest" + } + } ], - "summary": "Greet", - "deprecated": true, "responses": { "200": { "description": "OK", "schema": { "allOf": [ { - "$ref": "#/definitions/response.Body" + "$ref": "#/definitions/dto.Response" }, { "type": "object", "properties": { "data": { - "$ref": "#/definitions/schema.CurrentUserResponse" + "$ref": "#/definitions/leafdev_top_Leaf_leaf-library-3_internal_entity.Collection" } } } @@ -54,9 +59,1139 @@ const docTemplate = `{ } }, "400": { - "description": "Bad Request", + "description": "参数验证失败", "schema": { - "$ref": "#/definitions/response.Body" + "allOf": [ + { + "$ref": "#/definitions/dto.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.ValidateError" + } + } + } + } + ] + } + }, + "404": { + "description": "工作空间不存在", + "schema": { + "$ref": "#/definitions/dto.Response" + } + } + } + } + }, + "/collections/{collection_id}/documents": { + "get": { + "description": "获取指定集合下的文档树,如果指定了父文档ID则获取该文档下的子树", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Document" + ], + "summary": "列出文档树", + "parameters": [ + { + "type": "integer", + "description": "集合ID", + "name": "collection_id", + "in": "path", + "required": true + }, + { + "type": "integer", + "description": "父文档ID,不传则获取根文档", + "name": "parent_id", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/dto.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.DocumentDTO" + } + } + } + } + ] + } + }, + "403": { + "description": "无权访问", + "schema": { + "$ref": "#/definitions/dto.Response" + } + }, + "404": { + "description": "集合不存在或父文档不存在", + "schema": { + "$ref": "#/definitions/dto.Response" + } + } + } + } + }, + "/collections/{id}": { + "get": { + "description": "根据集合ID获取集合详情", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Collection" + ], + "summary": "获取集合", + "parameters": [ + { + "type": "integer", + "description": "集合ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/dto.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/leafdev_top_Leaf_leaf-library-3_internal_entity.Collection" + } + } + } + ] + } + }, + "404": { + "description": "集合不存在", + "schema": { + "$ref": "#/definitions/dto.Response" + } + } + } + }, + "delete": { + "description": "删除指定的集合", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Collection" + ], + "summary": "删除集合", + "parameters": [ + { + "type": "integer", + "description": "集合ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.Response" + } + }, + "404": { + "description": "集合不存在", + "schema": { + "$ref": "#/definitions/dto.Response" + } + } + } + } + }, + "/documents": { + "post": { + "description": "创建一个新的文档,可以是根文档或子文档", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Document" + ], + "summary": "创建文档", + "parameters": [ + { + "description": "创建文档请求", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.CreateDocumentRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/dto.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/dto.DocumentDTO" + } + } + } + ] + } + }, + "400": { + "description": "参数验证失败", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/dto.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.ValidateError" + } + } + } + } + ] + } + }, + "403": { + "description": "无权访问", + "schema": { + "$ref": "#/definitions/dto.Response" + } + }, + "404": { + "description": "集合不存在或父文档不存在", + "schema": { + "$ref": "#/definitions/dto.Response" + } + } + } + } + }, + "/documents/{document_id}/blocks": { + "get": { + "description": "获取指文档下的所有文档块", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Document" + ], + "summary": "列出文档块列表", + "parameters": [ + { + "type": "integer", + "description": "文档ID", + "name": "document_id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/dto.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/entity.DocumentBlock" + } + } + } + } + ] + } + }, + "404": { + "description": "文档不存在", + "schema": { + "$ref": "#/definitions/dto.Response" + } + } + } + }, + "post": { + "description": "在文档中创建新的文档块", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Document" + ], + "summary": "创建文档块", + "parameters": [ + { + "type": "integer", + "description": "文档ID", + "name": "document_id", + "in": "path", + "required": true + }, + { + "description": "创建文档块请求", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.CreateBlockRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/dto.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/entity.DocumentBlock" + } + } + } + ] + } + }, + "400": { + "description": "参数验证失败", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/dto.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.ValidateError" + } + } + } + } + ] + } + }, + "404": { + "description": "文档不存在", + "schema": { + "$ref": "#/definitions/dto.Response" + } + } + } + } + }, + "/documents/{document_id}/blocks/{block_id}": { + "put": { + "description": "更新文档块的内容", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Document" + ], + "summary": "更新文档块", + "parameters": [ + { + "type": "integer", + "description": "文档ID", + "name": "document_id", + "in": "path", + "required": true + }, + { + "type": "integer", + "description": "文档块ID", + "name": "block_id", + "in": "path", + "required": true + }, + { + "description": "更新文档块请求", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.UpdateBlockRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/dto.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/entity.DocumentBlock" + } + } + } + ] + } + }, + "400": { + "description": "参数验证失败", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/dto.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.ValidateError" + } + } + } + } + ] + } + }, + "404": { + "description": "文档块不存在", + "schema": { + "$ref": "#/definitions/dto.Response" + } + } + } + }, + "delete": { + "description": "删除指定的文档块", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Document" + ], + "summary": "删除文档块", + "parameters": [ + { + "type": "integer", + "description": "文档ID", + "name": "document_id", + "in": "path", + "required": true + }, + { + "type": "integer", + "description": "文档块ID", + "name": "block_id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.Response" + } + }, + "404": { + "description": "文档块不存在", + "schema": { + "$ref": "#/definitions/dto.Response" + } + } + } + } + }, + "/documents/{document_id}/blocks/{block_id}/move": { + "post": { + "description": "移动文档块的位置", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Document" + ], + "summary": "移动文档块", + "parameters": [ + { + "type": "integer", + "description": "文档ID", + "name": "document_id", + "in": "path", + "required": true + }, + { + "type": "integer", + "description": "文档块ID", + "name": "block_id", + "in": "path", + "required": true + }, + { + "description": "移动文档块请求", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.MoveBlockRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.Response" + } + }, + "400": { + "description": "参数验证失败", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/dto.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.ValidateError" + } + } + } + } + ] + } + }, + "404": { + "description": "文档块不存在", + "schema": { + "$ref": "#/definitions/dto.Response" + } + } + } + } + }, + "/documents/{id}": { + "get": { + "description": "根据文档ID获取文档详情", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Document" + ], + "summary": "获取文档", + "parameters": [ + { + "type": "integer", + "description": "文档ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/dto.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/dto.DocumentDTO" + } + } + } + ] + } + }, + "403": { + "description": "无权访问", + "schema": { + "$ref": "#/definitions/dto.Response" + } + }, + "404": { + "description": "文档不存在", + "schema": { + "$ref": "#/definitions/dto.Response" + } + } + } + }, + "put": { + "description": "更新文档的名称等信息", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Document" + ], + "summary": "更新文档", + "parameters": [ + { + "type": "integer", + "description": "文档ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "更新文档请求", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.UpdateDocumentRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/dto.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/entity.Document" + } + } + } + ] + } + }, + "400": { + "description": "参数验证失败", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/dto.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.ValidateError" + } + } + } + } + ] + } + }, + "404": { + "description": "文档不存在", + "schema": { + "$ref": "#/definitions/dto.Response" + } + } + } + }, + "delete": { + "description": "删除指定的文档", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Document" + ], + "summary": "删除文档", + "parameters": [ + { + "type": "integer", + "description": "文档ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.Response" + } + }, + "404": { + "description": "文档不存在", + "schema": { + "$ref": "#/definitions/dto.Response" + } + } + } + } + }, + "/workspaces": { + "get": { + "description": "获取当前用户的所有工作空间", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Workspace" + ], + "summary": "列出工作空间列表", + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/dto.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/entity.Workspace" + } + } + } + } + ] + } + } + } + }, + "post": { + "description": "创建一个新的工作空间", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Workspace" + ], + "summary": "创建工作空间", + "parameters": [ + { + "description": "创建工作空间请求", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.CreateWorkspaceRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/dto.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/entity.Workspace" + } + } + } + ] + } + }, + "400": { + "description": "参数验证失败", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/dto.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.ValidateError" + } + } + } + } + ] + } + } + } + } + }, + "/workspaces/{id}": { + "get": { + "description": "根据工作空间ID获取工作空间详情", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Workspace" + ], + "summary": "获取工作空间", + "parameters": [ + { + "type": "integer", + "description": "工作空间ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/dto.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/entity.Workspace" + } + } + } + ] + } + }, + "404": { + "description": "工作空间不存在", + "schema": { + "$ref": "#/definitions/dto.Response" + } + } + } + }, + "delete": { + "description": "删除指定的工作空间", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Workspace" + ], + "summary": "删除工作空间", + "parameters": [ + { + "type": "integer", + "description": "工作空间ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.Response" + } + }, + "404": { + "description": "工作空间不存在", + "schema": { + "$ref": "#/definitions/dto.Response" + } + } + } + } + }, + "/workspaces/{id}/members": { + "post": { + "description": "向工作空间添加新成员", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Workspace" + ], + "summary": "添加工作空间成员", + "parameters": [ + { + "type": "integer", + "description": "工作空间ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "添加成员请求", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.AddWorkspaceMemberRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/dto.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/entity.WorkspaceMember" + } + } + } + ] + } + }, + "400": { + "description": "参数验证失败", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/dto.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.ValidateError" + } + } + } + } + ] + } + }, + "404": { + "description": "工作空间不存在", + "schema": { + "$ref": "#/definitions/dto.Response" + } + }, + "409": { + "description": "成员已存在", + "schema": { + "$ref": "#/definitions/dto.Response" + } + } + } + } + }, + "/workspaces/{id}/members/{user_id}": { + "delete": { + "description": "从工作空间移除成员", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Workspace" + ], + "summary": "移除工作空间成员", + "parameters": [ + { + "type": "integer", + "description": "工作空间ID", + "name": "id", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "用户ID", + "name": "user_id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.Response" + } + }, + "404": { + "description": "工作空间不存在或成员不存在", + "schema": { + "$ref": "#/definitions/dto.Response" + } + } + } + } + }, + "/workspaces/{workspace_id}/collections": { + "get": { + "description": "获取指定工作空间下的所有集合", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Collection" + ], + "summary": "列出集合列表", + "parameters": [ + { + "type": "integer", + "description": "工作空间ID", + "name": "workspace_id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/dto.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/leafdev_top_Leaf_leaf-library-3_internal_entity.Collection" + } + } + } + } + ] + } + }, + "404": { + "description": "工作空间不存在", + "schema": { + "$ref": "#/definitions/dto.Response" } } } @@ -64,40 +1199,307 @@ const docTemplate = `{ } }, "definitions": { - "response.Body": { + "dto.AddWorkspaceMemberRequest": { + "type": "object", + "required": [ + "user_id" + ], + "properties": { + "user_id": { + "$ref": "#/definitions/user.ID" + } + } + }, + "dto.CreateBlockRequest": { + "type": "object", + "required": [ + "content", + "type" + ], + "properties": { + "after_block_id": { + "type": "integer" + }, + "content": { + "type": "string" + }, + "type": { + "type": "string" + } + } + }, + "dto.CreateCollectionRequest": { + "type": "object", + "required": [ + "workspace_id" + ], + "properties": { + "name": { + "type": "string" + }, + "workspace_id": { + "type": "integer" + } + } + }, + "dto.CreateDocumentRequest": { + "type": "object", + "required": [ + "collection_id", + "workspace_id" + ], + "properties": { + "collection_id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "parent_id": { + "type": "integer" + }, + "workspace_id": { + "type": "integer" + } + } + }, + "dto.CreateWorkspaceRequest": { "type": "object", "properties": { - "data": {}, - "error": { + "name": { + "type": "string" + } + } + }, + "dto.DocumentDTO": { + "type": "object", + "properties": { + "collection_id": { + "type": "integer" + }, + "created_at": { "type": "string" }, + "has_children": { + "type": "boolean" + }, + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "parent_id": { + "type": "integer" + }, + "updated_at": { + "type": "string" + }, + "workspace_id": { + "type": "integer" + } + } + }, + "dto.MoveBlockRequest": { + "type": "object", + "properties": { + "after_block_id": { + "type": "integer" + } + } + }, + "dto.Response": { + "type": "object" + }, + "dto.UpdateBlockRequest": { + "type": "object", + "required": [ + "content" + ], + "properties": { + "content": { + "type": "string" + } + } + }, + "dto.UpdateDocumentRequest": { + "type": "object", + "properties": { + "name": { + "type": "string" + } + } + }, + "dto.ValidateError": { + "type": "object", + "properties": { "message": { "type": "string" + } + } + }, + "entity.Document": { + "type": "object", + "properties": { + "collection": { + "$ref": "#/definitions/leafdev_top_Leaf_leaf-library-3_internal_entity.Collection" }, - "success": { + "collection_id": { + "type": "integer" + }, + "created_at": { + "type": "string" + }, + "deleted_at": { + "$ref": "#/definitions/gorm.DeletedAt" + }, + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "parent": { + "$ref": "#/definitions/entity.Document" + }, + "parent_id": { + "type": "integer" + }, + "updated_at": { + "type": "string" + }, + "workspace": { + "$ref": "#/definitions/entity.Workspace" + }, + "workspace_id": { + "type": "integer" + } + } + }, + "entity.DocumentBlock": { + "type": "object", + "properties": { + "content": { + "type": "string" + }, + "created_at": { + "type": "string" + }, + "document": { + "$ref": "#/definitions/entity.Document" + }, + "document_id": { + "type": "integer" + }, + "hash": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "order": { + "type": "number" + }, + "type": { + "type": "string" + }, + "updated_at": { + "type": "string" + } + } + }, + "entity.Workspace": { + "type": "object", + "properties": { + "created_at": { + "type": "string" + }, + "deleted_at": { + "$ref": "#/definitions/gorm.DeletedAt" + }, + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "updated_at": { + "type": "string" + }, + "user_id": { + "$ref": "#/definitions/user.ID" + } + } + }, + "entity.WorkspaceMember": { + "type": "object", + "properties": { + "created_at": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "updated_at": { + "type": "string" + }, + "user_id": { + "$ref": "#/definitions/user.ID" + }, + "workspace": { + "$ref": "#/definitions/entity.Workspace" + }, + "workspace_id": { + "type": "integer" + } + } + }, + "gorm.DeletedAt": { + "type": "object", + "properties": { + "time": { + "type": "string" + }, + "valid": { + "description": "Valid is true if Time is not NULL", "type": "boolean" } } }, - "schema.CurrentUserResponse": { + "leafdev_top_Leaf_leaf-library-3_internal_entity.Collection": { "type": "object", "properties": { - "ip": { + "created_at": { "type": "string" }, - "userEmail": { + "deleted_at": { + "$ref": "#/definitions/gorm.DeletedAt" + }, + "id": { + "type": "integer" + }, + "name": { "type": "string" }, - "userId": { + "updated_at": { "type": "string" }, - "userName": { - "type": "string" + "workspace": { + "$ref": "#/definitions/entity.Workspace" }, - "valid": { - "type": "boolean" + "workspace_id": { + "type": "integer" } } + }, + "user.ID": { + "type": "string", + "enum": [ + "anonymous" + ], + "x-enum-varnames": [ + "AnonymousUser" + ] } }, "securityDefinitions": { diff --git a/docs/swagger.json b/docs/swagger.json index e178447..d48762d 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -6,14 +6,9 @@ "version": "1.0" }, "paths": { - "/api/v1/ping": { - "get": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "description": "测试接口,将会返回当前用户的信息", + "/collections": { + "post": { + "description": "在工作空间下创建新的集合", "consumes": [ "application/json" ], @@ -21,23 +16,33 @@ "application/json" ], "tags": [ - "ping" + "Collection" + ], + "summary": "创建集合", + "parameters": [ + { + "description": "创建集合请求", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.CreateCollectionRequest" + } + } ], - "summary": "Greet", - "deprecated": true, "responses": { "200": { "description": "OK", "schema": { "allOf": [ { - "$ref": "#/definitions/response.Body" + "$ref": "#/definitions/dto.Response" }, { "type": "object", "properties": { "data": { - "$ref": "#/definitions/schema.CurrentUserResponse" + "$ref": "#/definitions/leafdev_top_Leaf_leaf-library-3_internal_entity.Collection" } } } @@ -45,9 +50,1139 @@ } }, "400": { - "description": "Bad Request", + "description": "参数验证失败", "schema": { - "$ref": "#/definitions/response.Body" + "allOf": [ + { + "$ref": "#/definitions/dto.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.ValidateError" + } + } + } + } + ] + } + }, + "404": { + "description": "工作空间不存在", + "schema": { + "$ref": "#/definitions/dto.Response" + } + } + } + } + }, + "/collections/{collection_id}/documents": { + "get": { + "description": "获取指定集合下的文档树,如果指定了父文档ID则获取该文档下的子树", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Document" + ], + "summary": "列出文档树", + "parameters": [ + { + "type": "integer", + "description": "集合ID", + "name": "collection_id", + "in": "path", + "required": true + }, + { + "type": "integer", + "description": "父文档ID,不传则获取根文档", + "name": "parent_id", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/dto.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.DocumentDTO" + } + } + } + } + ] + } + }, + "403": { + "description": "无权访问", + "schema": { + "$ref": "#/definitions/dto.Response" + } + }, + "404": { + "description": "集合不存在或父文档不存在", + "schema": { + "$ref": "#/definitions/dto.Response" + } + } + } + } + }, + "/collections/{id}": { + "get": { + "description": "根据集合ID获取集合详情", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Collection" + ], + "summary": "获取集合", + "parameters": [ + { + "type": "integer", + "description": "集合ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/dto.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/leafdev_top_Leaf_leaf-library-3_internal_entity.Collection" + } + } + } + ] + } + }, + "404": { + "description": "集合不存在", + "schema": { + "$ref": "#/definitions/dto.Response" + } + } + } + }, + "delete": { + "description": "删除指定的集合", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Collection" + ], + "summary": "删除集合", + "parameters": [ + { + "type": "integer", + "description": "集合ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.Response" + } + }, + "404": { + "description": "集合不存在", + "schema": { + "$ref": "#/definitions/dto.Response" + } + } + } + } + }, + "/documents": { + "post": { + "description": "创建一个新的文档,可以是根文档或子文档", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Document" + ], + "summary": "创建文档", + "parameters": [ + { + "description": "创建文档请求", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.CreateDocumentRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/dto.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/dto.DocumentDTO" + } + } + } + ] + } + }, + "400": { + "description": "参数验证失败", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/dto.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.ValidateError" + } + } + } + } + ] + } + }, + "403": { + "description": "无权访问", + "schema": { + "$ref": "#/definitions/dto.Response" + } + }, + "404": { + "description": "集合不存在或父文档不存在", + "schema": { + "$ref": "#/definitions/dto.Response" + } + } + } + } + }, + "/documents/{document_id}/blocks": { + "get": { + "description": "获取指文档下的所有文档块", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Document" + ], + "summary": "列出文档块列表", + "parameters": [ + { + "type": "integer", + "description": "文档ID", + "name": "document_id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/dto.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/entity.DocumentBlock" + } + } + } + } + ] + } + }, + "404": { + "description": "文档不存在", + "schema": { + "$ref": "#/definitions/dto.Response" + } + } + } + }, + "post": { + "description": "在文档中创建新的文档块", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Document" + ], + "summary": "创建文档块", + "parameters": [ + { + "type": "integer", + "description": "文档ID", + "name": "document_id", + "in": "path", + "required": true + }, + { + "description": "创建文档块请求", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.CreateBlockRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/dto.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/entity.DocumentBlock" + } + } + } + ] + } + }, + "400": { + "description": "参数验证失败", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/dto.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.ValidateError" + } + } + } + } + ] + } + }, + "404": { + "description": "文档不存在", + "schema": { + "$ref": "#/definitions/dto.Response" + } + } + } + } + }, + "/documents/{document_id}/blocks/{block_id}": { + "put": { + "description": "更新文档块的内容", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Document" + ], + "summary": "更新文档块", + "parameters": [ + { + "type": "integer", + "description": "文档ID", + "name": "document_id", + "in": "path", + "required": true + }, + { + "type": "integer", + "description": "文档块ID", + "name": "block_id", + "in": "path", + "required": true + }, + { + "description": "更新文档块请求", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.UpdateBlockRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/dto.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/entity.DocumentBlock" + } + } + } + ] + } + }, + "400": { + "description": "参数验证失败", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/dto.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.ValidateError" + } + } + } + } + ] + } + }, + "404": { + "description": "文档块不存在", + "schema": { + "$ref": "#/definitions/dto.Response" + } + } + } + }, + "delete": { + "description": "删除指定的文档块", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Document" + ], + "summary": "删除文档块", + "parameters": [ + { + "type": "integer", + "description": "文档ID", + "name": "document_id", + "in": "path", + "required": true + }, + { + "type": "integer", + "description": "文档块ID", + "name": "block_id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.Response" + } + }, + "404": { + "description": "文档块不存在", + "schema": { + "$ref": "#/definitions/dto.Response" + } + } + } + } + }, + "/documents/{document_id}/blocks/{block_id}/move": { + "post": { + "description": "移动文档块的位置", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Document" + ], + "summary": "移动文档块", + "parameters": [ + { + "type": "integer", + "description": "文档ID", + "name": "document_id", + "in": "path", + "required": true + }, + { + "type": "integer", + "description": "文档块ID", + "name": "block_id", + "in": "path", + "required": true + }, + { + "description": "移动文档块请求", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.MoveBlockRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.Response" + } + }, + "400": { + "description": "参数验证失败", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/dto.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.ValidateError" + } + } + } + } + ] + } + }, + "404": { + "description": "文档块不存在", + "schema": { + "$ref": "#/definitions/dto.Response" + } + } + } + } + }, + "/documents/{id}": { + "get": { + "description": "根据文档ID获取文档详情", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Document" + ], + "summary": "获取文档", + "parameters": [ + { + "type": "integer", + "description": "文档ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/dto.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/dto.DocumentDTO" + } + } + } + ] + } + }, + "403": { + "description": "无权访问", + "schema": { + "$ref": "#/definitions/dto.Response" + } + }, + "404": { + "description": "文档不存在", + "schema": { + "$ref": "#/definitions/dto.Response" + } + } + } + }, + "put": { + "description": "更新文档的名称等信息", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Document" + ], + "summary": "更新文档", + "parameters": [ + { + "type": "integer", + "description": "文档ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "更新文档请求", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.UpdateDocumentRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/dto.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/entity.Document" + } + } + } + ] + } + }, + "400": { + "description": "参数验证失败", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/dto.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.ValidateError" + } + } + } + } + ] + } + }, + "404": { + "description": "文档不存在", + "schema": { + "$ref": "#/definitions/dto.Response" + } + } + } + }, + "delete": { + "description": "删除指定的文档", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Document" + ], + "summary": "删除文档", + "parameters": [ + { + "type": "integer", + "description": "文档ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.Response" + } + }, + "404": { + "description": "文档不存在", + "schema": { + "$ref": "#/definitions/dto.Response" + } + } + } + } + }, + "/workspaces": { + "get": { + "description": "获取当前用户的所有工作空间", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Workspace" + ], + "summary": "列出工作空间列表", + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/dto.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/entity.Workspace" + } + } + } + } + ] + } + } + } + }, + "post": { + "description": "创建一个新的工作空间", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Workspace" + ], + "summary": "创建工作空间", + "parameters": [ + { + "description": "创建工作空间请求", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.CreateWorkspaceRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/dto.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/entity.Workspace" + } + } + } + ] + } + }, + "400": { + "description": "参数验证失败", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/dto.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.ValidateError" + } + } + } + } + ] + } + } + } + } + }, + "/workspaces/{id}": { + "get": { + "description": "根据工作空间ID获取工作空间详情", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Workspace" + ], + "summary": "获取工作空间", + "parameters": [ + { + "type": "integer", + "description": "工作空间ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/dto.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/entity.Workspace" + } + } + } + ] + } + }, + "404": { + "description": "工作空间不存在", + "schema": { + "$ref": "#/definitions/dto.Response" + } + } + } + }, + "delete": { + "description": "删除指定的工作空间", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Workspace" + ], + "summary": "删除工作空间", + "parameters": [ + { + "type": "integer", + "description": "工作空间ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.Response" + } + }, + "404": { + "description": "工作空间不存在", + "schema": { + "$ref": "#/definitions/dto.Response" + } + } + } + } + }, + "/workspaces/{id}/members": { + "post": { + "description": "向工作空间添加新成员", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Workspace" + ], + "summary": "添加工作空间成员", + "parameters": [ + { + "type": "integer", + "description": "工作空间ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "添加成员请求", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.AddWorkspaceMemberRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/dto.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/entity.WorkspaceMember" + } + } + } + ] + } + }, + "400": { + "description": "参数验证失败", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/dto.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.ValidateError" + } + } + } + } + ] + } + }, + "404": { + "description": "工作空间不存在", + "schema": { + "$ref": "#/definitions/dto.Response" + } + }, + "409": { + "description": "成员已存在", + "schema": { + "$ref": "#/definitions/dto.Response" + } + } + } + } + }, + "/workspaces/{id}/members/{user_id}": { + "delete": { + "description": "从工作空间移除成员", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Workspace" + ], + "summary": "移除工作空间成员", + "parameters": [ + { + "type": "integer", + "description": "工作空间ID", + "name": "id", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "用户ID", + "name": "user_id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.Response" + } + }, + "404": { + "description": "工作空间不存在或成员不存在", + "schema": { + "$ref": "#/definitions/dto.Response" + } + } + } + } + }, + "/workspaces/{workspace_id}/collections": { + "get": { + "description": "获取指定工作空间下的所有集合", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Collection" + ], + "summary": "列出集合列表", + "parameters": [ + { + "type": "integer", + "description": "工作空间ID", + "name": "workspace_id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/dto.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/leafdev_top_Leaf_leaf-library-3_internal_entity.Collection" + } + } + } + } + ] + } + }, + "404": { + "description": "工作空间不存在", + "schema": { + "$ref": "#/definitions/dto.Response" } } } @@ -55,40 +1190,307 @@ } }, "definitions": { - "response.Body": { + "dto.AddWorkspaceMemberRequest": { + "type": "object", + "required": [ + "user_id" + ], + "properties": { + "user_id": { + "$ref": "#/definitions/user.ID" + } + } + }, + "dto.CreateBlockRequest": { + "type": "object", + "required": [ + "content", + "type" + ], + "properties": { + "after_block_id": { + "type": "integer" + }, + "content": { + "type": "string" + }, + "type": { + "type": "string" + } + } + }, + "dto.CreateCollectionRequest": { + "type": "object", + "required": [ + "workspace_id" + ], + "properties": { + "name": { + "type": "string" + }, + "workspace_id": { + "type": "integer" + } + } + }, + "dto.CreateDocumentRequest": { + "type": "object", + "required": [ + "collection_id", + "workspace_id" + ], + "properties": { + "collection_id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "parent_id": { + "type": "integer" + }, + "workspace_id": { + "type": "integer" + } + } + }, + "dto.CreateWorkspaceRequest": { "type": "object", "properties": { - "data": {}, - "error": { + "name": { + "type": "string" + } + } + }, + "dto.DocumentDTO": { + "type": "object", + "properties": { + "collection_id": { + "type": "integer" + }, + "created_at": { "type": "string" }, + "has_children": { + "type": "boolean" + }, + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "parent_id": { + "type": "integer" + }, + "updated_at": { + "type": "string" + }, + "workspace_id": { + "type": "integer" + } + } + }, + "dto.MoveBlockRequest": { + "type": "object", + "properties": { + "after_block_id": { + "type": "integer" + } + } + }, + "dto.Response": { + "type": "object" + }, + "dto.UpdateBlockRequest": { + "type": "object", + "required": [ + "content" + ], + "properties": { + "content": { + "type": "string" + } + } + }, + "dto.UpdateDocumentRequest": { + "type": "object", + "properties": { + "name": { + "type": "string" + } + } + }, + "dto.ValidateError": { + "type": "object", + "properties": { "message": { "type": "string" + } + } + }, + "entity.Document": { + "type": "object", + "properties": { + "collection": { + "$ref": "#/definitions/leafdev_top_Leaf_leaf-library-3_internal_entity.Collection" }, - "success": { + "collection_id": { + "type": "integer" + }, + "created_at": { + "type": "string" + }, + "deleted_at": { + "$ref": "#/definitions/gorm.DeletedAt" + }, + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "parent": { + "$ref": "#/definitions/entity.Document" + }, + "parent_id": { + "type": "integer" + }, + "updated_at": { + "type": "string" + }, + "workspace": { + "$ref": "#/definitions/entity.Workspace" + }, + "workspace_id": { + "type": "integer" + } + } + }, + "entity.DocumentBlock": { + "type": "object", + "properties": { + "content": { + "type": "string" + }, + "created_at": { + "type": "string" + }, + "document": { + "$ref": "#/definitions/entity.Document" + }, + "document_id": { + "type": "integer" + }, + "hash": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "order": { + "type": "number" + }, + "type": { + "type": "string" + }, + "updated_at": { + "type": "string" + } + } + }, + "entity.Workspace": { + "type": "object", + "properties": { + "created_at": { + "type": "string" + }, + "deleted_at": { + "$ref": "#/definitions/gorm.DeletedAt" + }, + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "updated_at": { + "type": "string" + }, + "user_id": { + "$ref": "#/definitions/user.ID" + } + } + }, + "entity.WorkspaceMember": { + "type": "object", + "properties": { + "created_at": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "updated_at": { + "type": "string" + }, + "user_id": { + "$ref": "#/definitions/user.ID" + }, + "workspace": { + "$ref": "#/definitions/entity.Workspace" + }, + "workspace_id": { + "type": "integer" + } + } + }, + "gorm.DeletedAt": { + "type": "object", + "properties": { + "time": { + "type": "string" + }, + "valid": { + "description": "Valid is true if Time is not NULL", "type": "boolean" } } }, - "schema.CurrentUserResponse": { + "leafdev_top_Leaf_leaf-library-3_internal_entity.Collection": { "type": "object", "properties": { - "ip": { + "created_at": { "type": "string" }, - "userEmail": { + "deleted_at": { + "$ref": "#/definitions/gorm.DeletedAt" + }, + "id": { + "type": "integer" + }, + "name": { "type": "string" }, - "userId": { + "updated_at": { "type": "string" }, - "userName": { - "type": "string" + "workspace": { + "$ref": "#/definitions/entity.Workspace" }, - "valid": { - "type": "boolean" + "workspace_id": { + "type": "integer" } } + }, + "user.ID": { + "type": "string", + "enum": [ + "anonymous" + ], + "x-enum-varnames": [ + "AnonymousUser" + ] } }, "securityDefinitions": { diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 42be56c..198e629 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -1,38 +1,218 @@ definitions: - response.Body: + dto.AddWorkspaceMemberRequest: properties: - data: {} - error: + user_id: + $ref: '#/definitions/user.ID' + required: + - user_id + type: object + dto.CreateBlockRequest: + properties: + after_block_id: + type: integer + content: type: string + type: + type: string + required: + - content + - type + type: object + dto.CreateCollectionRequest: + properties: + name: + type: string + workspace_id: + type: integer + required: + - workspace_id + type: object + dto.CreateDocumentRequest: + properties: + collection_id: + type: integer + name: + type: string + parent_id: + type: integer + workspace_id: + type: integer + required: + - collection_id + - workspace_id + type: object + dto.CreateWorkspaceRequest: + properties: + name: + type: string + type: object + dto.DocumentDTO: + properties: + collection_id: + type: integer + created_at: + type: string + has_children: + type: boolean + id: + type: integer + name: + type: string + parent_id: + type: integer + updated_at: + type: string + workspace_id: + type: integer + type: object + dto.MoveBlockRequest: + properties: + after_block_id: + type: integer + type: object + dto.Response: + type: object + dto.UpdateBlockRequest: + properties: + content: + type: string + required: + - content + type: object + dto.UpdateDocumentRequest: + properties: + name: + type: string + type: object + dto.ValidateError: + properties: message: type: string - success: - type: boolean type: object - schema.CurrentUserResponse: + entity.Document: properties: - ip: + collection: + $ref: '#/definitions/leafdev_top_Leaf_leaf-library-3_internal_entity.Collection' + collection_id: + type: integer + created_at: type: string - userEmail: + deleted_at: + $ref: '#/definitions/gorm.DeletedAt' + id: + type: integer + name: type: string - userId: + parent: + $ref: '#/definitions/entity.Document' + parent_id: + type: integer + updated_at: type: string - userName: + workspace: + $ref: '#/definitions/entity.Workspace' + workspace_id: + type: integer + type: object + entity.DocumentBlock: + properties: + content: + type: string + created_at: + type: string + document: + $ref: '#/definitions/entity.Document' + document_id: + type: integer + hash: + type: string + id: + type: integer + order: + type: number + type: + type: string + updated_at: + type: string + type: object + entity.Workspace: + properties: + created_at: + type: string + deleted_at: + $ref: '#/definitions/gorm.DeletedAt' + id: + type: integer + name: + type: string + updated_at: + type: string + user_id: + $ref: '#/definitions/user.ID' + type: object + entity.WorkspaceMember: + properties: + created_at: + type: string + id: + type: integer + updated_at: + type: string + user_id: + $ref: '#/definitions/user.ID' + workspace: + $ref: '#/definitions/entity.Workspace' + workspace_id: + type: integer + type: object + gorm.DeletedAt: + properties: + time: type: string valid: + description: Valid is true if Time is not NULL type: boolean type: object + leafdev_top_Leaf_leaf-library-3_internal_entity.Collection: + properties: + created_at: + type: string + deleted_at: + $ref: '#/definitions/gorm.DeletedAt' + id: + type: integer + name: + type: string + updated_at: + type: string + workspace: + $ref: '#/definitions/entity.Workspace' + workspace_id: + type: integer + type: object + user.ID: + enum: + - anonymous + type: string + x-enum-varnames: + - AnonymousUser info: contact: {} title: API Docs version: "1.0" paths: - /api/v1/ping: - get: + /collections: + post: consumes: - application/json - deprecated: true - description: 测试接口,将会返回当前用户的信息 + description: 在工作空间下创建新的集合 + parameters: + - description: 创建集合请求 + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.CreateCollectionRequest' produces: - application/json responses: @@ -40,20 +220,705 @@ paths: description: OK schema: allOf: - - $ref: '#/definitions/response.Body' + - $ref: '#/definitions/dto.Response' - properties: data: - $ref: '#/definitions/schema.CurrentUserResponse' + $ref: '#/definitions/leafdev_top_Leaf_leaf-library-3_internal_entity.Collection' type: object "400": - description: Bad Request + description: 参数验证失败 schema: - $ref: '#/definitions/response.Body' - security: - - ApiKeyAuth: [] - summary: Greet + allOf: + - $ref: '#/definitions/dto.Response' + - properties: + data: + items: + $ref: '#/definitions/dto.ValidateError' + type: array + type: object + "404": + description: 工作空间不存在 + schema: + $ref: '#/definitions/dto.Response' + summary: 创建集合 tags: - - ping + - Collection + /collections/{collection_id}/documents: + get: + consumes: + - application/json + description: 获取指定集合下的文档树,如果指定了父文档ID则获取该文档下的子树 + parameters: + - description: 集合ID + in: path + name: collection_id + required: true + type: integer + - description: 父文档ID,不传则获取根文档 + in: query + name: parent_id + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/dto.Response' + - properties: + data: + items: + $ref: '#/definitions/dto.DocumentDTO' + type: array + type: object + "403": + description: 无权访问 + schema: + $ref: '#/definitions/dto.Response' + "404": + description: 集合不存在或父文档不存在 + schema: + $ref: '#/definitions/dto.Response' + summary: 列出文档树 + tags: + - Document + /collections/{id}: + delete: + consumes: + - application/json + description: 删除指定的集合 + parameters: + - description: 集合ID + in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/dto.Response' + "404": + description: 集合不存在 + schema: + $ref: '#/definitions/dto.Response' + summary: 删除集合 + tags: + - Collection + get: + consumes: + - application/json + description: 根据集合ID获取集合详情 + parameters: + - description: 集合ID + in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/dto.Response' + - properties: + data: + $ref: '#/definitions/leafdev_top_Leaf_leaf-library-3_internal_entity.Collection' + type: object + "404": + description: 集合不存在 + schema: + $ref: '#/definitions/dto.Response' + summary: 获取集合 + tags: + - Collection + /documents: + post: + consumes: + - application/json + description: 创建一个新的文档,可以是根文档或子文档 + parameters: + - description: 创建文档请求 + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.CreateDocumentRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/dto.Response' + - properties: + data: + $ref: '#/definitions/dto.DocumentDTO' + type: object + "400": + description: 参数验证失败 + schema: + allOf: + - $ref: '#/definitions/dto.Response' + - properties: + data: + items: + $ref: '#/definitions/dto.ValidateError' + type: array + type: object + "403": + description: 无权访问 + schema: + $ref: '#/definitions/dto.Response' + "404": + description: 集合不存在或父文档不存在 + schema: + $ref: '#/definitions/dto.Response' + summary: 创建文档 + tags: + - Document + /documents/{document_id}/blocks: + get: + consumes: + - application/json + description: 获取指文档下的所有文档块 + parameters: + - description: 文档ID + in: path + name: document_id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/dto.Response' + - properties: + data: + items: + $ref: '#/definitions/entity.DocumentBlock' + type: array + type: object + "404": + description: 文档不存在 + schema: + $ref: '#/definitions/dto.Response' + summary: 列出文档块列表 + tags: + - Document + post: + consumes: + - application/json + description: 在文档中创建新的文档块 + parameters: + - description: 文档ID + in: path + name: document_id + required: true + type: integer + - description: 创建文档块请求 + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.CreateBlockRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/dto.Response' + - properties: + data: + $ref: '#/definitions/entity.DocumentBlock' + type: object + "400": + description: 参数验证失败 + schema: + allOf: + - $ref: '#/definitions/dto.Response' + - properties: + data: + items: + $ref: '#/definitions/dto.ValidateError' + type: array + type: object + "404": + description: 文档不存在 + schema: + $ref: '#/definitions/dto.Response' + summary: 创建文档块 + tags: + - Document + /documents/{document_id}/blocks/{block_id}: + delete: + consumes: + - application/json + description: 删除指定的文档块 + parameters: + - description: 文档ID + in: path + name: document_id + required: true + type: integer + - description: 文档块ID + in: path + name: block_id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/dto.Response' + "404": + description: 文档块不存在 + schema: + $ref: '#/definitions/dto.Response' + summary: 删除文档块 + tags: + - Document + put: + consumes: + - application/json + description: 更新文档块的内容 + parameters: + - description: 文档ID + in: path + name: document_id + required: true + type: integer + - description: 文档块ID + in: path + name: block_id + required: true + type: integer + - description: 更新文档块请求 + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.UpdateBlockRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/dto.Response' + - properties: + data: + $ref: '#/definitions/entity.DocumentBlock' + type: object + "400": + description: 参数验证失败 + schema: + allOf: + - $ref: '#/definitions/dto.Response' + - properties: + data: + items: + $ref: '#/definitions/dto.ValidateError' + type: array + type: object + "404": + description: 文档块不存在 + schema: + $ref: '#/definitions/dto.Response' + summary: 更新文档块 + tags: + - Document + /documents/{document_id}/blocks/{block_id}/move: + post: + consumes: + - application/json + description: 移动文档块的位置 + parameters: + - description: 文档ID + in: path + name: document_id + required: true + type: integer + - description: 文档块ID + in: path + name: block_id + required: true + type: integer + - description: 移动文档块请求 + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.MoveBlockRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/dto.Response' + "400": + description: 参数验证失败 + schema: + allOf: + - $ref: '#/definitions/dto.Response' + - properties: + data: + items: + $ref: '#/definitions/dto.ValidateError' + type: array + type: object + "404": + description: 文档块不存在 + schema: + $ref: '#/definitions/dto.Response' + summary: 移动文档块 + tags: + - Document + /documents/{id}: + delete: + consumes: + - application/json + description: 删除指定的文档 + parameters: + - description: 文档ID + in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/dto.Response' + "404": + description: 文档不存在 + schema: + $ref: '#/definitions/dto.Response' + summary: 删除文档 + tags: + - Document + get: + consumes: + - application/json + description: 根据文档ID获取文档详情 + parameters: + - description: 文档ID + in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/dto.Response' + - properties: + data: + $ref: '#/definitions/dto.DocumentDTO' + type: object + "403": + description: 无权访问 + schema: + $ref: '#/definitions/dto.Response' + "404": + description: 文档不存在 + schema: + $ref: '#/definitions/dto.Response' + summary: 获取文档 + tags: + - Document + put: + consumes: + - application/json + description: 更新文档的名称等信息 + parameters: + - description: 文档ID + in: path + name: id + required: true + type: integer + - description: 更新文档请求 + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.UpdateDocumentRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/dto.Response' + - properties: + data: + $ref: '#/definitions/entity.Document' + type: object + "400": + description: 参数验证失败 + schema: + allOf: + - $ref: '#/definitions/dto.Response' + - properties: + data: + items: + $ref: '#/definitions/dto.ValidateError' + type: array + type: object + "404": + description: 文档不存在 + schema: + $ref: '#/definitions/dto.Response' + summary: 更新文档 + tags: + - Document + /workspaces: + get: + consumes: + - application/json + description: 获取当前用户的所有工作空间 + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/dto.Response' + - properties: + data: + items: + $ref: '#/definitions/entity.Workspace' + type: array + type: object + summary: 列出工作空间列表 + tags: + - Workspace + post: + consumes: + - application/json + description: 创建一个新的工作空间 + parameters: + - description: 创建工作空间请求 + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.CreateWorkspaceRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/dto.Response' + - properties: + data: + $ref: '#/definitions/entity.Workspace' + type: object + "400": + description: 参数验证失败 + schema: + allOf: + - $ref: '#/definitions/dto.Response' + - properties: + data: + items: + $ref: '#/definitions/dto.ValidateError' + type: array + type: object + summary: 创建工作空间 + tags: + - Workspace + /workspaces/{id}: + delete: + consumes: + - application/json + description: 删除指定的工作空间 + parameters: + - description: 工作空间ID + in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/dto.Response' + "404": + description: 工作空间不存在 + schema: + $ref: '#/definitions/dto.Response' + summary: 删除工作空间 + tags: + - Workspace + get: + consumes: + - application/json + description: 根据工作空间ID获取工作空间详情 + parameters: + - description: 工作空间ID + in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/dto.Response' + - properties: + data: + $ref: '#/definitions/entity.Workspace' + type: object + "404": + description: 工作空间不存在 + schema: + $ref: '#/definitions/dto.Response' + summary: 获取工作空间 + tags: + - Workspace + /workspaces/{id}/members: + post: + consumes: + - application/json + description: 向工作空间添加新成员 + parameters: + - description: 工作空间ID + in: path + name: id + required: true + type: integer + - description: 添加成员请求 + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.AddWorkspaceMemberRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/dto.Response' + - properties: + data: + $ref: '#/definitions/entity.WorkspaceMember' + type: object + "400": + description: 参数验证失败 + schema: + allOf: + - $ref: '#/definitions/dto.Response' + - properties: + data: + items: + $ref: '#/definitions/dto.ValidateError' + type: array + type: object + "404": + description: 工作空间不存在 + schema: + $ref: '#/definitions/dto.Response' + "409": + description: 成员已存在 + schema: + $ref: '#/definitions/dto.Response' + summary: 添加工作空间成员 + tags: + - Workspace + /workspaces/{id}/members/{user_id}: + delete: + consumes: + - application/json + description: 从工作空间移除成员 + parameters: + - description: 工作空间ID + in: path + name: id + required: true + type: integer + - description: 用户ID + in: path + name: user_id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/dto.Response' + "404": + description: 工作空间不存在或成员不存在 + schema: + $ref: '#/definitions/dto.Response' + summary: 移除工作空间成员 + tags: + - Workspace + /workspaces/{workspace_id}/collections: + get: + consumes: + - application/json + description: 获取指定工作空间下的所有集合 + parameters: + - description: 工作空间ID + in: path + name: workspace_id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/dto.Response' + - properties: + data: + items: + $ref: '#/definitions/leafdev_top_Leaf_leaf-library-3_internal_entity.Collection' + type: array + type: object + "404": + description: 工作空间不存在 + schema: + $ref: '#/definitions/dto.Response' + summary: 列出集合列表 + tags: + - Collection securityDefinitions: ApiKeyAuth: in: header diff --git a/go.sum b/go.sum index 23b6d66..ae3af8a 100644 --- a/go.sum +++ b/go.sum @@ -128,6 +128,7 @@ github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6 github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/subcommands v1.2.0 h1:vWQspBTo2nEqTUFita5/KeEWlUL8kQObDFbub/EN9oE= github.com/google/subcommands v1.2.0/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= diff --git a/internal/api/http/controller/collection.go b/internal/api/http/controller/collection.go new file mode 100644 index 0000000..2c2cfb7 --- /dev/null +++ b/internal/api/http/controller/collection.go @@ -0,0 +1,168 @@ +package controller + +import ( + "github.com/gofiber/fiber/v2" + _ "leafdev.top/Leaf/leaf-library-3/internal/entity" + "leafdev.top/Leaf/leaf-library-3/internal/pkg/validator" + "leafdev.top/Leaf/leaf-library-3/internal/services/auth" + "leafdev.top/Leaf/leaf-library-3/internal/services/collection" + "leafdev.top/Leaf/leaf-library-3/internal/services/workspace" + "leafdev.top/Leaf/leaf-library-3/internal/types/dto" + "leafdev.top/Leaf/leaf-library-3/internal/types/errs" +) + +type CollectionController struct { + collectionService *collection.Service + workspaceService *workspace.Service + authService *auth.Service +} + +func NewCollectionController( + collectionService *collection.Service, + workspaceService *workspace.Service, + authService *auth.Service, +) *CollectionController { + return &CollectionController{ + collectionService: collectionService, + workspaceService: workspaceService, + authService: authService, + } +} + +// CreateCollection 创建集合 +// @Summary 创建集合 +// @Description 在工作空间下创建新的集合 +// @Tags Collection +// @Accept json +// @Produce json +// @Param request body dto.CreateCollectionRequest true "创建集合请求" +// @Success 200 {object} dto.Response{data=entity.Collection} +// @Failure 400 {object} dto.Response{data=[]dto.ValidateError} "参数验证失败" +// @Failure 404 {object} dto.Response "工作空间不存在" +// @Router /collections [post] +func (c *CollectionController) CreateCollection(ctx *fiber.Ctx) error { + var req dto.CreateCollectionRequest + if err := ctx.BodyParser(&req); err != nil { + return err + } + + if validationErrors, ok, err := validator.Struct(req); !ok { + if err != nil { + return err + } + return dto.Ctx(ctx).Message((*validationErrors)[0].Message).Status(fiber.StatusBadRequest).Send() + } + + // 检查工作空间是否存在 + exists, err := c.workspaceService.Exists(ctx.Context(), req.WorkspaceID) + if err != nil { + return err + } + if !exists { + return dto.Ctx(ctx).Error(errs.ErrWorkspaceNotExists).Status(fiber.StatusNotFound).Send() + } + + collection, err := c.collectionService.Create(ctx.Context(), req.WorkspaceID, req.Name) + if err != nil { + return err + } + + return dto.Ctx(ctx).Success(collection).Send() +} + +// GetCollection 获取集合 +// @Summary 获取集合 +// @Description 根据集合ID获取集合详情 +// @Tags Collection +// @Accept json +// @Produce json +// @Param id path int true "集合ID" +// @Success 200 {object} dto.Response{data=entity.Collection} +// @Failure 404 {object} dto.Response "集合不存在" +// @Router /collections/{id} [get] +func (c *CollectionController) GetCollection(ctx *fiber.Ctx) error { + var req dto.GetCollectionRequest + if err := ctx.ParamsParser(&req); err != nil { + return err + } + + exists, err := c.collectionService.Exists(ctx.Context(), req.ID) + if err != nil { + return err + } + if !exists { + return dto.Ctx(ctx).Error(errs.ErrCollectionNotExists).Status(fiber.StatusNotFound).Send() + } + + collection, err := c.collectionService.Get(ctx.Context(), req.ID) + if err != nil { + return err + } + + return dto.Ctx(ctx).Success(collection).Send() +} + +// ListCollections 列出工作空间下的所有集合 +// @Summary 列出集合列表 +// @Description 获取指定工作空间下的所有集合 +// @Tags Collection +// @Accept json +// @Produce json +// @Param workspace_id path int true "工作空间ID" +// @Success 200 {object} dto.Response{data=[]entity.Collection} +// @Failure 404 {object} dto.Response "工作空间不存在" +// @Router /workspaces/{workspace_id}/collections [get] +func (c *CollectionController) ListCollections(ctx *fiber.Ctx) error { + var req dto.ListCollectionsRequest + if err := ctx.ParamsParser(&req); err != nil { + return err + } + + // 检查工作空间是否存在 + exists, err := c.workspaceService.Exists(ctx.Context(), req.WorkspaceID) + if err != nil { + return err + } + if !exists { + return dto.Ctx(ctx).Error(errs.ErrWorkspaceNotExists).Status(fiber.StatusNotFound).Send() + } + + collections, err := c.collectionService.List(ctx.Context(), req.WorkspaceID) + if err != nil { + return err + } + + return dto.Ctx(ctx).Success(collections).Send() +} + +// DeleteCollection 删除集合 +// @Summary 删除集合 +// @Description 删除指定的集合 +// @Tags Collection +// @Accept json +// @Produce json +// @Param id path int true "集合ID" +// @Success 200 {object} dto.Response +// @Failure 404 {object} dto.Response "集合不存在" +// @Router /collections/{id} [delete] +func (c *CollectionController) DeleteCollection(ctx *fiber.Ctx) error { + var req dto.DeleteCollectionRequest + if err := ctx.ParamsParser(&req); err != nil { + return err + } + + exists, err := c.collectionService.Exists(ctx.Context(), req.ID) + if err != nil { + return err + } + if !exists { + return dto.Ctx(ctx).Error(errs.ErrCollectionNotExists).Status(fiber.StatusNotFound).Send() + } + + err = c.collectionService.Delete(ctx.Context(), req.ID) + if err != nil { + return err + } + + return dto.Ctx(ctx).Success(nil).Send() +} diff --git a/internal/api/http/controller/document.go b/internal/api/http/controller/document.go new file mode 100644 index 0000000..00187a9 --- /dev/null +++ b/internal/api/http/controller/document.go @@ -0,0 +1,476 @@ +package controller + +import ( + "github.com/gofiber/fiber/v2" + "leafdev.top/Leaf/leaf-library-3/internal/pkg/validator" + "leafdev.top/Leaf/leaf-library-3/internal/services/auth" + "leafdev.top/Leaf/leaf-library-3/internal/services/collection" + "leafdev.top/Leaf/leaf-library-3/internal/services/document" + "leafdev.top/Leaf/leaf-library-3/internal/services/workspace" + "leafdev.top/Leaf/leaf-library-3/internal/types/dto" + "leafdev.top/Leaf/leaf-library-3/internal/types/errs" +) + +type DocumentController struct { + documentService *document.Service + collectionService *collection.Service + workspaceService *workspace.Service + authService *auth.Service +} + +func NewDocumentController( + documentService *document.Service, + collectionService *collection.Service, + workspaceService *workspace.Service, + authService *auth.Service, +) *DocumentController { + return &DocumentController{ + documentService: documentService, + collectionService: collectionService, + workspaceService: workspaceService, + authService: authService, + } +} + +// CreateDocument 创建文档 +// @Summary 创建文档 +// @Description 创建一个新的文档,可以是根文档或子文档 +// @Tags Document +// @Accept json +// @Produce json +// @Param request body dto.CreateDocumentRequest true "创建文档请求" +// @Success 200 {object} dto.Response{data=dto.DocumentDTO} +// @Failure 400 {object} dto.Response{data=[]dto.ValidateError} "参数验证失败" +// @Failure 403 {object} dto.Response "无权访问" +// @Failure 404 {object} dto.Response "集合不存在或父文档不存在" +// @Router /documents [post] +func (c *DocumentController) CreateDocument(ctx *fiber.Ctx) error { + var req dto.CreateDocumentRequest + if err := ctx.BodyParser(&req); err != nil { + return err + } + + if validationErrors, ok, err := validator.Struct(req); !ok { + if err != nil { + return err + } + return dto.Ctx(ctx).Data(validationErrors).Status(fiber.StatusBadRequest).Send() + } + + // 检查集合是否存在 + collection, err := c.collectionService.Get(ctx.Context(), req.CollectionID) + if err != nil { + return err + } + if collection == nil { + return dto.Ctx(ctx).Error(errs.ErrCollectionNotExists).Status(fiber.StatusNotFound).Send() + } + + // 检查用户是否有权限访问该集合 + user := c.authService.GetUser(ctx) + workspace, err := c.workspaceService.Get(ctx.Context(), collection.WorkspaceId) + if err != nil { + return err + } + if workspace == nil { + return dto.Ctx(ctx).Error(errs.ErrWorkspaceNotExists).Status(fiber.StatusNotFound).Send() + } + + isMember, err := c.workspaceService.IsMember(ctx.Context(), user.ID, workspace) + if err != nil { + return err + } + if !isMember { + return dto.Ctx(ctx).Error(errs.ErrNoPermission).Status(fiber.StatusForbidden).Send() + } + + // 如果是子文档,检查父文档是否存在且属于同一个集合 + if req.ParentID != nil { + parentDoc, err := c.documentService.Get(ctx.Context(), *req.ParentID) + if err != nil { + return err + } + if parentDoc == nil { + return dto.Ctx(ctx).Error(errs.ErrDocumentNotExists).Status(fiber.StatusNotFound).Send() + } + if parentDoc.CollectionId != req.CollectionID { + return dto.Ctx(ctx).Error(errs.ErrInvalidParentDocument).Status(fiber.StatusBadRequest).Send() + } + } + + doc, err := c.documentService.Create(ctx.Context(), req.Name, req.WorkspaceID, req.CollectionID, req.ParentID) + if err != nil { + return err + } + + docDTO, err := c.documentService.ToDTO(ctx.Context(), doc) + if err != nil { + return err + } + + return dto.Ctx(ctx).Success(docDTO).Send() +} + +// GetDocument 获取文档 +// @Summary 获取文档 +// @Description 根据文档ID获取文档详情 +// @Tags Document +// @Accept json +// @Produce json +// @Param id path int true "文档ID" +// @Success 200 {object} dto.Response{data=dto.DocumentDTO} +// @Failure 403 {object} dto.Response "无权访问" +// @Failure 404 {object} dto.Response "文档不存在" +// @Router /documents/{id} [get] +func (c *DocumentController) GetDocument(ctx *fiber.Ctx) error { + var params dto.DocumentIDParam + if err := ctx.ParamsParser(¶ms); err != nil { + return err + } + + doc, err := c.documentService.Get(ctx.Context(), params.ID) + if err != nil { + return err + } + if doc == nil { + return dto.Ctx(ctx).Error(errs.ErrDocumentNotExists).Status(fiber.StatusNotFound).Send() + } + + // 检查用户是否有权限访问该文档 + user := c.authService.GetUser(ctx) + collection, err := c.collectionService.Get(ctx.Context(), doc.CollectionId) + if err != nil { + return err + } + workspace, err := c.workspaceService.Get(ctx.Context(), collection.WorkspaceId) + if err != nil { + return err + } + + isMember, err := c.workspaceService.IsMember(ctx.Context(), user.ID, workspace) + if err != nil { + return err + } + if !isMember { + return dto.Ctx(ctx).Error(errs.ErrNoPermission).Status(fiber.StatusForbidden).Send() + } + + docDTO, err := c.documentService.ToDTO(ctx.Context(), doc) + if err != nil { + return err + } + + return dto.Ctx(ctx).Success(docDTO).Send() +} + +// ListDocuments 列出集合或父文档下的所有文档 +// @Summary 列出文档树 +// @Description 获取指定集合下的文档树,如果指定了父文档ID则获取该文档下的子树 +// @Tags Document +// @Accept json +// @Produce json +// @Param collection_id path int true "集合ID" +// @Param parent_id query int false "父文档ID,不传则获取根文档" +// @Success 200 {object} dto.Response{data=[]dto.DocumentDTO} +// @Failure 403 {object} dto.Response "无权访问" +// @Failure 404 {object} dto.Response "集合不存在或父文档不存在" +// @Router /collections/{collection_id}/documents [get] +func (c *DocumentController) ListDocuments(ctx *fiber.Ctx) error { + var pathParams dto.ListDocumentsPathParam + if err := ctx.ParamsParser(&pathParams); err != nil { + return err + } + + var queryParams dto.ListDocumentsQueryParam + if err := ctx.QueryParser(&queryParams); err != nil { + return err + } + + // 检查集合是否存在 + collection, err := c.collectionService.Get(ctx.Context(), pathParams.CollectionID) + if err != nil { + return err + } + if collection == nil { + return dto.Ctx(ctx).Error(errs.ErrCollectionNotExists).Status(fiber.StatusNotFound).Send() + } + + // 检查用户是否有权限访问该集合 + user := c.authService.GetUser(ctx) + workspace, err := c.workspaceService.Get(ctx.Context(), collection.WorkspaceId) + if err != nil { + return err + } + + isMember, err := c.workspaceService.IsMember(ctx.Context(), user.ID, workspace) + if err != nil { + return err + } + if !isMember { + return dto.Ctx(ctx).Error(errs.ErrNoPermission).Status(fiber.StatusForbidden).Send() + } + + // 如果指定了父文档,检查父文档是否存在且属于同一个集合 + if queryParams.ParentID != nil { + parentDoc, err := c.documentService.Get(ctx.Context(), *queryParams.ParentID) + if err != nil { + return err + } + if parentDoc == nil { + return dto.Ctx(ctx).Error(errs.ErrDocumentNotExists).Status(fiber.StatusNotFound).Send() + } + if parentDoc.CollectionId != pathParams.CollectionID { + return dto.Ctx(ctx).Error(errs.ErrInvalidParentDocument).Status(fiber.StatusBadRequest).Send() + } + } + + // 获取文档列表 + docs, err := c.documentService.List(ctx.Context(), pathParams.CollectionID, queryParams.ParentID) + if err != nil { + return err + } + + // 转换为 DTO + dtos := make([]*dto.DocumentDTO, len(docs)) + for i, doc := range docs { + docDTO, err := c.documentService.ToDTO(ctx.Context(), doc) + if err != nil { + return err + } + dtos[i] = docDTO + } + + return dto.Ctx(ctx).Success(dtos).Send() +} + +// UpdateDocument 更新文档 +// @Summary 更新文档 +// @Description 更新文档的名称等信息 +// @Tags Document +// @Accept json +// @Produce json +// @Param id path int true "文档ID" +// @Param request body dto.UpdateDocumentRequest true "更新文档请求" +// @Success 200 {object} dto.Response{data=entity.Document} +// @Failure 400 {object} dto.Response{data=[]dto.ValidateError} "参数验证失败" +// @Failure 404 {object} dto.Response "文档不存在" +// @Router /documents/{id} [put] +func (c *DocumentController) UpdateDocument(ctx *fiber.Ctx) error { + var params dto.DocumentIDParam + if err := ctx.ParamsParser(¶ms); err != nil { + return err + } + + var req dto.UpdateDocumentRequest + if err := ctx.BodyParser(&req); err != nil { + return err + } + + if validationErrors, ok, err := validator.Struct(req); !ok { + if err != nil { + return err + } + return dto.Ctx(ctx).Data(validationErrors).Status(fiber.StatusBadRequest).Send() + } + + doc, err := c.documentService.Update(ctx.Context(), params.ID, req.Name) + if err != nil { + return err + } + + docDTO, err := c.documentService.ToDTO(ctx.Context(), doc) + if err != nil { + return err + } + + return dto.Ctx(ctx).Success(docDTO).Send() +} + +// DeleteDocument 删除文档 +// @Summary 删除文档 +// @Description 删除指定的文档 +// @Tags Document +// @Accept json +// @Produce json +// @Param id path int true "文档ID" +// @Success 200 {object} dto.Response +// @Failure 404 {object} dto.Response "文档不存在" +// @Router /documents/{id} [delete] +func (c *DocumentController) DeleteDocument(ctx *fiber.Ctx) error { + var params dto.DocumentIDParam + if err := ctx.ParamsParser(¶ms); err != nil { + return err + } + + err := c.documentService.Delete(ctx.Context(), params.ID) + if err != nil { + return err + } + + return dto.Ctx(ctx).Success(nil).Send() +} + +// CreateBlock 创建文档块 +// @Summary 创建文档块 +// @Description 在文档中创建新的文档块 +// @Tags Document +// @Accept json +// @Produce json +// @Param document_id path int true "文档ID" +// @Param request body dto.CreateBlockRequest true "创建文档块请求" +// @Success 200 {object} dto.Response{data=entity.DocumentBlock} +// @Failure 400 {object} dto.Response{data=[]dto.ValidateError} "参数验证失败" +// @Failure 404 {object} dto.Response "文档不存在" +// @Router /documents/{document_id}/blocks [post] +func (c *DocumentController) CreateBlock(ctx *fiber.Ctx) error { + var params dto.ListBlocksRequest + if err := ctx.ParamsParser(¶ms); err != nil { + return err + } + + var req dto.CreateBlockRequest + if err := ctx.BodyParser(&req); err != nil { + return err + } + + if validationErrors, ok, err := validator.Struct(req); !ok { + if err != nil { + return err + } + return dto.Ctx(ctx).Data(validationErrors).Status(fiber.StatusBadRequest).Send() + } + + block, err := c.documentService.CreateBlock(ctx.Context(), params.DocumentID, req.Type, req.Content, req.AfterBlockID) + if err != nil { + return err + } + + return dto.Ctx(ctx).Success(block).Send() +} + +// UpdateBlock 更新文档块 +// @Summary 更新文档块 +// @Description 更新文档块的内容 +// @Tags Document +// @Accept json +// @Produce json +// @Param document_id path int true "文档ID" +// @Param block_id path int true "文档块ID" +// @Param request body dto.UpdateBlockRequest true "更新文档块请求" +// @Success 200 {object} dto.Response{data=entity.DocumentBlock} +// @Failure 400 {object} dto.Response{data=[]dto.ValidateError} "参数验证失败" +// @Failure 404 {object} dto.Response "文档块不存在" +// @Router /documents/{document_id}/blocks/{block_id} [put] +func (c *DocumentController) UpdateBlock(ctx *fiber.Ctx) error { + var params dto.BlockIDParam + if err := ctx.ParamsParser(¶ms); err != nil { + return err + } + + var req dto.UpdateBlockRequest + if err := ctx.BodyParser(&req); err != nil { + return err + } + + if validationErrors, ok, err := validator.Struct(req); !ok { + if err != nil { + return err + } + return dto.Ctx(ctx).Data(validationErrors).Status(fiber.StatusBadRequest).Send() + } + + block, err := c.documentService.UpdateBlock(ctx.Context(), params.ID, req.Content) + if err != nil { + return err + } + + return dto.Ctx(ctx).Success(block).Send() +} + +// DeleteBlock 删除文档块 +// @Summary 删除文档块 +// @Description 删除指定的文档块 +// @Tags Document +// @Accept json +// @Produce json +// @Param document_id path int true "文档ID" +// @Param block_id path int true "文档块ID" +// @Success 200 {object} dto.Response +// @Failure 404 {object} dto.Response "文档块不存在" +// @Router /documents/{document_id}/blocks/{block_id} [delete] +func (c *DocumentController) DeleteBlock(ctx *fiber.Ctx) error { + var params dto.BlockIDParam + if err := ctx.ParamsParser(¶ms); err != nil { + return err + } + + err := c.documentService.DeleteBlock(ctx.Context(), params.ID) + if err != nil { + return err + } + + return dto.Ctx(ctx).Success(nil).Send() +} + +// ListBlocks 列出文档下的所有块 +// @Summary 列出文档块列表 +// @Description 获取指文档下的所有文档块 +// @Tags Document +// @Accept json +// @Produce json +// @Param document_id path int true "文档ID" +// @Success 200 {object} dto.Response{data=[]entity.DocumentBlock} +// @Failure 404 {object} dto.Response "文档不存在" +// @Router /documents/{document_id}/blocks [get] +func (c *DocumentController) ListBlocks(ctx *fiber.Ctx) error { + var params dto.ListBlocksRequest + if err := ctx.ParamsParser(¶ms); err != nil { + return err + } + + blocks, err := c.documentService.ListBlocks(ctx.Context(), params.DocumentID) + if err != nil { + return err + } + + return dto.Ctx(ctx).Success(blocks).Send() +} + +// MoveBlock 移动文档块 +// @Summary 移动文档块 +// @Description 移动文档块的位置 +// @Tags Document +// @Accept json +// @Produce json +// @Param document_id path int true "文档ID" +// @Param block_id path int true "文档块ID" +// @Param request body dto.MoveBlockRequest true "移动文档块请求" +// @Success 200 {object} dto.Response +// @Failure 400 {object} dto.Response{data=[]dto.ValidateError} "参数验证失败" +// @Failure 404 {object} dto.Response "文档块不存在" +// @Router /documents/{document_id}/blocks/{block_id}/move [post] +func (c *DocumentController) MoveBlock(ctx *fiber.Ctx) error { + var params dto.BlockIDParam + if err := ctx.ParamsParser(¶ms); err != nil { + return err + } + + var req dto.MoveBlockRequest + if err := ctx.BodyParser(&req); err != nil { + return err + } + + if validationErrors, ok, err := validator.Struct(req); !ok { + if err != nil { + return err + } + return dto.Ctx(ctx).Data(validationErrors).Status(fiber.StatusBadRequest).Send() + } + + err := c.documentService.MoveBlock(ctx.Context(), params.ID, req.AfterBlockID) + if err != nil { + return err + } + + return dto.Ctx(ctx).Success(nil).Send() +} diff --git a/internal/api/http/controller/workspace.go b/internal/api/http/controller/workspace.go new file mode 100644 index 0000000..ef0357b --- /dev/null +++ b/internal/api/http/controller/workspace.go @@ -0,0 +1,240 @@ +package controller + +import ( + "github.com/gofiber/fiber/v2" + _ "leafdev.top/Leaf/leaf-library-3/internal/entity" + "leafdev.top/Leaf/leaf-library-3/internal/pkg/validator" + "leafdev.top/Leaf/leaf-library-3/internal/services/auth" + "leafdev.top/Leaf/leaf-library-3/internal/services/workspace" + "leafdev.top/Leaf/leaf-library-3/internal/types/dto" + "leafdev.top/Leaf/leaf-library-3/internal/types/errs" + "leafdev.top/Leaf/leaf-library-3/internal/types/user" +) + +type WorkspaceController struct { + workspaceService *workspace.Service + authService *auth.Service +} + +func NewWorkspaceController(workspaceService *workspace.Service, authService *auth.Service) *WorkspaceController { + return &WorkspaceController{ + workspaceService: workspaceService, + authService: authService, + } +} + +// CreateWorkspace 创建工作空间 +// @Summary 创建工作空间 +// @Description 创建一个新的工作空间 +// @Tags Workspace +// @Accept json +// @Produce json +// @Param request body dto.CreateWorkspaceRequest true "创建工作空间请求" +// @Success 200 {object} dto.Response{data=entity.Workspace} +// @Failure 400 {object} dto.Response{data=[]dto.ValidateError} "参数验证失败" +// @Router /workspaces [post] +func (c *WorkspaceController) CreateWorkspace(ctx *fiber.Ctx) error { + var req dto.CreateWorkspaceRequest + if err := ctx.BodyParser(&req); err != nil { + return err + } + + if validationErrors, ok, err := validator.Struct(req); !ok { + if err != nil { + return err + } + return dto.Ctx(ctx).Data(validationErrors).Status(fiber.StatusBadRequest).Send() + } + + // 从上下文获取用户ID + user := c.authService.GetUser(ctx) + userId := user.ID + + workspace, err := c.workspaceService.Create(ctx.Context(), userId, req.Name) + if err != nil { + return err + } + + return dto.Ctx(ctx).Success(workspace).Send() +} + +// GetWorkspace 获取工作空间 +// @Summary 获取工作空间 +// @Description 根据工作空间ID获取工作空间详情 +// @Tags Workspace +// @Accept json +// @Produce json +// @Param id path int true "工作空间ID" +// @Success 200 {object} dto.Response{data=entity.Workspace} +// @Failure 404 {object} dto.Response "工作空间不存在" +// @Router /workspaces/{id} [get] +func (c *WorkspaceController) GetWorkspace(ctx *fiber.Ctx) error { + var params dto.WorkspaceIDParam + if err := ctx.ParamsParser(¶ms); err != nil { + return err + } + + exists, err := c.workspaceService.Exists(ctx.Context(), params.ID) + if err != nil { + return err + } + if !exists { + return dto.Ctx(ctx).Error(errs.ErrWorkspaceNotExists).Status(fiber.StatusNotFound).Send() + } + + workspace, err := c.workspaceService.Get(ctx.Context(), params.ID) + if err != nil { + return err + } + + return dto.Ctx(ctx).Success(workspace).Send() +} + +// ListWorkspaces 列出用户的所有工作空间 +// @Summary 列出工作空间列表 +// @Description 获取当前用户的所有工作空间 +// @Tags Workspace +// @Accept json +// @Produce json +// @Success 200 {object} dto.Response{data=[]entity.Workspace} +// @Router /workspaces [get] +func (c *WorkspaceController) ListWorkspaces(ctx *fiber.Ctx) error { + // 从上下文获取用户ID + user := c.authService.GetUser(ctx) + userId := user.ID + + workspaces, err := c.workspaceService.List(ctx.Context(), userId) + if err != nil { + return err + } + + return dto.Ctx(ctx).Success(workspaces).Send() +} + +// DeleteWorkspace 删除工作空间 +// @Summary 删除工作空间 +// @Description 删除指定的工作空间 +// @Tags Workspace +// @Accept json +// @Produce json +// @Param id path int true "工作空间ID" +// @Success 200 {object} dto.Response +// @Failure 404 {object} dto.Response "工作空间不存在" +// @Router /workspaces/{id} [delete] +func (c *WorkspaceController) DeleteWorkspace(ctx *fiber.Ctx) error { + var params dto.WorkspaceIDParam + if err := ctx.ParamsParser(¶ms); err != nil { + return err + } + + exists, err := c.workspaceService.Exists(ctx.Context(), params.ID) + if err != nil { + return err + } + if !exists { + return dto.Ctx(ctx).Error(errs.ErrWorkspaceNotExists).Status(fiber.StatusNotFound).Send() + } + + err = c.workspaceService.DeleteWorkspace(ctx.Context(), params.ID) + if err != nil { + return err + } + + return dto.Ctx(ctx).Success(nil).Send() +} + +// AddMember 添加工作空间成员 +// @Summary 添加工作空间成员 +// @Description 向工作空间添加新成员 +// @Tags Workspace +// @Accept json +// @Produce json +// @Param id path int true "工作空间ID" +// @Param request body dto.AddWorkspaceMemberRequest true "添加成员请求" +// @Success 200 {object} dto.Response{data=entity.WorkspaceMember} +// @Failure 400 {object} dto.Response{data=[]dto.ValidateError} "参数验证失败" +// @Failure 404 {object} dto.Response "工作空间不存在" +// @Failure 409 {object} dto.Response "成员已存在" +// @Router /workspaces/{id}/members [post] +func (c *WorkspaceController) AddMember(ctx *fiber.Ctx) error { + var params dto.WorkspaceIDParam + if err := ctx.ParamsParser(¶ms); err != nil { + return err + } + + exists, err := c.workspaceService.Exists(ctx.Context(), params.ID) + if err != nil { + return err + } + if !exists { + return dto.Ctx(ctx).Error(errs.ErrWorkspaceNotExists).Status(fiber.StatusNotFound).Send() + } + + var req dto.AddWorkspaceMemberRequest + if err := ctx.BodyParser(&req); err != nil { + return err + } + + if validationErrors, ok, err := validator.Struct(req); !ok { + if err != nil { + return err + } + return dto.Ctx(ctx).Data(validationErrors).Status(fiber.StatusBadRequest).Send() + } + + workspace, err := c.workspaceService.Get(ctx.Context(), params.ID) + if err != nil { + return err + } + + member, err := c.workspaceService.AddMember(ctx.Context(), req.UserID, workspace) + if err != nil { + if err == errs.ErrMemberAlreadyExists { + return dto.Ctx(ctx).Error(err).Status(fiber.StatusConflict).Send() + } + return err + } + + return dto.Ctx(ctx).Success(member).Send() +} + +// RemoveMember 移除工作空间成员 +// @Summary 移除工作空间成员 +// @Description 从工作空间移除成员 +// @Tags Workspace +// @Accept json +// @Produce json +// @Param id path int true "工作空间ID" +// @Param user_id path string true "用户ID" +// @Success 200 {object} dto.Response +// @Failure 404 {object} dto.Response "工作空间不存在或成员不存在" +// @Router /workspaces/{id}/members/{user_id} [delete] +func (c *WorkspaceController) RemoveMember(ctx *fiber.Ctx) error { + var params dto.WorkspaceMemberParam + if err := ctx.ParamsParser(¶ms); err != nil { + return err + } + + exists, err := c.workspaceService.Exists(ctx.Context(), params.WorkspaceID) + if err != nil { + return err + } + if !exists { + return dto.Ctx(ctx).Error(errs.ErrWorkspaceNotExists).Status(fiber.StatusNotFound).Send() + } + + workspace, err := c.workspaceService.Get(ctx.Context(), params.WorkspaceID) + if err != nil { + return err + } + + err = c.workspaceService.RemoveMember(ctx.Context(), user.ID(params.UserID), workspace) + if err != nil { + if err == errs.ErrMemberNotExists { + return dto.Ctx(ctx).Error(err).Status(fiber.StatusNotFound).Send() + } + return err + } + + return dto.Ctx(ctx).Success(nil).Send() +} diff --git a/internal/api/http/provider.go b/internal/api/http/provider.go index 0b5424c..063a7ce 100644 --- a/internal/api/http/provider.go +++ b/internal/api/http/provider.go @@ -3,8 +3,8 @@ package http import ( "github.com/gofiber/fiber/v2" "github.com/google/wire" + "leafdev.top/Leaf/leaf-library-3/internal/api/http/controller" "leafdev.top/Leaf/leaf-library-3/internal/api/http/middleware" - v1 "leafdev.top/Leaf/leaf-library-3/internal/api/http/v1" "leafdev.top/Leaf/leaf-library-3/internal/base/conf" "leafdev.top/Leaf/leaf-library-3/internal/base/logger" "leafdev.top/Leaf/leaf-library-3/internal/services/auth" @@ -22,14 +22,21 @@ type Middleware struct { } type Handlers struct { - User *v1.UserController + // User *v1.UserController + Workspace *controller.WorkspaceController + Collection *controller.CollectionController + Document *controller.DocumentController } func NewHandler( - user *v1.UserController, + workspace *controller.WorkspaceController, + collection *controller.CollectionController, + document *controller.DocumentController, ) *Handlers { return &Handlers{ - User: user, + Workspace: workspace, + Collection: collection, + Document: document, } } @@ -51,7 +58,10 @@ var ProviderSet = wire.NewSet( NewMiddleware, // Init Controller - v1.NewUserController, + // controller.NewUserController, + controller.NewWorkspaceController, + controller.NewCollectionController, + controller.NewDocumentController, // Init Handler NewHandler, diff --git a/internal/api/http/v1/test.go b/internal/api/http/v1/test.go deleted file mode 100644 index 7a4dedd..0000000 --- a/internal/api/http/v1/test.go +++ /dev/null @@ -1,56 +0,0 @@ -package v1 - -import ( - "net/http" - - "github.com/gofiber/fiber/v2" - "leafdev.top/Leaf/leaf-library-3/internal/pkg/validator" - "leafdev.top/Leaf/leaf-library-3/internal/services/auth" - "leafdev.top/Leaf/leaf-library-3/internal/types/dto" -) - -type UserController struct { - authService *auth.Service -} - -func NewUserController(authService *auth.Service) *UserController { - return &UserController{authService} -} - -// Test godoc -// @Summary Greet -// @Description 测试接口,将会返回当前用户的信息 -// @Tags ping -// @Accept json -// @Produce json -// @Security ApiKeyAuth -// @deprecated true -// @Success 200 {object} response.Body{data=schema.CurrentUserResponse} -// @Failure 400 {object} response.Body -// @Router /api/v1/ping [get] -func (u *UserController) Test(c *fiber.Ctx) error { - user := u.authService.GetUser(c) - - // bind - var testRequest = &dto.TestRequest{} - err := c.QueryParser(testRequest) - if err != nil { - return err - } - - // 验证 - validationErrors, err := validator.Struct(testRequest) - if err != nil { - return dto.Ctx(c).Error(err).Data(validationErrors).Send() - } - - var currentUserResponse = &dto.CurrentUserResponse{ - IP: c.IP(), - Valid: user.Valid, - UserEmail: user.Token.Email, - UserId: user.Token.Sub, - UserName: user.Token.Name, - } - - return dto.Ctx(c).Status(http.StatusOK).Data(currentUserResponse).Send() -} diff --git a/internal/base/server/error.go b/internal/base/server/error.go index a391491..b899059 100644 --- a/internal/base/server/error.go +++ b/internal/base/server/error.go @@ -39,6 +39,7 @@ func errorConverter(logger *logger.Logger, ctx *fiber.Ctx, err error) error { case errors.Is(err, gorm.ErrRecordNotFound): errorMsg = errs.ErrNotFound + status = http.StatusNotFound default: logger.Sugar.Errorf("fiber error: %s", err) diff --git a/internal/dao/block_chunks.gen.go b/internal/dao/block_chunks.gen.go index 1dd2495..5fce9b7 100644 --- a/internal/dao/block_chunks.gen.go +++ b/internal/dao/block_chunks.gen.go @@ -27,7 +27,7 @@ func newBlockChunk(db *gorm.DB, opts ...gen.DOOption) blockChunk { tableName := _blockChunk.blockChunkDo.TableName() _blockChunk.ALL = field.NewAsterisk(tableName) - _blockChunk.Id = field.NewUint(tableName, "id") + _blockChunk.ID = field.NewUint(tableName, "id") _blockChunk.CreatedAt = field.NewTime(tableName, "created_at") _blockChunk.UpdatedAt = field.NewTime(tableName, "updated_at") _blockChunk.DocumentBlockId = field.NewUint(tableName, "document_block_id") @@ -87,7 +87,7 @@ type blockChunk struct { blockChunkDo ALL field.Asterisk - Id field.Uint + ID field.Uint CreatedAt field.Time UpdatedAt field.Time DocumentBlockId field.Uint @@ -109,7 +109,7 @@ func (b blockChunk) As(alias string) *blockChunk { func (b *blockChunk) updateTableName(table string) *blockChunk { b.ALL = field.NewAsterisk(table) - b.Id = field.NewUint(table, "id") + b.ID = field.NewUint(table, "id") b.CreatedAt = field.NewTime(table, "created_at") b.UpdatedAt = field.NewTime(table, "updated_at") b.DocumentBlockId = field.NewUint(table, "document_block_id") @@ -131,7 +131,7 @@ func (b *blockChunk) GetFieldByName(fieldName string) (field.OrderExpr, bool) { func (b *blockChunk) fillFieldMap() { b.fieldMap = make(map[string]field.Expr, 6) - b.fieldMap["id"] = b.Id + b.fieldMap["id"] = b.ID b.fieldMap["created_at"] = b.CreatedAt b.fieldMap["updated_at"] = b.UpdatedAt b.fieldMap["document_block_id"] = b.DocumentBlockId diff --git a/internal/dao/collections.gen.go b/internal/dao/collections.gen.go index f1f3bc5..033e884 100644 --- a/internal/dao/collections.gen.go +++ b/internal/dao/collections.gen.go @@ -27,7 +27,7 @@ func newCollection(db *gorm.DB, opts ...gen.DOOption) collection { tableName := _collection.collectionDo.TableName() _collection.ALL = field.NewAsterisk(tableName) - _collection.Id = field.NewUint(tableName, "id") + _collection.ID = field.NewUint(tableName, "id") _collection.CreatedAt = field.NewTime(tableName, "created_at") _collection.UpdatedAt = field.NewTime(tableName, "updated_at") _collection.Name = field.NewString(tableName, "name") @@ -48,7 +48,7 @@ type collection struct { collectionDo ALL field.Asterisk - Id field.Uint + ID field.Uint CreatedAt field.Time UpdatedAt field.Time Name field.String @@ -71,7 +71,7 @@ func (c collection) As(alias string) *collection { func (c *collection) updateTableName(table string) *collection { c.ALL = field.NewAsterisk(table) - c.Id = field.NewUint(table, "id") + c.ID = field.NewUint(table, "id") c.CreatedAt = field.NewTime(table, "created_at") c.UpdatedAt = field.NewTime(table, "updated_at") c.Name = field.NewString(table, "name") @@ -94,7 +94,7 @@ func (c *collection) GetFieldByName(fieldName string) (field.OrderExpr, bool) { func (c *collection) fillFieldMap() { c.fieldMap = make(map[string]field.Expr, 7) - c.fieldMap["id"] = c.Id + c.fieldMap["id"] = c.ID c.fieldMap["created_at"] = c.CreatedAt c.fieldMap["updated_at"] = c.UpdatedAt c.fieldMap["name"] = c.Name diff --git a/internal/dao/document_blocks.gen.go b/internal/dao/document_blocks.gen.go index e9bc706..287a76d 100644 --- a/internal/dao/document_blocks.gen.go +++ b/internal/dao/document_blocks.gen.go @@ -27,13 +27,14 @@ func newDocumentBlock(db *gorm.DB, opts ...gen.DOOption) documentBlock { tableName := _documentBlock.documentBlockDo.TableName() _documentBlock.ALL = field.NewAsterisk(tableName) - _documentBlock.Id = field.NewUint(tableName, "id") + _documentBlock.ID = field.NewUint(tableName, "id") _documentBlock.CreatedAt = field.NewTime(tableName, "created_at") _documentBlock.UpdatedAt = field.NewTime(tableName, "updated_at") _documentBlock.DocumentId = field.NewUint(tableName, "document_id") _documentBlock.Type = field.NewString(tableName, "type") _documentBlock.Content = field.NewString(tableName, "content") _documentBlock.Hash = field.NewString(tableName, "hash") + _documentBlock.Order_ = field.NewFloat64(tableName, "order") _documentBlock.Document = documentBlockBelongsToDocument{ db: db.Session(&gorm.Session{}), @@ -72,13 +73,14 @@ type documentBlock struct { documentBlockDo ALL field.Asterisk - Id field.Uint + ID field.Uint CreatedAt field.Time UpdatedAt field.Time DocumentId field.Uint Type field.String Content field.String Hash field.String + Order_ field.Float64 Document documentBlockBelongsToDocument fieldMap map[string]field.Expr @@ -96,13 +98,14 @@ func (d documentBlock) As(alias string) *documentBlock { func (d *documentBlock) updateTableName(table string) *documentBlock { d.ALL = field.NewAsterisk(table) - d.Id = field.NewUint(table, "id") + d.ID = field.NewUint(table, "id") d.CreatedAt = field.NewTime(table, "created_at") d.UpdatedAt = field.NewTime(table, "updated_at") d.DocumentId = field.NewUint(table, "document_id") d.Type = field.NewString(table, "type") d.Content = field.NewString(table, "content") d.Hash = field.NewString(table, "hash") + d.Order_ = field.NewFloat64(table, "order") d.fillFieldMap() @@ -119,14 +122,15 @@ func (d *documentBlock) GetFieldByName(fieldName string) (field.OrderExpr, bool) } func (d *documentBlock) fillFieldMap() { - d.fieldMap = make(map[string]field.Expr, 8) - d.fieldMap["id"] = d.Id + d.fieldMap = make(map[string]field.Expr, 9) + d.fieldMap["id"] = d.ID d.fieldMap["created_at"] = d.CreatedAt d.fieldMap["updated_at"] = d.UpdatedAt d.fieldMap["document_id"] = d.DocumentId d.fieldMap["type"] = d.Type d.fieldMap["content"] = d.Content d.fieldMap["hash"] = d.Hash + d.fieldMap["order"] = d.Order_ } diff --git a/internal/dao/documents.gen.go b/internal/dao/documents.gen.go index e336d53..078e1b9 100644 --- a/internal/dao/documents.gen.go +++ b/internal/dao/documents.gen.go @@ -27,7 +27,7 @@ func newDocument(db *gorm.DB, opts ...gen.DOOption) document { tableName := _document.documentDo.TableName() _document.ALL = field.NewAsterisk(tableName) - _document.Id = field.NewUint(tableName, "id") + _document.ID = field.NewUint(tableName, "id") _document.CreatedAt = field.NewTime(tableName, "created_at") _document.UpdatedAt = field.NewTime(tableName, "updated_at") _document.Name = field.NewString(tableName, "name") @@ -82,7 +82,7 @@ type document struct { documentDo ALL field.Asterisk - Id field.Uint + ID field.Uint CreatedAt field.Time UpdatedAt field.Time Name field.String @@ -111,7 +111,7 @@ func (d document) As(alias string) *document { func (d *document) updateTableName(table string) *document { d.ALL = field.NewAsterisk(table) - d.Id = field.NewUint(table, "id") + d.ID = field.NewUint(table, "id") d.CreatedAt = field.NewTime(table, "created_at") d.UpdatedAt = field.NewTime(table, "updated_at") d.Name = field.NewString(table, "name") @@ -136,7 +136,7 @@ func (d *document) GetFieldByName(fieldName string) (field.OrderExpr, bool) { func (d *document) fillFieldMap() { d.fieldMap = make(map[string]field.Expr, 11) - d.fieldMap["id"] = d.Id + d.fieldMap["id"] = d.ID d.fieldMap["created_at"] = d.CreatedAt d.fieldMap["updated_at"] = d.UpdatedAt d.fieldMap["name"] = d.Name diff --git a/internal/dao/workspace_members.gen.go b/internal/dao/workspace_members.gen.go index 9ae2fbe..f7e988d 100644 --- a/internal/dao/workspace_members.gen.go +++ b/internal/dao/workspace_members.gen.go @@ -27,7 +27,7 @@ func newWorkspaceMember(db *gorm.DB, opts ...gen.DOOption) workspaceMember { tableName := _workspaceMember.workspaceMemberDo.TableName() _workspaceMember.ALL = field.NewAsterisk(tableName) - _workspaceMember.Id = field.NewUint(tableName, "id") + _workspaceMember.ID = field.NewUint(tableName, "id") _workspaceMember.CreatedAt = field.NewTime(tableName, "created_at") _workspaceMember.UpdatedAt = field.NewTime(tableName, "updated_at") _workspaceMember.WorkspaceId = field.NewUint(tableName, "workspace_id") @@ -47,7 +47,7 @@ type workspaceMember struct { workspaceMemberDo ALL field.Asterisk - Id field.Uint + ID field.Uint CreatedAt field.Time UpdatedAt field.Time WorkspaceId field.Uint @@ -69,7 +69,7 @@ func (w workspaceMember) As(alias string) *workspaceMember { func (w *workspaceMember) updateTableName(table string) *workspaceMember { w.ALL = field.NewAsterisk(table) - w.Id = field.NewUint(table, "id") + w.ID = field.NewUint(table, "id") w.CreatedAt = field.NewTime(table, "created_at") w.UpdatedAt = field.NewTime(table, "updated_at") w.WorkspaceId = field.NewUint(table, "workspace_id") @@ -91,7 +91,7 @@ func (w *workspaceMember) GetFieldByName(fieldName string) (field.OrderExpr, boo func (w *workspaceMember) fillFieldMap() { w.fieldMap = make(map[string]field.Expr, 6) - w.fieldMap["id"] = w.Id + w.fieldMap["id"] = w.ID w.fieldMap["created_at"] = w.CreatedAt w.fieldMap["updated_at"] = w.UpdatedAt w.fieldMap["workspace_id"] = w.WorkspaceId diff --git a/internal/dao/workspaces.gen.go b/internal/dao/workspaces.gen.go index 4d4b4af..5008ae4 100644 --- a/internal/dao/workspaces.gen.go +++ b/internal/dao/workspaces.gen.go @@ -27,7 +27,7 @@ func newWorkspace(db *gorm.DB, opts ...gen.DOOption) workspace { tableName := _workspace.workspaceDo.TableName() _workspace.ALL = field.NewAsterisk(tableName) - _workspace.Id = field.NewUint(tableName, "id") + _workspace.ID = field.NewUint(tableName, "id") _workspace.CreatedAt = field.NewTime(tableName, "created_at") _workspace.UpdatedAt = field.NewTime(tableName, "updated_at") _workspace.Name = field.NewString(tableName, "name") @@ -43,7 +43,7 @@ type workspace struct { workspaceDo ALL field.Asterisk - Id field.Uint + ID field.Uint CreatedAt field.Time UpdatedAt field.Time Name field.String @@ -65,7 +65,7 @@ func (w workspace) As(alias string) *workspace { func (w *workspace) updateTableName(table string) *workspace { w.ALL = field.NewAsterisk(table) - w.Id = field.NewUint(table, "id") + w.ID = field.NewUint(table, "id") w.CreatedAt = field.NewTime(table, "created_at") w.UpdatedAt = field.NewTime(table, "updated_at") w.Name = field.NewString(table, "name") @@ -88,7 +88,7 @@ func (w *workspace) GetFieldByName(fieldName string) (field.OrderExpr, bool) { func (w *workspace) fillFieldMap() { w.fieldMap = make(map[string]field.Expr, 6) - w.fieldMap["id"] = w.Id + w.fieldMap["id"] = w.ID w.fieldMap["created_at"] = w.CreatedAt w.fieldMap["updated_at"] = w.UpdatedAt w.fieldMap["name"] = w.Name diff --git a/internal/database/migrations/2_add_parent_id_to_documents.sql b/internal/database/migrations/2_add_parent_id_to_documents.sql index 737d764..a021162 100644 --- a/internal/database/migrations/2_add_parent_id_to_documents.sql +++ b/internal/database/migrations/2_add_parent_id_to_documents.sql @@ -1,6 +1,6 @@ -- +goose Up ALTER TABLE documents - ADD COLUMN parent_id BIGINT REFERENCES documents (id); + ADD COLUMN parent_id BIGINT REFERENCES documents (id) NULL; -- +goose Down ALTER TABLE documents diff --git a/internal/database/migrations/3_add_order_to_document_blocks.sql b/internal/database/migrations/3_add_order_to_document_blocks.sql new file mode 100644 index 0000000..90e84f1 --- /dev/null +++ b/internal/database/migrations/3_add_order_to_document_blocks.sql @@ -0,0 +1,10 @@ +-- +goose Up +ALTER TABLE document_blocks + ADD COLUMN "order" DOUBLE PRECISION NOT NULL DEFAULT 1000.0; + +CREATE INDEX idx_document_blocks_order ON document_blocks ("order"); + +-- +goose Down +DROP INDEX IF EXISTS idx_document_blocks_order; +ALTER TABLE document_blocks + DROP COLUMN "order"; \ No newline at end of file diff --git a/internal/entity/Document.go b/internal/entity/Document.go index fcdc4fe..2919760 100644 --- a/internal/entity/Document.go +++ b/internal/entity/Document.go @@ -16,8 +16,8 @@ type Document struct { CollectionId dto.EntityId `json:"collection_id"` Collection *Collection `json:"collection"` - ParentId dto.EntityId `json:"parent_id"` - Parent *Document `json:"parent"` + ParentId *dto.EntityId `json:"parent_id"` + Parent *Document `json:"parent"` DeletedAt gorm.DeletedAt `json:"deleted_at"` } @@ -32,9 +32,10 @@ type DocumentBlock struct { DocumentId dto.EntityId `json:"document_id"` Document *Document `json:"document"` - Type string `json:"type"` - Content string `json:"content"` - Hash string `json:"hash"` + Type string `json:"type"` + Content string `json:"content"` + Hash string `json:"hash"` + Order float64 `json:"order" gorm:"index"` } func (*DocumentBlock) TableName() string { diff --git a/internal/entity/Model.go b/internal/entity/Model.go index 0e4739a..4532c5d 100644 --- a/internal/entity/Model.go +++ b/internal/entity/Model.go @@ -8,7 +8,7 @@ import ( // Model 是所有 entity 的基类,后期要将所有的 Base 改成这种形式 type Model struct { - Id dto.EntityId `gorm:"primarykey" json:"id"` + ID dto.EntityId `gorm:"primarykey" json:"id"` CreatedAt time.Time `gorm:"autoUpdateTime:milli" json:"created_at"` UpdatedAt time.Time `gorm:"autoUpdateTime:milli" json:"updated_at"` //DeletedAt gorm.DeletedAt `gorm:"index"` diff --git a/internal/entity/Workspace.go b/internal/entity/Workspace.go index 3ad2154..712cb7c 100644 --- a/internal/entity/Workspace.go +++ b/internal/entity/Workspace.go @@ -9,7 +9,7 @@ import ( type Workspace struct { Model Name string `json:"name"` - UserId user.Id `json:"user_id"` + UserId user.ID `json:"user_id"` DeletedAt gorm.DeletedAt `json:"deleted_at"` } @@ -20,7 +20,7 @@ func (*Workspace) TableName() string { type WorkspaceMember struct { Model WorkspaceId dto.EntityId `json:"workspace_id"` - UserId user.Id `json:"user_id"` + UserId user.ID `json:"user_id"` Workspace *Workspace `json:"workspace"` } diff --git a/internal/pkg/validator/validator.go b/internal/pkg/validator/validator.go index 6da6698..d179e2f 100644 --- a/internal/pkg/validator/validator.go +++ b/internal/pkg/validator/validator.go @@ -27,14 +27,14 @@ func init() { }) } -func Struct(data interface{}) (validationErrors *[]dto.ValidateError, err error) { +func Struct(data interface{}) (validationErrors *[]dto.ValidateError, ok bool, err error) { v := validate.Struct(data) var e error var ves []dto.ValidateError if v.Validate() { - return &ves, e // 返回指针 + return &ves, true, nil // 返回指针 } else { e = ErrValidationFailed @@ -60,5 +60,5 @@ func Struct(data interface{}) (validationErrors *[]dto.ValidateError, err error) // //} //} - return &ves, e + return &ves, false, e } diff --git a/internal/router/api.go b/internal/router/api.go index 9a42dd8..0f628c1 100644 --- a/internal/router/api.go +++ b/internal/router/api.go @@ -5,25 +5,15 @@ import ( "leafdev.top/Leaf/leaf-library-3/internal/api/http" ) -// 两种方法都可以 -//type Api struct { -// User *v1.UserController -//} - type Api struct { HttpHandler *http.Handlers Middleware *http.Middleware } func NewApiRoute( - //User *v1.UserController, HttpHandler *http.Handlers, Middleware *http.Middleware, ) *Api { - //return &Api{ - // User, - //} - return &Api{ HttpHandler, Middleware, @@ -31,20 +21,61 @@ func NewApiRoute( } func (a *Api) V1(r fiber.Router) { - auth := r.Group("/api/v1") + auth := r.Group("") { // 要求认证 auth.Use(a.Middleware.Auth.Handler()) - // RoutePermission 为权限验证 - //auth.Get("/ping", a.Middleware.RBAC.RoutePermission(), a.HttpHandler.User.Test) + // 工作空间路由 + workspaces := auth.Group("/workspaces") + { + workspaces.Post("/", a.HttpHandler.Workspace.CreateWorkspace) + workspaces.Get("/", a.HttpHandler.Workspace.ListWorkspaces) + workspaces.Get("/:id", a.HttpHandler.Workspace.GetWorkspace) + workspaces.Delete("/:id", a.HttpHandler.Workspace.DeleteWorkspace) - auth.Get("/ping", a.HttpHandler.User.Test) - } - - guest := r.Group("/api/v1") - { - guest.Get("/guest_ping", a.HttpHandler.User.Test) + // 工作空间成员管理 + workspaces.Post("/:id/members", a.HttpHandler.Workspace.AddMember) + workspaces.Delete("/:id/members/:user_id", a.HttpHandler.Workspace.RemoveMember) + + // 工作空间下的集合 + workspaces.Get("/:workspace_id/collections", a.HttpHandler.Collection.ListCollections) + } + + // 集合路由 + collections := auth.Group("/collections") + { + collections.Post("/", a.HttpHandler.Collection.CreateCollection) + collections.Get("/:id", a.HttpHandler.Collection.GetCollection) + + // 集合下的文档 + collections.Get("/:collection_id/documents", a.HttpHandler.Document.ListDocuments) + } + + // 文档路由 + documents := auth.Group("/documents") + { + documents.Post("/", a.HttpHandler.Document.CreateDocument) + documents.Get("/:id", a.HttpHandler.Document.GetDocument) + documents.Put("/:id", a.HttpHandler.Document.UpdateDocument) + documents.Delete("/:id", a.HttpHandler.Document.DeleteDocument) + + // 文档块 + documents.Post("/:document_id/blocks", a.HttpHandler.Document.CreateBlock) + documents.Get("/:document_id/blocks", a.HttpHandler.Document.ListBlocks) + } + + // 文档块路由 + blocks := auth.Group("/blocks") + { + blocks.Put("/:block_id", a.HttpHandler.Document.UpdateBlock) + blocks.Delete("/:block_id", a.HttpHandler.Document.DeleteBlock) + blocks.Post("/:block_id/move", a.HttpHandler.Document.MoveBlock) + } } + // guest := r.Group("/api/v1") + // { + // guest.Get("/guest_ping", a.HttpHandler.User.Test) + // } } diff --git a/internal/services/auth/auth.go b/internal/services/auth/auth.go index 8ae6281..a6e4438 100644 --- a/internal/services/auth/auth.go +++ b/internal/services/auth/auth.go @@ -27,7 +27,7 @@ func (a *Service) GetUser(ctx *fiber.Ctx) *user.User { userCtx := ctx.Locals(constants.AuthMiddlewareKey) u, ok := userCtx.(*user.User) - u.Id = u.Token.Sub + u.ID = u.Token.Sub if !ok { panic("User context is not valid") @@ -40,7 +40,7 @@ func (a *Service) GetCtx(ctx context.Context) *user.User { userCtx := ctx.Value(constants.AuthMiddlewareKey) u, ok := userCtx.(*user.User) - u.Id = u.Token.Sub + u.ID = u.Token.Sub if !ok { panic("User context is not valid") @@ -53,7 +53,7 @@ func (a *Service) GetUserSafe(ctx *fiber.Ctx) (*user.User, bool) { userCtx := ctx.Locals(constants.AuthMiddlewareKey) u, ok := userCtx.(*user.User) - u.Id = u.Token.Sub + u.ID = u.Token.Sub return u, ok } @@ -62,7 +62,7 @@ func (a *Service) GetCtxSafe(ctx context.Context) (*user.User, bool) { userCtx := ctx.Value(constants.AuthMiddlewareKey) u, ok := userCtx.(*user.User) - u.Id = u.Token.Sub + u.ID = u.Token.Sub return u, ok } @@ -90,7 +90,7 @@ func (a *Service) parseUserJWT(tokenType constants.JwtTokenTypes, jwtToken strin return nil, errs.NotValidToken } - sub = user.Id(subStr) + sub = user.ID(subStr) // 如果 token.Header 中没有 typ if token.Header["typ"] == "" { diff --git a/internal/services/collection/collection.go b/internal/services/collection/collection.go index d264c29..5352b73 100644 --- a/internal/services/collection/collection.go +++ b/internal/services/collection/collection.go @@ -19,11 +19,11 @@ func (s *Service) Get(ctx context.Context, collectionId dto.EntityId) (*entity.C return nil, errs.ErrCollectionNotExists } - return s.dao.WithContext(ctx).Collection.Where(s.dao.Collection.Id.Eq(collectionId.Uint())).First() + return s.dao.WithContext(ctx).Collection.Where(s.dao.Collection.ID.Eq(collectionId.Uint())).First() } func (s *Service) Exists(ctx context.Context, CollectionId dto.EntityId) (bool, error) { - num, err := s.dao.WithContext(ctx).Collection.Where(s.dao.Collection.Id.Eq(CollectionId.Uint())).Count() + num, err := s.dao.WithContext(ctx).Collection.Where(s.dao.Collection.ID.Eq(CollectionId.Uint())).Count() return num > 0, err } @@ -47,59 +47,32 @@ func (s *Service) Create(ctx context.Context, workspaceId dto.EntityId, name str return collection, nil } -// -//func (s *Service) AddMember(ctx context.Context, userId schema.UserId, CollectionEntity *entity.Collection) (*entity.CollectionMember, error) { -// memberExists, err := s.MemberExists(ctx, userId, CollectionEntity) -// if err != nil { -// return nil, err -// } -// -// if memberExists { -// return nil, errs.ErrMemberAlreadyExists -// } -// -// var CollectionMember = &entity.CollectionMember{ -// UserId: userId, -// CollectionId: CollectionEntity.Id, -// } -// -// err = s.dao.WithContext(ctx).CollectionMember.Create(CollectionMember) -// -// return CollectionMember, err -//} -// -//func (s *Service) RemoveMember(ctx context.Context, userId schema.UserId, CollectionEntity *entity.Collection) error { -// memberExists, err := s.MemberExists(ctx, userId, CollectionEntity) -// if err != nil { -// return err -// } -// -// if !memberExists { -// return errs.ErrMemberNotExists -// } -// -// _, err = s.dao.WithContext(ctx).CollectionMember. -// Where( -// s.dao.CollectionMember.UserId.Eq(userId.String()), -// s.dao.CollectionMember.CollectionId.Eq( -// CollectionEntity.Id.Uint()), -// ). -// Delete() -// -// return err -//} -// -//func (s *Service) MemberExists(ctx context.Context, userId schema.UserId, CollectionEntity *entity.Collection) (bool, error) { -// if num, err := s.dao.WithContext(ctx).CollectionMember.Where(s.dao.CollectionMember.UserId.Eq(userId.String()), s.dao.CollectionMember.CollectionId.Eq(CollectionEntity.Id.Uint())).Count(); err != nil { -// return false, err -// } else { -// return num > 0, nil -// } -//} -// -//func (s *Service) DeleteCollection(ctx context.Context, CollectionId dto.EntityId) error { -// // 删除始终是一个复杂的工程,所以先标记为软删除 -// _, err := s.dao.WithContext(ctx).Collection.Where(s.dao.Collection.Id.Eq(CollectionId.Uint())).Delete() -// -// return err -//} +func (s *Service) Update(ctx context.Context, collectionId dto.EntityId, name string) (*entity.Collection, error) { + collection, err := s.Get(ctx, collectionId) + if err != nil { + return nil, err + } + + collection.Name = name + err = s.dao.WithContext(ctx).Collection.Save(collection) + if err != nil { + return nil, err + } + + return collection, nil +} + +func (s *Service) Delete(ctx context.Context, collectionId dto.EntityId) error { + exists, err := s.Exists(ctx, collectionId) + if err != nil { + return err + } + + if !exists { + return errs.ErrCollectionNotExists + } + + // 使用软删除 + _, err = s.dao.WithContext(ctx).Collection.Where(s.dao.Collection.ID.Eq(collectionId.Uint())).Delete() + return err +} diff --git a/internal/services/document/collection.go b/internal/services/document/collection.go deleted file mode 100644 index d264c29..0000000 --- a/internal/services/document/collection.go +++ /dev/null @@ -1,105 +0,0 @@ -package collection - -import ( - "context" - - "leafdev.top/Leaf/leaf-library-3/internal/entity" - "leafdev.top/Leaf/leaf-library-3/internal/types/dto" - "leafdev.top/Leaf/leaf-library-3/internal/types/errs" -) - -func (s *Service) Get(ctx context.Context, collectionId dto.EntityId) (*entity.Collection, error) { - exists, err := s.Exists(ctx, collectionId) - - if err != nil { - return nil, err - } - - if !exists { - return nil, errs.ErrCollectionNotExists - } - - return s.dao.WithContext(ctx).Collection.Where(s.dao.Collection.Id.Eq(collectionId.Uint())).First() -} - -func (s *Service) Exists(ctx context.Context, CollectionId dto.EntityId) (bool, error) { - num, err := s.dao.WithContext(ctx).Collection.Where(s.dao.Collection.Id.Eq(CollectionId.Uint())).Count() - - return num > 0, err -} - -func (s *Service) List(ctx context.Context, workspaceId dto.EntityId) ([]*entity.Collection, error) { - return s.dao.WithContext(ctx).Collection.Where(s.dao.Collection.WorkspaceId.Eq(workspaceId.Uint())).Find() -} - -func (s *Service) Create(ctx context.Context, workspaceId dto.EntityId, name string) (*entity.Collection, error) { - var collection = &entity.Collection{ - Name: name, - WorkspaceId: workspaceId, - } - - err := s.dao.WithContext(ctx).Collection.Create(collection) - - if err != nil { - return nil, err - } - - return collection, nil -} - -// -//func (s *Service) AddMember(ctx context.Context, userId schema.UserId, CollectionEntity *entity.Collection) (*entity.CollectionMember, error) { -// memberExists, err := s.MemberExists(ctx, userId, CollectionEntity) -// if err != nil { -// return nil, err -// } -// -// if memberExists { -// return nil, errs.ErrMemberAlreadyExists -// } -// -// var CollectionMember = &entity.CollectionMember{ -// UserId: userId, -// CollectionId: CollectionEntity.Id, -// } -// -// err = s.dao.WithContext(ctx).CollectionMember.Create(CollectionMember) -// -// return CollectionMember, err -//} -// -//func (s *Service) RemoveMember(ctx context.Context, userId schema.UserId, CollectionEntity *entity.Collection) error { -// memberExists, err := s.MemberExists(ctx, userId, CollectionEntity) -// if err != nil { -// return err -// } -// -// if !memberExists { -// return errs.ErrMemberNotExists -// } -// -// _, err = s.dao.WithContext(ctx).CollectionMember. -// Where( -// s.dao.CollectionMember.UserId.Eq(userId.String()), -// s.dao.CollectionMember.CollectionId.Eq( -// CollectionEntity.Id.Uint()), -// ). -// Delete() -// -// return err -//} -// -//func (s *Service) MemberExists(ctx context.Context, userId schema.UserId, CollectionEntity *entity.Collection) (bool, error) { -// if num, err := s.dao.WithContext(ctx).CollectionMember.Where(s.dao.CollectionMember.UserId.Eq(userId.String()), s.dao.CollectionMember.CollectionId.Eq(CollectionEntity.Id.Uint())).Count(); err != nil { -// return false, err -// } else { -// return num > 0, nil -// } -//} -// -//func (s *Service) DeleteCollection(ctx context.Context, CollectionId dto.EntityId) error { -// // 删除始终是一个复杂的工程,所以先标记为软删除 -// _, err := s.dao.WithContext(ctx).Collection.Where(s.dao.Collection.Id.Eq(CollectionId.Uint())).Delete() -// -// return err -//} diff --git a/internal/services/document/document.go b/internal/services/document/document.go new file mode 100644 index 0000000..8bb7faa --- /dev/null +++ b/internal/services/document/document.go @@ -0,0 +1,256 @@ +package document + +import ( + "context" + "crypto/sha256" + "encoding/hex" + "fmt" + "time" + + "gorm.io/gorm" + "leafdev.top/Leaf/leaf-library-3/internal/entity" + "leafdev.top/Leaf/leaf-library-3/internal/types/dto" +) + +// Get 获取文档 +func (s *Service) Get(ctx context.Context, id dto.EntityId) (*entity.Document, error) { + return s.dao.Document.Where(s.dao.Document.ID.Eq(uint(id))).First() +} + +// Create 创建文档 +func (s *Service) Create(ctx context.Context, name string, workspaceId, collectionId dto.EntityId, parentId *dto.EntityId) (*entity.Document, error) { + doc := &entity.Document{ + Name: name, + WorkspaceId: workspaceId, + CollectionId: collectionId, + ParentId: parentId, + } + + err := s.dao.Document.Create(doc) + return doc, err +} + +// Update 更新文档 +func (s *Service) Update(ctx context.Context, id dto.EntityId, name string) (*entity.Document, error) { + doc, err := s.Get(ctx, id) + if err != nil { + return nil, err + } + + doc.Name = name + err = s.dao.Document.Save(doc) + return doc, err +} + +// Delete 删除文档 +func (s *Service) Delete(ctx context.Context, id dto.EntityId) error { + _, err := s.dao.Document.Where(s.dao.Document.ID.Eq(uint(id))).Delete() + return err +} + +// ListDocuments 列出文档树 +func (s *Service) ListDocuments(ctx context.Context, collectionId dto.EntityId, parentId *dto.EntityId) ([]*entity.Document, error) { + q := s.dao.WithContext(ctx).Document. + Where(s.dao.Document.CollectionId.Eq(collectionId.Uint())). + Order(s.dao.Document.CreatedAt) + + if parentId == nil { + // 获取根文档 + q = q.Where(s.dao.Document.ParentId.IsNull()) + } else { + // 获取指定父文档下的子文档 + q = q.Where(s.dao.Document.ParentId.Eq(parentId.Uint())) + } + + return q.Find() +} + +// generateBlockHash 生成块的哈希值 +func (s *Service) generateBlockHash(documentId dto.EntityId, blockType string, content string, timestamp time.Time) string { + // 组合所有相关信息 + data := fmt.Sprintf("%d:%s:%s:%d", documentId, blockType, content, timestamp.UnixNano()) + + // 计算 SHA-256 摘要 + hash := sha256.Sum256([]byte(data)) + + // 转换为十六进制字符串 + return hex.EncodeToString(hash[:]) +} + +// CreateBlock 创建文档块 +func (s *Service) CreateBlock(ctx context.Context, documentId dto.EntityId, blockType string, content string, afterBlockId *dto.EntityId) (*entity.DocumentBlock, error) { + // 生成块的哈希值 + now := time.Now() + hash := s.generateBlockHash(documentId, blockType, content, now) + + var order float64 + if afterBlockId == nil { + order = 1000.0 + } else { + // 获取前一个块 + prevBlock, err := s.dao.WithContext(ctx).DocumentBlock.Where(s.dao.DocumentBlock.ID.Eq(afterBlockId.Uint())).First() + if err != nil { + return nil, err + } + + // 获取前一个块后面的第一个块 + nextBlock, err := s.dao.WithContext(ctx).DocumentBlock. + Where(s.dao.DocumentBlock.DocumentId.Eq(uint(documentId))). + Where(s.dao.DocumentBlock.Order_.Gt(prevBlock.Order)). + Order(s.dao.DocumentBlock.Order_). + First() + + if err == gorm.ErrRecordNotFound { + // 如果没有后续块,则在前一个块的序号上加 1000 + order = prevBlock.Order + 1000.0 + } else if err != nil { + return nil, err + } else { + // 如果有后续块,则取前一个块和后一个块序号的中间值 + order = (prevBlock.Order + nextBlock.Order) / 2 + } + } + + block := &entity.DocumentBlock{ + DocumentId: documentId, + Type: blockType, + Content: content, + Order: order, + Hash: hash, + } + + err := s.dao.WithContext(ctx).DocumentBlock.Create(block) + return block, err +} + +// UpdateBlock 更新文档块 +func (s *Service) UpdateBlock(ctx context.Context, blockId dto.EntityId, content string) (*entity.DocumentBlock, error) { + block, err := s.dao.WithContext(ctx).DocumentBlock.Where(s.dao.DocumentBlock.ID.Eq(blockId.Uint())).First() + if err != nil { + return nil, err + } + + // 更新内容和哈希 + now := time.Now() + block.Content = content + block.Hash = s.generateBlockHash(block.DocumentId, block.Type, content, now) + err = s.dao.WithContext(ctx).DocumentBlock.Save(block) + return block, err +} + +// DeleteBlock 删除文档块 +func (s *Service) DeleteBlock(ctx context.Context, blockId dto.EntityId) error { + _, err := s.dao.WithContext(ctx).DocumentBlock.Where(s.dao.DocumentBlock.ID.Eq(blockId.Uint())).Delete() + return err +} + +// ListBlocks 列出文档的所有块 +func (s *Service) ListBlocks(ctx context.Context, documentId dto.EntityId) ([]*entity.DocumentBlock, error) { + return s.dao.WithContext(ctx).DocumentBlock. + Where(s.dao.DocumentBlock.DocumentId.Eq(uint(documentId))). + Order(s.dao.DocumentBlock.Order_). + Find() +} + +// MoveBlock 移动文档块 +func (s *Service) MoveBlock(ctx context.Context, blockId dto.EntityId, afterBlockId *dto.EntityId) error { + block, err := s.dao.WithContext(ctx).DocumentBlock.Where(s.dao.DocumentBlock.ID.Eq(blockId.Uint())).First() + if err != nil { + return err + } + + var newOrder float64 + if afterBlockId == nil { + newOrder = 1000.0 + } else { + // 获取目标位置的前一个块 + prevBlock, err := s.dao.WithContext(ctx).DocumentBlock.Where(s.dao.DocumentBlock.ID.Eq(afterBlockId.Uint())).First() + if err != nil { + return err + } + + // 获取目标位置的后一个块 + nextBlock, err := s.dao.WithContext(ctx).DocumentBlock. + Where(s.dao.DocumentBlock.DocumentId.Eq(uint(block.DocumentId))). + Where(s.dao.DocumentBlock.Order_.Gt(prevBlock.Order)). + Order(s.dao.DocumentBlock.Order_). + First() + + if err == gorm.ErrRecordNotFound { + newOrder = prevBlock.Order + 1000.0 + } else if err != nil { + return err + } else { + newOrder = (prevBlock.Order + nextBlock.Order) / 2 + } + } + + block.Order = newOrder + // 移动后重新生成 Hash + now := time.Now() + block.Hash = s.generateBlockHash(block.DocumentId, block.Type, block.Content, now) + return s.dao.WithContext(ctx).DocumentBlock.Save(block) +} + +// ReorderBlocks 重新排序文档块 +func (s *Service) ReorderBlocks(ctx context.Context, documentId dto.EntityId) error { + blocks, err := s.ListBlocks(ctx, documentId) + if err != nil { + return err + } + + // 重新设置每个块的顺序 + for i, block := range blocks { + block.Order = float64((i + 1) * 1000) + // 重新生成 Hash + now := time.Now() + block.Hash = s.generateBlockHash(block.DocumentId, block.Type, block.Content, now) + if err := s.dao.WithContext(ctx).DocumentBlock.Save(block); err != nil { + return err + } + } + + return nil +} + +// HasChildren 检查文档是否有子文档 +func (s *Service) HasChildren(ctx context.Context, id dto.EntityId) (bool, error) { + count, err := s.dao.Document.Where(s.dao.Document.ParentId.Eq(id.Uint())).Count() + if err != nil { + return false, err + } + return count > 0, nil +} + +// ToDTO 将文档实体转换为 DTO +func (s *Service) ToDTO(ctx context.Context, doc *entity.Document) (*dto.DocumentDTO, error) { + hasChildren, err := s.HasChildren(ctx, doc.ID) + if err != nil { + return nil, err + } + + return &dto.DocumentDTO{ + BaseDTO: dto.BaseDTO{ + ID: doc.ID, + CreatedAt: doc.CreatedAt, + UpdatedAt: doc.UpdatedAt, + }, + Name: doc.Name, + WorkspaceID: doc.WorkspaceId, + CollectionID: doc.CollectionId, + ParentID: doc.ParentId, + HasChildren: hasChildren, + }, nil +} + +// List 列出文档 +func (s *Service) List(ctx context.Context, collectionId dto.EntityId, parentId *dto.EntityId) ([]*entity.Document, error) { + q := s.dao.Document.Where(s.dao.Document.CollectionId.Eq(uint(collectionId))) + if parentId != nil { + q = q.Where(s.dao.Document.ParentId.Eq(uint(*parentId))) + } else { + q = q.Where(s.dao.Document.ParentId.IsNull()) + } + + return q.Find() +} diff --git a/internal/services/document/provider.go b/internal/services/document/provider.go index 3ad6fe7..f682f93 100644 --- a/internal/services/document/provider.go +++ b/internal/services/document/provider.go @@ -1,4 +1,4 @@ -package collection +package document import ( "leafdev.top/Leaf/leaf-library-3/internal/base/conf" diff --git a/internal/services/provider.go b/internal/services/provider.go index 5c6d34a..679968b 100644 --- a/internal/services/provider.go +++ b/internal/services/provider.go @@ -3,23 +3,32 @@ package services import ( "leafdev.top/Leaf/leaf-library-3/internal/base/logger" "leafdev.top/Leaf/leaf-library-3/internal/services/auth" + "leafdev.top/Leaf/leaf-library-3/internal/services/collection" + "leafdev.top/Leaf/leaf-library-3/internal/services/document" "leafdev.top/Leaf/leaf-library-3/internal/services/jwks" "leafdev.top/Leaf/leaf-library-3/internal/services/stream" + "leafdev.top/Leaf/leaf-library-3/internal/services/workspace" "github.com/google/wire" ) type Service struct { - logger *logger.Logger - Jwks *jwks.JWKS - Auth *auth.Service - Stream *stream.Service + logger *logger.Logger + Jwks *jwks.JWKS + Auth *auth.Service + Stream *stream.Service + Workspace *workspace.Service + Collection *collection.Service + Document *document.Service } var Provide = wire.NewSet( jwks.NewJWKS, auth.NewService, stream.NewService, + workspace.NewService, + collection.NewService, + document.NewService, NewService, ) @@ -28,11 +37,17 @@ func NewService( jwks *jwks.JWKS, auth *auth.Service, stream *stream.Service, + workspace *workspace.Service, + collection *collection.Service, + document *document.Service, ) *Service { return &Service{ logger, jwks, auth, stream, + workspace, + collection, + document, } } diff --git a/internal/services/workspace/workspace.go b/internal/services/workspace/workspace.go index 2abfd91..843e428 100644 --- a/internal/services/workspace/workspace.go +++ b/internal/services/workspace/workspace.go @@ -20,20 +20,20 @@ func (s *Service) Get(ctx context.Context, workspaceId dto.EntityId) (*entity.Wo return nil, errs.ErrWorkspaceNotExists } - return s.dao.WithContext(ctx).Workspace.Where(s.dao.Workspace.Id.Eq(workspaceId.Uint())).First() + return s.dao.WithContext(ctx).Workspace.Where(s.dao.Workspace.ID.Eq(workspaceId.Uint())).First() } func (s *Service) Exists(ctx context.Context, workspaceId dto.EntityId) (bool, error) { - num, err := s.dao.WithContext(ctx).Workspace.Where(s.dao.Workspace.Id.Eq(workspaceId.Uint())).Count() + num, err := s.dao.WithContext(ctx).Workspace.Where(s.dao.Workspace.ID.Eq(workspaceId.Uint())).Count() return num > 0, err } -func (s *Service) List(ctx context.Context, userId user.Id) ([]*entity.Workspace, error) { +func (s *Service) List(ctx context.Context, userId user.ID) ([]*entity.Workspace, error) { return s.dao.WithContext(ctx).Workspace.Where(s.dao.Workspace.UserId.Eq(userId.String())).Find() } -func (s *Service) Create(ctx context.Context, userId user.Id, name string) (*entity.Workspace, error) { +func (s *Service) Create(ctx context.Context, userId user.ID, name string) (*entity.Workspace, error) { var workspace = &entity.Workspace{ Name: name, UserId: userId, @@ -54,7 +54,7 @@ func (s *Service) Create(ctx context.Context, userId user.Id, name string) (*ent return workspace, nil } -func (s *Service) AddMember(ctx context.Context, userId user.Id, workspaceEntity *entity.Workspace) (*entity.WorkspaceMember, error) { +func (s *Service) AddMember(ctx context.Context, userId user.ID, workspaceEntity *entity.Workspace) (*entity.WorkspaceMember, error) { memberExists, err := s.MemberExists(ctx, userId, workspaceEntity) if err != nil { return nil, err @@ -66,7 +66,7 @@ func (s *Service) AddMember(ctx context.Context, userId user.Id, workspaceEntity var workspaceMember = &entity.WorkspaceMember{ UserId: userId, - WorkspaceId: workspaceEntity.Id, + WorkspaceId: workspaceEntity.ID, } err = s.dao.WithContext(ctx).WorkspaceMember.Create(workspaceMember) @@ -74,7 +74,7 @@ func (s *Service) AddMember(ctx context.Context, userId user.Id, workspaceEntity return workspaceMember, err } -func (s *Service) RemoveMember(ctx context.Context, userId user.Id, workspaceEntity *entity.Workspace) error { +func (s *Service) RemoveMember(ctx context.Context, userId user.ID, workspaceEntity *entity.Workspace) error { memberExists, err := s.MemberExists(ctx, userId, workspaceEntity) if err != nil { return err @@ -88,15 +88,15 @@ func (s *Service) RemoveMember(ctx context.Context, userId user.Id, workspaceEnt Where( s.dao.WorkspaceMember.UserId.Eq(userId.String()), s.dao.WorkspaceMember.WorkspaceId.Eq( - workspaceEntity.Id.Uint()), + workspaceEntity.ID.Uint()), ). Delete() return err } -func (s *Service) MemberExists(ctx context.Context, userId user.Id, workspaceEntity *entity.Workspace) (bool, error) { - if num, err := s.dao.WithContext(ctx).WorkspaceMember.Where(s.dao.WorkspaceMember.UserId.Eq(userId.String()), s.dao.WorkspaceMember.WorkspaceId.Eq(workspaceEntity.Id.Uint())).Count(); err != nil { +func (s *Service) MemberExists(ctx context.Context, userId user.ID, workspaceEntity *entity.Workspace) (bool, error) { + if num, err := s.dao.WithContext(ctx).WorkspaceMember.Where(s.dao.WorkspaceMember.UserId.Eq(userId.String()), s.dao.WorkspaceMember.WorkspaceId.Eq(workspaceEntity.ID.Uint())).Count(); err != nil { return false, err } else { return num > 0, nil @@ -105,7 +105,12 @@ func (s *Service) MemberExists(ctx context.Context, userId user.Id, workspaceEnt func (s *Service) DeleteWorkspace(ctx context.Context, workspaceId dto.EntityId) error { // 删除始终是一个复杂的工程,所以先标记为软删除 - _, err := s.dao.WithContext(ctx).Workspace.Where(s.dao.Workspace.Id.Eq(workspaceId.Uint())).Delete() + _, err := s.dao.WithContext(ctx).Workspace.Where(s.dao.Workspace.ID.Eq(workspaceId.Uint())).Delete() return err } + +// IsMember 检查用户是否是工作空间的成员 +func (s *Service) IsMember(ctx context.Context, userId user.ID, workspace *entity.Workspace) (bool, error) { + return s.MemberExists(ctx, userId, workspace) +} diff --git a/internal/types/dto/base.go b/internal/types/dto/base.go new file mode 100644 index 0000000..1453b81 --- /dev/null +++ b/internal/types/dto/base.go @@ -0,0 +1,10 @@ +package dto + +import "time" + +// BaseDTO 基础数据传输对象 +type BaseDTO struct { + ID EntityId `json:"id"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} diff --git a/internal/types/dto/collection.go b/internal/types/dto/collection.go new file mode 100644 index 0000000..9fd0342 --- /dev/null +++ b/internal/types/dto/collection.go @@ -0,0 +1,22 @@ +package dto + +// CreateCollectionRequest 创建集合请求 +type CreateCollectionRequest struct { + Name string `json:"name" validate:"required|minLen:1"` + WorkspaceID EntityId `json:"workspace_id" validate:"required"` +} + +// GetCollectionRequest 获取集合请求 +type GetCollectionRequest struct { + ID EntityId `params:"id"` +} + +// DeleteCollectionRequest 删除集合请求 +type DeleteCollectionRequest struct { + ID EntityId `params:"id"` +} + +// ListCollectionsRequest 列出工作空间下的集合请求 +type ListCollectionsRequest struct { + WorkspaceID EntityId `params:"workspace_id"` +} diff --git a/internal/types/dto/document.go b/internal/types/dto/document.go new file mode 100644 index 0000000..201c0aa --- /dev/null +++ b/internal/types/dto/document.go @@ -0,0 +1,71 @@ +package dto + +// CreateDocumentRequest 创建文档请求 +type CreateDocumentRequest struct { + Name string `json:"name" validate:"required|minLen:1"` + WorkspaceID EntityId `json:"workspace_id" validate:"required"` + CollectionID EntityId `json:"collection_id" validate:"required"` + ParentID *EntityId `json:"parent_id,omitempty"` +} + +// GetDocumentRequest 获取文档请求 +type GetDocumentRequest struct { + ID EntityId `params:"id"` +} + +// DeleteDocumentRequest 删除文档请求 +type DeleteDocumentRequest struct { + ID EntityId `params:"id"` +} + +// UpdateDocumentRequest 更新文档请求 +type UpdateDocumentRequest struct { + Name string `json:"name" validate:"required|minLen:1"` +} + +// ListDocumentsPathParam 列出文档的路径参数 +type ListDocumentsPathParam struct { + CollectionID EntityId `params:"collection_id"` +} + +// ListDocumentsQueryParam 列出文档的查询参数 +type ListDocumentsQueryParam struct { + ParentID *EntityId `query:"parent_id,omitempty"` +} + +// CreateBlockRequest 创建文档块请求 +type CreateBlockRequest struct { + Type string `json:"type" validate:"required"` + Content string `json:"content" validate:"required"` + AfterBlockID *EntityId `json:"after_block_id,omitempty"` +} + +// GetBlockRequest 获取文档块请求 +type GetBlockRequest struct { + ID EntityId `params:"block_id"` +} + +// UpdateBlockRequest 更新文档块请求 +type UpdateBlockRequest struct { + Content string `json:"content" validate:"required"` +} + +// MoveBlockRequest 移动文档块请求 +type MoveBlockRequest struct { + AfterBlockID *EntityId `json:"after_block_id,omitempty"` +} + +// ListBlocksRequest 列出文档下的块请求 +type ListBlocksRequest struct { + DocumentID EntityId `params:"document_id"` +} + +// DocumentDTO 文档数据传输对象 +type DocumentDTO struct { + BaseDTO + Name string `json:"name"` + WorkspaceID EntityId `json:"workspace_id"` + CollectionID EntityId `json:"collection_id"` + ParentID *EntityId `json:"parent_id,omitempty"` + HasChildren bool `json:"has_children"` +} diff --git a/internal/types/dto/dto.go b/internal/types/dto/dto.go index d2f5c4e..513a12f 100644 --- a/internal/types/dto/dto.go +++ b/internal/types/dto/dto.go @@ -1,8 +1,9 @@ package dto import ( - "github.com/gofiber/fiber/v2" "net/http" + + "github.com/gofiber/fiber/v2" ) type IError interface { @@ -23,14 +24,14 @@ type Body struct { Wrap bool `json:"-"` } -type HttpResponse struct { +type Response struct { body *Body httpStatus int ctx *fiber.Ctx } -func Ctx(c *fiber.Ctx) *HttpResponse { - return &HttpResponse{ +func Ctx(c *fiber.Ctx) *Response { + return &Response{ body: &Body{ Wrap: true, }, @@ -39,7 +40,7 @@ func Ctx(c *fiber.Ctx) *HttpResponse { } } -func (r *HttpResponse) Message(message string) *HttpResponse { +func (r *Response) Message(message string) *Response { r.body.Message = message if r.httpStatus == 0 { @@ -50,24 +51,24 @@ func (r *HttpResponse) Message(message string) *HttpResponse { } // WithoutWrap 将不在 body 中包裹 data -func (r *HttpResponse) WithoutWrap() *HttpResponse { +func (r *Response) WithoutWrap() *Response { r.body.Wrap = false return r } -func (r *HttpResponse) Wrap() *HttpResponse { +func (r *Response) Wrap() *Response { r.body.Wrap = true return r } -func (r *HttpResponse) Data(data any) *HttpResponse { +func (r *Response) Data(data any) *Response { r.body.Data = data return r } -func (r *HttpResponse) Error(err IError) *HttpResponse { +func (r *Response) Error(err IError) *Response { if err != nil { r.body.Error = err.Error() @@ -86,13 +87,13 @@ func (r *HttpResponse) Error(err IError) *HttpResponse { } -func (r *HttpResponse) Status(status int) *HttpResponse { +func (r *Response) Status(status int) *Response { r.httpStatus = status return r } -func (r *HttpResponse) Send() error { +func (r *Response) Send() error { if r.httpStatus == 0 { r.httpStatus = http.StatusOK } @@ -114,7 +115,18 @@ func (r *HttpResponse) Send() error { return r.ctx.Status(r.httpStatus).JSON(r.body.Data) } -//func (r *HttpResponse) ValidationError(validationErrors *[]ValidateError) *HttpResponse { +func (r *Response) Success(data any) *Response { + r.body.Data = data + r.body.Success = true + + if r.httpStatus == 0 { + r.httpStatus = http.StatusOK + } + + return r +} + +//func (r *Response) ValidationError(validationErrors *[]ValidateError) *Response { // if validationErrors == nil || len(*validationErrors) == 0 { // } // diff --git a/internal/types/dto/params.go b/internal/types/dto/params.go new file mode 100644 index 0000000..ac1a325 --- /dev/null +++ b/internal/types/dto/params.go @@ -0,0 +1,27 @@ +package dto + +// WorkspaceIDParam 工作空间 ID 参数 +type WorkspaceIDParam struct { + ID EntityId `params:"id"` +} + +// WorkspaceMemberParam 工作空间成员参数 +type WorkspaceMemberParam struct { + WorkspaceID EntityId `params:"id"` + UserID string `params:"user_id"` +} + +// CollectionIDParam 集合 ID 参数 +type CollectionIDParam struct { + ID EntityId `params:"id"` +} + +// DocumentIDParam 文档 ID 参数 +type DocumentIDParam struct { + ID EntityId `params:"id"` +} + +// BlockIDParam 文档块 ID 参数 +type BlockIDParam struct { + ID EntityId `params:"block_id"` +} diff --git a/internal/types/dto/user_response.go b/internal/types/dto/user_response.go index 5cab9ee..1ef510e 100644 --- a/internal/types/dto/user_response.go +++ b/internal/types/dto/user_response.go @@ -8,6 +8,6 @@ type CurrentUserResponse struct { IP string `json:"ip"` Valid bool `json:"valid"` UserEmail string `json:"userEmail"` - UserId user.Id `json:"userId"` + UserId user.ID `json:"userId"` UserName string `json:"userName"` } diff --git a/internal/types/dto/workspace.go b/internal/types/dto/workspace.go new file mode 100644 index 0000000..4835bdc --- /dev/null +++ b/internal/types/dto/workspace.go @@ -0,0 +1,29 @@ +package dto + +import "leafdev.top/Leaf/leaf-library-3/internal/types/user" + +// CreateWorkspaceRequest 创建工作空间请求 +type CreateWorkspaceRequest struct { + Name string `json:"name" validate:"required|minLen:1"` +} + +// GetWorkspaceRequest 获取工作空间请求 +type GetWorkspaceRequest struct { + ID EntityId `params:"id"` +} + +// DeleteWorkspaceRequest 删除工作空间请求 +type DeleteWorkspaceRequest struct { + ID EntityId `params:"id"` +} + +// AddWorkspaceMemberRequest 添加工作空间成员请求 +type AddWorkspaceMemberRequest struct { + UserID user.ID `json:"user_id" validate:"required"` +} + +// RemoveWorkspaceMemberRequest 移除工作空间成员请求 +type RemoveWorkspaceMemberRequest struct { + WorkspaceID EntityId `params:"id"` + UserID string `params:"user_id"` +} diff --git a/internal/types/errs/model.go b/internal/types/errs/model.go index b55c2bd..4304b87 100644 --- a/internal/types/errs/model.go +++ b/internal/types/errs/model.go @@ -3,6 +3,9 @@ package errs import "errors" var ( - ErrPageNotFound = errors.New("page not found") - ErrNotFound = errors.New("not found") + ErrPageNotFound = errors.New("page not found") + ErrNotFound = errors.New("not found") + ErrNoPermission = errors.New("no permission") + ErrDocumentNotExists = errors.New("document not exists") + ErrInvalidParentDocument = errors.New("invalid parent document") ) diff --git a/internal/types/user/user.go b/internal/types/user/user.go index da7f19d..e8ff629 100644 --- a/internal/types/user/user.go +++ b/internal/types/user/user.go @@ -6,14 +6,14 @@ import ( ) // AnonymousUser 调试模式下的用户 -const AnonymousUser Id = "anonymous" +const AnonymousUser ID = "anonymous" type Token struct { Aud string `json:"aud"` Iss string `json:"iss"` Iat float64 `json:"iat"` Exp float64 `json:"exp"` - Sub Id `json:"sub" mapstructure:"-"` + Sub ID `json:"sub" mapstructure:"-"` Scopes []string `json:"scopes"` Roles []Role `json:"roles,omitempty"` Permissions []Permission `json:"permissions"` @@ -30,7 +30,7 @@ type Token struct { type User struct { Token Token - Id Id + ID ID Valid bool } @@ -42,9 +42,9 @@ func (r Role) String() string { type Permission string -type Id string +type ID string -func (u *User) GetId() Id { +func (u *User) GetId() ID { return u.Token.Sub } @@ -108,7 +108,7 @@ func (u *User) HasPermissions(permissions ...Permission) bool { return true } -func (u Id) String() string { +func (u ID) String() string { return string(u) }