数据结构优化

This commit is contained in:
李琦
2026-01-16 13:01:21 +08:00
parent 7271199bef
commit 08953731ad
31 changed files with 1159 additions and 886 deletions

View File

@@ -7,6 +7,7 @@ require (
github.com/go-sql-driver/mysql v1.9.3
github.com/golang-jwt/jwt/v5 v5.3.0
github.com/google/uuid v1.6.0
github.com/lionsoul2014/ip2region/binding/golang v0.0.0-20260109033043-398149f17e54
golang.org/x/crypto v0.40.0
)
@@ -25,7 +26,6 @@ require (
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/lionsoul2014/ip2region/binding/golang v0.0.0-20260109033043-398149f17e54 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect

View File

@@ -34,45 +34,55 @@ func GetDashboardStats(c *gin.Context) {
}
// 4. 热门文章 Top 5
// 优先使用访问日志统计,如果没有则回退到posts表的read_count
// 优先使用访问日志统计,不足5条则用posts表的read_count补齐
topPosts, err := repositories.GetTopArticlesByAccess(5)
if err != nil || len(topPosts) == 0 {
// Fallback to post.read_count
dbPosts, err := repositories.GetTopPosts(5)
if err != nil {
topPosts = []struct {
ArticleID int `json:"article_id"`
Title string `json:"title"`
Count int `json:"count"`
}{}
}
if len(topPosts) < 5 {
// Fetch more than needed to ensure we find unique ones, or just fetch top 5
dbPosts, err := repositories.GetTopPosts(10)
if err == nil {
// Convert models.Post to the struct structure
// Create a temporary structure slice
topPosts = make([]struct {
ArticleID int `json:"article_id"`
Title string `json:"title"`
Count int `json:"count"`
}, 0)
// Create a map of existing IDs to avoid duplicates
existingIDs := make(map[int]bool)
for _, p := range topPosts {
existingIDs[p.ArticleID] = true
}
for _, p := range dbPosts {
topPosts = append(topPosts, struct {
ArticleID int `json:"article_id"`
Title string `json:"title"`
Count int `json:"count"`
}{
ArticleID: int(p.ID),
Title: p.Title,
Count: int(p.ReadCount),
})
if len(topPosts) >= 5 {
break
}
if !existingIDs[int(p.ID)] {
topPosts = append(topPosts, struct {
ArticleID int `json:"article_id"`
Title string `json:"title"`
Count int `json:"count"`
}{
ArticleID: int(p.ID),
Title: p.Title,
Count: int(p.ReadCount),
})
existingIDs[int(p.ID)] = true
}
}
}
}
// 5. 合作咨询总数
var inquiryCount int
config.DB.QueryRow("SELECT COUNT(*) FROM inquiries").Scan(&inquiryCount)
config.DB.QueryRow("SELECT COUNT(*) FROM inquiries WHERE deleted_at = 0").Scan(&inquiryCount)
// 6. 作品总数
var workCount int
// 假设 works 表存在,如果没有则返回 0
config.DB.QueryRow("SELECT COUNT(*) FROM works").Scan(&workCount)
workCount, _ := repositories.GetWorkCount()
// 7. 文章总数
var postCount int
config.DB.QueryRow("SELECT COUNT(*) FROM posts").Scan(&postCount)
postCount, _ := repositories.GetPostCount()
utils.Success(c, gin.H{
"postsTrend": postsTrend,

View File

@@ -3,6 +3,7 @@ package handlers
import (
"fmt"
"net/http"
"time"
"github.com/gin-gonic/gin"
"github.com/niangaodev/art-code/repositories"
@@ -43,7 +44,7 @@ func AdminGetRecentActivities(c *gin.Context) {
"id": log.ID,
"icon": icon,
"text": text,
"time": log.CreatedAt.Format("2006-01-02 15:04:05"),
"time": time.Unix(log.CreatedAt, 0).Format("2006-01-02 15:04:05"),
})
}

View File

@@ -7,6 +7,7 @@ import (
"github.com/niangaodev/art-code/config"
"github.com/niangaodev/art-code/handlers"
"github.com/niangaodev/art-code/middleware"
"github.com/niangaodev/art-code/repositories"
"github.com/niangaodev/art-code/utils"
)
@@ -15,6 +16,9 @@ func main() {
config.InitDB()
defer config.CloseDB()
// 运行数据库迁移 (Convert Datetime to BigInt)
repositories.MigrateToBigInt()
// 初始化ip2region (如果文件不存在将降级为普通IP记录)
// 请确保在server根目录或合适位置放入 ip2region.xdb
utils.InitIP2Region("ip2region.xdb")

View File

@@ -1,15 +1,12 @@
package models
import (
"time"
)
type Experience struct {
Year string `json:"year"`
Role string `json:"role"`
Company string `json:"company"`
}
// AboutProfile 关于我页面数据模型
type AboutProfile struct {
ID uint `json:"id"`
Name string `json:"name"`
@@ -23,6 +20,7 @@ type AboutProfile struct {
ExperiencesStr string `json:"-"` // Stored as string in DB
ExperienceList []Experience `json:"experiences"` // Exposed as array in JSON
IsPrimary bool `json:"isPrimary"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
CreatedAt int64 `json:"createdAt"`
UpdatedAt int64 `json:"updatedAt"`
DeletedAt int64 `json:"deletedAt"`
}

View File

@@ -1,16 +1,15 @@
package models
import "time"
// AccessLog 访问日志 (用于统计流量)
// AccessLog 访问日志模型
type AccessLog struct {
ID uint `json:"id" gorm:"primaryKey"`
IP string `json:"ip"`
UserAgent string `json:"user_agent"`
Path string `json:"path"`
Method string `json:"method"`
StatusCode int `json:"status_code"`
ResponseTime int64 `json:"response_time"` // 毫秒
Region string `json:"region"` // IP归属地
CreatedAt time.Time `json:"created_at"`
ID uint `json:"id" gorm:"primaryKey"`
IP string `json:"ip"`
UserAgent string `json:"user_agent"`
Path string `json:"path"`
Method string `json:"method"`
StatusCode int `json:"status_code"`
ResponseTime int64 `json:"response_time"` // 毫秒
Region string `json:"region"` // IP归属地
CreatedAt int64 `json:"created_at"`
DeletedAt int64 `json:"deleted_at"`
}

View File

@@ -1,27 +1,27 @@
package models
import "time"
// Inquiry 合作咨询
type Inquiry struct {
ID uint `json:"id"`
Name string `json:"name"`
Company string `json:"company"`
ContactMethod string `json:"contactMethod"` // email, wechat, phone
ContactValue string `json:"contactValue"`
Budget string `json:"budget"`
Description string `json:"description"`
Status int `json:"status"` // 0-Unread, 1-Read, 2-Contacted
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
ID uint `json:"id"`
Name string `json:"name"`
Company string `json:"company"`
ContactMethod string `json:"contactMethod"` // email, wechat, phone
ContactValue string `json:"contactValue"`
Budget string `json:"budget"`
Description string `json:"description"`
Status int `json:"status"` // 0-Unread, 1-Read, 2-Contacted
CreatedAt int64 `json:"createdAt"`
UpdatedAt int64 `json:"updatedAt"`
DeletedAt int64 `json:"deletedAt"`
}
// EmailSuffix 邮箱后缀配置
type EmailSuffix struct {
ID uint `json:"id"`
Suffix string `json:"suffix"`
IsActive bool `json:"isActive"`
SortOrder int `json:"sortOrder"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
ID uint `json:"id"`
Suffix string `json:"suffix"`
IsActive bool `json:"isActive"`
SortOrder int `json:"sortOrder"`
CreatedAt int64 `json:"createdAt"`
UpdatedAt int64 `json:"updatedAt"`
DeletedAt int64 `json:"deletedAt"`
}

View File

@@ -1,21 +1,18 @@
package models
import (
"time"
)
// OperationLog 操作日志模型
type OperationLog struct {
ID uint `json:"id"`
UserID uint `json:"userId"`
Username string `json:"username"`
IP string `json:"ip"`
Path string `json:"path"`
Method string `json:"method"`
Params string `json:"params"`
Status int `json:"status"`
Duration int `json:"duration"`
CreatedAt time.Time `json:"createdAt"`
ID uint `json:"id"`
UserID uint `json:"userId"`
Username string `json:"username"`
IP string `json:"ip"`
Path string `json:"path"`
Method string `json:"method"`
Params string `json:"params"`
Status int `json:"status"`
Duration int `json:"duration"`
CreatedAt int64 `json:"createdAt"`
DeletedAt int64 `json:"deletedAt"`
}
// OperationLogResponse 操作日志响应模型

View File

@@ -1,17 +1,14 @@
package models
import (
"time"
)
// Permission 权限模型
type Permission struct {
ID uint `json:"id"`
Name string `json:"name"`
Resource string `json:"resource"`
Action string `json:"action"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
ID uint `json:"id"`
Name string `json:"name"`
Resource string `json:"resource"`
Action string `json:"action"`
CreatedAt int64 `json:"createdAt"`
UpdatedAt int64 `json:"updatedAt"`
DeletedAt int64 `json:"deletedAt"`
}
// PermissionResponse 权限响应模型

View File

@@ -1,23 +1,20 @@
package models
import (
"time"
)
// Post 博客文章模型
type Post struct {
ID uint `json:"id"`
OriginalID string `json:"originalId,omitempty"` // For backward compatibility
Title string `json:"title"`
Category string `json:"category"`
Date string `json:"date"` // YYYY-MM-DD
Excerpt string `json:"excerpt"`
Content string `json:"content"`
ReadCount uint `json:"readCount"`
IsPublished int `json:"isPublished"` // 0: draft, 1: published
Tags []Tag `json:"tags"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
ID uint `json:"id"`
OriginalID string `json:"originalId,omitempty"` // For backward compatibility
Title string `json:"title"`
Category string `json:"category"`
// Date Removed from DB
Excerpt string `json:"excerpt"`
Content string `json:"content"`
ReadCount uint `json:"readCount"`
IsPublished int `json:"isPublished"` // 0: draft, 1: published
Tags []Tag `json:"tags"`
CreatedAt int64 `json:"createdAt"`
UpdatedAt int64 `json:"updatedAt"`
DeletedAt int64 `json:"deletedAt"`
}
// PostResponse 博客文章响应模型
@@ -32,34 +29,35 @@ type PostResponse struct {
// Tag 标签模型
type Tag struct {
ID uint `json:"id"`
Name string `json:"name"`
Slug string `json:"slug"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
ID uint `json:"id"`
Name string `json:"name"`
Slug string `json:"slug"`
CreatedAt int64 `json:"createdAt"`
UpdatedAt int64 `json:"updatedAt"`
DeletedAt int64 `json:"deletedAt"`
}
// PostTag 文章标签关联模型
type PostTag struct {
PostID string `json:"postId"`
TagID uint `json:"tagId"`
CreatedAt time.Time `json:"createdAt"`
PostID string `json:"postId"`
TagID uint `json:"tagId"`
CreatedAt int64 `json:"createdAt"`
}
// PostHistory 文章历史记录模型
type PostHistory struct {
ID uint `json:"id"`
PostID uint `json:"postId"`
Version int `json:"version"`
Title string `json:"title"`
Category string `json:"category"`
Date string `json:"date"`
Excerpt string `json:"excerpt"`
Content string `json:"content"`
IsPublished int `json:"isPublished"`
ModifiedBy uint `json:"modifiedBy"`
ModifiedAt time.Time `json:"modifiedAt"`
CreatedAt time.Time `json:"createdAt"`
ID uint `json:"id"`
PostID uint `json:"postId"`
Version int `json:"version"`
Title string `json:"title"`
Category string `json:"category"`
// Date Removed
Excerpt string `json:"excerpt"`
Content string `json:"content"`
IsPublished int `json:"isPublished"`
ModifiedBy uint `json:"modifiedBy"`
ModifiedAt int64 `json:"modifiedAt"`
CreatedAt int64 `json:"createdAt"`
}
// PostHistoryResponse 文章历史记录响应模型

View File

@@ -1,17 +1,14 @@
package models
import (
"time"
)
// Role 角色模型
type Role struct {
ID uint `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
Permissions []Permission `json:"permissions,omitempty"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
CreatedAt int64 `json:"createdAt"`
UpdatedAt int64 `json:"updatedAt"`
DeletedAt int64 `json:"deletedAt"`
}
// RoleResponse 角色响应模型

View File

@@ -1,30 +1,28 @@
package models
import (
"time"
)
// Testimonial 客户评价模型
type Testimonial struct {
ID uint `json:"id"`
Name string `json:"name"`
Role string `json:"role"`
Content string `json:"content"`
Avatar string `json:"avatar"`
Rating uint8 `json:"rating"`
SortOrder uint `json:"sortOrder"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
ID uint `json:"id"`
Name string `json:"name"`
Role string `json:"role"`
Content string `json:"content"`
Avatar string `json:"avatar"`
Rating uint8 `json:"rating"`
SortOrder uint `json:"sortOrder"`
CreatedAt int64 `json:"createdAt"`
UpdatedAt int64 `json:"updatedAt"`
DeletedAt int64 `json:"deletedAt"`
}
// Partner 合作伙伴模型
type Partner struct {
ID uint `json:"id"`
Name string `json:"name"`
Logo string `json:"logo"`
Description string `json:"description"`
URL string `json:"url"`
SortOrder uint `json:"sortOrder"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
ID uint `json:"id"`
Name string `json:"name"`
Logo string `json:"logo"`
Description string `json:"description"`
URL string `json:"url"`
SortOrder uint `json:"sortOrder"`
CreatedAt int64 `json:"createdAt"`
UpdatedAt int64 `json:"updatedAt"`
DeletedAt int64 `json:"deletedAt"`
}

View File

@@ -1,17 +1,14 @@
package models
import (
"time"
)
// Setting 系统配置模型
type Setting struct {
ID uint `json:"id"`
KeyName string `json:"keyName"`
Value string `json:"value"`
Description string `json:"description"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
ID uint `json:"id"`
KeyName string `json:"keyName"`
Value string `json:"value"`
Description string `json:"description"`
CreatedAt int64 `json:"createdAt"`
UpdatedAt int64 `json:"updatedAt"`
DeletedAt int64 `json:"deletedAt"`
}
// SettingResponse 系统配置响应模型

View File

@@ -1,19 +1,16 @@
package models
import (
"time"
)
// Snippet 代码片段模型
type Snippet struct {
ID string `json:"id"`
Title string `json:"title"`
Code string `json:"code"`
Type string `json:"type"`
Description string `json:"description"`
ViewCount uint `json:"viewCount"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
ID string `json:"id"`
Title string `json:"title"`
Code string `json:"code"`
Type string `json:"type"`
Description string `json:"description"`
ViewCount uint `json:"viewCount"`
CreatedAt int64 `json:"createdAt"`
UpdatedAt int64 `json:"updatedAt"`
DeletedAt int64 `json:"deletedAt"`
}
// SnippetResponse 代码片段响应模型

View File

@@ -1,20 +1,17 @@
package models
import (
"time"
)
// User 用户模型
type User struct {
ID uint `json:"id"`
Username string `json:"username"`
Email string `json:"email"`
PasswordHash string `json:"-"`
RoleID uint `json:"roleId"`
Role string `json:"role"` // 保持兼容或者作为Role Name
IsActive int `json:"isActive"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
ID uint `json:"id"`
Username string `json:"username"`
Email string `json:"email"`
PasswordHash string `json:"-"`
RoleID uint `json:"roleId"`
Role string `json:"role"` // 保持兼容或者作为Role Name
IsActive int `json:"isActive"`
CreatedAt int64 `json:"createdAt"`
UpdatedAt int64 `json:"updatedAt"`
DeletedAt int64 `json:"deletedAt"`
}
// UserResponse 用户响应模型

View File

@@ -1,13 +1,12 @@
package models
import "time"
// UserAccessLog 用户访问记录模型
// UserAccessLog 用户访问日志模型
type UserAccessLog struct {
ID uint `json:"id"`
UserID uint `json:"user_id"` // 用户ID未登录用户为0
UserIP string `json:"user_ip"` // 用户IP地址
UserLocation string `json:"user_location"` // 用户归属地
ArticleID uint `json:"article_id"` // 访问的文章ID
AccessTime time.Time `json:"access_time"` // 访问时间
ID uint `json:"id"`
UserID uint `json:"user_id"` // 用户ID未登录用户为0
UserIP string `json:"user_ip"` // 用户IP地址
UserLocation string `json:"user_location"` // 用户归属地
ArticleID uint `json:"article_id"` // 访问的文章ID
AccessTime int64 `json:"access_time"` // 访问时间
DeletedAt int64 `json:"deleted_at"`
}

View File

@@ -1,39 +1,38 @@
package models
import (
"time"
)
// Work 作品模型
type Work struct {
ID string `json:"id"`
Title string `json:"title"`
Category string `json:"category"`
Year string `json:"year"`
HeroImg string `json:"heroImg"`
Description string `json:"desc"`
IsFeatured int `json:"isFeatured"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
ID string `json:"id"`
Title string `json:"title"`
Category string `json:"category"`
Year string `json:"year"`
HeroImg string `json:"heroImg"`
Description string `json:"desc"`
IsFeatured int `json:"isFeatured"`
CreatedAt int64 `json:"createdAt"`
UpdatedAt int64 `json:"updatedAt"`
DeletedAt int64 `json:"deletedAt"`
}
// WorkTechStack 作品技术栈模型
type WorkTechStack struct {
ID uint `json:"id"`
WorkID string `json:"workId"`
Category string `json:"category"`
Item string `json:"item"`
CreatedAt time.Time `json:"createdAt"`
ID uint `json:"id"`
WorkID string `json:"workId"`
Category string `json:"category"`
Item string `json:"item"`
CreatedAt int64 `json:"createdAt"`
DeletedAt int64 `json:"deletedAt"`
}
// WorkGallery 作品图库模型
type WorkGallery struct {
ID uint `json:"id"`
WorkID string `json:"workId"`
ImageURL string `json:"imageUrl"`
SortOrder uint `json:"sortOrder"`
Description string `json:"description"`
CreatedAt time.Time `json:"createdAt"`
ID uint `json:"id"`
WorkID string `json:"workId"`
ImageURL string `json:"imageUrl"`
SortOrder uint `json:"sortOrder"`
Description string `json:"description"`
CreatedAt int64 `json:"createdAt"`
DeletedAt int64 `json:"deletedAt"`
}
// WorkResponse 作品响应模型,包含关联数据

File diff suppressed because it is too large Load Diff

View File

@@ -4,6 +4,7 @@ import (
"database/sql"
"encoding/json"
"log"
"time"
"github.com/niangaodev/art-code/config"
"github.com/niangaodev/art-code/models"
@@ -12,9 +13,9 @@ import (
// GetPrimaryAboutProfile 获取主页个人资料
func GetPrimaryAboutProfile() (*models.AboutProfile, error) {
query := `
SELECT id, name, avatar, location, bio, email, wechat, tech_stack, experiences, is_primary, created_at, updated_at
SELECT id, name, avatar, location, bio, email, wechat, tech_stack, experiences, is_primary, created_at, updated_at, deleted_at
FROM about_profiles
WHERE is_primary = TRUE
WHERE is_primary = TRUE AND deleted_at = 0
LIMIT 1
`
row := config.DB.QueryRow(query)
@@ -33,6 +34,7 @@ func GetPrimaryAboutProfile() (*models.AboutProfile, error) {
&profile.IsPrimary,
&profile.CreatedAt,
&profile.UpdatedAt,
&profile.DeletedAt,
); err != nil {
if err == sql.ErrNoRows {
// If no primary profile, try to get the first one
@@ -61,8 +63,9 @@ func GetPrimaryAboutProfile() (*models.AboutProfile, error) {
// GetFirstAboutProfile 获取第一个个人资料(备用)
func GetFirstAboutProfile() (*models.AboutProfile, error) {
query := `
SELECT id, name, avatar, location, bio, email, wechat, tech_stack, experiences, is_primary, created_at, updated_at
SELECT id, name, avatar, location, bio, email, wechat, tech_stack, experiences, is_primary, created_at, updated_at, deleted_at
FROM about_profiles
WHERE deleted_at = 0
ORDER BY id ASC
LIMIT 1
`
@@ -82,6 +85,7 @@ func GetFirstAboutProfile() (*models.AboutProfile, error) {
&profile.IsPrimary,
&profile.CreatedAt,
&profile.UpdatedAt,
&profile.DeletedAt,
); err != nil {
if err == sql.ErrNoRows {
return nil, nil
@@ -107,7 +111,7 @@ func GetFirstAboutProfile() (*models.AboutProfile, error) {
// GetAllAboutProfiles 获取所有个人资料(管理用)
func GetAllAboutProfiles() ([]models.AboutProfile, error) {
query := "SELECT id, name, avatar, location, bio, email, wechat, tech_stack, experiences, is_primary, created_at, updated_at FROM about_profiles"
query := "SELECT id, name, avatar, location, bio, email, wechat, tech_stack, experiences, is_primary, created_at, updated_at, deleted_at FROM about_profiles WHERE deleted_at = 0"
rows, err := config.DB.Query(query)
if err != nil {
log.Printf("Error querying about profiles: %v", err)
@@ -131,6 +135,7 @@ func GetAllAboutProfiles() ([]models.AboutProfile, error) {
&p.IsPrimary,
&p.CreatedAt,
&p.UpdatedAt,
&p.DeletedAt,
); err != nil {
continue
}
@@ -158,9 +163,10 @@ func CreateAboutProfile(profile *models.AboutProfile) error {
expBytes, _ := json.Marshal(profile.ExperienceList)
profile.ExperiencesStr = string(expBytes)
now := time.Now().Unix()
query := `
INSERT INTO about_profiles (name, avatar, location, bio, email, wechat, tech_stack, experiences, is_primary, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, NOW(), NOW())
INSERT INTO about_profiles (name, avatar, location, bio, email, wechat, tech_stack, experiences, is_primary, created_at, updated_at, deleted_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0)
`
result, err := config.DB.Exec(
query,
@@ -173,6 +179,8 @@ func CreateAboutProfile(profile *models.AboutProfile) error {
profile.TechStack,
profile.ExperiencesStr,
profile.IsPrimary,
now,
now,
)
if err != nil {
log.Printf("Error creating about profile: %v", err)
@@ -184,6 +192,8 @@ func CreateAboutProfile(profile *models.AboutProfile) error {
return err
}
profile.ID = uint(id)
profile.CreatedAt = now
profile.UpdatedAt = now
return nil
}
@@ -195,10 +205,11 @@ func UpdateAboutProfile(profile *models.AboutProfile) error {
expBytes, _ := json.Marshal(profile.ExperienceList)
profile.ExperiencesStr = string(expBytes)
now := time.Now().Unix()
query := `
UPDATE about_profiles
SET name = ?, avatar = ?, location = ?, bio = ?, email = ?, wechat = ?, tech_stack = ?, experiences = ?, is_primary = ?, updated_at = NOW()
WHERE id = ?
SET name = ?, avatar = ?, location = ?, bio = ?, email = ?, wechat = ?, tech_stack = ?, experiences = ?, is_primary = ?, updated_at = ?
WHERE id = ? AND deleted_at = 0
`
_, err := config.DB.Exec(
query,
@@ -211,6 +222,7 @@ func UpdateAboutProfile(profile *models.AboutProfile) error {
profile.TechStack,
profile.ExperiencesStr,
profile.IsPrimary,
now,
profile.ID,
)
if err != nil {
@@ -220,10 +232,11 @@ func UpdateAboutProfile(profile *models.AboutProfile) error {
return nil
}
// DeleteAboutProfile 删除个人资料
// DeleteAboutProfile 删除个人资料 (Soft Delete)
func DeleteAboutProfile(id uint) error {
query := "DELETE FROM about_profiles WHERE id = ?"
_, err := config.DB.Exec(query, id)
now := time.Now().Unix()
query := "UPDATE about_profiles SET deleted_at = ? WHERE id = ?"
_, err := config.DB.Exec(query, now, id)
if err != nil {
log.Printf("Error deleting about profile: %v", err)
return err

View File

@@ -2,6 +2,7 @@ package repositories
import (
"log"
"time"
"github.com/niangaodev/art-code/config"
"github.com/niangaodev/art-code/models"
@@ -9,8 +10,9 @@ import (
// CreateInquiry 创建咨询
func CreateInquiry(inquiry *models.Inquiry) error {
query := `INSERT INTO inquiries (name, company, contact_method, contact_value, budget, description, status) VALUES (?, ?, ?, ?, ?, ?, 0)`
_, err := config.DB.Exec(query, inquiry.Name, inquiry.Company, inquiry.ContactMethod, inquiry.ContactValue, inquiry.Budget, inquiry.Description)
now := time.Now().Unix()
query := `INSERT INTO inquiries (name, company, contact_method, contact_value, budget, description, status, created_at, updated_at, deleted_at) VALUES (?, ?, ?, ?, ?, ?, 0, ?, ?, 0)`
_, err := config.DB.Exec(query, inquiry.Name, inquiry.Company, inquiry.ContactMethod, inquiry.ContactValue, inquiry.Budget, inquiry.Description, now, now)
if err != nil {
log.Printf("Error creating inquiry: %v", err)
return err
@@ -20,7 +22,8 @@ func CreateInquiry(inquiry *models.Inquiry) error {
// GetInquiries 获取咨询列表 (Admin)
func GetInquiries() ([]models.Inquiry, error) {
rows, err := config.DB.Query("SELECT id, name, company, contact_method, contact_value, budget, description, status, created_at FROM inquiries ORDER BY created_at DESC")
// Added deleted_at check
rows, err := config.DB.Query("SELECT id, name, company, contact_method, contact_value, budget, description, status, created_at, updated_at, deleted_at FROM inquiries WHERE deleted_at = 0 ORDER BY created_at DESC")
if err != nil {
return nil, err
}
@@ -29,15 +32,21 @@ func GetInquiries() ([]models.Inquiry, error) {
var inquiries []models.Inquiry
for rows.Next() {
var i models.Inquiry
var createdAtStr string
if err := rows.Scan(&i.ID, &i.Name, &i.Company, &i.ContactMethod, &i.ContactValue, &i.Budget, &i.Description, &i.Status, &createdAtStr); err != nil {
if err := rows.Scan(
&i.ID,
&i.Name,
&i.Company,
&i.ContactMethod,
&i.ContactValue,
&i.Budget,
&i.Description,
&i.Status,
&i.CreatedAt,
&i.UpdatedAt,
&i.DeletedAt,
); err != nil {
return nil, err
}
// 解析时间字符串 (MySQL driver default behavior might differ based on parseTime param)
// Assuming config.DB has parseTime=true, if not we scan to string.
// For safety, let's scan to string or handle time parsing if needed, but standard scan to time.Time works if parseTime=true
// Based on previous files, I'll assume standard scan or ignore strict parsing for now, or scan to string and not parse to keep simple display.
// Let's assume standard behavior. If error, I'll fix.
inquiries = append(inquiries, i)
}
return inquiries, nil
@@ -45,7 +54,7 @@ func GetInquiries() ([]models.Inquiry, error) {
// GetEmailSuffixes 获取活跃的邮箱后缀
func GetEmailSuffixes() ([]models.EmailSuffix, error) {
rows, err := config.DB.Query("SELECT id, suffix, is_active, sort_order FROM email_suffixes WHERE is_active = 1 ORDER BY sort_order ASC")
rows, err := config.DB.Query("SELECT id, suffix, is_active, sort_order, created_at, updated_at, deleted_at FROM email_suffixes WHERE is_active = 1 AND deleted_at = 0 ORDER BY sort_order ASC")
if err != nil {
return nil, err
}
@@ -55,7 +64,15 @@ func GetEmailSuffixes() ([]models.EmailSuffix, error) {
for rows.Next() {
var s models.EmailSuffix
var isActive int
if err := rows.Scan(&s.ID, &s.Suffix, &isActive, &s.SortOrder); err != nil {
if err := rows.Scan(
&s.ID,
&s.Suffix,
&isActive,
&s.SortOrder,
&s.CreatedAt,
&s.UpdatedAt,
&s.DeletedAt,
); err != nil {
return nil, err
}
s.IsActive = isActive == 1
@@ -66,7 +83,7 @@ func GetEmailSuffixes() ([]models.EmailSuffix, error) {
// AdminGetEmailSuffixes 获取所有邮箱后缀 (Admin)
func AdminGetEmailSuffixes() ([]models.EmailSuffix, error) {
rows, err := config.DB.Query("SELECT id, suffix, is_active, sort_order, created_at FROM email_suffixes ORDER BY sort_order ASC")
rows, err := config.DB.Query("SELECT id, suffix, is_active, sort_order, created_at, updated_at, deleted_at FROM email_suffixes WHERE deleted_at = 0 ORDER BY sort_order ASC")
if err != nil {
return nil, err
}
@@ -76,8 +93,15 @@ func AdminGetEmailSuffixes() ([]models.EmailSuffix, error) {
for rows.Next() {
var s models.EmailSuffix
var isActive int
var createdAt []uint8 // Handle potential []byte
if err := rows.Scan(&s.ID, &s.Suffix, &isActive, &s.SortOrder, &createdAt); err != nil {
if err := rows.Scan(
&s.ID,
&s.Suffix,
&isActive,
&s.SortOrder,
&s.CreatedAt,
&s.UpdatedAt,
&s.DeletedAt,
); err != nil {
return nil, err
}
s.IsActive = isActive == 1
@@ -88,36 +112,40 @@ func AdminGetEmailSuffixes() ([]models.EmailSuffix, error) {
// CreateEmailSuffix 创建邮箱后缀
func CreateEmailSuffix(suffix *models.EmailSuffix) error {
query := `INSERT INTO email_suffixes (suffix, is_active, sort_order) VALUES (?, ?, ?)`
now := time.Now().Unix()
query := `INSERT INTO email_suffixes (suffix, is_active, sort_order, created_at, updated_at, deleted_at) VALUES (?, ?, ?, ?, ?, 0)`
isActive := 0
if suffix.IsActive {
isActive = 1
}
_, err := config.DB.Exec(query, suffix.Suffix, isActive, suffix.SortOrder)
_, err := config.DB.Exec(query, suffix.Suffix, isActive, suffix.SortOrder, now, now)
return err
}
// UpdateEmailSuffix 更新邮箱后缀
func UpdateEmailSuffix(suffix *models.EmailSuffix) error {
query := `UPDATE email_suffixes SET suffix = ?, is_active = ?, sort_order = ? WHERE id = ?`
now := time.Now().Unix()
query := `UPDATE email_suffixes SET suffix = ?, is_active = ?, sort_order = ?, updated_at = ? WHERE id = ? AND deleted_at = 0`
isActive := 0
if suffix.IsActive {
isActive = 1
}
_, err := config.DB.Exec(query, suffix.Suffix, isActive, suffix.SortOrder, suffix.ID)
_, err := config.DB.Exec(query, suffix.Suffix, isActive, suffix.SortOrder, now, suffix.ID)
return err
}
// DeleteEmailSuffix 删除邮箱后缀
// DeleteEmailSuffix 删除邮箱后缀 (Soft Delete)
func DeleteEmailSuffix(id uint) error {
query := `DELETE FROM email_suffixes WHERE id = ?`
_, err := config.DB.Exec(query, id)
now := time.Now().Unix()
query := `UPDATE email_suffixes SET deleted_at = ? WHERE id = ?`
_, err := config.DB.Exec(query, now, id)
return err
}
// UpdateInquiryStatus 更新咨询状态
func UpdateInquiryStatus(id uint, status int) error {
query := `UPDATE inquiries SET status = ? WHERE id = ?`
_, err := config.DB.Exec(query, status, id)
now := time.Now().Unix()
query := `UPDATE inquiries SET status = ?, updated_at = ? WHERE id = ? AND deleted_at = 0`
_, err := config.DB.Exec(query, status, now, id)
return err
}

View File

@@ -8,39 +8,39 @@ import (
"github.com/niangaodev/art-code/models"
)
// Helper to parse date strings flexibly
func parseDateString(dateStr string, isEnd bool) string {
// Helper to parse date string to unix timestamp
func parseDateToUnix(dateStr string, isEnd bool) int64 {
if dateStr == "" {
return ""
return 0
}
// Try parsing with time first
t, err := time.ParseInLocation("2006-01-02 15:04", dateStr, time.Local)
if err == nil {
if isEnd {
// If it's end time, go to end of that minute
return t.Format("2006-01-02 15:04") + ":59"
// HH:mm:59
return t.Add(59 * time.Second).Unix()
}
return t.Format("2006-01-02 15:04:05")
return t.Unix()
}
// Try parsing just date
t, err = time.ParseInLocation("2006-01-02", dateStr, time.Local)
if err == nil {
if isEnd {
// If it's end date, go to end of day
return t.Format("2006-01-02") + " 23:59:59"
// 23:59:59
return t.Add(24*time.Hour - 1*time.Second).Unix()
}
return t.Format("2006-01-02") + " 00:00:00"
return t.Unix()
}
// Return original if parsing fails (fallback)
return dateStr
return 0
}
// CreateAccessLog 创建访问日志
func CreateAccessLog(log *models.AccessLog) error {
query := `INSERT INTO access_logs (ip, user_agent, path, method, status_code, response_time, region) VALUES (?, ?, ?, ?, ?, ?, ?)`
_, err := config.DB.Exec(query, log.IP, log.UserAgent, log.Path, log.Method, log.StatusCode, log.ResponseTime, log.Region)
now := time.Now().Unix()
query := `INSERT INTO access_logs (ip, user_agent, path, method, status_code, response_time, region, created_at, deleted_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, 0)`
_, err := config.DB.Exec(query, log.IP, log.UserAgent, log.Path, log.Method, log.StatusCode, log.ResponseTime, log.Region, now)
return err
}
@@ -55,25 +55,27 @@ type UVTrendData struct {
// GetDailyUV 获取UV趋势
func GetDailyUV(startDate, endDate string) ([]UVTrendData, error) {
query := `
SELECT DATE_FORMAT(access_time, '%Y-%m-%d') as date, COUNT(DISTINCT user_ip) as count
SELECT FROM_UNIXTIME(access_time, '%Y-%m-%d') as date, COUNT(DISTINCT user_ip) as count
FROM user_access_logs
WHERE 1=1
`
args := []interface{}{}
if startDate != "" {
formattedStart := parseDateString(startDate, false)
startUnix := parseDateToUnix(startDate, false)
query += " AND access_time >= ?"
args = append(args, formattedStart)
args = append(args, startUnix)
} else {
// 默认最近7天
query += " AND access_time >= DATE_SUB(CURDATE(), INTERVAL 6 DAY)"
startUnix := time.Now().AddDate(0, 0, -6).Unix()
query += " AND access_time >= ?"
args = append(args, startUnix)
}
if endDate != "" {
formattedEnd := parseDateString(endDate, true)
endUnix := parseDateToUnix(endDate, true)
query += " AND access_time <= ?"
args = append(args, formattedEnd)
args = append(args, endUnix)
}
query += `
@@ -116,15 +118,15 @@ func GetUserRegions(startDate, endDate string) ([]struct {
args := []interface{}{}
if startDate != "" {
formattedStart := parseDateString(startDate, false)
startUnix := parseDateToUnix(startDate, false)
query += " AND access_time >= ?"
args = append(args, formattedStart)
args = append(args, startUnix)
}
if endDate != "" {
formattedEnd := parseDateString(endDate, true)
endUnix := parseDateToUnix(endDate, true)
query += " AND access_time <= ?"
args = append(args, formattedEnd)
args = append(args, endUnix)
}
query += `

View File

@@ -0,0 +1,116 @@
package repositories
import (
"fmt"
"log"
"github.com/niangaodev/art-code/config"
)
// MigrateToBigInt performs schema migration to BigInt timestamps
func MigrateToBigInt() {
tables := []string{
"posts", "users", "tags", "works", "snippets", "settings",
"about_profiles", "partners", "testimonials", "inquiries",
"email_suffixes", "access_logs", "user_access_logs",
"operation_logs", "permissions", "roles",
"work_tech_stack", "work_gallery", "post_tags", "post_history",
}
for _, table := range tables {
log.Printf("Migrating table: %s", table)
// 1. Add deleted_at if not exists
if !columnExists(table, "deleted_at") && table != "post_tags" { // post_tags doesn't need deleted_at in my previous plan? I added to all in SQL file? Yes.
// Actually post_tags in SQL file I modified: `created_at` bigint. No `deleted_at`.
// So skip deleted_at for post_tags.
if table != "post_tags" {
execSQL(fmt.Sprintf("ALTER TABLE `%s` ADD COLUMN `deleted_at` BIGINT NOT NULL DEFAULT 0", table))
}
}
// 2. Migrate created_at
migrateColumn(table, "created_at")
// 3. Migrate updated_at (if exists)
if columnExists(table, "updated_at") {
migrateColumn(table, "updated_at")
}
// 4. Migrate access_time (for user_access_logs)
if table == "user_access_logs" && columnExists(table, "access_time") {
migrateColumn(table, "access_time")
}
// 5. Remove 'date' from posts if exists
if table == "posts" && columnExists(table, "date") {
execSQL("ALTER TABLE `posts` DROP COLUMN `date`")
// Drop index idx_date if exists? MySQL drops index on column drop usually.
}
}
}
func columnExists(tableName, colName string) bool {
query := `
SELECT COUNT(*)
FROM information_schema.columns
WHERE table_schema = DATABASE()
AND table_name = ?
AND column_name = ?
`
var count int
err := config.DB.QueryRow(query, tableName, colName).Scan(&count)
if err != nil {
log.Printf("Error checking column %s.%s: %v", tableName, colName, err)
return false
}
return count > 0
}
func isBigInt(tableName, colName string) bool {
query := `
SELECT DATA_TYPE
FROM information_schema.columns
WHERE table_schema = DATABASE()
AND table_name = ?
AND column_name = ?
`
var dataType string
err := config.DB.QueryRow(query, tableName, colName).Scan(&dataType)
if err != nil {
return false
}
return dataType == "bigint" || dataType == "int"
}
func migrateColumn(table, col string) {
if !columnExists(table, col) {
return
}
if isBigInt(table, col) {
return // Already migrated
}
log.Printf("Converting %s.%s to BIGINT...", table, col)
// Rename old
oldCol := col + "_old_dt"
execSQL(fmt.Sprintf("ALTER TABLE `%s` CHANGE `%s` `%s` DATETIME", table, col, oldCol)) // Ensure it's treated as datetime for rename
// Add new
execSQL(fmt.Sprintf("ALTER TABLE `%s` ADD COLUMN `%s` BIGINT NOT NULL DEFAULT 0", table, col))
// Copy and Convert
execSQL(fmt.Sprintf("UPDATE `%s` SET `%s` = UNIX_TIMESTAMP(`%s`) WHERE `%s` IS NOT NULL", table, col, oldCol, oldCol))
// Drop old
execSQL(fmt.Sprintf("ALTER TABLE `%s` DROP COLUMN `%s`", table, oldCol))
}
func execSQL(query string) {
_, err := config.DB.Exec(query)
if err != nil {
// Log but continue (might fail if column doesn't exist etc)
log.Printf("SQL Error: %v | Query: %s", err, query)
}
}

View File

@@ -2,6 +2,7 @@ package repositories
import (
"log"
"time"
"github.com/niangaodev/art-code/config"
"github.com/niangaodev/art-code/models"
@@ -9,9 +10,10 @@ import (
// CreateOperationLog 创建操作日志
func CreateOperationLog(operationLog *models.OperationLog) error {
now := time.Now().Unix()
query := `
INSERT INTO operation_logs (user_id, username, ip, path, method, params, status, duration, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, NOW())
INSERT INTO operation_logs (user_id, username, ip, path, method, params, status, duration, created_at, deleted_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 0)
`
_, err := config.DB.Exec(
query,
@@ -23,6 +25,7 @@ func CreateOperationLog(operationLog *models.OperationLog) error {
operationLog.Params,
operationLog.Status,
operationLog.Duration,
now,
)
if err != nil {
log.Printf("Error creating operation log: %v", err)
@@ -39,7 +42,7 @@ func GetOperationLogs(page, pageSize int) ([]models.OperationLog, int64, error)
// 获取总记录数
var total int64
countQuery := "SELECT COUNT(*) FROM operation_logs"
countQuery := "SELECT COUNT(*) FROM operation_logs WHERE deleted_at = 0"
if err := config.DB.QueryRow(countQuery).Scan(&total); err != nil {
log.Printf("Error counting operation logs: %v", err)
return nil, 0, err
@@ -47,8 +50,9 @@ func GetOperationLogs(page, pageSize int) ([]models.OperationLog, int64, error)
// 获取分页数据
query := `
SELECT id, user_id, username, ip, path, method, params, status, duration, created_at
SELECT id, user_id, username, ip, path, method, params, status, duration, created_at, deleted_at
FROM operation_logs
WHERE deleted_at = 0
ORDER BY created_at DESC
LIMIT ? OFFSET ?
`
@@ -73,6 +77,7 @@ func GetOperationLogs(page, pageSize int) ([]models.OperationLog, int64, error)
&operationLog.Status,
&operationLog.Duration,
&operationLog.CreatedAt,
&operationLog.DeletedAt,
); err != nil {
log.Printf("Error scanning operation log: %v", err)
continue
@@ -95,7 +100,7 @@ func BuildOperationLogResponse(log *models.OperationLog) *models.OperationLogRes
Params: log.Params,
Status: log.Status,
Duration: log.Duration,
CreatedAt: log.CreatedAt.Format("2006-01-02 15:04:05"),
CreatedAt: time.Unix(log.CreatedAt, 0).Format("2006-01-02 15:04:05"),
}
}

View File

@@ -2,6 +2,7 @@ package repositories
import (
"log"
"time"
"github.com/niangaodev/art-code/config"
"github.com/niangaodev/art-code/models"
@@ -9,7 +10,8 @@ import (
// GetPermissions 获取所有权限
func GetPermissions() ([]models.Permission, error) {
query := "SELECT id, name, resource, action, created_at, updated_at FROM permissions"
// Filter deleted_at = 0
query := "SELECT id, name, resource, action, created_at, updated_at, deleted_at FROM permissions WHERE deleted_at = 0"
rows, err := config.DB.Query(query)
if err != nil {
log.Printf("Error getting permissions: %v", err)
@@ -20,7 +22,7 @@ func GetPermissions() ([]models.Permission, error) {
var permissions []models.Permission
for rows.Next() {
var permission models.Permission
if err := rows.Scan(&permission.ID, &permission.Name, &permission.Resource, &permission.Action, &permission.CreatedAt, &permission.UpdatedAt); err != nil {
if err := rows.Scan(&permission.ID, &permission.Name, &permission.Resource, &permission.Action, &permission.CreatedAt, &permission.UpdatedAt, &permission.DeletedAt); err != nil {
log.Printf("Error scanning permission: %v", err)
continue
}
@@ -33,10 +35,10 @@ func GetPermissions() ([]models.Permission, error) {
// GetPermissionsByRoleID 获取指定角色的权限
func GetPermissionsByRoleID(roleID uint) ([]models.Permission, error) {
query := `
SELECT p.id, p.name, p.resource, p.action, p.created_at, p.updated_at
SELECT p.id, p.name, p.resource, p.action, p.created_at, p.updated_at, p.deleted_at
FROM permissions p
JOIN role_permissions rp ON p.id = rp.permission_id
WHERE rp.role_id = ?
WHERE rp.role_id = ? AND p.deleted_at = 0
`
rows, err := config.DB.Query(query, roleID)
if err != nil {
@@ -48,7 +50,7 @@ func GetPermissionsByRoleID(roleID uint) ([]models.Permission, error) {
var permissions []models.Permission
for rows.Next() {
var permission models.Permission
if err := rows.Scan(&permission.ID, &permission.Name, &permission.Resource, &permission.Action, &permission.CreatedAt, &permission.UpdatedAt); err != nil {
if err := rows.Scan(&permission.ID, &permission.Name, &permission.Resource, &permission.Action, &permission.CreatedAt, &permission.UpdatedAt, &permission.DeletedAt); err != nil {
log.Printf("Error scanning permission: %v", err)
continue
}
@@ -65,16 +67,23 @@ func BuildPermissionResponse(permission *models.Permission) *models.PermissionRe
Name: permission.Name,
Resource: permission.Resource,
Action: permission.Action,
CreatedAt: permission.CreatedAt.Format("2006-01-02 15:04:05"),
UpdatedAt: permission.UpdatedAt.Format("2006-01-02 15:04:05"),
CreatedAt: time.Unix(permission.CreatedAt, 0).Format("2006-01-02 15:04:05"),
UpdatedAt: time.Unix(permission.UpdatedAt, 0).Format("2006-01-02 15:04:05"),
}
}
// BuildPermissionsResponse 构建权限列表响应
func BuildPermissionsResponse(permissions []models.Permission) []models.PermissionResponse {
var responses []models.PermissionResponse
for _, permission := range permissions {
responses = append(responses, *BuildPermissionResponse(&permission))
for _, p := range permissions {
responses = append(responses, models.PermissionResponse{
ID: p.ID,
Name: p.Name,
Resource: p.Resource,
Action: p.Action,
CreatedAt: time.Unix(p.CreatedAt, 0).Format("2006-01-02 15:04:05"),
UpdatedAt: time.Unix(p.UpdatedAt, 0).Format("2006-01-02 15:04:05"),
})
}
return responses
}

View File

@@ -9,44 +9,20 @@ import (
"github.com/niangaodev/art-code/models"
)
// Helper to parse date strings flexibly (duplicated to avoid circular dependency if moved to utils, or just keep simple)
// Ideally this should be in a utils package, but for now we'll keep it local to avoid refactoring everything
func parsePostDateString(dateStr string, isEnd bool) string {
if dateStr == "" {
return ""
}
// Try parsing with time first
t, err := time.ParseInLocation("2006-01-02 15:04", dateStr, time.Local)
if err == nil {
if isEnd {
return t.Format("2006-01-02 15:04") + ":59"
}
return t.Format("2006-01-02 15:04:05")
}
// Try parsing just date
t, err = time.ParseInLocation("2006-01-02", dateStr, time.Local)
if err == nil {
if isEnd {
return t.Format("2006-01-02") + " 23:59:59"
}
return t.Format("2006-01-02") + " 00:00:00"
}
return dateStr
}
// GetPosts 获取所有博客文章(支持搜索)
func GetPosts(keyword string) ([]models.Post, error) {
var rows *sql.Rows
var err error
// Common select fields (removed date)
selectFields := "id, title, category, excerpt, content, read_count, is_published, created_at, updated_at, deleted_at"
if keyword != "" {
// 使用全文搜索
query := `
SELECT id, title, category, date, excerpt, content, read_count, is_published, created_at, updated_at
SELECT ` + selectFields + `
FROM posts
WHERE is_published = 1 AND (
WHERE is_published = 1 AND deleted_at = 0 AND (
MATCH(title, content) AGAINST(? IN BOOLEAN MODE) OR
title LIKE ? OR
content LIKE ?
@@ -57,7 +33,7 @@ func GetPosts(keyword string) ([]models.Post, error) {
rows, err = config.DB.Query(query, keyword, likeKeyword, likeKeyword)
} else {
// 默认查询
query := "SELECT id, title, category, date, excerpt, content, read_count, is_published, created_at, updated_at FROM posts WHERE is_published = 1 ORDER BY created_at DESC"
query := "SELECT " + selectFields + " FROM posts WHERE is_published = 1 AND deleted_at = 0 ORDER BY created_at DESC"
rows, err = config.DB.Query(query)
}
@@ -74,13 +50,13 @@ func GetPosts(keyword string) ([]models.Post, error) {
&post.ID,
&post.Title,
&post.Category,
&post.Date,
&post.Excerpt,
&post.Content,
&post.ReadCount,
&post.IsPublished,
&post.CreatedAt,
&post.UpdatedAt,
&post.DeletedAt,
); err != nil {
log.Printf("Error scanning post: %v", err)
continue
@@ -93,7 +69,8 @@ func GetPosts(keyword string) ([]models.Post, error) {
// GetPostByID 根据ID获取博客文章
func GetPostByID(id uint) (*models.Post, error) {
query := "SELECT id, title, category, date, excerpt, content, read_count, is_published, created_at, updated_at FROM posts WHERE id = ? AND is_published = 1"
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"
row := config.DB.QueryRow(query, id)
var post models.Post
@@ -101,13 +78,13 @@ func GetPostByID(id uint) (*models.Post, error) {
&post.ID,
&post.Title,
&post.Category,
&post.Date,
&post.Excerpt,
&post.Content,
&post.ReadCount,
&post.IsPublished,
&post.CreatedAt,
&post.UpdatedAt,
&post.DeletedAt,
); err != nil {
if err == sql.ErrNoRows {
return nil, nil
@@ -127,7 +104,8 @@ func GetPostByID(id uint) (*models.Post, error) {
// GetAllPosts 获取所有博客文章(包括未发布的)
func GetAllPosts() ([]models.Post, error) {
query := "SELECT id, title, category, date, excerpt, content, read_count, is_published, created_at, updated_at FROM posts ORDER BY created_at DESC"
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)
if err != nil {
log.Printf("Error querying all posts: %v", err)
@@ -142,13 +120,13 @@ func GetAllPosts() ([]models.Post, error) {
&post.ID,
&post.Title,
&post.Category,
&post.Date,
&post.Excerpt,
&post.Content,
&post.ReadCount,
&post.IsPublished,
&post.CreatedAt,
&post.UpdatedAt,
&post.DeletedAt,
); err != nil {
log.Printf("Error scanning post: %v", err)
continue
@@ -161,18 +139,20 @@ func GetAllPosts() ([]models.Post, error) {
// CreatePost 创建博客文章
func CreatePost(post *models.Post) error {
now := time.Now().Unix()
query := `
INSERT INTO posts (title, category, date, excerpt, content, is_published, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, NOW(), NOW())
INSERT INTO posts (title, category, excerpt, content, is_published, created_at, updated_at, deleted_at)
VALUES (?, ?, ?, ?, ?, ?, ?, 0)
`
result, err := config.DB.Exec(
query,
post.Title,
post.Category,
post.Date,
post.Excerpt,
post.Content,
post.IsPublished,
now,
now,
)
if err != nil {
log.Printf("Error creating post: %v", err)
@@ -184,24 +164,27 @@ func CreatePost(post *models.Post) error {
return err
}
post.ID = uint(id)
post.CreatedAt = now
post.UpdatedAt = now
return nil
}
// UpdatePost 更新博客文章
func UpdatePost(post *models.Post) error {
now := time.Now().Unix()
query := `
UPDATE posts SET title = ?, category = ?, date = ?, excerpt = ?, content = ?, is_published = ?, updated_at = NOW()
WHERE id = ?
UPDATE posts SET title = ?, category = ?, excerpt = ?, content = ?, is_published = ?, updated_at = ?
WHERE id = ? AND deleted_at = 0
`
_, err := config.DB.Exec(
query,
post.Title,
post.Category,
post.Date,
post.Excerpt,
post.Content,
post.IsPublished,
now,
post.ID,
)
if err != nil {
@@ -212,10 +195,11 @@ func UpdatePost(post *models.Post) error {
return nil
}
// DeletePost 删除博客文章
// DeletePost 删除博客文章 (Soft Delete)
func DeletePost(id uint) error {
query := "DELETE FROM posts WHERE id = ?"
_, err := config.DB.Exec(query, id)
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
@@ -227,7 +211,7 @@ func DeletePost(id uint) error {
// GetPostCount 获取文章总数
func GetPostCount() (int, error) {
var count int
query := "SELECT COUNT(*) FROM posts"
query := "SELECT COUNT(*) FROM posts WHERE deleted_at = 0"
row := config.DB.QueryRow(query)
err := row.Scan(&count)
@@ -241,11 +225,14 @@ func GetPostCount() (int, error) {
// 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")
response := &models.PostResponse{
ID: post.ID,
Title: post.Title,
Category: post.Category,
Date: post.Date,
Date: dateStr,
Excerpt: post.Excerpt,
}
@@ -275,24 +262,55 @@ func SavePostHistory(post *models.Post, modifiedBy uint) error {
return err
}
// 插入新的历史记录
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, date, excerpt, content,
post_id, version, title, category, excerpt, content,
is_published, modified_by, modified_at, created_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, NOW(), NOW())
) 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.Date,
post.Excerpt,
post.Content,
post.IsPublished,
modifiedBy,
now,
now,
)
if err != nil {
log.Printf("Error saving post history: %v", err)
@@ -305,7 +323,7 @@ func SavePostHistory(post *models.Post, modifiedBy uint) error {
// GetPostHistory 获取文章历史记录
func GetPostHistory(postID uint) ([]models.PostHistory, error) {
query := `
SELECT id, post_id, version, title, category, date, excerpt, content,
SELECT id, post_id, version, title, category, excerpt, content,
is_published, modified_by, modified_at, created_at
FROM post_history
WHERE post_id = ?
@@ -327,7 +345,6 @@ func GetPostHistory(postID uint) ([]models.PostHistory, error) {
&h.Version,
&h.Title,
&h.Category,
&h.Date, // Scan date directly into h.Date
&h.Excerpt,
&h.Content,
&h.IsPublished,
@@ -347,7 +364,7 @@ func GetPostHistory(postID uint) ([]models.PostHistory, error) {
// GetPostHistoryByVersion 获取指定版本的文章历史记录
func GetPostHistoryByVersion(postID uint, version uint) (*models.PostHistory, error) {
query := `
SELECT id, post_id, version, title, category, date, excerpt, content,
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 = ?
@@ -361,7 +378,6 @@ func GetPostHistoryByVersion(postID uint, version uint) (*models.PostHistory, er
&h.Version,
&h.Title,
&h.Category,
&h.Date,
&h.Excerpt,
&h.Content,
&h.IsPublished,
@@ -387,11 +403,11 @@ func BuildPostHistoryResponse(history *models.PostHistory) *models.PostHistoryRe
Version: history.Version,
Title: history.Title,
Category: history.Category,
Date: history.Date,
Date: time.Unix(history.CreatedAt, 0).Format("2006-01-02"), // Compute date
IsPublished: history.IsPublished,
ModifiedBy: history.ModifiedBy,
ModifiedAt: history.ModifiedAt.Format("2006-01-02 15:04:05"),
CreatedAt: history.CreatedAt.Format("2006-01-02 15:04:05"),
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"),
}
}
@@ -415,36 +431,29 @@ type TrendData struct {
// GetNewPostsTrend 获取新增文章趋势 (带同比环比)
// 支持按日/周/月/年维度统计
func GetNewPostsTrend(startDate, endDate string) ([]TrendData, error) {
// 默认按日统计
dateFormat := "%Y-%m-%d"
// 根据时间范围自动调整粒度 (简化逻辑如果跨度大于3个月则按月大于3年则按年)
// 这里为了简化暂时保留前端传递的日期范围后端可以根据startDate和endDate计算跨度
// 但SQL中动态GROUP BY比较复杂这里先默认按日前端可以自行聚合或者我们根据需求扩展
// 如果需要更智能的粒度可以解析startDate和endDate
// ...
// Use FROM_UNIXTIME to format timestamp
query := `
SELECT DATE_FORMAT(created_at, ?) as date, COUNT(*) as count
SELECT FROM_UNIXTIME(created_at, '%Y-%m-%d') as date, COUNT(*) as count
FROM posts
WHERE 1=1
WHERE deleted_at = 0
`
args := []interface{}{dateFormat}
args := []interface{}{}
if startDate != "" {
formattedStart := parsePostDateString(startDate, false)
startUnix := parseDateToUnix(startDate, false)
query += " AND created_at >= ?"
args = append(args, formattedStart)
args = append(args, startUnix)
} else {
// 默认最近7天
query += " AND created_at >= DATE_SUB(CURDATE(), INTERVAL 6 DAY)"
// Default 7 days
startUnix := time.Now().AddDate(0, 0, -6).Unix()
query += " AND created_at >= ?"
args = append(args, startUnix)
}
if endDate != "" {
formattedEnd := parsePostDateString(endDate, true)
endUnix := parseDateToUnix(endDate, true)
query += " AND created_at <= ?"
args = append(args, formattedEnd)
args = append(args, endUnix)
}
query += `
@@ -474,13 +483,8 @@ func GetNewPostsTrend(startDate, endDate string) ([]TrendData, error) {
// GetTopPosts 获取热门文章 (按阅读量)
func GetTopPosts(limit int) ([]models.Post, error) {
query := `
SELECT id, title, category, date, excerpt, content, read_count, is_published, created_at, updated_at
FROM posts
WHERE is_published = 1
ORDER BY read_count DESC
LIMIT ?
`
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)
if err != nil {
return nil, err
@@ -494,13 +498,13 @@ func GetTopPosts(limit int) ([]models.Post, error) {
&post.ID,
&post.Title,
&post.Category,
&post.Date,
&post.Excerpt,
&post.Content,
&post.ReadCount,
&post.IsPublished,
&post.CreatedAt,
&post.UpdatedAt,
&post.DeletedAt,
); err != nil {
continue
}

View File

@@ -10,7 +10,7 @@ import (
// GetRoles 获取所有角色
func GetRoles() ([]models.Role, error) {
query := "SELECT id, name, description, created_at, updated_at FROM roles"
query := "SELECT id, name, description, created_at, updated_at, deleted_at FROM roles WHERE deleted_at = 0"
rows, err := config.DB.Query(query)
if err != nil {
log.Printf("Error getting roles: %v", err)
@@ -21,7 +21,7 @@ func GetRoles() ([]models.Role, error) {
var roles []models.Role
for rows.Next() {
var role models.Role
if err := rows.Scan(&role.ID, &role.Name, &role.Description, &role.CreatedAt, &role.UpdatedAt); err != nil {
if err := rows.Scan(&role.ID, &role.Name, &role.Description, &role.CreatedAt, &role.UpdatedAt, &role.DeletedAt); err != nil {
log.Printf("Error scanning role: %v", err)
continue
}
@@ -38,11 +38,11 @@ func GetRoles() ([]models.Role, error) {
// GetRoleByID 根据ID获取角色
func GetRoleByID(id uint) (*models.Role, error) {
query := "SELECT id, name, description, created_at, updated_at FROM roles WHERE id = ?"
query := "SELECT id, name, description, created_at, updated_at, deleted_at FROM roles WHERE id = ? AND deleted_at = 0"
row := config.DB.QueryRow(query, id)
var role models.Role
if err := row.Scan(&role.ID, &role.Name, &role.Description, &role.CreatedAt, &role.UpdatedAt); err != nil {
if err := row.Scan(&role.ID, &role.Name, &role.Description, &role.CreatedAt, &role.UpdatedAt, &role.DeletedAt); err != nil {
log.Printf("Error getting role by ID: %v", err)
return nil, err
}
@@ -56,11 +56,11 @@ func GetRoleByID(id uint) (*models.Role, error) {
// GetRoleByName 根据名称获取角色
func GetRoleByName(name string) (*models.Role, error) {
query := "SELECT id, name, description, created_at, updated_at FROM roles WHERE name = ?"
query := "SELECT id, name, description, created_at, updated_at, deleted_at FROM roles WHERE name = ? AND deleted_at = 0"
row := config.DB.QueryRow(query, name)
var role models.Role
if err := row.Scan(&role.ID, &role.Name, &role.Description, &role.CreatedAt, &role.UpdatedAt); err != nil {
if err := row.Scan(&role.ID, &role.Name, &role.Description, &role.CreatedAt, &role.UpdatedAt, &role.DeletedAt); err != nil {
log.Printf("Error getting role by name: %v", err)
return nil, err
}
@@ -70,8 +70,9 @@ func GetRoleByName(name string) (*models.Role, error) {
// CreateRole 创建角色
func CreateRole(role *models.Role) error {
query := "INSERT INTO roles (name, description, created_at, updated_at) VALUES (?, ?, NOW(), NOW())"
result, err := config.DB.Exec(query, role.Name, role.Description)
now := time.Now().Unix()
query := "INSERT INTO roles (name, description, created_at, updated_at, deleted_at) VALUES (?, ?, ?, ?, 0)"
result, err := config.DB.Exec(query, role.Name, role.Description, now, now)
if err != nil {
log.Printf("Error creating role: %v", err)
return err
@@ -82,16 +83,17 @@ func CreateRole(role *models.Role) error {
return err
}
role.ID = uint(id)
role.CreatedAt = time.Now()
role.UpdatedAt = time.Now()
role.CreatedAt = now
role.UpdatedAt = now
return nil
}
// UpdateRole 更新角色
func UpdateRole(role *models.Role) error {
query := "UPDATE roles SET name = ?, description = ?, updated_at = NOW() WHERE id = ?"
_, err := config.DB.Exec(query, role.Name, role.Description, role.ID)
now := time.Now().Unix()
query := "UPDATE roles SET name = ?, description = ?, updated_at = ? WHERE id = ? AND deleted_at = 0"
_, err := config.DB.Exec(query, role.Name, role.Description, now, role.ID)
if err != nil {
log.Printf("Error updating role: %v", err)
return err
@@ -99,10 +101,11 @@ func UpdateRole(role *models.Role) error {
return nil
}
// DeleteRole 删除角色
// DeleteRole 删除角色 (Soft Delete)
func DeleteRole(id uint) error {
query := "DELETE FROM roles WHERE id = ?"
_, err := config.DB.Exec(query, id)
now := time.Now().Unix()
query := "UPDATE roles SET deleted_at = ? WHERE id = ?"
_, err := config.DB.Exec(query, now, id)
if err != nil {
log.Printf("Error deleting role: %v", err)
return err
@@ -151,8 +154,8 @@ func BuildRoleResponse(role *models.Role) *models.RoleResponse {
Name: role.Name,
Description: role.Description,
Permissions: BuildPermissionsResponse(role.Permissions),
CreatedAt: role.CreatedAt.Format("2006-01-02 15:04:05"),
UpdatedAt: role.UpdatedAt.Format("2006-01-02 15:04:05"),
CreatedAt: time.Unix(role.CreatedAt, 0).Format("2006-01-02 15:04:05"),
UpdatedAt: time.Unix(role.UpdatedAt, 0).Format("2006-01-02 15:04:05"),
}
}

View File

@@ -3,6 +3,7 @@ package repositories
import (
"database/sql"
"log"
"time"
"github.com/niangaodev/art-code/config"
"github.com/niangaodev/art-code/models"
@@ -10,7 +11,7 @@ import (
// GetSettings 获取所有系统配置
func GetSettings() ([]models.Setting, error) {
query := "SELECT id, key_name, value, description, created_at, updated_at FROM settings ORDER BY key_name"
query := "SELECT id, key_name, value, description, created_at, updated_at, deleted_at FROM settings WHERE deleted_at = 0 ORDER BY key_name"
rows, err := config.DB.Query(query)
if err != nil {
log.Printf("Error querying settings: %v", err)
@@ -28,6 +29,7 @@ func GetSettings() ([]models.Setting, error) {
&setting.Description,
&setting.CreatedAt,
&setting.UpdatedAt,
&setting.DeletedAt,
); err != nil {
log.Printf("Error scanning setting: %v", err)
continue
@@ -40,7 +42,7 @@ func GetSettings() ([]models.Setting, error) {
// GetSettingByKey 根据键名获取系统配置
func GetSettingByKey(keyName string) (*models.Setting, error) {
query := "SELECT id, key_name, value, description, created_at, updated_at FROM settings WHERE key_name = ?"
query := "SELECT id, key_name, value, description, created_at, updated_at, deleted_at FROM settings WHERE key_name = ? AND deleted_at = 0"
row := config.DB.QueryRow(query, keyName)
var setting models.Setting
@@ -51,6 +53,7 @@ func GetSettingByKey(keyName string) (*models.Setting, error) {
&setting.Description,
&setting.CreatedAt,
&setting.UpdatedAt,
&setting.DeletedAt,
); err != nil {
if err == sql.ErrNoRows {
return nil, nil
@@ -64,14 +67,16 @@ func GetSettingByKey(keyName string) (*models.Setting, error) {
// UpdateSetting 更新系统配置
func UpdateSetting(setting *models.Setting) error {
now := time.Now().Unix()
query := `
UPDATE settings SET value = ?, description = ?, updated_at = NOW()
WHERE key_name = ?
UPDATE settings SET value = ?, description = ?, updated_at = ?
WHERE key_name = ? AND deleted_at = 0
`
_, err := config.DB.Exec(
query,
setting.Value,
setting.Description,
now,
setting.KeyName,
)
if err != nil {
@@ -84,15 +89,18 @@ func UpdateSetting(setting *models.Setting) error {
// CreateSetting 创建系统配置
func CreateSetting(setting *models.Setting) error {
now := time.Now().Unix()
query := `
INSERT INTO settings (key_name, value, description, created_at, updated_at)
VALUES (?, ?, ?, NOW(), NOW())
INSERT INTO settings (key_name, value, description, created_at, updated_at, deleted_at)
VALUES (?, ?, ?, ?, ?, 0)
`
result, err := config.DB.Exec(
query,
setting.KeyName,
setting.Value,
setting.Description,
now,
now,
)
if err != nil {
log.Printf("Error creating setting: %v", err)
@@ -106,14 +114,17 @@ func CreateSetting(setting *models.Setting) error {
return err
}
setting.ID = uint(id)
setting.CreatedAt = now
setting.UpdatedAt = now
return nil
}
// DeleteSetting 删除系统配置
// DeleteSetting 删除系统配置 (Soft Delete)
func DeleteSetting(keyName string) error {
query := "DELETE FROM settings WHERE key_name = ?"
_, err := config.DB.Exec(query, keyName)
now := time.Now().Unix()
query := "UPDATE settings SET deleted_at = ? WHERE key_name = ?"
_, err := config.DB.Exec(query, now, keyName)
if err != nil {
log.Printf("Error deleting setting: %v", err)
return err
@@ -129,8 +140,8 @@ func BuildSettingResponse(setting *models.Setting) *models.SettingResponse {
KeyName: setting.KeyName,
Value: setting.Value,
Description: setting.Description,
CreatedAt: setting.CreatedAt.Format("2006-01-02 15:04:05"),
UpdatedAt: setting.UpdatedAt.Format("2006-01-02 15:04:05"),
CreatedAt: time.Unix(setting.CreatedAt, 0).Format("2006-01-02 15:04:05"),
UpdatedAt: time.Unix(setting.UpdatedAt, 0).Format("2006-01-02 15:04:05"),
}
}

View File

@@ -3,6 +3,7 @@ package repositories
import (
"database/sql"
"log"
"time"
"github.com/niangaodev/art-code/config"
"github.com/niangaodev/art-code/models"
@@ -10,7 +11,7 @@ import (
// GetSnippets 获取所有代码片段
func GetSnippets() ([]models.Snippet, error) {
query := "SELECT id, title, code, type, description, view_count, created_at, updated_at FROM snippets ORDER BY created_at DESC"
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"
rows, err := config.DB.Query(query)
if err != nil {
log.Printf("Error querying snippets: %v", err)
@@ -30,6 +31,7 @@ func GetSnippets() ([]models.Snippet, error) {
&snippet.ViewCount,
&snippet.CreatedAt,
&snippet.UpdatedAt,
&snippet.DeletedAt,
); err != nil {
log.Printf("Error scanning snippet: %v", err)
continue
@@ -42,7 +44,7 @@ func GetSnippets() ([]models.Snippet, error) {
// GetSnippetByID 根据ID获取代码片段
func GetSnippetByID(id string) (*models.Snippet, error) {
query := "SELECT id, title, code, type, description, view_count, created_at, updated_at FROM snippets WHERE id = ?"
query := "SELECT id, title, code, type, description, view_count, created_at, updated_at, deleted_at FROM snippets WHERE id = ? AND deleted_at = 0"
row := config.DB.QueryRow(query, id)
var snippet models.Snippet
@@ -55,6 +57,7 @@ func GetSnippetByID(id string) (*models.Snippet, error) {
&snippet.ViewCount,
&snippet.CreatedAt,
&snippet.UpdatedAt,
&snippet.DeletedAt,
); err != nil {
if err == sql.ErrNoRows {
return nil, nil
@@ -93,9 +96,10 @@ func BuildSnippetsResponse(snippets []models.Snippet) []models.SnippetResponse {
// CreateSnippet 创建代码片段
func CreateSnippet(snippet *models.Snippet) error {
now := time.Now().Unix()
query := `
INSERT INTO snippets (id, title, code, type, description, view_count, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, 0, NOW(), NOW())
INSERT INTO snippets (id, title, code, type, description, view_count, created_at, updated_at, deleted_at)
VALUES (?, ?, ?, ?, ?, 0, ?, ?, 0)
`
_, err := config.DB.Exec(
query,
@@ -104,6 +108,8 @@ func CreateSnippet(snippet *models.Snippet) error {
snippet.Code,
snippet.Type,
snippet.Description,
now,
now,
)
if err != nil {
log.Printf("Error creating snippet: %v", err)
@@ -115,9 +121,10 @@ func CreateSnippet(snippet *models.Snippet) error {
// UpdateSnippet 更新代码片段
func UpdateSnippet(snippet *models.Snippet) error {
now := time.Now().Unix()
query := `
UPDATE snippets SET title = ?, code = ?, type = ?, description = ?, updated_at = NOW()
WHERE id = ?
UPDATE snippets SET title = ?, code = ?, type = ?, description = ?, updated_at = ?
WHERE id = ? AND deleted_at = 0
`
_, err := config.DB.Exec(
query,
@@ -125,6 +132,7 @@ func UpdateSnippet(snippet *models.Snippet) error {
snippet.Code,
snippet.Type,
snippet.Description,
now,
snippet.ID,
)
if err != nil {
@@ -135,10 +143,11 @@ func UpdateSnippet(snippet *models.Snippet) error {
return nil
}
// DeleteSnippet 删除代码片段
// DeleteSnippet 删除代码片段 (Soft Delete)
func DeleteSnippet(id string) error {
query := "DELETE FROM snippets WHERE id = ?"
_, err := config.DB.Exec(query, id)
now := time.Now().Unix()
query := "UPDATE snippets SET deleted_at = ? WHERE id = ?"
_, err := config.DB.Exec(query, now, id)
if err != nil {
log.Printf("Error deleting snippet: %v", err)
return err
@@ -150,7 +159,7 @@ func DeleteSnippet(id string) error {
// GetSnippetCount 获取代码片段总数
func GetSnippetCount() (int, error) {
var count int
query := "SELECT COUNT(*) FROM snippets"
query := "SELECT COUNT(*) FROM snippets WHERE deleted_at = 0"
row := config.DB.QueryRow(query)
err := row.Scan(&count)

View File

@@ -3,6 +3,7 @@ package repositories
import (
"database/sql"
"log"
"time"
"github.com/niangaodev/art-code/config"
"github.com/niangaodev/art-code/models"
@@ -10,7 +11,7 @@ import (
// GetTags 获取所有标签
func GetTags() ([]models.Tag, error) {
query := "SELECT id, name, slug, created_at, updated_at FROM tags ORDER BY name ASC"
query := "SELECT id, name, slug, created_at, updated_at, deleted_at FROM tags WHERE deleted_at = 0 ORDER BY name ASC"
rows, err := config.DB.Query(query)
if err != nil {
log.Printf("Error querying tags: %v", err)
@@ -27,6 +28,7 @@ func GetTags() ([]models.Tag, error) {
&tag.Slug,
&tag.CreatedAt,
&tag.UpdatedAt,
&tag.DeletedAt,
); err != nil {
log.Printf("Error scanning tag: %v", err)
continue
@@ -39,7 +41,7 @@ func GetTags() ([]models.Tag, error) {
// GetTagByID 根据ID获取标签
func GetTagByID(id uint) (*models.Tag, error) {
query := "SELECT id, name, slug, created_at, updated_at FROM tags WHERE id = ?"
query := "SELECT id, name, slug, created_at, updated_at, deleted_at FROM tags WHERE id = ? AND deleted_at = 0"
row := config.DB.QueryRow(query, id)
var tag models.Tag
@@ -49,6 +51,7 @@ func GetTagByID(id uint) (*models.Tag, error) {
&tag.Slug,
&tag.CreatedAt,
&tag.UpdatedAt,
&tag.DeletedAt,
); err != nil {
if err == sql.ErrNoRows {
return nil, nil
@@ -62,7 +65,7 @@ func GetTagByID(id uint) (*models.Tag, error) {
// GetTagBySlug 根据Slug获取标签
func GetTagBySlug(slug string) (*models.Tag, error) {
query := "SELECT id, name, slug, created_at, updated_at FROM tags WHERE slug = ?"
query := "SELECT id, name, slug, created_at, updated_at, deleted_at FROM tags WHERE slug = ? AND deleted_at = 0"
row := config.DB.QueryRow(query, slug)
var tag models.Tag
@@ -72,6 +75,7 @@ func GetTagBySlug(slug string) (*models.Tag, error) {
&tag.Slug,
&tag.CreatedAt,
&tag.UpdatedAt,
&tag.DeletedAt,
); err != nil {
if err == sql.ErrNoRows {
return nil, nil
@@ -85,14 +89,17 @@ func GetTagBySlug(slug string) (*models.Tag, error) {
// CreateTag 创建标签
func CreateTag(tag *models.Tag) error {
now := time.Now().Unix()
query := `
INSERT INTO tags (name, slug, created_at, updated_at)
VALUES (?, ?, NOW(), NOW())
INSERT INTO tags (name, slug, created_at, updated_at, deleted_at)
VALUES (?, ?, ?, ?, 0)
`
result, err := config.DB.Exec(
query,
tag.Name,
tag.Slug,
now,
now,
)
if err != nil {
log.Printf("Error creating tag: %v", err)
@@ -106,20 +113,24 @@ func CreateTag(tag *models.Tag) error {
return err
}
tag.ID = uint(id)
tag.CreatedAt = now
tag.UpdatedAt = now
return nil
}
// UpdateTag 更新标签
func UpdateTag(tag *models.Tag) error {
now := time.Now().Unix()
query := `
UPDATE tags SET name = ?, slug = ?, updated_at = NOW()
WHERE id = ?
UPDATE tags SET name = ?, slug = ?, updated_at = ?
WHERE id = ? AND deleted_at = 0
`
_, err := config.DB.Exec(
query,
tag.Name,
tag.Slug,
now,
tag.ID,
)
if err != nil {
@@ -130,21 +141,32 @@ func UpdateTag(tag *models.Tag) error {
return nil
}
// DeleteTag 删除标签
// DeleteTag 删除标签 (Soft Delete)
func DeleteTag(id uint) error {
// 先删除关联的文章标签关系
query := "DELETE FROM post_tags WHERE tag_id = ?"
_, err := config.DB.Exec(query, id)
now := time.Now().Unix()
// 软删除标签
query := "UPDATE tags SET deleted_at = ? WHERE id = ?"
_, err := config.DB.Exec(query, now, id)
if err != nil {
log.Printf("Error deleting post-tag relationships: %v", err)
log.Printf("Error deleting tag: %v", err)
return err
}
// 再删除标签
query = "DELETE FROM tags WHERE id = ?"
_, err = config.DB.Exec(query, id)
// 注意post_tags 关联表通常不做软删除,或者可以级联删除,或者在查询时过滤。
// 为了保持数据一致性,我们可以物理删除 post_tags 中的关联,或者也软删除(如果支持)。
// 这里选择物理删除关联,因为关联关系是“从属”的,标签没了,关系也没意义。
// 但如果是“软删除”,也许想保留恢复可能?
// 如果保留恢复可能post_tags 也应该软删除。
// 但 post_tags 没有 deleted_at。
// 所以:物理删除关联,或者不处理关联(查询时 JOIN tags 会过滤掉)。
// 最好是物理删除关联,或者保留关联但因为 tag 被软删除而不可见。
// 这里保留原有逻辑:物理删除关联。
deleteRelQuery := "DELETE FROM post_tags WHERE tag_id = ?"
_, err = config.DB.Exec(deleteRelQuery, id)
if err != nil {
log.Printf("Error deleting tag: %v", err)
log.Printf("Error deleting post-tag relationships: %v", err)
// Continue even if relation delete fails? No, return error.
return err
}
@@ -154,10 +176,10 @@ func DeleteTag(id uint) error {
// GetTagsByPostID 根据文章ID获取标签
func GetTagsByPostID(postID string) ([]models.Tag, error) {
query := `
SELECT t.id, t.name, t.slug, t.created_at, t.updated_at
SELECT t.id, t.name, t.slug, t.created_at, t.updated_at, t.deleted_at
FROM tags t
JOIN post_tags pt ON t.id = pt.tag_id
WHERE pt.post_id = ?
WHERE pt.post_id = ? AND t.deleted_at = 0
ORDER BY t.name ASC
`
rows, err := config.DB.Query(query, postID)
@@ -176,6 +198,7 @@ func GetTagsByPostID(postID string) ([]models.Tag, error) {
&tag.Slug,
&tag.CreatedAt,
&tag.UpdatedAt,
&tag.DeletedAt,
); err != nil {
log.Printf("Error scanning tag: %v", err)
continue
@@ -188,11 +211,12 @@ func GetTagsByPostID(postID string) ([]models.Tag, error) {
// AddTagToPost 为文章添加标签
func AddTagToPost(postID string, tagID uint) error {
now := time.Now().Unix()
query := `
INSERT IGNORE INTO post_tags (post_id, tag_id, created_at)
VALUES (?, ?, NOW())
VALUES (?, ?, ?)
`
_, err := config.DB.Exec(query, postID, tagID)
_, err := config.DB.Exec(query, postID, tagID, now)
if err != nil {
log.Printf("Error adding tag to post: %v", err)
return err
@@ -216,11 +240,11 @@ func RemoveTagFromPost(postID string, tagID uint) error {
// GetPostsByTagID 根据标签ID获取文章
func GetPostsByTagID(tagID uint) ([]models.Post, error) {
query := `
SELECT p.id, p.title, p.category, p.date, p.excerpt, p.content, p.read_count, p.is_published, p.created_at, p.updated_at
SELECT p.id, p.title, p.category, p.excerpt, p.content, p.read_count, p.is_published, p.created_at, p.updated_at, p.deleted_at
FROM posts p
JOIN post_tags pt ON p.id = pt.post_id
WHERE pt.tag_id = ? AND p.is_published = 1
ORDER BY p.date DESC
WHERE pt.tag_id = ? AND p.is_published = 1 AND p.deleted_at = 0
ORDER BY p.created_at DESC
`
rows, err := config.DB.Query(query, tagID)
if err != nil {
@@ -236,13 +260,13 @@ func GetPostsByTagID(tagID uint) ([]models.Post, error) {
&post.ID,
&post.Title,
&post.Category,
&post.Date,
&post.Excerpt,
&post.Content,
&post.ReadCount,
&post.IsPublished,
&post.CreatedAt,
&post.UpdatedAt,
&post.DeletedAt,
); err != nil {
log.Printf("Error scanning post: %v", err)
continue

View File

@@ -3,6 +3,7 @@ package repositories
import (
"database/sql"
"log"
"time"
"github.com/niangaodev/art-code/config"
"github.com/niangaodev/art-code/models"
@@ -11,10 +12,10 @@ import (
// GetUserByUsername 根据用户名获取用户
func GetUserByUsername(username string) (*models.User, error) {
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
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.username = ?
WHERE u.username = ? AND u.deleted_at = 0
`
row := config.DB.QueryRow(query, username)
@@ -32,6 +33,7 @@ func GetUserByUsername(username string) (*models.User, error) {
&user.IsActive,
&user.CreatedAt,
&user.UpdatedAt,
&user.DeletedAt,
); err != nil {
if err == sql.ErrNoRows {
return nil, nil
@@ -53,10 +55,10 @@ func GetUserByUsername(username string) (*models.User, error) {
// GetUserByID 根据ID获取用户
func GetUserByID(id uint) (*models.User, error) {
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
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.id = ?
WHERE u.id = ? AND u.deleted_at = 0
`
row := config.DB.QueryRow(query, id)
@@ -74,6 +76,7 @@ func GetUserByID(id uint) (*models.User, error) {
&user.IsActive,
&user.CreatedAt,
&user.UpdatedAt,
&user.DeletedAt,
); err != nil {
if err == sql.ErrNoRows {
return nil, nil
@@ -95,9 +98,10 @@ func GetUserByID(id uint) (*models.User, error) {
// GetUsers 获取所有用户
func GetUsers() ([]models.User, error) {
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
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
`
rows, err := config.DB.Query(query)
@@ -123,6 +127,7 @@ func GetUsers() ([]models.User, error) {
&user.IsActive,
&user.CreatedAt,
&user.UpdatedAt,
&user.DeletedAt,
); err != nil {
log.Printf("Error scanning user: %v", err)
continue
@@ -151,9 +156,10 @@ func CreateUser(user *models.User) error {
}
}
now := time.Now().Unix()
query := `
INSERT INTO users (username, email, password_hash, role_id, role, is_active, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, NOW(), NOW())
INSERT INTO users (username, email, password_hash, role_id, role, is_active, created_at, updated_at, deleted_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, 0)
`
var roleID interface{}
@@ -171,6 +177,8 @@ func CreateUser(user *models.User) error {
roleID,
user.Role, // Fallback legacy column
user.IsActive,
now,
now,
)
if err != nil {
log.Printf("Error creating user: %v", err)
@@ -184,6 +192,8 @@ func CreateUser(user *models.User) error {
return err
}
user.ID = uint(id)
user.CreatedAt = now
user.UpdatedAt = now
return nil
}
@@ -198,9 +208,10 @@ func UpdateUser(user *models.User) error {
}
}
now := time.Now().Unix()
query := `
UPDATE users SET username = ?, email = ?, role_id = ?, role = ?, is_active = ?, updated_at = NOW()
WHERE id = ?
UPDATE users SET username = ?, email = ?, role_id = ?, role = ?, is_active = ?, updated_at = ?
WHERE id = ? AND deleted_at = 0
`
var roleID interface{}
@@ -217,6 +228,7 @@ func UpdateUser(user *models.User) error {
roleID,
user.Role,
user.IsActive,
now,
user.ID,
)
if err != nil {
@@ -229,11 +241,12 @@ func UpdateUser(user *models.User) error {
// UpdateUserPassword 更新用户密码
func UpdateUserPassword(id uint, passwordHash string) error {
now := time.Now().Unix()
query := `
UPDATE users SET password_hash = ?, updated_at = NOW()
WHERE id = ?
UPDATE users SET password_hash = ?, updated_at = ?
WHERE id = ? AND deleted_at = 0
`
_, err := config.DB.Exec(query, passwordHash, id)
_, err := config.DB.Exec(query, passwordHash, now, id)
if err != nil {
log.Printf("Error updating user password: %v", err)
return err
@@ -242,10 +255,11 @@ func UpdateUserPassword(id uint, passwordHash string) error {
return nil
}
// DeleteUser 删除用户
// DeleteUser 删除用户 (Soft Delete)
func DeleteUser(id uint) error {
query := "DELETE FROM users WHERE id = ?"
_, err := config.DB.Exec(query, id)
now := time.Now().Unix()
query := "UPDATE users SET deleted_at = ? WHERE id = ?"
_, err := config.DB.Exec(query, now, id)
if err != nil {
log.Printf("Error deleting user: %v", err)
return err
@@ -257,7 +271,7 @@ func DeleteUser(id uint) error {
// GetUserCount 获取用户总数
func GetUserCount() (int, error) {
var count int
query := "SELECT COUNT(*) FROM users"
query := "SELECT COUNT(*) FROM users WHERE deleted_at = 0"
row := config.DB.QueryRow(query)
err := row.Scan(&count)
@@ -278,8 +292,8 @@ func BuildUserResponse(user *models.User) *models.UserResponse {
RoleID: user.RoleID,
Role: user.Role,
IsActive: user.IsActive,
CreatedAt: user.CreatedAt.Format("2006-01-02 15:04:05"),
UpdatedAt: user.UpdatedAt.Format("2006-01-02 15:04:05"),
CreatedAt: time.Unix(user.CreatedAt, 0).Format("2006-01-02 15:04:05"),
UpdatedAt: time.Unix(user.UpdatedAt, 0).Format("2006-01-02 15:04:05"),
}
}

View File

@@ -3,6 +3,7 @@ package repositories
import (
"database/sql"
"log"
"time"
"github.com/niangaodev/art-code/config"
"github.com/niangaodev/art-code/models"
@@ -10,7 +11,7 @@ import (
// GetWorks 获取所有作品
func GetWorks() ([]models.Work, error) {
query := "SELECT id, title, category, year, hero_img, description, is_featured, created_at, updated_at FROM works"
query := "SELECT id, title, category, year, hero_img, description, is_featured, created_at, updated_at, deleted_at FROM works WHERE deleted_at = 0"
rows, err := config.DB.Query(query)
if err != nil {
log.Printf("Error querying works: %v", err)
@@ -31,6 +32,7 @@ func GetWorks() ([]models.Work, error) {
&work.IsFeatured,
&work.CreatedAt,
&work.UpdatedAt,
&work.DeletedAt,
); err != nil {
log.Printf("Error scanning work: %v", err)
continue
@@ -43,7 +45,7 @@ func GetWorks() ([]models.Work, error) {
// GetWorkByID 根据ID获取作品
func GetWorkByID(id string) (*models.Work, error) {
query := "SELECT id, title, category, year, hero_img, description, is_featured, created_at, updated_at FROM works WHERE id = ?"
query := "SELECT id, title, category, year, hero_img, description, is_featured, created_at, updated_at, deleted_at FROM works WHERE id = ? AND deleted_at = 0"
row := config.DB.QueryRow(query, id)
var work models.Work
@@ -57,6 +59,7 @@ func GetWorkByID(id string) (*models.Work, error) {
&work.IsFeatured,
&work.CreatedAt,
&work.UpdatedAt,
&work.DeletedAt,
); err != nil {
if err == sql.ErrNoRows {
return nil, nil
@@ -70,7 +73,8 @@ func GetWorkByID(id string) (*models.Work, error) {
// GetWorkTechStack 获取作品的技术栈
func GetWorkTechStack(workID string) ([]models.WorkTechStack, error) {
query := "SELECT id, work_id, category, item, created_at FROM work_tech_stack WHERE work_id = ?"
// work_tech_stack has deleted_at? I added it to all tables.
query := "SELECT id, work_id, category, item, created_at, deleted_at FROM work_tech_stack WHERE work_id = ? AND deleted_at = 0"
rows, err := config.DB.Query(query, workID)
if err != nil {
log.Printf("Error querying work tech stack: %v", err)
@@ -87,6 +91,7 @@ func GetWorkTechStack(workID string) ([]models.WorkTechStack, error) {
&techStack.Category,
&techStack.Item,
&techStack.CreatedAt,
&techStack.DeletedAt,
); err != nil {
log.Printf("Error scanning work tech stack: %v", err)
continue
@@ -99,7 +104,8 @@ func GetWorkTechStack(workID string) ([]models.WorkTechStack, error) {
// GetWorkGallery 获取作品的图库
func GetWorkGallery(workID string) ([]models.WorkGallery, error) {
query := "SELECT id, work_id, image_url, sort_order, description, created_at FROM work_gallery WHERE work_id = ? ORDER BY sort_order"
// work_gallery has deleted_at
query := "SELECT id, work_id, image_url, sort_order, description, created_at, deleted_at FROM work_gallery WHERE work_id = ? AND deleted_at = 0 ORDER BY sort_order"
rows, err := config.DB.Query(query, workID)
if err != nil {
log.Printf("Error querying work gallery: %v", err)
@@ -117,6 +123,7 @@ func GetWorkGallery(workID string) ([]models.WorkGallery, error) {
&gallery.SortOrder,
&gallery.Description,
&gallery.CreatedAt,
&gallery.DeletedAt,
); err != nil {
log.Printf("Error scanning work gallery: %v", err)
continue
@@ -188,7 +195,7 @@ func BuildWorkResponse(work *models.Work) (*models.WorkResponse, error) {
// GetNextWorkID 获取下一个作品ID简单实现实际可能需要更复杂的逻辑
func GetNextWorkID(currentID string) (string, error) {
// 获取所有作品ID
query := "SELECT id FROM works"
query := "SELECT id FROM works WHERE deleted_at = 0"
rows, err := config.DB.Query(query)
if err != nil {
return "", err
@@ -227,9 +234,10 @@ func GetNextWorkID(currentID string) (string, error) {
// CreateWork 创建作品
func CreateWork(work *models.Work) error {
now := time.Now().Unix()
query := `
INSERT INTO works (id, title, category, year, hero_img, description, is_featured, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, NOW(), NOW())
INSERT INTO works (id, title, category, year, hero_img, description, is_featured, created_at, updated_at, deleted_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 0)
`
_, err := config.DB.Exec(
query,
@@ -240,6 +248,8 @@ func CreateWork(work *models.Work) error {
work.HeroImg,
work.Description,
work.IsFeatured,
now,
now,
)
if err != nil {
log.Printf("Error creating work: %v", err)
@@ -251,9 +261,10 @@ func CreateWork(work *models.Work) error {
// UpdateWork 更新作品
func UpdateWork(work *models.Work) error {
now := time.Now().Unix()
query := `
UPDATE works SET title = ?, category = ?, year = ?, hero_img = ?, description = ?, is_featured = ?, updated_at = NOW()
WHERE id = ?
UPDATE works SET title = ?, category = ?, year = ?, hero_img = ?, description = ?, is_featured = ?, updated_at = ?
WHERE id = ? AND deleted_at = 0
`
_, err := config.DB.Exec(
query,
@@ -263,6 +274,7 @@ func UpdateWork(work *models.Work) error {
work.HeroImg,
work.Description,
work.IsFeatured,
now,
work.ID,
)
if err != nil {
@@ -273,10 +285,11 @@ func UpdateWork(work *models.Work) error {
return nil
}
// DeleteWork 删除作品
// DeleteWork 删除作品 (Soft Delete)
func DeleteWork(id string) error {
query := "DELETE FROM works WHERE id = ?"
_, err := config.DB.Exec(query, id)
now := time.Now().Unix()
query := "UPDATE works SET deleted_at = ? WHERE id = ?"
_, err := config.DB.Exec(query, now, id)
if err != nil {
log.Printf("Error deleting work: %v", err)
return err
@@ -288,7 +301,7 @@ func DeleteWork(id string) error {
// GetWorkCount 获取作品总数
func GetWorkCount() (int, error) {
var count int
query := "SELECT COUNT(*) FROM works"
query := "SELECT COUNT(*) FROM works WHERE deleted_at = 0"
row := config.DB.QueryRow(query)
err := row.Scan(&count)