This commit is contained in:
kenneth 2025-10-27 15:24:08 +08:00
parent 4186cd0caf
commit df4c3dd46f
47 changed files with 1757 additions and 306 deletions

51
go.mod
View File

@ -3,35 +3,36 @@ module management
go 1.24.2
require (
github.com/a-h/templ v0.3.898
github.com/alexedwards/scs/v2 v2.8.0
github.com/a-h/templ v0.3.960
github.com/alexedwards/scs/v2 v2.9.0
github.com/bwmarrin/snowflake v0.3.0
github.com/drhin/logger v0.0.0-20250417021954-aa33afe047bc
github.com/evanw/esbuild v0.25.5
github.com/fsnotify/fsnotify v1.9.0
github.com/fvbock/endless v0.0.0-20170109170031-447134032cb6
github.com/go-chi/chi/v5 v5.2.2
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.26.0
github.com/go-playground/validator/v10 v10.28.0
github.com/google/uuid v1.6.0
github.com/h2non/filetype v1.1.3
github.com/hibiken/asynq v0.25.1
github.com/jackc/pgx/v5 v5.7.5
github.com/jackc/pgx/v5 v5.7.6
github.com/jmoiron/sqlx v1.4.0
github.com/justinas/nosurf v1.2.0
github.com/matoous/go-nanoid/v2 v2.1.0
github.com/mojocn/base64Captcha v1.3.8
github.com/patrickmn/go-cache v2.1.0+incompatible
github.com/redis/go-redis/v9 v9.10.0
github.com/spf13/cobra v1.9.1
github.com/spf13/viper v1.20.1
github.com/redis/go-redis/v9 v9.16.0
github.com/spf13/cobra v1.10.1
github.com/spf13/viper v1.21.0
github.com/zhang2092/browser v0.0.2
go.uber.org/zap v1.27.0
golang.org/x/crypto v0.39.0
golang.org/x/sync v0.15.0
gorm.io/datatypes v1.2.5
golang.org/x/crypto v0.43.0
golang.org/x/sync v0.17.0
gorm.io/datatypes v1.2.7
gorm.io/driver/postgres v1.6.0
gorm.io/gorm v1.30.0
gorm.io/gorm v1.31.0
)
require (
@ -39,9 +40,9 @@ require (
github.com/blang/semver/v4 v4.0.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/gabriel-vasile/mimetype v1.4.9 // indirect
github.com/gabriel-vasile/mimetype v1.4.10 // indirect
github.com/go-sql-driver/mysql v1.9.3 // indirect
github.com/go-viper/mapstructure/v2 v2.3.0 // indirect
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
@ -53,20 +54,18 @@ require (
github.com/natefinch/lumberjack v2.0.0+incompatible // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/robfig/cron/v3 v3.0.1 // indirect
github.com/sagikazarmark/locafero v0.9.0 // indirect
github.com/sourcegraph/conc v0.3.0 // indirect
github.com/spf13/afero v1.14.0 // indirect
github.com/spf13/cast v1.9.2 // indirect
github.com/spf13/pflag v1.0.6 // indirect
github.com/sagikazarmark/locafero v0.12.0 // indirect
github.com/spf13/afero v1.15.0 // indirect
github.com/spf13/cast v1.10.0 // indirect
github.com/spf13/pflag v1.0.10 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/image v0.28.0 // indirect
golang.org/x/net v0.41.0 // indirect
golang.org/x/sys v0.33.0 // indirect
golang.org/x/text v0.26.0 // indirect
golang.org/x/time v0.12.0 // indirect
google.golang.org/protobuf v1.36.6 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/image v0.32.0 // indirect
golang.org/x/sys v0.37.0 // indirect
golang.org/x/text v0.30.0 // indirect
golang.org/x/time v0.14.0 // indirect
google.golang.org/protobuf v1.36.10 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
gorm.io/driver/mysql v1.6.0 // indirect
)

124
go.sum
View File

@ -2,16 +2,18 @@ filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg=
github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/a-h/templ v0.3.898 h1:g9oxL/dmM6tvwRe2egJS8hBDQTncokbMoOFk1oJMX7s=
github.com/a-h/templ v0.3.898/go.mod h1:oLBbZVQ6//Q6zpvSMPTuBK0F3qOtBdFBcGRspcT+VNQ=
github.com/alexedwards/scs/v2 v2.8.0 h1:h31yUYoycPuL0zt14c0gd+oqxfRwIj6SOjHdKRZxhEw=
github.com/alexedwards/scs/v2 v2.8.0/go.mod h1:ToaROZxyKukJKT/xLcVQAChi5k6+Pn1Gvmdl7h3RRj8=
github.com/a-h/templ v0.3.960 h1:trshEpGa8clF5cdI39iY4ZrZG8Z/QixyzEyUnA7feTM=
github.com/a-h/templ v0.3.960/go.mod h1:oCZcnKRf5jjsGpf2yELzQfodLphd2mwecwG4Crk5HBo=
github.com/alexedwards/scs/v2 v2.9.0 h1:xa05mVpwTBm1iLeTMNFfAWpKUm4fXAW7CeAViqBVS90=
github.com/alexedwards/scs/v2 v2.9.0/go.mod h1:ToaROZxyKukJKT/xLcVQAChi5k6+Pn1Gvmdl7h3RRj8=
github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM=
github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ=
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
github.com/bwmarrin/snowflake v0.3.0 h1:xm67bEhkKh6ij1790JB83OujPR5CzNe8QuQqAgISZN0=
github.com/bwmarrin/snowflake v0.3.0/go.mod h1:NdZxfVWX+oR6y2K0o6qAYv6gIOP9rjG0/E9WsDpxqwE=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
@ -30,35 +32,32 @@ github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/fvbock/endless v0.0.0-20170109170031-447134032cb6 h1:6VSn3hB5U5GeA6kQw4TwWIWbOhtvR2hmbBJnTOtqTWc=
github.com/fvbock/endless v0.0.0-20170109170031-447134032cb6/go.mod h1:YxOVT5+yHzKvwhsiSIWmbAYM3Dr9AEEbER2dVayfBkg=
github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY=
github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok=
github.com/go-chi/chi/v5 v5.2.1 h1:KOIHODQj58PmL80G2Eak4WdvUzjSJSm0vG72crDCqb8=
github.com/go-chi/chi/v5 v5.2.1/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
github.com/go-chi/chi/v5 v5.2.2 h1:CMwsvRVTbXVytCk1Wd72Zy1LAsAh9GxMmSNWLHCG618=
github.com/go-chi/chi/v5 v5.2.2/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
github.com/gabriel-vasile/mimetype v1.4.10 h1:zyueNbySn/z8mJZHLt6IPw0KoZsiQNszIpU+bX4+ZK0=
github.com/gabriel-vasile/mimetype v1.4.10/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
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-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.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc/iMaVtFbr3Sw2k=
github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
github.com/go-playground/validator/v10 v10.28.0 h1:Q7ibns33JjyW48gHkuFT91qX48KG0ktULL6FgHdG688=
github.com/go-playground/validator/v10 v10.28.0/go.mod h1:GoI6I1SjPBh9p7ykNE/yj3fFYbyDOpwMn5KXd+m2hUU=
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo=
github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss=
github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/go-viper/mapstructure/v2 v2.3.0 h1:27XbWsHIqhbdR5TIC911OfYvgSaW93HM+dX7970Q7jk=
github.com/go-viper/mapstructure/v2 v2.3.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA=
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A=
github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gopherjs/gopherjs v1.17.2 h1:fQnZVsXk8uxXIStYb0N4bGk7jeyTalG/wsZjQ25dO0g=
@ -73,8 +72,8 @@ github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsI
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.5 h1:JHGfMnQY+IEtGM63d+NGMjoRpysB2JBwDr5fsngwmJs=
github.com/jackc/pgx/v5 v5.7.5/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M=
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/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
@ -111,36 +110,35 @@ github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/redis/go-redis/v9 v9.10.0 h1:FxwK3eV8p/CQa0Ch276C7u2d0eNC9kCmAYQ7mCXCzVs=
github.com/redis/go-redis/v9 v9.10.0/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw=
github.com/redis/go-redis/v9 v9.16.0 h1:OotgqgLSRCmzfqChbQyG1PHC3tLNR89DG4jdOERSEP4=
github.com/redis/go-redis/v9 v9.16.0/go.mod h1:u410H11HMLoB+TP67dz8rL9s6QW2j76l0//kSOd3370=
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
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/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sagikazarmark/locafero v0.9.0 h1:GbgQGNtTrEmddYDSAH9QLRyfAHY12md+8YFTqyMTC9k=
github.com/sagikazarmark/locafero v0.9.0/go.mod h1:UBUyz37V+EdMS3hDF3QWIiVr/2dPrx49OMO0Bn0hJqk=
github.com/sagikazarmark/locafero v0.12.0 h1:/NQhBAkUb4+fH1jivKHWusDYFjMOOKU88eegjfxfHb4=
github.com/sagikazarmark/locafero v0.12.0/go.mod h1:sZh36u/YSZ918v0Io+U9ogLYQJ9tLLBmM4eneO6WwsI=
github.com/smarty/assertions v1.15.0 h1:cR//PqUBUiQRakZWqBiFFQ9wb8emQGDb0HeGdqGByCY=
github.com/smarty/assertions v1.15.0/go.mod h1:yABtdzeQs6l1brC900WlRNwj6ZR55d7B+E8C6HtKdec=
github.com/smartystreets/goconvey v1.8.1 h1:qGjIddxOk4grTu9JPOU31tVfq3cNdBlNa5sSznIX1xY=
github.com/smartystreets/goconvey v1.8.1/go.mod h1:+/u4qLyY6x1jReYOp7GOM2FSt8aP9CzCZL03bI28W60=
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
github.com/spf13/afero v1.14.0 h1:9tH6MapGnn/j0eb0yIXiLjERO8RB6xIVZRDCX7PtqWA=
github.com/spf13/afero v1.14.0/go.mod h1:acJQ8t0ohCGuMN3O+Pv0V0hgMxNYDlvdk+VTfyZmbYo=
github.com/spf13/cast v1.9.2 h1:SsGfm7M8QOFtEzumm7UZrZdLLquNdzFYfIbEXntcFbE=
github.com/spf13/cast v1.9.2/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.20.1 h1:ZMi+z/lvLyPSCoNtFCpqjy0S4kPbirhpTMwl8BkW9X4=
github.com/spf13/viper v1.20.1/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4=
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s=
github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0=
github.com/spf13/pflag v1.0.9/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/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU=
github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
@ -152,16 +150,18 @@ go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM=
golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
golang.org/x/image v0.23.0/go.mod h1:wJJBTdLfCCf3tiHa1fNxpZmUI4mmoZvwMCPP0ddoNKY=
golang.org/x/image v0.28.0 h1:gdem5JW1OLS4FbkWgLO+7ZeFzYtL3xClb97GaUzYMFE=
golang.org/x/image v0.28.0/go.mod h1:GUJYXtnGKEUgggyzh+Vxt+AviiCcyiwpsl8iQ8MvwGY=
golang.org/x/image v0.32.0 h1:6lZQWq75h7L5IWNk0r+SCpUJ6tUVd3v4ZHnbRKLkUDQ=
golang.org/x/image v0.32.0/go.mod h1:/R37rrQmKXtO6tYXAjtDLwQgFLHmhW+V6ayXlxzP2Pc=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
@ -175,8 +175,6 @@ golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@ -184,8 +182,8 @@ golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@ -197,8 +195,8 @@ golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
@ -216,12 +214,10 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg=
golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
@ -229,10 +225,8 @@ golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.35.2 h1:8Ar7bF+apOIoThw1EdZl0p1oWvMqTHmpA2fRTyZO8io=
google.golang.org/protobuf v1.35.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/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=
@ -243,15 +237,15 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/datatypes v1.2.5 h1:9UogU3jkydFVW1bIVVeoYsTpLRgwDVW3rHfJG6/Ek9I=
gorm.io/datatypes v1.2.5/go.mod h1:I5FUdlKpLb5PMqeMQhm30CQ6jXP8Rj89xkTeCSAaAD4=
gorm.io/datatypes v1.2.7 h1:ww9GAhF1aGXZY3EB3cJPJ7//JiuQo7DlQA7NNlVaTdk=
gorm.io/datatypes v1.2.7/go.mod h1:M2iO+6S3hhi4nAyYe444Pcb0dcIiOMJ7QHaUXxyiNZY=
gorm.io/driver/mysql v1.6.0 h1:eNbLmNTpPpTOVZi8MMxCi2aaIm0ZpInbORNXDwyLGvg=
gorm.io/driver/mysql v1.6.0/go.mod h1:D/oCC2GWK3M/dqoLxnOlaNKmXz8WNTfcS9y5ovaSqKo=
gorm.io/driver/postgres v1.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4=
gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo=
gorm.io/driver/sqlite v1.4.3 h1:HBBcZSDnWi5BW3B3rwvVTc510KGkBkexlOg0QrmLUuU=
gorm.io/driver/sqlite v1.4.3/go.mod h1:0Aq3iPO+v9ZKbcdiz8gLWRw5VOPcBOPUQJFLq5e2ecI=
gorm.io/driver/sqlserver v1.5.4 h1:xA+Y1KDNspv79q43bPyjDMUgHoYHLhXYmdFcYPobg8g=
gorm.io/driver/sqlserver v1.5.4/go.mod h1:+frZ/qYmuna11zHPlh5oc2O6ZA/lS88Keb0XSH1Zh/g=
gorm.io/gorm v1.30.0 h1:qbT5aPv1UH8gI99OsRlvDToLxW5zR7FzS9acZDOZcgs=
gorm.io/gorm v1.30.0/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE=
gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ=
gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8=
gorm.io/driver/sqlserver v1.6.0 h1:VZOBQVsVhkHU/NzNhRJKoANt5pZGQAS1Bwc6m6dgfnc=
gorm.io/driver/sqlserver v1.6.0/go.mod h1:WQzt4IJo/WHKnckU9jXBLMJIVNMVeTu25dnOzehntWw=
gorm.io/gorm v1.31.0 h1:0VlycGreVhK7RF/Bwt51Fk8v0xLiiiFdbGDPIZQ7mJY=
gorm.io/gorm v1.31.0/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=

View File

@ -82,12 +82,11 @@ func (a *app) login(w http.ResponseWriter, r *http.Request) {
//err := a.userService.Login(ctx, &req)
risk, err := a.authService.Authenticate(ctx, req)
if err != nil {
log.Println(err)
a.render.JSONErr(w, err.Error())
return
}
log.Println(risk)
log.Println("risk:", risk)
a.render.JSONOk(w, "login successfully")
default:

View File

@ -2,6 +2,7 @@ package config
import (
"encoding/json"
"errors"
"net/http"
"strings"
@ -11,8 +12,8 @@ import (
systemService "management/internal/erpserver/service/v1"
"management/internal/erpserver/templ/system/config"
"management/internal/pkg/convertor"
"management/internal/pkg/database"
"management/internal/pkg/render"
"management/internal/pkg/sqldb"
)
type app struct {
@ -91,7 +92,7 @@ func (a *app) save(w http.ResponseWriter, r *http.Request) {
}
err := a.configService.Create(ctx, arg)
if err != nil {
if database.IsUniqueViolation(err) {
if errors.Is(err, sqldb.ErrDBDuplicatedEntry) {
a.render.JSONErr(w, "数据已存在")
return
}

View File

@ -1,6 +1,7 @@
package menu
import (
"errors"
"net/http"
"strconv"
"strings"
@ -10,9 +11,9 @@ import (
v1 "management/internal/erpserver/service/v1"
"management/internal/erpserver/templ/system/menu"
"management/internal/pkg/convertor"
"management/internal/pkg/database"
"management/internal/pkg/mid"
"management/internal/pkg/render"
"management/internal/pkg/sqldb"
"github.com/google/uuid"
)
@ -144,7 +145,7 @@ func (a *app) save(w http.ResponseWriter, r *http.Request) {
}
err := a.menuService.Create(ctx, arg)
if err != nil {
if database.IsUniqueViolation(err) {
if errors.Is(err, sqldb.ErrDBDuplicatedEntry) {
a.render.JSONErr(w, "菜单已存在")
return
}

View File

@ -8,8 +8,8 @@ import (
)
type DepartmentRepository interface {
Initialize(ctx context.Context) error
Create(ctx context.Context, obj *Department) error
Initialize(ctx context.Context) (*Department, error)
Create(ctx context.Context, obj *Department) (*Department, error)
Update(ctx context.Context, obj *Department) error
Get(ctx context.Context, id int32) (*Department, error)
All(ctx context.Context) ([]*Department, error)

View File

@ -9,7 +9,7 @@ import (
type RoleRepository interface {
Initialize(ctx context.Context) (*Role, error)
Create(ctx context.Context, obj *Role) error
Create(ctx context.Context, obj *Role) (*Role, error)
Update(ctx context.Context, obj *Role) error
Get(ctx context.Context, id int32) (*Role, error)
GetByVip(ctx context.Context, vip bool) (*Role, error)

View File

@ -11,8 +11,8 @@ import (
type UserRepository interface {
Initialize(ctx context.Context, departId, roleId int32) error
Create(ctx context.Context, obj *User) error
Update(ctx context.Context, obj *User) error
Create(ctx context.Context, obj *User) (*User, error)
Update(ctx context.Context, obj *User) (*User, error)
Get(ctx context.Context, id int32) (*User, error)
GetByEmail(ctx context.Context, email string) (*User, error)
All(ctx context.Context) ([]*User, error)

View File

@ -41,7 +41,7 @@ func (s *Seed) Run() error {
}
// 部门
err := s.departmentRepository.Initialize(ctx)
depart, err := s.departmentRepository.Initialize(ctx)
if err != nil {
return err
}
@ -53,7 +53,7 @@ func (s *Seed) Run() error {
}
// 用户
if err := s.userRepository.Initialize(ctx, 0, role.ID); err != nil {
if err := s.userRepository.Initialize(ctx, depart.ID, role.ID); err != nil {
return err
}

View File

@ -3,13 +3,14 @@ package config
import (
"bytes"
"context"
"database/sql"
"encoding/json"
"errors"
"fmt"
"management/internal/erpserver/model/dto"
"management/internal/erpserver/model/system"
"management/internal/erpserver/repository"
"management/internal/pkg/database"
"management/internal/pkg/know/pearadmin"
"management/internal/pkg/sqldb"
@ -32,7 +33,7 @@ func NewStore(db *repository.Store, log *logger.Logger) system.ConfigRepository
func (s *store) Initialize(ctx context.Context) error {
_, err := s.GetByKey(ctx, pearadmin.PearKey)
if err != nil {
if database.IsNoRows(err) {
if errors.Is(err, sql.ErrNoRows) {
b, e := json.Marshal(pearadmin.PearJson)
if e != nil {
return e

View File

@ -26,14 +26,14 @@ func NewStore(db *repository.Store, log *logger.Logger) system.DepartmentReposit
}
}
func (s *store) Initialize(ctx context.Context) error {
func (s *store) Initialize(ctx context.Context) (*system.Department, error) {
count, err := s.Count(ctx, dto.SearchDto{})
if err != nil {
return err
return nil, err
}
if count == 0 {
obj := system.Department{
obj := &system.Department{
Name: "公司",
ParentID: 0,
ParentPath: ",0,",
@ -42,21 +42,33 @@ func (s *store) Initialize(ctx context.Context) error {
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
return s.Create(ctx, &obj)
return s.Create(ctx, obj)
}
return nil
return s.Get(ctx, 1)
}
func (s *store) Create(ctx context.Context, obj *system.Department) error {
func (s *store) Create(ctx context.Context, obj *system.Department) (*system.Department, error) {
//goland:noinspection ALL
const q = `
INSERT INTO sys_department (
name, parent_id, parent_path, status, sort
) VALUES (
:name, :parent_id, :parent_path, :status, :sort
);`
) RETURNING *;`
return sqldb.NamedExecContext(ctx, s.log, s.db.DB(ctx), q, obj)
data := map[string]any{
"name": obj.Name,
"parent_id": obj.ParentID,
"parent_path": obj.ParentPath,
"status": obj.Status,
"sort": obj.Sort,
}
err := sqldb.NamedQueryStruct(ctx, s.log, s.db.DB(ctx), q, data, &obj)
if err != nil {
return nil, err
}
return obj, err
}
func (s *store) Update(ctx context.Context, obj *system.Department) error {

View File

@ -30,9 +30,24 @@ func (s *store) Create(ctx context.Context, obj *system.Menu) (*system.Menu, err
name, display_name, url, type, parent_id, parent_path, avatar, style, visible, is_list, status, sort
) VALUES (
:name, :display_name, :url, :type, :parent_id, :parent_path, :avatar, :style, :visible, :is_list, :status, :sort
);`
) RETURNING *;`
err := sqldb.NamedExecContext(ctx, s.log, s.db.DB(ctx), q, obj)
data := map[string]any{
"name": obj.Name,
"display_name": obj.DisplayName,
"url": obj.Url,
"type": obj.Type,
"parent_id": obj.ParentID,
"parent_path": obj.ParentPath,
"avatar": obj.Avatar,
"style": obj.Style,
"visible": obj.Visible,
"is_list": obj.IsList,
"status": obj.Status,
"sort": obj.Sort,
}
err := sqldb.NamedQueryStruct(ctx, s.log, s.db.DB(ctx), q, data, &obj)
if err != nil {
return nil, err
}

View File

@ -32,7 +32,8 @@ func (s *store) Initialize(ctx context.Context) (*system.Role, error) {
return nil, err
}
if count == 0 {
obj := system.Role{
var err error
obj := &system.Role{
Name: "Company",
DisplayName: "公司",
Vip: false,
@ -42,11 +43,12 @@ func (s *store) Initialize(ctx context.Context) (*system.Role, error) {
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
if err := s.Create(ctx, &obj); err != nil {
obj, err = s.Create(ctx, obj)
if err != nil {
return nil, err
}
obj1 := system.Role{
obj1 := &system.Role{
Name: "SuperAdmin",
DisplayName: "超级管理员",
Vip: true,
@ -56,26 +58,42 @@ func (s *store) Initialize(ctx context.Context) (*system.Role, error) {
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
if err := s.Create(ctx, &obj1); err != nil {
obj1, err = s.Create(ctx, obj1)
if err != nil {
return nil, err
}
return &obj1, nil
return obj1, nil
}
return s.GetByVip(ctx, true)
}
func (s *store) Create(ctx context.Context, obj *system.Role) error {
func (s *store) Create(ctx context.Context, obj *system.Role) (*system.Role, error) {
//goland:noinspection ALL
const q = `
INSERT INTO sys_role (
name, display_name, parent_id, parent_path, vip, status, sort
) VALUES (
:name, :display_name, :parent_id, :parent_path, :vip, :status, :sort
)`
) RETURNING *`
return sqldb.NamedExecContext(ctx, s.log, s.db.DB(ctx), q, obj)
data := map[string]any{
"name": obj.Name,
"display_name": obj.DisplayName,
"parent_id": obj.ParentID,
"parent_path": obj.ParentPath,
"vip": obj.Vip,
"status": obj.Status,
"sort": obj.Sort,
}
err := sqldb.NamedQueryStruct(ctx, s.log, s.db.DB(ctx), q, data, &obj)
if err != nil {
return nil, err
}
return obj, nil
}
func (s *store) Update(ctx context.Context, obj *system.Role) error {

View File

@ -67,25 +67,44 @@ func (s *store) Initialize(ctx context.Context, departId, roleId int32) error {
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
return s.Create(ctx, &user)
_, err = s.Create(ctx, &user)
return err
}
return nil
}
func (s *store) Create(ctx context.Context, obj *system.User) error {
func (s *store) Create(ctx context.Context, obj *system.User) (*system.User, error) {
//goland:noinspection ALL
const q = `
INSERT INTO sys_user (
uuid, email, username, hashed_password, salt, avatar, gender, department_id, role_id, status
) VALUES (
:uuid, :email, :username, :hashed_password, :salt, :avatar, :gender, :department_id, :role_id, :status
);`
) RETURNING *;`
return sqldb.NamedExecContext(ctx, s.log, s.db.DB(ctx), q, obj)
data := map[string]any{
"uuid": obj.Uuid.String(),
"email": obj.Email,
"username": obj.Username,
"hashed_password": obj.HashedPassword,
"salt": obj.Salt,
"avatar": obj.Avatar,
"gender": obj.Gender,
"department_id": obj.DepartmentID,
"role_id": obj.RoleID,
"status": obj.Status,
}
err := sqldb.NamedQueryStruct(ctx, s.log, s.db.DB(ctx), q, data, &obj)
if err != nil {
return nil, err
}
return obj, nil
}
func (s *store) Update(ctx context.Context, obj *system.User) error {
func (s *store) Update(ctx context.Context, obj *system.User) (*system.User, error) {
//goland:noinspection ALL
const q = `
UPDATE sys_user
@ -99,9 +118,28 @@ func (s *store) Update(ctx context.Context, obj *system.User) error {
status = :status,
change_password_at = :change_password_at,
updated_at = :updated_at
WHERE id = :id;`
WHERE id = :id RETURNING *;`
return sqldb.NamedExecContext(ctx, s.log, s.db.DB(ctx), q, obj)
data := map[string]any{
"email": obj.Email,
"username": obj.Username,
"hashed_password": obj.HashedPassword,
"avatar": obj.Avatar,
"gender": obj.Gender,
"department_id": obj.DepartmentID,
"role_id": obj.RoleID,
"status": obj.Status,
"change_password_at": obj.ChangePasswordAt,
"updated_at": obj.UpdatedAt,
"id": obj.ID,
}
err := sqldb.NamedQueryStruct(ctx, s.log, s.db.DB(ctx), q, data, &obj)
if err != nil {
return nil, err
}
return obj, nil
}
func (s *store) Get(ctx context.Context, id int32) (*system.User, error) {

View File

@ -167,7 +167,7 @@ func (a *Auth) validateUser(ctx context.Context, email, password string) (*syste
user.Role, err = a.roleService.Get(ctx, user.RoleID)
if err != nil {
return nil, err
return nil, errors.New("账号没有配置角色, 请联系管理员")
}
if user.Role == nil || user.Role.ID == 0 {

View File

@ -12,10 +12,10 @@ import (
"management/internal/erpserver/model/system"
"management/internal/erpserver/model/view"
"management/internal/erpserver/service/util"
"management/internal/erpserver/service/v1"
v1 "management/internal/erpserver/service/v1"
"management/internal/pkg/convertor"
"management/internal/pkg/database"
"management/internal/pkg/know"
"management/internal/pkg/sqldb"
)
type departmentService struct {
@ -58,9 +58,9 @@ func (s *departmentService) Create(ctx context.Context, req *form.Department) er
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
err := s.repo.Create(ctx, arg)
_, err := s.repo.Create(ctx, arg)
if err != nil {
if database.IsUniqueViolation(err) {
if errors.Is(err, sqldb.ErrDBDuplicatedEntry) {
return errors.New("部门已存在")
}
return err

View File

@ -12,10 +12,10 @@ import (
"management/internal/erpserver/model/system"
"management/internal/erpserver/model/view"
"management/internal/erpserver/service/util"
"management/internal/erpserver/service/v1"
v1 "management/internal/erpserver/service/v1"
"management/internal/pkg/convertor"
"management/internal/pkg/database"
"management/internal/pkg/know"
"management/internal/pkg/sqldb"
)
type roleService struct {
@ -60,9 +60,9 @@ func (s *roleService) Create(ctx context.Context, req *form.Role) error {
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
err := s.repo.Create(ctx, arg)
_, err := s.repo.Create(ctx, arg)
if err != nil {
if database.IsUniqueViolation(err) {
if errors.Is(err, sqldb.ErrDBDuplicatedEntry) {
return errors.New("角色名称已存在")
}
return err

View File

@ -10,11 +10,11 @@ import (
"management/internal/erpserver/model/form"
"management/internal/erpserver/model/system"
"management/internal/erpserver/model/view"
"management/internal/erpserver/service/v1"
v1 "management/internal/erpserver/service/v1"
"management/internal/pkg/crypto"
"management/internal/pkg/database"
"management/internal/pkg/know"
"management/internal/pkg/rand"
"management/internal/pkg/sqldb"
"github.com/google/uuid"
"go.uber.org/zap"
@ -75,9 +75,9 @@ func (s *userService) Create(ctx context.Context, req *form.User) error {
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
err = s.repo.Create(ctx, user)
_, err = s.repo.Create(ctx, user)
if err != nil {
if database.IsUniqueViolation(err) {
if errors.Is(err, sqldb.ErrDBDuplicatedEntry) {
return errors.New("用户已经存在")
}
return err
@ -106,7 +106,9 @@ func (s *userService) Update(ctx context.Context, req *form.User) error {
user.HashedPassword = hashedPassword
user.ChangePasswordAt = time.Now()
}
return s.repo.Update(ctx, user)
_, err = s.repo.Update(ctx, user)
return err
}
func (s *userService) All(ctx context.Context) ([]*system.User, error) {

View File

@ -1,6 +1,6 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.898
// templ: version: v0.3.960
package auth
//lint:file-ignore SA4006 This context is only used if a nested component is present.

View File

@ -1,6 +1,6 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.898
// templ: version: v0.3.960
package base
//lint:file-ignore SA4006 This context is only used if a nested component is present.

View File

@ -1,6 +1,6 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.898
// templ: version: v0.3.960
package component
//lint:file-ignore SA4006 This context is only used if a nested component is present.

View File

@ -1,6 +1,6 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.898
// templ: version: v0.3.960
package component
//lint:file-ignore SA4006 This context is only used if a nested component is present.

View File

@ -1,6 +1,6 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.898
// templ: version: v0.3.960
package home
//lint:file-ignore SA4006 This context is only used if a nested component is present.

View File

@ -1,6 +1,6 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.898
// templ: version: v0.3.960
package home
//lint:file-ignore SA4006 This context is only used if a nested component is present.

View File

@ -1,6 +1,6 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.898
// templ: version: v0.3.960
package auditlog
//lint:file-ignore SA4006 This context is only used if a nested component is present.

View File

@ -1,6 +1,6 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.898
// templ: version: v0.3.960
package category
//lint:file-ignore SA4006 This context is only used if a nested component is present.

View File

@ -1,6 +1,6 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.898
// templ: version: v0.3.960
package config
//lint:file-ignore SA4006 This context is only used if a nested component is present.

View File

@ -1,6 +1,6 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.898
// templ: version: v0.3.960
package config
//lint:file-ignore SA4006 This context is only used if a nested component is present.

View File

@ -1,6 +1,6 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.898
// templ: version: v0.3.960
package department
//lint:file-ignore SA4006 This context is only used if a nested component is present.
@ -61,7 +61,7 @@ func Edit(ctx context.Context, item *system.Department) templ.Component {
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<input type=\"hidden\" id=\"id\" name=\"id\" value=\"\"")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<input type=\"hidden\" id=\"id\" name=\"id\" value=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@ -70,11 +70,11 @@ func Edit(ctx context.Context, item *system.Department) templ.Component {
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/erpserver/templ/system/department/edit.templ`, Line: 21, Col: 75}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(` ` + templ_7745c5c3_Var3))
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "><div class=\"layui-tab layui-tab-card\"><ul class=\"layui-tab-title\"><li class=\"layui-this\">基础信息</li><li>其它</li></ul><div class=\"layui-tab-content\"><!-- 基础信息 --><div class=\"layui-tab-item layui-show\">")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "\"><div class=\"layui-tab layui-tab-card\"><ul class=\"layui-tab-title\"><li class=\"layui-this\">基础信息</li><li>其它</li></ul><div class=\"layui-tab-content\"><!-- 基础信息 --><div class=\"layui-tab-item layui-show\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}

View File

@ -1,6 +1,6 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.898
// templ: version: v0.3.960
package department
//lint:file-ignore SA4006 This context is only used if a nested component is present.

View File

@ -1,6 +1,6 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.898
// templ: version: v0.3.960
package loginlog
//lint:file-ignore SA4006 This context is only used if a nested component is present.

View File

@ -1,6 +1,6 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.898
// templ: version: v0.3.960
package menu
//lint:file-ignore SA4006 This context is only used if a nested component is present.

View File

@ -1,6 +1,6 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.898
// templ: version: v0.3.960
package menu
//lint:file-ignore SA4006 This context is only used if a nested component is present.

View File

@ -1,6 +1,6 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.898
// templ: version: v0.3.960
package role
//lint:file-ignore SA4006 This context is only used if a nested component is present.

View File

@ -1,6 +1,6 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.898
// templ: version: v0.3.960
package role
//lint:file-ignore SA4006 This context is only used if a nested component is present.

View File

@ -1,6 +1,6 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.898
// templ: version: v0.3.960
package role
//lint:file-ignore SA4006 This context is only used if a nested component is present.

View File

@ -1,6 +1,6 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.898
// templ: version: v0.3.960
package user
//lint:file-ignore SA4006 This context is only used if a nested component is present.

View File

@ -1,6 +1,6 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.898
// templ: version: v0.3.960
package user
//lint:file-ignore SA4006 This context is only used if a nested component is present.

View File

@ -1,6 +1,6 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.898
// templ: version: v0.3.960
package user
//lint:file-ignore SA4006 This context is only used if a nested component is present.

View File

@ -1,15 +1,48 @@
package database
import (
"errors"
// import (
// "database/sql"
// "errors"
"gorm.io/gorm"
)
// "github.com/jackc/pgx/v5/pgconn"
// "gorm.io/gorm"
// )
func IsUniqueViolation(err error) bool {
return errors.Is(err, gorm.ErrDuplicatedKey)
}
// func IsGORMUniqueViolation(err error) bool {
// return errors.Is(err, gorm.ErrDuplicatedKey)
// }
func IsNoRows(err error) bool {
return errors.Is(err, gorm.ErrRecordNotFound)
}
// func IsGORMNoRows(err error) bool {
// return errors.Is(err, gorm.ErrRecordNotFound)
// }
// // ****************** errors ******************
// const (
// foreignKeyViolation = "23503"
// uniqueViolation = "23505"
// )
// // var ErrUniqueViolation = &pgconn.PgError{
// // Code: UniqueViolation,
// // }
// func ErrorCode(err error) string {
// var pgErr *pgconn.PgError
// if errors.As(err, &pgErr) {
// return pgErr.Code
// }
// return ""
// }
// func IsUniqueViolation(err error) bool {
// var pgErr *pgconn.PgError
// if errors.As(err, &pgErr) {
// return pgErr.Code == uniqueViolation
// }
// return false
// }
// func IsNoRows(err error) bool {
// return errors.Is(err, sql.ErrNoRows)
// }

View File

@ -9,6 +9,7 @@ import (
"path"
"time"
"github.com/google/uuid"
"github.com/h2non/filetype"
gonanoid "github.com/matoous/go-nanoid/v2"
)
@ -20,82 +21,94 @@ const (
var ErrUnsupported = errors.New("文件格式不支持")
type FileType int
type Type int
const (
ALL FileType = 0
IMG FileType = 1
ALL Type = 0
IMG Type = 1
)
func UploadFilename(filepath string, t FileType) (string, error) {
fileOpen, err := os.Open(filepath)
if err != nil {
return "", err
}
defer fileOpen.Close()
fileBytes, err := io.ReadAll(fileOpen)
if err != nil {
return "", errors.New("failed to read file")
}
//func UploadFilename(filepath string, t Type) (string, error) {
// fileOpen, err := os.Open(filepath)
// if err != nil {
// return "", err
// }
// defer func(fileOpen *os.File) {
// _ = fileOpen.Close()
// }(fileOpen)
//
// fileBytes, err := io.ReadAll(fileOpen)
// if err != nil {
// return "", errors.New("failed to read file")
// }
//
// if t == IMG {
// // 判断是不是图片
// if !filetype.IsImage(fileBytes) {
// return "", ErrUnsupported
// }
// }
//
// kind, err := filetype.Match(fileBytes)
// if err != nil {
// return "", err
// }
//
// if kind == filetype.Unknown {
// return "", ErrUnsupported
// }
//
// // 使用 filetype 判断类型后已经去读了一些bytes了
// // 要恢复文件读取位置
// _, err = fileOpen.Seek(0, io.SeekStart)
// if err != nil {
// return "", err
// }
//
// dir := GetPath()
// exist, _ := Exists(dir)
// if !exist {
// if err := Mkdir(dir); err != nil {
// return "", err
// }
// }
//
// filename := GenFilename(kind.Extension)
// fullPath := path.Join(dir, filename)
// f, err := os.Create(fullPath)
// if err != nil {
// return "", err
// }
// defer func(f *os.File) {
// _ = f.Close()
// }(f)
//
// _, err = io.Copy(f, fileOpen)
// if err != nil {
// return "", err
// }
//
// return "/" + fullPath, nil
//}
func UploadFile(file *multipart.FileHeader, t Type) (string, error) {
if t == IMG {
// 判断是不是图片
if !filetype.IsImage(fileBytes) {
return "", ErrUnsupported
if file.Size > MaxImageSize {
return "", errors.New("failed to receive images too large")
}
}
kind, err := filetype.Match(fileBytes)
if err != nil {
return "", err
}
if kind == filetype.Unknown {
return "", ErrUnsupported
}
// 使用 filetype 判断类型后已经去读了一些bytes了
// 要恢复文件读取位置
_, err = fileOpen.Seek(0, io.SeekStart)
if err != nil {
return "", err
}
dir := GetPath()
exist, _ := Exists(dir)
if !exist {
if err := Mkdir(dir); err != nil {
return "", err
} else {
if file.Size > MaxFileSize {
return "", errors.New("failed to receive file too large")
}
}
filename := GenFilename(kind.Extension)
path := path.Join(dir, filename)
f, err := os.Create(path)
if err != nil {
return "", err
}
defer f.Close()
_, err = io.Copy(f, fileOpen)
if err != nil {
return "", err
}
return "/" + path, nil
}
func UploadFile(file *multipart.FileHeader, t FileType) (string, error) {
if file.Size > MaxFileSize {
return "", errors.New("failed to receive file too large")
}
fileOpen, err := file.Open()
if err != nil {
return "", errors.New("fialed to open file")
return "", errors.New("failed to open file")
}
defer fileOpen.Close()
defer func(fileOpen multipart.File) {
_ = fileOpen.Close()
}(fileOpen)
fileBytes, err := io.ReadAll(fileOpen)
if err != nil {
@ -134,19 +147,21 @@ func UploadFile(file *multipart.FileHeader, t FileType) (string, error) {
}
filename := GenFilename(kind.Extension)
path := path.Join(dir, filename)
f, err := os.Create(path)
fullPath := path.Join(dir, filename)
f, err := os.Create(fullPath)
if err != nil {
return "", err
}
defer f.Close()
defer func(f *os.File) {
_ = f.Close()
}(f)
_, err = io.Copy(f, fileOpen)
if err != nil {
return "", err
}
return "/" + path, nil
return "/" + fullPath, nil
}
func GetPath() string {
@ -154,6 +169,9 @@ func GetPath() string {
}
func GenFilename(ext string) string {
id, _ := gonanoid.New()
id, err := gonanoid.New()
if err != nil {
return uuid.New().String()
}
return fmt.Sprintf("%s.%s", id, ext)
}

View File

@ -40,7 +40,7 @@ func Audit(sess session.Manager, log *logger.Logger, task tasks.TaskDistributor)
opts := []asynq.Option{
asynq.MaxRetry(10),
asynq.ProcessIn(1 * time.Second),
asynq.Queue(tasks.QueueCritical),
asynq.Queue(tasks.QueueDefault),
}
c, cancel := context.WithTimeout(ctx, 2*time.Second)

View File

@ -0,0 +1,920 @@
/*
Code taken from https://github.com/lib/pq
Copyright (c) 2011-2013, 'pq' Contributors Portions Copyright (C) 2011 Blake Mizerany
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to use,
copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
Software, and to permit persons to whom the Software is furnished to do so, subject
to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
// Package dbarray provides support for database array types.
package dbarray
import (
"bytes"
"database/sql"
"database/sql/driver"
"encoding/hex"
"fmt"
"reflect"
"strconv"
"strings"
)
var typeByteSlice = reflect.TypeOf([]byte{})
var typeDriverValuer = reflect.TypeOf((*driver.Valuer)(nil)).Elem()
var typeSQLScanner = reflect.TypeOf((*sql.Scanner)(nil)).Elem()
// Array returns the optimal driver.Valuer and sql.Scanner for an array or
// slice of any dimension.
//
// For example:
//
// db.Query(`SELECT * FROM t WHERE id = ANY($1)`, pq.Array([]int{235, 401}))
//
// var x []sql.NullInt64
// db.QueryRow(`SELECT ARRAY[235, 401]`).Scan(pq.Array(&x))
//
// Scanning multi-dimensional arrays is not supported. Arrays where the lower
// bound is not one (such as `[0:0]={1}') are not supported.
func Array(a any) interface {
driver.Valuer
sql.Scanner
} {
switch a := a.(type) {
case []bool:
return (*Bool)(&a)
case []float64:
return (*Float64)(&a)
case []float32:
return (*Float32)(&a)
case []int64:
return (*Int64)(&a)
case []int32:
return (*Int32)(&a)
case []string:
return (*String)(&a)
case [][]byte:
return (*Bytea)(&a)
case *[]bool:
return (*Bool)(a)
case *[]float64:
return (*Float64)(a)
case *[]float32:
return (*Float32)(a)
case *[]int64:
return (*Int64)(a)
case *[]int32:
return (*Int32)(a)
case *[]string:
return (*String)(a)
case *[][]byte:
return (*Bytea)(a)
}
return Generic{a}
}
// Delimiter may be optionally implemented by driver.Valuer or sql.Scanner
// to override the array delimiter used by Generic.
type Delimiter interface {
// Delimiter returns the delimiter character(s) for this element's type.
Delimiter() string
}
// Bool represents a one-dimensional array of the PostgreSQL boolean type.
type Bool []bool
// Scan implements the sql.Scanner interface.
func (a *Bool) Scan(src any) error {
switch src := src.(type) {
case []byte:
return a.scanBytes(src)
case string:
return a.scanBytes([]byte(src))
case nil:
*a = nil
return nil
}
return fmt.Errorf("database: cannot convert %T to Bool", src)
}
func (a *Bool) scanBytes(src []byte) error {
elems, err := scanLinearArray(src, []byte{','}, "Bool")
if err != nil {
return err
}
if *a != nil && len(elems) == 0 {
*a = (*a)[:0]
} else {
b := make(Bool, len(elems))
for i, v := range elems {
if len(v) != 1 {
return fmt.Errorf("database: could not parse boolean array index %d: invalid boolean %q", i, v)
}
switch v[0] {
case 't':
b[i] = true
case 'f':
b[i] = false
default:
return fmt.Errorf("database: could not parse boolean array index %d: invalid boolean %q", i, v)
}
}
*a = b
}
return nil
}
// Value implements the driver.Valuer interface.
func (a Bool) Value() (driver.Value, error) {
if a == nil {
return nil, nil
}
if n := len(a); n > 0 {
// There will be exactly two curly brackets, N bytes of values,
// and N-1 bytes of delimiters.
b := make([]byte, 1+2*n)
for i := 0; i < n; i++ {
b[2*i] = ','
if a[i] {
b[1+2*i] = 't'
} else {
b[1+2*i] = 'f'
}
}
b[0] = '{'
b[2*n] = '}'
return string(b), nil
}
return "{}", nil
}
// Bytea represents a one-dimensional array of the PostgreSQL bytea type.
type Bytea [][]byte
// Scan implements the sql.Scanner interface.
func (a *Bytea) Scan(src any) error {
switch src := src.(type) {
case []byte:
return a.scanBytes(src)
case string:
return a.scanBytes([]byte(src))
case nil:
*a = nil
return nil
}
return fmt.Errorf("database: cannot convert %T to Bytea", src)
}
func (a *Bytea) scanBytes(src []byte) error {
elems, err := scanLinearArray(src, []byte{','}, "Bytea")
if err != nil {
return err
}
if *a != nil && len(elems) == 0 {
*a = (*a)[:0]
} else {
b := make(Bytea, len(elems))
for i, v := range elems {
b[i], err = parseBytea(v)
if err != nil {
return fmt.Errorf("could not parse bytea array index %d: %s", i, err.Error())
}
}
*a = b
}
return nil
}
// Value implements the driver.Valuer interface. It uses the "hex" format which
// is only supported on PostgreSQL 9.0 or newer.
func (a Bytea) Value() (driver.Value, error) {
if a == nil {
return nil, nil
}
if n := len(a); n > 0 {
// There will be at least two curly brackets, 2*N bytes of quotes,
// 3*N bytes of hex formatting, and N-1 bytes of delimiters.
size := 1 + 6*n
for _, x := range a {
size += hex.EncodedLen(len(x))
}
b := make([]byte, size)
for i, s := 0, b; i < n; i++ {
o := copy(s, `,"\\x`)
o += hex.Encode(s[o:], a[i])
s[o] = '"'
s = s[o+1:]
}
b[0] = '{'
b[size-1] = '}'
return string(b), nil
}
return "{}", nil
}
// Float64 represents a one-dimensional array of the PostgreSQL double
// precision type.
type Float64 []float64
// Scan implements the sql.Scanner interface.
func (a *Float64) Scan(src any) error {
switch src := src.(type) {
case []byte:
return a.scanBytes(src)
case string:
return a.scanBytes([]byte(src))
case nil:
*a = nil
return nil
}
return fmt.Errorf("database: cannot convert %T to Float64", src)
}
func (a *Float64) scanBytes(src []byte) error {
elems, err := scanLinearArray(src, []byte{','}, "Float64")
if err != nil {
return err
}
if *a != nil && len(elems) == 0 {
*a = (*a)[:0]
} else {
b := make(Float64, len(elems))
for i, v := range elems {
if b[i], err = strconv.ParseFloat(string(v), 64); err != nil {
return fmt.Errorf("database: parsing array element index %d: %v", i, err)
}
}
*a = b
}
return nil
}
// Value implements the driver.Valuer interface.
func (a Float64) Value() (driver.Value, error) {
if a == nil {
return nil, nil
}
if n := len(a); n > 0 {
// There will be at least two curly brackets, N bytes of values,
// and N-1 bytes of delimiters.
b := make([]byte, 1, 1+2*n)
b[0] = '{'
b = strconv.AppendFloat(b, a[0], 'f', -1, 64)
for i := 1; i < n; i++ {
b = append(b, ',')
b = strconv.AppendFloat(b, a[i], 'f', -1, 64)
}
return string(append(b, '}')), nil
}
return "{}", nil
}
// Float32 represents a one-dimensional array of the PostgreSQL double
// precision type.
type Float32 []float32
// Scan implements the sql.Scanner interface.
func (a *Float32) Scan(src any) error {
switch src := src.(type) {
case []byte:
return a.scanBytes(src)
case string:
return a.scanBytes([]byte(src))
case nil:
*a = nil
return nil
}
return fmt.Errorf("database: cannot convert %T to Float32", src)
}
func (a *Float32) scanBytes(src []byte) error {
elems, err := scanLinearArray(src, []byte{','}, "Float32")
if err != nil {
return err
}
if *a != nil && len(elems) == 0 {
*a = (*a)[:0]
} else {
b := make(Float32, len(elems))
for i, v := range elems {
var x float64
if x, err = strconv.ParseFloat(string(v), 32); err != nil {
return fmt.Errorf("database: parsing array element index %d: %v", i, err)
}
b[i] = float32(x)
}
*a = b
}
return nil
}
// Value implements the driver.Valuer interface.
func (a Float32) Value() (driver.Value, error) {
if a == nil {
return nil, nil
}
if n := len(a); n > 0 {
// There will be at least two curly brackets, N bytes of values,
// and N-1 bytes of delimiters.
b := make([]byte, 1, 1+2*n)
b[0] = '{'
b = strconv.AppendFloat(b, float64(a[0]), 'f', -1, 32)
for i := 1; i < n; i++ {
b = append(b, ',')
b = strconv.AppendFloat(b, float64(a[i]), 'f', -1, 32)
}
return string(append(b, '}')), nil
}
return "{}", nil
}
// Generic implements the driver.Valuer and sql.Scanner interfaces for
// an array or slice of any dimension.
type Generic struct{ A any }
func (Generic) evaluateDestination(rt reflect.Type) (reflect.Type, func([]byte, reflect.Value) error, string) {
var assign func([]byte, reflect.Value) error
var del = ","
// TODO calculate the assign function for other types
// TODO repeat this section on the element type of arrays or slices (multidimensional)
{
if reflect.PointerTo(rt).Implements(typeSQLScanner) {
// dest is always addressable because it is an element of a slice.
assign = func(src []byte, dest reflect.Value) (err error) {
ss := dest.Addr().Interface().(sql.Scanner)
if src == nil {
err = ss.Scan(nil)
} else {
err = ss.Scan(src)
}
return
}
goto FoundType
}
assign = func([]byte, reflect.Value) error {
return fmt.Errorf("database: scanning to %s is not implemented; only sql.Scanner", rt)
}
}
FoundType:
if ad, ok := reflect.Zero(rt).Interface().(Delimiter); ok {
del = ad.Delimiter()
}
return rt, assign, del
}
// Scan implements the sql.Scanner interface.
func (a Generic) Scan(src any) error {
dpv := reflect.ValueOf(a.A)
switch {
case dpv.Kind() != reflect.Ptr:
return fmt.Errorf("database: destination %T is not a pointer to array or slice", a.A)
case dpv.IsNil():
return fmt.Errorf("database: destination %T is nil", a.A)
}
dv := dpv.Elem()
switch dv.Kind() {
case reflect.Slice:
case reflect.Array:
default:
return fmt.Errorf("database: destination %T is not a pointer to array or slice", a.A)
}
switch src := src.(type) {
case []byte:
return a.scanBytes(src, dv)
case string:
return a.scanBytes([]byte(src), dv)
case nil:
if dv.Kind() == reflect.Slice {
dv.Set(reflect.Zero(dv.Type()))
return nil
}
}
return fmt.Errorf("database: cannot convert %T to %s", src, dv.Type())
}
func (a Generic) scanBytes(src []byte, dv reflect.Value) error {
dtype, assign, del := a.evaluateDestination(dv.Type().Elem())
dims, elems, err := parseArray(src, []byte(del))
if err != nil {
return err
}
// TODO allow multidimensional
if len(dims) > 1 {
return fmt.Errorf("database: scanning from multidimensional ARRAY%s is not implemented",
strings.Replace(fmt.Sprint(dims), " ", "][", -1))
}
// Treat a zero-dimensional array like an array with a single dimension of zero.
if len(dims) == 0 {
dims = append(dims, 0)
}
for i, rt := 0, dv.Type(); i < len(dims); i, rt = i+1, rt.Elem() {
switch rt.Kind() {
case reflect.Slice:
case reflect.Array:
if rt.Len() != dims[i] {
return fmt.Errorf("database: cannot convert ARRAY%s to %s",
strings.Replace(fmt.Sprint(dims), " ", "][", -1), dv.Type())
}
default:
// TODO handle multidimensional
}
}
values := reflect.MakeSlice(reflect.SliceOf(dtype), len(elems), len(elems))
for i, e := range elems {
if err := assign(e, values.Index(i)); err != nil {
return fmt.Errorf("database: parsing array element index %d: %v", i, err)
}
}
// TODO handle multidimensional
switch dv.Kind() {
case reflect.Slice:
dv.Set(values.Slice(0, dims[0]))
case reflect.Array:
for i := 0; i < dims[0]; i++ {
dv.Index(i).Set(values.Index(i))
}
}
return nil
}
// Value implements the driver.Valuer interface.
func (a Generic) Value() (driver.Value, error) {
if a.A == nil {
return nil, nil
}
rv := reflect.ValueOf(a.A)
switch rv.Kind() {
case reflect.Slice:
if rv.IsNil() {
return nil, nil
}
case reflect.Array:
default:
return nil, fmt.Errorf("database: Unable to convert %T to array", a.A)
}
if n := rv.Len(); n > 0 {
// There will be at least two curly brackets, N bytes of values,
// and N-1 bytes of delimiters.
b := make([]byte, 0, 1+2*n)
b, _, err := appendArray(b, rv, n)
return string(b), err
}
return "{}", nil
}
// Int64 represents a one-dimensional array of the PostgreSQL integer types.
type Int64 []int64
// Scan implements the sql.Scanner interface.
func (a *Int64) Scan(src any) error {
switch src := src.(type) {
case []byte:
return a.scanBytes(src)
case string:
return a.scanBytes([]byte(src))
case nil:
*a = nil
return nil
}
return fmt.Errorf("database: cannot convert %T to Int64", src)
}
func (a *Int64) scanBytes(src []byte) error {
elems, err := scanLinearArray(src, []byte{','}, "Int64")
if err != nil {
return err
}
if *a != nil && len(elems) == 0 {
*a = (*a)[:0]
} else {
b := make(Int64, len(elems))
for i, v := range elems {
if b[i], err = strconv.ParseInt(string(v), 10, 64); err != nil {
return fmt.Errorf("database: parsing array element index %d: %v", i, err)
}
}
*a = b
}
return nil
}
// Value implements the driver.Valuer interface.
func (a Int64) Value() (driver.Value, error) {
if a == nil {
return nil, nil
}
if n := len(a); n > 0 {
// There will be at least two curly brackets, N bytes of values,
// and N-1 bytes of delimiters.
b := make([]byte, 1, 1+2*n)
b[0] = '{'
b = strconv.AppendInt(b, a[0], 10)
for i := 1; i < n; i++ {
b = append(b, ',')
b = strconv.AppendInt(b, a[i], 10)
}
return string(append(b, '}')), nil
}
return "{}", nil
}
// Int32 represents a one-dimensional array of the PostgreSQL integer types.
type Int32 []int32
// Scan implements the sql.Scanner interface.
func (a *Int32) Scan(src any) error {
switch src := src.(type) {
case []byte:
return a.scanBytes(src)
case string:
return a.scanBytes([]byte(src))
case nil:
*a = nil
return nil
}
return fmt.Errorf("database: cannot convert %T to Int32", src)
}
func (a *Int32) scanBytes(src []byte) error {
elems, err := scanLinearArray(src, []byte{','}, "Int32")
if err != nil {
return err
}
if *a != nil && len(elems) == 0 {
*a = (*a)[:0]
} else {
b := make(Int32, len(elems))
for i, v := range elems {
x, err := strconv.ParseInt(string(v), 10, 32)
if err != nil {
return fmt.Errorf("database: parsing array element index %d: %v", i, err)
}
b[i] = int32(x)
}
*a = b
}
return nil
}
// Value implements the driver.Valuer interface.
func (a Int32) Value() (driver.Value, error) {
if a == nil {
return nil, nil
}
if n := len(a); n > 0 {
// There will be at least two curly brackets, N bytes of values,
// and N-1 bytes of delimiters.
b := make([]byte, 1, 1+2*n)
b[0] = '{'
b = strconv.AppendInt(b, int64(a[0]), 10)
for i := 1; i < n; i++ {
b = append(b, ',')
b = strconv.AppendInt(b, int64(a[i]), 10)
}
return string(append(b, '}')), nil
}
return "{}", nil
}
// String represents a one-dimensional array of the PostgreSQL character types.
type String []string
// Scan implements the sql.Scanner interface.
func (a *String) Scan(src any) error {
switch src := src.(type) {
case []byte:
return a.scanBytes(src)
case string:
return a.scanBytes([]byte(src))
case nil:
*a = nil
return nil
}
return fmt.Errorf("database: cannot convert %T to String", src)
}
func (a *String) scanBytes(src []byte) error {
elems, err := scanLinearArray(src, []byte{','}, "String")
if err != nil {
return err
}
if *a != nil && len(elems) == 0 {
*a = (*a)[:0]
} else {
b := make(String, len(elems))
for i, v := range elems {
if b[i] = string(v); v == nil {
return fmt.Errorf("database: parsing array element index %d: cannot convert nil to string", i)
}
}
*a = b
}
return nil
}
// Value implements the driver.Valuer interface.
func (a String) Value() (driver.Value, error) {
if a == nil {
return nil, nil
}
if n := len(a); n > 0 {
// There will be at least two curly brackets, 2*N bytes of quotes,
// and N-1 bytes of delimiters.
b := make([]byte, 1, 1+3*n)
b[0] = '{'
b = appendArrayQuotedBytes(b, []byte(a[0]))
for i := 1; i < n; i++ {
b = append(b, ',')
b = appendArrayQuotedBytes(b, []byte(a[i]))
}
return string(append(b, '}')), nil
}
return "{}", nil
}
// appendArray appends rv to the buffer, returning the extended buffer and
// the delimiter used between elements.
//
// It panics when n <= 0 or rv's Kind is not reflect.Array nor reflect.Slice.
func appendArray(b []byte, rv reflect.Value, n int) ([]byte, string, error) {
var del string
var err error
b = append(b, '{')
if b, del, err = appendArrayElement(b, rv.Index(0)); err != nil {
return b, del, err
}
for i := 1; i < n; i++ {
b = append(b, del...)
if b, del, err = appendArrayElement(b, rv.Index(i)); err != nil {
return b, del, err
}
}
return append(b, '}'), del, nil
}
// appendArrayElement appends rv to the buffer, returning the extended buffer
// and the delimiter to use before the next element.
//
// When rv's Kind is neither reflect.Array nor reflect.Slice, it is converted
// using driver.DefaultParameterConverter and the resulting []byte or string
// is double-quoted.
//
// See http://www.postgresql.org/docs/current/static/arrays.html#ARRAYS-IO
func appendArrayElement(b []byte, rv reflect.Value) ([]byte, string, error) {
if k := rv.Kind(); k == reflect.Array || k == reflect.Slice {
if t := rv.Type(); t != typeByteSlice && !t.Implements(typeDriverValuer) {
if n := rv.Len(); n > 0 {
return appendArray(b, rv, n)
}
return b, "", nil
}
}
var del = ","
var err error
var iv = rv.Interface()
if ad, ok := iv.(Delimiter); ok {
del = ad.Delimiter()
}
if iv, err = driver.DefaultParameterConverter.ConvertValue(iv); err != nil {
return b, del, err
}
switch v := iv.(type) {
case nil:
return append(b, "NULL"...), del, nil
case []byte:
return appendArrayQuotedBytes(b, v), del, nil
case string:
return appendArrayQuotedBytes(b, []byte(v)), del, nil
}
b, err = appendValue(b, iv)
return b, del, err
}
func appendArrayQuotedBytes(b, v []byte) []byte {
b = append(b, '"')
for {
i := bytes.IndexAny(v, `"\`)
if i < 0 {
b = append(b, v...)
break
}
if i > 0 {
b = append(b, v[:i]...)
}
b = append(b, '\\', v[i])
v = v[i+1:]
}
return append(b, '"')
}
func appendValue(b []byte, v driver.Value) ([]byte, error) {
return append(b, encode(nil, v, 0)...), nil
}
// parseArray extracts the dimensions and elements of an array represented in
// text format. Only representations emitted by the backend are supported.
// Notably, whitespace around brackets and delimiters is significant, and NULL
// is case-sensitive.
//
// See http://www.postgresql.org/docs/current/static/arrays.html#ARRAYS-IO
func parseArray(src, del []byte) (dims []int, elems [][]byte, err error) {
var depth, i int
if len(src) < 1 || src[0] != '{' {
return nil, nil, fmt.Errorf("database: unable to parse array; expected %q at offset %d", '{', 0)
}
Open:
for i < len(src) {
switch src[i] {
case '{':
depth++
i++
case '}':
elems = make([][]byte, 0)
goto Close
default:
break Open
}
}
dims = make([]int, i)
Element:
for i < len(src) {
switch src[i] {
case '{':
if depth == len(dims) {
break Element
}
depth++
dims[depth-1] = 0
i++
case '"':
var elem = []byte{}
var escape bool
for i++; i < len(src); i++ {
if escape {
elem = append(elem, src[i])
escape = false
} else {
switch src[i] {
default:
elem = append(elem, src[i])
case '\\':
escape = true
case '"':
elems = append(elems, elem)
i++
break Element
}
}
}
default:
for start := i; i < len(src); i++ {
if bytes.HasPrefix(src[i:], del) || src[i] == '}' {
elem := src[start:i]
if len(elem) == 0 {
return nil, nil, fmt.Errorf("database: unable to parse array; unexpected %q at offset %d", src[i], i)
}
if bytes.Equal(elem, []byte("NULL")) {
elem = nil
}
elems = append(elems, elem)
break Element
}
}
}
}
for i < len(src) {
if bytes.HasPrefix(src[i:], del) && depth > 0 {
dims[depth-1]++
i += len(del)
goto Element
} else if src[i] == '}' && depth > 0 {
dims[depth-1]++
depth--
i++
} else {
return nil, nil, fmt.Errorf("database: unable to parse array; unexpected %q at offset %d", src[i], i)
}
}
Close:
for i < len(src) {
if src[i] == '}' && depth > 0 {
depth--
i++
} else {
return nil, nil, fmt.Errorf("database: unable to parse array; unexpected %q at offset %d", src[i], i)
}
}
if depth > 0 {
err = fmt.Errorf("database: unable to parse array; expected %q at offset %d", '}', i)
}
if err == nil {
for _, d := range dims {
if (len(elems) % d) != 0 {
err = fmt.Errorf("database: multidimensional arrays must have elements with matching dimensions")
}
}
}
return
}
func scanLinearArray(src, del []byte, typ string) (elems [][]byte, err error) {
dims, elems, err := parseArray(src, del)
if err != nil {
return nil, err
}
if len(dims) > 1 {
return nil, fmt.Errorf("database: cannot convert ARRAY%s to %s", strings.Replace(fmt.Sprint(dims), " ", "][", -1), typ)
}
return elems, err
}

View File

@ -0,0 +1,235 @@
/*
Code taken from https://github.com/lib/pq
Copyright (c) 2011-2013, 'pq' Contributors Portions Copyright (C) 2011 Blake Mizerany
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to use,
copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
Software, and to permit persons to whom the Software is furnished to do so, subject
to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package dbarray
import (
"bytes"
"encoding/hex"
"fmt"
"strconv"
"time"
)
const (
infinityTSEnabledAlready = "database: infinity timestamp enabled already"
infinityTSNegativeMustBeSmaller = "database: infinity timestamp: negative value must be smaller (before) than positive"
)
var infinityTSEnabled = false
var infinityTSNegative time.Time
var infinityTSPositive time.Time
type parameterStatus struct {
// server version in the same format as server_version_num, or 0 if unavailable.
serverVersion int
}
// EnableInfinityTS controls the handling of Postgres' "-infinity" and
// "infinity" "timestamp"s.
//
// If EnableInfinityTS is not called, "-infinity" and "infinity" will return
// []byte("-infinity") and []byte("infinity") respectively, and potentially
// cause error "sql: Scan error on column index 0: unsupported driver -> Scan
// pair: []uint8 -> *time.Time", when scanning into a time.Time value.
//
// Once EnableInfinityTS has been called, all connections created using this
// driver will decode Postgres' "-infinity" and "infinity" for "timestamp",
// "timestamp with time zone" and "date" types to the predefined minimum and
// maximum times, respectively. When encoding time.Time values, any time which
// equals or precedes the predefined minimum time will be encoded to
// "-infinity". Any values at or past the maximum time will similarly be
// encoded to "infinity".
//
// If EnableInfinityTS is called with negative >= positive, it will panic.
// Calling EnableInfinityTS after a connection has been established results in
// undefined behavior. If EnableInfinityTS is called more than once, it will
// panic.
func EnableInfinityTS(negative time.Time, positive time.Time) {
if infinityTSEnabled {
panic(infinityTSEnabledAlready)
}
if !negative.Before(positive) {
panic(infinityTSNegativeMustBeSmaller)
}
infinityTSEnabled = true
infinityTSNegative = negative
infinityTSPositive = positive
}
func encode(parameterStatus *parameterStatus, x any, oid int) []byte {
const oidBytea = 17
switch v := x.(type) {
case int64:
return strconv.AppendInt(nil, v, 10)
case float64:
return strconv.AppendFloat(nil, v, 'f', -1, 64)
case []byte:
if oid == oidBytea {
return encodeBytea(parameterStatus.serverVersion, v)
}
return v
case string:
if oid == oidBytea {
return encodeBytea(parameterStatus.serverVersion, []byte(v))
}
return []byte(v)
case bool:
return strconv.AppendBool(nil, v)
case time.Time:
return formatTS(v)
default:
errorf("encode: unknown type for %T", v)
}
panic("not reached")
}
// formatTS formats t into a format postgres understands.
func formatTS(t time.Time) []byte {
if infinityTSEnabled {
// t <= -infinity : ! (t > -infinity)
if !t.After(infinityTSNegative) {
return []byte("-infinity")
}
// t >= infinity : ! (!t < infinity)
if !t.Before(infinityTSPositive) {
return []byte("infinity")
}
}
return formatTimestamp(t)
}
// formatTimestamp formats t into Postgres' text format for timestamps.
func formatTimestamp(t time.Time) []byte {
// Need to send dates before 0001 A.D. with " BC" suffix, instead of the
// minus sign preferred by Go.
// Beware, "0000" in ISO is "1 BC", "-0001" is "2 BC" and so on
bc := false
if t.Year() <= 0 {
// flip year sign, and add 1, e.g: "0" will be "1", and "-10" will be "11"
t = t.AddDate((-t.Year())*2+1, 0, 0)
bc = true
}
b := []byte(t.Format("2006-01-02 15:04:05.999999999Z07:00"))
_, offset := t.Zone()
offset %= 60
if offset != 0 {
// RFC3339Nano already printed the minus sign
if offset < 0 {
offset = -offset
}
b = append(b, ':')
if offset < 10 {
b = append(b, '0')
}
b = strconv.AppendInt(b, int64(offset), 10)
}
if bc {
b = append(b, " BC"...)
}
return b
}
func errorf(s string, args ...any) {
panic(fmt.Errorf("pq: %s", fmt.Sprintf(s, args...)))
}
// Parse a bytea value received from the server. Both "hex" and the legacy
// "escape" format are supported.
func parseBytea(s []byte) (result []byte, err error) {
if len(s) >= 2 && bytes.Equal(s[:2], []byte("\\x")) {
// bytea_output = hex
s = s[2:] // trim off leading "\\x"
result = make([]byte, hex.DecodedLen(len(s)))
_, err := hex.Decode(result, s)
if err != nil {
return nil, err
}
} else {
// bytea_output = escape
for len(s) > 0 {
if s[0] == '\\' {
// escaped '\\'
if len(s) >= 2 && s[1] == '\\' {
result = append(result, '\\')
s = s[2:]
continue
}
// '\\' followed by an octal number
if len(s) < 4 {
return nil, fmt.Errorf("invalid bytea sequence %v", s)
}
r, err := strconv.ParseUint(string(s[1:4]), 8, 8)
if err != nil {
return nil, fmt.Errorf("could not parse bytea value: %s", err.Error())
}
result = append(result, byte(r))
s = s[4:]
} else {
// We hit an unescaped, raw byte. Try to read in as many as
// possible in one go.
i := bytes.IndexByte(s, '\\')
if i == -1 {
result = append(result, s...)
break
}
result = append(result, s[:i]...)
s = s[i:]
}
}
}
return result, nil
}
func encodeBytea(serverVersion int, v []byte) (result []byte) {
if serverVersion >= 90000 {
// Use the hex format if we know that the server supports it
result = make([]byte, 2+hex.EncodedLen(len(v)))
result[0] = '\\'
result[1] = 'x'
hex.Encode(result[2:], v)
} else {
// .. or resort to "escape"
for _, b := range v {
if b == '\\' {
result = append(result, '\\', '\\')
} else if b < 0x20 || b > 0x7e {
result = append(result, []byte(fmt.Sprintf("\\%03o", b))...)
} else {
result = append(result, b)
}
}
}
return result
}

View File

@ -1,3 +1,4 @@
// Package sqldb provides support for access the database.
package sqldb
import (
@ -17,17 +18,64 @@ import (
"go.uber.org/zap"
)
// lib/pq errorCodeNames
// https://github.com/lib/pq/blob/master/error.go#L178
const (
uniqueViolation = "23505"
undefinedTable = "42P01"
)
// Set of error variables for CRUD operations.
var (
ErrDBNotFound = sql.ErrNoRows
ErrDBDuplicatedEntry = errors.New("duplicated entry")
ErrUndefinedTable = errors.New("undefined table")
)
// Config is the required properties to use the database.
//type Config struct {
// User string
// Password string
// Host string
// Name string
// Schema string
// MaxIdleConns int
// MaxOpenConns int
// DisableTLS bool
//}
// Open knows how to open a database connection based on the configuration.
//func Open(cfg Config) (*sqlx.DB, error) {
// sslMode := "require"
// if cfg.DisableTLS {
// sslMode = "disable"
// }
//
// q := make(url.Values)
// q.Set("sslmode", sslMode)
// q.Set("timezone", "utc")
// if cfg.Schema != "" {
// q.Set("search_path", cfg.Schema)
// }
//
// u := url.URL{
// Scheme: "postgres",
// User: url.UserPassword(cfg.User, cfg.Password),
// Host: cfg.Host,
// Path: cfg.Name,
// RawQuery: q.Encode(),
// }
//
// db, err := sqlx.Open("pgx", u.String())
// if err != nil {
// return nil, err
// }
// db.SetMaxIdleConns(cfg.MaxIdleConns)
// db.SetMaxOpenConns(cfg.MaxOpenConns)
//
// return db, nil
//}
type Config struct {
User string
Password string
@ -82,26 +130,66 @@ func NewDB(config *config.Config, log *logger.Logger) (*sqlx.DB, func(), error)
return db, cleanup, nil
}
// StatusCheck returns nil if it can successfully talk to the database. It
// returns a non-nil error otherwise.
func StatusCheck(ctx context.Context, db *sqlx.DB) error {
// If the user doesn't give us a deadline set 1 second.
if _, ok := ctx.Deadline(); !ok {
var cancel context.CancelFunc
ctx, cancel = context.WithTimeout(ctx, time.Second)
defer cancel()
}
for attempts := 1; ; attempts++ {
if err := db.Ping(); err == nil {
break
}
time.Sleep(time.Duration(attempts) * 100 * time.Millisecond)
if ctx.Err() != nil {
return ctx.Err()
}
}
if ctx.Err() != nil {
return ctx.Err()
}
// Run a simple query to determine connectivity.
// Running this query forces a round trip through the database.
const q = `SELECT TRUE`
var tmp bool
return db.QueryRowContext(ctx, q).Scan(&tmp)
}
// ExecContext is a helper function to execute a CUD operation with
// logging and tracing.
func ExecContext(ctx context.Context, log *logger.Logger, db sqlx.ExtContext, query string) error {
return NamedExecContext(ctx, log, db, query, struct{}{})
}
// NamedExecContext is a helper function to execute a CUD operation with
// logging and tracing where field replacement is necessary.
func NamedExecContext(ctx context.Context, log *logger.Logger, db sqlx.ExtContext, query string, data any) (err error) {
q := queryString(query, data)
defer func() {
if err != nil {
switch data.(type) {
case struct{}:
log.Error("database.NamedExecContext (data is struct)", err,
zap.String("query", query),
zap.Any("ERROR", err))
log.Info("database.NamedExecContext", zap.String("query", q), zap.Int("type", 6), zap.Error(err))
default:
log.Error("database.NamedExecContext", err,
zap.String("query", query),
zap.Any("ERROR", err))
log.Info("database.NamedExecContext", zap.String("query", q), zap.Int("type", 5), zap.Error(err))
}
}
}()
if _, err := sqlx.NamedExecContext(ctx, db, query, data); err != nil {
var pgError *pgconn.PgError
if errors.As(err, &pgError) {
switch pgError.Code {
var pqerr *pgconn.PgError
if errors.As(err, &pqerr) {
switch pqerr.Code {
case undefinedTable:
return ErrUndefinedTable
case uniqueViolation:
@ -114,71 +202,73 @@ func NamedExecContext(ctx context.Context, log *logger.Logger, db sqlx.ExtContex
return nil
}
func NamedQueryStruct(ctx context.Context, log *logger.Logger, db sqlx.ExtContext, query string, data any, dest any) (err error) {
q := queryString(query, data)
rows, err := sqlx.NamedQueryContext(ctx, db, q, data)
if err != nil {
var pqErr *pgconn.PgError
if errors.As(err, &pqErr) && pqErr.Code == undefinedTable {
return ErrUndefinedTable
}
log.Error("NamedQueryStruct NamedQueryContext error", err,
zap.String("query", q),
zap.Any("data", data),
)
return err
}
defer func(rows *sqlx.Rows) {
err := rows.Close()
if err != nil {
log.Error("rows close error", err)
}
}(rows)
if !rows.Next() {
return ErrDBNotFound
}
if err := rows.StructScan(dest); err != nil {
log.Error("NamedQueryStruct StructScan error", err,
zap.String("query", q),
zap.Any("data", data),
)
return err
}
return nil
// QuerySlice is a helper function for executing queries that return a
// collection of data to be unmarshalled into a slice.
func QuerySlice[T any](ctx context.Context, log *logger.Logger, db sqlx.ExtContext, query string, dest *[]T) error {
return namedQuerySlice(ctx, log, db, query, struct{}{}, dest, false)
}
func NamedQuerySlice[T any](ctx context.Context, log *logger.Logger, db sqlx.ExtContext, query string, data any, dest *[]T) (err error) {
// NamedQuerySlice is a helper function for executing queries that return a
// collection of data to be unmarshalled into a slice where field replacement is
// necessary.
func NamedQuerySlice[T any](ctx context.Context, log *logger.Logger, db sqlx.ExtContext, query string, data any, dest *[]T) error {
return namedQuerySlice(ctx, log, db, query, data, dest, false)
}
// NamedQuerySliceUsingIn is a helper function for executing queries that return
// a collection of data to be unmarshalled into a slice where field replacement
// is necessary. Use this if the query has an IN clause.
func NamedQuerySliceUsingIn[T any](ctx context.Context, log *logger.Logger, db sqlx.ExtContext, query string, data any, dest *[]T) error {
return namedQuerySlice(ctx, log, db, query, data, dest, true)
}
func namedQuerySlice[T any](ctx context.Context, log *logger.Logger, db sqlx.ExtContext, query string, data any, dest *[]T, withIn bool) (err error) {
q := queryString(query, data)
rows, err := sqlx.NamedQueryContext(ctx, db, q, data)
defer func() {
if err != nil {
log.Info("database.NamedQuerySlice", zap.String("query", q), zap.Int("type", 6), zap.Error(err))
}
}()
var rows *sqlx.Rows
switch withIn {
case true:
rows, err = func() (*sqlx.Rows, error) {
named, args, err := sqlx.Named(query, data)
if err != nil {
return nil, err
}
query, args, err := sqlx.In(named, args...)
if err != nil {
return nil, err
}
query = db.Rebind(query)
return db.QueryxContext(ctx, query, args...)
}()
default:
rows, err = sqlx.NamedQueryContext(ctx, db, query, data)
}
if err != nil {
var pqErr *pgconn.PgError
if errors.As(err, &pqErr) && pqErr.Code == undefinedTable {
return ErrUndefinedTable
}
log.Error("NamedQueryStruct NamedQueryContext error", err,
zap.String("query", q),
zap.Any("data", data),
)
return err
}
defer func(rows *sqlx.Rows) {
err := rows.Close()
if err != nil {
log.Error("rows close error", err)
}
_ = rows.Close()
}(rows)
var slice []T
for rows.Next() {
v := new(T)
if err := rows.StructScan(v); err != nil {
log.Error("NamedQuerySlice StructScan error", err,
zap.String("query", q),
zap.Any("data", data),
)
return err
}
slice = append(slice, *v)
@ -188,6 +278,80 @@ func NamedQuerySlice[T any](ctx context.Context, log *logger.Logger, db sqlx.Ext
return nil
}
// QueryStruct is a helper function for executing queries that return a
// single value to be unmarshalled into a struct type where field replacement is necessary.
func QueryStruct(ctx context.Context, log *logger.Logger, db sqlx.ExtContext, query string, dest any) error {
return namedQueryStruct(ctx, log, db, query, struct{}{}, dest, false)
}
// NamedQueryStruct is a helper function for executing queries that return a
// single value to be unmarshalled into a struct type where field replacement is necessary.
func NamedQueryStruct(ctx context.Context, log *logger.Logger, db sqlx.ExtContext, query string, data any, dest any) error {
return namedQueryStruct(ctx, log, db, query, data, dest, false)
}
// NamedQueryStructUsingIn is a helper function for executing queries that return
// a single value to be unmarshalled into a struct type where field replacement
// is necessary. Use this if the query has an IN clause.
func NamedQueryStructUsingIn(ctx context.Context, log *logger.Logger, db sqlx.ExtContext, query string, data any, dest any) error {
return namedQueryStruct(ctx, log, db, query, data, dest, true)
}
func namedQueryStruct(ctx context.Context, log *logger.Logger, db sqlx.ExtContext, query string, data any, dest any, withIn bool) (err error) {
q := queryString(query, data)
defer func() {
if err != nil {
log.Info("database.NamedQuerySlice", zap.String("query", q), zap.Int("type", 6), zap.Error(err))
}
}()
var rows *sqlx.Rows
switch withIn {
case true:
rows, err = func() (*sqlx.Rows, error) {
named, args, err := sqlx.Named(query, data)
if err != nil {
return nil, err
}
query, args, err := sqlx.In(named, args...)
if err != nil {
return nil, err
}
query = db.Rebind(query)
return db.QueryxContext(ctx, query, args...)
}()
default:
rows, err = sqlx.NamedQueryContext(ctx, db, query, data)
}
if err != nil {
var pqErr *pgconn.PgError
if errors.As(err, &pqErr) && pqErr.Code == undefinedTable {
return ErrUndefinedTable
}
return err
}
defer func(rows *sqlx.Rows) {
_ = rows.Close()
}(rows)
if !rows.Next() {
return ErrDBNotFound
}
if err := rows.StructScan(dest); err != nil {
return err
}
return nil
}
// queryString provides a pretty print version of the query and parameters.
func queryString(query string, args any) string {
query, params, err := sqlx.Named(query, args)
if err != nil {

View File

@ -44,7 +44,7 @@ func (d *RedisTaskDistributor) DistributeTaskConsumeAuditLog(
func (p *RedisTaskProcessor) ProcessTaskConsumeAuditLog(ctx context.Context, task *asynq.Task) error {
var payload PayloadConsumeAuditLog
if err := json.Unmarshal(task.Payload(), &payload); err != nil {
return fmt.Errorf("failed to unmarshal payload: %w", asynq.SkipRetry)
return fmt.Errorf("failed to unmarshal payload: %w", err)
}
if err := p.auditService.Create(ctx, payload.AuditLog); err != nil {

View File

@ -36,6 +36,7 @@ func NewRedisTaskProcessor(log *logger.Logger, opt *RedisClientConnector, auditS
QueueCritical: 10,
QueueDefault: 5,
},
ErrorHandler: asynq.ErrorHandlerFunc(func(ctx context.Context, task *asynq.Task, err error) {
log.Error("process task failed", err,
zap.String("type", task.Type()),