807 lines
21 KiB
Go
807 lines
21 KiB
Go
package repositories
|
||
|
||
import (
|
||
"encoding/json"
|
||
"fmt"
|
||
"log"
|
||
"strconv"
|
||
"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
|
||
}
|
||
|
||
// 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", "created_at", "updated_at", "deleted_at").
|
||
Preload("Category").
|
||
Preload("Column").
|
||
Preload("Tags").
|
||
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 获取所有博客文章(包括未发布的,后台用,支持搜索)
|
||
func GetAllPosts(page, pageSize int, keyword string) ([]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 keyword != "" {
|
||
likeKeyword := "%" + keyword + "%"
|
||
query = query.Where("(MATCH(title, content) AGAINST(? IN BOOLEAN MODE) OR title LIKE ? OR content LIKE ?)",
|
||
keyword, likeKeyword, likeKeyword)
|
||
}
|
||
|
||
// Count total
|
||
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
|
||
}
|
||
|
||
// BuildPostResponse 构建博客文章响应
|
||
func BuildPostResponse(post *models.Post, includeContent bool) *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
|
||
}
|
||
|
||
colName := ""
|
||
colSlug := ""
|
||
if post.Column != nil {
|
||
colName = post.Column.Name
|
||
// 如果 Column 有 Slug 字段,可以在这里添加
|
||
// colSlug = post.Column.Slug
|
||
}
|
||
|
||
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,
|
||
}
|
||
|
||
if includeContent {
|
||
// 使用指针类型,确保即使内容为空字符串也会出现在 JSON 中
|
||
content := post.Content
|
||
response.Content = &content
|
||
}
|
||
|
||
return response
|
||
}
|
||
|
||
// BuildPostsResponse 构建博客文章列表响应
|
||
func BuildPostsResponse(posts []models.Post) []models.PostResponse {
|
||
var responses []models.PostResponse
|
||
for _, post := range posts {
|
||
responses = append(responses, *BuildPostResponse(&post, false))
|
||
}
|
||
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 {
|
||
resp := &models.PostHistoryResponse{
|
||
ID: h.ID,
|
||
PostID: h.PostID,
|
||
Version: h.Version,
|
||
Title: h.Title,
|
||
CategoryID: h.CategoryID,
|
||
ColumnID: h.ColumnID,
|
||
TagIDs: h.GetTagIDList(),
|
||
Date: time.Unix(h.CreatedAt, 0).Format("2006-01-02"),
|
||
IsPublished: h.IsPublished,
|
||
ModifiedBy: h.ModifiedBy,
|
||
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
|
||
}
|
||
|
||
// 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")
|
||
}
|
||
|
||
fromTags, _ := json.Marshal(fromHistory.GetTagIDList())
|
||
toTags, _ := json.Marshal(toHistory.GetTagIDList())
|
||
|
||
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),
|
||
"categoryId": buildFieldDiff(strconv.FormatUint(uint64(fromHistory.CategoryID), 10), strconv.FormatUint(uint64(toHistory.CategoryID), 10), false),
|
||
"columnId": buildFieldDiff(formatOptionalUint(fromHistory.ColumnID), formatOptionalUint(toHistory.ColumnID), false),
|
||
"tagIds": buildFieldDiff(string(fromTags), string(toTags), false),
|
||
"isPublished": buildFieldDiff(strconv.Itoa(fromHistory.IsPublished), strconv.Itoa(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
|
||
}
|