初始化

This commit is contained in:
李琦
2026-01-15 13:51:44 +08:00
commit b7b6d3e39e
156 changed files with 38913 additions and 0 deletions

27
server/.gitignore vendored Normal file
View File

@@ -0,0 +1,27 @@
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib
# Test binary, built with `go test -c`
*.test
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
# Dependency directories (remove the comment below to include it)
# vendor/
# IDE directories and files
.vscode/
.idea/
*.swp
*.swo
*~
# Environment variables
.env
.env.local
.env.*.local

64
server/config/db.go Normal file
View File

@@ -0,0 +1,64 @@
package config
import (
"fmt"
"log"
"time"
"database/sql"
_ "github.com/go-sql-driver/mysql"
)
// DBConfig 数据库配置
var DBConfig = struct {
Username string
Password string
Host string
Port string
DBName string
}{
Username: "root",
Password: "root",
Host: "127.0.0.1",
Port: "3306",
DBName: "nl_blog",
}
// DB 全局数据库连接池
var DB *sql.DB
// InitDB 初始化数据库连接
func InitDB() {
// 构建DSN
dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=True&loc=Local",
DBConfig.Username, DBConfig.Password, DBConfig.Host, DBConfig.Port, DBConfig.DBName)
// 打开数据库连接
var err error
DB, err = sql.Open("mysql", dsn)
if err != nil {
log.Fatalf("Failed to open database connection: %v", err)
}
// 配置连接池
DB.SetMaxOpenConns(25) // 最大打开连接数
DB.SetMaxIdleConns(5) // 最大空闲连接数
DB.SetConnMaxLifetime(5 * time.Minute) // 连接最大生命周期
DB.SetConnMaxIdleTime(30 * time.Second) // 连接最大空闲时间
// 测试连接
if err := DB.Ping(); err != nil {
log.Fatalf("Failed to ping database: %v", err)
}
log.Println("Database connection established successfully!")
}
// CloseDB 关闭数据库连接
func CloseDB() {
if DB != nil {
DB.Close()
log.Println("Database connection closed!")
}
}

45
server/go.mod Normal file
View File

@@ -0,0 +1,45 @@
module github.com/niangaodev/art-code
go 1.25.5
require (
github.com/gin-gonic/gin v1.11.0
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
golang.org/x/crypto v0.40.0
)
require (
filippo.io/edwards25519 v1.1.0 // indirect
github.com/bytedance/sonic v1.14.0 // indirect
github.com/bytedance/sonic/loader v0.3.0 // indirect
github.com/cloudwego/base64x v0.1.6 // indirect
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
github.com/gin-contrib/sse v1.1.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.27.0 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/goccy/go-yaml v1.18.0 // indirect
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/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
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/quic-go/qpack v0.5.1 // indirect
github.com/quic-go/quic-go v0.54.0 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.3.0 // indirect
go.uber.org/mock v0.5.0 // indirect
golang.org/x/arch v0.20.0 // indirect
golang.org/x/mod v0.25.0 // indirect
golang.org/x/net v0.42.0 // indirect
golang.org/x/sync v0.16.0 // indirect
golang.org/x/sys v0.35.0 // indirect
golang.org/x/text v0.27.0 // indirect
golang.org/x/tools v0.34.0 // indirect
google.golang.org/protobuf v1.36.9 // indirect
)

96
server/go.sum Normal file
View File

@@ -0,0 +1,96 @@
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/bytedance/sonic v1.14.0 h1:/OfKt8HFw0kh2rj8N0F6C/qPGRESq0BbaNZgcNXXzQQ=
github.com/bytedance/sonic v1.14.0/go.mod h1:WoEbx8WTcFJfzCe0hbmyTGrfjt8PzNEBdxlNUO24NhA=
github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA=
github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk=
github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4=
github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo=
github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
github.com/quic-go/quic-go v0.54.0 h1:6s1YB9QotYI6Ospeiguknbp2Znb/jZYjZLRXn9kMQBg=
github.com/quic-go/quic-go v0.54.0/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA=
github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU=
go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM=
golang.org/x/arch v0.20.0 h1:dx1zTU0MAE98U+TQ8BLl7XsJbgze2WnNKF/8tGp/Q6c=
golang.org/x/arch v0.20.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM=
golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY=
golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w=
golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs=
golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8=
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=
golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo=
golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg=
google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw=
google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

54
server/handlers/auth.go Normal file
View File

@@ -0,0 +1,54 @@
package handlers
import (
"net/http"
"github.com/gin-gonic/gin"
"github.com/niangaodev/art-code/middleware"
"github.com/niangaodev/art-code/models"
"github.com/niangaodev/art-code/repositories"
"golang.org/x/crypto/bcrypt"
)
// AdminLogin 管理员登录
func AdminLogin(c *gin.Context) {
var req models.LoginRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request"})
return
}
// 获取用户
user, err := repositories.GetUserByUsername(req.Username)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get user"})
return
}
if user == nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid username or password"})
return
}
// 验证密码
if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(req.Password)); err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid username or password"})
return
}
// 生成JWT令牌
token, expire, err := middleware.GenerateToken(user.ID, user.Username, user.Role)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to generate token"})
return
}
// 构建响应
response := models.LoginResponse{
Token: token,
User: *repositories.BuildUserResponse(user),
Expire: expire,
}
c.JSON(http.StatusOK, response)
}

View File

@@ -0,0 +1,46 @@
package handlers
import (
"net/http"
"github.com/gin-gonic/gin"
"github.com/niangaodev/art-code/repositories"
)
// 仪表盘数据处理函数
func AdminGetDashboardStats(c *gin.Context) {
// 获取统计数据
userCount, err := repositories.GetUserCount()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get user count"})
return
}
postCount, err := repositories.GetPostCount()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get post count"})
return
}
workCount, err := repositories.GetWorkCount()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get work count"})
return
}
snippetCount, err := repositories.GetSnippetCount()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get snippet count"})
return
}
// 构建响应
stats := gin.H{
"users": userCount,
"posts": postCount,
"works": workCount,
"snippets": snippetCount,
}
c.JSON(http.StatusOK, stats)
}

81
server/handlers/log.go Normal file
View File

@@ -0,0 +1,81 @@
package handlers
import (
"fmt"
"net/http"
"github.com/gin-gonic/gin"
"github.com/niangaodev/art-code/repositories"
)
func AdminGetRecentActivities(c *gin.Context) {
// 获取最近10条操作日志
logs, _, err := repositories.GetOperationLogs(1, 10)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get recent activities"})
return
}
// 构建响应
var activities []gin.H
for _, log := range logs {
// 根据HTTP方法设置图标
var icon string
switch log.Method {
case "POST":
icon = ""
case "PUT", "PATCH":
icon = "✏️"
case "DELETE":
icon = "🗑️"
case "GET":
icon = "📋"
case "OPTIONS":
icon = "⚙️"
default:
icon = "📋"
}
// 构建活动文本描述
text := fmt.Sprintf("%s %s", log.Method, log.Path)
activities = append(activities, gin.H{
"id": log.ID,
"icon": icon,
"text": text,
"time": log.CreatedAt.Format("2006-01-02 15:04:05"),
})
}
c.JSON(http.StatusOK, activities)
}
// 获取操作日志列表
func AdminGetOperationLogs(c *gin.Context) {
// 获取分页参数
page := 1
pageSize := 10
// 从查询参数中获取分页信息
if c.Query("page") != "" {
c.ShouldBindQuery(&page)
}
if c.Query("pageSize") != "" {
c.ShouldBindQuery(&pageSize)
}
// 获取操作日志
logs, total, err := repositories.GetOperationLogs(page, pageSize)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get operation logs"})
return
}
c.JSON(http.StatusOK, gin.H{
"list": repositories.BuildOperationLogsResponse(logs),
"total": total,
"page": page,
"size": pageSize,
})
}

182
server/handlers/post.go Normal file
View File

@@ -0,0 +1,182 @@
package handlers
import (
"fmt"
"log"
"net/http"
"github.com/gin-gonic/gin"
"github.com/niangaodev/art-code/models"
"github.com/niangaodev/art-code/repositories"
)
// 获取博客文章列表
func GetPosts(c *gin.Context) {
// 从数据库获取所有博客文章
posts, err := repositories.GetPosts()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch posts"})
return
}
// 构建响应
responses := repositories.BuildPostsResponse(posts)
c.JSON(http.StatusOK, responses)
}
func GetPost(c *gin.Context) {
id := c.Param("id")
// 从数据库获取博客文章
post, err := repositories.GetPostByID(id)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch post"})
return
}
if post == nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Post not found"})
return
}
// 构建响应,包含内容
response := repositories.BuildPostResponse(post, true)
c.JSON(http.StatusOK, response)
}
func GetPostsByTagID(c *gin.Context) {
// 解析标签ID
idStr := c.Param("id")
var id uint
_, err := fmt.Sscanf(idStr, "%d", &id)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid tag ID"})
return
}
// 从数据库获取标签相关的文章
posts, err := repositories.GetPostsByTagID(id)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch posts by tag"})
return
}
// 构建响应
responses := repositories.BuildPostsResponse(posts)
c.JSON(http.StatusOK, responses)
}
// 获取所有文章(包括未发布的)
func AdminGetPosts(c *gin.Context) {
posts, err := repositories.GetAllPosts()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get posts"})
return
}
c.JSON(http.StatusOK, repositories.BuildPostsResponse(posts))
}
// 创建文章
func AdminCreatePost(c *gin.Context) {
var post models.Post
if err := c.ShouldBindJSON(&post); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request"})
return
}
// 创建文章
if err := repositories.CreatePost(&post); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create post"})
return
}
// 保存历史记录
userID, _ := c.Get("userID")
if err := repositories.SavePostHistory(&post, userID.(uint)); err != nil {
log.Printf("Error saving post history: %v", err)
}
c.JSON(http.StatusOK, gin.H{"message": "Post created successfully"})
}
// 更新文章
func AdminUpdatePost(c *gin.Context) {
postID := c.Param("id")
var post models.Post
if err := c.ShouldBindJSON(&post); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request"})
return
}
// 设置文章ID
post.ID = postID
// 更新文章
if err := repositories.UpdatePost(&post); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update post"})
return
}
// 保存历史记录
userID, _ := c.Get("userID")
if err := repositories.SavePostHistory(&post, userID.(uint)); err != nil {
log.Printf("Error saving post history: %v", err)
}
c.JSON(http.StatusOK, gin.H{"message": "Post updated successfully"})
}
// 删除文章
func AdminDeletePost(c *gin.Context) {
postID := c.Param("id")
// 删除文章
if err := repositories.DeletePost(postID); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete post"})
return
}
c.JSON(http.StatusOK, gin.H{"message": "Post deleted successfully"})
}
// 获取文章历史记录
func AdminGetPostHistory(c *gin.Context) {
postID := c.Param("id")
history, err := repositories.GetPostHistory(postID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get post history"})
return
}
c.JSON(http.StatusOK, repositories.BuildPostHistoryResponses(history))
}
// 获取指定版本的文章历史记录
func AdminGetPostHistoryByVersion(c *gin.Context) {
postID := c.Param("id")
version := c.Param("version")
// 转换版本号为uint
var versionUint uint
_, err := fmt.Sscanf(version, "%d", &versionUint)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid version"})
return
}
history, err := repositories.GetPostHistoryByVersion(postID, versionUint)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get post history"})
return
}
if history == nil {
c.JSON(http.StatusNotFound, gin.H{"error": "History not found"})
return
}
c.JSON(http.StatusOK, repositories.BuildPostHistoryResponse(history))
}

145
server/handlers/role.go Normal file
View File

@@ -0,0 +1,145 @@
package handlers
import (
"fmt"
"net/http"
"strconv"
"github.com/gin-gonic/gin"
"github.com/niangaodev/art-code/models"
"github.com/niangaodev/art-code/repositories"
)
// 获取角色列表
func AdminGetRoles(c *gin.Context) {
roles, err := repositories.GetRoles()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get roles"})
return
}
c.JSON(http.StatusOK, repositories.BuildRolesResponse(roles))
}
// 创建角色
func AdminCreateRole(c *gin.Context) {
var role models.Role
if err := c.ShouldBindJSON(&role); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request"})
return
}
// 创建角色
if err := repositories.CreateRole(&role); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create role"})
return
}
c.JSON(http.StatusOK, gin.H{"message": "Role created successfully"})
}
// 更新角色
func AdminUpdateRole(c *gin.Context) {
roleID := c.Param("id")
var role models.Role
if err := c.ShouldBindJSON(&role); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request"})
return
}
// 转换角色ID为uint
var idUint uint
_, err := fmt.Sscanf(roleID, "%d", &idUint)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid role ID"})
return
}
// 设置角色ID
role.ID = idUint
// 更新角色
if err := repositories.UpdateRole(&role); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update role"})
return
}
c.JSON(http.StatusOK, gin.H{"message": "Role updated successfully"})
}
// 删除角色
func AdminDeleteRole(c *gin.Context) {
roleID := c.Param("id")
// 转换角色ID为uint
var idUint uint
_, err := fmt.Sscanf(roleID, "%d", &idUint)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid role ID"})
return
}
// 删除角色
if err := repositories.DeleteRole(idUint); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete role"})
return
}
c.JSON(http.StatusOK, gin.H{"message": "Role deleted successfully"})
}
// 获取所有权限列表
func AdminGetPermissions(c *gin.Context) {
permissions, err := repositories.GetPermissions()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get permissions"})
return
}
c.JSON(http.StatusOK, repositories.BuildPermissionsResponse(permissions))
}
// 更新角色权限请求结构
type UpdateRolePermissionsRequest struct {
PermissionIDs []uint `json:"permissionIds" binding:"required"`
}
// 更新角色的权限
func AdminUpdateRolePermissions(c *gin.Context) {
roleIDStr := c.Param("id")
// 转换角色ID
var roleID uint
id, err := strconv.ParseUint(roleIDStr, 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid role ID"})
return
}
roleID = uint(id)
// 绑定请求数据
var req UpdateRolePermissionsRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request format"})
return
}
// 检查角色是否存在
role, err := repositories.GetRoleByID(roleID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to check role existence"})
return
}
if role == nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Role not found"})
return
}
// 更新权限
if err := repositories.AssignPermissionsToRole(roleID, req.PermissionIDs); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update role permissions"})
return
}
c.JSON(http.StatusOK, gin.H{"message": "Role permissions updated successfully"})
}

54
server/handlers/runner.go Normal file
View File

@@ -0,0 +1,54 @@
package handlers
import (
"context"
"net/http"
"time"
"github.com/gin-gonic/gin"
"github.com/niangaodev/art-code/runner"
)
type RunCodeRequest struct {
Language string `json:"language" binding:"required"`
Code string `json:"code" binding:"required"`
}
func RunCode(c *gin.Context) {
var req RunCodeRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request"})
return
}
// 对于前端语言,直接返回代码供前端渲染,或者提示不支持后端执行
switch req.Language {
case "html", "vue", "react", "css":
c.JSON(http.StatusOK, gin.H{
"output": req.Code, // 或者返回 "Client-side rendering only"
"isClient": true,
})
return
}
// 获取运行器
r, err := runner.GetRunner(req.Language)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// 设置超时上下文 (例如 5 秒)
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
// 执行代码
result, err := r.Run(ctx, req.Code)
if err != nil {
// 运行错误(如无法启动进程)
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, result)
}

View File

@@ -0,0 +1,67 @@
package handlers
import (
"net/http"
"github.com/gin-gonic/gin"
"github.com/niangaodev/art-code/models"
"github.com/niangaodev/art-code/repositories"
)
// 获取系统配置列表
func AdminGetSettings(c *gin.Context) {
settings, err := repositories.GetSettings()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get settings"})
return
}
c.JSON(http.StatusOK, repositories.BuildSettingsResponse(settings))
}
// 更新系统配置
func AdminUpdateSetting(c *gin.Context) {
var setting models.Setting
if err := c.ShouldBindJSON(&setting); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request"})
return
}
// 更新系统配置
if err := repositories.UpdateSetting(&setting); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update setting"})
return
}
c.JSON(http.StatusOK, gin.H{"message": "Setting updated successfully"})
}
// 创建系统配置
func AdminCreateSetting(c *gin.Context) {
var setting models.Setting
if err := c.ShouldBindJSON(&setting); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request"})
return
}
// 创建系统配置
if err := repositories.CreateSetting(&setting); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create setting"})
return
}
c.JSON(http.StatusOK, gin.H{"message": "Setting created successfully"})
}
// 删除系统配置
func AdminDeleteSetting(c *gin.Context) {
keyName := c.Param("key")
// 删除系统配置
if err := repositories.DeleteSetting(keyName); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete setting"})
return
}
c.JSON(http.StatusOK, gin.H{"message": "Setting deleted successfully"})
}

106
server/handlers/snippet.go Normal file
View File

@@ -0,0 +1,106 @@
package handlers
import (
"net/http"
"github.com/gin-gonic/gin"
"github.com/niangaodev/art-code/models"
"github.com/niangaodev/art-code/repositories"
)
// 获取代码片段列表
func GetSnippets(c *gin.Context) {
// 从数据库获取所有代码片段
snippets, err := repositories.GetSnippets()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch snippets"})
return
}
// 构建响应
responses := repositories.BuildSnippetsResponse(snippets)
c.JSON(http.StatusOK, responses)
}
func GetSnippet(c *gin.Context) {
id := c.Param("id")
// 从数据库获取代码片段
snippet, err := repositories.GetSnippetByID(id)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch snippet"})
return
}
if snippet == nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Snippet not found"})
return
}
// 构建响应
response := repositories.BuildSnippetResponse(snippet)
c.JSON(http.StatusOK, response)
}
// 获取代码片段列表 (Admin)
func AdminGetSnippets(c *gin.Context) {
snippets, err := repositories.GetSnippets()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get snippets"})
return
}
c.JSON(http.StatusOK, repositories.BuildSnippetsResponse(snippets))
}
// 创建代码片段
func AdminCreateSnippet(c *gin.Context) {
var snippet models.Snippet
if err := c.ShouldBindJSON(&snippet); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request"})
return
}
// 创建代码片段
if err := repositories.CreateSnippet(&snippet); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create snippet"})
return
}
c.JSON(http.StatusOK, gin.H{"message": "Snippet created successfully"})
}
// 更新代码片段
func AdminUpdateSnippet(c *gin.Context) {
snippetID := c.Param("id")
var snippet models.Snippet
if err := c.ShouldBindJSON(&snippet); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request"})
return
}
// 设置代码片段ID
snippet.ID = snippetID
// 更新代码片段
if err := repositories.UpdateSnippet(&snippet); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update snippet"})
return
}
c.JSON(http.StatusOK, gin.H{"message": "Snippet updated successfully"})
}
// 删除代码片段
func AdminDeleteSnippet(c *gin.Context) {
snippetID := c.Param("id")
// 删除代码片段
if err := repositories.DeleteSnippet(snippetID); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete snippet"})
return
}
c.JSON(http.StatusOK, gin.H{"message": "Snippet deleted successfully"})
}

132
server/handlers/tag.go Normal file
View File

@@ -0,0 +1,132 @@
package handlers
import (
"fmt"
"net/http"
"github.com/gin-gonic/gin"
"github.com/niangaodev/art-code/models"
"github.com/niangaodev/art-code/repositories"
)
// 获取标签列表
func GetTags(c *gin.Context) {
// 从数据库获取所有标签
tags, err := repositories.GetTags()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch tags"})
return
}
// 构建响应
responses := repositories.BuildTagsResponse(tags)
c.JSON(http.StatusOK, responses)
}
func GetTag(c *gin.Context) {
// 解析标签ID
idStr := c.Param("id")
var id uint
_, err := fmt.Sscanf(idStr, "%d", &id)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid tag ID"})
return
}
// 从数据库获取标签
tag, err := repositories.GetTagByID(id)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch tag"})
return
}
if tag == nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Tag not found"})
return
}
// 构建响应
response := repositories.BuildTagResponse(tag)
c.JSON(http.StatusOK, response)
}
// 获取标签列表 (Admin)
func AdminGetTags(c *gin.Context) {
// 从数据库获取所有标签
tags, err := repositories.GetTags()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get tags"})
return
}
c.JSON(http.StatusOK, repositories.BuildTagsResponse(tags))
}
// 创建标签
func AdminCreateTag(c *gin.Context) {
var tag models.Tag
if err := c.ShouldBindJSON(&tag); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request"})
return
}
// 创建标签
if err := repositories.CreateTag(&tag); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create tag"})
return
}
c.JSON(http.StatusOK, gin.H{"message": "Tag created successfully"})
}
// 更新标签
func AdminUpdateTag(c *gin.Context) {
tagID := c.Param("id")
var tag models.Tag
if err := c.ShouldBindJSON(&tag); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request"})
return
}
// 转换标签ID为uint
var idUint uint
_, err := fmt.Sscanf(tagID, "%d", &idUint)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid tag ID"})
return
}
// 设置标签ID
tag.ID = idUint
// 更新标签
if err := repositories.UpdateTag(&tag); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update tag"})
return
}
c.JSON(http.StatusOK, gin.H{"message": "Tag updated successfully"})
}
// 删除标签
func AdminDeleteTag(c *gin.Context) {
tagID := c.Param("id")
// 转换标签ID为uint
var idUint uint
_, err := fmt.Sscanf(tagID, "%d", &idUint)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid tag ID"})
return
}
// 删除标签
if err := repositories.DeleteTag(idUint); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete tag"})
return
}
c.JSON(http.StatusOK, gin.H{"message": "Tag deleted successfully"})
}

129
server/handlers/user.go Normal file
View File

@@ -0,0 +1,129 @@
package handlers
import (
"fmt"
"net/http"
"strconv"
"github.com/gin-gonic/gin"
"github.com/niangaodev/art-code/models"
"github.com/niangaodev/art-code/repositories"
"golang.org/x/crypto/bcrypt"
)
// GetUsers 获取所有用户
func AdminGetUsers(c *gin.Context) {
users, err := repositories.GetUsers()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get users"})
return
}
c.JSON(http.StatusOK, repositories.BuildUsersResponse(users))
}
// GetUser 获取单个用户
func AdminGetUser(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.ParseUint(idStr, 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid user ID"})
return
}
user, err := repositories.GetUserByID(uint(id))
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get user"})
return
}
if user == nil {
c.JSON(http.StatusNotFound, gin.H{"error": "User not found"})
return
}
// 构建响应
response := repositories.BuildUserResponse(user)
// 设置响应头
c.Header("Content-Type", "application/json; charset=utf-8")
// 返回JSON响应
c.JSON(http.StatusOK, response)
}
// CreateUser 创建用户
func AdminCreateUser(c *gin.Context) {
var user models.User
if err := c.ShouldBindJSON(&user); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request"})
return
}
// 设置默认密码并使用bcrypt哈希
defaultPassword := "admin123"
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(defaultPassword), bcrypt.DefaultCost)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to hash password"})
return
}
user.PasswordHash = string(hashedPassword)
// 创建用户
if err := repositories.CreateUser(&user); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create user"})
return
}
c.JSON(http.StatusOK, gin.H{"message": "User created successfully"})
}
// UpdateUser 更新用户
func AdminUpdateUser(c *gin.Context) {
userID := c.Param("id")
var user models.User
if err := c.ShouldBindJSON(&user); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request"})
return
}
// 转换用户ID为uint
var idUint uint
_, err := fmt.Sscanf(userID, "%d", &idUint)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid user ID"})
return
}
// 设置用户ID
user.ID = idUint
// 更新用户
if err := repositories.UpdateUser(&user); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update user"})
return
}
c.JSON(http.StatusOK, gin.H{"message": "User updated successfully"})
}
// DeleteUser 删除用户
func AdminDeleteUser(c *gin.Context) {
userID := c.Param("id")
// 转换用户ID为uint
var idUint uint
_, err := fmt.Sscanf(userID, "%d", &idUint)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid user ID"})
return
}
// 删除用户
if err := repositories.DeleteUser(idUint); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete user"})
return
}
c.JSON(http.StatusOK, gin.H{"message": "User deleted successfully"})
}

130
server/handlers/work.go Normal file
View File

@@ -0,0 +1,130 @@
package handlers
import (
"log"
"net/http"
"github.com/gin-gonic/gin"
"github.com/niangaodev/art-code/models"
"github.com/niangaodev/art-code/repositories"
)
// 获取作品列表
func GetWorks(c *gin.Context) {
// 从数据库获取所有作品
works, err := repositories.GetWorks()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch works"})
return
}
// 构建响应
var responses []interface{}
for _, work := range works {
response, err := repositories.BuildWorkResponse(&work)
if err != nil {
log.Printf("Error building work response: %v", err)
continue
}
responses = append(responses, response)
}
c.JSON(http.StatusOK, responses)
}
func GetWork(c *gin.Context) {
id := c.Param("id")
// 从数据库获取作品
work, err := repositories.GetWorkByID(id)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch work"})
return
}
if work == nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Work not found"})
return
}
// 构建响应
response, err := repositories.BuildWorkResponse(work)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to build work response"})
return
}
c.JSON(http.StatusOK, response)
}
// 获取作品列表 (Admin)
func AdminGetWorks(c *gin.Context) {
works, err := repositories.GetWorks()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get works"})
return
}
// 构建响应
var responses []interface{}
for _, work := range works {
response, err := repositories.BuildWorkResponse(&work)
if err != nil {
log.Printf("Error building work response: %v", err)
continue
}
responses = append(responses, response)
}
c.JSON(http.StatusOK, responses)
}
// 创建作品
func AdminCreateWork(c *gin.Context) {
var work models.Work
if err := c.ShouldBindJSON(&work); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request"})
return
}
// 创建作品
if err := repositories.CreateWork(&work); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create work"})
return
}
c.JSON(http.StatusOK, gin.H{"message": "Work created successfully"})
}
// 更新作品
func AdminUpdateWork(c *gin.Context) {
workID := c.Param("id")
var work models.Work
if err := c.ShouldBindJSON(&work); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request"})
return
}
// 设置作品ID
work.ID = workID
// 更新作品
if err := repositories.UpdateWork(&work); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update work"})
return
}
c.JSON(http.StatusOK, gin.H{"message": "Work updated successfully"})
}
// 删除作品
func AdminDeleteWork(c *gin.Context) {
workID := c.Param("id")
// 删除作品
if err := repositories.DeleteWork(workID); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete work"})
return
}
c.JSON(http.StatusOK, gin.H{"message": "Work deleted successfully"})
}

154
server/main.go Normal file
View File

@@ -0,0 +1,154 @@
package main
import (
"log"
"net/http"
"os"
"strings"
"github.com/gin-gonic/gin"
"github.com/niangaodev/art-code/config"
"github.com/niangaodev/art-code/handlers"
"github.com/niangaodev/art-code/middleware"
)
func main() {
// 初始化数据库连接
config.InitDB()
defer config.CloseDB()
// 创建Gin引擎
router := gin.Default()
// 配置CORS
router.Use(func(c *gin.Context) {
// 获取允许的域名,优先从环境变量获取,否则使用默认值
allowedOrigins := os.Getenv("ALLOWED_ORIGINS")
if allowedOrigins == "" {
allowedOrigins = "http://localhost:5173,http://localhost:3000" // 开发环境默认值
}
// 检查请求来源是否在允许列表中
requestOrigin := c.Request.Header.Get("Origin")
if requestOrigin != "" {
// 简单的CORS origin检查
for _, origin := range strings.Split(allowedOrigins, ",") {
if strings.TrimSpace(origin) == requestOrigin {
c.Writer.Header().Set("Access-Control-Allow-Origin", requestOrigin)
break
}
}
}
c.Writer.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
c.Writer.Header().Set("Access-Control-Allow-Headers", "Origin, Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization")
c.Writer.Header().Set("Access-Control-Allow-Credentials", "true")
if c.Request.Method == "OPTIONS" {
c.AbortWithStatus(http.StatusNoContent)
return
}
c.Next()
})
// API路由组
api := router.Group("/api")
{
// 作品路由
api.GET("/works", handlers.GetWorks)
api.GET("/works/:id", handlers.GetWork)
// 博客路由
api.GET("/posts", handlers.GetPosts)
api.GET("/posts/:id", handlers.GetPost)
// 代码片段路由
api.GET("/snippets", handlers.GetSnippets)
api.GET("/snippets/:id", handlers.GetSnippet)
// 标签路由
api.GET("/tags", handlers.GetTags)
api.GET("/tags/:id", handlers.GetTag)
api.GET("/tags/:id/posts", handlers.GetPostsByTagID)
// 代码执行路由
api.POST("/run", handlers.RunCode)
}
// 管理员API路由组
admin := router.Group("/api/admin")
{
// 登录路由(不需要认证)
admin.POST("/login", handlers.AdminLogin)
// 需要认证的路由
authAdmin := admin.Group("/")
authAdmin.Use(middleware.AuthMiddleware(), middleware.OperationLogMiddleware())
{
// 用户管理
authAdmin.GET("/users", middleware.PermissionMiddleware("users", "read"), handlers.AdminGetUsers)
authAdmin.GET("/users/:id", middleware.PermissionMiddleware("users", "read"), handlers.AdminGetUser)
authAdmin.POST("/users", middleware.PermissionMiddleware("users", "create"), handlers.AdminCreateUser)
authAdmin.PUT("/users/:id", middleware.PermissionMiddleware("users", "update"), handlers.AdminUpdateUser)
authAdmin.DELETE("/users/:id", middleware.PermissionMiddleware("users", "delete"), handlers.AdminDeleteUser)
// 角色管理
authAdmin.GET("/roles", middleware.PermissionMiddleware("roles", "read"), handlers.AdminGetRoles)
authAdmin.POST("/roles", middleware.PermissionMiddleware("roles", "create"), handlers.AdminCreateRole)
authAdmin.PUT("/roles/:id", middleware.PermissionMiddleware("roles", "update"), handlers.AdminUpdateRole)
authAdmin.DELETE("/roles/:id", middleware.PermissionMiddleware("roles", "delete"), handlers.AdminDeleteRole)
// 权限管理
authAdmin.GET("/permissions", middleware.PermissionMiddleware("roles", "read"), handlers.AdminGetPermissions)
authAdmin.PUT("/roles/:id/permissions", middleware.PermissionMiddleware("roles", "update"), handlers.AdminUpdateRolePermissions)
// 作品管理
authAdmin.GET("/works", middleware.PermissionMiddleware("works", "read"), handlers.AdminGetWorks)
authAdmin.POST("/works", middleware.PermissionMiddleware("works", "create"), handlers.AdminCreateWork)
authAdmin.PUT("/works/:id", middleware.PermissionMiddleware("works", "update"), handlers.AdminUpdateWork)
authAdmin.DELETE("/works/:id", middleware.PermissionMiddleware("works", "delete"), handlers.AdminDeleteWork)
// 代码片段管理
authAdmin.GET("/snippets", middleware.PermissionMiddleware("snippets", "read"), handlers.AdminGetSnippets)
authAdmin.POST("/snippets", middleware.PermissionMiddleware("snippets", "create"), handlers.AdminCreateSnippet)
authAdmin.PUT("/snippets/:id", middleware.PermissionMiddleware("snippets", "update"), handlers.AdminUpdateSnippet)
authAdmin.DELETE("/snippets/:id", middleware.PermissionMiddleware("snippets", "delete"), handlers.AdminDeleteSnippet)
// 系统配置管理
authAdmin.GET("/settings", middleware.PermissionMiddleware("settings", "read"), handlers.AdminGetSettings)
authAdmin.POST("/settings", middleware.PermissionMiddleware("settings", "create"), handlers.AdminCreateSetting)
authAdmin.PUT("/settings", middleware.PermissionMiddleware("settings", "update"), handlers.AdminUpdateSetting)
authAdmin.DELETE("/settings/:key", middleware.PermissionMiddleware("settings", "delete"), handlers.AdminDeleteSetting)
// 文章管理
authAdmin.GET("/posts", middleware.PermissionMiddleware("posts", "read"), handlers.AdminGetPosts)
authAdmin.POST("/posts", middleware.PermissionMiddleware("posts", "create"), handlers.AdminCreatePost)
authAdmin.PUT("/posts/:id", middleware.PermissionMiddleware("posts", "update"), handlers.AdminUpdatePost)
authAdmin.DELETE("/posts/:id", middleware.PermissionMiddleware("posts", "delete"), handlers.AdminDeletePost)
// 文章历史记录
authAdmin.GET("/posts/:id/history", middleware.PermissionMiddleware("posts", "read"), handlers.AdminGetPostHistory)
authAdmin.GET("/posts/:id/history/:version", middleware.PermissionMiddleware("posts", "read"), handlers.AdminGetPostHistoryByVersion)
// 操作日志管理
authAdmin.GET("/operation-logs", middleware.PermissionMiddleware("operation_logs", "read"), handlers.AdminGetOperationLogs)
// 仪表盘数据
authAdmin.GET("/dashboard/stats", middleware.PermissionMiddleware("dashboard", "read"), handlers.AdminGetDashboardStats)
authAdmin.GET("/dashboard/activities", middleware.PermissionMiddleware("dashboard", "read"), handlers.AdminGetRecentActivities)
// 标签管理
authAdmin.GET("/tags", middleware.PermissionMiddleware("tags", "read"), handlers.AdminGetTags)
authAdmin.POST("/tags", middleware.PermissionMiddleware("tags", "create"), handlers.AdminCreateTag)
authAdmin.PUT("/tags/:id", middleware.PermissionMiddleware("tags", "update"), handlers.AdminUpdateTag)
authAdmin.DELETE("/tags/:id", middleware.PermissionMiddleware("tags", "delete"), handlers.AdminDeleteTag)
}
}
// 启动服务器
log.Println("Server running on http://localhost:8081")
if err := router.Run(":8081"); err != nil {
log.Fatalf("Failed to start server: %v", err)
}
}

209
server/middleware/auth.go Normal file
View File

@@ -0,0 +1,209 @@
package middleware
import (
"errors"
"net/http"
"os"
"strings"
"time"
"github.com/gin-gonic/gin"
"github.com/golang-jwt/jwt/v5"
"github.com/niangaodev/art-code/repositories"
)
// JWT密钥
var jwtSecret = []byte(getJWTSecret())
// getJWTSecret 获取JWT密钥优先从环境变量获取否则使用默认值
func getJWTSecret() string {
secret := os.Getenv("JWT_SECRET")
if secret == "" {
return "your-secret-key" // 开发环境默认值,生产环境必须通过环境变量设置
}
return secret
}
// Claims JWT声明结构
type Claims struct {
UserID uint `json:"userId"`
Username string `json:"username"`
Role string `json:"role"`
RoleID uint `json:"roleId"`
jwt.RegisteredClaims
}
// GenerateToken 生成JWT令牌
func GenerateToken(userID uint, username, role string) (string, int64, error) {
// 设置令牌过期时间为7天
expireTime := time.Now().Add(7 * 24 * time.Hour)
expireUnix := expireTime.Unix()
// 获取用户的RoleID
var roleID uint
userRole, err := repositories.GetRoleByName(role)
if err == nil && userRole != nil {
roleID = userRole.ID
}
// 创建声明
claims := Claims{
UserID: userID,
Username: username,
Role: role,
RoleID: roleID,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(expireTime),
IssuedAt: jwt.NewNumericDate(time.Now()),
Subject: "admin-token",
},
}
// 创建令牌
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
// 签名令牌
tokenString, err := token.SignedString(jwtSecret)
if err != nil {
return "", 0, err
}
return tokenString, expireUnix, nil
}
// ParseToken 解析JWT令牌
func ParseToken(tokenString string) (*Claims, error) {
// 解析令牌
token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) {
return jwtSecret, nil
})
if err != nil {
return nil, err
}
// 验证令牌
if claims, ok := token.Claims.(*Claims); ok && token.Valid {
return claims, nil
}
return nil, errors.New("invalid token")
}
// AuthMiddleware JWT认证中间件
func AuthMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
// 从请求头中获取令牌
authHeader := c.GetHeader("Authorization")
if authHeader == "" {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Authorization header is required"})
c.Abort()
return
}
// 检查令牌格式
parts := strings.SplitN(authHeader, " ", 2)
if !(len(parts) == 2 && parts[0] == "Bearer") {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Authorization header format must be Bearer {token}"})
c.Abort()
return
}
// 解析令牌
claims, err := ParseToken(parts[1])
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid or expired token"})
c.Abort()
return
}
// 将用户信息存储到上下文
c.Set("userID", claims.UserID)
c.Set("username", claims.Username)
c.Set("role", claims.Role)
c.Set("roleID", claims.RoleID)
c.Next()
}
}
// RoleMiddleware 角色验证中间件 (Legacy support)
func RoleMiddleware(roles ...string) gin.HandlerFunc {
return func(c *gin.Context) {
// 从上下文中获取用户角色
role, exists := c.Get("role")
if !exists {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
c.Abort()
return
}
// 检查用户角色是否在允许列表中
allowed := false
for _, r := range roles {
if role == r {
allowed = true
break
}
}
if !allowed {
c.JSON(http.StatusForbidden, gin.H{"error": "Insufficient permissions"})
c.Abort()
return
}
c.Next()
}
}
// PermissionMiddleware 权限验证中间件
func PermissionMiddleware(resource, action string) gin.HandlerFunc {
return func(c *gin.Context) {
// 获取用户RoleID
roleIDInterface, exists := c.Get("roleID")
if !exists {
// 尝试从role name获取
roleName, exists := c.Get("role")
if !exists {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
c.Abort()
return
}
role, err := repositories.GetRoleByName(roleName.(string))
if err != nil || role == nil {
c.JSON(http.StatusForbidden, gin.H{"error": "Role not found"})
c.Abort()
return
}
roleIDInterface = role.ID
}
roleID := roleIDInterface.(uint)
// 获取该角色的所有权限
permissions, err := repositories.GetPermissionsByRoleID(roleID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to check permissions"})
c.Abort()
return
}
// 检查是否有匹配的权限
allowed := false
for _, p := range permissions {
if p.Resource == resource && p.Action == action {
allowed = true
break
}
}
if !allowed {
c.JSON(http.StatusForbidden, gin.H{"error": "Insufficient permissions"})
c.Abort()
return
}
c.Next()
}
}

68
server/middleware/log.go Normal file
View File

@@ -0,0 +1,68 @@
package middleware
import (
"bytes"
"io/ioutil"
"log"
"time"
"github.com/gin-gonic/gin"
"github.com/niangaodev/art-code/models"
"github.com/niangaodev/art-code/repositories"
)
// OperationLogMiddleware 操作日志中间件
func OperationLogMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
// 开始时间
startTime := time.Now()
// 读取请求参数
var requestBody []byte
if c.Request.Method != "GET" {
body, err := ioutil.ReadAll(c.Request.Body)
if err != nil {
log.Printf("Error reading request body: %v", err)
} else {
requestBody = body
// 重置请求体,以便后续处理
c.Request.Body = ioutil.NopCloser(bytes.NewBuffer(requestBody))
}
}
// 执行请求
c.Next()
// 结束时间
endTime := time.Now()
// 计算请求持续时间(毫秒)
duration := int(endTime.Sub(startTime).Milliseconds())
// 获取用户信息
userID, exists := c.Get("userID")
if !exists {
return
}
username, _ := c.Get("username")
// 构建操作日志
operationLog := &models.OperationLog{
UserID: userID.(uint),
Username: username.(string),
IP: c.ClientIP(),
Path: c.Request.URL.Path,
Method: c.Request.Method,
Params: string(requestBody),
Status: c.Writer.Status(),
Duration: duration,
}
// 异步记录日志,避免影响响应
go func() {
if err := repositories.CreateOperationLog(operationLog); err != nil {
log.Printf("Error creating operation log: %v", err)
}
}()
}
}

View File

@@ -0,0 +1,33 @@
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"`
}
// OperationLogResponse 操作日志响应模型
type OperationLogResponse 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 string `json:"createdAt"`
}

View File

@@ -0,0 +1,25 @@
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"`
}
// PermissionResponse 权限响应模型
type PermissionResponse struct {
ID uint `json:"id"`
Name string `json:"name"`
Resource string `json:"resource"`
Action string `json:"action"`
CreatedAt string `json:"createdAt"`
UpdatedAt string `json:"updatedAt"`
}

75
server/models/post.go Normal file
View File

@@ -0,0 +1,75 @@
package models
import (
"time"
)
// Post 博客文章模型
type Post struct {
ID string `json:"id"`
Title string `json:"title"`
Category string `json:"category"`
Date time.Time `json:"date"`
Excerpt string `json:"excerpt"`
Content string `json:"content"`
ReadCount uint `json:"readCount"`
IsPublished int `json:"isPublished"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}
// PostResponse 博客文章响应模型
type PostResponse struct {
ID string `json:"id"`
Title string `json:"title"`
Category string `json:"category"`
Date string `json:"date"`
Excerpt string `json:"excerpt,omitempty"`
Content string `json:"content,omitempty"`
}
// 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"`
}
// PostTag 文章标签关联模型
type PostTag struct {
PostID string `json:"postId"`
TagID uint `json:"tagId"`
CreatedAt time.Time `json:"createdAt"`
}
// PostHistory 文章历史记录模型
type PostHistory struct {
ID uint `json:"id"`
PostID string `json:"postId"`
Version uint `json:"version"`
Title string `json:"title"`
Category string `json:"category"`
Date time.Time `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"`
}
// PostHistoryResponse 文章历史记录响应模型
type PostHistoryResponse struct {
ID uint `json:"id"`
PostID string `json:"postId"`
Version uint `json:"version"`
Title string `json:"title"`
Category string `json:"category"`
Date string `json:"date"`
IsPublished int `json:"isPublished"`
ModifiedBy uint `json:"modifiedBy"`
ModifiedAt string `json:"modifiedAt"`
CreatedAt string `json:"createdAt"`
}

25
server/models/role.go Normal file
View File

@@ -0,0 +1,25 @@
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"`
}
// RoleResponse 角色响应模型
type RoleResponse struct {
ID uint `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
Permissions []PermissionResponse `json:"permissions,omitempty"`
CreatedAt string `json:"createdAt"`
UpdatedAt string `json:"updatedAt"`
}

25
server/models/setting.go Normal file
View File

@@ -0,0 +1,25 @@
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"`
}
// SettingResponse 系统配置响应模型
type SettingResponse struct {
ID uint `json:"id"`
KeyName string `json:"keyName"`
Value string `json:"value"`
Description string `json:"description"`
CreatedAt string `json:"createdAt"`
UpdatedAt string `json:"updatedAt"`
}

25
server/models/snippet.go Normal file
View File

@@ -0,0 +1,25 @@
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"`
}
// SnippetResponse 代码片段响应模型
type SnippetResponse struct {
ID string `json:"id"`
Title string `json:"title"`
Code string `json:"code"`
Type string `json:"type"`
}

43
server/models/user.go Normal file
View File

@@ -0,0 +1,43 @@
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"`
}
// UserResponse 用户响应模型
type UserResponse struct {
ID uint `json:"id"`
Username string `json:"username"`
Email string `json:"email"`
RoleID uint `json:"roleId"`
Role string `json:"role"`
IsActive int `json:"isActive"`
CreatedAt string `json:"createdAt"`
UpdatedAt string `json:"updatedAt"`
}
// LoginRequest 登录请求模型
type LoginRequest struct {
Username string `json:"username" binding:"required"`
Password string `json:"password" binding:"required"`
}
// LoginResponse 登录响应模型
type LoginResponse struct {
Token string `json:"token"`
User UserResponse `json:"user"`
Expire int64 `json:"expire"`
}

51
server/models/work.go Normal file
View File

@@ -0,0 +1,51 @@
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"`
}
// 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"`
}
// 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"`
}
// WorkResponse 作品响应模型,包含关联数据
type WorkResponse struct {
ID string `json:"id"`
Title string `json:"title"`
Category string `json:"category"`
Year string `json:"year"`
HeroImg string `json:"heroImg"`
Desc string `json:"desc"`
TechStack []map[string]interface{} `json:"techStack"`
Gallery []string `json:"gallery"`
Links map[string]interface{} `json:"links"`
Next string `json:"next"`
}

391
server/nl_blog.sql Normal file
View File

@@ -0,0 +1,391 @@
/*
Navicat Premium Dump SQL
Source Server : 开发环境-本地
Source Server Type : MySQL
Source Server Version : 80407 (8.4.7)
Source Host : localhost:3306
Source Schema : nl_blog
Target Server Type : MySQL
Target Server Version : 80407 (8.4.7)
File Encoding : 65001
Date: 14/01/2026 13:01:23
*/
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for access_logs
-- ----------------------------
DROP TABLE IF EXISTS `access_logs`;
CREATE TABLE `access_logs` (
`id` bigint UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`ip` varchar(45) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '访问者IP地址',
`user_agent` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL COMMENT '访问者浏览器信息',
`path` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '访问路径',
`method` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT 'HTTP方法',
`status_code` int UNSIGNED NOT NULL COMMENT 'HTTP状态码',
`response_time` int UNSIGNED NOT NULL COMMENT '响应时间(毫秒)',
`created_at` datetime NULL DEFAULT CURRENT_TIMESTAMP COMMENT '访问时间',
PRIMARY KEY (`id`) USING BTREE,
INDEX `idx_path`(`path` ASC) USING BTREE COMMENT '按访问路径查询索引',
INDEX `idx_created_at`(`created_at` ASC) USING BTREE COMMENT '按访问时间查询索引',
INDEX `idx_status_code`(`status_code` ASC) USING BTREE COMMENT '按状态码查询索引'
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '访问日志表' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of access_logs
-- ----------------------------
-- ----------------------------
-- Table structure for post_tags
-- ----------------------------
DROP TABLE IF EXISTS `post_tags`;
CREATE TABLE `post_tags` (
`post_id` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '关联的文章ID',
`tag_id` bigint UNSIGNED NOT NULL COMMENT '关联的标签ID',
`created_at` datetime NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (`post_id`, `tag_id`) USING BTREE,
INDEX `idx_post_id`(`post_id` ASC) USING BTREE,
INDEX `idx_tag_id`(`tag_id` ASC) USING BTREE,
CONSTRAINT `post_tags_ibfk_1` FOREIGN KEY (`post_id`) REFERENCES `posts` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT,
CONSTRAINT `post_tags_ibfk_2` FOREIGN KEY (`tag_id`) REFERENCES `tags` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '文章标签关联表' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of post_tags
-- ----------------------------
-- ----------------------------
-- Table structure for posts
-- ----------------------------
DROP TABLE IF EXISTS `posts`;
CREATE TABLE `posts` (
`id` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '文章唯一标识',
`title` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '文章标题',
`category` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '文章分类',
`date` date NOT NULL COMMENT '发布日期',
`excerpt` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL COMMENT '文章摘要',
`content` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '文章内容',
`read_count` int UNSIGNED NULL DEFAULT 0 COMMENT '阅读量',
`is_published` tinyint(1) NULL DEFAULT 1 COMMENT '是否已发布0草稿1已发布',
`created_at` datetime NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updated_at` datetime NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`) USING BTREE,
INDEX `idx_category`(`category` ASC) USING BTREE COMMENT '按分类查询索引',
INDEX `idx_date`(`date` ASC) USING BTREE COMMENT '按发布日期查询索引',
INDEX `idx_is_published`(`is_published` ASC) USING BTREE COMMENT '按发布状态查询索引',
FULLTEXT INDEX `idx_title_content`(`title`, `content`) COMMENT '标题和内容全文索引,用于搜索'
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '博客文章表' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of posts
-- ----------------------------
INSERT INTO `posts` VALUES ('refactor', '重构的艺术:如何优雅地处理遗留代码', '工程化', '2026-01-12', '在本文中,我们将探讨重构的核心原则和实用技巧,帮助你优雅地处理遗留代码,提高代码质量和可维护性。', '<h2>什么是代码重构?</h2><p>代码重构是在不改变代码外部行为的前提下,优化代码内部结构的过程...</p>', 0, 1, '2026-01-13 16:10:14', '2026-01-13 16:10:14');
INSERT INTO `posts` VALUES ('shader', '着色器魔法:从零开始写一个噪声生成器', '图形渲染', '2025-12-08', '深入了解WebGL着色器学习如何从零开始实现一个高性能的噪声生成器为你的3D作品增添独特的视觉效果。', '<h2>WebGL着色器基础</h2><p>WebGL着色器是运行在GPU上的小程序用于处理图形渲染...</p>', 0, 1, '2026-01-13 16:10:14', '2026-01-13 16:10:14');
INSERT INTO `posts` VALUES ('ux', '用户体验设计:从认知心理学到交互实践', '设计思维', '2025-11-20', '探索用户体验设计的核心原理,结合认知心理学知识,学习如何设计出真正符合用户需求的交互界面。', '<h2>认知心理学在UX设计中的应用</h2><p>了解用户的认知过程是设计良好用户体验的基础...</p>', 0, 1, '2026-01-13 16:10:14', '2026-01-13 16:10:14');
-- ----------------------------
-- Table structure for settings
-- ----------------------------
DROP TABLE IF EXISTS `settings`;
CREATE TABLE `settings` (
`id` bigint UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`key_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '配置项键名',
`value` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL COMMENT '配置项值',
`description` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL COMMENT '配置项描述',
`created_at` datetime NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updated_at` datetime NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`) USING BTREE,
UNIQUE INDEX `key_name`(`key_name` ASC) USING BTREE,
INDEX `idx_key_name`(`key_name` ASC) USING BTREE COMMENT '按键名查询索引'
) ENGINE = InnoDB AUTO_INCREMENT = 15 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '网站配置表' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of settings
-- ----------------------------
INSERT INTO `settings` VALUES (1, 'site_title', '年糕博客', '网站标题', '2026-01-13 16:10:14', '2026-01-13 16:10:14');
INSERT INTO `settings` VALUES (2, 'site_description', '分享前端技术、交互设计以及数字艺术的深度思考', '网站描述', '2026-01-13 16:10:14', '2026-01-13 16:10:14');
INSERT INTO `settings` VALUES (3, 'site_author', '年糕崽崽', '网站作者', '2026-01-13 16:10:14', '2026-01-13 16:10:14');
INSERT INTO `settings` VALUES (4, 'site_keywords', '前端, 设计, 技术博客', '网站关键词', '2026-01-13 16:10:14', '2026-01-13 16:10:14');
INSERT INTO `settings` VALUES (5, 'posts_per_page', '10', '每页显示的文章数量', '2026-01-13 16:10:14', '2026-01-13 16:10:14');
INSERT INTO `settings` VALUES (6, 'works_per_page', '6', '每页显示的作品数量', '2026-01-13 16:10:14', '2026-01-13 16:10:14');
INSERT INTO `settings` VALUES (7, 'snippets_per_page', '8', '每页显示的代码片段数量', '2026-01-13 16:10:14', '2026-01-13 16:10:14');
-- ----------------------------
-- Table structure for snippets
-- ----------------------------
DROP TABLE IF EXISTS `snippets`;
CREATE TABLE `snippets` (
`id` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '代码片段唯一标识',
`title` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '代码片段标题',
`code` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '代码内容',
`type` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '代码类型javascript、css、html等',
`description` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL COMMENT '代码片段描述',
`view_count` int UNSIGNED NULL DEFAULT 0 COMMENT '查看次数',
`created_at` datetime NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updated_at` datetime NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`) USING BTREE,
INDEX `idx_type`(`type` ASC) USING BTREE COMMENT '按代码类型查询索引',
INDEX `idx_view_count`(`view_count` ASC) USING BTREE COMMENT '按查看次数查询索引'
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '代码片段表' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of snippets
-- ----------------------------
INSERT INTO `snippets` VALUES ('1', 'React 鼠标追踪 Hook', 'import { useState, useEffect } from \'react\';\r\n\r\nexport const useMousePosition = () => {\r\n const [pos, setPos] = useState({ x: 0, y: 0 });\r\n useEffect(() => {\r\n const update = (e) => setPos({ x: e.clientX, y: e.clientY });\r\n window.addEventListener(\'mousemove\', update);\r\n return () => window.removeEventListener(\'mousemove\', update);\r\n }, []);\r\n return pos;\r\n};', 'mouse', '这是一个鼠标追踪', 0, '2026-01-14 08:32:19', '2026-01-14 08:32:19');
-- ----------------------------
-- Table structure for tags
-- ----------------------------
DROP TABLE IF EXISTS `tags`;
CREATE TABLE `tags` (
`id` bigint UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '标签名称',
`slug` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '标签别名用于URL',
`created_at` datetime NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updated_at` datetime NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`) USING BTREE,
UNIQUE INDEX `name`(`name` ASC) USING BTREE,
UNIQUE INDEX `slug`(`slug` ASC) USING BTREE,
INDEX `idx_slug`(`slug` ASC) USING BTREE COMMENT '按别名查询索引'
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '标签表' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of tags
-- ----------------------------
-- ----------------------------
-- Table structure for roles
-- ----------------------------
DROP TABLE IF EXISTS `roles`;
CREATE TABLE `roles` (
`id` bigint UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '角色名称',
`description` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL COMMENT '角色描述',
`created_at` datetime NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updated_at` datetime NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`) USING BTREE,
UNIQUE INDEX `name`(`name` ASC) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '角色表' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of roles
-- ----------------------------
INSERT INTO `roles` (`name`, `description`) VALUES ('admin', '系统管理员');
INSERT INTO `roles` (`name`, `description`) VALUES ('editor', '内容编辑');
INSERT INTO `roles` (`name`, `description`) VALUES ('viewer', '普通访客');
-- ----------------------------
-- Table structure for permissions
-- ----------------------------
DROP TABLE IF EXISTS `permissions`;
CREATE TABLE `permissions` (
`id` bigint UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '权限名称',
`resource` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '资源名称',
`action` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '操作名称',
`created_at` datetime NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updated_at` datetime NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`) USING BTREE,
UNIQUE INDEX `unique_resource_action`(`resource`, `action`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '权限表' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of permissions
-- ----------------------------
-- User permissions
INSERT INTO `permissions` (`name`, `resource`, `action`) VALUES ('Create User', 'users', 'create');
INSERT INTO `permissions` (`name`, `resource`, `action`) VALUES ('Read User', 'users', 'read');
INSERT INTO `permissions` (`name`, `resource`, `action`) VALUES ('Update User', 'users', 'update');
INSERT INTO `permissions` (`name`, `resource`, `action`) VALUES ('Delete User', 'users', 'delete');
-- Role permissions
INSERT INTO `permissions` (`name`, `resource`, `action`) VALUES ('Create Role', 'roles', 'create');
INSERT INTO `permissions` (`name`, `resource`, `action`) VALUES ('Read Role', 'roles', 'read');
INSERT INTO `permissions` (`name`, `resource`, `action`) VALUES ('Update Role', 'roles', 'update');
INSERT INTO `permissions` (`name`, `resource`, `action`) VALUES ('Delete Role', 'roles', 'delete');
-- Post permissions
INSERT INTO `permissions` (`name`, `resource`, `action`) VALUES ('Create Post', 'posts', 'create');
INSERT INTO `permissions` (`name`, `resource`, `action`) VALUES ('Read Post', 'posts', 'read');
INSERT INTO `permissions` (`name`, `resource`, `action`) VALUES ('Update Post', 'posts', 'update');
INSERT INTO `permissions` (`name`, `resource`, `action`) VALUES ('Delete Post', 'posts', 'delete');
-- Work permissions
INSERT INTO `permissions` (`name`, `resource`, `action`) VALUES ('Create Work', 'works', 'create');
INSERT INTO `permissions` (`name`, `resource`, `action`) VALUES ('Read Work', 'works', 'read');
INSERT INTO `permissions` (`name`, `resource`, `action`) VALUES ('Update Work', 'works', 'update');
INSERT INTO `permissions` (`name`, `resource`, `action`) VALUES ('Delete Work', 'works', 'delete');
-- Snippet permissions
INSERT INTO `permissions` (`name`, `resource`, `action`) VALUES ('Create Snippet', 'snippets', 'create');
INSERT INTO `permissions` (`name`, `resource`, `action`) VALUES ('Read Snippet', 'snippets', 'read');
INSERT INTO `permissions` (`name`, `resource`, `action`) VALUES ('Update Snippet', 'snippets', 'update');
INSERT INTO `permissions` (`name`, `resource`, `action`) VALUES ('Delete Snippet', 'snippets', 'delete');
-- Setting permissions
INSERT INTO `permissions` (`name`, `resource`, `action`) VALUES ('Create Setting', 'settings', 'create');
INSERT INTO `permissions` (`name`, `resource`, `action`) VALUES ('Read Setting', 'settings', 'read');
INSERT INTO `permissions` (`name`, `resource`, `action`) VALUES ('Update Setting', 'settings', 'update');
INSERT INTO `permissions` (`name`, `resource`, `action`) VALUES ('Delete Setting', 'settings', 'delete');
-- Tag permissions
INSERT INTO `permissions` (`name`, `resource`, `action`) VALUES ('Create Tag', 'tags', 'create');
INSERT INTO `permissions` (`name`, `resource`, `action`) VALUES ('Read Tag', 'tags', 'read');
INSERT INTO `permissions` (`name`, `resource`, `action`) VALUES ('Update Tag', 'tags', 'update');
INSERT INTO `permissions` (`name`, `resource`, `action`) VALUES ('Delete Tag', 'tags', 'delete');
-- Log permissions
INSERT INTO `permissions` (`name`, `resource`, `action`) VALUES ('Read Operation Log', 'operation_logs', 'read');
-- Dashboard permissions
INSERT INTO `permissions` (`name`, `resource`, `action`) VALUES ('Read Dashboard', 'dashboard', 'read');
-- ----------------------------
-- Table structure for role_permissions
-- ----------------------------
DROP TABLE IF EXISTS `role_permissions`;
CREATE TABLE `role_permissions` (
`role_id` bigint UNSIGNED NOT NULL COMMENT '角色ID',
`permission_id` bigint UNSIGNED NOT NULL COMMENT '权限ID',
PRIMARY KEY (`role_id`, `permission_id`) USING BTREE,
CONSTRAINT `role_permissions_ibfk_1` FOREIGN KEY (`role_id`) REFERENCES `roles` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT,
CONSTRAINT `role_permissions_ibfk_2` FOREIGN KEY (`permission_id`) REFERENCES `permissions` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '角色权限关联表' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of role_permissions
-- ----------------------------
-- Admin has all permissions (assuming ids 1-12)
INSERT INTO `role_permissions` SELECT 1, id FROM permissions;
-- Editor has Post permissions (ids 9-12)
INSERT INTO `role_permissions` SELECT 2, id FROM permissions WHERE resource = 'posts';
-- Viewer has read permissions (assuming read action)
INSERT INTO `role_permissions` SELECT 3, id FROM permissions WHERE action = 'read';
-- ----------------------------
-- Table structure for users
-- ----------------------------
DROP TABLE IF EXISTS `users`;
CREATE TABLE `users` (
`id` bigint UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`username` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '用户名',
`email` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '邮箱地址',
`password_hash` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '密码哈希值',
`role_id` bigint UNSIGNED NULL COMMENT '角色ID',
`role` enum('admin','editor','viewer') CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT 'viewer' COMMENT '用户角色(兼容旧版)',
`is_active` tinyint(1) NULL DEFAULT 1 COMMENT '是否激活0禁用1激活',
`created_at` datetime NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updated_at` datetime NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`) USING BTREE,
UNIQUE INDEX `username`(`username` ASC) USING BTREE,
UNIQUE INDEX `email`(`email` ASC) USING BTREE,
INDEX `idx_username`(`username` ASC) USING BTREE COMMENT '按用户名查询索引',
INDEX `idx_email`(`email` ASC) USING BTREE COMMENT '按邮箱查询索引',
INDEX `idx_role`(`role` ASC) USING BTREE COMMENT '按角色查询索引',
CONSTRAINT `users_ibfk_1` FOREIGN KEY (`role_id`) REFERENCES `roles` (`id`) ON DELETE SET NULL ON UPDATE RESTRICT
) ENGINE = InnoDB AUTO_INCREMENT = 5 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '用户表' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Table structure for operation_logs
-- ----------------------------
DROP TABLE IF EXISTS `operation_logs`;
CREATE TABLE `operation_logs` (
`id` bigint UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`user_id` bigint UNSIGNED NOT NULL COMMENT '操作用户ID',
`username` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '操作用户名',
`ip` varchar(45) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '操作IP地址',
`path` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '操作路径',
`method` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT 'HTTP方法',
`params` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL COMMENT '请求参数',
`status` int NOT NULL COMMENT '响应状态码',
`duration` int NOT NULL COMMENT '响应时间(毫秒)',
`created_at` datetime NULL DEFAULT CURRENT_TIMESTAMP COMMENT '操作时间',
PRIMARY KEY (`id`) USING BTREE,
INDEX `idx_user_id`(`user_id` ASC) USING BTREE,
INDEX `idx_created_at`(`created_at` ASC) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '操作日志表' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of users
-- ----------------------------
INSERT INTO `users` (`id`, `username`, `email`, `password_hash`, `role_id`, `role`, `is_active`, `created_at`, `updated_at`) VALUES (1, 'lq', 'liqiworker@gmail.com', '$2a$10$J9q3NfJ2X8H7Q5z5Q7z5Q7z5Q7z5Q7z5Q7z5Q7z5Q7z5Q7z5Q', 1, 'admin', 1, '2026-01-13 16:10:14', '2026-01-14 12:57:55');
INSERT INTO `users` (`id`, `username`, `email`, `password_hash`, `role_id`, `role`, `is_active`, `created_at`, `updated_at`) VALUES (2, 'editor', 'editor@example.com', '$2a$10$J9q3NfJ2X8H7Q5z5Q7z5Q7z5Q7z5Q7z5Q7z5Q7z5Q7z5Q7z5Q', 2, 'editor', 1, '2026-01-13 16:10:14', '2026-01-13 16:10:14');
-- ----------------------------
-- Table structure for work_gallery
-- ----------------------------
DROP TABLE IF EXISTS `work_gallery`;
CREATE TABLE `work_gallery` (
`id` bigint UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`work_id` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '关联的作品ID',
`image_url` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '图片URL',
`sort_order` int UNSIGNED NULL DEFAULT 0 COMMENT '排序顺序,数值越小越靠前',
`description` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '图片描述',
`created_at` datetime NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (`id`) USING BTREE,
INDEX `idx_work_id`(`work_id` ASC) USING BTREE,
INDEX `idx_sort_order`(`sort_order` ASC) USING BTREE,
CONSTRAINT `work_gallery_ibfk_1` FOREIGN KEY (`work_id`) REFERENCES `works` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT
) ENGINE = InnoDB AUTO_INCREMENT = 5 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '作品图库表' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of work_gallery
-- ----------------------------
INSERT INTO `work_gallery` VALUES (1, 'nova', 'https://images.unsplash.com/photo-1642543492481-44e81e3914a7?q=80&w=2070', 1, 'Nova 交易平台首页', '2026-01-13 16:12:17');
INSERT INTO `work_gallery` VALUES (2, 'nova', 'https://images.unsplash.com/photo-1551288049-bebda4e38f71?q=80&w=2070', 2, 'Nova 交易平台交易界面', '2026-01-13 16:12:17');
INSERT INTO `work_gallery` VALUES (3, 'archdaily', 'https://images.unsplash.com/photo-1503387762-592deb58ef4e?q=80&w=2089', 1, 'ArchDaily 网站首页', '2026-01-13 16:12:17');
INSERT INTO `work_gallery` VALUES (4, 'archdaily', 'https://images.unsplash.com/photo-1518005020951-ecc859466abc?q=80&w=1920', 2, 'ArchDaily 文章详情页', '2026-01-13 16:12:17');
-- ----------------------------
-- Table structure for work_tech_stack
-- ----------------------------
DROP TABLE IF EXISTS `work_tech_stack`;
CREATE TABLE `work_tech_stack` (
`id` bigint UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`work_id` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '关联的作品ID',
`category` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '技术分类(如:前端、后端、数据库等)',
`item` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '具体技术项Vue 3、Golang、MySQL等',
`created_at` datetime NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (`id`) USING BTREE,
INDEX `idx_work_id`(`work_id` ASC) USING BTREE,
INDEX `idx_category`(`category` ASC) USING BTREE,
CONSTRAINT `work_tech_stack_ibfk_1` FOREIGN KEY (`work_id`) REFERENCES `works` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT
) ENGINE = InnoDB AUTO_INCREMENT = 10 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '作品技术栈表' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of work_tech_stack
-- ----------------------------
INSERT INTO `work_tech_stack` VALUES (1, 'nova', '前端层', 'React 18', '2026-01-13 16:12:17');
INSERT INTO `work_tech_stack` VALUES (2, 'nova', '前端层', 'TypeScript', '2026-01-13 16:12:17');
INSERT INTO `work_tech_stack` VALUES (3, 'nova', '前端层', 'D3.js', '2026-01-13 16:12:17');
INSERT INTO `work_tech_stack` VALUES (4, 'nova', '后端服务', 'Golang', '2026-01-13 16:12:17');
INSERT INTO `work_tech_stack` VALUES (5, 'nova', '后端服务', 'gRPC', '2026-01-13 16:12:17');
INSERT INTO `work_tech_stack` VALUES (6, 'archdaily', '核心前端', 'Vue 3', '2026-01-13 16:12:17');
INSERT INTO `work_tech_stack` VALUES (7, 'archdaily', '核心前端', 'Nuxt.js', '2026-01-13 16:12:17');
INSERT INTO `work_tech_stack` VALUES (8, 'archdaily', '核心前端', 'GSAP', '2026-01-13 16:12:17');
INSERT INTO `work_tech_stack` VALUES (9, 'archdaily', 'CMS', 'Strapi', '2026-01-13 16:12:17');
-- ----------------------------
-- Table structure for works
-- ----------------------------
DROP TABLE IF EXISTS `works`;
CREATE TABLE `works` (
`id` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '作品唯一标识',
`title` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '作品标题',
`category` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '作品分类',
`year` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '创作年份',
`hero_img` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '作品主图URL',
`description` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '作品详细描述',
`is_featured` tinyint(1) NULL DEFAULT 0 COMMENT '是否为精选作品01',
`created_at` datetime NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updated_at` datetime NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`) USING BTREE,
INDEX `idx_category`(`category` ASC) USING BTREE COMMENT '按分类查询索引',
INDEX `idx_year`(`year` ASC) USING BTREE COMMENT '按年份查询索引',
INDEX `idx_is_featured`(`is_featured` ASC) USING BTREE COMMENT '按精选状态查询索引'
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '作品表' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of works
-- ----------------------------
INSERT INTO `works` VALUES ('archdaily', 'ArchDaily 网站重构', '建筑设计', '2022', 'https://images.unsplash.com/photo-1487958449943-2429e8be8625?q=80&w=2070', 'ArchDaily 是全球最受欢迎的建筑网站之一。这次重构的目标是提升移动端体验。我们使用了 Nuxt 3 进行服务端渲染SSR', 1, '2026-01-13 16:10:14', '2026-01-13 16:10:14');
INSERT INTO `works` VALUES ('nova', 'Nova 交易平台', '金融科技', '2023', 'https://images.unsplash.com/photo-1611974765270-ca12586343bb?q=80&w=2070', 'Nova 是一个专为机构交易员设计的高频交易终端。我们面临的最大挑战是如何在处理毫秒级市场数据的同时,保持界面的流畅响应。我们采用 Web Worker 来处理繁重的数据计算,避免阻塞主线程。', 1, '2026-01-13 16:10:14', '2026-01-13 16:10:14');
SET FOREIGN_KEY_CHECKS = 1;

View File

@@ -0,0 +1,109 @@
package repositories
import (
"log"
"github.com/niangaodev/art-code/config"
"github.com/niangaodev/art-code/models"
)
// CreateOperationLog 创建操作日志
func CreateOperationLog(operationLog *models.OperationLog) error {
query := `
INSERT INTO operation_logs (user_id, username, ip, path, method, params, status, duration, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, NOW())
`
_, err := config.DB.Exec(
query,
operationLog.UserID,
operationLog.Username,
operationLog.IP,
operationLog.Path,
operationLog.Method,
operationLog.Params,
operationLog.Status,
operationLog.Duration,
)
if err != nil {
log.Printf("Error creating operation log: %v", err)
return err
}
return nil
}
// GetOperationLogs 获取操作日志列表
func GetOperationLogs(page, pageSize int) ([]models.OperationLog, int64, error) {
// 计算偏移量
offset := (page - 1) * pageSize
// 获取总记录数
var total int64
countQuery := "SELECT COUNT(*) FROM operation_logs"
if err := config.DB.QueryRow(countQuery).Scan(&total); err != nil {
log.Printf("Error counting operation logs: %v", err)
return nil, 0, err
}
// 获取分页数据
query := `
SELECT id, user_id, username, ip, path, method, params, status, duration, created_at
FROM operation_logs
ORDER BY created_at DESC
LIMIT ? OFFSET ?
`
rows, err := config.DB.Query(query, pageSize, offset)
if err != nil {
log.Printf("Error querying operation logs: %v", err)
return nil, 0, err
}
defer rows.Close()
var logs []models.OperationLog
for rows.Next() {
var operationLog models.OperationLog
if err := rows.Scan(
&operationLog.ID,
&operationLog.UserID,
&operationLog.Username,
&operationLog.IP,
&operationLog.Path,
&operationLog.Method,
&operationLog.Params,
&operationLog.Status,
&operationLog.Duration,
&operationLog.CreatedAt,
); err != nil {
log.Printf("Error scanning operation log: %v", err)
continue
}
logs = append(logs, operationLog)
}
return logs, total, nil
}
// BuildOperationLogResponse 构建操作日志响应
func BuildOperationLogResponse(log *models.OperationLog) *models.OperationLogResponse {
return &models.OperationLogResponse{
ID: log.ID,
UserID: log.UserID,
Username: log.Username,
IP: log.IP,
Path: log.Path,
Method: log.Method,
Params: log.Params,
Status: log.Status,
Duration: log.Duration,
CreatedAt: log.CreatedAt.Format("2006-01-02 15:04:05"),
}
}
// BuildOperationLogsResponse 构建操作日志列表响应
func BuildOperationLogsResponse(logs []models.OperationLog) []models.OperationLogResponse {
var responses []models.OperationLogResponse
for _, log := range logs {
responses = append(responses, *BuildOperationLogResponse(&log))
}
return responses
}

View File

@@ -0,0 +1,80 @@
package repositories
import (
"log"
"github.com/niangaodev/art-code/config"
"github.com/niangaodev/art-code/models"
)
// GetPermissions 获取所有权限
func GetPermissions() ([]models.Permission, error) {
query := "SELECT id, name, resource, action, created_at, updated_at FROM permissions"
rows, err := config.DB.Query(query)
if err != nil {
log.Printf("Error getting permissions: %v", err)
return nil, err
}
defer rows.Close()
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 {
log.Printf("Error scanning permission: %v", err)
continue
}
permissions = append(permissions, permission)
}
return permissions, nil
}
// GetPermissionsByRoleID 获取指定角色的权限
func GetPermissionsByRoleID(roleID uint) ([]models.Permission, error) {
query := `
SELECT p.id, p.name, p.resource, p.action, p.created_at, p.updated_at
FROM permissions p
JOIN role_permissions rp ON p.id = rp.permission_id
WHERE rp.role_id = ?
`
rows, err := config.DB.Query(query, roleID)
if err != nil {
log.Printf("Error getting permissions by role id: %v", err)
return nil, err
}
defer rows.Close()
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 {
log.Printf("Error scanning permission: %v", err)
continue
}
permissions = append(permissions, permission)
}
return permissions, nil
}
// BuildPermissionResponse 构建权限响应
func BuildPermissionResponse(permission *models.Permission) *models.PermissionResponse {
return &models.PermissionResponse{
ID: permission.ID,
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"),
}
}
// BuildPermissionsResponse 构建权限列表响应
func BuildPermissionsResponse(permissions []models.Permission) []models.PermissionResponse {
var responses []models.PermissionResponse
for _, permission := range permissions {
responses = append(responses, *BuildPermissionResponse(&permission))
}
return responses
}

View File

@@ -0,0 +1,351 @@
package repositories
import (
"database/sql"
"log"
"github.com/niangaodev/art-code/config"
"github.com/niangaodev/art-code/models"
)
// GetPosts 获取所有博客文章
func GetPosts() ([]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 date DESC"
rows, err := config.DB.Query(query)
if err != nil {
log.Printf("Error querying posts: %v", err)
return nil, err
}
defer rows.Close()
var posts []models.Post
for rows.Next() {
var post models.Post
if err := rows.Scan(
&post.ID,
&post.Title,
&post.Category,
&post.Date,
&post.Excerpt,
&post.Content,
&post.ReadCount,
&post.IsPublished,
&post.CreatedAt,
&post.UpdatedAt,
); err != nil {
log.Printf("Error scanning post: %v", err)
continue
}
posts = append(posts, post)
}
return posts, nil
}
// GetPostByID 根据ID获取博客文章
func GetPostByID(id string) (*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"
row := config.DB.QueryRow(query, id)
var post models.Post
if err := row.Scan(
&post.ID,
&post.Title,
&post.Category,
&post.Date,
&post.Excerpt,
&post.Content,
&post.ReadCount,
&post.IsPublished,
&post.CreatedAt,
&post.UpdatedAt,
); err != nil {
if err == sql.ErrNoRows {
return nil, nil
}
log.Printf("Error scanning post by ID: %v", err)
return nil, err
}
// 更新阅读量
updateReadCountQuery := "UPDATE posts SET read_count = read_count + 1 WHERE id = ?"
if _, err := config.DB.Exec(updateReadCountQuery, id); err != nil {
log.Printf("Error updating post read count: %v", err)
}
return &post, nil
}
// GetAllPosts 获取所有博客文章(包括未发布的)
func GetAllPosts() ([]models.Post, error) {
query := "SELECT id, title, category, date, excerpt, content, read_count, is_published, created_at, updated_at FROM posts ORDER BY date DESC"
rows, err := config.DB.Query(query)
if err != nil {
log.Printf("Error querying all posts: %v", err)
return nil, err
}
defer rows.Close()
var posts []models.Post
for rows.Next() {
var post models.Post
if err := rows.Scan(
&post.ID,
&post.Title,
&post.Category,
&post.Date,
&post.Excerpt,
&post.Content,
&post.ReadCount,
&post.IsPublished,
&post.CreatedAt,
&post.UpdatedAt,
); err != nil {
log.Printf("Error scanning post: %v", err)
continue
}
posts = append(posts, post)
}
return posts, nil
}
// CreatePost 创建博客文章
func CreatePost(post *models.Post) error {
query := `
INSERT INTO posts (id, title, category, date, excerpt, content, is_published, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, NOW(), NOW())
`
_, err := config.DB.Exec(
query,
post.ID,
post.Title,
post.Category,
post.Date,
post.Excerpt,
post.Content,
post.IsPublished,
)
if err != nil {
log.Printf("Error creating post: %v", err)
return err
}
return nil
}
// UpdatePost 更新博客文章
func UpdatePost(post *models.Post) error {
query := `
UPDATE posts SET title = ?, category = ?, date = ?, excerpt = ?, content = ?, is_published = ?, updated_at = NOW()
WHERE id = ?
`
_, err := config.DB.Exec(
query,
post.Title,
post.Category,
post.Date,
post.Excerpt,
post.Content,
post.IsPublished,
post.ID,
)
if err != nil {
log.Printf("Error updating post: %v", err)
return err
}
return nil
}
// DeletePost 删除博客文章
func DeletePost(id string) error {
query := "DELETE FROM posts WHERE id = ?"
_, err := config.DB.Exec(query, id)
if err != nil {
log.Printf("Error deleting post: %v", err)
return err
}
return nil
}
// GetPostCount 获取文章总数
func GetPostCount() (int, error) {
var count int
query := "SELECT COUNT(*) FROM posts"
row := config.DB.QueryRow(query)
err := row.Scan(&count)
if err != nil {
log.Printf("Error getting post count: %v", err)
return 0, err
}
return count, nil
}
// BuildPostResponse 构建博客文章响应
func BuildPostResponse(post *models.Post, includeContent bool) *models.PostResponse {
response := &models.PostResponse{
ID: post.ID,
Title: post.Title,
Category: post.Category,
Date: post.Date.Format("2006-01-02"),
Excerpt: post.Excerpt,
}
if includeContent {
response.Content = post.Content
}
return response
}
// BuildPostsResponse 构建博客文章列表响应
func BuildPostsResponse(posts []models.Post) []models.PostResponse {
var responses []models.PostResponse
for _, post := range posts {
responses = append(responses, *BuildPostResponse(&post, false))
}
return responses
}
// SavePostHistory 保存文章历史记录
func SavePostHistory(post *models.Post, modifiedBy uint) error {
// 获取当前最大版本号
var maxVersion uint
query := "SELECT COALESCE(MAX(version), 0) FROM post_history WHERE post_id = ?"
if err := config.DB.QueryRow(query, post.ID).Scan(&maxVersion); err != nil {
log.Printf("Error getting max version: %v", err)
return err
}
// 插入新的历史记录
insertQuery := `
INSERT INTO post_history (
post_id, version, title, category, date, excerpt, content,
is_published, modified_by, modified_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, NOW())
`
_, err := config.DB.Exec(
insertQuery,
post.ID,
maxVersion+1,
post.Title,
post.Category,
post.Date,
post.Excerpt,
post.Content,
post.IsPublished,
modifiedBy,
)
if err != nil {
log.Printf("Error saving post history: %v", err)
return err
}
return nil
}
// GetPostHistory 获取文章历史记录
func GetPostHistory(postID string) ([]models.PostHistory, error) {
query := `
SELECT id, post_id, version, title, category, date, 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 {
log.Printf("Error querying post history: %v", err)
return nil, err
}
defer rows.Close()
var history []models.PostHistory
for rows.Next() {
var h models.PostHistory
if err := rows.Scan(
&h.ID,
&h.PostID,
&h.Version,
&h.Title,
&h.Category,
&h.Date,
&h.Excerpt,
&h.Content,
&h.IsPublished,
&h.ModifiedBy,
&h.ModifiedAt,
&h.CreatedAt,
); err != nil {
log.Printf("Error scanning post history: %v", err)
continue
}
history = append(history, h)
}
return history, nil
}
// GetPostHistoryByVersion 获取指定版本的文章历史记录
func GetPostHistoryByVersion(postID string, version uint) (*models.PostHistory, error) {
query := `
SELECT id, post_id, version, title, category, date, 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.Date,
&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: history.Date.Format("2006-01-02"),
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"),
}
}
// BuildPostHistoryResponses 构建文章历史记录列表响应
func BuildPostHistoryResponses(history []models.PostHistory) []models.PostHistoryResponse {
var responses []models.PostHistoryResponse
for _, h := range history {
responses = append(responses, *BuildPostHistoryResponse(&h))
}
return responses
}

View File

@@ -0,0 +1,166 @@
package repositories
import (
"log"
"time"
"github.com/niangaodev/art-code/config"
"github.com/niangaodev/art-code/models"
)
// GetRoles 获取所有角色
func GetRoles() ([]models.Role, error) {
query := "SELECT id, name, description, created_at, updated_at FROM roles"
rows, err := config.DB.Query(query)
if err != nil {
log.Printf("Error getting roles: %v", err)
return nil, err
}
defer rows.Close()
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 {
log.Printf("Error scanning role: %v", err)
continue
}
// 获取权限
permissions, _ := GetPermissionsByRoleID(role.ID)
role.Permissions = permissions
roles = append(roles, role)
}
return roles, nil
}
// GetRoleByID 根据ID获取角色
func GetRoleByID(id uint) (*models.Role, error) {
query := "SELECT id, name, description, created_at, updated_at FROM roles WHERE id = ?"
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 {
log.Printf("Error getting role by ID: %v", err)
return nil, err
}
// 获取权限
permissions, _ := GetPermissionsByRoleID(role.ID)
role.Permissions = permissions
return &role, nil
}
// GetRoleByName 根据名称获取角色
func GetRoleByName(name string) (*models.Role, error) {
query := "SELECT id, name, description, created_at, updated_at FROM roles WHERE name = ?"
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 {
log.Printf("Error getting role by name: %v", err)
return nil, err
}
return &role, nil
}
// 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)
if err != nil {
log.Printf("Error creating role: %v", err)
return err
}
id, err := result.LastInsertId()
if err != nil {
return err
}
role.ID = uint(id)
role.CreatedAt = time.Now()
role.UpdatedAt = time.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)
if err != nil {
log.Printf("Error updating role: %v", err)
return err
}
return nil
}
// DeleteRole 删除角色
func DeleteRole(id uint) error {
query := "DELETE FROM roles WHERE id = ?"
_, err := config.DB.Exec(query, id)
if err != nil {
log.Printf("Error deleting role: %v", err)
return err
}
return nil
}
// AssignPermissionsToRole 给角色分配权限
func AssignPermissionsToRole(roleID uint, permissionIDs []uint) error {
// 开启事务
tx, err := config.DB.Begin()
if err != nil {
return err
}
// 先删除旧权限
_, err = tx.Exec("DELETE FROM role_permissions WHERE role_id = ?", roleID)
if err != nil {
tx.Rollback()
return err
}
// 插入新权限
stmt, err := tx.Prepare("INSERT INTO role_permissions (role_id, permission_id) VALUES (?, ?)")
if err != nil {
tx.Rollback()
return err
}
defer stmt.Close()
for _, permID := range permissionIDs {
_, err = stmt.Exec(roleID, permID)
if err != nil {
tx.Rollback()
return err
}
}
return tx.Commit()
}
// BuildRoleResponse 构建角色响应
func BuildRoleResponse(role *models.Role) *models.RoleResponse {
return &models.RoleResponse{
ID: role.ID,
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"),
}
}
// BuildRolesResponse 构建角色列表响应
func BuildRolesResponse(roles []models.Role) []models.RoleResponse {
var responses []models.RoleResponse
for _, role := range roles {
responses = append(responses, *BuildRoleResponse(&role))
}
return responses
}

View File

@@ -0,0 +1,144 @@
package repositories
import (
"database/sql"
"log"
"github.com/niangaodev/art-code/config"
"github.com/niangaodev/art-code/models"
)
// GetSettings 获取所有系统配置
func GetSettings() ([]models.Setting, error) {
query := "SELECT id, key_name, value, description, created_at, updated_at FROM settings ORDER BY key_name"
rows, err := config.DB.Query(query)
if err != nil {
log.Printf("Error querying settings: %v", err)
return nil, err
}
defer rows.Close()
var settings []models.Setting
for rows.Next() {
var setting models.Setting
if err := rows.Scan(
&setting.ID,
&setting.KeyName,
&setting.Value,
&setting.Description,
&setting.CreatedAt,
&setting.UpdatedAt,
); err != nil {
log.Printf("Error scanning setting: %v", err)
continue
}
settings = append(settings, setting)
}
return settings, nil
}
// GetSettingByKey 根据键名获取系统配置
func GetSettingByKey(keyName string) (*models.Setting, error) {
query := "SELECT id, key_name, value, description, created_at, updated_at FROM settings WHERE key_name = ?"
row := config.DB.QueryRow(query, keyName)
var setting models.Setting
if err := row.Scan(
&setting.ID,
&setting.KeyName,
&setting.Value,
&setting.Description,
&setting.CreatedAt,
&setting.UpdatedAt,
); err != nil {
if err == sql.ErrNoRows {
return nil, nil
}
log.Printf("Error scanning setting by key: %v", err)
return nil, err
}
return &setting, nil
}
// UpdateSetting 更新系统配置
func UpdateSetting(setting *models.Setting) error {
query := `
UPDATE settings SET value = ?, description = ?, updated_at = NOW()
WHERE key_name = ?
`
_, err := config.DB.Exec(
query,
setting.Value,
setting.Description,
setting.KeyName,
)
if err != nil {
log.Printf("Error updating setting: %v", err)
return err
}
return nil
}
// CreateSetting 创建系统配置
func CreateSetting(setting *models.Setting) error {
query := `
INSERT INTO settings (key_name, value, description, created_at, updated_at)
VALUES (?, ?, ?, NOW(), NOW())
`
result, err := config.DB.Exec(
query,
setting.KeyName,
setting.Value,
setting.Description,
)
if err != nil {
log.Printf("Error creating setting: %v", err)
return err
}
// 获取自增ID
id, err := result.LastInsertId()
if err != nil {
log.Printf("Error getting last insert ID: %v", err)
return err
}
setting.ID = uint(id)
return nil
}
// DeleteSetting 删除系统配置
func DeleteSetting(keyName string) error {
query := "DELETE FROM settings WHERE key_name = ?"
_, err := config.DB.Exec(query, keyName)
if err != nil {
log.Printf("Error deleting setting: %v", err)
return err
}
return nil
}
// BuildSettingResponse 构建系统配置响应
func BuildSettingResponse(setting *models.Setting) *models.SettingResponse {
return &models.SettingResponse{
ID: setting.ID,
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"),
}
}
// BuildSettingsResponse 构建系统配置列表响应
func BuildSettingsResponse(settings []models.Setting) []models.SettingResponse {
var responses []models.SettingResponse
for _, setting := range settings {
responses = append(responses, *BuildSettingResponse(&setting))
}
return responses
}

View File

@@ -0,0 +1,163 @@
package repositories
import (
"database/sql"
"log"
"github.com/niangaodev/art-code/config"
"github.com/niangaodev/art-code/models"
)
// 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"
rows, err := config.DB.Query(query)
if err != nil {
log.Printf("Error querying snippets: %v", err)
return nil, 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,
); err != nil {
log.Printf("Error scanning snippet: %v", err)
continue
}
snippets = append(snippets, snippet)
}
return snippets, nil
}
// 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 = ?"
row := config.DB.QueryRow(query, id)
var snippet models.Snippet
if err := row.Scan(
&snippet.ID,
&snippet.Title,
&snippet.Code,
&snippet.Type,
&snippet.Description,
&snippet.ViewCount,
&snippet.CreatedAt,
&snippet.UpdatedAt,
); err != nil {
if err == sql.ErrNoRows {
return nil, nil
}
log.Printf("Error scanning snippet by ID: %v", err)
return nil, err
}
// 更新查看次数
updateViewCountQuery := "UPDATE snippets SET view_count = view_count + 1 WHERE id = ?"
if _, err := config.DB.Exec(updateViewCountQuery, id); err != nil {
log.Printf("Error updating snippet view count: %v", err)
}
return &snippet, nil
}
// BuildSnippetResponse 构建代码片段响应
func BuildSnippetResponse(snippet *models.Snippet) *models.SnippetResponse {
return &models.SnippetResponse{
ID: snippet.ID,
Title: snippet.Title,
Code: snippet.Code,
Type: snippet.Type,
}
}
// BuildSnippetsResponse 构建代码片段列表响应
func BuildSnippetsResponse(snippets []models.Snippet) []models.SnippetResponse {
var responses []models.SnippetResponse
for _, snippet := range snippets {
responses = append(responses, *BuildSnippetResponse(&snippet))
}
return responses
}
// CreateSnippet 创建代码片段
func CreateSnippet(snippet *models.Snippet) error {
query := `
INSERT INTO snippets (id, title, code, type, description, view_count, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, 0, NOW(), NOW())
`
_, err := config.DB.Exec(
query,
snippet.ID,
snippet.Title,
snippet.Code,
snippet.Type,
snippet.Description,
)
if err != nil {
log.Printf("Error creating snippet: %v", err)
return err
}
return nil
}
// UpdateSnippet 更新代码片段
func UpdateSnippet(snippet *models.Snippet) error {
query := `
UPDATE snippets SET title = ?, code = ?, type = ?, description = ?, updated_at = NOW()
WHERE id = ?
`
_, err := config.DB.Exec(
query,
snippet.Title,
snippet.Code,
snippet.Type,
snippet.Description,
snippet.ID,
)
if err != nil {
log.Printf("Error updating snippet: %v", err)
return err
}
return nil
}
// DeleteSnippet 删除代码片段
func DeleteSnippet(id string) error {
query := "DELETE FROM snippets WHERE id = ?"
_, err := config.DB.Exec(query, id)
if err != nil {
log.Printf("Error deleting snippet: %v", err)
return err
}
return nil
}
// GetSnippetCount 获取代码片段总数
func GetSnippetCount() (int, error) {
var count int
query := "SELECT COUNT(*) FROM snippets"
row := config.DB.QueryRow(query)
err := row.Scan(&count)
if err != nil {
log.Printf("Error getting snippet count: %v", err)
return 0, err
}
return count, nil
}

View File

@@ -0,0 +1,264 @@
package repositories
import (
"database/sql"
"log"
"github.com/niangaodev/art-code/config"
"github.com/niangaodev/art-code/models"
)
// GetTags 获取所有标签
func GetTags() ([]models.Tag, error) {
query := "SELECT id, name, slug, created_at, updated_at FROM tags ORDER BY name ASC"
rows, err := config.DB.Query(query)
if err != nil {
log.Printf("Error querying tags: %v", err)
return nil, err
}
defer rows.Close()
var tags []models.Tag
for rows.Next() {
var tag models.Tag
if err := rows.Scan(
&tag.ID,
&tag.Name,
&tag.Slug,
&tag.CreatedAt,
&tag.UpdatedAt,
); err != nil {
log.Printf("Error scanning tag: %v", err)
continue
}
tags = append(tags, tag)
}
return tags, nil
}
// GetTagByID 根据ID获取标签
func GetTagByID(id uint) (*models.Tag, error) {
query := "SELECT id, name, slug, created_at, updated_at FROM tags WHERE id = ?"
row := config.DB.QueryRow(query, id)
var tag models.Tag
if err := row.Scan(
&tag.ID,
&tag.Name,
&tag.Slug,
&tag.CreatedAt,
&tag.UpdatedAt,
); err != nil {
if err == sql.ErrNoRows {
return nil, nil
}
log.Printf("Error scanning tag by ID: %v", err)
return nil, err
}
return &tag, nil
}
// GetTagBySlug 根据Slug获取标签
func GetTagBySlug(slug string) (*models.Tag, error) {
query := "SELECT id, name, slug, created_at, updated_at FROM tags WHERE slug = ?"
row := config.DB.QueryRow(query, slug)
var tag models.Tag
if err := row.Scan(
&tag.ID,
&tag.Name,
&tag.Slug,
&tag.CreatedAt,
&tag.UpdatedAt,
); err != nil {
if err == sql.ErrNoRows {
return nil, nil
}
log.Printf("Error scanning tag by slug: %v", err)
return nil, err
}
return &tag, nil
}
// CreateTag 创建标签
func CreateTag(tag *models.Tag) error {
query := `
INSERT INTO tags (name, slug, created_at, updated_at)
VALUES (?, ?, NOW(), NOW())
`
result, err := config.DB.Exec(
query,
tag.Name,
tag.Slug,
)
if err != nil {
log.Printf("Error creating tag: %v", err)
return err
}
// 获取自增ID
id, err := result.LastInsertId()
if err != nil {
log.Printf("Error getting last insert ID: %v", err)
return err
}
tag.ID = uint(id)
return nil
}
// UpdateTag 更新标签
func UpdateTag(tag *models.Tag) error {
query := `
UPDATE tags SET name = ?, slug = ?, updated_at = NOW()
WHERE id = ?
`
_, err := config.DB.Exec(
query,
tag.Name,
tag.Slug,
tag.ID,
)
if err != nil {
log.Printf("Error updating tag: %v", err)
return err
}
return nil
}
// DeleteTag 删除标签
func DeleteTag(id uint) error {
// 先删除关联的文章标签关系
query := "DELETE FROM post_tags WHERE tag_id = ?"
_, err := config.DB.Exec(query, id)
if err != nil {
log.Printf("Error deleting post-tag relationships: %v", err)
return err
}
// 再删除标签
query = "DELETE FROM tags WHERE id = ?"
_, err = config.DB.Exec(query, id)
if err != nil {
log.Printf("Error deleting tag: %v", err)
return err
}
return nil
}
// GetTagsByPostID 根据文章ID获取标签
func GetTagsByPostID(postID string) ([]models.Tag, error) {
query := `
SELECT t.id, t.name, t.slug, t.created_at, t.updated_at
FROM tags t
JOIN post_tags pt ON t.id = pt.tag_id
WHERE pt.post_id = ?
ORDER BY t.name ASC
`
rows, err := config.DB.Query(query, postID)
if err != nil {
log.Printf("Error querying tags by post ID: %v", err)
return nil, err
}
defer rows.Close()
var tags []models.Tag
for rows.Next() {
var tag models.Tag
if err := rows.Scan(
&tag.ID,
&tag.Name,
&tag.Slug,
&tag.CreatedAt,
&tag.UpdatedAt,
); err != nil {
log.Printf("Error scanning tag: %v", err)
continue
}
tags = append(tags, tag)
}
return tags, nil
}
// AddTagToPost 为文章添加标签
func AddTagToPost(postID string, tagID uint) error {
query := `
INSERT IGNORE INTO post_tags (post_id, tag_id, created_at)
VALUES (?, ?, NOW())
`
_, err := config.DB.Exec(query, postID, tagID)
if err != nil {
log.Printf("Error adding tag to post: %v", err)
return err
}
return nil
}
// RemoveTagFromPost 从文章移除标签
func RemoveTagFromPost(postID string, tagID uint) error {
query := "DELETE FROM post_tags WHERE post_id = ? AND tag_id = ?"
_, err := config.DB.Exec(query, postID, tagID)
if err != nil {
log.Printf("Error removing tag from post: %v", err)
return err
}
return nil
}
// 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
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
`
rows, err := config.DB.Query(query, tagID)
if err != nil {
log.Printf("Error querying posts by tag ID: %v", err)
return nil, err
}
defer rows.Close()
var posts []models.Post
for rows.Next() {
var post models.Post
if err := rows.Scan(
&post.ID,
&post.Title,
&post.Category,
&post.Date,
&post.Excerpt,
&post.Content,
&post.ReadCount,
&post.IsPublished,
&post.CreatedAt,
&post.UpdatedAt,
); err != nil {
log.Printf("Error scanning post: %v", err)
continue
}
posts = append(posts, post)
}
return posts, nil
}
// BuildTagsResponse 构建标签列表响应
func BuildTagsResponse(tags []models.Tag) []models.Tag {
return tags
}
// BuildTagResponse 构建标签响应
func BuildTagResponse(tag *models.Tag) *models.Tag {
return tag
}

View File

@@ -0,0 +1,293 @@
package repositories
import (
"database/sql"
"log"
"github.com/niangaodev/art-code/config"
"github.com/niangaodev/art-code/models"
)
// 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
FROM users u
LEFT JOIN roles r ON u.role_id = r.id
WHERE u.username = ?
`
row := config.DB.QueryRow(query, username)
var user models.User
var roleID sql.NullInt64 // Handle nullable role_id
var roleName sql.NullString // Handle nullable role name
if err := row.Scan(
&user.ID,
&user.Username,
&user.Email,
&user.PasswordHash,
&roleID,
&roleName,
&user.IsActive,
&user.CreatedAt,
&user.UpdatedAt,
); err != nil {
if err == sql.ErrNoRows {
return nil, nil
}
log.Printf("Error scanning user by username: %v", err)
return nil, err
}
if roleID.Valid {
user.RoleID = uint(roleID.Int64)
}
if roleName.Valid {
user.Role = roleName.String
}
return &user, nil
}
// 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
FROM users u
LEFT JOIN roles r ON u.role_id = r.id
WHERE u.id = ?
`
row := config.DB.QueryRow(query, id)
var user models.User
var roleID sql.NullInt64
var roleName sql.NullString
if err := row.Scan(
&user.ID,
&user.Username,
&user.Email,
&user.PasswordHash,
&roleID,
&roleName,
&user.IsActive,
&user.CreatedAt,
&user.UpdatedAt,
); err != nil {
if err == sql.ErrNoRows {
return nil, nil
}
log.Printf("Error scanning user by ID: %v", err)
return nil, err
}
if roleID.Valid {
user.RoleID = uint(roleID.Int64)
}
if roleName.Valid {
user.Role = roleName.String
}
return &user, nil
}
// 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
FROM users u
LEFT JOIN roles r ON u.role_id = r.id
ORDER BY u.created_at DESC
`
rows, err := config.DB.Query(query)
if err != nil {
log.Printf("Error querying users: %v", err)
return nil, err
}
defer rows.Close()
var users []models.User
for rows.Next() {
var user models.User
var roleID sql.NullInt64
var roleName sql.NullString
if err := rows.Scan(
&user.ID,
&user.Username,
&user.Email,
&user.PasswordHash,
&roleID,
&roleName,
&user.IsActive,
&user.CreatedAt,
&user.UpdatedAt,
); err != nil {
log.Printf("Error scanning user: %v", err)
continue
}
if roleID.Valid {
user.RoleID = uint(roleID.Int64)
}
if roleName.Valid {
user.Role = roleName.String
}
users = append(users, user)
}
return users, nil
}
// CreateUser 创建用户
func CreateUser(user *models.User) error {
// 如果提供了RoleID确保它有效。如果没有RoleID但有Role name尝试查找RoleID
if user.RoleID == 0 && user.Role != "" {
role, err := GetRoleByName(user.Role)
if err == nil && role != nil {
user.RoleID = role.ID
}
}
query := `
INSERT INTO users (username, email, password_hash, role_id, role, is_active, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, NOW(), NOW())
`
var roleID interface{}
if user.RoleID != 0 {
roleID = user.RoleID
} else {
roleID = nil
}
result, err := config.DB.Exec(
query,
user.Username,
user.Email,
user.PasswordHash,
roleID,
user.Role, // Fallback legacy column
user.IsActive,
)
if err != nil {
log.Printf("Error creating user: %v", err)
return err
}
// 获取自增ID
id, err := result.LastInsertId()
if err != nil {
log.Printf("Error getting last insert ID: %v", err)
return err
}
user.ID = uint(id)
return nil
}
// UpdateUser 更新用户
func UpdateUser(user *models.User) error {
// 同样尝试解析RoleID
if user.RoleID == 0 && user.Role != "" {
role, err := GetRoleByName(user.Role)
if err == nil && role != nil {
user.RoleID = role.ID
}
}
query := `
UPDATE users SET username = ?, email = ?, role_id = ?, role = ?, is_active = ?, updated_at = NOW()
WHERE id = ?
`
var roleID interface{}
if user.RoleID != 0 {
roleID = user.RoleID
} else {
roleID = nil
}
_, err := config.DB.Exec(
query,
user.Username,
user.Email,
roleID,
user.Role,
user.IsActive,
user.ID,
)
if err != nil {
log.Printf("Error updating user: %v", err)
return err
}
return nil
}
// UpdateUserPassword 更新用户密码
func UpdateUserPassword(id uint, passwordHash string) error {
query := `
UPDATE users SET password_hash = ?, updated_at = NOW()
WHERE id = ?
`
_, err := config.DB.Exec(query, passwordHash, id)
if err != nil {
log.Printf("Error updating user password: %v", err)
return err
}
return nil
}
// DeleteUser 删除用户
func DeleteUser(id uint) error {
query := "DELETE FROM users WHERE id = ?"
_, err := config.DB.Exec(query, id)
if err != nil {
log.Printf("Error deleting user: %v", err)
return err
}
return nil
}
// GetUserCount 获取用户总数
func GetUserCount() (int, error) {
var count int
query := "SELECT COUNT(*) FROM users"
row := config.DB.QueryRow(query)
err := row.Scan(&count)
if err != nil {
log.Printf("Error getting user count: %v", err)
return 0, err
}
return count, nil
}
// BuildUserResponse 构建用户响应
func BuildUserResponse(user *models.User) *models.UserResponse {
return &models.UserResponse{
ID: user.ID,
Username: user.Username,
Email: user.Email,
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"),
}
}
// BuildUsersResponse 构建用户列表响应
func BuildUsersResponse(users []models.User) []models.UserResponse {
var responses []models.UserResponse
for _, user := range users {
responses = append(responses, *BuildUserResponse(&user))
}
return responses
}

View File

@@ -0,0 +1,301 @@
package repositories
import (
"database/sql"
"log"
"github.com/niangaodev/art-code/config"
"github.com/niangaodev/art-code/models"
)
// GetWorks 获取所有作品
func GetWorks() ([]models.Work, error) {
query := "SELECT id, title, category, year, hero_img, description, is_featured, created_at, updated_at FROM works"
rows, err := config.DB.Query(query)
if err != nil {
log.Printf("Error querying works: %v", err)
return nil, 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,
); err != nil {
log.Printf("Error scanning work: %v", err)
continue
}
works = append(works, work)
}
return works, nil
}
// 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 = ?"
row := config.DB.QueryRow(query, id)
var work models.Work
if err := row.Scan(
&work.ID,
&work.Title,
&work.Category,
&work.Year,
&work.HeroImg,
&work.Description,
&work.IsFeatured,
&work.CreatedAt,
&work.UpdatedAt,
); err != nil {
if err == sql.ErrNoRows {
return nil, nil
}
log.Printf("Error scanning work by ID: %v", err)
return nil, err
}
return &work, nil
}
// GetWorkTechStack 获取作品的技术栈
func GetWorkTechStack(workID string) ([]models.WorkTechStack, error) {
query := "SELECT id, work_id, category, item, created_at FROM work_tech_stack WHERE work_id = ?"
rows, err := config.DB.Query(query, workID)
if err != nil {
log.Printf("Error querying work tech stack: %v", err)
return nil, err
}
defer rows.Close()
var techStacks []models.WorkTechStack
for rows.Next() {
var techStack models.WorkTechStack
if err := rows.Scan(
&techStack.ID,
&techStack.WorkID,
&techStack.Category,
&techStack.Item,
&techStack.CreatedAt,
); err != nil {
log.Printf("Error scanning work tech stack: %v", err)
continue
}
techStacks = append(techStacks, techStack)
}
return techStacks, nil
}
// 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"
rows, err := config.DB.Query(query, workID)
if err != nil {
log.Printf("Error querying work gallery: %v", err)
return nil, err
}
defer rows.Close()
var galleries []models.WorkGallery
for rows.Next() {
var gallery models.WorkGallery
if err := rows.Scan(
&gallery.ID,
&gallery.WorkID,
&gallery.ImageURL,
&gallery.SortOrder,
&gallery.Description,
&gallery.CreatedAt,
); err != nil {
log.Printf("Error scanning work gallery: %v", err)
continue
}
galleries = append(galleries, gallery)
}
return galleries, nil
}
// BuildWorkResponse 构建作品响应,包含关联数据
func BuildWorkResponse(work *models.Work) (*models.WorkResponse, error) {
// 获取技术栈
techStacks, err := GetWorkTechStack(work.ID)
if err != nil {
return nil, err
}
// 按类别分组技术栈
techStackMap := make(map[string][]string)
for _, ts := range techStacks {
techStackMap[ts.Category] = append(techStackMap[ts.Category], ts.Item)
}
// 转换为前端期望的格式
var techStackResponse []map[string]interface{}
for category, items := range techStackMap {
techStackResponse = append(techStackResponse, map[string]interface{}{
"category": category,
"items": items,
})
}
// 获取图库
galleries, err := GetWorkGallery(work.ID)
if err != nil {
return nil, err
}
// 提取图片URL
var galleryImages []string
for _, g := range galleries {
galleryImages = append(galleryImages, g.ImageURL)
}
// 获取下一个作品ID
nextWorkID, err := GetNextWorkID(work.ID)
if err != nil {
log.Printf("Error getting next work ID: %v", err)
nextWorkID = ""
}
return &models.WorkResponse{
ID: work.ID,
Title: work.Title,
Category: work.Category,
Year: work.Year,
HeroImg: work.HeroImg,
Desc: work.Description,
TechStack: techStackResponse,
Gallery: galleryImages,
Links: map[string]interface{}{
"live": "#",
},
Next: nextWorkID,
}, nil
}
// GetNextWorkID 获取下一个作品ID简单实现实际可能需要更复杂的逻辑
func GetNextWorkID(currentID string) (string, error) {
// 获取所有作品ID
query := "SELECT id FROM works"
rows, err := config.DB.Query(query)
if err != nil {
return "", err
}
defer rows.Close()
var ids []string
for rows.Next() {
var id string
if err := rows.Scan(&id); err != nil {
continue
}
ids = append(ids, id)
}
// 找到当前ID的索引
index := -1
for i, id := range ids {
if id == currentID {
index = i
break
}
}
// 如果没找到或者是最后一个,返回第一个
if index == -1 || index == len(ids)-1 {
if len(ids) > 0 {
return ids[0], nil
}
return "", nil
}
// 返回下一个
return ids[index+1], nil
}
// CreateWork 创建作品
func CreateWork(work *models.Work) error {
query := `
INSERT INTO works (id, title, category, year, hero_img, description, is_featured, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, NOW(), NOW())
`
_, err := config.DB.Exec(
query,
work.ID,
work.Title,
work.Category,
work.Year,
work.HeroImg,
work.Description,
work.IsFeatured,
)
if err != nil {
log.Printf("Error creating work: %v", err)
return err
}
return nil
}
// UpdateWork 更新作品
func UpdateWork(work *models.Work) error {
query := `
UPDATE works SET title = ?, category = ?, year = ?, hero_img = ?, description = ?, is_featured = ?, updated_at = NOW()
WHERE id = ?
`
_, err := config.DB.Exec(
query,
work.Title,
work.Category,
work.Year,
work.HeroImg,
work.Description,
work.IsFeatured,
work.ID,
)
if err != nil {
log.Printf("Error updating work: %v", err)
return err
}
return nil
}
// DeleteWork 删除作品
func DeleteWork(id string) error {
query := "DELETE FROM works WHERE id = ?"
_, err := config.DB.Exec(query, id)
if err != nil {
log.Printf("Error deleting work: %v", err)
return err
}
return nil
}
// GetWorkCount 获取作品总数
func GetWorkCount() (int, error) {
var count int
query := "SELECT COUNT(*) FROM works"
row := config.DB.QueryRow(query)
err := row.Scan(&count)
if err != nil {
log.Printf("Error getting work count: %v", err)
return 0, err
}
return count, nil
}

View File

@@ -0,0 +1,68 @@
package runner
import (
"bytes"
"context"
"io/ioutil"
"os"
"os/exec"
"path/filepath"
"time"
"github.com/google/uuid"
)
type GoRunner struct {
TempDir string
}
func (r *GoRunner) Run(ctx context.Context, code string) (*ExecutionResult, error) {
// 创建唯一的工作目录
workDir := filepath.Join(r.TempDir, uuid.New().String())
if err := os.MkdirAll(workDir, 0755); err != nil {
return nil, err
}
defer os.RemoveAll(workDir) // 清理
// 写入main.go
// 注意这里假设代码是一个完整的package main
// 如果用户只提供了函数片段,可能需要包装。这里假设是完整代码。
filePath := filepath.Join(workDir, "main.go")
if err := ioutil.WriteFile(filePath, []byte(code), 0644); err != nil {
return nil, err
}
// 准备命令
// 使用 go run 运行
cmd := exec.CommandContext(ctx, "go", "run", "main.go")
cmd.Dir = workDir
// 捕获输出
var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr
startTime := time.Now()
err := cmd.Run()
duration := time.Since(startTime).Milliseconds()
result := &ExecutionResult{
Output: stdout.String(),
Error: stderr.String(),
Duration: duration,
}
if err != nil {
if exitErr, ok := err.(*exec.ExitError); ok {
result.ExitCode = exitErr.ExitCode()
} else {
result.ExitCode = -1
}
// 如果是超时
if ctx.Err() == context.DeadlineExceeded {
result.Error += "\nExecution timed out"
}
}
return result, nil
}

View File

@@ -0,0 +1,60 @@
package runner
import (
"bytes"
"context"
"io/ioutil"
"os"
"os/exec"
"path/filepath"
"time"
"github.com/google/uuid"
)
type NodeRunner struct {
TempDir string
}
func (r *NodeRunner) Run(ctx context.Context, code string) (*ExecutionResult, error) {
workDir := filepath.Join(r.TempDir, uuid.New().String())
if err := os.MkdirAll(workDir, 0755); err != nil {
return nil, err
}
defer os.RemoveAll(workDir)
filePath := filepath.Join(workDir, "script.js")
if err := ioutil.WriteFile(filePath, []byte(code), 0644); err != nil {
return nil, err
}
cmd := exec.CommandContext(ctx, "node", "script.js")
cmd.Dir = workDir
var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr
startTime := time.Now()
err := cmd.Run()
duration := time.Since(startTime).Milliseconds()
result := &ExecutionResult{
Output: stdout.String(),
Error: stderr.String(),
Duration: duration,
}
if err != nil {
if exitErr, ok := err.(*exec.ExitError); ok {
result.ExitCode = exitErr.ExitCode()
} else {
result.ExitCode = -1
}
if ctx.Err() == context.DeadlineExceeded {
result.Error += "\nExecution timed out"
}
}
return result, nil
}

View File

@@ -0,0 +1,60 @@
package runner
import (
"bytes"
"context"
"io/ioutil"
"os"
"os/exec"
"path/filepath"
"time"
"github.com/google/uuid"
)
type PHPRunner struct {
TempDir string
}
func (r *PHPRunner) Run(ctx context.Context, code string) (*ExecutionResult, error) {
workDir := filepath.Join(r.TempDir, uuid.New().String())
if err := os.MkdirAll(workDir, 0755); err != nil {
return nil, err
}
defer os.RemoveAll(workDir)
filePath := filepath.Join(workDir, "script.php")
if err := ioutil.WriteFile(filePath, []byte(code), 0644); err != nil {
return nil, err
}
cmd := exec.CommandContext(ctx, "php", "script.php")
cmd.Dir = workDir
var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr
startTime := time.Now()
err := cmd.Run()
duration := time.Since(startTime).Milliseconds()
result := &ExecutionResult{
Output: stdout.String(),
Error: stderr.String(),
Duration: duration,
}
if err != nil {
if exitErr, ok := err.(*exec.ExitError); ok {
result.ExitCode = exitErr.ExitCode()
} else {
result.ExitCode = -1
}
if ctx.Err() == context.DeadlineExceeded {
result.Error += "\nExecution timed out"
}
}
return result, nil
}

40
server/runner/runner.go Normal file
View File

@@ -0,0 +1,40 @@
package runner
import (
"context"
"fmt"
"os"
"path/filepath"
)
// ExecutionResult 执行结果
type ExecutionResult struct {
Output string `json:"output"`
Error string `json:"error"`
Duration int64 `json:"duration"` // 毫秒
ExitCode int `json:"exitCode"`
}
// CodeRunner 代码执行接口
type CodeRunner interface {
Run(ctx context.Context, code string) (*ExecutionResult, error)
}
// GetRunner 获取对应语言的运行器
func GetRunner(language string) (CodeRunner, error) {
tempDir := filepath.Join(os.TempDir(), "art-code-runner")
if err := os.MkdirAll(tempDir, 0755); err != nil {
return nil, fmt.Errorf("failed to create temp dir: %v", err)
}
switch language {
case "go", "golang":
return &GoRunner{TempDir: tempDir}, nil
case "php":
return &PHPRunner{TempDir: tempDir}, nil
case "javascript", "js", "node":
return &NodeRunner{TempDir: tempDir}, nil
default:
return nil, fmt.Errorf("unsupported language: %s", language)
}
}

View File

@@ -0,0 +1,95 @@
package main
import (
"database/sql"
"fmt"
"log"
_ "github.com/go-sql-driver/mysql"
)
func main() {
// MySQL连接信息
username := "root"
password := "root"
hostname := "127.0.0.1"
port := "3306"
dbname := "nl_blog"
// 构建DSN
dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s", username, password, hostname, port, dbname)
// 连接到数据库
db, err := sql.Open("mysql", dsn)
if err != nil {
log.Fatalf("Failed to open database connection: %v", err)
}
defer db.Close()
if err := db.Ping(); err != nil {
log.Fatalf("Failed to ping database: %v", err)
}
fmt.Println("Connected to 'nl_blog' database successfully!")
// 1. 确认 lq 用户存在且 role_id 正确
var userID, roleID int
var usernameStr string
err = db.QueryRow("SELECT id, username, role_id FROM users WHERE username = 'lq'").Scan(&userID, &usernameStr, &roleID)
if err != nil {
log.Printf("Error finding user 'lq': %v", err)
// 如果用户不存在,可以尝试修复(可选)
} else {
fmt.Printf("User found: %s (ID: %d, RoleID: %d)\n", usernameStr, userID, roleID)
}
// 2. 清空并重新填充 role_permissions 表
// 注意:这里假设 roles 表中 ID 1=admin, 2=editor, 3=viewer
// 并且 permissions 表已经有数据。如果 permissions 表也是空的,需要先填充 permissions。
// 先检查 permissions 表是否有数据
var permCount int
db.QueryRow("SELECT COUNT(*) FROM permissions").Scan(&permCount)
if permCount == 0 {
fmt.Println("Permissions table is empty! Repopulating permissions...")
// 插入权限数据 (简略版,确保核心权限存在)
perms := []string{
"INSERT INTO `permissions` (`name`, `resource`, `action`) VALUES ('Create User', 'users', 'create')",
"INSERT INTO `permissions` (`name`, `resource`, `action`) VALUES ('Read User', 'users', 'read')",
"INSERT INTO `permissions` (`name`, `resource`, `action`) VALUES ('Update User', 'users', 'update')",
"INSERT INTO `permissions` (`name`, `resource`, `action`) VALUES ('Delete User', 'users', 'delete')",
"INSERT INTO `permissions` (`name`, `resource`, `action`) VALUES ('Read Dashboard', 'dashboard', 'read')",
"INSERT INTO `permissions` (`name`, `resource`, `action`) VALUES ('Create Post', 'posts', 'create')",
"INSERT INTO `permissions` (`name`, `resource`, `action`) VALUES ('Read Post', 'posts', 'read')",
"INSERT INTO `permissions` (`name`, `resource`, `action`) VALUES ('Update Post', 'posts', 'update')",
"INSERT INTO `permissions` (`name`, `resource`, `action`) VALUES ('Delete Post', 'posts', 'delete')",
}
for _, sql := range perms {
if _, err := db.Exec(sql); err != nil {
log.Printf("Error inserting permission: %v", err)
}
}
}
// 清空 role_permissions
_, err = db.Exec("DELETE FROM role_permissions")
if err != nil {
log.Fatalf("Failed to clear role_permissions: %v", err)
}
// 重新关联 Admin (Role ID 1) - 赋予所有权限
res, err := db.Exec("INSERT INTO role_permissions (role_id, permission_id) SELECT 1, id FROM permissions")
if err != nil {
log.Fatalf("Failed to assign permissions to admin: %v", err)
}
rowsAffected, _ := res.RowsAffected()
fmt.Printf("Assigned %d permissions to Admin role.\n", rowsAffected)
// 重新关联 Editor (Role ID 2) - 赋予 posts 相关权限
res, err = db.Exec("INSERT INTO role_permissions (role_id, permission_id) SELECT 2, id FROM permissions WHERE resource = 'posts'")
if err != nil {
log.Printf("Failed to assign permissions to editor: %v", err)
}
fmt.Println("Database permissions fixed successfully!")
}

View File

@@ -0,0 +1,30 @@
-- 1. 确保 Admin 角色存在
INSERT IGNORE INTO `roles` (`id`, `name`, `description`) VALUES (1, 'admin', '系统管理员');
-- 2. 确保 lq 用户关联到 Admin 角色
UPDATE `users` SET `role_id` = 1 WHERE `username` = 'lq';
-- 3. 确保权限表中有数据 (如果为空)
INSERT IGNORE INTO `permissions` (`name`, `resource`, `action`) VALUES
('Create User', 'users', 'create'),
('Read User', 'users', 'read'),
('Update User', 'users', 'update'),
('Delete User', 'users', 'delete'),
('Read Dashboard', 'dashboard', 'read'),
('Create Post', 'posts', 'create'),
('Read Post', 'posts', 'read'),
('Update Post', 'posts', 'update'),
('Delete Post', 'posts', 'delete'),
('Create Role', 'roles', 'create'),
('Read Role', 'roles', 'read'),
('Update Role', 'roles', 'update'),
('Delete Role', 'roles', 'delete');
-- 4. 清空旧的关联 (为了安全起见,防止重复)
DELETE FROM `role_permissions` WHERE role_id = 1;
-- 5. 重新赋予 Admin 所有权限
INSERT INTO `role_permissions` (`role_id`, `permission_id`)
SELECT 1, id FROM `permissions`;
SELECT 'Permissions fixed successfully' as result;

70
server/scripts/init_db.go Normal file
View File

@@ -0,0 +1,70 @@
package main
import (
"database/sql"
"fmt"
"io/ioutil"
"log"
"strings"
_ "github.com/go-sql-driver/mysql"
)
func main() {
// MySQL连接信息
username := "root"
password := "root"
hostname := "127.0.0.1"
port := "3306"
// 构建DSN (Data Source Name)
dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/", username, password, hostname, port)
// 连接到MySQL服务器
db, err := sql.Open("mysql", dsn)
if err != nil {
log.Fatalf("Failed to open database connection: %v", err)
}
defer db.Close()
// 测试连接
if err := db.Ping(); err != nil {
log.Fatalf("Failed to ping database: %v", err)
}
fmt.Println("Connected to MySQL server successfully!")
// 读取SQL文件 (Assuming running from server root or adjusted path)
// If running from scripts/, path should be ../nl_blog.sql
sqlFile, err := ioutil.ReadFile("../nl_blog.sql")
if err != nil {
// Try current dir if run from root
sqlFile, err = ioutil.ReadFile("nl_blog.sql")
if err != nil {
log.Fatalf("Failed to read SQL file: %v", err)
}
}
// 分割SQL语句
sqlStatements := strings.Split(string(sqlFile), ";")
// 执行每个SQL语句
for _, stmt := range sqlStatements {
// 跳过空语句
stmt = strings.TrimSpace(stmt)
if stmt == "" {
continue
}
// 执行语句
_, err := db.Exec(stmt)
if err != nil {
log.Printf("Error executing statement: %v", err)
continue
}
fmt.Printf("Executed statement: %s\n", stmt)
}
fmt.Println("Database initialization completed!")
}

View File

@@ -0,0 +1,56 @@
package main
import (
"database/sql"
"fmt"
"log"
_ "github.com/go-sql-driver/mysql"
"golang.org/x/crypto/bcrypt"
)
func main() {
// MySQL连接信息
username := "root"
password := "root"
hostname := "127.0.0.1"
port := "3306"
dbname := "nl_blog"
// 构建DSN
dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s", username, password, hostname, port, dbname)
// 连接到数据库
db, err := sql.Open("mysql", dsn)
if err != nil {
log.Fatalf("Failed to open database connection: %v", err)
}
defer db.Close()
if err := db.Ping(); err != nil {
log.Fatalf("Failed to ping database: %v", err)
}
targetUser := "lq"
newPassword := "qiqi991012"
// 生成哈希密码
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(newPassword), bcrypt.DefaultCost)
if err != nil {
log.Fatalf("Failed to hash password: %v", err)
}
// 更新密码
query := "UPDATE users SET password_hash = ? WHERE username = ?"
res, err := db.Exec(query, string(hashedPassword), targetUser)
if err != nil {
log.Fatalf("Failed to update password: %v", err)
}
rowsAffected, _ := res.RowsAffected()
if rowsAffected == 0 {
fmt.Printf("User '%s' not found or password not changed.\n", targetUser)
} else {
fmt.Printf("Password for user '%s' updated successfully.\n", targetUser)
}
}

164
server/scripts/verify_db.go Normal file
View File

@@ -0,0 +1,164 @@
package main
import (
"database/sql"
"fmt"
"log"
_ "github.com/go-sql-driver/mysql"
)
func main() {
// MySQL连接信息
username := "root"
password := "root"
hostname := "127.0.0.1"
port := "3306"
// 构建DSN (Data Source Name) - 首先连接到MySQL服务器不指定数据库
dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/", username, password, hostname, port)
// 连接到MySQL服务器
db, err := sql.Open("mysql", dsn)
if err != nil {
log.Fatalf("Failed to open database connection: %v", err)
}
defer db.Close()
// 测试连接
if err := db.Ping(); err != nil {
log.Fatalf("Failed to ping database: %v", err)
}
fmt.Println("Connected to MySQL server successfully!")
// 1. 检查数据库是否存在
var dbExists bool
checkDBQuery := "SELECT COUNT(*) > 0 FROM information_schema.SCHEMATA WHERE SCHEMA_NAME = 'nl_blog'"
err = db.QueryRow(checkDBQuery).Scan(&dbExists)
if err != nil {
log.Fatalf("Failed to check database existence: %v", err)
}
if dbExists {
fmt.Println("✅ Database 'nl_blog' exists!")
} else {
log.Fatalf("❌ Database 'nl_blog' does not exist!")
}
// 2. 连接到nl_blog数据库
dsnWithDB := fmt.Sprintf("%s:%s@tcp(%s:%s)/nl_blog", username, password, hostname, port)
db, err = sql.Open("mysql", dsnWithDB)
if err != nil {
log.Fatalf("Failed to open database connection to nl_blog: %v", err)
}
defer db.Close()
if err := db.Ping(); err != nil {
log.Fatalf("Failed to ping nl_blog database: %v", err)
}
fmt.Println("Connected to 'nl_blog' database successfully!")
// 3. 查询数据库中的所有表
showTablesQuery := "SHOW TABLES"
rows, err := db.Query(showTablesQuery)
if err != nil {
log.Fatalf("Failed to show tables: %v", err)
}
defer rows.Close()
fmt.Println("\nTables in 'nl_blog' database:")
var tableName string
tableCount := 0
for rows.Next() {
if err := rows.Scan(&tableName); err != nil {
log.Fatalf("Failed to scan table name: %v", err)
}
fmt.Printf("✅ %s\n", tableName)
tableCount++
}
if tableCount == 0 {
log.Fatalf("❌ No tables found in 'nl_blog' database!")
} else {
fmt.Printf("\nTotal tables: %d\n", tableCount)
}
// 4. 检查主要表的结构和数据
checkTableStructure(db, "works")
checkTableStructure(db, "posts")
checkTableStructure(db, "snippets")
checkTableStructure(db, "users")
checkTableStructure(db, "settings")
fmt.Println("\n✅ Database verification completed successfully!")
}
// 检查表结构和数据
func checkTableStructure(db *sql.DB, tableName string) {
fmt.Printf("\n--- Checking table: %s ---", tableName)
// 检查表是否存在
var tableExists bool
checkTableQuery := "SELECT COUNT(*) > 0 FROM information_schema.TABLES WHERE TABLE_SCHEMA = 'nl_blog' AND TABLE_NAME = ?"
err := db.QueryRow(checkTableQuery, tableName).Scan(&tableExists)
if err != nil {
log.Fatalf("Failed to check table existence: %v", err)
}
if !tableExists {
log.Fatalf("❌ Table '%s' does not exist!", tableName)
}
// 查询表中的记录数量
var count int
countQuery := fmt.Sprintf("SELECT COUNT(*) FROM %s", tableName)
err = db.QueryRow(countQuery).Scan(&count)
if err != nil {
log.Fatalf("Failed to count rows in table '%s': %v", tableName, err)
}
fmt.Printf("\n✅ Table '%s' exists with %d records", tableName, count)
// 查询表结构前5个字段
// 简化查询,只获取字段名、类型、是否为空和键信息,不获取默认值和额外信息
schemaQuery := `SELECT COLUMN_NAME, DATA_TYPE, IS_NULLABLE, COLUMN_KEY
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = 'nl_blog' AND TABLE_NAME = ?
ORDER BY ORDINAL_POSITION
LIMIT 5`
rows, err := db.Query(schemaQuery, tableName)
if err != nil {
log.Fatalf("Failed to get schema for table '%s': %v", tableName, err)
}
defer rows.Close()
fmt.Println("\nTable structure (top 5 fields):")
var field, fieldType, null, key string
for rows.Next() {
if err := rows.Scan(&field, &fieldType, &null, &key); err != nil {
log.Fatalf("Failed to scan field: %v", err)
}
fmt.Printf(" %s | %s | %s | %s\n", field, fieldType, null, key)
}
// 如果是posts表查询前2条记录
if tableName == "posts" && count > 0 {
fmt.Println("\nSample data (top 2 records):")
sampleQuery := "SELECT id, title, category, date FROM posts ORDER BY date DESC LIMIT 2"
rows, err := db.Query(sampleQuery)
if err != nil {
log.Fatalf("Failed to get sample data from '%s': %v", tableName, err)
}
defer rows.Close()
var id, title, category, date string
for rows.Next() {
if err := rows.Scan(&id, &title, &category, &date); err != nil {
log.Fatalf("Failed to scan sample data: %v", err)
}
fmt.Printf(" ID: %s | Title: %s | Category: %s | Date: %s\n", id, title, category, date)
}
}
}