Files
nl-blogs/server/repositories/post_repository.go
2026-01-16 17:03:34 +08:00

557 lines
13 KiB
Go

package repositories
import (
"database/sql"
"log"
"time"
"github.com/niangaodev/art-code/config"
"github.com/niangaodev/art-code/models"
)
// 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) ([]models.Post, error) {
query := `
SELECT p.id, p.title, p.category_id, c.name, c.slug, p.excerpt, p.content, p.read_count, p.is_published, p.created_at, p.updated_at, p.deleted_at
FROM posts p
LEFT JOIN categories c ON p.category_id = c.id
`
whereClause := " WHERE p.is_published = 1 AND p.deleted_at = 0"
args := []interface{}{}
if tagID > 0 {
query += " JOIN post_tags pt ON p.id = pt.post_id"
whereClause += " AND pt.tag_id = ?"
args = append(args, tagID)
}
if categoryID > 0 {
whereClause += " AND p.category_id = ?"
args = append(args, categoryID)
}
if keyword != "" {
whereClause += ` AND (
MATCH(p.title, p.content) AGAINST(? IN BOOLEAN MODE) OR
p.title LIKE ? OR
p.content LIKE ?
)`
likeKeyword := "%" + keyword + "%"
args = append(args, keyword, likeKeyword, likeKeyword)
}
query += whereClause + " ORDER BY p.created_at DESC"
rows, err := config.DB.Query(query, args...)
if err != nil {
log.Printf("Error querying posts: %v", err)
return nil, err
}
defer rows.Close()
var posts []models.Post
for rows.Next() {
var post models.Post
var catID sql.NullInt64
var catName sql.NullString
var catSlug sql.NullString
if err := rows.Scan(
&post.ID,
&post.Title,
&catID,
&catName,
&catSlug,
&post.Excerpt,
&post.Content,
&post.ReadCount,
&post.IsPublished,
&post.CreatedAt,
&post.UpdatedAt,
&post.DeletedAt,
); err != nil {
log.Printf("Error scanning post: %v", err)
continue
}
if catID.Valid {
post.CategoryID = uint(catID.Int64)
post.Category = &models.Category{
ID: uint(catID.Int64),
Name: catName.String,
Slug: catSlug.String,
}
}
// TODO: Fetch tags if needed, or lazy load
posts = append(posts, post)
}
return posts, nil
}
// GetPostByID 根据ID获取博客文章
func GetPostByID(id uint) (*models.Post, error) {
query := `
SELECT p.id, p.title, p.category_id, c.name, c.slug, p.excerpt, p.content, p.read_count, p.is_published, p.created_at, p.updated_at, p.deleted_at
FROM posts p
LEFT JOIN categories c ON p.category_id = c.id
WHERE p.id = ? AND p.is_published = 1 AND p.deleted_at = 0
`
row := config.DB.QueryRow(query, id)
var post models.Post
var catID sql.NullInt64
var catName sql.NullString
var catSlug sql.NullString
if err := row.Scan(
&post.ID,
&post.Title,
&catID,
&catName,
&catSlug,
&post.Excerpt,
&post.Content,
&post.ReadCount,
&post.IsPublished,
&post.CreatedAt,
&post.UpdatedAt,
&post.DeletedAt,
); err != nil {
if err == sql.ErrNoRows {
return nil, nil
}
log.Printf("Error scanning post by ID: %v", err)
return nil, err
}
if catID.Valid {
post.CategoryID = uint(catID.Int64)
post.Category = &models.Category{
ID: uint(catID.Int64),
Name: catName.String,
Slug: catSlug.String,
}
}
// 获取标签
tags, err := GetTagsByPostID(post.ID)
if err == nil {
post.Tags = tags
}
// 更新阅读量
updateReadCountQuery := "UPDATE posts SET read_count = read_count + 1 WHERE id = ?"
if _, err := config.DB.Exec(updateReadCountQuery, id); err != nil {
log.Printf("Error updating post read count: %v", err)
}
return &post, nil
}
// GetAllPosts 获取所有博客文章(包括未发布的,后台用)
func GetAllPosts(page, pageSize int) ([]models.Post, int64, error) {
offset := (page - 1) * pageSize
// Count total
var total int64
config.DB.QueryRow("SELECT COUNT(*) FROM posts WHERE deleted_at = 0").Scan(&total)
query := `
SELECT p.id, p.title, p.category_id, c.name, c.slug, p.excerpt, p.content, p.read_count, p.is_published, p.created_at, p.updated_at, p.deleted_at
FROM posts p
LEFT JOIN categories c ON p.category_id = c.id
WHERE p.deleted_at = 0
ORDER BY p.created_at DESC
LIMIT ? OFFSET ?
`
rows, err := config.DB.Query(query, pageSize, offset)
if err != nil {
log.Printf("Error querying all posts: %v", err)
return nil, 0, err
}
defer rows.Close()
var posts []models.Post
for rows.Next() {
var post models.Post
var catID sql.NullInt64
var catName sql.NullString
var catSlug sql.NullString
if err := rows.Scan(
&post.ID,
&post.Title,
&catID,
&catName,
&catSlug,
&post.Excerpt,
&post.Content,
&post.ReadCount,
&post.IsPublished,
&post.CreatedAt,
&post.UpdatedAt,
&post.DeletedAt,
); err != nil {
log.Printf("Error scanning post: %v", err)
continue
}
if catID.Valid {
post.CategoryID = uint(catID.Int64)
post.Category = &models.Category{
ID: uint(catID.Int64),
Name: catName.String,
Slug: catSlug.String,
}
}
posts = append(posts, post)
}
return posts, total, nil
}
// CreatePost 创建博客文章
func CreatePost(post *models.Post) error {
now := time.Now().Unix()
// Insert Post
query := `
INSERT INTO posts (title, category_id, excerpt, content, is_published, created_at, updated_at, deleted_at)
VALUES (?, ?, ?, ?, ?, ?, ?, 0)
`
result, err := config.DB.Exec(
query,
post.Title,
post.CategoryID,
post.Excerpt,
post.Content,
post.IsPublished,
now,
now,
)
if err != nil {
log.Printf("Error creating post: %v", err)
return err
}
id, err := result.LastInsertId()
if err != nil {
return err
}
post.ID = uint(id)
post.CreatedAt = now
post.UpdatedAt = now
// Insert Tags
if len(post.Tags) > 0 {
for _, tag := range post.Tags {
AddTagToPost(post.ID, tag.ID)
}
}
return nil
}
// UpdatePost 更新博客文章
func UpdatePost(post *models.Post) error {
now := time.Now().Unix()
query := `
UPDATE posts SET title = ?, category_id = ?, excerpt = ?, content = ?, is_published = ?, updated_at = ?
WHERE id = ? AND deleted_at = 0
`
_, err := config.DB.Exec(
query,
post.Title,
post.CategoryID,
post.Excerpt,
post.Content,
post.IsPublished,
now,
post.ID,
)
if err != nil {
log.Printf("Error updating post: %v", err)
return err
}
// Update Tags: Delete all and re-insert
// Note: This is a simple approach. Better approach is to diff.
config.DB.Exec("DELETE FROM post_tags WHERE post_id = ?", post.ID)
if len(post.Tags) > 0 {
for _, tag := range post.Tags {
AddTagToPost(post.ID, tag.ID)
}
}
return nil
}
// UpdatePostStatus 更新文章状态
func UpdatePostStatus(id uint, status int) error {
now := time.Now().Unix()
query := "UPDATE posts SET is_published = ?, updated_at = ? WHERE id = ? AND deleted_at = 0"
_, err := config.DB.Exec(query, status, now, id)
return err
}
// DeletePost 删除博客文章 (Soft Delete)
func DeletePost(id uint) error {
now := time.Now().Unix()
query := "UPDATE posts SET deleted_at = ? WHERE id = ?"
_, err := config.DB.Exec(query, now, id)
if err != nil {
log.Printf("Error deleting post: %v", err)
return err
}
return nil
}
// GetPostCount 获取文章总数
func GetPostCount() (int, error) {
var count int
query := "SELECT COUNT(*) FROM posts WHERE deleted_at = 0"
row := config.DB.QueryRow(query)
err := row.Scan(&count)
if err != nil {
log.Printf("Error getting post count: %v", err)
return 0, err
}
return 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
}
response := &models.PostResponse{
ID: post.ID,
Title: post.Title,
CategoryID: post.CategoryID,
CategoryName: catName,
CategorySlug: catSlug,
Date: dateStr,
Excerpt: post.Excerpt,
Tags: post.Tags,
}
if includeContent {
response.Content = post.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 uint
query := "SELECT COALESCE(MAX(version), 0) FROM post_history WHERE post_id = ?"
if err := config.DB.QueryRow(query, post.ID).Scan(&maxVersion); err != nil {
log.Printf("Error getting max version: %v", err)
return err
}
now := time.Now().Unix()
insertQuery := `
INSERT INTO post_history (
post_id, version, title, category_id, excerpt, content,
is_published, modified_by, modified_at, created_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`
_, err := config.DB.Exec(
insertQuery,
post.ID,
maxVersion+1,
post.Title,
post.CategoryID,
post.Excerpt,
post.Content,
post.IsPublished,
modifiedBy,
now,
now,
)
if err != nil {
log.Printf("Error saving post history: %v", err)
return err
}
return nil
}
// GetTopPosts 获取热门文章 (按阅读量)
func GetTopPosts(limit int) ([]models.Post, error) {
// Simple query without category join for dashboard to avoid complexity if not needed
// Or join if needed. Dashboard usually needs Title.
query := "SELECT id, title, read_count FROM posts WHERE is_published = 1 AND deleted_at = 0 ORDER BY read_count DESC LIMIT ?"
rows, err := config.DB.Query(query, limit)
if err != nil {
return nil, err
}
defer rows.Close()
var posts []models.Post
for rows.Next() {
var post models.Post
if err := rows.Scan(&post.ID, &post.Title, &post.ReadCount); err != nil {
continue
}
posts = append(posts, post)
}
return posts, nil
}
// GetNewPostsTrend 获取新增文章趋势
func GetNewPostsTrend(startDate, endDate string) ([]TrendData, error) {
// Same as before
query := `
SELECT FROM_UNIXTIME(created_at, '%Y-%m-%d') as date, COUNT(*) as count
FROM posts
WHERE deleted_at = 0
`
args := []interface{}{}
if startDate != "" {
startUnix := parseDateToUnix(startDate, false)
query += " AND created_at >= ?"
args = append(args, startUnix)
} else {
startUnix := time.Now().AddDate(0, 0, -6).Unix()
query += " AND created_at >= ?"
args = append(args, startUnix)
}
if endDate != "" {
endUnix := parseDateToUnix(endDate, true)
query += " AND created_at <= ?"
args = append(args, endUnix)
}
query += `
GROUP BY date
ORDER BY date ASC
`
rows, err := config.DB.Query(query, args...)
if err != nil {
return nil, err
}
defer rows.Close()
var results []TrendData
for rows.Next() {
var r TrendData
if err := rows.Scan(&r.Date, &r.Count); err != nil {
return nil, err
}
r.YoY = 0
r.MoM = 0
results = append(results, r)
}
return results, nil
}
// GetPostHistory 获取文章修改历史
func GetPostHistory(postID uint) ([]models.PostHistory, error) {
query := `
SELECT id, post_id, version, title, category_id, excerpt, content, is_published, modified_by, modified_at, created_at
FROM post_history
WHERE post_id = ?
ORDER BY version DESC
`
rows, err := config.DB.Query(query, postID)
if err != nil {
return nil, err
}
defer rows.Close()
var history []models.PostHistory
for rows.Next() {
var h models.PostHistory
if err := rows.Scan(
&h.ID, &h.PostID, &h.Version, &h.Title, &h.CategoryID,
&h.Excerpt, &h.Content, &h.IsPublished,
&h.ModifiedBy, &h.ModifiedAt, &h.CreatedAt,
); err != nil {
continue
}
history = append(history, h)
}
return history, nil
}
// GetPostHistoryByVersion 获取特定版本的历史记录
func GetPostHistoryByVersion(postID uint, version uint) (*models.PostHistory, error) {
query := `
SELECT id, post_id, version, title, category_id, excerpt, content, is_published, modified_by, modified_at, created_at
FROM post_history
WHERE post_id = ? AND version = ?
`
var h models.PostHistory
err := config.DB.QueryRow(query, postID, version).Scan(
&h.ID, &h.PostID, &h.Version, &h.Title, &h.CategoryID,
&h.Excerpt, &h.Content, &h.IsPublished,
&h.ModifiedBy, &h.ModifiedAt, &h.CreatedAt,
)
if err != nil {
return nil, err
}
return &h, nil
}
// BuildPostHistoryResponse 构建历史记录响应
func BuildPostHistoryResponse(h *models.PostHistory) *models.PostHistoryResponse {
return &models.PostHistoryResponse{
ID: h.ID,
PostID: h.PostID,
Version: h.Version,
Title: h.Title,
CategoryID: h.CategoryID,
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"),
}
}
// BuildPostHistoryResponses 构建历史记录列表响应
func BuildPostHistoryResponses(history []models.PostHistory) []models.PostHistoryResponse {
var responses []models.PostHistoryResponse
for _, h := range history {
responses = append(responses, *BuildPostHistoryResponse(&h))
}
return responses
}