package handlers import ( "net/http" "strconv" "strings" "time" "goblog/database" "goblog/models" "goblog/utils" "github.com/gin-gonic/gin" "gorm.io/gorm" ) // 文章列表请求参数 type PostListQuery struct { Page int `form:"page,default=1"` PageSize int `form:"page_size,default=10"` CategoryID uint `form:"category_id"` TagID uint `form:"tag_id"` Status string `form:"status"` Keyword string `form:"keyword"` } // 创建文章请求 type CreatePostRequest struct { Title string `json:"title" binding:"required"` Content string `json:"content" binding:"required"` Summary string `json:"summary"` Cover string `json:"cover"` CategoryID uint `json:"category_id" binding:"required"` Tags []string `json:"tags"` Status string `json:"status"` IsTop bool `json:"is_top"` } // 更新文章请求 type UpdatePostRequest struct { Title string `json:"title"` Content string `json:"content"` Summary string `json:"summary"` Cover string `json:"cover"` CategoryID uint `json:"category_id"` Tags []string `json:"tags"` Status string `json:"status"` IsTop bool `json:"is_top"` } // Markdown 预览请求 type MarkdownPreviewRequest struct { Content string `json:"content"` } // PreviewMarkdown 将 Markdown 渲染为 HTML(后台编辑器预览用) func PreviewMarkdown(c *gin.Context) { var req MarkdownPreviewRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"html": string(utils.RenderMarkdown(req.Content))}) } // 生成 slug func generateSlug(title string) string { slug := strings.ToLower(title) slug = strings.ReplaceAll(slug, " ", "-") slug = strings.ReplaceAll(slug, "_", "-") // 简化处理,实际项目中可能需要更完善的 slug 生成 return slug } // findOrCreateTags 根据名称列表查找或创建标签 func findOrCreateTags(tagNames []string) []models.Tag { var tags []models.Tag for _, tagName := range tagNames { var tag models.Tag database.DB.FirstOrCreate(&tag, models.Tag{ Name: tagName, Slug: generateSlug(tagName), }) tags = append(tags, tag) } return tags } // 获取文章列表 func GetPosts(c *gin.Context) { var query PostListQuery if err := c.ShouldBindQuery(&query); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } // 分页参数校验 if query.Page < 1 { query.Page = 1 } if query.PageSize < 1 || query.PageSize > 100 { query.PageSize = 10 } db := database.DB.Model(&models.Post{}).Preload("Category").Preload("Tags").Preload("Author") // 管理员可查看全部;登录用户可看到自己的全部文章(含草稿);未登录只能看已发布 if isAdmin(c) { if query.Status != "" { db = db.Where("status = ?", query.Status) } } else if userId, exists := c.Get("userID"); exists { // 登录用户:只看自己的,不限状态(后台管理需要看到草稿) db = db.Where("author_id = ?", userId) } else { // 未登录:只看已发布 db = db.Where("status = ?", "published") } if query.CategoryID > 0 { db = db.Where("category_id = ?", query.CategoryID) } if query.TagID > 0 { db = db.Joins("JOIN post_tags ON post_tags.post_id = posts.id"). Where("post_tags.tag_id = ?", query.TagID) } if query.Keyword != "" { db = db.Where("title LIKE ? OR content LIKE ?", "%"+query.Keyword+"%", "%"+query.Keyword+"%") } var total int64 db.Count(&total) var posts []models.Post offset := (query.Page - 1) * query.PageSize db.Order("is_top DESC, published_at DESC, created_at DESC"). Offset(offset).Limit(query.PageSize).Find(&posts) c.JSON(http.StatusOK, gin.H{ "data": posts, "total": total, "page": query.Page, "size": query.PageSize, }) } // 获取单篇文章 func GetPost(c *gin.Context) { id := c.Param("id") var post models.Post query := database.DB.Preload("Category").Preload("Tags").Preload("Author").Preload("Comments", func(db *gorm.DB) *gorm.DB { return db.Where("status = ? AND parent_id IS NULL", "approved").Preload("Children", func(db *gorm.DB) *gorm.DB { return db.Where("status = ?", "approved") }) }) // 尝试按 ID 或 slug 查找 if _, err := strconv.Atoi(id); err == nil { query = query.Where("id = ?", id) } else { query = query.Where("slug = ?", id) } // 非管理员只能查看已发布文章,但可以查看自己的草稿(编辑用) if !isAdmin(c) { if userId, exists := c.Get("userID"); exists { query = query.Where("status = ? OR author_id = ?", "published", userId) } else { query = query.Where("status = ?", "published") } } if err := query.First(&post).Error; err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "文章不存在"}) return } // 增加浏览量 database.DB.Model(&post).UpdateColumn("views", gorm.Expr("views + 1")) c.JSON(http.StatusOK, gin.H{"data": post}) } // 创建文章 func CreatePost(c *gin.Context) { var req CreatePostRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } userID, _ := c.Get("userID") post := models.Post{ Title: req.Title, Slug: generateSlug(req.Title), Content: req.Content, Summary: req.Summary, Cover: req.Cover, AuthorID: userID.(uint), CategoryID: req.CategoryID, Status: req.Status, IsTop: req.IsTop && isAdmin(c), // 非管理员不允许置顶 } if req.Status == "published" { now := time.Now() post.PublishedAt = &now } // 处理标签 if len(req.Tags) > 0 { post.Tags = findOrCreateTags(req.Tags) } if err := database.DB.Create(&post).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "创建文章失败"}) return } c.JSON(http.StatusCreated, gin.H{"data": post}) } // 更新文章 func UpdatePost(c *gin.Context) { id := c.Param("id") var post models.Post if err := database.DB.First(&post, id).Error; err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "文章不存在"}) return } // 非管理员只能编辑自己的文章 if !isAdmin(c) { userID, _ := c.Get("userID") if post.AuthorID != userID.(uint) { c.JSON(http.StatusForbidden, gin.H{"error": "无权编辑他人的文章"}) return } } var req UpdatePostRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } updates := map[string]interface{}{} if req.Title != "" { updates["title"] = req.Title updates["slug"] = generateSlug(req.Title) } if req.Content != "" { updates["content"] = req.Content } if req.Summary != "" { updates["summary"] = req.Summary } if req.Cover != "" { updates["cover"] = req.Cover } if req.CategoryID > 0 { updates["category_id"] = req.CategoryID } if req.Status != "" { updates["status"] = req.Status if req.Status == "published" && post.Status != "published" { now := time.Now() updates["published_at"] = &now } } // 如果文章是已发布状态但没有发布时间,设置发布时间 if post.Status == "published" && post.PublishedAt == nil { now := time.Now() updates["published_at"] = &now } // 非管理员不允许置顶 if isAdmin(c) { updates["is_top"] = req.IsTop } // 处理标签 if len(req.Tags) > 0 { tags := findOrCreateTags(req.Tags) if err := database.DB.Model(&post).Association("Tags").Replace(tags); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "更新文章标签失败"}) return } } if err := database.DB.Model(&post).Updates(updates).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "更新文章失败"}) return } c.JSON(http.StatusOK, gin.H{"data": post}) } // 删除文章 func DeletePost(c *gin.Context) { id := c.Param("id") var post models.Post if err := database.DB.First(&post, id).Error; err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "文章不存在"}) return } // 非管理员只能删除自己的文章 if !isAdmin(c) { userID, _ := c.Get("userID") if post.AuthorID != userID.(uint) { c.JSON(http.StatusForbidden, gin.H{"error": "无权删除他人的文章"}) return } } if err := database.DB.Delete(&post).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "删除文章失败"}) return } c.JSON(http.StatusOK, gin.H{"message": "删除成功"}) } // 判断是否为管理员 func isAdmin(c *gin.Context) bool { role, exists := c.Get("role") return exists && role == "admin" }