BUG修复,返回结构统一

This commit is contained in:
李琦
2026-01-16 17:03:34 +08:00
parent 87ed11db7a
commit e6edf7210b
57 changed files with 3642 additions and 1074 deletions

View File

@@ -4,12 +4,60 @@ import (
"database/sql"
"encoding/json"
"log"
"strconv"
"strings"
"time"
"github.com/niangaodev/art-code/config"
"github.com/niangaodev/art-code/models"
)
// unescapeJSONString 解码转义的 JSON 字符串
// 处理两种情况:
// 1. 被引号包裹的转义 JSON 字符串:`"[\"Vue 3\",...]"`
// 2. 包含转义引号的 JSON 字符串:`[\"Vue 3\",...]`
func unescapeJSONString(s string) (string, error) {
// 如果字符串以引号开头和结尾,说明是被引号包裹的转义 JSON 字符串
if len(s) >= 2 && s[0] == '"' && s[len(s)-1] == '"' {
unquoted, err := strconv.Unquote(s)
if err != nil {
return s, err
}
return unquoted, nil
}
// 如果字符串包含转义的引号 \",需要将其转换为普通引号
// 例如:[\"Vue 3\",...] -> ["Vue 3",...]
if len(s) > 0 {
// 尝试直接解析,如果失败则尝试替换转义引号
var test interface{}
if err := json.Unmarshal([]byte(s), &test); err != nil {
// 如果解析失败,尝试将 \" 替换为 "
unescaped := s
// 替换转义的反斜杠+引号
// 注意:这里需要小心处理,因为 \\" 应该变成 \"
// 但 \" 应该变成 "
// 使用正则表达式或字符串替换
// 简单方法:将 \" 替换为 "(但需要确保不会误替换 \\"
// 更安全的方法:使用 json.Unmarshal 两次解析
// 或者使用 strings.ReplaceAll 但需要小心
// 尝试将 \" 替换为 "
unescaped = strings.ReplaceAll(unescaped, `\"`, `"`)
// 如果替换后能解析,返回替换后的字符串
if err2 := json.Unmarshal([]byte(unescaped), &test); err2 == nil {
return unescaped, nil
}
} else {
// 如果能直接解析,返回原字符串
return s, nil
}
}
// 如果都失败,返回原字符串
return s, nil
}
// GetPrimaryAboutProfile 获取主页个人资料
func GetPrimaryAboutProfile() (*models.AboutProfile, error) {
query := `
@@ -44,17 +92,22 @@ func GetPrimaryAboutProfile() (*models.AboutProfile, error) {
return nil, err
}
// Unmarshal JSON
// Unmarshal JSON - 确保 TechList 和 ExperienceList 始终是数组而不是 nil
profile.TechList = []string{}
profile.ExperienceList = []models.Experience{}
if profile.TechStack != "" {
_ = json.Unmarshal([]byte(profile.TechStack), &profile.TechList)
} else {
profile.TechList = []string{}
if err := json.Unmarshal([]byte(profile.TechStack), &profile.TechList); err != nil {
log.Printf("Error unmarshaling techStack: %v, raw: %s", err, profile.TechStack)
profile.TechList = []string{}
}
}
if profile.ExperiencesStr != "" {
_ = json.Unmarshal([]byte(profile.ExperiencesStr), &profile.ExperienceList)
} else {
profile.ExperienceList = []models.Experience{}
if err := json.Unmarshal([]byte(profile.ExperiencesStr), &profile.ExperienceList); err != nil {
log.Printf("Error unmarshaling experiences: %v, raw: %s", err, profile.ExperiencesStr)
profile.ExperienceList = []models.Experience{}
}
}
return &profile, nil
@@ -94,16 +147,100 @@ func GetFirstAboutProfile() (*models.AboutProfile, error) {
return nil, err
}
// Unmarshal JSON - 确保 TechList 和 ExperienceList 始终是数组而不是 nil
profile.TechList = []string{}
profile.ExperienceList = []models.Experience{}
if profile.TechStack != "" {
_ = json.Unmarshal([]byte(profile.TechStack), &profile.TechList)
} else {
profile.TechList = []string{}
// 先尝试解码转义的 JSON 字符串
unescaped, err := unescapeJSONString(profile.TechStack)
if err != nil {
log.Printf("Error unescaping techStack: %v, raw: %s", err, profile.TechStack)
} else {
if err := json.Unmarshal([]byte(unescaped), &profile.TechList); err != nil {
log.Printf("Error unmarshaling techStack: %v, raw: %s, unescaped: %s", err, profile.TechStack, unescaped)
profile.TechList = []string{}
}
}
}
if profile.ExperiencesStr != "" {
_ = json.Unmarshal([]byte(profile.ExperiencesStr), &profile.ExperienceList)
} else {
profile.ExperienceList = []models.Experience{}
// 先尝试解码转义的 JSON 字符串
unescaped, err := unescapeJSONString(profile.ExperiencesStr)
if err != nil {
log.Printf("Error unescaping experiences: %v, raw: %s", err, profile.ExperiencesStr)
} else {
if err := json.Unmarshal([]byte(unescaped), &profile.ExperienceList); err != nil {
log.Printf("Error unmarshaling experiences: %v, raw: %s, unescaped: %s", err, profile.ExperiencesStr, unescaped)
profile.ExperienceList = []models.Experience{}
}
}
}
return &profile, nil
}
// GetAboutProfileByID 根据 ID 获取个人资料
func GetAboutProfileByID(id uint) (*models.AboutProfile, error) {
query := `
SELECT id, name, avatar, location, bio, email, wechat, tech_stack, experiences, is_primary, created_at, updated_at, deleted_at
FROM about_profiles
WHERE id = ? AND deleted_at = 0
LIMIT 1
`
row := config.DB.QueryRow(query, id)
var profile models.AboutProfile
if err := row.Scan(
&profile.ID,
&profile.Name,
&profile.Avatar,
&profile.Location,
&profile.Bio,
&profile.Email,
&profile.Wechat,
&profile.TechStack,
&profile.ExperiencesStr,
&profile.IsPrimary,
&profile.CreatedAt,
&profile.UpdatedAt,
&profile.DeletedAt,
); err != nil {
if err == sql.ErrNoRows {
return nil, nil
}
log.Printf("Error scanning about profile by ID: %v", err)
return nil, err
}
// Unmarshal JSON - 确保 TechList 和 ExperienceList 始终是数组而不是 nil
profile.TechList = []string{}
profile.ExperienceList = []models.Experience{}
if profile.TechStack != "" {
// 先尝试解码转义的 JSON 字符串
unescaped, err := unescapeJSONString(profile.TechStack)
if err != nil {
log.Printf("Error unescaping techStack: %v, raw: %s", err, profile.TechStack)
} else {
if err := json.Unmarshal([]byte(unescaped), &profile.TechList); err != nil {
log.Printf("Error unmarshaling techStack: %v, raw: %s, unescaped: %s", err, profile.TechStack, unescaped)
profile.TechList = []string{}
}
}
}
if profile.ExperiencesStr != "" {
// 先尝试解码转义的 JSON 字符串
unescaped, err := unescapeJSONString(profile.ExperiencesStr)
if err != nil {
log.Printf("Error unescaping experiences: %v, raw: %s", err, profile.ExperiencesStr)
} else {
if err := json.Unmarshal([]byte(unescaped), &profile.ExperienceList); err != nil {
log.Printf("Error unmarshaling experiences: %v, raw: %s, unescaped: %s", err, profile.ExperiencesStr, unescaped)
profile.ExperienceList = []models.Experience{}
}
}
}
return &profile, nil
@@ -139,15 +276,34 @@ func GetAllAboutProfiles() ([]models.AboutProfile, error) {
); err != nil {
continue
}
// Unmarshal JSON - 确保 TechList 和 ExperienceList 始终是数组而不是 nil
p.TechList = []string{}
p.ExperienceList = []models.Experience{}
if p.TechStack != "" {
_ = json.Unmarshal([]byte(p.TechStack), &p.TechList)
} else {
p.TechList = []string{}
// 先尝试解码转义的 JSON 字符串
unescaped, err := unescapeJSONString(p.TechStack)
if err != nil {
log.Printf("Error unescaping techStack: %v, raw: %s", err, p.TechStack)
} else {
if err := json.Unmarshal([]byte(unescaped), &p.TechList); err != nil {
log.Printf("Error unmarshaling techStack: %v, raw: %s, unescaped: %s", err, p.TechStack, unescaped)
p.TechList = []string{}
}
}
}
if p.ExperiencesStr != "" {
_ = json.Unmarshal([]byte(p.ExperiencesStr), &p.ExperienceList)
} else {
p.ExperienceList = []models.Experience{}
// 先尝试解码转义的 JSON 字符串
unescaped, err := unescapeJSONString(p.ExperiencesStr)
if err != nil {
log.Printf("Error unescaping experiences: %v, raw: %s", err, p.ExperiencesStr)
} else {
if err := json.Unmarshal([]byte(unescaped), &p.ExperienceList); err != nil {
log.Printf("Error unmarshaling experiences: %v, raw: %s, unescaped: %s", err, p.ExperiencesStr, unescaped)
p.ExperienceList = []models.Experience{}
}
}
}
profiles = append(profiles, p)
}
@@ -156,6 +312,14 @@ func GetAllAboutProfiles() ([]models.AboutProfile, error) {
// CreateAboutProfile 创建个人资料
func CreateAboutProfile(profile *models.AboutProfile) error {
// 确保 TechList 和 ExperienceList 不为 nil
if profile.TechList == nil {
profile.TechList = []string{}
}
if profile.ExperienceList == nil {
profile.ExperienceList = []models.Experience{}
}
// Marshal JSON
techBytes, _ := json.Marshal(profile.TechList)
profile.TechStack = string(techBytes)
@@ -194,11 +358,24 @@ func CreateAboutProfile(profile *models.AboutProfile) error {
profile.ID = uint(id)
profile.CreatedAt = now
profile.UpdatedAt = now
// 确保返回的数据包含 TechList 和 ExperienceList已从 JSON 解析)
// 这些字段已经在上面被 Marshal 了,现在需要确保它们被正确设置
// 由于我们已经 Marshal 了TechList 和 ExperienceList 应该保持原样
return nil
}
// UpdateAboutProfile 更新个人资料
func UpdateAboutProfile(profile *models.AboutProfile) error {
// 确保 TechList 和 ExperienceList 不为 nil
if profile.TechList == nil {
profile.TechList = []string{}
}
if profile.ExperienceList == nil {
profile.ExperienceList = []models.Experience{}
}
// Marshal JSON
techBytes, _ := json.Marshal(profile.TechList)
profile.TechStack = string(techBytes)

View File

@@ -0,0 +1,144 @@
package repositories
import (
"database/sql"
"log"
"time"
"github.com/niangaodev/art-code/config"
"github.com/niangaodev/art-code/models"
)
// GetCategories 获取所有分类
func GetCategories() ([]models.Category, error) {
query := "SELECT id, name, slug, description, sort_order, created_at, updated_at, deleted_at FROM categories WHERE deleted_at = 0 ORDER BY sort_order ASC, created_at DESC"
rows, err := config.DB.Query(query)
if err != nil {
log.Printf("Error querying categories: %v", err)
return nil, err
}
defer rows.Close()
var categories []models.Category
for rows.Next() {
var category models.Category
var description sql.NullString // Use NullString for nullable column
if err := rows.Scan(
&category.ID,
&category.Name,
&category.Slug,
&description, // Scan into NullString
&category.SortOrder,
&category.CreatedAt,
&category.UpdatedAt,
&category.DeletedAt,
); err != nil {
log.Printf("Error scanning category: %v", err)
continue
}
if description.Valid {
category.Description = description.String
}
categories = append(categories, category)
}
return categories, nil
}
// GetCategoryByID 根据ID获取分类
func GetCategoryByID(id uint) (*models.Category, error) {
query := "SELECT id, name, slug, description, sort_order, created_at, updated_at, deleted_at FROM categories WHERE id = ? AND deleted_at = 0"
row := config.DB.QueryRow(query, id)
var category models.Category
var description sql.NullString // Use NullString for nullable column
if err := row.Scan(
&category.ID,
&category.Name,
&category.Slug,
&description, // Scan into NullString
&category.SortOrder,
&category.CreatedAt,
&category.UpdatedAt,
&category.DeletedAt,
); err != nil {
if err == sql.ErrNoRows {
return nil, nil
}
log.Printf("Error scanning category by ID: %v", err)
return nil, err
}
if description.Valid {
category.Description = description.String
}
return &category, nil
}
// CreateCategory 创建分类
func CreateCategory(category *models.Category) error {
now := time.Now().Unix()
query := `
INSERT INTO categories (name, slug, description, sort_order, created_at, updated_at, deleted_at)
VALUES (?, ?, ?, ?, ?, ?, 0)
`
result, err := config.DB.Exec(
query,
category.Name,
category.Slug,
category.Description,
category.SortOrder,
now,
now,
)
if err != nil {
log.Printf("Error creating category: %v", err)
return err
}
id, err := result.LastInsertId()
if err != nil {
return err
}
category.ID = uint(id)
category.CreatedAt = now
category.UpdatedAt = now
return nil
}
// UpdateCategory 更新分类
func UpdateCategory(category *models.Category) error {
now := time.Now().Unix()
query := `
UPDATE categories SET name = ?, slug = ?, description = ?, sort_order = ?, updated_at = ?
WHERE id = ? AND deleted_at = 0
`
_, err := config.DB.Exec(
query,
category.Name,
category.Slug,
category.Description,
category.SortOrder,
now,
category.ID,
)
if err != nil {
log.Printf("Error updating category: %v", err)
return err
}
return nil
}
// DeleteCategory 删除分类
func DeleteCategory(id uint) error {
now := time.Now().Unix()
query := "UPDATE categories SET deleted_at = ? WHERE id = ?"
_, err := config.DB.Exec(query, now, id)
if err != nil {
log.Printf("Error deleting category: %v", err)
return err
}
return nil
}

View File

@@ -0,0 +1,222 @@
package repositories
import (
"database/sql"
"log"
"time"
"github.com/niangaodev/art-code/config"
"github.com/niangaodev/art-code/models"
)
// GetColumns 获取所有专栏
func GetColumns() ([]models.Column, error) {
query := "SELECT id, name, description, cover, is_active, sort_order, created_at, updated_at, deleted_at FROM columns WHERE deleted_at = 0 ORDER BY sort_order ASC, created_at DESC"
rows, err := config.DB.Query(query)
if err != nil {
log.Printf("Error querying columns: %v", err)
return nil, err
}
defer rows.Close()
var columns []models.Column
for rows.Next() {
var col models.Column
var description sql.NullString // Use NullString
var cover sql.NullString // Use NullString
if err := rows.Scan(
&col.ID,
&col.Name,
&description,
&cover,
&col.IsActive,
&col.SortOrder,
&col.CreatedAt,
&col.UpdatedAt,
&col.DeletedAt,
); err != nil {
log.Printf("Error scanning column: %v", err)
continue
}
if description.Valid {
col.Description = description.String
}
if cover.Valid {
col.Cover = cover.String
}
columns = append(columns, col)
}
return columns, nil
}
// GetColumnByID 根据ID获取专栏
func GetColumnByID(id uint) (*models.Column, error) {
query := "SELECT id, name, description, cover, is_active, sort_order, created_at, updated_at, deleted_at FROM columns WHERE id = ? AND deleted_at = 0"
row := config.DB.QueryRow(query, id)
var col models.Column
var description sql.NullString // Use NullString
var cover sql.NullString // Use NullString
if err := row.Scan(
&col.ID,
&col.Name,
&description,
&cover,
&col.IsActive,
&col.SortOrder,
&col.CreatedAt,
&col.UpdatedAt,
&col.DeletedAt,
); err != nil {
if err == sql.ErrNoRows {
return nil, nil
}
log.Printf("Error scanning column by ID: %v", err)
return nil, err
}
if description.Valid {
col.Description = description.String
}
if cover.Valid {
col.Cover = cover.String
}
return &col, nil
}
// CreateColumn 创建专栏
func CreateColumn(col *models.Column) error {
now := time.Now().Unix()
query := `
INSERT INTO columns (name, description, cover, is_active, sort_order, created_at, updated_at, deleted_at)
VALUES (?, ?, ?, ?, ?, ?, ?, 0)
`
result, err := config.DB.Exec(
query,
col.Name,
col.Description,
col.Cover,
col.IsActive,
col.SortOrder,
now,
now,
)
if err != nil {
log.Printf("Error creating column: %v", err)
return err
}
id, err := result.LastInsertId()
if err != nil {
return err
}
col.ID = uint(id)
col.CreatedAt = now
col.UpdatedAt = now
return nil
}
// UpdateColumn 更新专栏
func UpdateColumn(col *models.Column) error {
now := time.Now().Unix()
query := `
UPDATE columns SET name = ?, description = ?, cover = ?, is_active = ?, sort_order = ?, updated_at = ?
WHERE id = ? AND deleted_at = 0
`
_, err := config.DB.Exec(
query,
col.Name,
col.Description,
col.Cover,
col.IsActive,
col.SortOrder,
now,
col.ID,
)
if err != nil {
log.Printf("Error updating column: %v", err)
return err
}
return nil
}
// DeleteColumn 删除专栏
func DeleteColumn(id uint) error {
now := time.Now().Unix()
query := "UPDATE columns SET deleted_at = ? WHERE id = ?"
_, err := config.DB.Exec(query, now, id)
if err != nil {
log.Printf("Error deleting column: %v", err)
return err
}
return nil
}
// GetPostsByColumnID 获取专栏下的文章
func GetPostsByColumnID(columnID uint) ([]models.Post, error) {
query := `
SELECT p.id, p.title, p.category_id, c.name as category_name, p.excerpt, p.content, p.read_count, p.is_published, p.created_at, p.updated_at, p.deleted_at
FROM posts p
JOIN column_posts cp ON p.id = cp.post_id
LEFT JOIN categories c ON p.category_id = c.id
WHERE cp.column_id = ? AND p.deleted_at = 0 AND p.is_published = 1
ORDER BY cp.sort_order ASC, p.created_at DESC
`
rows, err := config.DB.Query(query, columnID)
if err != nil {
log.Printf("Error querying posts by column ID: %v", err)
return nil, err
}
defer rows.Close()
var posts []models.Post
for rows.Next() {
var post models.Post
var categoryName sql.NullString
if err := rows.Scan(
&post.ID,
&post.Title,
&post.CategoryID,
&categoryName,
&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 categoryName.Valid {
post.Category = &models.Category{ID: post.CategoryID, Name: categoryName.String}
}
posts = append(posts, post)
}
return posts, nil
}
// AddPostToColumn 添加文章到专栏
func AddPostToColumn(columnID, postID, sortOrder uint) error {
now := time.Now().Unix()
// Check if exists first to avoid duplicates or use INSERT IGNORE/REPLACE if simple
// Assuming unique key on (column_id, post_id)
query := `
INSERT INTO column_posts (column_id, post_id, sort_order, created_at)
VALUES (?, ?, ?, ?)
ON DUPLICATE KEY UPDATE sort_order = VALUES(sort_order)
`
_, err := config.DB.Exec(query, columnID, postID, sortOrder, now)
return err
}
// RemovePostFromColumn 从专栏移除文章
func RemovePostFromColumn(columnID, postID uint) error {
query := "DELETE FROM column_posts WHERE column_id = ? AND post_id = ?"
_, err := config.DB.Exec(query, columnID, postID)
return err
}

View File

@@ -9,34 +9,49 @@ import (
"github.com/niangaodev/art-code/models"
)
// GetPosts 获取所有博客文章(支持搜索)
func GetPosts(keyword string) ([]models.Post, error) {
var rows *sql.Rows
var err error
// TrendData 趋势数据
type TrendData struct {
Date string `json:"date"`
Count int `json:"value"`
YoY float64 `json:"yoy"`
MoM float64 `json:"mom"`
}
// Common select fields (removed date)
selectFields := "id, title, category, excerpt, content, read_count, is_published, created_at, updated_at, deleted_at"
// 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
`
if keyword != "" {
// 使用全文搜索
query := `
SELECT ` + selectFields + `
FROM posts
WHERE is_published = 1 AND deleted_at = 0 AND (
MATCH(title, content) AGAINST(? IN BOOLEAN MODE) OR
title LIKE ? OR
content LIKE ?
)
ORDER BY created_at DESC
`
likeKeyword := "%" + keyword + "%"
rows, err = config.DB.Query(query, keyword, likeKeyword, likeKeyword)
} else {
// 默认查询
query := "SELECT " + selectFields + " FROM posts WHERE is_published = 1 AND deleted_at = 0 ORDER BY created_at DESC"
rows, err = config.DB.Query(query)
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
@@ -46,10 +61,16 @@ func GetPosts(keyword string) ([]models.Post, error) {
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,
&post.Category,
&catID,
&catName,
&catSlug,
&post.Excerpt,
&post.Content,
&post.ReadCount,
@@ -61,6 +82,18 @@ func GetPosts(keyword string) ([]models.Post, error) {
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)
}
@@ -69,15 +102,25 @@ func GetPosts(keyword string) ([]models.Post, error) {
// GetPostByID 根据ID获取博客文章
func GetPostByID(id uint) (*models.Post, error) {
selectFields := "id, title, category, excerpt, content, read_count, is_published, created_at, updated_at, deleted_at"
query := "SELECT " + selectFields + " FROM posts WHERE id = ? AND is_published = 1 AND deleted_at = 0"
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,
&post.Category,
&catID,
&catName,
&catSlug,
&post.Excerpt,
&post.Content,
&post.ReadCount,
@@ -93,6 +136,21 @@ func GetPostByID(id uint) (*models.Post, error) {
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 {
@@ -102,24 +160,42 @@ func GetPostByID(id uint) (*models.Post, error) {
return &post, nil
}
// GetAllPosts 获取所有博客文章(包括未发布的)
func GetAllPosts() ([]models.Post, error) {
selectFields := "id, title, category, excerpt, content, read_count, is_published, created_at, updated_at, deleted_at"
query := "SELECT " + selectFields + " FROM posts WHERE deleted_at = 0 ORDER BY created_at DESC"
rows, err := config.DB.Query(query)
// 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, 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,
&post.Category,
&catID,
&catName,
&catSlug,
&post.Excerpt,
&post.Content,
&post.ReadCount,
@@ -131,23 +207,34 @@ func GetAllPosts() ([]models.Post, error) {
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, nil
return posts, total, nil
}
// CreatePost 创建博客文章
func CreatePost(post *models.Post) error {
now := time.Now().Unix()
// Insert Post
query := `
INSERT INTO posts (title, category, excerpt, content, is_published, created_at, updated_at, deleted_at)
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.Category,
post.CategoryID,
post.Excerpt,
post.Content,
post.IsPublished,
@@ -167,6 +254,13 @@ func CreatePost(post *models.Post) error {
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
}
@@ -174,13 +268,13 @@ func CreatePost(post *models.Post) error {
func UpdatePost(post *models.Post) error {
now := time.Now().Unix()
query := `
UPDATE posts SET title = ?, category = ?, excerpt = ?, content = ?, is_published = ?, updated_at = ?
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.Category,
post.CategoryID,
post.Excerpt,
post.Content,
post.IsPublished,
@@ -192,9 +286,26 @@ func UpdatePost(post *models.Post) error {
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()
@@ -204,7 +315,6 @@ func DeletePost(id uint) error {
log.Printf("Error deleting post: %v", err)
return err
}
return nil
}
@@ -228,12 +338,22 @@ func BuildPostResponse(post *models.Post, includeContent bool) *models.PostRespo
// 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,
Category: post.Category,
Date: dateStr,
Excerpt: post.Excerpt,
ID: post.ID,
Title: post.Title,
CategoryID: post.CategoryID,
CategoryName: catName,
CategorySlug: catSlug,
Date: dateStr,
Excerpt: post.Excerpt,
Tags: post.Tags,
}
if includeContent {
@@ -263,48 +383,18 @@ func SavePostHistory(post *models.Post, modifiedBy uint) error {
}
now := time.Now().Unix()
// 插入新的历史记录 (PostHistory struct updated to int64)
// Note: post_history table also needs to be updated to support bigint timestamps if not already.
// Assuming user wanted ALL tables updated, but I missed checking post_history structure explicitly in sql file scan.
// But assuming I applied "all tables" logic if it existed.
// Wait, post_history wasn't in the SQL file dump I read earlier?
// I will double check. If it's missing, I might get errors.
// The SQL dump showed `posts`, `users` etc. `post_history` was NOT in the dump I read?
// Let me check the Read output again.
// It wasn't there! `post_tags` was there. `post_history` is missing from the SQL dump provided by the user?
// Or maybe I missed it.
// If it doesn't exist, this code will fail.
// But `GetPostHistory` exists in the repo, so the table MUST exist.
// I will assume it exists and uses the same convention.
// PostHistory model has Date string?
// Check models/post.go:
// type PostHistory struct { ... Date string ... }
// The struct I updated earlier removed Date?
// No, I checked PostHistory in models/post.go, it had Date string.
// And I updated it to:
// Date string (removed?)
// Let's check my model update for PostHistory.
// I removed `Date string` from PostHistory?
// `type PostHistory struct { ... Title string; Category string; Excerpt string ... }`
// Yes, I removed Date.
// So I should remove `date` from Insert too.
insertQuery := `
INSERT INTO post_history (
post_id, version, title, category, excerpt, content,
post_id, version, title, category_id, excerpt, content,
is_published, modified_by, modified_at, created_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`
// Date string generation? Post history usually snapshots the post state.
// If post has no date column, history shouldn't either.
_, err := config.DB.Exec(
insertQuery,
post.ID,
maxVersion+1,
post.Title,
post.Category,
post.CategoryID,
post.Excerpt,
post.Content,
post.IsPublished,
@@ -320,118 +410,31 @@ func SavePostHistory(post *models.Post, modifiedBy uint) error {
return nil
}
// GetPostHistory 获取文章历史记录
func GetPostHistory(postID uint) ([]models.PostHistory, error) {
query := `
SELECT id, post_id, version, title, category, 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)
// 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 {
log.Printf("Error querying post history: %v", err)
return nil, err
}
defer rows.Close()
var history []models.PostHistory
var posts []models.Post
for rows.Next() {
var h models.PostHistory
if err := rows.Scan(
&h.ID,
&h.PostID,
&h.Version,
&h.Title,
&h.Category,
&h.Excerpt,
&h.Content,
&h.IsPublished,
&h.ModifiedBy,
&h.ModifiedAt,
&h.CreatedAt,
); err != nil {
log.Printf("Error scanning post history: %v", err)
var post models.Post
if err := rows.Scan(&post.ID, &post.Title, &post.ReadCount); err != nil {
continue
}
history = append(history, h)
posts = append(posts, post)
}
return history, nil
return posts, nil
}
// GetPostHistoryByVersion 获取指定版本的文章历史记录
func GetPostHistoryByVersion(postID uint, version uint) (*models.PostHistory, error) {
query := `
SELECT id, post_id, version, title, category, excerpt, content,
is_published, modified_by, modified_at, created_at
FROM post_history
WHERE post_id = ? AND version = ?
`
row := config.DB.QueryRow(query, postID, version)
var h models.PostHistory
if err := row.Scan(
&h.ID,
&h.PostID,
&h.Version,
&h.Title,
&h.Category,
&h.Excerpt,
&h.Content,
&h.IsPublished,
&h.ModifiedBy,
&h.ModifiedAt,
&h.CreatedAt,
); err != nil {
if err == sql.ErrNoRows {
return nil, nil
}
log.Printf("Error scanning post history by version: %v", err)
return nil, err
}
return &h, nil
}
// BuildPostHistoryResponse 构建文章历史记录响应
func BuildPostHistoryResponse(history *models.PostHistory) *models.PostHistoryResponse {
return &models.PostHistoryResponse{
ID: history.ID,
PostID: history.PostID,
Version: history.Version,
Title: history.Title,
Category: history.Category,
Date: time.Unix(history.CreatedAt, 0).Format("2006-01-02"), // Compute date
IsPublished: history.IsPublished,
ModifiedBy: history.ModifiedBy,
ModifiedAt: time.Unix(history.ModifiedAt, 0).Format("2006-01-02 15:04:05"),
CreatedAt: time.Unix(history.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
}
// TrendData 趋势数据结构
type TrendData struct {
Date string `json:"date"`
Count int `json:"value"`
YoY float64 `json:"yoy"` // Year-over-Year 同比
MoM float64 `json:"mom"` // Month-over-Month 环比
}
// GetNewPostsTrend 获取新增文章趋势 (带同比环比)
// 支持按日/周/月/年维度统计
// GetNewPostsTrend 获取新增文章趋势
func GetNewPostsTrend(startDate, endDate string) ([]TrendData, error) {
// Use FROM_UNIXTIME to format timestamp
// Same as before
query := `
SELECT FROM_UNIXTIME(created_at, '%Y-%m-%d') as date, COUNT(*) as count
FROM posts
@@ -444,7 +447,6 @@ func GetNewPostsTrend(startDate, endDate string) ([]TrendData, error) {
query += " AND created_at >= ?"
args = append(args, startUnix)
} else {
// Default 7 days
startUnix := time.Now().AddDate(0, 0, -6).Unix()
query += " AND created_at >= ?"
args = append(args, startUnix)
@@ -473,7 +475,6 @@ func GetNewPostsTrend(startDate, endDate string) ([]TrendData, error) {
if err := rows.Scan(&r.Date, &r.Count); err != nil {
return nil, err
}
// 暂未实现真实的同比环比计算逻辑设为0
r.YoY = 0
r.MoM = 0
results = append(results, r)
@@ -481,34 +482,75 @@ func GetNewPostsTrend(startDate, endDate string) ([]TrendData, error) {
return results, nil
}
// GetTopPosts 获取热门文章 (按阅读量)
func GetTopPosts(limit int) ([]models.Post, error) {
selectFields := "id, title, category, excerpt, content, read_count, is_published, created_at, updated_at, deleted_at"
query := "SELECT " + selectFields + " FROM posts WHERE is_published = 1 AND deleted_at = 0 ORDER BY read_count DESC LIMIT ?"
rows, err := config.DB.Query(query, limit)
// 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 posts []models.Post
var history []models.PostHistory
for rows.Next() {
var post models.Post
var h models.PostHistory
if err := rows.Scan(
&post.ID,
&post.Title,
&post.Category,
&post.Excerpt,
&post.Content,
&post.ReadCount,
&post.IsPublished,
&post.CreatedAt,
&post.UpdatedAt,
&post.DeletedAt,
&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
}
posts = append(posts, post)
history = append(history, h)
}
return posts, nil
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
}

View File

@@ -145,11 +145,38 @@ func BuildSettingResponse(setting *models.Setting) *models.SettingResponse {
}
}
// BuildSettingsResponse 构建系统配置列表响应
func BuildSettingsResponse(settings []models.Setting) []models.SettingResponse {
var responses []models.SettingResponse
for _, setting := range settings {
responses = append(responses, *BuildSettingResponse(&setting))
// GetAllSettings 获取所有系统配置 (Map format for easier consumption)
func GetAllSettings() (map[string]string, error) {
settings, err := GetSettings()
if err != nil {
return nil, err
}
return responses
result := make(map[string]string)
for _, s := range settings {
result[s.KeyName] = s.Value
}
return result, nil
}
// UpdateSettings 批量更新系统配置
func UpdateSettings(settings map[string]string) error {
tx, err := config.DB.Begin()
if err != nil {
return err
}
now := time.Now().Unix()
query := "UPDATE settings SET value = ?, updated_at = ? WHERE key_name = ? AND deleted_at = 0"
for key, value := range settings {
_, err := tx.Exec(query, value, now, key)
if err != nil {
tx.Rollback()
log.Printf("Error updating setting %s: %v", key, err)
return err
}
}
return tx.Commit()
}

View File

@@ -156,17 +156,53 @@ func DeleteSnippet(id string) error {
return nil
}
// GetSnippetCount 获取代码片段总数
func GetSnippetCount() (int, error) {
var count int
query := "SELECT COUNT(*) FROM snippets WHERE deleted_at = 0"
row := config.DB.QueryRow(query)
// GetAdminSnippets 获取后台代码片段列表 (分页)
func GetAdminSnippets(page, pageSize int) ([]models.Snippet, int, error) {
offset := (page - 1) * pageSize
err := row.Scan(&count)
// 获取总数
var total int
countQuery := "SELECT COUNT(*) FROM snippets WHERE deleted_at = 0"
err := config.DB.QueryRow(countQuery).Scan(&total)
if err != nil {
log.Printf("Error getting snippet count: %v", err)
return 0, err
return nil, 0, err
}
return count, nil
// 获取列表
query := `
SELECT id, title, code, type, description, view_count, created_at, updated_at, deleted_at
FROM snippets
WHERE deleted_at = 0
ORDER BY created_at DESC
LIMIT ? OFFSET ?
`
rows, err := config.DB.Query(query, pageSize, offset)
if err != nil {
log.Printf("Error querying admin snippets: %v", err)
return nil, 0, err
}
defer rows.Close()
var snippets []models.Snippet
for rows.Next() {
var snippet models.Snippet
if err := rows.Scan(
&snippet.ID,
&snippet.Title,
&snippet.Code,
&snippet.Type,
&snippet.Description,
&snippet.ViewCount,
&snippet.CreatedAt,
&snippet.UpdatedAt,
&snippet.DeletedAt,
); err != nil {
log.Printf("Error scanning snippet: %v", err)
continue
}
snippets = append(snippets, snippet)
}
return snippets, total, nil
}

View File

@@ -174,7 +174,7 @@ func DeleteTag(id uint) error {
}
// GetTagsByPostID 根据文章ID获取标签
func GetTagsByPostID(postID string) ([]models.Tag, error) {
func GetTagsByPostID(postID uint) ([]models.Tag, error) {
query := `
SELECT t.id, t.name, t.slug, t.created_at, t.updated_at, t.deleted_at
FROM tags t
@@ -210,7 +210,7 @@ func GetTagsByPostID(postID string) ([]models.Tag, error) {
}
// AddTagToPost 为文章添加标签
func AddTagToPost(postID string, tagID uint) error {
func AddTagToPost(postID uint, tagID uint) error {
now := time.Now().Unix()
query := `
INSERT IGNORE INTO post_tags (post_id, tag_id, created_at)
@@ -226,7 +226,7 @@ func AddTagToPost(postID string, tagID uint) error {
}
// RemoveTagFromPost 从文章移除标签
func RemoveTagFromPost(postID string, tagID uint) error {
func RemoveTagFromPost(postID uint, tagID uint) error {
query := "DELETE FROM post_tags WHERE post_id = ? AND tag_id = ?"
_, err := config.DB.Exec(query, postID, tagID)
if err != nil {

View File

@@ -95,19 +95,31 @@ func GetUserByID(id uint) (*models.User, error) {
return &user, nil
}
// GetUsers 获取所有用户
func GetUsers() ([]models.User, error) {
// GetUsers 获取所有用户 (分页)
func GetUsers(page, pageSize int) ([]models.User, int, error) {
offset := (page - 1) * pageSize
// 获取总数
var total int
countQuery := "SELECT COUNT(*) FROM users WHERE deleted_at = 0"
err := config.DB.QueryRow(countQuery).Scan(&total)
if err != nil {
log.Printf("Error getting user count: %v", err)
return nil, 0, err
}
query := `
SELECT u.id, u.username, u.email, u.password_hash, u.role_id, COALESCE(r.name, u.role), u.is_active, u.created_at, u.updated_at, u.deleted_at
FROM users u
LEFT JOIN roles r ON u.role_id = r.id
WHERE u.deleted_at = 0
ORDER BY u.created_at DESC
LIMIT ? OFFSET ?
`
rows, err := config.DB.Query(query)
rows, err := config.DB.Query(query, pageSize, offset)
if err != nil {
log.Printf("Error querying users: %v", err)
return nil, err
return nil, 0, err
}
defer rows.Close()
@@ -143,7 +155,7 @@ func GetUsers() ([]models.User, error) {
users = append(users, user)
}
return users, nil
return users, total, nil
}
// CreateUser 创建用户

View File

@@ -298,6 +298,74 @@ func DeleteWork(id string) error {
return nil
}
// GetAdminWorks 获取后台作品列表 (分页)
func GetAdminWorks(page, pageSize int) ([]models.Work, int, error) {
offset := (page - 1) * pageSize
// 获取总数
var total int
countQuery := "SELECT COUNT(*) FROM works WHERE deleted_at = 0"
err := config.DB.QueryRow(countQuery).Scan(&total)
if err != nil {
log.Printf("Error getting work count: %v", err)
return nil, 0, err
}
query := `
SELECT id, title, category, year, hero_img, description, is_featured, created_at, updated_at, deleted_at
FROM works
WHERE deleted_at = 0
ORDER BY created_at DESC
LIMIT ? OFFSET ?
`
rows, err := config.DB.Query(query, pageSize, offset)
if err != nil {
log.Printf("Error querying admin works: %v", err)
return nil, 0, err
}
defer rows.Close()
var works []models.Work
for rows.Next() {
var work models.Work
if err := rows.Scan(
&work.ID,
&work.Title,
&work.Category,
&work.Year,
&work.HeroImg,
&work.Description,
&work.IsFeatured,
&work.CreatedAt,
&work.UpdatedAt,
&work.DeletedAt,
); err != nil {
log.Printf("Error scanning work: %v", err)
continue
}
works = append(works, work)
}
return works, total, nil
}
// BuildWorksResponse 构建作品列表响应
func BuildWorksResponse(works []models.Work) []models.WorkResponse {
var responses []models.WorkResponse
for _, work := range works {
// 这里不包含详情,简化处理
responses = append(responses, models.WorkResponse{
ID: work.ID,
Title: work.Title,
Category: work.Category,
Year: work.Year,
HeroImg: work.HeroImg,
Desc: work.Description,
})
}
return responses
}
// GetWorkCount 获取作品总数
func GetWorkCount() (int, error) {
var count int