package handler import ( "io" "net/http" "net/url" "strconv" "strings" "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}) } // requireAuth 需要管理员权限 func requireAuth(c *gin.Context) bool { token, err := c.Cookie("admin_token") if err != nil || token != "authenticated" { c.JSON(http.StatusUnauthorized, Response{Code: 401, Message: "请先登录后台管理"}) return false } return true } // CreateNote 创建笔记 func (h *NoteHandler) CreateNote(c *gin.Context) { if !requireAuth(c) { return } 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 } 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, 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 } success(c, note) } // UpdateNote 更新笔记 func (h *NoteHandler) UpdateNote(c *gin.Context) { if !requireAuth(c) { return } 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) { if !requireAuth(c) { return } 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 } // ExportNote 导出笔记为 Markdown 文件 func (h *NoteHandler) ExportNote(c *gin.Context) { idStr := c.Param("id") id, err := strconv.ParseUint(idStr, 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.IsFolder { fail(c, http.StatusBadRequest, "不支持导出目录,请选择具体笔记") return } // 设置下载头 filename := note.Title + ".md" c.Header("Content-Disposition", "attachment; filename*=UTF-8''"+urlEncode(filename)) c.Header("Content-Type", "text/markdown; charset=utf-8") // 添加 front matter frontMatter := "---\n" frontMatter += "title: " + note.Title + "\n" if note.Category != "" { frontMatter += "category: " + note.Category + "\n" } if note.Tags != "" { frontMatter += "tags: " + note.Tags + "\n" } frontMatter += "---\n\n" c.String(http.StatusOK, frontMatter+note.Content) } // ImportNotes 导入 Markdown 文件创建笔记 func (h *NoteHandler) ImportNotes(c *gin.Context) { file, err := c.FormFile("file") if err != nil { fail(c, http.StatusBadRequest, "请选择文件") return } // 验证文件类型 if file.Header.Get("Content-Type") != "text/markdown" && !strings.HasSuffix(file.Filename, ".md") { fail(c, http.StatusBadRequest, "仅支持 .md 文件") return } // 读取文件内容 f, err := file.Open() if err != nil { fail(c, http.StatusInternalServerError, "读取文件失败") return } defer f.Close() contentBytes, err := io.ReadAll(f) 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) }