This commit is contained in:
ivamp 2024-12-07 02:44:32 +08:00
parent d9ccbf13f7
commit c30ce8f0dd
40 changed files with 5288 additions and 407 deletions

View File

@ -13,7 +13,7 @@ import (
"leafdev.top/Leaf/leaf-library-3/internal/api/grpc/interceptor" "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/grpc/v1/documents"
"leafdev.top/Leaf/leaf-library-3/internal/api/http" "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"
"leafdev.top/Leaf/leaf-library-3/internal/base/conf" "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/base/logger"
@ -27,8 +27,11 @@ import (
"leafdev.top/Leaf/leaf-library-3/internal/router" "leafdev.top/Leaf/leaf-library-3/internal/router"
"leafdev.top/Leaf/leaf-library-3/internal/services" "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/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/jwks"
"leafdev.top/Leaf/leaf-library-3/internal/services/stream" "leafdev.top/Leaf/leaf-library-3/internal/services/stream"
"leafdev.top/Leaf/leaf-library-3/internal/services/workspace"
) )
// Injectors from wire.go: // Injectors from wire.go:
@ -36,24 +39,29 @@ import (
func CreateApp() (*base.Application, error) { func CreateApp() (*base.Application, error) {
config := conf.NewConfig() config := conf.NewConfig()
loggerLogger := logger.NewZapLogger(config) loggerLogger := logger.NewZapLogger(config)
db := orm.NewGORM(config, loggerLogger)
query := dao.NewQuery(db)
service := workspace.NewService(config, query)
jwksJWKS := jwks.NewJWKS(config, loggerLogger) jwksJWKS := jwks.NewJWKS(config, loggerLogger)
service := auth.NewService(config, jwksJWKS, loggerLogger) authService := auth.NewService(config, jwksJWKS, loggerLogger)
userController := v1.NewUserController(service) workspaceController := controller.NewWorkspaceController(service, authService)
handlers := http.NewHandler(userController) collectionService := collection.NewService(config, query)
middleware := http.NewMiddleware(config, loggerLogger, service) 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) routerApi := router.NewApiRoute(handlers, middleware)
swaggerRouter := router.NewSwaggerRoute() swaggerRouter := router.NewSwaggerRoute()
httpServer := server.NewHTTPServer(config, routerApi, swaggerRouter, middleware, loggerLogger) httpServer := server.NewHTTPServer(config, routerApi, swaggerRouter, middleware, loggerLogger)
db := orm.NewGORM(config, loggerLogger)
query := dao.NewQuery(db)
handler := documents.NewHandler(query) handler := documents.NewHandler(query)
interceptorAuth := interceptor.NewAuth(service, loggerLogger, config) interceptorAuth := interceptor.NewAuth(authService, loggerLogger, config)
interceptorLogger := interceptor.NewLogger(loggerLogger) interceptorLogger := interceptor.NewLogger(loggerLogger)
grpcInterceptor := grpc.NewInterceptor(interceptorAuth, interceptorLogger) grpcInterceptor := grpc.NewInterceptor(interceptorAuth, interceptorLogger)
grpcHandlers := grpc.NewHandler(handler, grpcInterceptor) grpcHandlers := grpc.NewHandler(handler, grpcInterceptor)
apiApi := api.NewApi(grpcHandlers, handlers) apiApi := api.NewApi(grpcHandlers, handlers)
streamService := stream.NewService(config) 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) redisRedis := redis.NewRedis(config)
batchBatch := batch.NewBatch(loggerLogger) batchBatch := batch.NewBatch(loggerLogger)
s3S3 := s3.NewS3(config) s3S3 := s3.NewS3(config)

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,38 +1,218 @@
definitions: definitions:
response.Body: dto.AddWorkspaceMemberRequest:
properties: properties:
data: {} user_id:
error: $ref: '#/definitions/user.ID'
required:
- user_id
type: object
dto.CreateBlockRequest:
properties:
after_block_id:
type: integer
content:
type: string 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: message:
type: string type: string
success:
type: boolean
type: object type: object
schema.CurrentUserResponse: entity.Document:
properties: properties:
ip: collection:
$ref: '#/definitions/leafdev_top_Leaf_leaf-library-3_internal_entity.Collection'
collection_id:
type: integer
created_at:
type: string type: string
userEmail: deleted_at:
$ref: '#/definitions/gorm.DeletedAt'
id:
type: integer
name:
type: string type: string
userId: parent:
$ref: '#/definitions/entity.Document'
parent_id:
type: integer
updated_at:
type: string 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 type: string
valid: valid:
description: Valid is true if Time is not NULL
type: boolean type: boolean
type: object 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: info:
contact: {} contact: {}
title: API Docs title: API Docs
version: "1.0" version: "1.0"
paths: paths:
/api/v1/ping: /collections:
get: post:
consumes: consumes:
- application/json - application/json
deprecated: true description: 在工作空间下创建新的集合
description: 测试接口,将会返回当前用户的信息 parameters:
- description: 创建集合请求
in: body
name: request
required: true
schema:
$ref: '#/definitions/dto.CreateCollectionRequest'
produces: produces:
- application/json - application/json
responses: responses:
@ -40,20 +220,705 @@ paths:
description: OK description: OK
schema: schema:
allOf: allOf:
- $ref: '#/definitions/response.Body' - $ref: '#/definitions/dto.Response'
- properties: - properties:
data: data:
$ref: '#/definitions/schema.CurrentUserResponse' $ref: '#/definitions/leafdev_top_Leaf_leaf-library-3_internal_entity.Collection'
type: object type: object
"400": "400":
description: Bad Request description: 参数验证失败
schema: schema:
$ref: '#/definitions/response.Body' allOf:
security: - $ref: '#/definitions/dto.Response'
- ApiKeyAuth: [] - properties:
summary: Greet data:
items:
$ref: '#/definitions/dto.ValidateError'
type: array
type: object
"404":
description: 工作空间不存在
schema:
$ref: '#/definitions/dto.Response'
summary: 创建集合
tags: 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: securityDefinitions:
ApiKeyAuth: ApiKeyAuth:
in: header in: header

1
go.sum
View File

@ -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.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 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 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/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.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=

View File

@ -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()
}

View File

@ -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(&params); 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(&params); 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(&params); 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(&params); 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(&params); 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(&params); 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(&params); 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(&params); 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()
}

View File

@ -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(&params); 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(&params); 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(&params); 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(&params); 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()
}

View File

@ -3,8 +3,8 @@ package http
import ( import (
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
"github.com/google/wire" "github.com/google/wire"
"leafdev.top/Leaf/leaf-library-3/internal/api/http/controller"
"leafdev.top/Leaf/leaf-library-3/internal/api/http/middleware" "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/conf"
"leafdev.top/Leaf/leaf-library-3/internal/base/logger" "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/auth"
@ -22,14 +22,21 @@ type Middleware struct {
} }
type Handlers struct { type Handlers struct {
User *v1.UserController // User *v1.UserController
Workspace *controller.WorkspaceController
Collection *controller.CollectionController
Document *controller.DocumentController
} }
func NewHandler( func NewHandler(
user *v1.UserController, workspace *controller.WorkspaceController,
collection *controller.CollectionController,
document *controller.DocumentController,
) *Handlers { ) *Handlers {
return &Handlers{ return &Handlers{
User: user, Workspace: workspace,
Collection: collection,
Document: document,
} }
} }
@ -51,7 +58,10 @@ var ProviderSet = wire.NewSet(
NewMiddleware, NewMiddleware,
// Init Controller // Init Controller
v1.NewUserController, // controller.NewUserController,
controller.NewWorkspaceController,
controller.NewCollectionController,
controller.NewDocumentController,
// Init Handler // Init Handler
NewHandler, NewHandler,

View File

@ -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()
}

View File

@ -39,6 +39,7 @@ func errorConverter(logger *logger.Logger, ctx *fiber.Ctx, err error) error {
case errors.Is(err, gorm.ErrRecordNotFound): case errors.Is(err, gorm.ErrRecordNotFound):
errorMsg = errs.ErrNotFound errorMsg = errs.ErrNotFound
status = http.StatusNotFound
default: default:
logger.Sugar.Errorf("fiber error: %s", err) logger.Sugar.Errorf("fiber error: %s", err)

View File

@ -27,7 +27,7 @@ func newBlockChunk(db *gorm.DB, opts ...gen.DOOption) blockChunk {
tableName := _blockChunk.blockChunkDo.TableName() tableName := _blockChunk.blockChunkDo.TableName()
_blockChunk.ALL = field.NewAsterisk(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.CreatedAt = field.NewTime(tableName, "created_at")
_blockChunk.UpdatedAt = field.NewTime(tableName, "updated_at") _blockChunk.UpdatedAt = field.NewTime(tableName, "updated_at")
_blockChunk.DocumentBlockId = field.NewUint(tableName, "document_block_id") _blockChunk.DocumentBlockId = field.NewUint(tableName, "document_block_id")
@ -87,7 +87,7 @@ type blockChunk struct {
blockChunkDo blockChunkDo
ALL field.Asterisk ALL field.Asterisk
Id field.Uint ID field.Uint
CreatedAt field.Time CreatedAt field.Time
UpdatedAt field.Time UpdatedAt field.Time
DocumentBlockId field.Uint DocumentBlockId field.Uint
@ -109,7 +109,7 @@ func (b blockChunk) As(alias string) *blockChunk {
func (b *blockChunk) updateTableName(table string) *blockChunk { func (b *blockChunk) updateTableName(table string) *blockChunk {
b.ALL = field.NewAsterisk(table) 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.CreatedAt = field.NewTime(table, "created_at")
b.UpdatedAt = field.NewTime(table, "updated_at") b.UpdatedAt = field.NewTime(table, "updated_at")
b.DocumentBlockId = field.NewUint(table, "document_block_id") 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() { func (b *blockChunk) fillFieldMap() {
b.fieldMap = make(map[string]field.Expr, 6) 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["created_at"] = b.CreatedAt
b.fieldMap["updated_at"] = b.UpdatedAt b.fieldMap["updated_at"] = b.UpdatedAt
b.fieldMap["document_block_id"] = b.DocumentBlockId b.fieldMap["document_block_id"] = b.DocumentBlockId

View File

@ -27,7 +27,7 @@ func newCollection(db *gorm.DB, opts ...gen.DOOption) collection {
tableName := _collection.collectionDo.TableName() tableName := _collection.collectionDo.TableName()
_collection.ALL = field.NewAsterisk(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.CreatedAt = field.NewTime(tableName, "created_at")
_collection.UpdatedAt = field.NewTime(tableName, "updated_at") _collection.UpdatedAt = field.NewTime(tableName, "updated_at")
_collection.Name = field.NewString(tableName, "name") _collection.Name = field.NewString(tableName, "name")
@ -48,7 +48,7 @@ type collection struct {
collectionDo collectionDo
ALL field.Asterisk ALL field.Asterisk
Id field.Uint ID field.Uint
CreatedAt field.Time CreatedAt field.Time
UpdatedAt field.Time UpdatedAt field.Time
Name field.String Name field.String
@ -71,7 +71,7 @@ func (c collection) As(alias string) *collection {
func (c *collection) updateTableName(table string) *collection { func (c *collection) updateTableName(table string) *collection {
c.ALL = field.NewAsterisk(table) 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.CreatedAt = field.NewTime(table, "created_at")
c.UpdatedAt = field.NewTime(table, "updated_at") c.UpdatedAt = field.NewTime(table, "updated_at")
c.Name = field.NewString(table, "name") c.Name = field.NewString(table, "name")
@ -94,7 +94,7 @@ func (c *collection) GetFieldByName(fieldName string) (field.OrderExpr, bool) {
func (c *collection) fillFieldMap() { func (c *collection) fillFieldMap() {
c.fieldMap = make(map[string]field.Expr, 7) 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["created_at"] = c.CreatedAt
c.fieldMap["updated_at"] = c.UpdatedAt c.fieldMap["updated_at"] = c.UpdatedAt
c.fieldMap["name"] = c.Name c.fieldMap["name"] = c.Name

View File

@ -27,13 +27,14 @@ func newDocumentBlock(db *gorm.DB, opts ...gen.DOOption) documentBlock {
tableName := _documentBlock.documentBlockDo.TableName() tableName := _documentBlock.documentBlockDo.TableName()
_documentBlock.ALL = field.NewAsterisk(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.CreatedAt = field.NewTime(tableName, "created_at")
_documentBlock.UpdatedAt = field.NewTime(tableName, "updated_at") _documentBlock.UpdatedAt = field.NewTime(tableName, "updated_at")
_documentBlock.DocumentId = field.NewUint(tableName, "document_id") _documentBlock.DocumentId = field.NewUint(tableName, "document_id")
_documentBlock.Type = field.NewString(tableName, "type") _documentBlock.Type = field.NewString(tableName, "type")
_documentBlock.Content = field.NewString(tableName, "content") _documentBlock.Content = field.NewString(tableName, "content")
_documentBlock.Hash = field.NewString(tableName, "hash") _documentBlock.Hash = field.NewString(tableName, "hash")
_documentBlock.Order_ = field.NewFloat64(tableName, "order")
_documentBlock.Document = documentBlockBelongsToDocument{ _documentBlock.Document = documentBlockBelongsToDocument{
db: db.Session(&gorm.Session{}), db: db.Session(&gorm.Session{}),
@ -72,13 +73,14 @@ type documentBlock struct {
documentBlockDo documentBlockDo
ALL field.Asterisk ALL field.Asterisk
Id field.Uint ID field.Uint
CreatedAt field.Time CreatedAt field.Time
UpdatedAt field.Time UpdatedAt field.Time
DocumentId field.Uint DocumentId field.Uint
Type field.String Type field.String
Content field.String Content field.String
Hash field.String Hash field.String
Order_ field.Float64
Document documentBlockBelongsToDocument Document documentBlockBelongsToDocument
fieldMap map[string]field.Expr fieldMap map[string]field.Expr
@ -96,13 +98,14 @@ func (d documentBlock) As(alias string) *documentBlock {
func (d *documentBlock) updateTableName(table string) *documentBlock { func (d *documentBlock) updateTableName(table string) *documentBlock {
d.ALL = field.NewAsterisk(table) 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.CreatedAt = field.NewTime(table, "created_at")
d.UpdatedAt = field.NewTime(table, "updated_at") d.UpdatedAt = field.NewTime(table, "updated_at")
d.DocumentId = field.NewUint(table, "document_id") d.DocumentId = field.NewUint(table, "document_id")
d.Type = field.NewString(table, "type") d.Type = field.NewString(table, "type")
d.Content = field.NewString(table, "content") d.Content = field.NewString(table, "content")
d.Hash = field.NewString(table, "hash") d.Hash = field.NewString(table, "hash")
d.Order_ = field.NewFloat64(table, "order")
d.fillFieldMap() d.fillFieldMap()
@ -119,14 +122,15 @@ func (d *documentBlock) GetFieldByName(fieldName string) (field.OrderExpr, bool)
} }
func (d *documentBlock) fillFieldMap() { func (d *documentBlock) fillFieldMap() {
d.fieldMap = make(map[string]field.Expr, 8) d.fieldMap = make(map[string]field.Expr, 9)
d.fieldMap["id"] = d.Id d.fieldMap["id"] = d.ID
d.fieldMap["created_at"] = d.CreatedAt d.fieldMap["created_at"] = d.CreatedAt
d.fieldMap["updated_at"] = d.UpdatedAt d.fieldMap["updated_at"] = d.UpdatedAt
d.fieldMap["document_id"] = d.DocumentId d.fieldMap["document_id"] = d.DocumentId
d.fieldMap["type"] = d.Type d.fieldMap["type"] = d.Type
d.fieldMap["content"] = d.Content d.fieldMap["content"] = d.Content
d.fieldMap["hash"] = d.Hash d.fieldMap["hash"] = d.Hash
d.fieldMap["order"] = d.Order_
} }

View File

@ -27,7 +27,7 @@ func newDocument(db *gorm.DB, opts ...gen.DOOption) document {
tableName := _document.documentDo.TableName() tableName := _document.documentDo.TableName()
_document.ALL = field.NewAsterisk(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.CreatedAt = field.NewTime(tableName, "created_at")
_document.UpdatedAt = field.NewTime(tableName, "updated_at") _document.UpdatedAt = field.NewTime(tableName, "updated_at")
_document.Name = field.NewString(tableName, "name") _document.Name = field.NewString(tableName, "name")
@ -82,7 +82,7 @@ type document struct {
documentDo documentDo
ALL field.Asterisk ALL field.Asterisk
Id field.Uint ID field.Uint
CreatedAt field.Time CreatedAt field.Time
UpdatedAt field.Time UpdatedAt field.Time
Name field.String Name field.String
@ -111,7 +111,7 @@ func (d document) As(alias string) *document {
func (d *document) updateTableName(table string) *document { func (d *document) updateTableName(table string) *document {
d.ALL = field.NewAsterisk(table) 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.CreatedAt = field.NewTime(table, "created_at")
d.UpdatedAt = field.NewTime(table, "updated_at") d.UpdatedAt = field.NewTime(table, "updated_at")
d.Name = field.NewString(table, "name") d.Name = field.NewString(table, "name")
@ -136,7 +136,7 @@ func (d *document) GetFieldByName(fieldName string) (field.OrderExpr, bool) {
func (d *document) fillFieldMap() { func (d *document) fillFieldMap() {
d.fieldMap = make(map[string]field.Expr, 11) 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["created_at"] = d.CreatedAt
d.fieldMap["updated_at"] = d.UpdatedAt d.fieldMap["updated_at"] = d.UpdatedAt
d.fieldMap["name"] = d.Name d.fieldMap["name"] = d.Name

View File

@ -27,7 +27,7 @@ func newWorkspaceMember(db *gorm.DB, opts ...gen.DOOption) workspaceMember {
tableName := _workspaceMember.workspaceMemberDo.TableName() tableName := _workspaceMember.workspaceMemberDo.TableName()
_workspaceMember.ALL = field.NewAsterisk(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.CreatedAt = field.NewTime(tableName, "created_at")
_workspaceMember.UpdatedAt = field.NewTime(tableName, "updated_at") _workspaceMember.UpdatedAt = field.NewTime(tableName, "updated_at")
_workspaceMember.WorkspaceId = field.NewUint(tableName, "workspace_id") _workspaceMember.WorkspaceId = field.NewUint(tableName, "workspace_id")
@ -47,7 +47,7 @@ type workspaceMember struct {
workspaceMemberDo workspaceMemberDo
ALL field.Asterisk ALL field.Asterisk
Id field.Uint ID field.Uint
CreatedAt field.Time CreatedAt field.Time
UpdatedAt field.Time UpdatedAt field.Time
WorkspaceId field.Uint WorkspaceId field.Uint
@ -69,7 +69,7 @@ func (w workspaceMember) As(alias string) *workspaceMember {
func (w *workspaceMember) updateTableName(table string) *workspaceMember { func (w *workspaceMember) updateTableName(table string) *workspaceMember {
w.ALL = field.NewAsterisk(table) 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.CreatedAt = field.NewTime(table, "created_at")
w.UpdatedAt = field.NewTime(table, "updated_at") w.UpdatedAt = field.NewTime(table, "updated_at")
w.WorkspaceId = field.NewUint(table, "workspace_id") w.WorkspaceId = field.NewUint(table, "workspace_id")
@ -91,7 +91,7 @@ func (w *workspaceMember) GetFieldByName(fieldName string) (field.OrderExpr, boo
func (w *workspaceMember) fillFieldMap() { func (w *workspaceMember) fillFieldMap() {
w.fieldMap = make(map[string]field.Expr, 6) 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["created_at"] = w.CreatedAt
w.fieldMap["updated_at"] = w.UpdatedAt w.fieldMap["updated_at"] = w.UpdatedAt
w.fieldMap["workspace_id"] = w.WorkspaceId w.fieldMap["workspace_id"] = w.WorkspaceId

View File

@ -27,7 +27,7 @@ func newWorkspace(db *gorm.DB, opts ...gen.DOOption) workspace {
tableName := _workspace.workspaceDo.TableName() tableName := _workspace.workspaceDo.TableName()
_workspace.ALL = field.NewAsterisk(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.CreatedAt = field.NewTime(tableName, "created_at")
_workspace.UpdatedAt = field.NewTime(tableName, "updated_at") _workspace.UpdatedAt = field.NewTime(tableName, "updated_at")
_workspace.Name = field.NewString(tableName, "name") _workspace.Name = field.NewString(tableName, "name")
@ -43,7 +43,7 @@ type workspace struct {
workspaceDo workspaceDo
ALL field.Asterisk ALL field.Asterisk
Id field.Uint ID field.Uint
CreatedAt field.Time CreatedAt field.Time
UpdatedAt field.Time UpdatedAt field.Time
Name field.String Name field.String
@ -65,7 +65,7 @@ func (w workspace) As(alias string) *workspace {
func (w *workspace) updateTableName(table string) *workspace { func (w *workspace) updateTableName(table string) *workspace {
w.ALL = field.NewAsterisk(table) 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.CreatedAt = field.NewTime(table, "created_at")
w.UpdatedAt = field.NewTime(table, "updated_at") w.UpdatedAt = field.NewTime(table, "updated_at")
w.Name = field.NewString(table, "name") w.Name = field.NewString(table, "name")
@ -88,7 +88,7 @@ func (w *workspace) GetFieldByName(fieldName string) (field.OrderExpr, bool) {
func (w *workspace) fillFieldMap() { func (w *workspace) fillFieldMap() {
w.fieldMap = make(map[string]field.Expr, 6) 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["created_at"] = w.CreatedAt
w.fieldMap["updated_at"] = w.UpdatedAt w.fieldMap["updated_at"] = w.UpdatedAt
w.fieldMap["name"] = w.Name w.fieldMap["name"] = w.Name

View File

@ -1,6 +1,6 @@
-- +goose Up -- +goose Up
ALTER TABLE documents ALTER TABLE documents
ADD COLUMN parent_id BIGINT REFERENCES documents (id); ADD COLUMN parent_id BIGINT REFERENCES documents (id) NULL;
-- +goose Down -- +goose Down
ALTER TABLE documents ALTER TABLE documents

View File

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

View File

@ -16,7 +16,7 @@ type Document struct {
CollectionId dto.EntityId `json:"collection_id"` CollectionId dto.EntityId `json:"collection_id"`
Collection *Collection `json:"collection"` Collection *Collection `json:"collection"`
ParentId dto.EntityId `json:"parent_id"` ParentId *dto.EntityId `json:"parent_id"`
Parent *Document `json:"parent"` Parent *Document `json:"parent"`
DeletedAt gorm.DeletedAt `json:"deleted_at"` DeletedAt gorm.DeletedAt `json:"deleted_at"`
@ -35,6 +35,7 @@ type DocumentBlock struct {
Type string `json:"type"` Type string `json:"type"`
Content string `json:"content"` Content string `json:"content"`
Hash string `json:"hash"` Hash string `json:"hash"`
Order float64 `json:"order" gorm:"index"`
} }
func (*DocumentBlock) TableName() string { func (*DocumentBlock) TableName() string {

View File

@ -8,7 +8,7 @@ import (
// Model 是所有 entity 的基类,后期要将所有的 Base 改成这种形式 // Model 是所有 entity 的基类,后期要将所有的 Base 改成这种形式
type Model struct { 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"` CreatedAt time.Time `gorm:"autoUpdateTime:milli" json:"created_at"`
UpdatedAt time.Time `gorm:"autoUpdateTime:milli" json:"updated_at"` UpdatedAt time.Time `gorm:"autoUpdateTime:milli" json:"updated_at"`
//DeletedAt gorm.DeletedAt `gorm:"index"` //DeletedAt gorm.DeletedAt `gorm:"index"`

View File

@ -9,7 +9,7 @@ import (
type Workspace struct { type Workspace struct {
Model Model
Name string `json:"name"` Name string `json:"name"`
UserId user.Id `json:"user_id"` UserId user.ID `json:"user_id"`
DeletedAt gorm.DeletedAt `json:"deleted_at"` DeletedAt gorm.DeletedAt `json:"deleted_at"`
} }
@ -20,7 +20,7 @@ func (*Workspace) TableName() string {
type WorkspaceMember struct { type WorkspaceMember struct {
Model Model
WorkspaceId dto.EntityId `json:"workspace_id"` WorkspaceId dto.EntityId `json:"workspace_id"`
UserId user.Id `json:"user_id"` UserId user.ID `json:"user_id"`
Workspace *Workspace `json:"workspace"` Workspace *Workspace `json:"workspace"`
} }

View File

@ -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) v := validate.Struct(data)
var e error var e error
var ves []dto.ValidateError var ves []dto.ValidateError
if v.Validate() { if v.Validate() {
return &ves, e // 返回指针 return &ves, true, nil // 返回指针
} else { } else {
e = ErrValidationFailed e = ErrValidationFailed
@ -60,5 +60,5 @@ func Struct(data interface{}) (validationErrors *[]dto.ValidateError, err error)
// //} // //}
//} //}
return &ves, e return &ves, false, e
} }

View File

@ -5,25 +5,15 @@ import (
"leafdev.top/Leaf/leaf-library-3/internal/api/http" "leafdev.top/Leaf/leaf-library-3/internal/api/http"
) )
// 两种方法都可以
//type Api struct {
// User *v1.UserController
//}
type Api struct { type Api struct {
HttpHandler *http.Handlers HttpHandler *http.Handlers
Middleware *http.Middleware Middleware *http.Middleware
} }
func NewApiRoute( func NewApiRoute(
//User *v1.UserController,
HttpHandler *http.Handlers, HttpHandler *http.Handlers,
Middleware *http.Middleware, Middleware *http.Middleware,
) *Api { ) *Api {
//return &Api{
// User,
//}
return &Api{ return &Api{
HttpHandler, HttpHandler,
Middleware, Middleware,
@ -31,20 +21,61 @@ func NewApiRoute(
} }
func (a *Api) V1(r fiber.Router) { func (a *Api) V1(r fiber.Router) {
auth := r.Group("/api/v1") auth := r.Group("")
{ {
// 要求认证 // 要求认证
auth.Use(a.Middleware.Auth.Handler()) auth.Use(a.Middleware.Auth.Handler())
// RoutePermission 为权限验证 // 工作空间路由
//auth.Get("/ping", a.Middleware.RBAC.RoutePermission(), a.HttpHandler.User.Test) workspaces := auth.Group("/workspaces")
auth.Get("/ping", a.HttpHandler.User.Test)
}
guest := r.Group("/api/v1")
{ {
guest.Get("/guest_ping", a.HttpHandler.User.Test) 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)
// 工作空间成员管理
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)
// }
} }

View File

@ -27,7 +27,7 @@ func (a *Service) GetUser(ctx *fiber.Ctx) *user.User {
userCtx := ctx.Locals(constants.AuthMiddlewareKey) userCtx := ctx.Locals(constants.AuthMiddlewareKey)
u, ok := userCtx.(*user.User) u, ok := userCtx.(*user.User)
u.Id = u.Token.Sub u.ID = u.Token.Sub
if !ok { if !ok {
panic("User context is not valid") panic("User context is not valid")
@ -40,7 +40,7 @@ func (a *Service) GetCtx(ctx context.Context) *user.User {
userCtx := ctx.Value(constants.AuthMiddlewareKey) userCtx := ctx.Value(constants.AuthMiddlewareKey)
u, ok := userCtx.(*user.User) u, ok := userCtx.(*user.User)
u.Id = u.Token.Sub u.ID = u.Token.Sub
if !ok { if !ok {
panic("User context is not valid") 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) userCtx := ctx.Locals(constants.AuthMiddlewareKey)
u, ok := userCtx.(*user.User) u, ok := userCtx.(*user.User)
u.Id = u.Token.Sub u.ID = u.Token.Sub
return u, ok return u, ok
} }
@ -62,7 +62,7 @@ func (a *Service) GetCtxSafe(ctx context.Context) (*user.User, bool) {
userCtx := ctx.Value(constants.AuthMiddlewareKey) userCtx := ctx.Value(constants.AuthMiddlewareKey)
u, ok := userCtx.(*user.User) u, ok := userCtx.(*user.User)
u.Id = u.Token.Sub u.ID = u.Token.Sub
return u, ok return u, ok
} }
@ -90,7 +90,7 @@ func (a *Service) parseUserJWT(tokenType constants.JwtTokenTypes, jwtToken strin
return nil, errs.NotValidToken return nil, errs.NotValidToken
} }
sub = user.Id(subStr) sub = user.ID(subStr)
// 如果 token.Header 中没有 typ // 如果 token.Header 中没有 typ
if token.Header["typ"] == "" { if token.Header["typ"] == "" {

View File

@ -19,11 +19,11 @@ func (s *Service) Get(ctx context.Context, collectionId dto.EntityId) (*entity.C
return nil, errs.ErrCollectionNotExists 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) { 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 return num > 0, err
} }
@ -47,59 +47,32 @@ func (s *Service) Create(ctx context.Context, workspaceId dto.EntityId, name str
return collection, nil return collection, nil
} }
// func (s *Service) Update(ctx context.Context, collectionId dto.EntityId, name string) (*entity.Collection, error) {
//func (s *Service) AddMember(ctx context.Context, userId schema.UserId, CollectionEntity *entity.Collection) (*entity.CollectionMember, error) { collection, err := s.Get(ctx, collectionId)
// memberExists, err := s.MemberExists(ctx, userId, CollectionEntity) if err != nil {
// if err != nil { return nil, err
// return nil, err }
// }
// collection.Name = name
// if memberExists { err = s.dao.WithContext(ctx).Collection.Save(collection)
// return nil, errs.ErrMemberAlreadyExists if err != nil {
// } return nil, err
// }
// var CollectionMember = &entity.CollectionMember{
// UserId: userId, return collection, nil
// CollectionId: CollectionEntity.Id, }
// }
// func (s *Service) Delete(ctx context.Context, collectionId dto.EntityId) error {
// err = s.dao.WithContext(ctx).CollectionMember.Create(CollectionMember) exists, err := s.Exists(ctx, collectionId)
// if err != nil {
// return CollectionMember, err return err
//} }
//
//func (s *Service) RemoveMember(ctx context.Context, userId schema.UserId, CollectionEntity *entity.Collection) error { if !exists {
// memberExists, err := s.MemberExists(ctx, userId, CollectionEntity) return errs.ErrCollectionNotExists
// if err != nil { }
// return err
// } // 使用软删除
// _, err = s.dao.WithContext(ctx).Collection.Where(s.dao.Collection.ID.Eq(collectionId.Uint())).Delete()
// if !memberExists { return err
// 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
//}

View File

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

View File

@ -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()
}

View File

@ -1,4 +1,4 @@
package collection package document
import ( import (
"leafdev.top/Leaf/leaf-library-3/internal/base/conf" "leafdev.top/Leaf/leaf-library-3/internal/base/conf"

View File

@ -3,8 +3,11 @@ package services
import ( import (
"leafdev.top/Leaf/leaf-library-3/internal/base/logger" "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/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/jwks"
"leafdev.top/Leaf/leaf-library-3/internal/services/stream" "leafdev.top/Leaf/leaf-library-3/internal/services/stream"
"leafdev.top/Leaf/leaf-library-3/internal/services/workspace"
"github.com/google/wire" "github.com/google/wire"
) )
@ -14,12 +17,18 @@ type Service struct {
Jwks *jwks.JWKS Jwks *jwks.JWKS
Auth *auth.Service Auth *auth.Service
Stream *stream.Service Stream *stream.Service
Workspace *workspace.Service
Collection *collection.Service
Document *document.Service
} }
var Provide = wire.NewSet( var Provide = wire.NewSet(
jwks.NewJWKS, jwks.NewJWKS,
auth.NewService, auth.NewService,
stream.NewService, stream.NewService,
workspace.NewService,
collection.NewService,
document.NewService,
NewService, NewService,
) )
@ -28,11 +37,17 @@ func NewService(
jwks *jwks.JWKS, jwks *jwks.JWKS,
auth *auth.Service, auth *auth.Service,
stream *stream.Service, stream *stream.Service,
workspace *workspace.Service,
collection *collection.Service,
document *document.Service,
) *Service { ) *Service {
return &Service{ return &Service{
logger, logger,
jwks, jwks,
auth, auth,
stream, stream,
workspace,
collection,
document,
} }
} }

View File

@ -20,20 +20,20 @@ func (s *Service) Get(ctx context.Context, workspaceId dto.EntityId) (*entity.Wo
return nil, errs.ErrWorkspaceNotExists 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) { 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 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() 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{ var workspace = &entity.Workspace{
Name: name, Name: name,
UserId: userId, UserId: userId,
@ -54,7 +54,7 @@ func (s *Service) Create(ctx context.Context, userId user.Id, name string) (*ent
return workspace, nil 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) memberExists, err := s.MemberExists(ctx, userId, workspaceEntity)
if err != nil { if err != nil {
return nil, err return nil, err
@ -66,7 +66,7 @@ func (s *Service) AddMember(ctx context.Context, userId user.Id, workspaceEntity
var workspaceMember = &entity.WorkspaceMember{ var workspaceMember = &entity.WorkspaceMember{
UserId: userId, UserId: userId,
WorkspaceId: workspaceEntity.Id, WorkspaceId: workspaceEntity.ID,
} }
err = s.dao.WithContext(ctx).WorkspaceMember.Create(workspaceMember) 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 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) memberExists, err := s.MemberExists(ctx, userId, workspaceEntity)
if err != nil { if err != nil {
return err return err
@ -88,15 +88,15 @@ func (s *Service) RemoveMember(ctx context.Context, userId user.Id, workspaceEnt
Where( Where(
s.dao.WorkspaceMember.UserId.Eq(userId.String()), s.dao.WorkspaceMember.UserId.Eq(userId.String()),
s.dao.WorkspaceMember.WorkspaceId.Eq( s.dao.WorkspaceMember.WorkspaceId.Eq(
workspaceEntity.Id.Uint()), workspaceEntity.ID.Uint()),
). ).
Delete() Delete()
return err return err
} }
func (s *Service) MemberExists(ctx context.Context, userId user.Id, workspaceEntity *entity.Workspace) (bool, error) { 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 { 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 return false, err
} else { } else {
return num > 0, nil 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 { 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 return err
} }
// IsMember 检查用户是否是工作空间的成员
func (s *Service) IsMember(ctx context.Context, userId user.ID, workspace *entity.Workspace) (bool, error) {
return s.MemberExists(ctx, userId, workspace)
}

View File

@ -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"`
}

View File

@ -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"`
}

View File

@ -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"`
}

View File

@ -1,8 +1,9 @@
package dto package dto
import ( import (
"github.com/gofiber/fiber/v2"
"net/http" "net/http"
"github.com/gofiber/fiber/v2"
) )
type IError interface { type IError interface {
@ -23,14 +24,14 @@ type Body struct {
Wrap bool `json:"-"` Wrap bool `json:"-"`
} }
type HttpResponse struct { type Response struct {
body *Body body *Body
httpStatus int httpStatus int
ctx *fiber.Ctx ctx *fiber.Ctx
} }
func Ctx(c *fiber.Ctx) *HttpResponse { func Ctx(c *fiber.Ctx) *Response {
return &HttpResponse{ return &Response{
body: &Body{ body: &Body{
Wrap: true, 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 r.body.Message = message
if r.httpStatus == 0 { if r.httpStatus == 0 {
@ -50,24 +51,24 @@ func (r *HttpResponse) Message(message string) *HttpResponse {
} }
// WithoutWrap 将不在 body 中包裹 data // WithoutWrap 将不在 body 中包裹 data
func (r *HttpResponse) WithoutWrap() *HttpResponse { func (r *Response) WithoutWrap() *Response {
r.body.Wrap = false r.body.Wrap = false
return r return r
} }
func (r *HttpResponse) Wrap() *HttpResponse { func (r *Response) Wrap() *Response {
r.body.Wrap = true r.body.Wrap = true
return r return r
} }
func (r *HttpResponse) Data(data any) *HttpResponse { func (r *Response) Data(data any) *Response {
r.body.Data = data r.body.Data = data
return r return r
} }
func (r *HttpResponse) Error(err IError) *HttpResponse { func (r *Response) Error(err IError) *Response {
if err != nil { if err != nil {
r.body.Error = err.Error() 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 r.httpStatus = status
return r return r
} }
func (r *HttpResponse) Send() error { func (r *Response) Send() error {
if r.httpStatus == 0 { if r.httpStatus == 0 {
r.httpStatus = http.StatusOK r.httpStatus = http.StatusOK
} }
@ -114,7 +115,18 @@ func (r *HttpResponse) Send() error {
return r.ctx.Status(r.httpStatus).JSON(r.body.Data) 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 { // if validationErrors == nil || len(*validationErrors) == 0 {
// } // }
// //

View File

@ -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"`
}

View File

@ -8,6 +8,6 @@ type CurrentUserResponse struct {
IP string `json:"ip"` IP string `json:"ip"`
Valid bool `json:"valid"` Valid bool `json:"valid"`
UserEmail string `json:"userEmail"` UserEmail string `json:"userEmail"`
UserId user.Id `json:"userId"` UserId user.ID `json:"userId"`
UserName string `json:"userName"` UserName string `json:"userName"`
} }

View File

@ -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"`
}

View File

@ -5,4 +5,7 @@ import "errors"
var ( var (
ErrPageNotFound = errors.New("page not found") ErrPageNotFound = errors.New("page not found")
ErrNotFound = errors.New("not found") ErrNotFound = errors.New("not found")
ErrNoPermission = errors.New("no permission")
ErrDocumentNotExists = errors.New("document not exists")
ErrInvalidParentDocument = errors.New("invalid parent document")
) )

View File

@ -6,14 +6,14 @@ import (
) )
// AnonymousUser 调试模式下的用户 // AnonymousUser 调试模式下的用户
const AnonymousUser Id = "anonymous" const AnonymousUser ID = "anonymous"
type Token struct { type Token struct {
Aud string `json:"aud"` Aud string `json:"aud"`
Iss string `json:"iss"` Iss string `json:"iss"`
Iat float64 `json:"iat"` Iat float64 `json:"iat"`
Exp float64 `json:"exp"` Exp float64 `json:"exp"`
Sub Id `json:"sub" mapstructure:"-"` Sub ID `json:"sub" mapstructure:"-"`
Scopes []string `json:"scopes"` Scopes []string `json:"scopes"`
Roles []Role `json:"roles,omitempty"` Roles []Role `json:"roles,omitempty"`
Permissions []Permission `json:"permissions"` Permissions []Permission `json:"permissions"`
@ -30,7 +30,7 @@ type Token struct {
type User struct { type User struct {
Token Token Token Token
Id Id ID ID
Valid bool Valid bool
} }
@ -42,9 +42,9 @@ func (r Role) String() string {
type Permission string type Permission string
type Id string type ID string
func (u *User) GetId() Id { func (u *User) GetId() ID {
return u.Token.Sub return u.Token.Sub
} }
@ -108,7 +108,7 @@ func (u *User) HasPermissions(permissions ...Permission) bool {
return true return true
} }
func (u Id) String() string { func (u ID) String() string {
return string(u) return string(u)
} }