BUG修复,返回结构统一
This commit is contained in:
@@ -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)
|
||||
|
||||
|
||||
144
server/repositories/category_repository.go
Normal file
144
server/repositories/category_repository.go
Normal 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
|
||||
}
|
||||
222
server/repositories/column_repository.go
Normal file
222
server/repositories/column_repository.go
Normal 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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 创建用户
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user