增加基础设置页面

This commit is contained in:
2026-07-29 16:45:22 +08:00
parent 0b22d4f126
commit a9a890a849
15 changed files with 472 additions and 84 deletions
+2
View File
@@ -6,6 +6,7 @@ import (
"log/slog"
"github.com/glebarez/sqlite"
"gorm.io/gorm"
"gorm.io/gorm/logger"
)
@@ -18,6 +19,7 @@ func Init(cfg *config.DatabaseConfig) error {
DB, err = gorm.Open(sqlite.Open(cfg.DSN), &gorm.Config{
Logger: logger.Default.LogMode(logger.Info),
})
if err != nil {
return err
}
+16 -13
View File
@@ -10,8 +10,9 @@ require (
github.com/golang-jwt/jwt/v5 v5.2.1
github.com/spf13/viper v1.21.0
github.com/yuin/goldmark v1.8.5
golang.org/x/crypto v0.48.0
gorm.io/gorm v1.31.1
golang.org/x/crypto v0.53.0
gorm.io/driver/sqlite v1.6.0
gorm.io/gorm v1.31.2
)
require (
@@ -29,15 +30,17 @@ require (
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/goccy/go-yaml v1.18.0 // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // 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/mattn/go-sqlite3 v1.14.22 // indirect
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/ncruces/go-strftime v1.0.0 // 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
@@ -53,15 +56,15 @@ require (
go.uber.org/mock v0.5.0 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/arch v0.20.0 // indirect
golang.org/x/mod v0.32.0 // indirect
golang.org/x/net v0.49.0 // indirect
golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.41.0 // indirect
golang.org/x/text v0.34.0 // indirect
golang.org/x/tools v0.41.0 // indirect
golang.org/x/mod v0.37.0 // indirect
golang.org/x/net v0.56.0 // indirect
golang.org/x/sync v0.21.0 // indirect
golang.org/x/sys v0.46.0 // indirect
golang.org/x/text v0.39.0 // indirect
golang.org/x/tools v0.47.0 // indirect
google.golang.org/protobuf v1.36.9 // indirect
modernc.org/libc v1.22.5 // indirect
modernc.org/mathutil v1.5.0 // indirect
modernc.org/memory v1.5.0 // indirect
modernc.org/sqlite v1.23.1 // indirect
modernc.org/libc v1.74.1 // indirect
modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.11.0 // indirect
modernc.org/sqlite v1.54.0 // indirect
)
+56 -29
View File
@@ -46,10 +46,12 @@ github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVI
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/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
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/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
@@ -66,10 +68,14 @@ 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/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
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/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
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=
@@ -78,7 +84,6 @@ 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/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
@@ -118,21 +123,21 @@ go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
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.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c=
golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU=
golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto=
golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio=
golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ=
golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0=
golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o=
golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec=
golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM=
golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc=
golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg=
golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw=
golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/text v0.39.0 h1:UbZz4pLOvn600D6Oh6GGEI6VAmndrEBLv8/6BEXzyus=
golang.org/x/text v0.39.0/go.mod h1:3UwRclnC2g0TU9x8PZiyfOajCd1zaUNHF9cvqcQZ+ZM=
golang.org/x/tools v0.47.0 h1:7Kn5x/d1svx/PzryTsqeoZN4TZwqeH5pGWjefhLi/1Q=
golang.org/x/tools v0.47.0/go.mod h1:dFHnyTvFWY212G+h7ZY4Vsp/K3U4/7W9TyVaAul8uCA=
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=
@@ -141,13 +146,35 @@ gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8
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=
gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg=
gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=
modernc.org/libc v1.22.5 h1:91BNch/e5B0uPbJFgqbxXuOnxBQjlS//icfQEGmvyjE=
modernc.org/libc v1.22.5/go.mod h1:jj+Z7dTNX8fBScMVNRAYZ/jF91K8fdT2hYMThc3YjBY=
modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ=
modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
modernc.org/memory v1.5.0 h1:N+/8c5rE6EqugZwHii4IFsaJ7MUhoWX07J5tC/iI5Ds=
modernc.org/memory v1.5.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU=
modernc.org/sqlite v1.23.1 h1:nrSBg4aRQQwq59JpvGEQ15tNxoO5pX/kUjcRNwSAGQM=
modernc.org/sqlite v1.23.1/go.mod h1:OrDj17Mggn6MhE+iPbBNf7RGKODDE9NFT0f3EwDzJqk=
gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ=
gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8=
gorm.io/gorm v1.31.2 h1:3o8FXNo9v9S858gil+3LlZA1LkCOzgb4g5BL64FgaCo=
gorm.io/gorm v1.31.2/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=
modernc.org/cc/v4 v4.29.0 h1:CXgwL8cvxmyzBQZzbSl/6xFtMCryb6u8IOqDci39cgc=
modernc.org/cc/v4 v4.29.0/go.mod h1:OnovgIhbbMXMu1aISnJ0wvVD1KnW+cAUJkIrAWh+kVI=
modernc.org/ccgo/v4 v4.34.6 h1:sBgfIwyN0TQ9C5hwIeuqyeAKyMWnbvj2fvpF4L11uzU=
modernc.org/ccgo/v4 v4.34.6/go.mod h1:SZ8YcN9NG7XVsQYdm6jYBvi8PQP1qi+kqB6OhjqI3Fk=
modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM=
modernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU=
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
modernc.org/gc/v3 v3.1.4 h1:2g65LGVSmFQrXeITAw97x7hCRvZFcyE1uDP+7Vng7JI=
modernc.org/gc/v3 v3.1.4/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
modernc.org/libc v1.74.1 h1:bdR4VTKFMC4966QSNZ05XLGI/VwzVa2kTUX51Dm0riQ=
modernc.org/libc v1.74.1/go.mod h1:uH4t5bOx3G3g9Xcmj10YKlTcVISlRDwv8VoQJG9n8Os=
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
modernc.org/opt v0.2.0 h1:tGyef5ApycA7FSEOMraay9SaTk5zmbx7Tu+cJs4QKZg=
modernc.org/opt v0.2.0/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
modernc.org/sqlite v1.54.0 h1:JCxR4qwkJvOaqAoYcgDoO25Nc+ROg6EJ2LfBVzdrgog=
modernc.org/sqlite v1.54.0/go.mod h1:4ntCLuNmnH8+GNqjka1wNg7KJd5/Hi5FYp8K+XQ7GZw=
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
BIN
View File
Binary file not shown.
+17 -2
View File
@@ -7,6 +7,7 @@ import (
"goblog/models"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
// 创建评论请求
@@ -24,7 +25,10 @@ func GetComments(c *gin.Context) {
postID := c.Query("post_id")
status := c.Query("status")
db := database.DB.Model(&models.Comment{}).Preload("User")
db := database.DB.Model(&models.Comment{}).Preload("User").
Preload("Post", func(db *gorm.DB) *gorm.DB {
return db.Select("id", "title", "slug")
})
if postID != "" {
db = db.Where("post_id = ?", postID)
@@ -36,7 +40,18 @@ func GetComments(c *gin.Context) {
var comments []models.Comment
db.Order("created_at DESC").Find(&comments)
c.JSON(http.StatusOK, gin.H{"data": comments})
// Comment.Post 在 JSON 中被隐藏,额外附加文章标题和别名供前端展示
type commentWithPost struct {
models.Comment
PostTitle string `json:"post_title"`
PostSlug string `json:"post_slug"`
}
list := make([]commentWithPost, 0, len(comments))
for _, cm := range comments {
list = append(list, commentWithPost{cm, cm.Post.Title, cm.Post.Slug})
}
c.JSON(http.StatusOK, gin.H{"data": list})
}
// 创建评论
+84
View File
@@ -0,0 +1,84 @@
package handlers
import (
"net/http"
"strconv"
"goblog/config"
"goblog/database"
"goblog/models"
"github.com/gin-gonic/gin"
)
// 站点基础设置键名
const (
OptionSiteName = "site_name"
OptionSiteDesc = "site_desc"
OptionShowComments = "show_comments" // "1" 显示评论 / "0" 隐藏评论
OptionCommentPageSize = "comments_per_page" // 前台评论每页条数
)
// GetSettings 获取站点基础设置(管理员)
func GetSettings(c *gin.Context) {
cfg := config.GetGlobalConfig()
if cfg == nil {
cfg = config.Load()
}
opts, err := models.GetOptionsByUser(database.DB, 0)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "读取设置失败"})
return
}
// 默认值:站点信息取配置文件,评论默认显示、每页 10 条
settings := map[string]string{
OptionSiteName: cfg.App.Name,
OptionSiteDesc: cfg.App.Description,
OptionShowComments: "1",
OptionCommentPageSize: "10",
}
for name := range settings {
if v, ok := opts[name]; ok && v != "" {
settings[name] = v
}
}
c.JSON(http.StatusOK, gin.H{"data": settings})
}
// UpdateSettingsRequest 更新站点基础设置请求
type UpdateSettingsRequest struct {
SiteName string `json:"site_name" binding:"required"`
SiteDesc string `json:"site_desc"`
ShowComments bool `json:"show_comments"`
CommentsPerPage int `json:"comments_per_page" binding:"required,min=1,max=100"`
}
// UpdateSettings 保存站点基础设置(管理员)
func UpdateSettings(c *gin.Context) {
var req UpdateSettingsRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "参数错误: " + err.Error()})
return
}
showComments := "0"
if req.ShowComments {
showComments = "1"
}
options := map[string]string{
OptionSiteName: req.SiteName,
OptionSiteDesc: req.SiteDesc,
OptionShowComments: showComments,
OptionCommentPageSize: strconv.Itoa(req.CommentsPerPage),
}
if err := models.BatchSetOptions(database.DB, 0, options); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "保存设置失败"})
return
}
c.JSON(http.StatusOK, gin.H{"message": "设置已保存"})
}
+69 -25
View File
@@ -122,12 +122,11 @@ func IndexView(c *gin.Context) {
siteName := cfg.App.Name
siteDesc := cfg.App.Description
var option models.Option
if err := database.DB.Where("name = ? AND user_id = ?", "site_name", 0).First(&option).Error; err == nil {
siteName = option.Value
if v, err := models.GetOptionValue(database.DB, "site_name", 0); err == nil {
siteName = v
}
if err := database.DB.Where("name = ? AND user_id = ?", "site_desc", 0).First(&option).Error; err == nil {
siteDesc = option.Value
if v, err := models.GetOptionValue(database.DB, "site_desc", 0); err == nil {
siteDesc = v
}
data := IndexData{
@@ -164,6 +163,10 @@ type PostDetailData struct {
Year int
PrevPost *models.Post // 上一篇
NextPost *models.Post // 下一篇
// 评论展示设置与分页
ShowComments bool
CommentPage int
CommentTotalPages int
// Sidebar 数据
Categories []models.Category
Tags []models.Tag
@@ -180,14 +183,17 @@ func PostView(c *gin.Context) {
slug := c.Param("slug")
var post models.Post
err := database.DB.Preload("Category").Preload("Tags").Preload("Author").
Preload("Comments", func(db *gorm.DB) *gorm.DB {
return db.Where("status = ? AND parent_id IS NULL", "approved").Preload("Children", func(db *gorm.DB) *gorm.DB {
return db.Where("status = ?", "approved")
})
}).
Where("slug = ? AND status = ?", slug, "published").
First(&post).Error
query := database.DB.Preload("Category").Preload("Tags").Preload("Author").
Where("status = ?", "published").
Session(&gorm.Session{}) // 创建可安全复用的查询会话,避免两次查询条件叠加
err := query.Where("slug = ?", slug).First(&post).Error
// slug 未命中时,若参数为纯数字则回退按文章 ID 查询(兼容 /post/17 形式的链接)
if err != nil {
if id, convErr := strconv.Atoi(slug); convErr == nil {
err = query.Where("id = ?", id).First(&post).Error
}
}
if err != nil {
c.HTML(http.StatusNotFound, "index", gin.H{
@@ -209,6 +215,43 @@ func PostView(c *gin.Context) {
var commentCount int64
database.DB.Model(&models.Comment{}).Where("post_id = ? AND status = ?", post.ID, "approved").Count(&commentCount)
// 读取评论展示设置
showComments := true
if v, optErr := models.GetOptionValue(database.DB, OptionShowComments, 0); optErr == nil {
showComments = v != "0"
}
commentPageSize := 10
if v, optErr := models.GetOptionValue(database.DB, OptionCommentPageSize, 0); optErr == nil {
if n, convErr := strconv.Atoi(v); convErr == nil && n > 0 {
commentPageSize = n
}
}
// 顶级评论分页查询(回复随父评论一起展示,不单独分页)
commentPage, _ := strconv.Atoi(c.DefaultQuery("cpage", "1"))
if commentPage < 1 {
commentPage = 1
}
var comments []models.Comment
commentTotalPages := 0
if showComments {
var topCount int64
database.DB.Model(&models.Comment{}).
Where("post_id = ? AND status = ? AND parent_id IS NULL", post.ID, "approved").
Count(&topCount)
commentTotalPages = int(math.Ceil(float64(topCount) / float64(commentPageSize)))
if commentTotalPages > 0 && commentPage > commentTotalPages {
commentPage = commentTotalPages
}
database.DB.Where("post_id = ? AND status = ? AND parent_id IS NULL", post.ID, "approved").
Preload("Children", func(db *gorm.DB) *gorm.DB {
return db.Where("status = ?", "approved")
}).
Order("created_at ASC").
Offset((commentPage - 1) * commentPageSize).Limit(commentPageSize).
Find(&comments)
}
// 获取上一篇(发布时间更早的文章)
var prevPost models.Post
prevErr := database.DB.Where("status = ? AND published_at < ?", "published", post.PublishedAt).
@@ -223,12 +266,11 @@ func PostView(c *gin.Context) {
siteName := cfg.App.Name
siteDesc := cfg.App.Description
var option models.Option
if err := database.DB.Where("name = ? AND user_id = ?", "site_name", 0).First(&option).Error; err == nil {
siteName = option.Value
if v, err := models.GetOptionValue(database.DB, "site_name", 0); err == nil {
siteName = v
}
if err := database.DB.Where("name = ? AND user_id = ?", "site_desc", 0).First(&option).Error; err == nil {
siteDesc = option.Value
if v, err := models.GetOptionValue(database.DB, "site_desc", 0); err == nil {
siteDesc = v
}
// 加载侧边栏数据
@@ -241,9 +283,13 @@ func PostView(c *gin.Context) {
Description: post.Summary,
Post: post,
Pages: pages,
Comments: post.Comments,
Comments: comments,
CommentCount: commentCount,
Year: time.Now().Year(),
// 评论展示设置与分页
ShowComments: showComments,
CommentPage: commentPage,
CommentTotalPages: commentTotalPages,
// Sidebar 数据
Categories: categories,
Tags: tags,
@@ -306,14 +352,12 @@ func PageView(c *gin.Context) {
siteName := cfg.App.Name
siteDesc := cfg.App.Description
var option models.Option
if err := database.DB.Where("name = ? AND user_id = ?", "site_name", 0).First(&option).Error; err == nil {
siteName = option.Value
if v, err := models.GetOptionValue(database.DB, "site_name", 0); err == nil {
siteName = v
}
if err := database.DB.Where("name = ? AND user_id = ?", "site_desc", 0).First(&option).Error; err == nil {
siteDesc = option.Value
if v, err := models.GetOptionValue(database.DB, "site_desc", 0); err == nil {
siteDesc = v
}
// 加载侧边栏数据
categories, tags, postCount, categoryCount, tagCount := loadSidebarData()
+3 -6
View File
@@ -226,12 +226,9 @@ func GetOptionsByUser(db *gorm.DB, userID uint) (map[string]string, error) {
func BatchSetOptions(db *gorm.DB, userID uint, options map[string]string) error {
return db.Transaction(func(tx *gorm.DB) error {
for name, value := range options {
opt := Option{
Name: name,
UserID: userID,
Value: value,
}
if err := tx.Save(&opt).Error; err != nil {
// 复合主键在 SQLite 旧表上可能缺失唯一约束,Save 会重复插入,
// 改用查找-更新-不存在才插入的方式保证幂等
if err := SetOptionValue(tx, name, userID, value); err != nil {
slog.Error("保存配置失败", "name", name, "error", err)
return err
}
+5 -1
View File
@@ -95,7 +95,7 @@ func SetupRoutes(r *gin.Engine, cfg interface{}) {
admin.DELETE("/pages/:id", handlers.DeletePage)
// 评论管理
admin.GET("/admin/comments", handlers.GetComments)
admin.GET("/comments", handlers.GetComments)
admin.PUT("/comments/:id/approve", handlers.ApproveComment)
admin.PUT("/comments/:id/spam", handlers.MarkSpamComment)
admin.DELETE("/comments/:id", handlers.DeleteComment)
@@ -107,6 +107,10 @@ func SetupRoutes(r *gin.Engine, cfg interface{}) {
admin.PUT("/users/:id/password", handlers.AdminResetPassword)
admin.DELETE("/users/:id", handlers.DeleteUser)
// 基础设置
admin.GET("/settings", handlers.GetSettings)
admin.PUT("/settings", handlers.UpdateSettings)
// 文件管理(图片上传)
admin.POST("/upload", handlers.UploadImage)
admin.GET("/uploads", handlers.ListUploads)
+92 -4
View File
@@ -46,6 +46,9 @@ document.addEventListener('DOMContentLoaded', () => {
// 用户表单
document.getElementById('user-form').addEventListener('submit', handleUserSubmit);
// 基础设置表单
document.getElementById('settings-form').addEventListener('submit', handleSettingsSubmit);
// 封面图上传
const coverFileInput = document.getElementById('post-cover-file');
if (coverFileInput) {
@@ -116,6 +119,9 @@ function showPage(pageName) {
case 'themes':
loadThemes();
break;
case 'settings':
loadSettings();
break;
case 'theme-editor':
// 主题编辑器由 editTheme() 函数加载
break;
@@ -486,18 +492,24 @@ async function loadPages() {
}
}
// 加载评论
async function loadComments() {
// 加载评论(postId 为空时加载全部)
async function loadComments(postId = '') {
try {
const res = await fetch(`${API_BASE}/comments`, { headers: getHeaders() });
const url = postId ? `${API_BASE}/admin/comments?post_id=${postId}` : `${API_BASE}/admin/comments`;
const res = await fetch(url, { headers: getHeaders() });
const data = await res.json();
// 加载全部评论时刷新文章筛选下拉框
if (!postId) {
populateCommentPostFilter(data.data || []);
}
const tbody = document.getElementById('comments-list');
tbody.innerHTML = data.data?.map(c => `
<tr>
<td>${c.author}</td>
<td>${c.content.substring(0, 50)}${c.content.length > 50 ? '...' : ''}</td>
<td>文章 #${c.post_id}</td>
<td><a href="/post/${encodeURIComponent(c.post_slug || c.post_id)}" target="_blank">${c.post_title || '文章 #' + c.post_id}</a></td>
<td><span class="status-badge status-${c.status}">${c.status}</span></td>
<td>${new Date(c.created_at).toLocaleString()}</td>
<td>
@@ -752,6 +764,82 @@ async function loadUsers() {
}
}
// 填充评论筛选下拉框(所有有评论的文章)
function populateCommentPostFilter(comments) {
const select = document.getElementById('comment-post-filter');
if (!select) return;
// 按文章去重
const posts = new Map();
comments.forEach(c => {
if (!posts.has(c.post_id)) {
posts.set(c.post_id, c.post_title || ('文章 #' + c.post_id));
}
});
select.innerHTML = '<option value="">全部文章</option>';
posts.forEach((title, id) => {
const opt = document.createElement('option');
opt.value = id;
opt.textContent = title;
opt.title = title; // 悬停显示完整标题
select.appendChild(opt);
});
select.value = '';
}
// 按选中文章过滤评论列表
function filterCommentsByPost() {
loadComments(document.getElementById('comment-post-filter').value);
}
// 加载基础设置
async function loadSettings() {
try {
const res = await fetch(`${API_BASE}/admin/settings`, { headers: getHeaders() });
const data = await res.json();
if (!res.ok) {
alert(data.error || '加载设置失败');
return;
}
const s = data.data || {};
document.getElementById('setting-site-name').value = s.site_name || '';
document.getElementById('setting-site-desc').value = s.site_desc || '';
document.getElementById('setting-show-comments').checked = s.show_comments !== '0';
document.getElementById('setting-comments-per-page').value = s.comments_per_page || '10';
} catch (err) {
console.error('加载设置失败:', err);
}
}
// 保存基础设置
async function handleSettingsSubmit(e) {
e.preventDefault();
const body = {
site_name: document.getElementById('setting-site-name').value.trim(),
site_desc: document.getElementById('setting-site-desc').value.trim(),
show_comments: document.getElementById('setting-show-comments').checked,
comments_per_page: parseInt(document.getElementById('setting-comments-per-page').value, 10)
};
try {
const res = await fetch(`${API_BASE}/admin/settings`, {
method: 'PUT',
headers: getHeaders(),
body: JSON.stringify(body)
});
const data = await res.json();
if (res.ok) {
alert('设置已保存');
} else {
alert(data.error || '保存失败');
}
} catch (err) {
alert('保存失败: ' + err.message);
}
}
let usersCache = [];
// 显示新建用户表单
+33 -1
View File
@@ -24,6 +24,7 @@
<a href="#users" class="menu-item" data-page="users">用户管理</a>
<a href="#files" class="menu-item" data-page="files">文件管理</a>
<a href="#themes" class="menu-item" data-page="themes">主题管理</a>
<a href="#settings" class="menu-item" data-page="settings">基础设置</a>
</nav>
<div class="user-info">
<span id="username">Admin</span>
@@ -241,6 +242,9 @@
<div id="comments-page" class="page hidden">
<div class="page-header">
<h1>评论管理</h1>
<select id="comment-post-filter" class="comment-post-filter" onchange="filterCommentsByPost()">
<option value="">全部文章</option>
</select>
</div>
<table class="data-table">
<thead>
@@ -279,6 +283,34 @@
</table>
</div>
<!-- 基础设置 -->
<div id="settings-page" class="page hidden">
<div class="page-header">
<h1>基础设置</h1>
</div>
<form id="settings-form" class="settings-form">
<div class="form-group">
<label>站点名称</label>
<input type="text" id="setting-site-name" required>
</div>
<div class="form-group">
<label>站点描述</label>
<textarea id="setting-site-desc" rows="3"></textarea>
</div>
<div class="form-group">
<label class="checkbox-label">
<input type="checkbox" id="setting-show-comments">
前台显示评论
</label>
</div>
<div class="form-group">
<label>评论每页显示条数</label>
<input type="number" id="setting-comments-per-page" min="1" max="100" required>
</div>
<button type="submit" class="btn-primary">保存设置</button>
</form>
</div>
<!-- 文件管理 -->
<div id="files-page" class="page hidden">
<div class="page-header">
@@ -442,6 +474,6 @@
</div>
</div>
<script src="/static/admin/app.js?v=12"></script>
<script src="/static/admin/app.js?v=16"></script>
</body>
</html>
+50
View File
@@ -950,3 +950,53 @@ body {
padding-left: 24px;
}
/* 评论管理:文章筛选下拉框(固定宽度,超长标题省略显示) */
.comment-post-filter {
width: 260px;
padding: 8px 10px;
border: 1px solid #ddd;
border-radius: 4px;
background: #fff;
font-size: 14px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
/* 基础设置表单 */
.settings-form {
max-width: 560px;
background: #fff;
padding: 24px;
border-radius: 8px;
}
.settings-form label {
display: block;
margin-bottom: 6px;
font-weight: 500;
}
.settings-form input[type="text"],
.settings-form input[type="number"],
.settings-form textarea {
width: 100%;
padding: 8px 10px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
box-sizing: border-box;
}
.settings-form .checkbox-label {
display: flex;
align-items: center;
gap: 8px;
font-weight: 500;
}
.settings-form .checkbox-label input[type="checkbox"] {
width: auto;
margin: 0;
}
+15 -1
View File
@@ -59,7 +59,8 @@
</div>
</nav>
<section class="comments-section">
{{if .ShowComments}}
<section class="comments-section" id="comments">
<h3>评论 ({{.CommentCount}})</h3>
<form class="comment-form" action="/comment" method="POST">
@@ -112,5 +113,18 @@
<p class="no-comments">暂无评论,来发表第一条评论吧!</p>
{{end}}
</div>
{{if gt .CommentTotalPages 1}}
<div class="pagination comment-pagination">
{{if gt .CommentPage 1}}
<a href="/post/{{.Post.Slug}}?cpage={{sub .CommentPage 1}}#comments" class="prev">← 上一页</a>
{{end}}
<span class="page-info">{{.CommentPage}} / {{.CommentTotalPages}}</span>
{{if lt .CommentPage .CommentTotalPages}}
<a href="/post/{{.Post.Slug}}?cpage={{add .CommentPage 1}}#comments" class="next">下一页 →</a>
{{end}}
</div>
{{end}}
</section>
{{end}}
{{end}}
+15 -1
View File
@@ -53,7 +53,8 @@
</div>
</nav>
<section class="comments-section">
{{if .ShowComments}}
<section class="comments-section" id="comments">
<h3>评论 ({{.CommentCount}})</h3>
<form class="comment-form" action="/comment" method="POST">
@@ -106,5 +107,18 @@
<p class="no-comments">暂无评论,来发表第一条评论吧!</p>
{{end}}
</div>
{{if gt .CommentTotalPages 1}}
<div class="pagination comment-pagination">
{{if gt .CommentPage 1}}
<a href="/post/{{.Post.Slug}}?cpage={{sub .CommentPage 1}}#comments" class="prev">← 上一页</a>
{{end}}
<span class="page-info">{{.CommentPage}} / {{.CommentTotalPages}}</span>
{{if lt .CommentPage .CommentTotalPages}}
<a href="/post/{{.Post.Slug}}?cpage={{add .CommentPage 1}}#comments" class="next">下一页 →</a>
{{end}}
</div>
{{end}}
</section>
{{end}}
{{end}}
+15 -1
View File
@@ -59,7 +59,8 @@
</div>
</nav>
<section class="comments-section">
{{if .ShowComments}}
<section class="comments-section" id="comments">
<h3>评论 ({{.CommentCount}})</h3>
<form class="comment-form" action="/comment" method="POST">
@@ -112,5 +113,18 @@
<p class="no-comments">暂无评论,来发表第一条评论吧!</p>
{{end}}
</div>
{{if gt .CommentTotalPages 1}}
<div class="pagination comment-pagination">
{{if gt .CommentPage 1}}
<a href="/post/{{.Post.Slug}}?cpage={{sub .CommentPage 1}}#comments" class="prev">← 上一页</a>
{{end}}
<span class="page-info">{{.CommentPage}} / {{.CommentTotalPages}}</span>
{{if lt .CommentPage .CommentTotalPages}}
<a href="/post/{{.Post.Slug}}?cpage={{add .CommentPage 1}}#comments" class="next">下一页 →</a>
{{end}}
</div>
{{end}}
</section>
{{end}}
{{end}}