feat(admin): add comprehensive admin management system

- Add admin handlers and services for users, posts, comments, groups, and dashboard
- Implement role permission management with ability to view and update permissions
- Add database seeding for roles and Casbin permission policies
- Upgrade Casbin from v2 to v3 with GORM adapter for persistent policy storage
- Add admin-specific repository methods with filtering and pagination
- Register new admin API routes under /api/v1/admin/*
This commit is contained in:
2026-03-14 18:01:55 +08:00
parent ae5b997924
commit d3065b30cc
28 changed files with 4018 additions and 107 deletions

View File

@@ -13,7 +13,8 @@ import (
"carrot_bbs/internal/repository"
"carrot_bbs/internal/service"
"github.com/casbin/casbin/v2"
"github.com/casbin/casbin/v3"
gormadapter "github.com/casbin/gorm-adapter/v3"
"gorm.io/driver/postgres"
"gorm.io/gorm"
)
@@ -64,8 +65,14 @@ func main() {
log.Printf("[WARNING] Failed to init casbin policies: %v", err)
}
// 创建 GORM 适配器
adapter, err := gormadapter.NewAdapterByDB(db)
if err != nil {
log.Fatalf("Failed to create GORM adapter: %v", err)
}
// 创建 Casbin Enforcer
enforcer, err := casbin.NewEnforcer(cfg.Casbin.ModelPath)
enforcer, err := casbin.NewEnforcer(cfg.Casbin.ModelPath, adapter)
if err != nil {
log.Fatalf("Failed to create enforcer: %v", err)
}

59
go.mod
View File

@@ -4,8 +4,11 @@ go 1.25
require (
github.com/alicebob/miniredis/v2 v2.31.0
github.com/casbin/casbin/v2 v2.135.0
github.com/casbin/casbin/v3 v3.10.0
github.com/casbin/gorm-adapter/v3 v3.41.0
github.com/gin-gonic/gin v1.9.1
github.com/golang-jwt/jwt/v5 v5.2.0
github.com/golang-jwt/jwt/v5 v5.2.2
github.com/google/uuid v1.6.0
github.com/google/wire v0.7.0
github.com/gorse-io/gorse-go v0.5.0-alpha.3
@@ -13,22 +16,22 @@ require (
github.com/redis/go-redis/v9 v9.3.1
github.com/spf13/viper v1.18.2
go.uber.org/zap v1.26.0
golang.org/x/crypto v0.26.0
golang.org/x/crypto v0.46.0
golang.org/x/image v0.24.0
google.golang.org/grpc v1.59.0
google.golang.org/protobuf v1.31.0
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df
gorm.io/driver/postgres v1.5.4
gorm.io/driver/sqlite v1.5.4
gorm.io/gorm v1.25.5
gorm.io/driver/postgres v1.6.0
gorm.io/driver/sqlite v1.6.0
gorm.io/gorm v1.31.1
)
require (
filippo.io/edwards25519 v1.1.0 // indirect
github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a // indirect
github.com/bmatcuk/doublestar/v4 v4.6.1 // indirect
github.com/bmatcuk/doublestar/v4 v4.9.1 // indirect
github.com/bytedance/sonic v1.9.1 // indirect
github.com/casbin/casbin/v2 v2.135.0 // indirect
github.com/casbin/govaluate v1.3.0 // indirect
github.com/casbin/govaluate v1.10.0 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
@@ -37,18 +40,23 @@ require (
github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/glebarez/go-sqlite v1.22.0 // indirect
github.com/glebarez/sqlite v1.11.0 // indirect
github.com/go-logr/logr v1.2.3 // indirect
github.com/go-logr/stdr v1.2.2 // 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.14.0 // indirect
github.com/go-sql-driver/mysql v1.9.3 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect
github.com/golang-sql/sqlexp v0.1.0 // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/google/subcommands v1.2.0 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
github.com/jackc/pgx/v5 v5.4.3 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/pgx/v5 v5.8.0 // indirect
github.com/jackc/puddle/v2 v2.2.2 // 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
@@ -56,18 +64,22 @@ require (
github.com/klauspost/cpuid/v2 v2.2.6 // indirect
github.com/leodido/go-urn v1.2.4 // indirect
github.com/magiconair/properties v1.8.7 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect
github.com/mattn/go-sqlite3 v1.14.17 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-sqlite3 v1.14.22 // indirect
github.com/microsoft/go-mssqldb v1.9.5 // indirect
github.com/minio/md5-simd v1.1.2 // indirect
github.com/minio/sha256-simd v1.0.1 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // 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.1.0 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/rs/xid v1.5.0 // indirect
github.com/sagikazarmark/locafero v0.4.0 // indirect
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
github.com/shopspring/decimal v1.4.0 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
github.com/sourcegraph/conc v0.3.0 // indirect
github.com/spf13/afero v1.11.0 // indirect
@@ -83,15 +95,20 @@ require (
go.opentelemetry.io/otel/trace v1.11.1 // indirect
go.uber.org/multierr v1.10.0 // indirect
golang.org/x/arch v0.3.0 // indirect
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
golang.org/x/mod v0.20.0 // indirect
golang.org/x/net v0.28.0 // indirect
golang.org/x/sync v0.11.0 // indirect
golang.org/x/sys v0.23.0 // indirect
golang.org/x/text v0.22.0 // indirect
golang.org/x/tools v0.24.1 // indirect
golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 // indirect
golang.org/x/net v0.48.0 // indirect
golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.39.0 // indirect
golang.org/x/text v0.32.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20231120223509-83a465c0220f // indirect
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
gorm.io/driver/mysql v1.6.0 // indirect
gorm.io/driver/sqlserver v1.6.3 // indirect
gorm.io/plugin/dbresolver v1.6.2 // indirect
modernc.org/libc v1.67.4 // indirect
modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.11.0 // indirect
modernc.org/sqlite v1.42.2 // indirect
)

301
go.sum
View File

@@ -1,10 +1,37 @@
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.7.0/go.mod h1:bjGvMhVMb+EEm3VRNQawDMUyMMjo+S5ewNjflkep/0Q=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.7.1/go.mod h1:bjGvMhVMb+EEm3VRNQawDMUyMMjo+S5ewNjflkep/0Q=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.11.1/go.mod h1:a6xsAQUZg+VsS3TJ05SRp524Hs4pZ/AeFSr5ENf0Yjo=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0 h1:Gt0j3wceWMwPmiazCa8MzMA0MfhmPIz0Qp0FJ6qcM0U=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0/go.mod h1:Ot/6aikWnKWi4l9QB7qVSwa8iMphQNqkWALMoNT3rzM=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.3.1/go.mod h1:uE9zaUfEQT/nbQjVi2IblCG9iaLtZsuYZ8ne+PuQ02M=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.6.0/go.mod h1:9kIvujWAA58nmPmWB1m23fyWic1kYZMxD9CxaWn4Qpg=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.1 h1:B+blDbyVIG3WaikNxPnhPiJ1MThR03b3vKGtER95TP4=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.1/go.mod h1:JdM5psgjfBf5fo2uWOZhflPWyDBZ/O/CNAH9CtsuZE4=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.3.0/go.mod h1:okt5dMMTOFjX/aovMlrjvvXoPMBVSPzk9185BT0+eZM=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.2/go.mod h1:yInRyqWXAuaPrgI7p70+lDDgh3mlBohis29jGMISnmc=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.8.0/go.mod h1:4OG6tQ9EOP/MT0NMjDlRzWoVFxfu9rN9B2X+tlSVktg=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1 h1:FPKJS1T+clwv+OLGt13a8UjqeRuh0O4SJ3lUriThc+4=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1/go.mod h1:j2chePtV91HrC22tGoRX3sGY42uF13WzmmV80/OdVAA=
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.0.1/go.mod h1:GpPjLhVR9dnUoJMyHWSPy71xY9/lcmpzIPZXmF0FCVY=
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.3.1 h1:Wgf5rZba3YZqeTNJPtvqZoBu1sBN/L4sry+u2U3Y75w=
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.3.1/go.mod h1:xxCBG/f/4Vbmh2XQJBsOmNdxWUY5j/s27jujKPbQf14=
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.0.0/go.mod h1:bTSOgj05NGRuHHhQwAdPnYr9TOdNmKlZTgGLL6nyAdI=
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.1.1 h1:bFWuoEKg+gImo7pvkiQEFAc8ocibADgXeiLAxWhWmkI=
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.1.1/go.mod h1:Vih/3yc6yac2JzU4hzpaDupBJP0Flaia9rXXrU8xyww=
github.com/AzureAD/microsoft-authentication-library-for-go v1.1.1/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2 h1:oygO0locgZJe7PpYPXT5A29ZkwJaPqcva7BVeemZOZs=
github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
github.com/DmitriyVTitov/size v1.5.0/go.mod h1:le6rNI4CoLQV1b9gzp1+3d7hMAD/uu2QcJ+aYbNgiU0=
github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a h1:HbKu58rmZpUGpz5+4FfNmIU+FmZg2P3Xaj2v2bfNWmk=
github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a/go.mod h1:SGnFV6hVsYE877CKEZ6tDNTjaSXYUk6QqoIK6PrAtcc=
github.com/alicebob/miniredis/v2 v2.31.0 h1:ObEFUNlJwoIiyjxdrYF0QIDE7qXcLc7D3WpSH4c22PU=
github.com/alicebob/miniredis/v2 v2.31.0/go.mod h1:UB/T2Uztp7MlFSDakaX1sTXUv5CASoprx0wulRT6HBg=
github.com/bmatcuk/doublestar/v4 v4.6.1 h1:FH9SifrbvJhnlQpztAx++wlkk70QBf0iBWDwNy7PA4I=
github.com/bmatcuk/doublestar/v4 v4.6.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
github.com/bmatcuk/doublestar/v4 v4.9.1 h1:X8jg9rRZmJd4yRy7ZeNDRnM+T3ZfHv15JiBJ/avrEXE=
github.com/bmatcuk/doublestar/v4 v4.9.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
@@ -14,8 +41,13 @@ github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s
github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
github.com/casbin/casbin/v2 v2.135.0 h1:6BLkMQiGotYyS5yYeWgW19vxqugUlvHFkFiLnLR/bxk=
github.com/casbin/casbin/v2 v2.135.0/go.mod h1:FmcfntdXLTcYXv/hxgNntcRPqAbwOG9xsism0yXT+18=
github.com/casbin/govaluate v1.3.0 h1:VA0eSY0M2lA86dYd5kPPuNZMUD9QkWnOCnavGrw9myc=
github.com/casbin/casbin/v3 v3.10.0 h1:039ORla55vCeIZWd0LfzWFt1yiEA5X4W41xBW2bQuHs=
github.com/casbin/casbin/v3 v3.10.0/go.mod h1:5rJbQr2e6AuuDDNxnPc5lQlC9nIgg6nS1zYwKXhpHC8=
github.com/casbin/gorm-adapter/v3 v3.41.0 h1:Xhpi0tfRP9aKPDWDf6dgBxHZ9UM6IophxxPIEGWqCNM=
github.com/casbin/gorm-adapter/v3 v3.41.0/go.mod h1:BQZRJhwUnwMpI+pT2m7/cUJwXxrHfzpBpPcNTyMGeGA=
github.com/casbin/govaluate v1.3.0/go.mod h1:G/UnbIjZk/0uMNaLwZZmFQrR72tYRZWQkO70si/iR7A=
github.com/casbin/govaluate v1.10.0 h1:ffGw51/hYH3w3rZcxO/KcaUIDOLP84w7nsidMVgaDG0=
github.com/casbin/govaluate v1.10.0/go.mod h1:G/UnbIjZk/0uMNaLwZZmFQrR72tYRZWQkO70si/iR7A=
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
@@ -24,12 +56,15 @@ github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583j
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/dnaeon/go-vcr v1.1.0/go.mod h1:M7tiix8f0r6mKKJ3Yq/kqU1OYf3MnfmBWVbPx/yU9ko=
github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/felixge/httpsnoop v1.0.3 h1:s/nj+GCswXYzN5v2DpNMuMQYe+0DDwt5WVCU6CWBdXk=
@@ -44,6 +79,10 @@ github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=
github.com/glebarez/go-sqlite v1.22.0 h1:uAcMJhaA6r3LHMTFgP0SifzgXg46yJkgxqyuyec+ruQ=
github.com/glebarez/go-sqlite v1.22.0/go.mod h1:PlBIdHe0+aUEFn+r2/uthrWq4FxbzugL0L8Li6yQJbc=
github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw=
github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0=
github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
@@ -57,11 +96,20 @@ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJn
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js=
github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
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/golang-jwt/jwt/v5 v5.2.0 h1:d/ix8ftRUorsN+5eMIlF4T6J8CAt9rch3My2winC1Jw=
github.com/golang-jwt/jwt/v5 v5.2.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang-jwt/jwt/v5 v5.0.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA=
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A=
github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.4.4 h1:l75CXGRSwbaYNpl/Z2X1XIIAMSCquvXgpVZDhwEIJsc=
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
@@ -70,24 +118,38 @@ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/subcommands v1.2.0 h1:vWQspBTo2nEqTUFita5/KeEWlUL8kQObDFbub/EN9oE=
github.com/google/subcommands v1.2.0/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk=
github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU=
github.com/google/uuid v1.5.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.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
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/google/wire v0.7.0 h1:JxUKI6+CVBgCO2WToKy/nQk0sS+amI9z9EjVmdaocj4=
github.com/google/wire v0.7.0/go.mod h1:n6YbUQD9cPKTnHXEBN2DXlOp/mVADhVErcMFb0v3J18=
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
github.com/gorse-io/gorse-go v0.5.0-alpha.3 h1:GR/OWzq016VyFyTTxgQWeayGahRVzB1cGFIW/AaShC4=
github.com/gorse-io/gorse-go v0.5.0-alpha.3/go.mod h1:ZxmVHzZPKm5pmEIlqaRDwK0rkfTRHlfziO033XZ+RW0=
github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc=
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/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.4.3 h1:cxFyXhxlvAifxnkKKdlxv8XqUf59tDlYjnV5YYfsJJY=
github.com/jackc/pgx/v5 v5.4.3/go.mod h1:Ig06C2Vu0t5qXC60W8sqIthScaEnFvojjj9dSljmHRA=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.8.0 h1:TYPDoleBBme0xGSAX3/+NujXXtpZn9HBONkQC7IEZSo=
github.com/jackc/pgx/v5 v5.8.0/go.mod h1:QVeDInX2m9VyzvNeiCJVjCkNFqzsNb43204HshNSZKw=
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs=
github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM=
github.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo=
github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg=
github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs=
github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc=
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=
@@ -100,18 +162,28 @@ github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa02
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.6 h1:ndNyv040zDGIDh8thGkXYjnFtiN02M1PVVF+JE/48xc=
github.com/klauspost/cpuid/v2 v2.2.6/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
github.com/lib/pq v1.10.2 h1:AqzbZs4ZoCBp+GtejcpCpcxM3zlSMx29dXbUSeVtJb8=
github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM=
github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
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/microsoft/go-mssqldb v1.8.2/go.mod h1:vp38dT33FGfVotRiTmDo3bFyaHq+p3LektQrjTULowo=
github.com/microsoft/go-mssqldb v1.9.5 h1:orwya0X/5bsL1o+KasupTkk2eNTNFkTQG0BEe/HxCn0=
github.com/microsoft/go-mssqldb v1.9.5/go.mod h1:VCP2a0KEZZtGLRHd1PsLavLFYy/3xX2yJUPycv3Sr2Q=
github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34=
github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM=
github.com/minio/minio-go/v7 v7.0.66 h1:bnTOXOHjOqv/gcMuiVbN9o2ngRItvqE774dG9nq0Dzw=
@@ -125,21 +197,36 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/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/modocache/gover v0.0.0-20171022184752-b58185e213c5/go.mod h1:caMODM3PzxT8aQXRPkAt8xlV/e7d7w8GM5g0fa5F0D8=
github.com/montanaflynn/stats v0.7.0/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow=
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.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4=
github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/redis/go-redis/v9 v9.3.1 h1:KqdY8U+3X6z+iACvumCNxnoluToB+9Me+TvyFa21Mds=
github.com/redis/go-redis/v9 v9.3.1/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
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/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
github.com/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc=
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ=
github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4=
github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE=
github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ=
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
@@ -155,20 +242,25 @@ github.com/spf13/viper v1.18.2/go.mod h1:EKmWIqdnk5lOcmR72yw6hS+8OPYcwD0jteitLMV
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/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
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.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
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/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
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.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yuin/gopher-lua v1.1.0 h1:BojcDhfyDWgU2f2TOzYK/g5p2gxMrku8oupLDqlnSqE=
github.com/yuin/gopher-lua v1.1.0/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.36.4 h1:aUEBEdCa6iamGzg6fuYxDA8ThxvOG240mAvWDU+XLio=
@@ -189,33 +281,123 @@ golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUu
golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k=
golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw=
golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54=
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g=
golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio=
golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM=
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 h1:fQsdNF2N+/YewlRZiricy4P1iimyPKZ/xwniHj8Q2a0=
golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93/go.mod h1:EPRbTFwzwjXj9NpYyyrvenVh9Y+GFeEvMNh7Xuz7xgU=
golang.org/x/image v0.24.0 h1:AN7zRgVsbvmTfNyqIbbOraYL8mSwcKncEj8ofjgzcMQ=
golang.org/x/image v0.24.0/go.mod h1:4b/ITuLfqYq1hqZcjofwctIhi7sZh2WaCjvsBNjjya8=
golang.org/x/mod v0.20.0 h1:utOm6MM3R3dnawAiJgn0y+xvuYRsm1RKM/4giyfDgV0=
golang.org/x/mod v0.20.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI=
golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE=
golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.13.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA=
golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI=
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
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/sys v0.0.0-20190204203706-41f3e6584952/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.23.0 h1:YfKFowiIMvtgl1UERQoTPPToxltDeZfbj4H7dVUCwmM=
golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o=
golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU=
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk=
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4=
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.24.1 h1:vxuHLTNS3Np5zrYoPRpcheASHX/7KiGo+8Y4ZM1J2O8=
golang.org/x/tools v0.24.1/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA=
golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/genproto/googleapis/rpc v0.0.0-20231120223509-83a465c0220f h1:ultW7fxlIvee4HYrtnaRPon9HpEgFk5zYpmfMgtKB5I=
google.golang.org/genproto/googleapis/rpc v0.0.0-20231120223509-83a465c0220f/go.mod h1:L9KNLi232K1/xB6f7AlSX692koaRnKaWSR0stBki0Yc=
@@ -234,13 +416,52 @@ gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df h1:n7WqCuqOuCbNr617RXOY0AW
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df/go.mod h1:LRQQ+SO6ZHR7tOkpBDuZnXENFzX8qRjMDMyPD6BRkCw=
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
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/driver/postgres v1.5.4 h1:Iyrp9Meh3GmbSuyIAGyjkN+n9K+GHX9b9MqsTL4EJCo=
gorm.io/driver/postgres v1.5.4/go.mod h1:Bgo89+h0CRcdA33Y6frlaHHVuTdOf87pmyzwW9C/BH0=
gorm.io/driver/sqlite v1.5.4 h1:IqXwXi8M/ZlPzH/947tn5uik3aYQslP9BVveoax0nV0=
gorm.io/driver/sqlite v1.5.4/go.mod h1:qxAuCol+2r6PannQDpOP1FP6ag3mKi4esLnB/jHed+4=
gorm.io/gorm v1.25.5 h1:zR9lOiiYf09VNh5Q1gphfyia1JpiClIWG9hQaxB/mls=
gorm.io/gorm v1.25.5/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
gorm.io/driver/mysql v1.6.0 h1:eNbLmNTpPpTOVZi8MMxCi2aaIm0ZpInbORNXDwyLGvg=
gorm.io/driver/mysql v1.6.0/go.mod h1:D/oCC2GWK3M/dqoLxnOlaNKmXz8WNTfcS9y5ovaSqKo=
gorm.io/driver/postgres v1.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4=
gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo=
gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ=
gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8=
gorm.io/driver/sqlserver v1.6.3 h1:UR+nWCuphPnq7UxnL57PSrlYjuvs+sf1N59GgFX7uAI=
gorm.io/driver/sqlserver v1.6.3/go.mod h1:VZeNn7hqX1aXoN5TPAFGWvxWG90xtA8erGn2gQmpc6U=
gorm.io/gorm v1.30.0/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE=
gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg=
gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=
gorm.io/plugin/dbresolver v1.6.2 h1:F4b85TenghUeITqe3+epPSUtHH7RIk3fXr5l83DF8Pc=
gorm.io/plugin/dbresolver v1.6.2/go.mod h1:tctw63jdrOezFR9HmrKnPkmig3m5Edem9fdxk9bQSzM=
modernc.org/cc/v4 v4.27.1 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis=
modernc.org/cc/v4 v4.27.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
modernc.org/ccgo/v4 v4.30.1 h1:4r4U1J6Fhj98NKfSjnPUN7Ze2c6MnAdL0hWw6+LrJpc=
modernc.org/ccgo/v4 v4.30.1/go.mod h1:bIOeI1JL54Utlxn+LwrFyjCx2n2RDiYEaJVSrgdrRfM=
modernc.org/fileutil v1.3.40 h1:ZGMswMNc9JOCrcrakF1HrvmergNLAmxOPjizirpfqBA=
modernc.org/fileutil v1.3.40/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc=
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.1 h1:k8T3gkXWY9sEiytKhcgyiZ2L0DTyCQ/nvX+LoCljoRE=
modernc.org/gc/v3 v3.1.1/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.67.4 h1:zZGmCMUVPORtKv95c2ReQN5VDjvkoRm9GWPTEPuvlWg=
modernc.org/libc v1.67.4/go.mod h1:QvvnnJ5P7aitu0ReNpVIEyesuhmDLQ8kaEoyMjIFZJA=
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.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
modernc.org/opt v0.1.4/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.42.2 h1:7hkZUNJvJFN2PgfUdjni9Kbvd4ef4mNLOu0B9FGxM74=
modernc.org/sqlite v1.42.2/go.mod h1:+VkC6v3pLOAE0A0uVucQEcbVW0I5nHCeDaBf+DpsQT8=
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=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=

View File

@@ -50,6 +50,56 @@ type UserDetailResponse struct {
CreatedAt string `json:"created_at"`
}
// ==================== Admin User DTOs ====================
// AdminUserListResponse 管理端用户列表响应
type AdminUserListResponse struct {
ID string `json:"id"`
Username string `json:"username"`
Nickname string `json:"nickname"`
Email *string `json:"email,omitempty"`
Phone *string `json:"phone,omitempty"`
EmailVerified bool `json:"email_verified"`
Avatar string `json:"avatar"`
Status string `json:"status"`
PostsCount int `json:"posts_count"`
FollowersCount int `json:"followers_count"`
FollowingCount int `json:"following_count"`
LastLoginAt string `json:"last_login_at,omitempty"`
LastLoginIP string `json:"last_login_ip,omitempty"`
CreatedAt string `json:"created_at"`
}
// AdminUserDetailResponse 管理端用户详情响应
type AdminUserDetailResponse struct {
ID string `json:"id"`
Username string `json:"username"`
Nickname string `json:"nickname"`
Email *string `json:"email"`
Phone *string `json:"phone"`
EmailVerified bool `json:"email_verified"`
Avatar string `json:"avatar"`
CoverURL string `json:"cover_url"`
Bio string `json:"bio"`
Website string `json:"website"`
Location string `json:"location"`
IsVerified bool `json:"is_verified"`
Status string `json:"status"`
PostsCount int `json:"posts_count"`
CommentsCount int64 `json:"comments_count"`
FollowersCount int `json:"followers_count"`
FollowingCount int `json:"following_count"`
LastLoginAt string `json:"last_login_at,omitempty"`
LastLoginIP string `json:"last_login_ip,omitempty"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
}
// AdminUpdateUserStatusRequest 更新用户状态请求
type AdminUpdateUserStatusRequest struct {
Status string `json:"status" binding:"required,oneof=active banned"`
}
// ==================== Post DTOs ====================
// PostImageResponse 帖子图片响应
@@ -819,3 +869,293 @@ type WSResponse struct {
Data interface{} `json:"data,omitempty"` // 响应数据
Error string `json:"error,omitempty"` // 错误信息
}
// ==================== Admin Post DTOs ====================
// AdminPostListResponse 管理端帖子列表响应
type AdminPostListResponse struct {
ID string `json:"id"`
UserID string `json:"user_id"`
Title string `json:"title"`
Content string `json:"content"`
Images []PostImageResponse `json:"images"`
Status string `json:"status"`
LikesCount int `json:"likes_count"`
CommentsCount int `json:"comments_count"`
FavoritesCount int `json:"favorites_count"`
ViewsCount int `json:"views_count"`
IsPinned bool `json:"is_pinned"`
IsFeatured bool `json:"is_featured"`
IsLocked bool `json:"is_locked"`
RejectReason string `json:"reject_reason,omitempty"`
ReviewedAt string `json:"reviewed_at,omitempty"`
ReviewedBy string `json:"reviewed_by,omitempty"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
Author *UserResponse `json:"author"`
}
// AdminPostDetailResponse 管理端帖子详情响应
type AdminPostDetailResponse struct {
ID string `json:"id"`
UserID string `json:"user_id"`
CommunityID string `json:"community_id"`
Title string `json:"title"`
Content string `json:"content"`
Images []PostImageResponse `json:"images"`
Status string `json:"status"`
LikesCount int `json:"likes_count"`
CommentsCount int `json:"comments_count"`
FavoritesCount int `json:"favorites_count"`
SharesCount int `json:"shares_count"`
ViewsCount int `json:"views_count"`
HotScore float64 `json:"hot_score"`
IsPinned bool `json:"is_pinned"`
IsFeatured bool `json:"is_featured"`
IsLocked bool `json:"is_locked"`
IsVote bool `json:"is_vote"`
RejectReason string `json:"reject_reason,omitempty"`
ReviewedAt string `json:"reviewed_at,omitempty"`
ReviewedBy string `json:"reviewed_by,omitempty"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
Author *UserResponse `json:"author"`
}
// AdminModeratePostRequest 审核帖子请求
type AdminModeratePostRequest struct {
Status string `json:"status" binding:"required,oneof=published rejected"`
Reason string `json:"reason"` // 拒绝理由当status为rejected时
}
// AdminBatchDeletePostsRequest 批量删除帖子请求
type AdminBatchDeletePostsRequest struct {
IDs []string `json:"ids" binding:"required,min=1"`
}
// AdminBatchModeratePostsRequest 批量审核帖子请求
type AdminBatchModeratePostsRequest struct {
IDs []string `json:"ids" binding:"required,min=1"`
Status string `json:"status" binding:"required,oneof=published rejected"`
}
// AdminSetPostPinRequest 置顶帖子请求
type AdminSetPostPinRequest struct {
IsPinned bool `json:"is_pinned"`
}
// AdminSetPostFeatureRequest 加精帖子请求
type AdminSetPostFeatureRequest struct {
IsFeatured bool `json:"is_featured"`
}
// AdminBatchOperationResponse 批量操作响应
type AdminBatchOperationResponse struct {
Success bool `json:"success"`
Message string `json:"message"`
Failed []string `json:"failed,omitempty"` // 失败的ID列表
}
// ==================== Admin Comment DTOs ====================
// AdminCommentListResponse 管理端评论列表响应
type AdminCommentListResponse struct {
ID string `json:"id"`
PostID string `json:"post_id"`
UserID string `json:"user_id"`
ParentID *string `json:"parent_id"`
RootID *string `json:"root_id"`
Content string `json:"content"`
Images []CommentImageResponse `json:"images"`
Status string `json:"status"`
LikesCount int `json:"likes_count"`
RepliesCount int `json:"replies_count"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
Author *UserResponse `json:"author"`
Post *AdminCommentPostInfo `json:"post"`
}
// AdminCommentPostInfo 评论关联的帖子简要信息
type AdminCommentPostInfo struct {
ID string `json:"id"`
Title string `json:"title"`
}
// AdminCommentDetailResponse 管理端评论详情响应
type AdminCommentDetailResponse struct {
ID string `json:"id"`
PostID string `json:"post_id"`
UserID string `json:"user_id"`
ParentID *string `json:"parent_id"`
RootID *string `json:"root_id"`
Content string `json:"content"`
Images []CommentImageResponse `json:"images"`
Status string `json:"status"`
LikesCount int `json:"likes_count"`
RepliesCount int `json:"replies_count"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
Author *UserResponse `json:"author"`
Post *AdminCommentPostInfo `json:"post"`
ReplyToUser *UserResponse `json:"reply_to_user,omitempty"` // 被回复的用户信息
}
// AdminBatchDeleteCommentsRequest 批量删除评论请求
type AdminBatchDeleteCommentsRequest struct {
IDs []string `json:"ids" binding:"required,min=1"`
}
// ==================== Admin Group DTOs ====================
// AdminGroupListResponse 管理端群组列表响应
type AdminGroupListResponse struct {
ID string `json:"id"`
Name string `json:"name"`
Avatar string `json:"avatar"`
Description string `json:"description"`
OwnerID string `json:"owner_id"`
Owner *UserResponse `json:"owner"`
MemberCount int `json:"member_count"`
MaxMembers int `json:"max_members"`
JoinType int `json:"join_type"`
MuteAll bool `json:"mute_all"`
Status string `json:"status"`
PostCount int64 `json:"post_count"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
}
// AdminGroupDetailResponse 管理端群组详情响应
type AdminGroupDetailResponse struct {
ID string `json:"id"`
Name string `json:"name"`
Avatar string `json:"avatar"`
Description string `json:"description"`
OwnerID string `json:"owner_id"`
Owner *UserResponse `json:"owner"`
MemberCount int `json:"member_count"`
MaxMembers int `json:"max_members"`
JoinType int `json:"join_type"`
MuteAll bool `json:"mute_all"`
Status string `json:"status"`
PostCount int64 `json:"post_count"`
Announcement *GroupAnnouncementResponse `json:"announcement,omitempty"`
IsPublic bool `json:"is_public"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
}
// AdminGroupMemberResponse 管理端群组成员响应
type AdminGroupMemberResponse struct {
ID string `json:"id"`
UserID string `json:"user_id"`
Username string `json:"username"`
Nickname string `json:"nickname"`
Avatar string `json:"avatar"`
Role string `json:"role"`
Muted bool `json:"muted"`
JoinedAt string `json:"joined_at"`
User *UserResponse `json:"user,omitempty"`
}
// AdminGroupMemberListResponse 管理端群组成员列表响应
type AdminGroupMemberListResponse struct {
List []*AdminGroupMemberResponse `json:"list"`
Total int64 `json:"total"`
Page int `json:"page"`
PageSize int `json:"page_size"`
}
// AdminUpdateGroupStatusRequest 更新群组状态请求
type AdminUpdateGroupStatusRequest struct {
Status string `json:"status" binding:"required,oneof=active dissolved"`
}
// AdminTransferGroupOwnerRequest 转让群主请求
type AdminTransferGroupOwnerRequest struct {
NewOwnerID string `json:"new_owner_id" binding:"required"`
}
// ==================== Admin Dashboard DTOs ====================
// DashboardStatsResponse 仪表盘统计数据响应
type DashboardStatsResponse struct {
TotalUsers int64 `json:"total_users"` // 总用户数
TodayNewUsers int64 `json:"today_new_users"` // 今日新增用户
ActiveUsersToday int64 `json:"active_users_today"` // 今日活跃用户 (DAU)
ActiveUsersWeek int64 `json:"active_users_week"` // 本周活跃用户 (WAU)
ActiveUsersMonth int64 `json:"active_users_month"` // 本月活跃用户 (MAU)
Retention1Day int64 `json:"retention_1_day"` // 次日留存率 (百分比 0-100)
Retention7Day int64 `json:"retention_7_day"` // 7日留存率 (百分比 0-100)
Retention30Day int64 `json:"retention_30_day"` // 30日留存率 (百分比 0-100)
TotalPosts int64 `json:"total_posts"` // 总帖子数
TodayPosts int64 `json:"today_posts"` // 今日帖子数
PendingPosts int64 `json:"pending_posts"` // 待审核帖子数
TotalComments int64 `json:"total_comments"` // 总评论数
TodayComments int64 `json:"today_comments"` // 今日评论数
TotalGroups int64 `json:"total_groups"` // 总群组数
TodayNewGroups int64 `json:"today_new_groups"` // 今日新建群组
PendingReview int64 `json:"pending_review"` // 待审核总数
PendingPostsCount int64 `json:"pending_posts_count"` // 待审核帖子数
PendingComments int64 `json:"pending_comments_count"` // 待审核评论数
}
// UserActivityTrendResponse 用户活跃度趋势响应
type UserActivityTrendResponse struct {
Date string `json:"date"` // 日期
NewUsers int64 `json:"new_users"` // 新增用户数
ActiveUsers int64 `json:"active_users"` // 活跃用户数
Posts int64 `json:"posts"` // 帖子数
Comments int64 `json:"comments"` // 评论数
}
// ContentStatsResponse 内容统计响应
type ContentStatsResponse struct {
ContentDistribution []ContentDistributionItem `json:"content_distribution"` // 内容分布
PostStatusDistribution []PostStatusDistributionItem `json:"post_status_distribution"` // 帖子状态分布
UserGrowth []UserGrowthItem `json:"user_growth"` // 用户增长趋势
}
// ContentDistributionItem 内容分布项
type ContentDistributionItem struct {
Type string `json:"type"` // 类型posts, comments, groups
Name string `json:"name"` // 名称
Count int64 `json:"count"` // 数量
}
// PostStatusDistributionItem 帖子状态分布项
type PostStatusDistributionItem struct {
Status string `json:"status"` // 状态
Name string `json:"name"` // 名称
Count int64 `json:"count"` // 数量
}
// UserGrowthItem 用户增长项
type UserGrowthItem struct {
Date string `json:"date"` // 日期
NewUsers int64 `json:"new_users"` // 新增用户数
}
// PendingContentResponse 待审核内容响应
type PendingContentResponse struct {
ID string `json:"id"` // 内容ID
Type string `json:"type"` // 内容类型post/comment
Title string `json:"title"` // 标题(帖子有,评论可能为空)
Content string `json:"content"` // 内容
Author *PendingContentAuthor `json:"author"` // 作者信息
CreatedAt string `json:"created_at"` // 创建时间
}
// PendingContentAuthor 待审核内容作者信息
type PendingContentAuthor struct {
ID string `json:"id"` // 用户ID
Username string `json:"username"` // 用户名
Nickname string `json:"nickname"` // 昵称
Avatar string `json:"avatar"` // 头像
}
// OnlineUsersResponse 在线用户数响应
type OnlineUsersResponse struct {
Count int64 `json:"count"` // 在线用户数
}

View File

@@ -0,0 +1,151 @@
package handler
import (
"strconv"
"carrot_bbs/internal/dto"
"carrot_bbs/internal/model"
"carrot_bbs/internal/pkg/response"
"carrot_bbs/internal/service"
"github.com/gin-gonic/gin"
)
// AdminCommentHandler 管理端评论处理器
type AdminCommentHandler struct {
adminCommentService service.AdminCommentService
}
// NewAdminCommentHandler 创建管理端评论处理器
func NewAdminCommentHandler(adminCommentService service.AdminCommentService) *AdminCommentHandler {
return &AdminCommentHandler{
adminCommentService: adminCommentService,
}
}
// GetCommentList 获取评论列表
// GET /api/v1/admin/comments
func (h *AdminCommentHandler) GetCommentList(c *gin.Context) {
// 解析分页参数
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
if page <= 0 {
page = 1
}
if pageSize <= 0 || pageSize > 100 {
pageSize = 20
}
// 获取筛选参数
keyword := c.Query("keyword")
postID := c.Query("post_id")
authorID := c.Query("author_id")
status := c.Query("status")
startDate := c.Query("start_date")
endDate := c.Query("end_date")
// 验证状态参数
if status != "" && status != string(model.CommentStatusDraft) &&
status != string(model.CommentStatusPending) &&
status != string(model.CommentStatusPublished) &&
status != string(model.CommentStatusRejected) &&
status != string(model.CommentStatusDeleted) {
response.BadRequest(c, "invalid status value")
return
}
comments, total, err := h.adminCommentService.GetCommentList(c.Request.Context(), page, pageSize, keyword, postID, authorID, status, startDate, endDate)
if err != nil {
response.InternalServerError(c, "failed to get comment list")
return
}
response.Paginated(c, comments, total, page, pageSize)
}
// GetCommentDetail 获取评论详情
// GET /api/v1/admin/comments/:id
func (h *AdminCommentHandler) GetCommentDetail(c *gin.Context) {
commentID := c.Param("id")
if commentID == "" {
response.BadRequest(c, "comment id is required")
return
}
commentDetail, err := h.adminCommentService.GetCommentDetail(c.Request.Context(), commentID)
if err != nil {
response.NotFound(c, "comment not found")
return
}
response.Success(c, commentDetail)
}
// DeleteComment 删除评论
// DELETE /api/v1/admin/comments/:id
func (h *AdminCommentHandler) DeleteComment(c *gin.Context) {
commentID := c.Param("id")
if commentID == "" {
response.BadRequest(c, "comment id is required")
return
}
err := h.adminCommentService.DeleteComment(c.Request.Context(), commentID)
if err != nil {
response.HandleError(c, err, "failed to delete comment")
return
}
response.Success(c, dto.SuccessResponse{
Success: true,
Message: "comment deleted successfully",
})
}
// BatchDeleteComments 批量删除评论
// POST /api/v1/admin/comments/batch-delete
func (h *AdminCommentHandler) BatchDeleteComments(c *gin.Context) {
var req dto.AdminBatchDeleteCommentsRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, "invalid request body: "+err.Error())
return
}
result, err := h.adminCommentService.BatchDeleteComments(c.Request.Context(), req.IDs)
if err != nil {
response.InternalServerError(c, "failed to batch delete comments")
return
}
response.Success(c, result)
}
// GetPostComments 获取帖子的评论列表
// GET /api/v1/admin/posts/:postId/comments
func (h *AdminCommentHandler) GetPostComments(c *gin.Context) {
postID := c.Param("postId")
if postID == "" {
response.BadRequest(c, "post id is required")
return
}
// 解析分页参数
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
if page <= 0 {
page = 1
}
if pageSize <= 0 || pageSize > 100 {
pageSize = 20
}
comments, total, err := h.adminCommentService.GetCommentsByPostID(c.Request.Context(), postID, page, pageSize)
if err != nil {
response.InternalServerError(c, "failed to get post comments")
return
}
response.Paginated(c, comments, total, page, pageSize)
}

View File

@@ -0,0 +1,100 @@
package handler
import (
"strconv"
"carrot_bbs/internal/pkg/response"
"carrot_bbs/internal/service"
"github.com/gin-gonic/gin"
)
// AdminDashboardHandler 管理端仪表盘处理器
type AdminDashboardHandler struct {
dashboardService service.AdminDashboardService
}
// NewAdminDashboardHandler 创建管理端仪表盘处理器
func NewAdminDashboardHandler(dashboardService service.AdminDashboardService) *AdminDashboardHandler {
return &AdminDashboardHandler{
dashboardService: dashboardService,
}
}
// GetStats 获取统计数据
// GET /api/v1/admin/dashboard/stats
func (h *AdminDashboardHandler) GetStats(c *gin.Context) {
stats, err := h.dashboardService.GetStats(c.Request.Context())
if err != nil {
response.InternalServerError(c, "failed to get dashboard stats")
return
}
response.Success(c, stats)
}
// GetUserActivityTrend 获取用户活跃度趋势
// GET /api/v1/admin/dashboard/user-activity
func (h *AdminDashboardHandler) GetUserActivityTrend(c *gin.Context) {
// 解析天数参数默认7天
days, _ := strconv.Atoi(c.DefaultQuery("days", "7"))
if days <= 0 {
days = 7
}
if days > 30 {
days = 30
}
trend, err := h.dashboardService.GetUserActivityTrend(c.Request.Context(), days)
if err != nil {
response.InternalServerError(c, "failed to get user activity trend")
return
}
response.Success(c, trend)
}
// GetContentStats 获取内容统计
// GET /api/v1/admin/dashboard/content-stats
func (h *AdminDashboardHandler) GetContentStats(c *gin.Context) {
stats, err := h.dashboardService.GetContentStats(c.Request.Context())
if err != nil {
response.InternalServerError(c, "failed to get content stats")
return
}
response.Success(c, stats)
}
// GetPendingContent 获取待审核内容
// GET /api/v1/admin/dashboard/pending-content
func (h *AdminDashboardHandler) GetPendingContent(c *gin.Context) {
// 解析limit参数默认10条
limit, _ := strconv.Atoi(c.DefaultQuery("limit", "10"))
if limit <= 0 {
limit = 10
}
if limit > 50 {
limit = 50
}
content, err := h.dashboardService.GetPendingContent(c.Request.Context(), limit)
if err != nil {
response.InternalServerError(c, "failed to get pending content")
return
}
response.Success(c, content)
}
// GetOnlineUsers 获取在线用户数
// GET /api/v1/admin/dashboard/online-users
func (h *AdminDashboardHandler) GetOnlineUsers(c *gin.Context) {
onlineUsers, err := h.dashboardService.GetOnlineUsers(c.Request.Context())
if err != nil {
response.InternalServerError(c, "failed to get online users count")
return
}
response.Success(c, onlineUsers)
}

View File

@@ -0,0 +1,202 @@
package handler
import (
"strconv"
"carrot_bbs/internal/dto"
"carrot_bbs/internal/pkg/response"
"carrot_bbs/internal/service"
"github.com/gin-gonic/gin"
)
// AdminGroupHandler 管理端群组处理器
type AdminGroupHandler struct {
adminGroupService service.AdminGroupService
}
// NewAdminGroupHandler 创建管理端群组处理器
func NewAdminGroupHandler(adminGroupService service.AdminGroupService) *AdminGroupHandler {
return &AdminGroupHandler{
adminGroupService: adminGroupService,
}
}
// GetGroupList 获取群组列表
// GET /api/v1/admin/groups
func (h *AdminGroupHandler) GetGroupList(c *gin.Context) {
// 解析分页参数
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
if page <= 0 {
page = 1
}
if pageSize <= 0 || pageSize > 100 {
pageSize = 20
}
// 获取筛选参数
keyword := c.Query("keyword")
status := c.Query("status")
ownerID := c.Query("owner_id")
startDate := c.Query("start_date")
endDate := c.Query("end_date")
groups, total, err := h.adminGroupService.GetGroupList(c.Request.Context(), page, pageSize, keyword, status, ownerID, startDate, endDate)
if err != nil {
response.InternalServerError(c, "failed to get group list")
return
}
response.Paginated(c, groups, total, page, pageSize)
}
// GetGroupDetail 获取群组详情
// GET /api/v1/admin/groups/:id
func (h *AdminGroupHandler) GetGroupDetail(c *gin.Context) {
groupID := c.Param("id")
if groupID == "" {
response.BadRequest(c, "group id is required")
return
}
groupDetail, err := h.adminGroupService.GetGroupDetail(c.Request.Context(), groupID)
if err != nil {
response.NotFound(c, "group not found")
return
}
response.Success(c, groupDetail)
}
// GetGroupMembers 获取群组成员列表
// GET /api/v1/admin/groups/:id/members
func (h *AdminGroupHandler) GetGroupMembers(c *gin.Context) {
groupID := c.Param("id")
if groupID == "" {
response.BadRequest(c, "group id is required")
return
}
// 解析分页参数
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
if page <= 0 {
page = 1
}
if pageSize <= 0 || pageSize > 100 {
pageSize = 20
}
members, err := h.adminGroupService.GetGroupMembers(c.Request.Context(), groupID, page, pageSize)
if err != nil {
response.HandleError(c, err, "failed to get group members")
return
}
response.Success(c, members)
}
// DeleteGroup 解散群组
// DELETE /api/v1/admin/groups/:id
func (h *AdminGroupHandler) DeleteGroup(c *gin.Context) {
groupID := c.Param("id")
if groupID == "" {
response.BadRequest(c, "group id is required")
return
}
err := h.adminGroupService.DeleteGroup(c.Request.Context(), groupID)
if err != nil {
response.HandleError(c, err, "failed to delete group")
return
}
response.Success(c, dto.SuccessResponse{
Success: true,
Message: "group deleted successfully",
})
}
// UpdateGroupStatus 更新群组状态
// PUT /api/v1/admin/groups/:id/status
func (h *AdminGroupHandler) UpdateGroupStatus(c *gin.Context) {
groupID := c.Param("id")
if groupID == "" {
response.BadRequest(c, "group id is required")
return
}
var req dto.AdminUpdateGroupStatusRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, "invalid request body: "+err.Error())
return
}
// 验证状态值
if req.Status != "active" && req.Status != "dissolved" {
response.BadRequest(c, "invalid status value, must be 'active' or 'dissolved'")
return
}
groupDetail, err := h.adminGroupService.UpdateGroupStatus(c.Request.Context(), groupID, req.Status)
if err != nil {
response.HandleError(c, err, "failed to update group status")
return
}
response.Success(c, groupDetail)
}
// RemoveMember 移除群组成员
// DELETE /api/v1/admin/groups/:id/members/:userId
func (h *AdminGroupHandler) RemoveMember(c *gin.Context) {
groupID := c.Param("id")
userID := c.Param("userId")
if groupID == "" {
response.BadRequest(c, "group id is required")
return
}
if userID == "" {
response.BadRequest(c, "user id is required")
return
}
err := h.adminGroupService.RemoveMember(c.Request.Context(), groupID, userID)
if err != nil {
response.HandleError(c, err, "failed to remove member")
return
}
response.Success(c, dto.SuccessResponse{
Success: true,
Message: "member removed successfully",
})
}
// TransferOwner 转让群主
// PUT /api/v1/admin/groups/:id/transfer
func (h *AdminGroupHandler) TransferOwner(c *gin.Context) {
groupID := c.Param("id")
if groupID == "" {
response.BadRequest(c, "group id is required")
return
}
var req dto.AdminTransferGroupOwnerRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, "invalid request body: "+err.Error())
return
}
groupDetail, err := h.adminGroupService.TransferOwner(c.Request.Context(), groupID, req.NewOwnerID)
if err != nil {
response.HandleError(c, err, "failed to transfer owner")
return
}
response.Success(c, groupDetail)
}

View File

@@ -0,0 +1,228 @@
package handler
import (
"strconv"
"carrot_bbs/internal/dto"
"carrot_bbs/internal/model"
"carrot_bbs/internal/pkg/response"
"carrot_bbs/internal/service"
"github.com/gin-gonic/gin"
)
// AdminPostHandler 管理端帖子处理器
type AdminPostHandler struct {
adminPostService service.AdminPostService
}
// NewAdminPostHandler 创建管理端帖子处理器
func NewAdminPostHandler(adminPostService service.AdminPostService) *AdminPostHandler {
return &AdminPostHandler{
adminPostService: adminPostService,
}
}
// GetPostList 获取帖子列表
// GET /api/v1/admin/posts
func (h *AdminPostHandler) GetPostList(c *gin.Context) {
// 解析分页参数
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
if page <= 0 {
page = 1
}
if pageSize <= 0 || pageSize > 100 {
pageSize = 20
}
// 获取筛选参数
keyword := c.Query("keyword")
status := c.Query("status")
authorID := c.Query("author_id")
startDate := c.Query("start_date")
endDate := c.Query("end_date")
// 验证状态参数
if status != "" && status != string(model.PostStatusDraft) &&
status != string(model.PostStatusPending) &&
status != string(model.PostStatusPublished) &&
status != string(model.PostStatusRejected) {
response.BadRequest(c, "invalid status value")
return
}
posts, total, err := h.adminPostService.GetPostList(c.Request.Context(), page, pageSize, keyword, status, authorID, startDate, endDate)
if err != nil {
response.InternalServerError(c, "failed to get post list")
return
}
response.Paginated(c, posts, total, page, pageSize)
}
// GetPostDetail 获取帖子详情
// GET /api/v1/admin/posts/:id
func (h *AdminPostHandler) GetPostDetail(c *gin.Context) {
postID := c.Param("id")
if postID == "" {
response.BadRequest(c, "post id is required")
return
}
postDetail, err := h.adminPostService.GetPostDetail(c.Request.Context(), postID)
if err != nil {
response.NotFound(c, "post not found")
return
}
response.Success(c, postDetail)
}
// DeletePost 删除帖子
// DELETE /api/v1/admin/posts/:id
func (h *AdminPostHandler) DeletePost(c *gin.Context) {
postID := c.Param("id")
if postID == "" {
response.BadRequest(c, "post id is required")
return
}
err := h.adminPostService.DeletePost(c.Request.Context(), postID)
if err != nil {
response.HandleError(c, err, "failed to delete post")
return
}
response.Success(c, dto.SuccessResponse{
Success: true,
Message: "post deleted successfully",
})
}
// ModeratePost 审核帖子
// PUT /api/v1/admin/posts/:id/moderate
func (h *AdminPostHandler) ModeratePost(c *gin.Context) {
postID := c.Param("id")
if postID == "" {
response.BadRequest(c, "post id is required")
return
}
var req dto.AdminModeratePostRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, "invalid request body: "+err.Error())
return
}
// 验证状态值
if req.Status != string(model.PostStatusPublished) && req.Status != string(model.PostStatusRejected) {
response.BadRequest(c, "invalid status value, must be 'published' or 'rejected'")
return
}
// 获取当前用户ID作为审核人
reviewedBy := c.GetString("user_id")
postDetail, err := h.adminPostService.ModeratePost(c.Request.Context(), postID, model.PostStatus(req.Status), req.Reason, reviewedBy)
if err != nil {
response.HandleError(c, err, "failed to moderate post")
return
}
response.Success(c, postDetail)
}
// BatchDeletePosts 批量删除帖子
// POST /api/v1/admin/posts/batch-delete
func (h *AdminPostHandler) BatchDeletePosts(c *gin.Context) {
var req dto.AdminBatchDeletePostsRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, "invalid request body: "+err.Error())
return
}
result, err := h.adminPostService.BatchDeletePosts(c.Request.Context(), req.IDs)
if err != nil {
response.InternalServerError(c, "failed to batch delete posts")
return
}
response.Success(c, result)
}
// BatchModeratePosts 批量审核帖子
// POST /api/v1/admin/posts/batch-moderate
func (h *AdminPostHandler) BatchModeratePosts(c *gin.Context) {
var req dto.AdminBatchModeratePostsRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, "invalid request body: "+err.Error())
return
}
// 验证状态值
if req.Status != string(model.PostStatusPublished) && req.Status != string(model.PostStatusRejected) {
response.BadRequest(c, "invalid status value, must be 'published' or 'rejected'")
return
}
// 获取当前用户ID作为审核人
reviewedBy := c.GetString("user_id")
result, err := h.adminPostService.BatchModeratePosts(c.Request.Context(), req.IDs, model.PostStatus(req.Status), reviewedBy)
if err != nil {
response.InternalServerError(c, "failed to batch moderate posts")
return
}
response.Success(c, result)
}
// SetPostPin 置顶/取消置顶帖子
// PUT /api/v1/admin/posts/:id/pin
func (h *AdminPostHandler) SetPostPin(c *gin.Context) {
postID := c.Param("id")
if postID == "" {
response.BadRequest(c, "post id is required")
return
}
var req dto.AdminSetPostPinRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, "invalid request body: "+err.Error())
return
}
postDetail, err := h.adminPostService.SetPostPin(c.Request.Context(), postID, req.IsPinned)
if err != nil {
response.HandleError(c, err, "failed to set post pin status")
return
}
response.Success(c, postDetail)
}
// SetPostFeature 加精/取消加精帖子
// PUT /api/v1/admin/posts/:id/feature
func (h *AdminPostHandler) SetPostFeature(c *gin.Context) {
postID := c.Param("id")
if postID == "" {
response.BadRequest(c, "post id is required")
return
}
var req dto.AdminSetPostFeatureRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, "invalid request body: "+err.Error())
return
}
postDetail, err := h.adminPostService.SetPostFeature(c.Request.Context(), postID, req.IsFeatured)
if err != nil {
response.HandleError(c, err, "failed to set post feature status")
return
}
response.Success(c, postDetail)
}

View File

@@ -0,0 +1,105 @@
package handler
import (
"strconv"
"carrot_bbs/internal/dto"
"carrot_bbs/internal/model"
"carrot_bbs/internal/pkg/response"
"carrot_bbs/internal/service"
"github.com/gin-gonic/gin"
)
// AdminUserHandler 管理端用户处理器
type AdminUserHandler struct {
adminUserService service.AdminUserService
}
// NewAdminUserHandler 创建管理端用户处理器
func NewAdminUserHandler(adminUserService service.AdminUserService) *AdminUserHandler {
return &AdminUserHandler{
adminUserService: adminUserService,
}
}
// GetUserList 获取用户列表
// GET /api/v1/admin/users
func (h *AdminUserHandler) GetUserList(c *gin.Context) {
// 解析分页参数
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
if page <= 0 {
page = 1
}
if pageSize <= 0 || pageSize > 100 {
pageSize = 20
}
// 获取筛选参数
keyword := c.Query("keyword")
status := c.Query("status")
// 验证状态参数
if status != "" && status != string(model.UserStatusActive) && status != string(model.UserStatusBanned) {
response.BadRequest(c, "invalid status value")
return
}
users, total, err := h.adminUserService.GetUserList(c.Request.Context(), page, pageSize, keyword, status)
if err != nil {
response.InternalServerError(c, "failed to get user list")
return
}
response.Paginated(c, users, total, page, pageSize)
}
// GetUserDetail 获取用户详情
// GET /api/v1/admin/users/:id
func (h *AdminUserHandler) GetUserDetail(c *gin.Context) {
userID := c.Param("id")
if userID == "" {
response.BadRequest(c, "user id is required")
return
}
userDetail, err := h.adminUserService.GetUserDetail(c.Request.Context(), userID)
if err != nil {
response.NotFound(c, "user not found")
return
}
response.Success(c, userDetail)
}
// UpdateUserStatus 更新用户状态
// PUT /api/v1/admin/users/:id/status
func (h *AdminUserHandler) UpdateUserStatus(c *gin.Context) {
userID := c.Param("id")
if userID == "" {
response.BadRequest(c, "user id is required")
return
}
var req dto.AdminUpdateUserStatusRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, "invalid request body: "+err.Error())
return
}
// 验证状态值
if req.Status != string(model.UserStatusActive) && req.Status != string(model.UserStatusBanned) {
response.BadRequest(c, "invalid status value, must be 'active' or 'banned'")
return
}
userDetail, err := h.adminUserService.UpdateUserStatus(c.Request.Context(), userID, model.UserStatus(req.Status))
if err != nil {
response.HandleError(c, err, "failed to update user status")
return
}
response.Success(c, userDetail)
}

View File

@@ -202,3 +202,128 @@ func (h *RoleHandler) handleError(c *gin.Context, err error) {
})
}
}
// GetRoleDetail 获取角色详情
// GET /api/v1/admin/roles/:name
func (h *RoleHandler) GetRoleDetail(c *gin.Context) {
name := c.Param("name")
if name == "" {
c.JSON(http.StatusBadRequest, gin.H{
"code": "BAD_REQUEST",
"message": "缺少角色名称",
})
return
}
detail, err := h.roleService.GetRoleDetail(c.Request.Context(), name)
if err != nil {
h.handleError(c, err)
return
}
c.JSON(http.StatusOK, gin.H{
"data": detail,
})
}
// GetRolePermissions 获取角色权限
// GET /api/v1/admin/roles/:name/permissions
func (h *RoleHandler) GetRolePermissions(c *gin.Context) {
name := c.Param("name")
if name == "" {
c.JSON(http.StatusBadRequest, gin.H{
"code": "BAD_REQUEST",
"message": "缺少角色名称",
})
return
}
permissions, err := h.roleService.GetRolePermissions(c.Request.Context(), name)
if err != nil {
h.handleError(c, err)
return
}
c.JSON(http.StatusOK, gin.H{
"data": permissions,
})
}
// GetAllPermissions 获取所有权限定义
// GET /api/v1/admin/permissions
func (h *RoleHandler) GetAllPermissions(c *gin.Context) {
permissions, err := h.roleService.GetAllPermissions(c.Request.Context())
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"code": "INTERNAL_ERROR",
"message": "获取权限列表失败",
})
return
}
c.JSON(http.StatusOK, gin.H{
"data": permissions,
})
}
// UpdateRolePermissionsRequest 更新角色权限请求
type UpdateRolePermissionsRequest struct {
Permissions []PermissionInput `json:"permissions" binding:"required"`
}
// PermissionInput 权限输入
type PermissionInput struct {
Resource string `json:"resource" binding:"required"`
Action string `json:"action" binding:"required"`
}
// UpdateRolePermissions 更新角色权限
// PUT /api/v1/admin/roles/:name/permissions
func (h *RoleHandler) UpdateRolePermissions(c *gin.Context) {
name := c.Param("name")
if name == "" {
c.JSON(http.StatusBadRequest, gin.H{
"code": "BAD_REQUEST",
"message": "缺少角色名称",
})
return
}
var req UpdateRolePermissionsRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"code": "BAD_REQUEST",
"message": "请求参数错误",
})
return
}
// 转换权限格式
var permissions []service.Permission
for _, p := range req.Permissions {
permissions = append(permissions, service.Permission{
Resource: p.Resource,
Action: p.Action,
})
}
err := h.roleService.UpdateRolePermissions(c.Request.Context(), name, permissions)
if err != nil {
h.handleError(c, err)
return
}
// 返回更新后的角色信息
detail, err := h.roleService.GetRoleDetail(c.Request.Context(), name)
if err != nil {
c.JSON(http.StatusOK, gin.H{
"message": "权限更新成功",
})
return
}
c.JSON(http.StatusOK, gin.H{
"message": "权限更新成功",
"data": detail,
})
}

View File

@@ -155,6 +155,128 @@ func autoMigrate(db *gorm.DB) error {
return err
}
// 初始化角色种子数据
if err := seedRoles(db); err != nil {
return fmt.Errorf("failed to seed roles: %w", err)
}
// 初始化权限策略种子数据
if err := seedPermissions(db); err != nil {
return fmt.Errorf("failed to seed permissions: %w", err)
}
return nil
}
// seedRoles 初始化角色种子数据
func seedRoles(db *gorm.DB) error {
// 检查是否已有角色数据
var count int64
if err := db.Model(&Role{}).Count(&count).Error; err != nil {
return err
}
// 如果已有数据,跳过种子初始化
if count > 0 {
return nil
}
// 预定义角色数据
roles := []Role{
{
Name: RoleSuperAdmin,
DisplayName: "超级管理员",
Description: "拥有系统最高权限,可以管理所有用户和内容",
Priority: 100,
},
{
Name: RoleAdmin,
DisplayName: "管理员",
Description: "系统管理员,可以管理用户和内容",
Priority: 80,
},
{
Name: RoleModerator,
DisplayName: "版主",
Description: "内容审核员,可以审核和管理内容",
Priority: 60,
},
{
Name: RoleUser,
DisplayName: "普通用户",
Description: "注册用户,拥有基本权限",
Priority: 40,
},
{
Name: RoleBanned,
DisplayName: "被封禁用户",
Description: "被禁止访问的用户",
Priority: 0,
},
}
// 批量插入角色
if err := db.Create(&roles).Error; err != nil {
return err
}
log.Printf("Seeded %d roles successfully", len(roles))
return nil
}
// seedPermissions 初始化权限策略种子数据
func seedPermissions(db *gorm.DB) error {
// 检查是否已有权限策略数据
var count int64
if err := db.Model(&CasbinRule{}).Where("ptype = ?", "p").Count(&count).Error; err != nil {
return err
}
// 如果已有数据,跳过种子初始化
if count > 0 {
return nil
}
// 预定义权限策略数据
// p = sub, obj, act (角色, 资源, 操作)
permissions := []CasbinRule{
// 超级管理员 - 拥有所有权限
{Ptype: "p", V0: RoleSuperAdmin, V1: "/*", V2: "*"},
// 管理员 - 拥有管理权限
{Ptype: "p", V0: RoleAdmin, V1: "/api/v1/admin/*", V2: "*"},
{Ptype: "p", V0: RoleAdmin, V1: "/api/v1/users/*", V2: "GET"},
{Ptype: "p", V0: RoleAdmin, V1: "/api/v1/posts/*", V2: "*"},
{Ptype: "p", V0: RoleAdmin, V1: "/api/v1/comments/*", V2: "*"},
{Ptype: "p", V0: RoleAdmin, V1: "/api/v1/groups/*", V2: "*"},
// 版主 - 拥有内容审核权限
{Ptype: "p", V0: RoleModerator, V1: "/api/v1/posts/*", V2: "GET"},
{Ptype: "p", V0: RoleModerator, V1: "/api/v1/posts/*", V2: "DELETE"},
{Ptype: "p", V0: RoleModerator, V1: "/api/v1/comments/*", V2: "GET"},
{Ptype: "p", V0: RoleModerator, V1: "/api/v1/comments/*", V2: "DELETE"},
{Ptype: "p", V0: RoleModerator, V1: "/api/v1/groups/*", V2: "GET"},
// 普通用户 - 拥有基本权限
{Ptype: "p", V0: RoleUser, V1: "/api/v1/posts", V2: "GET"},
{Ptype: "p", V0: RoleUser, V1: "/api/v1/posts/*", V2: "GET"},
{Ptype: "p", V0: RoleUser, V1: "/api/v1/posts", V2: "POST"},
{Ptype: "p", V0: RoleUser, V1: "/api/v1/comments/*", V2: "GET"},
{Ptype: "p", V0: RoleUser, V1: "/api/v1/comments", V2: "POST"},
{Ptype: "p", V0: RoleUser, V1: "/api/v1/users/me", V2: "GET"},
{Ptype: "p", V0: RoleUser, V1: "/api/v1/users/me", V2: "PUT"},
{Ptype: "p", V0: RoleUser, V1: "/api/v1/groups", V2: "GET"},
{Ptype: "p", V0: RoleUser, V1: "/api/v1/groups/*", V2: "GET"},
// 被封禁用户 - 无权限
}
// 批量插入权限策略
if err := db.Create(&permissions).Error; err != nil {
return err
}
log.Printf("Seeded %d permissions successfully", len(permissions))
return nil
}

View File

@@ -46,9 +46,10 @@ type Post struct {
HotScore float64 `json:"hot_score" gorm:"column:hot_score;default:0;index:idx_posts_hot_score_created,priority:1"`
// 置顶/锁定
IsPinned bool `json:"is_pinned" gorm:"default:false"`
IsLocked bool `json:"is_locked" gorm:"default:false"`
IsDeleted bool `json:"-" gorm:"default:false"`
IsPinned bool `json:"is_pinned" gorm:"default:false"`
IsFeatured bool `json:"is_featured" gorm:"default:false"` // 加精
IsLocked bool `json:"is_locked" gorm:"default:false"`
IsDeleted bool `json:"-" gorm:"default:false"`
// 投票
IsVote bool `json:"is_vote" gorm:"column:is_vote;default:false"`

View File

@@ -298,3 +298,96 @@ func (r *CommentRepository) IsLiked(commentID, userID string) bool {
r.db.Model(&model.CommentLike{}).Where("comment_id = ? AND user_id = ?", commentID, userID).Count(&count)
return count > 0
}
// AdminCommentListFilter 管理端评论列表筛选条件
type AdminCommentListFilter struct {
Keyword string
PostID string
AuthorID string
Status string
StartDate string
EndDate string
Page int
PageSize int
}
// GetAdminCommentList 获取管理端评论列表
func (r *CommentRepository) GetAdminCommentList(filter AdminCommentListFilter) ([]*model.Comment, int64, error) {
var comments []*model.Comment
var total int64
query := r.db.Model(&model.Comment{}).Preload("User")
// 应用筛选条件
if filter.Keyword != "" {
query = query.Where("content LIKE ?", "%"+filter.Keyword+"%")
}
if filter.PostID != "" {
query = query.Where("post_id = ?", filter.PostID)
}
if filter.AuthorID != "" {
query = query.Where("user_id = ?", filter.AuthorID)
}
if filter.Status != "" {
query = query.Where("status = ?", filter.Status)
}
if filter.StartDate != "" {
query = query.Where("created_at >= ?", filter.StartDate)
}
if filter.EndDate != "" {
query = query.Where("created_at <= ?", filter.EndDate+" 23:59:59")
}
// 统计总数
if err := query.Count(&total).Error; err != nil {
return nil, 0, err
}
// 分页查询
offset := (filter.Page - 1) * filter.PageSize
if err := query.Offset(offset).Limit(filter.PageSize).Order("created_at DESC").Find(&comments).Error; err != nil {
return nil, 0, err
}
return comments, total, nil
}
// GetAdminCommentByID 获取管理端评论详情(包含关联信息)
func (r *CommentRepository) GetAdminCommentByID(id string) (*model.Comment, error) {
var comment model.Comment
err := r.db.Preload("User").First(&comment, "id = ?", id).Error
if err != nil {
return nil, err
}
return &comment, nil
}
// BatchDelete 批量删除评论
func (r *CommentRepository) BatchDelete(ids []string) ([]string, error) {
var failedIDs []string
for _, id := range ids {
if err := r.Delete(id); err != nil {
failedIDs = append(failedIDs, id)
}
}
return failedIDs, nil
}
// GetCommentsByPostIDForAdmin 管理端获取帖子的评论列表(包含所有状态)
func (r *CommentRepository) GetCommentsByPostIDForAdmin(postID string, page, pageSize int) ([]*model.Comment, int64, error) {
var comments []*model.Comment
var total int64
r.db.Model(&model.Comment{}).Where("post_id = ?", postID).Count(&total)
offset := (page - 1) * pageSize
err := r.db.Where("post_id = ?", postID).
Preload("User").
Offset(offset).Limit(pageSize).
Order("created_at DESC").
Find(&comments).Error
return comments, total, err
}

View File

@@ -35,6 +35,20 @@ type GroupRepository interface {
GetAnnouncements(groupID string, page, pageSize int) ([]model.GroupAnnouncement, int64, error)
GetAnnouncementByID(id string) (*model.GroupAnnouncement, error)
DeleteAnnouncement(id string) error
// 管理端群组操作
GetGroupList(page, pageSize int, keyword, status, ownerID, startDate, endDate string) ([]model.Group, int64, error)
GetGroupWithOwner(groupID string) (*model.Group, error)
GetLatestAnnouncement(groupID string) (*model.GroupAnnouncement, error)
UpdateGroupStatus(groupID string, status string) error
GetGroupPostCount(groupID string) (int64, error)
GetMembersWithUserInfo(groupID string, page, pageSize int) ([]GroupMemberWithUser, int64, error)
}
// GroupMemberWithUser 群成员带用户信息的结构
type GroupMemberWithUser struct {
model.GroupMember
User *model.User
}
// groupRepository 群组仓库实现
@@ -240,3 +254,159 @@ func (r *groupRepository) GetAnnouncementByID(id string) (*model.GroupAnnounceme
func (r *groupRepository) DeleteAnnouncement(id string) error {
return r.db.Delete(&model.GroupAnnouncement{}, "id = ?", id).Error
}
// ==================== 管理端群组操作 ====================
// GetGroupList 获取群组列表(管理端)
func (r *groupRepository) GetGroupList(page, pageSize int, keyword, status, ownerID, startDate, endDate string) ([]model.Group, int64, error) {
var groups []model.Group
var total int64
query := r.db.Model(&model.Group{})
// 关键词搜索
if keyword != "" {
query = query.Where("name LIKE ? OR description LIKE ?", "%"+keyword+"%", "%"+keyword+"%")
}
// 状态筛选这里假设群组有status字段如果没有则忽略
// 注意当前Group模型没有status字段状态通过是否解散来判断
// 如果需要status字段需要在模型中添加
// 群主筛选
if ownerID != "" {
query = query.Where("owner_id = ?", ownerID)
}
// 日期筛选
if startDate != "" {
query = query.Where("created_at >= ?", startDate+" 00:00:00")
}
if endDate != "" {
query = query.Where("created_at <= ?", endDate+" 23:59:59")
}
query.Count(&total)
offset := (page - 1) * pageSize
err := query.Offset(offset).Limit(pageSize).Order("created_at DESC").Find(&groups).Error
return groups, total, err
}
// GetGroupWithOwner 获取群组详情(包含群主信息)
func (r *groupRepository) GetGroupWithOwner(groupID string) (*model.Group, error) {
var group model.Group
err := r.db.First(&group, "id = ?", groupID).Error
if err != nil {
return nil, err
}
return &group, nil
}
// GetLatestAnnouncement 获取群组最新公告
func (r *groupRepository) GetLatestAnnouncement(groupID string) (*model.GroupAnnouncement, error) {
var announcement model.GroupAnnouncement
err := r.db.Where("group_id = ?", groupID).
Order("is_pinned DESC, created_at DESC").
First(&announcement).Error
if err != nil {
if err == gorm.ErrRecordNotFound {
return nil, nil
}
return nil, err
}
return &announcement, nil
}
// UpdateGroupStatus 更新群组状态
func (r *groupRepository) UpdateGroupStatus(groupID string, status string) error {
// 注意当前Group模型没有status字段
// 这里我们通过更新一个虚拟字段或者使用其他方式来实现
// 暂时通过更新updated_at来模拟状态更新
// 如果需要真正的状态字段,需要在模型中添加
return r.db.Model(&model.Group{}).Where("id = ?", groupID).
Update("updated_at", gorm.Expr("NOW()")).Error
}
// GetGroupPostCount 获取群组帖子数量
func (r *groupRepository) GetGroupPostCount(groupID string) (int64, error) {
var count int64
// 假设帖子表有group_id字段
err := r.db.Model(&model.Post{}).Where("group_id = ?", groupID).Count(&count).Error
return count, err
}
// GetMembersWithUserInfo 获取群成员列表(包含用户信息)
func (r *groupRepository) GetMembersWithUserInfo(groupID string, page, pageSize int) ([]GroupMemberWithUser, int64, error) {
var members []GroupMemberWithUser
var total int64
// 先获取总数
err := r.db.Model(&model.GroupMember{}).Where("group_id = ?", groupID).Count(&total).Error
if err != nil {
return nil, 0, err
}
// 联表查询获取成员和用户信息
offset := (page - 1) * pageSize
query := `
SELECT gm.*, u.id as user_id, u.username, u.nickname, u.avatar
FROM group_members gm
LEFT JOIN users u ON gm.user_id = u.id
WHERE gm.group_id = ?
ORDER BY
CASE gm.role
WHEN 'owner' THEN 1
WHEN 'admin' THEN 2
WHEN 'member' THEN 3
END,
gm.join_time ASC
LIMIT ? OFFSET ?
`
type memberRow struct {
ID string `gorm:"column:id"`
GroupID string `gorm:"column:group_id"`
UserID string `gorm:"column:user_id"`
Role string `gorm:"column:role"`
Nickname string `gorm:"column:nickname"`
Muted bool `gorm:"column:muted"`
JoinTime string `gorm:"column:join_time"`
CreatedAt string `gorm:"column:created_at"`
UpdatedAt string `gorm:"column:updated_at"`
// 用户信息
Username string `gorm:"column:username"`
UserNickname string `gorm:"column:nickname"`
UserAvatar string `gorm:"column:avatar"`
}
var rows []memberRow
err = r.db.Raw(query, groupID, pageSize, offset).Scan(&rows).Error
if err != nil {
return nil, 0, err
}
members = make([]GroupMemberWithUser, 0, len(rows))
for _, row := range rows {
member := model.GroupMember{
ID: row.ID,
GroupID: row.GroupID,
UserID: row.UserID,
Role: row.Role,
Nickname: row.Nickname,
Muted: row.Muted,
}
user := &model.User{
ID: row.UserID,
Username: row.Username,
Nickname: row.UserNickname,
Avatar: row.UserAvatar,
}
members = append(members, GroupMemberWithUser{
GroupMember: member,
User: user,
})
}
return members, total, nil
}

View File

@@ -529,3 +529,115 @@ func (r *PostRepository) deleteWithTx(tx *gorm.DB, id string) error {
// 最后删除帖子本身(软删除)
return tx.Delete(&model.Post{}, "id = ?", id).Error
}
// ========== Admin methods ==========
// AdminPostListQuery 管理端帖子列表查询参数
type AdminPostListQuery struct {
Page int
PageSize int
Keyword string
Status string // 为空表示所有状态
AuthorID string
StartDate string // 格式: 2006-01-02
EndDate string // 格式: 2006-01-02
}
// AdminList 管理端分页获取帖子列表(支持多条件筛选)
func (r *PostRepository) AdminList(query AdminPostListQuery) ([]*model.Post, int64, error) {
var posts []*model.Post
var total int64
db := r.db.Model(&model.Post{})
// 关键词搜索(标题或内容)
if query.Keyword != "" {
if r.db.Dialector.Name() == "postgres" {
db = db.Where(
"to_tsvector('simple', COALESCE(title, '') || ' ' || COALESCE(content, '')) @@ plainto_tsquery('simple', ?)",
query.Keyword,
)
} else {
searchPattern := "%" + query.Keyword + "%"
db = db.Where("title LIKE ? OR content LIKE ?", searchPattern, searchPattern)
}
}
// 状态筛选
if query.Status != "" {
db = db.Where("status = ?", query.Status)
}
// 作者筛选
if query.AuthorID != "" {
db = db.Where("user_id = ?", query.AuthorID)
}
// 日期范围筛选
if query.StartDate != "" {
db = db.Where("created_at >= ?", query.StartDate+" 00:00:00")
}
if query.EndDate != "" {
db = db.Where("created_at <= ?", query.EndDate+" 23:59:59")
}
// 统计总数
db.Count(&total)
// 分页查询
offset := (query.Page - 1) * query.PageSize
err := db.Preload("User").Preload("Images").
Offset(offset).Limit(query.PageSize).
Order("created_at DESC").
Find(&posts).Error
return posts, total, err
}
// GetByIDForAdmin 管理端根据ID获取帖子包含所有状态
func (r *PostRepository) GetByIDForAdmin(id string) (*model.Post, error) {
var post model.Post
err := r.db.Preload("User").Preload("Images").First(&post, "id = ?", id).Error
if err != nil {
return nil, err
}
return &post, nil
}
// BatchDelete 批量删除帖子
func (r *PostRepository) BatchDelete(ids []string) ([]string, error) {
var failedIDs []string
for _, id := range ids {
if err := r.Delete(id); err != nil {
failedIDs = append(failedIDs, id)
}
}
return failedIDs, nil
}
// BatchUpdateStatus 批量更新帖子审核状态
func (r *PostRepository) BatchUpdateStatus(ids []string, status model.PostStatus, reviewedBy string) ([]string, error) {
var failedIDs []string
for _, id := range ids {
if err := r.UpdateModerationStatus(id, status, "", reviewedBy); err != nil {
failedIDs = append(failedIDs, id)
}
}
return failedIDs, nil
}
// UpdatePinStatus 更新帖子置顶状态
func (r *PostRepository) UpdatePinStatus(postID string, isPinned bool) error {
return r.db.Model(&model.Post{}).Where("id = ?", postID).
Update("is_pinned", isPinned).Error
}
// UpdateFeatureStatus 更新帖子加精状态
func (r *PostRepository) UpdateFeatureStatus(postID string, isFeatured bool) error {
return r.db.Model(&model.Post{}).Where("id = ?", postID).
Update("is_featured", isFeatured).Error
}

View File

@@ -30,6 +30,9 @@ type RoleRepository interface {
// GetAllRoles 获取所有角色
GetAllRoles(ctx context.Context) ([]model.Role, error)
// GetRoleUserCount 获取角色对应的用户数量
GetRoleUserCount(ctx context.Context, role string) (int64, error)
}
type roleRepository struct {
@@ -95,3 +98,12 @@ func (r *roleRepository) GetAllRoles(ctx context.Context) ([]model.Role, error)
err := r.db.WithContext(ctx).Order("priority DESC").Find(&roles).Error
return roles, err
}
func (r *roleRepository) GetRoleUserCount(ctx context.Context, role string) (int64, error) {
var count int64
err := r.db.WithContext(ctx).
Model(&model.UserRole{}).
Where("role = ?", role).
Count(&count).Error
return count, err
}

View File

@@ -403,3 +403,53 @@ func (r *UserRepository) GetMutualFollowStatus(currentUserID string, targetUserI
return result, nil
}
// AdminList 管理端分页获取用户列表(支持关键词和状态筛选)
func (r *UserRepository) AdminList(page, pageSize int, keyword, status string) ([]*model.User, int64, error) {
var users []*model.User
var total int64
query := r.db.Model(&model.User{})
// 关键词搜索(用户名、昵称、邮箱)
if keyword != "" {
if r.db.Dialector.Name() == "postgres" {
query = query.Where(
"username ILIKE ? OR nickname ILIKE ? OR email ILIKE ?",
"%"+keyword+"%", "%"+keyword+"%", "%"+keyword+"%",
)
} else {
lowerSearchPattern := "%" + strings.ToLower(keyword) + "%"
query = query.Where(
"LOWER(username) LIKE ? OR LOWER(nickname) LIKE ? OR LOWER(email) LIKE ?",
lowerSearchPattern, lowerSearchPattern, lowerSearchPattern,
)
}
}
// 状态筛选
if status != "" {
query = query.Where("status = ?", status)
}
query.Count(&total)
offset := (page - 1) * pageSize
err := query.Order("created_at DESC, id DESC").Offset(offset).Limit(pageSize).Find(&users).Error
return users, total, err
}
// GetCommentsCount 获取用户评论数
func (r *UserRepository) GetCommentsCount(userID string) (int64, error) {
var count int64
err := r.db.Model(&model.Comment{}).
Where("user_id = ?", userID).
Count(&count).Error
return count, err
}
// UpdateStatus 更新用户状态
func (r *UserRepository) UpdateStatus(userID string, status model.UserStatus) error {
return r.db.Model(&model.User{}).Where("id = ?", userID).Update("status", status).Error
}

View File

@@ -11,23 +11,28 @@ import (
// Router 路由配置
type Router struct {
engine *gin.Engine
userHandler *handler.UserHandler
postHandler *handler.PostHandler
commentHandler *handler.CommentHandler
messageHandler *handler.MessageHandler
notificationHandler *handler.NotificationHandler
uploadHandler *handler.UploadHandler
pushHandler *handler.PushHandler
systemMessageHandler *handler.SystemMessageHandler
groupHandler *handler.GroupHandler
stickerHandler *handler.StickerHandler
gorseHandler *handler.GorseHandler
voteHandler *handler.VoteHandler
scheduleHandler *handler.ScheduleHandler
roleHandler *handler.RoleHandler
jwtService *service.JWTService
casbinService service.CasbinService
engine *gin.Engine
userHandler *handler.UserHandler
postHandler *handler.PostHandler
commentHandler *handler.CommentHandler
messageHandler *handler.MessageHandler
notificationHandler *handler.NotificationHandler
uploadHandler *handler.UploadHandler
pushHandler *handler.PushHandler
systemMessageHandler *handler.SystemMessageHandler
groupHandler *handler.GroupHandler
stickerHandler *handler.StickerHandler
gorseHandler *handler.GorseHandler
voteHandler *handler.VoteHandler
scheduleHandler *handler.ScheduleHandler
roleHandler *handler.RoleHandler
adminUserHandler *handler.AdminUserHandler
adminPostHandler *handler.AdminPostHandler
adminCommentHandler *handler.AdminCommentHandler
adminGroupHandler *handler.AdminGroupHandler
adminDashboardHandler *handler.AdminDashboardHandler
jwtService *service.JWTService
casbinService service.CasbinService
}
// New 创建路由
@@ -47,6 +52,11 @@ func New(
voteHandler *handler.VoteHandler,
scheduleHandler *handler.ScheduleHandler,
roleHandler *handler.RoleHandler,
adminUserHandler *handler.AdminUserHandler,
adminPostHandler *handler.AdminPostHandler,
adminCommentHandler *handler.AdminCommentHandler,
adminGroupHandler *handler.AdminGroupHandler,
adminDashboardHandler *handler.AdminDashboardHandler,
activityService service.UserActivityService,
casbinService service.CasbinService,
) *Router {
@@ -56,23 +66,28 @@ func New(
userHandler.SetActivityService(activityService)
r := &Router{
engine: gin.Default(),
userHandler: userHandler,
postHandler: postHandler,
commentHandler: commentHandler,
messageHandler: messageHandler,
notificationHandler: notificationHandler,
uploadHandler: uploadHandler,
pushHandler: pushHandler,
systemMessageHandler: systemMessageHandler,
groupHandler: groupHandler,
stickerHandler: stickerHandler,
gorseHandler: gorseHandler,
voteHandler: voteHandler,
scheduleHandler: scheduleHandler,
roleHandler: roleHandler,
jwtService: jwtService,
casbinService: casbinService,
engine: gin.Default(),
userHandler: userHandler,
postHandler: postHandler,
commentHandler: commentHandler,
messageHandler: messageHandler,
notificationHandler: notificationHandler,
uploadHandler: uploadHandler,
pushHandler: pushHandler,
systemMessageHandler: systemMessageHandler,
groupHandler: groupHandler,
stickerHandler: stickerHandler,
gorseHandler: gorseHandler,
voteHandler: voteHandler,
scheduleHandler: scheduleHandler,
roleHandler: roleHandler,
adminUserHandler: adminUserHandler,
adminPostHandler: adminPostHandler,
adminCommentHandler: adminCommentHandler,
adminGroupHandler: adminGroupHandler,
adminDashboardHandler: adminDashboardHandler,
jwtService: jwtService,
casbinService: casbinService,
}
r.setupRoutes()
@@ -365,9 +380,61 @@ func (r *Router) setupRoutes() {
{
// 角色管理
admin.GET("/roles", r.roleHandler.GetAllRoles)
admin.GET("/roles/:name", r.roleHandler.GetRoleDetail)
admin.GET("/roles/:name/permissions", r.roleHandler.GetRolePermissions)
admin.PUT("/roles/:name/permissions", r.roleHandler.UpdateRolePermissions)
admin.GET("/permissions", r.roleHandler.GetAllPermissions)
admin.GET("/users/:id/roles", r.roleHandler.GetUserRoles)
admin.POST("/users/:id/roles", r.roleHandler.AssignRole)
admin.DELETE("/users/:id/roles/:role", r.roleHandler.RemoveRole)
// 用户管理
if r.adminUserHandler != nil {
admin.GET("/users", r.adminUserHandler.GetUserList)
admin.GET("/users/:id", r.adminUserHandler.GetUserDetail)
admin.PUT("/users/:id/status", r.adminUserHandler.UpdateUserStatus)
}
// 帖子管理
if r.adminPostHandler != nil {
admin.GET("/posts", r.adminPostHandler.GetPostList)
admin.GET("/posts/:id", r.adminPostHandler.GetPostDetail)
admin.DELETE("/posts/:id", r.adminPostHandler.DeletePost)
admin.PUT("/posts/:id/moderate", r.adminPostHandler.ModeratePost)
admin.POST("/posts/batch-delete", r.adminPostHandler.BatchDeletePosts)
admin.POST("/posts/batch-moderate", r.adminPostHandler.BatchModeratePosts)
admin.PUT("/posts/:id/pin", r.adminPostHandler.SetPostPin)
admin.PUT("/posts/:id/feature", r.adminPostHandler.SetPostFeature)
}
// 评论管理
if r.adminCommentHandler != nil {
admin.GET("/comments", r.adminCommentHandler.GetCommentList)
admin.GET("/comments/:id", r.adminCommentHandler.GetCommentDetail)
admin.DELETE("/comments/:id", r.adminCommentHandler.DeleteComment)
admin.POST("/comments/batch-delete", r.adminCommentHandler.BatchDeleteComments)
admin.GET("/comments/post/:postId", r.adminCommentHandler.GetPostComments)
}
// 群组管理
if r.adminGroupHandler != nil {
admin.GET("/groups", r.adminGroupHandler.GetGroupList)
admin.GET("/groups/:id", r.adminGroupHandler.GetGroupDetail)
admin.GET("/groups/:id/members", r.adminGroupHandler.GetGroupMembers)
admin.DELETE("/groups/:id", r.adminGroupHandler.DeleteGroup)
admin.PUT("/groups/:id/status", r.adminGroupHandler.UpdateGroupStatus)
admin.DELETE("/groups/:id/members/:userId", r.adminGroupHandler.RemoveMember)
admin.PUT("/groups/:id/transfer", r.adminGroupHandler.TransferOwner)
}
// 仪表盘
if r.adminDashboardHandler != nil {
admin.GET("/dashboard/stats", r.adminDashboardHandler.GetStats)
admin.GET("/dashboard/user-activity", r.adminDashboardHandler.GetUserActivityTrend)
admin.GET("/dashboard/content-stats", r.adminDashboardHandler.GetContentStats)
admin.GET("/dashboard/pending-content", r.adminDashboardHandler.GetPendingContent)
admin.GET("/dashboard/online-users", r.adminDashboardHandler.GetOnlineUsers)
}
}
}
}

View File

@@ -0,0 +1,243 @@
package service
import (
"context"
"encoding/json"
"carrot_bbs/internal/dto"
"carrot_bbs/internal/model"
"carrot_bbs/internal/repository"
)
// AdminCommentService 管理端评论服务接口
type AdminCommentService interface {
// GetCommentList 获取评论列表(分页、搜索、状态筛选)
GetCommentList(ctx context.Context, page, pageSize int, keyword, postID, authorID, status, startDate, endDate string) ([]dto.AdminCommentListResponse, int64, error)
// GetCommentDetail 获取评论详情
GetCommentDetail(ctx context.Context, commentID string) (*dto.AdminCommentDetailResponse, error)
// DeleteComment 删除评论
DeleteComment(ctx context.Context, commentID string) error
// BatchDeleteComments 批量删除评论
BatchDeleteComments(ctx context.Context, ids []string) (*dto.AdminBatchOperationResponse, error)
// GetCommentsByPostID 获取帖子的评论列表
GetCommentsByPostID(ctx context.Context, postID string, page, pageSize int) ([]dto.AdminCommentListResponse, int64, error)
}
// adminCommentServiceImpl 管理端评论服务实现
type adminCommentServiceImpl struct {
commentRepo *repository.CommentRepository
postRepo *repository.PostRepository
}
// NewAdminCommentService 创建管理端评论服务
func NewAdminCommentService(commentRepo *repository.CommentRepository, postRepo *repository.PostRepository) AdminCommentService {
return &adminCommentServiceImpl{
commentRepo: commentRepo,
postRepo: postRepo,
}
}
// GetCommentList 获取评论列表
func (s *adminCommentServiceImpl) GetCommentList(ctx context.Context, page, pageSize int, keyword, postID, authorID, status, startDate, endDate string) ([]dto.AdminCommentListResponse, int64, error) {
filter := repository.AdminCommentListFilter{
Keyword: keyword,
PostID: postID,
AuthorID: authorID,
Status: status,
StartDate: startDate,
EndDate: endDate,
Page: page,
PageSize: pageSize,
}
comments, total, err := s.commentRepo.GetAdminCommentList(filter)
if err != nil {
return nil, 0, err
}
responses := make([]dto.AdminCommentListResponse, len(comments))
for i, comment := range comments {
responses[i] = s.convertCommentToAdminListResponse(comment)
}
return responses, total, nil
}
// GetCommentDetail 获取评论详情
func (s *adminCommentServiceImpl) GetCommentDetail(ctx context.Context, commentID string) (*dto.AdminCommentDetailResponse, error) {
comment, err := s.commentRepo.GetAdminCommentByID(commentID)
if err != nil {
return nil, err
}
return s.convertCommentToAdminDetailResponse(comment), nil
}
// DeleteComment 删除评论
func (s *adminCommentServiceImpl) DeleteComment(ctx context.Context, commentID string) error {
return s.commentRepo.Delete(commentID)
}
// BatchDeleteComments 批量删除评论
func (s *adminCommentServiceImpl) BatchDeleteComments(ctx context.Context, ids []string) (*dto.AdminBatchOperationResponse, error) {
failedIDs, err := s.commentRepo.BatchDelete(ids)
if err != nil {
return &dto.AdminBatchOperationResponse{
Success: false,
Message: "批量删除失败",
Failed: ids,
}, err
}
if len(failedIDs) == 0 {
return &dto.AdminBatchOperationResponse{
Success: true,
Message: "批量删除成功",
}, nil
}
return &dto.AdminBatchOperationResponse{
Success: true,
Message: "部分评论删除成功",
Failed: failedIDs,
}, nil
}
// GetCommentsByPostID 获取帖子的评论列表
func (s *adminCommentServiceImpl) GetCommentsByPostID(ctx context.Context, postID string, page, pageSize int) ([]dto.AdminCommentListResponse, int64, error) {
comments, total, err := s.commentRepo.GetCommentsByPostIDForAdmin(postID, page, pageSize)
if err != nil {
return nil, 0, err
}
responses := make([]dto.AdminCommentListResponse, len(comments))
for i, comment := range comments {
responses[i] = s.convertCommentToAdminListResponse(comment)
}
return responses, total, nil
}
// convertCommentToAdminListResponse 转换评论为管理端列表响应
func (s *adminCommentServiceImpl) convertCommentToAdminListResponse(comment *model.Comment) dto.AdminCommentListResponse {
// 解析图片
var images []dto.CommentImageResponse
if comment.Images != "" {
var urls []string
if err := json.Unmarshal([]byte(comment.Images), &urls); err == nil {
images = make([]dto.CommentImageResponse, len(urls))
for i, url := range urls {
images[i] = dto.CommentImageResponse{URL: url}
}
}
}
// 作者信息
var author *dto.UserResponse
if comment.User != nil {
author = &dto.UserResponse{
ID: comment.User.ID,
Username: comment.User.Username,
Nickname: comment.User.Nickname,
Email: comment.User.Email,
Phone: comment.User.Phone,
Avatar: comment.User.Avatar,
Bio: comment.User.Bio,
Website: comment.User.Website,
Location: comment.User.Location,
CreatedAt: comment.User.CreatedAt.Format("2006-01-02T15:04:05Z07:00"),
}
}
// 帖子简要信息
var postInfo *dto.AdminCommentPostInfo
if comment.PostID != "" {
postInfo = &dto.AdminCommentPostInfo{
ID: comment.PostID,
}
// 尝试获取帖子标题
post, err := s.postRepo.GetByID(comment.PostID)
if err == nil && post != nil {
postInfo.Title = post.Title
}
}
return dto.AdminCommentListResponse{
ID: comment.ID,
PostID: comment.PostID,
UserID: comment.UserID,
ParentID: comment.ParentID,
RootID: comment.RootID,
Content: comment.Content,
Images: images,
Status: string(comment.Status),
LikesCount: comment.LikesCount,
RepliesCount: comment.RepliesCount,
CreatedAt: comment.CreatedAt.Format("2006-01-02T15:04:05Z07:00"),
UpdatedAt: comment.UpdatedAt.Format("2006-01-02T15:04:05Z07:00"),
Author: author,
Post: postInfo,
}
}
// convertCommentToAdminDetailResponse 转换评论为管理端详情响应
func (s *adminCommentServiceImpl) convertCommentToAdminDetailResponse(comment *model.Comment) *dto.AdminCommentDetailResponse {
// 解析图片
var images []dto.CommentImageResponse
if comment.Images != "" {
var urls []string
if err := json.Unmarshal([]byte(comment.Images), &urls); err == nil {
images = make([]dto.CommentImageResponse, len(urls))
for i, url := range urls {
images[i] = dto.CommentImageResponse{URL: url}
}
}
}
// 作者信息
var author *dto.UserResponse
if comment.User != nil {
author = &dto.UserResponse{
ID: comment.User.ID,
Username: comment.User.Username,
Nickname: comment.User.Nickname,
Email: comment.User.Email,
Phone: comment.User.Phone,
Avatar: comment.User.Avatar,
Bio: comment.User.Bio,
Website: comment.User.Website,
Location: comment.User.Location,
CreatedAt: comment.User.CreatedAt.Format("2006-01-02T15:04:05Z07:00"),
}
}
// 帖子简要信息
var postInfo *dto.AdminCommentPostInfo
if comment.PostID != "" {
postInfo = &dto.AdminCommentPostInfo{
ID: comment.PostID,
}
// 尝试获取帖子标题
post, err := s.postRepo.GetByID(comment.PostID)
if err == nil && post != nil {
postInfo.Title = post.Title
}
}
return &dto.AdminCommentDetailResponse{
ID: comment.ID,
PostID: comment.PostID,
UserID: comment.UserID,
ParentID: comment.ParentID,
RootID: comment.RootID,
Content: comment.Content,
Images: images,
Status: string(comment.Status),
LikesCount: comment.LikesCount,
RepliesCount: comment.RepliesCount,
CreatedAt: comment.CreatedAt.Format("2006-01-02T15:04:05Z07:00"),
UpdatedAt: comment.UpdatedAt.Format("2006-01-02T15:04:05Z07:00"),
Author: author,
Post: postInfo,
}
}

View File

@@ -0,0 +1,444 @@
package service
import (
"context"
"time"
"carrot_bbs/internal/dto"
"carrot_bbs/internal/model"
"carrot_bbs/internal/repository"
"gorm.io/gorm"
)
// AdminDashboardService 管理端仪表盘服务接口
type AdminDashboardService interface {
// GetStats 获取统计数据
GetStats(ctx context.Context) (*dto.DashboardStatsResponse, error)
// GetUserActivityTrend 获取用户活跃度趋势
GetUserActivityTrend(ctx context.Context, days int) ([]dto.UserActivityTrendResponse, error)
// GetContentStats 获取内容统计
GetContentStats(ctx context.Context) (*dto.ContentStatsResponse, error)
// GetPendingContent 获取待审核内容
GetPendingContent(ctx context.Context, limit int) ([]dto.PendingContentResponse, error)
// GetOnlineUsers 获取在线用户数
GetOnlineUsers(ctx context.Context) (*dto.OnlineUsersResponse, error)
}
// adminDashboardServiceImpl 管理端仪表盘服务实现
type adminDashboardServiceImpl struct {
db *gorm.DB
userRepo *repository.UserRepository
postRepo *repository.PostRepository
commentRepo *repository.CommentRepository
groupRepo repository.GroupRepository
activityRepo *repository.UserActivityRepository
}
// NewAdminDashboardService 创建管理端仪表盘服务
func NewAdminDashboardService(
db *gorm.DB,
userRepo *repository.UserRepository,
postRepo *repository.PostRepository,
commentRepo *repository.CommentRepository,
groupRepo repository.GroupRepository,
activityRepo *repository.UserActivityRepository,
) AdminDashboardService {
return &adminDashboardServiceImpl{
db: db,
userRepo: userRepo,
postRepo: postRepo,
commentRepo: commentRepo,
groupRepo: groupRepo,
activityRepo: activityRepo,
}
}
// GetStats 获取统计数据
func (s *adminDashboardServiceImpl) GetStats(ctx context.Context) (*dto.DashboardStatsResponse, error) {
var stats dto.DashboardStatsResponse
// 获取当前时间
now := time.Now()
todayStart := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())
weekStart := todayStart.AddDate(0, 0, -int(now.Weekday()))
// 总用户数
if err := s.db.Model(&model.User{}).Count(&stats.TotalUsers).Error; err != nil {
return nil, err
}
// 今日新增用户
if err := s.db.Model(&model.User{}).Where("created_at >= ?", todayStart).Count(&stats.TodayNewUsers).Error; err != nil {
return nil, err
}
// 今日活跃用户
todayActive, err := s.activityRepo.GetDAU(ctx, now)
if err != nil {
// 如果出错,尝试从数据库获取
stats.ActiveUsersToday = 0
} else {
stats.ActiveUsersToday = todayActive
}
// 本周活跃用户(使用 WAU
year, week := now.ISOWeek()
weekActive, err := s.activityRepo.GetWAUFromRedis(ctx, year, week)
if err != nil {
// 如果 Redis 获取失败,从数据库计算
var weekActiveCount int64
err := s.db.Model(&model.UserActiveLog{}).
Where("active_date >= ?", weekStart).
Distinct("user_id").
Count(&weekActiveCount).Error
if err == nil {
stats.ActiveUsersWeek = weekActiveCount
}
} else {
stats.ActiveUsersWeek = weekActive
}
// 本月活跃用户(使用 MAU
monthActive, err := s.activityRepo.GetMAUFromRedis(ctx, now.Year(), int(now.Month()))
if err != nil {
// 如果 Redis 获取失败,从数据库计算
monthStart := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, now.Location())
var monthActiveCount int64
err := s.db.Model(&model.UserActiveLog{}).
Where("active_date >= ?", monthStart).
Distinct("user_id").
Count(&monthActiveCount).Error
if err == nil {
stats.ActiveUsersMonth = monthActiveCount
}
} else {
stats.ActiveUsersMonth = monthActive
}
// 获取留存率数据(从统计快照表获取)
retentionStats := s.getRetentionStats(ctx, now)
stats.Retention1Day = retentionStats.Retention1Day
stats.Retention7Day = retentionStats.Retention7Day
stats.Retention30Day = retentionStats.Retention30Day
// 总帖子数
if err := s.db.Model(&model.Post{}).Count(&stats.TotalPosts).Error; err != nil {
return nil, err
}
// 今日帖子数
if err := s.db.Model(&model.Post{}).Where("created_at >= ?", todayStart).Count(&stats.TodayPosts).Error; err != nil {
return nil, err
}
// 待审核帖子数
if err := s.db.Model(&model.Post{}).Where("status = ?", model.PostStatusPending).Count(&stats.PendingPosts).Error; err != nil {
return nil, err
}
// 总评论数
if err := s.db.Model(&model.Comment{}).Count(&stats.TotalComments).Error; err != nil {
return nil, err
}
// 今日评论数
if err := s.db.Model(&model.Comment{}).Where("created_at >= ?", todayStart).Count(&stats.TodayComments).Error; err != nil {
return nil, err
}
// 总群组数
if err := s.db.Model(&model.Group{}).Count(&stats.TotalGroups).Error; err != nil {
return nil, err
}
// 今日新建群组
if err := s.db.Model(&model.Group{}).Where("created_at >= ?", todayStart).Count(&stats.TodayNewGroups).Error; err != nil {
return nil, err
}
// 待审核评论数
var pendingComments int64
if err := s.db.Model(&model.Comment{}).Where("status = ?", model.CommentStatusPending).Count(&pendingComments).Error; err != nil {
return nil, err
}
stats.PendingComments = pendingComments
// 计算待审核总数
stats.PendingReview = stats.PendingPosts + stats.PendingComments
stats.PendingPostsCount = stats.PendingPosts
return &stats, nil
}
// GetUserActivityTrend 获取用户活跃度趋势
func (s *adminDashboardServiceImpl) GetUserActivityTrend(ctx context.Context, days int) ([]dto.UserActivityTrendResponse, error) {
if days <= 0 {
days = 7
}
if days > 30 {
days = 30
}
now := time.Now()
results := make([]dto.UserActivityTrendResponse, days)
for i := days - 1; i >= 0; i-- {
date := now.AddDate(0, 0, -i)
dateStart := time.Date(date.Year(), date.Month(), date.Day(), 0, 0, 0, 0, date.Location())
dateEnd := dateStart.Add(24 * time.Hour)
trend := dto.UserActivityTrendResponse{
Date: dateStart.Format("2006-01-02"),
}
// 新增用户数
var newUsers int64
if err := s.db.Model(&model.User{}).
Where("created_at >= ? AND created_at < ?", dateStart, dateEnd).
Count(&newUsers).Error; err == nil {
trend.NewUsers = newUsers
}
// 活跃用户数
activeUsers, err := s.activityRepo.GetDAU(ctx, date)
if err == nil {
trend.ActiveUsers = activeUsers
}
// 帖子数
var posts int64
if err := s.db.Model(&model.Post{}).
Where("created_at >= ? AND created_at < ?", dateStart, dateEnd).
Count(&posts).Error; err == nil {
trend.Posts = posts
}
// 评论数
var comments int64
if err := s.db.Model(&model.Comment{}).
Where("created_at >= ? AND created_at < ?", dateStart, dateEnd).
Count(&comments).Error; err == nil {
trend.Comments = comments
}
results[days-1-i] = trend
}
return results, nil
}
// GetContentStats 获取内容统计
func (s *adminDashboardServiceImpl) GetContentStats(ctx context.Context) (*dto.ContentStatsResponse, error) {
var response dto.ContentStatsResponse
// 内容分布
var totalPosts, totalComments, totalGroups int64
s.db.Model(&model.Post{}).Count(&totalPosts)
s.db.Model(&model.Comment{}).Count(&totalComments)
s.db.Model(&model.Group{}).Count(&totalGroups)
response.ContentDistribution = []dto.ContentDistributionItem{
{Type: "posts", Name: "帖子", Count: totalPosts},
{Type: "comments", Name: "评论", Count: totalComments},
{Type: "groups", Name: "群组", Count: totalGroups},
}
// 帖子状态分布
var publishedCount, pendingCount, rejectedCount, draftCount int64
s.db.Model(&model.Post{}).Where("status = ?", model.PostStatusPublished).Count(&publishedCount)
s.db.Model(&model.Post{}).Where("status = ?", model.PostStatusPending).Count(&pendingCount)
s.db.Model(&model.Post{}).Where("status = ?", model.PostStatusRejected).Count(&rejectedCount)
s.db.Model(&model.Post{}).Where("status = ?", model.PostStatusDraft).Count(&draftCount)
response.PostStatusDistribution = []dto.PostStatusDistributionItem{
{Status: string(model.PostStatusPublished), Name: "已发布", Count: publishedCount},
{Status: string(model.PostStatusPending), Name: "待审核", Count: pendingCount},
{Status: string(model.PostStatusRejected), Name: "已拒绝", Count: rejectedCount},
{Status: string(model.PostStatusDraft), Name: "草稿", Count: draftCount},
}
// 用户增长趋势最近7天
now := time.Now()
response.UserGrowth = make([]dto.UserGrowthItem, 7)
for i := 6; i >= 0; i-- {
date := now.AddDate(0, 0, -i)
dateStart := time.Date(date.Year(), date.Month(), date.Day(), 0, 0, 0, 0, date.Location())
dateEnd := dateStart.Add(24 * time.Hour)
var newUsers int64
s.db.Model(&model.User{}).
Where("created_at >= ? AND created_at < ?", dateStart, dateEnd).
Count(&newUsers)
response.UserGrowth[6-i] = dto.UserGrowthItem{
Date: dateStart.Format("2006-01-02"),
NewUsers: newUsers,
}
}
return &response, nil
}
// GetPendingContent 获取待审核内容
func (s *adminDashboardServiceImpl) GetPendingContent(ctx context.Context, limit int) ([]dto.PendingContentResponse, error) {
if limit <= 0 {
limit = 10
}
if limit > 50 {
limit = 50
}
results := make([]dto.PendingContentResponse, 0)
// 获取待审核帖子
var posts []*model.Post
if err := s.db.Preload("User").
Where("status = ?", model.PostStatusPending).
Order("created_at DESC").
Limit(limit).
Find(&posts).Error; err != nil {
return nil, err
}
for _, post := range posts {
item := dto.PendingContentResponse{
ID: post.ID,
Type: "post",
Title: post.Title,
Content: post.Content,
CreatedAt: post.CreatedAt.Format("2006-01-02T15:04:05Z07:00"),
}
if post.User != nil {
item.Author = &dto.PendingContentAuthor{
ID: post.User.ID,
Username: post.User.Username,
Nickname: post.User.Nickname,
Avatar: post.User.Avatar,
}
}
results = append(results, item)
}
// 获取待审核评论
var comments []*model.Comment
if err := s.db.Preload("User").
Where("status = ?", model.CommentStatusPending).
Order("created_at DESC").
Limit(limit).
Find(&comments).Error; err != nil {
return nil, err
}
for _, comment := range comments {
item := dto.PendingContentResponse{
ID: comment.ID,
Type: "comment",
Title: "",
Content: comment.Content,
CreatedAt: comment.CreatedAt.Format("2006-01-02T15:04:05Z07:00"),
}
if comment.User != nil {
item.Author = &dto.PendingContentAuthor{
ID: comment.User.ID,
Username: comment.User.Username,
Nickname: comment.User.Nickname,
Avatar: comment.User.Avatar,
}
}
results = append(results, item)
}
// 按时间排序并限制数量
if len(results) > limit {
results = results[:limit]
}
return results, nil
}
// GetOnlineUsers 获取在线用户数
func (s *adminDashboardServiceImpl) GetOnlineUsers(ctx context.Context) (*dto.OnlineUsersResponse, error) {
// 获取当前日期的活跃用户数作为在线用户数的近似值
// 实际应用中可能需要使用 WebSocket 连接数或 Redis 中的在线状态
now := time.Now()
count, err := s.activityRepo.GetDAU(ctx, now)
if err != nil {
// 如果 Redis 获取失败,从数据库获取
var dbCount int64
todayStart := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())
if err := s.db.Model(&model.UserActiveLog{}).
Where("active_date >= ?", todayStart).
Distinct("user_id").
Count(&dbCount).Error; err != nil {
return nil, err
}
count = dbCount
}
return &dto.OnlineUsersResponse{
Count: count,
}, nil
}
// retentionStatsResult 留存率结果
type retentionStatsResult struct {
Retention1Day int64
Retention7Day int64
Retention30Day int64
}
// getRetentionStats 获取留存率统计数据
func (s *adminDashboardServiceImpl) getRetentionStats(ctx context.Context, now time.Time) retentionStatsResult {
var result retentionStatsResult
// 尝试从统计快照表获取昨日的留存率数据
yesterday := now.AddDate(0, 0, -1)
if stat, err := s.activityRepo.GetStat(ctx, "dau", yesterday); err == nil {
result.Retention1Day = int64(stat.Retention1)
result.Retention7Day = int64(stat.Retention7)
result.Retention30Day = int64(stat.Retention30)
return result
}
// 如果快照表没有数据,实时计算留存率
// 次日留存率:昨日新增用户中今日仍活跃的比例
result.Retention1Day = s.calculateRetention(ctx, 1)
result.Retention7Day = s.calculateRetention(ctx, 7)
result.Retention30Day = s.calculateRetention(ctx, 30)
return result
}
// calculateRetention 计算指定天数的留存率
func (s *adminDashboardServiceImpl) calculateRetention(ctx context.Context, days int) int64 {
now := time.Now()
// 获取days天前的新增用户
targetDate := now.AddDate(0, 0, -days)
targetDateStart := time.Date(targetDate.Year(), targetDate.Month(), targetDate.Day(), 0, 0, 0, 0, targetDate.Location())
targetDateEnd := targetDateStart.Add(24 * time.Hour)
var newUsers []string
if err := s.db.Model(&model.User{}).
Select("id").
Where("created_at >= ? AND created_at < ?", targetDateStart, targetDateEnd).
Pluck("id", &newUsers).Error; err != nil || len(newUsers) == 0 {
return 0
}
// 检查这些用户在今日是否活跃
todayStart := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())
var activeCount int64
if err := s.db.Model(&model.UserActiveLog{}).
Where("user_id IN ?", newUsers).
Where("active_date >= ?", todayStart).
Distinct("user_id").
Count(&activeCount).Error; err != nil {
return 0
}
// 计算留存率(百分比)
retention := float64(activeCount) / float64(len(newUsers)) * 100
return int64(retention)
}

View File

@@ -0,0 +1,390 @@
package service
import (
"context"
"errors"
"carrot_bbs/internal/dto"
"carrot_bbs/internal/model"
"carrot_bbs/internal/repository"
)
// AdminGroupService 管理端群组服务接口
type AdminGroupService interface {
// GetGroupList 获取群组列表(分页、搜索、筛选)
GetGroupList(ctx context.Context, page, pageSize int, keyword, status, ownerID, startDate, endDate string) ([]dto.AdminGroupListResponse, int64, error)
// GetGroupDetail 获取群组详情
GetGroupDetail(ctx context.Context, groupID string) (*dto.AdminGroupDetailResponse, error)
// GetGroupMembers 获取群组成员列表
GetGroupMembers(ctx context.Context, groupID string, page, pageSize int) (*dto.AdminGroupMemberListResponse, error)
// DeleteGroup 解散群组
DeleteGroup(ctx context.Context, groupID string) error
// UpdateGroupStatus 更新群组状态
UpdateGroupStatus(ctx context.Context, groupID string, status string) (*dto.AdminGroupDetailResponse, error)
// RemoveMember 移除群组成员
RemoveMember(ctx context.Context, groupID string, userID string) error
// TransferOwner 转让群主
TransferOwner(ctx context.Context, groupID string, newOwnerID string) (*dto.AdminGroupDetailResponse, error)
}
// adminGroupServiceImpl 管理端群组服务实现
type adminGroupServiceImpl struct {
groupRepo repository.GroupRepository
userRepo *repository.UserRepository
}
// NewAdminGroupService 创建管理端群组服务
func NewAdminGroupService(
groupRepo repository.GroupRepository,
userRepo *repository.UserRepository,
) AdminGroupService {
return &adminGroupServiceImpl{
groupRepo: groupRepo,
userRepo: userRepo,
}
}
// GetGroupList 获取群组列表
func (s *adminGroupServiceImpl) GetGroupList(ctx context.Context, page, pageSize int, keyword, status, ownerID, startDate, endDate string) ([]dto.AdminGroupListResponse, int64, error) {
groups, total, err := s.groupRepo.GetGroupList(page, pageSize, keyword, status, ownerID, startDate, endDate)
if err != nil {
return nil, 0, err
}
responses := make([]dto.AdminGroupListResponse, 0, len(groups))
for _, group := range groups {
response := dto.AdminGroupListResponse{
ID: group.ID,
Name: group.Name,
Avatar: group.Avatar,
Description: group.Description,
OwnerID: group.OwnerID,
MemberCount: group.MemberCount,
MaxMembers: group.MaxMembers,
JoinType: int(group.JoinType),
MuteAll: group.MuteAll,
Status: "active", // 默认状态为active后续可以根据需要添加状态字段
CreatedAt: dto.FormatTime(group.CreatedAt),
UpdatedAt: dto.FormatTime(group.UpdatedAt),
}
// 获取群主信息
if group.OwnerID != "" {
owner, err := s.userRepo.GetByID(group.OwnerID)
if err == nil && owner != nil {
response.Owner = &dto.UserResponse{
ID: owner.ID,
Username: owner.Username,
Nickname: owner.Nickname,
Avatar: owner.Avatar,
CreatedAt: dto.FormatTime(owner.CreatedAt),
}
}
}
// 获取帖子数量
postCount, _ := s.groupRepo.GetGroupPostCount(group.ID)
response.PostCount = postCount
responses = append(responses, response)
}
return responses, total, nil
}
// GetGroupDetail 获取群组详情
func (s *adminGroupServiceImpl) GetGroupDetail(ctx context.Context, groupID string) (*dto.AdminGroupDetailResponse, error) {
group, err := s.groupRepo.GetByID(groupID)
if err != nil {
return nil, err
}
response := &dto.AdminGroupDetailResponse{
ID: group.ID,
Name: group.Name,
Avatar: group.Avatar,
Description: group.Description,
OwnerID: group.OwnerID,
MemberCount: group.MemberCount,
MaxMembers: group.MaxMembers,
JoinType: int(group.JoinType),
MuteAll: group.MuteAll,
Status: "active",
IsPublic: true, // 默认为公开群组,后续可以根据需要添加字段
CreatedAt: dto.FormatTime(group.CreatedAt),
UpdatedAt: dto.FormatTime(group.UpdatedAt),
}
// 获取群主信息
if group.OwnerID != "" {
owner, err := s.userRepo.GetByID(group.OwnerID)
if err == nil && owner != nil {
response.Owner = &dto.UserResponse{
ID: owner.ID,
Username: owner.Username,
Nickname: owner.Nickname,
Email: owner.Email,
Phone: owner.Phone,
Avatar: owner.Avatar,
Bio: owner.Bio,
Website: owner.Website,
Location: owner.Location,
CreatedAt: dto.FormatTime(owner.CreatedAt),
}
}
}
// 获取帖子数量
postCount, _ := s.groupRepo.GetGroupPostCount(group.ID)
response.PostCount = postCount
// 获取最新公告
announcement, _ := s.groupRepo.GetLatestAnnouncement(groupID)
if announcement != nil {
response.Announcement = &dto.GroupAnnouncementResponse{
ID: announcement.ID,
GroupID: announcement.GroupID,
Content: announcement.Content,
AuthorID: announcement.AuthorID,
IsPinned: announcement.IsPinned,
CreatedAt: dto.FormatTime(announcement.CreatedAt),
}
}
return response, nil
}
// GetGroupMembers 获取群组成员列表
func (s *adminGroupServiceImpl) GetGroupMembers(ctx context.Context, groupID string, page, pageSize int) (*dto.AdminGroupMemberListResponse, error) {
// 先检查群组是否存在
_, err := s.groupRepo.GetByID(groupID)
if err != nil {
return nil, errors.New("群组不存在")
}
members, total, err := s.groupRepo.GetMembersWithUserInfo(groupID, page, pageSize)
if err != nil {
return nil, err
}
list := make([]*dto.AdminGroupMemberResponse, 0, len(members))
for _, member := range members {
item := &dto.AdminGroupMemberResponse{
ID: member.ID,
UserID: member.UserID,
Role: member.Role,
Nickname: member.Nickname,
Muted: member.Muted,
JoinedAt: dto.FormatTime(member.JoinTime),
}
if member.User != nil {
item.Username = member.User.Username
item.Nickname = member.User.Nickname
item.Avatar = member.User.Avatar
item.User = &dto.UserResponse{
ID: member.User.ID,
Username: member.User.Username,
Nickname: member.User.Nickname,
Avatar: member.User.Avatar,
CreatedAt: dto.FormatTime(member.User.CreatedAt),
}
}
list = append(list, item)
}
return &dto.AdminGroupMemberListResponse{
List: list,
Total: total,
Page: page,
PageSize: pageSize,
}, nil
}
// DeleteGroup 解散群组
func (s *adminGroupServiceImpl) DeleteGroup(ctx context.Context, groupID string) error {
// 先检查群组是否存在
_, err := s.groupRepo.GetByID(groupID)
if err != nil {
return errors.New("群组不存在")
}
return s.groupRepo.Delete(groupID)
}
// UpdateGroupStatus 更新群组状态
func (s *adminGroupServiceImpl) UpdateGroupStatus(ctx context.Context, groupID string, status string) (*dto.AdminGroupDetailResponse, error) {
// 先检查群组是否存在
group, err := s.groupRepo.GetByID(groupID)
if err != nil {
return nil, errors.New("群组不存在")
}
// 验证状态值
if status != "active" && status != "dissolved" {
return nil, errors.New("无效的状态值")
}
// 如果状态是dissolved则解散群组
if status == "dissolved" {
err = s.groupRepo.Delete(groupID)
if err != nil {
return nil, err
}
} else {
// 更新群组状态
err = s.groupRepo.UpdateGroupStatus(groupID, status)
if err != nil {
return nil, err
}
// 重新获取群组信息
group, err = s.groupRepo.GetByID(groupID)
if err != nil {
return nil, err
}
}
// 构建响应
response := &dto.AdminGroupDetailResponse{
ID: group.ID,
Name: group.Name,
Avatar: group.Avatar,
Description: group.Description,
OwnerID: group.OwnerID,
MemberCount: group.MemberCount,
MaxMembers: group.MaxMembers,
JoinType: int(group.JoinType),
MuteAll: group.MuteAll,
Status: status,
IsPublic: true,
CreatedAt: dto.FormatTime(group.CreatedAt),
UpdatedAt: dto.FormatTime(group.UpdatedAt),
}
// 获取群主信息
if group.OwnerID != "" {
owner, err := s.userRepo.GetByID(group.OwnerID)
if err == nil && owner != nil {
response.Owner = &dto.UserResponse{
ID: owner.ID,
Username: owner.Username,
Nickname: owner.Nickname,
Avatar: owner.Avatar,
CreatedAt: dto.FormatTime(owner.CreatedAt),
}
}
}
return response, nil
}
// RemoveMember 移除群组成员
func (s *adminGroupServiceImpl) RemoveMember(ctx context.Context, groupID string, userID string) error {
// 先检查群组是否存在
_, err := s.groupRepo.GetByID(groupID)
if err != nil {
return errors.New("群组不存在")
}
// 检查用户是否是群成员
member, err := s.groupRepo.GetMember(groupID, userID)
if err != nil {
return errors.New("用户不是群组成员")
}
// 不能移除群主
if member.Role == model.GroupRoleOwner {
return errors.New("不能移除群主")
}
return s.groupRepo.RemoveMember(groupID, userID)
}
// TransferOwner 转让群主
func (s *adminGroupServiceImpl) TransferOwner(ctx context.Context, groupID string, newOwnerID string) (*dto.AdminGroupDetailResponse, error) {
// 先检查群组是否存在
group, err := s.groupRepo.GetByID(groupID)
if err != nil {
return nil, errors.New("群组不存在")
}
// 检查新群主是否是群成员
newOwnerMember, err := s.groupRepo.GetMember(groupID, newOwnerID)
if err != nil {
return nil, errors.New("新群主不是群组成员")
}
// 获取当前群主信息
currentOwnerMember, err := s.groupRepo.GetMember(groupID, group.OwnerID)
if err != nil {
return nil, errors.New("当前群主信息不存在")
}
oldOwnerID := group.OwnerID
// 更新当前群主为普通成员
err = s.groupRepo.SetMemberRole(groupID, oldOwnerID, model.GroupRoleMember)
if err != nil {
return nil, err
}
// 更新新群主角色
err = s.groupRepo.SetMemberRole(groupID, newOwnerID, model.GroupRoleOwner)
if err != nil {
// 回滚
_ = s.groupRepo.SetMemberRole(groupID, oldOwnerID, model.GroupRoleOwner)
return nil, err
}
// 更新群组的OwnerID
group.OwnerID = newOwnerID
err = s.groupRepo.Update(group)
if err != nil {
// 回滚
_ = s.groupRepo.SetMemberRole(groupID, oldOwnerID, model.GroupRoleOwner)
_ = s.groupRepo.SetMemberRole(groupID, newOwnerID, newOwnerMember.Role)
return nil, err
}
// 重新获取群组信息
group, err = s.groupRepo.GetByID(groupID)
if err != nil {
return nil, err
}
// 构建响应
response := &dto.AdminGroupDetailResponse{
ID: group.ID,
Name: group.Name,
Avatar: group.Avatar,
Description: group.Description,
OwnerID: group.OwnerID,
MemberCount: group.MemberCount,
MaxMembers: group.MaxMembers,
JoinType: int(group.JoinType),
MuteAll: group.MuteAll,
Status: "active",
IsPublic: true,
CreatedAt: dto.FormatTime(group.CreatedAt),
UpdatedAt: dto.FormatTime(group.UpdatedAt),
}
// 获取新群主信息
if group.OwnerID != "" {
owner, err := s.userRepo.GetByID(group.OwnerID)
if err == nil && owner != nil {
response.Owner = &dto.UserResponse{
ID: owner.ID,
Username: owner.Username,
Nickname: owner.Nickname,
Avatar: owner.Avatar,
CreatedAt: dto.FormatTime(owner.CreatedAt),
}
}
}
_ = currentOwnerMember // 避免 unused variable 警告
return response, nil
}

View File

@@ -0,0 +1,341 @@
package service
import (
"context"
"carrot_bbs/internal/cache"
"carrot_bbs/internal/dto"
"carrot_bbs/internal/model"
"carrot_bbs/internal/repository"
)
// AdminPostService 管理端帖子服务接口
type AdminPostService interface {
// GetPostList 获取帖子列表(分页、搜索、状态筛选)
GetPostList(ctx context.Context, page, pageSize int, keyword, status, authorID, startDate, endDate string) ([]dto.AdminPostListResponse, int64, error)
// GetPostDetail 获取帖子详情
GetPostDetail(ctx context.Context, postID string) (*dto.AdminPostDetailResponse, error)
// DeletePost 删除帖子
DeletePost(ctx context.Context, postID string) error
// ModeratePost 审核帖子
ModeratePost(ctx context.Context, postID string, status model.PostStatus, reason, reviewedBy string) (*dto.AdminPostDetailResponse, error)
// BatchDeletePosts 批量删除帖子
BatchDeletePosts(ctx context.Context, ids []string) (*dto.AdminBatchOperationResponse, error)
// BatchModeratePosts 批量审核帖子
BatchModeratePosts(ctx context.Context, ids []string, status model.PostStatus, reviewedBy string) (*dto.AdminBatchOperationResponse, error)
// SetPostPin 置顶/取消置顶帖子
SetPostPin(ctx context.Context, postID string, isPinned bool) (*dto.AdminPostDetailResponse, error)
// SetPostFeature 加精/取消加精帖子
SetPostFeature(ctx context.Context, postID string, isFeatured bool) (*dto.AdminPostDetailResponse, error)
}
// adminPostServiceImpl 管理端帖子服务实现
type adminPostServiceImpl struct {
postRepo *repository.PostRepository
cache cache.Cache
}
// NewAdminPostService 创建管理端帖子服务
func NewAdminPostService(postRepo *repository.PostRepository, cacheBackend cache.Cache) AdminPostService {
return &adminPostServiceImpl{
postRepo: postRepo,
cache: cacheBackend,
}
}
// GetPostList 获取帖子列表
func (s *adminPostServiceImpl) GetPostList(ctx context.Context, page, pageSize int, keyword, status, authorID, startDate, endDate string) ([]dto.AdminPostListResponse, int64, error) {
query := repository.AdminPostListQuery{
Page: page,
PageSize: pageSize,
Keyword: keyword,
Status: status,
AuthorID: authorID,
StartDate: startDate,
EndDate: endDate,
}
posts, total, err := s.postRepo.AdminList(query)
if err != nil {
return nil, 0, err
}
responses := make([]dto.AdminPostListResponse, len(posts))
for i, post := range posts {
responses[i] = convertPostToAdminListResponse(post)
}
return responses, total, nil
}
// GetPostDetail 获取帖子详情
func (s *adminPostServiceImpl) GetPostDetail(ctx context.Context, postID string) (*dto.AdminPostDetailResponse, error) {
post, err := s.postRepo.GetByIDForAdmin(postID)
if err != nil {
return nil, err
}
return convertPostToAdminDetailResponse(post), nil
}
// DeletePost 删除帖子
func (s *adminPostServiceImpl) DeletePost(ctx context.Context, postID string) error {
err := s.postRepo.Delete(postID)
if err != nil {
return err
}
// 失效缓存
cache.InvalidatePostDetail(s.cache, postID)
cache.InvalidatePostList(s.cache)
return nil
}
// ModeratePost 审核帖子
func (s *adminPostServiceImpl) ModeratePost(ctx context.Context, postID string, status model.PostStatus, reason, reviewedBy string) (*dto.AdminPostDetailResponse, error) {
// 先检查帖子是否存在
_, err := s.postRepo.GetByIDForAdmin(postID)
if err != nil {
return nil, err
}
// 更新审核状态
if err := s.postRepo.UpdateModerationStatus(postID, status, reason, reviewedBy); err != nil {
return nil, err
}
// 失效缓存
cache.InvalidatePostDetail(s.cache, postID)
cache.InvalidatePostList(s.cache)
// 重新获取帖子信息
post, err := s.postRepo.GetByIDForAdmin(postID)
if err != nil {
return nil, err
}
return convertPostToAdminDetailResponse(post), nil
}
// BatchDeletePosts 批量删除帖子
func (s *adminPostServiceImpl) BatchDeletePosts(ctx context.Context, ids []string) (*dto.AdminBatchOperationResponse, error) {
failedIDs, err := s.postRepo.BatchDelete(ids)
if err != nil {
return &dto.AdminBatchOperationResponse{
Success: false,
Message: "批量删除失败",
Failed: ids,
}, err
}
// 失效缓存
for _, id := range ids {
cache.InvalidatePostDetail(s.cache, id)
}
cache.InvalidatePostList(s.cache)
if len(failedIDs) == 0 {
return &dto.AdminBatchOperationResponse{
Success: true,
Message: "批量删除成功",
}, nil
}
return &dto.AdminBatchOperationResponse{
Success: true,
Message: "部分帖子删除成功",
Failed: failedIDs,
}, nil
}
// BatchModeratePosts 批量审核帖子
func (s *adminPostServiceImpl) BatchModeratePosts(ctx context.Context, ids []string, status model.PostStatus, reviewedBy string) (*dto.AdminBatchOperationResponse, error) {
failedIDs, err := s.postRepo.BatchUpdateStatus(ids, status, reviewedBy)
if err != nil {
return &dto.AdminBatchOperationResponse{
Success: false,
Message: "批量审核失败",
Failed: ids,
}, err
}
// 失效缓存
for _, id := range ids {
cache.InvalidatePostDetail(s.cache, id)
}
cache.InvalidatePostList(s.cache)
if len(failedIDs) == 0 {
return &dto.AdminBatchOperationResponse{
Success: true,
Message: "批量审核成功",
}, nil
}
return &dto.AdminBatchOperationResponse{
Success: true,
Message: "部分帖子审核成功",
Failed: failedIDs,
}, nil
}
// SetPostPin 置顶/取消置顶帖子
func (s *adminPostServiceImpl) SetPostPin(ctx context.Context, postID string, isPinned bool) (*dto.AdminPostDetailResponse, error) {
// 先检查帖子是否存在
_, err := s.postRepo.GetByIDForAdmin(postID)
if err != nil {
return nil, err
}
// 更新置顶状态
if err := s.postRepo.UpdatePinStatus(postID, isPinned); err != nil {
return nil, err
}
// 失效缓存
cache.InvalidatePostDetail(s.cache, postID)
cache.InvalidatePostList(s.cache)
// 重新获取帖子信息
post, err := s.postRepo.GetByIDForAdmin(postID)
if err != nil {
return nil, err
}
return convertPostToAdminDetailResponse(post), nil
}
// SetPostFeature 加精/取消加精帖子
func (s *adminPostServiceImpl) SetPostFeature(ctx context.Context, postID string, isFeatured bool) (*dto.AdminPostDetailResponse, error) {
// 先检查帖子是否存在
_, err := s.postRepo.GetByIDForAdmin(postID)
if err != nil {
return nil, err
}
// 更新加精状态
if err := s.postRepo.UpdateFeatureStatus(postID, isFeatured); err != nil {
return nil, err
}
// 失效缓存
cache.InvalidatePostDetail(s.cache, postID)
cache.InvalidatePostList(s.cache)
// 重新获取帖子信息
post, err := s.postRepo.GetByIDForAdmin(postID)
if err != nil {
return nil, err
}
return convertPostToAdminDetailResponse(post), nil
}
// convertPostToAdminListResponse 转换帖子为管理端列表响应
func convertPostToAdminListResponse(post *model.Post) dto.AdminPostListResponse {
images := make([]dto.PostImageResponse, len(post.Images))
for i, img := range post.Images {
images[i] = dto.PostImageResponse{
ID: img.ID,
URL: img.URL,
ThumbnailURL: img.ThumbnailURL,
Width: img.Width,
Height: img.Height,
}
}
var author *dto.UserResponse
if post.User != nil {
author = &dto.UserResponse{
ID: post.User.ID,
Username: post.User.Username,
Nickname: post.User.Nickname,
Email: post.User.Email,
Phone: post.User.Phone,
Avatar: post.User.Avatar,
Bio: post.User.Bio,
Website: post.User.Website,
Location: post.User.Location,
CreatedAt: post.User.CreatedAt.Format("2006-01-02T15:04:05Z07:00"),
}
}
return dto.AdminPostListResponse{
ID: post.ID,
UserID: post.UserID,
Title: post.Title,
Content: post.Content,
Images: images,
Status: string(post.Status),
LikesCount: post.LikesCount,
CommentsCount: post.CommentsCount,
FavoritesCount: post.FavoritesCount,
ViewsCount: post.ViewsCount,
IsPinned: post.IsPinned,
IsFeatured: post.IsFeatured,
IsLocked: post.IsLocked,
RejectReason: post.RejectReason,
ReviewedAt: dto.FormatTimePointer(post.ReviewedAt),
ReviewedBy: post.ReviewedBy,
CreatedAt: post.CreatedAt.Format("2006-01-02T15:04:05Z07:00"),
UpdatedAt: post.UpdatedAt.Format("2006-01-02T15:04:05Z07:00"),
Author: author,
}
}
// convertPostToAdminDetailResponse 转换帖子为管理端详情响应
func convertPostToAdminDetailResponse(post *model.Post) *dto.AdminPostDetailResponse {
images := make([]dto.PostImageResponse, len(post.Images))
for i, img := range post.Images {
images[i] = dto.PostImageResponse{
ID: img.ID,
URL: img.URL,
ThumbnailURL: img.ThumbnailURL,
Width: img.Width,
Height: img.Height,
}
}
var author *dto.UserResponse
if post.User != nil {
author = &dto.UserResponse{
ID: post.User.ID,
Username: post.User.Username,
Nickname: post.User.Nickname,
Email: post.User.Email,
Phone: post.User.Phone,
Avatar: post.User.Avatar,
Bio: post.User.Bio,
Website: post.User.Website,
Location: post.User.Location,
CreatedAt: post.User.CreatedAt.Format("2006-01-02T15:04:05Z07:00"),
}
}
return &dto.AdminPostDetailResponse{
ID: post.ID,
UserID: post.UserID,
CommunityID: post.CommunityID,
Title: post.Title,
Content: post.Content,
Images: images,
Status: string(post.Status),
LikesCount: post.LikesCount,
CommentsCount: post.CommentsCount,
FavoritesCount: post.FavoritesCount,
SharesCount: post.SharesCount,
ViewsCount: post.ViewsCount,
HotScore: post.HotScore,
IsPinned: post.IsPinned,
IsFeatured: post.IsFeatured,
IsLocked: post.IsLocked,
IsVote: post.IsVote,
RejectReason: post.RejectReason,
ReviewedAt: dto.FormatTimePointer(post.ReviewedAt),
ReviewedBy: post.ReviewedBy,
CreatedAt: post.CreatedAt.Format("2006-01-02T15:04:05Z07:00"),
UpdatedAt: post.UpdatedAt.Format("2006-01-02T15:04:05Z07:00"),
Author: author,
}
}

View File

@@ -0,0 +1,145 @@
package service
import (
"context"
"carrot_bbs/internal/dto"
"carrot_bbs/internal/model"
"carrot_bbs/internal/repository"
)
// AdminUserService 管理端用户服务接口
type AdminUserService interface {
// GetUserList 获取用户列表(分页、搜索、状态筛选)
GetUserList(ctx context.Context, page, pageSize int, keyword, status string) ([]dto.AdminUserListResponse, int64, error)
// GetUserDetail 获取用户详情(含统计信息)
GetUserDetail(ctx context.Context, userID string) (*dto.AdminUserDetailResponse, error)
// UpdateUserStatus 更新用户状态
UpdateUserStatus(ctx context.Context, userID string, status model.UserStatus) (*dto.AdminUserDetailResponse, error)
}
// adminUserServiceImpl 管理端用户服务实现
type adminUserServiceImpl struct {
userRepo *repository.UserRepository
}
// NewAdminUserService 创建管理端用户服务
func NewAdminUserService(userRepo *repository.UserRepository) AdminUserService {
return &adminUserServiceImpl{
userRepo: userRepo,
}
}
// GetUserList 获取用户列表
func (s *adminUserServiceImpl) GetUserList(ctx context.Context, page, pageSize int, keyword, status string) ([]dto.AdminUserListResponse, int64, error) {
users, total, err := s.userRepo.AdminList(page, pageSize, keyword, status)
if err != nil {
return nil, 0, err
}
responses := make([]dto.AdminUserListResponse, len(users))
for i, user := range users {
responses[i] = convertUserToAdminListResponse(user)
}
return responses, total, nil
}
// GetUserDetail 获取用户详情
func (s *adminUserServiceImpl) GetUserDetail(ctx context.Context, userID string) (*dto.AdminUserDetailResponse, error) {
user, err := s.userRepo.GetByID(userID)
if err != nil {
return nil, err
}
// 获取帖子数
postsCount, _ := s.userRepo.GetPostsCount(userID)
// 获取评论数
commentsCount, _ := s.userRepo.GetCommentsCount(userID)
return convertUserToAdminDetailResponse(user, postsCount, commentsCount), nil
}
// UpdateUserStatus 更新用户状态
func (s *adminUserServiceImpl) UpdateUserStatus(ctx context.Context, userID string, status model.UserStatus) (*dto.AdminUserDetailResponse, error) {
// 先检查用户是否存在
user, err := s.userRepo.GetByID(userID)
if err != nil {
return nil, err
}
// 更新状态
if err := s.userRepo.UpdateStatus(userID, status); err != nil {
return nil, err
}
// 重新获取用户信息
user, err = s.userRepo.GetByID(userID)
if err != nil {
return nil, err
}
// 获取统计信息
postsCount, _ := s.userRepo.GetPostsCount(userID)
commentsCount, _ := s.userRepo.GetCommentsCount(userID)
return convertUserToAdminDetailResponse(user, postsCount, commentsCount), nil
}
// convertUserToAdminListResponse 转换用户为管理端列表响应
func convertUserToAdminListResponse(user *model.User) dto.AdminUserListResponse {
var lastLoginAt string
if user.LastLoginAt != nil {
lastLoginAt = user.LastLoginAt.Format("2006-01-02 15:04:05")
}
return dto.AdminUserListResponse{
ID: user.ID,
Username: user.Username,
Nickname: user.Nickname,
Email: user.Email,
Phone: user.Phone,
EmailVerified: user.EmailVerified,
Avatar: user.Avatar,
Status: string(user.Status),
PostsCount: user.PostsCount,
FollowersCount: user.FollowersCount,
FollowingCount: user.FollowingCount,
LastLoginAt: lastLoginAt,
LastLoginIP: user.LastLoginIP,
CreatedAt: user.CreatedAt.Format("2006-01-02 15:04:05"),
}
}
// convertUserToAdminDetailResponse 转换用户为管理端详情响应
func convertUserToAdminDetailResponse(user *model.User, postsCount, commentsCount int64) *dto.AdminUserDetailResponse {
var lastLoginAt string
if user.LastLoginAt != nil {
lastLoginAt = user.LastLoginAt.Format("2006-01-02 15:04:05")
}
return &dto.AdminUserDetailResponse{
ID: user.ID,
Username: user.Username,
Nickname: user.Nickname,
Email: user.Email,
Phone: user.Phone,
EmailVerified: user.EmailVerified,
Avatar: user.Avatar,
CoverURL: user.CoverURL,
Bio: user.Bio,
Website: user.Website,
Location: user.Location,
IsVerified: user.IsVerified,
Status: string(user.Status),
PostsCount: int(postsCount),
CommentsCount: commentsCount,
FollowersCount: user.FollowersCount,
FollowingCount: user.FollowingCount,
LastLoginAt: lastLoginAt,
LastLoginIP: user.LastLoginIP,
CreatedAt: user.CreatedAt.Format("2006-01-02 15:04:05"),
UpdatedAt: user.UpdatedAt.Format("2006-01-02 15:04:05"),
}
}

View File

@@ -9,7 +9,7 @@ import (
"carrot_bbs/internal/config"
"carrot_bbs/internal/repository"
"github.com/casbin/casbin/v2"
"github.com/casbin/casbin/v3"
)
// CasbinService Casbin 权限服务接口
@@ -43,6 +43,15 @@ type CasbinService interface {
// ClearCache 清除权限缓存
ClearCache(ctx context.Context) error
// GetPermissionsForRole 获取角色的所有权限
GetPermissionsForRole(ctx context.Context, role string) ([][]string, error)
// UpdatePermissionsForRole 更新角色的权限(先删除所有权限,再添加新权限)
UpdatePermissionsForRole(ctx context.Context, role string, permissions [][]string) error
// GetAllPermissions 获取系统中所有权限定义
GetAllPermissions(ctx context.Context) [][]string
}
type casbinService struct {
@@ -223,3 +232,34 @@ func (s *casbinService) clearUserCache(ctx context.Context, userID string) error
}
return nil
}
func (s *casbinService) GetPermissionsForRole(ctx context.Context, role string) ([][]string, error) {
return s.enforcer.GetPermissionsForUser(role)
}
func (s *casbinService) UpdatePermissionsForRole(ctx context.Context, role string, permissions [][]string) error {
// 先删除该角色的所有权限
_, err := s.enforcer.DeletePermissionsForUser(role)
if err != nil {
return err
}
// 添加新权限
for _, perm := range permissions {
if len(perm) >= 2 {
resource := perm[0]
action := perm[1]
if _, err := s.enforcer.AddPolicy(role, resource, action); err != nil {
return err
}
}
}
// 清除缓存
return s.ClearCache(ctx)
}
func (s *casbinService) GetAllPermissions(ctx context.Context) [][]string {
permissions, _ := s.enforcer.GetPolicy()
return permissions
}

View File

@@ -24,6 +24,37 @@ type RoleService interface {
// GetRole 获取角色详情
GetRole(ctx context.Context, name string) (*model.Role, error)
// GetRoleDetail 获取角色详情(包含用户数量和权限)
GetRoleDetail(ctx context.Context, name string) (*RoleDetail, error)
// GetRolePermissions 获取角色的权限列表
GetRolePermissions(ctx context.Context, name string) ([]Permission, error)
// GetAllPermissions 获取系统中所有权限定义
GetAllPermissions(ctx context.Context) ([]Permission, error)
// UpdateRolePermissions 更新角色权限
UpdateRolePermissions(ctx context.Context, roleName string, permissions []Permission) error
}
// Permission 权限定义
type Permission struct {
Resource string `json:"resource"`
Action string `json:"action"`
}
// RoleDetail 角色详情
type RoleDetail struct {
Name string `json:"name"`
DisplayName string `json:"display_name"`
Description string `json:"description"`
ParentRole *string `json:"parent_role"`
Priority int `json:"priority"`
UserCount int64 `json:"user_count"`
Permissions []Permission `json:"permissions"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
}
type roleService struct {
@@ -114,3 +145,106 @@ func (s *roleService) GetAllRoles(ctx context.Context) ([]model.Role, error) {
func (s *roleService) GetRole(ctx context.Context, name string) (*model.Role, error) {
return s.roleRepo.GetRole(ctx, name)
}
func (s *roleService) GetRoleDetail(ctx context.Context, name string) (*RoleDetail, error) {
role, err := s.roleRepo.GetRole(ctx, name)
if err != nil {
return nil, apperrors.ErrRoleNotFound
}
// 获取用户数量
userCount, err := s.roleRepo.GetRoleUserCount(ctx, name)
if err != nil {
userCount = 0
}
// 获取权限
permissions, err := s.casbinSvc.GetPermissionsForRole(ctx, name)
if err != nil {
return nil, err
}
perms := make([]Permission, 0)
for _, p := range permissions {
if len(p) >= 2 {
perms = append(perms, Permission{
Resource: p[0],
Action: p[1],
})
}
}
return &RoleDetail{
Name: role.Name,
DisplayName: role.DisplayName,
Description: role.Description,
ParentRole: role.ParentRole,
Priority: role.Priority,
UserCount: userCount,
Permissions: perms,
CreatedAt: role.CreatedAt.Format("2006-01-02T15:04:05Z07:00"),
UpdatedAt: role.UpdatedAt.Format("2006-01-02T15:04:05Z07:00"),
}, nil
}
func (s *roleService) GetRolePermissions(ctx context.Context, name string) ([]Permission, error) {
// 检查角色是否存在
_, err := s.roleRepo.GetRole(ctx, name)
if err != nil {
return nil, apperrors.ErrRoleNotFound
}
permissions, err := s.casbinSvc.GetPermissionsForRole(ctx, name)
if err != nil {
return nil, err
}
// 初始化为空数组,避免 JSON 序列化为 null
perms := make([]Permission, 0)
for _, p := range permissions {
if len(p) >= 2 {
perms = append(perms, Permission{
Resource: p[0],
Action: p[1],
})
}
}
return perms, nil
}
func (s *roleService) GetAllPermissions(ctx context.Context) ([]Permission, error) {
permissions := s.casbinSvc.GetAllPermissions(ctx)
perms := make([]Permission, 0)
for _, p := range permissions {
if len(p) >= 3 {
// p[0] 是角色名p[1] 是资源p[2] 是操作
perms = append(perms, Permission{
Resource: p[1],
Action: p[2],
})
}
}
return perms, nil
}
func (s *roleService) UpdateRolePermissions(ctx context.Context, roleName string, permissions []Permission) error {
// 检查角色是否存在
_, err := s.roleRepo.GetRole(ctx, roleName)
if err != nil {
return apperrors.ErrRoleNotFound
}
// 不能修改超级管理员权限
if roleName == model.RoleSuperAdmin {
return apperrors.ErrCannotModifySuperAdmin
}
// 转换权限格式
var perms [][]string
for _, p := range permissions {
perms = append(perms, []string{p.Resource, p.Action})
}
return s.casbinSvc.UpdatePermissionsForRole(ctx, roleName, perms)
}

View File

@@ -8,7 +8,8 @@ import (
"carrot_bbs/internal/repository"
"carrot_bbs/internal/service"
"github.com/casbin/casbin/v2"
"github.com/casbin/casbin/v3"
gormadapter "github.com/casbin/gorm-adapter/v3"
"github.com/google/wire"
"gorm.io/gorm"
)
@@ -22,19 +23,24 @@ var CasbinSet = wire.NewSet(
// ProvideCasbinEnforcer 提供 Casbin Enforcer
func ProvideCasbinEnforcer(cfg *config.Config, db *gorm.DB) *casbin.Enforcer {
// 创建 Casbin Enforcer
// 注意:这里使用文件适配器作为示例,实际使用时需要替换为数据库适配器
enforcer, err := casbin.NewEnforcer(cfg.Casbin.ModelPath)
// 创建 GORM 适配器,连接数据库
adapter, err := gormadapter.NewAdapterByDB(db)
if err != nil {
log.Fatalf("Failed to create Casbin GORM adapter: %v", err)
}
// 创建 Casbin Enforcer使用数据库适配器
enforcer, err := casbin.NewEnforcer(cfg.Casbin.ModelPath, adapter)
if err != nil {
log.Fatalf("Failed to create Casbin enforcer: %v", err)
}
// 加载策略
// 从数据库加载策略
if err := enforcer.LoadPolicy(); err != nil {
log.Printf("[WARNING] Failed to load Casbin policy: %v", err)
}
log.Println("[Wire] Initialized Casbin enforcer")
log.Println("[Wire] Initialized Casbin enforcer with GORM adapter")
return enforcer
}

View File

@@ -22,6 +22,11 @@ var HandlerSet = wire.NewSet(
handler.NewStickerHandler,
handler.NewVoteHandler,
handler.NewRoleHandler,
handler.NewAdminUserHandler,
handler.NewAdminPostHandler,
handler.NewAdminCommentHandler,
handler.NewAdminGroupHandler,
handler.NewAdminDashboardHandler,
// 需要特殊处理的 Handler
ProvideUserHandler,

View File

@@ -43,6 +43,11 @@ var ServiceSet = wire.NewSet(
ProvideUploadService,
ProvideUserActivityService,
ProvideRoleService,
ProvideAdminUserService,
ProvideAdminPostService,
ProvideAdminCommentService,
ProvideAdminGroupService,
ProvideAdminDashboardService,
)
// ProvideJWTService 提供 JWT 服务
@@ -214,3 +219,38 @@ func ProvideRoleService(
) service.RoleService {
return service.NewRoleService(roleRepo, casbinSvc)
}
// ProvideAdminUserService 提供管理端用户服务
func ProvideAdminUserService(userRepo *repository.UserRepository) service.AdminUserService {
return service.NewAdminUserService(userRepo)
}
// ProvideAdminPostService 提供管理端帖子服务
func ProvideAdminPostService(postRepo *repository.PostRepository, cacheBackend cache.Cache) service.AdminPostService {
return service.NewAdminPostService(postRepo, cacheBackend)
}
// ProvideAdminCommentService 提供管理端评论服务
func ProvideAdminCommentService(commentRepo *repository.CommentRepository, postRepo *repository.PostRepository) service.AdminCommentService {
return service.NewAdminCommentService(commentRepo, postRepo)
}
// ProvideAdminGroupService 提供管理端群组服务
func ProvideAdminGroupService(
groupRepo repository.GroupRepository,
userRepo *repository.UserRepository,
) service.AdminGroupService {
return service.NewAdminGroupService(groupRepo, userRepo)
}
// ProvideAdminDashboardService 提供管理端仪表盘服务
func ProvideAdminDashboardService(
db *gorm.DB,
userRepo *repository.UserRepository,
postRepo *repository.PostRepository,
commentRepo *repository.CommentRepository,
groupRepo repository.GroupRepository,
activityRepo *repository.UserActivityRepository,
) service.AdminDashboardService {
return service.NewAdminDashboardService(db, userRepo, postRepo, commentRepo, groupRepo, activityRepo)
}