From df4c3dd46f90232bcce0f788aee19f5afd54597f Mon Sep 17 00:00:00 2001 From: kenneth <1185230223@qq.com> Date: Mon, 27 Oct 2025 15:24:08 +0800 Subject: [PATCH] update --- go.mod | 51 +- go.sum | 124 ++- .../erpserver/handler/system/auth/auth.go | 3 +- .../erpserver/handler/system/config/config.go | 5 +- .../erpserver/handler/system/menu/menu.go | 5 +- internal/erpserver/model/system/department.go | 4 +- internal/erpserver/model/system/role.go | 2 +- internal/erpserver/model/system/user.go | 4 +- internal/erpserver/repository/seed/seed.go | 4 +- .../repository/system/config/config.go | 5 +- .../system/department/department.go | 28 +- .../erpserver/repository/system/menu/menu.go | 19 +- .../erpserver/repository/system/role/role.go | 34 +- .../erpserver/repository/system/user/user.go | 52 +- internal/erpserver/service/v1/auth/auth.go | 2 +- .../erpserver/service/v1/system/department.go | 8 +- internal/erpserver/service/v1/system/role.go | 8 +- internal/erpserver/service/v1/system/user.go | 12 +- internal/erpserver/templ/auth/login_templ.go | 2 +- internal/erpserver/templ/base/base_templ.go | 2 +- .../templ/component/content_templ.go | 2 +- .../erpserver/templ/component/upload_templ.go | 2 +- .../erpserver/templ/home/dashboard_templ.go | 2 +- internal/erpserver/templ/home/home_templ.go | 2 +- .../templ/system/auditlog/list_templ.go | 2 +- .../templ/system/category/list_templ.go | 2 +- .../templ/system/config/edit_templ.go | 2 +- .../templ/system/config/list_templ.go | 2 +- .../templ/system/department/edit_templ.go | 8 +- .../templ/system/department/list_templ.go | 2 +- .../templ/system/loginlog/list_templ.go | 2 +- .../erpserver/templ/system/menu/edit_templ.go | 2 +- .../erpserver/templ/system/menu/list_templ.go | 2 +- .../erpserver/templ/system/role/edit_templ.go | 2 +- .../erpserver/templ/system/role/list_templ.go | 2 +- .../templ/system/role/setmenu_templ.go | 2 +- .../erpserver/templ/system/user/edit_templ.go | 2 +- .../erpserver/templ/system/user/list_templ.go | 2 +- .../templ/system/user/profile_templ.go | 2 +- internal/pkg/database/error.go | 53 +- internal/pkg/file/upload.go | 154 +-- internal/pkg/mid/audit_v3.go | 2 +- internal/pkg/sqldb/dbarray/dbarray.go | 920 ++++++++++++++++++ internal/pkg/sqldb/dbarray/encode.go | 235 +++++ internal/pkg/sqldb/sqldb.go | 278 ++++-- internal/tasks/audit.go | 2 +- internal/tasks/processor.go | 1 + 47 files changed, 1757 insertions(+), 306 deletions(-) create mode 100644 internal/pkg/sqldb/dbarray/dbarray.go create mode 100644 internal/pkg/sqldb/dbarray/encode.go diff --git a/go.mod b/go.mod index 50aca6e..643e9a1 100644 --- a/go.mod +++ b/go.mod @@ -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 ) diff --git a/go.sum b/go.sum index 433147d..8047d73 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/internal/erpserver/handler/system/auth/auth.go b/internal/erpserver/handler/system/auth/auth.go index 9e2f392..ac2824a 100644 --- a/internal/erpserver/handler/system/auth/auth.go +++ b/internal/erpserver/handler/system/auth/auth.go @@ -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: diff --git a/internal/erpserver/handler/system/config/config.go b/internal/erpserver/handler/system/config/config.go index 4d13ffe..3f9d7ea 100644 --- a/internal/erpserver/handler/system/config/config.go +++ b/internal/erpserver/handler/system/config/config.go @@ -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 } diff --git a/internal/erpserver/handler/system/menu/menu.go b/internal/erpserver/handler/system/menu/menu.go index 542b5a2..293d6a5 100644 --- a/internal/erpserver/handler/system/menu/menu.go +++ b/internal/erpserver/handler/system/menu/menu.go @@ -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 } diff --git a/internal/erpserver/model/system/department.go b/internal/erpserver/model/system/department.go index 1b015ac..c79b8f2 100644 --- a/internal/erpserver/model/system/department.go +++ b/internal/erpserver/model/system/department.go @@ -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) diff --git a/internal/erpserver/model/system/role.go b/internal/erpserver/model/system/role.go index ab91db6..bb452f3 100644 --- a/internal/erpserver/model/system/role.go +++ b/internal/erpserver/model/system/role.go @@ -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) diff --git a/internal/erpserver/model/system/user.go b/internal/erpserver/model/system/user.go index e711b9f..d8541ad 100644 --- a/internal/erpserver/model/system/user.go +++ b/internal/erpserver/model/system/user.go @@ -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) diff --git a/internal/erpserver/repository/seed/seed.go b/internal/erpserver/repository/seed/seed.go index 057c689..8d908a0 100644 --- a/internal/erpserver/repository/seed/seed.go +++ b/internal/erpserver/repository/seed/seed.go @@ -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 } diff --git a/internal/erpserver/repository/system/config/config.go b/internal/erpserver/repository/system/config/config.go index d3469f1..9338472 100644 --- a/internal/erpserver/repository/system/config/config.go +++ b/internal/erpserver/repository/system/config/config.go @@ -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 diff --git a/internal/erpserver/repository/system/department/department.go b/internal/erpserver/repository/system/department/department.go index 8e4fdc5..5b2694b 100644 --- a/internal/erpserver/repository/system/department/department.go +++ b/internal/erpserver/repository/system/department/department.go @@ -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 { diff --git a/internal/erpserver/repository/system/menu/menu.go b/internal/erpserver/repository/system/menu/menu.go index 19f6685..ae815a0 100644 --- a/internal/erpserver/repository/system/menu/menu.go +++ b/internal/erpserver/repository/system/menu/menu.go @@ -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 } diff --git a/internal/erpserver/repository/system/role/role.go b/internal/erpserver/repository/system/role/role.go index 9b918c9..f397659 100644 --- a/internal/erpserver/repository/system/role/role.go +++ b/internal/erpserver/repository/system/role/role.go @@ -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 { diff --git a/internal/erpserver/repository/system/user/user.go b/internal/erpserver/repository/system/user/user.go index c10f5fa..cbe3f54 100644 --- a/internal/erpserver/repository/system/user/user.go +++ b/internal/erpserver/repository/system/user/user.go @@ -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) { diff --git a/internal/erpserver/service/v1/auth/auth.go b/internal/erpserver/service/v1/auth/auth.go index 6e35bb7..ad0be0c 100644 --- a/internal/erpserver/service/v1/auth/auth.go +++ b/internal/erpserver/service/v1/auth/auth.go @@ -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 { diff --git a/internal/erpserver/service/v1/system/department.go b/internal/erpserver/service/v1/system/department.go index de8fe3d..32cc1f7 100644 --- a/internal/erpserver/service/v1/system/department.go +++ b/internal/erpserver/service/v1/system/department.go @@ -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 diff --git a/internal/erpserver/service/v1/system/role.go b/internal/erpserver/service/v1/system/role.go index d7b6431..9ad3424 100644 --- a/internal/erpserver/service/v1/system/role.go +++ b/internal/erpserver/service/v1/system/role.go @@ -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 diff --git a/internal/erpserver/service/v1/system/user.go b/internal/erpserver/service/v1/system/user.go index 826cea7..a2d8f22 100644 --- a/internal/erpserver/service/v1/system/user.go +++ b/internal/erpserver/service/v1/system/user.go @@ -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) { diff --git a/internal/erpserver/templ/auth/login_templ.go b/internal/erpserver/templ/auth/login_templ.go index 20cb16d..e6f3a61 100644 --- a/internal/erpserver/templ/auth/login_templ.go +++ b/internal/erpserver/templ/auth/login_templ.go @@ -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. diff --git a/internal/erpserver/templ/base/base_templ.go b/internal/erpserver/templ/base/base_templ.go index 6a25c27..97f3763 100644 --- a/internal/erpserver/templ/base/base_templ.go +++ b/internal/erpserver/templ/base/base_templ.go @@ -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. diff --git a/internal/erpserver/templ/component/content_templ.go b/internal/erpserver/templ/component/content_templ.go index f41db24..0b54d6c 100644 --- a/internal/erpserver/templ/component/content_templ.go +++ b/internal/erpserver/templ/component/content_templ.go @@ -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. diff --git a/internal/erpserver/templ/component/upload_templ.go b/internal/erpserver/templ/component/upload_templ.go index 1320fca..0e69d86 100644 --- a/internal/erpserver/templ/component/upload_templ.go +++ b/internal/erpserver/templ/component/upload_templ.go @@ -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. diff --git a/internal/erpserver/templ/home/dashboard_templ.go b/internal/erpserver/templ/home/dashboard_templ.go index d3a6aed..ef9d671 100644 --- a/internal/erpserver/templ/home/dashboard_templ.go +++ b/internal/erpserver/templ/home/dashboard_templ.go @@ -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. diff --git a/internal/erpserver/templ/home/home_templ.go b/internal/erpserver/templ/home/home_templ.go index 7fcbdb7..ca12e84 100644 --- a/internal/erpserver/templ/home/home_templ.go +++ b/internal/erpserver/templ/home/home_templ.go @@ -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. diff --git a/internal/erpserver/templ/system/auditlog/list_templ.go b/internal/erpserver/templ/system/auditlog/list_templ.go index 6a5c43b..7562329 100644 --- a/internal/erpserver/templ/system/auditlog/list_templ.go +++ b/internal/erpserver/templ/system/auditlog/list_templ.go @@ -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. diff --git a/internal/erpserver/templ/system/category/list_templ.go b/internal/erpserver/templ/system/category/list_templ.go index e2da930..2c75c40 100644 --- a/internal/erpserver/templ/system/category/list_templ.go +++ b/internal/erpserver/templ/system/category/list_templ.go @@ -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. diff --git a/internal/erpserver/templ/system/config/edit_templ.go b/internal/erpserver/templ/system/config/edit_templ.go index 789a318..90de023 100644 --- a/internal/erpserver/templ/system/config/edit_templ.go +++ b/internal/erpserver/templ/system/config/edit_templ.go @@ -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. diff --git a/internal/erpserver/templ/system/config/list_templ.go b/internal/erpserver/templ/system/config/list_templ.go index 3e8c9ac..46fbcf1 100644 --- a/internal/erpserver/templ/system/config/list_templ.go +++ b/internal/erpserver/templ/system/config/list_templ.go @@ -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. diff --git a/internal/erpserver/templ/system/department/edit_templ.go b/internal/erpserver/templ/system/department/edit_templ.go index 8745090..f67f358 100644 --- a/internal/erpserver/templ/system/department/edit_templ.go +++ b/internal/erpserver/templ/system/department/edit_templ.go @@ -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, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "\">
  • 基础信息
  • 其它
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } diff --git a/internal/erpserver/templ/system/department/list_templ.go b/internal/erpserver/templ/system/department/list_templ.go index bfd1a0b..a260bf2 100644 --- a/internal/erpserver/templ/system/department/list_templ.go +++ b/internal/erpserver/templ/system/department/list_templ.go @@ -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. diff --git a/internal/erpserver/templ/system/loginlog/list_templ.go b/internal/erpserver/templ/system/loginlog/list_templ.go index a055188..19af078 100644 --- a/internal/erpserver/templ/system/loginlog/list_templ.go +++ b/internal/erpserver/templ/system/loginlog/list_templ.go @@ -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. diff --git a/internal/erpserver/templ/system/menu/edit_templ.go b/internal/erpserver/templ/system/menu/edit_templ.go index fc135cf..d9dcd9a 100644 --- a/internal/erpserver/templ/system/menu/edit_templ.go +++ b/internal/erpserver/templ/system/menu/edit_templ.go @@ -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. diff --git a/internal/erpserver/templ/system/menu/list_templ.go b/internal/erpserver/templ/system/menu/list_templ.go index 7d38d88..7a92d95 100644 --- a/internal/erpserver/templ/system/menu/list_templ.go +++ b/internal/erpserver/templ/system/menu/list_templ.go @@ -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. diff --git a/internal/erpserver/templ/system/role/edit_templ.go b/internal/erpserver/templ/system/role/edit_templ.go index dc75de3..9e2191f 100644 --- a/internal/erpserver/templ/system/role/edit_templ.go +++ b/internal/erpserver/templ/system/role/edit_templ.go @@ -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. diff --git a/internal/erpserver/templ/system/role/list_templ.go b/internal/erpserver/templ/system/role/list_templ.go index 8744608..aaa55f9 100644 --- a/internal/erpserver/templ/system/role/list_templ.go +++ b/internal/erpserver/templ/system/role/list_templ.go @@ -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. diff --git a/internal/erpserver/templ/system/role/setmenu_templ.go b/internal/erpserver/templ/system/role/setmenu_templ.go index cb39b06..63fab78 100644 --- a/internal/erpserver/templ/system/role/setmenu_templ.go +++ b/internal/erpserver/templ/system/role/setmenu_templ.go @@ -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. diff --git a/internal/erpserver/templ/system/user/edit_templ.go b/internal/erpserver/templ/system/user/edit_templ.go index c33069b..d3d0155 100644 --- a/internal/erpserver/templ/system/user/edit_templ.go +++ b/internal/erpserver/templ/system/user/edit_templ.go @@ -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. diff --git a/internal/erpserver/templ/system/user/list_templ.go b/internal/erpserver/templ/system/user/list_templ.go index 833dcda..b86a0b5 100644 --- a/internal/erpserver/templ/system/user/list_templ.go +++ b/internal/erpserver/templ/system/user/list_templ.go @@ -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. diff --git a/internal/erpserver/templ/system/user/profile_templ.go b/internal/erpserver/templ/system/user/profile_templ.go index 66eb5ca..82ba6ac 100644 --- a/internal/erpserver/templ/system/user/profile_templ.go +++ b/internal/erpserver/templ/system/user/profile_templ.go @@ -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. diff --git a/internal/pkg/database/error.go b/internal/pkg/database/error.go index fa64364..093a731 100644 --- a/internal/pkg/database/error.go +++ b/internal/pkg/database/error.go @@ -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) +// } diff --git a/internal/pkg/file/upload.go b/internal/pkg/file/upload.go index 808aa4a..528aa12 100644 --- a/internal/pkg/file/upload.go +++ b/internal/pkg/file/upload.go @@ -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) } diff --git a/internal/pkg/mid/audit_v3.go b/internal/pkg/mid/audit_v3.go index 9850e89..d0db2f6 100644 --- a/internal/pkg/mid/audit_v3.go +++ b/internal/pkg/mid/audit_v3.go @@ -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) diff --git a/internal/pkg/sqldb/dbarray/dbarray.go b/internal/pkg/sqldb/dbarray/dbarray.go new file mode 100644 index 0000000..f0fa955 --- /dev/null +++ b/internal/pkg/sqldb/dbarray/dbarray.go @@ -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 +} diff --git a/internal/pkg/sqldb/dbarray/encode.go b/internal/pkg/sqldb/dbarray/encode.go new file mode 100644 index 0000000..1693b9b --- /dev/null +++ b/internal/pkg/sqldb/dbarray/encode.go @@ -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 +} diff --git a/internal/pkg/sqldb/sqldb.go b/internal/pkg/sqldb/sqldb.go index 36bf91f..779b617 100644 --- a/internal/pkg/sqldb/sqldb.go +++ b/internal/pkg/sqldb/sqldb.go @@ -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 { diff --git a/internal/tasks/audit.go b/internal/tasks/audit.go index 0a50012..2be78d0 100644 --- a/internal/tasks/audit.go +++ b/internal/tasks/audit.go @@ -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 { diff --git a/internal/tasks/processor.go b/internal/tasks/processor.go index d2cb9bd..3b115a9 100644 --- a/internal/tasks/processor.go +++ b/internal/tasks/processor.go @@ -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()),