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 模式 - 统一服务监听地址为变量,并增加启动日志提示 ```
527 lines
13 KiB
Go
527 lines
13 KiB
Go
// 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)
|