Files
jiang13-bbs/backend/handler/snippet.go

425 lines
13 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package handler
import (
"errors"
"net/http"
"strconv"
"github.com/freefire/jiang13-bbs/middleware"
"github.com/freefire/jiang13-bbs/model"
"github.com/freefire/jiang13-bbs/service"
"github.com/gin-gonic/gin"
)
// snippetError 统一错误映射
func snippetError(c *gin.Context, err error) {
switch {
case errors.Is(err, service.ErrSnippetNotFound):
c.JSON(http.StatusNotFound, gin.H{"error": "代码片段不存在"})
case errors.Is(err, service.ErrSnippetForbidden):
c.JSON(http.StatusForbidden, gin.H{"error": "无权操作该代码片段"})
case errors.Is(err, service.ErrSnippetBadInput):
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
case errors.Is(err, service.ErrCommentNotFound):
c.JSON(http.StatusNotFound, gin.H{"error": "评论不存在"})
case errors.Is(err, service.ErrCommentForbidden):
c.JSON(http.StatusForbidden, gin.H{"error": "无权操作该评论"})
default:
c.JSON(http.StatusInternalServerError, gin.H{"error": "服务器错误"})
}
}
// SnippetList 公开片段广场
func (h *Handlers) SnippetList(c *gin.Context) {
q := c.Query("q")
lang := c.Query("lang")
sort := c.Query("sort")
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
size, _ := strconv.Atoi(c.DefaultQuery("size", "12"))
items, total, err := h.SnippetSvc.ListPublic(q, lang, sort, page, size)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "获取片段列表失败"})
return
}
c.JSON(http.StatusOK, gin.H{"items": nonNilSlice(items), "total": total})
}
// SnippetLanguages 语言白名单
func (h *Handlers) SnippetLanguages(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"languages": service.SnippetLanguages()})
}
// SnippetStats 广场聚合统计
func (h *Handlers) SnippetStats(c *gin.Context) {
stats, err := h.SnippetSvc.Stats()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "获取统计失败"})
return
}
c.JSON(http.StatusOK, gin.H{"stats": stats})
}
// SnippetDetail 公开片段详情
func (h *Handlers) SnippetDetail(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 viewerID uint
if claims := middleware.CurrentUser(c); claims != nil {
viewerID = claims.ID
}
isStaff := h.isStaff(c)
detail, err := h.SnippetSvc.GetPublic(uint(id), viewerID, isStaff)
if err != nil {
snippetError(c, err)
return
}
c.JSON(http.StatusOK, gin.H{"snippet": detail})
}
// SnippetMine 我的片段列表
func (h *Handlers) SnippetMine(c *gin.Context) {
claims := middleware.CurrentUser(c)
if claims == nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "请先登录"})
return
}
q := c.Query("q")
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
size, _ := strconv.Atoi(c.DefaultQuery("size", "12"))
items, total, err := h.SnippetSvc.ListMine(claims.ID, q, page, size)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "获取片段列表失败"})
return
}
c.JSON(http.StatusOK, gin.H{"items": nonNilSlice(items), "total": total})
}
// SnippetCreate 创建片段
func (h *Handlers) SnippetCreate(c *gin.Context) {
claims := middleware.CurrentUser(c)
if claims == nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "请先登录"})
return
}
var in service.SnippetInput
if err := c.ShouldBindJSON(&in); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "请求参数无效"})
return
}
snippet, err := h.SnippetSvc.Create(&in, claims.ID)
if err != nil {
snippetError(c, err)
return
}
c.JSON(http.StatusCreated, gin.H{"snippet": snippet})
}
// SnippetUpdate 更新片段
func (h *Handlers) SnippetUpdate(c *gin.Context) {
claims := middleware.CurrentUser(c)
if claims == nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "请先登录"})
return
}
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的片段 ID"})
return
}
var in service.SnippetInput
if err := c.ShouldBindJSON(&in); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "请求参数无效"})
return
}
snippet, err := h.SnippetSvc.Update(uint(id), &in, claims.ID, h.isStaff(c))
if err != nil {
snippetError(c, err)
return
}
c.JSON(http.StatusOK, gin.H{"snippet": snippet})
}
// SnippetDelete 删除片段(owner 或 staff)
func (h *Handlers) SnippetDelete(c *gin.Context) {
claims := middleware.CurrentUser(c)
if claims == nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "请先登录"})
return
}
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的片段 ID"})
return
}
if err := h.SnippetSvc.Delete(uint(id), claims.ID, h.isStaff(c)); err != nil {
snippetError(c, err)
return
}
c.JSON(http.StatusOK, gin.H{"ok": true})
}
// SnippetFork Fork 公开片段
func (h *Handlers) SnippetFork(c *gin.Context) {
claims := middleware.CurrentUser(c)
if claims == nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "请先登录"})
return
}
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的片段 ID"})
return
}
snippet, err := h.SnippetSvc.Fork(uint(id), claims.ID)
if err != nil {
snippetError(c, err)
return
}
c.JSON(http.StatusCreated, gin.H{"snippet": snippet})
}
// SnippetCopy 记录复制(节流由 service 内部处理)
func (h *Handlers) SnippetCopy(c *gin.Context) {
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的片段 ID"})
return
}
key := c.ClientIP()
if claims := middleware.CurrentUser(c); claims != nil {
key = "user:" + strconv.FormatUint(uint64(claims.ID), 10)
}
if err := h.SnippetSvc.RecordCopy(uint(id), key); err != nil {
snippetError(c, err)
return
}
c.JSON(http.StatusOK, gin.H{"ok": true})
}
// SnippetHistory 片段编辑历史
func (h *Handlers) SnippetHistory(c *gin.Context) {
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的片段 ID"})
return
}
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
size, _ := strconv.Atoi(c.DefaultQuery("size", "10"))
items, total, err := h.SnippetSvc.ListHistory(uint(id), page, size)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "获取历史失败"})
return
}
c.JSON(http.StatusOK, gin.H{"items": nonNilSlice(items), "total": total})
}
// SnippetForks 公开 fork 列表
func (h *Handlers) SnippetForks(c *gin.Context) {
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的片段 ID"})
return
}
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
size, _ := strconv.Atoi(c.DefaultQuery("size", "12"))
items, total, err := h.SnippetSvc.ListForks(uint(id), page, size)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "获取 Fork 列表失败"})
return
}
c.JSON(http.StatusOK, gin.H{"items": nonNilSlice(items), "total": total})
}
// SnippetComments 片段评论列表
func (h *Handlers) SnippetComments(c *gin.Context) {
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的片段 ID"})
return
}
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
size, _ := strconv.Atoi(c.DefaultQuery("size", "20"))
items, total, err := h.SnippetSvc.ListComments(uint(id), page, size)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "获取评论失败"})
return
}
c.JSON(http.StatusOK, gin.H{"items": nonNilSlice(items), "total": total})
}
// SnippetCommentCreate 创建片段评论
func (h *Handlers) SnippetCommentCreate(c *gin.Context) {
claims := middleware.CurrentUser(c)
if claims == nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "请先登录"})
return
}
snippetID, err := strconv.ParseUint(c.Param("id"), 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的片段 ID"})
return
}
var in struct {
Content string `json:"content"`
ParentID *uint `json:"parent_id"`
}
if err := c.ShouldBindJSON(&in); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "请求参数无效"})
return
}
comment, err := h.SnippetSvc.CreateComment(claims.ID, uint(snippetID), in.Content, in.ParentID)
if err != nil {
snippetError(c, err)
return
}
c.JSON(http.StatusCreated, gin.H{"comment": comment})
}
// SnippetCommentUpdate 更新片段评论
func (h *Handlers) SnippetCommentUpdate(c *gin.Context) {
claims := middleware.CurrentUser(c)
if claims == nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "请先登录"})
return
}
cid, err := strconv.ParseUint(c.Param("cid"), 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的评论 ID"})
return
}
var in struct {
Content string `json:"content"`
}
if err := c.ShouldBindJSON(&in); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "请求参数无效"})
return
}
comment, err := h.SnippetSvc.UpdateComment(uint(cid), claims.ID, in.Content)
if err != nil {
snippetError(c, err)
return
}
c.JSON(http.StatusOK, gin.H{"comment": comment})
}
// SnippetCommentDelete 删除片段评论
func (h *Handlers) SnippetCommentDelete(c *gin.Context) {
claims := middleware.CurrentUser(c)
if claims == nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "请先登录"})
return
}
cid, err := strconv.ParseUint(c.Param("cid"), 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的评论 ID"})
return
}
if err := h.SnippetSvc.DeleteComment(uint(cid), claims.ID, h.isStaff(c)); err != nil {
snippetError(c, err)
return
}
c.JSON(http.StatusOK, gin.H{"ok": true})
}
// SnippetCommentHistory 片段评论编辑历史(登录即可见;分页)
func (h *Handlers) SnippetCommentHistory(c *gin.Context) {
claims := middleware.CurrentUser(c)
cid, err := strconv.ParseUint(c.Param("cid"), 10, 64)
if err != nil || cid == 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的评论 ID"})
return
}
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
size, _ := strconv.Atoi(c.DefaultQuery("size", "10"))
items, total, err := h.SnippetSvc.ListCommentHistory(h.loadActor(claims.ID), uint(cid), page, size)
if err != nil {
switch {
case errors.Is(err, service.ErrCommentNotFound):
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
case errors.Is(err, service.ErrCommentForbidden):
c.JSON(http.StatusForbidden, gin.H{"error": err.Error()})
default:
c.JSON(http.StatusInternalServerError, gin.H{"error": "获取编辑历史失败"})
}
return
}
if page < 1 {
page = 1
}
if size < 1 || size > 50 {
size = 10
}
c.JSON(http.StatusOK, gin.H{
"items": items,
"total": total,
"page": page,
"size": size,
})
}
// AdminSnippetList 管理端片段列表
func (h *Handlers) AdminSnippetList(c *gin.Context) {
q := c.Query("q")
visibility := c.Query("visibility")
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
size, _ := strconv.Atoi(c.DefaultQuery("size", "20"))
items, total, err := h.SnippetSvc.AdminList(q, visibility, page, size)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "获取片段列表失败"})
return
}
c.JSON(http.StatusOK, gin.H{"items": nonNilSlice(items), "total": total})
}
// AdminSnippetDelete 管理端软删片段
func (h *Handlers) AdminSnippetDelete(c *gin.Context) {
claims := middleware.CurrentUser(c)
if claims == nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "请先登录"})
return
}
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的片段 ID"})
return
}
if err := h.SnippetSvc.AdminDelete(uint(id), claims.ID); err != nil {
snippetError(c, err)
return
}
c.JSON(http.StatusOK, gin.H{"ok": true})
}
// AdminSnippetSetVisibility 管理端强制修改可见性
func (h *Handlers) AdminSnippetSetVisibility(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 struct {
Visibility string `json:"visibility"`
}
if err := c.ShouldBindJSON(&in); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "请求参数无效"})
return
}
if err := h.SnippetSvc.AdminSetVisibility(uint(id), in.Visibility); err != nil {
snippetError(c, err)
return
}
c.JSON(http.StatusOK, gin.H{"ok": true})
}
// isStaff 判断当前用户是否为管理角色
func (h *Handlers) isStaff(c *gin.Context) bool {
claims := middleware.CurrentUser(c)
if claims == nil {
return false
}
return model.IsStaff(model.Role(claims.Role))
}