优化页面、修复BUG
This commit is contained in:
@@ -1,11 +1,15 @@
|
||||
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"
|
||||
)
|
||||
|
||||
@@ -91,14 +95,17 @@ func GetPostByID(id uint) (*models.Post, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 更新阅读量
|
||||
config.DB.Model(&models.Post{}).
|
||||
Where("id = ?", id).
|
||||
UpdateColumn("read_count", gorm.Expr("read_count + ?", 1))
|
||||
|
||||
// 不再在此处更新阅读量,由 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
|
||||
@@ -408,11 +415,19 @@ func SavePostHistory(post *models.Post, modifiedBy uint) error {
|
||||
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,
|
||||
@@ -541,30 +556,163 @@ func GetPostHistoryByVersion(postID uint, version uint) (*models.PostHistory, er
|
||||
}
|
||||
|
||||
// BuildPostHistoryResponse 构建历史记录响应
|
||||
func BuildPostHistoryResponse(h *models.PostHistory) *models.PostHistoryResponse {
|
||||
return &models.PostHistoryResponse{
|
||||
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))
|
||||
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) {
|
||||
|
||||
@@ -61,3 +61,39 @@ func DeleteSearchLog(id uint) error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// HotKeyword 热门搜索关键词
|
||||
type HotKeyword struct {
|
||||
Keyword string `json:"keyword"`
|
||||
Count int `json:"count"`
|
||||
}
|
||||
|
||||
// GetHotKeywords 获取热门搜索关键词
|
||||
func GetHotKeywords(limit, days int) ([]HotKeyword, error) {
|
||||
if limit <= 0 {
|
||||
limit = 10
|
||||
}
|
||||
if days <= 0 {
|
||||
days = 30
|
||||
}
|
||||
|
||||
since := time.Now().AddDate(0, 0, -days).Unix()
|
||||
|
||||
var results []HotKeyword
|
||||
err := config.DB.Model(&models.SearchLog{}).
|
||||
Select("keyword, COUNT(*) as count").
|
||||
Where("deleted_at = ?", 0).
|
||||
Where("search_type = ?", "keyword").
|
||||
Where("created_at >= ?", since).
|
||||
Where("CHAR_LENGTH(keyword) >= ?", 2).
|
||||
Group("keyword").
|
||||
Order("count DESC").
|
||||
Limit(limit).
|
||||
Scan(&results).Error
|
||||
if err != nil {
|
||||
log.Printf("Error querying hot keywords: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
@@ -23,6 +23,30 @@ func CreateUserAccessLog(logEntry *models.UserAccessLog) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// HasVisitedThisHour checks if the visitor already has an access log for this article in the current hour.
|
||||
func HasVisitedThisHour(articleID, userID uint, visitorKey string) (bool, error) {
|
||||
hourStart := time.Now().Truncate(time.Hour).Unix()
|
||||
hourEnd := hourStart + 3600
|
||||
|
||||
var count int64
|
||||
query := config.DB.Model(&models.UserAccessLog{}).
|
||||
Where("deleted_at = ?", 0).
|
||||
Where("article_id = ?", articleID).
|
||||
Where("access_time >= ? AND access_time < ?", hourStart, hourEnd)
|
||||
|
||||
if userID > 0 {
|
||||
query = query.Where("user_id = ?", userID)
|
||||
} else {
|
||||
query = query.Where("visitor_key = ?", visitorKey)
|
||||
}
|
||||
|
||||
if err := query.Count(&count).Error; err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return count > 0, nil
|
||||
}
|
||||
|
||||
// AccessStats 访问统计数据结构
|
||||
type AccessStats struct {
|
||||
Date string `json:"date"`
|
||||
|
||||
Reference in New Issue
Block a user