feat(bookmark): 初始化书签服务并配置路由与权限控制

新增书签服务主程序,使用 Gin 框架搭建 HTTP 服务器,并注册书签相关接口处理器。
配置 CORS 中间件以支持跨域请求。服务监听端口为 8081。

feat(user_np): 初始化用户权限服务并注册认证接口

新增用户权限服务主程序,使用 Gin 框架搭建 HTTP 服务器,并注册登录、注册及修改密码等接口处理器。
服务监听端口为 8082。

refactor(config): 重构 OpenAPI 配置文件结构并拆分模块

将原有合并的 OpenAPI 配置文件按功能模块拆分为 bookmark 和 user_np 两个独立目录,
分别管理各自的 server、client 及 API 定义文件,便于后续维护和扩展。

refactor(vfs): 调整虚拟文件系统 API 接口路径与参数定义

更新 VFS API 配置文件,修改部分接口路径及参数结构,
如将文件路径参数由 path 转为 query 参数,并优化响应结构体定义。
This commit is contained in:
zzy
2025-09-23 21:52:51 +08:00
parent 60d6628b0d
commit 1e81e603de
26 changed files with 1832 additions and 1685 deletions

View File

@ -3,16 +3,10 @@
package handlers
import (
"crypto/rand"
"encoding/hex"
"fmt"
"log"
"net/http"
"net/url"
api "git.zzyxyz.com/zzy/zzyxyz_go_api/gen/bookmarks"
"git.zzyxyz.com/zzy/zzyxyz_go_api/internal/models"
"github.com/casbin/casbin/v2"
"github.com/gin-gonic/gin"
_ "github.com/mattn/go-sqlite3"
"gorm.io/driver/sqlite"
@ -23,27 +17,9 @@ type BookMarksImpl struct {
db *gorm.DB
}
// GetFolderMounts implements api.ServerInterface.
func (b *BookMarksImpl) GetFolderMounts(c *gin.Context, id int64) {
panic("unimplemented")
}
// MountFolder implements api.ServerInterface.
func (b *BookMarksImpl) MountFolder(c *gin.Context, id int64) {
panic("unimplemented")
}
// UnmountFolder implements api.ServerInterface.
func (b *BookMarksImpl) UnmountFolder(c *gin.Context, id int64) {
panic("unimplemented")
}
const forlder_root_id = 1
var enforcer *casbin.Enforcer
var adminToken *string
func validateApiKey(url *url.URL, apiKey string) bool {
func validateApiKey(apiKey string) bool {
if adminToken != nil && apiKey == *adminToken {
return true
}
@ -58,8 +34,9 @@ func AuthMiddleware() api.MiddlewareFunc {
// 提取 API Key
apiKey := c.GetHeader("X-BookMark-Token")
return
// 验证 API Key您需要实现这个逻辑
if apiKey == "" || !validateApiKey(c.Request.URL, apiKey) {
if apiKey == "" || !validateApiKey(apiKey) {
c.JSON(http.StatusUnauthorized, api.Error{
Errtype: "Unauthorized",
Message: "Invalid or missing API key",
@ -73,56 +50,17 @@ func AuthMiddleware() api.MiddlewareFunc {
}
func NewBookMarkPermission() (*api.GinServerOptions, error) {
// 一行代码生成安全的随机token
token := make([]byte, 16)
rand.Read(token) // 忽略错误处理以简化代码(生产环境建议处理)
tokenStr := hex.EncodeToString(token)
adminToken = &tokenStr
log.Printf("Admin API Token (for Swagger testing): %s", *adminToken)
if e, err := casbin.NewEnforcer("./config/model.conf", ".data/policy.csv"); err == nil {
log.Fatalf("Failed to create casbin enforcer: %v", err)
} else {
enforcer = e
}
return &api.GinServerOptions{
Middlewares: []api.MiddlewareFunc{AuthMiddleware()},
}, nil
}
func (b *BookMarksImpl) GetFolderDefaultRoot(folderID *int64) (*models.Folder, error) {
var db *gorm.DB = b.db
var real_root_id int64 = forlder_root_id
// 设置默认父文件夹ID为根目录
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 {
func bookmarkReq2Model(req api.BookmarkRequest) models.Bookmark {
return models.Bookmark{
Name: req.Name,
Link: req.Link,
Detail: req.Detail,
Description: req.Description,
ParentID: parentID,
}
}
@ -133,28 +71,10 @@ func bookmarkModel2Res(bookmark models.Bookmark) api.BookmarkResponse {
Link: bookmark.Link,
Detail: bookmark.Detail,
Description: bookmark.Description,
ParentId: bookmark.ParentID,
CreatedAt: bookmark.CreatedAt,
}
}
func folderReq2Model(req api.FolderRequest, parentID *int64) models.Folder {
return models.Folder{
Name: req.Name,
ParentID: parentID,
}
}
func folderModel2Res(folder models.Folder) api.FolderResponse {
return api.FolderResponse{
Id: folder.ID,
Name: folder.Name,
ParentId: folder.ParentID,
CreatedAt: folder.CreatedAt,
UpdatedAt: folder.UpdatedAt,
}
}
func NewBookMarks(dbPath string) (*BookMarksImpl, error) {
var err error
var db *gorm.DB
@ -164,35 +84,27 @@ func NewBookMarks(dbPath string) (*BookMarksImpl, error) {
}
// 自动迁移表结构
err = db.AutoMigrate(&models.Folder{}, &models.Bookmark{})
err = db.AutoMigrate(&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",
ParentID: nil, // 根目录指向NULL
}
if err := db.Create(&rootFolder).Error; err != nil {
return nil, fmt.Errorf("failed to create root folder: %w", err)
}
}
return &BookMarksImpl{db: db}, nil
}
func (b *BookMarksImpl) FindBMFromExternalID(externalID int64) (models.Bookmark, error) {
var db = b.db
var bookmark models.Bookmark
// 使用ExternalID查询书签
if err := db.Where("external_id = ?", externalID).First(&bookmark).Error; err != nil {
return bookmark, err
}
return bookmark, nil
}
// CreateBookmark implements api.ServerInterface.
func (b *BookMarksImpl) CreateBookmark(c *gin.Context) {
func (b *BookMarksImpl) CreateBookmark(c *gin.Context, id int64) {
var db = b.db
var req api.BookmarkRequest
if err := c.ShouldBindJSON(&req); err != nil {
@ -203,19 +115,27 @@ func (b *BookMarksImpl) CreateBookmark(c *gin.Context) {
return
}
var parentID int64
if folder, err := b.GetFolderDefaultRoot(req.ParentId); err != nil || folder == nil {
c.JSON(http.StatusNotFound, api.Error{
Errtype: "NotFoundError",
Message: "Parent folder not found",
// 检查外部ID是否已经存在
var existingBookmark models.Bookmark
result := db.Where("external_id = ?", id).First(&existingBookmark)
if result.Error == nil {
// ExternalID已存在返回冲突错误
c.JSON(http.StatusConflict, api.Error{
Errtype: "ConflictError",
Message: "Bookmark with this External ID already exists",
})
return
} else if result.Error != gorm.ErrRecordNotFound {
// 数据库查询出错
c.JSON(http.StatusInternalServerError, api.Error{
Errtype: "DatabaseError",
Message: "Database query error",
})
return
} else {
parentID = folder.ID
}
bookmark := bookmarkReq2Model(req, parentID)
bookmark := bookmarkReq2Model(req)
bookmark.ExternalID = id // 设置外部ID
if err := db.Create(&bookmark).Error; err != nil {
c.JSON(http.StatusInternalServerError, api.Error{
Errtype: "DatabaseError",
@ -228,61 +148,13 @@ func (b *BookMarksImpl) CreateBookmark(c *gin.Context) {
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.ParentId); 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 {
// 使用ExternalID删除书签
if err := db.Where("external_id = ?", id).Delete(&bookmark).Error; err != nil {
c.JSON(http.StatusInternalServerError, api.Error{
Errtype: "DatabaseError",
Message: "Failed to delete bookmark",
@ -293,72 +165,13 @@ func (b *BookMarksImpl) DeleteBookmark(c *gin.Context, id int64) {
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.ParentID == nil {
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 {
// 使用ExternalID查询书签
if err := db.Where("external_id = ?", id).First(&bookmark).Error; err != nil {
c.JSON(http.StatusNotFound, api.Error{
Errtype: "NotFoundError",
Message: "Bookmark not found",
@ -371,91 +184,6 @@ func (b *BookMarksImpl) GetBookmark(c *gin.Context, id int64) {
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,
ParentId: folder.ParentID,
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
@ -470,9 +198,9 @@ func (b *BookMarksImpl) UpdateBookmark(c *gin.Context, id int64) {
return
}
// 查找要更新的书签
// 查找要更新的书签使用ExternalID
var bookmark models.Bookmark
if err := db.First(&bookmark, id).Error; err != nil {
if err := db.Where("external_id = ?", id).First(&bookmark).Error; err != nil {
c.JSON(http.StatusNotFound, api.Error{
Errtype: "NotFoundError",
Message: "Bookmark not found",
@ -494,19 +222,6 @@ func (b *BookMarksImpl) UpdateBookmark(c *gin.Context, id int64) {
bookmark.Description = req.Description
}
// 更新父文件夹ID如果提供且有效
if req.ParentId != nil && *req.ParentId != 0 {
var parentFolder models.Folder
if err := db.First(&parentFolder, *req.ParentId).Error; err != nil {
c.JSON(http.StatusNotFound, api.Error{
Errtype: "NotFoundError",
Message: "Parent folder not found",
})
return
}
bookmark.ParentID = *req.ParentId
}
// 保存更新
if err := db.Save(&bookmark).Error; err != nil {
c.JSON(http.StatusInternalServerError, api.Error{
@ -521,88 +236,5 @@ func (b *BookMarksImpl) UpdateBookmark(c *gin.Context, id int64) {
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.ParentId != nil && *req.ParentId != 0 {
// 不能将文件夹设置为自己的子文件夹
if *req.ParentId == 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.ParentId).Error; err != nil {
c.JSON(http.StatusNotFound, api.Error{
Errtype: "NotFoundError",
Message: "Parent folder not found",
})
return
}
folder.ParentID = req.ParentId
}
// 保存更新
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)
}
// ExportFolderTree implements api.ServerInterface.
func (b *BookMarksImpl) ExportFolderTree(c *gin.Context) {
panic("unimplemented")
}
// ImportFolderTree implements api.ServerInterface.
func (b *BookMarksImpl) ImportFolderTree(c *gin.Context) {
panic("unimplemented")
}
// Make sure we conform to ServerInterface
var _ api.ServerInterface = (*BookMarksImpl)(nil)

View File

@ -1,310 +0,0 @@
// internal/handlers/bookmark_test.go
package handlers_test
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"testing"
api "git.zzyxyz.com/zzy/zzyxyz_go_api/gen/bookmarks"
"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.ParentId) // 默认根目录
}
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.ParentId) // 默认根目录
}
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,
ParentId: &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)
// FIXME 确保更新时间发生了变化 时钟粒度不足
// 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))
}

View File

@ -0,0 +1,172 @@
// internal/handlers/user_np.go
package handlers
import (
"net/http"
api "git.zzyxyz.com/zzy/zzyxyz_go_api/gen/user_np"
"git.zzyxyz.com/zzy/zzyxyz_go_api/internal/models"
"github.com/gin-gonic/gin"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
)
type UserNPImpl struct {
db *gorm.DB
}
func NewUserNP(dbPath string) (*UserNPImpl, 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.UserNP{})
if err != nil {
return nil, err
}
return &UserNPImpl{db: db}, nil
}
// PostAuthLogin 用户登录
func (u *UserNPImpl) PostAuthLogin(c *gin.Context) {
var req api.PostAuthLoginJSONRequestBody
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// 查找用户
var user models.UserNP
if err := u.db.Where("username = ?", req.Username).First(&user).Error; err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "用户名或密码错误"})
return
}
// 验证密码
if !user.CheckPassword(req.Password) {
c.JSON(http.StatusUnauthorized, gin.H{"error": "用户名或密码错误"})
return
}
// 生成JWT token
token, err := user.GenerateSimpleJWT()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "无法生成访问令牌"})
return
}
// 更新用户token字段可选
user.Token = &token
u.db.Save(&user)
c.JSON(http.StatusOK, api.LoginResponse{
Token: &token,
UserId: &user.ID,
})
}
// PostAuthRegister 用户注册
func (u *UserNPImpl) PostAuthRegister(c *gin.Context) {
var req api.PostAuthRegisterJSONRequestBody
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// 检查用户名是否已存在
var existingUser models.UserNP
if err := u.db.Where("username = ?", req.Username).First(&existingUser).Error; err == nil {
c.JSON(http.StatusConflict, gin.H{"error": "用户名已存在"})
return
}
// 创建新用户
user := models.UserNP{
Username: req.Username,
Email: req.Email,
}
// 加密密码
if err := user.HashPassword(req.Password); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "密码处理失败"})
return
}
// 保存到数据库
if err := u.db.Create(&user).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "用户创建失败"})
return
}
c.JSON(http.StatusCreated, nil)
}
// PutAuthPassword 修改密码
func (u *UserNPImpl) PutAuthPassword(c *gin.Context) {
// 获取Authorization头中的token
authHeader := c.GetHeader("Authorization")
if authHeader == "" {
c.JSON(http.StatusUnauthorized, gin.H{"error": "缺少访问令牌"})
return
}
// 验证token并获取用户名
username, err := models.CheckSimpleJWT(authHeader)
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
return
}
var req api.PutAuthPasswordJSONRequestBody
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// 查找用户
var user models.UserNP
if err := u.db.Where("username = ?", username).First(&user).Error; err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "用户不存在"})
return
}
// 验证旧密码
if !user.CheckPassword(req.OldPassword) {
c.JSON(http.StatusUnauthorized, gin.H{"error": "旧密码错误"})
return
}
// 加密新密码
if err := user.HashPassword(req.NewPassword); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "密码处理失败"})
return
}
// 保存到数据库
if err := u.db.Save(&user).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "密码更新失败"})
return
}
// 生成新的JWT token
token, err := user.GenerateSimpleJWT()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "无法生成新的访问令牌"})
return
}
// 更新用户token字段
user.Token = &token
u.db.Save(&user)
c.JSON(http.StatusOK, nil)
}
// Make sure we conform to ServerInterface
var _ api.ServerInterface = (*UserNPImpl)(nil)

View File

@ -1 +1,354 @@
package handlers
import (
"log"
"net/http"
"strings"
"sync"
api "git.zzyxyz.com/zzy/zzyxyz_go_api/gen/vfs"
"git.zzyxyz.com/zzy/zzyxyz_go_api/internal/models"
"github.com/gin-gonic/gin"
)
// // 一行代码生成安全的随机token
// token := make([]byte, 16)
// rand.Read(token) // 忽略错误处理以简化代码(生产环境建议处理)
// tokenStr := hex.EncodeToString(token)
// adminToken = &tokenStr
// log.Printf("Admin API Token (for Swagger testing): %s", *adminToken)
// if e, err := casbin.NewEnforcer("./config/model.conf", ".data/policy.csv"); err == nil {
// log.Fatalf("Failed to create casbin enforcer: %v", err)
// } else {
// enforcer = e
// }
// ServiceProxy 服务代理接口
type ServiceProxy interface {
// Get 从后端服务获取数据
Get(c *gin.Context, servicePath string, node *models.VfsNode) (any, error)
// Create 在后端服务创建资源
Create(c *gin.Context, servicePath string, node *models.VfsNode, data []byte) (string, error) // 返回创建的资源ID
// Update 更新后端服务资源
Update(c *gin.Context, servicePath string, node *models.VfsNode, data []byte) error
// Delete 删除后端服务资源
Delete(c *gin.Context, servicePath string, node *models.VfsNode) error
// GetName 获取代理名称
GetName() string
}
// ProxyEntry 代理表条目
type ProxyEntry struct {
Name string
Proxy ServiceProxy // 对应的代理实现
}
type VfsImpl struct {
vfs *models.Vfs
proxyTable []*ProxyEntry // 动态代理表
proxyMutex sync.RWMutex // 保护代理表的读写锁
}
func NewVfsHandler(vfs models.Vfs) (*VfsImpl, error) {
return &VfsImpl{
vfs: &vfs,
proxyTable: make([]*ProxyEntry, 0),
proxyMutex: sync.RWMutex{},
}, nil
}
// CreateUser implements api.ServerInterface.
func (v *VfsImpl) CreateUser(c *gin.Context) {
panic("unimplemented")
}
// DeleteUser implements api.ServerInterface.
func (v *VfsImpl) DeleteUser(c *gin.Context) {
panic("unimplemented")
}
// CreateVFSNode implements api.ServerInterface.
func (v *VfsImpl) CreateVFSNode(c *gin.Context, params api.CreateVFSNodeParams) {
// 解析路径组件
parentPath, nodeName, nodeType, err := models.ParsePathComponents(params.Path)
if err != nil {
c.JSON(http.StatusBadRequest, api.Error{
Errtype: "error",
Message: err.Error(),
})
return
}
// 创建节点
node, err := v.vfs.CreateNodeByComponents(parentPath, nodeName, nodeType)
if err != nil {
c.JSON(http.StatusInternalServerError, api.Error{
Errtype: "CreateNodeByComponents",
Message: err.Error(),
})
return
}
if nodeType == models.VfsNodeTypeService {
if !v.Proxy2Service(c, node) {
v.vfs.DeleteVFSNode(node)
return
}
}
// 返回创建成功的节点
c.JSON(http.StatusCreated, api.VFSNodeResponse{
Name: node.Name,
Type: ModelType2ResponseType(node.Type),
CreatedAt: node.CreatedAt,
UpdatedAt: node.UpdatedAt,
})
}
// DeleteVFSNode implements api.ServerInterface.
func (v *VfsImpl) DeleteVFSNode(c *gin.Context, params api.DeleteVFSNodeParams) {
node, err := v.vfs.GetNodeByPath(params.Path)
if err != nil {
c.JSON(http.StatusNotFound, api.Error{
Errtype: "error",
Message: err.Error(),
})
return
}
if node.Type == models.VfsNodeTypeService {
if !v.Proxy2Service(c, node) {
return
}
}
if err := v.vfs.DeleteVFSNode(node); err != nil {
c.JSON(http.StatusInternalServerError, api.Error{
Errtype: "error",
Message: err.Error(),
})
return
}
c.JSON(http.StatusNoContent, nil)
}
// GetVFSNode implements api.ServerInterface.
func (v *VfsImpl) GetVFSNode(c *gin.Context, params api.GetVFSNodeParams) {
node, err := v.vfs.GetNodeByPath(params.Path)
if err != nil {
c.JSON(http.StatusNotFound, api.Error{
Errtype: "error",
Message: err.Error(),
})
return
}
switch node.Type {
case models.VfsNodeTypeDirectory:
if entries, err := v.vfs.GetChildren(node.ID); err != nil {
c.JSON(http.StatusInternalServerError, api.Error{
Errtype: "error",
Message: err.Error(),
})
return
} else {
var responseEntries []api.VFSDirectoryEntry
for _, entry := range entries {
responseEntries = append(responseEntries, api.VFSDirectoryEntry{
Name: entry.Name,
Type: ModelType2ResponseType(entry.Type),
})
}
c.JSON(http.StatusOK, responseEntries)
return
}
case models.VfsNodeTypeService:
v.Proxy2Service(c, node)
default:
c.JSON(http.StatusBadRequest, api.Error{
Errtype: "error",
Message: "Not a directory",
})
}
}
// UpdateVFSNode implements api.ServerInterface.
func (v *VfsImpl) UpdateVFSNode(c *gin.Context, params api.UpdateVFSNodeParams) {
var req api.UpdateVFSNodeJSONRequestBody
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, api.Error{
Errtype: "ParameterError",
Message: "Invalid request parameters",
})
return
}
node, err := v.vfs.GetNodeByPath(params.Path)
if err != nil {
c.JSON(http.StatusNotFound, api.Error{
Errtype: "error",
Message: err.Error(),
})
return
}
if req.NewName != nil {
node.Name = *req.NewName
}
// FIXME
if node.Type == models.VfsNodeTypeService {
if !v.Proxy2Service(c, node) {
return
}
}
// TODO
if err := v.vfs.UpdateVFSNode(node); err != nil {
c.JSON(http.StatusInternalServerError, api.Error{
Errtype: "error",
Message: err.Error(),
})
return
}
}
// Make sure we conform to ServerInterface
var _ api.ServerInterface = (*VfsImpl)(nil)
func ModelType2ResponseType(nodeType models.VfsNodeType) api.VFSNodeType {
var reponseType api.VFSNodeType
switch nodeType {
case models.VfsNodeTypeFile:
reponseType = api.File
case models.VfsNodeTypeDirectory:
reponseType = api.Directory
case models.VfsNodeTypeService:
reponseType = api.Service
}
return reponseType
}
// FindProxyByServiceName 根据服务节点名称查找对应的代理
func (v *VfsImpl) FindProxyByServiceName(serviceName string) ServiceProxy {
v.proxyMutex.RLock()
defer v.proxyMutex.RUnlock()
if serviceName == "" {
return nil
}
// 根据服务名称匹配前缀
for _, entry := range v.proxyTable {
if entry.Name == serviceName {
return entry.Proxy
}
}
return nil
}
func (v *VfsImpl) RegisterProxy(entry *ProxyEntry) {
v.proxyMutex.Lock()
defer v.proxyMutex.Unlock()
v.proxyTable = append(v.proxyTable, entry)
}
// Proxy2Service 通用服务代理处理函数
func (v *VfsImpl) Proxy2Service(c *gin.Context, node *models.VfsNode) bool {
exts := strings.Split(node.Name, ".")
var serviceName = exts[1]
log.Println("Proxy2Service: ", serviceName)
// 查找对应的代理
proxy := v.FindProxyByServiceName(serviceName)
if proxy == nil {
c.JSON(http.StatusNotImplemented, api.Error{
Errtype: "error",
Message: "Service proxy not found for: " + serviceName,
})
return false
}
// 根据HTTP方法调用相应的代理方法
switch c.Request.Method {
case http.MethodGet:
result, err := proxy.Get(c, serviceName, node)
if err != nil {
c.JSON(http.StatusInternalServerError, api.Error{
Errtype: "error",
Message: "Failed to get service data: " + err.Error(),
})
return false
}
c.JSON(http.StatusOK, result)
return true
case http.MethodPost:
// 读取请求体数据
data, err := c.GetRawData()
if err != nil {
c.JSON(http.StatusBadRequest, api.Error{
Errtype: "error",
Message: "Failed to read request data: " + err.Error(),
})
return false
}
resourceID, err := proxy.Create(c, serviceName, node, data)
if err != nil {
c.JSON(http.StatusInternalServerError, api.Error{
Errtype: "error",
Message: "Failed to create service resource: " + err.Error(),
})
return false
}
c.JSON(http.StatusCreated, gin.H{"resource_id": resourceID})
return true
case http.MethodPut, http.MethodPatch:
// 读取请求体数据
data, err := c.GetRawData()
if err != nil {
c.JSON(http.StatusBadRequest, api.Error{
Errtype: "error",
Message: "Failed to read request data: " + err.Error(),
})
return false
}
err = proxy.Update(c, serviceName, node, data)
if err != nil {
c.JSON(http.StatusInternalServerError, api.Error{
Errtype: "error",
Message: "Failed to update service resource: " + err.Error(),
})
return false
}
c.JSON(http.StatusOK, gin.H{"message": "Updated successfully"})
return true
case http.MethodDelete:
err := proxy.Delete(c, serviceName, node)
if err != nil {
c.JSON(http.StatusInternalServerError, api.Error{
Errtype: "error",
Message: "Failed to delete service resource: " + err.Error(),
})
return false
}
c.JSON(http.StatusNoContent, nil)
return true
default:
c.JSON(http.StatusMethodNotAllowed, api.Error{
Errtype: "error",
Message: "Method not allowed",
})
return false
}
}

View File

@ -0,0 +1,160 @@
// internal/handlers/vfs_driver/vfs_bookmark.go
package vfsdriver
import (
"context"
"encoding/json"
"fmt"
api "git.zzyxyz.com/zzy/zzyxyz_go_api/gen/bookmarks"
"git.zzyxyz.com/zzy/zzyxyz_go_api/internal/handlers"
"git.zzyxyz.com/zzy/zzyxyz_go_api/internal/models"
"github.com/gin-gonic/gin"
)
type VfsBookMarkService struct {
client *api.ClientWithResponses
}
func NewVfsBookMarkService(serverURL string) (*VfsBookMarkService, error) {
client, err := api.NewClientWithResponses(serverURL)
if err != nil {
return nil, err
}
return &VfsBookMarkService{
client: client,
}, nil
}
// Create implements ServiceProxy.
func (v *VfsBookMarkService) Create(c *gin.Context, servicePath string, node *models.VfsNode, data []byte) (string, error) {
ctx := context.Background()
// 解析传入的数据为 BookmarkRequest
var req api.BookmarkRequest
if err := json.Unmarshal(data, &req); err != nil {
return "", err
}
// 调用 bookmark 服务创建书签
resp, err := v.client.CreateBookmarkWithResponse(ctx, int64(node.ID), req)
if err != nil {
return "", err
}
// 处理响应
if resp.JSON201 != nil {
result, err := json.Marshal(resp.JSON201)
if err != nil {
return "", err
}
return string(result), nil
}
// 处理错误情况
if resp.JSON400 != nil {
return "", fmt.Errorf("bad request: %s", resp.JSON400.Message)
}
if resp.JSON500 != nil {
return "", fmt.Errorf("server error: %s", resp.JSON500.Message)
}
return "", fmt.Errorf("unknown error")
}
// Delete implements ServiceProxy.
func (v *VfsBookMarkService) Delete(c *gin.Context, servicePath string, node *models.VfsNode) error {
ctx := context.Background()
// 调用 bookmark 服务删除书签
resp, err := v.client.DeleteBookmarkWithResponse(ctx, int64(node.ID))
if err != nil {
return err
}
// 处理响应
if resp.StatusCode() == 204 {
return nil
}
// 处理错误情况
if resp.JSON404 != nil {
return fmt.Errorf("not found: %s", resp.JSON404.Message)
}
if resp.JSON500 != nil {
return fmt.Errorf("server error: %s", resp.JSON500.Message)
}
return fmt.Errorf("unknown error")
}
// Get implements ServiceProxy.
func (v *VfsBookMarkService) Get(c *gin.Context, servicePath string, node *models.VfsNode) (any, error) {
ctx := context.Background()
// 调用 bookmark 服务获取书签
resp, err := v.client.GetBookmarkWithResponse(ctx, int64(node.ID))
if err != nil {
return nil, err
}
// 处理响应
if resp.JSON200 != nil {
return resp.JSON200, nil
}
// 处理错误情况
if resp.JSON404 != nil {
return nil, fmt.Errorf("not found: %s", resp.JSON404.Message)
}
return nil, fmt.Errorf("unknown error")
}
// GetName implements ServiceProxy.
func (v *VfsBookMarkService) GetName() string {
return "bookmark"
}
// Update implements ServiceProxy.
func (v *VfsBookMarkService) Update(c *gin.Context, servicePath string, node *models.VfsNode, data []byte) error {
ctx := context.Background()
// 解析传入的数据为 BookmarkRequest
var req api.BookmarkRequest
if err := json.Unmarshal(data, &req); err != nil {
return err
}
// 调用 bookmark 服务更新书签
resp, err := v.client.UpdateBookmarkWithResponse(ctx, int64(node.ID), req)
if err != nil {
return err
}
// 处理响应
if resp.JSON200 != nil {
return nil
}
// 处理错误情况
if resp.JSON400 != nil {
return fmt.Errorf("bad request: %s", resp.JSON400.Message)
}
if resp.JSON404 != nil {
return fmt.Errorf("not found: %s", resp.JSON404.Message)
}
if resp.JSON500 != nil {
return fmt.Errorf("server error: %s", resp.JSON500.Message)
}
return fmt.Errorf("unknown error")
}
var _ handlers.ServiceProxy = (*VfsBookMarkService)(nil)