package repositories import ( "encoding/json" "fmt" "log" "strconv" "strings" "time" "github.com/niangaodev/art-code/config" "github.com/niangaodev/art-code/models" "github.com/sergi/go-diff/diffmatchpatch" "gorm.io/gorm" ) // TrendData 趋势数据 type TrendData struct { Date string `json:"date"` Count int `json:"value"` YoY float64 `json:"yoy"` MoM float64 `json:"mom"` } // GetPosts 获取所有博客文章(支持搜索、分类、标签筛选,支持分页) func GetPosts(keyword string, categoryID uint, tagID uint, columnID uint, page int, pageSize int) ([]models.Post, int64, error) { var posts []models.Post var total int64 // 构建基础查询 query := config.DB.Model(&models.Post{}). Where("is_published = ? AND deleted_at = ?", 1, 0) if tagID > 0 { query = query.Joins("JOIN post_tags pt ON posts.id = pt.post_id"). Where("pt.tag_id = ?", tagID) } if categoryID > 0 { query = query.Where("category_id = ?", categoryID) } if columnID > 0 { query = query.Where("column_id = ?", columnID) } if keyword != "" { likeKeyword := "%" + keyword + "%" query = query.Where("(MATCH(title, content) AGAINST(? IN BOOLEAN MODE) OR title LIKE ? OR content LIKE ?)", keyword, likeKeyword, likeKeyword) } // 计算总数 countQuery := query err := countQuery.Count(&total).Error if err != nil { log.Printf("Error counting posts: %v", err) return nil, 0, err } // 应用分页 offset := (page - 1) * pageSize err = query. Preload("Category"). Preload("Column"). Preload("Tags"). Order("created_at DESC"). Limit(pageSize). Offset(offset). Find(&posts).Error if err != nil { log.Printf("Error querying posts: %v", err) return nil, 0, err } return posts, total, nil } // GetPostsByUserID 获取用户已发布文章(用于公开资料页) func GetPostsByUserID(userID uint, sort string, limit int) ([]models.Post, error) { if limit <= 0 { limit = 5 } if limit > 20 { limit = 20 } orderClause := "created_at DESC" if sort == "popular" { orderClause = "read_count DESC, created_at DESC" } var posts []models.Post err := config.DB.Model(&models.Post{}). Where("user_id = ? AND is_published = ? AND deleted_at = ?", userID, 1, 0). Preload("Category"). Preload("Column"). Preload("Tags"). Order(orderClause). Limit(limit). Find(&posts).Error if err != nil { log.Printf("Error querying posts by user ID: %v", err) return nil, err } return posts, nil } // GetPostByID 根据ID获取博客文章 func GetPostByID(id uint) (*models.Post, error) { var post models.Post // 显式选择所有字段,确保 content 字段被加载 err := config.DB.Model(&models.Post{}). Select("id", "title", "category_id", "column_id", "excerpt", "content", "read_count", "is_published", "user_id", "created_at", "updated_at", "deleted_at"). Preload("Category"). Preload("Column"). Preload("Tags"). Preload("Author", func(db *gorm.DB) *gorm.DB { return db.Select("id", "username", "avatar", "email") }). Where("id = ? AND is_published = ? AND deleted_at = ?", id, 1, 0). First(&post).Error if err != nil { if err == gorm.ErrRecordNotFound { return nil, nil } log.Printf("Error getting post by ID: %v", err) return nil, err } // 不再在此处更新阅读量,由 handler 按小时去重后递增 return &post, nil } // IncrementReadCount 递增文章阅读量 func IncrementReadCount(id uint) error { return config.DB.Model(&models.Post{}). Where("id = ?", id). UpdateColumn("read_count", gorm.Expr("read_count + ?", 1)).Error } // GetAllPosts 获取所有博客文章(包括未发布的,后台用,支持搜索与筛选) type AdminPostFilter struct { Keyword string CategoryID uint ColumnID uint IsPublished int // -1 表示全部 StartDate string EndDate string } func GetAllPosts(page, pageSize int, filter AdminPostFilter) ([]models.Post, int64, error) { offset := (page - 1) * pageSize var posts []models.Post var total int64 query := config.DB.Model(&models.Post{}). Where("deleted_at = ?", 0) if filter.Keyword != "" { likeKeyword := "%" + filter.Keyword + "%" query = query.Where("(MATCH(title, content) AGAINST(? IN BOOLEAN MODE) OR title LIKE ? OR content LIKE ?)", filter.Keyword, likeKeyword, likeKeyword) } if filter.CategoryID > 0 { query = query.Where("category_id = ?", filter.CategoryID) } if filter.ColumnID > 0 { query = query.Where("column_id = ?", filter.ColumnID) } if filter.IsPublished >= 0 { query = query.Where("is_published = ?", filter.IsPublished) } if filter.StartDate != "" { if ts, err := parseDateStart(filter.StartDate); err == nil { query = query.Where("created_at >= ?", ts) } } if filter.EndDate != "" { if ts, err := parseDateEnd(filter.EndDate); err == nil { query = query.Where("created_at <= ?", ts) } } err := query.Count(&total).Error if err != nil { log.Printf("Error counting posts: %v", err) return nil, 0, err } // Get posts err = query. Preload("Category"). Preload("Column"). Preload("Tags"). Order("created_at DESC"). Limit(pageSize). Offset(offset). Find(&posts).Error if err != nil { log.Printf("Error querying all posts: %v", err) return nil, 0, err } return posts, total, nil } // CreatePost 创建博客文章 func CreatePost(post *models.Post) error { err := config.DB.Create(post).Error if err != nil { log.Printf("Error creating post: %v", err) return err } // Insert Tags if len(post.Tags) > 0 { err = config.DB.Model(post).Association("Tags").Replace(post.Tags) if err != nil { log.Printf("Error associating tags: %v", err) return err } } // Handle column association if ColumnID is set if post.ColumnID != nil && *post.ColumnID > 0 { // Add to column_posts table err = AddPostToColumn(*post.ColumnID, post.ID, 0) if err != nil { log.Printf("Error adding post to column: %v", err) // Don't fail the whole operation, just log } } return nil } // UpdatePost 更新博客文章 func UpdatePost(post *models.Post) error { // 使用 map 更新,并明确指定要更新的字段,确保即使字段是空字符串也会被更新 updateData := map[string]interface{}{ "title": post.Title, "category_id": post.CategoryID, "excerpt": post.Excerpt, "content": post.Content, // 明确包含 content,即使为空字符串也会更新 "is_published": post.IsPublished, "updated_at": time.Now().Unix(), } // Update column_id if provided if post.ColumnID != nil { updateData["column_id"] = post.ColumnID } else { // Set to NULL if explicitly set to nil updateData["column_id"] = nil } // 使用 Select 明确指定要更新的字段,确保所有字段都被更新 err := config.DB.Model(&models.Post{}). Where("id = ? AND deleted_at = ?", post.ID, 0). Select("title", "category_id", "column_id", "excerpt", "content", "is_published", "updated_at"). Updates(updateData).Error if err != nil { log.Printf("Error updating post: %v", err) return err } // Update Tags if len(post.Tags) > 0 { err = config.DB.Model(&models.Post{ID: post.ID}).Association("Tags").Replace(post.Tags) if err != nil { log.Printf("Error updating tags: %v", err) return err } } else { // Clear all tags err = config.DB.Model(&models.Post{ID: post.ID}).Association("Tags").Clear() if err != nil { log.Printf("Error clearing tags: %v", err) return err } } // Handle column association // Always sync column_posts table with column_id // First, remove from all columns err = config.DB.Where("post_id = ?", post.ID).Delete(&models.ColumnPost{}).Error if err != nil { log.Printf("Error removing post from columns: %v", err) // Don't fail the whole operation } // Then add to new column if specified if post.ColumnID != nil && *post.ColumnID > 0 { err = AddPostToColumn(*post.ColumnID, post.ID, 0) if err != nil { log.Printf("Error adding post to column: %v", err) // Don't fail the whole operation } } return nil } // UpdatePostRelations 只更新文章的关联关系(分类、专栏、标签),不更新内容 func UpdatePostRelations(postID uint, categoryID uint, columnID *uint, tagIDs []uint) error { // 更新分类和专栏 updateData := map[string]interface{}{ "category_id": categoryID, "updated_at": time.Now().Unix(), } if columnID != nil { updateData["column_id"] = columnID } else { updateData["column_id"] = nil } err := config.DB.Model(&models.Post{}). Where("id = ? AND deleted_at = ?", postID, 0). Updates(updateData).Error if err != nil { log.Printf("Error updating post relations: %v", err) return err } // 更新标签关联 post := &models.Post{ID: postID} if len(tagIDs) > 0 { // 构建标签对象 tags := make([]models.Tag, 0, len(tagIDs)) for _, tagID := range tagIDs { tags = append(tags, models.Tag{ID: tagID}) } err = config.DB.Model(post).Association("Tags").Replace(tags) if err != nil { log.Printf("Error updating tags: %v", err) return err } } else { // 清除所有标签 err = config.DB.Model(post).Association("Tags").Clear() if err != nil { log.Printf("Error clearing tags: %v", err) return err } } // 处理专栏关联 // 先移除所有专栏关联 err = config.DB.Where("post_id = ?", postID).Delete(&models.ColumnPost{}).Error if err != nil { log.Printf("Error removing post from columns: %v", err) // Don't fail the whole operation } // 然后添加新的专栏关联 if columnID != nil && *columnID > 0 { err = AddPostToColumn(*columnID, postID, 0) if err != nil { log.Printf("Error adding post to column: %v", err) // Don't fail the whole operation } } return nil } // UpdatePostStatus 更新文章状态 func UpdatePostStatus(id uint, status int) error { err := config.DB.Model(&models.Post{}). Where("id = ? AND deleted_at = ?", id, 0). Updates(map[string]interface{}{ "is_published": status, "updated_at": time.Now().Unix(), }).Error return err } // DeletePost 删除博客文章 (Soft Delete) func DeletePost(id uint) error { err := config.DB.Model(&models.Post{}). Where("id = ?", id). Update("deleted_at", time.Now().Unix()).Error if err != nil { log.Printf("Error deleting post: %v", err) return err } return nil } // GetPostCount 获取文章总数 func GetPostCount() (int, error) { var count int64 err := config.DB.Model(&models.Post{}). Where("deleted_at = ?", 0). Count(&count).Error if err != nil { log.Printf("Error getting post count: %v", err) return 0, err } return int(count), nil } type userBrief struct { Username string Avatar string Email string } func resolveUserBrief(post *models.Post, userMap map[uint]userBrief) (username, avatar, email string) { if post.Author != nil && post.Author.ID > 0 { return post.Author.Username, post.Author.Avatar, post.Author.Email } if userMap != nil { if u, ok := userMap[post.UserID]; ok { return u.Username, u.Avatar, u.Email } } if post.UserID > 0 { return lookupUserBrief(post.UserID) } return "", "", "" } func collectPostUserIDs(posts []models.Post) []uint { ids := make([]uint, 0, len(posts)) seen := make(map[uint]struct{}) for _, post := range posts { if post.UserID == 0 { continue } if _, ok := seen[post.UserID]; ok { continue } seen[post.UserID] = struct{}{} ids = append(ids, post.UserID) } return ids } func lookupUsersBrief(ids []uint) map[uint]userBrief { result := make(map[uint]userBrief) if len(ids) == 0 { return result } var users []models.User err := config.DB.Model(&models.User{}). Select("id", "username", "avatar", "email"). Where("id IN ? AND deleted_at = ?", ids, 0). Find(&users).Error if err != nil { log.Printf("Error batch loading users: %v", err) return result } for _, user := range users { result[user.ID] = userBrief{ Username: user.Username, Avatar: user.Avatar, Email: user.Email, } } return result } // BuildPostResponse 构建博客文章响应 func BuildPostResponse(post *models.Post, includeContent bool) *models.PostResponse { return buildPostResponse(post, includeContent, true, nil) } func buildPostResponse(post *models.Post, includeContent, includeSnippets bool, userMap map[uint]userBrief) *models.PostResponse { // Format CreatedAt to Date string dateStr := time.Unix(post.CreatedAt, 0).Format("2006-01-02") catName := "" catSlug := "" if post.Category != nil { catName = post.Category.Name catSlug = post.Category.Slug } else if post.CategoryID > 0 { catName = lookupCategoryName(post.CategoryID) } colName := "" colSlug := "" if post.Column != nil { colName = post.Column.Name } userName, userAvatar, userEmail := resolveUserBrief(post, userMap) response := &models.PostResponse{ ID: post.ID, Title: post.Title, CategoryID: post.CategoryID, CategoryName: catName, CategorySlug: catSlug, ColumnID: post.ColumnID, ColumnName: colName, ColumnSlug: colSlug, Date: dateStr, Excerpt: post.Excerpt, Tags: post.Tags, IsPublished: post.IsPublished, ReadCount: post.ReadCount, UserID: post.UserID, UserName: userName, UserAvatar: userAvatar, UserEmail: userEmail, } if includeContent { // 使用指针类型,确保即使内容为空字符串也会出现在 JSON 中 content := post.Content response.Content = &content } if includeSnippets { if snippets, err := GetSnippetsByPostID(post.ID); err == nil && len(snippets) > 0 { response.Snippets = snippets } } return response } // BuildPostsResponse 构建博客文章列表响应(不含正文与 snippets,批量查作者) func BuildPostsResponse(posts []models.Post) []models.PostResponse { if len(posts) == 0 { return []models.PostResponse{} } userMap := lookupUsersBrief(collectPostUserIDs(posts)) responses := make([]models.PostResponse, 0, len(posts)) for i := range posts { responses = append(responses, *buildPostResponse(&posts[i], false, false, userMap)) } return responses } // SavePostHistory 保存文章历史记录 func SavePostHistory(post *models.Post, modifiedBy uint) error { // 获取当前最大版本号 var maxVersion int err := config.DB.Model(&models.PostHistory{}). Where("post_id = ?", post.ID). Select("COALESCE(MAX(version), 0)"). Scan(&maxVersion).Error if err != nil { log.Printf("Error getting max version: %v", err) return err } tagIDs := make([]uint, 0, len(post.Tags)) for _, tag := range post.Tags { tagIDs = append(tagIDs, tag.ID) } tagJSON, _ := json.Marshal(tagIDs) history := &models.PostHistory{ PostID: post.ID, Version: maxVersion + 1, Title: post.Title, CategoryID: post.CategoryID, ColumnID: post.ColumnID, TagIDs: string(tagJSON), Excerpt: post.Excerpt, Content: post.Content, IsPublished: post.IsPublished, ModifiedBy: modifiedBy, } err = config.DB.Create(history).Error if err != nil { log.Printf("Error saving post history: %v", err) return err } return nil } // GetTopPosts 获取热门文章 (按阅读量) func GetTopPosts(limit int) ([]models.Post, error) { var posts []models.Post err := config.DB.Model(&models.Post{}). Preload("Category"). Preload("Tags"). Where("is_published = ? AND deleted_at = ?", 1, 0). Order("read_count DESC"). Limit(limit). Find(&posts).Error if err != nil { return nil, err } return posts, nil } // GetNewPostsTrend 获取新增文章趋势 func GetNewPostsTrend(startDate, endDate string) ([]TrendData, error) { // 确定日期范围 var startTime, endTime time.Time if startDate != "" { startUnix := parseDateToUnix(startDate, false) startTime = time.Unix(startUnix, 0) } else { startTime = time.Now().AddDate(0, 0, -6) startTime = time.Date(startTime.Year(), startTime.Month(), startTime.Day(), 0, 0, 0, 0, time.Local) } if endDate != "" { endUnix := parseDateToUnix(endDate, true) endTime = time.Unix(endUnix, 0) } else { endTime = time.Now() endTime = time.Date(endTime.Year(), endTime.Month(), endTime.Day(), 23, 59, 59, 0, time.Local) } // 查询数据库 query := config.DB.Model(&models.Post{}). Select("FROM_UNIXTIME(created_at, '%Y-%m-%d') as date, COUNT(*) as count"). Where("deleted_at = ?", 0). Where("created_at >= ?", startTime.Unix()). Where("created_at <= ?", endTime.Unix()) var results []TrendData err := query.Group("date"). Order("date ASC"). Scan(&results).Error if err != nil { return nil, err } // 创建日期到数据的映射 resultMap := make(map[string]TrendData) for _, r := range results { resultMap[r.Date] = r } // 生成完整日期列表 var fullResults []TrendData current := time.Date(startTime.Year(), startTime.Month(), startTime.Day(), 0, 0, 0, 0, time.Local) endDay := time.Date(endTime.Year(), endTime.Month(), endTime.Day(), 0, 0, 0, 0, time.Local) for !current.After(endDay) { dateStr := current.Format("2006-01-02") if data, exists := resultMap[dateStr]; exists { data.YoY = 0 data.MoM = 0 fullResults = append(fullResults, data) } else { // 填充0值 fullResults = append(fullResults, TrendData{ Date: dateStr, Count: 0, YoY: 0, MoM: 0, }) } current = current.AddDate(0, 0, 1) } return fullResults, nil } // GetPostHistory 获取文章修改历史 func GetPostHistory(postID uint) ([]models.PostHistory, error) { var history []models.PostHistory err := config.DB.Model(&models.PostHistory{}). Where("post_id = ?", postID). Order("version DESC"). Find(&history).Error if err != nil { return nil, err } return history, nil } // GetPostHistoryByVersion 获取特定版本的历史记录 func GetPostHistoryByVersion(postID uint, version uint) (*models.PostHistory, error) { var h models.PostHistory err := config.DB.Model(&models.PostHistory{}). Where("post_id = ? AND version = ?", postID, version). First(&h).Error if err != nil { if err == gorm.ErrRecordNotFound { return nil, nil } return nil, err } return &h, nil } // BuildPostHistoryResponse 构建历史记录响应 func BuildPostHistoryResponse(h *models.PostHistory, includeFull bool) *models.PostHistoryResponse { opName, opAvatar, opEmail := lookupUserBrief(h.ModifiedBy) resp := &models.PostHistoryResponse{ ID: h.ID, PostID: h.PostID, Version: h.Version, Title: h.Title, CategoryID: h.CategoryID, CategoryName: lookupCategoryName(h.CategoryID), ColumnID: h.ColumnID, ColumnName: lookupColumnName(h.ColumnID), TagIDs: h.GetTagIDList(), TagNames: lookupTagNames(h.GetTagIDList()), Date: time.Unix(h.CreatedAt, 0).Format("2006-01-02"), IsPublished: h.IsPublished, ModifiedBy: h.ModifiedBy, ModifiedByName: opName, UserID: h.ModifiedBy, UserName: opName, UserAvatar: opAvatar, UserEmail: opEmail, ModifiedAt: time.Unix(h.ModifiedAt, 0).Format("2006-01-02 15:04:05"), CreatedAt: time.Unix(h.CreatedAt, 0).Format("2006-01-02 15:04:05"), } if includeFull { resp.Excerpt = h.Excerpt resp.Content = h.Content } return resp } func lookupCategoryName(id uint) string { if id == 0 { return "" } var name string config.DB.Model(&models.Category{}).Where("id = ?", id).Pluck("name", &name) return name } func lookupColumnName(id *uint) string { if id == nil || *id == 0 { return "" } var name string config.DB.Model(&models.Column{}).Where("id = ?", *id).Pluck("name", &name) return name } func lookupTagNames(ids []uint) []string { if len(ids) == 0 { return nil } var tags []models.Tag config.DB.Model(&models.Tag{}).Where("id IN ?", ids).Find(&tags) names := make([]string, 0, len(tags)) for _, t := range tags { names = append(names, t.Name) } return names } func lookupUserBrief(id uint) (username, avatar, email string) { if id == 0 { return "", "", "" } var user models.User err := config.DB.Model(&models.User{}). Select("username", "avatar", "email"). Where("id = ? AND deleted_at = ?", id, 0). First(&user).Error if err != nil { return "", "", "" } return user.Username, user.Avatar, user.Email } func lookupUsername(id uint) string { name, _, _ := lookupUserBrief(id) return name } func formatPublishedLabel(v int) string { if v == 1 { return "已发布" } return "草稿" } // BuildPostHistoryResponses 构建历史记录列表响应 func BuildPostHistoryResponses(history []models.PostHistory) []models.PostHistoryResponse { var responses []models.PostHistoryResponse for _, h := range history { responses = append(responses, *BuildPostHistoryResponse(&h, false)) } return responses } func buildFieldDiff(from, to string, withLineDiff bool) models.PostHistoryFieldDiff { diff := models.PostHistoryFieldDiff{ From: from, To: to, Changed: from != to, } if withLineDiff && from != to { dmp := diffmatchpatch.New() patches := dmp.PatchMake(from, to) diff.Diff = dmp.PatchToText(patches) } return diff } // GetPostHistoryDiff compares two history versions. func GetPostHistoryDiff(postID uint, fromVersion, toVersion uint) (*models.PostHistoryDiffResponse, error) { fromHistory, err := GetPostHistoryByVersion(postID, fromVersion) if err != nil { return nil, err } if fromHistory == nil { return nil, fmt.Errorf("from version not found") } toHistory, err := GetPostHistoryByVersion(postID, toVersion) if err != nil { return nil, err } if toHistory == nil { return nil, fmt.Errorf("to version not found") } fields := map[string]models.PostHistoryFieldDiff{ "title": buildFieldDiff(fromHistory.Title, toHistory.Title, false), "excerpt": buildFieldDiff(fromHistory.Excerpt, toHistory.Excerpt, false), "content": buildFieldDiff(fromHistory.Content, toHistory.Content, true), "category": buildFieldDiff(lookupCategoryName(fromHistory.CategoryID), lookupCategoryName(toHistory.CategoryID), false), "column": buildFieldDiff(lookupColumnName(fromHistory.ColumnID), lookupColumnName(toHistory.ColumnID), false), "tags": buildFieldDiff(strings.Join(lookupTagNames(fromHistory.GetTagIDList()), ", "), strings.Join(lookupTagNames(toHistory.GetTagIDList()), ", "), false), "isPublished": buildFieldDiff(formatPublishedLabel(fromHistory.IsPublished), formatPublishedLabel(toHistory.IsPublished), false), } return &models.PostHistoryDiffResponse{ FromVersion: int(fromVersion), ToVersion: int(toVersion), Fields: fields, }, nil } func formatOptionalUint(v *uint) string { if v == nil { return "" } return strconv.FormatUint(uint64(*v), 10) } // RestorePostFromHistory restores a post from a history snapshot and saves a new history entry. func RestorePostFromHistory(postID, version, modifiedBy uint) (*models.Post, error) { history, err := GetPostHistoryByVersion(postID, version) if err != nil { return nil, err } if history == nil { return nil, fmt.Errorf("history version not found") } var post models.Post if err := config.DB.Preload("Tags").Where("id = ? AND deleted_at = ?", postID, 0).First(&post).Error; err != nil { return nil, err } post.Title = history.Title post.CategoryID = history.CategoryID post.ColumnID = history.ColumnID post.Excerpt = history.Excerpt post.Content = history.Content post.IsPublished = history.IsPublished if err := UpdatePost(&post); err != nil { return nil, err } tagIDs := history.GetTagIDList() tags := make([]models.Tag, 0, len(tagIDs)) for _, id := range tagIDs { tags = append(tags, models.Tag{ID: id}) } if err := config.DB.Model(&post).Association("Tags").Replace(tags); err != nil { return nil, err } post.Tags = tags if err := SavePostHistory(&post, modifiedBy); err != nil { return nil, err } reloaded, err := GetPostByIDAdmin(postID) if err != nil { return nil, err } return reloaded, nil } // GetPostByIDAdmin loads a post for admin use without incrementing read count. func GetPostByIDAdmin(id uint) (*models.Post, error) { var post models.Post err := config.DB.Model(&models.Post{}). Preload("Category"). Preload("Column"). Preload("Tags"). Where("id = ? AND deleted_at = ?", id, 0). First(&post).Error if err != nil { if err == gorm.ErrRecordNotFound { return nil, nil } return nil, err } return &post, nil } // GetRecommendedPostsByIP 基于IP的协同过滤推荐算法 // 查找相同IP访问过的其他文章,按访问频率排序返回 func GetRecommendedPostsByIP(currentPostID uint, userIP string, limit int) ([]models.Post, error) { if userIP == "" { // 如果IP为空,返回热门文章作为备选 return GetTopPosts(limit) } // 1. 查找相同IP访问过的其他文章ID及其访问次数 type PostAccessCount struct { ArticleID uint Count int64 } var accessCounts []PostAccessCount err := config.DB.Model(&models.UserAccessLog{}). Select("article_id, COUNT(*) as count"). Where("user_ip = ? AND article_id != ? AND deleted_at = ?", userIP, currentPostID, 0). Group("article_id"). Order("count DESC"). Limit(limit * 2). // 多查询一些,因为后面还要过滤未发布的文章 Scan(&accessCounts).Error if err != nil { log.Printf("Error querying recommended posts by IP: %v", err) // 如果查询失败,返回热门文章作为备选 return GetTopPosts(limit) } if len(accessCounts) == 0 { // 如果没有访问记录,返回热门文章作为备选 return GetTopPosts(limit) } // 2. 提取文章ID列表 articleIDs := make([]uint, 0, len(accessCounts)) for _, ac := range accessCounts { articleIDs = append(articleIDs, ac.ArticleID) } // 3. 查询这些文章的详细信息(排除未发布的文章) var posts []models.Post err = config.DB.Model(&models.Post{}). Preload("Category"). Preload("Tags"). Where("id IN ? AND is_published = ? AND deleted_at = ?", articleIDs, 1, 0). Find(&posts).Error if err != nil { log.Printf("Error fetching recommended posts: %v", err) return GetTopPosts(limit) } // 4. 按照访问次数排序(保持与 accessCounts 的顺序一致) postMap := make(map[uint]models.Post) for _, post := range posts { postMap[post.ID] = post } var sortedPosts []models.Post for _, ac := range accessCounts { if post, exists := postMap[ac.ArticleID]; exists { sortedPosts = append(sortedPosts, post) if len(sortedPosts) >= limit { break } } } // 5. 如果推荐的文章数量不足,用热门文章补充 if len(sortedPosts) < limit { topPosts, err := GetTopPosts(limit - len(sortedPosts)) if err == nil { // 排除已经推荐的文章 existingIDs := make(map[uint]bool) for _, p := range sortedPosts { existingIDs[p.ID] = true } for _, p := range topPosts { if !existingIDs[p.ID] && p.ID != currentPostID { sortedPosts = append(sortedPosts, p) if len(sortedPosts) >= limit { break } } } } } return sortedPosts, nil } func parseDateStart(dateStr string) (int64, error) { t, err := time.ParseInLocation("2006-01-02", dateStr, time.Local) if err != nil { return 0, err } return t.Unix(), nil } func parseDateEnd(dateStr string) (int64, error) { t, err := time.ParseInLocation("2006-01-02", dateStr, time.Local) if err != nil { return 0, err } return t.Add(24*time.Hour - time.Second).Unix(), nil }