qoder生成项目
This commit is contained in:
242
handlers/auth.go
Normal file
242
handlers/auth.go
Normal 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
125
handlers/category.go
Normal 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
137
handlers/comment.go
Normal 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
145
handlers/page.go
Normal 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
274
handlers/post.go
Normal 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
112
handlers/tag.go
Normal 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
241
handlers/view.go
Normal 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)))
|
||||
}
|
||||
Reference in New Issue
Block a user