diff --git a/.air.toml b/.air.toml new file mode 100644 index 0000000..c5f6fba --- /dev/null +++ b/.air.toml @@ -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 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..eddd9f5 --- /dev/null +++ b/.gitignore @@ -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/ \ No newline at end of file diff --git a/cmd/main.go b/cmd/main.go new file mode 100644 index 0000000..1e78498 --- /dev/null +++ b/cmd/main.go @@ -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() +} diff --git a/example.yaml b/example.yaml new file mode 100644 index 0000000..c96bb5f --- /dev/null +++ b/example.yaml @@ -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 # 是否同时输出到控制台 diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..dec9e28 --- /dev/null +++ b/go.mod @@ -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 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..71d4b65 --- /dev/null +++ b/go.sum @@ -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= diff --git a/internal/config/config.go b/internal/config/config.go new file mode 100644 index 0000000..985a858 --- /dev/null +++ b/internal/config/config.go @@ -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) +} diff --git a/internal/db/db.go b/internal/db/db.go new file mode 100644 index 0000000..1a1535d --- /dev/null +++ b/internal/db/db.go @@ -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) +} diff --git a/internal/db/migrations/000001_init.down.sql b/internal/db/migrations/000001_init.down.sql new file mode 100644 index 0000000..f46e592 --- /dev/null +++ b/internal/db/migrations/000001_init.down.sql @@ -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; diff --git a/internal/db/migrations/000001_init.up.sql b/internal/db/migrations/000001_init.up.sql new file mode 100644 index 0000000..e452a82 --- /dev/null +++ b/internal/db/migrations/000001_init.up.sql @@ -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'; +-------------------------------------------------------------------------------------------------------------- diff --git a/internal/db/migrations/000002_seed.down.sql b/internal/db/migrations/000002_seed.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/internal/db/migrations/000002_seed.up.sql b/internal/db/migrations/000002_seed.up.sql new file mode 100644 index 0000000..55dc579 --- /dev/null +++ b/internal/db/migrations/000002_seed.up.sql @@ -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; diff --git a/internal/db/migrations/000003_add_posts.down.sql b/internal/db/migrations/000003_add_posts.down.sql new file mode 100644 index 0000000..dc74d09 --- /dev/null +++ b/internal/db/migrations/000003_add_posts.down.sql @@ -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; \ No newline at end of file diff --git a/internal/db/migrations/000003_add_posts.up.sql b/internal/db/migrations/000003_add_posts.up.sql new file mode 100644 index 0000000..de72e34 --- /dev/null +++ b/internal/db/migrations/000003_add_posts.up.sql @@ -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; \ No newline at end of file diff --git a/internal/db/migrations/000004_add_categories.down.sql b/internal/db/migrations/000004_add_categories.down.sql new file mode 100644 index 0000000..0604119 --- /dev/null +++ b/internal/db/migrations/000004_add_categories.down.sql @@ -0,0 +1,2 @@ +DROP TABLE IF EXISTS categories; +DROP TABLE IF EXISTS post_category; \ No newline at end of file diff --git a/internal/db/migrations/000004_add_categories.up.sql b/internal/db/migrations/000004_add_categories.up.sql new file mode 100644 index 0000000..58adb49 --- /dev/null +++ b/internal/db/migrations/000004_add_categories.up.sql @@ -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'; \ No newline at end of file diff --git a/internal/db/migrations/000005_add_post_stats_drop_posts_view.down.sql b/internal/db/migrations/000005_add_post_stats_drop_posts_view.down.sql new file mode 100644 index 0000000..9a11686 --- /dev/null +++ b/internal/db/migrations/000005_add_post_stats_drop_posts_view.down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS post_stats; \ No newline at end of file diff --git a/internal/db/migrations/000005_add_post_stats_drop_posts_view.up.sql b/internal/db/migrations/000005_add_post_stats_drop_posts_view.up.sql new file mode 100644 index 0000000..919e0f6 --- /dev/null +++ b/internal/db/migrations/000005_add_post_stats_drop_posts_view.up.sql @@ -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; \ No newline at end of file diff --git a/internal/db/migrations/000006_add_icon_column_to_sys_menus.down.sql b/internal/db/migrations/000006_add_icon_column_to_sys_menus.down.sql new file mode 100644 index 0000000..0437151 --- /dev/null +++ b/internal/db/migrations/000006_add_icon_column_to_sys_menus.down.sql @@ -0,0 +1,2 @@ +ALTER TABLE sys_menus +DROP COLUMN IF EXISTS icon; \ No newline at end of file diff --git a/internal/db/migrations/000006_add_icon_column_to_sys_menus.up.sql b/internal/db/migrations/000006_add_icon_column_to_sys_menus.up.sql new file mode 100644 index 0000000..51bab1e --- /dev/null +++ b/internal/db/migrations/000006_add_icon_column_to_sys_menus.up.sql @@ -0,0 +1,2 @@ +ALTER TABLE sys_menus +ADD COLUMN IF NOT EXISTS icon INTEGER; \ No newline at end of file diff --git a/internal/db/migrations/000007_add_post_viewers.down.sql b/internal/db/migrations/000007_add_post_viewers.down.sql new file mode 100644 index 0000000..f69cab7 --- /dev/null +++ b/internal/db/migrations/000007_add_post_viewers.down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS post_viewers; \ No newline at end of file diff --git a/internal/db/migrations/000007_add_post_viewers.up.sql b/internal/db/migrations/000007_add_post_viewers.up.sql new file mode 100644 index 0000000..bb00caa --- /dev/null +++ b/internal/db/migrations/000007_add_post_viewers.up.sql @@ -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 '最后访问时间'; \ No newline at end of file diff --git a/internal/db/query/category.sql b/internal/db/query/category.sql new file mode 100644 index 0000000..ec912e5 --- /dev/null +++ b/internal/db/query/category.sql @@ -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; \ No newline at end of file diff --git a/internal/db/query/file.sql b/internal/db/query/file.sql new file mode 100644 index 0000000..00bfcae --- /dev/null +++ b/internal/db/query/file.sql @@ -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; diff --git a/internal/db/query/post.sql b/internal/db/query/post.sql new file mode 100644 index 0000000..a386584 --- /dev/null +++ b/internal/db/query/post.sql @@ -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; \ No newline at end of file diff --git a/internal/db/query/post_stats.sql b/internal/db/query/post_stats.sql new file mode 100644 index 0000000..d2f8efa --- /dev/null +++ b/internal/db/query/post_stats.sql @@ -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; \ No newline at end of file diff --git a/internal/db/query/sys_api.sql b/internal/db/query/sys_api.sql new file mode 100644 index 0000000..ffabc71 --- /dev/null +++ b/internal/db/query/sys_api.sql @@ -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; \ No newline at end of file diff --git a/internal/db/query/sys_menu.sql b/internal/db/query/sys_menu.sql new file mode 100644 index 0000000..4488199 --- /dev/null +++ b/internal/db/query/sys_menu.sql @@ -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; \ No newline at end of file diff --git a/internal/db/query/sys_permission.sql b/internal/db/query/sys_permission.sql new file mode 100644 index 0000000..d97b6c3 --- /dev/null +++ b/internal/db/query/sys_permission.sql @@ -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; + diff --git a/internal/db/query/sys_role.sql b/internal/db/query/sys_role.sql new file mode 100644 index 0000000..139b20d --- /dev/null +++ b/internal/db/query/sys_role.sql @@ -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; \ No newline at end of file diff --git a/internal/db/query/sys_user.sql b/internal/db/query/sys_user.sql new file mode 100644 index 0000000..8695549 --- /dev/null +++ b/internal/db/query/sys_user.sql @@ -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; \ No newline at end of file diff --git a/internal/db/schema/categories.sql b/internal/db/schema/categories.sql new file mode 100644 index 0000000..23b5cc6 --- /dev/null +++ b/internal/db/schema/categories.sql @@ -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 '分类编码'; \ No newline at end of file diff --git a/internal/db/schema/files.sql b/internal/db/schema/files.sql new file mode 100644 index 0000000..9666ffa --- /dev/null +++ b/internal/db/schema/files.sql @@ -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 '修改时间'; diff --git a/internal/db/schema/func.sql b/internal/db/schema/func.sql new file mode 100644 index 0000000..7787356 --- /dev/null +++ b/internal/db/schema/func.sql @@ -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'; \ No newline at end of file diff --git a/internal/db/schema/post_category.sql b/internal/db/schema/post_category.sql new file mode 100644 index 0000000..471cf4a --- /dev/null +++ b/internal/db/schema/post_category.sql @@ -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'; \ No newline at end of file diff --git a/internal/db/schema/post_stats.sql b/internal/db/schema/post_stats.sql new file mode 100644 index 0000000..6b25146 --- /dev/null +++ b/internal/db/schema/post_stats.sql @@ -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 '文章阅读量'; diff --git a/internal/db/schema/post_viewers.sql b/internal/db/schema/post_viewers.sql new file mode 100644 index 0000000..bb00caa --- /dev/null +++ b/internal/db/schema/post_viewers.sql @@ -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 '最后访问时间'; \ No newline at end of file diff --git a/internal/db/schema/posts.sql b/internal/db/schema/posts.sql new file mode 100644 index 0000000..edead4e --- /dev/null +++ b/internal/db/schema/posts.sql @@ -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 '修改时间'; \ No newline at end of file diff --git a/internal/db/schema/sys_api_permission.sql b/internal/db/schema/sys_api_permission.sql new file mode 100644 index 0000000..804b62d --- /dev/null +++ b/internal/db/schema/sys_api_permission.sql @@ -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'; \ No newline at end of file diff --git a/internal/db/schema/sys_apis.sql b/internal/db/schema/sys_apis.sql new file mode 100644 index 0000000..fc76f81 --- /dev/null +++ b/internal/db/schema/sys_apis.sql @@ -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 '更新时间'; \ No newline at end of file diff --git a/internal/db/schema/sys_menu_permission.sql b/internal/db/schema/sys_menu_permission.sql new file mode 100644 index 0000000..d3bfe3e --- /dev/null +++ b/internal/db/schema/sys_menu_permission.sql @@ -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'; \ No newline at end of file diff --git a/internal/db/schema/sys_menus.sql b/internal/db/schema/sys_menus.sql new file mode 100644 index 0000000..da08ac4 --- /dev/null +++ b/internal/db/schema/sys_menus.sql @@ -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 '更新时间'; \ No newline at end of file diff --git a/internal/db/schema/sys_permissions.sql b/internal/db/schema/sys_permissions.sql new file mode 100644 index 0000000..4de2b82 --- /dev/null +++ b/internal/db/schema/sys_permissions.sql @@ -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 '更新时间'; \ No newline at end of file diff --git a/internal/db/schema/sys_role_permission.sql b/internal/db/schema/sys_role_permission.sql new file mode 100644 index 0000000..baba932 --- /dev/null +++ b/internal/db/schema/sys_role_permission.sql @@ -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'; \ No newline at end of file diff --git a/internal/db/schema/sys_roles.sql b/internal/db/schema/sys_roles.sql new file mode 100644 index 0000000..45c7433 --- /dev/null +++ b/internal/db/schema/sys_roles.sql @@ -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 '更新时间'; \ No newline at end of file diff --git a/internal/db/schema/sys_user_role.sql b/internal/db/schema/sys_user_role.sql new file mode 100644 index 0000000..6242372 --- /dev/null +++ b/internal/db/schema/sys_user_role.sql @@ -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'; \ No newline at end of file diff --git a/internal/db/schema/sys_users.sql b/internal/db/schema/sys_users.sql new file mode 100644 index 0000000..49dd4fa --- /dev/null +++ b/internal/db/schema/sys_users.sql @@ -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 '更新时间'; \ No newline at end of file diff --git a/internal/db/sqlc/category.sql.go b/internal/db/sqlc/category.sql.go new file mode 100644 index 0000000..0811b62 --- /dev/null +++ b/internal/db/sqlc/category.sql.go @@ -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 +} diff --git a/internal/db/sqlc/copyfrom.go b/internal/db/sqlc/copyfrom.go new file mode 100644 index 0000000..80e2b71 --- /dev/null +++ b/internal/db/sqlc/copyfrom.go @@ -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}) +} diff --git a/internal/db/sqlc/db.go b/internal/db/sqlc/db.go new file mode 100644 index 0000000..c3d191a --- /dev/null +++ b/internal/db/sqlc/db.go @@ -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, + } +} diff --git a/internal/db/sqlc/file.sql.go b/internal/db/sqlc/file.sql.go new file mode 100644 index 0000000..847df5b --- /dev/null +++ b/internal/db/sqlc/file.sql.go @@ -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 +} diff --git a/internal/db/sqlc/models.go b/internal/db/sqlc/models.go new file mode 100644 index 0000000..96720a8 --- /dev/null +++ b/internal/db/sqlc/models.go @@ -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"` +} diff --git a/internal/db/sqlc/post.sql.go b/internal/db/sqlc/post.sql.go new file mode 100644 index 0000000..1ea2aee --- /dev/null +++ b/internal/db/sqlc/post.sql.go @@ -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 +} diff --git a/internal/db/sqlc/post_stats.sql.go b/internal/db/sqlc/post_stats.sql.go new file mode 100644 index 0000000..3914257 --- /dev/null +++ b/internal/db/sqlc/post_stats.sql.go @@ -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 +} diff --git a/internal/db/sqlc/sys_api.sql.go b/internal/db/sqlc/sys_api.sql.go new file mode 100644 index 0000000..8581e60 --- /dev/null +++ b/internal/db/sqlc/sys_api.sql.go @@ -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 +} diff --git a/internal/db/sqlc/sys_menu.sql.go b/internal/db/sqlc/sys_menu.sql.go new file mode 100644 index 0000000..94194c4 --- /dev/null +++ b/internal/db/sqlc/sys_menu.sql.go @@ -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 +} diff --git a/internal/db/sqlc/sys_permission.sql.go b/internal/db/sqlc/sys_permission.sql.go new file mode 100644 index 0000000..a058c4d --- /dev/null +++ b/internal/db/sqlc/sys_permission.sql.go @@ -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 +} diff --git a/internal/db/sqlc/sys_role.sql.go b/internal/db/sqlc/sys_role.sql.go new file mode 100644 index 0000000..350b60c --- /dev/null +++ b/internal/db/sqlc/sys_role.sql.go @@ -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 +} diff --git a/internal/db/sqlc/sys_user.sql.go b/internal/db/sqlc/sys_user.sql.go new file mode 100644 index 0000000..288ebc2 --- /dev/null +++ b/internal/db/sqlc/sys_user.sql.go @@ -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 +} diff --git a/internal/handler/category.go b/internal/handler/category.go new file mode 100644 index 0000000..1e0e046 --- /dev/null +++ b/internal/handler/category.go @@ -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) +} diff --git a/internal/handler/module.go b/internal/handler/module.go new file mode 100644 index 0000000..31cdfda --- /dev/null +++ b/internal/handler/module.go @@ -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, + ), +) diff --git a/internal/handler/post.go b/internal/handler/post.go new file mode 100644 index 0000000..71b543e --- /dev/null +++ b/internal/handler/post.go @@ -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) +} diff --git a/internal/handler/sys_api.go b/internal/handler/sys_api.go new file mode 100644 index 0000000..72089e7 --- /dev/null +++ b/internal/handler/sys_api.go @@ -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) +} diff --git a/internal/handler/sys_file.go b/internal/handler/sys_file.go new file mode 100644 index 0000000..a561189 --- /dev/null +++ b/internal/handler/sys_file.go @@ -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) +} diff --git a/internal/handler/sys_menu.go b/internal/handler/sys_menu.go new file mode 100644 index 0000000..d703281 --- /dev/null +++ b/internal/handler/sys_menu.go @@ -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) + +} diff --git a/internal/handler/sys_post.go b/internal/handler/sys_post.go new file mode 100644 index 0000000..72357a3 --- /dev/null +++ b/internal/handler/sys_post.go @@ -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) +} diff --git a/internal/handler/sys_role.go b/internal/handler/sys_role.go new file mode 100644 index 0000000..55c3b57 --- /dev/null +++ b/internal/handler/sys_role.go @@ -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) +} diff --git a/internal/handler/sys_user.go b/internal/handler/sys_user.go new file mode 100644 index 0000000..ab214cb --- /dev/null +++ b/internal/handler/sys_user.go @@ -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) +} diff --git a/internal/handler/web.go b/internal/handler/web.go new file mode 100644 index 0000000..72e4bbc --- /dev/null +++ b/internal/handler/web.go @@ -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"), + }) +} diff --git a/internal/middleware/auth.go b/internal/middleware/auth.go new file mode 100644 index 0000000..0bf3e0c --- /dev/null +++ b/internal/middleware/auth.go @@ -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 +} diff --git a/internal/middleware/jwt.go b/internal/middleware/jwt.go new file mode 100644 index 0000000..cc5b0be --- /dev/null +++ b/internal/middleware/jwt.go @@ -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 +} diff --git a/internal/middleware/logger.go b/internal/middleware/logger.go new file mode 100644 index 0000000..142c4e9 --- /dev/null +++ b/internal/middleware/logger.go @@ -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...) + } + }) +} diff --git a/internal/model/common/request.go b/internal/model/common/request.go new file mode 100644 index 0000000..f8a3a87 --- /dev/null +++ b/internal/model/common/request.go @@ -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"` +} diff --git a/internal/model/common/response.go b/internal/model/common/response.go new file mode 100644 index 0000000..f9c749b --- /dev/null +++ b/internal/model/common/response.go @@ -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"` // 总记录数 +} diff --git a/internal/model/enum/enum.go b/internal/model/enum/enum.go new file mode 100644 index 0000000..bfacb5c --- /dev/null +++ b/internal/model/enum/enum.go @@ -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: 已下线 +) diff --git a/internal/model/request/category.go b/internal/model/request/category.go new file mode 100644 index 0000000..d37a2ce --- /dev/null +++ b/internal/model/request/category.go @@ -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"` +} diff --git a/internal/model/request/post.go b/internal/model/request/post.go new file mode 100644 index 0000000..9d909ab --- /dev/null +++ b/internal/model/request/post.go @@ -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"` +} diff --git a/internal/model/request/sys_api.go b/internal/model/request/sys_api.go new file mode 100644 index 0000000..ce7bb64 --- /dev/null +++ b/internal/model/request/sys_api.go @@ -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"` +} diff --git a/internal/model/request/sys_menu.go b/internal/model/request/sys_menu.go new file mode 100644 index 0000000..fbda2ef --- /dev/null +++ b/internal/model/request/sys_menu.go @@ -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"` +} diff --git a/internal/model/request/sys_permission.go b/internal/model/request/sys_permission.go new file mode 100644 index 0000000..725b8fc --- /dev/null +++ b/internal/model/request/sys_permission.go @@ -0,0 +1 @@ +package request diff --git a/internal/model/request/sys_role.go b/internal/model/request/sys_role.go new file mode 100644 index 0000000..5b11c25 --- /dev/null +++ b/internal/model/request/sys_role.go @@ -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"` +} diff --git a/internal/model/request/sys_user.go b/internal/model/request/sys_user.go new file mode 100644 index 0000000..d653175 --- /dev/null +++ b/internal/model/request/sys_user.go @@ -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"` +} diff --git a/internal/model/response/post.go b/internal/model/response/post.go new file mode 100644 index 0000000..90f8529 --- /dev/null +++ b/internal/model/response/post.go @@ -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"` +} diff --git a/internal/model/response/sys_file.go b/internal/model/response/sys_file.go new file mode 100644 index 0000000..6c75027 --- /dev/null +++ b/internal/model/response/sys_file.go @@ -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 +} diff --git a/internal/model/response/sys_user.go b/internal/model/response/sys_user.go new file mode 100644 index 0000000..69fae98 --- /dev/null +++ b/internal/model/response/sys_user.go @@ -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, + } +} diff --git a/internal/pkg/cache/cache.go b/internal/pkg/cache/cache.go new file mode 100644 index 0000000..b5aec24 --- /dev/null +++ b/internal/pkg/cache/cache.go @@ -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() +} diff --git a/internal/pkg/dberr/postgres.go b/internal/pkg/dberr/postgres.go new file mode 100644 index 0000000..b00d040 --- /dev/null +++ b/internal/pkg/dberr/postgres.go @@ -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 +} diff --git a/internal/pkg/errs/errs.go b/internal/pkg/errs/errs.go new file mode 100644 index 0000000..88d59ad --- /dev/null +++ b/internal/pkg/errs/errs.go @@ -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)已存在") +) diff --git a/internal/pkg/httputil/request.go b/internal/pkg/httputil/request.go new file mode 100644 index 0000000..35b8204 --- /dev/null +++ b/internal/pkg/httputil/request.go @@ -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 +} diff --git a/internal/pkg/httputil/response.go b/internal/pkg/httputil/response.go new file mode 100644 index 0000000..42befec --- /dev/null +++ b/internal/pkg/httputil/response.go @@ -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) +} diff --git a/internal/pkg/logger/logger.go b/internal/pkg/logger/logger.go new file mode 100644 index 0000000..71e05fd --- /dev/null +++ b/internal/pkg/logger/logger.go @@ -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 +} diff --git a/internal/pkg/validator/null_int32.go b/internal/pkg/validator/null_int32.go new file mode 100644 index 0000000..e3e3087 --- /dev/null +++ b/internal/pkg/validator/null_int32.go @@ -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 +} diff --git a/internal/pkg/validator/validator.go b/internal/pkg/validator/validator.go new file mode 100644 index 0000000..21591ed --- /dev/null +++ b/internal/pkg/validator/validator.go @@ -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) +} diff --git a/internal/router/client.go b/internal/router/client.go new file mode 100644 index 0000000..477b6f2 --- /dev/null +++ b/internal/router/client.go @@ -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) { + +} diff --git a/internal/router/router.go b/internal/router/router.go new file mode 100644 index 0000000..2e22c77 --- /dev/null +++ b/internal/router/router.go @@ -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 +} diff --git a/internal/router/system.go b/internal/router/system.go new file mode 100644 index 0000000..cc4b031 --- /dev/null +++ b/internal/router/system.go @@ -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) +} diff --git a/internal/service/category.go b/internal/service/category.go new file mode 100644 index 0000000..2965f8d --- /dev/null +++ b/internal/service/category.go @@ -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) +} diff --git a/internal/service/module.go b/internal/service/module.go new file mode 100644 index 0000000..cc08d8a --- /dev/null +++ b/internal/service/module.go @@ -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, + ), +) diff --git a/internal/service/post.go b/internal/service/post.go new file mode 100644 index 0000000..958b86a --- /dev/null +++ b/internal/service/post.go @@ -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 +} diff --git a/internal/service/sys_api.go b/internal/service/sys_api.go new file mode 100644 index 0000000..d7c86aa --- /dev/null +++ b/internal/service/sys_api.go @@ -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) +} diff --git a/internal/service/sys_file.go b/internal/service/sys_file.go new file mode 100644 index 0000000..f93d28e --- /dev/null +++ b/internal/service/sys_file.go @@ -0,0 +1,118 @@ +package service + +import ( + "context" + "mime/multipart" + "os" + "path/filepath" + db "server/internal/db/sqlc" + "server/internal/model/common" + "server/internal/model/response" + "server/internal/pkg/httputil" + + gonanoid "github.com/matoous/go-nanoid/v2" +) + +type SysFileService struct { + queries *db.Queries +} + +func NewSysFileService(queries *db.Queries) *SysFileService { + return &SysFileService{ + queries: queries, + } +} + +// MakeSavedDir 创建目录并返回 +func MakeSavedDir(folder string) (string, error) { + rootDir, err := os.Getwd() + + if err != nil { + return "", err + } + + uploadDir := filepath.Join(rootDir, "uploads", folder) + + if err = os.MkdirAll(uploadDir, 0755); err != nil { + return "", err + } + + return uploadDir, nil +} + +func (s *SysFileService) ListPage(ctx context.Context, p *common.Pagination) ([]db.File, int64, error) { + params := db.GetFilesParams{ + Limit: p.PageSize, + Offset: (p.Page - 1) * p.PageSize, + } + + total, err := s.queries.CountFiles(ctx) + + if err != nil { + return nil, 0, err + } + + list, err := s.queries.GetFiles(ctx, params) + + if err != nil { + return nil, 0, err + } + + return response.ToFiles(list), total, nil +} + +func (s *SysFileService) Upload(ctx context.Context, folder string, file *multipart.FileHeader) (*db.CreateFileRow, error) { + // 生成文件名 + fileID, err := gonanoid.New() + if err != nil { + return nil, err + } + + savedDir, err := MakeSavedDir(folder) + if err != nil { + return nil, err + } + + fileExt := filepath.Ext(file.Filename) + filename := fileID + fileExt + filePath := filepath.Join(folder, filename) + // 用于保存文件 + savedPath := filepath.Join(savedDir, filename) + + // 打开上传的文件 + src, err := file.Open() + if err != nil { + return nil, err + } + defer src.Close() + + // 创建目标文件 + dst, err := os.Create(savedPath) + if err != nil { + return nil, err + } + defer dst.Close() + + // 复制文件内容 + if _, err = dst.ReadFrom(src); err != nil { + return nil, err + } + + params := db.CreateFileParams{ + FileName: filename, + FilePath: filePath, + OriginalName: file.Filename, + FolderName: folder, + MimeType: file.Header.Get("Content-Type"), + FileSize: file.Size, + } + + result, err := s.queries.CreateFile(ctx, params) + if err != nil { + return nil, err + } + + result.FilePath = httputil.BuildFileUrl(&result.FilePath) + + return &result, nil +} diff --git a/internal/service/sys_menu.go b/internal/service/sys_menu.go new file mode 100644 index 0000000..54e3d7d --- /dev/null +++ b/internal/service/sys_menu.go @@ -0,0 +1,178 @@ +package service + +import ( + "context" + db "server/internal/db/sqlc" + "server/internal/model/common" + "server/internal/model/enum" + "server/internal/model/request" + "server/internal/pkg/dberr" + "server/internal/pkg/errs" + + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgxpool" +) + +type SysMenuService struct { + pool *pgxpool.Pool + queries *db.Queries +} + +func NewSysMenuService(queries *db.Queries, pool *pgxpool.Pool) *SysMenuService { + return &SysMenuService{ + queries: queries, + pool: pool, + } +} + +func (s *SysMenuService) Create(ctx context.Context, req request.CreateSysMenuRequest) 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) + + menu := db.CreateSysMenuParams{ + Name: req.Name, + Path: req.Path, + Component: req.Component, + Type: *req.Type, + Status: *req.Status, + Hidden: req.Hidden, + Sort: req.Sort, + Icon: req.Icon, + } + + permissionId, err := q.CreateSysPermission(ctx, db.CreateSysPermissionParams{ + Type: int16(enum.PermissionTypeMenu), + Code: &req.PermissionCode, + }) + + if err != nil { + return dberr.MapUniqueViolation(err, dberr.SysPermissionsCodeKey, errs.ErrPermissionCodeAlreadyExists) + } + + menuId, err := q.CreateSysMenu(ctx, menu) + if err != nil { + return err + } + + if err = q.CreateSysMenuPermission(ctx, db.CreateSysMenuPermissionParams{ + MenuID: menuId, + PermissionID: permissionId, + }); err != nil { + return err + } + + if err = tx.Commit(ctx); err != nil { + return err + } + + return nil +} + +func (s *SysMenuService) Update(ctx context.Context, id int32, req request.UpdateSysMenuRequest) error { + tx, err := s.pool.BeginTx(ctx, pgx.TxOptions{}) + if err != nil { + return err + } + defer tx.Rollback(ctx) + + q := db.New(tx) + + // 构造 menu 参数 + menu := db.UpdateSysMenuParams{ + ID: id, + Name: req.Name, + Path: req.Path, + Component: req.Component, + Hidden: req.Hidden, + Sort: req.Sort, + Type: req.Type, + Status: req.Status, + Icon: req.Icon, + } + + if req.ParentID.Set { + menu.UpdateParentID = true + if req.ParentID.Valid { + menu.ParentID = &req.ParentID.Value + } + } + + // 执行更新 + rows, err := q.UpdateSysMenu(ctx, menu) + + if err = dberr.MapRowsAffected(rows, err, errs.ErrSysMenuNotFound); err != nil { + return err + } + + permission := db.UpdateSysMenuPermissionCodeParams{ + MenuID: id, + Code: req.PermissionCode, + } + + if err = q.UpdateSysMenuPermissionCode(ctx, permission); err != nil { + return dberr.MapUniqueViolation(err, dberr.SysPermissionsCodeKey, errs.ErrPermissionCodeAlreadyExists) + } + + return tx.Commit(ctx) +} + +func (s *SysMenuService) ListPage(ctx context.Context, p *common.Pagination) ([]db.ListSysMenusRow, int64, error) { + params := db.ListSysMenusParams{ + Limit: p.PageSize, + Offset: (p.Page - 1) * p.PageSize, + } + + total, err := s.queries.CountSysMenus(ctx) + + if err != nil { + return nil, 0, err + } + + list, err := s.queries.ListSysMenus(ctx, params) + if err != nil { + return nil, 0, err + } + + return list, total, nil +} + +func (s *SysMenuService) GetMenus(ctx context.Context) ([]db.GetAllSysMenusRow, error) { + return s.queries.GetAllSysMenus(ctx) +} + +func (s *SysMenuService) 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.DeleteSysMenu(ctx, id) + + if err = dberr.MapRowsAffected(rows, err, errs.ErrSysMenuNotFound); err != nil { + return err + } + + if err = q.DeleteSysPermissionByMenuID(ctx, id); err != nil { + return err + } + + if err = q.DeleteSysMenuPermission(ctx, id); err != nil { + return err + } + + return tx.Commit(ctx) +} diff --git a/internal/service/sys_post.go b/internal/service/sys_post.go new file mode 100644 index 0000000..4f758c9 --- /dev/null +++ b/internal/service/sys_post.go @@ -0,0 +1,163 @@ +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" + "server/internal/pkg/httputil" + + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgxpool" +) + +type SysPostService struct { + queries *db.Queries + pool *pgxpool.Pool +} + +func NewSysPostService(queries *db.Queries, pool *pgxpool.Pool) *SysPostService { + return &SysPostService{ + queries: queries, + pool: pool, + } +} + +func (s *SysPostService) ListPage(ctx context.Context, p *common.Pagination) ([]db.ListPostsRow, int64, error) { + params := db.ListPostsParams{ + Limit: p.PageSize, + Offset: (p.Page - 1) * p.PageSize, + } + + total, err := s.queries.CountPosts(ctx) + if err != nil { + return nil, 0, err + } + + list, err := s.queries.ListPosts(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 *SysPostService) FindByID(ctx context.Context, id int32) (*db.GetPostByIdRow, error) { + post, err := s.queries.GetPostById(ctx, id) + if err != nil { + return nil, dberr.MapNoRows(err, errs.ErrPostNotFound) + } + url := httputil.BuildFileUrl(post.Cover) + post.Cover = &url + + return &post, nil +} + +func (s *SysPostService) Create(ctx context.Context, req request.CreatePostRequest) (int32, error) { + // 开启事务 + tx, err := s.pool.BeginTx(ctx, pgx.TxOptions{}) + if err != nil { + return 0, err + } + + defer func() { + _ = tx.Rollback(ctx) + }() + + q := db.New(tx) + + params := db.CreatePostParams{ + Title: req.Title, + CoverID: req.CoverID, + Slug: req.Slug, + Content: req.Content, + Summary: req.Summary, + Status: *req.Status, + Sort: req.Sort, + PublishedAt: req.PublishedAt, + } + + postId, err := q.CreatePost(ctx, params) + if err != nil { + return 0, dberr.MapUniqueViolation(err, dberr.PostSlugKey, errs.ErrSlugAlreadyExists) + } + + if err = q.CreatePostCategory(ctx, db.CreatePostCategoryParams{ + PostID: postId, + CategoryID: *req.CategoryID, + }); err != nil { + return 0, err + } + + // 提交 + if err = tx.Commit(ctx); err != nil { + return 0, err + } + + return postId, nil +} + +func (s *SysPostService) Update(ctx context.Context, id int32, req request.UpdatePostRequest) error { + // 开启事务 + tx, err := s.pool.BeginTx(ctx, pgx.TxOptions{}) + if err != nil { + return err + } + + defer func() { + _ = tx.Rollback(ctx) + }() + + q := db.New(tx) + + params := db.UpdatePostParams{ + Title: req.Title, + CoverID: req.CoverID, + Slug: req.Slug, + Content: req.Content, + Summary: req.Summary, + Status: req.Status, + Sort: req.Sort, + PublishedAt: req.PublishedAt, + ID: id, + } + + rows, err := q.UpdatePost(ctx, params) + if err != nil { + return dberr.MapUniqueViolation(err, dberr.PostSlugKey, errs.ErrSlugAlreadyExists) + } + + if err = dberr.MapRowsAffected(rows, nil, errs.ErrPostNotFound); err != nil { + return err + } + + if err = q.DeletePostCategory(ctx, id); err != nil { + return err + } + + if err = q.CreatePostCategory(ctx, db.CreatePostCategoryParams{ + PostID: id, + CategoryID: *req.CategoryID, + }); err != nil { + return err + } + + // 提交 + if err = tx.Commit(ctx); err != nil { + return err + } + + return nil +} + +func (s *SysPostService) Delete(ctx context.Context, id int32) error { + rows, err := s.queries.DeletePost(ctx, id) + return dberr.MapRowsAffected(rows, err, errs.ErrPostNotFound) +} diff --git a/internal/service/sys_role.go b/internal/service/sys_role.go new file mode 100644 index 0000000..18d3332 --- /dev/null +++ b/internal/service/sys_role.go @@ -0,0 +1,211 @@ +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 SysRoleService struct { + queries *db.Queries + pool *pgxpool.Pool + cache *cache.Caches +} + +func NewSysRoleService(queries *db.Queries, pool *pgxpool.Pool, cache *cache.Caches) *SysRoleService { + return &SysRoleService{ + queries: queries, + pool: pool, + cache: cache, + } +} + +func (s *SysRoleService) ListPage(ctx context.Context, p *common.Pagination) ([]db.SysRole, int64, error) { + params := db.ListSysRolesParams{ + Limit: p.PageSize, + Offset: (p.Page - 1) * p.PageSize, + } + + total, err := s.queries.CountSysRoles(ctx) + + if err != nil { + return nil, 0, err + } + + list, err := s.queries.ListSysRoles(ctx, params) + if err != nil { + return nil, 0, err + } + + return list, total, nil +} + +func (s *SysRoleService) GetRoles(ctx context.Context) ([]db.SysRole, error) { + return s.queries.GetAllSysRoles(ctx) +} + +func (s *SysRoleService) GetRoleMenus(ctx context.Context, id int32) ([]db.GetSysRoleMenusRow, error) { + _, err := s.queries.GetSysRoleByID(ctx, id) + if err != nil { + return nil, dberr.MapNoRows(err, errs.ErrSysRoleNotFound) + } + + return s.queries.GetSysRoleMenus(ctx, id) +} + +func (s *SysRoleService) GetRoleApis(ctx context.Context, id int32) ([]db.GetSysRoleApisRow, error) { + _, err := s.queries.GetSysRoleByID(ctx, id) + if err != nil { + return nil, dberr.MapNoRows(err, errs.ErrSysRoleNotFound) + } + + return s.queries.GetSysRoleApis(ctx, id) +} + +func (s *SysRoleService) Create(ctx context.Context, req request.CreateSysRoleRequest) error { + params := db.CreateSysRoleParams{ + Name: req.Name, + Code: req.Code, + } + + // 清理缓存 + s.cache.ClearAllSysUserCache() + + err := s.queries.CreateSysRole(ctx, params) + if err != nil { + return dberr.MapUniqueViolation(err, dberr.SysRoleCodeKey, errs.ErrCodeAlreadyExists) + } + return nil +} + +func (s *SysRoleService) Update(ctx context.Context, id int32, req request.UpdateSysRoleRequest) error { + params := db.UpdateSysRoleParams{ + ID: id, + Name: req.Name, + } + + // 清理缓存 + s.cache.ClearAllSysUserCache() + + rows, err := s.queries.UpdateSysRole(ctx, params) + return dberr.MapRowsAffected(rows, err, errs.ErrSysRoleNotFound) +} + +func (s *SysRoleService) SetRoleMenus(ctx context.Context, roleID int32, req request.SetSysRoleMenusRequest) error { + _, err := s.queries.GetSysRoleByID(ctx, roleID) + if err != nil { + return dberr.MapNoRows(err, errs.ErrSysRoleNotFound) + } + + 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) + + permissionIds, err := s.queries.GetSysPermissionIdsByMenuIDs(ctx, req.MenuIDs) + if err != nil { + return err + } + + params := make([]db.CreateSysRolePermissionParams, 0, len(permissionIds)) + for _, id := range permissionIds { + params = append(params, db.CreateSysRolePermissionParams{ + RoleID: roleID, + PermissionID: id, + }) + } + + if err = q.DeleteSysRolePermission(ctx, db.DeleteSysRolePermissionParams{ + RoleID: roleID, + Type: int16(enum.PermissionTypeMenu), + }); err != nil { + return err + } + + _, err = q.CreateSysRolePermission(ctx, params) + if err != nil { + return err + } + + if err = tx.Commit(ctx); err != nil { + return err + } + + // 清理缓存 + s.cache.ClearAllSysUserCache() + + return err +} + +func (s *SysRoleService) SetRoleApis(ctx context.Context, roleID int32, req request.SetSysRoleApisRequest) error { + _, err := s.queries.GetSysRoleByID(ctx, roleID) + if err != nil { + return dberr.MapNoRows(err, errs.ErrSysRoleNotFound) + } + + 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) + + permissionIds, err := s.queries.GetSysPermissionIdsByApiIDs(ctx, req.ApiIDs) + if err != nil { + return err + } + + params := make([]db.CreateSysRolePermissionParams, 0, len(permissionIds)) + for _, id := range permissionIds { + params = append(params, db.CreateSysRolePermissionParams{ + RoleID: roleID, + PermissionID: id, + }) + } + + if err = q.DeleteSysRolePermission(ctx, db.DeleteSysRolePermissionParams{ + RoleID: roleID, + Type: int16(enum.PermissionTypeApi), + }); err != nil { + return err + } + + _, err = q.CreateSysRolePermission(ctx, params) + if err != nil { + return err + } + + if err = tx.Commit(ctx); err != nil { + return err + } + + // 清理缓存 + s.cache.ClearAllSysUserCache() + + return err +} + +func (s *SysRoleService) Delete(ctx context.Context, id int32) error { + // 清理缓存 + s.cache.ClearAllSysUserCache() + rows, err := s.queries.DeleteSysRole(ctx, id) + return dberr.MapRowsAffected(rows, err, errs.ErrSysRoleNotFound) +} diff --git a/internal/service/sys_user.go b/internal/service/sys_user.go new file mode 100644 index 0000000..6ff0d2b --- /dev/null +++ b/internal/service/sys_user.go @@ -0,0 +1,290 @@ +package service + +import ( + "context" + db "server/internal/db/sqlc" + "server/internal/middleware" + "server/internal/model/common" + "server/internal/model/request" + "server/internal/model/response" + "server/internal/pkg/cache" + "server/internal/pkg/dberr" + "server/internal/pkg/errs" + "server/internal/pkg/httputil" + + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgxpool" + "golang.org/x/crypto/bcrypt" + "golang.org/x/sync/errgroup" +) + +type SysUserService struct { + queries *db.Queries + pool *pgxpool.Pool + jwt *middleware.JWTMiddleware + cache *cache.Caches +} + +func NewSysUserService(queries *db.Queries, pool *pgxpool.Pool, jwt *middleware.JWTMiddleware, cache *cache.Caches) *SysUserService { + return &SysUserService{ + queries: queries, + pool: pool, + jwt: jwt, + cache: cache, + } +} + +// generatePasswordHash 生成密码哈希 +func generatePasswordHash(password string) (string, error) { + hashed, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) + if err != nil { + return "", err + } + return string(hashed), nil +} + +// comparePasswordHash 比较密码哈希 +func comparePasswordHash(passwordHash, inputPassword string) error { + return bcrypt.CompareHashAndPassword([]byte(passwordHash), []byte(inputPassword)) +} + +func (s *SysUserService) Login(ctx context.Context, req request.LoginRequest) (*response.LoginResponse, error) { + user, err := s.queries.GetSysUserByAccount(ctx, req.Account) + + if err != nil { + return nil, errs.ErrInvalidCredentials + } + + if err = comparePasswordHash(user.PasswordHash, req.Password); err != nil { + return nil, errs.ErrInvalidCredentials + } + + accessToken, accessTokenExp, err := s.jwt.GenerateAccessToken(user.ID) + if err != nil { + return nil, err + } + + refreshToken, refreshTokenExp, err := s.jwt.GenerateRefreshToken(user.ID) + if err != nil { + return nil, err + } + + return &response.LoginResponse{ + AccessToken: accessToken, + AccessTokenExp: accessTokenExp, + RefreshToken: refreshToken, + RefreshTokenExp: refreshTokenExp, + }, nil +} + +func (s *SysUserService) RefreshToken(ctx context.Context, refreshToken string) (*response.LoginResponse, error) { + claims, err := s.jwt.ParseRefreshToken(refreshToken) + if err != nil { + return nil, errs.ErrInvalidRefreshToken + } + + accessToken, accessTokenExp, err := s.jwt.GenerateAccessToken(claims.UserID) + if err != nil { + return nil, err + } + + return &response.LoginResponse{ + AccessToken: accessToken, + AccessTokenExp: accessTokenExp, + }, nil +} + +func (s *SysUserService) GetUserInfo(ctx context.Context, id int32, isAdmin bool) (*response.SysUserInfo, error) { + g, ctx := errgroup.WithContext(ctx) + + var ( + user db.GetSysUserByIDRow + roles []db.SysRole + menus []db.SysMenu + ) + + g.Go(func() error { + u, err := s.queries.GetSysUserByID(ctx, id) + if err != nil { + return dberr.MapNoRows(err, errs.ErrUserNotFound) + } + user = u + return nil + }) + + g.Go(func() error { + r, err := s.queries.GetSysUserRoles(ctx, id) + if err != nil { + return err + } + roles = r + return nil + }) + + g.Go(func() error { + var ( + m []db.SysMenu + err error + ) + if isAdmin { + m, err = s.queries.GetSysAdminMenus(ctx) + } else { + m, err = s.queries.GetSysUserMenus(ctx, id) + } + if err != nil { + return err + } + menus = m + return nil + }) + + if err := g.Wait(); err != nil { + return nil, err + } + + // 处理角色 + userInfo := response.NewSysUserInfo(user, roles, menus) + + return userInfo, nil +} + +func (s *SysUserService) ListPage(ctx context.Context, p *common.Pagination) ([]db.ListSysUsersRow, int64, error) { + params := db.ListSysUsersParams{ + Limit: p.PageSize, + Offset: (p.Page - 1) * p.PageSize, + } + + total, err := s.queries.CountSysUsers(ctx) + + if err != nil { + return nil, 0, err + } + + users, err := s.queries.ListSysUsers(ctx, params) + if err != nil { + return nil, 0, err + } + + // 处理每个用户的头像URL + for i := range users { + url := httputil.BuildFileUrl(users[i].AvatarUrl) + users[i].AvatarUrl = &url + } + + return users, total, nil +} + +func (s *SysUserService) GetRoles(ctx context.Context, id int32) ([]db.SysRole, error) { + // 先查询用户是否存在 + _, err := s.queries.GetSysUserByID(ctx, id) + if err != nil { + return nil, dberr.MapNoRows(err, errs.ErrUserNotFound) + } + + return s.queries.GetSysUserRoles(ctx, id) +} + +func (s *SysUserService) Create(ctx context.Context, req request.CreateSysUserRequest) error { + passwordHash, err := generatePasswordHash(req.Password) + if err != nil { + return err + } + + user := db.CreateSysUserParams{ + Account: req.Account, + Username: req.Username, + PasswordHash: passwordHash, + AvatarID: req.AvatarID, + } + + if err = s.queries.CreateSysUser(ctx, user); err != nil { + return dberr.MapUniqueViolation(err, dberr.SysUserAccountKey, errs.ErrAccountAlreadyExists) + } + + return nil +} + +func (s *SysUserService) Update(ctx context.Context, id int32, req request.UpdateSysUserRequest) error { + user := db.UpdateSysUserParams{ + Username: req.Username, + ID: id, + } + + if req.AvatarID.Set { + user.UpdateAvatarID = true + if req.AvatarID.Valid { + user.AvatarID = &req.AvatarID.Value + } + } + + rows, err := s.queries.UpdateSysUser(ctx, user) + return dberr.MapRowsAffected(rows, err, errs.ErrUserNotFound) +} + +func (s *SysUserService) SetRoles(ctx context.Context, userID int32, req request.SetSysUserRolesRequest) error { + // 先查询用户是否存在 + _, err := s.queries.GetSysUserByID(ctx, userID) + if err != nil { + return dberr.MapNoRows(err, errs.ErrUserNotFound) + } + + 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) + + if err = q.ClearSysUserRoles(ctx, userID); err != nil { + return err + } + + for _, roleID := range req.RoleIDs { + if err = q.CreateSysUserRole(ctx, db.CreateSysUserRoleParams{ + UserID: userID, + RoleID: roleID, + }); err != nil { + return err + } + } + + if err = tx.Commit(ctx); err != nil { + return err + } + + // 清理缓存 + s.cache.ClearSysUserCache(userID) + + return nil +} + +func (s *SysUserService) UpdatePassword(ctx context.Context, id int32, req request.UpdateSysUserPassword) error { + passwordHash, err := generatePasswordHash(req.Password) + if err != nil { + return err + } + + params := db.UpdateSysUserPasswordParams{ + ID: id, + PasswordHash: passwordHash, + } + + rows, err := s.queries.UpdateSysUserPassword(ctx, params) + return dberr.MapRowsAffected(rows, err, errs.ErrUserNotFound) +} + +func (s *SysUserService) Delete(ctx context.Context, id int32) error { + if id == 1 { + return errs.ErrCannotDeleteSuperAdmin + } + + // 清理缓存 + s.cache.ClearSysUserCache(id) + + rows, err := s.queries.DeleteSysUser(ctx, id) + return dberr.MapRowsAffected(rows, err, errs.ErrUserNotFound) +} diff --git a/internal/utils/utils.go b/internal/utils/utils.go new file mode 100644 index 0000000..0853f1e --- /dev/null +++ b/internal/utils/utils.go @@ -0,0 +1,28 @@ +package utils + +import ( + "net" + "net/http" + "strings" +) + +func ClientIP(r *http.Request) string { + if xff := r.Header.Get("X-Forwarded-For"); xff != "" { + parts := strings.Split(xff, ",") + if len(parts) > 0 { + return strings.TrimSpace(parts[0]) + } + } + + if ip := r.Header.Get("X-Real-IP"); ip != "" { + return ip + } + + // RemoteAddr: IP:port + host, _, err := net.SplitHostPort(r.RemoteAddr) + if err == nil { + return host + } + + return r.RemoteAddr +} diff --git a/sqlc.yaml b/sqlc.yaml new file mode 100644 index 0000000..f56c60e --- /dev/null +++ b/sqlc.yaml @@ -0,0 +1,22 @@ +version: "2" +sql: + - engine: "postgresql" + queries: "./internal/db/query/*" + schema: "./internal/db/schema/*" + gen: + go: + package: "db" + out: "./internal/db/sqlc" + emit_empty_slices: true + emit_interface: false + sql_package: "pgx/v5" + emit_json_tags: true + emit_pointers_for_null_types: true + overrides: + - db_type: "timestamptz" + go_type: + type: "time.Time" + - db_type: "timestamptz" + nullable: true + go_type: + type: "*time.Time" \ No newline at end of file