diff --git a/Makefile b/Makefile index 20f4e72..e35f6a9 100644 --- a/Makefile +++ b/Makefile @@ -44,6 +44,10 @@ db_schema: wire: wire ./... +.PHONY: templ +templ: + templ generate + .PHONY: test test: go test -v -cover ./... diff --git a/cmd/erp.go b/cmd/erp.go index 8582bc8..776426e 100644 --- a/cmd/erp.go +++ b/cmd/erp.go @@ -10,6 +10,7 @@ import ( "management/internal/erpserver" "management/internal/erpserver/repository/seed" "management/internal/pkg/config" + "management/internal/pkg/mid" "github.com/drhin/logger" "github.com/fvbock/endless" @@ -69,6 +70,9 @@ func runErp() error { defer fn() + // 初始化审计缓冲器 + mid.InitAuditBuffer(app.AuditLogService, l) + address := fmt.Sprintf("%s:%d", conf.App.Host, conf.App.Port) log.Printf("Starting manage server on %s", address) diff --git a/configs/config.dev.yaml b/configs/config.dev.yaml index 23f0aad..2a5a09c 100644 --- a/configs/config.dev.yaml +++ b/configs/config.dev.yaml @@ -12,7 +12,7 @@ db: max_open_conns: 100 conn_max_lifetime: 7h conn_max_idle_time: 30m - log_mode: true + log_mode: false redis: host: 127.0.0.1 port: 6379 diff --git a/go.mod b/go.mod index 5d9448c..ab41bab 100644 --- a/go.mod +++ b/go.mod @@ -3,8 +3,9 @@ module management go 1.24.2 require ( - github.com/alexedwards/scs/postgresstore v0.0.0-20250417082927-ab20b3feb5e9 + github.com/a-h/templ v0.3.898 github.com/alexedwards/scs/v2 v2.8.0 + github.com/allegro/bigcache/v3 v3.1.0 github.com/drhin/logger v0.0.0-20250417021954-aa33afe047bc github.com/fsnotify/fsnotify v1.9.0 github.com/fvbock/endless v0.0.0-20170109170031-447134032cb6 @@ -22,9 +23,11 @@ require ( github.com/redis/go-redis/v9 v9.8.0 github.com/spf13/cobra v1.9.1 github.com/spf13/viper v1.20.1 + github.com/tdewolff/minify v2.3.6+incompatible github.com/zhang2092/browser v0.0.2 go.uber.org/zap v1.27.0 golang.org/x/crypto v0.38.0 + gorm.io/datatypes v1.2.5 gorm.io/driver/postgres v1.5.11 gorm.io/gorm v1.30.0 ) @@ -32,8 +35,13 @@ require ( require ( filippo.io/edwards25519 v1.1.0 // indirect github.com/blang/semver/v4 v4.0.0 // indirect + github.com/bytedance/sonic v1.13.3 // indirect + github.com/bytedance/sonic/loader v0.2.4 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/cloudwego/base64x v0.1.5 // indirect + github.com/dgraph-io/ristretto v0.2.0 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect + github.com/dustin/go-humanize v1.0.1 // indirect github.com/gabriel-vasile/mimetype v1.4.9 // indirect github.com/go-sql-driver/mysql v1.9.2 // indirect github.com/go-viper/mapstructure/v2 v2.2.1 // indirect @@ -44,16 +52,23 @@ require ( github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect + github.com/klauspost/cpuid/v2 v2.0.9 // indirect github.com/leodido/go-urn v1.4.0 // indirect github.com/natefinch/lumberjack v2.0.0+incompatible // indirect + github.com/patrickmn/go-cache v2.1.0+incompatible // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect + github.com/pkg/errors v0.9.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.8.0 // indirect github.com/spf13/pflag v1.0.6 // indirect github.com/subosito/gotenv v1.6.0 // indirect + github.com/tdewolff/parse v2.3.4+incompatible // indirect + github.com/tdewolff/test v1.0.11 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect go.uber.org/multierr v1.11.0 // indirect + golang.org/x/arch v0.0.0-20210923205945-b76863e36670 // indirect golang.org/x/image v0.27.0 // indirect golang.org/x/net v0.40.0 // indirect golang.org/x/sync v0.15.0 // indirect @@ -61,6 +76,5 @@ require ( golang.org/x/text v0.26.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - gorm.io/datatypes v1.2.5 // indirect gorm.io/driver/mysql v1.6.0 // indirect ) diff --git a/go.sum b/go.sum index 6d52bdc..53edff9 100644 --- a/go.sum +++ b/go.sum @@ -2,26 +2,40 @@ 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/alexedwards/scs/postgresstore v0.0.0-20250417082927-ab20b3feb5e9 h1:FGBhs+LG4w1y511QLcuLr1xfhI7Fbyq6Da1TCf6EQq4= -github.com/alexedwards/scs/postgresstore v0.0.0-20250417082927-ab20b3feb5e9/go.mod h1:TDDdV/xnjj+/4zBQ9a2k+i2AbuAdY7SQjPUh5zoTZ3M= +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/allegro/bigcache/v3 v3.1.0 h1:H2Vp8VOvxcrB91o86fUSVJFqeuz8kpyyB02eH3bSzwk= +github.com/allegro/bigcache/v3 v3.1.0/go.mod h1:aPyh7jEvrog9zAwx5N7+JUQX5dZTSGpxF1LAR4dr35I= 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/bytedance/sonic v1.13.3 h1:MS8gmaH16Gtirygw7jV91pDCN33NyMrPbN7qiYhEsF0= +github.com/bytedance/sonic v1.13.3/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4= +github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= +github.com/bytedance/sonic/loader v0.2.4 h1:ZWCw4stuXUsn1/+zQDqeE7JKP+QO47tz7QCNan80NzY= +github.com/bytedance/sonic/loader v0.2.4/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI= 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/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4= +github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= +github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgraph-io/ristretto v0.2.0 h1:XAfl+7cmoUDWW/2Lx8TGZQjjxIQ2Ley9DSf52dru4WE= +github.com/dgraph-io/ristretto v0.2.0/go.mod h1:8uBHCU/PBV4Ag0CJrP47b9Ofby5dqWNh4FicAdoqFNU= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/drhin/logger v0.0.0-20250417021954-aa33afe047bc h1:/vyhHw4e3eNwpEEJ80m889cVcLV7g+AiTVKzRgCl6As= github.com/drhin/logger v0.0.0-20250417021954-aa33afe047bc/go.mod h1:QUg+qnn7zvYONlsRRGWVKI4zArm4vZN2e5JFTydJICM= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= @@ -44,6 +58,10 @@ github.com/go-sql-driver/mysql v1.9.2 h1:4cNKDYQ1I84SXslGddlsrMhc8k4LeDVj6Ad6WRj github.com/go-sql-driver/mysql v1.9.2/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/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.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= @@ -76,22 +94,31 @@ github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7 github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/justinas/nosurf v1.1.1 h1:92Aw44hjSK4MxJeMSyDa7jwuI9GR2J/JCQiaKvXXSlk= github.com/justinas/nosurf v1.1.1/go.mod h1:ALpWdSbuNGy2lZWtyXdjkYv4edL23oSEgfBT1gPJ5BQ= +github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= -github.com/lib/pq v1.4.0 h1:TmtCFbH+Aw0AixwyttznSMQDgbR5Yed/Gg6S8Funrhc= -github.com/lib/pq v1.4.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/matoous/go-nanoid/v2 v2.1.0 h1:P64+dmq21hhWdtvZfEAofnvJULaRR1Yib0+PnU669bE= github.com/matoous/go-nanoid/v2 v2.1.0/go.mod h1:KlbGNQ+FhrUNIHUxZdL63t7tl4LaPkZNpUULS8H4uVM= +github.com/mattn/go-sqlite3 v1.14.15 h1:vfoHhTN1af61xCRSWzFIWzx2YskyMTwHLrExkBOjvxI= +github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= +github.com/microsoft/go-mssqldb v1.7.2 h1:CHkFJiObW7ItKTJfHo1QX7QBBD1iV+mn1eOyRP3b/PA= +github.com/microsoft/go-mssqldb v1.7.2/go.mod h1:kOvZKUdrhhFQmxLZqbwUV0rHkNkZpthMITIb2Ko1IoA= github.com/mojocn/base64Captcha v1.3.8 h1:rrN9BhCwXKS8ht1e21kvR3iTaMgf4qPC9sRoV52bqEg= github.com/mojocn/base64Captcha v1.3.8/go.mod h1:QFZy927L8HVP3+VV5z2b1EAEiv1KxVJKZbAucVgLUy4= github.com/natefinch/lumberjack v2.0.0+incompatible h1:4QJd3OLAMgj7ph+yZTuX13Ld4UpgHp07nNdFX7mqFfM= github.com/natefinch/lumberjack v2.0.0+incompatible/go.mod h1:Wi9p2TTF5DG5oU+6YfsmYQpsTIOm0B1VNzQg9Mw6nPk= +github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= +github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/redis/go-redis/v9 v9.8.0 h1:q3nRvjrlge/6UD7eTu/DSg2uYiU2mCL0G/uzBWqhicI= @@ -118,12 +145,25 @@ github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An github.com/spf13/viper v1.20.1 h1:ZMi+z/lvLyPSCoNtFCpqjy0S4kPbirhpTMwl8BkW9X4= github.com/spf13/viper v1.20.1/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/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.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +github.com/tdewolff/minify v2.3.6+incompatible h1:2hw5/9ZvxhWLvBUnHE06gElGYz+Jv9R4Eys0XUzItYo= +github.com/tdewolff/minify v2.3.6+incompatible/go.mod h1:9Ov578KJUmAWpS6NeZwRZyT56Uf6o3Mcz9CEsg8USYs= +github.com/tdewolff/parse v2.3.4+incompatible h1:x05/cnGwIMf4ceLuDMBOdQ1qGniMoxpP46ghf0Qzh38= +github.com/tdewolff/parse v2.3.4+incompatible/go.mod h1:8oBwCsVmUkgHO8M5iCzSIDtpzXOT0WXX9cWhz+bIzJQ= +github.com/tdewolff/test v1.0.11 h1:FdLbwQVHxqG16SlkGveC0JVyrJN62COWTRyUFzfbtBE= +github.com/tdewolff/test v1.0.11/go.mod h1:XPuWBzvdUzhCuxWO1ojpXsyzsA5bFoS3tO/Q3kFuTG8= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/zhang2092/browser v0.0.2 h1:4jyWWkSCabGxqn74lCDvNnA4o9TlmsUXuwQHPZFsflQ= github.com/zhang2092/browser v0.0.2/go.mod h1:k/HIdVgWmpi9WvGuIU8pu8aK4rMCI4vr0ICCsk5H8T8= @@ -133,6 +173,8 @@ 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= +golang.org/x/arch v0.0.0-20210923205945-b76863e36670 h1:18EFjUmQOcUvxNYSkA6jO9VAiXCnxFY6NyDX0bHDmkU= +golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= 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= @@ -168,8 +210,6 @@ 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.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ= -golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8= golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -203,8 +243,6 @@ 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.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4= -golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= 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/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -231,7 +269,10 @@ 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.5.11 h1:ubBVAfbKEUld/twyKZ0IYn9rSQh448EdelLYk9Mv314= gorm.io/driver/postgres v1.5.11/go.mod h1:DX3GReXH+3FPWGrrgffdvCk3DQ1dwDPdmbenSkweRGI= -gorm.io/gorm v1.26.1 h1:ghB2gUI9FkS46luZtn6DLZ0f6ooBJ5IbVej2ENFDjRw= -gorm.io/gorm v1.26.1/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE= +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= +nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= diff --git a/internal/erpserver/app.go b/internal/erpserver/app.go index 9f2eee9..71478ae 100644 --- a/internal/erpserver/app.go +++ b/internal/erpserver/app.go @@ -2,6 +2,7 @@ package erpserver import ( "management/internal/erpserver/model/system" + v1 "management/internal/erpserver/service/v1" "github.com/go-chi/chi/v5" ) @@ -16,6 +17,8 @@ type App struct { LoginLogRepo system.LoginLogRepository AuditLogRepo system.AuditLogRepository + AuditLogService v1.AuditLogService + Router *chi.Mux } @@ -29,6 +32,8 @@ func NewApp( LoginLogRepo system.LoginLogRepository, AuditLogRepo system.AuditLogRepository, + AuditLogService v1.AuditLogService, + Router *chi.Mux, ) App { return App{ @@ -41,6 +46,8 @@ func NewApp( LoginLogRepo: LoginLogRepo, AuditLogRepo: AuditLogRepo, + AuditLogService: AuditLogService, + Router: Router, } } diff --git a/internal/erpserver/handler/handler.go b/internal/erpserver/handler/handler.go index 4522b40..7a66aef 100644 --- a/internal/erpserver/handler/handler.go +++ b/internal/erpserver/handler/handler.go @@ -2,6 +2,7 @@ package handler import ( "context" + "fmt" "net/http" "management/internal/erpserver/model/dto" @@ -10,7 +11,9 @@ import ( "management/internal/pkg/render" "management/internal/pkg/session" + "github.com/a-h/templ" "github.com/drhin/logger" + "go.uber.org/zap" ) //type RouterGroup interface { @@ -40,14 +43,14 @@ func NewHandler( } // ===================================================================================================================== -// middleware 帮助方法 +// mid 帮助方法 func (h *Handler) AuthUser(ctx context.Context) dto.AuthorizeUser { u, err := h.session.GetUser(ctx, know.StoreName) if err != nil { return dto.AuthorizeUser{} } - return *u + return u } func (h *Handler) RenewToken(ctx context.Context) error { @@ -61,6 +64,14 @@ func (h *Handler) Destroy(ctx context.Context) error { // ===================================================================================================================== // render 帮助方法 +func (h *Handler) Render(ctx context.Context, w http.ResponseWriter, t templ.Component) { + if err := t.Render(ctx, w); err != nil { + h.Log.Error(err.Error(), err, + zap.String("templ render", fmt.Sprintf("%v", t)), + ) + } +} + func (h *Handler) HTML(w http.ResponseWriter, r *http.Request, name string, data map[string]any) { h.render.HTML(w, r, name, data) } diff --git a/internal/erpserver/handler/system/home.go b/internal/erpserver/handler/system/home.go index 2d9f092..7ceb3fe 100644 --- a/internal/erpserver/handler/system/home.go +++ b/internal/erpserver/handler/system/home.go @@ -5,6 +5,7 @@ import ( "management/internal/erpserver/handler" v1 "management/internal/erpserver/service/v1" + "management/internal/erpserver/templ/home" ) type HomeHandler struct { @@ -26,19 +27,22 @@ func NewHomeHandler( } func (h *HomeHandler) Home(w http.ResponseWriter, r *http.Request) { - h.HTML(w, r, "home/home.tmpl", nil) + ctx := r.Context() + h.Render(ctx, w, home.Home(ctx)) + //h.HTML(w, r, "home/home.tmpl", nil) } func (h *HomeHandler) Dashboard(w http.ResponseWriter, r *http.Request) { ctx := r.Context() auth := h.AuthUser(ctx) - user, _ := h.userService.Get(ctx, auth.ID) + //user, _ := h.userService.Get(ctx, auth.ID) lt, _ := h.loginLogService.LoginTime(ctx, auth.Email) c := h.loginLogService.LoginCount(ctx, auth.Email) - h.HTML(w, r, "home/dashboard.tmpl", map[string]any{ - "Auth": auth, - "User": user, - "LoginTime": lt, - "LoginCount": c, - }) + h.Render(ctx, w, home.Dashboard(ctx, int(c), lt.ThisLoginTime, lt.LastLoginTime)) + //h.HTML(w, r, "home/dashboard.tmpl", map[string]any{ + // "Auth": auth, + // "User": user, + // "LoginTime": lt, + // "LoginCount": c, + //}) } diff --git a/internal/erpserver/handler/system/role.go b/internal/erpserver/handler/system/role.go index d9cc6fa..a5378ca 100644 --- a/internal/erpserver/handler/system/role.go +++ b/internal/erpserver/handler/system/role.go @@ -9,6 +9,7 @@ import ( "management/internal/erpserver/model/form" "management/internal/erpserver/model/system" v1 "management/internal/erpserver/service/v1" + "management/internal/erpserver/templ/system/role" "management/internal/pkg/binding" "management/internal/pkg/convertor" "management/internal/pkg/render" @@ -31,7 +32,9 @@ func NewRoleHandler(handler *handler.Handler, roleService v1.RoleService, menuSe func (h *RoleHandler) List(w http.ResponseWriter, r *http.Request) { switch r.Method { case http.MethodGet: - h.HTML(w, r, "role/list.tmpl", nil) + ctx := r.Context() + h.Render(ctx, w, role.List(ctx)) + //h.HTML(w, r, "role/list.tmpl", nil) case http.MethodPost: var q dto.SearchDto q.SearchTimeBegin, q.SearchTimeEnd = convertor.DefaultStartTimeAndEndTime(r.PostFormValue("timeBegin"), r.PostFormValue("timeEnd")) @@ -60,30 +63,36 @@ func (h *RoleHandler) List(w http.ResponseWriter, r *http.Request) { } func (h *RoleHandler) Add(w http.ResponseWriter, r *http.Request) { - h.HTML(w, r, "role/edit.tmpl", map[string]any{ - "Item": &system.Role{Sort: 6666}, - }) + ctx := r.Context() + h.Render(ctx, w, role.Edit(ctx, &system.Role{Sort: 6666})) + //h.HTML(w, r, "role/edit.tmpl", map[string]any{ + // "Item": &system.Role{Sort: 6666}, + //}) } func (h *RoleHandler) AddChildren(w http.ResponseWriter, r *http.Request) { vars := r.URL.Query() parentID := convertor.QueryInt[int32](vars, "parentID", 0) vm := &system.Role{ParentID: parentID, Sort: 6666} - h.HTML(w, r, "role/edit.tmpl", map[string]any{ - "Item": vm, - }) + ctx := r.Context() + h.Render(ctx, w, role.Edit(ctx, vm)) + //h.HTML(w, r, "role/edit.tmpl", map[string]any{ + // "Item": vm, + //}) } func (h *RoleHandler) Edit(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() vars := r.URL.Query() id := convertor.QueryInt[int32](vars, "id", 0) vm := &system.Role{Sort: 6666} if id > 0 { - vm, _ = h.roleService.Get(r.Context(), id) + vm, _ = h.roleService.Get(ctx, id) } - h.HTML(w, r, "role/edit.tmpl", map[string]any{ - "Item": vm, - }) + h.Render(ctx, w, role.Edit(ctx, vm)) + //h.HTML(w, r, "role/edit.tmpl", map[string]any{ + // "Item": vm, + //}) } func (h *RoleHandler) Save(w http.ResponseWriter, r *http.Request) { @@ -158,32 +167,32 @@ func (h *RoleHandler) RefreshRoleMenus(w http.ResponseWriter, r *http.Request) { // 获取需要刷新的角色ID roleID := convertor.ConvertInt[int32](r.PostFormValue("roleID"), 0) - role, err := h.roleService.Get(ctx, roleID) + roleModel, err := h.roleService.Get(ctx, roleID) if err != nil { h.JSONErr(w, err.Error()) return } - if role == nil { + if roleModel == nil { h.JSONErr(w, "角色不存在") return } // 刷新角色菜单 (角色所拥有的菜单集合) - _, err = h.menuService.SetListByRoleID(ctx, role.ID) + _, err = h.menuService.SetListByRoleID(ctx, roleModel.ID) if err != nil { h.JSONErr(w, err.Error()) return } // 刷新角色菜单 (角色所拥有的菜单集合) - _, err = h.menuService.SetListByRoleIDToMap(ctx, role.ID) + _, err = h.menuService.SetListByRoleIDToMap(ctx, roleModel.ID) if err != nil { h.JSONErr(w, err.Error()) return } // 刷新角色菜单树 (pear admin layui 使用的格式) - _, err = h.menuService.SetOwerMenus(ctx, role.ID) + _, err = h.menuService.SetOwerMenus(ctx, roleModel.ID) if err != nil { h.JSONErr(w, err.Error()) return @@ -220,7 +229,7 @@ func (h *RoleHandler) SetMenu(w http.ResponseWriter, r *http.Request) { h.JSONErr(w, "角色异常, 请刷新重试") return } - role, err := h.roleService.Get(ctx, id) + roleModel, err := h.roleService.Get(ctx, id) if err != nil { h.JSONErr(w, err.Error()) return @@ -248,7 +257,7 @@ func (h *RoleHandler) SetMenu(w http.ResponseWriter, r *http.Request) { } rms = append(rms, &system.RoleMenu{ - RoleID: role.ID, + RoleID: roleModel.ID, MenuID: menu.ID, }) } diff --git a/internal/erpserver/handler/system/router.go b/internal/erpserver/handler/system/router.go index 140f553..523604f 100644 --- a/internal/erpserver/handler/system/router.go +++ b/internal/erpserver/handler/system/router.go @@ -3,7 +3,7 @@ package system // //import ( // v1 "management/internal/erpserver/service/v1" -// mi "management/internal/pkg/middleware" +// mi "management/internal/pkg/mid" // "management/internal/pkg/session" // // "github.com/go-chi/chi/v5" diff --git a/internal/erpserver/handler/system/user.go b/internal/erpserver/handler/system/user.go index 1d57ac9..6cac07f 100644 --- a/internal/erpserver/handler/system/user.go +++ b/internal/erpserver/handler/system/user.go @@ -10,6 +10,7 @@ import ( "management/internal/erpserver/model/form" systemmodel "management/internal/erpserver/model/system" v1 "management/internal/erpserver/service/v1" + "management/internal/erpserver/templ/auth" "management/internal/pkg/binding" "management/internal/pkg/convertor" "management/internal/pkg/render" @@ -176,7 +177,9 @@ func (h *UserHandler) Login(w http.ResponseWriter, r *http.Request) { } _ = h.Destroy(ctx) - h.HTML(w, r, "oauth/login.tmpl", nil) + component := auth.Login(ctx) + h.Render(ctx, w, component) + //h.HTML(w, r, "oauth/login.tmpl", nil) case http.MethodPost: defer func(Body io.ReadCloser) { err := Body.Close() diff --git a/internal/erpserver/http.go b/internal/erpserver/http.go index b82c06d..8a98cb8 100644 --- a/internal/erpserver/http.go +++ b/internal/erpserver/http.go @@ -6,7 +6,7 @@ import ( "management/internal/erpserver/handler/common" "management/internal/erpserver/handler/system" v1 "management/internal/erpserver/service/v1" - mi "management/internal/pkg/middleware" + mi "management/internal/pkg/mid" "management/internal/pkg/session" static "management/web/statics" @@ -35,7 +35,7 @@ func NewHTTPServer( r.Use(middleware.RequestID) r.Use(middleware.RealIP) - // r.Use(middleware.Logger) + // r.Use(mid.Logger) r.Use(middleware.Recoverer) //staticServer := http.FileServer(http.Dir("./web/statics/")) @@ -47,6 +47,7 @@ func NewHTTPServer( r.Group(func(r chi.Router) { r.Use(mi.NoSurf) // CSRF + r.Use(mi.NoSurfContext) // CSRF Store Context r.Use(mi.LoadSession(sm)) // Session r.Get("/", userHandler.Login) @@ -63,14 +64,14 @@ func NewHTTPServer( r.Get("/pear.json", configHandler.Pear) r.Route("/upload", func(r chi.Router) { - r.Use(mi.Audit(sm, auditLogService, log)) + r.Use(mi.Audit(sm, log)) r.Get("/img", uploadHandler.Img) r.Get("/file", uploadHandler.File) r.Get("/multi_files", uploadHandler.MultiFiles) }) r.Route("/system", func(r chi.Router) { - r.Use(mi.Audit(sm, auditLogService, log)) + r.Use(mi.Audit(sm, log)) r.Get("/menus", menuHandler.Menus) diff --git a/internal/erpserver/model/dto/authorize_user.go b/internal/erpserver/model/dto/authorize_user.go index 1cc4327..7997904 100644 --- a/internal/erpserver/model/dto/authorize_user.go +++ b/internal/erpserver/model/dto/authorize_user.go @@ -9,6 +9,7 @@ type AuthorizeUser struct { Uuid uuid.UUID `json:"uuid"` Email string `json:"email"` Username string `json:"username"` + Avatar string `json:"avatar"` RoleID int32 `json:"role_id"` RoleName string `json:"role_name"` OS string `json:"os"` diff --git a/internal/erpserver/model/system/audit_log.go b/internal/erpserver/model/system/audit_log.go index d284f34..cc9270d 100644 --- a/internal/erpserver/model/system/audit_log.go +++ b/internal/erpserver/model/system/audit_log.go @@ -12,6 +12,7 @@ import ( type AuditLogRepository interface { Create(ctx context.Context, obj *AuditLog) error + BatchCreate(ctx context.Context, objs []*AuditLog) error List(ctx context.Context, q dto.SearchDto) ([]*AuditLog, int64, error) } diff --git a/internal/erpserver/repository/system/audit_log.go b/internal/erpserver/repository/system/audit_log.go index 8ce3157..5bfe089 100644 --- a/internal/erpserver/repository/system/audit_log.go +++ b/internal/erpserver/repository/system/audit_log.go @@ -22,6 +22,10 @@ func (s *auditLogRepository) Create(ctx context.Context, obj *system.AuditLog) e return s.repo.DB(ctx).Create(obj).Error } +func (s *auditLogRepository) BatchCreate(ctx context.Context, objs []*system.AuditLog) error { + return s.repo.DB(ctx).Create(objs).Error +} + func (s *auditLogRepository) List(ctx context.Context, q dto.SearchDto) ([]*system.AuditLog, int64, error) { query := s.repo.DB(ctx). Model(&system.AuditLog{}). diff --git a/internal/erpserver/service/util/util.go b/internal/erpserver/service/util/util.go index 43c8631..dfa6bf2 100644 --- a/internal/erpserver/service/util/util.go +++ b/internal/erpserver/service/util/util.go @@ -8,14 +8,14 @@ import ( "time" "management/internal/erpserver/model/view" - "management/internal/pkg/redis" + "management/internal/pkg/cache" ) func GetCacheExpire() time.Duration { return time.Hour*6 + time.Duration(rand.Intn(600))*time.Second // 6小时±10分钟 } -func GetOrSetCache(ctx context.Context, redis redis.Cache, key string, expire time.Duration, getData func() (any, error), result any) error { +func GetOrSetCache(ctx context.Context, redis cache.Cache, key string, expire time.Duration, getData func() (any, error), result any) error { if data, err := redis.GetBytes(ctx, key); err == nil { return json.Unmarshal(data, result) } diff --git a/internal/erpserver/service/v1/service.go b/internal/erpserver/service/v1/service.go index 8b6eb9e..077e006 100644 --- a/internal/erpserver/service/v1/service.go +++ b/internal/erpserver/service/v1/service.go @@ -8,7 +8,7 @@ import ( "management/internal/erpserver/model/system" "management/internal/erpserver/model/view" "management/internal/erpserver/repository" - "management/internal/pkg/redis" + "management/internal/pkg/cache" "management/internal/pkg/session" "github.com/drhin/logger" @@ -18,14 +18,14 @@ type Service struct { Log *logger.Logger Tx repository.Transaction Session session.Manager - Redis redis.Cache + Redis cache.Cache } func NewService( log *logger.Logger, tx repository.Transaction, session session.Manager, - redis redis.Cache, + redis cache.Cache, ) *Service { return &Service{ Log: log, @@ -72,6 +72,7 @@ type LoginLogService interface { type AuditLogService interface { Create(ctx context.Context, req *system.AuditLog) error + BatchCreate(ctx context.Context, objs []*system.AuditLog) error List(ctx context.Context, q dto.SearchDto) ([]*system.AuditLog, int64, error) } diff --git a/internal/erpserver/service/v1/system/audit_log.go b/internal/erpserver/service/v1/system/audit_log.go index 3cd9b6f..83ff9a8 100644 --- a/internal/erpserver/service/v1/system/audit_log.go +++ b/internal/erpserver/service/v1/system/audit_log.go @@ -24,6 +24,10 @@ func (b *auditLogService) Create(ctx context.Context, req *system.AuditLog) erro return b.repo.Create(ctx, req) } +func (b *auditLogService) BatchCreate(ctx context.Context, objs []*system.AuditLog) error { + return b.repo.BatchCreate(ctx, objs) +} + func (b *auditLogService) List(ctx context.Context, q dto.SearchDto) ([]*system.AuditLog, int64, error) { return b.repo.List(ctx, q) } diff --git a/internal/erpserver/service/v1/system/user.go b/internal/erpserver/service/v1/system/user.go index 41359a8..c7d995b 100644 --- a/internal/erpserver/service/v1/system/user.go +++ b/internal/erpserver/service/v1/system/user.go @@ -179,11 +179,12 @@ func (s *userService) login(ctx context.Context, req *form.Login) error { } func (s *userService) loginSuccess(ctx context.Context, user *system.User, req *form.Login) error { - return s.Session.PutUser(ctx, know.StoreName, &dto.AuthorizeUser{ + return s.Session.PutUser(ctx, know.StoreName, dto.AuthorizeUser{ ID: user.ID, Uuid: user.Uuid, Email: user.Email, Username: user.Username, + Avatar: user.Avatar, RoleID: user.Role.ID, RoleName: user.Role.DisplayName, OS: req.Os, diff --git a/internal/erpserver/templ/auth/login.templ b/internal/erpserver/templ/auth/login.templ new file mode 100644 index 0000000..befb7d0 --- /dev/null +++ b/internal/erpserver/templ/auth/login.templ @@ -0,0 +1,150 @@ +package auth + +import ( + "context" + + "management/internal/pkg/mid" +) + +templ Login(ctx context.Context) { + {{ token := mid.GetCsrfToken(ctx) }} + + + + + + 登录 + + + + + + +
+
+ + +
+
+ + + + + + +} \ No newline at end of file diff --git a/internal/erpserver/templ/auth/login_templ.go b/internal/erpserver/templ/auth/login_templ.go new file mode 100644 index 0000000..04acae0 --- /dev/null +++ b/internal/erpserver/templ/auth/login_templ.go @@ -0,0 +1,59 @@ +// Code generated by templ - DO NOT EDIT. + +// templ: version: v0.3.898 +package auth + +//lint:file-ignore SA4006 This context is only used if a nested component is present. + +import "github.com/a-h/templ" +import templruntime "github.com/a-h/templ/runtime" + +import ( + "context" + + "management/internal/pkg/mid" +) + +func Login(ctx context.Context) templ.Component { + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { + return templ_7745c5c3_CtxErr + } + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var1 := templ.GetChildren(ctx) + if templ_7745c5c3_Var1 == nil { + templ_7745c5c3_Var1 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + token := mid.GetCsrfToken(ctx) + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "登录
\"\"
\"\" Pear Admin 4.0
以 超 乎 想 象 的 速 度 构 建 内 部 工 具
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +var _ = templruntime.GeneratedTemplate diff --git a/internal/erpserver/templ/base/base.templ b/internal/erpserver/templ/base/base.templ new file mode 100644 index 0000000..2813ef0 --- /dev/null +++ b/internal/erpserver/templ/base/base.templ @@ -0,0 +1,79 @@ +package base + +import ( + "context" + + tpl "github.com/a-h/templ" +) + +templ Base(ctx context.Context, style, script tpl.Component) { + + + + + + + component + + + + + + + + @style + + + + { children... } + + + + + + + @script + + +} \ No newline at end of file diff --git a/internal/erpserver/templ/base/base_templ.go b/internal/erpserver/templ/base/base_templ.go new file mode 100644 index 0000000..8c26951 --- /dev/null +++ b/internal/erpserver/templ/base/base_templ.go @@ -0,0 +1,70 @@ +// Code generated by templ - DO NOT EDIT. + +// templ: version: v0.3.898 +package base + +//lint:file-ignore SA4006 This context is only used if a nested component is present. + +import "github.com/a-h/templ" +import templruntime "github.com/a-h/templ/runtime" + +import ( + "context" + + tpl "github.com/a-h/templ" +) + +func Base(ctx context.Context, style, script tpl.Component) templ.Component { + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { + return templ_7745c5c3_CtxErr + } + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var1 := templ.GetChildren(ctx) + if templ_7745c5c3_Var1 == nil { + templ_7745c5c3_Var1 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "component") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = style.Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templ_7745c5c3_Var1.Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = script.Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +var _ = templruntime.GeneratedTemplate diff --git a/internal/erpserver/templ/component/btn.go b/internal/erpserver/templ/component/btn.go new file mode 100644 index 0000000..82ebd33 --- /dev/null +++ b/internal/erpserver/templ/component/btn.go @@ -0,0 +1,140 @@ +package component + +import ( + "path/filepath" + "strings" + + "management/internal/erpserver/model/dto" + + "github.com/a-h/templ" +) + +func TemplBtn(buttons []dto.OwnerMenuDto, searchBtn bool, actionNames ...string) templ.Component { + var res string + if len(actionNames) == 0 { + return templ.Raw(res) + } + + if len(buttons) == 0 { + return templ.Raw(res) + } + + res = `` + return templ.Raw(res) +} + +func TemplLink(buttons []dto.OwnerMenuDto, actionNames ...string) templ.Component { + var res string + if len(actionNames) == 0 { + return templ.Raw(res) + } + + if len(buttons) == 0 { + return templ.Raw(res) + } + + res = `` + return templ.Raw(res) +} + +func GenBtn(buttons []dto.OwnerMenuDto, actionNames ...string) string { + var res string + if len(buttons) == 0 { + return res + } + + if len(actionNames) == 0 { + return res + } + + for _, action := range actionNames { + for _, btn := range buttons { + btn.Style = strings.ReplaceAll(btn.Style, "pear", "layui") + base := filepath.Base(btn.Url) + if base == action { + res += `` + } + } + } + + return res +} + +func GenLink(buttons []dto.OwnerMenuDto, actionNames ...string) string { + if len(buttons) == 0 { + return "" + } + + if len(actionNames) == 0 { + return "" + } + + var res string + for _, action := range actionNames { + for _, btn := range buttons { + btn.Style = strings.ReplaceAll(btn.Style, "pear", "layui") + base := filepath.Base(btn.Url) + if base == action { + res += `` + } + } + } + + return res +} + +func SubmitBtn(buttons []dto.OwnerMenuDto, actionNames ...string) templ.Component { + var res string + if len(buttons) == 0 { + return templ.Raw(res) + } + + if len(actionNames) == 0 { + return templ.Raw(res) + } + + for _, action := range actionNames { + for _, btn := range buttons { + btn.Style = strings.ReplaceAll(btn.Style, "pear", "layui") + base := filepath.Base(btn.Url) + if base == action { + res += `` + } + } + } + + return templ.Raw(res) +} + +func firstLower(s string) string { + if len(s) == 0 { + return s + } + + return strings.ToLower(s[:1]) + s[1:] +} diff --git a/internal/erpserver/templ/home/dashboard.templ b/internal/erpserver/templ/home/dashboard.templ new file mode 100644 index 0000000..79c1296 --- /dev/null +++ b/internal/erpserver/templ/home/dashboard.templ @@ -0,0 +1,38 @@ +package home + +import ( + "context" + "time" + + "management/internal/pkg/mid" +) + +templ Dashboard(ctx context.Context, loginCount int, thisLoginTime, lastLoginTime time.Time) { + {{ user := mid.GetUser(ctx) }} +
+
+
+ +
+ @info(user.Username, user.RoleName, loginCount, thisLoginTime, lastLoginTime) +
+
+
+
+} + +templ avatar(avatar string) { + logo +} + +templ info(username, roleName string, loginCount int, thisLoginTime, lastLoginTime time.Time) { +

欢迎您,{ username } ({ roleName })

+ if loginCount == 1 { +

这是您第 1 次登录,本次登录日期:{ thisLoginTime.Format(time.DateTime) },如果不是您本人登录,请及时修改密码 。

+ } else { +

这是您第 { loginCount } 次登录,本次登录日期:{ thisLoginTime.Format(time.DateTime) }, + 上次登录日期:{ lastLoginTime.Format(time.DateTime) },如果不是您本人登录,请及时修改密码 。

+ } +} \ No newline at end of file diff --git a/internal/erpserver/templ/home/dashboard_templ.go b/internal/erpserver/templ/home/dashboard_templ.go new file mode 100644 index 0000000..d3a6aed --- /dev/null +++ b/internal/erpserver/templ/home/dashboard_templ.go @@ -0,0 +1,224 @@ +// Code generated by templ - DO NOT EDIT. + +// templ: version: v0.3.898 +package home + +//lint:file-ignore SA4006 This context is only used if a nested component is present. + +import "github.com/a-h/templ" +import templruntime "github.com/a-h/templ/runtime" + +import ( + "context" + "time" + + "management/internal/pkg/mid" +) + +func Dashboard(ctx context.Context, loginCount int, thisLoginTime, lastLoginTime time.Time) templ.Component { + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { + return templ_7745c5c3_CtxErr + } + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var1 := templ.GetChildren(ctx) + if templ_7745c5c3_Var1 == nil { + templ_7745c5c3_Var1 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + user := mid.GetUser(ctx) + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = avatar(user.Avatar).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = info(user.Username, user.RoleName, loginCount, thisLoginTime, lastLoginTime).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +func avatar(avatar string) templ.Component { + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { + return templ_7745c5c3_CtxErr + } + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var2 := templ.GetChildren(ctx) + if templ_7745c5c3_Var2 == nil { + templ_7745c5c3_Var2 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "\"logo\"") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +func info(username, roleName string, loginCount int, thisLoginTime, lastLoginTime time.Time) templ.Component { + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { + return templ_7745c5c3_CtxErr + } + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var4 := templ.GetChildren(ctx) + if templ_7745c5c3_Var4 == nil { + templ_7745c5c3_Var4 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "

欢迎您,") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var5 string + templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(username) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/erpserver/templ/home/dashboard.templ`, Line: 31, Col: 42} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, " (") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var6 string + templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(roleName) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/erpserver/templ/home/dashboard.templ`, Line: 31, Col: 56} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, ")

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if loginCount == 1 { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "

这是您第 1 次登录,本次登录日期:") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var7 string + templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(thisLoginTime.Format(time.DateTime)) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/erpserver/templ/home/dashboard.templ`, Line: 33, Col: 96} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, ",如果不是您本人登录,请及时修改密码 。

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } else { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "

这是您第 ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var8 string + templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(loginCount) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/erpserver/templ/home/dashboard.templ`, Line: 35, Col: 36} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, " 次登录,本次登录日期:") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var9 string + templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(thisLoginTime.Format(time.DateTime)) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/erpserver/templ/home/dashboard.templ`, Line: 35, Col: 109} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, ", 上次登录日期:") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var10 string + templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(lastLoginTime.Format(time.DateTime)) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/erpserver/templ/home/dashboard.templ`, Line: 36, Col: 70} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, ",如果不是您本人登录,请及时修改密码 。

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + return nil + }) +} + +var _ = templruntime.GeneratedTemplate diff --git a/internal/erpserver/templ/home/home.templ b/internal/erpserver/templ/home/home.templ new file mode 100644 index 0000000..6bc4edb --- /dev/null +++ b/internal/erpserver/templ/home/home.templ @@ -0,0 +1,174 @@ +package home + +import ( + "context" + + "management/internal/pkg/mid" +) + +templ Home(ctx context.Context) { + + + + + + + 管理后台 + + + + + + + + + + + +
+ +
+ + + + + +
+ + +
+ +
+ + + +
+
+
+
+ +
+ +
+
+ + + +
+ +
+ +
+
+
+ +
+ +
+ + + + + + +} \ No newline at end of file diff --git a/internal/erpserver/templ/home/home_templ.go b/internal/erpserver/templ/home/home_templ.go new file mode 100644 index 0000000..7ab90c7 --- /dev/null +++ b/internal/erpserver/templ/home/home_templ.go @@ -0,0 +1,70 @@ +// Code generated by templ - DO NOT EDIT. + +// templ: version: v0.3.898 +package home + +//lint:file-ignore SA4006 This context is only used if a nested component is present. + +import "github.com/a-h/templ" +import templruntime "github.com/a-h/templ/runtime" + +import ( + "context" + + "management/internal/pkg/mid" +) + +func Home(ctx context.Context) templ.Component { + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { + return templ_7745c5c3_CtxErr + } + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var1 := templ.GetChildren(ctx) + if templ_7745c5c3_Var1 == nil { + templ_7745c5c3_Var1 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "管理后台
\"\"
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +var _ = templruntime.GeneratedTemplate diff --git a/internal/erpserver/templ/minifyjs/compress.go b/internal/erpserver/templ/minifyjs/compress.go new file mode 100644 index 0000000..a4e1a46 --- /dev/null +++ b/internal/erpserver/templ/minifyjs/compress.go @@ -0,0 +1,17 @@ +package minifyjs + +import ( + "github.com/tdewolff/minify" + "github.com/tdewolff/minify/js" +) + +func Compile(raw string) string { + m := minify.New() + m.AddFunc("text/javascript", js.Minify) + + minified, err := m.String("text/javascript", raw) + if err != nil { + return raw // 失败时返回原始代码 + } + return minified +} diff --git a/internal/erpserver/templ/system/role/edit.templ b/internal/erpserver/templ/system/role/edit.templ new file mode 100644 index 0000000..4907368 --- /dev/null +++ b/internal/erpserver/templ/system/role/edit.templ @@ -0,0 +1,212 @@ +package role + +import ( + "context" + "time" + + "management/internal/erpserver/templ/base" + "management/internal/pkg/mid" + "management/internal/erpserver/templ/component" + "management/internal/erpserver/model/system" +) + +templ Edit(ctx context.Context, item *system.Role) { + @base.Base(ctx, editCss(), editJs(ctx, item)) { + {{ meuns := mid.GetCurMenus(ctx) }} + {{ ht := mid.GetHtmlCsrfToken(ctx) }} +
+
+
+ @ht + + +
+
    +
  • 基础信息
  • +
  • 其它
  • +
+ +
+ +
+ if item.ID > 0 { +
+
ID
+
+ { item.ID } +
+
+ } + +
+
上级
+
+
    +
    +
    + +
    +
    名称
    +
    + +
    +
    + +
    +
    显示名称
    +
    + +
    +
    + +
    + +
    + +
    +
    + +
    + +
    + +
    +
    +
    + + +
    + if item.ID > 0 { +
    +
    创建时间
    +
    + { item.CreatedAt.Format(time.DateTime) } +
    +
    +
    +
    更新时间
    +
    + { item.UpdatedAt.Format(time.DateTime) } +
    +
    + } +
    +
    +
    + +
    +
    + @component.SubmitBtn(meuns, "save") + +
    +
    +
    +
    +
    + } +} + +templ editCss() { + +} + +templ editJs(ctx context.Context, item *system.Role) { + {{ token := mid.GetCsrfToken(ctx) }} + +} \ No newline at end of file diff --git a/internal/erpserver/templ/system/role/edit_templ.go b/internal/erpserver/templ/system/role/edit_templ.go new file mode 100644 index 0000000..b5f2aa0 --- /dev/null +++ b/internal/erpserver/templ/system/role/edit_templ.go @@ -0,0 +1,295 @@ +// Code generated by templ - DO NOT EDIT. + +// templ: version: v0.3.898 +package role + +//lint:file-ignore SA4006 This context is only used if a nested component is present. + +import "github.com/a-h/templ" +import templruntime "github.com/a-h/templ/runtime" + +import ( + "context" + "time" + + "management/internal/erpserver/model/system" + "management/internal/erpserver/templ/base" + "management/internal/erpserver/templ/component" + "management/internal/pkg/mid" +) + +func Edit(ctx context.Context, item *system.Role) templ.Component { + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { + return templ_7745c5c3_CtxErr + } + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var1 := templ.GetChildren(ctx) + if templ_7745c5c3_Var1 == nil { + templ_7745c5c3_Var1 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + templ_7745c5c3_Var2 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + meuns := mid.GetCurMenus(ctx) + ht := mid.GetHtmlCsrfToken(ctx) + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "
    ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = ht.Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "
    • 基础信息
    • 其它
    ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if item.ID > 0 { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "
    ID
    ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var4 string + templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(item.ID) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/erpserver/templ/system/role/edit.templ`, Line: 36, Col: 53} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "
    ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "
    上级
      名称
      显示名称
      ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if item.ID > 0 { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "
      创建时间
      ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var8 string + templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(item.CreatedAt.Format(time.DateTime)) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/erpserver/templ/system/role/edit.templ`, Line: 95, Col: 78} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "
      更新时间
      ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var9 string + templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(item.UpdatedAt.Format(time.DateTime)) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/erpserver/templ/system/role/edit.templ`, Line: 101, Col: 78} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "
      ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "
      ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = component.SubmitBtn(meuns, "save").Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "
      ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) + templ_7745c5c3_Err = base.Base(ctx, editCss(), editJs(ctx, item)).Render(templ.WithChildren(ctx, templ_7745c5c3_Var2), templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +func editCss() templ.Component { + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { + return templ_7745c5c3_CtxErr + } + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var10 := templ.GetChildren(ctx) + if templ_7745c5c3_Var10 == nil { + templ_7745c5c3_Var10 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +func editJs(ctx context.Context, item *system.Role) templ.Component { + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { + return templ_7745c5c3_CtxErr + } + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var11 := templ.GetChildren(ctx) + if templ_7745c5c3_Var11 == nil { + templ_7745c5c3_Var11 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + token := mid.GetCsrfToken(ctx) + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +var _ = templruntime.GeneratedTemplate diff --git a/internal/erpserver/templ/system/role/list.templ b/internal/erpserver/templ/system/role/list.templ new file mode 100644 index 0000000..da97f69 --- /dev/null +++ b/internal/erpserver/templ/system/role/list.templ @@ -0,0 +1,314 @@ +package role + +import ( + "context" + + "management/internal/erpserver/templ/base" + "management/internal/pkg/mid" + "management/internal/erpserver/templ/component" +) + +templ List(ctx context.Context) { + @base.Base(ctx, listCss(), listJs(ctx)) { + {{ meuns := mid.GetCurMenus(ctx) }} + @component.TemplBtn(meuns, true, "add", "refresh_cache", "rebuild_parent_path") + @component.TemplLink(meuns, "edit", "set_menu", "refresh_role_menus") + + + +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      + } +} + +templ listCss() { +} + +templ listJs(ctx context.Context) { + {{ token := mid.GetCsrfToken(ctx) }} + +} \ No newline at end of file diff --git a/internal/erpserver/templ/system/role/list_templ.go b/internal/erpserver/templ/system/role/list_templ.go new file mode 100644 index 0000000..8744608 --- /dev/null +++ b/internal/erpserver/templ/system/role/list_templ.go @@ -0,0 +1,194 @@ +// Code generated by templ - DO NOT EDIT. + +// templ: version: v0.3.898 +package role + +//lint:file-ignore SA4006 This context is only used if a nested component is present. + +import "github.com/a-h/templ" +import templruntime "github.com/a-h/templ/runtime" + +import ( + "context" + + "management/internal/erpserver/templ/base" + "management/internal/erpserver/templ/component" + "management/internal/pkg/mid" +) + +func List(ctx context.Context) templ.Component { + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { + return templ_7745c5c3_CtxErr + } + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var1 := templ.GetChildren(ctx) + if templ_7745c5c3_Var1 == nil { + templ_7745c5c3_Var1 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + templ_7745c5c3_Var2 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + meuns := mid.GetCurMenus(ctx) + templ_7745c5c3_Err = component.TemplBtn(meuns, true, "add", "refresh_cache", "rebuild_parent_path").Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, " ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = component.TemplLink(meuns, "edit", "set_menu", "refresh_role_menus").Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "
      ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) + templ_7745c5c3_Err = base.Base(ctx, listCss(), listJs(ctx)).Render(templ.WithChildren(ctx, templ_7745c5c3_Var2), templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +func listCss() templ.Component { + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { + return templ_7745c5c3_CtxErr + } + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var3 := templ.GetChildren(ctx) + if templ_7745c5c3_Var3 == nil { + templ_7745c5c3_Var3 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + return nil + }) +} + +func listJs(ctx context.Context) templ.Component { + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { + return templ_7745c5c3_CtxErr + } + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var4 := templ.GetChildren(ctx) + if templ_7745c5c3_Var4 == nil { + templ_7745c5c3_Var4 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + token := mid.GetCsrfToken(ctx) + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +var _ = templruntime.GeneratedTemplate diff --git a/internal/erpserver/wire.go b/internal/erpserver/wire.go index fbf24c5..503a77a 100644 --- a/internal/erpserver/wire.go +++ b/internal/erpserver/wire.go @@ -12,8 +12,8 @@ import ( "management/internal/erpserver/service/v1" commonService "management/internal/erpserver/service/v1/common" systemService "management/internal/erpserver/service/v1/system" + "management/internal/pkg/cache" "management/internal/pkg/config" - "management/internal/pkg/redis" "management/internal/pkg/render" "management/internal/pkg/session" @@ -70,7 +70,8 @@ var serverSet = wire.NewSet( func NewWire(*config.Config, *logger.Logger) (App, func(), error) { panic(wire.Build( repositorySet, - redis.New, + cache.ConnectRedis, + cache.NewRedisCache, session.NewSCSManager, serviceSet, render.New, diff --git a/internal/erpserver/wire_gen.go b/internal/erpserver/wire_gen.go index e41d472..9a8b0df 100644 --- a/internal/erpserver/wire_gen.go +++ b/internal/erpserver/wire_gen.go @@ -17,8 +17,8 @@ import ( "management/internal/erpserver/service/v1" "management/internal/erpserver/service/v1/common" system2 "management/internal/erpserver/service/v1/system" + "management/internal/pkg/cache" "management/internal/pkg/config" - "management/internal/pkg/redis" "management/internal/pkg/render" "management/internal/pkg/session" ) @@ -39,22 +39,24 @@ func NewWire(configConfig *config.Config, loggerLogger *logger.Logger) (App, fun configRepository := system.NewConfigRepository(repositoryRepository) loginLogRepository := system.NewLoginLogRepository(repositoryRepository) auditLogRepository := system.NewAuditLogRepository(repositoryRepository) - manager, err := session.NewSCSManager(db, configConfig) - if err != nil { - cleanup() - return App{}, nil, err - } transaction := repository.NewTransaction(repositoryRepository) - cache, cleanup2, err := redis.New(configConfig, loggerLogger) + client, cleanup2, err := cache.ConnectRedis(configConfig, loggerLogger) if err != nil { cleanup() return App{}, nil, err } - service := v1.NewService(loggerLogger, transaction, manager, cache) + manager, err := session.NewSCSManager(client, configConfig) + if err != nil { + cleanup2() + cleanup() + return App{}, nil, err + } + cacheCache := cache.NewRedisCache(client) + service := v1.NewService(loggerLogger, transaction, manager, cacheCache) + auditLogService := system2.NewAuditLogService(service, auditLogRepository) roleService := system2.NewRoleService(service, roleRepository) roleMenuService := system2.NewRoleMenuService(service, roleMenuRepository) menuService := system2.NewMenuService(service, menuRepository, roleService, roleMenuService) - auditLogService := system2.NewAuditLogService(service, auditLogRepository) renderRender, err := render.New(manager, menuService) if err != nil { cleanup2() @@ -78,7 +80,7 @@ func NewWire(configConfig *config.Config, loggerLogger *logger.Logger) (App, fun roleHandler := system3.NewRoleHandler(handlerHandler, roleService, menuService) departmentHandler := system3.NewDepartmentHandler(handlerHandler, departmentService) mux := NewHTTPServer(manager, loggerLogger, menuService, auditLogService, captchaHandler, uploadHandler, configHandler, homeHandler, userHandler, loginLogHandler, auditHandler, menuHandler, roleHandler, departmentHandler) - app := NewApp(userRepository, roleRepository, menuRepository, roleMenuRepository, departmentRepository, configRepository, loginLogRepository, auditLogRepository, mux) + app := NewApp(userRepository, roleRepository, menuRepository, roleMenuRepository, departmentRepository, configRepository, loginLogRepository, auditLogRepository, auditLogService, mux) return app, func() { cleanup2() cleanup() diff --git a/internal/pkg/cache/cache.go b/internal/pkg/cache/cache.go new file mode 100644 index 0000000..48ec929 --- /dev/null +++ b/internal/pkg/cache/cache.go @@ -0,0 +1,19 @@ +package cache + +import ( + "context" + "time" + + "github.com/redis/go-redis/v9" +) + +type Cache interface { + Encode(a any) ([]byte, error) + Set(ctx context.Context, key string, value interface{}, expiration time.Duration) error + Del(ctx context.Context, keys ...string) error + Get(ctx context.Context, key string) (string, error) + GetBytes(ctx context.Context, key string) ([]byte, error) + Scan(ctx context.Context, cursor uint64, match string, count int64) *redis.ScanCmd + Keys(ctx context.Context, pattern string) ([]string, error) + ListKeys(ctx context.Context, pattern string, pageID int, pageSize int) ([]string, int, error) +} diff --git a/internal/pkg/redis/redis.go b/internal/pkg/cache/redis.go similarity index 80% rename from internal/pkg/redis/redis.go rename to internal/pkg/cache/redis.go index 0c7c749..d4a34cb 100644 --- a/internal/pkg/redis/redis.go +++ b/internal/pkg/cache/redis.go @@ -1,4 +1,4 @@ -package redis +package cache import ( "bytes" @@ -16,28 +16,17 @@ import ( var ErrRedisKeyNotFound = errors.New("redis key not found") -type Cache interface { - Encode(a any) ([]byte, error) - Set(ctx context.Context, key string, value interface{}, expiration time.Duration) error - Del(ctx context.Context, keys ...string) error - Get(ctx context.Context, key string) (string, error) - GetBytes(ctx context.Context, key string) ([]byte, error) - Scan(ctx context.Context, cursor uint64, match string, count int64) *redis.ScanCmd - Keys(ctx context.Context, pattern string) ([]string, error) - ListKeys(ctx context.Context, pattern string, pageID int, pageSize int) ([]string, int, error) -} - -type redisCache struct { - client *redis.Client -} - -func New(conf *config.Config, log *logger.Logger) (Cache, func(), error) { +func ConnectRedis(conf *config.Config, log *logger.Logger) (*redis.Client, func(), error) { rdb := redis.NewClient(&redis.Options{ Addr: fmt.Sprintf("%s:%d", conf.Redis.Host, conf.Redis.Port), Password: conf.Redis.Password, DB: conf.Redis.DB, }) - _, err := rdb.Ping(context.Background()).Result() + + ctx, cancel := context.WithTimeout(context.Background(), time.Second*3) + defer cancel() + + _, err := rdb.Ping(ctx).Result() if err != nil { return nil, nil, err } @@ -48,9 +37,17 @@ func New(conf *config.Config, log *logger.Logger) (Cache, func(), error) { } } + return rdb, cleanup, nil +} + +type redisCache struct { + client *redis.Client +} + +func NewRedisCache(client *redis.Client) Cache { return &redisCache{ - client: rdb, - }, cleanup, nil + client: client, + } } func (r *redisCache) Encode(a any) ([]byte, error) { diff --git a/internal/pkg/mid/audit_v1.go b/internal/pkg/mid/audit_v1.go new file mode 100644 index 0000000..32ca650 --- /dev/null +++ b/internal/pkg/mid/audit_v1.go @@ -0,0 +1,58 @@ +package mid + +//import ( +// "context" +// "errors" +// "net/http" +// "time" +// +// systemmodel "management/internal/erpserver/model/system" +// v1 "management/internal/erpserver/service/v1" +// "management/internal/pkg/know" +// "management/internal/pkg/session" +// +// "github.com/drhin/logger" +// "go.uber.org/zap" +//) +// +//func Audit(sess session.Manager, auditLogService v1.AuditLogService, log *logger.Logger) func(http.Handler) http.Handler { +// return func(next http.Handler) http.Handler { +// return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { +// start := time.Now() +// +// // 提前获取用户信息(同步操作) +// user, err := sess.GetUser(r.Context(), know.StoreName) +// if err != nil { +// log.Error("获取用户会话失败", err) +// next.ServeHTTP(w, r) // 继续处理请求 +// return +// } +// +// defer func() { +// go func() { +// if user.ID == 0 { +// log.Error("用户信息为空", errors.New("scs get user is empty")) +// return +// } +// +// ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) +// defer cancel() +// +// al := systemmodel.NewAuditLog(r, user.Email, user.OS, user.Browser, start, time.Now()) +// if err := auditLogService.Create(ctx, al); err != nil { +// log.Error(err.Error(), err, +// zap.Int32("user_id", user.ID), +// zap.String("user", user.Email), +// zap.String("ip", al.Ip), +// zap.String("os", al.Os), +// zap.String("method", al.Method), +// zap.String("path", al.Url), +// ) +// } +// }() +// }() +// +// next.ServeHTTP(w, r) +// }) +// } +//} diff --git a/internal/pkg/mid/audit_v2.go b/internal/pkg/mid/audit_v2.go new file mode 100644 index 0000000..bc940c2 --- /dev/null +++ b/internal/pkg/mid/audit_v2.go @@ -0,0 +1,228 @@ +package mid + +import ( + "context" + "errors" + "net/http" + "sync" + "time" + + systemmodel "management/internal/erpserver/model/system" + v1 "management/internal/erpserver/service/v1" + "management/internal/pkg/know" + "management/internal/pkg/session" + + "github.com/drhin/logger" + "go.uber.org/zap" +) + +// AuditBuffer 审计日志缓冲器 +type AuditBuffer struct { + auditLogService v1.AuditLogService + log *logger.Logger + buffer chan *systemmodel.AuditLog + stopCh chan struct{} + wg sync.WaitGroup + batchSize int + flushInterval time.Duration +} + +// NewAuditBuffer 创建审计日志缓冲器 +func NewAuditBuffer(auditLogService v1.AuditLogService, log *logger.Logger) *AuditBuffer { + return &AuditBuffer{ + auditLogService: auditLogService, + log: log, + buffer: make(chan *systemmodel.AuditLog, 10000), // 缓冲区大小 + stopCh: make(chan struct{}), + batchSize: 50, // 批量大小 + flushInterval: 3 * time.Second, // 刷新间隔 + } +} + +// Start 启动缓冲器 +func (ab *AuditBuffer) Start() { + ab.wg.Add(1) + go ab.processBuffer() +} + +// Stop 停止缓冲器 +func (ab *AuditBuffer) Stop() { + close(ab.stopCh) + ab.wg.Wait() + close(ab.buffer) +} + +// Add 添加审计日志到缓冲区 +func (ab *AuditBuffer) Add(auditLog *systemmodel.AuditLog) { + select { + case ab.buffer <- auditLog: + // 成功添加到缓冲区 + default: + // 缓冲区满,记录警告但不阻塞 + ab.log.Warn("审计日志缓冲区已满,丢弃日志") + } +} + +// processBuffer 处理缓冲区中的日志 +func (ab *AuditBuffer) processBuffer() { + defer ab.wg.Done() + + ticker := time.NewTicker(ab.flushInterval) + defer ticker.Stop() + + batch := make([]*systemmodel.AuditLog, 0, ab.batchSize) + + flushBatch := func() { + if len(batch) == 0 { + return + } + + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + // 批量插入 + if err := ab.batchInsert(ctx, batch); err != nil { + ab.log.Error("批量插入审计日志失败", err, zap.Int("count", len(batch))) + } else { + ab.log.Debug("批量插入审计日志成功", zap.Int("count", len(batch))) + } + + // 清空批次 + batch = batch[:0] + } + + for { + select { + case <-ab.stopCh: + // 停止信号,处理剩余的日志 + for len(ab.buffer) > 0 { + select { + case auditLog := <-ab.buffer: + batch = append(batch, auditLog) + if len(batch) >= ab.batchSize { + flushBatch() + } + default: + break + } + } + flushBatch() // 处理最后一批 + return + + case <-ticker.C: + // 定时刷新 + flushBatch() + + case auditLog := <-ab.buffer: + // 收到新的审计日志 + batch = append(batch, auditLog) + if len(batch) >= ab.batchSize { + flushBatch() + } + } + } +} + +// batchInsert 批量插入数据库 +func (ab *AuditBuffer) batchInsert(ctx context.Context, auditLogs []*systemmodel.AuditLog) error { + maxRetries := 3 + for i := 0; i < maxRetries; i++ { + // 假设你的服务有批量创建方法,如果没有,需要添加 + if err := ab.auditLogService.BatchCreate(ctx, auditLogs); err != nil { + if i == maxRetries-1 { + return err + } + ab.log.Error("批量插入失败,准备重试", err, zap.Int("retry", i+1)) + time.Sleep(time.Duration(i+1) * time.Second) + continue + } + return nil + } + return nil +} + +// 全局缓冲器实例 +var globalAuditBuffer *AuditBuffer + +// InitAuditBuffer 初始化全局缓冲器 +func InitAuditBuffer(auditLogService v1.AuditLogService, log *logger.Logger) { + globalAuditBuffer = NewAuditBuffer(auditLogService, log) + globalAuditBuffer.Start() +} + +// StopAuditBuffer 停止全局缓冲器 +func StopAuditBuffer() { + if globalAuditBuffer != nil { + globalAuditBuffer.Stop() + } +} + +// Audit 优化后的中间件 +func Audit(sess session.Manager, log *logger.Logger) func(http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + start := time.Now() + + // 提前获取用户信息 + user, err := sess.GetUser(r.Context(), know.StoreName) + if err != nil { + log.Error("获取用户会话失败", err) + next.ServeHTTP(w, r) + return + } + + // 处理请求 + next.ServeHTTP(w, r) + + // 异步添加到缓冲区 + go func() { + if user.ID == 0 { + log.Error("用户信息为空", errors.New("user is empty")) + return + } + + auditLog := systemmodel.NewAuditLog(r, user.Email, user.OS, user.Browser, start, time.Now()) + + // 添加到缓冲区,不会阻塞 + if globalAuditBuffer != nil { + globalAuditBuffer.Add(auditLog) + } + }() + }) + } +} + +// 如果你的AuditLogService没有BatchCreate方法,需要添加这个接口 +// 在你的service接口中添加: +/* +type AuditLogService interface { + Create(ctx context.Context, auditLog *systemmodel.AuditLog) error + BatchCreate(ctx context.Context, auditLogs []*systemmodel.AuditLog) error + // ... 其他方法 +} +*/ + +// 以及对应的实现(PostgreSQL批量插入示例) +/* +func (s *auditLogService) BatchCreate(ctx context.Context, auditLogs []*systemmodel.AuditLog) error { + if len(auditLogs) == 0 { + return nil + } + + // 构建批量插入SQL + query := `INSERT INTO audit_logs (user_id, email, ip, os, browser, method, url, start_time, end_time, duration) VALUES ` + values := make([]interface{}, 0, len(auditLogs)*10) + + for i, log := range auditLogs { + if i > 0 { + query += ", " + } + query += "($" + strconv.Itoa(i*10+1) + ", $" + strconv.Itoa(i*10+2) + ", $" + strconv.Itoa(i*10+3) + ", $" + strconv.Itoa(i*10+4) + ", $" + strconv.Itoa(i*10+5) + ", $" + strconv.Itoa(i*10+6) + ", $" + strconv.Itoa(i*10+7) + ", $" + strconv.Itoa(i*10+8) + ", $" + strconv.Itoa(i*10+9) + ", $" + strconv.Itoa(i*10+10) + ")" + + values = append(values, log.UserID, log.Email, log.Ip, log.Os, log.Browser, log.Method, log.Url, log.StartTime, log.EndTime, log.Duration) + } + + _, err := s.db.ExecContext(ctx, query, values...) + return err +} +*/ diff --git a/internal/pkg/mid/authorize_v1.go b/internal/pkg/mid/authorize_v1.go new file mode 100644 index 0000000..27f1a7c --- /dev/null +++ b/internal/pkg/mid/authorize_v1.go @@ -0,0 +1,99 @@ +package mid + +//import ( +// "log" +// "net/http" +// "time" +// +// "management/internal/erpserver/model/dto" +// v1 "management/internal/erpserver/service/v1" +// "management/internal/pkg/know" +// "management/internal/pkg/session" +//) +// +//var publicRoutes = map[string]bool{ +// "/home.html": true, +// "/dashboard": true, +// "/system/menus": true, +// "/upload/img": true, +// "/upload/file": true, +// "/upload/multi_files": true, +// "/pear.json": true, +// "/logout": true, +//} +// +//func Authorize( +// sess session.Manager, +// menuService v1.MenuService, +//) func(http.Handler) http.Handler { +// return func(next http.Handler) http.Handler { +// return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { +// ctx := r.Context() +// path := r.URL.Path +// +// // 登陆检查 +// n := time.Now() +// user, err := sess.GetUser(ctx, know.StoreName) +// if err != nil || user.ID == 0 { +// http.Redirect(w, r, "/", http.StatusFound) +// return +// } +// +// log.Printf("scs get user: %s", time.Since(n).String()) +// +// // 公共路由放行 +// if publicRoutes[path] { +// ctx = setUser(ctx, user) +// next.ServeHTTP(w, r.WithContext(ctx)) +// return +// } +// +// n1 := time.Now() +// // 权限检查 +// menus, err := menuService.ListByRoleIDToMap(ctx, user.RoleID) +// if err != nil || !hasPermission(menus, path) { +// http.Error(w, "Forbidden", http.StatusForbidden) +// return +// } +// +// log.Printf("listByRoleIDToMap: %s", time.Since(n1).String()) +// +// n2 := time.Now() +// cur := getCurrentMenus(menus, path) +// log.Printf("getCurrentMenus: %s", time.Since(n2).String()) +// +// ctx = setUser(ctx, user) +// ctx = setCurMenus(ctx, cur) +// +// next.ServeHTTP(w, r.WithContext(ctx)) +// }) +// } +//} +// +//func hasPermission(menus map[string]*dto.OwnerMenuDto, path string) bool { +// _, ok := menus[path] +// return ok +//} +// +//func getCurrentMenus(data map[string]*dto.OwnerMenuDto, path string) []dto.OwnerMenuDto { +// var res []dto.OwnerMenuDto +// +// menu, ok := data[path] +// if !ok { +// return res +// } +// +// for _, item := range data { +// if menu.IsList { +// if item.ParentID == menu.ID || item.ID == menu.ID { +// res = append(res, *item) +// } +// } else { +// if item.ParentID == menu.ParentID { +// res = append(res, *item) +// } +// } +// } +// +// return res +//} diff --git a/internal/pkg/mid/authorize_v2.go b/internal/pkg/mid/authorize_v2.go new file mode 100644 index 0000000..fdf537c --- /dev/null +++ b/internal/pkg/mid/authorize_v2.go @@ -0,0 +1,402 @@ +package mid + +// +//import ( +// "context" +// "log" +// "net/http" +// "sync" +// "time" +// +// "management/internal/erpserver/model/dto" +// v1 "management/internal/erpserver/service/v1" +// "management/internal/pkg/know" +// "management/internal/pkg/session" +//) +// +//var publicRoutes = map[string]bool{ +// "/home.html": true, +// "/dashboard": true, +// "/system/menus": true, +// "/upload/img": true, +// "/upload/file": true, +// "/upload/multi_files": true, +// "/pear.json": true, +// "/logout": true, +//} +// +//// MenuCacheItem 菜单缓存项 +//type MenuCacheItem struct { +// Data map[string]*dto.OwnerMenuDto +// ExpireAt time.Time +// LoadTime time.Time +// Version int64 +//} +// +//// IsExpired 检查是否过期 +//func (item *MenuCacheItem) IsExpired() bool { +// return time.Now().After(item.ExpireAt) +//} +// +//// MenuCache 内存缓存管理器 +//type MenuCache struct { +// mu sync.RWMutex +// cache map[int32]*MenuCacheItem // roleID -> MenuCacheItem +// maxSize int // 最大缓存条目数 +// ttl time.Duration // 缓存TTL +// refreshTTL time.Duration // 刷新TTL (提前刷新时间) +// stats CacheStats // 缓存统计 +// cleanupTick *time.Ticker // 清理定时器 +// stopCh chan struct{} // 停止信号 +//} +// +//// CacheStats 缓存统计信息 +//type CacheStats struct { +// mu sync.RWMutex +// Hits int64 +// Misses int64 +// Evictions int64 +// RefreshCount int64 +//} +// +//// NewMenuCache 创建新的菜单缓存 +//func NewMenuCache(maxSize int, ttl time.Duration) *MenuCache { +// cache := &MenuCache{ +// cache: make(map[int32]*MenuCacheItem), +// maxSize: maxSize, +// ttl: ttl, +// refreshTTL: ttl - time.Duration(float64(ttl)*0.1), // 提前10%刷新 +// stopCh: make(chan struct{}), +// cleanupTick: time.NewTicker(time.Minute * 5), // 每5分钟清理一次 +// } +// +// // 启动后台清理协程 +// go cache.cleanup() +// +// return cache +//} +// +//// Get 获取缓存数据 +//func (mc *MenuCache) Get(roleID int32) (map[string]*dto.OwnerMenuDto, bool) { +// mc.mu.RLock() +// item, exists := mc.cache[roleID] +// mc.mu.RUnlock() +// +// if !exists { +// mc.recordMiss() +// return nil, false +// } +// +// // 检查是否过期 +// if item.IsExpired() { +// mc.recordMiss() +// // 异步删除过期项 +// go func() { +// mc.mu.Lock() +// delete(mc.cache, roleID) +// mc.mu.Unlock() +// }() +// return nil, false +// } +// +// mc.recordHit() +// return item.Data, true +//} +// +//// Set 设置缓存数据 +//func (mc *MenuCache) Set(roleID int32, data map[string]*dto.OwnerMenuDto, version int64) { +// mc.mu.Lock() +// defer mc.mu.Unlock() +// +// // 如果缓存已满,执行LRU淘汰 +// if len(mc.cache) >= mc.maxSize { +// mc.evictLRU() +// } +// +// item := &MenuCacheItem{ +// Data: data, +// ExpireAt: time.Now().Add(mc.ttl), +// LoadTime: time.Now(), +// Version: version, +// } +// +// mc.cache[roleID] = item +//} +// +//// NeedRefresh 检查是否需要提前刷新 +//func (mc *MenuCache) NeedRefresh(roleID int32) bool { +// mc.mu.RLock() +// item, exists := mc.cache[roleID] +// mc.mu.RUnlock() +// +// if !exists { +// return true +// } +// +// // 提前刷新策略:在过期前10%时间开始刷新 +// refreshTime := item.LoadTime.Add(mc.refreshTTL) +// return time.Now().After(refreshTime) +//} +// +//// evictLRU 淘汰最久未使用的缓存项 +//func (mc *MenuCache) evictLRU() { +// var oldestRoleID int32 +// var oldestTime time.Time = time.Now() +// +// for roleID, item := range mc.cache { +// if item.LoadTime.Before(oldestTime) { +// oldestTime = item.LoadTime +// oldestRoleID = roleID +// } +// } +// +// if oldestRoleID != 0 { +// delete(mc.cache, oldestRoleID) +// mc.stats.mu.Lock() +// mc.stats.Evictions++ +// mc.stats.mu.Unlock() +// } +//} +// +//// cleanup 后台清理过期缓存 +//func (mc *MenuCache) cleanup() { +// for { +// select { +// case <-mc.cleanupTick.C: +// mc.cleanupExpired() +// case <-mc.stopCh: +// mc.cleanupTick.Stop() +// return +// } +// } +//} +// +//// cleanupExpired 清理过期缓存 +//func (mc *MenuCache) cleanupExpired() { +// mc.mu.Lock() +// defer mc.mu.Unlock() +// +// now := time.Now() +// for roleID, item := range mc.cache { +// if now.After(item.ExpireAt) { +// delete(mc.cache, roleID) +// } +// } +//} +// +//// GetStats 获取缓存统计信息 +//func (mc *MenuCache) GetStats() CacheStats { +// mc.stats.mu.RLock() +// defer mc.stats.mu.RUnlock() +// return mc.stats +//} +// +//// recordHit 记录缓存命中 +//func (mc *MenuCache) recordHit() { +// mc.stats.mu.Lock() +// mc.stats.Hits++ +// mc.stats.mu.Unlock() +//} +// +//// recordMiss 记录缓存未命中 +//func (mc *MenuCache) recordMiss() { +// mc.stats.mu.Lock() +// mc.stats.Misses++ +// mc.stats.mu.Unlock() +//} +// +//// Close 关闭缓存 +//func (mc *MenuCache) Close() { +// close(mc.stopCh) +//} +// +//// CachedMenuService 带缓存的菜单服务包装器 +//type CachedMenuService struct { +// menuService v1.MenuService +// cache *MenuCache +// mu sync.RWMutex +// refreshing map[int32]bool // 正在刷新的roleID +//} +// +//// NewCachedMenuService 创建带缓存的菜单服务 +//func NewCachedMenuService(menuService v1.MenuService, cache *MenuCache) *CachedMenuService { +// return &CachedMenuService{ +// menuService: menuService, +// cache: cache, +// refreshing: make(map[int32]bool), +// } +//} +// +//// ListByRoleIDToMap 获取菜单数据(带缓存) +//func (cms *CachedMenuService) ListByRoleIDToMap(ctx context.Context, roleID int32) (map[string]*dto.OwnerMenuDto, error) { +// // 先尝试从缓存获取 +// if data, hit := cms.cache.Get(roleID); hit { +// // 检查是否需要异步刷新 +// if cms.cache.NeedRefresh(roleID) { +// go cms.asyncRefresh(ctx, roleID) +// } +// return data, nil +// } +// +// // 缓存未命中,同步获取数据 +// return cms.loadAndCache(ctx, roleID) +//} +// +//// loadAndCache 加载数据并缓存 +//func (cms *CachedMenuService) loadAndCache(ctx context.Context, roleID int32) (map[string]*dto.OwnerMenuDto, error) { +// // 防止并发重复加载 +// cms.mu.Lock() +// if cms.refreshing[roleID] { +// cms.mu.Unlock() +// // 如果正在加载,等待一小段时间后重试缓存 +// time.Sleep(time.Millisecond * 10) +// if data, hit := cms.cache.Get(roleID); hit { +// return data, nil +// } +// // 重试失败,继续执行加载逻辑 +// cms.mu.Lock() +// } +// cms.refreshing[roleID] = true +// cms.mu.Unlock() +// +// defer func() { +// cms.mu.Lock() +// delete(cms.refreshing, roleID) +// cms.mu.Unlock() +// }() +// +// // 从原始服务获取数据 +// data, err := cms.menuService.ListByRoleIDToMap(ctx, roleID) +// if err != nil { +// return nil, err +// } +// +// // 缓存数据 +// version := time.Now().UnixNano() +// cms.cache.Set(roleID, data, version) +// +// return data, nil +//} +// +//// asyncRefresh 异步刷新缓存 +//func (cms *CachedMenuService) asyncRefresh(ctx context.Context, roleID int32) { +// // 使用背景上下文,避免原请求取消影响刷新 +// bgCtx := context.Background() +// +// cms.mu.RLock() +// if cms.refreshing[roleID] { +// cms.mu.RUnlock() +// return +// } +// cms.mu.RUnlock() +// +// _, err := cms.loadAndCache(bgCtx, roleID) +// if err != nil { +// log.Printf("async refresh menu cache failed for roleID %d: %v", roleID, err) +// } +// +// cms.cache.stats.mu.Lock() +// cms.cache.stats.RefreshCount++ +// cms.cache.stats.mu.Unlock() +//} +// +//// 全局缓存实例 +//var ( +// menuCache *MenuCache +// cachedMenuSvc *CachedMenuService +// cacheInitOnce sync.Once +//) +// +//// InitMenuCache 初始化菜单缓存(在应用启动时调用) +//func InitMenuCache(menuService v1.MenuService) { +// cacheInitOnce.Do(func() { +// // 配置参数:最大1000个角色的缓存,TTL 10分钟 +// menuCache = NewMenuCache(1000, time.Minute*10) +// cachedMenuSvc = NewCachedMenuService(menuService, menuCache) +// }) +//} +// +//// GetCachedMenuService 获取缓存菜单服务实例 +//func GetCachedMenuService() *CachedMenuService { +// return cachedMenuSvc +//} +// +//// Authorize 修改后的授权中间件 +//func Authorize( +// sess session.Manager, +// menuService v1.MenuService, +//) func(http.Handler) http.Handler { +// // 初始化缓存 +// InitMenuCache(menuService) +// +// return func(next http.Handler) http.Handler { +// return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { +// ctx := r.Context() +// path := r.URL.Path +// +// // 登陆检查 +// n := time.Now() +// user, err := sess.GetUser(ctx, know.StoreName) +// if err != nil || user.ID == 0 { +// http.Redirect(w, r, "/", http.StatusFound) +// return +// } +// +// log.Printf("scs get user: %s", time.Since(n).String()) +// +// // 公共路由放行 +// if publicRoutes[path] { +// ctx = setUser(ctx, user) +// next.ServeHTTP(w, r.WithContext(ctx)) +// return +// } +// +// n1 := time.Now() +// // 权限检查 - 使用缓存服务 +// menus, err := GetCachedMenuService().ListByRoleIDToMap(ctx, user.RoleID) +// if err != nil || !hasPermission(menus, path) { +// http.Error(w, "Forbidden", http.StatusForbidden) +// return +// } +// +// log.Printf("listByRoleIDToMap (cached): %s", time.Since(n1).String()) +// +// n2 := time.Now() +// cur := getCurrentMenus(menus, path) +// log.Printf("getCurrentMenus: %s", time.Since(n2).String()) +// +// ctx = setUser(ctx, user) +// ctx = setCurMenus(ctx, cur) +// +// next.ServeHTTP(w, r.WithContext(ctx)) +// }) +// } +//} +// +//func hasPermission(menus map[string]*dto.OwnerMenuDto, path string) bool { +// _, ok := menus[path] +// return ok +//} +// +//func getCurrentMenus(data map[string]*dto.OwnerMenuDto, path string) []dto.OwnerMenuDto { +// var res []dto.OwnerMenuDto +// +// menu, ok := data[path] +// if !ok { +// return res +// } +// +// for _, item := range data { +// if menu.IsList { +// if item.ParentID == menu.ID || item.ID == menu.ID { +// res = append(res, *item) +// } +// } else { +// if item.ParentID == menu.ParentID { +// res = append(res, *item) +// } +// } +// } +// +// return res +//} diff --git a/internal/pkg/mid/authorize_v3.go b/internal/pkg/mid/authorize_v3.go new file mode 100644 index 0000000..218d9a4 --- /dev/null +++ b/internal/pkg/mid/authorize_v3.go @@ -0,0 +1,537 @@ +package mid + +//import ( +// "context" +// "encoding/json" +// "errors" +// "fmt" +// "log" +// "net/http" +// "strconv" +// "sync" +// "time" +// +// "management/internal/erpserver/model/dto" +// v1 "management/internal/erpserver/service/v1" +// "management/internal/pkg/know" +// "management/internal/pkg/session" +// +// "github.com/allegro/bigcache/v3" +//) +// +//var publicRoutes = map[string]bool{ +// "/home.html": true, +// "/dashboard": true, +// "/system/menus": true, +// "/upload/img": true, +// "/upload/file": true, +// "/upload/multi_files": true, +// "/pear.json": true, +// "/logout": true, +//} +// +//// MenuCacheEntry 菜单缓存条目 +//type MenuCacheEntry struct { +// Data map[string]*dto.OwnerMenuDto `json:"data"` +// Timestamp int64 `json:"timestamp"` +// Version int64 `json:"version"` +//} +// +//// CacheStats 缓存统计信息 +//type CacheStats struct { +// mu sync.RWMutex +// Hits int64 +// Misses int64 +// Errors int64 +// RefreshCount int64 +// AsyncRefreshHits int64 +// LastRefreshTime time.Time +//} +// +//// GetHitRate 获取命中率 +//func (cs *CacheStats) GetHitRate() float64 { +// cs.mu.RLock() +// defer cs.mu.RUnlock() +// +// total := cs.Hits + cs.Misses +// if total == 0 { +// return 0 +// } +// return float64(cs.Hits) / float64(total) * 100 +//} +// +//// MenuCacheManager BigCache菜单缓存管理器 +//type MenuCacheManager struct { +// cache *bigcache.BigCache +// refreshTTL time.Duration +// stats *CacheStats +// mu sync.RWMutex +// refreshing map[int64]bool +// stopCh chan struct{} +// monitorTicker *time.Ticker +//} +// +//// NewMenuCacheManager 创建菜单缓存管理器 +//func NewMenuCacheManager(ttl time.Duration) (*MenuCacheManager, error) { +// config := bigcache.DefaultConfig(ttl) +// +// // 生产环境优化配置 +// config.Shards = 256 // 分片数,减少锁竞争 +// config.MaxEntriesInWindow = 10000 // 窗口内最大条目数 +// config.MaxEntrySize = 1024 * 50 // 最大条目50KB +// config.HardMaxCacheSize = 512 // 最大缓存512MB +// config.StatsEnabled = true // 启用统计 +// config.Verbose = false // 关闭详细日志 +// config.CleanWindow = 5 * time.Minute // 清理窗口 +// +// ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) +// defer cancel() +// +// cache, err := bigcache.New(ctx, config) +// if err != nil { +// return nil, fmt.Errorf("failed to create BigCache: %w", err) +// } +// +// manager := &MenuCacheManager{ +// cache: cache, +// refreshTTL: time.Duration(float64(ttl) * 0.8), // 80%时间后开始刷新 +// stats: &CacheStats{}, +// refreshing: make(map[int64]bool), +// stopCh: make(chan struct{}), +// monitorTicker: time.NewTicker(time.Minute), // 每分钟监控一次 +// } +// +// // 启动监控协程 +// go manager.monitor() +// +// return manager, nil +//} +// +//// Get 获取缓存数据 +//func (mcm *MenuCacheManager) Get(roleID int32) (map[string]*dto.OwnerMenuDto, bool, bool) { +// key := mcm.makeKey(roleID) +// +// data, err := mcm.cache.Get(key) +// if err != nil { +// if !errors.Is(err, bigcache.ErrEntryNotFound) { +// mcm.recordError() +// log.Printf("BigCache get error for roleID %d: %v", roleID, err) +// } +// mcm.recordMiss() +// return nil, false, false +// } +// +// // 反序列化 +// var entry MenuCacheEntry +// if err := json.Unmarshal(data, &entry); err != nil { +// mcm.recordError() +// log.Printf("Failed to unmarshal cache entry for roleID %d: %v", roleID, err) +// mcm.recordMiss() +// return nil, false, false +// } +// +// mcm.recordHit() +// +// // 检查是否需要刷新 +// needRefresh := time.Since(time.Unix(entry.Timestamp, 0)) > mcm.refreshTTL +// +// return entry.Data, true, needRefresh +//} +// +//// Set 设置缓存数据 +//func (mcm *MenuCacheManager) Set(roleID int32, data map[string]*dto.OwnerMenuDto) error { +// key := mcm.makeKey(roleID) +// +// entry := MenuCacheEntry{ +// Data: data, +// Timestamp: time.Now().Unix(), +// Version: time.Now().UnixNano(), +// } +// +// // 序列化 +// entryData, err := json.Marshal(entry) +// if err != nil { +// mcm.recordError() +// return fmt.Errorf("failed to marshal cache entry: %w", err) +// } +// +// // 存储到BigCache +// err = mcm.cache.Set(key, entryData) +// if err != nil { +// mcm.recordError() +// return fmt.Errorf("failed to set cache: %w", err) +// } +// +// return nil +//} +// +//// Delete 删除缓存数据 +//func (mcm *MenuCacheManager) Delete(roleID int32) error { +// key := mcm.makeKey(roleID) +// err := mcm.cache.Delete(key) +// if err != nil && !errors.Is(err, bigcache.ErrEntryNotFound) { +// mcm.recordError() +// return fmt.Errorf("failed to delete cache: %w", err) +// } +// return nil +//} +// +//// makeKey 生成缓存key +//func (mcm *MenuCacheManager) makeKey(roleID int32) string { +// return "menu:role:" + strconv.Itoa(int(roleID)) +//} +// +//// GetStats 获取统计信息 +//func (mcm *MenuCacheManager) GetStats() *CacheStats { +// mcm.stats.mu.RLock() +// defer mcm.stats.mu.RUnlock() +// +// // 复制统计数据 +// stats := *mcm.stats +// return &stats +//} +// +//// GetBigCacheStats 获取BigCache原生统计 +//func (mcm *MenuCacheManager) GetBigCacheStats() bigcache.Stats { +// return mcm.cache.Stats() +//} +// +//// recordHit 记录命中 +//func (mcm *MenuCacheManager) recordHit() { +// mcm.stats.mu.Lock() +// mcm.stats.Hits++ +// mcm.stats.mu.Unlock() +//} +// +//// recordMiss 记录未命中 +//func (mcm *MenuCacheManager) recordMiss() { +// mcm.stats.mu.Lock() +// mcm.stats.Misses++ +// mcm.stats.mu.Unlock() +//} +// +//// recordError 记录错误 +//func (mcm *MenuCacheManager) recordError() { +// mcm.stats.mu.Lock() +// mcm.stats.Errors++ +// mcm.stats.mu.Unlock() +//} +// +//// recordRefresh 记录刷新 +//func (mcm *MenuCacheManager) recordRefresh() { +// mcm.stats.mu.Lock() +// mcm.stats.RefreshCount++ +// mcm.stats.LastRefreshTime = time.Now() +// mcm.stats.mu.Unlock() +//} +// +//// recordAsyncRefreshHit 记录异步刷新命中 +//func (mcm *MenuCacheManager) recordAsyncRefreshHit() { +// mcm.stats.mu.Lock() +// mcm.stats.AsyncRefreshHits++ +// mcm.stats.mu.Unlock() +//} +// +//// monitor 监控协程 +//func (mcm *MenuCacheManager) monitor() { +// for { +// select { +// case <-mcm.monitorTicker.C: +// mcm.logStats() +// case <-mcm.stopCh: +// mcm.monitorTicker.Stop() +// return +// } +// } +//} +// +//// logStats 记录统计信息 +//func (mcm *MenuCacheManager) logStats() { +// stats := mcm.GetStats() +// bigCacheStats := mcm.GetBigCacheStats() +// +// log.Printf("MenuCache Stats - Hits: %d, Misses: %d, Errors: %d, HitRate: %.2f%%, "+ +// "RefreshCount: %d, AsyncRefreshHits: %d, BigCache Hits: %d, BigCache Misses: %d", +// stats.Hits, stats.Misses, stats.Errors, stats.GetHitRate(), +// stats.RefreshCount, stats.AsyncRefreshHits, +// bigCacheStats.Hits, bigCacheStats.Misses) +//} +// +//// Close 关闭缓存管理器 +//func (mcm *MenuCacheManager) Close() error { +// close(mcm.stopCh) +// return mcm.cache.Close() +//} +// +//// CachedMenuService 带BigCache的菜单服务 +//type CachedMenuService struct { +// menuService v1.MenuService +// cache *MenuCacheManager +// mu sync.RWMutex +// refreshing map[int32]bool +//} +// +//// NewCachedMenuService 创建带缓存的菜单服务 +//func NewCachedMenuService(menuService v1.MenuService, cache *MenuCacheManager) *CachedMenuService { +// return &CachedMenuService{ +// menuService: menuService, +// cache: cache, +// refreshing: make(map[int32]bool), +// } +//} +// +//// ListByRoleIDToMap 获取菜单数据(带BigCache缓存) +//func (cms *CachedMenuService) ListByRoleIDToMap(ctx context.Context, roleID int32) (map[string]*dto.OwnerMenuDto, error) { +// // 尝试从缓存获取 +// data, hit, needRefresh := cms.cache.Get(roleID) +// if hit { +// // 如果需要刷新且当前没有在刷新中,启动异步刷新 +// if needRefresh && !cms.isRefreshing(roleID) { +// go cms.asyncRefresh(roleID) +// } +// return data, nil +// } +// +// // 缓存未命中,同步获取 +// return cms.loadAndCache(ctx, roleID) +//} +// +//// loadAndCache 加载数据并缓存 +//func (cms *CachedMenuService) loadAndCache(ctx context.Context, roleID int32) (map[string]*dto.OwnerMenuDto, error) { +// // 防止并发重复加载 +// cms.mu.Lock() +// if cms.refreshing[roleID] { +// cms.mu.Unlock() +// // 等待一小段时间后重试缓存 +// time.Sleep(time.Millisecond * 5) +// if data, hit, _ := cms.cache.Get(roleID); hit { +// return data, nil +// } +// // 重试失败,继续加载 +// cms.mu.Lock() +// } +// cms.refreshing[roleID] = true +// cms.mu.Unlock() +// +// defer func() { +// cms.mu.Lock() +// delete(cms.refreshing, roleID) +// cms.mu.Unlock() +// }() +// +// // 从原始服务获取数据 +// data, err := cms.menuService.ListByRoleIDToMap(ctx, roleID) +// if err != nil { +// return nil, err +// } +// +// // 缓存数据 +// if cacheErr := cms.cache.Set(roleID, data); cacheErr != nil { +// log.Printf("Failed to cache menu data for roleID %d: %v", roleID, cacheErr) +// // 缓存失败不影响业务逻辑,继续返回数据 +// } +// +// return data, nil +//} +// +//// asyncRefresh 异步刷新缓存 +//func (cms *CachedMenuService) asyncRefresh(roleID int32) { +// // 检查是否已在刷新中 +// if cms.isRefreshing(roleID) { +// return +// } +// +// // 使用背景上下文 +// ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) +// defer cancel() +// +// _, err := cms.loadAndCache(ctx, roleID) +// if err != nil { +// log.Printf("Async refresh menu cache failed for roleID %d: %v", roleID, err) +// } else { +// cms.cache.recordRefresh() +// cms.cache.recordAsyncRefreshHit() +// } +//} +// +//// isRefreshing 检查是否正在刷新 +//func (cms *CachedMenuService) isRefreshing(roleID int32) bool { +// cms.mu.RLock() +// refreshing := cms.refreshing[roleID] +// cms.mu.RUnlock() +// return refreshing +//} +// +//// InvalidateRole 使指定角色缓存失效 +//func (cms *CachedMenuService) InvalidateRole(roleID int32) error { +// return cms.cache.Delete(roleID) +//} +// +//// GetCacheStats 获取缓存统计 +//func (cms *CachedMenuService) GetCacheStats() *CacheStats { +// return cms.cache.GetStats() +//} +// +//// 全局实例 +//var ( +// menuCacheManager *MenuCacheManager +// cachedMenuSvc *CachedMenuService +// cacheInitOnce sync.Once +// cacheInitErr error +//) +// +//// InitMenuCache 初始化菜单缓存 +//func InitMenuCache(menuService v1.MenuService) error { +// cacheInitOnce.Do(func() { +// // 缓存TTL设置为15分钟 +// manager, err := NewMenuCacheManager(15 * time.Minute) +// if err != nil { +// cacheInitErr = fmt.Errorf("failed to initialize menu cache: %w", err) +// return +// } +// +// menuCacheManager = manager +// cachedMenuSvc = NewCachedMenuService(menuService, manager) +// +// log.Println("MenuCache initialized successfully with BigCache") +// }) +// +// return cacheInitErr +//} +// +//// GetCachedMenuService 获取缓存菜单服务 +//func GetCachedMenuService() *CachedMenuService { +// return cachedMenuSvc +//} +// +//// GetMenuCacheManager 获取缓存管理器 +//func GetMenuCacheManager() *MenuCacheManager { +// return menuCacheManager +//} +// +//// CloseMenuCache 关闭菜单缓存 +//func CloseMenuCache() error { +// if menuCacheManager != nil { +// return menuCacheManager.Close() +// } +// return nil +//} +// +//// Authorize 修改后的授权中间件 +//func Authorize( +// sess session.Manager, +// menuService v1.MenuService, +//) func(http.Handler) http.Handler { +// // 初始化缓存 +// if err := InitMenuCache(menuService); err != nil { +// log.Printf("Failed to initialize menu cache: %v", err) +// // 缓存初始化失败,降级到原始服务 +// return func(next http.Handler) http.Handler { +// return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { +// ctx := r.Context() +// path := r.URL.Path +// +// // 登陆检查 +// n := time.Now() +// user, err := sess.GetUser(ctx, know.StoreName) +// if err != nil || user.ID == 0 { +// http.Redirect(w, r, "/", http.StatusFound) +// return +// } +// log.Printf("scs get user: %s", time.Since(n).String()) +// +// // 公共路由放行 +// if publicRoutes[path] { +// ctx = setUser(ctx, user) +// next.ServeHTTP(w, r.WithContext(ctx)) +// return +// } +// +// n1 := time.Now() +// // 权限检查 - 使用原始服务 +// menus, err := menuService.ListByRoleIDToMap(ctx, user.RoleID) +// if err != nil || !hasPermission(menus, path) { +// http.Error(w, "Forbidden", http.StatusForbidden) +// return +// } +// log.Printf("listByRoleIDToMap (fallback): %s", time.Since(n1).String()) +// +// n2 := time.Now() +// cur := getCurrentMenus(menus, path) +// log.Printf("getCurrentMenus: %s", time.Since(n2).String()) +// +// ctx = setUser(ctx, user) +// ctx = setCurMenus(ctx, cur) +// next.ServeHTTP(w, r.WithContext(ctx)) +// }) +// } +// } +// +// return func(next http.Handler) http.Handler { +// return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { +// ctx := r.Context() +// path := r.URL.Path +// +// // 登陆检查 +// n := time.Now() +// user, err := sess.GetUser(ctx, know.StoreName) +// if err != nil || user.ID == 0 { +// http.Redirect(w, r, "/", http.StatusFound) +// return +// } +// log.Printf("scs get user: %s", time.Since(n).String()) +// +// // 公共路由放行 +// if publicRoutes[path] { +// ctx = setUser(ctx, user) +// next.ServeHTTP(w, r.WithContext(ctx)) +// return +// } +// +// n1 := time.Now() +// // 权限检查 - 使用BigCache缓存服务 +// menus, err := GetCachedMenuService().ListByRoleIDToMap(ctx, user.RoleID) +// if err != nil || !hasPermission(menus, path) { +// http.Error(w, "Forbidden", http.StatusForbidden) +// return +// } +// log.Printf("listByRoleIDToMap (BigCache): %s", time.Since(n1).String()) +// +// n2 := time.Now() +// cur := getCurrentMenus(menus, path) +// log.Printf("getCurrentMenus: %s", time.Since(n2).String()) +// +// ctx = setUser(ctx, user) +// ctx = setCurMenus(ctx, cur) +// next.ServeHTTP(w, r.WithContext(ctx)) +// }) +// } +//} +// +//func hasPermission(menus map[string]*dto.OwnerMenuDto, path string) bool { +// _, ok := menus[path] +// return ok +//} +// +//func getCurrentMenus(data map[string]*dto.OwnerMenuDto, path string) []dto.OwnerMenuDto { +// var res []dto.OwnerMenuDto +// +// menu, ok := data[path] +// if !ok { +// return res +// } +// +// for _, item := range data { +// if menu.IsList { +// if item.ParentID == menu.ID || item.ID == menu.ID { +// res = append(res, *item) +// } +// } else { +// if item.ParentID == menu.ParentID { +// res = append(res, *item) +// } +// } +// } +// +// return res +//} diff --git a/internal/pkg/mid/authorize_v4.go b/internal/pkg/mid/authorize_v4.go new file mode 100644 index 0000000..c64da85 --- /dev/null +++ b/internal/pkg/mid/authorize_v4.go @@ -0,0 +1,121 @@ +package mid + +import ( + "fmt" + "log" + "net/http" + "time" + + "management/internal/erpserver/model/dto" + v1 "management/internal/erpserver/service/v1" + "management/internal/pkg/know" + "management/internal/pkg/session" + + "github.com/patrickmn/go-cache" +) + +var publicRoutes = map[string]bool{ + "/home.html": true, + "/dashboard": true, + "/system/menus": true, + "/upload/img": true, + "/upload/file": true, + "/upload/multi_files": true, + "/pear.json": true, + "/logout": true, +} + +// 定义一个全局的go-cache实例 +var menuCache *cache.Cache + +func init() { + // 初始化go-cache,设置默认过期时间为5分钟,每10分钟清理一次过期项 + menuCache = cache.New(5*time.Minute, 10*time.Minute) +} + +func Authorize( + sess session.Manager, + menuService v1.MenuService, +) func(http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + path := r.URL.Path + + // 登陆检查 + user, err := sess.GetUser(ctx, know.StoreName) + if err != nil || user.ID == 0 { + http.Redirect(w, r, "/", http.StatusFound) + return + } + + // 公共路由放行 + if publicRoutes[path] { + ctx = setUser(ctx, user) + next.ServeHTTP(w, r.WithContext(ctx)) + return + } + + n1 := time.Now() + // 权限检查 + var menus map[string]*dto.OwnerMenuDto + cacheKey := fmt.Sprintf("user_menus:%d", user.RoleID) // 使用用户RoleID作为缓存key + + // 尝试从内存缓存中获取菜单数据 + if cachedMenus, found := menuCache.Get(cacheKey); found { + menus = cachedMenus.(map[string]*dto.OwnerMenuDto) + log.Printf("listByRoleIDToMap (from cache): %s", time.Since(n1).String()) + + } else { + // 内存缓存未命中,从menuService获取,并存入内存缓存 + menus, err = menuService.ListByRoleIDToMap(ctx, user.RoleID) + if err != nil { + http.Error(w, "Forbidden", http.StatusForbidden) + return + } + menuCache.Set(cacheKey, menus, cache.DefaultExpiration) // 使用默认过期时间 + log.Printf("listByRoleIDToMap (from service, then cached): %s", time.Since(n1).String()) + } + + if !hasPermission(menus, path) { + http.Error(w, "Forbidden", http.StatusForbidden) + return + } + + cur := getCurrentMenus(menus, path) + + ctx = setUser(ctx, user) + ctx = setCurMenus(ctx, cur) + + next.ServeHTTP(w, r.WithContext(ctx)) + }) + } +} + +func hasPermission(menus map[string]*dto.OwnerMenuDto, path string) bool { + _, ok := menus[path] + return ok +} + +func getCurrentMenus(data map[string]*dto.OwnerMenuDto, path string) []dto.OwnerMenuDto { + var res []dto.OwnerMenuDto + + menu, ok := data[path] + if !ok { + return res + } + + for _, item := range data { + if menu.IsList { + if item.ParentID == menu.ID || item.ID == menu.ID { + res = append(res, *item) + } + } else { + if item.ParentID == menu.ParentID { + res = append(res, *item) + } + } + } + + return res +} diff --git a/internal/pkg/mid/csrf.go b/internal/pkg/mid/csrf.go new file mode 100644 index 0000000..01d1276 --- /dev/null +++ b/internal/pkg/mid/csrf.go @@ -0,0 +1,22 @@ +package mid + +import ( + "fmt" + "net/http" + + "github.com/justinas/nosurf" +) + +func NoSurf(next http.Handler) http.Handler { + return nosurf.New(next) +} + +func NoSurfContext(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + token := nosurf.Token(r) + + ctx := setCsrfToken(r.Context(), token) + ctx = setHtmlCsrfToken(ctx, fmt.Sprintf(``, token)) + next.ServeHTTP(w, r.WithContext(ctx)) + }) +} diff --git a/internal/pkg/mid/mid.go b/internal/pkg/mid/mid.go new file mode 100644 index 0000000..c3fd099 --- /dev/null +++ b/internal/pkg/mid/mid.go @@ -0,0 +1,75 @@ +package mid + +import ( + "context" + + "management/internal/erpserver/model/dto" + + "github.com/a-h/templ" +) + +type userKey struct{} + +func setUser(ctx context.Context, usr dto.AuthorizeUser) context.Context { + return context.WithValue(ctx, userKey{}, usr) +} + +// GetUser returns the user from the context. +func GetUser(ctx context.Context) dto.AuthorizeUser { + v, ok := ctx.Value(userKey{}).(dto.AuthorizeUser) + if !ok { + return dto.AuthorizeUser{} + } + + return v +} + +type menuKey struct{} + +func setCurMenus(ctx context.Context, ms []dto.OwnerMenuDto) context.Context { + return context.WithValue(ctx, menuKey{}, ms) +} + +func GetCurMenus(ctx context.Context) []dto.OwnerMenuDto { + v, ok := ctx.Value(menuKey{}).([]dto.OwnerMenuDto) + if !ok { + return []dto.OwnerMenuDto{} + } + + return v +} + +type NoSurfToken struct { + Token string + HtmlToken string +} + +type csrfKey struct{} + +func setCsrfToken(ctx context.Context, token string) context.Context { + return context.WithValue(ctx, csrfKey{}, token) +} + +func GetCsrfToken(ctx context.Context) string { + v, ok := ctx.Value(csrfKey{}).(string) + if !ok { + return "" + } + + return v +} + +type htmlCsrfKey struct{} + +func setHtmlCsrfToken(ctx context.Context, token string) context.Context { + return context.WithValue(ctx, htmlCsrfKey{}, templ.Raw(token)) +} + +func GetHtmlCsrfToken(ctx context.Context) templ.Component { + v, ok := ctx.Value(htmlCsrfKey{}).(templ.Component) + if !ok { + return templ.Raw("") + } + + return v +} diff --git a/internal/pkg/middleware/session.go b/internal/pkg/mid/session.go similarity index 88% rename from internal/pkg/middleware/session.go rename to internal/pkg/mid/session.go index 382d2e9..c6787d4 100644 --- a/internal/pkg/middleware/session.go +++ b/internal/pkg/mid/session.go @@ -1,4 +1,4 @@ -package middleware +package mid import ( "net/http" diff --git a/internal/pkg/middleware/audit.go b/internal/pkg/middleware/audit.go deleted file mode 100644 index 0683df4..0000000 --- a/internal/pkg/middleware/audit.go +++ /dev/null @@ -1,58 +0,0 @@ -package middleware - -import ( - "context" - "errors" - "net/http" - "time" - - systemmodel "management/internal/erpserver/model/system" - v1 "management/internal/erpserver/service/v1" - "management/internal/pkg/know" - "management/internal/pkg/session" - - "github.com/drhin/logger" - "go.uber.org/zap" -) - -func Audit(sess session.Manager, auditLogService v1.AuditLogService, log *logger.Logger) func(http.Handler) http.Handler { - return func(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - start := time.Now() - - // 提前获取用户信息(同步操作) - user, err := sess.GetUser(r.Context(), know.StoreName) - if err != nil { - log.Error("获取用户会话失败", err) - next.ServeHTTP(w, r) // 继续处理请求 - return - } - - defer func() { - go func() { - if user.ID == 0 { - log.Error("用户信息为空", errors.New("scs get user is empty")) - return - } - - ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) - defer cancel() - - al := systemmodel.NewAuditLog(r, user.Email, user.OS, user.Browser, start, time.Now()) - if err := auditLogService.Create(ctx, al); err != nil { - log.Error(err.Error(), err, - zap.Int32("user_id", user.ID), - zap.String("user", user.Email), - zap.String("ip", al.Ip), - zap.String("os", al.Os), - zap.String("method", al.Method), - zap.String("path", al.Url), - ) - } - }() - }() - - next.ServeHTTP(w, r) - }) - } -} diff --git a/internal/pkg/middleware/authorize.go b/internal/pkg/middleware/authorize.go deleted file mode 100644 index 984237f..0000000 --- a/internal/pkg/middleware/authorize.go +++ /dev/null @@ -1,60 +0,0 @@ -package middleware - -import ( - "net/http" - - "management/internal/erpserver/model/dto" - v1 "management/internal/erpserver/service/v1" - "management/internal/pkg/know" - "management/internal/pkg/session" -) - -var publicRoutes = map[string]bool{ - "/home.html": true, - "/dashboard": true, - "/system/menus": true, - "/upload/img": true, - "/upload/file": true, - "/upload/multi_files": true, - "/pear.json": true, - "/logout": true, -} - -func Authorize( - sess session.Manager, - menuService v1.MenuService, -) func(http.Handler) http.Handler { - return func(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - ctx := r.Context() - path := r.URL.Path - - // 登陆检查 - user, err := sess.GetUser(ctx, know.StoreName) - if err != nil || user.ID == 0 { - http.Redirect(w, r, "/", http.StatusFound) - return - } - - // 公共路由放行 - if publicRoutes[path] { - next.ServeHTTP(w, r) - return - } - - // 权限检查 - menus, err := menuService.ListByRoleIDToMap(ctx, user.RoleID) - if err != nil || !hasPermission(menus, path) { - http.Error(w, "Forbidden", http.StatusForbidden) - return - } - - next.ServeHTTP(w, r) - }) - } -} - -func hasPermission(menus map[string]*dto.OwnerMenuDto, path string) bool { - _, ok := menus[path] - return ok -} diff --git a/internal/pkg/middleware/csrf.go b/internal/pkg/middleware/csrf.go deleted file mode 100644 index 4722fc5..0000000 --- a/internal/pkg/middleware/csrf.go +++ /dev/null @@ -1,11 +0,0 @@ -package middleware - -import ( - "net/http" - - "github.com/justinas/nosurf" -) - -func NoSurf(next http.Handler) http.Handler { - return nosurf.New(next) -} diff --git a/internal/pkg/render/html.go b/internal/pkg/render/html.go index 2da8a93..0b64675 100644 --- a/internal/pkg/render/html.go +++ b/internal/pkg/render/html.go @@ -61,7 +61,7 @@ func (r *render) setDefaultData(req *http.Request, data map[string]any) map[stri ctx := req.Context() authUser, err := r.session.GetUser(ctx, know.StoreName) - if err != nil || authUser == nil { + if err != nil || authUser.ID == 0 { data["IsAuthenticated"] = false } else { data["IsAuthenticated"] = true diff --git a/internal/pkg/session/redis.go b/internal/pkg/session/redis.go index 3ffb127..82c2072 100644 --- a/internal/pkg/session/redis.go +++ b/internal/pkg/session/redis.go @@ -1,83 +1,86 @@ package session -// import ( -// "context" -// "time" +import ( + "context" + "errors" + "time" -// "management/internal/pkg/redis" -// ) + "github.com/redis/go-redis/v9" +) -// var ( -// storePrefix = "scs:session:" -// ctx = context.Background() -// DefaultRedisStore = newRedisStore() -// ) +// 为所有 Redis 操作定义一个合理的超时时间。 +// 这个值应该根据你的服务SLA和网络状况来定,通常在50-500毫秒之间是比较合理的。 +// 最佳实践:这个值应该来自配置文件,而不是硬编码。 +const redisTimeout = 200 * time.Millisecond -// type redisStore struct{} +// RedisStore 表示一个使用 go-redis/v9 客户端的 scs.Store 实现。 +type RedisStore struct { + // 内嵌 go-redis 客户端 + client *redis.Client +} -// func newRedisStore() *redisStore { -// return &redisStore{} -// } +// NewRedisStore 是 RedisStore 的构造函数。 +func NewRedisStore(client *redis.Client) *RedisStore { + return &RedisStore{ + client: client, + } +} -// // Delete should remove the session token and corresponding data from the -// // session store. If the token does not exist then Delete should be a no-op -// // and return nil (not an error). -// func (s *redisStore) Delete(token string) error { -// return redis.Del(ctx, storePrefix+token) -// } +// Find 方法根据 session token 从 Redis 中查找 session 数据。 +// 如果 token 不存在或已过期,exists 返回 false。 +func (s *RedisStore) Find(token string) ([]byte, bool, error) { + // ✅ 最佳实践: 为数据库操作创建带超时的上下文 + ctx, cancel := context.WithTimeout(context.Background(), redisTimeout) + // ✅ 必须: 无论函数如何返回,都调用 cancel() 来释放上下文资源 + defer cancel() -// // Find should return the data for a session token from the store. If the -// // session token is not found or is expired, the found return value should -// // be false (and the err return value should be nil). Similarly, tampered -// // or malformed tokens should result in a found return value of false and a -// // nil err value. The err return value should be used for system errors only. -// func (s *redisStore) Find(token string) (b []byte, found bool, err error) { -// val, err := redis.GetBytes(ctx, storePrefix+token) -// if err != nil { -// return nil, false, err -// } else { -// return val, true, nil -// } -// } + // 使用 go-redis 的 Get 方法 + data, err := s.client.Get(ctx, token).Bytes() + if err != nil { + // 如果 key 不存在,go-redis 会返回 redis.Nil 错误 + if errors.Is(err, redis.Nil) { + return nil, false, nil + } + return nil, false, err + } -// // Commit should add the session token and data to the store, with the given -// // expiry time. If the session token already exists, then the data and -// // expiry time should be overwritten. -// func (s *redisStore) Commit(token string, b []byte, expiry time.Time) error { -// // TODO: 这边可以调整时间 -// exp, err := time.ParseInLocation(time.DateTime, time.Now().Format("2006-01-02")+" 23:59:59", time.Local) -// if err != nil { -// return err -// } + return data, true, nil +} -// t := time.Now() -// expired := exp.Sub(t) -// return redis.Set(ctx, storePrefix+token, b, expired) -// } +// Commit 方法将 session 数据和过期时间存入 Redis。 +// 如果 token 已存在,则更新其数据和过期时间。 +func (s *RedisStore) Commit(token string, b []byte, expiry time.Time) error { + // ✅ 最佳实践: 为数据库操作创建带超时的上下文 + ctx, cancel := context.WithTimeout(context.Background(), redisTimeout) + // ✅ 必须: 无论函数如何返回,都调用 cancel() 来释放上下文资源 + defer cancel() -// // All should return a map containing data for all active sessions (i.e. -// // sessions which have not expired). The map key should be the session -// // token and the map value should be the session data. If no active -// // sessions exist this should return an empty (not nil) map. -// func (s *redisStore) All() (map[string][]byte, error) { -// sessions := make(map[string][]byte) + // 计算 Redis 的 TTL (Time To Live) + // time.Until(expiry) 会计算出当前时间到 expiry 之间的时间差 + ttl := time.Until(expiry) -// iter := redis.Scan(ctx, 0, storePrefix+"*", 0).Iterator() -// for iter.Next(ctx) { -// key := iter.Val() -// token := key[len(storePrefix):] -// data, exists, err := s.Find(token) -// if err != nil { -// return nil, err -// } + // 使用 go-redis 的 Set 方法,并设置过期时间 + // 如果 expiry 时间已经过去,ttl 会是负数,Redis 会立即删除这个 key,这正是我们期望的行为。 + err := s.client.Set(ctx, token, b, ttl).Err() + if err != nil { + return err + } -// if exists { -// sessions[token] = data -// } -// } -// if err := iter.Err(); err != nil { -// return nil, err -// } + return nil +} -// return sessions, nil -// } +// Delete 方法根据 session token 从 Redis 中删除 session 数据。 +func (s *RedisStore) Delete(token string) error { + // ✅ 最佳实践: 为数据库操作创建带超时的上下文 + ctx, cancel := context.WithTimeout(context.Background(), redisTimeout) + // ✅ 必须: 无论函数如何返回,都调用 cancel() 来释放上下文资源 + defer cancel() + + // 使用 go-redis 的 Del 方法 + err := s.client.Del(ctx, token).Err() + if err != nil { + return err + } + + return nil +} diff --git a/internal/pkg/session/session.go b/internal/pkg/session/session.go index 12b645f..005ccc7 100644 --- a/internal/pkg/session/session.go +++ b/internal/pkg/session/session.go @@ -10,9 +10,8 @@ import ( "management/internal/erpserver/model/dto" "management/internal/pkg/config" - "github.com/alexedwards/scs/postgresstore" "github.com/alexedwards/scs/v2" - "gorm.io/gorm" + "github.com/redis/go-redis/v9" ) var ErrNoSession = errors.New("session user not found") @@ -20,8 +19,8 @@ var ErrNoSession = errors.New("session user not found") // Manager 抽象核心会话操作 type Manager interface { Load(next http.Handler) http.Handler - GetUser(ctx context.Context, key string) (*dto.AuthorizeUser, error) - PutUser(ctx context.Context, key string, user *dto.AuthorizeUser) error + GetUser(ctx context.Context, key string) (dto.AuthorizeUser, error) + PutUser(ctx context.Context, key string, user dto.AuthorizeUser) error RenewToken(ctx context.Context) error Destroy(ctx context.Context) error } @@ -30,7 +29,7 @@ type SCSSession struct { manager *scs.SessionManager } -func NewSCSManager(db *gorm.DB, config *config.Config) (Manager, error) { +func NewSCSManager(client *redis.Client, conf *config.Config) (Manager, error) { sessionManager := scs.New() sessionManager.Lifetime = 24 * time.Hour sessionManager.IdleTimeout = 2 * time.Hour @@ -38,21 +37,21 @@ func NewSCSManager(db *gorm.DB, config *config.Config) (Manager, error) { sessionManager.Cookie.HttpOnly = true sessionManager.Cookie.Persist = true sessionManager.Cookie.SameSite = http.SameSiteStrictMode - sessionManager.Cookie.Secure = config.App.Prod + sessionManager.Cookie.Secure = conf.App.Prod - sqlDB, err := db.DB() - if err != nil { - return nil, err - } + //sqlDB, err := db.DB() + //if err != nil { + // return nil, err + //} // postgres // github.com/alexedwards/scs/postgresstore - sessionManager.Store = postgresstore.New(sqlDB) + // sessionManager.Store = postgresstore.New(sqlDB) // pgx // github.com/alexedwards/scs/pgxstore // sessionManager.Store = pgxstore.New(pool) // redis - // sessionManager.Store = newRedisStore() + sessionManager.Store = NewRedisStore(client) return &SCSSession{manager: sessionManager}, nil } @@ -60,21 +59,21 @@ func (s *SCSSession) Load(next http.Handler) http.Handler { return s.manager.LoadAndSave(next) } -func (s *SCSSession) GetUser(ctx context.Context, key string) (*dto.AuthorizeUser, error) { +func (s *SCSSession) GetUser(ctx context.Context, key string) (dto.AuthorizeUser, error) { data, ok := s.manager.Get(ctx, key).([]byte) if !ok || len(data) == 0 { - return nil, ErrNoSession + return dto.AuthorizeUser{}, ErrNoSession } var user dto.AuthorizeUser if err := json.Unmarshal(data, &user); err != nil { - return nil, err + return dto.AuthorizeUser{}, err } - return &user, nil + return user, nil } -func (s *SCSSession) PutUser(ctx context.Context, key string, user *dto.AuthorizeUser) error { - data, err := json.Marshal(user) +func (s *SCSSession) PutUser(ctx context.Context, key string, user dto.AuthorizeUser) error { + data, err := json.Marshal(&user) if err != nil { return err } diff --git a/web/statics/admin/css/style.css b/web/statics/admin/css/style.css index 6c08c3c..d377f6b 100644 --- a/web/statics/admin/css/style.css +++ b/web/statics/admin/css/style.css @@ -183,7 +183,7 @@ xm-select .xm-body .xm-option.selected .xm-option-icon { color: var(--global-pri .own-pannel { position: relative; - border: 1px solid #eee; + /*border: 1px solid #eee;*/ border-radius: 2px; /*box-shadow: 1px 1px 4px rgb(0 0 0 / 8%);*/ background-color: #fff; diff --git a/web/templates/manage/oauth/login.tmpl b/web/templates/manage/oauth/login.tmpl index 032520b..5a43fdd 100644 --- a/web/templates/manage/oauth/login.tmpl +++ b/web/templates/manage/oauth/login.tmpl @@ -1,5 +1,5 @@ - + @@ -9,11 +9,15 @@ - + -
      +