handlers区分backend和frontend,需要确认。
This commit is contained in:
@@ -0,0 +1,108 @@
|
||||
package backend
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"html/template"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"sync"
|
||||
|
||||
"goblog/database"
|
||||
"goblog/models"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// adminTemplatePath 后台单页模板(含 {{define}} 分块,按权限渲染)
|
||||
const adminTemplatePath = "./static/admin/index.html"
|
||||
|
||||
// AdminPageData 后台页面模板数据
|
||||
type AdminPageData struct {
|
||||
Menus []AdminMenuGroup // 当前角色可见菜单(登录接口同源)
|
||||
PageSet map[string]bool // 菜单中的页面标识集合,模板据此渲染对应页面框架
|
||||
DisplayName string // 侧边栏显示的用户名
|
||||
CurrentUser template.JS // JSON 序列化的当前用户信息(id/username/nickname/role),注入前端
|
||||
}
|
||||
|
||||
var (
|
||||
adminTmplOnce sync.Once
|
||||
adminTmpl *template.Template
|
||||
adminTmplErr error
|
||||
)
|
||||
|
||||
// getAdminTemplate 懒加载后台模板(仅解析一次)
|
||||
func getAdminTemplate() (*template.Template, error) {
|
||||
adminTmplOnce.Do(func() {
|
||||
adminTmpl, adminTmplErr = template.ParseFiles(adminTemplatePath)
|
||||
if adminTmplErr != nil {
|
||||
slog.Error("解析后台模板失败", "path", adminTemplatePath, "error", adminTmplErr)
|
||||
}
|
||||
})
|
||||
return adminTmpl, adminTmplErr
|
||||
}
|
||||
|
||||
// menuPageSet 从菜单提取页面标识集合
|
||||
func menuPageSet(menus []AdminMenuGroup) map[string]bool {
|
||||
set := make(map[string]bool)
|
||||
for _, g := range menus {
|
||||
for _, item := range g.Items {
|
||||
set[item.Key] = true
|
||||
}
|
||||
}
|
||||
return set
|
||||
}
|
||||
|
||||
// AdminLoginView 独立登录页
|
||||
func AdminLoginView(c *gin.Context) {
|
||||
c.File("./static/admin/login.html")
|
||||
}
|
||||
|
||||
// AdminView 后台管理页面:校验登录态后按角色用模板渲染菜单与页面框架
|
||||
// 依赖 OptionalAuth 中间件注入 userID/role 等上下文
|
||||
func AdminView(c *gin.Context) {
|
||||
userID, exists := c.Get("userID")
|
||||
if !exists {
|
||||
c.Redirect(http.StatusFound, "/admin/login")
|
||||
return
|
||||
}
|
||||
|
||||
// 校验用户当前状态(禁用账号不允许进入后台)
|
||||
var user models.User
|
||||
if err := database.DB.First(&user, userID).Error; err != nil || user.Status == 0 || !user.IsActive {
|
||||
c.Redirect(http.StatusFound, "/admin/login")
|
||||
return
|
||||
}
|
||||
|
||||
tmpl, tmplErr := getAdminTemplate()
|
||||
if tmplErr != nil {
|
||||
c.String(http.StatusInternalServerError, "后台模板加载失败")
|
||||
return
|
||||
}
|
||||
|
||||
// 菜单与登录接口保持同一来源,页面框架只渲染菜单内的页面
|
||||
menus := GetMenusByRole(user.Role)
|
||||
displayName := user.Nickname
|
||||
if displayName == "" {
|
||||
displayName = user.Username
|
||||
}
|
||||
|
||||
// 将用户基本信息注入前端,避免依赖 localStorage
|
||||
currentUserJSON, _ := json.Marshal(map[string]interface{}{
|
||||
"id": user.ID,
|
||||
"username": user.Username,
|
||||
"nickname": user.Nickname,
|
||||
"role": user.Role,
|
||||
})
|
||||
|
||||
data := AdminPageData{
|
||||
Menus: menus,
|
||||
PageSet: menuPageSet(menus),
|
||||
DisplayName: displayName,
|
||||
CurrentUser: template.JS(currentUserJSON),
|
||||
}
|
||||
|
||||
c.Header("Content-Type", "text/html; charset=utf-8")
|
||||
if err := tmpl.ExecuteTemplate(c.Writer, "admin_index", data); err != nil {
|
||||
slog.Error("渲染后台页面失败", "error", err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,256 @@
|
||||
package backend
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"goblog/database"
|
||||
"goblog/models"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
)
|
||||
|
||||
var jwtSecret = []byte("your-secret-key-change-in-production")
|
||||
|
||||
func JWTSecret() []byte {
|
||||
return jwtSecret
|
||||
}
|
||||
|
||||
// AdminMenuItem 后台菜单项
|
||||
type AdminMenuItem struct {
|
||||
Key string `json:"key"` // 页面标识,与页面 div 的 data-page 对应
|
||||
Name string `json:"name"` // 菜单显示名称
|
||||
}
|
||||
|
||||
// AdminMenuGroup 后台菜单分组(Group 为空表示顶级菜单)
|
||||
type AdminMenuGroup struct {
|
||||
Group string `json:"group"`
|
||||
Items []AdminMenuItem `json:"items"`
|
||||
}
|
||||
|
||||
// GetMenusByRole 根据角色返回后台菜单
|
||||
func GetMenusByRole(role string) []AdminMenuGroup {
|
||||
if role == "admin" {
|
||||
return []AdminMenuGroup{
|
||||
{Group: "", Items: []AdminMenuItem{
|
||||
{Key: "dashboard", Name: "仪表盘"},
|
||||
}},
|
||||
{Group: "内容管理", Items: []AdminMenuItem{
|
||||
{Key: "posts", Name: "文章管理"},
|
||||
{Key: "categories", Name: "分类管理"},
|
||||
{Key: "tags", Name: "标签管理"},
|
||||
{Key: "pages", Name: "页面管理"},
|
||||
{Key: "comments", Name: "评论管理"},
|
||||
}},
|
||||
{Group: "系统管理", Items: []AdminMenuItem{
|
||||
{Key: "users", Name: "用户管理"},
|
||||
{Key: "files", Name: "文件管理"},
|
||||
{Key: "themes", Name: "主题管理"},
|
||||
{Key: "settings", Name: "基础设置"},
|
||||
}},
|
||||
}
|
||||
}
|
||||
// 普通用户:仪表盘 + 文章管理 + 评论管理
|
||||
return []AdminMenuGroup{
|
||||
{Group: "", Items: []AdminMenuItem{
|
||||
{Key: "dashboard", Name: "仪表盘"},
|
||||
{Key: "posts", Name: "文章管理"},
|
||||
{Key: "comments", Name: "评论管理"},
|
||||
}},
|
||||
}
|
||||
}
|
||||
|
||||
// 登录请求
|
||||
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 || !user.IsActive {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "账号已被禁用"})
|
||||
return
|
||||
}
|
||||
|
||||
// 使用 User 模型的 CheckPassword 方法
|
||||
if !user.CheckPassword(req.Password) {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "用户名或密码错误"})
|
||||
return
|
||||
}
|
||||
|
||||
// 记录登录信息
|
||||
if err := user.RecordLogin(database.DB); err != nil {
|
||||
slog.Warn("记录登录信息失败", "user_id", user.ID, "error", err)
|
||||
}
|
||||
|
||||
// 生成 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
|
||||
}
|
||||
|
||||
// 同时写入 Cookie,供服务端渲染后台页面时识别登录态
|
||||
c.SetCookie("token", tokenString, 7*24*3600, "/", "", false, true)
|
||||
|
||||
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,
|
||||
"last_login_at": user.LastLoginAt,
|
||||
"login_count": user.LoginCount,
|
||||
},
|
||||
// 菜单由登录接口根据角色返回
|
||||
"menus": GetMenusByRole(user.Role),
|
||||
})
|
||||
}
|
||||
|
||||
// 退出登录(清除服务端 Cookie)
|
||||
func Logout(c *gin.Context) {
|
||||
c.SetCookie("token", "", -1, "/", "", false, true)
|
||||
c.JSON(http.StatusOK, gin.H{"message": "已退出登录"})
|
||||
}
|
||||
|
||||
// WebLogout 前台页面退出登录(清除 Cookie 后回首页)
|
||||
func WebLogout(c *gin.Context) {
|
||||
c.SetCookie("token", "", -1, "/", "", false, true)
|
||||
c.Redirect(http.StatusFound, "/")
|
||||
}
|
||||
|
||||
// 获取当前用户信息
|
||||
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
|
||||
}
|
||||
|
||||
// 使用 User 模型的 CheckPassword 方法
|
||||
if !user.CheckPassword(req.OldPassword) {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "原密码错误"})
|
||||
return
|
||||
}
|
||||
|
||||
// 使用 User 模型的 SetPassword 方法
|
||||
if err := user.SetPassword(req.NewPassword); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "密码加密失败"})
|
||||
return
|
||||
}
|
||||
|
||||
if err := database.DB.Save(&user).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "修改密码失败"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "密码修改成功"})
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
package backend
|
||||
|
||||
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": "删除成功"})
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
package backend
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"goblog/database"
|
||||
"goblog/models"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// 创建评论请求
|
||||
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").
|
||||
Preload("Post", func(db *gorm.DB) *gorm.DB {
|
||||
return db.Select("id", "title", "slug")
|
||||
})
|
||||
|
||||
// 非管理员只能查看自己文章上的评论
|
||||
if !isAdmin(c) {
|
||||
if userID, exists := c.Get("userID"); exists {
|
||||
db = db.Where("post_id IN (?)",
|
||||
database.DB.Model(&models.Post{}).Select("id").Where("author_id = ?", userID))
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
// Comment.Post 在 JSON 中被隐藏,额外附加文章标题和别名供前端展示
|
||||
type commentWithPost struct {
|
||||
models.Comment
|
||||
PostTitle string `json:"post_title"`
|
||||
PostSlug string `json:"post_slug"`
|
||||
}
|
||||
list := make([]commentWithPost, 0, len(comments))
|
||||
for _, cm := range comments {
|
||||
list = append(list, commentWithPost{cm, cm.Post.Title, cm.Post.Slug})
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"data": list})
|
||||
}
|
||||
|
||||
// 创建评论
|
||||
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})
|
||||
}
|
||||
|
||||
// loadAndVerifyComment 加载评论并校验权限,成功返回评论,失败直接写响应并返回 nil
|
||||
func loadAndVerifyComment(c *gin.Context) *models.Comment {
|
||||
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 nil
|
||||
}
|
||||
|
||||
if !canManageComment(c, &comment) {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "无权管理该评论"})
|
||||
return nil
|
||||
}
|
||||
|
||||
return &comment
|
||||
}
|
||||
|
||||
// canManageComment 检查当前用户是否有权管理该评论(管理员始终可以,普通用户只能管理自己文章上的评论)
|
||||
func canManageComment(c *gin.Context, comment *models.Comment) bool {
|
||||
if isAdmin(c) {
|
||||
return true
|
||||
}
|
||||
if userID, exists := c.Get("userID"); exists {
|
||||
var post models.Post
|
||||
if err := database.DB.Select("author_id").First(&post, comment.PostID).Error; err != nil {
|
||||
return false
|
||||
}
|
||||
return post.AuthorID == userID.(uint)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// 审核评论
|
||||
func ApproveComment(c *gin.Context) {
|
||||
comment := loadAndVerifyComment(c)
|
||||
if comment == nil {
|
||||
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) {
|
||||
comment := loadAndVerifyComment(c)
|
||||
if comment == nil {
|
||||
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) {
|
||||
comment := loadAndVerifyComment(c)
|
||||
if comment == nil {
|
||||
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": "删除成功"})
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package backend
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"goblog/database"
|
||||
"goblog/models"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// GetDashboardStats 返回仪表盘统计数字(按角色过滤:管理员统计全部,普通用户统计自己的)
|
||||
func GetDashboardStats(c *gin.Context) {
|
||||
// 文章数:管理员统计全部,普通用户统计自己的(与文章管理页可见范围一致)
|
||||
postQuery := database.DB.Model(&models.Post{})
|
||||
if !isAdmin(c) {
|
||||
userID, _ := c.Get("userID")
|
||||
postQuery = postQuery.Where("author_id = ?", userID)
|
||||
}
|
||||
var postCount int64
|
||||
postQuery.Count(&postCount)
|
||||
|
||||
var categoryCount, tagCount int64
|
||||
database.DB.Model(&models.Category{}).Count(&categoryCount)
|
||||
database.DB.Model(&models.Tag{}).Count(&tagCount)
|
||||
|
||||
// 待审核评论数:管理员统计全部,普通用户统计自己文章上的
|
||||
commentQuery := database.DB.Model(&models.Comment{}).Where("status = ?", "pending")
|
||||
if !isAdmin(c) {
|
||||
userID, _ := c.Get("userID")
|
||||
commentQuery = commentQuery.Where("post_id IN (?)",
|
||||
database.DB.Model(&models.Post{}).Select("id").Where("author_id = ?", userID))
|
||||
}
|
||||
var pendingCommentCount int64
|
||||
commentQuery.Count(&pendingCommentCount)
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"posts": postCount,
|
||||
"categories": categoryCount,
|
||||
"tags": tagCount,
|
||||
"pending_comments": pendingCommentCount,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
package backend
|
||||
|
||||
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": "删除成功"})
|
||||
}
|
||||
@@ -0,0 +1,334 @@
|
||||
package backend
|
||||
|
||||
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"
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
package backend
|
||||
|
||||
import (
|
||||
"goblog/config"
|
||||
"goblog/database"
|
||||
"goblog/models"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// GetSettings 获取站点基础设置(管理员)
|
||||
func GetSettings(c *gin.Context) {
|
||||
cfg := config.GetConfig()
|
||||
|
||||
opts, err := models.GetOptionsByUser(database.DB, 0)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "读取设置失败"})
|
||||
return
|
||||
}
|
||||
|
||||
// 默认值:站点信息取配置文件,评论默认显示、每页 10 条
|
||||
settings := map[string]string{
|
||||
models.OptionSiteName: cfg.App.Name,
|
||||
models.OptionSiteDesc: cfg.App.Description,
|
||||
models.OptionShowComments: "1",
|
||||
models.OptionCommentPageSize: "10",
|
||||
}
|
||||
for name := range settings {
|
||||
if v, ok := opts[name]; ok && v != "" {
|
||||
settings[name] = v
|
||||
}
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"data": settings})
|
||||
}
|
||||
|
||||
// UpdateSettingsRequest 更新站点基础设置请求
|
||||
type UpdateSettingsRequest struct {
|
||||
SiteName string `json:"site_name" binding:"required"`
|
||||
SiteDesc string `json:"site_desc"`
|
||||
ShowComments bool `json:"show_comments"`
|
||||
CommentsPerPage int `json:"comments_per_page" binding:"required,min=1,max=100"`
|
||||
}
|
||||
|
||||
// UpdateSettings 保存站点基础设置(管理员)
|
||||
func UpdateSettings(c *gin.Context) {
|
||||
var req UpdateSettingsRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "参数错误: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
showComments := "0"
|
||||
if req.ShowComments {
|
||||
showComments = "1"
|
||||
}
|
||||
options := map[string]string{
|
||||
models.OptionSiteName: req.SiteName,
|
||||
models.OptionSiteDesc: req.SiteDesc,
|
||||
models.OptionShowComments: showComments,
|
||||
models.OptionCommentPageSize: strconv.Itoa(req.CommentsPerPage),
|
||||
}
|
||||
|
||||
if err := models.BatchSetOptions(database.DB, 0, options); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "保存设置失败"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "设置已保存"})
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
package backend
|
||||
|
||||
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": "删除成功"})
|
||||
}
|
||||
@@ -0,0 +1,315 @@
|
||||
package backend
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"goblog/database"
|
||||
"goblog/models"
|
||||
"goblog/utils"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// 获取可用主题列表
|
||||
func GetThemes(c *gin.Context) {
|
||||
themes := []string{}
|
||||
|
||||
// 读取 templates 目录
|
||||
entries, err := os.ReadDir("templates")
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "无法读取主题目录",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() {
|
||||
// 检查是否是有效主题(包含 base.html)
|
||||
basePath := filepath.Join("templates", entry.Name(), "base.html")
|
||||
if _, err := os.Stat(basePath); err == nil {
|
||||
themes = append(themes, entry.Name())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"themes": themes,
|
||||
})
|
||||
}
|
||||
|
||||
// 获取当前主题
|
||||
func GetCurrentTheme(c *gin.Context) {
|
||||
currentTheme := "default"
|
||||
|
||||
// 使用新的 Option 查询方法
|
||||
theme, err := models.GetOptionValue(database.DB, "theme", 0)
|
||||
if err == nil && theme != "" {
|
||||
currentTheme = theme
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"theme": currentTheme,
|
||||
})
|
||||
}
|
||||
|
||||
// 切换主题
|
||||
func SwitchTheme(c *gin.Context) {
|
||||
type Request struct {
|
||||
Theme string `json:"theme" binding:"required"`
|
||||
}
|
||||
|
||||
var req Request
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": "请提供主题名称",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 验证主题是否存在
|
||||
themePath := filepath.Join("templates", req.Theme)
|
||||
if _, err := os.Stat(themePath); os.IsNotExist(err) {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": "主题不存在",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 验证主题是否有效
|
||||
basePath := filepath.Join(themePath, "base.html")
|
||||
if _, err := os.Stat(basePath); os.IsNotExist(err) {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": "主题文件不完整",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 更新数据库中的主题设置
|
||||
err := models.SetOptionValue(database.DB, "theme", 0, req.Theme)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "保存主题设置失败"})
|
||||
return
|
||||
}
|
||||
|
||||
// 重新加载模板(立即生效)
|
||||
renderer := utils.GetGlobalRenderer()
|
||||
if renderer != nil {
|
||||
renderer.Reload(req.Theme)
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"message": "主题切换成功",
|
||||
"theme": req.Theme,
|
||||
})
|
||||
}
|
||||
|
||||
// 创建新主题
|
||||
func CreateTheme(c *gin.Context) {
|
||||
type Request struct {
|
||||
Name string `json:"name" binding:"required"`
|
||||
Template string `json:"template"`
|
||||
}
|
||||
|
||||
var req Request
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": "请提供主题名称",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 验证主题名称(只能包含字母、数字和下划线)
|
||||
validName := true
|
||||
for _, ch := range req.Name {
|
||||
if !((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || (ch >= '0' && ch <= '9') || ch == '_') {
|
||||
validName = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if !validName || req.Name == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": "主题名称只能包含字母、数字和下划线",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 检查主题是否已存在
|
||||
themePath := filepath.Join("templates", req.Name)
|
||||
if _, err := os.Stat(themePath); err == nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": "主题已存在",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 创建主题目录
|
||||
if err := os.MkdirAll(themePath, 0700); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "创建主题目录失败",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 创建新主题时必须选择已有主题作为模板
|
||||
if req.Template == "" || req.Template == "blank" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": "必须选择一个已有主题作为模板",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 验证所选模板是否存在并复制模板文件
|
||||
templateSrc := filepath.Join("templates", req.Template)
|
||||
if _, err := os.Stat(templateSrc); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": "所选模板不存在",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 复制模板文件
|
||||
files := []string{"base.html", "index.html", "post.html", "page.html"}
|
||||
for _, file := range files {
|
||||
srcFile := filepath.Join(templateSrc, file)
|
||||
dstFile := filepath.Join(themePath, file)
|
||||
if data, err := os.ReadFile(srcFile); err == nil {
|
||||
os.WriteFile(dstFile, data, 0644)
|
||||
}
|
||||
}
|
||||
|
||||
// 复制模板的 CSS 文件
|
||||
templateCSS := filepath.Join("static", "css", req.Template+".css")
|
||||
if data, err := os.ReadFile(templateCSS); err == nil {
|
||||
dstCSS := filepath.Join("static", "css", req.Name+".css")
|
||||
os.MkdirAll(filepath.Dir(dstCSS), 0755)
|
||||
os.WriteFile(dstCSS, data, 0644)
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"message": "主题创建成功",
|
||||
"theme": req.Name,
|
||||
})
|
||||
}
|
||||
|
||||
// 获取主题文件列表
|
||||
func GetThemeFiles(c *gin.Context) {
|
||||
themeName := c.Param("theme")
|
||||
|
||||
themePath := filepath.Join("templates", themeName)
|
||||
if _, err := os.Stat(themePath); os.IsNotExist(err) {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": "主题不存在",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
files := []string{}
|
||||
|
||||
// 读取模板文件
|
||||
entries, err := os.ReadDir(themePath)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "读取文件列表失败",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
for _, entry := range entries {
|
||||
if !entry.IsDir() {
|
||||
ext := filepath.Ext(entry.Name())
|
||||
if ext == ".html" || ext == ".css" || ext == ".js" {
|
||||
files = append(files, entry.Name())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 检查是否有 CSS 文件
|
||||
cssPath := filepath.Join("static", "css", themeName+".css")
|
||||
if _, err := os.Stat(cssPath); err == nil {
|
||||
files = append(files, themeName+".css")
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"files": files,
|
||||
})
|
||||
}
|
||||
|
||||
// resolveThemeFilePath 根据主题名和文件名解析实际文件路径
|
||||
func resolveThemeFilePath(themeName, filename string) string {
|
||||
if filename == themeName+".css" {
|
||||
return filepath.Join("static", "css", filename)
|
||||
}
|
||||
return filepath.Join("templates", themeName, filename)
|
||||
}
|
||||
|
||||
// 获取主题文件内容
|
||||
func GetThemeFile(c *gin.Context) {
|
||||
themeName := c.Param("theme")
|
||||
filename := c.Param("file")
|
||||
filePath := resolveThemeFilePath(themeName, filename)
|
||||
|
||||
if _, err := os.Stat(filePath); os.IsNotExist(err) {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": "文件不存在",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
content, err := os.ReadFile(filePath)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "读取文件失败",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"content": string(content),
|
||||
})
|
||||
}
|
||||
|
||||
// 保存主题文件
|
||||
func SaveThemeFile(c *gin.Context) {
|
||||
themeName := c.Param("theme")
|
||||
filename := c.Param("file")
|
||||
|
||||
type Request struct {
|
||||
Content string `json:"content"`
|
||||
}
|
||||
|
||||
var req Request
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": "请提供文件内容",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
filePath := resolveThemeFilePath(themeName, filename)
|
||||
|
||||
// 保存文件
|
||||
if err := os.WriteFile(filePath, []byte(req.Content), 0600); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "保存文件失败",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 如果是模板文件且是当前使用的主题,重新加载
|
||||
if filename != themeName+".css" {
|
||||
currentTheme, err := models.GetOptionValue(database.DB, "theme", 0)
|
||||
if err == nil && currentTheme == themeName {
|
||||
renderer := utils.GetGlobalRenderer()
|
||||
if renderer != nil {
|
||||
renderer.Reload(themeName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"message": "文件保存成功",
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,217 @@
|
||||
package backend
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
const (
|
||||
// uploadDir 上传文件保存目录
|
||||
uploadDir = "./static/uploads"
|
||||
// uploadURLPrefix 上传文件对外访问的 URL 前缀
|
||||
uploadURLPrefix = "/static/uploads/"
|
||||
// maxUploadSize 单个文件最大大小(5MB)
|
||||
maxUploadSize = 5 << 20
|
||||
)
|
||||
|
||||
// allowedImageExts 允许上传的图片扩展名
|
||||
var allowedImageExts = map[string]bool{
|
||||
".jpg": true,
|
||||
".jpeg": true,
|
||||
".png": true,
|
||||
".gif": true,
|
||||
".webp": true,
|
||||
}
|
||||
|
||||
// allowedImageMIMEs 允许上传的图片真实 MIME 类型
|
||||
var allowedImageMIMEs = map[string]bool{
|
||||
"image/jpeg": true,
|
||||
"image/png": true,
|
||||
"image/gif": true,
|
||||
"image/webp": true,
|
||||
}
|
||||
|
||||
// UploadedFile 上传文件信息
|
||||
type UploadedFile struct {
|
||||
Name string `json:"name"`
|
||||
URL string `json:"url"`
|
||||
Size int64 `json:"size"`
|
||||
ModTime string `json:"mod_time"`
|
||||
}
|
||||
|
||||
// sanitizeBaseName 清理文件名(去除扩展名和不安全字符)
|
||||
func sanitizeBaseName(filename string) string {
|
||||
base := strings.TrimSuffix(filepath.Base(filename), filepath.Ext(filename))
|
||||
base = strings.ReplaceAll(base, " ", "-")
|
||||
|
||||
var b strings.Builder
|
||||
for _, r := range base {
|
||||
if r == '-' || r == '_' ||
|
||||
(r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') {
|
||||
b.WriteRune(r)
|
||||
}
|
||||
}
|
||||
|
||||
res := b.String()
|
||||
if res == "" {
|
||||
res = "image"
|
||||
}
|
||||
if len(res) > 40 {
|
||||
res = res[:40]
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
// UploadImage 上传图片(校验格式与大小)
|
||||
func UploadImage(c *gin.Context) {
|
||||
file, err := c.FormFile("file")
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "请选择要上传的文件"})
|
||||
return
|
||||
}
|
||||
|
||||
// 校验文件大小
|
||||
if file.Size > maxUploadSize {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": fmt.Sprintf("文件大小超过限制(最大 %d MB)", maxUploadSize>>20),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 校验扩展名
|
||||
ext := strings.ToLower(filepath.Ext(file.Filename))
|
||||
if !allowedImageExts[ext] {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "不支持的图片格式,仅支持 jpg/jpeg/png/gif/webp"})
|
||||
return
|
||||
}
|
||||
|
||||
// 打开文件并检测真实 MIME 类型(防止伪造扩展名)
|
||||
src, err := file.Open()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "读取文件失败"})
|
||||
return
|
||||
}
|
||||
defer src.Close()
|
||||
|
||||
head := make([]byte, 512)
|
||||
n, _ := src.Read(head)
|
||||
contentType := http.DetectContentType(head[:n])
|
||||
if !allowedImageMIMEs[contentType] {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "文件内容不是有效的图片"})
|
||||
return
|
||||
}
|
||||
|
||||
// 重置读取位置,准备写入
|
||||
if _, err := src.Seek(0, io.SeekStart); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "读取文件失败"})
|
||||
return
|
||||
}
|
||||
|
||||
// 确保上传目录存在
|
||||
if err := os.MkdirAll(uploadDir, 0o755); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "创建上传目录失败"})
|
||||
return
|
||||
}
|
||||
|
||||
// 生成唯一文件名:时间戳_原始名.扩展名
|
||||
filename := fmt.Sprintf("%d_%s%s", time.Now().UnixNano(), sanitizeBaseName(file.Filename), ext)
|
||||
dstPath := filepath.Join(uploadDir, filename)
|
||||
|
||||
dst, err := os.Create(dstPath)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "保存文件失败"})
|
||||
return
|
||||
}
|
||||
defer dst.Close()
|
||||
|
||||
if _, err := io.Copy(dst, src); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "保存文件失败"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"url": uploadURLPrefix + filename,
|
||||
"name": filename,
|
||||
"size": file.Size,
|
||||
})
|
||||
}
|
||||
|
||||
// ListUploads 列出已上传的图片
|
||||
func ListUploads(c *gin.Context) {
|
||||
if err := os.MkdirAll(uploadDir, 0o755); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "读取上传目录失败"})
|
||||
return
|
||||
}
|
||||
|
||||
entries, err := os.ReadDir(uploadDir)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "读取上传目录失败"})
|
||||
return
|
||||
}
|
||||
|
||||
files := make([]UploadedFile, 0, len(entries))
|
||||
for _, e := range entries {
|
||||
if e.IsDir() {
|
||||
continue
|
||||
}
|
||||
ext := strings.ToLower(filepath.Ext(e.Name()))
|
||||
if !allowedImageExts[ext] {
|
||||
continue
|
||||
}
|
||||
info, err := e.Info()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
files = append(files, UploadedFile{
|
||||
Name: e.Name(),
|
||||
URL: uploadURLPrefix + e.Name(),
|
||||
Size: info.Size(),
|
||||
ModTime: info.ModTime().Format("2006-01-02 15:04:05"),
|
||||
})
|
||||
}
|
||||
|
||||
// 按修改时间倒序(最新的在前)
|
||||
sort.Slice(files, func(i, j int) bool {
|
||||
return files[i].ModTime > files[j].ModTime
|
||||
})
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"data": files})
|
||||
}
|
||||
|
||||
// DeleteUpload 删除已上传的图片
|
||||
func DeleteUpload(c *gin.Context) {
|
||||
name := c.Param("name")
|
||||
|
||||
// 防止路径穿越
|
||||
if name == "" || strings.Contains(name, "..") || strings.ContainsAny(name, `/\`) {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "非法的文件名"})
|
||||
return
|
||||
}
|
||||
|
||||
ext := strings.ToLower(filepath.Ext(name))
|
||||
if !allowedImageExts[ext] {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "非法的文件类型"})
|
||||
return
|
||||
}
|
||||
|
||||
path := filepath.Join(uploadDir, name)
|
||||
if _, err := os.Stat(path); err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "文件不存在"})
|
||||
return
|
||||
}
|
||||
|
||||
if err := os.Remove(path); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "删除文件失败"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "删除成功"})
|
||||
}
|
||||
@@ -0,0 +1,255 @@
|
||||
package backend
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"goblog/database"
|
||||
"goblog/models"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// 创建用户请求
|
||||
type CreateUserRequest 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"`
|
||||
Role string `json:"role"`
|
||||
Status *int `json:"status"`
|
||||
}
|
||||
|
||||
// 更新用户请求
|
||||
type AdminUpdateUserRequest struct {
|
||||
Nickname string `json:"nickname"`
|
||||
Email string `json:"email"`
|
||||
Role string `json:"role"`
|
||||
Status *int `json:"status"`
|
||||
}
|
||||
|
||||
// 重置密码请求
|
||||
type ResetPasswordRequest struct {
|
||||
NewPassword string `json:"new_password" binding:"required,min=6"`
|
||||
}
|
||||
|
||||
// 获取用户列表(管理员)
|
||||
func GetUsers(c *gin.Context) {
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
|
||||
keyword := c.Query("keyword")
|
||||
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
if pageSize < 1 || pageSize > 100 {
|
||||
pageSize = 20
|
||||
}
|
||||
|
||||
query := database.DB.Model(&models.User{})
|
||||
if keyword != "" {
|
||||
like := "%" + keyword + "%"
|
||||
query = query.Where("username LIKE ? OR nickname LIKE ? OR email LIKE ?", like, like, like)
|
||||
}
|
||||
|
||||
var total int64
|
||||
query.Count(&total)
|
||||
|
||||
var users []models.User
|
||||
if err := query.Order("id ASC").Offset((page - 1) * pageSize).Limit(pageSize).Find(&users).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "获取用户列表失败"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"data": users,
|
||||
"total": total,
|
||||
"page": page,
|
||||
"page_size": pageSize,
|
||||
})
|
||||
}
|
||||
|
||||
// 创建用户(管理员)
|
||||
func CreateUser(c *gin.Context) {
|
||||
var req CreateUserRequest
|
||||
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
|
||||
}
|
||||
|
||||
role := req.Role
|
||||
if role != "admin" {
|
||||
role = "user"
|
||||
}
|
||||
|
||||
status := 1
|
||||
if req.Status != nil {
|
||||
status = *req.Status
|
||||
}
|
||||
|
||||
user := models.User{
|
||||
Username: req.Username,
|
||||
Nickname: req.Nickname,
|
||||
Email: req.Email,
|
||||
Role: role,
|
||||
Status: status,
|
||||
IsActive: true,
|
||||
}
|
||||
|
||||
if err := user.SetPassword(req.Password); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "密码加密失败"})
|
||||
return
|
||||
}
|
||||
|
||||
if err := database.DB.Create(&user).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "创建用户失败"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusCreated, gin.H{"data": user})
|
||||
}
|
||||
|
||||
// 更新用户信息(管理员)
|
||||
func AdminUpdateUser(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
operatorID, _ := c.Get("userID")
|
||||
|
||||
var user models.User
|
||||
if err := database.DB.First(&user, id).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "用户不存在"})
|
||||
return
|
||||
}
|
||||
|
||||
var req AdminUpdateUserRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
isSelf := operatorID == user.ID
|
||||
|
||||
updates := map[string]interface{}{
|
||||
"nickname": req.Nickname,
|
||||
}
|
||||
|
||||
if req.Email != "" && req.Email != user.Email {
|
||||
// 检查邮箱是否已被其他用户使用
|
||||
var count int64
|
||||
database.DB.Model(&models.User{}).Where("email = ? AND id != ?", req.Email, user.ID).Count(&count)
|
||||
if count > 0 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "邮箱已被其他用户使用"})
|
||||
return
|
||||
}
|
||||
updates["email"] = req.Email
|
||||
}
|
||||
|
||||
if req.Role != "" && req.Role != user.Role {
|
||||
if isSelf {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "不能修改自己的角色"})
|
||||
return
|
||||
}
|
||||
if req.Role != "admin" && req.Role != "user" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的角色"})
|
||||
return
|
||||
}
|
||||
updates["role"] = req.Role
|
||||
}
|
||||
|
||||
if req.Status != nil && *req.Status != user.Status {
|
||||
if isSelf {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "不能禁用自己的账号"})
|
||||
return
|
||||
}
|
||||
updates["status"] = *req.Status
|
||||
}
|
||||
|
||||
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 AdminResetPassword(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
|
||||
var user models.User
|
||||
if err := database.DB.First(&user, id).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "用户不存在"})
|
||||
return
|
||||
}
|
||||
|
||||
var req ResetPasswordRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
if err := user.SetPassword(req.NewPassword); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "密码加密失败"})
|
||||
return
|
||||
}
|
||||
|
||||
if err := database.DB.Save(&user).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "重置密码失败"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "密码重置成功"})
|
||||
}
|
||||
|
||||
// 删除用户(管理员)
|
||||
func DeleteUser(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
operatorID, _ := c.Get("userID")
|
||||
|
||||
var user models.User
|
||||
if err := database.DB.First(&user, id).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "用户不存在"})
|
||||
return
|
||||
}
|
||||
|
||||
if operatorID == user.ID {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "不能删除自己的账号"})
|
||||
return
|
||||
}
|
||||
|
||||
// 检查该用户是否有文章
|
||||
var count int64
|
||||
database.DB.Model(&models.Post{}).Where("author_id = ?", user.ID).Count(&count)
|
||||
if count > 0 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "该用户名下还有文章,无法删除"})
|
||||
return
|
||||
}
|
||||
|
||||
// 检查该用户是否有页面
|
||||
database.DB.Model(&models.Page{}).Where("author_id = ?", user.ID).Count(&count)
|
||||
if count > 0 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "该用户名下还有页面,无法删除"})
|
||||
return
|
||||
}
|
||||
|
||||
if err := database.DB.Delete(&user).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "删除用户失败"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "删除成功"})
|
||||
}
|
||||
Reference in New Issue
Block a user