Files
zzyxyz_go_api/internal/handlers/bookmark.go
zzy 36b311ff86 ```
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 模式
- 统一服务监听地址为变量,并增加启动日志提示
```
2025-09-21 15:45:37 +08:00

527 lines
13 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// internal/handlers/note_link.go
package handlers
import (
"fmt"
"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
}
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
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.Limit(1).Find(&rootFolder, forlder_root_id)
if result.Error != nil {
return nil, result.Error
}
if result.RowsAffected == 0 {
// 根文件夹不存在,创建它
rootFolder = models.Folder{
ID: forlder_root_id,
Name: "Root",
ParentPathID: forlder_root_id, // 根目录指向自己
}
if err := db.Create(&rootFolder).Error; err != nil {
return nil, fmt.Errorf("failed to create root folder: %w", err)
}
}
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
}
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 := bookmarkReq2Model(req, parentID)
if err := db.Create(&bookmark).Error; err != nil {
c.JSON(http.StatusInternalServerError, api.Error{
Errtype: "DatabaseError",
Message: "Failed to create bookmark",
})
return
}
response := bookmarkModel2Res(bookmark)
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
}
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 := folderReq2Model(req, parentID)
if err := db.Create(&folder).Error; err != nil {
c.JSON(http.StatusInternalServerError, api.Error{
Errtype: "DatabaseError",
Message: "Failed to create folder",
})
return
}
response := folderModel2Res(folder)
c.JSON(http.StatusCreated, response)
}
// DeleteBookmark implements api.ServerInterface.
func (b *BookMarksImpl) DeleteBookmark(c *gin.Context, id int64) {
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) {
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) {
c.JSON(http.StatusNotImplemented, api.Error{
Errtype: "error",
Message: "Not implemented",
})
}
// 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 := bookmarkModel2Res(bookmark)
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 := bookmarkModel2Res(bookmark)
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 := folderModel2Res(folder)
c.JSON(http.StatusOK, response)
}
// Make sure we conform to ServerInterface
var _ api.ServerInterface = (*BookMarksImpl)(nil)