c8f03dd932
功能特性: - Markdown 编辑与实时预览 - 代码语法高亮 - 目录树形结构管理 - 图片粘贴上传 - Markdown 文件导入导出 - 笔记密码保护 - 前后端分离架构 技术栈: - Go + Gin + GORM + SQLite - 原生 HTML/CSS/JavaScript - Highlight.js
409 lines
9.0 KiB
Go
409 lines
9.0 KiB
Go
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)
|
||
}
|