qoder生成项目

This commit is contained in:
2026-02-13 10:53:54 +08:00
commit 4cab051f9c
24 changed files with 3627 additions and 0 deletions

242
handlers/auth.go Normal file
View File

@@ -0,0 +1,242 @@
package handlers
import (
"net/http"
"time"
"goblog/database"
"goblog/models"
"github.com/gin-gonic/gin"
"github.com/golang-jwt/jwt/v5"
"golang.org/x/crypto/bcrypt"
)
var jwtSecret = []byte("your-secret-key-change-in-production")
func JWTSecret() []byte {
return jwtSecret
}
// 登录请求
type LoginRequest struct {
Username string `json:"username" binding:"required"`
Password string `json:"password" binding:"required"`
}
// 注册请求
type RegisterRequest struct {
Username string `json:"username" binding:"required,min=3,max=50"`
Password string `json:"password" binding:"required,min=6"`
Nickname string `json:"nickname"`
Email string `json:"email" binding:"required,email"`
}
// JWT Claims
type Claims struct {
UserID uint `json:"user_id"`
Username string `json:"username"`
Role string `json:"role"`
jwt.RegisteredClaims
}
// 登录
func Login(c *gin.Context) {
var req LoginRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
var user models.User
if err := database.DB.Where("username = ?", req.Username).First(&user).Error; err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "用户名或密码错误"})
return
}
if user.Status == 0 {
c.JSON(http.StatusForbidden, gin.H{"error": "账号已被禁用"})
return
}
if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(req.Password)); err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "用户名或密码错误"})
return
}
// 生成 JWT
claims := Claims{
UserID: user.ID,
Username: user.Username,
Role: user.Role,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Now().Add(7 * 24 * time.Hour)),
IssuedAt: jwt.NewNumericDate(time.Now()),
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
tokenString, err := token.SignedString(jwtSecret)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "生成令牌失败"})
return
}
c.JSON(http.StatusOK, gin.H{
"token": tokenString,
"user": gin.H{
"id": user.ID,
"username": user.Username,
"nickname": user.Nickname,
"email": user.Email,
"role": user.Role,
"avatar": user.Avatar,
},
})
}
// 注册
func Register(c *gin.Context) {
var req RegisterRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// 检查用户名是否已存在
var count int64
database.DB.Model(&models.User{}).Where("username = ?", req.Username).Count(&count)
if count > 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "用户名已存在"})
return
}
// 检查邮箱是否已存在
database.DB.Model(&models.User{}).Where("email = ?", req.Email).Count(&count)
if count > 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "邮箱已被注册"})
return
}
// 加密密码
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "密码加密失败"})
return
}
user := models.User{
Username: req.Username,
Password: string(hashedPassword),
Nickname: req.Nickname,
Email: req.Email,
Role: "user",
Status: 1,
}
if err := database.DB.Create(&user).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "注册失败"})
return
}
c.JSON(http.StatusCreated, gin.H{
"message": "注册成功",
"user": gin.H{
"id": user.ID,
"username": user.Username,
"nickname": user.Nickname,
"email": user.Email,
},
})
}
// 获取当前用户信息
func GetCurrentUser(c *gin.Context) {
userID, _ := c.Get("userID")
var user models.User
if err := database.DB.First(&user, userID).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "用户不存在"})
return
}
c.JSON(http.StatusOK, gin.H{"data": user})
}
// 更新用户信息
func UpdateUser(c *gin.Context) {
userID, _ := c.Get("userID")
var user models.User
if err := database.DB.First(&user, userID).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "用户不存在"})
return
}
var req struct {
Nickname string `json:"nickname"`
Email string `json:"email"`
Avatar string `json:"avatar"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
updates := map[string]interface{}{}
if req.Nickname != "" {
updates["nickname"] = req.Nickname
}
if req.Email != "" {
updates["email"] = req.Email
}
if req.Avatar != "" {
updates["avatar"] = req.Avatar
}
if err := database.DB.Model(&user).Updates(updates).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "更新失败"})
return
}
c.JSON(http.StatusOK, gin.H{"data": user})
}
// 修改密码
func ChangePassword(c *gin.Context) {
userID, _ := c.Get("userID")
var user models.User
if err := database.DB.First(&user, userID).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "用户不存在"})
return
}
var req struct {
OldPassword string `json:"old_password" binding:"required"`
NewPassword string `json:"new_password" binding:"required,min=6"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(req.OldPassword)); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "原密码错误"})
return
}
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.NewPassword), bcrypt.DefaultCost)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "密码加密失败"})
return
}
user.Password = string(hashedPassword)
if err := database.DB.Save(&user).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "修改密码失败"})
return
}
c.JSON(http.StatusOK, gin.H{"message": "密码修改成功"})
}

125
handlers/category.go Normal file
View File

@@ -0,0 +1,125 @@
package handlers
import (
"net/http"
"goblog/database"
"goblog/models"
"github.com/gin-gonic/gin"
)
// 创建分类请求
type CreateCategoryRequest struct {
Name string `json:"name" binding:"required"`
Slug string `json:"slug"`
Description string `json:"description"`
ParentID *uint `json:"parent_id"`
}
// 获取分类列表
func GetCategories(c *gin.Context) {
var categories []models.Category
database.DB.Order("id ASC").Find(&categories)
c.JSON(http.StatusOK, gin.H{"data": categories})
}
// 获取单个分类
func GetCategory(c *gin.Context) {
id := c.Param("id")
var category models.Category
if err := database.DB.First(&category, id).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "分类不存在"})
return
}
c.JSON(http.StatusOK, gin.H{"data": category})
}
// 创建分类
func CreateCategory(c *gin.Context) {
var req CreateCategoryRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
slug := req.Slug
if slug == "" {
slug = generateSlug(req.Name)
}
category := models.Category{
Name: req.Name,
Slug: slug,
Description: req.Description,
ParentID: req.ParentID,
}
if err := database.DB.Create(&category).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "创建分类失败"})
return
}
c.JSON(http.StatusCreated, gin.H{"data": category})
}
// 更新分类
func UpdateCategory(c *gin.Context) {
id := c.Param("id")
var category models.Category
if err := database.DB.First(&category, id).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "分类不存在"})
return
}
var req CreateCategoryRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
updates := map[string]interface{}{
"name": req.Name,
"description": req.Description,
"parent_id": req.ParentID,
}
if req.Slug != "" {
updates["slug"] = req.Slug
}
if err := database.DB.Model(&category).Updates(updates).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "更新分类失败"})
return
}
c.JSON(http.StatusOK, gin.H{"data": category})
}
// 删除分类
func DeleteCategory(c *gin.Context) {
id := c.Param("id")
var category models.Category
if err := database.DB.First(&category, id).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "分类不存在"})
return
}
// 检查是否有文章使用此分类
var count int64
database.DB.Model(&models.Post{}).Where("category_id = ?", id).Count(&count)
if count > 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "该分类下还有文章,无法删除"})
return
}
if err := database.DB.Delete(&category).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "删除分类失败"})
return
}
c.JSON(http.StatusOK, gin.H{"message": "删除成功"})
}

137
handlers/comment.go Normal file
View File

@@ -0,0 +1,137 @@
package handlers
import (
"net/http"
"goblog/database"
"goblog/models"
"github.com/gin-gonic/gin"
)
// 创建评论请求
type CreateCommentRequest struct {
PostID uint `json:"post_id" binding:"required"`
ParentID *uint `json:"parent_id"`
Author string `json:"author" binding:"required"`
Email string `json:"email" binding:"required,email"`
Website string `json:"website"`
Content string `json:"content" binding:"required"`
}
// 获取评论列表
func GetComments(c *gin.Context) {
postID := c.Query("post_id")
status := c.Query("status")
db := database.DB.Model(&models.Comment{}).Preload("User")
if postID != "" {
db = db.Where("post_id = ?", postID)
}
if status != "" {
db = db.Where("status = ?", status)
}
var comments []models.Comment
db.Order("created_at DESC").Find(&comments)
c.JSON(http.StatusOK, gin.H{"data": comments})
}
// 创建评论
func CreateComment(c *gin.Context) {
var req CreateCommentRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// 检查文章是否存在
var post models.Post
if err := database.DB.First(&post, req.PostID).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "文章不存在"})
return
}
comment := models.Comment{
PostID: req.PostID,
ParentID: req.ParentID,
Author: req.Author,
Email: req.Email,
Website: req.Website,
Content: req.Content,
IP: c.ClientIP(),
Status: "pending", // 默认待审核
}
// 如果用户已登录
if userID, exists := c.Get("userID"); exists {
uid := userID.(uint)
comment.UserID = &uid
comment.Status = "approved" // 登录用户评论自动通过
}
if err := database.DB.Create(&comment).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "发表评论失败"})
return
}
c.JSON(http.StatusCreated, gin.H{"data": comment})
}
// 审核评论
func ApproveComment(c *gin.Context) {
id := c.Param("id")
var comment models.Comment
if err := database.DB.First(&comment, id).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "评论不存在"})
return
}
comment.Status = "approved"
if err := database.DB.Save(&comment).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "审核失败"})
return
}
c.JSON(http.StatusOK, gin.H{"message": "审核通过"})
}
// 标记为垃圾评论
func MarkSpamComment(c *gin.Context) {
id := c.Param("id")
var comment models.Comment
if err := database.DB.First(&comment, id).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "评论不存在"})
return
}
comment.Status = "spam"
if err := database.DB.Save(&comment).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "操作失败"})
return
}
c.JSON(http.StatusOK, gin.H{"message": "已标记为垃圾评论"})
}
// 删除评论
func DeleteComment(c *gin.Context) {
id := c.Param("id")
var comment models.Comment
if err := database.DB.First(&comment, id).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "评论不存在"})
return
}
if err := database.DB.Delete(&comment).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "删除评论失败"})
return
}
c.JSON(http.StatusOK, gin.H{"message": "删除成功"})
}

145
handlers/page.go Normal file
View File

@@ -0,0 +1,145 @@
package handlers
import (
"net/http"
"strconv"
"goblog/database"
"goblog/models"
"github.com/gin-gonic/gin"
)
// 获取页面列表
func GetPages(c *gin.Context) {
var pages []models.Page
database.DB.Where("status = ?", "published").Order("`order` ASC").Find(&pages)
c.JSON(http.StatusOK, gin.H{"data": pages})
}
// 获取单个页面
func GetPage(c *gin.Context) {
id := c.Param("id")
var page models.Page
query := database.DB
if _, err := strconv.Atoi(id); err == nil {
query = query.Where("id = ?", id)
} else {
query = query.Where("slug = ?", id)
}
// 非管理员只能查看已发布页面
if !isAdmin(c) {
query = query.Where("status = ?", "published")
}
if err := query.First(&page).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "页面不存在"})
return
}
c.JSON(http.StatusOK, gin.H{"data": page})
}
// 创建页面
func CreatePage(c *gin.Context) {
var req struct {
Title string `json:"title" binding:"required"`
Slug string `json:"slug"`
Content string `json:"content" binding:"required"`
Status string `json:"status"`
Order int `json:"order"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
userID, _ := c.Get("userID")
slug := req.Slug
if slug == "" {
slug = generateSlug(req.Title)
}
page := models.Page{
Title: req.Title,
Slug: slug,
Content: req.Content,
AuthorID: userID.(uint),
Status: req.Status,
Order: req.Order,
}
if err := database.DB.Create(&page).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "创建页面失败"})
return
}
c.JSON(http.StatusCreated, gin.H{"data": page})
}
// 更新页面
func UpdatePage(c *gin.Context) {
id := c.Param("id")
var page models.Page
if err := database.DB.First(&page, id).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "页面不存在"})
return
}
var req struct {
Title string `json:"title"`
Slug string `json:"slug"`
Content string `json:"content"`
Status string `json:"status"`
Order int `json:"order"`
}
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
}
if req.Slug != "" {
updates["slug"] = req.Slug
}
if req.Content != "" {
updates["content"] = req.Content
}
if req.Status != "" {
updates["status"] = req.Status
}
updates["order"] = req.Order
if err := database.DB.Model(&page).Updates(updates).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "更新页面失败"})
return
}
c.JSON(http.StatusOK, gin.H{"data": page})
}
// 删除页面
func DeletePage(c *gin.Context) {
id := c.Param("id")
var page models.Page
if err := database.DB.First(&page, id).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "页面不存在"})
return
}
if err := database.DB.Delete(&page).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "删除页面失败"})
return
}
c.JSON(http.StatusOK, gin.H{"message": "删除成功"})
}

274
handlers/post.go Normal file
View File

@@ -0,0 +1,274 @@
package handlers
import (
"net/http"
"strconv"
"strings"
"time"
"goblog/database"
"goblog/models"
"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"`
}
// 生成 slug
func generateSlug(title string) string {
slug := strings.ToLower(title)
slug = strings.ReplaceAll(slug, " ", "-")
slug = strings.ReplaceAll(slug, "_", "-")
// 简化处理,实际项目中可能需要更完善的 slug 生成
return slug
}
// 获取文章列表
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
}
db := database.DB.Model(&models.Post{}).Preload("Category").Preload("Tags").Preload("Author")
// 前端只显示已发布的文章
if !isAdmin(c) {
db = db.Where("status = ?", "published")
} else if query.Status != "" {
db = db.Where("status = ?", query.Status)
}
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) {
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,
}
if req.Status == "published" {
now := time.Now()
post.PublishedAt = &now
}
// 处理标签
if len(req.Tags) > 0 {
var tags []models.Tag
for _, tagName := range req.Tags {
var tag models.Tag
database.DB.FirstOrCreate(&tag, models.Tag{
Name: tagName,
Slug: generateSlug(tagName),
})
tags = append(tags, tag)
}
post.Tags = 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
}
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
}
}
updates["is_top"] = req.IsTop
// 处理标签
if len(req.Tags) > 0 {
var tags []models.Tag
for _, tagName := range req.Tags {
var tag models.Tag
database.DB.FirstOrCreate(&tag, models.Tag{
Name: tagName,
Slug: generateSlug(tagName),
})
tags = append(tags, tag)
}
database.DB.Model(&post).Association("Tags").Replace(tags)
}
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 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"
}

112
handlers/tag.go Normal file
View File

@@ -0,0 +1,112 @@
package handlers
import (
"net/http"
"goblog/database"
"goblog/models"
"github.com/gin-gonic/gin"
)
// 获取标签列表
func GetTags(c *gin.Context) {
var tags []models.Tag
database.DB.Order("post_count DESC").Find(&tags)
c.JSON(http.StatusOK, gin.H{"data": tags})
}
// 获取单个标签
func GetTag(c *gin.Context) {
id := c.Param("id")
var tag models.Tag
if err := database.DB.First(&tag, id).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "标签不存在"})
return
}
c.JSON(http.StatusOK, gin.H{"data": tag})
}
// 创建标签
func CreateTag(c *gin.Context) {
var req struct {
Name string `json:"name" binding:"required"`
Slug string `json:"slug"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
slug := req.Slug
if slug == "" {
slug = generateSlug(req.Name)
}
tag := models.Tag{
Name: req.Name,
Slug: slug,
}
if err := database.DB.Create(&tag).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "创建标签失败"})
return
}
c.JSON(http.StatusCreated, gin.H{"data": tag})
}
// 更新标签
func UpdateTag(c *gin.Context) {
id := c.Param("id")
var tag models.Tag
if err := database.DB.First(&tag, id).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "标签不存在"})
return
}
var req struct {
Name string `json:"name"`
Slug string `json:"slug"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
updates := map[string]interface{}{}
if req.Name != "" {
updates["name"] = req.Name
}
if req.Slug != "" {
updates["slug"] = req.Slug
}
if err := database.DB.Model(&tag).Updates(updates).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "更新标签失败"})
return
}
c.JSON(http.StatusOK, gin.H{"data": tag})
}
// 删除标签
func DeleteTag(c *gin.Context) {
id := c.Param("id")
var tag models.Tag
if err := database.DB.First(&tag, id).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "标签不存在"})
return
}
if err := database.DB.Delete(&tag).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "删除标签失败"})
return
}
c.JSON(http.StatusOK, gin.H{"message": "删除成功"})
}

241
handlers/view.go Normal file
View File

@@ -0,0 +1,241 @@
package handlers
import (
"html/template"
"math"
"net/http"
"strconv"
"time"
"goblog/config"
"goblog/database"
"goblog/models"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
// 模板函数
func templateFuncs() template.FuncMap {
return template.FuncMap{
"add": func(a, b int) int {
return a + b
},
"sub": func(a, b int) int {
return a - b
},
"html": func(s string) template.HTML {
return template.HTML(s)
},
}
}
// 初始化模板 - 返回 gin 可用的 HTMLRender
func InitTemplates(theme string) (*template.Template, error) {
tmpl, err := template.New("").Funcs(templateFuncs()).ParseGlob("templates/" + theme + "/*.html")
return tmpl, err
}
// 首页
type IndexData struct {
Title string
SiteName string
SiteDesc string
Description string
Posts []models.Post
Pages []models.Page
CurrentPage int
TotalPages int
Year int
}
func IndexView(c *gin.Context) {
cfg := config.Load()
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
if page < 1 {
page = 1
}
pageSize := 10
categoryID, _ := strconv.Atoi(c.Query("category"))
tagID, _ := strconv.Atoi(c.Query("tag"))
db := database.DB.Model(&models.Post{}).Preload("Category").Preload("Tags").Preload("Author").
Where("status = ?", "published").
Where("published_at IS NOT NULL")
if categoryID > 0 {
db = db.Where("category_id = ?", categoryID)
}
if tagID > 0 {
db = db.Joins("JOIN post_tags ON post_tags.post_id = posts.id").
Where("post_tags.tag_id = ?", tagID)
}
var total int64
db.Count(&total)
var posts []models.Post
offset := (page - 1) * pageSize
db.Order("is_top DESC, published_at DESC, created_at DESC").
Offset(offset).Limit(pageSize).Find(&posts)
// 获取页面
var pages []models.Page
database.DB.Where("status = ?", "published").Order("`order` ASC").Find(&pages)
totalPages := int(math.Ceil(float64(total) / float64(pageSize)))
data := IndexData{
Title: "首页",
SiteName: cfg.App.Name,
SiteDesc: cfg.App.Description,
Description: cfg.App.Description,
Posts: posts,
Pages: pages,
CurrentPage: page,
TotalPages: totalPages,
Year: time.Now().Year(),
}
c.HTML(http.StatusOK, "base.html", data)
}
// 文章详情页
type PostDetailData struct {
Title string
SiteName string
SiteDesc string
Description string
Post models.Post
Pages []models.Page
Comments []models.Comment
CommentCount int64
Year int
}
func PostView(c *gin.Context) {
cfg := config.Load()
slug := c.Param("slug")
var post models.Post
err := 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")
})
}).
Where("slug = ? AND status = ?", slug, "published").
First(&post).Error
if err != nil {
c.HTML(http.StatusNotFound, "base.html", gin.H{
"Title": "404 - 页面不存在",
"SiteName": cfg.App.Name,
"Year": time.Now().Year(),
})
return
}
// 增加浏览量
database.DB.Model(&post).UpdateColumn("views", gorm.Expr("views + 1"))
// 获取页面
var pages []models.Page
database.DB.Where("status = ?", "published").Order("`order` ASC").Find(&pages)
// 统计评论数
var commentCount int64
database.DB.Model(&models.Comment{}).Where("post_id = ? AND status = ?", post.ID, "approved").Count(&commentCount)
data := PostDetailData{
Title: post.Title,
SiteName: cfg.App.Name,
SiteDesc: cfg.App.Description,
Description: post.Summary,
Post: post,
Pages: pages,
Comments: post.Comments,
CommentCount: commentCount,
Year: time.Now().Year(),
}
c.HTML(http.StatusOK, "base.html", data)
}
// 独立页面
type PageDetailData struct {
Title string
SiteName string
SiteDesc string
Description string
Page models.Page
Pages []models.Page
Year int
}
func PageView(c *gin.Context) {
cfg := config.Load()
slug := c.Param("slug")
var page models.Page
err := database.DB.Where("slug = ? AND status = ?", slug, "published").First(&page).Error
if err != nil {
c.HTML(http.StatusNotFound, "base.html", gin.H{
"Title": "404 - 页面不存在",
"SiteName": cfg.App.Name,
"Year": time.Now().Year(),
})
return
}
// 获取所有页面
var pages []models.Page
database.DB.Where("status = ?", "published").Order("`order` ASC").Find(&pages)
data := PageDetailData{
Title: page.Title,
SiteName: cfg.App.Name,
SiteDesc: cfg.App.Description,
Description: "",
Page: page,
Pages: pages,
Year: time.Now().Year(),
}
c.HTML(http.StatusOK, "base.html", data)
}
// 提交评论(表单提交)
func SubmitComment(c *gin.Context) {
postID, _ := strconv.Atoi(c.PostForm("post_id"))
author := c.PostForm("author")
email := c.PostForm("email")
website := c.PostForm("website")
content := c.PostForm("content")
// 检查文章是否存在
var post models.Post
if err := database.DB.First(&post, postID).Error; err != nil {
c.String(http.StatusBadRequest, "文章不存在")
return
}
comment := models.Comment{
PostID: uint(postID),
Author: author,
Email: email,
Website: website,
Content: content,
IP: c.ClientIP(),
Status: "pending",
}
if err := database.DB.Create(&comment).Error; err != nil {
c.String(http.StatusInternalServerError, "发表评论失败")
return
}
c.Redirect(http.StatusFound, "/post/"+post.Slug+"#comment-"+strconv.Itoa(int(comment.ID)))
}