package handler import ( "errors" "net/http" "net/url" "strconv" "github.com/freefire/jiang13-bbs/middleware" "github.com/freefire/jiang13-bbs/model" "github.com/freefire/jiang13-bbs/service" "github.com/gin-gonic/gin" "gorm.io/gorm" ) // ---------- 公开端 ---------- // LibraryList 已发布条目目录 func (h *Handlers) LibraryList(c *gin.Context) { list, err := h.LibrarySvc.ListPublished() if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "获取书库失败"}) return } c.JSON(http.StatusOK, gin.H{"docs": nonNilSlice(list)}) } // LibraryDetail 公开条目详情(按 slug,仅已发布) func (h *Handlers) LibraryDetail(c *gin.Context) { d, err := h.LibrarySvc.GetPublishedBySlug(c.Param("slug")) if err != nil { if errors.Is(err, service.ErrLibraryNotFound) { c.JSON(http.StatusNotFound, gin.H{"error": "条目不存在"}) return } c.JSON(http.StatusInternalServerError, gin.H{"error": "获取条目失败"}) return } c.JSON(http.StatusOK, gin.H{"doc": d}) } // LibraryFileDownload 强制下载(attachment;成功后计数) func (h *Handlers) LibraryFileDownload(c *gin.Context) { f, ok := h.libraryPublicFile(c) if !ok { return } h.LibrarySvc.IncrDownload(f.ID) c.Header("Content-Disposition", "attachment; filename*=UTF-8''"+url.PathEscape(f.Name)) c.Header("X-Content-Type-Options", "nosniff") c.Header("Content-Security-Policy", "sandbox") ct := f.MIME if ct == "" { ct = "application/octet-stream" } c.Header("Content-Type", ct) c.File(h.LibrarySvc.FilePath(f)) } // libraryPublicFile 公开文件公共校验(存在 + 所属条目已发布) func (h *Handlers) libraryPublicFile(c *gin.Context) (*model.LibraryFile, bool) { id, err := strconv.ParseUint(c.Param("fid"), 10, 64) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "无效的文件 ID"}) return nil, false } f, err := h.LibrarySvc.GetPublicFile(uint(id)) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "文件不存在"}) return nil, false } return f, true } // ---------- 管理端 ---------- // AdminListLibraryDocs 后台条目列表(含未发布) func (h *Handlers) AdminListLibraryDocs(c *gin.Context) { list, err := h.LibrarySvc.ListAll() if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "获取书库失败"}) return } c.JSON(http.StatusOK, gin.H{"docs": nonNilSlice(list)}) } // AdminCreateLibraryDoc 新建条目 func (h *Handlers) AdminCreateLibraryDoc(c *gin.Context) { var in service.LibraryInput if err := c.ShouldBindJSON(&in); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "请求参数无效"}) return } d, err := h.LibrarySvc.Create(&in) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } c.JSON(http.StatusCreated, gin.H{"doc": d}) } // AdminUpdateLibraryDoc 编辑条目 func (h *Handlers) AdminUpdateLibraryDoc(c *gin.Context) { id, err := strconv.ParseUint(c.Param("id"), 10, 64) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "无效的条目 ID"}) return } var in service.LibraryInput if err := c.ShouldBindJSON(&in); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "请求参数无效"}) return } d, err := h.LibrarySvc.Update(uint(id), &in) if err != nil { if errors.Is(err, service.ErrLibraryNotFound) || errors.Is(err, gorm.ErrRecordNotFound) { c.JSON(http.StatusNotFound, gin.H{"error": "条目不存在"}) return } c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"doc": d}) } // AdminDeleteLibraryDoc 删除条目(软删) func (h *Handlers) AdminDeleteLibraryDoc(c *gin.Context) { id, err := strconv.ParseUint(c.Param("id"), 10, 64) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "无效的条目 ID"}) return } if err := h.LibrarySvc.Delete(uint(id)); err != nil { if errors.Is(err, service.ErrLibraryNotFound) { c.JSON(http.StatusNotFound, gin.H{"error": "条目不存在"}) return } c.JSON(http.StatusInternalServerError, gin.H{"error": "删除失败"}) return } c.JSON(http.StatusOK, gin.H{"ok": true}) } // AdminPurgeLibraryDoc 彻底删除条目(仅限已软删;清除章节/文件行与磁盘文件) func (h *Handlers) AdminPurgeLibraryDoc(c *gin.Context) { id, err := strconv.ParseUint(c.Param("id"), 10, 64) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "无效的条目 ID"}) return } if err := h.LibrarySvc.Purge(uint(id)); err != nil { if errors.Is(err, service.ErrLibraryNotFound) { c.JSON(http.StatusNotFound, gin.H{"error": "条目不存在"}) return } if errors.Is(err, service.ErrLibraryNotDeleted) { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } c.JSON(http.StatusInternalServerError, gin.H{"error": "删除失败"}) return } c.JSON(http.StatusOK, gin.H{"ok": true}) } // AdminUploadLibraryFile 上传文件到条目 func (h *Handlers) AdminUploadLibraryFile(c *gin.Context) { docID, err := strconv.ParseUint(c.Param("id"), 10, 64) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "无效的条目 ID"}) return } claims := middleware.CurrentUser(c) if claims == nil { c.JSON(http.StatusUnauthorized, gin.H{"error": "请先登录"}) return } maxBytes, err := h.Setting.AttachmentMaxBytes() if err != nil || maxBytes < 1 { maxBytes = service.FileMaxBytes } overhead := int64(64 << 10) // multipart 边界开销 limit := maxBytes + overhead if c.Request.ContentLength > limit { c.JSON(http.StatusRequestEntityTooLarge, gin.H{"error": "文件过大"}) return } c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, limit) file, err := c.FormFile("file") if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "请选择文件"}) return } if file.Size > maxBytes { mb := int(maxBytes >> 20) if mb < 1 { mb = 1 } c.JSON(http.StatusRequestEntityTooLarge, gin.H{"error": "文件不能超过 " + strconv.Itoa(mb) + "MB"}) return } src, err := file.Open() if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "无法读取文件"}) return } defer src.Close() f, err := h.LibrarySvc.AddFile(uint(docID), claims.ID, file.Filename, src) if err != nil { if errors.Is(err, service.ErrLibraryExtDenied) { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } if errors.Is(err, service.ErrLibraryNotFound) { c.JSON(http.StatusNotFound, gin.H{"error": "条目不存在"}) return } c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } c.JSON(http.StatusCreated, gin.H{"file": f}) } // AdminDeleteLibraryFile 删除条目文件 func (h *Handlers) AdminDeleteLibraryFile(c *gin.Context) { id, err := strconv.ParseUint(c.Param("fid"), 10, 64) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "无效的文件 ID"}) return } if err := h.LibrarySvc.DeleteFile(uint(id)); err != nil { if errors.Is(err, service.ErrLibraryNotFound) { c.JSON(http.StatusNotFound, gin.H{"error": "文件不存在"}) return } c.JSON(http.StatusInternalServerError, gin.H{"error": "删除失败"}) return } c.JSON(http.StatusOK, gin.H{"ok": true}) } // ---------- 管理端:章节 ---------- // AdminCreateLibrarySection 新建章节(parent_id 空=章,非空=节) func (h *Handlers) AdminCreateLibrarySection(c *gin.Context) { docID, err := strconv.ParseUint(c.Param("id"), 10, 64) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "无效的条目 ID"}) return } var in service.SectionInput if err := c.ShouldBindJSON(&in); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "请求参数无效"}) return } sec, err := h.LibrarySvc.CreateSection(uint(docID), &in) if err != nil { if errors.Is(err, service.ErrLibraryNotFound) { c.JSON(http.StatusNotFound, gin.H{"error": "条目不存在"}) return } c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } c.JSON(http.StatusCreated, gin.H{"section": sec}) } // AdminUpdateLibrarySection 编辑章节标题与正文 func (h *Handlers) AdminUpdateLibrarySection(c *gin.Context) { id, err := strconv.ParseUint(c.Param("sid"), 10, 64) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "无效的章节 ID"}) return } var in service.SectionInput if err := c.ShouldBindJSON(&in); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "请求参数无效"}) return } sec, err := h.LibrarySvc.UpdateSection(uint(id), &in) if err != nil { if errors.Is(err, service.ErrSectionNotFound) { c.JSON(http.StatusNotFound, gin.H{"error": "章节不存在"}) return } c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"section": sec}) } // AdminDeleteLibrarySection 删除章节(章级联删除其下小节) func (h *Handlers) AdminDeleteLibrarySection(c *gin.Context) { id, err := strconv.ParseUint(c.Param("sid"), 10, 64) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "无效的章节 ID"}) return } if err := h.LibrarySvc.DeleteSection(uint(id)); err != nil { if errors.Is(err, service.ErrSectionNotFound) { c.JSON(http.StatusNotFound, gin.H{"error": "章节不存在"}) return } c.JSON(http.StatusInternalServerError, gin.H{"error": "删除失败"}) return } c.JSON(http.StatusOK, gin.H{"ok": true}) } // AdminMoveLibrarySection 章节上移/下移(同父兄弟间交换顺序) func (h *Handlers) AdminMoveLibrarySection(c *gin.Context) { id, err := strconv.ParseUint(c.Param("sid"), 10, 64) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "无效的章节 ID"}) return } var in struct { Direction string `json:"direction"` } if err := c.ShouldBindJSON(&in); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "请求参数无效"}) return } if err := h.LibrarySvc.MoveSection(uint(id), in.Direction); err != nil { if errors.Is(err, service.ErrSectionNotFound) { c.JSON(http.StatusNotFound, gin.H{"error": "章节不存在"}) return } c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"ok": true}) } // AdminImportLibraryMarkdown 导入 md 并按标题拆章 func (h *Handlers) AdminImportLibraryMarkdown(c *gin.Context) { docID, err := strconv.ParseUint(c.Param("id"), 10, 64) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "无效的条目 ID"}) return } var in struct { Filename string `json:"filename"` Content string `json:"content"` SplitLevel string `json:"split_level"` } if err := c.ShouldBindJSON(&in); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "请求参数无效"}) return } sections, err := h.LibrarySvc.ImportMarkdown(uint(docID), in.Filename, in.Content, in.SplitLevel) if err != nil { if errors.Is(err, service.ErrLibraryNotFound) { c.JSON(http.StatusNotFound, gin.H{"error": "条目不存在"}) return } c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } c.JSON(http.StatusCreated, gin.H{"created": len(sections), "sections": nonNilSlice(sections)}) } // AdminImportLibraryMarkdownBatch 批量导入 md:多个文件按传入顺序依次拆章 func (h *Handlers) AdminImportLibraryMarkdownBatch(c *gin.Context) { docID, err := strconv.ParseUint(c.Param("id"), 10, 64) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "无效的条目 ID"}) return } var in struct { Files []service.ImportFileInput `json:"files"` SplitLevel string `json:"split_level"` } if err := c.ShouldBindJSON(&in); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "请求参数无效"}) return } sections, err := h.LibrarySvc.ImportMarkdownBatch(uint(docID), in.Files, in.SplitLevel) if err != nil { if errors.Is(err, service.ErrLibraryNotFound) { c.JSON(http.StatusNotFound, gin.H{"error": "条目不存在"}) return } c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } c.JSON(http.StatusCreated, gin.H{"created": len(sections), "sections": nonNilSlice(sections)}) }