557 lines
13 KiB
Go
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
|
|
}
|