Files
nl-blogs/server/handlers/post.go
2026-06-26 13:37:16 +08:00

613 lines
15 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package handlers
import (
"fmt"
"log"
"strconv"
"github.com/gin-gonic/gin"
"github.com/niangaodev/art-code/models"
"github.com/niangaodev/art-code/repositories"
"github.com/niangaodev/art-code/utils"
)
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
}
// 获取博客文章列表 (前台)
func GetPosts(c *gin.Context) {
// 获取查询参数
keyword := c.Query("q")
categoryIDStr := c.Query("category")
tagIDStr := c.Query("tag")
columnIDStr := c.Query("column")
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)
}
}
var columnID uint
if columnIDStr != "" {
if id, err := strconv.ParseUint(columnIDStr, 10, 32); err == nil {
columnID = uint(id)
}
}
// 获取分页参数
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)
if err != nil {
utils.ServerError(c, err)
return
}
// 异步记录搜索日志
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)
}
// 构建响应(返回分页格式)
responses := repositories.BuildPostsResponse(posts)
if responses == nil {
responses = []models.PostResponse{}
}
// 返回分页格式的响应
res := gin.H{
"list": responses,
"total": total,
"page": page,
"size": pageSize,
}
utils.Success(c, res)
}
func GetPost(c *gin.Context) {
idStr := c.Param("id")
// 转换ID
var id uint
if _, err := fmt.Sscanf(idStr, "%d", &id); err != nil {
utils.Error(c, 400, "Invalid post ID")
return
}
// 从数据库获取博客文章
post, err := repositories.GetPostByID(id)
if err != nil {
utils.ServerError(c, err)
return
}
if post == nil {
// 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")
return
}
// 构建响应,包含内容
response := repositories.BuildPostResponse(post, true)
visitorKey := utils.GetOrSetVisitorID(c)
var userID uint = 0
if uid, exists := c.Get("userID"); exists {
userID = uid.(uint)
}
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
}
if err := repositories.IncrementReadCount(postID); err != nil {
log.Printf("Failed to increment read count for post %d: %v", postID, err)
}
var location string
func() {
defer func() {
if r := recover(); r != nil {
log.Printf("Panic in GetRegion for IP %s (post %d): %v", clientIP, postID, r)
location = "Unknown"
}
}()
location = utils.GetRegion(clientIP)
}()
if location == "" {
location = "Unknown"
}
logEntry := &models.UserAccessLog{
UserID: userID,
UserIP: clientIP,
UserLocation: location,
ArticleID: postID,
VisitorKey: visitorKey,
}
if err := repositories.CreateUserAccessLog(logEntry); err != nil {
log.Printf("Failed to create user access log for PostID=%d, IP=%s: %v", postID, clientIP, err)
}
}()
}
// 获取所有文章(包括未发布的,后台用)
func AdminGetPosts(c *gin.Context) {
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
if page < 1 {
page = 1
}
pageSize, _ := strconv.Atoi(c.DefaultQuery("pageSize", "20")) // 默认20条
if pageSize < 1 {
pageSize = 20
}
// 获取搜索与筛选参数
filter := repositories.AdminPostFilter{
Keyword: c.Query("keyword"),
}
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")
posts, total, err := repositories.GetAllPosts(page, pageSize, filter)
if err != nil {
utils.ServerError(c, err)
return
}
list := repositories.BuildPostsResponse(posts)
res := gin.H{
"list": list,
"total": total,
"page": page,
"size": pageSize,
}
if list == nil {
res["list"] = []interface{}{}
}
utils.Success(c, res)
}
// 创建文章
func AdminCreatePost(c *gin.Context) {
var post models.Post
if err := c.ShouldBindJSON(&post); err != nil {
utils.Error(c, 400, "Invalid request")
return
}
// 创建文章
if userID, ok := c.Get("userID"); ok {
if uid, ok := userID.(uint); ok {
post.UserID = uid
}
}
if err := repositories.CreatePost(&post); err != nil {
utils.ServerError(c, err)
return
}
historySaved := trySavePostHistory(c, post.ID)
utils.SuccessWithMsg(c, "Post created successfully", gin.H{"id": post.ID, "historySaved": historySaved})
}
// 更新文章
func AdminUpdatePost(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 post models.Post
if err := c.ShouldBindJSON(&post); err != nil {
utils.Error(c, 400, "Invalid request")
return
}
// 设置文章ID
post.ID = postID
// 更新文章
if err := repositories.UpdatePost(&post); err != nil {
utils.ServerError(c, err)
return
}
historySaved := trySavePostHistory(c, postID)
utils.SuccessWithMsg(c, "Post updated successfully", gin.H{"historySaved": historySaved})
}
// 更新文章关联关系(只更新分类、专栏、标签,不更新内容)
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
}
historySaved := trySavePostHistory(c, postID)
utils.SuccessWithMsg(c, "Post relations updated successfully", gin.H{"historySaved": historySaved})
}
// 切换文章发布状态
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
}
historySaved := trySavePostHistory(c, postID)
utils.SuccessWithMsg(c, "Post status updated successfully", gin.H{"historySaved": historySaved})
}
// 删除文章
func AdminDeletePost(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
}
// 删除文章
if err := repositories.DeletePost(postID); err != nil {
utils.ServerError(c, err)
return
}
utils.SuccessWithMsg(c, "Post deleted successfully", nil)
}
// 获取文章历史记录
func AdminGetPostHistory(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
}
history, err := repositories.GetPostHistory(postID)
if err != nil {
utils.ServerError(c, err)
return
}
res := repositories.BuildPostHistoryResponses(history)
if res == nil {
utils.Success(c, []interface{}{})
} else {
utils.Success(c, res)
}
}
// 获取指定版本的文章历史记录
func AdminGetPostHistoryByVersion(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
}
version := c.Param("version")
// 转换版本号为uint
var versionUint uint
_, err := fmt.Sscanf(version, "%d", &versionUint)
if err != nil {
utils.Error(c, 400, "Invalid version")
return
}
history, err := repositories.GetPostHistoryByVersion(postID, versionUint)
if err != nil {
utils.ServerError(c, err)
return
}
if history == nil {
utils.Error(c, 404, "History not found")
return
}
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))
}
// 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
}
// 获取分页参数
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)
if err != nil {
utils.ServerError(c, err)
return
}
// 构建响应(返回分页格式)
responses := repositories.BuildPostsResponse(posts)
if responses == nil {
responses = []models.PostResponse{}
}
// 返回分页格式的响应
res := gin.H{
"list": responses,
"total": total,
"page": page,
"size": pageSize,
}
utils.Success(c, res)
}
// 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)
}
}