package handler import ( "archive/zip" "encoding/json" "fmt" "io" "net/http" "net/url" "path/filepath" "strconv" "strings" "time" "github.com/gin-gonic/gin" "note-manager/model" "note-manager/service" ) // NoteHandler 笔记请求处理器 type NoteHandler struct { svc *service.NoteService } // NewNoteHandler 创建处理器实例 func NewNoteHandler(svc *service.NoteService) *NoteHandler { return &NoteHandler{svc: svc} } // Response 通用响应结构 type Response struct { Code int `json:"code"` Message string `json:"message"` Data interface{} `json:"data,omitempty"` } // PageResponse 分页响应结构 type PageResponse struct { Code int `json:"code"` Message string `json:"message"` Data interface{} `json:"data"` Total int64 `json:"total"` Page int `json:"page"` PageSize int `json:"page_size"` TotalPages int `json:"total_pages"` } func success(c *gin.Context, data interface{}) { c.JSON(http.StatusOK, Response{Code: 0, Message: "success", Data: data}) } func fail(c *gin.Context, status int, msg string) { c.JSON(status, Response{Code: -1, Message: msg}) } // CreateNote 创建笔记 func (h *NoteHandler) CreateNote(c *gin.Context) { var req model.NoteCreateRequest if err := c.ShouldBindJSON(&req); err != nil { fail(c, http.StatusBadRequest, "请求参数错误: "+err.Error()) return } note, err := h.svc.CreateNote(req) if err != nil { fail(c, http.StatusInternalServerError, err.Error()) return } success(c, note) } // GetNote 获取笔记详情(公开只读接口) // 安全策略:仅允许返回「公开且无密码」的笔记。有密码或未公开的笔记一律返回需授权提示, // 防止通过该接口绕过密码保护读取内容。 func (h *NoteHandler) GetNote(c *gin.Context) { 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)) if err != nil { fail(c, http.StatusNotFound, err.Error()) return } // 公开接口白名单:仅公开且无密码的笔记返回完整内容 if !note.IsPublic || note.Password != "" { fail(c, http.StatusForbidden, "该笔记受保护,无法直接访问") return } success(c, note) } // GetAdminNote 获取笔记详情(管理后台用,返回完整内容) func (h *NoteHandler) GetAdminNote(c *gin.Context) { 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)) if err != nil { fail(c, http.StatusNotFound, err.Error()) return } success(c, note) } // AccessNote 验证密码后获取笔记内容 func (h *NoteHandler) AccessNote(c *gin.Context) { id, err := strconv.ParseUint(c.Param("id"), 10, 64) if err != nil { fail(c, http.StatusBadRequest, "无效的笔记 ID") return } var req struct { Password string `json:"password"` } if err := c.ShouldBindJSON(&req); err != nil { // 没有密码参数,尝试从 URL 参数获取 req.Password = c.Query("password") } note, upgrade, err := h.svc.GetNoteContent(uint(id), req.Password) if err != nil { if err.Error() == "密码错误" { fail(c, http.StatusUnauthorized, err.Error()) return } fail(c, http.StatusNotFound, err.Error()) return } // 若旧 SHA-256 哈希命中,自动升级为 bcrypt if upgrade { _ = h.svc.UpgradePasswordHash(note.ID, req.Password) } success(c, note) } // UpdateNote 更新笔记 func (h *NoteHandler) UpdateNote(c *gin.Context) { id, err := strconv.ParseUint(c.Param("id"), 10, 64) if err != nil { fail(c, http.StatusBadRequest, "无效的笔记 ID") return } var req model.NoteUpdateRequest if err := c.ShouldBindJSON(&req); err != nil { fail(c, http.StatusBadRequest, "请求参数错误: "+err.Error()) return } note, err := h.svc.UpdateNote(uint(id), req) if err != nil { fail(c, http.StatusInternalServerError, err.Error()) return } success(c, note) } // DeleteNote 删除笔记(软删除,进入回收站) func (h *NoteHandler) DeleteNote(c *gin.Context) { 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 { fail(c, http.StatusInternalServerError, err.Error()) return } success(c, nil) } // ListNotes 获取笔记列表 func (h *NoteHandler) ListNotes(c *gin.Context) { var pinned *bool if v := c.Query("pinned"); v != "" { b := v == "true" || v == "1" pinned = &b } var favorite *bool if v := c.Query("favorite"); v != "" { b := v == "true" || v == "1" favorite = &b } items, total, totalPages, err := h.svc.ListNotes( c.DefaultQuery("page", "1"), c.DefaultQuery("page_size", ""), c.Query("category"), c.Query("tag"), pinned, favorite, ) if err != nil { fail(c, http.StatusInternalServerError, err.Error()) return } page := parseIntDefault(c.DefaultQuery("page", "1"), 1) pageSize := parseIntDefault(c.DefaultQuery("page_size", "20"), 20) c.JSON(http.StatusOK, PageResponse{ Code: 0, Message: "success", Data: items, Total: total, Page: page, PageSize: pageSize, TotalPages: totalPages, }) } // SearchNotes 搜索笔记 func (h *NoteHandler) SearchNotes(c *gin.Context) { keyword := c.Query("q") items, total, totalPages, err := h.svc.SearchNotes( keyword, c.DefaultQuery("page", "1"), c.DefaultQuery("page_size", ""), ) if err != nil { fail(c, http.StatusBadRequest, err.Error()) return } page := parseIntDefault(c.DefaultQuery("page", "1"), 1) pageSize := parseIntDefault(c.DefaultQuery("page_size", "20"), 20) c.JSON(http.StatusOK, PageResponse{ Code: 0, Message: "success", Data: items, Total: total, Page: page, PageSize: pageSize, TotalPages: totalPages, }) } // GetCategories 获取分类列表 func (h *NoteHandler) GetCategories(c *gin.Context) { categories, err := h.svc.GetCategories() if err != nil { fail(c, http.StatusInternalServerError, err.Error()) return } success(c, categories) } // GetTags 获取所有标签 func (h *NoteHandler) GetTags(c *gin.Context) { tags, err := h.svc.GetTags() if err != nil { fail(c, http.StatusInternalServerError, err.Error()) return } success(c, tags) } // GetTree 获取树形结构(管理后台用) func (h *NoteHandler) GetTree(c *gin.Context) { tree, err := h.svc.GetAllTree() if err != nil { fail(c, http.StatusInternalServerError, err.Error()) return } success(c, tree) } // GetPublicTree 获取公开树形结构(前台用) func (h *NoteHandler) GetPublicTree(c *gin.Context) { tree, err := h.svc.GetPublicTree() if err != nil { fail(c, http.StatusInternalServerError, err.Error()) return } success(c, tree) } func parseIntDefault(s string, defaultVal int) int { v, err := strconv.Atoi(s) if err != nil { return defaultVal } return v } // ─────────────── 回收站 ─────────────── // ListTrash 回收站列表 func (h *NoteHandler) ListTrash(c *gin.Context) { items, err := h.svc.ListTrash() if err != nil { fail(c, http.StatusInternalServerError, err.Error()) return } success(c, items) } // RestoreNote 恢复笔记 func (h *NoteHandler) RestoreNote(c *gin.Context) { 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 { fail(c, http.StatusInternalServerError, err.Error()) return } success(c, nil) } // PurgeNote 彻底删除笔记 func (h *NoteHandler) PurgeNote(c *gin.Context) { 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 { fail(c, http.StatusInternalServerError, err.Error()) return } success(c, nil) } // EmptyTrash 清空回收站 func (h *NoteHandler) EmptyTrash(c *gin.Context) { if err := h.svc.EmptyTrash(); err != nil { fail(c, http.StatusInternalServerError, err.Error()) return } success(c, nil) } // ─────────────── 版本历史 ─────────────── // ListVersions 版本列表 func (h *NoteHandler) ListVersions(c *gin.Context) { 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)) if err != nil { fail(c, http.StatusInternalServerError, err.Error()) return } success(c, versions) } // RestoreVersion 恢复到指定版本 func (h *NoteHandler) RestoreVersion(c *gin.Context) { id, err := strconv.ParseUint(c.Param("id"), 10, 64) if err != nil { fail(c, http.StatusBadRequest, "无效的笔记 ID") return } versionID, err := strconv.ParseUint(c.PostForm("version_id"), 10, 64) if err != nil { fail(c, http.StatusBadRequest, "无效的版本 ID") return } note, err := h.svc.RestoreVersion(uint(id), uint(versionID)) if err != nil { fail(c, http.StatusInternalServerError, err.Error()) return } success(c, note) } // ─────────────── 分享 ─────────────── // CreateShare 创建分享 func (h *NoteHandler) CreateShare(c *gin.Context) { 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) if err != nil { fail(c, http.StatusInternalServerError, err.Error()) return } success(c, gin.H{ "id": note.ID, "share_token": note.ShareToken, "url": "/share/" + note.ShareToken, "expire_at": note.ShareExpireAt, }) } // RevokeShare 撤销分享 func (h *NoteHandler) RevokeShare(c *gin.Context) { 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 { fail(c, http.StatusInternalServerError, err.Error()) return } success(c, nil) } // GetSharedNote 通过令牌获取分享笔记的 JSON 接口 // 分享的笔记若设置了访问密码,则需通过 password 参数/请求体校验 func (h *NoteHandler) GetSharedNote(c *gin.Context) { token := c.Param("token") var req struct { Password string `json:"password"` } if err := c.ShouldBindJSON(&req); err != nil { req.Password = c.Query("password") } note, err := h.svc.GetSharedNote(token) if err != nil { fail(c, http.StatusNotFound, err.Error()) return } // 若分享的笔记有访问密码,校验 if note.Password != "" { ok, _ := model.CheckPassword(req.Password, note.Password) if !ok { fail(c, http.StatusUnauthorized, "密码错误") return } } success(c, note) } // SharePage 分享阅读页(简单 HTML,展示分享的笔记内容) func (h *NoteHandler) SharePage(c *gin.Context) { c.HTML(http.StatusOK, "share.html", gin.H{ "token": c.Param("token"), }) } // ExportNote 导出笔记为 Markdown 文件 func (h *NoteHandler) ExportNote(c *gin.Context) { 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)) if err != nil { fail(c, http.StatusNotFound, err.Error()) return } // 构造 Markdown(带 YAML front matter) content := buildMarkdown(note) filename := sanitizeFileName(note.Title) + ".md" c.Header("Content-Disposition", "attachment; filename=\""+filename+"\"") c.Header("Content-Type", "text/markdown; charset=utf-8") c.String(http.StatusOK, content) } // ExportAll 批量导出全部笔记为 zip func (h *NoteHandler) ExportAll(c *gin.Context) { tree, err := h.svc.GetAllTree() if err != nil { fail(c, http.StatusInternalServerError, err.Error()) return } // 构建节点 map 与根节点列表 type node struct { item model.NoteListItem children []*node } nodeMap := make(map[uint]*node) for i := range tree { nodeMap[tree[i].ID] = &node{item: tree[i]} } var roots []*node for i := range tree { n := nodeMap[tree[i].ID] if tree[i].ParentID == 0 { roots = append(roots, n) } else if p, ok := nodeMap[tree[i].ParentID]; ok { p.children = append(p.children, n) } else { roots = append(roots, n) } } c.Header("Content-Type", "application/zip") c.Header("Content-Disposition", "attachment; filename=notes-export.zip") zw := zip.NewWriter(c.Writer) defer zw.Close() var zipNotes func(n *node, path string) error zipNotes = func(n *node, path string) error { if n.item.IsFolder { dirPath := filepath.Join(path, sanitizeFileName(n.item.Title)) for _, ch := range n.children { if err := zipNotes(ch, dirPath); err != nil { return err } } return nil } note, err := h.svc.GetNote(n.item.ID) if err != nil { return err } body := buildMarkdown(note) fname := filepath.Join(path, sanitizeFileName(note.Title)+".md") fw, err := zw.Create(fname) if err != nil { return err } _, err = fw.Write([]byte(body)) return err } for _, root := range roots { if err := zipNotes(root, ""); err != nil { fail(c, http.StatusInternalServerError, err.Error()) return } } } // ImportNotes 导入 Markdown 文件 func (h *NoteHandler) ImportNotes(c *gin.Context) { file, err := c.FormFile("file") if err != nil { fail(c, http.StatusBadRequest, "请选择要导入的文件") return } if !strings.HasSuffix(strings.ToLower(file.Filename), ".md") { fail(c, http.StatusBadRequest, "仅支持导入 .md 文件") return } // 读取文件内容 src, err := file.Open() if err != nil { fail(c, http.StatusInternalServerError, "读取文件失败") return } defer src.Close() contentBytes, err := io.ReadAll(src) if err != nil { fail(c, http.StatusInternalServerError, "读取文件失败") return } // 解析 front matter title, body := parseFrontMatter(string(contentBytes)) if title == "" { title = strings.TrimSuffix(file.Filename, ".md") } // 创建笔记 req := model.NoteCreateRequest{ Title: title, Content: body, } note, err := h.svc.CreateNote(req) if err != nil { fail(c, http.StatusInternalServerError, err.Error()) return } success(c, note) } // parseFrontMatter 解析 YAML front matter func parseFrontMatter(content string) (title string, body string) { if !strings.HasPrefix(content, "---") { return "", content } parts := strings.SplitN(content, "---", 3) if len(parts) < 3 { return "", content } frontMatter := parts[1] body = strings.TrimSpace(parts[2]) // 解析 title for _, line := range strings.Split(frontMatter, "\n") { if strings.HasPrefix(line, "title:") { title = strings.TrimSpace(strings.TrimPrefix(line, "title:")) break } } return title, body } // urlEncode URL 编码(RFC 3986) func urlEncode(s string) string { return url.QueryEscape(s) } // buildMarkdown 将笔记构造为带 YAML front matter 的 Markdown func buildMarkdown(note *model.Note) string { var b strings.Builder b.WriteString("---\n") b.WriteString("title: " + note.Title + "\n") if note.Category != "" { b.WriteString("category: " + note.Category + "\n") } // tags 是 JSON 数组字符串,转成 YAML 列表 if note.Tags != "" { var tags []string if err := json.Unmarshal([]byte(note.Tags), &tags); err == nil && len(tags) > 0 { b.WriteString("tags:\n") for _, t := range tags { b.WriteString(" - " + t + "\n") } } } b.WriteString("created: " + note.CreatedAt.Format("2006-01-02 15:04:05") + "\n") b.WriteString("updated: " + note.UpdatedAt.Format("2006-01-02 15:04:05") + "\n") b.WriteString("---\n\n") b.WriteString(note.Content) return b.String() } // sanitizeFileName 清理文件名中的非法字符 func sanitizeFileName(name string) string { replacer := strings.NewReplacer("/", "_", "\\", "_", ":", "_", "*", "_", "?", "_", "\"", "_", "<", "_", ">", "_", "|", "_") name = replacer.Replace(name) name = strings.TrimSpace(name) if name == "" { return fmt.Sprintf("note-%d", time.Now().Unix()) } return name } // ─────────────── 自动保存草稿 ─────────────── // SaveDraft 保存笔记草稿(自动保存) func (h *NoteHandler) SaveDraft(c *gin.Context) { id, err := strconv.ParseUint(c.Param("id"), 10, 64) if err != nil { fail(c, http.StatusBadRequest, "无效的笔记 ID") return } var req struct { Content string `json:"content"` } if err := c.ShouldBindJSON(&req); err != nil { fail(c, http.StatusBadRequest, "请求参数错误") return } if err := h.svc.SaveDraft(uint(id), req.Content); err != nil { fail(c, http.StatusInternalServerError, err.Error()) return } success(c, nil) } // DiscardDraft 清除指定笔记的草稿 func (h *NoteHandler) DiscardDraft(c *gin.Context) { 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 { fail(c, http.StatusInternalServerError, err.Error()) return } success(c, nil) } // ─────────────── 双向链接 / 知识图谱 ─────────────── // GetBacklinks 获取指定笔记的反向链接列表 func (h *NoteHandler) GetBacklinks(c *gin.Context) { 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), "") if err != nil { fail(c, http.StatusInternalServerError, err.Error()) return } success(c, links) } // GetKnowledgeGraph 获取知识图谱数据(节点 + 边) func (h *NoteHandler) GetKnowledgeGraph(c *gin.Context) { graph, err := h.svc.GetKnowledgeGraph() if err != nil { fail(c, http.StatusInternalServerError, err.Error()) return } success(c, graph) } // ─────────────── 标签管理 ─────────────── // GetTagUsage 获取标签及使用次数 func (h *NoteHandler) GetTagUsage(c *gin.Context) { usage, err := h.svc.GetTagUsage() if err != nil { fail(c, http.StatusInternalServerError, err.Error()) return } success(c, usage) } // RenameTag 重命名标签 func (h *NoteHandler) RenameTag(c *gin.Context) { var req struct { OldName string `json:"old_name" binding:"required"` NewName string `json:"new_name" binding:"required"` } if err := c.ShouldBindJSON(&req); err != nil { fail(c, http.StatusBadRequest, "请求参数错误") return } changed, err := h.svc.RenameTag(req.OldName, req.NewName) if err != nil { fail(c, http.StatusBadRequest, err.Error()) return } success(c, gin.H{"changed": changed}) } // MergeTag 合并标签(from → to) func (h *NoteHandler) MergeTag(c *gin.Context) { var req struct { From string `json:"from" binding:"required"` To string `json:"to" binding:"required"` } if err := c.ShouldBindJSON(&req); err != nil { fail(c, http.StatusBadRequest, "请求参数错误") return } changed, err := h.svc.MergeTag(req.From, req.To) if err != nil { fail(c, http.StatusBadRequest, err.Error()) return } success(c, gin.H{"changed": changed}) } // DeleteTag 删除标签(从所有笔记中移除) func (h *NoteHandler) DeleteTag(c *gin.Context) { var req struct { Name string `json:"name" binding:"required"` } if err := c.ShouldBindJSON(&req); err != nil { fail(c, http.StatusBadRequest, "请求参数错误") return } changed, err := h.svc.DeleteTag(req.Name) if err != nil { fail(c, http.StatusBadRequest, err.Error()) return } success(c, gin.H{"changed": changed}) }