diff --git a/database/database.go b/database/database.go index eeffc23..d3bb7d5 100644 --- a/database/database.go +++ b/database/database.go @@ -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 } diff --git a/go.mod b/go.mod index 431a8fb..c92f6a3 100644 --- a/go.mod +++ b/go.mod @@ -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 ) diff --git a/go.sum b/go.sum index 8993b60..96b350a 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/goblog.db b/goblog.db index d6277ef..c770187 100644 Binary files a/goblog.db and b/goblog.db differ diff --git a/handlers/comment.go b/handlers/comment.go index e61cf61..468ac5f 100644 --- a/handlers/comment.go +++ b/handlers/comment.go @@ -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}) } // 创建评论 diff --git a/handlers/setting.go b/handlers/setting.go new file mode 100644 index 0000000..6466078 --- /dev/null +++ b/handlers/setting.go @@ -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": "设置已保存"}) +} diff --git a/handlers/view.go b/handlers/view.go index 6e8c1c1..84f1ed8 100644 --- a/handlers/view.go +++ b/handlers/view.go @@ -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() diff --git a/models/models.go b/models/models.go index 6a06e1f..498cff0 100644 --- a/models/models.go +++ b/models/models.go @@ -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 } diff --git a/routers/router.go b/routers/router.go index 361a409..a87a308 100644 --- a/routers/router.go +++ b/routers/router.go @@ -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) diff --git a/static/admin/app.js b/static/admin/app.js index 42fd03a..4ee043d 100644 --- a/static/admin/app.js +++ b/static/admin/app.js @@ -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 => ` ${c.author} ${c.content.substring(0, 50)}${c.content.length > 50 ? '...' : ''} - 文章 #${c.post_id} + ${c.post_title || '文章 #' + c.post_id} ${c.status} ${new Date(c.created_at).toLocaleString()} @@ -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 = ''; + 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 = []; // 显示新建用户表单 diff --git a/static/admin/index.html b/static/admin/index.html index 5dd54ae..5631b1a 100644 --- a/static/admin/index.html +++ b/static/admin/index.html @@ -24,6 +24,7 @@ 用户管理 文件管理 主题管理 + 基础设置
Admin @@ -241,6 +242,9 @@ + + + - + diff --git a/static/admin/style.css b/static/admin/style.css index c18c9d2..2e278ea 100644 --- a/static/admin/style.css +++ b/static/admin/style.css @@ -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; +} + diff --git a/templates/default/post.html b/templates/default/post.html index ca957cf..8136e0c 100644 --- a/templates/default/post.html +++ b/templates/default/post.html @@ -59,7 +59,8 @@
-
+{{if .ShowComments}} +

评论 ({{.CommentCount}})

@@ -112,5 +113,18 @@

暂无评论,来发表第一条评论吧!

{{end}} + + {{if gt .CommentTotalPages 1}} + + {{end}}
{{end}} +{{end}} diff --git a/templates/minimal/post.html b/templates/minimal/post.html index e09c821..868c580 100644 --- a/templates/minimal/post.html +++ b/templates/minimal/post.html @@ -53,7 +53,8 @@ -
+{{if .ShowComments}} +

评论 ({{.CommentCount}})

@@ -106,5 +107,18 @@

暂无评论,来发表第一条评论吧!

{{end}} + + {{if gt .CommentTotalPages 1}} + + {{end}}
{{end}} +{{end}} diff --git a/templates/mytheme/post.html b/templates/mytheme/post.html index 0f7bf3b..216bc7a 100644 --- a/templates/mytheme/post.html +++ b/templates/mytheme/post.html @@ -59,7 +59,8 @@ -
+{{if .ShowComments}} +

评论 ({{.CommentCount}})

@@ -112,5 +113,18 @@

暂无评论,来发表第一条评论吧!

{{end}} + + {{if gt .CommentTotalPages 1}} + + {{end}}
{{end}} +{{end}}