note_handler.go 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408
  1. package handler
  2. import (
  3. "io"
  4. "net/http"
  5. "net/url"
  6. "strconv"
  7. "strings"
  8. "github.com/gin-gonic/gin"
  9. "note-manager/model"
  10. "note-manager/service"
  11. )
  12. // NoteHandler 笔记请求处理器
  13. type NoteHandler struct {
  14. svc *service.NoteService
  15. }
  16. // NewNoteHandler 创建处理器实例
  17. func NewNoteHandler(svc *service.NoteService) *NoteHandler {
  18. return &NoteHandler{svc: svc}
  19. }
  20. // Response 通用响应结构
  21. type Response struct {
  22. Code int `json:"code"`
  23. Message string `json:"message"`
  24. Data interface{} `json:"data,omitempty"`
  25. }
  26. // PageResponse 分页响应结构
  27. type PageResponse struct {
  28. Code int `json:"code"`
  29. Message string `json:"message"`
  30. Data interface{} `json:"data"`
  31. Total int64 `json:"total"`
  32. Page int `json:"page"`
  33. PageSize int `json:"page_size"`
  34. TotalPages int `json:"total_pages"`
  35. }
  36. func success(c *gin.Context, data interface{}) {
  37. c.JSON(http.StatusOK, Response{Code: 0, Message: "success", Data: data})
  38. }
  39. func fail(c *gin.Context, status int, msg string) {
  40. c.JSON(status, Response{Code: -1, Message: msg})
  41. }
  42. // requireAuth 需要管理员权限
  43. func requireAuth(c *gin.Context) bool {
  44. token, err := c.Cookie("admin_token")
  45. if err != nil || token != "authenticated" {
  46. c.JSON(http.StatusUnauthorized, Response{Code: 401, Message: "请先登录后台管理"})
  47. return false
  48. }
  49. return true
  50. }
  51. // CreateNote 创建笔记
  52. func (h *NoteHandler) CreateNote(c *gin.Context) {
  53. if !requireAuth(c) {
  54. return
  55. }
  56. var req model.NoteCreateRequest
  57. if err := c.ShouldBindJSON(&req); err != nil {
  58. fail(c, http.StatusBadRequest, "请求参数错误: "+err.Error())
  59. return
  60. }
  61. note, err := h.svc.CreateNote(req)
  62. if err != nil {
  63. fail(c, http.StatusInternalServerError, err.Error())
  64. return
  65. }
  66. success(c, note)
  67. }
  68. // GetNote 获取笔记详情
  69. func (h *NoteHandler) GetNote(c *gin.Context) {
  70. id, err := strconv.ParseUint(c.Param("id"), 10, 64)
  71. if err != nil {
  72. fail(c, http.StatusBadRequest, "无效的笔记 ID")
  73. return
  74. }
  75. note, err := h.svc.GetNote(uint(id))
  76. if err != nil {
  77. fail(c, http.StatusNotFound, err.Error())
  78. return
  79. }
  80. success(c, note)
  81. }
  82. // AccessNote 验证密码后获取笔记内容
  83. func (h *NoteHandler) AccessNote(c *gin.Context) {
  84. id, err := strconv.ParseUint(c.Param("id"), 10, 64)
  85. if err != nil {
  86. fail(c, http.StatusBadRequest, "无效的笔记 ID")
  87. return
  88. }
  89. var req struct {
  90. Password string `json:"password"`
  91. }
  92. if err := c.ShouldBindJSON(&req); err != nil {
  93. // 没有密码参数,尝试从 URL 参数获取
  94. req.Password = c.Query("password")
  95. }
  96. note, err := h.svc.GetNoteContent(uint(id), req.Password)
  97. if err != nil {
  98. if err.Error() == "密码错误" {
  99. fail(c, http.StatusUnauthorized, err.Error())
  100. return
  101. }
  102. fail(c, http.StatusNotFound, err.Error())
  103. return
  104. }
  105. success(c, note)
  106. }
  107. // UpdateNote 更新笔记
  108. func (h *NoteHandler) UpdateNote(c *gin.Context) {
  109. if !requireAuth(c) {
  110. return
  111. }
  112. id, err := strconv.ParseUint(c.Param("id"), 10, 64)
  113. if err != nil {
  114. fail(c, http.StatusBadRequest, "无效的笔记 ID")
  115. return
  116. }
  117. var req model.NoteUpdateRequest
  118. if err := c.ShouldBindJSON(&req); err != nil {
  119. fail(c, http.StatusBadRequest, "请求参数错误: "+err.Error())
  120. return
  121. }
  122. note, err := h.svc.UpdateNote(uint(id), req)
  123. if err != nil {
  124. fail(c, http.StatusInternalServerError, err.Error())
  125. return
  126. }
  127. success(c, note)
  128. }
  129. // DeleteNote 删除笔记
  130. func (h *NoteHandler) DeleteNote(c *gin.Context) {
  131. if !requireAuth(c) {
  132. return
  133. }
  134. id, err := strconv.ParseUint(c.Param("id"), 10, 64)
  135. if err != nil {
  136. fail(c, http.StatusBadRequest, "无效的笔记 ID")
  137. return
  138. }
  139. if err := h.svc.DeleteNote(uint(id)); err != nil {
  140. fail(c, http.StatusInternalServerError, err.Error())
  141. return
  142. }
  143. success(c, nil)
  144. }
  145. // ListNotes 获取笔记列表
  146. func (h *NoteHandler) ListNotes(c *gin.Context) {
  147. var pinned *bool
  148. if v := c.Query("pinned"); v != "" {
  149. b := v == "true" || v == "1"
  150. pinned = &b
  151. }
  152. var favorite *bool
  153. if v := c.Query("favorite"); v != "" {
  154. b := v == "true" || v == "1"
  155. favorite = &b
  156. }
  157. items, total, totalPages, err := h.svc.ListNotes(
  158. c.DefaultQuery("page", "1"),
  159. c.DefaultQuery("page_size", ""),
  160. c.Query("category"),
  161. c.Query("tag"),
  162. pinned,
  163. favorite,
  164. )
  165. if err != nil {
  166. fail(c, http.StatusInternalServerError, err.Error())
  167. return
  168. }
  169. page := parseIntDefault(c.DefaultQuery("page", "1"), 1)
  170. pageSize := parseIntDefault(c.DefaultQuery("page_size", "20"), 20)
  171. c.JSON(http.StatusOK, PageResponse{
  172. Code: 0,
  173. Message: "success",
  174. Data: items,
  175. Total: total,
  176. Page: page,
  177. PageSize: pageSize,
  178. TotalPages: totalPages,
  179. })
  180. }
  181. // SearchNotes 搜索笔记
  182. func (h *NoteHandler) SearchNotes(c *gin.Context) {
  183. keyword := c.Query("q")
  184. items, total, totalPages, err := h.svc.SearchNotes(
  185. keyword,
  186. c.DefaultQuery("page", "1"),
  187. c.DefaultQuery("page_size", ""),
  188. )
  189. if err != nil {
  190. fail(c, http.StatusBadRequest, err.Error())
  191. return
  192. }
  193. page := parseIntDefault(c.DefaultQuery("page", "1"), 1)
  194. pageSize := parseIntDefault(c.DefaultQuery("page_size", "20"), 20)
  195. c.JSON(http.StatusOK, PageResponse{
  196. Code: 0,
  197. Message: "success",
  198. Data: items,
  199. Total: total,
  200. Page: page,
  201. PageSize: pageSize,
  202. TotalPages: totalPages,
  203. })
  204. }
  205. // GetCategories 获取分类列表
  206. func (h *NoteHandler) GetCategories(c *gin.Context) {
  207. categories, err := h.svc.GetCategories()
  208. if err != nil {
  209. fail(c, http.StatusInternalServerError, err.Error())
  210. return
  211. }
  212. success(c, categories)
  213. }
  214. // GetTags 获取所有标签
  215. func (h *NoteHandler) GetTags(c *gin.Context) {
  216. tags, err := h.svc.GetTags()
  217. if err != nil {
  218. fail(c, http.StatusInternalServerError, err.Error())
  219. return
  220. }
  221. success(c, tags)
  222. }
  223. // GetTree 获取树形结构(管理后台用)
  224. func (h *NoteHandler) GetTree(c *gin.Context) {
  225. tree, err := h.svc.GetAllTree()
  226. if err != nil {
  227. fail(c, http.StatusInternalServerError, err.Error())
  228. return
  229. }
  230. success(c, tree)
  231. }
  232. // GetPublicTree 获取公开树形结构(前台用)
  233. func (h *NoteHandler) GetPublicTree(c *gin.Context) {
  234. tree, err := h.svc.GetPublicTree()
  235. if err != nil {
  236. fail(c, http.StatusInternalServerError, err.Error())
  237. return
  238. }
  239. success(c, tree)
  240. }
  241. func parseIntDefault(s string, defaultVal int) int {
  242. v, err := strconv.Atoi(s)
  243. if err != nil {
  244. return defaultVal
  245. }
  246. return v
  247. }
  248. // ExportNote 导出笔记为 Markdown 文件
  249. func (h *NoteHandler) ExportNote(c *gin.Context) {
  250. idStr := c.Param("id")
  251. id, err := strconv.ParseUint(idStr, 10, 64)
  252. if err != nil {
  253. fail(c, http.StatusBadRequest, "无效的笔记 ID")
  254. return
  255. }
  256. note, err := h.svc.GetNote(uint(id))
  257. if err != nil {
  258. fail(c, http.StatusNotFound, err.Error())
  259. return
  260. }
  261. // 如果是目录,导出目录下所有笔记
  262. if note.IsFolder {
  263. fail(c, http.StatusBadRequest, "不支持导出目录,请选择具体笔记")
  264. return
  265. }
  266. // 设置下载头
  267. filename := note.Title + ".md"
  268. c.Header("Content-Disposition", "attachment; filename*=UTF-8''"+urlEncode(filename))
  269. c.Header("Content-Type", "text/markdown; charset=utf-8")
  270. // 添加 front matter
  271. frontMatter := "---\n"
  272. frontMatter += "title: " + note.Title + "\n"
  273. if note.Category != "" {
  274. frontMatter += "category: " + note.Category + "\n"
  275. }
  276. if note.Tags != "" {
  277. frontMatter += "tags: " + note.Tags + "\n"
  278. }
  279. frontMatter += "---\n\n"
  280. c.String(http.StatusOK, frontMatter+note.Content)
  281. }
  282. // ImportNotes 导入 Markdown 文件创建笔记
  283. func (h *NoteHandler) ImportNotes(c *gin.Context) {
  284. file, err := c.FormFile("file")
  285. if err != nil {
  286. fail(c, http.StatusBadRequest, "请选择文件")
  287. return
  288. }
  289. // 验证文件类型
  290. if file.Header.Get("Content-Type") != "text/markdown" &&
  291. !strings.HasSuffix(file.Filename, ".md") {
  292. fail(c, http.StatusBadRequest, "仅支持 .md 文件")
  293. return
  294. }
  295. // 读取文件内容
  296. f, err := file.Open()
  297. if err != nil {
  298. fail(c, http.StatusInternalServerError, "读取文件失败")
  299. return
  300. }
  301. defer f.Close()
  302. contentBytes, err := io.ReadAll(f)
  303. if err != nil {
  304. fail(c, http.StatusInternalServerError, "读取文件失败")
  305. return
  306. }
  307. // 解析 front matter
  308. title, body := parseFrontMatter(string(contentBytes))
  309. if title == "" {
  310. title = strings.TrimSuffix(file.Filename, ".md")
  311. }
  312. // 创建笔记
  313. req := model.NoteCreateRequest{
  314. Title: title,
  315. Content: body,
  316. }
  317. note, err := h.svc.CreateNote(req)
  318. if err != nil {
  319. fail(c, http.StatusInternalServerError, err.Error())
  320. return
  321. }
  322. success(c, note)
  323. }
  324. // parseFrontMatter 解析 YAML front matter
  325. func parseFrontMatter(content string) (title string, body string) {
  326. if !strings.HasPrefix(content, "---") {
  327. return "", content
  328. }
  329. parts := strings.SplitN(content, "---", 3)
  330. if len(parts) < 3 {
  331. return "", content
  332. }
  333. frontMatter := parts[1]
  334. body = strings.TrimSpace(parts[2])
  335. // 解析 title
  336. for _, line := range strings.Split(frontMatter, "\n") {
  337. if strings.HasPrefix(line, "title:") {
  338. title = strings.TrimSpace(strings.TrimPrefix(line, "title:"))
  339. break
  340. }
  341. }
  342. return title, body
  343. }
  344. // urlEncode URL 编码(RFC 3986)
  345. func urlEncode(s string) string {
  346. return url.QueryEscape(s)
  347. }