2026-01-15 13:51:44 +08:00
|
|
|
|
package handlers
|
|
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
|
"fmt"
|
|
|
|
|
|
"log"
|
2026-01-16 17:03:34 +08:00
|
|
|
|
"strconv"
|
2026-01-15 13:51:44 +08:00
|
|
|
|
|
|
|
|
|
|
"github.com/gin-gonic/gin"
|
|
|
|
|
|
"github.com/niangaodev/art-code/models"
|
|
|
|
|
|
"github.com/niangaodev/art-code/repositories"
|
2026-01-16 10:19:30 +08:00
|
|
|
|
"github.com/niangaodev/art-code/utils"
|
2026-01-15 13:51:44 +08:00
|
|
|
|
)
|
|
|
|
|
|
|
2026-06-25 16:57:06 +08:00
|
|
|
|
func trySavePostHistory(c *gin.Context, postID uint) bool {
|
|
|
|
|
|
userID, _ := c.Get("userID")
|
|
|
|
|
|
modifiedBy, ok := userID.(uint)
|
|
|
|
|
|
if !ok {
|
|
|
|
|
|
log.Printf("Error saving post history for post %d: invalid userID", postID)
|
|
|
|
|
|
return false
|
|
|
|
|
|
}
|
|
|
|
|
|
saved, err := repositories.GetPostByIDAdmin(postID)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
log.Printf("Error saving post history for post %d: reload failed: %v", postID, err)
|
|
|
|
|
|
return false
|
|
|
|
|
|
}
|
|
|
|
|
|
if saved == nil {
|
|
|
|
|
|
log.Printf("Error saving post history for post %d: post not found after save", postID)
|
|
|
|
|
|
return false
|
|
|
|
|
|
}
|
|
|
|
|
|
if err := repositories.SavePostHistory(saved, modifiedBy); err != nil {
|
|
|
|
|
|
log.Printf("Error saving post history for post %d: %v", postID, err)
|
|
|
|
|
|
return false
|
|
|
|
|
|
}
|
|
|
|
|
|
return true
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-16 17:03:34 +08:00
|
|
|
|
// 获取博客文章列表 (前台)
|
2026-01-15 13:51:44 +08:00
|
|
|
|
func GetPosts(c *gin.Context) {
|
2026-01-15 17:03:59 +08:00
|
|
|
|
// 获取查询参数
|
|
|
|
|
|
keyword := c.Query("q")
|
2026-01-16 17:03:34 +08:00
|
|
|
|
categoryIDStr := c.Query("category")
|
|
|
|
|
|
tagIDStr := c.Query("tag")
|
2026-01-19 16:14:08 +08:00
|
|
|
|
columnIDStr := c.Query("column")
|
2026-01-16 17:03:34 +08:00
|
|
|
|
|
|
|
|
|
|
var categoryID uint
|
|
|
|
|
|
if categoryIDStr != "" {
|
|
|
|
|
|
if id, err := strconv.ParseUint(categoryIDStr, 10, 32); err == nil {
|
|
|
|
|
|
categoryID = uint(id)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var tagID uint
|
|
|
|
|
|
if tagIDStr != "" {
|
|
|
|
|
|
if id, err := strconv.ParseUint(tagIDStr, 10, 32); err == nil {
|
|
|
|
|
|
tagID = uint(id)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-01-15 17:03:59 +08:00
|
|
|
|
|
2026-01-19 16:14:08 +08:00
|
|
|
|
var columnID uint
|
|
|
|
|
|
if columnIDStr != "" {
|
|
|
|
|
|
if id, err := strconv.ParseUint(columnIDStr, 10, 32); err == nil {
|
|
|
|
|
|
columnID = uint(id)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-23 16:05:23 +08:00
|
|
|
|
// 获取分页参数
|
|
|
|
|
|
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
|
|
|
|
|
if page < 1 {
|
|
|
|
|
|
page = 1
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 从数据库读取 posts_per_page 配置作为默认值
|
|
|
|
|
|
pageSize := 10 // 默认值
|
|
|
|
|
|
setting, err := repositories.GetSettingByKey("posts_per_page")
|
|
|
|
|
|
if err == nil && setting != nil {
|
|
|
|
|
|
if ps, err := strconv.Atoi(setting.Value); err == nil && ps > 0 {
|
|
|
|
|
|
pageSize = ps
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 如果请求中指定了 pageSize,则使用请求的值
|
|
|
|
|
|
if pageSizeStr := c.Query("pageSize"); pageSizeStr != "" {
|
|
|
|
|
|
if ps, err := strconv.Atoi(pageSizeStr); err == nil && ps > 0 {
|
|
|
|
|
|
pageSize = ps
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 从数据库获取博客文章(支持分页)
|
|
|
|
|
|
posts, total, err := repositories.GetPosts(keyword, categoryID, tagID, columnID, page, pageSize)
|
2026-01-15 13:51:44 +08:00
|
|
|
|
if err != nil {
|
2026-01-16 17:03:34 +08:00
|
|
|
|
utils.ServerError(c, err)
|
2026-01-15 13:51:44 +08:00
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-19 16:14:08 +08:00
|
|
|
|
// 异步记录搜索日志
|
|
|
|
|
|
userIP := c.ClientIP()
|
|
|
|
|
|
userLocation := utils.GetRegion(userIP)
|
|
|
|
|
|
|
|
|
|
|
|
if keyword != "" {
|
|
|
|
|
|
LogSearch(keyword, "keyword", userIP, userLocation)
|
|
|
|
|
|
}
|
|
|
|
|
|
if categoryID > 0 {
|
|
|
|
|
|
LogSearch(strconv.FormatUint(uint64(categoryID), 10), "category", userIP, userLocation)
|
|
|
|
|
|
}
|
|
|
|
|
|
if tagID > 0 {
|
|
|
|
|
|
LogSearch(strconv.FormatUint(uint64(tagID), 10), "tag", userIP, userLocation)
|
|
|
|
|
|
}
|
|
|
|
|
|
if columnID > 0 {
|
|
|
|
|
|
LogSearch(strconv.FormatUint(uint64(columnID), 10), "column", userIP, userLocation)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-23 16:05:23 +08:00
|
|
|
|
// 构建响应(返回分页格式)
|
2026-01-15 13:51:44 +08:00
|
|
|
|
responses := repositories.BuildPostsResponse(posts)
|
2026-01-16 17:03:34 +08:00
|
|
|
|
if responses == nil {
|
2026-01-23 16:05:23 +08:00
|
|
|
|
responses = []models.PostResponse{}
|
2026-01-16 17:03:34 +08:00
|
|
|
|
}
|
2026-01-23 16:05:23 +08:00
|
|
|
|
|
|
|
|
|
|
// 返回分页格式的响应
|
|
|
|
|
|
res := gin.H{
|
|
|
|
|
|
"list": responses,
|
|
|
|
|
|
"total": total,
|
|
|
|
|
|
"page": page,
|
|
|
|
|
|
"size": pageSize,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
utils.Success(c, res)
|
2026-01-15 13:51:44 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func GetPost(c *gin.Context) {
|
2026-01-15 17:03:59 +08:00
|
|
|
|
idStr := c.Param("id")
|
|
|
|
|
|
// 转换ID
|
|
|
|
|
|
var id uint
|
|
|
|
|
|
if _, err := fmt.Sscanf(idStr, "%d", &id); err != nil {
|
2026-01-16 17:03:34 +08:00
|
|
|
|
utils.Error(c, 400, "Invalid post ID")
|
2026-01-15 17:03:59 +08:00
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-15 13:51:44 +08:00
|
|
|
|
// 从数据库获取博客文章
|
|
|
|
|
|
post, err := repositories.GetPostByID(id)
|
|
|
|
|
|
if err != nil {
|
2026-01-16 17:03:34 +08:00
|
|
|
|
utils.ServerError(c, err)
|
2026-01-15 13:51:44 +08:00
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if post == nil {
|
2026-01-16 17:03:34 +08:00
|
|
|
|
// User requested empty object for details if not found?
|
|
|
|
|
|
// "Details return empty object... otherwise frontend errors"
|
|
|
|
|
|
// If I return 404, frontend api.ts might throw.
|
|
|
|
|
|
// If I return 200 with empty result, frontend might handle it better if it checks result.
|
|
|
|
|
|
// But empty object {} is safer than null.
|
|
|
|
|
|
// Let's return error for now as it's more standard, but user asked for "empty object".
|
|
|
|
|
|
// Actually, let's look at api.ts: `fetchPost` returns `Post` object.
|
|
|
|
|
|
// If it gets null, it might crash access properties.
|
|
|
|
|
|
// If it gets {}, it's fine (properties undefined).
|
|
|
|
|
|
// But usually we want 404.
|
|
|
|
|
|
// Let's stick to Error for Not Found, but ensure api.ts handles it or returns default object.
|
|
|
|
|
|
// My api.ts update handles errors by returning default object for details!
|
|
|
|
|
|
utils.Error(c, 404, "Post not found")
|
2026-01-15 13:51:44 +08:00
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 构建响应,包含内容
|
|
|
|
|
|
response := repositories.BuildPostResponse(post, true)
|
|
|
|
|
|
|
2026-06-24 16:50:04 +08:00
|
|
|
|
visitorKey := utils.GetOrSetVisitorID(c)
|
|
|
|
|
|
|
|
|
|
|
|
var userID uint = 0
|
|
|
|
|
|
if uid, exists := c.Get("userID"); exists {
|
|
|
|
|
|
userID = uid.(uint)
|
|
|
|
|
|
}
|
2026-01-20 14:31:39 +08:00
|
|
|
|
|
2026-06-26 13:37:16 +08:00
|
|
|
|
postID := post.ID
|
|
|
|
|
|
clientIP := c.ClientIP()
|
|
|
|
|
|
|
|
|
|
|
|
utils.Success(c, response)
|
|
|
|
|
|
|
|
|
|
|
|
// 访问量统计与访问日志异步执行,不阻塞文章响应
|
|
|
|
|
|
go func() {
|
|
|
|
|
|
defer func() {
|
|
|
|
|
|
if r := recover(); r != nil {
|
|
|
|
|
|
log.Printf("Panic in visit stats goroutine for post %d: %v", postID, r)
|
|
|
|
|
|
}
|
|
|
|
|
|
}()
|
|
|
|
|
|
|
|
|
|
|
|
alreadyVisited, err := repositories.HasVisitedThisHour(postID, userID, visitorKey)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
log.Printf("Failed to check visit dedup for post %d: %v", postID, err)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
if alreadyVisited {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-06-24 16:50:04 +08:00
|
|
|
|
|
2026-06-26 13:37:16 +08:00
|
|
|
|
if err := repositories.IncrementReadCount(postID); err != nil {
|
|
|
|
|
|
log.Printf("Failed to increment read count for post %d: %v", postID, err)
|
2026-06-24 16:50:04 +08:00
|
|
|
|
}
|
2026-01-16 10:19:30 +08:00
|
|
|
|
|
2026-06-26 13:37:16 +08:00
|
|
|
|
var location string
|
|
|
|
|
|
func() {
|
2026-01-20 14:31:39 +08:00
|
|
|
|
defer func() {
|
|
|
|
|
|
if r := recover(); r != nil {
|
2026-06-26 13:37:16 +08:00
|
|
|
|
log.Printf("Panic in GetRegion for IP %s (post %d): %v", clientIP, postID, r)
|
|
|
|
|
|
location = "Unknown"
|
2026-01-20 14:31:39 +08:00
|
|
|
|
}
|
|
|
|
|
|
}()
|
2026-06-26 13:37:16 +08:00
|
|
|
|
location = utils.GetRegion(clientIP)
|
|
|
|
|
|
}()
|
2026-01-20 14:31:39 +08:00
|
|
|
|
|
2026-06-26 13:37:16 +08:00
|
|
|
|
if location == "" {
|
|
|
|
|
|
location = "Unknown"
|
|
|
|
|
|
}
|
2026-01-16 10:19:30 +08:00
|
|
|
|
|
2026-06-26 13:37:16 +08:00
|
|
|
|
logEntry := &models.UserAccessLog{
|
|
|
|
|
|
UserID: userID,
|
|
|
|
|
|
UserIP: clientIP,
|
|
|
|
|
|
UserLocation: location,
|
|
|
|
|
|
ArticleID: postID,
|
|
|
|
|
|
VisitorKey: visitorKey,
|
|
|
|
|
|
}
|
2026-01-16 10:19:30 +08:00
|
|
|
|
|
2026-06-26 13:37:16 +08:00
|
|
|
|
if err := repositories.CreateUserAccessLog(logEntry); err != nil {
|
|
|
|
|
|
log.Printf("Failed to create user access log for PostID=%d, IP=%s: %v", postID, clientIP, err)
|
|
|
|
|
|
}
|
|
|
|
|
|
}()
|
2026-01-15 13:51:44 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-16 17:03:34 +08:00
|
|
|
|
// 获取所有文章(包括未发布的,后台用)
|
|
|
|
|
|
func AdminGetPosts(c *gin.Context) {
|
|
|
|
|
|
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
2026-01-23 16:05:23 +08:00
|
|
|
|
if page < 1 {
|
|
|
|
|
|
page = 1
|
|
|
|
|
|
}
|
2026-01-15 13:51:44 +08:00
|
|
|
|
|
2026-01-23 16:05:23 +08:00
|
|
|
|
pageSize, _ := strconv.Atoi(c.DefaultQuery("pageSize", "20")) // 默认20条
|
|
|
|
|
|
if pageSize < 1 {
|
|
|
|
|
|
pageSize = 20
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-25 17:57:51 +08:00
|
|
|
|
// 获取搜索与筛选参数
|
|
|
|
|
|
filter := repositories.AdminPostFilter{
|
|
|
|
|
|
Keyword: c.Query("keyword"),
|
2026-01-23 16:05:23 +08:00
|
|
|
|
}
|
2026-06-25 17:57:51 +08:00
|
|
|
|
if filter.Keyword == "" {
|
|
|
|
|
|
filter.Keyword = c.Query("q")
|
|
|
|
|
|
}
|
|
|
|
|
|
if categoryIDStr := c.Query("categoryId"); categoryIDStr != "" {
|
|
|
|
|
|
if id, err := strconv.ParseUint(categoryIDStr, 10, 32); err == nil {
|
|
|
|
|
|
filter.CategoryID = uint(id)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if columnIDStr := c.Query("columnId"); columnIDStr != "" {
|
|
|
|
|
|
if id, err := strconv.ParseUint(columnIDStr, 10, 32); err == nil {
|
|
|
|
|
|
filter.ColumnID = uint(id)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if pubStr := c.Query("isPublished"); pubStr != "" {
|
|
|
|
|
|
if v, err := strconv.Atoi(pubStr); err == nil {
|
|
|
|
|
|
filter.IsPublished = v
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
filter.IsPublished = -1
|
|
|
|
|
|
}
|
|
|
|
|
|
filter.StartDate = c.Query("startDate")
|
|
|
|
|
|
filter.EndDate = c.Query("endDate")
|
2026-01-23 16:05:23 +08:00
|
|
|
|
|
2026-06-25 17:57:51 +08:00
|
|
|
|
posts, total, err := repositories.GetAllPosts(page, pageSize, filter)
|
2026-01-15 13:51:44 +08:00
|
|
|
|
if err != nil {
|
2026-01-16 17:03:34 +08:00
|
|
|
|
utils.ServerError(c, err)
|
2026-01-15 13:51:44 +08:00
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-16 17:03:34 +08:00
|
|
|
|
list := repositories.BuildPostsResponse(posts)
|
|
|
|
|
|
res := gin.H{
|
|
|
|
|
|
"list": list,
|
|
|
|
|
|
"total": total,
|
|
|
|
|
|
"page": page,
|
|
|
|
|
|
"size": pageSize,
|
|
|
|
|
|
}
|
|
|
|
|
|
if list == nil {
|
|
|
|
|
|
res["list"] = []interface{}{}
|
2026-01-15 13:51:44 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-16 17:03:34 +08:00
|
|
|
|
utils.Success(c, res)
|
2026-01-15 13:51:44 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 创建文章
|
|
|
|
|
|
func AdminCreatePost(c *gin.Context) {
|
|
|
|
|
|
var post models.Post
|
|
|
|
|
|
if err := c.ShouldBindJSON(&post); err != nil {
|
2026-01-16 17:03:34 +08:00
|
|
|
|
utils.Error(c, 400, "Invalid request")
|
2026-01-15 13:51:44 +08:00
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 创建文章
|
2026-06-25 17:57:51 +08:00
|
|
|
|
if userID, ok := c.Get("userID"); ok {
|
|
|
|
|
|
if uid, ok := userID.(uint); ok {
|
|
|
|
|
|
post.UserID = uid
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-01-15 13:51:44 +08:00
|
|
|
|
if err := repositories.CreatePost(&post); err != nil {
|
2026-01-16 17:03:34 +08:00
|
|
|
|
utils.ServerError(c, err)
|
2026-01-15 13:51:44 +08:00
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-25 16:57:06 +08:00
|
|
|
|
historySaved := trySavePostHistory(c, post.ID)
|
2026-01-15 13:51:44 +08:00
|
|
|
|
|
2026-06-25 16:57:06 +08:00
|
|
|
|
utils.SuccessWithMsg(c, "Post created successfully", gin.H{"id": post.ID, "historySaved": historySaved})
|
2026-01-15 13:51:44 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 更新文章
|
|
|
|
|
|
func AdminUpdatePost(c *gin.Context) {
|
2026-01-15 17:03:59 +08:00
|
|
|
|
postIDStr := c.Param("id")
|
|
|
|
|
|
var postID uint
|
|
|
|
|
|
if _, err := fmt.Sscanf(postIDStr, "%d", &postID); err != nil {
|
2026-01-16 17:03:34 +08:00
|
|
|
|
utils.Error(c, 400, "Invalid post ID")
|
2026-01-15 17:03:59 +08:00
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-15 13:51:44 +08:00
|
|
|
|
var post models.Post
|
|
|
|
|
|
if err := c.ShouldBindJSON(&post); err != nil {
|
2026-01-16 17:03:34 +08:00
|
|
|
|
utils.Error(c, 400, "Invalid request")
|
2026-01-15 13:51:44 +08:00
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 设置文章ID
|
|
|
|
|
|
post.ID = postID
|
|
|
|
|
|
|
|
|
|
|
|
// 更新文章
|
|
|
|
|
|
if err := repositories.UpdatePost(&post); err != nil {
|
2026-01-16 17:03:34 +08:00
|
|
|
|
utils.ServerError(c, err)
|
2026-01-15 13:51:44 +08:00
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-25 16:57:06 +08:00
|
|
|
|
historySaved := trySavePostHistory(c, postID)
|
2026-01-15 13:51:44 +08:00
|
|
|
|
|
2026-06-25 16:57:06 +08:00
|
|
|
|
utils.SuccessWithMsg(c, "Post updated successfully", gin.H{"historySaved": historySaved})
|
2026-01-16 17:03:34 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-23 16:05:23 +08:00
|
|
|
|
// 更新文章关联关系(只更新分类、专栏、标签,不更新内容)
|
|
|
|
|
|
func AdminUpdatePostRelations(c *gin.Context) {
|
|
|
|
|
|
postIDStr := c.Param("id")
|
|
|
|
|
|
var postID uint
|
|
|
|
|
|
if _, err := fmt.Sscanf(postIDStr, "%d", &postID); err != nil {
|
|
|
|
|
|
utils.Error(c, 400, "Invalid post ID")
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var req struct {
|
|
|
|
|
|
CategoryID uint `json:"categoryId"`
|
|
|
|
|
|
ColumnID *uint `json:"columnId"`
|
|
|
|
|
|
TagIDs []uint `json:"tagIds"`
|
|
|
|
|
|
}
|
|
|
|
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
|
|
|
|
utils.Error(c, 400, "Invalid request")
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 更新关联关系
|
|
|
|
|
|
if err := repositories.UpdatePostRelations(postID, req.CategoryID, req.ColumnID, req.TagIDs); err != nil {
|
|
|
|
|
|
utils.ServerError(c, err)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-25 16:57:06 +08:00
|
|
|
|
historySaved := trySavePostHistory(c, postID)
|
2026-06-24 17:06:22 +08:00
|
|
|
|
|
2026-06-25 16:57:06 +08:00
|
|
|
|
utils.SuccessWithMsg(c, "Post relations updated successfully", gin.H{"historySaved": historySaved})
|
2026-01-23 16:05:23 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-16 17:03:34 +08:00
|
|
|
|
// 切换文章发布状态
|
|
|
|
|
|
func AdminTogglePostStatus(c *gin.Context) {
|
|
|
|
|
|
postIDStr := c.Param("id")
|
|
|
|
|
|
var postID uint
|
|
|
|
|
|
if _, err := fmt.Sscanf(postIDStr, "%d", &postID); err != nil {
|
|
|
|
|
|
utils.Error(c, 400, "Invalid post ID")
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var req struct {
|
|
|
|
|
|
IsPublished int `json:"isPublished"`
|
|
|
|
|
|
}
|
|
|
|
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
|
|
|
|
utils.Error(c, 400, "Invalid request")
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if err := repositories.UpdatePostStatus(postID, req.IsPublished); err != nil {
|
|
|
|
|
|
utils.ServerError(c, err)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-25 16:57:06 +08:00
|
|
|
|
historySaved := trySavePostHistory(c, postID)
|
2026-06-24 17:06:22 +08:00
|
|
|
|
|
2026-06-25 16:57:06 +08:00
|
|
|
|
utils.SuccessWithMsg(c, "Post status updated successfully", gin.H{"historySaved": historySaved})
|
2026-01-15 13:51:44 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 删除文章
|
|
|
|
|
|
func AdminDeletePost(c *gin.Context) {
|
2026-01-15 17:03:59 +08:00
|
|
|
|
postIDStr := c.Param("id")
|
|
|
|
|
|
var postID uint
|
|
|
|
|
|
if _, err := fmt.Sscanf(postIDStr, "%d", &postID); err != nil {
|
2026-01-16 17:03:34 +08:00
|
|
|
|
utils.Error(c, 400, "Invalid post ID")
|
2026-01-15 17:03:59 +08:00
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-01-15 13:51:44 +08:00
|
|
|
|
|
|
|
|
|
|
// 删除文章
|
|
|
|
|
|
if err := repositories.DeletePost(postID); err != nil {
|
2026-01-16 17:03:34 +08:00
|
|
|
|
utils.ServerError(c, err)
|
2026-01-15 13:51:44 +08:00
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-16 17:03:34 +08:00
|
|
|
|
utils.SuccessWithMsg(c, "Post deleted successfully", nil)
|
2026-01-15 13:51:44 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 获取文章历史记录
|
|
|
|
|
|
func AdminGetPostHistory(c *gin.Context) {
|
2026-01-15 17:03:59 +08:00
|
|
|
|
postIDStr := c.Param("id")
|
|
|
|
|
|
var postID uint
|
|
|
|
|
|
if _, err := fmt.Sscanf(postIDStr, "%d", &postID); err != nil {
|
2026-01-16 17:03:34 +08:00
|
|
|
|
utils.Error(c, 400, "Invalid post ID")
|
2026-01-15 17:03:59 +08:00
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-15 13:51:44 +08:00
|
|
|
|
history, err := repositories.GetPostHistory(postID)
|
|
|
|
|
|
if err != nil {
|
2026-01-16 17:03:34 +08:00
|
|
|
|
utils.ServerError(c, err)
|
2026-01-15 13:51:44 +08:00
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-16 17:03:34 +08:00
|
|
|
|
res := repositories.BuildPostHistoryResponses(history)
|
|
|
|
|
|
if res == nil {
|
|
|
|
|
|
utils.Success(c, []interface{}{})
|
|
|
|
|
|
} else {
|
|
|
|
|
|
utils.Success(c, res)
|
|
|
|
|
|
}
|
2026-01-15 13:51:44 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 获取指定版本的文章历史记录
|
|
|
|
|
|
func AdminGetPostHistoryByVersion(c *gin.Context) {
|
2026-01-15 17:03:59 +08:00
|
|
|
|
postIDStr := c.Param("id")
|
|
|
|
|
|
var postID uint
|
|
|
|
|
|
if _, err := fmt.Sscanf(postIDStr, "%d", &postID); err != nil {
|
2026-01-16 17:03:34 +08:00
|
|
|
|
utils.Error(c, 400, "Invalid post ID")
|
2026-01-15 17:03:59 +08:00
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-15 13:51:44 +08:00
|
|
|
|
version := c.Param("version")
|
|
|
|
|
|
|
|
|
|
|
|
// 转换版本号为uint
|
|
|
|
|
|
var versionUint uint
|
|
|
|
|
|
_, err := fmt.Sscanf(version, "%d", &versionUint)
|
|
|
|
|
|
if err != nil {
|
2026-01-16 17:03:34 +08:00
|
|
|
|
utils.Error(c, 400, "Invalid version")
|
2026-01-15 13:51:44 +08:00
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
history, err := repositories.GetPostHistoryByVersion(postID, versionUint)
|
|
|
|
|
|
if err != nil {
|
2026-01-16 17:03:34 +08:00
|
|
|
|
utils.ServerError(c, err)
|
2026-01-15 13:51:44 +08:00
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if history == nil {
|
2026-01-16 17:03:34 +08:00
|
|
|
|
utils.Error(c, 404, "History not found")
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-24 16:50:04 +08:00
|
|
|
|
utils.Success(c, repositories.BuildPostHistoryResponse(history, true))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// AdminGetPostHistoryDiff 对比两个历史版本
|
|
|
|
|
|
func AdminGetPostHistoryDiff(c *gin.Context) {
|
|
|
|
|
|
postIDStr := c.Param("id")
|
|
|
|
|
|
var postID uint
|
|
|
|
|
|
if _, err := fmt.Sscanf(postIDStr, "%d", &postID); err != nil {
|
|
|
|
|
|
utils.Error(c, 400, "Invalid post ID")
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fromVersion, err := strconv.ParseUint(c.Query("from"), 10, 32)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
utils.Error(c, 400, "Invalid from version")
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
toVersion, err := strconv.ParseUint(c.Query("to"), 10, 32)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
utils.Error(c, 400, "Invalid to version")
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
diff, err := repositories.GetPostHistoryDiff(postID, uint(fromVersion), uint(toVersion))
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
utils.Error(c, 404, err.Error())
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
utils.Success(c, diff)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// AdminRestorePostHistory 恢复指定历史版本
|
|
|
|
|
|
func AdminRestorePostHistory(c *gin.Context) {
|
|
|
|
|
|
postIDStr := c.Param("id")
|
|
|
|
|
|
var postID uint
|
|
|
|
|
|
if _, err := fmt.Sscanf(postIDStr, "%d", &postID); err != nil {
|
|
|
|
|
|
utils.Error(c, 400, "Invalid post ID")
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
versionStr := c.Param("version")
|
|
|
|
|
|
versionUint, err := strconv.ParseUint(versionStr, 10, 32)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
utils.Error(c, 400, "Invalid version")
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
userID, _ := c.Get("userID")
|
|
|
|
|
|
post, err := repositories.RestorePostFromHistory(postID, uint(versionUint), userID.(uint))
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
utils.ServerError(c, err)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
utils.Success(c, repositories.BuildPostResponse(post, true))
|
2026-01-16 17:03:34 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// GetPostsByTagID 根据标签ID获取文章
|
|
|
|
|
|
func GetPostsByTagID(c *gin.Context) {
|
|
|
|
|
|
tagIDStr := c.Param("id")
|
|
|
|
|
|
var tagID uint
|
|
|
|
|
|
if _, err := fmt.Sscanf(tagIDStr, "%d", &tagID); err != nil {
|
|
|
|
|
|
utils.Error(c, 400, "Invalid tag ID")
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-23 16:05:23 +08:00
|
|
|
|
// 获取分页参数
|
|
|
|
|
|
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
|
|
|
|
|
if page < 1 {
|
|
|
|
|
|
page = 1
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 从数据库读取 posts_per_page 配置作为默认值
|
|
|
|
|
|
pageSize := 10 // 默认值
|
|
|
|
|
|
setting, err := repositories.GetSettingByKey("posts_per_page")
|
|
|
|
|
|
if err == nil && setting != nil {
|
|
|
|
|
|
if ps, err := strconv.Atoi(setting.Value); err == nil && ps > 0 {
|
|
|
|
|
|
pageSize = ps
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 如果请求中指定了 pageSize,则使用请求的值
|
|
|
|
|
|
if pageSizeStr := c.Query("pageSize"); pageSizeStr != "" {
|
|
|
|
|
|
if ps, err := strconv.Atoi(pageSizeStr); err == nil && ps > 0 {
|
|
|
|
|
|
pageSize = ps
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
posts, total, err := repositories.GetPosts("", 0, tagID, 0, page, pageSize)
|
2026-01-16 17:03:34 +08:00
|
|
|
|
if err != nil {
|
|
|
|
|
|
utils.ServerError(c, err)
|
2026-01-15 13:51:44 +08:00
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-23 16:05:23 +08:00
|
|
|
|
// 构建响应(返回分页格式)
|
|
|
|
|
|
responses := repositories.BuildPostsResponse(posts)
|
|
|
|
|
|
if responses == nil {
|
|
|
|
|
|
responses = []models.PostResponse{}
|
2026-01-16 17:03:34 +08:00
|
|
|
|
}
|
2026-01-23 16:05:23 +08:00
|
|
|
|
|
|
|
|
|
|
// 返回分页格式的响应
|
|
|
|
|
|
res := gin.H{
|
|
|
|
|
|
"list": responses,
|
|
|
|
|
|
"total": total,
|
|
|
|
|
|
"page": page,
|
|
|
|
|
|
"size": pageSize,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
utils.Success(c, res)
|
2026-01-15 13:51:44 +08:00
|
|
|
|
}
|
2026-01-20 15:43:17 +08:00
|
|
|
|
|
|
|
|
|
|
// GetRecommendedPosts 获取推荐文章(基于IP的协同过滤)
|
|
|
|
|
|
func GetRecommendedPosts(c *gin.Context) {
|
|
|
|
|
|
idStr := c.Param("id")
|
|
|
|
|
|
var id uint
|
|
|
|
|
|
if _, err := fmt.Sscanf(idStr, "%d", &id); err != nil {
|
|
|
|
|
|
utils.Error(c, 400, "Invalid post ID")
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 获取客户端IP
|
|
|
|
|
|
userIP := c.ClientIP()
|
|
|
|
|
|
|
|
|
|
|
|
// 获取推荐文章(默认3篇)
|
|
|
|
|
|
limit := 3
|
|
|
|
|
|
if limitStr := c.Query("limit"); limitStr != "" {
|
|
|
|
|
|
if l, err := strconv.Atoi(limitStr); err == nil && l > 0 && l <= 10 {
|
|
|
|
|
|
limit = l
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
posts, err := repositories.GetRecommendedPostsByIP(id, userIP, limit)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
utils.ServerError(c, err)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 构建响应
|
|
|
|
|
|
responses := repositories.BuildPostsResponse(posts)
|
|
|
|
|
|
if responses == nil {
|
|
|
|
|
|
utils.Success(c, []interface{}{})
|
|
|
|
|
|
} else {
|
|
|
|
|
|
utils.Success(c, responses)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|