```
feat(api): 初始化项目基础结构与API定义 新增 `.gitignore` 文件,忽略编译输出、生成代码及数据库文件。 新增 `README.md`,包含 Gin 框架和 Swagger 工具的安装与使用说明。 新增 `config/api.yaml`,定义 bookmarks 相关的文件夹与书签操作的 OpenAPI 3.0 接口规范。 新增 `config/cfg.yaml`,配置 oapi-codegen 工具生成 Gin 服务和模型代码。 新增 `go.mod` 和 `go.sum` 文件,初始化 Go 模块并引入 Gin、GORM、SQLite 及 oapi-codegen 等依赖。 ```
This commit is contained in:
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
.*/
|
||||
dist/
|
||||
gen/
|
||||
|
||||
*.exe
|
||||
*.sqlite3
|
22
README.md
Normal file
22
README.md
Normal file
@ -0,0 +1,22 @@
|
||||
|
||||
```shell
|
||||
# https://gin-gonic.com/zh-cn/docs/quickstart/
|
||||
go get -u github.com/gin-gonic/gin
|
||||
|
||||
# https://github.com/swaggo/gin-swagger
|
||||
go install github.com/swaggo/swag/cmd/swag@latest
|
||||
|
||||
swag init
|
||||
|
||||
go get -u github.com/swaggo/gin-swagger
|
||||
go get -u github.com/swaggo/files
|
||||
```
|
||||
|
||||
```shell
|
||||
# [link](https://github.com/oapi-codegen/oapi-codegen/?tab=readme-ov-file)
|
||||
# this will then modify your `go.mod`
|
||||
go get -tool github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen@latest
|
||||
|
||||
#go:generate go tool oapi-codegen -config cfg.yaml ../../api.yaml
|
||||
go tool oapi-codegen -config cfg.yaml api.yaml
|
||||
```
|
523
config/api.yaml
Normal file
523
config/api.yaml
Normal file
@ -0,0 +1,523 @@
|
||||
openapi: '3.0.3'
|
||||
info:
|
||||
title: zzyxyz_api
|
||||
description: API服务
|
||||
version: '1.0'
|
||||
servers:
|
||||
- url: http://localhost:8080/api
|
||||
description: 开发环境
|
||||
- url: https://api.zzyxyz.com/api
|
||||
description: 生产环境
|
||||
tags:
|
||||
- name: folder
|
||||
description: 文件夹相关操作
|
||||
- name: data
|
||||
description: 书签相关操作
|
||||
security:
|
||||
- ApiKeyAuth: []
|
||||
|
||||
paths:
|
||||
/bookmarks/v1/folder:
|
||||
post:
|
||||
summary: 创建文件夹
|
||||
description: 创建一个存储书签或者文件夹的文件夹
|
||||
operationId: createFolder
|
||||
tags: [folder]
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/FolderRequest'
|
||||
responses:
|
||||
'201':
|
||||
description: 创建成功的文件夹
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/FolderResponse'
|
||||
'400':
|
||||
description: 请求参数错误
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Error'
|
||||
'500':
|
||||
$ref: '#/components/responses/ServerInternalError'
|
||||
|
||||
/bookmarks/v1/folder/{id}:
|
||||
get:
|
||||
summary: 获取文件夹基础信息
|
||||
description: 获取文件夹基础信息不包含内容,只有元数据
|
||||
operationId: getFolderInfo
|
||||
tags: [folder]
|
||||
parameters:
|
||||
- name: id
|
||||
in: path
|
||||
required: true
|
||||
example: 1
|
||||
schema:
|
||||
type: integer
|
||||
format: int64
|
||||
responses:
|
||||
'200':
|
||||
description: 文件夹详情
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/FolderResponse'
|
||||
'404':
|
||||
description: 文件夹不存在
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Error'
|
||||
|
||||
put:
|
||||
summary: 更新文件夹
|
||||
description: 修改文件夹的元数据(包括修改名称和移动文件夹)
|
||||
operationId: updateFolder
|
||||
tags: [folder]
|
||||
parameters:
|
||||
- name: id
|
||||
in: path
|
||||
example: 1
|
||||
required: true
|
||||
schema:
|
||||
type: integer
|
||||
format: int64
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/FolderRequest'
|
||||
responses:
|
||||
'200':
|
||||
description: 更新后的文件夹
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/FolderResponse'
|
||||
'400':
|
||||
description: 请求参数错误
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Error'
|
||||
'404':
|
||||
description: 文件夹不存在
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Error'
|
||||
'500':
|
||||
$ref: '#/components/responses/ServerInternalError'
|
||||
|
||||
delete:
|
||||
summary: 删除文件夹
|
||||
description: 删除文件夹(文件夹不能有内容)
|
||||
operationId: deleteFolder
|
||||
tags: [folder]
|
||||
parameters:
|
||||
- name: id
|
||||
in: path
|
||||
example: 1
|
||||
required: true
|
||||
schema:
|
||||
type: integer
|
||||
format: int64
|
||||
responses:
|
||||
'204':
|
||||
description: 删除成功
|
||||
'400':
|
||||
description: 文件夹不为空,无法删除
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Error'
|
||||
'404':
|
||||
description: 文件夹不存在
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Error'
|
||||
'500':
|
||||
$ref: '#/components/responses/ServerInternalError'
|
||||
|
||||
/bookmarks/v1/folder/{id}/content:
|
||||
get:
|
||||
summary: 获取文件夹的内容
|
||||
description: 只获取当前文件夹的内容,不会递归搜索
|
||||
operationId: getFolderContent
|
||||
tags: [folder]
|
||||
parameters:
|
||||
- name: id
|
||||
in: path
|
||||
example: 1
|
||||
required: true
|
||||
schema:
|
||||
type: integer
|
||||
format: int64
|
||||
responses:
|
||||
'200':
|
||||
description: 文件夹子节点列表
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/FolderContentResponse'
|
||||
'404':
|
||||
description: 文件夹不存在
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Error'
|
||||
delete:
|
||||
summary: 删除文件夹的内容
|
||||
description: 删除文件夹的内容(危险操作)
|
||||
operationId: deleteFolderContent
|
||||
tags: [folder]
|
||||
parameters:
|
||||
- name: id
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: integer
|
||||
format: int64
|
||||
- name: mode
|
||||
in: query
|
||||
description: 删除模式
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
enum: [onlyContent, onlyEmptyFolder, onlyFolder, all]
|
||||
responses:
|
||||
'200':
|
||||
description: 删除成功
|
||||
'500':
|
||||
$ref: '#/components/responses/ServerInternalError'
|
||||
|
||||
/bookmarks/v1/data:
|
||||
post:
|
||||
summary: 创建书签
|
||||
description: 在文件夹下创建一个书签
|
||||
operationId: createBookmark
|
||||
tags: [data]
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/BookmarkRequest'
|
||||
responses:
|
||||
'201':
|
||||
description: 创建成功的书签
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/BookmarkResponse'
|
||||
'400':
|
||||
description: 请求参数错误
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Error'
|
||||
'500':
|
||||
$ref: '#/components/responses/ServerInternalError'
|
||||
|
||||
/bookmarks/v1/data/{id}:
|
||||
get:
|
||||
summary: 获取书签详情
|
||||
description: 通过id获取书签内容
|
||||
operationId: getBookmark
|
||||
tags: [data]
|
||||
parameters:
|
||||
- name: id
|
||||
in: path
|
||||
example: 1
|
||||
required: true
|
||||
schema:
|
||||
type: integer
|
||||
format: int64
|
||||
responses:
|
||||
'200':
|
||||
description: 书签详情
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/BookmarkResponse'
|
||||
'404':
|
||||
description: 书签不存在
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Error'
|
||||
|
||||
put:
|
||||
summary: 更新书签
|
||||
description: 更新指定id的书签
|
||||
operationId: updateBookmark
|
||||
tags: [data]
|
||||
parameters:
|
||||
- name: id
|
||||
in: path
|
||||
example: 1
|
||||
required: true
|
||||
schema:
|
||||
type: integer
|
||||
format: int64
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/BookmarkRequest'
|
||||
responses:
|
||||
'200':
|
||||
description: 更新后的书签
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/BookmarkResponse'
|
||||
'400':
|
||||
description: 请求参数错误
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Error'
|
||||
'404':
|
||||
description: 书签不存在
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Error'
|
||||
'500':
|
||||
$ref: '#/components/responses/ServerInternalError'
|
||||
|
||||
delete:
|
||||
summary: 删除书签
|
||||
description: 删除指定id的书签
|
||||
operationId: deleteBookmark
|
||||
tags: [data]
|
||||
parameters:
|
||||
- name: id
|
||||
in: path
|
||||
example: 1
|
||||
required: true
|
||||
schema:
|
||||
type: integer
|
||||
format: int64
|
||||
responses:
|
||||
'204':
|
||||
description: 删除成功
|
||||
'404':
|
||||
description: 书签不存在
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Error'
|
||||
'500':
|
||||
$ref: '#/components/responses/ServerInternalError'
|
||||
|
||||
components:
|
||||
securitySchemes:
|
||||
ApiKeyAuth:
|
||||
type: apiKey
|
||||
in: header
|
||||
name: X-API-Key
|
||||
responses:
|
||||
ServerInternalError:
|
||||
description: 服务器内部错误
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Error'
|
||||
Unauthorized:
|
||||
description: 未授权
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Error'
|
||||
schemas:
|
||||
FolderRequest:
|
||||
description: 文件夹请求结构体
|
||||
type: object
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
minLength: 1
|
||||
maxLength: 255
|
||||
description: 文件夹名称
|
||||
example: 测试名称
|
||||
parent_path_id:
|
||||
type: integer
|
||||
format: int64
|
||||
description: 父文件夹ID 若为空则自动创建在用户根目录下
|
||||
example: 1
|
||||
required:
|
||||
- name
|
||||
|
||||
BookmarkRequest:
|
||||
type: object
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
minLength: 1
|
||||
maxLength: 255
|
||||
description: 书签名称
|
||||
example: 测试名称
|
||||
link:
|
||||
type: string
|
||||
description: 书签链接
|
||||
example: /swagger/index.html
|
||||
detail:
|
||||
type: string
|
||||
description: 书签详情链接
|
||||
description:
|
||||
type: string
|
||||
description: 书签描述
|
||||
parent_path_id:
|
||||
type: integer
|
||||
format: int64
|
||||
description: 父文件夹ID 若为空则自动创建在用户根目录下
|
||||
example: 1
|
||||
required:
|
||||
- name
|
||||
|
||||
FolderResponse:
|
||||
type: object
|
||||
description: 文件夹响应结构体
|
||||
properties:
|
||||
id:
|
||||
type: integer
|
||||
format: int64
|
||||
description: 文件夹ID
|
||||
name:
|
||||
type: string
|
||||
description: 文件夹名称
|
||||
parent_path_id:
|
||||
type: integer
|
||||
format: int64
|
||||
description: 父文件夹ID
|
||||
created_at:
|
||||
type: string
|
||||
format: date-time
|
||||
description: 创建时间
|
||||
updated_at:
|
||||
type: string
|
||||
format: date-time
|
||||
description: 更新时间
|
||||
sub_folder_count:
|
||||
type: integer
|
||||
description: 子文件夹数量
|
||||
bookmark_count:
|
||||
type: integer
|
||||
description: 书签数量
|
||||
required:
|
||||
- id
|
||||
- name
|
||||
- parent_path_id
|
||||
- created_at
|
||||
- updated_at
|
||||
- sub_folder_count
|
||||
- bookmark_count
|
||||
|
||||
BookmarkResponse:
|
||||
type: object
|
||||
description: 书签相应结构体
|
||||
properties:
|
||||
id:
|
||||
type: integer
|
||||
format: int64
|
||||
description: 书签ID
|
||||
name:
|
||||
type: string
|
||||
description: 书签名称
|
||||
link:
|
||||
type: string
|
||||
description: 书签链接
|
||||
detail:
|
||||
type: string
|
||||
description: 书签详情链接
|
||||
description:
|
||||
type: string
|
||||
description: 书签描述
|
||||
parent_path_id:
|
||||
type: integer
|
||||
format: int64
|
||||
description: 父文件夹ID
|
||||
created_at:
|
||||
type: string
|
||||
format: date-time
|
||||
description: 创建时间
|
||||
updated_at:
|
||||
type: string
|
||||
format: date-time
|
||||
description: 更新时间
|
||||
required:
|
||||
- id
|
||||
- name
|
||||
- parent_path_id
|
||||
- created_at
|
||||
- updated_at
|
||||
|
||||
FolderContentResponse:
|
||||
type: object
|
||||
properties:
|
||||
sub_folders:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/FolderBriefResponse'
|
||||
description: 子文件夹列表
|
||||
bookmarks:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/BookmarkBriefResponse'
|
||||
description: 书签列表
|
||||
required:
|
||||
- sub_folders
|
||||
- bookmarks
|
||||
|
||||
FolderBriefResponse:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: integer
|
||||
format: int64
|
||||
description: 文件夹ID
|
||||
name:
|
||||
type: string
|
||||
description: 文件夹名称
|
||||
required:
|
||||
- id
|
||||
- name
|
||||
|
||||
BookmarkBriefResponse:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: integer
|
||||
format: int64
|
||||
description: 书签ID
|
||||
name:
|
||||
type: string
|
||||
description: 书签名称
|
||||
required:
|
||||
- id
|
||||
- name
|
||||
|
||||
Error:
|
||||
type: object
|
||||
description: 错误信息
|
||||
properties:
|
||||
errtype:
|
||||
type: string
|
||||
example: "ParameterError"
|
||||
description: 错误类型
|
||||
message:
|
||||
example: "传递的第一个参数错误"
|
||||
type: string
|
||||
description: 错误信息
|
||||
required:
|
||||
- errtype
|
||||
- message
|
6
config/cfg.yaml
Normal file
6
config/cfg.yaml
Normal file
@ -0,0 +1,6 @@
|
||||
# yaml-language-server: ...
|
||||
package: api
|
||||
generate:
|
||||
gin-server: true
|
||||
models: true
|
||||
output: ./gen/api/gen.go
|
67
go.mod
Normal file
67
go.mod
Normal file
@ -0,0 +1,67 @@
|
||||
module git.zzyxyz.com/zzy/zzyxyz_go_api
|
||||
|
||||
go 1.25.1
|
||||
|
||||
require (
|
||||
github.com/gin-gonic/gin v1.10.1
|
||||
github.com/mattn/go-sqlite3 v1.14.32
|
||||
github.com/oapi-codegen/runtime v1.1.2
|
||||
github.com/stretchr/testify v1.11.1
|
||||
gorm.io/driver/sqlite v1.6.0
|
||||
gorm.io/gorm v1.30.5
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect
|
||||
github.com/bytedance/gopkg v0.1.3 // indirect
|
||||
github.com/bytedance/sonic v1.14.1 // indirect
|
||||
github.com/bytedance/sonic/loader v0.3.0 // indirect
|
||||
github.com/cloudwego/base64x v0.1.6 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/dprotaso/go-yit v0.0.0-20220510233725-9ba8df137936 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.10 // indirect
|
||||
github.com/getkin/kin-openapi v0.132.0 // indirect
|
||||
github.com/gin-contrib/sse v1.1.0 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.22.0 // indirect
|
||||
github.com/go-openapi/swag/jsonname v0.24.0 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/go-playground/validator/v10 v10.27.0 // indirect
|
||||
github.com/goccy/go-json v0.10.5 // indirect
|
||||
github.com/google/uuid v1.5.0 // indirect
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
github.com/jinzhu/now v1.1.5 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
github.com/mailru/easyjson v0.9.0 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect
|
||||
github.com/oapi-codegen/oapi-codegen/v2 v2.5.0 // indirect
|
||||
github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 // indirect
|
||||
github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||
github.com/perimeterx/marshmallow v1.1.5 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/speakeasy-api/jsonpath v0.6.0 // indirect
|
||||
github.com/speakeasy-api/openapi-overlay v0.10.2 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.3.0 // indirect
|
||||
github.com/vmware-labs/yaml-jsonpath v0.3.2 // indirect
|
||||
golang.org/x/arch v0.21.0 // indirect
|
||||
golang.org/x/crypto v0.42.0 // indirect
|
||||
golang.org/x/mod v0.28.0 // indirect
|
||||
golang.org/x/net v0.43.0 // indirect
|
||||
golang.org/x/sync v0.17.0 // indirect
|
||||
golang.org/x/sys v0.36.0 // indirect
|
||||
golang.org/x/text v0.29.0 // indirect
|
||||
golang.org/x/tools v0.36.0 // indirect
|
||||
google.golang.org/protobuf v1.36.8 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
tool github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen
|
248
go.sum
Normal file
248
go.sum
Normal file
@ -0,0 +1,248 @@
|
||||
github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk=
|
||||
github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7Dml6nw9rQ=
|
||||
github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk=
|
||||
github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w=
|
||||
github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M=
|
||||
github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM=
|
||||
github.com/bytedance/sonic v1.14.1 h1:FBMC0zVz5XUmE4z9wF4Jey0An5FueFvOsTKKKtwIl7w=
|
||||
github.com/bytedance/sonic v1.14.1/go.mod h1:gi6uhQLMbTdeP0muCnrjHLeCUPyb70ujhnNlhOylAFc=
|
||||
github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA=
|
||||
github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
|
||||
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
|
||||
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dprotaso/go-yit v0.0.0-20191028211022-135eb7262960/go.mod h1:9HQzr9D/0PGwMEbC3d5AB7oi67+h4TsQqItC1GVYG58=
|
||||
github.com/dprotaso/go-yit v0.0.0-20220510233725-9ba8df137936 h1:PRxIJD8XjimM5aTknUK9w6DHLDox2r2M3DI4i2pnd3w=
|
||||
github.com/dprotaso/go-yit v0.0.0-20220510233725-9ba8df137936/go.mod h1:ttYvX5qlB+mlV1okblJqcSMtR4c52UKxDiX9GRBS8+Q=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
|
||||
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
||||
github.com/gabriel-vasile/mimetype v1.4.10 h1:zyueNbySn/z8mJZHLt6IPw0KoZsiQNszIpU+bX4+ZK0=
|
||||
github.com/gabriel-vasile/mimetype v1.4.10/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
||||
github.com/getkin/kin-openapi v0.132.0 h1:3ISeLMsQzcb5v26yeJrBcdTCEQTag36ZjaGk7MIRUwk=
|
||||
github.com/getkin/kin-openapi v0.132.0/go.mod h1:3OlG51PCYNsPByuiMB0t4fjnNlIDnaEDsjiKUV8nL58=
|
||||
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
|
||||
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
|
||||
github.com/gin-gonic/gin v1.10.1 h1:T0ujvqyCSqRopADpgPgiTT63DUQVSfojyME59Ei63pQ=
|
||||
github.com/gin-gonic/gin v1.10.1/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
|
||||
github.com/go-openapi/jsonpointer v0.22.0 h1:TmMhghgNef9YXxTu1tOopo+0BGEytxA+okbry0HjZsM=
|
||||
github.com/go-openapi/jsonpointer v0.22.0/go.mod h1:xt3jV88UtExdIkkL7NloURjRQjbeUgcxFblMjq2iaiU=
|
||||
github.com/go-openapi/swag/jsonname v0.24.0 h1:2wKS9bgRV/xB8c62Qg16w4AUiIrqqiniJFtZGi3dg5k=
|
||||
github.com/go-openapi/swag/jsonname v0.24.0/go.mod h1:GXqrPzGJe611P7LG4QB9JKPtUZ7flE4DOVechNaDd7Q=
|
||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||
github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4=
|
||||
github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
|
||||
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
|
||||
github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM=
|
||||
github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
|
||||
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
||||
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
||||
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
||||
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||
github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU=
|
||||
github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
||||
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d/go.mod h1:2PavIy+JPciBPrBUjwbNvtwB6RQlve+hkpll6QSNmOE=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||
github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4=
|
||||
github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs=
|
||||
github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw=
|
||||
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8=
|
||||
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
|
||||
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
|
||||
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
|
||||
github.com/oapi-codegen/oapi-codegen/v2 v2.5.0 h1:iJvF8SdB/3/+eGOXEpsWkD8FQAHj6mqkb6Fnsoc8MFU=
|
||||
github.com/oapi-codegen/oapi-codegen/v2 v2.5.0/go.mod h1:fwlMxUEMuQK5ih9aymrxKPQqNm2n8bdLk1ppjH+lr9w=
|
||||
github.com/oapi-codegen/runtime v1.1.2 h1:P2+CubHq8fO4Q6fV1tqDBZHCwpVpvPg7oKiYzQgXIyI=
|
||||
github.com/oapi-codegen/runtime v1.1.2/go.mod h1:SK9X900oXmPWilYR5/WKPzt3Kqxn/uS/+lbpREv+eCg=
|
||||
github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 h1:G7ERwszslrBzRxj//JalHPu/3yz+De2J+4aLtSRlHiY=
|
||||
github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037/go.mod h1:2bpvgLBZEtENV5scfDFEtB/5+1M4hkQhDQrccEJ/qGw=
|
||||
github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 h1:bQx3WeLcUWy+RletIKwUIt4x3t8n2SxavmoclizMb8c=
|
||||
github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90/go.mod h1:y5+oSEHCPT/DGrS++Wc/479ERge0zTFxaF8PbGKcg2o=
|
||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.10.2/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
|
||||
github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc=
|
||||
github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0=
|
||||
github.com/onsi/ginkgo/v2 v2.1.3/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c=
|
||||
github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
|
||||
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
|
||||
github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY=
|
||||
github.com/onsi/gomega v1.19.0 h1:4ieX6qQjPP/BfC3mpsAtIGGlxTWPeA3Inl/7DtXw1tw=
|
||||
github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro=
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
||||
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||
github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s=
|
||||
github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
|
||||
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
|
||||
github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
|
||||
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
|
||||
github.com/speakeasy-api/jsonpath v0.6.0 h1:IhtFOV9EbXplhyRqsVhHoBmmYjblIRh5D1/g8DHMXJ8=
|
||||
github.com/speakeasy-api/jsonpath v0.6.0/go.mod h1:ymb2iSkyOycmzKwbEAYPJV/yi2rSmvBCLZJcyD+VVWw=
|
||||
github.com/speakeasy-api/openapi-overlay v0.10.2 h1:VOdQ03eGKeiHnpb1boZCGm7x8Haj6gST0P3SGTX95GU=
|
||||
github.com/speakeasy-api/openapi-overlay v0.10.2/go.mod h1:n0iOU7AqKpNFfEt6tq7qYITC4f0yzVVdFw0S7hukemg=
|
||||
github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||
github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA=
|
||||
github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
|
||||
github.com/vmware-labs/yaml-jsonpath v0.3.2 h1:/5QKeCBGdsInyDCyVNLbXyilb61MXGi9NP674f9Hobk=
|
||||
github.com/vmware-labs/yaml-jsonpath v0.3.2/go.mod h1:U6whw1z03QyqgWdgXxvVnQ90zN1BWz5V+51Ewf8k+rQ=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
golang.org/x/arch v0.21.0 h1:iTC9o7+wP6cPWpDWkivCvQFGAHDQ59SrSxsLPcnkArw=
|
||||
golang.org/x/arch v0.21.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI=
|
||||
golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U=
|
||||
golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
|
||||
golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
|
||||
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
|
||||
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
|
||||
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
|
||||
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg=
|
||||
golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
||||
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
||||
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc=
|
||||
google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20191026110619-0b21df46bc1d/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ=
|
||||
gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8=
|
||||
gorm.io/gorm v1.30.5 h1:dvEfYwxL+i+xgCNSGGBT1lDjCzfELK8fHZxL3Ee9X0s=
|
||||
gorm.io/gorm v1.30.5/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE=
|
449
internal/handlers/bookmark.go
Normal file
449
internal/handlers/bookmark.go
Normal file
@ -0,0 +1,449 @@
|
||||
// internal/handlers/note_link.go
|
||||
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"git.zzyxyz.com/zzy/zzyxyz_go_api/gen/api"
|
||||
"git.zzyxyz.com/zzy/zzyxyz_go_api/internal/models"
|
||||
"github.com/gin-gonic/gin"
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type BookMarksImpl struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewBookMarks(dbPath string) (*BookMarksImpl, error) {
|
||||
var err error
|
||||
var db *gorm.DB
|
||||
db, err = gorm.Open(sqlite.Open(dbPath), &gorm.Config{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 自动迁移表结构
|
||||
err = db.AutoMigrate(&models.Folder{}, &models.Bookmark{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 创建根文件夹(如果不存在)
|
||||
var rootFolder models.Folder
|
||||
result := db.First(&rootFolder, 1)
|
||||
if result.Error != nil {
|
||||
// 根文件夹不存在,创建它
|
||||
rootFolder = models.Folder{
|
||||
ID: 1,
|
||||
Name: "Root",
|
||||
ParentPathID: 1, // 根目录指向自己
|
||||
}
|
||||
db.Create(&rootFolder)
|
||||
}
|
||||
|
||||
return &BookMarksImpl{db: db}, nil
|
||||
}
|
||||
|
||||
// CreateBookmark implements api.ServerInterface.
|
||||
func (b *BookMarksImpl) CreateBookmark(c *gin.Context) {
|
||||
var db = b.db
|
||||
var req api.BookmarkRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, api.Error{
|
||||
Errtype: "ParameterError",
|
||||
Message: "Invalid request parameters",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 设置默认父文件夹ID为根目录(1)
|
||||
parentID := int64(1)
|
||||
if req.ParentPathId != nil && *req.ParentPathId != 0 {
|
||||
parentID = *req.ParentPathId
|
||||
}
|
||||
|
||||
// 检查父文件夹是否存在
|
||||
var parentFolder models.Folder
|
||||
if err := db.First(&parentFolder, parentID).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, api.Error{
|
||||
Errtype: "NotFoundError",
|
||||
Message: "Parent folder not found",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 创建书签
|
||||
bookmark := models.Bookmark{
|
||||
Name: req.Name,
|
||||
Detail: *req.Detail,
|
||||
Description: *req.Description,
|
||||
ParentPathID: parentID,
|
||||
}
|
||||
|
||||
if err := db.Create(&bookmark).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, api.Error{
|
||||
Errtype: "DatabaseError",
|
||||
Message: "Failed to create bookmark",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 构造响应
|
||||
response := api.BookmarkResponse{
|
||||
Id: bookmark.ID,
|
||||
Name: bookmark.Name,
|
||||
Detail: &bookmark.Detail,
|
||||
Description: &bookmark.Description,
|
||||
ParentPathId: bookmark.ParentPathID,
|
||||
CreatedAt: bookmark.CreatedAt,
|
||||
UpdatedAt: bookmark.UpdatedAt,
|
||||
}
|
||||
|
||||
c.JSON(http.StatusCreated, response)
|
||||
}
|
||||
|
||||
// CreateFolder implements api.ServerInterface.
|
||||
func (b *BookMarksImpl) CreateFolder(c *gin.Context) {
|
||||
var db = b.db
|
||||
var req api.FolderRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, api.Error{
|
||||
Errtype: "ParameterError",
|
||||
Message: "Invalid request parameters",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 设置默认父文件夹ID为根目录(1)
|
||||
parentID := int64(1)
|
||||
if req.ParentPathId != nil && *req.ParentPathId != 0 {
|
||||
parentID = *req.ParentPathId
|
||||
}
|
||||
|
||||
// 检查父文件夹是否存在
|
||||
var parentFolder models.Folder
|
||||
if err := db.First(&parentFolder, parentID).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, api.Error{
|
||||
Errtype: "NotFoundError",
|
||||
Message: "Parent folder not found",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 创建文件夹
|
||||
folder := models.Folder{
|
||||
Name: req.Name,
|
||||
ParentPathID: parentID,
|
||||
}
|
||||
|
||||
if err := db.Create(&folder).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, api.Error{
|
||||
Errtype: "DatabaseError",
|
||||
Message: "Failed to create folder",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 构造响应
|
||||
response := api.FolderResponse{
|
||||
Id: folder.ID,
|
||||
Name: folder.Name,
|
||||
ParentPathId: folder.ParentPathID,
|
||||
CreatedAt: folder.CreatedAt,
|
||||
UpdatedAt: folder.UpdatedAt,
|
||||
}
|
||||
|
||||
c.JSON(http.StatusCreated, response)
|
||||
}
|
||||
|
||||
// DeleteBookmark implements api.ServerInterface.
|
||||
func (b *BookMarksImpl) DeleteBookmark(c *gin.Context, id int64) {
|
||||
panic("unimplemented")
|
||||
}
|
||||
|
||||
// DeleteFolder implements api.ServerInterface.
|
||||
func (b *BookMarksImpl) DeleteFolder(c *gin.Context, id int64) {
|
||||
panic("unimplemented")
|
||||
}
|
||||
|
||||
// DeleteFolderContent implements api.ServerInterface.
|
||||
func (b *BookMarksImpl) DeleteFolderContent(c *gin.Context, id int64, params api.DeleteFolderContentParams) {
|
||||
panic("unimplemented")
|
||||
}
|
||||
|
||||
// GetBookmark implements api.ServerInterface.
|
||||
func (b *BookMarksImpl) GetBookmark(c *gin.Context, id int64) {
|
||||
var db = b.db
|
||||
var bookmark models.Bookmark
|
||||
|
||||
// 查询书签
|
||||
if err := db.First(&bookmark, id).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, api.Error{
|
||||
Errtype: "NotFoundError",
|
||||
Message: "Bookmark not found",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 构造响应
|
||||
response := api.BookmarkResponse{
|
||||
Id: bookmark.ID,
|
||||
Name: bookmark.Name,
|
||||
Link: &bookmark.Link,
|
||||
Detail: &bookmark.Detail,
|
||||
Description: &bookmark.Description,
|
||||
ParentPathId: bookmark.ParentPathID,
|
||||
CreatedAt: bookmark.CreatedAt,
|
||||
UpdatedAt: bookmark.UpdatedAt,
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, response)
|
||||
}
|
||||
|
||||
// GetFolderContent implements api.ServerInterface.
|
||||
func (b *BookMarksImpl) GetFolderContent(c *gin.Context, id int64) {
|
||||
var db = b.db
|
||||
|
||||
// 检查文件夹是否存在
|
||||
var folder models.Folder
|
||||
if err := db.First(&folder, id).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, api.Error{
|
||||
Errtype: "NotFoundError",
|
||||
Message: "Folder not found",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 查询子文件夹
|
||||
var subFolders []models.Folder
|
||||
db.Where("parent_path_id = ?", id).Find(&subFolders)
|
||||
|
||||
// 查询书签
|
||||
var bookmarks []models.Bookmark
|
||||
db.Where("parent_path_id = ?", id).Find(&bookmarks)
|
||||
|
||||
// 构造子文件夹响应列表
|
||||
var subFolderResponses []api.FolderBriefResponse
|
||||
for _, f := range subFolders {
|
||||
subFolderResponses = append(subFolderResponses, api.FolderBriefResponse{
|
||||
Id: f.ID,
|
||||
Name: f.Name,
|
||||
})
|
||||
}
|
||||
|
||||
// 构造书签响应列表
|
||||
var bookmarkResponses []api.BookmarkBriefResponse
|
||||
for _, bm := range bookmarks {
|
||||
bookmarkResponses = append(bookmarkResponses, api.BookmarkBriefResponse{
|
||||
Id: bm.ID,
|
||||
Name: bm.Name,
|
||||
})
|
||||
}
|
||||
|
||||
// 构造响应
|
||||
response := api.FolderContentResponse{
|
||||
SubFolders: subFolderResponses,
|
||||
Bookmarks: bookmarkResponses,
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, response)
|
||||
}
|
||||
|
||||
// GetFolderInfo implements api.ServerInterface.
|
||||
func (b *BookMarksImpl) GetFolderInfo(c *gin.Context, id int64) {
|
||||
var db = b.db
|
||||
var folder models.Folder
|
||||
|
||||
// 查询文件夹
|
||||
if err := db.First(&folder, id).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, api.Error{
|
||||
Errtype: "NotFoundError",
|
||||
Message: "Folder not found",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 统计子文件夹数量
|
||||
var subFolderCount int64
|
||||
db.Model(&models.Folder{}).Where("parent_path_id = ?", id).Count(&subFolderCount)
|
||||
|
||||
// 统计书签数量
|
||||
var bookmarkCount int64
|
||||
db.Model(&models.Bookmark{}).Where("parent_path_id = ?", id).Count(&bookmarkCount)
|
||||
|
||||
// 构造响应
|
||||
response := api.FolderResponse{
|
||||
Id: folder.ID,
|
||||
Name: folder.Name,
|
||||
ParentPathId: folder.ParentPathID,
|
||||
CreatedAt: folder.CreatedAt,
|
||||
UpdatedAt: folder.UpdatedAt,
|
||||
SubFolderCount: int(subFolderCount),
|
||||
BookmarkCount: int(bookmarkCount),
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, response)
|
||||
}
|
||||
|
||||
// UpdateBookmark implements api.ServerInterface.
|
||||
func (b *BookMarksImpl) UpdateBookmark(c *gin.Context, id int64) {
|
||||
var db = b.db
|
||||
var req api.BookmarkRequest
|
||||
|
||||
// 绑定请求参数
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, api.Error{
|
||||
Errtype: "ParameterError",
|
||||
Message: "Invalid request parameters",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 查找要更新的书签
|
||||
var bookmark models.Bookmark
|
||||
if err := db.First(&bookmark, id).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, api.Error{
|
||||
Errtype: "NotFoundError",
|
||||
Message: "Bookmark not found",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 更新书签字段
|
||||
if req.Name != "" {
|
||||
bookmark.Name = req.Name
|
||||
}
|
||||
if req.Link != nil {
|
||||
bookmark.Link = *req.Link
|
||||
}
|
||||
if req.Detail != nil {
|
||||
bookmark.Detail = *req.Detail
|
||||
}
|
||||
if req.Description != nil {
|
||||
bookmark.Description = *req.Description
|
||||
}
|
||||
|
||||
// 更新父文件夹ID(如果提供且有效)
|
||||
if req.ParentPathId != nil && *req.ParentPathId != 0 {
|
||||
var parentFolder models.Folder
|
||||
if err := db.First(&parentFolder, *req.ParentPathId).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, api.Error{
|
||||
Errtype: "NotFoundError",
|
||||
Message: "Parent folder not found",
|
||||
})
|
||||
return
|
||||
}
|
||||
bookmark.ParentPathID = *req.ParentPathId
|
||||
}
|
||||
|
||||
// 保存更新
|
||||
if err := db.Save(&bookmark).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, api.Error{
|
||||
Errtype: "DatabaseError",
|
||||
Message: "Failed to update bookmark",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 构造响应
|
||||
response := api.BookmarkResponse{
|
||||
Id: bookmark.ID,
|
||||
Name: bookmark.Name,
|
||||
Link: &bookmark.Link,
|
||||
Detail: &bookmark.Detail,
|
||||
Description: &bookmark.Description,
|
||||
ParentPathId: bookmark.ParentPathID,
|
||||
CreatedAt: bookmark.CreatedAt,
|
||||
UpdatedAt: bookmark.UpdatedAt,
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, response)
|
||||
}
|
||||
|
||||
// UpdateFolder implements api.ServerInterface.
|
||||
func (b *BookMarksImpl) UpdateFolder(c *gin.Context, id int64) {
|
||||
var db = b.db
|
||||
var req api.FolderRequest
|
||||
|
||||
// 绑定请求参数
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, api.Error{
|
||||
Errtype: "ParameterError",
|
||||
Message: "Invalid request parameters",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 查找要更新的文件夹
|
||||
var folder models.Folder
|
||||
if err := db.First(&folder, id).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, api.Error{
|
||||
Errtype: "NotFoundError",
|
||||
Message: "Folder not found",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 更新文件夹名称(如果提供)
|
||||
if req.Name != "" {
|
||||
folder.Name = req.Name
|
||||
}
|
||||
|
||||
// 更新父文件夹ID(如果提供且有效)
|
||||
if req.ParentPathId != nil && *req.ParentPathId != 0 {
|
||||
// 不能将文件夹设置为自己的子文件夹
|
||||
if *req.ParentPathId == id {
|
||||
c.JSON(http.StatusBadRequest, api.Error{
|
||||
Errtype: "ParameterError",
|
||||
Message: "Cannot set folder as its own parent",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
var parentFolder models.Folder
|
||||
if err := db.First(&parentFolder, *req.ParentPathId).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, api.Error{
|
||||
Errtype: "NotFoundError",
|
||||
Message: "Parent folder not found",
|
||||
})
|
||||
return
|
||||
}
|
||||
folder.ParentPathID = *req.ParentPathId
|
||||
}
|
||||
|
||||
// 保存更新
|
||||
if err := db.Save(&folder).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, api.Error{
|
||||
Errtype: "DatabaseError",
|
||||
Message: "Failed to update folder",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 统计子文件夹数量
|
||||
var subFolderCount int64
|
||||
db.Model(&models.Folder{}).Where("parent_path_id = ?", id).Count(&subFolderCount)
|
||||
|
||||
// 统计书签数量
|
||||
var bookmarkCount int64
|
||||
db.Model(&models.Bookmark{}).Where("parent_path_id = ?", id).Count(&bookmarkCount)
|
||||
|
||||
// 构造响应
|
||||
response := api.FolderResponse{
|
||||
Id: folder.ID,
|
||||
Name: folder.Name,
|
||||
ParentPathId: folder.ParentPathID,
|
||||
CreatedAt: folder.CreatedAt,
|
||||
UpdatedAt: folder.UpdatedAt,
|
||||
SubFolderCount: int(subFolderCount),
|
||||
BookmarkCount: int(bookmarkCount),
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, response)
|
||||
}
|
||||
|
||||
// Make sure we conform to ServerInterface
|
||||
var _ api.ServerInterface = (*BookMarksImpl)(nil)
|
310
internal/handlers/bookmark_test.go
Normal file
310
internal/handlers/bookmark_test.go
Normal file
@ -0,0 +1,310 @@
|
||||
// internal/handlers/bookmark_test.go
|
||||
package handlers_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"git.zzyxyz.com/zzy/zzyxyz_go_api/gen/api"
|
||||
"git.zzyxyz.com/zzy/zzyxyz_go_api/internal/handlers"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
type BookmarkTestSuite struct {
|
||||
suite.Suite
|
||||
server *gin.Engine
|
||||
bookmarks *handlers.BookMarksImpl
|
||||
testDBPath string
|
||||
}
|
||||
|
||||
func (suite *BookmarkTestSuite) SetupSuite() {
|
||||
// 使用内存数据库进行测试
|
||||
suite.testDBPath = ":memory:"
|
||||
|
||||
// 设置Gin为测试模式
|
||||
gin.SetMode(gin.TestMode)
|
||||
|
||||
var err error
|
||||
suite.bookmarks, err = handlers.NewBookMarks(suite.testDBPath)
|
||||
assert.NoError(suite.T(), err)
|
||||
|
||||
suite.server = gin.New()
|
||||
api.RegisterHandlers(suite.server, suite.bookmarks)
|
||||
}
|
||||
|
||||
func (suite *BookmarkTestSuite) TestCreateBookmark() {
|
||||
// 准备测试数据
|
||||
detail := "Test detail"
|
||||
description := "Test description"
|
||||
link := "https://example.com"
|
||||
request := api.BookmarkRequest{
|
||||
Name: "Test Bookmark",
|
||||
Detail: &detail,
|
||||
Description: &description,
|
||||
Link: &link,
|
||||
}
|
||||
|
||||
// 将请求转换为JSON
|
||||
jsonData, _ := json.Marshal(request)
|
||||
req, _ := http.NewRequest("POST", "/bookmarks/v1/data", bytes.NewBuffer(jsonData))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
// 发送请求
|
||||
resp := httptest.NewRecorder()
|
||||
suite.server.ServeHTTP(resp, req)
|
||||
|
||||
// 验证响应
|
||||
assert.Equal(suite.T(), http.StatusCreated, resp.Code)
|
||||
|
||||
var response api.BookmarkResponse
|
||||
err := json.Unmarshal(resp.Body.Bytes(), &response)
|
||||
assert.NoError(suite.T(), err)
|
||||
assert.Equal(suite.T(), request.Name, response.Name)
|
||||
assert.Equal(suite.T(), *request.Detail, *response.Detail)
|
||||
assert.Equal(suite.T(), *request.Description, *response.Description)
|
||||
assert.Equal(suite.T(), int64(1), response.ParentPathId) // 默认根目录
|
||||
}
|
||||
|
||||
func (suite *BookmarkTestSuite) TestGetBookmark() {
|
||||
// 先创建一个书签
|
||||
detail := "Test detail for get"
|
||||
description := "Test description for get"
|
||||
link := "https://example.com/get"
|
||||
request := api.BookmarkRequest{
|
||||
Name: "Test Get Bookmark",
|
||||
Detail: &detail,
|
||||
Description: &description,
|
||||
Link: &link,
|
||||
}
|
||||
|
||||
jsonData, _ := json.Marshal(request)
|
||||
req, _ := http.NewRequest("POST", "/bookmarks/v1/data", bytes.NewBuffer(jsonData))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
resp := httptest.NewRecorder()
|
||||
suite.server.ServeHTTP(resp, req)
|
||||
|
||||
assert.Equal(suite.T(), http.StatusCreated, resp.Code)
|
||||
var createdBookmark api.BookmarkResponse
|
||||
err := json.Unmarshal(resp.Body.Bytes(), &createdBookmark)
|
||||
assert.NoError(suite.T(), err)
|
||||
|
||||
// 获取创建的书签
|
||||
req, _ = http.NewRequest("GET", fmt.Sprintf("/bookmarks/v1/data/%d", createdBookmark.Id), nil)
|
||||
resp = httptest.NewRecorder()
|
||||
suite.server.ServeHTTP(resp, req)
|
||||
|
||||
assert.Equal(suite.T(), http.StatusOK, resp.Code)
|
||||
|
||||
var response api.BookmarkResponse
|
||||
err = json.Unmarshal(resp.Body.Bytes(), &response)
|
||||
assert.NoError(suite.T(), err)
|
||||
assert.Equal(suite.T(), createdBookmark.Id, response.Id)
|
||||
assert.Equal(suite.T(), request.Name, response.Name)
|
||||
}
|
||||
|
||||
func (suite *BookmarkTestSuite) TestCreateFolder() {
|
||||
// 准备测试数据
|
||||
request := api.FolderRequest{
|
||||
Name: "Test Folder",
|
||||
}
|
||||
|
||||
// 将请求转换为JSON
|
||||
jsonData, _ := json.Marshal(request)
|
||||
req, _ := http.NewRequest("POST", "/bookmarks/v1/folder", bytes.NewBuffer(jsonData))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
// 发送请求
|
||||
resp := httptest.NewRecorder()
|
||||
suite.server.ServeHTTP(resp, req)
|
||||
|
||||
// 验证响应
|
||||
assert.Equal(suite.T(), http.StatusCreated, resp.Code)
|
||||
|
||||
var response api.FolderResponse
|
||||
err := json.Unmarshal(resp.Body.Bytes(), &response)
|
||||
assert.NoError(suite.T(), err)
|
||||
assert.Equal(suite.T(), request.Name, response.Name)
|
||||
assert.Equal(suite.T(), int64(1), response.ParentPathId) // 默认根目录
|
||||
}
|
||||
|
||||
func (suite *BookmarkTestSuite) TestGetFolderInfo() {
|
||||
// 先创建一个文件夹
|
||||
request := api.FolderRequest{
|
||||
Name: "Test Get Folder",
|
||||
}
|
||||
|
||||
jsonData, _ := json.Marshal(request)
|
||||
req, _ := http.NewRequest("POST", "/bookmarks/v1/folder", bytes.NewBuffer(jsonData))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
resp := httptest.NewRecorder()
|
||||
suite.server.ServeHTTP(resp, req)
|
||||
|
||||
assert.Equal(suite.T(), http.StatusCreated, resp.Code)
|
||||
var createdFolder api.FolderResponse
|
||||
err := json.Unmarshal(resp.Body.Bytes(), &createdFolder)
|
||||
assert.NoError(suite.T(), err)
|
||||
|
||||
// 获取创建的文件夹信息
|
||||
req, _ = http.NewRequest("GET", fmt.Sprintf("/bookmarks/v1/folder/%d", createdFolder.Id), nil)
|
||||
resp = httptest.NewRecorder()
|
||||
suite.server.ServeHTTP(resp, req)
|
||||
|
||||
assert.Equal(suite.T(), http.StatusOK, resp.Code)
|
||||
|
||||
var response api.FolderResponse
|
||||
err = json.Unmarshal(resp.Body.Bytes(), &response)
|
||||
assert.NoError(suite.T(), err)
|
||||
assert.Equal(suite.T(), createdFolder.Id, response.Id)
|
||||
assert.Equal(suite.T(), request.Name, response.Name)
|
||||
assert.Equal(suite.T(), 0, response.BookmarkCount)
|
||||
assert.Equal(suite.T(), 0, response.SubFolderCount)
|
||||
}
|
||||
|
||||
func (suite *BookmarkTestSuite) TestGetFolderContent() {
|
||||
// 创建一个文件夹
|
||||
folderRequest := api.FolderRequest{
|
||||
Name: "Content Test Folder",
|
||||
}
|
||||
|
||||
folderJson, _ := json.Marshal(folderRequest)
|
||||
req, _ := http.NewRequest("POST", "/bookmarks/v1/folder", bytes.NewBuffer(folderJson))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
resp := httptest.NewRecorder()
|
||||
suite.server.ServeHTTP(resp, req)
|
||||
|
||||
assert.Equal(suite.T(), http.StatusCreated, resp.Code)
|
||||
var createdFolder api.FolderResponse
|
||||
err := json.Unmarshal(resp.Body.Bytes(), &createdFolder)
|
||||
assert.NoError(suite.T(), err)
|
||||
|
||||
// 在该文件夹中创建一个书签
|
||||
detail := "Bookmark in folder"
|
||||
description := "Test bookmark in folder"
|
||||
link := "https://example.com/folder"
|
||||
bookmarkRequest := api.BookmarkRequest{
|
||||
Name: "Folder Bookmark",
|
||||
Detail: &detail,
|
||||
Description: &description,
|
||||
Link: &link,
|
||||
ParentPathId: &createdFolder.Id,
|
||||
}
|
||||
|
||||
bookmarkJson, _ := json.Marshal(bookmarkRequest)
|
||||
req, _ = http.NewRequest("POST", "/bookmarks/v1/data", bytes.NewBuffer(bookmarkJson))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
resp = httptest.NewRecorder()
|
||||
suite.server.ServeHTTP(resp, req)
|
||||
|
||||
assert.Equal(suite.T(), http.StatusCreated, resp.Code)
|
||||
|
||||
// 获取文件夹内容
|
||||
req, _ = http.NewRequest("GET", fmt.Sprintf("/bookmarks/v1/folder/%d/content", createdFolder.Id), nil)
|
||||
resp = httptest.NewRecorder()
|
||||
suite.server.ServeHTTP(resp, req)
|
||||
|
||||
assert.Equal(suite.T(), http.StatusOK, resp.Code)
|
||||
|
||||
var response api.FolderContentResponse
|
||||
err = json.Unmarshal(resp.Body.Bytes(), &response)
|
||||
assert.NoError(suite.T(), err)
|
||||
assert.Len(suite.T(), response.Bookmarks, 1)
|
||||
assert.Equal(suite.T(), "Folder Bookmark", response.Bookmarks[0].Name)
|
||||
assert.Len(suite.T(), response.SubFolders, 0)
|
||||
}
|
||||
|
||||
func (suite *BookmarkTestSuite) TestUpdateBookmark() {
|
||||
// 先创建一个书签
|
||||
detail := "Original detail"
|
||||
description := "Original description"
|
||||
link := "https://example.com/original"
|
||||
request := api.BookmarkRequest{
|
||||
Name: "Original Bookmark",
|
||||
Detail: &detail,
|
||||
Description: &description,
|
||||
Link: &link,
|
||||
}
|
||||
|
||||
jsonData, _ := json.Marshal(request)
|
||||
req, _ := http.NewRequest("POST", "/bookmarks/v1/data", bytes.NewBuffer(jsonData))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
resp := httptest.NewRecorder()
|
||||
suite.server.ServeHTTP(resp, req)
|
||||
|
||||
assert.Equal(suite.T(), http.StatusCreated, resp.Code)
|
||||
var createdBookmark api.BookmarkResponse
|
||||
err := json.Unmarshal(resp.Body.Bytes(), &createdBookmark)
|
||||
assert.NoError(suite.T(), err)
|
||||
|
||||
// 更新书签
|
||||
newName := "Updated Bookmark"
|
||||
newDetail := "Updated detail"
|
||||
updateRequest := api.BookmarkRequest{
|
||||
Name: newName,
|
||||
Detail: &newDetail,
|
||||
}
|
||||
|
||||
updateJson, _ := json.Marshal(updateRequest)
|
||||
req, _ = http.NewRequest("PUT", fmt.Sprintf("/bookmarks/v1/data/%d", createdBookmark.Id), bytes.NewBuffer(updateJson))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
resp = httptest.NewRecorder()
|
||||
suite.server.ServeHTTP(resp, req)
|
||||
|
||||
assert.Equal(suite.T(), http.StatusOK, resp.Code)
|
||||
|
||||
var response api.BookmarkResponse
|
||||
err = json.Unmarshal(resp.Body.Bytes(), &response)
|
||||
assert.NoError(suite.T(), err)
|
||||
assert.Equal(suite.T(), newName, response.Name)
|
||||
assert.Equal(suite.T(), newDetail, *response.Detail)
|
||||
// 确保更新时间发生了变化
|
||||
assert.True(suite.T(), response.UpdatedAt.After(response.CreatedAt) || response.UpdatedAt.Equal(response.CreatedAt))
|
||||
}
|
||||
|
||||
func (suite *BookmarkTestSuite) TestUpdateFolder() {
|
||||
// 先创建一个文件夹
|
||||
request := api.FolderRequest{
|
||||
Name: "Original Folder",
|
||||
}
|
||||
|
||||
jsonData, _ := json.Marshal(request)
|
||||
req, _ := http.NewRequest("POST", "/bookmarks/v1/folder", bytes.NewBuffer(jsonData))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
resp := httptest.NewRecorder()
|
||||
suite.server.ServeHTTP(resp, req)
|
||||
|
||||
assert.Equal(suite.T(), http.StatusCreated, resp.Code)
|
||||
var createdFolder api.FolderResponse
|
||||
err := json.Unmarshal(resp.Body.Bytes(), &createdFolder)
|
||||
assert.NoError(suite.T(), err)
|
||||
|
||||
// 更新文件夹
|
||||
newName := "Updated Folder"
|
||||
updateRequest := api.FolderRequest{
|
||||
Name: newName,
|
||||
}
|
||||
|
||||
updateJson, _ := json.Marshal(updateRequest)
|
||||
req, _ = http.NewRequest("PUT", fmt.Sprintf("/bookmarks/v1/folder/%d", createdFolder.Id), bytes.NewBuffer(updateJson))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
resp = httptest.NewRecorder()
|
||||
suite.server.ServeHTTP(resp, req)
|
||||
|
||||
assert.Equal(suite.T(), http.StatusOK, resp.Code)
|
||||
|
||||
var response api.FolderResponse
|
||||
err = json.Unmarshal(resp.Body.Bytes(), &response)
|
||||
assert.NoError(suite.T(), err)
|
||||
assert.Equal(suite.T(), newName, response.Name)
|
||||
// 确保更新时间发生了变化
|
||||
assert.True(suite.T(), response.UpdatedAt.After(response.CreatedAt) || response.UpdatedAt.Equal(response.CreatedAt))
|
||||
}
|
||||
|
||||
func TestBookmarkTestSuite(t *testing.T) {
|
||||
suite.Run(t, new(BookmarkTestSuite))
|
||||
}
|
277
internal/handlers/todo_list.go
Normal file
277
internal/handlers/todo_list.go
Normal file
@ -0,0 +1,277 @@
|
||||
// internal/handlers/todo_list.go
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type Task struct {
|
||||
ID int `json:"id"`
|
||||
Title string `json:"title" binding:"required"`
|
||||
Description string `json:"description"`
|
||||
Completed bool `json:"completed"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
UpdatedAt string `json:"updated_at"`
|
||||
}
|
||||
|
||||
type TaskRequest struct {
|
||||
Title string `json:"title" binding:"required"`
|
||||
Description string `json:"description"`
|
||||
}
|
||||
|
||||
// 使用内存存储任务数据,按命名空间隔离
|
||||
var (
|
||||
tasks = make(map[string]map[int]Task) // namespace -> taskID -> Task
|
||||
taskMutex = sync.RWMutex{}
|
||||
nextTaskIDs = make(map[string]int) // namespace -> nextID
|
||||
)
|
||||
|
||||
func TodoHandler(r *gin.RouterGroup) {
|
||||
todos := r.Group("/todo/v1")
|
||||
{
|
||||
todos.GET("/tasks", getTasks)
|
||||
todos.POST("/tasks", createTask)
|
||||
todos.PUT("/tasks/:id", updateTask)
|
||||
todos.DELETE("/tasks/:id", deleteTask)
|
||||
todos.PATCH("/tasks/:id/complete", completeTask)
|
||||
}
|
||||
}
|
||||
|
||||
// getNamespace 从请求头或查询参数中获取命名空间
|
||||
func getNamespace(c *gin.Context) string {
|
||||
// 优先从请求头获取命名空间
|
||||
namespace := c.GetHeader("X-Namespace")
|
||||
if namespace == "" {
|
||||
// 如果没有则从查询参数获取
|
||||
namespace = c.Query("namespace")
|
||||
}
|
||||
|
||||
// 如果都没有提供,默认使用"default"
|
||||
if namespace == "" {
|
||||
namespace = "default"
|
||||
}
|
||||
|
||||
return namespace
|
||||
}
|
||||
|
||||
// @Summary 获取任务列表
|
||||
// @Description 获取所有待办任务
|
||||
// @Tags todo
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param namespace header string false "命名空间"
|
||||
// @Param namespace query string false "命名空间"
|
||||
// @Success 200 {array} Task
|
||||
// @Router /todo/v1/tasks [get]
|
||||
func getTasks(c *gin.Context) {
|
||||
namespace := getNamespace(c)
|
||||
|
||||
taskMutex.RLock()
|
||||
defer taskMutex.RUnlock()
|
||||
|
||||
namespaceTasks, exists := tasks[namespace]
|
||||
if !exists {
|
||||
c.JSON(http.StatusOK, []Task{})
|
||||
return
|
||||
}
|
||||
|
||||
taskList := make([]Task, 0, len(namespaceTasks))
|
||||
for _, task := range namespaceTasks {
|
||||
taskList = append(taskList, task)
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, taskList)
|
||||
}
|
||||
|
||||
// @Summary 创建任务
|
||||
// @Description 创建一个新的待办任务
|
||||
// @Tags todo
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param namespace header string false "命名空间"
|
||||
// @Param namespace query string false "命名空间"
|
||||
// @Param task body TaskRequest true "任务信息"
|
||||
// @Success 201 {object} Task
|
||||
// @Router /todo/v1/tasks [post]
|
||||
func createTask(c *gin.Context) {
|
||||
namespace := getNamespace(c)
|
||||
|
||||
var req TaskRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
taskMutex.Lock()
|
||||
defer taskMutex.Unlock()
|
||||
|
||||
// 初始化命名空间
|
||||
if tasks[namespace] == nil {
|
||||
tasks[namespace] = make(map[int]Task)
|
||||
nextTaskIDs[namespace] = 1
|
||||
}
|
||||
|
||||
// 生成新任务ID
|
||||
taskID := nextTaskIDs[namespace]
|
||||
nextTaskIDs[namespace]++
|
||||
|
||||
// 创建任务
|
||||
now := time.Now().Format("2006-01-02 15:04:05")
|
||||
task := Task{
|
||||
ID: taskID,
|
||||
Title: req.Title,
|
||||
Description: req.Description,
|
||||
Completed: false,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
|
||||
// 存储任务
|
||||
tasks[namespace][taskID] = task
|
||||
|
||||
c.JSON(http.StatusCreated, task)
|
||||
}
|
||||
|
||||
// @Summary 更新任务
|
||||
// @Description 更新指定ID的任务
|
||||
// @Tags todo
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param namespace header string false "命名空间"
|
||||
// @Param namespace query string false "命名空间"
|
||||
// @Param id path int true "任务ID"
|
||||
// @Param task body TaskRequest true "任务信息"
|
||||
// @Success 200 {object} Task
|
||||
// @Router /todo/v1/tasks/{id} [put]
|
||||
func updateTask(c *gin.Context) {
|
||||
namespace := getNamespace(c)
|
||||
|
||||
id, err := strconv.Atoi(c.Param("id"))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid task ID"})
|
||||
return
|
||||
}
|
||||
|
||||
var req TaskRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
taskMutex.Lock()
|
||||
defer taskMutex.Unlock()
|
||||
|
||||
// 检查命名空间是否存在
|
||||
if tasks[namespace] == nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Task not found"})
|
||||
return
|
||||
}
|
||||
|
||||
// 检查任务是否存在
|
||||
task, exists := tasks[namespace][id]
|
||||
if !exists {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Task not found"})
|
||||
return
|
||||
}
|
||||
|
||||
// 更新任务
|
||||
now := time.Now().Format("2006-01-02 15:04:05")
|
||||
task.Title = req.Title
|
||||
task.Description = req.Description
|
||||
task.UpdatedAt = now
|
||||
|
||||
// 保存更新
|
||||
tasks[namespace][id] = task
|
||||
|
||||
c.JSON(http.StatusOK, task)
|
||||
}
|
||||
|
||||
// @Summary 删除任务
|
||||
// @Description 删除指定ID的任务
|
||||
// @Tags todo
|
||||
// @Produce json
|
||||
// @Param namespace header string false "命名空间"
|
||||
// @Param namespace query string false "命名空间"
|
||||
// @Param id path int true "任务ID"
|
||||
// @Success 200 {object} string
|
||||
// @Router /todo/v1/tasks/{id} [delete]
|
||||
func deleteTask(c *gin.Context) {
|
||||
namespace := getNamespace(c)
|
||||
|
||||
id, err := strconv.Atoi(c.Param("id"))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid task ID"})
|
||||
return
|
||||
}
|
||||
|
||||
taskMutex.Lock()
|
||||
defer taskMutex.Unlock()
|
||||
|
||||
// 检查命名空间是否存在
|
||||
if tasks[namespace] == nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Task not found"})
|
||||
return
|
||||
}
|
||||
|
||||
// 检查任务是否存在
|
||||
_, exists := tasks[namespace][id]
|
||||
if !exists {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Task not found"})
|
||||
return
|
||||
}
|
||||
|
||||
// 删除任务
|
||||
delete(tasks[namespace], id)
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Task deleted successfully"})
|
||||
}
|
||||
|
||||
// @Summary 完成任务
|
||||
// @Description 标记指定ID的任务为完成状态
|
||||
// @Tags todo
|
||||
// @Produce json
|
||||
// @Param namespace header string false "命名空间"
|
||||
// @Param namespace query string false "命名空间"
|
||||
// @Param id path int true "任务ID"
|
||||
// @Success 200 {object} Task
|
||||
// @Router /todo/v1/tasks/{id}/complete [patch]
|
||||
func completeTask(c *gin.Context) {
|
||||
namespace := getNamespace(c)
|
||||
|
||||
id, err := strconv.Atoi(c.Param("id"))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid task ID"})
|
||||
return
|
||||
}
|
||||
|
||||
taskMutex.Lock()
|
||||
defer taskMutex.Unlock()
|
||||
|
||||
// 检查命名空间是否存在
|
||||
if tasks[namespace] == nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Task not found"})
|
||||
return
|
||||
}
|
||||
|
||||
// 检查任务是否存在
|
||||
task, exists := tasks[namespace][id]
|
||||
if !exists {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Task not found"})
|
||||
return
|
||||
}
|
||||
|
||||
// 标记为完成
|
||||
now := time.Now().Format("2006-01-02 15:04:05")
|
||||
task.Completed = true
|
||||
task.UpdatedAt = now
|
||||
|
||||
// 保存更新
|
||||
tasks[namespace][id] = task
|
||||
|
||||
c.JSON(http.StatusOK, task)
|
||||
}
|
55
internal/models/bookmark.go
Normal file
55
internal/models/bookmark.go
Normal file
@ -0,0 +1,55 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// Bookmark 书签结构体
|
||||
type Bookmark struct {
|
||||
ID int64 `json:"id" gorm:"primaryKey"`
|
||||
Name string `json:"name" gorm:"not null;index;size:255"`
|
||||
Link string `json:"link" gorm:"not null"`
|
||||
Detail string `json:"detail" gorm:"type:text"`
|
||||
Description string `json:"description" gorm:"type:text"`
|
||||
ParentPathID int64 `json:"parent_path_id" gorm:"index;not null;default:1"`
|
||||
CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime"`
|
||||
UpdatedAt time.Time `json:"updated_at" gorm:"autoUpdateTime"`
|
||||
DeletedAt gorm.DeletedAt `json:"deleted_at" gorm:"index"`
|
||||
}
|
||||
|
||||
// Folder 文件夹结构体
|
||||
type Folder struct {
|
||||
ID int64 `json:"id" gorm:"primaryKey"`
|
||||
Name string `json:"name" gorm:"not null;index;size:255"`
|
||||
ParentPathID int64 `json:"parent_path_id" gorm:"index;not null;default:1"`
|
||||
CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime"`
|
||||
UpdatedAt time.Time `json:"updated_at" gorm:"autoUpdateTime"`
|
||||
DeletedAt gorm.DeletedAt `json:"deleted_at" gorm:"index"`
|
||||
}
|
||||
|
||||
// IsValidParent 检查父文件夹ID是否有效
|
||||
func (f *Folder) IsValidParent(db *gorm.DB, parentID int64) bool {
|
||||
// 检查父文件夹是否存在且未被删除
|
||||
if err := db.Where("id = ?", parentID).First(&Folder{}).Error; err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// 防止循环引用(不能将文件夹设置为自己的子文件夹)
|
||||
if parentID == f.ID {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// IsValidParent 检查书签的父文件夹ID是否有效
|
||||
func (b *Bookmark) IsValidParent(db *gorm.DB, parentID int64) bool {
|
||||
// 检查父文件夹是否存在且未被删除
|
||||
if err := db.Where("id = ?", parentID).First(&Folder{}).Error; err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
70
main.go
Normal file
70
main.go
Normal file
@ -0,0 +1,70 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"git.zzyxyz.com/zzy/zzyxyz_go_api/gen/api"
|
||||
"git.zzyxyz.com/zzy/zzyxyz_go_api/internal/handlers"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
//go:generate go tool oapi-codegen -config config/cfg.yaml config/api.yaml
|
||||
|
||||
//go:embed config/api.yaml dist/*
|
||||
var staticFiles embed.FS
|
||||
|
||||
func Helloworld(g *gin.Context) {
|
||||
g.JSON(http.StatusOK, "helloworld")
|
||||
}
|
||||
|
||||
func main() {
|
||||
router := gin.Default()
|
||||
|
||||
router.GET("/ping", func(c *gin.Context) {
|
||||
c.JSON(200, gin.H{
|
||||
"message": "pong",
|
||||
})
|
||||
})
|
||||
|
||||
api_router := router.Group("/api")
|
||||
{
|
||||
// create a type that satisfies the `api.ServerInterface`,
|
||||
// which contains an implementation of every operation from the generated code
|
||||
if server, err := handlers.NewBookMarks("user.sqlite3"); err != nil {
|
||||
log.Fatal("Failed to create bookmarks server:", err)
|
||||
} else {
|
||||
api.RegisterHandlers(api_router, server)
|
||||
}
|
||||
|
||||
eg := api_router.Group("/example")
|
||||
{
|
||||
eg.GET("/helloworld", Helloworld)
|
||||
}
|
||||
}
|
||||
|
||||
handlers.TodoHandler(api_router)
|
||||
|
||||
// FIXME 可能有更好的方式实现这个代码
|
||||
// 提供嵌入的静态文件访问 - OpenAPI YAML 文件和 dist 目录
|
||||
router.GET("/swagger.yaml", func(c *gin.Context) {
|
||||
file, _ := staticFiles.ReadFile("config/api.yaml")
|
||||
c.Data(http.StatusOK, "application/x-yaml", file)
|
||||
})
|
||||
router.GET("/swagger/*filepath", func(ctx *gin.Context) {
|
||||
// 直接修改请求路径实现映射
|
||||
r := ctx.Request
|
||||
originalPath := r.URL.Path
|
||||
// 将 /swagger/* 映射为 /dist/*
|
||||
r.URL.Path = "/dist" + ctx.Param("filepath")
|
||||
|
||||
fs := http.FileServer(http.FS(staticFiles))
|
||||
fs.ServeHTTP(ctx.Writer, r)
|
||||
|
||||
// 恢复原始路径
|
||||
r.URL.Path = originalPath
|
||||
})
|
||||
log.Fatal(router.Run("127.0.0.1:8080"))
|
||||
}
|
Reference in New Issue
Block a user