```
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:
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,6 +1,7 @@
|
||||
.*/
|
||||
dist/
|
||||
gen/
|
||||
bin/
|
||||
|
||||
*.exe
|
||||
*.sqlite3
|
||||
|
38
README.md
38
README.md
@ -20,3 +20,41 @@ 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
|
||||
```
|
||||
|
||||
```shell
|
||||
# https://go-lang.org.cn/doc/tutorial/govulncheck
|
||||
go install golang.org/x/vuln/cmd/govulncheck@latest
|
||||
govulncheck ./...
|
||||
|
||||
go install github.com/magefile/mage@latest
|
||||
mage -init
|
||||
|
||||
mage build
|
||||
```
|
||||
|
||||
```go
|
||||
//go:build mage
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/magefile/mage/sh"
|
||||
)
|
||||
|
||||
// Runs go mod download and then installs the binary.
|
||||
func Build() error {
|
||||
if err := sh.Run("go", "mod", "download"); err != nil {
|
||||
return err
|
||||
}
|
||||
return sh.Run("go", "install", "./...")
|
||||
}
|
||||
```
|
||||
|
||||
```json
|
||||
// on vscode settings.json
|
||||
{
|
||||
"gopls": {
|
||||
"buildFlags": ["-tags=mage"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
1
go.mod
1
go.mod
@ -4,6 +4,7 @@ go 1.25.1
|
||||
|
||||
require (
|
||||
github.com/gin-gonic/gin v1.10.1
|
||||
github.com/magefile/mage v1.15.0
|
||||
github.com/mattn/go-sqlite3 v1.14.32
|
||||
github.com/oapi-codegen/runtime v1.1.2
|
||||
github.com/stretchr/testify v1.11.1
|
||||
|
2
go.sum
2
go.sum
@ -88,6 +88,8 @@ 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/magefile/mage v1.15.0 h1:BvGheCMAsG3bWUDbZ8AyXXpCNwU9u5CB6sM+HNb9HYg=
|
||||
github.com/magefile/mage v1.15.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A=
|
||||
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=
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
|
81
magefile.go
Normal file
81
magefile.go
Normal file
@ -0,0 +1,81 @@
|
||||
//go:build mage
|
||||
// +build mage
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
|
||||
"github.com/magefile/mage/mg" // mg contains helpful utility functions, like Deps
|
||||
)
|
||||
|
||||
// Default target to run when none is specified
|
||||
// If not set, running mage will list available targets
|
||||
// var Default = Build
|
||||
|
||||
// A build step that requires additional params, or platform specific steps for example
|
||||
// Build builds the application for multiple platforms.
|
||||
func Build() error {
|
||||
mg.Deps(InstallDeps)
|
||||
|
||||
// Define target platforms
|
||||
platforms := []struct {
|
||||
OS string
|
||||
Arch string
|
||||
}{
|
||||
{"linux", "amd64"},
|
||||
{"linux", "arm64"},
|
||||
{"darwin", "amd64"},
|
||||
{"darwin", "arm64"},
|
||||
{"windows", "amd64"},
|
||||
}
|
||||
|
||||
for _, p := range platforms {
|
||||
fmt.Printf("Building for %s/%s...\n", p.OS, p.Arch)
|
||||
|
||||
// Set environment variables for cross-compilation
|
||||
env := append(os.Environ(),
|
||||
fmt.Sprintf("GOOS=%s", p.OS),
|
||||
fmt.Sprintf("GOARCH=%s", p.Arch))
|
||||
|
||||
// Determine output name
|
||||
outputName := fmt.Sprintf("./bin/zzyxyz_go_api-%s-%s", p.OS, p.Arch)
|
||||
if p.OS == "windows" {
|
||||
outputName += ".exe"
|
||||
}
|
||||
|
||||
// Run build command
|
||||
cmd := exec.Command("go", "build", "-o", outputName, ".")
|
||||
cmd.Env = env
|
||||
|
||||
if err := cmd.Run(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// A custom install step if you need your bin someplace other than go/bin
|
||||
func Install() error {
|
||||
mg.Deps(Build)
|
||||
// fmt.Println("Installing...")
|
||||
// return os.Rename("./MyApp", "/usr/bin/MyApp")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Manage your deps, or running package managers.
|
||||
func InstallDeps() error {
|
||||
fmt.Println("Installing Deps...")
|
||||
// cmd := exec.Command("go", "get", "github.com/stretchr/piglatin")
|
||||
// return cmd.Run()
|
||||
return nil
|
||||
}
|
||||
|
||||
// Clean up after yourself
|
||||
func Clean() {
|
||||
fmt.Println("Cleaning...")
|
||||
// os.RemoveAll("MyApp")
|
||||
}
|
18
main.go
18
main.go
@ -16,11 +16,8 @@ import (
|
||||
//go:embed config/api.yaml dist/*
|
||||
var staticFiles embed.FS
|
||||
|
||||
func Helloworld(g *gin.Context) {
|
||||
g.JSON(http.StatusOK, "helloworld")
|
||||
}
|
||||
|
||||
func main() {
|
||||
gin.SetMode(gin.ReleaseMode)
|
||||
router := gin.Default()
|
||||
|
||||
router.GET("/ping", func(c *gin.Context) {
|
||||
@ -39,14 +36,9 @@ func main() {
|
||||
api.RegisterHandlers(api_router, server)
|
||||
}
|
||||
|
||||
eg := api_router.Group("/example")
|
||||
{
|
||||
eg.GET("/helloworld", Helloworld)
|
||||
}
|
||||
handlers.TodoHandler(api_router)
|
||||
}
|
||||
|
||||
handlers.TodoHandler(api_router)
|
||||
|
||||
// FIXME 可能有更好的方式实现这个代码
|
||||
// 提供嵌入的静态文件访问 - OpenAPI YAML 文件和 dist 目录
|
||||
router.GET("/swagger.yaml", func(c *gin.Context) {
|
||||
@ -66,5 +58,9 @@ func main() {
|
||||
// 恢复原始路径
|
||||
r.URL.Path = originalPath
|
||||
})
|
||||
log.Fatal(router.Run("127.0.0.1:8080"))
|
||||
|
||||
var listener = "127.0.0.1:8080"
|
||||
log.Printf("Starting server at http://%s", listener)
|
||||
log.Printf("Swagger UI: http://%s/swagger/index.html", listener)
|
||||
log.Fatal(router.Run(listener))
|
||||
}
|
||||
|
Reference in New Issue
Block a user