feat: 多租户账号体系 + 前台收藏按钮

- 新增 users 表(user_id 数据隔离,bcrypt 密码)
- 认证: 注册/登录(用户名+密码)/会话绑定用户, 首个用户成为管理员并接管旧数据
- 数据隔离: 笔记/分类/标签/回收站/版本/草稿/图谱/FTS 全部按用户隔离
- 前台: 登录/注册弹窗, 登录后★收藏自己的笔记, 游客只读公开笔记
- 后台: 用户名+密码登录, 每人管理自己的工作区, 越权访问返回404
- 冒烟测试重构+新增多租户隔离用例(78/78)
This commit is contained in:
Your Name
2026-08-11 12:58:12 +08:00
parent 2196189791
commit d9793300f9
16 changed files with 1216 additions and 278 deletions
+29 -20
View File
@@ -4,20 +4,18 @@ import (
"net/http"
"github.com/gin-gonic/gin"
"note-manager/config"
"note-manager/middleware"
"note-manager/service"
)
// AdminHandler 后台管理处理器
// AdminHandler 后台管理处理器(登录页与页面跳转,多租户账号认证)
type AdminHandler struct {
noteSvc *service.NoteService
config *config.Config
userSvc *service.UserService
}
// NewAdminHandler 创建后台管理处理器
func NewAdminHandler(noteSvc *service.NoteService, cfg *config.Config) *AdminHandler {
return &AdminHandler{noteSvc: noteSvc, config: cfg}
func NewAdminHandler(userSvc *service.UserService) *AdminHandler {
return &AdminHandler{userSvc: userSvc}
}
// LoginPage 登录页面
@@ -27,24 +25,35 @@ func (h *AdminHandler) LoginPage(c *gin.Context) {
})
}
// Login 验证登录
// Login 验证登录(用户名 + 密码,兼容 JSON 与 form
func (h *AdminHandler) Login(c *gin.Context) {
username := c.PostForm("username")
password := c.PostForm("password")
if password == h.config.AdminPass {
// 生成随机会话 token
token, _ := middleware.NewSessionToken()
// 通过环境变量判断是否启用 HTTPS(生产建议配置)
secure := c.Request.TLS != nil
middleware.SetAuthCookie(c, token, secure)
c.JSON(http.StatusOK, gin.H{
"code": 0,
"message": "登录成功",
if username == "" && password == "" {
// 尝试 JSON
var req struct {
Username string `json:"username"`
Password string `json:"password"`
}
if err := c.ShouldBindJSON(&req); err == nil {
username = req.Username
password = req.Password
}
}
u, err := h.userSvc.Login(username, password)
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{
"code": 401,
"message": err.Error(),
})
return
}
c.JSON(http.StatusUnauthorized, gin.H{
"code": 401,
"message": "密码错误",
token, _ := middleware.NewSession(u.ID)
secure := c.Request.TLS != nil
middleware.SetAuthCookie(c, token, secure)
c.JSON(http.StatusOK, gin.H{
"code": 0,
"message": "登录成功",
})
}
@@ -80,7 +89,7 @@ func (h *AdminHandler) CheckAuth(c *gin.Context) {
})
}
// IndexPage 后台管理首页
// IndexPage 后台管理首页(需登录,否则跳转登录页)
func (h *AdminHandler) IndexPage(c *gin.Context) {
token := middleware.GetAuthToken(c)
if !middleware.IsValidSession(token) {
+99
View File
@@ -0,0 +1,99 @@
package handler
import (
"net/http"
"github.com/gin-gonic/gin"
"note-manager/middleware"
"note-manager/service"
)
// AuthHandler 认证处理器(多租户账号:注册/登录/登出/当前用户)
type AuthHandler struct {
userSvc *service.UserService
}
// NewAuthHandler 创建认证处理器
func NewAuthHandler(userSvc *service.UserService) *AuthHandler {
return &AuthHandler{userSvc: userSvc}
}
// REGISTER 注册请求
type registerReq struct {
Username string `json:"username"`
Password string `json:"password"`
DisplayName string `json:"display_name"`
}
// Register 注册新用户
// 首个注册用户自动成为 admin,并接管历史遗留笔记。
func (h *AuthHandler) Register(c *gin.Context) {
var req registerReq
if err := c.ShouldBindJSON(&req); err != nil {
fail(c, http.StatusBadRequest, "请求参数错误")
return
}
u, err := h.userSvc.Register(req.Username, req.Password, req.DisplayName)
if err != nil {
fail(c, http.StatusBadRequest, err.Error())
return
}
// 注册成功即自动登录
token, _ := middleware.NewSession(u.ID)
secure := c.Request.TLS != nil
middleware.SetAuthCookie(c, token, secure)
success(c, gin.H{"id": u.ID, "username": u.Username, "display_name": u.DisplayName, "role": u.Role})
}
// Login 登录(用户名 + 密码)
func (h *AuthHandler) Login(c *gin.Context) {
var req struct {
Username string `json:"username"`
Password string `json:"password"`
}
// 兼容 form 提交(后台登录页)
if err := c.ShouldBindJSON(&req); err != nil {
req.Username = c.PostForm("username")
req.Password = c.PostForm("password")
}
if req.Password == "" || req.Username == "" {
fail(c, http.StatusBadRequest, "请输入用户名和密码")
return
}
u, err := h.userSvc.Login(req.Username, req.Password)
if err != nil {
fail(c, http.StatusUnauthorized, err.Error())
return
}
token, _ := middleware.NewSession(u.ID)
secure := c.Request.TLS != nil
middleware.SetAuthCookie(c, token, secure)
success(c, gin.H{"id": u.ID, "username": u.Username, "display_name": u.DisplayName, "role": u.Role})
}
// Logout 登出
func (h *AuthHandler) Logout(c *gin.Context) {
middleware.RevokeSession(middleware.GetAuthToken(c))
middleware.ClearAuthCookie(c)
success(c, nil)
}
// Me 返回当前登录用户信息(未登录返回 null)
func (h *AuthHandler) Me(c *gin.Context) {
uid := middleware.GetUserID(c)
if uid == 0 {
success(c, nil)
return
}
u, err := h.userSvc.GetByID(uid)
if err != nil {
success(c, nil)
return
}
success(c, gin.H{
"id": u.ID,
"username": u.Username,
"display_name": u.DisplayName,
"role": u.Role,
})
}
+122 -70
View File
@@ -13,6 +13,7 @@ import (
"time"
"github.com/gin-gonic/gin"
"note-manager/middleware"
"note-manager/model"
"note-manager/service"
)
@@ -53,15 +54,16 @@ func fail(c *gin.Context, status int, msg string) {
c.JSON(status, Response{Code: -1, Message: msg})
}
// CreateNote 创建笔记
// CreateNote 创建笔记(归属当前登录用户)
func (h *NoteHandler) CreateNote(c *gin.Context) {
userID := middleware.GetUserID(c)
var req model.NoteCreateRequest
if err := c.ShouldBindJSON(&req); err != nil {
fail(c, http.StatusBadRequest, "请求参数错误: "+err.Error())
return
}
note, err := h.svc.CreateNote(req)
note, err := h.svc.CreateNote(userID, req)
if err != nil {
fail(c, http.StatusInternalServerError, err.Error())
return
@@ -70,9 +72,8 @@ func (h *NoteHandler) CreateNote(c *gin.Context) {
success(c, note)
}
// GetNote 获取笔记详情(公开只读接口)
// 安全策略:仅允许返回「公开且无密码」的笔记。有密码或未公开的笔记一律返回需授权提示,
// 防止通过该接口绕过密码保护读取内容。
// GetNote 获取笔记详情(公开只读接口,游客只看公开笔记
// 安全策略:游客仅允许访问「公开且无密码」的笔记;登录用户访问自己的任意笔记。
func (h *NoteHandler) GetNote(c *gin.Context) {
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
if err != nil {
@@ -80,22 +81,27 @@ func (h *NoteHandler) GetNote(c *gin.Context) {
return
}
note, err := h.svc.GetNote(uint(id))
if middleware.IsLoggedIn(c) {
userID := middleware.GetUserID(c)
note, err := h.svc.GetNote(userID, uint(id))
if err != nil {
fail(c, http.StatusNotFound, err.Error())
return
}
success(c, note)
return
}
// 游客:仅公开且无密码的笔记
note, err := h.svc.GetNotePublic(uint(id))
if err != nil {
fail(c, http.StatusNotFound, err.Error())
fail(c, http.StatusForbidden, err.Error())
return
}
// 公开接口白名单:仅公开且无密码的笔记返回完整内容
if !note.IsPublic || note.Password != "" {
fail(c, http.StatusForbidden, "该笔记受保护,无法直接访问")
return
}
success(c, note)
}
// GetAdminNote 获取笔记详情(管理后台用,返回完整内容)
// GetAdminNote 获取笔记详情(个人管理用,返回完整内容,校验归属
func (h *NoteHandler) GetAdminNote(c *gin.Context) {
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
if err != nil {
@@ -103,7 +109,8 @@ func (h *NoteHandler) GetAdminNote(c *gin.Context) {
return
}
note, err := h.svc.GetNote(uint(id))
userID := middleware.GetUserID(c)
note, err := h.svc.GetNote(userID, uint(id))
if err != nil {
fail(c, http.StatusNotFound, err.Error())
return
@@ -146,8 +153,9 @@ func (h *NoteHandler) AccessNote(c *gin.Context) {
success(c, note)
}
// UpdateNote 更新笔记
// UpdateNote 更新笔记(校验归属)
func (h *NoteHandler) UpdateNote(c *gin.Context) {
userID := middleware.GetUserID(c)
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
if err != nil {
fail(c, http.StatusBadRequest, "无效的笔记 ID")
@@ -160,8 +168,12 @@ func (h *NoteHandler) UpdateNote(c *gin.Context) {
return
}
note, err := h.svc.UpdateNote(uint(id), req)
note, err := h.svc.UpdateNote(userID, uint(id), req)
if err != nil {
if err == service.ErrNotFound {
fail(c, http.StatusNotFound, err.Error())
return
}
fail(c, http.StatusInternalServerError, err.Error())
return
}
@@ -169,15 +181,20 @@ func (h *NoteHandler) UpdateNote(c *gin.Context) {
success(c, note)
}
// DeleteNote 删除笔记(软删除,进入回收站)
// DeleteNote 删除笔记(软删除,进入回收站,校验归属
func (h *NoteHandler) DeleteNote(c *gin.Context) {
userID := middleware.GetUserID(c)
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
if err != nil {
fail(c, http.StatusBadRequest, "无效的笔记 ID")
return
}
if err := h.svc.DeleteNote(uint(id)); err != nil {
if err := h.svc.DeleteNote(userID, uint(id)); err != nil {
if err == service.ErrNotFound {
fail(c, http.StatusNotFound, err.Error())
return
}
fail(c, http.StatusInternalServerError, err.Error())
return
}
@@ -185,7 +202,7 @@ func (h *NoteHandler) DeleteNote(c *gin.Context) {
success(c, nil)
}
// ListNotes 获取笔记列表
// ListNotes 获取笔记列表(登录用户看自己的;游客看公开笔记)
func (h *NoteHandler) ListNotes(c *gin.Context) {
var pinned *bool
if v := c.Query("pinned"); v != "" {
@@ -198,7 +215,12 @@ func (h *NoteHandler) ListNotes(c *gin.Context) {
favorite = &b
}
loggedIn := middleware.IsLoggedIn(c)
userID := middleware.GetUserID(c)
items, total, totalPages, err := h.svc.ListNotes(
userID,
loggedIn,
c.DefaultQuery("page", "1"),
c.DefaultQuery("page_size", ""),
c.Query("category"),
@@ -225,10 +247,14 @@ func (h *NoteHandler) ListNotes(c *gin.Context) {
})
}
// SearchNotes 搜索笔记
// SearchNotes 搜索笔记(登录用户搜自己的;游客搜公开笔记)
func (h *NoteHandler) SearchNotes(c *gin.Context) {
keyword := c.Query("q")
loggedIn := middleware.IsLoggedIn(c)
userID := middleware.GetUserID(c)
items, total, totalPages, err := h.svc.SearchNotes(
userID,
loggedIn,
keyword,
c.DefaultQuery("page", "1"),
c.DefaultQuery("page_size", ""),
@@ -252,9 +278,11 @@ func (h *NoteHandler) SearchNotes(c *gin.Context) {
})
}
// GetCategories 获取分类列表
// GetCategories 获取分类列表(登录用户看自己的;游客看公开笔记分类)
func (h *NoteHandler) GetCategories(c *gin.Context) {
categories, err := h.svc.GetCategories()
loggedIn := middleware.IsLoggedIn(c)
userID := middleware.GetUserID(c)
categories, err := h.svc.GetCategories(userID, loggedIn)
if err != nil {
fail(c, http.StatusInternalServerError, err.Error())
return
@@ -262,9 +290,11 @@ func (h *NoteHandler) GetCategories(c *gin.Context) {
success(c, categories)
}
// GetTags 获取所有标签
// GetTags 获取所有标签(登录用户看自己的;游客看公开笔记标签)
func (h *NoteHandler) GetTags(c *gin.Context) {
tags, err := h.svc.GetTags()
loggedIn := middleware.IsLoggedIn(c)
userID := middleware.GetUserID(c)
tags, err := h.svc.GetTags(userID, loggedIn)
if err != nil {
fail(c, http.StatusInternalServerError, err.Error())
return
@@ -272,9 +302,10 @@ func (h *NoteHandler) GetTags(c *gin.Context) {
success(c, tags)
}
// GetTree 获取树形结构(管理后台用)
// GetTree 获取树形结构(个人管理用)
func (h *NoteHandler) GetTree(c *gin.Context) {
tree, err := h.svc.GetAllTree()
userID := middleware.GetUserID(c)
tree, err := h.svc.GetAllTree(userID)
if err != nil {
fail(c, http.StatusInternalServerError, err.Error())
return
@@ -282,9 +313,11 @@ func (h *NoteHandler) GetTree(c *gin.Context) {
success(c, tree)
}
// GetPublicTree 获取公开树形结构(前台用
// GetPublicTree 获取树形结构(登录用户看自己的全部;游客看所有公开笔记
func (h *NoteHandler) GetPublicTree(c *gin.Context) {
tree, err := h.svc.GetPublicTree()
loggedIn := middleware.IsLoggedIn(c)
userID := middleware.GetUserID(c)
tree, err := h.svc.GetPublicTree(userID, loggedIn)
if err != nil {
fail(c, http.StatusInternalServerError, err.Error())
return
@@ -302,9 +335,10 @@ func parseIntDefault(s string, defaultVal int) int {
// ─────────────── 回收站 ───────────────
// ListTrash 回收站列表
// ListTrash 回收站列表(当前用户)
func (h *NoteHandler) ListTrash(c *gin.Context) {
items, err := h.svc.ListTrash()
userID := middleware.GetUserID(c)
items, err := h.svc.ListTrash(userID)
if err != nil {
fail(c, http.StatusInternalServerError, err.Error())
return
@@ -312,37 +346,40 @@ func (h *NoteHandler) ListTrash(c *gin.Context) {
success(c, items)
}
// RestoreNote 恢复笔记
// RestoreNote 恢复笔记(校验归属)
func (h *NoteHandler) RestoreNote(c *gin.Context) {
userID := middleware.GetUserID(c)
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
if err != nil {
fail(c, http.StatusBadRequest, "无效的笔记 ID")
return
}
if err := h.svc.RestoreNote(uint(id)); err != nil {
if err := h.svc.RestoreNote(userID, uint(id)); err != nil {
fail(c, http.StatusInternalServerError, err.Error())
return
}
success(c, nil)
}
// PurgeNote 彻底删除笔记
// PurgeNote 彻底删除笔记(校验归属)
func (h *NoteHandler) PurgeNote(c *gin.Context) {
userID := middleware.GetUserID(c)
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
if err != nil {
fail(c, http.StatusBadRequest, "无效的笔记 ID")
return
}
if err := h.svc.PurgeNote(uint(id)); err != nil {
if err := h.svc.PurgeNote(userID, uint(id)); err != nil {
fail(c, http.StatusInternalServerError, err.Error())
return
}
success(c, nil)
}
// EmptyTrash 清空回收站
// EmptyTrash 清空回收站(当前用户)
func (h *NoteHandler) EmptyTrash(c *gin.Context) {
if err := h.svc.EmptyTrash(); err != nil {
userID := middleware.GetUserID(c)
if err := h.svc.EmptyTrash(userID); err != nil {
fail(c, http.StatusInternalServerError, err.Error())
return
}
@@ -351,14 +388,15 @@ func (h *NoteHandler) EmptyTrash(c *gin.Context) {
// ─────────────── 版本历史 ───────────────
// ListVersions 版本列表
// ListVersions 版本列表(校验归属)
func (h *NoteHandler) ListVersions(c *gin.Context) {
userID := middleware.GetUserID(c)
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
if err != nil {
fail(c, http.StatusBadRequest, "无效的笔记 ID")
return
}
versions, err := h.svc.ListVersions(uint(id))
versions, err := h.svc.ListVersions(userID, uint(id))
if err != nil {
fail(c, http.StatusInternalServerError, err.Error())
return
@@ -366,8 +404,9 @@ func (h *NoteHandler) ListVersions(c *gin.Context) {
success(c, versions)
}
// RestoreVersion 恢复到指定版本
// RestoreVersion 恢复到指定版本(校验归属)
func (h *NoteHandler) RestoreVersion(c *gin.Context) {
userID := middleware.GetUserID(c)
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
if err != nil {
fail(c, http.StatusBadRequest, "无效的笔记 ID")
@@ -378,7 +417,7 @@ func (h *NoteHandler) RestoreVersion(c *gin.Context) {
fail(c, http.StatusBadRequest, "无效的版本 ID")
return
}
note, err := h.svc.RestoreVersion(uint(id), uint(versionID))
note, err := h.svc.RestoreVersion(userID, uint(id), uint(versionID))
if err != nil {
fail(c, http.StatusInternalServerError, err.Error())
return
@@ -388,35 +427,37 @@ func (h *NoteHandler) RestoreVersion(c *gin.Context) {
// ─────────────── 分享 ───────────────
// CreateShare 创建分享
// CreateShare 创建分享(校验归属)
func (h *NoteHandler) CreateShare(c *gin.Context) {
userID := middleware.GetUserID(c)
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
if err != nil {
fail(c, http.StatusBadRequest, "无效的笔记 ID")
return
}
expireHours, _ := strconv.Atoi(c.DefaultPostForm("expire_hours", "0"))
note, err := h.svc.CreateShare(uint(id), expireHours)
note, err := h.svc.CreateShare(userID, uint(id), expireHours)
if err != nil {
fail(c, http.StatusInternalServerError, err.Error())
return
}
success(c, gin.H{
"id": note.ID,
"id": note.ID,
"share_token": note.ShareToken,
"url": "/share/" + note.ShareToken,
"expire_at": note.ShareExpireAt,
"url": "/share/" + note.ShareToken,
"expire_at": note.ShareExpireAt,
})
}
// RevokeShare 撤销分享
// RevokeShare 撤销分享(校验归属)
func (h *NoteHandler) RevokeShare(c *gin.Context) {
userID := middleware.GetUserID(c)
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
if err != nil {
fail(c, http.StatusBadRequest, "无效的笔记 ID")
return
}
if err := h.svc.RevokeShare(uint(id)); err != nil {
if err := h.svc.RevokeShare(userID, uint(id)); err != nil {
fail(c, http.StatusInternalServerError, err.Error())
return
}
@@ -459,15 +500,16 @@ func (h *NoteHandler) SharePage(c *gin.Context) {
})
}
// ExportNote 导出笔记为 Markdown 文件
// ExportNote 导出笔记为 Markdown 文件(校验归属)
func (h *NoteHandler) ExportNote(c *gin.Context) {
userID := middleware.GetUserID(c)
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
if err != nil {
fail(c, http.StatusBadRequest, "无效的笔记 ID")
return
}
note, err := h.svc.GetNote(uint(id))
note, err := h.svc.GetNote(userID, uint(id))
if err != nil {
fail(c, http.StatusNotFound, err.Error())
return
@@ -482,9 +524,10 @@ func (h *NoteHandler) ExportNote(c *gin.Context) {
c.String(http.StatusOK, content)
}
// ExportAll 批量导出全部笔记为 zip
// ExportAll 批量导出当前用户全部笔记为 zip
func (h *NoteHandler) ExportAll(c *gin.Context) {
tree, err := h.svc.GetAllTree()
userID := middleware.GetUserID(c)
tree, err := h.svc.GetAllTree(userID)
if err != nil {
fail(c, http.StatusInternalServerError, err.Error())
return
@@ -527,7 +570,7 @@ func (h *NoteHandler) ExportAll(c *gin.Context) {
}
return nil
}
note, err := h.svc.GetNote(n.item.ID)
note, err := h.svc.GetNote(userID, n.item.ID)
if err != nil {
return err
}
@@ -549,8 +592,9 @@ func (h *NoteHandler) ExportAll(c *gin.Context) {
}
}
// ImportNotes 导入 Markdown 文件
// ImportNotes 导入 Markdown 文件(归属当前用户)
func (h *NoteHandler) ImportNotes(c *gin.Context) {
userID := middleware.GetUserID(c)
file, err := c.FormFile("file")
if err != nil {
fail(c, http.StatusBadRequest, "请选择要导入的文件")
@@ -587,7 +631,7 @@ func (h *NoteHandler) ImportNotes(c *gin.Context) {
Content: body,
}
note, err := h.svc.CreateNote(req)
note, err := h.svc.CreateNote(userID, req)
if err != nil {
fail(c, http.StatusInternalServerError, err.Error())
return
@@ -664,8 +708,9 @@ func sanitizeFileName(name string) string {
// ─────────────── 自动保存草稿 ───────────────
// SaveDraft 保存笔记草稿(自动保存)
// SaveDraft 保存笔记草稿(自动保存,校验归属
func (h *NoteHandler) SaveDraft(c *gin.Context) {
userID := middleware.GetUserID(c)
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
if err != nil {
fail(c, http.StatusBadRequest, "无效的笔记 ID")
@@ -678,21 +723,22 @@ func (h *NoteHandler) SaveDraft(c *gin.Context) {
fail(c, http.StatusBadRequest, "请求参数错误")
return
}
if err := h.svc.SaveDraft(uint(id), req.Content); err != nil {
if err := h.svc.SaveDraft(userID, uint(id), req.Content); err != nil {
fail(c, http.StatusInternalServerError, err.Error())
return
}
success(c, nil)
}
// DiscardDraft 清除指定笔记的草稿
// DiscardDraft 清除指定笔记的草稿(校验归属)
func (h *NoteHandler) DiscardDraft(c *gin.Context) {
userID := middleware.GetUserID(c)
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
if err != nil {
fail(c, http.StatusBadRequest, "无效的笔记 ID")
return
}
if err := h.svc.ClearDraft(uint(id)); err != nil {
if err := h.svc.ClearDraft(userID, uint(id)); err != nil {
fail(c, http.StatusInternalServerError, err.Error())
return
}
@@ -701,14 +747,15 @@ func (h *NoteHandler) DiscardDraft(c *gin.Context) {
// ─────────────── 双向链接 / 知识图谱 ───────────────
// GetBacklinks 获取指定笔记的反向链接列表
// GetBacklinks 获取指定笔记的反向链接列表(校验归属)
func (h *NoteHandler) GetBacklinks(c *gin.Context) {
userID := middleware.GetUserID(c)
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
if err != nil {
fail(c, http.StatusBadRequest, "无效的笔记 ID")
return
}
links, err := h.svc.GetBacklinks(uint(id), "")
links, err := h.svc.GetBacklinks(userID, uint(id), "")
if err != nil {
fail(c, http.StatusInternalServerError, err.Error())
return
@@ -716,9 +763,10 @@ func (h *NoteHandler) GetBacklinks(c *gin.Context) {
success(c, links)
}
// GetKnowledgeGraph 获取知识图谱数据(节点 + 边)
// GetKnowledgeGraph 获取当前用户知识图谱数据(节点 + 边)
func (h *NoteHandler) GetKnowledgeGraph(c *gin.Context) {
graph, err := h.svc.GetKnowledgeGraph()
userID := middleware.GetUserID(c)
graph, err := h.svc.GetKnowledgeGraph(userID)
if err != nil {
fail(c, http.StatusInternalServerError, err.Error())
return
@@ -728,9 +776,10 @@ func (h *NoteHandler) GetKnowledgeGraph(c *gin.Context) {
// ─────────────── 标签管理 ───────────────
// GetTagUsage 获取标签及使用次数
// GetTagUsage 获取当前用户标签及使用次数
func (h *NoteHandler) GetTagUsage(c *gin.Context) {
usage, err := h.svc.GetTagUsage()
userID := middleware.GetUserID(c)
usage, err := h.svc.GetTagUsage(userID)
if err != nil {
fail(c, http.StatusInternalServerError, err.Error())
return
@@ -740,6 +789,7 @@ func (h *NoteHandler) GetTagUsage(c *gin.Context) {
// RenameTag 重命名标签
func (h *NoteHandler) RenameTag(c *gin.Context) {
userID := middleware.GetUserID(c)
var req struct {
OldName string `json:"old_name" binding:"required"`
NewName string `json:"new_name" binding:"required"`
@@ -748,7 +798,7 @@ func (h *NoteHandler) RenameTag(c *gin.Context) {
fail(c, http.StatusBadRequest, "请求参数错误")
return
}
changed, err := h.svc.RenameTag(req.OldName, req.NewName)
changed, err := h.svc.RenameTag(userID, req.OldName, req.NewName)
if err != nil {
fail(c, http.StatusBadRequest, err.Error())
return
@@ -758,6 +808,7 @@ func (h *NoteHandler) RenameTag(c *gin.Context) {
// MergeTag 合并标签(from → to
func (h *NoteHandler) MergeTag(c *gin.Context) {
userID := middleware.GetUserID(c)
var req struct {
From string `json:"from" binding:"required"`
To string `json:"to" binding:"required"`
@@ -766,7 +817,7 @@ func (h *NoteHandler) MergeTag(c *gin.Context) {
fail(c, http.StatusBadRequest, "请求参数错误")
return
}
changed, err := h.svc.MergeTag(req.From, req.To)
changed, err := h.svc.MergeTag(userID, req.From, req.To)
if err != nil {
fail(c, http.StatusBadRequest, err.Error())
return
@@ -774,8 +825,9 @@ func (h *NoteHandler) MergeTag(c *gin.Context) {
success(c, gin.H{"changed": changed})
}
// DeleteTag 删除标签(从所有笔记中移除)
// DeleteTag 删除标签(从当前用户所有笔记中移除)
func (h *NoteHandler) DeleteTag(c *gin.Context) {
userID := middleware.GetUserID(c)
var req struct {
Name string `json:"name" binding:"required"`
}
@@ -783,7 +835,7 @@ func (h *NoteHandler) DeleteTag(c *gin.Context) {
fail(c, http.StatusBadRequest, "请求参数错误")
return
}
changed, err := h.svc.DeleteTag(req.Name)
changed, err := h.svc.DeleteTag(userID, req.Name)
if err != nil {
fail(c, http.StatusBadRequest, err.Error())
return