```
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:
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)
|
||||
}
|
Reference in New Issue
Block a user