初始化
This commit is contained in:
27
server/.gitignore
vendored
Normal file
27
server/.gitignore
vendored
Normal 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
64
server/config/db.go
Normal 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
45
server/go.mod
Normal 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
96
server/go.sum
Normal 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
54
server/handlers/auth.go
Normal 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)
|
||||
}
|
||||
46
server/handlers/dashboard.go
Normal file
46
server/handlers/dashboard.go
Normal 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
81
server/handlers/log.go
Normal 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
182
server/handlers/post.go
Normal 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
145
server/handlers/role.go
Normal 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
54
server/handlers/runner.go
Normal 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)
|
||||
}
|
||||
67
server/handlers/setting.go
Normal file
67
server/handlers/setting.go
Normal 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
106
server/handlers/snippet.go
Normal 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
132
server/handlers/tag.go
Normal 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
129
server/handlers/user.go
Normal 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
130
server/handlers/work.go
Normal 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
154
server/main.go
Normal 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
209
server/middleware/auth.go
Normal 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
68
server/middleware/log.go
Normal 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)
|
||||
}
|
||||
}()
|
||||
}
|
||||
}
|
||||
33
server/models/operation_log.go
Normal file
33
server/models/operation_log.go
Normal 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"`
|
||||
}
|
||||
25
server/models/permission.go
Normal file
25
server/models/permission.go
Normal 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
75
server/models/post.go
Normal 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
25
server/models/role.go
Normal 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
25
server/models/setting.go
Normal 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
25
server/models/snippet.go
Normal 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
43
server/models/user.go
Normal 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
51
server/models/work.go
Normal 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
391
server/nl_blog.sql
Normal 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 '是否为精选作品(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_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;
|
||||
109
server/repositories/operation_log_repository.go
Normal file
109
server/repositories/operation_log_repository.go
Normal 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
|
||||
}
|
||||
80
server/repositories/permission_repository.go
Normal file
80
server/repositories/permission_repository.go
Normal 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
|
||||
}
|
||||
351
server/repositories/post_repository.go
Normal file
351
server/repositories/post_repository.go
Normal 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
|
||||
}
|
||||
166
server/repositories/role_repository.go
Normal file
166
server/repositories/role_repository.go
Normal 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
|
||||
}
|
||||
144
server/repositories/setting_repository.go
Normal file
144
server/repositories/setting_repository.go
Normal 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
|
||||
}
|
||||
163
server/repositories/snippet_repository.go
Normal file
163
server/repositories/snippet_repository.go
Normal 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
|
||||
}
|
||||
264
server/repositories/tag_repository.go
Normal file
264
server/repositories/tag_repository.go
Normal 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
|
||||
}
|
||||
293
server/repositories/user_repository.go
Normal file
293
server/repositories/user_repository.go
Normal 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
|
||||
}
|
||||
301
server/repositories/work_repository.go
Normal file
301
server/repositories/work_repository.go
Normal 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
|
||||
}
|
||||
68
server/runner/go_runner.go
Normal file
68
server/runner/go_runner.go
Normal 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
|
||||
}
|
||||
60
server/runner/node_runner.go
Normal file
60
server/runner/node_runner.go
Normal 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
|
||||
}
|
||||
60
server/runner/php_runner.go
Normal file
60
server/runner/php_runner.go
Normal 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
40
server/runner/runner.go
Normal 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)
|
||||
}
|
||||
}
|
||||
95
server/scripts/fix_permissions.go
Normal file
95
server/scripts/fix_permissions.go
Normal 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!")
|
||||
}
|
||||
30
server/scripts/fix_permissions.sql
Normal file
30
server/scripts/fix_permissions.sql
Normal 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
70
server/scripts/init_db.go
Normal 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!")
|
||||
}
|
||||
56
server/scripts/reset_password.go
Normal file
56
server/scripts/reset_password.go
Normal 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
164
server/scripts/verify_db.go
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user