feat: 初始化云笔记项目

功能特性:
- Markdown 编辑与实时预览
- 代码语法高亮
- 目录树形结构管理
- 图片粘贴上传
- Markdown 文件导入导出
- 笔记密码保护
- 前后端分离架构

技术栈:
- Go + Gin + GORM + SQLite
- 原生 HTML/CSS/JavaScript
- Highlight.js
Šī revīzija ir iekļauta:
Note Manager
2026-05-08 15:07:22 +08:00
revīzija c8f03dd932
19 mainīti faili ar 4333 papildinājumiem un 0 dzēšanām
+408
Parādīt failu
@@ -0,0 +1,408 @@
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)
}