chore: initial commit

This commit is contained in:
2026-07-22 17:50:33 +08:00
parent 82936a8987
commit ee9f50b859
107 changed files with 8346 additions and 0 deletions

52
.air.toml Normal file
View File

@@ -0,0 +1,52 @@
root = "."
testdata_dir = "testdata"
tmp_dir = "tmp"
[build]
args_bin = []
bin = "./tmp/main"
cmd = "go build -o ./tmp/main ./cmd"
delay = 1000
exclude_dir = ["assets", "tmp", "vendor", "testdata"]
exclude_file = []
exclude_regex = ["_test.go"]
exclude_unchanged = false
follow_symlink = false
full_bin = ""
include_dir = []
include_ext = ["go", "tpl", "tmpl", "html"]
include_file = []
kill_delay = "0s"
log = "build-errors.log"
poll = false
poll_interval = 0
post_cmd = []
pre_cmd = []
rerun = false
rerun_delay = 500
send_interrupt = false
stop_on_error = true
[color]
app = ""
build = "yellow"
main = "magenta"
runner = "green"
watcher = "cyan"
[log]
main_only = false
silent = false
time = false
[misc]
clean_on_exit = false
[proxy]
app_port = 0
enabled = false
proxy_port = 0
[screen]
clear_on_rebuild = false
keep_scroll = true

31
.gitignore vendored Normal file
View File

@@ -0,0 +1,31 @@
### Go template
# If you prefer the allow list template instead of the deny list, see community template:
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
#
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib
# Test binary, built with `go test -c`
*.test
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
# Dependency directories (remove the comment below to include it)
# vendor/
# Go workspace file
go.work
go.work.sum
# env file
.env
tmp/
.idea/
uploads/
logs/

66
cmd/main.go Normal file
View File

@@ -0,0 +1,66 @@
package main
import (
"context"
"fmt"
"log/slog"
"net/http"
"server/internal/config"
"server/internal/db"
"server/internal/handler"
"server/internal/middleware"
"server/internal/pkg/cache"
"server/internal/pkg/logger"
"server/internal/router"
"server/internal/service"
"github.com/go-chi/chi/v5"
"go.uber.org/fx"
)
func NewHttpServer(lc fx.Lifecycle, r *chi.Mux, cfg *config.Config) *http.Server {
srv := &http.Server{
Addr: fmt.Sprintf("%s:%s", cfg.Server.Host, cfg.Server.Port),
Handler: r,
}
lc.Append(fx.Hook{
OnStart: func(ctx context.Context) error {
go func() {
err := srv.ListenAndServe()
if err != nil {
slog.Error("server error", "error", err)
}
}()
slog.Info(fmt.Sprintf("starting HTTP server http://%s", srv.Addr))
return nil
},
OnStop: func(ctx context.Context) error {
return srv.Shutdown(ctx)
},
})
return srv
}
func main() {
app := fx.New(
fx.Provide(
config.NewConfig,
logger.New,
cache.NewCaches,
middleware.NewJWTMiddleware,
middleware.NewAuthMiddleware,
middleware.NewLoggerMiddleware,
router.NewRouter,
db.NewDB,
db.NewQueries,
),
handler.Module,
service.Module,
fx.Invoke(func(*slog.Logger) {}),
fx.Invoke(NewHttpServer),
)
app.Run()
}

33
example.yaml Normal file
View File

@@ -0,0 +1,33 @@
server:
port: 8080
host: 127.0.0.1
database:
host: db_host
port: db_port
user: postgres
password: db_password
dbname: db_name
sslMode: disable
jwt:
secret:
signing_method: HS256
expire: 30m
refresh_expire: 720h
issuer:
audience:
token_header: Authorization
token_prefix: "Bearer "
file:
base_url: http://127.0.0.1:8080/static/ #静态文件目录 用于本地访问开放静态资源
log:
level: debug # debug / info / warn / error
filename: logs/app.log
max_size: 100 # 单个文件最大 MB
max_backups: 30 # 最多保留旧文件个数
max_age: 30 # 最多保留天数
compress: true # 是否压缩旧日志
console: true # 是否同时输出到控制台

42
go.mod Normal file
View File

@@ -0,0 +1,42 @@
module server
go 1.24.2
require (
github.com/go-chi/chi/v5 v5.2.3
github.com/go-playground/locales v0.14.1
github.com/go-playground/universal-translator v0.18.1
github.com/go-playground/validator/v10 v10.30.1
github.com/golang-jwt/jwt/v5 v5.3.0
github.com/jackc/pgx/v5 v5.7.6
github.com/knadh/koanf v1.5.0
github.com/matoous/go-nanoid/v2 v2.1.0
github.com/maypok86/otter/v2 v2.3.0
go.uber.org/fx v1.24.0
golang.org/x/crypto v0.46.0
golang.org/x/sync v0.19.0
)
require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.12 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/kr/pretty v0.3.1 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/mitchellh/copystructure v1.2.0 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/mitchellh/reflectwalk v1.0.2 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/spf13/pflag v1.0.10 // indirect
github.com/stretchr/testify v1.11.1 // indirect
go.uber.org/dig v1.19.0 // indirect
go.uber.org/multierr v1.10.0 // indirect
go.uber.org/zap v1.26.0 // indirect
golang.org/x/sys v0.39.0 // indirect
golang.org/x/text v0.32.0 // indirect
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

443
go.sum Normal file
View File

@@ -0,0 +1,443 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
github.com/aws/aws-sdk-go-v2 v1.9.2/go.mod h1:cK/D0BBs0b/oWPIcX/Z/obahJK1TT7IPVjy53i/mX/4=
github.com/aws/aws-sdk-go-v2/config v1.8.3/go.mod h1:4AEiLtAb8kLs7vgw2ZV3p2VZ1+hBavOc84hqxVNpCyw=
github.com/aws/aws-sdk-go-v2/credentials v1.4.3/go.mod h1:FNNC6nQZQUuyhq5aE5c7ata8o9e4ECGmS4lAXC7o1mQ=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.6.0/go.mod h1:gqlclDEZp4aqJOancXK6TN24aKhT0W0Ae9MHk3wzTMM=
github.com/aws/aws-sdk-go-v2/internal/ini v1.2.4/go.mod h1:ZcBrrI3zBKlhGFNYWvju0I3TR93I7YIgAfy82Fh4lcQ=
github.com/aws/aws-sdk-go-v2/service/appconfig v1.4.2/go.mod h1:FZ3HkCe+b10uFZZkFdvf98LHW21k49W8o8J366lqVKY=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.3.2/go.mod h1:72HRZDLMtmVQiLG2tLfQcaWLCssELvGl+Zf2WVxMmR8=
github.com/aws/aws-sdk-go-v2/service/sso v1.4.2/go.mod h1:NBvT9R1MEF+Ud6ApJKM0G+IkPchKS7p7c2YPKwHmBOk=
github.com/aws/aws-sdk-go-v2/service/sts v1.7.2/go.mod h1:8EzeIqfWt2wWT4rJVu3f21TfrhJ8AEMzVybRNSb/b4g=
github.com/aws/smithy-go v1.8.0/go.mod h1:SObp3lf9smib00L/v3U2eAKG8FyQ7iLrJnQiAmR5n+E=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw=
github.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/go-chi/chi/v5 v5.2.3 h1:WQIt9uxdsAbgIYgid+BpYc+liqQZGMHRaUwp0JUcvdE=
github.com/go-chi/chi/v5 v5.2.3/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
github.com/go-ldap/ldap v3.0.2+incompatible/go.mod h1:qfd9rJvER9Q0/D/Sqn1DfHRoBp40uXYvFoEVrNEPqRc=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w=
github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/go-test/deep v1.0.2-0.20181118220953-042da051cf31/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
github.com/hashicorp/consul/api v1.13.0/go.mod h1:ZlVrynguJKcYr54zGaDbaL3fOvKC9m72FhPvA8T35KQ=
github.com/hashicorp/consul/sdk v0.8.0/go.mod h1:GBvyrGALthsZObzUGsfgHZQDXjg4lOjagTIwIR1vPms=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
github.com/hashicorp/go-hclog v0.0.0-20180709165350-ff2cf002a8dd/go.mod h1:9bjs9uLqI8l75knNv3lV1kA55veR+WUPSiKIWcQHudI=
github.com/hashicorp/go-hclog v0.8.0/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ=
github.com/hashicorp/go-hclog v0.12.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=
github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA=
github.com/hashicorp/go-plugin v1.0.1/go.mod h1:++UyYGoz3o5w9ZzAdZxtQKrWWP+iqPBn3cQptSMzBuY=
github.com/hashicorp/go-retryablehttp v0.5.4/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs=
github.com/hashicorp/go-rootcerts v1.0.1/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8=
github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8=
github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
github.com/hashicorp/go-sockaddr v1.0.2/go.mod h1:rB4wwRAUzs07qva3c5SdrY/NEtAUjGlgmH/UkBUC97A=
github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-version v1.1.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
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/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
github.com/hashicorp/mdns v1.0.4/go.mod h1:mtBihi+LeNXGtG8L9dX59gAEa12BDtBQSp4v/YAJqrc=
github.com/hashicorp/memberlist v0.3.0/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE=
github.com/hashicorp/serf v0.9.6/go.mod h1:TXZNMjZQijwlDvp+r0b63xZ45H7JmCmgg4gpTwn9UV4=
github.com/hashicorp/vault/api v1.0.4/go.mod h1:gDcqh3WGcR1cpF5AJz/B1UFheUEneMoIospckxBxk6Q=
github.com/hashicorp/vault/sdk v0.1.13/go.mod h1:B+hVj7TpuQY1Y/GPbCpffmgd+tSEwvhkWnjtSYCaS2M=
github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM=
github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM=
github.com/hjson/hjson-go/v4 v4.0.0 h1:wlm6IYYqHjOdXH1gHev4VoXCaW20HdQAGCxdOEEg2cs=
github.com/hjson/hjson-go/v4 v4.0.0/go.mod h1:KaYt3bTw3zhBjYqnXkYywcYctk0A2nxeEFTse3rH13E=
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-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.7.6 h1:rWQc5FwZSPX58r1OQmkuaNicxdmExaEz5A2DO2hUuTk=
github.com/jackc/pgx/v5 v5.7.6/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M=
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/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc=
github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/knadh/koanf v1.5.0 h1:q2TSd/3Pyc/5yP9ldIrSdIz26MCcyNQzW0pEAugLPNs=
github.com/knadh/koanf v1.5.0/go.mod h1:Hgyjp4y8v44hpZtPzs7JZfRAW5AhN7KfZcwv1RYggDs=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.0/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/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/matoous/go-nanoid/v2 v2.1.0 h1:P64+dmq21hhWdtvZfEAofnvJULaRR1Yib0+PnU669bE=
github.com/matoous/go-nanoid/v2 v2.1.0/go.mod h1:KlbGNQ+FhrUNIHUxZdL63t7tl4LaPkZNpUULS8H4uVM=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84=
github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/maypok86/otter/v2 v2.3.0 h1:8H8AVVFUSzJwIegKwv1uF5aGitTY+AIrtktg7OcLs8w=
github.com/maypok86/otter/v2 v2.3.0/go.mod h1:XgIdlpmL6jYz882/CAx1E4C1ukfgDKSaw4mWq59+7l8=
github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso=
github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI=
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
github.com/mitchellh/cli v1.1.0/go.mod h1:xcISNoH86gajksDmfB23e/pu+B+GeFRMYmoHXxx3xhI=
github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw=
github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/go-testing-interface v0.0.0-20171004221916-a61a99592b77/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo=
github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/npillmayer/nestext v0.1.3/go.mod h1:h2lrijH8jpicr25dFY+oAJLyzlya6jhnuG+zWp9L0Uk=
github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA=
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
github.com/pelletier/go-toml v1.7.0 h1:7utD74fnzVc/cpcyy8sjrlFr5vYpypUixARcHIMIGuI=
github.com/pelletier/go-toml v1.7.0/go.mod h1:vwGMzjaWMwyfHwgIBhI2YUM4fB6nL6lVAvS1LBMMhTE=
github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s=
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=
github.com/prometheus/client_golang v1.11.1/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=
github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
github.com/rhnvrm/simples3 v0.6.1/go.mod h1:Y+3vYm2V7Y4VijFoJHHTrja6OgPrJ2cBti8dPGkC3sA=
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc=
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
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.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
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/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
go.etcd.io/etcd/api/v3 v3.5.4/go.mod h1:5GB2vv4A4AOn3yk7MftYGHkUfGtDHnEraIjym4dYz5A=
go.etcd.io/etcd/client/pkg/v3 v3.5.4/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g=
go.etcd.io/etcd/client/v3 v3.5.4/go.mod h1:ZaRkVgBZC+L+dLCjTcF1hRXpgZXQPOvnA/Ak/gq3kiY=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/dig v1.19.0 h1:BACLhebsYdpQ7IROQ1AGPjrXcP5dF80U3gKoFzbaq/4=
go.uber.org/dig v1.19.0/go.mod h1:Us0rSJiThwCv2GteUN0Q7OKvU7n5J4dxZ9JKUXozFdE=
go.uber.org/fx v1.24.0 h1:wE8mruvpg2kiiL1Vqd0CC+tr0/24XIB10Iwp2lLWzkg=
go.uber.org/fx v1.24.0/go.mod h1:AmDeGyS+ZARGKM4tlH4FY2Jr63VjbEDJHtqXTGP5hbo=
go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk=
go.uber.org/goleak v1.2.0/go.mod h1:XJYK+MuIchqpmGmUSAzotztawfKvYLUIgg7guXrwVUo=
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ=
go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo=
go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo=
go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
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-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
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-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190129075346-302c3dd5f1cc/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-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
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/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20181227161524-e6919f6577db/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
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/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190404172233-64821d5d2107/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=
google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.22.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0=
google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d/go.mod h1:cuepJuh7vyXfUyUwEgHQXw849cJrilpS5NeIjOWESAw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
gopkg.in/square/go-jose.v2 v2.3.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
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.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/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.0-20210107192922-496545a6307b/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=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc=

106
internal/config/config.go Normal file
View File

@@ -0,0 +1,106 @@
package config
import (
"fmt"
"log/slog"
"os"
"time"
"github.com/knadh/koanf"
"github.com/knadh/koanf/parsers/yaml"
"github.com/knadh/koanf/providers/file"
)
const (
Development = "development"
Production = "production"
)
var K = koanf.New(".")
type Server struct {
Port string `koanf:"port"`
Host string `koanf:"host"`
}
type Database struct {
Host string `koanf:"host"`
Port int `koanf:"port"`
User string `koanf:"user"`
Password string `koanf:"password"`
DBName string `koanf:"dbname"`
SSLMode string `koanf:"sslMode"`
}
type JWTConfig struct {
Secret string `koanf:"secret"`
SigningMethod string `koanf:"signing_method"`
Expire time.Duration `koanf:"expire"`
RefreshExpire time.Duration `koanf:"refresh_expire"`
Issuer string `koanf:"issuer"`
Audience string `koanf:"audience"`
TokenHeader string `koanf:"token_header"`
TokenPrefix string `koanf:"token_prefix"`
}
type LogConfig struct {
Level string `koanf:"level"`
Filename string `koanf:"filename"`
MaxSize int `koanf:"max_size"`
MaxBackups int `koanf:"max_backups"`
MaxAge int `koanf:"max_age"`
Compress bool `koanf:"compress"`
Console bool `koanf:"console"`
}
type Config struct {
Server Server `koanf:"server"`
Database Database `koanf:"database"`
JWTConfig JWTConfig `koanf:"jwt"`
Log LogConfig `koanf:"log"`
}
// GetEnv 获取环境变量,确保环境变量始终有效
func GetEnv() string {
env := os.Getenv("APP_ENV")
if env == "" {
env = Development
}
switch env {
case Development, Production:
return env
default:
return Development
}
}
func IsDev() bool {
return GetEnv() == Development
}
func NewConfig() (*Config, error) {
env := GetEnv()
filepath := fmt.Sprintf("%s.yaml", env)
if err := K.Load(file.Provider(filepath), yaml.Parser()); err != nil {
slog.Error("error loading config", "error", err)
return nil, err
}
var cfg Config
if err := K.Unmarshal("", &cfg); err != nil {
slog.Error("error unmarshaling config", "error", err)
return nil, err
}
return &cfg, nil
}
func GetString(key string) string {
return K.String(key)
}
func GetInt(key string) int {
return K.Int(key)
}

48
internal/db/db.go Normal file
View File

@@ -0,0 +1,48 @@
package db
import (
"context"
"fmt"
"server/internal/config"
"server/internal/db/sqlc"
"github.com/jackc/pgx/v5/pgxpool"
"go.uber.org/fx"
)
func NewDB(lc fx.Lifecycle, cfg *config.Config) (*pgxpool.Pool, error) {
ctx := context.Background()
dbConfig := cfg.Database
dsn := fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=%s",
dbConfig.Host, dbConfig.Port, dbConfig.User, dbConfig.Password, dbConfig.DBName, dbConfig.SSLMode)
// 创建连接池
pool, err := pgxpool.New(ctx, dsn)
if err != nil {
return nil, err
}
// 注册fx生命周期钩子
lc.Append(fx.Hook{
OnStart: func(ctx context.Context) error {
return nil
},
OnStop: func(ctx context.Context) error {
pool.Close()
return nil
},
})
// 测试连接
if err = pool.Ping(ctx); err != nil {
pool.Close()
return nil, fmt.Errorf("failed to ping database: %w", err)
}
return pool, nil
}
func NewQueries(pool *pgxpool.Pool) *db.Queries {
return db.New(pool)
}

View File

@@ -0,0 +1,32 @@
-- migrate/down.sql
-- 此文件用于撤销 up.sql 文件的所有操作
-- 注意:执行此脚本将会永久删除相关表和数据!
-- 移除表上的触发器
-- 必须在删除表和函数之前移除依赖它们的触发器
DROP TRIGGER IF EXISTS update_sys_users_updated_at ON sys_users;
DROP TRIGGER IF EXISTS update_sys_roles_updated_at ON sys_roles;
DROP TRIGGER IF EXISTS update_sys_menus_updated_at ON sys_menus;
DROP TRIGGER IF EXISTS update_sys_permissions_updated_at ON sys_permissions;
DROP TRIGGER IF EXISTS update_sys_apis_updated_at ON sys_apis;
DROP TRIGGER IF EXISTS update_files_updated_at ON files;
-- 删除索引
DROP INDEX IF EXISTS idx_sys_users_account;
-- 移除表
-- 按照与创建相反的顺序删除,先删除依赖其他表的表
DROP TABLE IF EXISTS sys_api_permission;
DROP TABLE IF EXISTS sys_apis;
DROP TABLE IF EXISTS sys_menu_permission;
DROP TABLE IF EXISTS sys_role_permission;
DROP TABLE IF EXISTS sys_user_role;
DROP TABLE IF EXISTS sys_permissions;
DROP TABLE IF EXISTS sys_menus;
DROP TABLE IF EXISTS sys_roles;
DROP TABLE IF EXISTS sys_users;
DROP TABLE IF EXISTS files;
-- 移除触发器函数
-- 在所有使用此函数的触发器都被移除后,可以安全地删除此函数
DROP FUNCTION IF EXISTS update_updated_at_column;

View File

@@ -0,0 +1,221 @@
--------------------------------------------------------------------------------------------------------------
-- function
CREATE
OR REPLACE FUNCTION update_updated_at_column()
RETURNS TRIGGER AS
$$
BEGIN
NEW.updated_at
:= NOW();
RETURN NEW;
END;
$$
language 'plpgsql';
--------------------------------------------------------------------------------------------------------------
CREATE TABLE sys_users
(
id INTEGER PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
account TEXT NOT NULL UNIQUE,
username TEXT NOT NULL,
password_hash TEXT NOT NULL,
avatar_id INTEGER,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ
);
CREATE INDEX idx_sys_users_account ON sys_users (account);
CREATE TRIGGER update_sys_users_updated_at
BEFORE UPDATE
ON sys_users
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column();
COMMENT ON COLUMN sys_users.id IS '主键ID';
COMMENT ON COLUMN sys_users.account IS '用户账号';
COMMENT ON COLUMN sys_users.username IS '用户名称';
COMMENT ON COLUMN sys_users.password_hash IS 'hash密码';
COMMENT ON COLUMN sys_users.avatar_id IS '头像文件id';
COMMENT ON COLUMN sys_users.created_at IS '创建时间';
COMMENT ON COLUMN sys_users.updated_at IS '更新时间';
--------------------------------------------------------------------------------------------------------------
CREATE TABLE sys_roles
(
id INTEGER PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
name TEXT NOT NULL,
code TEXT NOT NULL UNIQUE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ
);
CREATE TRIGGER update_sys_roles_updated_at
BEFORE UPDATE
ON sys_roles
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column();
COMMENT ON COLUMN sys_roles.id IS '主键ID';
COMMENT ON COLUMN sys_roles.name IS '角色名称';
COMMENT ON COLUMN sys_roles.code IS '角色编码,唯一';
COMMENT ON COLUMN sys_roles.created_at IS '创建时间';
COMMENT ON COLUMN sys_roles.updated_at IS '更新时间';
--------------------------------------------------------------------------------------------------------------
CREATE TABLE sys_permissions
(
id INTEGER PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
code TEXT UNIQUE,
type SMALLINT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ,
CONSTRAINT unique_type_code UNIQUE (type, code)
);
CREATE TRIGGER update_sys_permissions_updated_at
BEFORE UPDATE
ON sys_permissions
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column();
COMMENT ON COLUMN sys_permissions.id IS '主键ID';
COMMENT ON COLUMN sys_permissions.code IS '权限字符,唯一';
COMMENT ON COLUMN sys_permissions.code IS '权限类型0: 菜单 1: api';
COMMENT ON COLUMN sys_permissions.created_at IS '创建时间';
COMMENT ON COLUMN sys_permissions.updated_at IS '更新时间';
--------------------------------------------------------------------------------------------------------------
CREATE TABLE sys_menus
(
id INTEGER PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
name TEXT NOT NULL,
path TEXT NOT NULL,
component TEXT,
type SMALLINT NOT NULL,
hidden BOOLEAN DEFAULT FALSE,
sort INTEGER DEFAULT 0,
status SMALLINT NOT NULL DEFAULT 1,
parent_id INTEGER,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ,
CHECK (type IN (0, 1, 2)),
CHECK (status IN (0, 1))
);
CREATE TRIGGER update_sys_menus_updated_at
BEFORE UPDATE
ON sys_menus
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column();
COMMENT ON COLUMN sys_menus.id IS '主键ID';
COMMENT ON COLUMN sys_menus.name IS '菜单名称';
COMMENT ON COLUMN sys_menus.path IS '菜单路径';
COMMENT ON COLUMN sys_menus.component IS '组件路径';
COMMENT ON COLUMN sys_menus.type IS '菜单类型 0: 目录 1: 菜单 2: button';
COMMENT ON COLUMN sys_menus.sort IS '菜单排序';
COMMENT ON COLUMN sys_menus.hidden IS '是否隐藏菜单';
COMMENT ON COLUMN sys_menus.status IS '菜单状态 1: 启用 0: 禁用';
COMMENT ON COLUMN sys_menus.parent_id IS '父级id';
COMMENT ON COLUMN sys_menus.created_at IS '创建时间';
COMMENT ON COLUMN sys_menus.updated_at IS '更新时间';
--------------------------------------------------------------------------------------------------------------
CREATE TABLE sys_apis
(
id INTEGER PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
group_name TEXT NOT NULL,
name TEXT NOT NULL,
method TEXT NOT NULL,
path TEXT NOT NULL,
sort INTEGER DEFAULT 0,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ,
UNIQUE (method, path)
);
CREATE TRIGGER update_sys_apis_updated_at
BEFORE UPDATE
ON sys_apis
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column();
COMMENT ON COLUMN sys_apis.id IS '主键ID';
COMMENT ON COLUMN sys_apis.group_name IS '分组名称';
COMMENT ON COLUMN sys_apis.name IS '接口名称';
COMMENT ON COLUMN sys_apis.method IS 'api方法';
COMMENT ON COLUMN sys_apis.path IS 'api路径';
COMMENT ON COLUMN sys_apis.sort IS '排序';
COMMENT ON COLUMN sys_apis.created_at IS '创建时间';
COMMENT ON COLUMN sys_apis.updated_at IS '更新时间';
--------------------------------------------------------------------------------------------------------------
CREATE TABLE files
(
id INTEGER PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
file_name TEXT NOT NULL,
file_path TEXT NOT NULL,
original_name TEXT NOT NULL,
folder_name TEXT NOT NULL,
mime_type TEXT NOT NULL,
file_size BIGINT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ
);
CREATE TRIGGER update_files_updated_at
BEFORE UPDATE
ON files
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column();
COMMENT ON COLUMN files.id IS '主键ID';
COMMENT ON COLUMN files.file_name IS '文件名 xxx.jpg';
COMMENT ON COLUMN files.file_path IS '文件路径 带path /xxx/xxx.jpg';
COMMENT ON COLUMN files.original_name IS '原始文件名';
COMMENT ON COLUMN files.folder_name IS '文件夹名称';
COMMENT ON COLUMN files.mime_type IS 'mime_type';
COMMENT ON COLUMN files.file_size IS '文件大小';
COMMENT ON COLUMN files.created_at IS '上传时间';
COMMENT ON COLUMN files.updated_at IS '修改时间';
--------------------------------------------------------------------------------------------------------------
CREATE TABLE sys_user_role
(
user_id INTEGER NOT NULL,
role_id INTEGER NOT NULL,
PRIMARY KEY (user_id, role_id)
);
COMMENT ON COLUMN sys_user_role.user_id IS '用户ID';
COMMENT ON COLUMN sys_user_role.role_id IS '角色ID';
--------------------------------------------------------------------------------------------------------------
CREATE TABLE sys_role_permission
(
role_id INTEGER NOT NULL,
permission_id INTEGER NOT NULL,
PRIMARY KEY (role_id, permission_id)
);
COMMENT ON COLUMN sys_role_permission.role_id IS '角色id';
COMMENT ON COLUMN sys_role_permission.permission_id IS '权限id';
--------------------------------------------------------------------------------------------------------------
CREATE TABLE sys_menu_permission
(
menu_id INTEGER NOT NULL,
permission_id INTEGER NOT NULL,
PRIMARY KEY (menu_id, permission_id)
);
COMMENT ON COLUMN sys_menu_permission.menu_id IS '菜单id';
COMMENT ON COLUMN sys_menu_permission.permission_id IS '权限id';
--------------------------------------------------------------------------------------------------------------
CREATE TABLE sys_api_permission
(
api_id INTEGER NOT NULL,
permission_id INTEGER NOT NULL,
PRIMARY KEY (api_id, permission_id)
);
COMMENT ON COLUMN sys_api_permission.api_id IS 'api ID';
COMMENT ON COLUMN sys_api_permission.permission_id IS '权限id';
--------------------------------------------------------------------------------------------------------------

View File

@@ -0,0 +1,83 @@
-- 创建用户
INSERT INTO sys_users (account, username, password_hash, avatar_id, created_at)
VALUES ('admin', '管理员', '$2a$10$d/xYJVUFTmpSVUaV3uawZ.7hj6FanbooMJ9YjsFOmQKNopWc6oR9i', NULL,
'2025-10-18 03:05:54.692714');
-- 批量插入菜单及关联权限
WITH input(name, path, component, type, sort, status, code)
AS (VALUES ('首页', '/', '/system/home/index', 1, 0, 1, 'home'),
('用户管理', '/user', '/system/user/index', 1, 1, 1, 'user'),
('角色管理', '/role', '/system/role/index', 1, 2, 1, 'role'),
('菜单管理', '/menu', '/system/menu/index', 1, 3, 1, 'menu'),
('接口管理', '/sys_api', '/system/api/index', 1, 4, 1, 'api'),
('文件管理', '/file', '/system/file/index', 1, 5, 1, 'file')),
-- 插入菜单
inserted_menus AS (
INSERT INTO sys_menus (name, path, component, type, sort, status)
SELECT name, path, component, type, sort, status FROM input RETURNING id),
menu_rows AS (SELECT id, ROW_NUMBER() OVER () AS rn FROM inserted_menus),
-- 插入权限
inserted_permissions AS (
INSERT INTO sys_permissions (type, code)
SELECT 0, code FROM input RETURNING id),
permission_rows AS (SELECT id, ROW_NUMBER() OVER () AS rn FROM inserted_permissions)
-- 关联权限
INSERT
INTO sys_menu_permission(menu_id, permission_id)
SELECT m.id, p.id
FROM menu_rows m
JOIN permission_rows p ON m.rn = p.rn;
-- 批量插入接口及关联权限
WITH inserted_apis AS (
-- 插入菜单
INSERT INTO sys_apis (group_name, name, method, path)
VALUES ('用户管理', '用户列表', 'GET', '/admin/user'),
('用户管理', '用户信息', 'GET', '/admin/user/info'),
('用户管理', '查询用户', 'GET', '/admin/user/{id}'),
('用户管理', '用户角色', 'GET', '/admin/user/{id}/roles'),
('用户管理', '创建用户', 'POST', '/admin/user'),
('用户管理', '修改用户', 'PATCH', '/admin/user/{id}'),
('用户管理', '删除用户', 'DELETE', '/admin/user/{id}'),
('用户管理', '分配用户角色', 'PUT', '/admin/user/{id}/roles'),
('用户管理', '修改密码', 'PATCH', '/admin/user/{id}/password'),
--
('角色管理', '角色列表', 'GET', '/admin/role'),
('角色管理', '创建角色', 'POST', '/admin/role'),
('角色管理', '所有角色', 'GET', '/admin/role/all'),
('角色管理', '单个角色', 'GET', '/admin/role/{id}'),
('角色管理', '角色菜单', 'GET', '/admin/role/{id}/menus'),
('角色管理', '角色接口', 'GET', '/admin/role/{id}/apis'),
('角色管理', '修改角色', 'PATCH', '/admin/role/{id}'),
('角色管理', '删除角色', 'DELETE', '/admin/role/{id}'),
('角色管理', '分配角色菜单', 'PUT', '/admin/role/{id}/menus'),
('角色管理', '分配角色接口', 'PUT', '/admin/role/{id}/apis'),
--
('接口管理', '接口列表', 'GET', '/admin/api'),
('接口管理', '所有接口', 'GET', '/admin/api/all'),
('接口管理', '接口分组', 'GET', '/admin/api/group_names'),
('接口管理', '创建接口', 'POST', '/admin/api'),
('接口管理', '修改接口', 'PATCH', '/admin/api/{id}'),
('接口管理', '删除接口', 'DELETE', '/admin/api/{id}'),
--
('菜单管理', '菜单列表', 'GET', '/admin/menu'),
('菜单管理', '创建菜单', 'POST', '/admin/menu'),
('菜单管理', '所有菜单', 'GET', '/admin/menu/all'),
('菜单管理', '修改菜单', 'PATCH', '/admin/menu/{id}'),
('菜单管理', '删除菜单', 'DELETE', '/admin/menu/{id}'),
--
('文件管理', '文件列表', 'GET', '/admin/file'),
('文件管理', '上传文件', 'POST', '/admin/file') RETURNING id),
api_rows AS (SELECT id, ROW_NUMBER() OVER () AS rn
FROM inserted_apis),
-- 创建权限
inserted_permissions AS (
INSERT INTO sys_permissions (type)
SELECT 1 FROM api_rows RETURNING id),
permission_rows AS (SELECT id, ROW_NUMBER() OVER () AS rn
FROM inserted_permissions)
INSERT
INTO sys_api_permission(api_id, permission_id)
SELECT a.id, b.id
FROM api_rows a
INNER JOIN permission_rows b ON a.rn = b.rn;

View File

@@ -0,0 +1,19 @@
DELETE FROM sys_api_permission
WHERE api_id IN (SELECT id FROM sys_apis WHERE group_name = '文章管理');
DELETE FROM sys_permissions
WHERE id IN (
SELECT permission_id FROM sys_api_permission
WHERE api_id IN (SELECT id FROM sys_apis WHERE group_name = '文章管理')
);
DELETE FROM sys_apis WHERE group_name = '文章管理';
DELETE FROM sys_menu_permission
WHERE menu_id IN (SELECT id FROM sys_menus WHERE path = '/post');
DELETE FROM sys_permissions WHERE code = 'post' AND type = 1;
DELETE FROM sys_menus WHERE path = '/post';
DROP TABLE IF EXISTS posts;

View File

@@ -0,0 +1,82 @@
CREATE TABLE posts
(
id INTEGER PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
title TEXT NOT NULL,
cover_id INTEGER,
slug TEXT UNIQUE NOT NULL,
content TEXT NOT NULL,
summary TEXT NOT NULL,
status SMALLINT NOT NULL DEFAULT 1,
view INTEGER DEFAULT 0,
sort INTEGER DEFAULT 0,
published_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ,
CHECK (status IN (0, 1, 2))
);
CREATE TRIGGER update_posts_updated_at
BEFORE UPDATE
ON posts
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column();
COMMENT ON COLUMN posts.id IS '主键ID';
COMMENT ON COLUMN posts.title IS '文章标题';
COMMENT ON COLUMN posts.cover_id IS '文章封面图片id';
COMMENT ON COLUMN posts.slug IS 'URL友好地址';
COMMENT ON COLUMN posts.content IS '文章正文';
COMMENT ON COLUMN posts.summary IS '摘要';
COMMENT ON COLUMN posts.status IS '状态 0:草稿 1:已发布 2:已下线';
COMMENT ON COLUMN posts.view IS '阅读量';
COMMENT ON COLUMN posts.sort IS '文章排序';
COMMENT ON COLUMN posts.published_at IS '发布时间';
COMMENT ON COLUMN posts.created_at IS '创建时间';
COMMENT ON COLUMN posts.updated_at IS '修改时间';
-- 批量插入菜单及关联权限
WITH input(name, path, component, type, sort, status, code)
AS (VALUES ('文章管理', '/post', '/blog/post/index', 1, 6, 1, 'post'),
('文章编辑', '/post/edit', '/blog/post-edit/index', 1, 7, 1, 'post:edit')
),
-- 插入菜单
inserted_menus AS (
INSERT INTO sys_menus (name, path, component, type, sort, status)
SELECT name, path, component, type, sort, status FROM input RETURNING id),
menu_rows AS (SELECT id, ROW_NUMBER() OVER () AS rn FROM inserted_menus),
-- 插入权限
inserted_permissions AS (
INSERT INTO sys_permissions (type, code)
SELECT 0, code FROM input RETURNING id),
permission_rows AS (SELECT id, ROW_NUMBER() OVER () AS rn FROM inserted_permissions)
-- 关联权限
INSERT
INTO sys_menu_permission(menu_id, permission_id)
SELECT m.id, p.id
FROM menu_rows m
JOIN permission_rows p ON m.rn = p.rn;
-- 批量插入接口及关联权限
WITH inserted_apis AS (
-- 插入菜单
INSERT INTO sys_apis (group_name, name, method, path)
VALUES ('文章管理', '文章列表', 'GET', '/admin/post'),
('文章管理', '创建文章', 'POST', '/admin/post'),
('文章管理', '修改文章', 'PATCH', '/admin/post/{id}'),
('文章管理', '删除文章', 'DELETE', '/admin/post/{id}')
RETURNING id),
api_rows AS (SELECT id, ROW_NUMBER() OVER () AS rn
FROM inserted_apis),
-- 创建权限
inserted_permissions AS (
INSERT INTO sys_permissions (type)
SELECT 1 FROM api_rows RETURNING id),
permission_rows AS (SELECT id, ROW_NUMBER() OVER () AS rn
FROM inserted_permissions)
INSERT
INTO sys_api_permission(api_id, permission_id)
SELECT a.id, b.id
FROM api_rows a
INNER JOIN permission_rows b ON a.rn = b.rn;

View File

@@ -0,0 +1,2 @@
DROP TABLE IF EXISTS categories;
DROP TABLE IF EXISTS post_category;

View File

@@ -0,0 +1,30 @@
-- 分类表
CREATE TABLE categories
(
id INTEGER PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
name TEXT NOT NULL,
code TEXT UNIQUE NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ
);
CREATE TRIGGER update_categories_updated_at
BEFORE UPDATE
ON categories
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column();
COMMENT ON COLUMN categories.id IS '分类ID';
COMMENT ON COLUMN categories.name IS '分类名称';
COMMENT ON COLUMN categories.code IS '分类编码';
-- 关联表
CREATE TABLE post_category
(
post_id INT NOT NULL,
category_id INT NOT NULL,
PRIMARY KEY (post_id, category_id)
);
COMMENT ON COLUMN post_category.post_id IS '文章ID';
COMMENT ON COLUMN post_category.category_id IS '分类ID';

View File

@@ -0,0 +1 @@
DROP TABLE IF EXISTS post_stats;

View File

@@ -0,0 +1,12 @@
CREATE TABLE post_stats
(
post_id INTEGER PRIMARY KEY,
view INTEGER NOT NULL DEFAULT 0
);
COMMENT ON COLUMN post_stats.post_id IS '文章id';
COMMENT ON COLUMN post_stats.view IS '文章阅读量';
-- 删除view字段
ALTER TABLE posts
DROP COLUMN IF EXISTS view;

View File

@@ -0,0 +1,2 @@
ALTER TABLE sys_menus
DROP COLUMN IF EXISTS icon;

View File

@@ -0,0 +1,2 @@
ALTER TABLE sys_menus
ADD COLUMN IF NOT EXISTS icon INTEGER;

View File

@@ -0,0 +1 @@
DROP TABLE IF EXISTS post_viewers;

View File

@@ -0,0 +1,12 @@
CREATE TABLE post_viewers
(
post_id INTEGER NOT NULL,
ip INET NOT NULL,
last_viewed_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
PRIMARY KEY(post_id, ip)
);
COMMENT ON COLUMN post_viewers.post_id IS '文章id';
COMMENT ON COLUMN post_viewers.ip IS '访问ip';
COMMENT ON COLUMN post_viewers.last_viewed_at IS '最后访问时间';

View File

@@ -0,0 +1,42 @@
-- name: CreateCategory :exec
INSERT INTO categories(name, code)
VALUES ($1, $2);
-- name: ListCategories :many
SELECT *
FROM categories
ORDER BY id LIMIT $1
OFFSET $2;
-- name: CountCategories :one
SELECT COUNT(*)
FROM categories;
-- name: ListAllCategories :many
SELECT *
FROM categories;
-- name: GetCategoryById :one
SELECT *
FROM categories
WHERE id = $1;
-- name: UpdateCategory :execrows
UPDATE categories
SET name = coalesce(sqlc.narg('name'), name),
code = coalesce(sqlc.narg('code'), code)
WHERE id = sqlc.arg('id');
-- name: DeleteCategory :execrows
DELETE
FROM categories
WHERE id = $1;
-- name: CreatePostCategory :exec
INSERT INTO post_category(post_id, category_id)
VALUES ($1, $2);
-- name: DeletePostCategory :exec
DELETE
FROM post_category
WHERE post_id = $1;

View File

@@ -0,0 +1,13 @@
-- name: CreateFile :one
INSERT INTO files(file_name, file_path, original_name, folder_name, mime_type, file_size)
VALUES ($1, $2, $3, $4, $5, $6) RETURNING id,file_path,file_name;
-- name: GetFiles :many
SELECT *
FROM files
ORDER BY id LIMIT $1
OFFSET $2;
-- name: CountFiles :one
SELECT COUNT(*)
FROM files;

145
internal/db/query/post.sql Normal file
View File

@@ -0,0 +1,145 @@
-- name: CreatePost :one
INSERT INTO posts(title, cover_id, slug, content, summary, status, sort, published_at)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
RETURNING id;
-- name: ListPosts :many
WITH paginated_posts AS (
-- 第一步:先精准查出当前页需要的文章 ID
SELECT id,
title,
cover_id,
slug,
summary,
status,
sort,
published_at,
created_at,
updated_at
FROM posts
ORDER BY sort DESC, published_at DESC, id DESC
LIMIT $1 OFFSET $2)
-- 第二步:用这极少量的记录去进行 JOIN
SELECT p.*,
f.file_path AS cover,
c.name AS category_name,
c.id AS category_id,
COALESCE(ps.view, 0) AS view
FROM paginated_posts p
LEFT JOIN files f ON f.id = p.cover_id
LEFT JOIN post_category pc ON pc.post_id = p.id
LEFT JOIN categories c ON c.id = pc.category_id
LEFT JOIN post_stats ps ON ps.post_id = p.id
ORDER BY p.sort DESC, p.published_at DESC, p.id DESC;
-- name: CountPosts :one
SELECT COUNT(*)
FROM posts;
-- name: GetPostById :one
SELECT p.*,
f.file_path AS cover,
c.name AS category_name,
c.id AS category_id
FROM posts p
LEFT JOIN files f ON f.id = p.cover_id
LEFT JOIN post_category pc ON pc.post_id = p.id
LEFT JOIN categories c ON c.id = pc.category_id
WHERE p.id = $1
LIMIT 1;
-- name: UpdatePost :execrows
UPDATE posts
SET title = coalesce(sqlc.narg('title'), title),
cover_id = coalesce(sqlc.narg('cover_id'), cover_id),
slug = coalesce(sqlc.narg('slug'), slug),
content = coalesce(sqlc.narg('content'), content),
summary = coalesce(sqlc.narg('summary'), summary),
status = coalesce(sqlc.narg('status'), status),
sort = coalesce(sqlc.narg('sort'), sort),
published_at = coalesce(sqlc.narg('published_at'), published_at)
WHERE id = sqlc.arg('id');
-- name: DeletePost :execrows
DELETE
FROM posts
WHERE id = $1;
-- web -------------------------------------------------------
-- name: GetPublicPostBySlug :one
SELECT p.*,
f.file_path AS cover,
COALESCE(ps.view, 0) AS view
FROM posts p
LEFT JOIN files f ON f.id = p.cover_id
LEFT JOIN post_stats ps ON ps.post_id = p.id
WHERE p.slug = $1
AND p.status = 1
AND p.published_at < NOW()
LIMIT 1;
-- name: ListPublishedPosts :many
WITH paginated_posts AS (
-- 第一步:先精准查出当前页需要的文章 ID
SELECT id,
title,
cover_id,
slug,
summary,
sort,
published_at
FROM posts
WHERE status = 1
AND published_at < NOW()
ORDER BY sort DESC, published_at DESC, id DESC
LIMIT $1 OFFSET $2)
SELECT p.*,
f.file_path AS cover,
c.name AS category_name,
c.id AS category_id,
COALESCE(ps.view, 0) AS view
FROM paginated_posts p
LEFT JOIN files f ON f.id = p.cover_id
LEFT JOIN post_category pc ON pc.post_id = p.id
LEFT JOIN categories c ON c.id = pc.category_id
LEFT JOIN post_stats ps ON ps.post_id = p.id
ORDER BY p.sort DESC, p.published_at DESC, p.id DESC;
-- name: CountPublishedPosts :one
SELECT COUNT(*)
FROM posts
WHERE status = 1
AND published_at < NOW();
-- name: ListArchives :many
SELECT
p.id,
p.slug,
p.title,
p.published_at,
c."name" AS category_name
FROM
posts p
LEFT JOIN post_category pc ON p.id = pc.post_id
LEFT JOIN categories c ON c.id = pc.category_id
WHERE
p.status = 1
AND p.published_at < NOW()
ORDER BY
p.published_at DESC, p.id DESC;
-- name: ListCategoryStats :many
SELECT
c.id,
c.name,
COUNT(p.id) AS post_count
FROM
categories c
LEFT JOIN post_category pc ON c.id = pc.category_id
LEFT JOIN posts p ON p.id = pc.post_id
AND p.status = 1
AND p.published_at < NOW()
GROUP BY
c.id,
c.name;

View File

@@ -0,0 +1,29 @@
-- name: IncrementPostStatsView :exec
WITH visitor AS (
INSERT INTO post_viewers (
post_id,
ip,
last_viewed_at
)
VALUES (
$1,
$2,
NOW()
)
ON CONFLICT (post_id, ip)
DO UPDATE
SET last_viewed_at = NOW()
WHERE post_viewers.last_viewed_at < NOW() - INTERVAL '30 minutes'
RETURNING post_id
)
INSERT INTO post_stats (
post_id,
view
)
SELECT
post_id,
1
FROM visitor
ON CONFLICT (post_id)
DO UPDATE
SET view = post_stats.view + 1;

View File

@@ -0,0 +1,58 @@
-- name: CreateSysApi :one
INSERT INTO sys_apis (name, group_name, method, path, sort)
VALUES ($1, $2, $3, $4, $5)
RETURNING id;
-- name: GetSysApis :many
SELECT *
FROM sys_apis
ORDER BY id
LIMIT $1 OFFSET $2;
-- name: CountSysApis :one
SELECT COUNT(*)
FROM sys_apis;
-- name: GetSysApiByID :one
SELECT *
FROM sys_apis
WHERE id = $1;
-- name: UpdateSysApi :execrows
UPDATE sys_apis
SET name = $2,
method = $3,
path = $4,
sort = $5,
group_name = $6
WHERE id = $1;
-- name: DeleteSysApi :execrows
DELETE
FROM sys_apis
WHERE id = $1;
-- name: CreateSysApiPermission :exec
INSERT INTO sys_api_permission (api_id, permission_id)
VALUES ($1, $2);
-- name: DeleteSysApiPermission :exec
DELETE
FROM sys_api_permission
WHERE api_id = $1;
-- name: DeleteSysPermissionBySysApiID :exec
DELETE
FROM sys_permissions
WHERE id IN (SELECT permission_id
FROM sys_api_permission
WHERE api_id = $1);
-- name: GetSysApiGroupNames :many
SELECT DISTINCT group_name
FROM sys_apis;
-- name: GetAllSysApis :many
SELECT *
FROM sys_apis
ORDER BY sort ASC;

View File

@@ -0,0 +1,71 @@
-- name: CreateSysMenu :one
INSERT INTO sys_menus (name, path, component, type, hidden, sort, status, parent_id, icon)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
RETURNING id;
-- name: ListSysMenus :many
SELECT m.*, p.code AS permission_code
FROM sys_menus m
LEFT JOIN sys_menu_permission mp ON m.id = mp.menu_id
LEFT JOIN sys_permissions p ON mp.permission_id = p.id
ORDER BY m.id
LIMIT $1 OFFSET $2;
-- name: CountSysMenus :one
SELECT COUNT(*)
FROM sys_menus;
-- name: GetAllSysMenus :many
SELECT m.*,
p.code AS permission_code
FROM sys_menus m
LEFT JOIN sys_menu_permission mp ON m.id = mp.menu_id
LEFT JOIN sys_permissions p ON p.id = mp.permission_id
ORDER BY m.sort ASC,
m.id ASC;
-- name: GetSysMenuByID :one
SELECT *
FROM sys_menus
WHERE id = $1;
-- name: UpdateSysMenu :execrows
UPDATE sys_menus
SET name = coalesce(sqlc.narg('name'), name),
path = coalesce(sqlc.narg('path'), path),
component = coalesce(sqlc.narg('component'), component),
type = coalesce(sqlc.narg('type'), type),
hidden = coalesce(sqlc.narg('hidden'), hidden),
sort = coalesce(sqlc.narg('sort'), sort),
status = coalesce(sqlc.narg('status'), status),
icon = coalesce(sqlc.narg('icon'), status),
parent_id = CASE WHEN @update_parent_id::boolean THEN @parent_id ELSE parent_id END
WHERE id = sqlc.arg('id');
-- name: DeleteSysMenu :execrows
DELETE
FROM sys_menus
WHERE id = $1;
-- name: CreateSysMenuPermission :exec
INSERT INTO sys_menu_permission (menu_id, permission_id)
VALUES ($1, $2);
-- name: DeleteSysMenuPermission :exec
DELETE
FROM sys_menu_permission
WHERE menu_id = $1;
-- name: DeleteSysPermissionByMenuID :exec
DELETE
FROM sys_permissions
WHERE id IN (SELECT permission_id
FROM sys_menu_permission
WHERE menu_id = $1);
-- name: UpdateSysMenuPermissionCode :exec
UPDATE sys_permissions p
SET code = coalesce(sqlc.narg('code'), code)
FROM sys_menu_permission mp
WHERE p.id = mp.permission_id
AND mp.menu_id = $1;

View File

@@ -0,0 +1,14 @@
-- name: CreateSysPermission :one
INSERT INTO sys_permissions(type,code)
VALUES ($1,$2) RETURNING id;
-- name: UpdateSysPermission :exec
UPDATE sys_permissions
SET code = $2
WHERE id = $1;
-- name: DeleteSysPermission :exec
DELETE
FROM sys_permissions
WHERE id = $1;

View File

@@ -0,0 +1,76 @@
-- name: CreateSysRole :exec
INSERT INTO sys_roles (name, code)
VALUES ($1, $2);
-- name: ListSysRoles :many
SELECT *
FROM sys_roles
ORDER BY id
LIMIT $1 OFFSET $2;
-- name: CountSysRoles :one
SELECT COUNT(*)
FROM sys_roles;
-- name: GetSysRoleByID :one
SELECT *
FROM sys_roles
WHERE id = $1;
-- name: ExistsSysRoleCode :one
SELECT EXISTS(SELECT 1
FROM sys_roles
WHERE code = sqlc.arg(code)
AND (
sqlc.narg(id)::integer IS NULL
OR id <> sqlc.narg(id)::integer
));
-- name: UpdateSysRole :execrows
UPDATE sys_roles
SET name = coalesce(sqlc.narg('name'), name)
WHERE id = sqlc.arg('id');
-- name: DeleteSysRole :execrows
DELETE
FROM sys_roles
WHERE id = $1;
-- name: CreateSysRolePermission :copyfrom
INSERT INTO sys_role_permission (role_id, permission_id)
VALUES ($1, $2);
-- name: GetSysPermissionIdsByMenuIDs :many
SELECT permission_id
FROM sys_menu_permission
WHERE menu_id = ANY ($1::int[]);
-- name: GetSysPermissionIdsByApiIDs :many
SELECT permission_id
FROM sys_api_permission
WHERE api_id = ANY ($1::int[]);
-- name: GetAllSysRoles :many
SELECT *
from sys_roles;
-- name: GetSysRoleMenus :many
SELECT m.id, m.name
FROM sys_role_permission rp
JOIN sys_menu_permission mp ON mp.permission_id = rp.permission_id
JOIN sys_menus m ON m.id = mp.menu_id
WHERE rp.role_id = $1;
-- name: GetSysRoleApis :many
SELECT a.id, a."name"
FROM sys_role_permission rp
JOIN sys_api_permission ap ON ap.permission_id = rp.permission_id
JOIN sys_apis a ON a.id = ap.api_id
WHERE rp.role_id = $1;
-- name: DeleteSysRolePermission :exec
DELETE
FROM sys_role_permission rp USING sys_permissions p
WHERE rp.permission_id = p.id
AND p."type" = $2
AND rp.role_id = $1;

View File

@@ -0,0 +1,107 @@
-- name: CreateSysUser :exec
INSERT INTO sys_users (account, username, password_hash, avatar_id)
VALUES ($1, $2, $3, $4);
-- name: GetSysUserByID :one
SELECT u.id,
u.account,
u.username,
u.avatar_id,
f.file_path AS avatar_url,
u.created_at,
u.updated_at
FROM sys_users u
LEFT JOIN files f ON u.avatar_id = f.id
WHERE u.id = $1;
-- name: GetSysUserByAccount :one
SELECT u.id,
u.account,
u.username,
u.password_hash,
u.avatar_id,
f.file_path AS avatar_url,
u.created_at,
u.updated_at
FROM sys_users u
LEFT JOIN files f ON u.avatar_id = f.id
WHERE u.account = $1;
-- name: ListSysUsers :many
SELECT u.id,
u.account,
u.username,
u.avatar_id,
f.file_path AS avatar_url,
u.created_at,
u.updated_at
FROM sys_users u
LEFT JOIN files f ON u.avatar_id = f.id
ORDER BY u.id
LIMIT $1 OFFSET $2;
-- name: CountSysUsers :one
SELECT COUNT(*)
FROM sys_users;
-- name: UpdateSysUser :execrows
UPDATE sys_users
SET username = coalesce(sqlc.narg('username'), username),
avatar_id = CASE WHEN @update_avatar_id::boolean THEN @avatar_id ELSE avatar_id END
WHERE id = sqlc.arg('id');
-- name: UpdateSysUserPassword :execrows
UPDATE sys_users
SET password_hash = $2
WHERE id = $1;
-- name: DeleteSysUser :execrows
DELETE
FROM sys_users
WHERE id = $1
AND id <> 1;
-- name: ClearSysUserRoles :exec
DELETE
FROM sys_user_role
WHERE user_id = $1;
-- name: CreateSysUserRole :exec
INSERT INTO sys_user_role (user_id, role_id)
VALUES ($1, $2);
-- name: GetSysUserRoles :many
SELECT r.*
FROM sys_roles r
JOIN sys_user_role ur ON r.id = ur.role_id
WHERE ur.user_id = $1;
-- name: GetSysAdminMenus :many
SELECT *
FROM sys_menus
ORDER BY sort, id;
-- name: GetSysUserMenus :many
SELECT DISTINCT m.*
FROM sys_users u
JOIN sys_user_role ur ON ur.user_id = u.id
JOIN sys_roles r ON r.id = ur.role_id
JOIN sys_role_permission rp ON rp.role_id = r.id
JOIN sys_permissions p ON p.id = rp.permission_id
JOIN sys_menu_permission mp ON mp.permission_id = p.id
JOIN sys_menus m ON m.id = mp.menu_id
WHERE u.id = $1
ORDER BY m.sort, m.id;
-- name: GetSysUserApis :many
SELECT DISTINCT a.id,
a.PATH,
a.METHOD
FROM sys_users u
JOIN sys_user_role ur ON u.id = ur.user_id
JOIN sys_role_permission rp ON rp.role_id = ur.role_id
JOIN sys_permissions p ON p.id = rp.permission_id
AND p.TYPE = 1
JOIN sys_api_permission ap ON ap.permission_id = p.id
JOIN sys_apis a ON a.id = ap.api_id
WHERE u.id = $1;

View File

@@ -0,0 +1,19 @@
-- 分类表
CREATE TABLE categories
(
id INTEGER PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
name TEXT NOT NULL,
code TEXT UNIQUE NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ
);
CREATE TRIGGER update_categories_updated_at
BEFORE UPDATE
ON categories
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column();
COMMENT ON COLUMN categories.id IS '分类ID';
COMMENT ON COLUMN categories.name IS '分类名称';
COMMENT ON COLUMN categories.code IS '分类编码';

View File

@@ -0,0 +1,28 @@
CREATE TABLE files
(
id INTEGER PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
file_name TEXT NOT NULL,
file_path TEXT NOT NULL,
original_name TEXT NOT NULL,
folder_name TEXT NOT NULL,
mime_type TEXT NOT NULL,
file_size BIGINT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ
);
CREATE TRIGGER update_files_updated_at
BEFORE UPDATE
ON files
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column();
COMMENT ON COLUMN files.id IS '主键ID';
COMMENT ON COLUMN files.file_name IS '文件名 xxx.jpg';
COMMENT ON COLUMN files.file_path IS '文件路径 带path /xxx/xxx.jpg';
COMMENT ON COLUMN files.original_name IS '原始文件名';
COMMENT ON COLUMN files.folder_name IS '文件夹名称';
COMMENT ON COLUMN files.mime_type IS 'mime_type';
COMMENT ON COLUMN files.file_size IS '文件大小';
COMMENT ON COLUMN files.created_at IS '上传时间';
COMMENT ON COLUMN files.updated_at IS '修改时间';

View File

@@ -0,0 +1,11 @@
CREATE
OR REPLACE FUNCTION update_updated_at_column()
RETURNS TRIGGER AS
$$
BEGIN
NEW.updated_at
:= NOW();
RETURN NEW;
END;
$$
language 'plpgsql';

View File

@@ -0,0 +1,9 @@
CREATE TABLE post_category
(
post_id INT NOT NULL,
category_id INT NOT NULL,
PRIMARY KEY (post_id, category_id)
);
COMMENT ON COLUMN post_category.post_id IS '文章ID';
COMMENT ON COLUMN post_category.category_id IS '分类ID';

View File

@@ -0,0 +1,8 @@
CREATE TABLE post_stats
(
post_id INTEGER PRIMARY KEY,
view INTEGER NOT NULL DEFAULT 0
);
COMMENT ON COLUMN post_stats.post_id IS '文章id';
COMMENT ON COLUMN post_stats.view IS '文章阅读量';

View File

@@ -0,0 +1,12 @@
CREATE TABLE post_viewers
(
post_id INTEGER NOT NULL,
ip INET NOT NULL,
last_viewed_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
PRIMARY KEY(post_id, ip)
);
COMMENT ON COLUMN post_viewers.post_id IS '文章id';
COMMENT ON COLUMN post_viewers.ip IS '访问ip';
COMMENT ON COLUMN post_viewers.last_viewed_at IS '最后访问时间';

View File

@@ -0,0 +1,35 @@
CREATE TABLE posts
(
id INTEGER PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
title TEXT NOT NULL,
cover_id INTEGER,
slug TEXT UNIQUE NOT NULL,
content TEXT NOT NULL,
summary TEXT NOT NULL,
status SMALLINT NOT NULL DEFAULT 1,
sort INTEGER DEFAULT 0,
published_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ,
CHECK (status IN (0, 1, 2))
);
CREATE TRIGGER update_posts_updated_at
BEFORE UPDATE
ON posts
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column();
COMMENT ON COLUMN posts.id IS '主键ID';
COMMENT ON COLUMN posts.title IS '文章标题';
COMMENT ON COLUMN posts.cover_id IS '文章封面图片id';
COMMENT ON COLUMN posts.slug IS 'URL友好地址';
COMMENT ON COLUMN posts.content IS '文章正文';
COMMENT ON COLUMN posts.summary IS '摘要';
COMMENT ON COLUMN posts.status IS '状态 0:草稿 1:已发布 2:已下线';
COMMENT ON COLUMN posts.sort IS '文章排序';
COMMENT ON COLUMN posts.published_at IS '发布时间';
COMMENT ON COLUMN posts.created_at IS '创建时间';
COMMENT ON COLUMN posts.updated_at IS '修改时间';

View File

@@ -0,0 +1,9 @@
CREATE TABLE sys_api_permission
(
api_id INTEGER NOT NULL,
permission_id INTEGER NOT NULL,
PRIMARY KEY (api_id, permission_id)
);
COMMENT ON COLUMN sys_api_permission.api_id IS 'api ID';
COMMENT ON COLUMN sys_api_permission.permission_id IS '权限id';

View File

@@ -0,0 +1,28 @@
CREATE TABLE sys_apis
(
id INTEGER PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
group_name TEXT NOT NULL,
name TEXT NOT NULL,
method TEXT NOT NULL,
path TEXT NOT NULL,
sort INTEGER DEFAULT 0,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ,
UNIQUE (method, path)
);
CREATE TRIGGER update_sys_apis_updated_at
BEFORE UPDATE
ON sys_apis
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column();
COMMENT ON COLUMN sys_apis.id IS '主键ID';
COMMENT ON COLUMN sys_apis.group_name IS '分组名称';
COMMENT ON COLUMN sys_apis.name IS '接口名称';
COMMENT ON COLUMN sys_apis.method IS 'api方法';
COMMENT ON COLUMN sys_apis.path IS 'api路径';
COMMENT ON COLUMN sys_apis.sort IS '排序';
COMMENT ON COLUMN sys_apis.created_at IS '创建时间';
COMMENT ON COLUMN sys_apis.updated_at IS '更新时间';

View File

@@ -0,0 +1,9 @@
CREATE TABLE sys_menu_permission
(
menu_id INTEGER NOT NULL,
permission_id INTEGER NOT NULL,
PRIMARY KEY (menu_id, permission_id)
);
COMMENT ON COLUMN sys_menu_permission.menu_id IS '菜单id';
COMMENT ON COLUMN sys_menu_permission.permission_id IS '权限id';

View File

@@ -0,0 +1,38 @@
CREATE TABLE sys_menus
(
id INTEGER PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
name TEXT NOT NULL,
path TEXT NOT NULL,
component TEXT,
type SMALLINT NOT NULL,
hidden BOOLEAN DEFAULT FALSE,
sort INTEGER DEFAULT 0,
status SMALLINT NOT NULL DEFAULT 1,
parent_id INTEGER,
icon INTEGER,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ,
CHECK (type IN (0, 1, 2)),
CHECK (status IN (0, 1))
);
CREATE TRIGGER update_sys_menus_updated_at
BEFORE UPDATE
ON sys_menus
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column();
COMMENT ON COLUMN sys_menus.id IS '主键ID';
COMMENT ON COLUMN sys_menus.name IS '菜单名称';
COMMENT ON COLUMN sys_menus.path IS '菜单路径';
COMMENT ON COLUMN sys_menus.component IS '组件路径';
COMMENT ON COLUMN sys_menus.type IS '菜单类型 0: 目录 1: 菜单 2: button';
COMMENT ON COLUMN sys_menus.sort IS '菜单排序';
COMMENT ON COLUMN sys_menus.hidden IS '是否隐藏菜单';
COMMENT ON COLUMN sys_menus.status IS '菜单状态 1: 启用 0: 禁用';
COMMENT ON COLUMN sys_menus.parent_id IS '父级id';
COMMENT ON COLUMN sys_menus.icon IS '菜单icon';
COMMENT ON COLUMN sys_menus.created_at IS '创建时间';
COMMENT ON COLUMN sys_menus.updated_at IS '更新时间';

View File

@@ -0,0 +1,22 @@
CREATE TABLE sys_permissions
(
id INTEGER PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
code TEXT UNIQUE,
type SMALLINT NOT NULL,
created_at TIMESTAMPTZtz NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ,
CONSTRAINT unique_type_code UNIQUE (type, code)
);
CREATE TRIGGER update_sys_permissions_updated_at
BEFORE UPDATE
ON sys_permissions
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column();
COMMENT ON COLUMN sys_permissions.id IS '主键ID';
COMMENT ON COLUMN sys_permissions.code IS '权限字符,唯一';
COMMENT ON COLUMN sys_permissions.code IS '权限类型0: 菜单 1: api';
COMMENT ON COLUMN sys_permissions.created_at IS '创建时间';
COMMENT ON COLUMN sys_permissions.updated_at IS '更新时间';

View File

@@ -0,0 +1,9 @@
CREATE TABLE sys_role_permission
(
role_id INTEGER NOT NULL,
permission_id INTEGER NOT NULL,
PRIMARY KEY (role_id, permission_id)
);
COMMENT ON COLUMN sys_role_permission.role_id IS '角色id';
COMMENT ON COLUMN sys_role_permission.permission_id IS '权限id';

View File

@@ -0,0 +1,20 @@
CREATE TABLE sys_roles
(
id INTEGER PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
name TEXT NOT NULL,
code TEXT NOT NULL UNIQUE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ
);
CREATE TRIGGER update_sys_roles_updated_at
BEFORE UPDATE
ON sys_roles
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column();
COMMENT ON COLUMN sys_roles.id IS '主键ID';
COMMENT ON COLUMN sys_roles.name IS '角色名称';
COMMENT ON COLUMN sys_roles.code IS '角色编码,唯一';
COMMENT ON COLUMN sys_roles.created_at IS '创建时间';
COMMENT ON COLUMN sys_roles.updated_at IS '更新时间';

View File

@@ -0,0 +1,9 @@
CREATE TABLE sys_user_role
(
user_id INTEGER NOT NULL,
role_id INTEGER NOT NULL,
PRIMARY KEY (user_id, role_id)
);
COMMENT ON COLUMN sys_user_role.user_id IS '用户ID';
COMMENT ON COLUMN sys_user_role.role_id IS '角色ID';

View File

@@ -0,0 +1,27 @@
CREATE TABLE sys_users
(
id INTEGER PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
account TEXT NOT NULL UNIQUE,
username TEXT NOT NULL,
password_hash TEXT NOT NULL,
avatar_id INTEGER,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ
);
CREATE INDEX idx_sys_users_account ON sys_users (account);
CREATE TRIGGER update_sys_users_updated_at
BEFORE UPDATE
ON sys_users
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column();
COMMENT ON COLUMN sys_users.id IS '主键ID';
COMMENT ON COLUMN sys_users.account IS '用户账号';
COMMENT ON COLUMN sys_users.username IS '用户名称';
COMMENT ON COLUMN sys_users.password_hash IS 'hash密码';
COMMENT ON COLUMN sys_users.avatar_id IS '头像文件id';
COMMENT ON COLUMN sys_users.created_at IS '创建时间';
COMMENT ON COLUMN sys_users.updated_at IS '更新时间';

View File

@@ -0,0 +1,186 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.31.1
// source: category.sql
package db
import (
"context"
)
const countCategories = `-- name: CountCategories :one
SELECT COUNT(*)
FROM categories
`
func (q *Queries) CountCategories(ctx context.Context) (int64, error) {
row := q.db.QueryRow(ctx, countCategories)
var count int64
err := row.Scan(&count)
return count, err
}
const createCategory = `-- name: CreateCategory :exec
INSERT INTO categories(name, code)
VALUES ($1, $2)
`
type CreateCategoryParams struct {
Name string `json:"name"`
Code string `json:"code"`
}
func (q *Queries) CreateCategory(ctx context.Context, arg CreateCategoryParams) error {
_, err := q.db.Exec(ctx, createCategory, arg.Name, arg.Code)
return err
}
const createPostCategory = `-- name: CreatePostCategory :exec
INSERT INTO post_category(post_id, category_id)
VALUES ($1, $2)
`
type CreatePostCategoryParams struct {
PostID int32 `json:"post_id"`
CategoryID int32 `json:"category_id"`
}
func (q *Queries) CreatePostCategory(ctx context.Context, arg CreatePostCategoryParams) error {
_, err := q.db.Exec(ctx, createPostCategory, arg.PostID, arg.CategoryID)
return err
}
const deleteCategory = `-- name: DeleteCategory :execrows
DELETE
FROM categories
WHERE id = $1
`
func (q *Queries) DeleteCategory(ctx context.Context, id int32) (int64, error) {
result, err := q.db.Exec(ctx, deleteCategory, id)
if err != nil {
return 0, err
}
return result.RowsAffected(), nil
}
const deletePostCategory = `-- name: DeletePostCategory :exec
DELETE
FROM post_category
WHERE post_id = $1
`
func (q *Queries) DeletePostCategory(ctx context.Context, postID int32) error {
_, err := q.db.Exec(ctx, deletePostCategory, postID)
return err
}
const getCategoryById = `-- name: GetCategoryById :one
SELECT id, name, code, created_at, updated_at
FROM categories
WHERE id = $1
`
func (q *Queries) GetCategoryById(ctx context.Context, id int32) (Category, error) {
row := q.db.QueryRow(ctx, getCategoryById, id)
var i Category
err := row.Scan(
&i.ID,
&i.Name,
&i.Code,
&i.CreatedAt,
&i.UpdatedAt,
)
return i, err
}
const listAllCategories = `-- name: ListAllCategories :many
SELECT id, name, code, created_at, updated_at
FROM categories
`
func (q *Queries) ListAllCategories(ctx context.Context) ([]Category, error) {
rows, err := q.db.Query(ctx, listAllCategories)
if err != nil {
return nil, err
}
defer rows.Close()
items := []Category{}
for rows.Next() {
var i Category
if err := rows.Scan(
&i.ID,
&i.Name,
&i.Code,
&i.CreatedAt,
&i.UpdatedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const listCategories = `-- name: ListCategories :many
SELECT id, name, code, created_at, updated_at
FROM categories
ORDER BY id LIMIT $1
OFFSET $2
`
type ListCategoriesParams struct {
Limit int32 `json:"limit"`
Offset int32 `json:"offset"`
}
func (q *Queries) ListCategories(ctx context.Context, arg ListCategoriesParams) ([]Category, error) {
rows, err := q.db.Query(ctx, listCategories, arg.Limit, arg.Offset)
if err != nil {
return nil, err
}
defer rows.Close()
items := []Category{}
for rows.Next() {
var i Category
if err := rows.Scan(
&i.ID,
&i.Name,
&i.Code,
&i.CreatedAt,
&i.UpdatedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const updateCategory = `-- name: UpdateCategory :execrows
UPDATE categories
SET name = coalesce($1, name),
code = coalesce($2, code)
WHERE id = $3
`
type UpdateCategoryParams struct {
Name *string `json:"name"`
Code *string `json:"code"`
ID int32 `json:"id"`
}
func (q *Queries) UpdateCategory(ctx context.Context, arg UpdateCategoryParams) (int64, error) {
result, err := q.db.Exec(ctx, updateCategory, arg.Name, arg.Code, arg.ID)
if err != nil {
return 0, err
}
return result.RowsAffected(), nil
}

View File

@@ -0,0 +1,43 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.31.1
// source: copyfrom.go
package db
import (
"context"
)
// iteratorForCreateSysRolePermission implements pgx.CopyFromSource.
type iteratorForCreateSysRolePermission struct {
rows []CreateSysRolePermissionParams
skippedFirstNextCall bool
}
func (r *iteratorForCreateSysRolePermission) Next() bool {
if len(r.rows) == 0 {
return false
}
if !r.skippedFirstNextCall {
r.skippedFirstNextCall = true
return true
}
r.rows = r.rows[1:]
return len(r.rows) > 0
}
func (r iteratorForCreateSysRolePermission) Values() ([]interface{}, error) {
return []interface{}{
r.rows[0].RoleID,
r.rows[0].PermissionID,
}, nil
}
func (r iteratorForCreateSysRolePermission) Err() error {
return nil
}
func (q *Queries) CreateSysRolePermission(ctx context.Context, arg []CreateSysRolePermissionParams) (int64, error) {
return q.db.CopyFrom(ctx, []string{"sys_role_permission"}, []string{"role_id", "permission_id"}, &iteratorForCreateSysRolePermission{rows: arg})
}

33
internal/db/sqlc/db.go Normal file
View File

@@ -0,0 +1,33 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.31.1
package db
import (
"context"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgconn"
)
type DBTX interface {
Exec(context.Context, string, ...interface{}) (pgconn.CommandTag, error)
Query(context.Context, string, ...interface{}) (pgx.Rows, error)
QueryRow(context.Context, string, ...interface{}) pgx.Row
CopyFrom(ctx context.Context, tableName pgx.Identifier, columnNames []string, rowSrc pgx.CopyFromSource) (int64, error)
}
func New(db DBTX) *Queries {
return &Queries{db: db}
}
type Queries struct {
db DBTX
}
func (q *Queries) WithTx(tx pgx.Tx) *Queries {
return &Queries{
db: tx,
}
}

View File

@@ -0,0 +1,98 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.31.1
// source: file.sql
package db
import (
"context"
)
const countFiles = `-- name: CountFiles :one
SELECT COUNT(*)
FROM files
`
func (q *Queries) CountFiles(ctx context.Context) (int64, error) {
row := q.db.QueryRow(ctx, countFiles)
var count int64
err := row.Scan(&count)
return count, err
}
const createFile = `-- name: CreateFile :one
INSERT INTO files(file_name, file_path, original_name, folder_name, mime_type, file_size)
VALUES ($1, $2, $3, $4, $5, $6) RETURNING id,file_path,file_name
`
type CreateFileParams struct {
FileName string `json:"file_name"`
FilePath string `json:"file_path"`
OriginalName string `json:"original_name"`
FolderName string `json:"folder_name"`
MimeType string `json:"mime_type"`
FileSize int64 `json:"file_size"`
}
type CreateFileRow struct {
ID int32 `json:"id"`
FilePath string `json:"file_path"`
FileName string `json:"file_name"`
}
func (q *Queries) CreateFile(ctx context.Context, arg CreateFileParams) (CreateFileRow, error) {
row := q.db.QueryRow(ctx, createFile,
arg.FileName,
arg.FilePath,
arg.OriginalName,
arg.FolderName,
arg.MimeType,
arg.FileSize,
)
var i CreateFileRow
err := row.Scan(&i.ID, &i.FilePath, &i.FileName)
return i, err
}
const getFiles = `-- name: GetFiles :many
SELECT id, file_name, file_path, original_name, folder_name, mime_type, file_size, created_at, updated_at
FROM files
ORDER BY id LIMIT $1
OFFSET $2
`
type GetFilesParams struct {
Limit int32 `json:"limit"`
Offset int32 `json:"offset"`
}
func (q *Queries) GetFiles(ctx context.Context, arg GetFilesParams) ([]File, error) {
rows, err := q.db.Query(ctx, getFiles, arg.Limit, arg.Offset)
if err != nil {
return nil, err
}
defer rows.Close()
items := []File{}
for rows.Next() {
var i File
if err := rows.Scan(
&i.ID,
&i.FileName,
&i.FilePath,
&i.OriginalName,
&i.FolderName,
&i.MimeType,
&i.FileSize,
&i.CreatedAt,
&i.UpdatedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}

206
internal/db/sqlc/models.go Normal file
View File

@@ -0,0 +1,206 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.31.1
package db
import (
"net/netip"
"time"
)
type Category struct {
// 分类ID
ID int32 `json:"id"`
// 分类名称
Name string `json:"name"`
// 分类编码
Code string `json:"code"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt *time.Time `json:"updated_at"`
}
type File struct {
// 主键ID
ID int32 `json:"id"`
// 文件名 xxx.jpg
FileName string `json:"file_name"`
// 文件路径 带path /xxx/xxx.jpg
FilePath string `json:"file_path"`
// 原始文件名
OriginalName string `json:"original_name"`
// 文件夹名称
FolderName string `json:"folder_name"`
// mime_type
MimeType string `json:"mime_type"`
// 文件大小
FileSize int64 `json:"file_size"`
// 上传时间
CreatedAt time.Time `json:"created_at"`
// 修改时间
UpdatedAt *time.Time `json:"updated_at"`
}
type Post struct {
// 主键ID
ID int32 `json:"id"`
// 文章标题
Title string `json:"title"`
// 文章封面图片id
CoverID *int32 `json:"cover_id"`
// URL友好地址
Slug string `json:"slug"`
// 文章正文
Content string `json:"content"`
// 摘要
Summary string `json:"summary"`
// 状态 0:草稿 1:已发布 2:已下线
Status int16 `json:"status"`
// 文章排序
Sort *int32 `json:"sort"`
// 发布时间
PublishedAt time.Time `json:"published_at"`
// 创建时间
CreatedAt time.Time `json:"created_at"`
// 修改时间
UpdatedAt *time.Time `json:"updated_at"`
}
type PostCategory struct {
// 文章ID
PostID int32 `json:"post_id"`
// 分类ID
CategoryID int32 `json:"category_id"`
}
type PostStat struct {
// 文章id
PostID int32 `json:"post_id"`
// 文章阅读量
View int32 `json:"view"`
}
type PostViewer struct {
// 文章id
PostID int32 `json:"post_id"`
// 访问ip
Ip netip.Addr `json:"ip"`
// 最后访问时间
LastViewedAt time.Time `json:"last_viewed_at"`
}
type SysApi struct {
// 主键ID
ID int32 `json:"id"`
// 分组名称
GroupName string `json:"group_name"`
// 接口名称
Name string `json:"name"`
// api方法
Method string `json:"method"`
// api路径
Path string `json:"path"`
// 排序
Sort *int32 `json:"sort"`
// 创建时间
CreatedAt time.Time `json:"created_at"`
// 更新时间
UpdatedAt *time.Time `json:"updated_at"`
}
type SysApiPermission struct {
// api ID
ApiID int32 `json:"api_id"`
// 权限id
PermissionID int32 `json:"permission_id"`
}
type SysMenu struct {
// 主键ID
ID int32 `json:"id"`
// 菜单名称
Name string `json:"name"`
// 菜单路径
Path string `json:"path"`
// 组件路径
Component *string `json:"component"`
// 菜单类型 0: 目录 1: 菜单 2: button
Type int16 `json:"type"`
// 是否隐藏菜单
Hidden *bool `json:"hidden"`
// 菜单排序
Sort *int32 `json:"sort"`
// 菜单状态 1: 启用 0: 禁用
Status int16 `json:"status"`
// 父级id
ParentID *int32 `json:"parent_id"`
// 菜单icon
Icon *int32 `json:"icon"`
// 创建时间
CreatedAt time.Time `json:"created_at"`
// 更新时间
UpdatedAt *time.Time `json:"updated_at"`
}
type SysMenuPermission struct {
// 菜单id
MenuID int32 `json:"menu_id"`
// 权限id
PermissionID int32 `json:"permission_id"`
}
type SysPermission struct {
// 主键ID
ID int32 `json:"id"`
// 权限类型0: 菜单 1: api
Code *string `json:"code"`
Type int16 `json:"type"`
// 创建时间
CreatedAt interface{} `json:"created_at"`
// 更新时间
UpdatedAt *time.Time `json:"updated_at"`
}
type SysRole struct {
// 主键ID
ID int32 `json:"id"`
// 角色名称
Name string `json:"name"`
// 角色编码,唯一
Code string `json:"code"`
// 创建时间
CreatedAt time.Time `json:"created_at"`
// 更新时间
UpdatedAt *time.Time `json:"updated_at"`
}
type SysRolePermission struct {
// 角色id
RoleID int32 `json:"role_id"`
// 权限id
PermissionID int32 `json:"permission_id"`
}
type SysUser struct {
// 主键ID
ID int32 `json:"id"`
// 用户账号
Account string `json:"account"`
// 用户名称
Username string `json:"username"`
// hash密码
PasswordHash string `json:"password_hash"`
// 头像文件id
AvatarID *int32 `json:"avatar_id"`
// 创建时间
CreatedAt time.Time `json:"created_at"`
// 更新时间
UpdatedAt *time.Time `json:"updated_at"`
}
type SysUserRole struct {
// 用户ID
UserID int32 `json:"user_id"`
// 角色ID
RoleID int32 `json:"role_id"`
}

View File

@@ -0,0 +1,491 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.31.1
// source: post.sql
package db
import (
"context"
"time"
)
const countPosts = `-- name: CountPosts :one
SELECT COUNT(*)
FROM posts
`
func (q *Queries) CountPosts(ctx context.Context) (int64, error) {
row := q.db.QueryRow(ctx, countPosts)
var count int64
err := row.Scan(&count)
return count, err
}
const countPublishedPosts = `-- name: CountPublishedPosts :one
SELECT COUNT(*)
FROM posts
WHERE status = 1
AND published_at < NOW()
`
func (q *Queries) CountPublishedPosts(ctx context.Context) (int64, error) {
row := q.db.QueryRow(ctx, countPublishedPosts)
var count int64
err := row.Scan(&count)
return count, err
}
const createPost = `-- name: CreatePost :one
INSERT INTO posts(title, cover_id, slug, content, summary, status, sort, published_at)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
RETURNING id
`
type CreatePostParams struct {
Title string `json:"title"`
CoverID *int32 `json:"cover_id"`
Slug string `json:"slug"`
Content string `json:"content"`
Summary string `json:"summary"`
Status int16 `json:"status"`
Sort *int32 `json:"sort"`
PublishedAt time.Time `json:"published_at"`
}
func (q *Queries) CreatePost(ctx context.Context, arg CreatePostParams) (int32, error) {
row := q.db.QueryRow(ctx, createPost,
arg.Title,
arg.CoverID,
arg.Slug,
arg.Content,
arg.Summary,
arg.Status,
arg.Sort,
arg.PublishedAt,
)
var id int32
err := row.Scan(&id)
return id, err
}
const deletePost = `-- name: DeletePost :execrows
DELETE
FROM posts
WHERE id = $1
`
func (q *Queries) DeletePost(ctx context.Context, id int32) (int64, error) {
result, err := q.db.Exec(ctx, deletePost, id)
if err != nil {
return 0, err
}
return result.RowsAffected(), nil
}
const getPostById = `-- name: GetPostById :one
SELECT p.id, p.title, p.cover_id, p.slug, p.content, p.summary, p.status, p.sort, p.published_at, p.created_at, p.updated_at,
f.file_path AS cover,
c.name AS category_name,
c.id AS category_id
FROM posts p
LEFT JOIN files f ON f.id = p.cover_id
LEFT JOIN post_category pc ON pc.post_id = p.id
LEFT JOIN categories c ON c.id = pc.category_id
WHERE p.id = $1
LIMIT 1
`
type GetPostByIdRow struct {
ID int32 `json:"id"`
Title string `json:"title"`
CoverID *int32 `json:"cover_id"`
Slug string `json:"slug"`
Content string `json:"content"`
Summary string `json:"summary"`
Status int16 `json:"status"`
Sort *int32 `json:"sort"`
PublishedAt time.Time `json:"published_at"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt *time.Time `json:"updated_at"`
Cover *string `json:"cover"`
CategoryName *string `json:"category_name"`
CategoryID *int32 `json:"category_id"`
}
func (q *Queries) GetPostById(ctx context.Context, id int32) (GetPostByIdRow, error) {
row := q.db.QueryRow(ctx, getPostById, id)
var i GetPostByIdRow
err := row.Scan(
&i.ID,
&i.Title,
&i.CoverID,
&i.Slug,
&i.Content,
&i.Summary,
&i.Status,
&i.Sort,
&i.PublishedAt,
&i.CreatedAt,
&i.UpdatedAt,
&i.Cover,
&i.CategoryName,
&i.CategoryID,
)
return i, err
}
const getPublicPostBySlug = `-- name: GetPublicPostBySlug :one
SELECT p.id, p.title, p.cover_id, p.slug, p.content, p.summary, p.status, p.sort, p.published_at, p.created_at, p.updated_at,
f.file_path AS cover,
COALESCE(ps.view, 0) AS view
FROM posts p
LEFT JOIN files f ON f.id = p.cover_id
LEFT JOIN post_stats ps ON ps.post_id = p.id
WHERE p.slug = $1
AND p.status = 1
AND p.published_at < NOW()
LIMIT 1
`
type GetPublicPostBySlugRow struct {
ID int32 `json:"id"`
Title string `json:"title"`
CoverID *int32 `json:"cover_id"`
Slug string `json:"slug"`
Content string `json:"content"`
Summary string `json:"summary"`
Status int16 `json:"status"`
Sort *int32 `json:"sort"`
PublishedAt time.Time `json:"published_at"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt *time.Time `json:"updated_at"`
Cover *string `json:"cover"`
View int32 `json:"view"`
}
// web -------------------------------------------------------
func (q *Queries) GetPublicPostBySlug(ctx context.Context, slug string) (GetPublicPostBySlugRow, error) {
row := q.db.QueryRow(ctx, getPublicPostBySlug, slug)
var i GetPublicPostBySlugRow
err := row.Scan(
&i.ID,
&i.Title,
&i.CoverID,
&i.Slug,
&i.Content,
&i.Summary,
&i.Status,
&i.Sort,
&i.PublishedAt,
&i.CreatedAt,
&i.UpdatedAt,
&i.Cover,
&i.View,
)
return i, err
}
const listArchives = `-- name: ListArchives :many
SELECT
p.id,
p.slug,
p.title,
p.published_at,
c."name" AS category_name
FROM
posts p
LEFT JOIN post_category pc ON p.id = pc.post_id
LEFT JOIN categories c ON c.id = pc.category_id
WHERE
p.status = 1
AND p.published_at < NOW()
ORDER BY
p.published_at DESC, p.id DESC
`
type ListArchivesRow struct {
ID int32 `json:"id"`
Slug string `json:"slug"`
Title string `json:"title"`
PublishedAt time.Time `json:"published_at"`
CategoryName *string `json:"category_name"`
}
func (q *Queries) ListArchives(ctx context.Context) ([]ListArchivesRow, error) {
rows, err := q.db.Query(ctx, listArchives)
if err != nil {
return nil, err
}
defer rows.Close()
items := []ListArchivesRow{}
for rows.Next() {
var i ListArchivesRow
if err := rows.Scan(
&i.ID,
&i.Slug,
&i.Title,
&i.PublishedAt,
&i.CategoryName,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const listCategoryStats = `-- name: ListCategoryStats :many
SELECT
c.id,
c.name,
COUNT(p.id) AS post_count
FROM
categories c
LEFT JOIN post_category pc ON c.id = pc.category_id
LEFT JOIN posts p ON p.id = pc.post_id
AND p.status = 1
AND p.published_at < NOW()
GROUP BY
c.id,
c.name
`
type ListCategoryStatsRow struct {
ID int32 `json:"id"`
Name string `json:"name"`
PostCount int64 `json:"post_count"`
}
func (q *Queries) ListCategoryStats(ctx context.Context) ([]ListCategoryStatsRow, error) {
rows, err := q.db.Query(ctx, listCategoryStats)
if err != nil {
return nil, err
}
defer rows.Close()
items := []ListCategoryStatsRow{}
for rows.Next() {
var i ListCategoryStatsRow
if err := rows.Scan(&i.ID, &i.Name, &i.PostCount); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const listPosts = `-- name: ListPosts :many
WITH paginated_posts AS (
-- 第一步:先精准查出当前页需要的文章 ID
SELECT id,
title,
cover_id,
slug,
summary,
status,
sort,
published_at,
created_at,
updated_at
FROM posts
ORDER BY sort DESC, published_at DESC, id DESC
LIMIT $1 OFFSET $2)
SELECT p.id, p.title, p.cover_id, p.slug, p.summary, p.status, p.sort, p.published_at, p.created_at, p.updated_at,
f.file_path AS cover,
c.name AS category_name,
c.id AS category_id,
COALESCE(ps.view, 0) AS view
FROM paginated_posts p
LEFT JOIN files f ON f.id = p.cover_id
LEFT JOIN post_category pc ON pc.post_id = p.id
LEFT JOIN categories c ON c.id = pc.category_id
LEFT JOIN post_stats ps ON ps.post_id = p.id
ORDER BY p.sort DESC, p.published_at DESC, p.id DESC
`
type ListPostsParams struct {
Limit int32 `json:"limit"`
Offset int32 `json:"offset"`
}
type ListPostsRow struct {
ID int32 `json:"id"`
Title string `json:"title"`
CoverID *int32 `json:"cover_id"`
Slug string `json:"slug"`
Summary string `json:"summary"`
Status int16 `json:"status"`
Sort *int32 `json:"sort"`
PublishedAt time.Time `json:"published_at"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt *time.Time `json:"updated_at"`
Cover *string `json:"cover"`
CategoryName *string `json:"category_name"`
CategoryID *int32 `json:"category_id"`
View int32 `json:"view"`
}
// 第二步:用这极少量的记录去进行 JOIN
func (q *Queries) ListPosts(ctx context.Context, arg ListPostsParams) ([]ListPostsRow, error) {
rows, err := q.db.Query(ctx, listPosts, arg.Limit, arg.Offset)
if err != nil {
return nil, err
}
defer rows.Close()
items := []ListPostsRow{}
for rows.Next() {
var i ListPostsRow
if err := rows.Scan(
&i.ID,
&i.Title,
&i.CoverID,
&i.Slug,
&i.Summary,
&i.Status,
&i.Sort,
&i.PublishedAt,
&i.CreatedAt,
&i.UpdatedAt,
&i.Cover,
&i.CategoryName,
&i.CategoryID,
&i.View,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const listPublishedPosts = `-- name: ListPublishedPosts :many
WITH paginated_posts AS (
-- 第一步:先精准查出当前页需要的文章 ID
SELECT id,
title,
cover_id,
slug,
summary,
sort,
published_at
FROM posts
WHERE status = 1
AND published_at < NOW()
ORDER BY sort DESC, published_at DESC, id DESC
LIMIT $1 OFFSET $2)
SELECT p.id, p.title, p.cover_id, p.slug, p.summary, p.sort, p.published_at,
f.file_path AS cover,
c.name AS category_name,
c.id AS category_id,
COALESCE(ps.view, 0) AS view
FROM paginated_posts p
LEFT JOIN files f ON f.id = p.cover_id
LEFT JOIN post_category pc ON pc.post_id = p.id
LEFT JOIN categories c ON c.id = pc.category_id
LEFT JOIN post_stats ps ON ps.post_id = p.id
ORDER BY p.sort DESC, p.published_at DESC, p.id DESC
`
type ListPublishedPostsParams struct {
Limit int32 `json:"limit"`
Offset int32 `json:"offset"`
}
type ListPublishedPostsRow struct {
ID int32 `json:"id"`
Title string `json:"title"`
CoverID *int32 `json:"cover_id"`
Slug string `json:"slug"`
Summary string `json:"summary"`
Sort *int32 `json:"sort"`
PublishedAt time.Time `json:"published_at"`
Cover *string `json:"cover"`
CategoryName *string `json:"category_name"`
CategoryID *int32 `json:"category_id"`
View int32 `json:"view"`
}
func (q *Queries) ListPublishedPosts(ctx context.Context, arg ListPublishedPostsParams) ([]ListPublishedPostsRow, error) {
rows, err := q.db.Query(ctx, listPublishedPosts, arg.Limit, arg.Offset)
if err != nil {
return nil, err
}
defer rows.Close()
items := []ListPublishedPostsRow{}
for rows.Next() {
var i ListPublishedPostsRow
if err := rows.Scan(
&i.ID,
&i.Title,
&i.CoverID,
&i.Slug,
&i.Summary,
&i.Sort,
&i.PublishedAt,
&i.Cover,
&i.CategoryName,
&i.CategoryID,
&i.View,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const updatePost = `-- name: UpdatePost :execrows
UPDATE posts
SET title = coalesce($1, title),
cover_id = coalesce($2, cover_id),
slug = coalesce($3, slug),
content = coalesce($4, content),
summary = coalesce($5, summary),
status = coalesce($6, status),
sort = coalesce($7, sort),
published_at = coalesce($8, published_at)
WHERE id = $9
`
type UpdatePostParams struct {
Title *string `json:"title"`
CoverID *int32 `json:"cover_id"`
Slug *string `json:"slug"`
Content *string `json:"content"`
Summary *string `json:"summary"`
Status *int16 `json:"status"`
Sort *int32 `json:"sort"`
PublishedAt *time.Time `json:"published_at"`
ID int32 `json:"id"`
}
func (q *Queries) UpdatePost(ctx context.Context, arg UpdatePostParams) (int64, error) {
result, err := q.db.Exec(ctx, updatePost,
arg.Title,
arg.CoverID,
arg.Slug,
arg.Content,
arg.Summary,
arg.Status,
arg.Sort,
arg.PublishedAt,
arg.ID,
)
if err != nil {
return 0, err
}
return result.RowsAffected(), nil
}

View File

@@ -0,0 +1,52 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.31.1
// source: post_stats.sql
package db
import (
"context"
"net/netip"
)
const incrementPostStatsView = `-- name: IncrementPostStatsView :exec
WITH visitor AS (
INSERT INTO post_viewers (
post_id,
ip,
last_viewed_at
)
VALUES (
$1,
$2,
NOW()
)
ON CONFLICT (post_id, ip)
DO UPDATE
SET last_viewed_at = NOW()
WHERE post_viewers.last_viewed_at < NOW() - INTERVAL '30 minutes'
RETURNING post_id
)
INSERT INTO post_stats (
post_id,
view
)
SELECT
post_id,
1
FROM visitor
ON CONFLICT (post_id)
DO UPDATE
SET view = post_stats.view + 1
`
type IncrementPostStatsViewParams struct {
PostID int32 `json:"post_id"`
Ip netip.Addr `json:"ip"`
}
func (q *Queries) IncrementPostStatsView(ctx context.Context, arg IncrementPostStatsViewParams) error {
_, err := q.db.Exec(ctx, incrementPostStatsView, arg.PostID, arg.Ip)
return err
}

View File

@@ -0,0 +1,259 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.31.1
// source: sys_api.sql
package db
import (
"context"
)
const countSysApis = `-- name: CountSysApis :one
SELECT COUNT(*)
FROM sys_apis
`
func (q *Queries) CountSysApis(ctx context.Context) (int64, error) {
row := q.db.QueryRow(ctx, countSysApis)
var count int64
err := row.Scan(&count)
return count, err
}
const createSysApi = `-- name: CreateSysApi :one
INSERT INTO sys_apis (name, group_name, method, path, sort)
VALUES ($1, $2, $3, $4, $5)
RETURNING id
`
type CreateSysApiParams struct {
Name string `json:"name"`
GroupName string `json:"group_name"`
Method string `json:"method"`
Path string `json:"path"`
Sort *int32 `json:"sort"`
}
func (q *Queries) CreateSysApi(ctx context.Context, arg CreateSysApiParams) (int32, error) {
row := q.db.QueryRow(ctx, createSysApi,
arg.Name,
arg.GroupName,
arg.Method,
arg.Path,
arg.Sort,
)
var id int32
err := row.Scan(&id)
return id, err
}
const createSysApiPermission = `-- name: CreateSysApiPermission :exec
INSERT INTO sys_api_permission (api_id, permission_id)
VALUES ($1, $2)
`
type CreateSysApiPermissionParams struct {
ApiID int32 `json:"api_id"`
PermissionID int32 `json:"permission_id"`
}
func (q *Queries) CreateSysApiPermission(ctx context.Context, arg CreateSysApiPermissionParams) error {
_, err := q.db.Exec(ctx, createSysApiPermission, arg.ApiID, arg.PermissionID)
return err
}
const deleteSysApi = `-- name: DeleteSysApi :execrows
DELETE
FROM sys_apis
WHERE id = $1
`
func (q *Queries) DeleteSysApi(ctx context.Context, id int32) (int64, error) {
result, err := q.db.Exec(ctx, deleteSysApi, id)
if err != nil {
return 0, err
}
return result.RowsAffected(), nil
}
const deleteSysApiPermission = `-- name: DeleteSysApiPermission :exec
DELETE
FROM sys_api_permission
WHERE api_id = $1
`
func (q *Queries) DeleteSysApiPermission(ctx context.Context, apiID int32) error {
_, err := q.db.Exec(ctx, deleteSysApiPermission, apiID)
return err
}
const deleteSysPermissionBySysApiID = `-- name: DeleteSysPermissionBySysApiID :exec
DELETE
FROM sys_permissions
WHERE id IN (SELECT permission_id
FROM sys_api_permission
WHERE api_id = $1)
`
func (q *Queries) DeleteSysPermissionBySysApiID(ctx context.Context, apiID int32) error {
_, err := q.db.Exec(ctx, deleteSysPermissionBySysApiID, apiID)
return err
}
const getAllSysApis = `-- name: GetAllSysApis :many
SELECT id, group_name, name, method, path, sort, created_at, updated_at
FROM sys_apis
ORDER BY sort ASC
`
func (q *Queries) GetAllSysApis(ctx context.Context) ([]SysApi, error) {
rows, err := q.db.Query(ctx, getAllSysApis)
if err != nil {
return nil, err
}
defer rows.Close()
items := []SysApi{}
for rows.Next() {
var i SysApi
if err := rows.Scan(
&i.ID,
&i.GroupName,
&i.Name,
&i.Method,
&i.Path,
&i.Sort,
&i.CreatedAt,
&i.UpdatedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getSysApiByID = `-- name: GetSysApiByID :one
SELECT id, group_name, name, method, path, sort, created_at, updated_at
FROM sys_apis
WHERE id = $1
`
func (q *Queries) GetSysApiByID(ctx context.Context, id int32) (SysApi, error) {
row := q.db.QueryRow(ctx, getSysApiByID, id)
var i SysApi
err := row.Scan(
&i.ID,
&i.GroupName,
&i.Name,
&i.Method,
&i.Path,
&i.Sort,
&i.CreatedAt,
&i.UpdatedAt,
)
return i, err
}
const getSysApiGroupNames = `-- name: GetSysApiGroupNames :many
SELECT DISTINCT group_name
FROM sys_apis
`
func (q *Queries) GetSysApiGroupNames(ctx context.Context) ([]string, error) {
rows, err := q.db.Query(ctx, getSysApiGroupNames)
if err != nil {
return nil, err
}
defer rows.Close()
items := []string{}
for rows.Next() {
var group_name string
if err := rows.Scan(&group_name); err != nil {
return nil, err
}
items = append(items, group_name)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getSysApis = `-- name: GetSysApis :many
SELECT id, group_name, name, method, path, sort, created_at, updated_at
FROM sys_apis
ORDER BY id
LIMIT $1 OFFSET $2
`
type GetSysApisParams struct {
Limit int32 `json:"limit"`
Offset int32 `json:"offset"`
}
func (q *Queries) GetSysApis(ctx context.Context, arg GetSysApisParams) ([]SysApi, error) {
rows, err := q.db.Query(ctx, getSysApis, arg.Limit, arg.Offset)
if err != nil {
return nil, err
}
defer rows.Close()
items := []SysApi{}
for rows.Next() {
var i SysApi
if err := rows.Scan(
&i.ID,
&i.GroupName,
&i.Name,
&i.Method,
&i.Path,
&i.Sort,
&i.CreatedAt,
&i.UpdatedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const updateSysApi = `-- name: UpdateSysApi :execrows
UPDATE sys_apis
SET name = $2,
method = $3,
path = $4,
sort = $5,
group_name = $6
WHERE id = $1
`
type UpdateSysApiParams struct {
ID int32 `json:"id"`
Name string `json:"name"`
Method string `json:"method"`
Path string `json:"path"`
Sort *int32 `json:"sort"`
GroupName string `json:"group_name"`
}
func (q *Queries) UpdateSysApi(ctx context.Context, arg UpdateSysApiParams) (int64, error) {
result, err := q.db.Exec(ctx, updateSysApi,
arg.ID,
arg.Name,
arg.Method,
arg.Path,
arg.Sort,
arg.GroupName,
)
if err != nil {
return 0, err
}
return result.RowsAffected(), nil
}

View File

@@ -0,0 +1,327 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.31.1
// source: sys_menu.sql
package db
import (
"context"
"time"
)
const countSysMenus = `-- name: CountSysMenus :one
SELECT COUNT(*)
FROM sys_menus
`
func (q *Queries) CountSysMenus(ctx context.Context) (int64, error) {
row := q.db.QueryRow(ctx, countSysMenus)
var count int64
err := row.Scan(&count)
return count, err
}
const createSysMenu = `-- name: CreateSysMenu :one
INSERT INTO sys_menus (name, path, component, type, hidden, sort, status, parent_id, icon)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
RETURNING id
`
type CreateSysMenuParams struct {
Name string `json:"name"`
Path string `json:"path"`
Component *string `json:"component"`
Type int16 `json:"type"`
Hidden *bool `json:"hidden"`
Sort *int32 `json:"sort"`
Status int16 `json:"status"`
ParentID *int32 `json:"parent_id"`
Icon *int32 `json:"icon"`
}
func (q *Queries) CreateSysMenu(ctx context.Context, arg CreateSysMenuParams) (int32, error) {
row := q.db.QueryRow(ctx, createSysMenu,
arg.Name,
arg.Path,
arg.Component,
arg.Type,
arg.Hidden,
arg.Sort,
arg.Status,
arg.ParentID,
arg.Icon,
)
var id int32
err := row.Scan(&id)
return id, err
}
const createSysMenuPermission = `-- name: CreateSysMenuPermission :exec
INSERT INTO sys_menu_permission (menu_id, permission_id)
VALUES ($1, $2)
`
type CreateSysMenuPermissionParams struct {
MenuID int32 `json:"menu_id"`
PermissionID int32 `json:"permission_id"`
}
func (q *Queries) CreateSysMenuPermission(ctx context.Context, arg CreateSysMenuPermissionParams) error {
_, err := q.db.Exec(ctx, createSysMenuPermission, arg.MenuID, arg.PermissionID)
return err
}
const deleteSysMenu = `-- name: DeleteSysMenu :execrows
DELETE
FROM sys_menus
WHERE id = $1
`
func (q *Queries) DeleteSysMenu(ctx context.Context, id int32) (int64, error) {
result, err := q.db.Exec(ctx, deleteSysMenu, id)
if err != nil {
return 0, err
}
return result.RowsAffected(), nil
}
const deleteSysMenuPermission = `-- name: DeleteSysMenuPermission :exec
DELETE
FROM sys_menu_permission
WHERE menu_id = $1
`
func (q *Queries) DeleteSysMenuPermission(ctx context.Context, menuID int32) error {
_, err := q.db.Exec(ctx, deleteSysMenuPermission, menuID)
return err
}
const deleteSysPermissionByMenuID = `-- name: DeleteSysPermissionByMenuID :exec
DELETE
FROM sys_permissions
WHERE id IN (SELECT permission_id
FROM sys_menu_permission
WHERE menu_id = $1)
`
func (q *Queries) DeleteSysPermissionByMenuID(ctx context.Context, menuID int32) error {
_, err := q.db.Exec(ctx, deleteSysPermissionByMenuID, menuID)
return err
}
const getAllSysMenus = `-- name: GetAllSysMenus :many
SELECT m.id, m.name, m.path, m.component, m.type, m.hidden, m.sort, m.status, m.parent_id, m.icon, m.created_at, m.updated_at,
p.code AS permission_code
FROM sys_menus m
LEFT JOIN sys_menu_permission mp ON m.id = mp.menu_id
LEFT JOIN sys_permissions p ON p.id = mp.permission_id
ORDER BY m.sort ASC,
m.id ASC
`
type GetAllSysMenusRow struct {
ID int32 `json:"id"`
Name string `json:"name"`
Path string `json:"path"`
Component *string `json:"component"`
Type int16 `json:"type"`
Hidden *bool `json:"hidden"`
Sort *int32 `json:"sort"`
Status int16 `json:"status"`
ParentID *int32 `json:"parent_id"`
Icon *int32 `json:"icon"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt *time.Time `json:"updated_at"`
PermissionCode *string `json:"permission_code"`
}
func (q *Queries) GetAllSysMenus(ctx context.Context) ([]GetAllSysMenusRow, error) {
rows, err := q.db.Query(ctx, getAllSysMenus)
if err != nil {
return nil, err
}
defer rows.Close()
items := []GetAllSysMenusRow{}
for rows.Next() {
var i GetAllSysMenusRow
if err := rows.Scan(
&i.ID,
&i.Name,
&i.Path,
&i.Component,
&i.Type,
&i.Hidden,
&i.Sort,
&i.Status,
&i.ParentID,
&i.Icon,
&i.CreatedAt,
&i.UpdatedAt,
&i.PermissionCode,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getSysMenuByID = `-- name: GetSysMenuByID :one
SELECT id, name, path, component, type, hidden, sort, status, parent_id, icon, created_at, updated_at
FROM sys_menus
WHERE id = $1
`
func (q *Queries) GetSysMenuByID(ctx context.Context, id int32) (SysMenu, error) {
row := q.db.QueryRow(ctx, getSysMenuByID, id)
var i SysMenu
err := row.Scan(
&i.ID,
&i.Name,
&i.Path,
&i.Component,
&i.Type,
&i.Hidden,
&i.Sort,
&i.Status,
&i.ParentID,
&i.Icon,
&i.CreatedAt,
&i.UpdatedAt,
)
return i, err
}
const listSysMenus = `-- name: ListSysMenus :many
SELECT m.id, m.name, m.path, m.component, m.type, m.hidden, m.sort, m.status, m.parent_id, m.icon, m.created_at, m.updated_at, p.code AS permission_code
FROM sys_menus m
LEFT JOIN sys_menu_permission mp ON m.id = mp.menu_id
LEFT JOIN sys_permissions p ON mp.permission_id = p.id
ORDER BY m.id
LIMIT $1 OFFSET $2
`
type ListSysMenusParams struct {
Limit int32 `json:"limit"`
Offset int32 `json:"offset"`
}
type ListSysMenusRow struct {
ID int32 `json:"id"`
Name string `json:"name"`
Path string `json:"path"`
Component *string `json:"component"`
Type int16 `json:"type"`
Hidden *bool `json:"hidden"`
Sort *int32 `json:"sort"`
Status int16 `json:"status"`
ParentID *int32 `json:"parent_id"`
Icon *int32 `json:"icon"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt *time.Time `json:"updated_at"`
PermissionCode *string `json:"permission_code"`
}
func (q *Queries) ListSysMenus(ctx context.Context, arg ListSysMenusParams) ([]ListSysMenusRow, error) {
rows, err := q.db.Query(ctx, listSysMenus, arg.Limit, arg.Offset)
if err != nil {
return nil, err
}
defer rows.Close()
items := []ListSysMenusRow{}
for rows.Next() {
var i ListSysMenusRow
if err := rows.Scan(
&i.ID,
&i.Name,
&i.Path,
&i.Component,
&i.Type,
&i.Hidden,
&i.Sort,
&i.Status,
&i.ParentID,
&i.Icon,
&i.CreatedAt,
&i.UpdatedAt,
&i.PermissionCode,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const updateSysMenu = `-- name: UpdateSysMenu :execrows
UPDATE sys_menus
SET name = coalesce($1, name),
path = coalesce($2, path),
component = coalesce($3, component),
type = coalesce($4, type),
hidden = coalesce($5, hidden),
sort = coalesce($6, sort),
status = coalesce($7, status),
icon = coalesce($8, status),
parent_id = CASE WHEN $9::boolean THEN $10 ELSE parent_id END
WHERE id = $11
`
type UpdateSysMenuParams struct {
Name *string `json:"name"`
Path *string `json:"path"`
Component *string `json:"component"`
Type *int16 `json:"type"`
Hidden *bool `json:"hidden"`
Sort *int32 `json:"sort"`
Status *int16 `json:"status"`
Icon *int32 `json:"icon"`
UpdateParentID bool `json:"update_parent_id"`
ParentID *int32 `json:"parent_id"`
ID int32 `json:"id"`
}
func (q *Queries) UpdateSysMenu(ctx context.Context, arg UpdateSysMenuParams) (int64, error) {
result, err := q.db.Exec(ctx, updateSysMenu,
arg.Name,
arg.Path,
arg.Component,
arg.Type,
arg.Hidden,
arg.Sort,
arg.Status,
arg.Icon,
arg.UpdateParentID,
arg.ParentID,
arg.ID,
)
if err != nil {
return 0, err
}
return result.RowsAffected(), nil
}
const updateSysMenuPermissionCode = `-- name: UpdateSysMenuPermissionCode :exec
UPDATE sys_permissions p
SET code = coalesce($2, code)
FROM sys_menu_permission mp
WHERE p.id = mp.permission_id
AND mp.menu_id = $1
`
type UpdateSysMenuPermissionCodeParams struct {
MenuID int32 `json:"menu_id"`
Code *string `json:"code"`
}
func (q *Queries) UpdateSysMenuPermissionCode(ctx context.Context, arg UpdateSysMenuPermissionCodeParams) error {
_, err := q.db.Exec(ctx, updateSysMenuPermissionCode, arg.MenuID, arg.Code)
return err
}

View File

@@ -0,0 +1,54 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.31.1
// source: sys_permission.sql
package db
import (
"context"
)
const createSysPermission = `-- name: CreateSysPermission :one
INSERT INTO sys_permissions(type,code)
VALUES ($1,$2) RETURNING id
`
type CreateSysPermissionParams struct {
Type int16 `json:"type"`
Code *string `json:"code"`
}
func (q *Queries) CreateSysPermission(ctx context.Context, arg CreateSysPermissionParams) (int32, error) {
row := q.db.QueryRow(ctx, createSysPermission, arg.Type, arg.Code)
var id int32
err := row.Scan(&id)
return id, err
}
const deleteSysPermission = `-- name: DeleteSysPermission :exec
DELETE
FROM sys_permissions
WHERE id = $1
`
func (q *Queries) DeleteSysPermission(ctx context.Context, id int32) error {
_, err := q.db.Exec(ctx, deleteSysPermission, id)
return err
}
const updateSysPermission = `-- name: UpdateSysPermission :exec
UPDATE sys_permissions
SET code = $2
WHERE id = $1
`
type UpdateSysPermissionParams struct {
ID int32 `json:"id"`
Code *string `json:"code"`
}
func (q *Queries) UpdateSysPermission(ctx context.Context, arg UpdateSysPermissionParams) error {
_, err := q.db.Exec(ctx, updateSysPermission, arg.ID, arg.Code)
return err
}

View File

@@ -0,0 +1,321 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.31.1
// source: sys_role.sql
package db
import (
"context"
)
const countSysRoles = `-- name: CountSysRoles :one
SELECT COUNT(*)
FROM sys_roles
`
func (q *Queries) CountSysRoles(ctx context.Context) (int64, error) {
row := q.db.QueryRow(ctx, countSysRoles)
var count int64
err := row.Scan(&count)
return count, err
}
const createSysRole = `-- name: CreateSysRole :exec
INSERT INTO sys_roles (name, code)
VALUES ($1, $2)
`
type CreateSysRoleParams struct {
Name string `json:"name"`
Code string `json:"code"`
}
func (q *Queries) CreateSysRole(ctx context.Context, arg CreateSysRoleParams) error {
_, err := q.db.Exec(ctx, createSysRole, arg.Name, arg.Code)
return err
}
type CreateSysRolePermissionParams struct {
RoleID int32 `json:"role_id"`
PermissionID int32 `json:"permission_id"`
}
const deleteSysRole = `-- name: DeleteSysRole :execrows
DELETE
FROM sys_roles
WHERE id = $1
`
func (q *Queries) DeleteSysRole(ctx context.Context, id int32) (int64, error) {
result, err := q.db.Exec(ctx, deleteSysRole, id)
if err != nil {
return 0, err
}
return result.RowsAffected(), nil
}
const deleteSysRolePermission = `-- name: DeleteSysRolePermission :exec
DELETE
FROM sys_role_permission rp USING sys_permissions p
WHERE rp.permission_id = p.id
AND p."type" = $2
AND rp.role_id = $1
`
type DeleteSysRolePermissionParams struct {
RoleID int32 `json:"role_id"`
Type int16 `json:"type"`
}
func (q *Queries) DeleteSysRolePermission(ctx context.Context, arg DeleteSysRolePermissionParams) error {
_, err := q.db.Exec(ctx, deleteSysRolePermission, arg.RoleID, arg.Type)
return err
}
const existsSysRoleCode = `-- name: ExistsSysRoleCode :one
SELECT EXISTS(SELECT 1
FROM sys_roles
WHERE code = $1
AND (
$2::integer IS NULL
OR id <> $2::integer
))
`
type ExistsSysRoleCodeParams struct {
Code string `json:"code"`
ID *int32 `json:"id"`
}
func (q *Queries) ExistsSysRoleCode(ctx context.Context, arg ExistsSysRoleCodeParams) (bool, error) {
row := q.db.QueryRow(ctx, existsSysRoleCode, arg.Code, arg.ID)
var exists bool
err := row.Scan(&exists)
return exists, err
}
const getAllSysRoles = `-- name: GetAllSysRoles :many
SELECT id, name, code, created_at, updated_at
from sys_roles
`
func (q *Queries) GetAllSysRoles(ctx context.Context) ([]SysRole, error) {
rows, err := q.db.Query(ctx, getAllSysRoles)
if err != nil {
return nil, err
}
defer rows.Close()
items := []SysRole{}
for rows.Next() {
var i SysRole
if err := rows.Scan(
&i.ID,
&i.Name,
&i.Code,
&i.CreatedAt,
&i.UpdatedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getSysPermissionIdsByApiIDs = `-- name: GetSysPermissionIdsByApiIDs :many
SELECT permission_id
FROM sys_api_permission
WHERE api_id = ANY ($1::int[])
`
func (q *Queries) GetSysPermissionIdsByApiIDs(ctx context.Context, dollar_1 []int32) ([]int32, error) {
rows, err := q.db.Query(ctx, getSysPermissionIdsByApiIDs, dollar_1)
if err != nil {
return nil, err
}
defer rows.Close()
items := []int32{}
for rows.Next() {
var permission_id int32
if err := rows.Scan(&permission_id); err != nil {
return nil, err
}
items = append(items, permission_id)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getSysPermissionIdsByMenuIDs = `-- name: GetSysPermissionIdsByMenuIDs :many
SELECT permission_id
FROM sys_menu_permission
WHERE menu_id = ANY ($1::int[])
`
func (q *Queries) GetSysPermissionIdsByMenuIDs(ctx context.Context, dollar_1 []int32) ([]int32, error) {
rows, err := q.db.Query(ctx, getSysPermissionIdsByMenuIDs, dollar_1)
if err != nil {
return nil, err
}
defer rows.Close()
items := []int32{}
for rows.Next() {
var permission_id int32
if err := rows.Scan(&permission_id); err != nil {
return nil, err
}
items = append(items, permission_id)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getSysRoleApis = `-- name: GetSysRoleApis :many
SELECT a.id, a."name"
FROM sys_role_permission rp
JOIN sys_api_permission ap ON ap.permission_id = rp.permission_id
JOIN sys_apis a ON a.id = ap.api_id
WHERE rp.role_id = $1
`
type GetSysRoleApisRow struct {
ID int32 `json:"id"`
Name string `json:"name"`
}
func (q *Queries) GetSysRoleApis(ctx context.Context, roleID int32) ([]GetSysRoleApisRow, error) {
rows, err := q.db.Query(ctx, getSysRoleApis, roleID)
if err != nil {
return nil, err
}
defer rows.Close()
items := []GetSysRoleApisRow{}
for rows.Next() {
var i GetSysRoleApisRow
if err := rows.Scan(&i.ID, &i.Name); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getSysRoleByID = `-- name: GetSysRoleByID :one
SELECT id, name, code, created_at, updated_at
FROM sys_roles
WHERE id = $1
`
func (q *Queries) GetSysRoleByID(ctx context.Context, id int32) (SysRole, error) {
row := q.db.QueryRow(ctx, getSysRoleByID, id)
var i SysRole
err := row.Scan(
&i.ID,
&i.Name,
&i.Code,
&i.CreatedAt,
&i.UpdatedAt,
)
return i, err
}
const getSysRoleMenus = `-- name: GetSysRoleMenus :many
SELECT m.id, m.name
FROM sys_role_permission rp
JOIN sys_menu_permission mp ON mp.permission_id = rp.permission_id
JOIN sys_menus m ON m.id = mp.menu_id
WHERE rp.role_id = $1
`
type GetSysRoleMenusRow struct {
ID int32 `json:"id"`
Name string `json:"name"`
}
func (q *Queries) GetSysRoleMenus(ctx context.Context, roleID int32) ([]GetSysRoleMenusRow, error) {
rows, err := q.db.Query(ctx, getSysRoleMenus, roleID)
if err != nil {
return nil, err
}
defer rows.Close()
items := []GetSysRoleMenusRow{}
for rows.Next() {
var i GetSysRoleMenusRow
if err := rows.Scan(&i.ID, &i.Name); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const listSysRoles = `-- name: ListSysRoles :many
SELECT id, name, code, created_at, updated_at
FROM sys_roles
ORDER BY id
LIMIT $1 OFFSET $2
`
type ListSysRolesParams struct {
Limit int32 `json:"limit"`
Offset int32 `json:"offset"`
}
func (q *Queries) ListSysRoles(ctx context.Context, arg ListSysRolesParams) ([]SysRole, error) {
rows, err := q.db.Query(ctx, listSysRoles, arg.Limit, arg.Offset)
if err != nil {
return nil, err
}
defer rows.Close()
items := []SysRole{}
for rows.Next() {
var i SysRole
if err := rows.Scan(
&i.ID,
&i.Name,
&i.Code,
&i.CreatedAt,
&i.UpdatedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const updateSysRole = `-- name: UpdateSysRole :execrows
UPDATE sys_roles
SET name = coalesce($1, name)
WHERE id = $2
`
type UpdateSysRoleParams struct {
Name *string `json:"name"`
ID int32 `json:"id"`
}
func (q *Queries) UpdateSysRole(ctx context.Context, arg UpdateSysRoleParams) (int64, error) {
result, err := q.db.Exec(ctx, updateSysRole, arg.Name, arg.ID)
if err != nil {
return 0, err
}
return result.RowsAffected(), nil
}

View File

@@ -0,0 +1,426 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.31.1
// source: sys_user.sql
package db
import (
"context"
"time"
)
const clearSysUserRoles = `-- name: ClearSysUserRoles :exec
DELETE
FROM sys_user_role
WHERE user_id = $1
`
func (q *Queries) ClearSysUserRoles(ctx context.Context, userID int32) error {
_, err := q.db.Exec(ctx, clearSysUserRoles, userID)
return err
}
const countSysUsers = `-- name: CountSysUsers :one
SELECT COUNT(*)
FROM sys_users
`
func (q *Queries) CountSysUsers(ctx context.Context) (int64, error) {
row := q.db.QueryRow(ctx, countSysUsers)
var count int64
err := row.Scan(&count)
return count, err
}
const createSysUser = `-- name: CreateSysUser :exec
INSERT INTO sys_users (account, username, password_hash, avatar_id)
VALUES ($1, $2, $3, $4)
`
type CreateSysUserParams struct {
Account string `json:"account"`
Username string `json:"username"`
PasswordHash string `json:"password_hash"`
AvatarID *int32 `json:"avatar_id"`
}
func (q *Queries) CreateSysUser(ctx context.Context, arg CreateSysUserParams) error {
_, err := q.db.Exec(ctx, createSysUser,
arg.Account,
arg.Username,
arg.PasswordHash,
arg.AvatarID,
)
return err
}
const createSysUserRole = `-- name: CreateSysUserRole :exec
INSERT INTO sys_user_role (user_id, role_id)
VALUES ($1, $2)
`
type CreateSysUserRoleParams struct {
UserID int32 `json:"user_id"`
RoleID int32 `json:"role_id"`
}
func (q *Queries) CreateSysUserRole(ctx context.Context, arg CreateSysUserRoleParams) error {
_, err := q.db.Exec(ctx, createSysUserRole, arg.UserID, arg.RoleID)
return err
}
const deleteSysUser = `-- name: DeleteSysUser :execrows
DELETE
FROM sys_users
WHERE id = $1
AND id <> 1
`
func (q *Queries) DeleteSysUser(ctx context.Context, id int32) (int64, error) {
result, err := q.db.Exec(ctx, deleteSysUser, id)
if err != nil {
return 0, err
}
return result.RowsAffected(), nil
}
const getSysAdminMenus = `-- name: GetSysAdminMenus :many
SELECT id, name, path, component, type, hidden, sort, status, parent_id, icon, created_at, updated_at
FROM sys_menus
ORDER BY sort, id
`
func (q *Queries) GetSysAdminMenus(ctx context.Context) ([]SysMenu, error) {
rows, err := q.db.Query(ctx, getSysAdminMenus)
if err != nil {
return nil, err
}
defer rows.Close()
items := []SysMenu{}
for rows.Next() {
var i SysMenu
if err := rows.Scan(
&i.ID,
&i.Name,
&i.Path,
&i.Component,
&i.Type,
&i.Hidden,
&i.Sort,
&i.Status,
&i.ParentID,
&i.Icon,
&i.CreatedAt,
&i.UpdatedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getSysUserApis = `-- name: GetSysUserApis :many
SELECT DISTINCT a.id,
a.PATH,
a.METHOD
FROM sys_users u
JOIN sys_user_role ur ON u.id = ur.user_id
JOIN sys_role_permission rp ON rp.role_id = ur.role_id
JOIN sys_permissions p ON p.id = rp.permission_id
AND p.TYPE = 1
JOIN sys_api_permission ap ON ap.permission_id = p.id
JOIN sys_apis a ON a.id = ap.api_id
WHERE u.id = $1
`
type GetSysUserApisRow struct {
ID int32 `json:"id"`
Path string `json:"path"`
Method string `json:"method"`
}
func (q *Queries) GetSysUserApis(ctx context.Context, id int32) ([]GetSysUserApisRow, error) {
rows, err := q.db.Query(ctx, getSysUserApis, id)
if err != nil {
return nil, err
}
defer rows.Close()
items := []GetSysUserApisRow{}
for rows.Next() {
var i GetSysUserApisRow
if err := rows.Scan(&i.ID, &i.Path, &i.Method); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getSysUserByAccount = `-- name: GetSysUserByAccount :one
SELECT u.id,
u.account,
u.username,
u.password_hash,
u.avatar_id,
f.file_path AS avatar_url,
u.created_at,
u.updated_at
FROM sys_users u
LEFT JOIN files f ON u.avatar_id = f.id
WHERE u.account = $1
`
type GetSysUserByAccountRow struct {
ID int32 `json:"id"`
Account string `json:"account"`
Username string `json:"username"`
PasswordHash string `json:"password_hash"`
AvatarID *int32 `json:"avatar_id"`
AvatarUrl *string `json:"avatar_url"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt *time.Time `json:"updated_at"`
}
func (q *Queries) GetSysUserByAccount(ctx context.Context, account string) (GetSysUserByAccountRow, error) {
row := q.db.QueryRow(ctx, getSysUserByAccount, account)
var i GetSysUserByAccountRow
err := row.Scan(
&i.ID,
&i.Account,
&i.Username,
&i.PasswordHash,
&i.AvatarID,
&i.AvatarUrl,
&i.CreatedAt,
&i.UpdatedAt,
)
return i, err
}
const getSysUserByID = `-- name: GetSysUserByID :one
SELECT u.id,
u.account,
u.username,
u.avatar_id,
f.file_path AS avatar_url,
u.created_at,
u.updated_at
FROM sys_users u
LEFT JOIN files f ON u.avatar_id = f.id
WHERE u.id = $1
`
type GetSysUserByIDRow struct {
ID int32 `json:"id"`
Account string `json:"account"`
Username string `json:"username"`
AvatarID *int32 `json:"avatar_id"`
AvatarUrl *string `json:"avatar_url"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt *time.Time `json:"updated_at"`
}
func (q *Queries) GetSysUserByID(ctx context.Context, id int32) (GetSysUserByIDRow, error) {
row := q.db.QueryRow(ctx, getSysUserByID, id)
var i GetSysUserByIDRow
err := row.Scan(
&i.ID,
&i.Account,
&i.Username,
&i.AvatarID,
&i.AvatarUrl,
&i.CreatedAt,
&i.UpdatedAt,
)
return i, err
}
const getSysUserMenus = `-- name: GetSysUserMenus :many
SELECT DISTINCT m.id, m.name, m.path, m.component, m.type, m.hidden, m.sort, m.status, m.parent_id, m.icon, m.created_at, m.updated_at
FROM sys_users u
JOIN sys_user_role ur ON ur.user_id = u.id
JOIN sys_roles r ON r.id = ur.role_id
JOIN sys_role_permission rp ON rp.role_id = r.id
JOIN sys_permissions p ON p.id = rp.permission_id
JOIN sys_menu_permission mp ON mp.permission_id = p.id
JOIN sys_menus m ON m.id = mp.menu_id
WHERE u.id = $1
ORDER BY m.sort, m.id
`
func (q *Queries) GetSysUserMenus(ctx context.Context, id int32) ([]SysMenu, error) {
rows, err := q.db.Query(ctx, getSysUserMenus, id)
if err != nil {
return nil, err
}
defer rows.Close()
items := []SysMenu{}
for rows.Next() {
var i SysMenu
if err := rows.Scan(
&i.ID,
&i.Name,
&i.Path,
&i.Component,
&i.Type,
&i.Hidden,
&i.Sort,
&i.Status,
&i.ParentID,
&i.Icon,
&i.CreatedAt,
&i.UpdatedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getSysUserRoles = `-- name: GetSysUserRoles :many
SELECT r.id, r.name, r.code, r.created_at, r.updated_at
FROM sys_roles r
JOIN sys_user_role ur ON r.id = ur.role_id
WHERE ur.user_id = $1
`
func (q *Queries) GetSysUserRoles(ctx context.Context, userID int32) ([]SysRole, error) {
rows, err := q.db.Query(ctx, getSysUserRoles, userID)
if err != nil {
return nil, err
}
defer rows.Close()
items := []SysRole{}
for rows.Next() {
var i SysRole
if err := rows.Scan(
&i.ID,
&i.Name,
&i.Code,
&i.CreatedAt,
&i.UpdatedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const listSysUsers = `-- name: ListSysUsers :many
SELECT u.id,
u.account,
u.username,
u.avatar_id,
f.file_path AS avatar_url,
u.created_at,
u.updated_at
FROM sys_users u
LEFT JOIN files f ON u.avatar_id = f.id
ORDER BY u.id
LIMIT $1 OFFSET $2
`
type ListSysUsersParams struct {
Limit int32 `json:"limit"`
Offset int32 `json:"offset"`
}
type ListSysUsersRow struct {
ID int32 `json:"id"`
Account string `json:"account"`
Username string `json:"username"`
AvatarID *int32 `json:"avatar_id"`
AvatarUrl *string `json:"avatar_url"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt *time.Time `json:"updated_at"`
}
func (q *Queries) ListSysUsers(ctx context.Context, arg ListSysUsersParams) ([]ListSysUsersRow, error) {
rows, err := q.db.Query(ctx, listSysUsers, arg.Limit, arg.Offset)
if err != nil {
return nil, err
}
defer rows.Close()
items := []ListSysUsersRow{}
for rows.Next() {
var i ListSysUsersRow
if err := rows.Scan(
&i.ID,
&i.Account,
&i.Username,
&i.AvatarID,
&i.AvatarUrl,
&i.CreatedAt,
&i.UpdatedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const updateSysUser = `-- name: UpdateSysUser :execrows
UPDATE sys_users
SET username = coalesce($1, username),
avatar_id = CASE WHEN $2::boolean THEN $3 ELSE avatar_id END
WHERE id = $4
`
type UpdateSysUserParams struct {
Username *string `json:"username"`
UpdateAvatarID bool `json:"update_avatar_id"`
AvatarID *int32 `json:"avatar_id"`
ID int32 `json:"id"`
}
func (q *Queries) UpdateSysUser(ctx context.Context, arg UpdateSysUserParams) (int64, error) {
result, err := q.db.Exec(ctx, updateSysUser,
arg.Username,
arg.UpdateAvatarID,
arg.AvatarID,
arg.ID,
)
if err != nil {
return 0, err
}
return result.RowsAffected(), nil
}
const updateSysUserPassword = `-- name: UpdateSysUserPassword :execrows
UPDATE sys_users
SET password_hash = $2
WHERE id = $1
`
type UpdateSysUserPasswordParams struct {
ID int32 `json:"id"`
PasswordHash string `json:"password_hash"`
}
func (q *Queries) UpdateSysUserPassword(ctx context.Context, arg UpdateSysUserPasswordParams) (int64, error) {
result, err := q.db.Exec(ctx, updateSysUserPassword, arg.ID, arg.PasswordHash)
if err != nil {
return 0, err
}
return result.RowsAffected(), nil
}

View File

@@ -0,0 +1,96 @@
package handler
import (
"net/http"
"server/internal/model/common"
"server/internal/model/request"
"server/internal/pkg/httputil"
"server/internal/service"
)
type CategoryHandler struct {
categoryService *service.CategoryService
}
func NewCategoryHandler(categoryService *service.CategoryService) *CategoryHandler {
return &CategoryHandler{categoryService: categoryService}
}
func (h *CategoryHandler) ListPage(w http.ResponseWriter, r *http.Request) {
pagination := httputil.Pagination(r)
list, total, err := h.categoryService.ListPage(r.Context(), pagination)
if err != nil {
httputil.Fail(w, err)
return
}
resp := common.PageResponse{
Page: pagination.Page,
PageSize: pagination.PageSize,
List: list,
Total: total,
}
httputil.OkWithPage(w, &resp)
}
func (h *CategoryHandler) ListAll(w http.ResponseWriter, r *http.Request) {
list, err := h.categoryService.ListAll(r.Context())
if err != nil {
httputil.Fail(w, err)
return
}
httputil.Ok(w, list)
}
func (h *CategoryHandler) Create(w http.ResponseWriter, r *http.Request) {
var req request.CreateCategoryRequest
if err := httputil.BindJson(r, &req); err != nil {
httputil.Fail(w, err)
return
}
if err := h.categoryService.Create(r.Context(), req); err != nil {
httputil.Fail(w, err)
return
}
httputil.Ok(w)
}
func (h *CategoryHandler) Update(w http.ResponseWriter, r *http.Request) {
id, err := httputil.URLParamInt32(r, "id")
if err != nil {
httputil.Fail(w, err)
return
}
var req request.UpdateCategoryRequest
if err = httputil.BindJson(r, &req); err != nil {
httputil.Fail(w, err)
return
}
if err = h.categoryService.Update(r.Context(), id, req); err != nil {
httputil.Fail(w, err)
return
}
httputil.Ok(w)
}
func (h *CategoryHandler) Delete(w http.ResponseWriter, r *http.Request) {
id, err := httputil.URLParamInt32(r, "id")
if err != nil {
httputil.Fail(w, err)
return
}
if err = h.categoryService.Delete(r.Context(), id); err != nil {
httputil.Fail(w, err)
return
}
httputil.Ok(w)
}

View File

@@ -0,0 +1,20 @@
package handler
import (
"go.uber.org/fx"
)
var Module = fx.Module("handlers",
fx.Provide(
NewSysUserHandler,
NewSysRoleHandler,
NewSysMenuHandler,
NewSysApiHandler,
NewSysFileHandler,
NewSysPostHandler,
NewCategoryHandler,
NewPostHandler,
NewWebHandler,
),
)

83
internal/handler/post.go Normal file
View File

@@ -0,0 +1,83 @@
package handler
import (
"net/http"
"net/netip"
"server/internal/model/common"
"server/internal/pkg/errs"
"server/internal/pkg/httputil"
"server/internal/service"
"server/internal/utils"
"github.com/go-chi/chi/v5"
)
type PostHandler struct {
postService *service.PostService
}
func NewPostHandler(postService *service.PostService) *PostHandler {
return &PostHandler{postService: postService}
}
func (h *PostHandler) ListPage(w http.ResponseWriter, r *http.Request) {
pagination := httputil.Pagination(r)
list, total, err := h.postService.ListPage(r.Context(), pagination)
if err != nil {
httputil.Fail(w, err)
return
}
resp := common.PageResponse{
Page: pagination.Page,
PageSize: pagination.PageSize,
List: list,
Total: total,
}
httputil.OkWithPage(w, &resp)
}
func (h *PostHandler) GetPost(w http.ResponseWriter, r *http.Request) {
slug := chi.URLParam(r, "slug")
if slug == "" {
httputil.Fail(w, errs.ErrSlugRequired)
return
}
ip, err := netip.ParseAddr(utils.ClientIP(r))
if err != nil {
httputil.Fail(w, err)
return
}
post, err := h.postService.GetPost(r.Context(), slug, ip)
if err != nil {
httputil.Fail(w, err)
return
}
httputil.Ok(w, post)
}
func (h *PostHandler) ListCategoryStats(w http.ResponseWriter, r *http.Request) {
list, err := h.postService.ListCategoryStats(r.Context())
if err != nil {
httputil.Fail(w, err)
return
}
httputil.Ok(w, list)
}
func (h *PostHandler) ListArchives(w http.ResponseWriter, r *http.Request) {
list, err := h.postService.ListArchives(r.Context())
if err != nil {
httputil.Fail(w, err)
return
}
httputil.Ok(w, list)
}

112
internal/handler/sys_api.go Normal file
View File

@@ -0,0 +1,112 @@
package handler
import (
"net/http"
"server/internal/model/common"
"server/internal/model/request"
"server/internal/pkg/httputil"
"server/internal/service"
)
type SysApiHandler struct {
apiService *service.SysApiService
}
func NewSysApiHandler(apiService *service.SysApiService) *SysApiHandler {
return &SysApiHandler{apiService: apiService}
}
func (h *SysApiHandler) ListPage(w http.ResponseWriter, r *http.Request) {
pagination := httputil.Pagination(r)
list, total, err := h.apiService.ListPage(r.Context(), pagination)
if err != nil {
httputil.Fail(w, err)
return
}
resp := common.PageResponse{
Page: pagination.Page,
PageSize: pagination.PageSize,
List: list,
Total: total,
}
httputil.OkWithPage(w, &resp)
}
func (h *SysApiHandler) GetAllSysApis(w http.ResponseWriter, r *http.Request) {
list, err := h.apiService.GetAllSysApis(r.Context())
if err != nil {
httputil.Fail(w, err)
return
}
httputil.Ok(w, list)
}
func (h *SysApiHandler) GetApiGroupNames(w http.ResponseWriter, r *http.Request) {
list, err := h.apiService.GetApiGroupNames(r.Context())
if err != nil {
httputil.Fail(w, err)
return
}
httputil.Ok(w, list)
}
func (h *SysApiHandler) Create(w http.ResponseWriter, r *http.Request) {
var req request.CreateSysApiRequest
if err := httputil.BindJson(r, &req); err != nil {
httputil.Fail(w, err)
return
}
if err := h.apiService.Create(r.Context(), req); err != nil {
httputil.Fail(w, err)
return
}
httputil.Ok(w)
}
func (h *SysApiHandler) Update(w http.ResponseWriter, r *http.Request) {
var req request.UpdateSysApiRequest
id, err := httputil.URLParamInt32(r, "id")
if err != nil {
httputil.Fail(w, err)
return
}
if err = httputil.BindJson(r, &req); err != nil {
httputil.Fail(w, err)
return
}
if err = h.apiService.Update(r.Context(), id, req); err != nil {
httputil.Fail(w, err)
return
}
httputil.Ok(w)
}
func (h *SysApiHandler) Delete(w http.ResponseWriter, r *http.Request) {
id, err := httputil.URLParamInt32(r, "id")
if err != nil {
httputil.Fail(w, err)
return
}
if err = h.apiService.Delete(r.Context(), id); err != nil {
httputil.Fail(w, err)
return
}
httputil.Ok(w)
}

View File

@@ -0,0 +1,58 @@
package handler
import (
"net/http"
"server/internal/model/common"
"server/internal/pkg/httputil"
"server/internal/service"
)
type SysFileHandler struct {
service *service.SysFileService
}
func NewSysFileHandler(service *service.SysFileService) *SysFileHandler {
return &SysFileHandler{
service: service,
}
}
func (h *SysFileHandler) ListPage(w http.ResponseWriter, r *http.Request) {
pagination := httputil.Pagination(r)
list, total, err := h.service.ListPage(r.Context(), pagination)
if err != nil {
httputil.Fail(w, err)
return
}
resp := common.PageResponse{
Page: pagination.Page,
PageSize: pagination.PageSize,
List: list,
Total: total,
}
httputil.OkWithPage(w, &resp)
}
func (h *SysFileHandler) Upload(w http.ResponseWriter, r *http.Request) {
folder := r.FormValue("folder")
if folder == "" {
folder = "/"
}
_, header, err := r.FormFile("file")
if err != nil {
httputil.Fail(w, err)
return
}
file, err := h.service.Upload(r.Context(), folder, header)
if err != nil {
httputil.Fail(w, err)
return
}
httputil.Ok(w, file)
}

View File

@@ -0,0 +1,100 @@
package handler
import (
"net/http"
"server/internal/model/common"
"server/internal/model/request"
"server/internal/pkg/httputil"
"server/internal/service"
)
type SysMenuHandler struct {
menuService *service.SysMenuService
}
func NewSysMenuHandler(menuService *service.SysMenuService) *SysMenuHandler {
return &SysMenuHandler{menuService: menuService}
}
func (h *SysMenuHandler) ListPage(w http.ResponseWriter, r *http.Request) {
pagination := httputil.Pagination(r)
list, total, err := h.menuService.ListPage(r.Context(), pagination)
if err != nil {
httputil.Fail(w, err)
return
}
resp := common.PageResponse{
Page: pagination.Page,
PageSize: pagination.PageSize,
List: list,
Total: total,
}
httputil.OkWithPage(w, &resp)
}
func (h *SysMenuHandler) GetMenus(w http.ResponseWriter, r *http.Request) {
list, err := h.menuService.GetMenus(r.Context())
if err != nil {
httputil.Fail(w, err)
return
}
httputil.Ok(w, list)
}
func (h *SysMenuHandler) Create(w http.ResponseWriter, r *http.Request) {
var req request.CreateSysMenuRequest
if err := httputil.BindJson(r, &req); err != nil {
httputil.Fail(w, err)
return
}
if err := h.menuService.Create(r.Context(), req); err != nil {
httputil.Fail(w, err)
return
}
httputil.Ok(w)
}
func (h *SysMenuHandler) Update(w http.ResponseWriter, r *http.Request) {
var req request.UpdateSysMenuRequest
id, err := httputil.URLParamInt32(r, "id")
if err != nil {
httputil.Fail(w, err)
return
}
if err = httputil.BindJson(r, &req); err != nil {
httputil.Fail(w, err)
return
}
if err = h.menuService.Update(r.Context(), id, req); err != nil {
httputil.Fail(w, err)
return
}
httputil.Ok(w)
}
func (h *SysMenuHandler) Delete(w http.ResponseWriter, r *http.Request) {
id, err := httputil.URLParamInt32(r, "id")
if err != nil {
httputil.Fail(w, err)
return
}
if err = h.menuService.Delete(r.Context(), id); err != nil {
httputil.Fail(w, err)
return
}
httputil.Ok(w)
}

View File

@@ -0,0 +1,108 @@
package handler
import (
"net/http"
"server/internal/model/common"
"server/internal/model/request"
"server/internal/pkg/httputil"
"server/internal/service"
)
type SysPostHandler struct {
sysPostService *service.SysPostService
}
func NewSysPostHandler(postService *service.SysPostService) *SysPostHandler {
return &SysPostHandler{sysPostService: postService}
}
func (h *SysPostHandler) ListPage(w http.ResponseWriter, r *http.Request) {
pagination := httputil.Pagination(r)
list, total, err := h.sysPostService.ListPage(r.Context(), pagination)
if err != nil {
httputil.Fail(w, err)
}
resp := common.PageResponse{
Page: pagination.Page,
PageSize: pagination.PageSize,
List: list,
Total: total,
}
httputil.OkWithPage(w, &resp)
}
func (h *SysPostHandler) GetPostById(w http.ResponseWriter, r *http.Request) {
id, err := httputil.URLParamInt32(r, "id")
if err != nil {
httputil.Fail(w, err)
return
}
post, err := h.sysPostService.FindByID(r.Context(), id)
if err != nil {
httputil.Fail(w, err)
return
}
httputil.Ok(w, post)
}
func (h *SysPostHandler) Create(w http.ResponseWriter, r *http.Request) {
var req request.CreatePostRequest
if err := httputil.BindJson(r, &req); err != nil {
httputil.Fail(w, err)
return
}
postID, err := h.sysPostService.Create(r.Context(), req)
if err != nil {
httputil.Fail(w, err)
return
}
httputil.Ok(w, map[string]int32{
"post_id": postID,
})
}
func (h *SysPostHandler) Update(w http.ResponseWriter, r *http.Request) {
id, err := httputil.URLParamInt32(r, "id")
if err != nil {
httputil.Fail(w, err)
return
}
var req request.UpdatePostRequest
if err = httputil.BindJson(r, &req); err != nil {
httputil.Fail(w, err)
return
}
if err = h.sysPostService.Update(r.Context(), id, req); err != nil {
httputil.Fail(w, err)
return
}
httputil.Ok(w)
}
func (h *SysPostHandler) Delete(w http.ResponseWriter, r *http.Request) {
id, err := httputil.URLParamInt32(r, "id")
if err != nil {
httputil.Fail(w, err)
return
}
if err = h.sysPostService.Delete(r.Context(), id); err != nil {
httputil.Fail(w, err)
return
}
httputil.Ok(w)
}

View File

@@ -0,0 +1,177 @@
package handler
import (
"net/http"
"server/internal/model/common"
"server/internal/model/request"
"server/internal/pkg/httputil"
"server/internal/service"
)
type SysRoleHandler struct {
roleService *service.SysRoleService
}
func NewSysRoleHandler(roleService *service.SysRoleService) *SysRoleHandler {
return &SysRoleHandler{roleService: roleService}
}
func (h *SysRoleHandler) ListPage(w http.ResponseWriter, r *http.Request) {
pagination := httputil.Pagination(r)
list, total, err := h.roleService.ListPage(r.Context(), pagination)
if err != nil {
httputil.Fail(w, err)
return
}
resp := common.PageResponse{
Page: pagination.Page,
PageSize: pagination.PageSize,
List: list,
Total: total,
}
httputil.OkWithPage(w, &resp)
}
func (h *SysRoleHandler) GetRoleMenus(w http.ResponseWriter, r *http.Request) {
id, err := httputil.URLParamInt32(r, "id")
if err != nil {
httputil.Fail(w, err)
return
}
list, err := h.roleService.GetRoleMenus(r.Context(), id)
if err != nil {
httputil.Fail(w, err)
return
}
httputil.Ok(w, list)
}
func (h *SysRoleHandler) GetRoleApis(w http.ResponseWriter, r *http.Request) {
id, err := httputil.URLParamInt32(r, "id")
if err != nil {
httputil.Fail(w, err)
return
}
list, err := h.roleService.GetRoleApis(r.Context(), id)
if err != nil {
httputil.Fail(w, err)
return
}
httputil.Ok(w, list)
}
func (h *SysRoleHandler) GetRoles(w http.ResponseWriter, r *http.Request) {
roles, err := h.roleService.GetRoles(r.Context())
if err != nil {
httputil.Fail(w, err)
return
}
httputil.Ok(w, roles)
}
func (h *SysRoleHandler) Create(w http.ResponseWriter, r *http.Request) {
var req request.CreateSysRoleRequest
if err := httputil.BindJson(r, &req); err != nil {
httputil.Fail(w, err)
return
}
if err := h.roleService.Create(r.Context(), req); err != nil {
httputil.Fail(w, err)
return
}
httputil.Ok(w)
}
func (h *SysRoleHandler) Update(w http.ResponseWriter, r *http.Request) {
var req request.UpdateSysRoleRequest
id, err := httputil.URLParamInt32(r, "id")
if err != nil {
httputil.Fail(w, err)
return
}
if err = httputil.BindJson(r, &req); err != nil {
httputil.Fail(w, err)
return
}
if err = h.roleService.Update(r.Context(), id, req); err != nil {
httputil.Fail(w, err)
return
}
httputil.Ok(w)
}
func (h *SysRoleHandler) SetRoleMenus(w http.ResponseWriter, r *http.Request) {
var req request.SetSysRoleMenusRequest
id, err := httputil.URLParamInt32(r, "id")
if err != nil {
httputil.Fail(w, err)
return
}
if err = httputil.BindJson(r, &req); err != nil {
httputil.Fail(w, err)
return
}
if err = h.roleService.SetRoleMenus(r.Context(), id, req); err != nil {
httputil.Fail(w, err)
return
}
httputil.Ok(w)
}
func (h *SysRoleHandler) SetRoleApis(w http.ResponseWriter, r *http.Request) {
var req request.SetSysRoleApisRequest
id, err := httputil.URLParamInt32(r, "id")
if err != nil {
httputil.Fail(w, err)
return
}
if err = httputil.BindJson(r, &req); err != nil {
httputil.Fail(w, err)
return
}
if err = h.roleService.SetRoleApis(r.Context(), id, req); err != nil {
httputil.Fail(w, err)
return
}
httputil.Ok(w)
}
func (h *SysRoleHandler) Delete(w http.ResponseWriter, r *http.Request) {
id, err := httputil.URLParamInt32(r, "id")
if err != nil {
httputil.Fail(w, err)
return
}
if err = h.roleService.Delete(r.Context(), id); err != nil {
httputil.Fail(w, err)
return
}
httputil.Ok(w)
}

View File

@@ -0,0 +1,239 @@
package handler
import (
"net/http"
"server/internal/middleware"
"server/internal/model/common"
"server/internal/model/request"
"server/internal/pkg/errs"
"server/internal/pkg/httputil"
"server/internal/service"
"time"
)
type SysUserHandler struct {
userService *service.SysUserService
}
func NewSysUserHandler(userService *service.SysUserService) *SysUserHandler {
return &SysUserHandler{
userService: userService,
}
}
func (h *SysUserHandler) Login(w http.ResponseWriter, r *http.Request) {
var req request.LoginRequest
if err := httputil.BindJson(r, &req); err != nil {
httputil.Fail(w, errs.ErrInvalidCredentials)
return
}
res, err := h.userService.Login(r.Context(), req)
if err != nil {
httputil.Fail(w, err)
return
}
http.SetCookie(w, &http.Cookie{
Name: "refresh_token",
Value: res.RefreshToken,
Path: "/",
HttpOnly: true,
Secure: true,
SameSite: http.SameSiteNoneMode,
Expires: res.RefreshTokenExp,
})
httputil.Ok(w, map[string]any{
"access_token": res.AccessToken,
"access_token_exp": res.AccessTokenExp,
})
}
func (h *SysUserHandler) Logout(w http.ResponseWriter, r *http.Request) {
http.SetCookie(w, &http.Cookie{
Name: "refresh_token",
Value: "",
Path: "/",
HttpOnly: true,
Secure: true,
SameSite: http.SameSiteNoneMode,
MaxAge: -1,
Expires: time.Unix(0, 0),
})
httputil.Ok(w)
}
func (h *SysUserHandler) RefreshToken(w http.ResponseWriter, r *http.Request) {
cookie, err := r.Cookie("refresh_token")
if err != nil {
httputil.Fail(w, errs.ErrInvalidRefreshToken)
return
}
res, err := h.userService.RefreshToken(r.Context(), cookie.Value)
if err != nil {
httputil.Fail(w, err)
return
}
httputil.Ok(w, map[string]any{
"access_token": res.AccessToken,
"access_token_exp": res.AccessTokenExp,
})
}
func (h *SysUserHandler) ListPage(w http.ResponseWriter, r *http.Request) {
pagination := httputil.Pagination(r)
list, total, err := h.userService.ListPage(r.Context(), pagination)
if err != nil {
httputil.Fail(w, err)
return
}
resp := common.PageResponse{
Page: pagination.Page,
PageSize: pagination.PageSize,
List: list,
Total: total,
}
httputil.OkWithPage(w, &resp)
}
func (h *SysUserHandler) GetUserInfo(w http.ResponseWriter, r *http.Request) {
claims, ok := middleware.GetClaims(r.Context())
if !ok || claims.UserID == 0 {
httputil.Fail(w, errs.ErrUnauthorized)
return
}
isAdmin, _ := r.Context().Value(middleware.IsAdminKey).(bool)
uid := claims.UserID
user, err := h.userService.GetUserInfo(r.Context(), uid, isAdmin)
if err != nil {
httputil.Fail(w, err)
return
}
httputil.Ok(w, user)
}
func (h *SysUserHandler) GetRoles(w http.ResponseWriter, r *http.Request) {
id, err := httputil.URLParamInt32(r, "id")
if err != nil {
httputil.Fail(w, err)
return
}
list, err := h.userService.GetRoles(r.Context(), id)
if err != nil {
httputil.Fail(w, err)
return
}
httputil.Ok(w, list)
}
func (h *SysUserHandler) Create(w http.ResponseWriter, r *http.Request) {
var req request.CreateSysUserRequest
if err := httputil.BindJson(r, &req); err != nil {
httputil.Fail(w, err)
return
}
if err := h.userService.Create(r.Context(), req); err != nil {
httputil.Fail(w, err)
return
}
httputil.Ok(w)
}
func (h *SysUserHandler) Update(w http.ResponseWriter, r *http.Request) {
var req request.UpdateSysUserRequest
id, err := httputil.URLParamInt32(r, "id")
if err != nil {
httputil.Fail(w, err)
return
}
if err = httputil.BindJson(r, &req); err != nil {
httputil.Fail(w, err)
return
}
if err = h.userService.Update(r.Context(), id, req); err != nil {
httputil.Fail(w, err)
return
}
httputil.Ok(w)
}
func (h *SysUserHandler) SetRoles(w http.ResponseWriter, r *http.Request) {
var req request.SetSysUserRolesRequest
id, err := httputil.URLParamInt32(r, "id")
if err != nil {
httputil.Fail(w, err)
return
}
if err = httputil.BindJson(r, &req); err != nil {
httputil.Fail(w, err)
return
}
if err = h.userService.SetRoles(r.Context(), id, req); err != nil {
httputil.Fail(w, err)
return
}
httputil.Ok(w)
}
func (h *SysUserHandler) UpdatePassword(w http.ResponseWriter, r *http.Request) {
var req request.UpdateSysUserPassword
id, err := httputil.URLParamInt32(r, "id")
if err != nil {
httputil.Fail(w, err)
return
}
if err = httputil.BindJson(r, &req); err != nil {
httputil.Fail(w, err)
return
}
if err = h.userService.UpdatePassword(r.Context(), id, req); err != nil {
httputil.Fail(w, err)
return
}
httputil.Ok(w)
}
func (h *SysUserHandler) Delete(w http.ResponseWriter, r *http.Request) {
id, err := httputil.URLParamInt32(r, "id")
if err != nil {
httputil.Fail(w, err)
return
}
if err = h.userService.Delete(r.Context(), id); err != nil {
httputil.Fail(w, err)
return
}
httputil.Ok(w)
}

30
internal/handler/web.go Normal file
View File

@@ -0,0 +1,30 @@
package handler
import (
"net/http"
"server/internal/pkg/httputil"
"server/internal/utils"
)
type WebHandler struct {
}
func NewWebHandler() *WebHandler {
return &WebHandler{}
}
func (h *WebHandler) GetClientInfo(w http.ResponseWriter, r *http.Request) {
ip := utils.ClientIP(r)
userAgent := r.UserAgent()
httputil.Ok(w, map[string]any{
"ip": ip,
"userAgent": userAgent,
"method": r.Method,
"host": r.Host,
"path": r.URL.Path,
"origin": r.Header.Get("Origin"),
"accept": r.Header.Get("Accept"),
"acceptLanguage": r.Header.Get("Accept-Language"),
})
}

101
internal/middleware/auth.go Normal file
View File

@@ -0,0 +1,101 @@
package middleware
import (
"context"
"net/http"
db "server/internal/db/sqlc"
"server/internal/pkg/cache"
"server/internal/pkg/errs"
"server/internal/pkg/httputil"
"strings"
"github.com/go-chi/chi/v5"
)
const (
IsAdminKey contextKey = "is_admin"
)
type AuthMiddleware struct {
queries *db.Queries
cache *cache.Caches
}
func NewAuthMiddleware(queries *db.Queries, cache *cache.Caches) *AuthMiddleware {
return &AuthMiddleware{
cache: cache,
queries: queries,
}
}
func (m *AuthMiddleware) Middleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
claims, ok := GetClaims(ctx)
if !ok || claims.UserID == 0 {
httputil.Fail(w, errs.ErrUnauthorized)
return
}
// 判断是否有管理员权限 目前只判断uid是否为1
isAdmin := userIsAdmin(claims.UserID)
if isAdmin {
ctx = context.WithValue(ctx, IsAdminKey, isAdmin)
next.ServeHTTP(w, r.WithContext(ctx))
return
}
hasPermission, err := userHasApiPermission(ctx, r, m.queries, claims.UserID, m.cache)
if err != nil {
httputil.Fail(w, err)
return
}
if !hasPermission {
httputil.Fail(w, errs.ErrPermissionDenied)
return
}
next.ServeHTTP(w, r)
})
}
func userIsAdmin(uid int32) bool {
if uid == 1 {
return true
}
return false
}
func userHasApiPermission(ctx context.Context, r *http.Request, queries *db.Queries, uid int32, cache *cache.Caches) (bool, error) {
var (
apis []db.GetSysUserApisRow
err error
)
apis, ok := cache.SysUserApisCache.GetIfPresent(uid)
if !ok {
apis, err = queries.GetSysUserApis(ctx, uid)
if err != nil {
return false, err
}
cache.SysUserApisCache.Set(uid, apis)
}
requestPath := chi.RouteContext(r.Context()).RoutePattern()
requestPath = strings.TrimPrefix(requestPath, "/api")
requestMethod := r.Method
for _, api := range apis {
if api.Path == requestPath && api.Method == requestMethod {
return true, nil
}
}
return false, nil
}

184
internal/middleware/jwt.go Normal file
View File

@@ -0,0 +1,184 @@
package middleware
import (
"context"
"errors"
"net/http"
"server/internal/config"
"server/internal/pkg/errs"
"server/internal/pkg/httputil"
"strings"
"time"
"github.com/golang-jwt/jwt/v5"
)
type JWTMiddleware struct {
cfg *config.JWTConfig
}
type contextKey string
const UserContextKey contextKey = "user"
const RefreshTokenType = "refresh"
type Claims struct {
UserID int32 `json:"user_id"`
jwt.RegisteredClaims
}
type RefreshClaims struct {
UserID int32 `json:"user_id"`
Type string `json:"type"`
jwt.RegisteredClaims
}
func NewJWTMiddleware(cfg *config.Config) *JWTMiddleware {
jwtCfg := cfg.JWTConfig
return &JWTMiddleware{cfg: &jwtCfg}
}
func GetClaims(ctx context.Context) (*Claims, bool) {
claims, ok := ctx.Value(UserContextKey).(*Claims)
return claims, ok
}
// ParseToken 解析accessToken
func (m *JWTMiddleware) ParseToken(tokenStr string) (*Claims, error) {
token, err := jwt.ParseWithClaims(
tokenStr,
&Claims{},
func(token *jwt.Token) (interface{}, error) {
return []byte(m.cfg.Secret), nil
},
)
if err != nil || !token.Valid {
return nil, errs.ErrInvalidToken
}
claims, ok := token.Claims.(*Claims)
if !ok {
return nil, errs.ErrInvalidTokenClaims
}
if claims.UserID == 0 {
return nil, errs.ErrInvalidTokenClaims
}
return claims, nil
}
// ParseRefreshToken 解析RefreshToken
func (m *JWTMiddleware) ParseRefreshToken(tokenStr string) (*RefreshClaims, error) {
token, err := jwt.ParseWithClaims(
tokenStr,
&RefreshClaims{},
func(token *jwt.Token) (interface{}, error) {
return []byte(m.cfg.Secret), nil
},
)
if err != nil {
// Token 已过期
if errors.Is(err, jwt.ErrTokenExpired) {
return nil, errs.ErrExpiredRefreshToken
}
// 签名错误、格式错误、非法 Token
return nil, errs.ErrInvalidRefreshToken
}
if !token.Valid {
return nil, errs.ErrInvalidRefreshToken
}
claims, ok := token.Claims.(*RefreshClaims)
if !ok {
return nil, errs.ErrInvalidRefreshToken
}
if claims.UserID == 0 {
return nil, errs.ErrInvalidRefreshToken
}
if claims.Type != RefreshTokenType {
return nil, errs.ErrInvalidRefreshToken
}
return claims, nil
}
func (m *JWTMiddleware) Middleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
auth := r.Header.Get("Authorization")
if auth == "" {
httputil.Fail(w, errs.ErrUnauthenticated)
return
}
parts := strings.SplitN(auth, " ", 2)
if len(parts) != 2 || parts[0] != "Bearer" {
httputil.Fail(w, errs.ErrUnauthenticated)
return
}
tokenStr := parts[1]
claims, err := m.ParseToken(tokenStr)
if err != nil {
httputil.Fail(w, errs.ErrInvalidToken)
return
}
ctx := context.WithValue(r.Context(), UserContextKey, claims)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
func (m *JWTMiddleware) GenerateAccessToken(userID int32) (string, time.Time, error) {
now := time.Now()
expiresAt := now.Add(m.cfg.Expire)
accessClaims := Claims{
UserID: userID,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(expiresAt),
IssuedAt: jwt.NewNumericDate(now),
},
}
accessToken := jwt.NewWithClaims(jwt.SigningMethodHS256, accessClaims)
token, err := accessToken.SignedString([]byte(m.cfg.Secret))
if err != nil {
return "", time.Time{}, err
}
return token, expiresAt, nil
}
func (m *JWTMiddleware) GenerateRefreshToken(userID int32) (string, time.Time, error) {
now := time.Now()
expiresAt := now.Add(m.cfg.RefreshExpire)
refreshClaims := RefreshClaims{
UserID: userID,
Type: RefreshTokenType,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(expiresAt),
IssuedAt: jwt.NewNumericDate(now),
},
}
refreshToken := jwt.NewWithClaims(jwt.SigningMethodHS256, refreshClaims)
token, err := refreshToken.SignedString([]byte(m.cfg.Secret))
if err != nil {
return "", time.Time{}, err
}
return token, expiresAt, nil
}

View File

@@ -0,0 +1,103 @@
package middleware
import (
"encoding/json"
"log/slog"
"net/http"
"server/internal/model/common"
"server/internal/utils"
"time"
gonanoid "github.com/matoous/go-nanoid/v2"
)
type LoggerMiddleware struct {
}
type responseWriter struct {
http.ResponseWriter
statusCode int
bytes int
errorMsg string
}
func (rw *responseWriter) WriteHeader(code int) {
rw.statusCode = code
rw.ResponseWriter.WriteHeader(code)
}
func generateRequestID() string {
id, _ := gonanoid.New(16) // 16字符
return id
}
// extractErrorMessage 从响应体中提取错误信息
func (rw *responseWriter) extractErrorMessage(body []byte) string {
var resp common.Response
if err := json.Unmarshal(body, &resp); err == nil && resp.Message != "" {
return resp.Message
}
return string(body)
}
func (rw *responseWriter) Write(b []byte) (int, error) {
n, err := rw.ResponseWriter.Write(b)
rw.bytes += n
// 只在错误状态码且未记录错误时处理
if rw.statusCode >= 400 && rw.errorMsg == "" && n > 0 {
rw.errorMsg = rw.extractErrorMessage(b[:n])
}
return n, err
}
func NewLoggerMiddleware() *LoggerMiddleware {
return &LoggerMiddleware{}
}
func (m *LoggerMiddleware) Middleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
reqID := generateRequestID()
wrapped := &responseWriter{
ResponseWriter: w,
statusCode: http.StatusOK,
}
next.ServeHTTP(wrapped, r)
duration := time.Since(start)
ip := utils.ClientIP(r)
fullPath := r.URL.Path
if r.URL.RawQuery != "" {
fullPath = fullPath + "?" + r.URL.RawQuery
}
fields := []any{
"request_id", reqID,
"method", r.Method,
"path", fullPath,
"status", wrapped.statusCode,
"duration", duration,
"client_ip", ip,
}
if wrapped.errorMsg != "" {
fields = append(fields, "error", wrapped.errorMsg)
}
switch {
case wrapped.statusCode >= 500:
slog.Error("request", fields...)
case wrapped.statusCode >= 400:
slog.Warn("request", fields...)
default:
slog.Info("request", fields...)
}
})
}

View File

@@ -0,0 +1,6 @@
package common
type Pagination struct {
Page int32 `form:"page,default=1" validate:"gte=1"`
PageSize int32 `form:"page_size,default=10" validate:"gte=1,lte=100"`
}

View File

@@ -0,0 +1,16 @@
package common
// Response 通用响应结构体
type Response struct {
Message string `json:"msg,omitempty"` // 消息说明
Data interface{} `json:"data,omitempty"` // 业务数据
}
// PageResponse 分页查询通用相应结构体
type PageResponse struct {
Message string `json:"msg,omitempty"` // 消息说明
Page int32 `json:"page"` // 当前页码
PageSize int32 `json:"page_size"` // 每页数量
List interface{} `json:"list,omitempty"` // 业务数据
Total int64 `json:"total"` // 总记录数
}

View File

@@ -0,0 +1,33 @@
package enum
// Status 通用状态枚举
type Status int16
const (
StatusDisabled Status = iota // 0
StatusEnabled // 1
)
// MenuType 菜单类型枚举
type MenuType int16
const (
MenuTypeDirectory MenuType = iota // 0
MenuTypeMenu // 1
MenuTypeButton // 2
)
type PermissionType int16
const (
PermissionTypeMenu PermissionType = iota // 0
PermissionTypeApi // 1
)
type PostStatus int16
const (
PostStatusDraft PostStatus = iota // 0: 草稿
PostStatusPublished // 1: 已发布
PostStatusOffline // 2: 已下线
)

View File

@@ -0,0 +1,11 @@
package request
type CreateCategoryRequest struct {
Name string `json:"name" validate:"required,min=1"`
Code string `json:"code" validate:"required,min=1"`
}
type UpdateCategoryRequest struct {
Name *string `json:"name" validate:"omitempty,min=1"`
Code *string `json:"code" validate:"omitempty,min=1"`
}

View File

@@ -0,0 +1,30 @@
package request
import (
"time"
)
type CreatePostRequest struct {
Title string `json:"title" validate:"required,min=1"`
CoverID *int32 `json:"cover_id" validate:"required,min=1"`
Slug string `json:"slug" validate:"required,min=0"`
Content string `json:"content" validate:"required,min=0"`
Summary string `json:"summary" validate:"required,min=0"`
Status *int16 `json:"status" validate:"required,oneof=0 1 2"`
Sort *int32 `json:"sort" validate:"required,min=0"`
PublishedAt time.Time `json:"published_at" validate:"required"`
CategoryID *int32 `json:"category_id" validate:"required,min=1"`
}
type UpdatePostRequest struct {
Title *string `json:"title" validate:"omitempty,min=1"`
CoverID *int32 `json:"cover_id" validate:"omitempty,min=1"`
Slug *string `json:"slug" validate:"omitempty,min=1"`
Content *string `json:"content" validate:"required,min=0"`
Summary *string `json:"summary" validate:"omitempty,min=1"`
Status *int16 `json:"status" validate:"omitempty,oneof=0 1 2"`
View *int32 `json:"view" validate:"omitempty,min=1"`
Sort *int32 `json:"sort" validate:"omitempty,min=0"`
PublishedAt *time.Time `json:"published_at" validate:"omitempty"`
CategoryID *int32 `json:"category_id" validate:"required,min=1"`
}

View File

@@ -0,0 +1,17 @@
package request
type CreateSysApiRequest struct {
Name string `json:"name" validate:"required,min=1,max=100"`
GroupName string `json:"group_name" validate:"required,min=1,max=100"`
Path string `json:"path" validate:"required,min=1,max=100"`
Method string `json:"method" validate:"required,oneof=GET POST PUT PATCH DELETE"`
Sort *int32 `json:"sort" validate:"required,min=0"`
}
type UpdateSysApiRequest struct {
Name string `json:"name" validate:"required,min=1,max=100"`
GroupName string `json:"group_name" validate:"required,min=1,max=100"`
Path string `json:"path" validate:"required,min=1,max=100"`
Method string `json:"method" validate:"required,oneof=GET POST PUT PATCH DELETE"`
Sort *int32 `json:"sort" validate:"required,min=0"`
}

View File

@@ -0,0 +1,31 @@
package request
import (
"server/internal/pkg/validator"
)
type CreateSysMenuRequest struct {
Name string `json:"name" validate:"required,min=1,max=100"`
Path string `json:"path" validate:"required,min=1,max=100"`
Type *int16 `json:"type" validate:"required,oneof=0 1 2"`
Component *string `json:"component" validate:"omitempty"`
Hidden *bool `json:"hidden" validate:"omitempty"`
Sort *int32 `json:"sort" validate:"required,min=0"`
Status *int16 `json:"status" validate:"required,oneof=0 1"`
ParentID *int32 `json:"parent_id" validate:"omitempty,gt=0"`
Icon *int32 `json:"icon" validate:"omitempty"`
PermissionCode string `json:"permission_code" validate:"required,min=1,max=100"`
}
type UpdateSysMenuRequest struct {
Name *string `json:"name" validate:"omitempty,min=1,max=100"`
Path *string `json:"path" validate:"omitempty,min=1,max=100"`
Type *int16 `json:"type" validate:"omitempty,oneof=0 1 2"`
Component *string `json:"component" validate:"omitempty"`
Hidden *bool `json:"hidden" validate:"omitempty"`
Sort *int32 `json:"sort" validate:"omitempty,min=0"`
Status *int16 `json:"status" validate:"omitempty,oneof=0 1"`
ParentID validator.NullInt32 `json:"parent_id" validate:"omitempty"`
Icon *int32 `json:"icon" validate:"omitempty"`
PermissionCode *string `json:"permission_code" validate:"omitempty,min=1,max=100"`
}

View File

@@ -0,0 +1 @@
package request

View File

@@ -0,0 +1,18 @@
package request
type CreateSysRoleRequest struct {
Name string `json:"name" validate:"required,min=1,max=100"`
Code string `json:"code" validate:"required,max=100"`
}
type UpdateSysRoleRequest struct {
Name *string `json:"name" validate:"min=1,max=100"`
}
type SetSysRoleMenusRequest struct {
MenuIDs []int32 `json:"menu_ids" validate:"required,dive,gt=0"`
}
type SetSysRoleApisRequest struct {
ApiIDs []int32 `json:"api_ids" validate:"required,dive,gt=0"`
}

View File

@@ -0,0 +1,30 @@
package request
import (
"server/internal/pkg/validator"
)
type CreateSysUserRequest struct {
Username string `json:"username" validate:"required"`
Account string `json:"account" validate:"required,min=5,max=100"`
Password string `json:"password" validate:"required,min=6,max=255"`
AvatarID *int32 `json:"avatar_id" validate:"omitempty"`
}
type UpdateSysUserRequest struct {
Username *string `json:"username" validate:"omitempty,min=0,max=50"`
AvatarID validator.NullInt32 `json:"avatar_id" validate:"omitempty"`
}
type UpdateSysUserPassword struct {
Password string `json:"password" validate:"required,min=6,max=255"`
}
type SetSysUserRolesRequest struct {
RoleIDs []int32 `json:"role_ids" validate:"required,dive,gt=0"`
}
type LoginRequest struct {
Account string `json:"account" validate:"required,min=5,max=100"`
Password string `json:"password" validate:"required,min=6,max=255"`
}

View File

@@ -0,0 +1,25 @@
package response
import (
"time"
)
type ArchivePost struct {
ID int32 `json:"id"`
Slug string `json:"slug"`
Title string `json:"title"`
PublishedAt time.Time `json:"published_at"`
PublishedAtDisplay string `json:"published_at_display"`
CategoryName string `json:"category_name"`
}
type ArchiveMonth struct {
Month int `json:"month"`
Archive []ArchivePost `json:"list"`
}
type ArchiveYear struct {
Year int `json:"year"`
Total int `json:"total"`
ArchiveMonth []ArchiveMonth `json:"list"`
}

View File

@@ -0,0 +1,15 @@
package response
import (
db "server/internal/db/sqlc"
"server/internal/pkg/httputil"
)
func ToFiles(files []db.File) []db.File {
result := make([]db.File, len(files))
for i := range files {
result[i] = files[i]
result[i].FilePath = httputil.BuildFileUrl(&files[i].FilePath)
}
return result
}

View File

@@ -0,0 +1,45 @@
package response
import (
db "server/internal/db/sqlc"
"server/internal/pkg/httputil"
"time"
)
type SysUserRolesResponse struct {
ID uint `json:"id"`
Code string `json:"code"`
Description string `json:"description"`
}
type SysUserInfo struct {
ID int32 `json:"id"`
Account string `json:"account"`
Username string `json:"username"`
AvatarUrl string `json:"avatar_url"`
Roles []string `json:"roles"`
Menus []db.SysMenu `json:"menus"`
}
type LoginResponse struct {
AccessToken string `json:"access_token"`
AccessTokenExp time.Time `json:"access_token_exp"`
RefreshToken string `json:"refresh_token"`
RefreshTokenExp time.Time `json:"refresh_token_exp"`
}
func NewSysUserInfo(user db.GetSysUserByIDRow, roles []db.SysRole, menus []db.SysMenu) *SysUserInfo {
roleCodes := make([]string, len(roles))
for i, role := range roles {
roleCodes[i] = role.Code
}
return &SysUserInfo{
ID: user.ID,
Account: user.Account,
Username: user.Username,
AvatarUrl: httputil.BuildFileUrl(user.AvatarUrl),
Roles: roleCodes,
Menus: menus,
}
}

31
internal/pkg/cache/cache.go vendored Normal file
View File

@@ -0,0 +1,31 @@
package cache
import (
"server/internal/db/sqlc"
"github.com/maypok86/otter/v2"
)
type Caches struct {
SysUserApisCache *otter.Cache[int32, []db.GetSysUserApisRow]
}
func NewCaches() *Caches {
sysUserApisCache := otter.Must(&otter.Options[int32, []db.GetSysUserApisRow]{
MaximumSize: 1_000,
})
return &Caches{
SysUserApisCache: sysUserApisCache,
}
}
// ClearSysUserCache 清理单个用户缓存
func (c *Caches) ClearSysUserCache(userID int32) {
c.SysUserApisCache.Invalidate(userID)
}
// ClearAllSysUserCache 清理所有用户缓存
func (c *Caches) ClearAllSysUserCache() {
c.SysUserApisCache.InvalidateAll()
}

View File

@@ -0,0 +1,49 @@
package dberr
import (
"errors"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgconn"
)
const (
SysUserAccountKey = "sys_users_account_key"
SysRoleCodeKey = "sys_roles_code_key"
PostSlugKey = "posts_slug_key"
SysPermissionsCodeKey = "sys_permissions_code_key"
SysApisMethodPathKey = "sys_apis_method_path_key"
CategoryCodeKey = "categories_code_key"
)
func MapRowsAffected(rows int64, err error, notFoundErr error) error {
if err != nil {
return err
}
if rows == 0 {
return notFoundErr
}
return nil
}
func MapNoRows(err error, notFoundErr error) error {
if errors.Is(err, pgx.ErrNoRows) {
return notFoundErr
}
return err
}
func MapUniqueViolation(err error, constraint string, uniqueErr error) error {
if err == nil {
return nil
}
var pgErr *pgconn.PgError
if errors.As(err, &pgErr) &&
pgErr.Code == "23505" &&
pgErr.ConstraintName == constraint {
return uniqueErr
}
return err
}

48
internal/pkg/errs/errs.go Normal file
View File

@@ -0,0 +1,48 @@
package errs
import "net/http"
type AppError struct {
HTTPCode int
Msg string
}
func (e *AppError) Error() string {
return e.Msg
}
func New(httpCode int, msg string) *AppError {
return &AppError{
HTTPCode: httpCode,
Msg: msg,
}
}
var (
ErrInvalidCredentials = New(http.StatusUnauthorized, "用户名或密码错误")
ErrUnauthenticated = New(http.StatusUnauthorized, "用户未登录或登录已失效")
ErrUnauthorized = New(http.StatusUnauthorized, "认证失败:无法获取当前用户信息")
ErrInvalidToken = New(http.StatusUnauthorized, "登录凭证无效")
ErrInvalidTokenClaims = New(http.StatusUnauthorized, "登录凭证解析失败")
ErrInvalidRefreshToken = New(http.StatusBadRequest, "invalid_grant")
ErrExpiredRefreshToken = New(http.StatusBadRequest, "invalid_grant")
ErrUserNotFound = New(http.StatusNotFound, "用户数据不存在")
ErrCategoryNotFound = New(http.StatusNotFound, "分类数据不存在")
ErrSysApiNotFound = New(http.StatusNotFound, "接口数据不存在")
ErrPostNotFound = New(http.StatusNotFound, "文章数据不存在")
ErrSysMenuNotFound = New(http.StatusNotFound, "菜单数据不存在")
ErrSysRoleNotFound = New(http.StatusNotFound, "角色数据不存在")
ErrCannotDeleteSuperAdmin = New(http.StatusForbidden, "超级管理员账号无法被删除")
ErrSlugRequired = New(http.StatusBadRequest, "slug不能为空")
ErrIDRequired = New(http.StatusBadRequest, "id不能为空")
ErrPermissionDenied = New(http.StatusForbidden, "没有权限访问该资源")
ErrInvalidID = New(http.StatusBadRequest, "id非法请检查传入的id")
ErrEmptyBody = New(http.StatusBadRequest, "请求内容(body)不能为空")
ErrInvalidJSON = New(http.StatusBadRequest, "请求数据格式错误")
ErrAccountAlreadyExists = New(http.StatusBadRequest, "账号已存在")
ErrCodeAlreadyExists = New(http.StatusBadRequest, "角色编码已存在")
ErrSlugAlreadyExists = New(http.StatusBadRequest, "slug已存在")
ErrPermissionCodeAlreadyExists = New(http.StatusBadRequest, "权限编码已存在")
ErrCategoryCodeAlreadyExists = New(http.StatusBadRequest, "分类编码已存在")
ErrSysApiMethodPathAlreadyExists = New(http.StatusBadRequest, "接口方法(method)路径(path)已存在")
)

View File

@@ -0,0 +1,90 @@
package httputil
import (
"encoding/json"
"errors"
"io"
"net/http"
"net/url"
"server/internal/config"
"server/internal/model/common"
"server/internal/pkg/errs"
"server/internal/pkg/validator"
"strconv"
"github.com/go-chi/chi/v5"
)
// Pagination 分页请求参数
func Pagination(r *http.Request) *common.Pagination {
q := r.URL.Query()
// page 默认 1
page := int32(1)
if p := q.Get("page"); p != "" {
if v, err := strconv.Atoi(p); err == nil && v >= 1 {
page = int32(v)
}
}
// pageSize 默认 10
pageSize := int32(10)
if ps := q.Get("page_size"); ps != "" {
if v, err := strconv.Atoi(ps); err == nil && v >= 1 && v <= 100 {
pageSize = int32(v)
}
}
return &common.Pagination{
Page: page,
PageSize: pageSize,
}
}
func URLParamInt32(r *http.Request, key string) (int32, error) {
v := chi.URLParam(r, key)
if v == "" {
return 0, errs.ErrIDRequired
}
n, err := strconv.ParseInt(v, 10, 32)
if err != nil {
return 0, errs.ErrInvalidID
}
return int32(n), nil
}
func BindJson(r *http.Request, dest any) error {
if err := json.NewDecoder(r.Body).Decode(dest); err != nil {
if errors.Is(err, io.EOF) {
return errs.ErrEmptyBody
}
return errs.ErrInvalidJSON
}
if err := validator.Struct(dest); err != nil {
return err
}
return nil
}
func BuildFileUrl(path *string) string {
if path == nil || *path == "" {
return ""
}
baseURL := config.GetString("file.base_url")
if baseURL == "" {
return ""
}
result, err := url.JoinPath(baseURL, *path)
if err != nil {
return ""
}
return result
}

View File

@@ -0,0 +1,65 @@
package httputil
import (
"encoding/json"
"errors"
"log/slog"
"net/http"
"server/internal/model/common"
"server/internal/pkg/errs"
validatorI18 "server/internal/pkg/validator"
"strings"
"github.com/go-playground/validator/v10"
)
func respondWithJSON(w http.ResponseWriter, code int, data any) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(code)
if err := json.NewEncoder(w).Encode(data); err != nil {
slog.Error("respondWithJSON: failed to encode response", "error", err)
}
}
func Ok(w http.ResponseWriter, data ...any) {
resp := common.Response{
Message: "ok",
}
if len(data) > 0 && data[0] != nil {
resp.Data = data[0]
}
respondWithJSON(w, http.StatusOK, resp)
}
func OkWithPage(w http.ResponseWriter, pageResp *common.PageResponse) {
pageResp.Message = "ok"
respondWithJSON(w, http.StatusOK, pageResp)
}
// Fail 响应失败
func Fail(w http.ResponseWriter, err error) {
var msg string
// 默认code 500
httpStatusCode := http.StatusInternalServerError
var validationErrs validator.ValidationErrors
var appErr *errs.AppError
if errors.As(err, &validationErrs) {
httpStatusCode = http.StatusBadRequest
msgs := validatorI18.Translate(err)
msg = strings.Join(msgs, "; ")
} else if errors.As(err, &appErr) {
httpStatusCode = appErr.HTTPCode
msg = appErr.Msg
} else {
msg = err.Error()
}
resp := common.Response{
Message: msg,
}
respondWithJSON(w, httpStatusCode, resp)
}

View File

@@ -0,0 +1,76 @@
package logger
import (
"io"
"log/slog"
"os"
"strings"
"server/internal/config"
"gopkg.in/natefinch/lumberjack.v2"
)
// New 根据配置创建 slog.Logger并将其设置为默认 logger。
// 日志会写入文件(自动按大小/时间滚动),并可选同时输出到控制台。
func New(cfg *config.Config) *slog.Logger {
logCfg := cfg.Log
// 文件滚动写入
fileWriter := &lumberjack.Logger{
Filename: defaultStr(logCfg.Filename, "logs/app.log"),
MaxSize: defaultInt(logCfg.MaxSize, 100), // MB
MaxBackups: defaultInt(logCfg.MaxBackups, 30),
MaxAge: defaultInt(logCfg.MaxAge, 30), // days
Compress: logCfg.Compress,
}
var writer io.Writer = fileWriter
if logCfg.Console {
writer = io.MultiWriter(os.Stdout, fileWriter)
}
opts := &slog.HandlerOptions{
Level: parseLevel(logCfg.Level),
AddSource: false,
}
// 生产环境用 JSON便于日志采集/检索;开发环境用文本更易读
var handler slog.Handler
if config.IsDev() {
handler = slog.NewTextHandler(writer, opts)
} else {
handler = slog.NewJSONHandler(writer, opts)
}
l := slog.New(handler)
slog.SetDefault(l) // 让全局 slog.Info/Error 也走同一套配置
return l
}
func parseLevel(level string) slog.Level {
switch strings.ToLower(level) {
case "debug":
return slog.LevelDebug
case "warn", "warning":
return slog.LevelWarn
case "error":
return slog.LevelError
default:
return slog.LevelInfo
}
}
func defaultStr(v, def string) string {
if v == "" {
return def
}
return v
}
func defaultInt(v, def int) int {
if v <= 0 {
return def
}
return v
}

View File

@@ -0,0 +1,28 @@
package validator
import "encoding/json"
// NullInt32 Valid(false) + Set(true) = null | Valid(false) + Set(false) = nil
type NullInt32 struct {
Value int32
Valid bool
Set bool
}
func (i *NullInt32) UnmarshalJSON(data []byte) error {
i.Set = true
if string(data) == "null" {
i.Valid = false
return nil
}
var temp int32
if err := json.Unmarshal(data, &temp); err != nil {
return err
}
i.Value = temp
i.Valid = true
return nil
}

View File

@@ -0,0 +1,66 @@
package validator
import (
"errors"
"reflect"
"github.com/go-playground/locales/zh"
ut "github.com/go-playground/universal-translator"
"github.com/go-playground/validator/v10"
zhtranslations "github.com/go-playground/validator/v10/translations/zh"
)
var (
trans ut.Translator
validate *validator.Validate
)
func InitValidatorZh() {
langZh := zh.New()
uni := ut.New(langZh, langZh)
var found bool
trans, found = uni.GetTranslator("zh")
if !found {
panic("translator 'zh' not found")
}
err := zhtranslations.RegisterDefaultTranslations(validate, trans)
if err != nil {
panic("failed to register zh translations: " + err.Error())
}
}
func init() {
validate = validator.New()
// 注册自定义标签名称
validate.RegisterTagNameFunc(func(field reflect.StructField) string {
name := field.Tag.Get("json")
if name == "-" {
return ""
}
return name
})
// 初始化中文翻译器
InitValidatorZh()
}
func Translate(err error) []string {
var validateErrs validator.ValidationErrors
if errors.As(err, &validateErrs) {
messages := make([]string, 0, len(validateErrs))
for _, e := range validateErrs {
messages = append(messages, e.Translate(trans))
}
return messages
}
// 非校验错误 直接返回
return []string{err.Error()}
}
func Struct(s any) error {
return validate.Struct(s)
}

18
internal/router/client.go Normal file
View File

@@ -0,0 +1,18 @@
package router
import (
"server/internal/handler"
"github.com/go-chi/chi/v5"
)
func SetupPostRoutes(r chi.Router, h *handler.PostHandler) {
r.Get("/post", h.ListPage)
r.Get("/post/{slug}", h.GetPost)
r.Get("/category/stats", h.ListCategoryStats)
r.Get("/post/archive", h.ListArchives)
}
func SetupWebRoutes(r chi.Router, h *handler.WebHandler) {
}

80
internal/router/router.go Normal file
View File

@@ -0,0 +1,80 @@
package router
import (
"net/http"
"os"
"path/filepath"
"server/internal/config"
"server/internal/handler"
"server/internal/middleware"
"github.com/go-chi/chi/v5"
"go.uber.org/fx"
)
type Handlers struct {
fx.In
// auth
JwtMiddleware *middleware.JWTMiddleware
AuthMiddleware *middleware.AuthMiddleware
LoggerMiddleware *middleware.LoggerMiddleware
// system
SysUserHandler *handler.SysUserHandler
SysRoleHandler *handler.SysRoleHandler
SysMenuHandler *handler.SysMenuHandler
SysApiHandler *handler.SysApiHandler
SysFileHandler *handler.SysFileHandler
SysPostHandler *handler.SysPostHandler
CategoryHandler *handler.CategoryHandler
// web
PostHandler *handler.PostHandler
WebHandler *handler.WebHandler
}
func setupStaticFiles(r chi.Router) {
rootDir, _ := os.Getwd()
uploadsDir := filepath.Join(rootDir, "uploads")
r.Handle("/static/*", http.StripPrefix("/static/", http.FileServer(http.Dir(uploadsDir))))
}
func NewRouter(m Handlers) *chi.Mux {
r := chi.NewRouter()
r.Use(m.LoggerMiddleware.Middleware)
if config.IsDev() {
// 开放静态目录
setupStaticFiles(r)
}
r.Route("/api/admin", func(r chi.Router) {
r.Post("/auth/login", m.SysUserHandler.Login)
r.Post("/auth/refresh-token", m.SysUserHandler.RefreshToken)
r.Post("/auth/logout", m.SysUserHandler.Logout)
r.Group(func(r chi.Router) {
r.Use(m.JwtMiddleware.Middleware)
r.Use(m.AuthMiddleware.Middleware)
SetupSysUserRoutes(r, m.SysUserHandler)
SetupSysRoleRoutes(r, m.SysRoleHandler)
SetupSysMenuRoutes(r, m.SysMenuHandler)
SetupSysApiRoutes(r, m.SysApiHandler)
SetupSysFileRoutes(r, m.SysFileHandler)
SetupCategoryRoutes(r, m.CategoryHandler)
SetupSysPostRoutes(r, m.SysPostHandler)
})
})
// web api
r.Route("/api", func(r chi.Router) {
r.Group(func(r chi.Router) {
SetupPostRoutes(r, m.PostHandler)
SetupWebRoutes(r, m.WebHandler)
})
})
return r
}

68
internal/router/system.go Normal file
View File

@@ -0,0 +1,68 @@
package router
import (
"server/internal/handler"
"github.com/go-chi/chi/v5"
)
func SetupSysUserRoutes(r chi.Router, h *handler.SysUserHandler) {
r.Get("/user", h.ListPage)
r.Get("/user/info", h.GetUserInfo)
r.Get("/user/{id}/roles", h.GetRoles)
r.Post("/user", h.Create)
r.Patch("/user/{id}", h.Update)
r.Put("/user/{id}/roles", h.SetRoles)
r.Patch("/user/{id}/password", h.UpdatePassword)
r.Delete("/user/{id}", h.Delete)
}
func SetupSysRoleRoutes(r chi.Router, h *handler.SysRoleHandler) {
r.Get("/role", h.ListPage)
r.Get("/role/{id}/menus", h.GetRoleMenus)
r.Get("/role/{id}/apis", h.GetRoleApis)
r.Get("/role/all", h.GetRoles)
r.Post("/role", h.Create)
r.Patch("/role/{id}", h.Update)
r.Put("/role/{id}/menus", h.SetRoleMenus)
r.Put("/role/{id}/apis", h.SetRoleApis)
r.Delete("/role/{id}", h.Delete)
}
func SetupSysMenuRoutes(r chi.Router, h *handler.SysMenuHandler) {
r.Get("/menu", h.ListPage)
r.Get("/menu/all", h.GetMenus)
r.Post("/menu", h.Create)
r.Patch("/menu/{id}", h.Update)
r.Delete("/menu/{id}", h.Delete)
}
func SetupSysApiRoutes(r chi.Router, h *handler.SysApiHandler) {
r.Get("/api", h.ListPage)
r.Get("/api/all", h.GetAllSysApis)
r.Get("/api/group-names", h.GetApiGroupNames)
r.Post("/api", h.Create)
r.Patch("/api/{id}", h.Update)
r.Delete("/api/{id}", h.Delete)
}
func SetupSysFileRoutes(r chi.Router, h *handler.SysFileHandler) {
r.Get("/file", h.ListPage)
r.Post("/file", h.Upload)
}
func SetupSysPostRoutes(r chi.Router, h *handler.SysPostHandler) {
r.Get("/post", h.ListPage)
r.Get("/post/{id}", h.GetPostById)
r.Post("/post", h.Create)
r.Patch("/post/{id}", h.Update)
r.Delete("/post/{id}", h.Delete)
}
func SetupCategoryRoutes(r chi.Router, h *handler.CategoryHandler) {
r.Get("/category", h.ListPage)
r.Get("/category/all", h.ListAll)
r.Post("/category", h.Create)
r.Patch("/category/{id}", h.Update)
r.Delete("/category/{id}", h.Delete)
}

View File

@@ -0,0 +1,74 @@
package service
import (
"context"
db "server/internal/db/sqlc"
"server/internal/model/common"
"server/internal/model/request"
"server/internal/pkg/dberr"
"server/internal/pkg/errs"
)
type CategoryService struct {
queries *db.Queries
}
func NewCategoryService(queries *db.Queries) *CategoryService {
return &CategoryService{
queries: queries,
}
}
func (s *CategoryService) ListPage(ctx context.Context, p *common.Pagination) ([]db.Category, int64, error) {
params := db.ListCategoriesParams{
Limit: p.PageSize,
Offset: (p.Page - 1) * p.PageSize,
}
total, err := s.queries.CountCategories(ctx)
if err != nil {
return nil, 0, err
}
list, err := s.queries.ListCategories(ctx, params)
if err != nil {
return nil, 0, err
}
return list, total, nil
}
func (s *CategoryService) ListAll(ctx context.Context) ([]db.Category, error) {
return s.queries.ListAllCategories(ctx)
}
func (s *CategoryService) Create(ctx context.Context, req request.CreateCategoryRequest) error {
params := db.CreateCategoryParams{
Name: req.Name,
Code: req.Code,
}
err := s.queries.CreateCategory(ctx, params)
return dberr.MapUniqueViolation(err, dberr.CategoryCodeKey, errs.ErrCategoryCodeAlreadyExists)
}
func (s *CategoryService) Update(ctx context.Context, id int32, req request.UpdateCategoryRequest) error {
params := db.UpdateCategoryParams{
ID: id,
Name: req.Name,
Code: req.Code,
}
rows, err := s.queries.UpdateCategory(ctx, params)
// 先判断数据条目是否存在
if err = dberr.MapRowsAffected(rows, err, errs.ErrCategoryNotFound); err != nil {
// 再判断code是否重复
return dberr.MapUniqueViolation(err, dberr.CategoryCodeKey, errs.ErrCategoryCodeAlreadyExists)
}
return nil
}
func (s *CategoryService) Delete(ctx context.Context, id int32) error {
rows, err := s.queries.DeleteCategory(ctx, id)
return dberr.MapRowsAffected(rows, err, errs.ErrCategoryNotFound)
}

View File

@@ -0,0 +1,19 @@
package service
import (
"go.uber.org/fx"
)
var Module = fx.Module("services",
fx.Provide(
NewSysUserService,
NewSysRoleService,
NewSysMenuService,
NewSysApiService,
NewSysFileService,
NewSysPostService,
NewPostService,
NewCategoryService,
),
)

113
internal/service/post.go Normal file
View File

@@ -0,0 +1,113 @@
package service
import (
"context"
"net/netip"
db "server/internal/db/sqlc"
"server/internal/model/common"
"server/internal/model/response"
"server/internal/pkg/dberr"
"server/internal/pkg/errs"
"server/internal/pkg/httputil"
)
type PostService struct {
queries *db.Queries
}
func NewPostService(queries *db.Queries) *PostService {
return &PostService{
queries: queries,
}
}
func (s *PostService) ListPage(ctx context.Context, p *common.Pagination) ([]db.ListPublishedPostsRow, int64, error) {
params := db.ListPublishedPostsParams{
Limit: p.PageSize,
Offset: (p.Page - 1) * p.PageSize,
}
total, err := s.queries.CountPublishedPosts(ctx)
if err != nil {
return nil, 0, err
}
list, err := s.queries.ListPublishedPosts(ctx, params)
if err != nil {
return nil, 0, err
}
for i := range list {
url := httputil.BuildFileUrl(list[i].Cover)
list[i].Cover = &url
}
return list, total, nil
}
func (s *PostService) GetPost(ctx context.Context, slug string, ip netip.Addr) (*db.GetPublicPostBySlugRow, error) {
post, err := s.queries.GetPublicPostBySlug(ctx, slug)
if err != nil {
return nil, dberr.MapNoRows(err, errs.ErrPostNotFound)
}
_ = s.queries.IncrementPostStatsView(ctx, db.IncrementPostStatsViewParams{
PostID: post.ID,
Ip: ip,
})
return &post, nil
}
func (s *PostService) ListCategoryStats(ctx context.Context) ([]db.ListCategoryStatsRow, error) {
return s.queries.ListCategoryStats(ctx)
}
func (s *PostService) ListArchives(ctx context.Context) ([]response.ArchiveYear, error) {
list, err := s.queries.ListArchives(ctx)
if err != nil {
return nil, err
}
archive := make([]response.ArchiveYear, 0)
for _, item := range list {
y := item.PublishedAt.Year()
m := int(item.PublishedAt.Month())
if len(archive) == 0 || archive[len(archive)-1].Year != y {
archive = append(archive, response.ArchiveYear{
Year: y,
Total: 0,
ArchiveMonth: make([]response.ArchiveMonth, 0),
})
}
// 获取索引
lastYearIndex := len(archive) - 1
months := archive[lastYearIndex].ArchiveMonth
if len(months) == 0 || months[len(months)-1].Month != m {
archive[lastYearIndex].ArchiveMonth = append(archive[lastYearIndex].ArchiveMonth, response.ArchiveMonth{
Month: m,
Archive: make([]response.ArchivePost, 0),
})
}
lastMonthIndex := len(archive[lastYearIndex].ArchiveMonth) - 1
archive[lastYearIndex].ArchiveMonth[lastMonthIndex].Archive =
append(archive[lastYearIndex].ArchiveMonth[lastMonthIndex].Archive, response.ArchivePost{
ID: item.ID,
Slug: item.Slug,
Title: item.Title,
PublishedAt: item.PublishedAt,
PublishedAtDisplay: item.PublishedAt.Format("01-02"),
CategoryName: *item.CategoryName,
})
archive[lastYearIndex].Total++
}
return archive, nil
}

158
internal/service/sys_api.go Normal file
View File

@@ -0,0 +1,158 @@
package service
import (
"context"
db "server/internal/db/sqlc"
"server/internal/model/common"
"server/internal/model/enum"
"server/internal/model/request"
"server/internal/pkg/cache"
"server/internal/pkg/dberr"
"server/internal/pkg/errs"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgxpool"
)
type SysApiService struct {
queries *db.Queries
pool *pgxpool.Pool
cache *cache.Caches
}
func NewSysApiService(queries *db.Queries, pool *pgxpool.Pool, cache *cache.Caches) *SysApiService {
return &SysApiService{queries: queries, pool: pool, cache: cache}
}
func (s *SysApiService) ListPage(ctx context.Context, p *common.Pagination) ([]db.SysApi, int64, error) {
params := db.GetSysApisParams{
Limit: p.PageSize,
Offset: (p.Page - 1) * p.PageSize,
}
total, err := s.queries.CountSysApis(ctx)
if err != nil {
return nil, 0, err
}
list, err := s.queries.GetSysApis(ctx, params)
if err != nil {
return nil, 0, err
}
return list, total, nil
}
func (s *SysApiService) GetAllSysApis(ctx context.Context) ([]db.SysApi, error) {
return s.queries.GetAllSysApis(ctx)
}
func (s *SysApiService) GetApiGroupNames(ctx context.Context) ([]string, error) {
return s.queries.GetSysApiGroupNames(ctx)
}
func (s *SysApiService) Create(ctx context.Context, req request.CreateSysApiRequest) error {
tx, err := s.pool.BeginTx(ctx, pgx.TxOptions{})
if err != nil {
return err
}
defer func(ctx context.Context) {
_ = tx.Rollback(ctx)
}(ctx)
q := db.New(tx)
api := db.CreateSysApiParams{
Name: req.Name,
GroupName: req.GroupName,
Method: req.Method,
Path: req.Path,
Sort: req.Sort,
}
// 创建权限
permissionId, err := q.CreateSysPermission(ctx, db.CreateSysPermissionParams{
Type: int16(enum.PermissionTypeApi),
})
if err != nil {
return err
}
// 创建api
apiId, err := q.CreateSysApi(ctx, api)
if err != nil {
return dberr.MapUniqueViolation(err, dberr.SysApisMethodPathKey, errs.ErrSysApiMethodPathAlreadyExists)
}
// 关联权限
if err = q.CreateSysApiPermission(ctx, db.CreateSysApiPermissionParams{
ApiID: apiId,
PermissionID: permissionId,
}); err != nil {
return err
}
if err = tx.Commit(ctx); err != nil {
return err
}
// 清理缓存
s.cache.ClearAllSysUserCache()
return nil
}
func (s *SysApiService) Update(ctx context.Context, id int32, req request.UpdateSysApiRequest) error {
api := db.UpdateSysApiParams{
ID: id,
Name: req.Name,
GroupName: req.GroupName,
Method: req.Method,
Path: req.Path,
Sort: req.Sort,
}
// 清理缓存
s.cache.ClearAllSysUserCache()
rows, err := s.queries.UpdateSysApi(ctx, api)
if err = dberr.MapRowsAffected(rows, err, errs.ErrSysApiNotFound); err != nil {
return dberr.MapUniqueViolation(err, dberr.SysApisMethodPathKey, errs.ErrSysApiMethodPathAlreadyExists)
}
return nil
}
func (s *SysApiService) Delete(ctx context.Context, id int32) error {
tx, err := s.pool.BeginTx(ctx, pgx.TxOptions{})
if err != nil {
return err
}
defer tx.Rollback(ctx)
q := db.New(tx)
rows, err := q.DeleteSysApi(ctx, id)
if err = dberr.MapRowsAffected(rows, err, errs.ErrSysApiNotFound); err != nil {
return err
}
if err = q.DeleteSysPermissionBySysApiID(ctx, id); err != nil {
return err
}
if err = q.DeleteSysApiPermission(ctx, id); err != nil {
return err
}
// 清理缓存
s.cache.ClearAllSysUserCache()
return tx.Commit(ctx)
}

Some files were not shown because too many files have changed in this diff Show More