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) } }() } // AdminGetPost 后台按 ID 获取文章详情(含未发布),供编辑表单加载 func AdminGetPost(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 } post, err := repositories.GetPostByIDAdmin(id) if err != nil { utils.ServerError(c, err) return } if post == nil { utils.Error(c, 404, "Post not found") return } utils.Success(c, repositories.BuildPostResponse(post, true)) } // 获取所有文章(包括未发布的,后台用) 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) } }