feat(bookmark): 实现书签和文件夹的增删改查功能

- 添加 GetFolderDefaultRoot 方法用于获取默认根文件夹
- 实现 bookmark 和 folder 的请求/模型/响应转换函数
- 完善 CreateBookmark、CreateFolder、DeleteBookmark、DeleteFolder 等接口逻辑
- 支持删除空文件夹,非空文件夹禁止删除
- 修复根文件夹创建逻辑并添加错误处理
- 删除未实现的示例路由和无用代码

build: 引入 mage 构建工具支持多平台编译

- 添加 magefile.go 实现跨平台构建脚本
- 更新 go.mod 和 go.sum 引入 github.com/magefile/mage 依赖
- 在 README 中补充 mage 使用说明和 VSCode 配置

docs: 更新 README 并添加 mage 使用示例

- 添加 govulncheck 和 mage 构建相关文档
- 补充 VSCode gopls 配置说明

ci: 更新 .gitignore 忽略 bin 目录和可执行文件

- 添加 bin/ 到忽略列表
- 统一忽略 *.exe 和 *.sqlite3 文件

refactor(main): 优化主函数启动逻辑与日志输出

- 移除示例 helloworld 路由
- 设置 gin 为 ReleaseMode 模式
- 统一服务监听地址为变量,并增加启动日志提示
```
This commit is contained in:
zzy
2025-09-21 15:45:37 +08:00
parent 7ff8591be8
commit 36b311ff86
7 changed files with 295 additions and 99 deletions

View File

@ -3,6 +3,7 @@
package handlers
import (
"fmt"
"net/http"
"git.zzyxyz.com/zzy/zzyxyz_go_api/gen/api"
@ -17,6 +18,71 @@ type BookMarksImpl struct {
db *gorm.DB
}
const forlder_root_id = 1
func (b *BookMarksImpl) GetFolderDefaultRoot(folderID *int64) (*models.Folder, error) {
var db *gorm.DB = b.db
var real_root_id int64 = forlder_root_id
// 设置默认父文件夹ID为根目录(1)
parentID := real_root_id
if folderID != nil && *folderID != 0 {
parentID = *folderID
}
// 检查文件夹是否存在Find 不会在找不到记录时返回错误)
var parentFolder models.Folder
result := db.Limit(1).Find(&parentFolder, parentID)
if result.Error != nil {
return nil, result.Error
}
// 检查是否找到了记录
if result.RowsAffected == 0 {
return nil, nil
}
return &parentFolder, nil
}
func bookmarkReq2Model(req api.BookmarkRequest, parentID int64) models.Bookmark {
return models.Bookmark{
Name: req.Name,
Detail: *req.Detail,
Description: *req.Description,
ParentPathID: parentID,
}
}
func bookmarkModel2Res(bookmark models.Bookmark) api.BookmarkResponse {
return api.BookmarkResponse{
Id: bookmark.ID,
Name: bookmark.Name,
Link: &bookmark.Link,
Detail: &bookmark.Detail,
Description: &bookmark.Description,
ParentPathId: bookmark.ParentPathID,
CreatedAt: bookmark.CreatedAt,
}
}
func folderReq2Model(req api.FolderRequest, parentID int64) models.Folder {
return models.Folder{
Name: req.Name,
ParentPathID: parentID,
}
}
func folderModel2Res(folder models.Folder) api.FolderResponse {
return api.FolderResponse{
Id: folder.ID,
Name: folder.Name,
ParentPathId: folder.ParentPathID,
CreatedAt: folder.CreatedAt,
UpdatedAt: folder.UpdatedAt,
}
}
func NewBookMarks(dbPath string) (*BookMarksImpl, error) {
var err error
var db *gorm.DB
@ -33,15 +99,21 @@ func NewBookMarks(dbPath string) (*BookMarksImpl, error) {
// 创建根文件夹(如果不存在)
var rootFolder models.Folder
result := db.First(&rootFolder, 1)
result := db.Limit(1).Find(&rootFolder, forlder_root_id)
if result.Error != nil {
return nil, result.Error
}
if result.RowsAffected == 0 {
// 根文件夹不存在,创建它
rootFolder = models.Folder{
ID: 1,
ID: forlder_root_id,
Name: "Root",
ParentPathID: 1, // 根目录指向自己
ParentPathID: forlder_root_id, // 根目录指向自己
}
if err := db.Create(&rootFolder).Error; err != nil {
return nil, fmt.Errorf("failed to create root folder: %w", err)
}
db.Create(&rootFolder)
}
return &BookMarksImpl{db: db}, nil
@ -59,29 +131,18 @@ func (b *BookMarksImpl) CreateBookmark(c *gin.Context) {
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 {
var parentID int64
if folder, err := b.GetFolderDefaultRoot(req.ParentPathId); err != nil || folder == nil {
c.JSON(http.StatusNotFound, api.Error{
Errtype: "NotFoundError",
Message: "Parent folder not found",
})
return
} else {
parentID = folder.ID
}
// 创建书签
bookmark := models.Bookmark{
Name: req.Name,
Detail: *req.Detail,
Description: *req.Description,
ParentPathID: parentID,
}
bookmark := bookmarkReq2Model(req, parentID)
if err := db.Create(&bookmark).Error; err != nil {
c.JSON(http.StatusInternalServerError, api.Error{
@ -91,17 +152,7 @@ func (b *BookMarksImpl) CreateBookmark(c *gin.Context) {
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,
}
response := bookmarkModel2Res(bookmark)
c.JSON(http.StatusCreated, response)
}
@ -117,27 +168,19 @@ func (b *BookMarksImpl) CreateFolder(c *gin.Context) {
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 {
var parentID int64
if folder, err := b.GetFolderDefaultRoot(req.ParentPathId); err != nil || folder == nil {
c.JSON(http.StatusNotFound, api.Error{
Errtype: "NotFoundError",
Message: "Parent folder not found",
})
return
} else {
parentID = folder.ID
}
// 创建文件夹
folder := models.Folder{
Name: req.Name,
ParentPathID: parentID,
}
folder := folderReq2Model(req, parentID)
if err := db.Create(&folder).Error; err != nil {
c.JSON(http.StatusInternalServerError, api.Error{
@ -147,31 +190,94 @@ func (b *BookMarksImpl) CreateFolder(c *gin.Context) {
return
}
// 构造响应
response := api.FolderResponse{
Id: folder.ID,
Name: folder.Name,
ParentPathId: folder.ParentPathID,
CreatedAt: folder.CreatedAt,
UpdatedAt: folder.UpdatedAt,
}
response := folderModel2Res(folder)
c.JSON(http.StatusCreated, response)
}
// DeleteBookmark implements api.ServerInterface.
func (b *BookMarksImpl) DeleteBookmark(c *gin.Context, id int64) {
panic("unimplemented")
var db = b.db
var bookmark models.Bookmark
// 查询书签是否存在
if err := db.First(&bookmark, id).Error; err != nil {
// FIXME maybe use 204 means already deleted status is same as delete
c.JSON(http.StatusNotFound, api.Error{
Errtype: "NotFoundError",
Message: "Bookmark not found",
})
return
}
// 删除书签
if err := db.Delete(&bookmark).Error; err != nil {
c.JSON(http.StatusInternalServerError, api.Error{
Errtype: "DatabaseError",
Message: "Failed to delete bookmark",
})
return
}
c.Status(http.StatusNoContent)
}
// DeleteFolder implements api.ServerInterface.
func (b *BookMarksImpl) DeleteFolder(c *gin.Context, id int64) {
panic("unimplemented")
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
}
if folder.ID == folder.ParentPathID {
c.JSON(http.StatusBadRequest, api.Error{
Errtype: "ParameterError",
Message: "Cannot delete root 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)
// 如果文件夹不为空,拒绝删除
if subFolderCount > 0 || bookmarkCount > 0 {
c.JSON(http.StatusBadRequest, api.Error{
Errtype: "ParameterError",
Message: "Cannot delete non-empty folder, please delete contents first",
})
return
}
// 删除空文件夹
if err := db.Delete(&folder).Error; err != nil {
c.JSON(http.StatusInternalServerError, api.Error{
Errtype: "DatabaseError",
Message: "Failed to delete folder",
})
return
}
c.Status(http.StatusNoContent)
}
// DeleteFolderContent implements api.ServerInterface.
func (b *BookMarksImpl) DeleteFolderContent(c *gin.Context, id int64, params api.DeleteFolderContentParams) {
panic("unimplemented")
c.JSON(http.StatusNotImplemented, api.Error{
Errtype: "error",
Message: "Not implemented",
})
}
// GetBookmark implements api.ServerInterface.
@ -189,17 +295,7 @@ func (b *BookMarksImpl) GetBookmark(c *gin.Context, id int64) {
}
// 构造响应
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,
}
response := bookmarkModel2Res(bookmark)
c.JSON(http.StatusOK, response)
}
@ -349,17 +445,7 @@ func (b *BookMarksImpl) UpdateBookmark(c *gin.Context, id int64) {
}
// 构造响应
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,
}
response := bookmarkModel2Res(bookmark)
c.JSON(http.StatusOK, response)
}
@ -432,16 +518,7 @@ func (b *BookMarksImpl) UpdateFolder(c *gin.Context, id 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),
}
response := folderModel2Res(folder)
c.JSON(http.StatusOK, response)
}