优化页面、修复BUG

This commit is contained in:
李琦
2026-06-24 16:50:04 +08:00
parent 88ba9be318
commit 3f653bc336
36 changed files with 1672 additions and 2401 deletions

View File

@@ -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) {

View File

@@ -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
}

View File

@@ -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"`