From b48fa05d9ffb25e709e45e03fa22486a86097d3d Mon Sep 17 00:00:00 2001 From: loveuer Date: Sun, 13 Jul 2025 22:57:57 +0800 Subject: [PATCH] =?UTF-8?q?wip:=20=E7=99=BB=E5=BD=95=E5=92=8C=E8=AE=A4?= =?UTF-8?q?=E8=AF=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- go.mod | 28 +++--- go.sum | 100 ++++++++++++++++++--- internal/api/api.go | 19 +++- internal/cmd/svc.go | 13 ++- internal/handler/login.go | 22 ++++- internal/opt/opt.go | 1 + pkg/database/cache/cache.go | 98 ++++++++++++++++++++ pkg/database/cache/memory.go | 155 ++++++++++++++++++++++++++++++++ pkg/database/cache/new.go | 84 +++++++++++++++++ pkg/database/cache/new_test.go | 108 ++++++++++++++++++++++ pkg/database/cache/option.go | 55 ++++++++++++ pkg/database/cache/redis.go | 153 +++++++++++++++++++++++++++++++ pkg/database/db/db.go | 49 ++++++++++ pkg/database/db/new.go | 48 ++++++++++ pkg/database/db/new_test.go | 25 ++++++ pkg/database/db/option.go | 45 ++++++++++ pkg/middleware/logger/logger.go | 48 +++++++++- pkg/resp/err.go | 55 ++++++++++++ pkg/resp/msg.go | 33 +++++++ pkg/resp/resp.go | 104 +++++++++++++++++++++ pkg/tool/ctx.go | 44 +++++++++ pkg/tool/gin.go | 12 +++ pkg/tool/human.go | 50 +++++++++++ pkg/tool/ip.go | 59 ++++++++++++ pkg/tool/loadash.go | 76 ++++++++++++++++ pkg/tool/must.go | 53 +++++++++++ pkg/tool/password.go | 84 +++++++++++++++++ pkg/tool/password_test.go | 20 +++++ pkg/tool/random.go | 75 ++++++++++++++++ pkg/tool/string.go | 11 +++ pkg/tool/table.go | 124 +++++++++++++++++++++++++ pkg/tool/tools.go | 73 +++++++++++++++ pkg/tool/tools_test.go | 70 +++++++++++++++ 33 files changed, 1961 insertions(+), 33 deletions(-) create mode 100644 pkg/database/cache/cache.go create mode 100644 pkg/database/cache/memory.go create mode 100644 pkg/database/cache/new.go create mode 100644 pkg/database/cache/new_test.go create mode 100644 pkg/database/cache/option.go create mode 100644 pkg/database/cache/redis.go create mode 100644 pkg/database/db/db.go create mode 100644 pkg/database/db/new.go create mode 100644 pkg/database/db/new_test.go create mode 100644 pkg/database/db/option.go create mode 100644 pkg/tool/ctx.go create mode 100644 pkg/tool/gin.go create mode 100644 pkg/tool/human.go create mode 100644 pkg/tool/ip.go create mode 100644 pkg/tool/loadash.go create mode 100644 pkg/tool/must.go create mode 100644 pkg/tool/password.go create mode 100644 pkg/tool/password_test.go create mode 100644 pkg/tool/random.go create mode 100644 pkg/tool/string.go create mode 100644 pkg/tool/table.go create mode 100644 pkg/tool/tools.go create mode 100644 pkg/tool/tools_test.go diff --git a/go.mod b/go.mod index 855fe9b..2edd7a4 100644 --- a/go.mod +++ b/go.mod @@ -4,8 +4,19 @@ go 1.24.2 require ( gitea.loveuer.com/yizhisec/packages v0.0.12 + github.com/fatih/color v1.18.0 + github.com/gin-gonic/gin v1.10.1 + github.com/glebarez/sqlite v1.11.0 + github.com/go-playground/validator/v10 v10.20.0 + github.com/go-redis/redis/v8 v8.11.0 github.com/gofiber/fiber/v3 v3.0.0-beta.4 + github.com/google/uuid v1.6.0 + github.com/jedib0t/go-pretty/v6 v6.6.7 + github.com/spf13/cast v1.9.2 github.com/spf13/cobra v1.9.1 + golang.org/x/crypto v0.31.0 + gorm.io/driver/mysql v1.6.0 + gorm.io/driver/postgres v1.6.0 gorm.io/gorm v1.30.0 ) @@ -14,36 +25,32 @@ require ( github.com/andybalholm/brotli v1.1.1 // indirect github.com/bytedance/sonic v1.11.6 // indirect github.com/bytedance/sonic/loader v0.1.1 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cloudwego/base64x v0.1.4 // indirect github.com/cloudwego/iasm v0.2.0 // indirect + github.com/dgraph-io/ristretto/v2 v2.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/fatih/color v1.18.0 // indirect github.com/fxamacker/cbor/v2 v2.7.0 // indirect github.com/gabriel-vasile/mimetype v1.4.3 // indirect github.com/gin-contrib/sse v0.1.0 // indirect - github.com/gin-gonic/gin v1.10.1 // indirect github.com/glebarez/go-sqlite v1.21.2 // indirect - github.com/glebarez/sqlite v1.11.0 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect - github.com/go-playground/validator/v10 v10.20.0 // indirect github.com/go-sql-driver/mysql v1.8.1 // indirect github.com/goccy/go-json v0.10.2 // indirect github.com/gofiber/schema v1.2.0 // indirect github.com/gofiber/utils/v2 v2.0.0-beta.7 // indirect - github.com/google/uuid v1.6.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/jackc/pgx/v5 v5.6.0 // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect - github.com/jedib0t/go-pretty/v6 v6.6.7 // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/compress v1.17.11 // indirect github.com/klauspost/cpuid/v2 v2.2.7 // indirect - github.com/kr/text v0.2.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect @@ -62,18 +69,13 @@ require ( github.com/valyala/fasthttp v1.58.0 // indirect github.com/valyala/tcplisten v1.0.0 // indirect github.com/x448/float16 v0.8.4 // indirect - go.uber.org/multierr v1.11.0 // indirect - go.uber.org/zap v1.27.0 // indirect golang.org/x/arch v0.8.0 // indirect - golang.org/x/crypto v0.31.0 // indirect golang.org/x/net v0.31.0 // indirect golang.org/x/sync v0.11.0 // indirect - golang.org/x/sys v0.30.0 // indirect + golang.org/x/sys v0.31.0 // indirect golang.org/x/text v0.22.0 // indirect google.golang.org/protobuf v1.34.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - gorm.io/driver/mysql v1.6.0 // indirect - gorm.io/driver/postgres v1.6.0 // indirect modernc.org/libc v1.22.5 // indirect modernc.org/mathutil v1.5.0 // indirect modernc.org/memory v1.5.0 // indirect diff --git a/go.sum b/go.sum index 340d7f5..b85c74c 100644 --- a/go.sum +++ b/go.sum @@ -8,19 +8,33 @@ github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4= github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM= github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= +github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY= +github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +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.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y= github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= 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/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgraph-io/ristretto/v2 v2.2.0 h1:bkY3XzJcXoMuELV8F+vS8kzNgicwQFAaGINAEJdWGOM= +github.com/dgraph-io/ristretto/v2 v2.2.0/go.mod h1:RZrm63UmcBAaYWC1DotLYBmTvgkrs0+XhBd7Npn7/zI= +github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da h1:aIftn67I1fkbMa512G+w+Pxci9hJPB8oMnkcP3iZF38= +github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= +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/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/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= +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.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= @@ -41,6 +55,8 @@ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJn github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8= github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= +github.com/go-redis/redis/v8 v8.11.0 h1:O1Td0mQ8UFChQ3N9zFQqo6kTU2cJ+/it88gDB+zg0wo= +github.com/go-redis/redis/v8 v8.11.0/go.mod h1:DLomh7y2e3ggQXQLd1YgmvIfecPJoFl7WU5SOQ/r06M= github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= @@ -51,13 +67,25 @@ github.com/gofiber/schema v1.2.0 h1:j+ZRrNnUa/0ZuWrn/6kAtAufEr4jCJ+JuTURAMxNSZg= github.com/gofiber/schema v1.2.0/go.mod h1:YYwj01w3hVfaNjhtJzaqetymL56VW642YS3qZPhuE6c= github.com/gofiber/utils/v2 v2.0.0-beta.7 h1:NnHFrRHvhrufPABdWajcKZejz9HnCWmT/asoxRsiEbQ= github.com/gofiber/utils/v2 v2.0.0-beta.7/go.mod h1:J/M03s+HMdZdvhAeyh76xT72IfVqBzuz/OJkrMa7cwU= -github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= -github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/pprof v0.0.0-20240227163752-401108e1b7e7 h1:y3N7Bm7Y9/CtpiVkw/ZWj6lSlDF3F74SfKwfTCer72Q= github.com/google/pprof v0.0.0-20240227163752-401108e1b7e7/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= @@ -82,8 +110,8 @@ github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa02 github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM= github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= -github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= -github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +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= @@ -100,6 +128,16 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/nxadm/tail v1.4.4 h1:DQuhQpB1tVlglWS2hLQ5OV6B5r8aGxSrPc5Qo6uTN78= +github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= +github.com/onsi/ginkgo v1.15.0 h1:1V1NfVQR87RtWAgp1lv9JZJ5Jap+XFGKPi00andXGi4= +github.com/onsi/ginkgo v1.15.0/go.mod h1:hF8qUzuuC8DJGygJH3726JnCZX4MYbRB8yFfISqnKUg= +github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= +github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= +github.com/onsi/gomega v1.10.5 h1:7n6FEkpFmfCoo2t+YYqXH0evK+a9ICQz0xcAy9dYcaQ= +github.com/onsi/gomega v1.10.5/go.mod h1:gza4q3jKQJijlu05nKWRCW/GavJumGt8aNRxWg7mt48= github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c h1:dAMKvw0MlJT1GshSTtih8C2gDs04w8dReiOGXrGLNoY= @@ -115,6 +153,8 @@ github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUc github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/spf13/cast v1.9.2 h1:SsGfm7M8QOFtEzumm7UZrZdLLquNdzFYfIbEXntcFbE= +github.com/spf13/cast v1.9.2/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo= github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= @@ -148,33 +188,73 @@ github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= -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= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc= golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo= golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= +golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/api/api.go b/internal/api/api.go index e7a5850..5171518 100644 --- a/internal/api/api.go +++ b/internal/api/api.go @@ -2,20 +2,33 @@ package api import ( "context" + "github.com/go-playground/validator/v10" "loveuer/utodo/internal/handler" "loveuer/utodo/internal/opt" g_handler "loveuer/utodo/pkg/handler" + "loveuer/utodo/pkg/middleware/logger" "loveuer/utodo/pkg/middleware/trace" "github.com/gofiber/fiber/v3" - l3 "github.com/gofiber/fiber/v3/middleware/logger" r3 "github.com/gofiber/fiber/v3/middleware/recover" ) +type structValidator struct { + validate *validator.Validate +} + +func (v *structValidator) Validate(out any) error { + return v.validate.Struct(out) +} + func New(ctx context.Context) *fiber.App { - app := fiber.New() + app := fiber.New(fiber.Config{ + BodyLimit: 5 * 1024 * 1024, + StructValidator: &structValidator{validate: validator.New()}, + }) app.Use(trace.New()) - app.Use(l3.New()) + // app.Use(l3.New()) + app.Use(logger.New()) app.Use(r3.New()) app.Get("/healthz", g_handler.Healthz(opt.Cfg.Name, opt.Cfg.Version)) diff --git a/internal/cmd/svc.go b/internal/cmd/svc.go index 235a820..9d6e426 100644 --- a/internal/cmd/svc.go +++ b/internal/cmd/svc.go @@ -6,9 +6,10 @@ import ( "loveuer/utodo/internal/model" "loveuer/utodo/internal/opt" g_api "loveuer/utodo/pkg/api" + "loveuer/utodo/pkg/database/cache" + "loveuer/utodo/pkg/tool" "gitea.loveuer.com/yizhisec/packages/database/db" - "gitea.loveuer.com/yizhisec/packages/tool" "github.com/spf13/cobra" ) @@ -21,6 +22,7 @@ func svcCmd() *cobra.Command { cmd.Flags().StringVar(&opt.Cfg.Svc.Address, "address", ":9119", "address to listen on") cmd.Flags().StringVar(&opt.Cfg.Svc.DBFile, "db", "/data/data.db", "database file") + cmd.Flags().StringVar(&opt.Cfg.Svc.Cache, "cache", "", "cache uri: empty for memory cache; for redis(rdp://user:password@host:port)") return cmd } @@ -31,7 +33,14 @@ func svcRun(cmd *cobra.Command, args []string) error { stopApi func(context.Context) error ) - if err = db.Init(db.WithSqlite(opt.Cfg.Svc.DBFile)); err != nil { + if err = db.Init(db.WithCtx(cmd.Context()), db.WithSqlite(opt.Cfg.Svc.DBFile)); err != nil { + return err + } + + if err = cache.Init( + cache.WithCtx(cmd.Context()), + tool.If(opt.Cfg.Svc.Cache == "", cache.WithMemory(), cache.WithRedisURI(opt.Cfg.Svc.Cache)), + ); err != nil { return err } diff --git a/internal/handler/login.go b/internal/handler/login.go index 43f4ba9..427ea1a 100644 --- a/internal/handler/login.go +++ b/internal/handler/login.go @@ -1,14 +1,30 @@ package handler import ( - "loveuer/utodo/pkg/logger" + "loveuer/utodo/pkg/resp" "github.com/gofiber/fiber/v3" ) func Login() fiber.Handler { return func(c fiber.Ctx) error { - logger.InfoCtx(c.Context(), "login") - return c.SendString("Hello, World!") + type Req struct { + Username string `json:"username"` + Phone string `json:"phone"` + Email string `json:"email"` + Password string `json:"password" validate:"required,min=8"` + } + + var ( + err error + req Req + ) + + if err = c.Bind().JSON(&req); err != nil { + return resp.R400(c, "", nil, err.Error()) + } + + // TODO: 完成登录 + return nil } } diff --git a/internal/opt/opt.go b/internal/opt/opt.go index 2c88ce2..a7a9899 100644 --- a/internal/opt/opt.go +++ b/internal/opt/opt.go @@ -7,6 +7,7 @@ type config struct { Svc struct { Address string DBFile string + Cache string } } diff --git a/pkg/database/cache/cache.go b/pkg/database/cache/cache.go new file mode 100644 index 0000000..1ff7bf4 --- /dev/null +++ b/pkg/database/cache/cache.go @@ -0,0 +1,98 @@ +package cache + +import ( + "context" + "encoding/json" + "errors" + "sync" + "time" +) + +type encoded_value interface { + MarshalBinary() ([]byte, error) +} + +type decoded_value interface { + UnmarshalBinary(bs []byte) error +} + +type Scanner interface { + Scan(model any) error +} + +type scan struct { + err error + bs []byte +} + +func newScan(bs []byte, err error) *scan { + return &scan{bs: bs, err: err} +} + +func (s *scan) Scan(model any) error { + if s.err != nil { + return s.err + } + + return unmarshaler(s.bs, model) +} + +type Cache interface { + Get(ctx context.Context, key string) ([]byte, error) + Gets(ctx context.Context, keys ...string) ([][]byte, error) + GetScan(ctx context.Context, key string) Scanner + GetEx(ctx context.Context, key string, duration time.Duration) ([]byte, error) + GetExScan(ctx context.Context, key string, duration time.Duration) Scanner + // Set value 会被序列化, 优先使用 MarshalBinary 方法, 没有则执行 json.Marshal + Set(ctx context.Context, key string, value any) error + Sets(ctx context.Context, vm map[string]any) error + // SetEx value 会被序列化, 优先使用 MarshalBinary 方法, 没有则执行 json.Marshal + SetEx(ctx context.Context, key string, value any, duration time.Duration) error + Del(ctx context.Context, keys ...string) error + GetDel(ctx context.Context, key string) ([]byte, error) + GetDelScan(ctx context.Context, key string) Scanner + Close() + Client() any +} + +var ( + lock = &sync.Mutex{} + marshaler func(data any) ([]byte, error) = json.Marshal + unmarshaler func(data []byte, model any) error = json.Unmarshal + ErrorKeyNotFound = errors.New("key not found") + ErrorStoreFailed = errors.New("store failed") + Default Cache +) + +func handleValue(value any) ([]byte, error) { + var ( + bs []byte + err error + ) + + switch val := value.(type) { + case []byte: + return val, nil + } + + if imp, ok := value.(encoded_value); ok { + bs, err = imp.MarshalBinary() + } else { + bs, err = marshaler(value) + } + + return bs, err +} + +func SetMarshaler(fn func(data any) ([]byte, error)) { + lock.Lock() + defer lock.Unlock() + + marshaler = fn +} + +func SetUnmarshaler(fn func(data []byte, model any) error) { + lock.Lock() + defer lock.Unlock() + unmarshaler = fn +} diff --git a/pkg/database/cache/memory.go b/pkg/database/cache/memory.go new file mode 100644 index 0000000..7200e17 --- /dev/null +++ b/pkg/database/cache/memory.go @@ -0,0 +1,155 @@ +package cache + +import ( + "context" + "errors" + "time" + + "github.com/dgraph-io/ristretto/v2" +) + +var _ Cache = (*_mem)(nil) + +type _mem struct { + ctx context.Context + cache *ristretto.Cache[string, []byte] +} + +func newMemory(ctx context.Context, ins *ristretto.Cache[string, []byte]) Cache { + return &_mem{ + ctx: ctx, + cache: ins, + } +} + +func (m *_mem) Client() any { + return m.cache +} + +func (c *_mem) Close() { + c.cache.Close() +} + +func (c *_mem) Del(ctx context.Context, keys ...string) error { + for _, key := range keys { + c.cache.Del(key) + } + + return nil +} + +func (c *_mem) Get(ctx context.Context, key string) ([]byte, error) { + val, ok := c.cache.Get(key) + if !ok { + return val, ErrorKeyNotFound + } + + return val, nil +} + +func (c *_mem) GetDel(ctx context.Context, key string) ([]byte, error) { + val, err := c.Get(ctx, key) + if err != nil { + return val, err + } + + c.cache.Del(key) + + return val, err +} + +func (c *_mem) GetDelScan(ctx context.Context, key string) Scanner { + val, err := c.GetDel(ctx, key) + return newScan(val, err) +} + +func (c *_mem) GetEx(ctx context.Context, key string, duration time.Duration) ([]byte, error) { + val, err := c.Get(ctx, key) + if err != nil { + return val, err + } + + c.cache.SetWithTTL(key, val, 1, duration) + + return val, err +} + +func (m *_mem) GetExScan(ctx context.Context, key string, duration time.Duration) Scanner { + val, err := m.GetEx(ctx, key, duration) + return newScan(val, err) +} + +func (m *_mem) GetScan(ctx context.Context, key string) Scanner { + val, err := m.Get(ctx, key) + return newScan(val, err) +} + +func (m *_mem) Gets(ctx context.Context, keys ...string) ([][]byte, error) { + vals := make([][]byte, 0, len(keys)) + + for _, key := range keys { + val, err := m.Get(ctx, key) + if err != nil { + if errors.Is(err, ErrorKeyNotFound) { + continue + } + + return vals, err + } + + vals = append(vals, val) + } + + if len(vals) != len(keys) { + return vals, ErrorKeyNotFound + } + + return vals, nil +} + +func (m *_mem) Set(ctx context.Context, key string, value any) error { + val, err := handleValue(value) + if err != nil { + return err + } + + if ok := m.cache.Set(key, val, 1); !ok { + return ErrorStoreFailed + } + + m.cache.Wait() + + return nil +} + +func (m *_mem) SetEx(ctx context.Context, key string, value any, duration time.Duration) error { + val, err := handleValue(value) + if err != nil { + return err + } + + if ok := m.cache.SetWithTTL(key, val, 1, duration); !ok { + return ErrorStoreFailed + } + + m.cache.Wait() + + return nil +} + +func (m *_mem) Sets(ctx context.Context, vm map[string]any) error { + for key, value := range vm { + val, err := handleValue(value) + if err != nil { + return err + } + + if ok := m.cache.Set(key, val, 1); !ok { + return ErrorStoreFailed + } + } + + m.cache.Wait() + + return nil +} diff --git a/pkg/database/cache/new.go b/pkg/database/cache/new.go new file mode 100644 index 0000000..890155b --- /dev/null +++ b/pkg/database/cache/new.go @@ -0,0 +1,84 @@ +package cache + +import ( + "context" + "fmt" + "net/url" + + "gitea.loveuer.com/yizhisec/packages/tool" + "github.com/dgraph-io/ristretto/v2" + "github.com/go-redis/redis/v8" +) + +func New(opts ...Option) (Cache, error) { + var ( + err error + cfg = &option{ + ctx: context.Background(), + } + ) + + for _, opt := range opts { + opt(cfg) + } + + if cfg.redis != nil { + var ( + ins *url.URL + client *redis.Client + ) + + if ins, err = url.Parse(*cfg.redis); err != nil { + return nil, err + } + + username := ins.User.Username() + password, _ := ins.User.Password() + + client = redis.NewClient(&redis.Options{ + Addr: ins.Host, + Username: username, + Password: password, + }) + + if err = client.Ping(tool.CtxTimeout(cfg.ctx, 5)).Err(); err != nil { + return nil, err + } + + return newRedis(cfg.ctx, client), nil + } + + if cfg.memory { + var ( + ins *ristretto.Cache[string, []byte] + ) + + if ins, err = ristretto.NewCache(&ristretto.Config[string, []byte]{ + NumCounters: 1e7, // number of keys to track frequency of (10M). + MaxCost: 1 << 30, // maximum cost of cache (1GB). + BufferItems: 64, // number of keys per Get buffer. + }); err != nil { + return nil, err + } + + return newMemory(cfg.ctx, ins), nil + } + + return nil, fmt.Errorf("invalid cache option") +} + +func Init(opts ...Option) (err error) { + opt := &option{} + + for _, optFn := range opts { + optFn(opt) + } + + if opt.memory { + Default, err = New(opts...) + return err + } + + Default, err = New(opts...) + return err +} diff --git a/pkg/database/cache/new_test.go b/pkg/database/cache/new_test.go new file mode 100644 index 0000000..e7c7267 --- /dev/null +++ b/pkg/database/cache/new_test.go @@ -0,0 +1,108 @@ +package cache + +import ( + "testing" +) + +func TestNew(t *testing.T) { + /* if err := Init(WithRedis("127.0.0.1", 6379, "", "MyPassw0rd")); err != nil { + t.Fatal(err) + } + + type User struct { + Name string `json:"name"` + Age int `json:"age"` + } + + if err := Default.Set(t.Context(), "zyp:haha", &User{ + Name: "cache", + Age: 18, + }); err != nil { + t.Fatal(err) + } + + s := Default.GetDelScan(t.Context(), "zyp:haha") + u := new(User) + + if err := s.Scan(u); err != nil { + t.Fatal(err) + } + + t.Logf("%#v", *u) + + if err := Default.SetEx(t.Context(), "zyp:haha", &User{ + Name: "redis", + Age: 2, + }, time.Hour); err != nil { + t.Fatal(err) + }*/ +} + +func TestNoAuth(t *testing.T) { + //if err := Init(WithRedis("10.125.1.28", 6379, "", "")); err != nil { + // t.Fatal(err) + //} + // + //type User struct { + // Name string `json:"name"` + // Age int `json:"age"` + //} + // + //if err := Default.Set(t.Context(), "zyp:haha", &User{ + // Name: "cache", + // Age: 18, + //}); err != nil { + // t.Fatal(err) + //} + // + //s := Default.GetDelScan(t.Context(), "zyp:haha") + //u := new(User) + // + //if err := s.Scan(u); err != nil { + // t.Fatal(err) + //} + // + //t.Logf("%#v", *u) + // + //if err := Default.SetEx(t.Context(), "zyp:haha", &User{ + // Name: "redis", + // Age: 2, + //}, time.Hour); err != nil { + // t.Fatal(err) + //} +} + +func TestMemoryDefault(t *testing.T) { + if err := Init(WithMemory()); err != nil { + t.Fatal("init err:", err) + } + + if err := Default.Set(t.Context(), "123", "123"); err != nil { + t.Fatal("set err:", err) + } + + val, err := Default.Get(t.Context(), "123") + if err != nil { + t.Fatal("get err:", err) + } + + t.Logf("%s", val) +} + +func TestMemoryNew(t *testing.T) { + client, err := New(WithMemory()) + if err != nil { + t.Fatal("init err:", err) + } + + if err := client.Set(t.Context(), "123", "123"); err != nil { + t.Fatal("set err:", err) + } + + val, err := client.Get(t.Context(), "123") + if err != nil { + t.Fatal("get err:", err) + } + + t.Logf("%s", val) +} diff --git a/pkg/database/cache/option.go b/pkg/database/cache/option.go new file mode 100644 index 0000000..989de21 --- /dev/null +++ b/pkg/database/cache/option.go @@ -0,0 +1,55 @@ +package cache + +import ( + "context" + "fmt" + "net/url" +) + +type option struct { + ctx context.Context + redis *string + memory bool +} + +type Option func(*option) + +func WithCtx(ctx context.Context) Option { + return func(c *option) { + if ctx != nil { + c.ctx = ctx + } + } +} + +func WithRedis(host string, port int, username, password string) Option { + return func(c *option) { + uri := fmt.Sprintf("redis://%s:%d", host, port) + if username != "" || password != "" { + uri = fmt.Sprintf("redis://%s:%s@%s:%d", username, password, host, port) + } + + c.redis = &uri + } +} + +func WithRedisURI(uri string) Option { + return func(c *option) { + ins, err := url.Parse(uri) + if err != nil { + return + } + + if ins.Scheme != "redis" { + return + } + + c.redis = &uri + } +} + +func WithMemory() Option { + return func(c *option) { + c.memory = true + } +} diff --git a/pkg/database/cache/redis.go b/pkg/database/cache/redis.go new file mode 100644 index 0000000..6af0e1e --- /dev/null +++ b/pkg/database/cache/redis.go @@ -0,0 +1,153 @@ +package cache + +import ( + "context" + "errors" + "sync" + "time" + + "gitea.loveuer.com/yizhisec/packages/tool" + "github.com/go-redis/redis/v8" + "github.com/spf13/cast" +) + +var _ Cache = (*_redis)(nil) + +type _redis struct { + sync.Mutex + ctx context.Context + client *redis.Client +} + +func (r *_redis) Client() any { + return r.client +} + +func newRedis(ctx context.Context, client *redis.Client) Cache { + r := &_redis{ctx: ctx, client: client} + + go func() { + <-r.ctx.Done() + if client != nil { + r.Close() + } + }() + + return r +} + +func (r *_redis) GetDel(ctx context.Context, key string) ([]byte, error) { + s, err := r.client.GetDel(ctx, key).Result() + if err != nil { + if errors.Is(err, redis.Nil) { + return nil, ErrorKeyNotFound + } + + return nil, err + } + + return tool.StringToBytes(s), nil +} + +func (r *_redis) GetDelScan(ctx context.Context, key string) Scanner { + bs, err := r.GetDel(ctx, key) + return newScan(bs, err) +} + +func (r *_redis) Get(ctx context.Context, key string) ([]byte, error) { + result, err := r.client.Get(ctx, key).Result() + if err != nil { + if errors.Is(err, redis.Nil) { + return nil, ErrorKeyNotFound + } + + return nil, err + } + + return tool.StringToBytes(result), nil +} + +func (r *_redis) Gets(ctx context.Context, keys ...string) ([][]byte, error) { + result, err := r.client.MGet(ctx, keys...).Result() + if err != nil { + if errors.Is(err, redis.Nil) { + return nil, ErrorKeyNotFound + } + + return nil, err + } + + return tool.Map( + result, + func(item any, index int) []byte { + return tool.StringToBytes(cast.ToString(item)) + }, + ), nil +} + +func (r *_redis) GetScan(ctx context.Context, key string) Scanner { + return newScan(r.Get(ctx, key)) +} + +func (r *_redis) GetEx(ctx context.Context, key string, duration time.Duration) ([]byte, error) { + result, err := r.client.GetEx(ctx, key, duration).Result() + if err != nil { + if errors.Is(err, redis.Nil) { + return nil, ErrorKeyNotFound + } + + return nil, err + } + + return tool.StringToBytes(result), nil +} + +func (r *_redis) GetExScan(ctx context.Context, key string, duration time.Duration) Scanner { + return newScan(r.GetEx(ctx, key, duration)) +} + +func (r *_redis) Set(ctx context.Context, key string, value any) error { + bs, err := handleValue(value) + if err != nil { + return err + } + + _, err = r.client.Set(ctx, key, bs, redis.KeepTTL).Result() + return err +} + +func (r *_redis) Sets(ctx context.Context, values map[string]any) error { + vm := make(map[string]any) + for k, v := range values { + bs, err := handleValue(v) + if err != nil { + return err + } + + vm[k] = bs + } + + return r.client.MSet(ctx, vm).Err() +} + +func (r *_redis) SetEx(ctx context.Context, key string, value any, duration time.Duration) error { + bs, err := handleValue(value) + if err != nil { + return err + } + + _, err = r.client.SetEX(ctx, key, bs, duration).Result() + + return err +} + +func (r *_redis) Del(ctx context.Context, keys ...string) error { + return r.client.Del(ctx, keys...).Err() +} + +func (r *_redis) Close() { + r.Lock() + defer r.Unlock() + _ = r.client.Close() + r.client = nil +} diff --git a/pkg/database/db/db.go b/pkg/database/db/db.go new file mode 100644 index 0000000..73adcb3 --- /dev/null +++ b/pkg/database/db/db.go @@ -0,0 +1,49 @@ +package db + +import ( + "context" + "gorm.io/gorm" +) + +type Config struct { + Debug bool + DryRun bool +} + +type DB interface { + Session(ctx context.Context, configs ...Config) *gorm.DB +} + +type db struct { + tx *gorm.DB +} + +var ( + Default DB +) + +func (db *db) Session(ctx context.Context, configs ...Config) *gorm.DB { + var ( + sc = &gorm.Session{Context: ctx} + session *gorm.DB + ) + + if len(configs) == 0 { + session = db.tx.Session(sc) + return session + } + + cfg := configs[0] + + if cfg.DryRun { + sc.DryRun = true + } + + session = db.tx.Session(sc) + + if cfg.Debug { + session = session.Debug() + } + + return session +} diff --git a/pkg/database/db/new.go b/pkg/database/db/new.go new file mode 100644 index 0000000..8e005ee --- /dev/null +++ b/pkg/database/db/new.go @@ -0,0 +1,48 @@ +package db + +import ( + "github.com/glebarez/sqlite" + "gorm.io/driver/mysql" + "gorm.io/driver/postgres" + "gorm.io/gorm" +) + +var defaultSqlite = "data.db" + +func New(opts ...OptionFn) (DB, error) { + var ( + err error + conf = &config{ + sqlite: &defaultSqlite, + } + tx *gorm.DB + ) + + for _, opt := range opts { + opt(conf) + } + + if conf.mysql != nil { + tx, err = gorm.Open(mysql.Open(*conf.mysql)) + goto CHECK + } + + if conf.pg != nil { + tx, err = gorm.Open(postgres.Open(*conf.pg)) + goto CHECK + } + + tx, err = gorm.Open(sqlite.Open(*conf.sqlite)) + +CHECK: + if err != nil { + return nil, err + } + + return &db{tx: tx}, nil +} + +func Init(opts ...OptionFn) (err error) { + Default, err = New(opts...) + return err +} diff --git a/pkg/database/db/new_test.go b/pkg/database/db/new_test.go new file mode 100644 index 0000000..d7ba95b --- /dev/null +++ b/pkg/database/db/new_test.go @@ -0,0 +1,25 @@ +package db + +import ( + "testing" +) + +func TestNew(t *testing.T) { + //mdb, err := New(WithMysql("127.0.0.1", 3306, "root", "MyPassw0rd", "mydb")) + //if err != nil { + // t.Fatal(err) + //} + // + //type User struct { + // Id uint64 `gorm:"primaryKey"` + // Username string `gorm:"unique"` + //} + // + //if err = mdb.Session(t.Context()).AutoMigrate(&User{}); err != nil { + // t.Fatal(err) + //} + // + //if err = mdb.Session(t.Context()).Create(&User{Username: "zyp"}).Error; err != nil { + // t.Fatal(err) + //} +} diff --git a/pkg/database/db/option.go b/pkg/database/db/option.go new file mode 100644 index 0000000..4faf60b --- /dev/null +++ b/pkg/database/db/option.go @@ -0,0 +1,45 @@ +package db + +import ( + "context" + "fmt" +) + +type config struct { + ctx context.Context + mysql *string + pg *string + sqlite *string +} + +type OptionFn func(*config) + +func WithCtx(ctx context.Context) OptionFn { + return func(c *config) { + if ctx != nil { + c.ctx = ctx + } + } +} + +func WithMysql(host string, port int, user string, password string, database string) OptionFn { + return func(c *config) { + dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=True&loc=Local", user, password, host, port, database) + c.mysql = &dsn + } +} + +func WithPg(host string, port int, user string, password string, database string) OptionFn { + return func(c *config) { + dsn := fmt.Sprintf("host=%s user=%s password=%s dbname=%s port=%d sslmode=disable TimeZone=Asia/Shanghai", host, user, password, database, port) + c.pg = &dsn + } +} + +func WithSqlite(path string) OptionFn { + return func(c *config) { + if path != "" { + c.sqlite = &path + } + } +} diff --git a/pkg/middleware/logger/logger.go b/pkg/middleware/logger/logger.go index c0df11d..9345a90 100644 --- a/pkg/middleware/logger/logger.go +++ b/pkg/middleware/logger/logger.go @@ -1,9 +1,53 @@ package logger -import "github.com/gofiber/fiber/v3" +import ( + "fmt" + "github.com/gofiber/fiber/v3" + "github.com/spf13/cast" + "loveuer/utodo/pkg/logger" + "strconv" + "strings" + "sync" + "time" +) func New() fiber.Handler { + pool := sync.Pool{ + New: func() any { + return &strings.Builder{} + }, + } + return func(c fiber.Ctx) error { - return c.Next() + start := time.Now() + err := c.Next() + + duration := time.Since(start) + method := c.Method() + path := c.Path() + status := c.Response().StatusCode() + traceId := c.Context().Value(logger.CtxKey) + + buf := pool.Get().(*strings.Builder) + defer pool.Put(buf) + + buf.Reset() + + buf.WriteString("API | ") + buf.WriteString(start.Format("2006-01-02T15:04:05")) + buf.WriteString(" | ") + buf.WriteString(method) + buf.WriteString(" | ") + buf.WriteString(path) + buf.WriteString(" | ") + buf.WriteString(duration.String()) + buf.WriteString(" | ") + buf.WriteString(strconv.Itoa(status)) + buf.WriteString(" | ") + buf.WriteString(cast.ToString(traceId)) + + fmt.Println(buf.String()) + + return err } } diff --git a/pkg/resp/err.go b/pkg/resp/err.go index 29cd62c..396c72a 100644 --- a/pkg/resp/err.go +++ b/pkg/resp/err.go @@ -1 +1,56 @@ package resp + +import "net/http" + +type Error struct { + Status int `json:"status"` + Msg string `json:"msg"` + Err error `json:"err"` + Data any `json:"data"` +} + +func (e *Error) Error() string { + return e.Err.Error() +} + +func (e *Error) _r() *res { + data := &res{ + Status: e.Status, + Msg: e.Msg, + Data: e.Data, + Err: e.Err, + } + + if data.Status < 0 || data.Status > 999 { + data.Status = 500 + } + + return data +} + +func NewError(err error, args ...any) *Error { + e := &Error{ + Status: http.StatusInternalServerError, + Err: err, + } + + if len(args) > 0 { + if status, ok := args[0].(int); ok { + e.Status = status + } + } + + e.Msg = Msg(e.Status) + + if len(args) > 1 { + if msg, ok := args[1].(string); ok { + e.Msg = msg + } + } + + if len(args) > 2 { + e.Data = args[2] + } + + return e +} diff --git a/pkg/resp/msg.go b/pkg/resp/msg.go index 29cd62c..494396d 100644 --- a/pkg/resp/msg.go +++ b/pkg/resp/msg.go @@ -1 +1,34 @@ package resp + +const ( + Msg200 = "操作成功" + Msg400 = "参数错误" + Msg401 = "该账号登录已失效, 请重新登录" + Msg401NoMulti = "用户已在其他地方登录" + Msg403 = "权限不足" + Msg404 = "资源不存在" + Msg500 = "服务器开小差了" + Msg501 = "服务不可用" + Msg503 = "服务不可用或正在升级, 请联系管理员" +) + +func Msg(status int) string { + switch status { + case 400: + return Msg400 + case 401: + return Msg401 + case 403: + return Msg403 + case 404: + return Msg404 + case 500: + return Msg500 + case 501: + return Msg501 + case 503: + return Msg503 + } + + return "未知错误" +} diff --git a/pkg/resp/resp.go b/pkg/resp/resp.go index 29cd62c..1e7fc6a 100644 --- a/pkg/resp/resp.go +++ b/pkg/resp/resp.go @@ -1 +1,105 @@ package resp + +import ( + "errors" + "github.com/gofiber/fiber/v3" +) + +type res struct { + Status int `json:"status"` + Msg string `json:"msg"` + Data any `json:"data"` + Err any `json:"err"` +} + +func R200(c fiber.Ctx, data any, msgs ...string) error { + r := &res{ + Status: 200, + Msg: Msg200, + Data: data, + } + + if len(msgs) > 0 && msgs[0] != "" { + r.Msg = msgs[0] + } + + return c.JSON(r) +} + +func RC(c fiber.Ctx, status int, args ...any) error { + return _r(c, &res{Status: status}, args...) +} + +func RE(c fiber.Ctx, err error) error { + var re *Error + + if errors.As(err, &re) { + return _r(c, re._r()) + } + + return R500(c, "", nil, err) +} + +func _r(c fiber.Ctx, r *res, args ...any) error { + length := len(args) + switch length { + case 0: + break + case 1: + if msg, ok := args[0].(string); ok { + r.Msg = msg + } else { + r.Data = args[0] + } + case 2: + r.Data = args[1] + case 3: + r.Err = args[2] + } + + if r.Msg == "" { + r.Msg = Msg(r.Status) + } + + return c.Status(r.Status).JSON(r) +} + +func R400(c fiber.Ctx, args ...any) error { + r := &res{ + Status: 400, + } + + return _r(c, r, args...) +} + +func R401(c fiber.Ctx, args ...any) error { + r := &res{ + Status: 401, + } + + return _r(c, r, args...) +} + +func R403(c fiber.Ctx, args ...any) error { + r := &res{ + Status: 403, + } + + return _r(c, r, args...) +} + +func R500(c fiber.Ctx, args ...any) error { + r := &res{ + Status: 500, + } + + return _r(c, r, args...) +} + +func R501(c fiber.Ctx, args ...any) error { + r := &res{ + Status: 501, + } + + return _r(c, r, args...) +} diff --git a/pkg/tool/ctx.go b/pkg/tool/ctx.go new file mode 100644 index 0000000..e5320c1 --- /dev/null +++ b/pkg/tool/ctx.go @@ -0,0 +1,44 @@ +package tool + +import ( + "context" + "fmt" + "gitea.loveuer.com/yizhisec/packages/opt" + "time" +) + +func Timeout(seconds ...int) (ctx context.Context) { + var ( + duration time.Duration + ) + + if len(seconds) > 0 && seconds[0] > 0 { + duration = time.Duration(seconds[0]) * time.Second + } else { + duration = time.Duration(30) * time.Second + } + + ctx, _ = context.WithTimeout(context.Background(), duration) + + return +} + +func CtxTimeout(ctx context.Context, seconds ...int) context.Context { + var ( + duration time.Duration + ) + + if len(seconds) > 0 && seconds[0] > 0 { + duration = time.Duration(seconds[0]) * time.Second + } else { + duration = time.Duration(30) * time.Second + } + + nctx, _ := context.WithTimeout(ctx, duration) + + return nctx +} + +func CtxTrace(ctx context.Context, key string) context.Context { + return context.WithValue(ctx, opt.TraceKey, fmt.Sprintf("%36s", key)) +} diff --git a/pkg/tool/gin.go b/pkg/tool/gin.go new file mode 100644 index 0000000..1a0ac00 --- /dev/null +++ b/pkg/tool/gin.go @@ -0,0 +1,12 @@ +package tool + +import "github.com/gin-gonic/gin" + +func Local(c *gin.Context, key string) any { + data, ok := c.Get(key) + if !ok { + return nil + } + + return data +} diff --git a/pkg/tool/human.go b/pkg/tool/human.go new file mode 100644 index 0000000..92144e3 --- /dev/null +++ b/pkg/tool/human.go @@ -0,0 +1,50 @@ +package tool + +import "fmt" + +func HumanDuration(nano int64) string { + duration := float64(nano) + unit := "ns" + if duration >= 1000 { + duration /= 1000 + unit = "us" + } + + if duration >= 1000 { + duration /= 1000 + unit = "ms" + } + + if duration >= 1000 { + duration /= 1000 + unit = " s" + } + + return fmt.Sprintf("%6.2f%s", duration, unit) +} + +func HumanSize(size int64) string { + const ( + _ = iota + KB = 1 << (10 * iota) // 1 KB = 1024 bytes + MB // 1 MB = 1024 KB + GB // 1 GB = 1024 MB + TB // 1 TB = 1024 GB + PB // 1 PB = 1024 TB + ) + + switch { + case size >= PB: + return fmt.Sprintf("%.2f PB", float64(size)/PB) + case size >= TB: + return fmt.Sprintf("%.2f TB", float64(size)/TB) + case size >= GB: + return fmt.Sprintf("%.2f GB", float64(size)/GB) + case size >= MB: + return fmt.Sprintf("%.2f MB", float64(size)/MB) + case size >= KB: + return fmt.Sprintf("%.2f KB", float64(size)/KB) + default: + return fmt.Sprintf("%d bytes", size) + } +} diff --git a/pkg/tool/ip.go b/pkg/tool/ip.go new file mode 100644 index 0000000..587cfbf --- /dev/null +++ b/pkg/tool/ip.go @@ -0,0 +1,59 @@ +package tool + +import ( + "net" +) + +var ( + privateIPv4Blocks []*net.IPNet + privateIPv6Blocks []*net.IPNet +) + +func init() { + // IPv4私有地址段 + for _, cidr := range []string{ + "10.0.0.0/8", // A类私有地址 + "172.16.0.0/12", // B类私有地址 + "192.168.0.0/16", // C类私有地址 + "169.254.0.0/16", // 链路本地地址 + "127.0.0.0/8", // 环回地址 + } { + _, block, _ := net.ParseCIDR(cidr) + privateIPv4Blocks = append(privateIPv4Blocks, block) + } + + // IPv6私有地址段 + for _, cidr := range []string{ + "fc00::/7", // 唯一本地地址 + "fe80::/10", // 链路本地地址 + "::1/128", // 环回地址 + } { + _, block, _ := net.ParseCIDR(cidr) + privateIPv6Blocks = append(privateIPv6Blocks, block) + } +} + +func IsPrivateIP(ipStr string) bool { + ip := net.ParseIP(ipStr) + if ip == nil { + return false + } + + // 处理IPv4和IPv4映射的IPv6地址 + if ip4 := ip.To4(); ip4 != nil { + for _, block := range privateIPv4Blocks { + if block.Contains(ip4) { + return true + } + } + return false + } + + // 处理IPv6地址 + for _, block := range privateIPv6Blocks { + if block.Contains(ip) { + return true + } + } + return false +} diff --git a/pkg/tool/loadash.go b/pkg/tool/loadash.go new file mode 100644 index 0000000..73fbfec --- /dev/null +++ b/pkg/tool/loadash.go @@ -0,0 +1,76 @@ +package tool + +import "math" + +func Map[T, R any](vals []T, fn func(item T, index int) R) []R { + var result = make([]R, len(vals)) + for idx, v := range vals { + result[idx] = fn(v, idx) + } + return result +} + +func Chunk[T any](vals []T, size int) [][]T { + if size <= 0 { + panic("Second parameter must be greater than 0") + } + + chunksNum := len(vals) / size + if len(vals)%size != 0 { + chunksNum += 1 + } + + result := make([][]T, 0, chunksNum) + + for i := 0; i < chunksNum; i++ { + last := (i + 1) * size + if last > len(vals) { + last = len(vals) + } + result = append(result, vals[i*size:last:last]) + } + + return result +} + +// 对 vals 取样 x 个 +func Sample[T any](vals []T, x int) []T { + if x < 0 { + panic("Second parameter can't be negative") + } + + n := len(vals) + if n == 0 { + return []T{} + } + + if x >= n { + return vals + } + + // 处理x=1的特殊情况 + if x == 1 { + return []T{vals[(n-1)/2]} + } + + // 计算采样步长并生成结果数组 + step := float64(n-1) / float64(x-1) + result := make([]T, x) + + for i := 0; i < x; i++ { + // 计算采样位置并四舍五入 + pos := float64(i) * step + index := int(math.Round(pos)) + result[i] = vals[index] + } + + return result +} + +func If[T any](cond bool, trueVal, falseVal T) T { + if cond { + return trueVal + } + + return falseVal +} diff --git a/pkg/tool/must.go b/pkg/tool/must.go new file mode 100644 index 0000000..742463c --- /dev/null +++ b/pkg/tool/must.go @@ -0,0 +1,53 @@ +package tool + +import ( + "context" + "gitea.loveuer.com/yizhisec/packages/logger" + "sync" +) + +func Must(errs ...error) { + for _, err := range errs { + if err != nil { + logger.Panic(err.Error()) + } + } +} + +func MustWithData[T any](data T, err error) T { + Must(err) + return data +} + +func MustStop(ctx context.Context, stopFns ...func(ctx context.Context) error) { + if len(stopFns) == 0 { + return + } + + ok := make(chan struct{}) + + wg := &sync.WaitGroup{} + wg.Add(len(stopFns)) + + for _, fn := range stopFns { + go func() { + defer wg.Done() + + if err := fn(ctx); err != nil { + logger.ErrorCtx(ctx, "stop function failed, err = %s", err.Error()) + } + }() + } + + go func() { + select { + case <-ctx.Done(): + logger.FatalCtx(ctx, "stop function timeout, force down") + case _, _ = <-ok: + return + } + }() + + wg.Wait() + close(ok) +} diff --git a/pkg/tool/password.go b/pkg/tool/password.go new file mode 100644 index 0000000..b6027f0 --- /dev/null +++ b/pkg/tool/password.go @@ -0,0 +1,84 @@ +package tool + +import ( + "crypto/sha256" + "encoding/hex" + "errors" + "fmt" + "gitea.loveuer.com/yizhisec/packages/logger" + "golang.org/x/crypto/pbkdf2" + "regexp" + "strconv" + "strings" +) + +const ( + EncryptHeader string = "pbkdf2:sha256" // 用户密码加密 +) + +func NewPassword(password string) string { + return EncryptPassword(password, RandomString(8), int(RandomInt(50000)+100000)) +} + +func ComparePassword(in, db string) bool { + strs := strings.Split(db, "$") + if len(strs) != 3 { + logger.Error("password in db invalid: %s", db) + return false + } + + encs := strings.Split(strs[0], ":") + if len(encs) != 3 { + logger.Error("password in db invalid: %s", db) + return false + } + + encIteration, err := strconv.Atoi(encs[2]) + if err != nil { + logger.Error("password in db invalid: %s, convert iter err: %s", db, err) + return false + } + + return EncryptPassword(in, strs[1], encIteration) == db +} + +func EncryptPassword(password, salt string, iter int) string { + hash := pbkdf2.Key([]byte(password), []byte(salt), iter, 32, sha256.New) + encrypted := hex.EncodeToString(hash) + return fmt.Sprintf("%s:%d$%s$%s", EncryptHeader, iter, salt, encrypted) +} + +func CheckPassword(password string) error { + if len(password) < 8 || len(password) > 32 { + return errors.New("密码长度不符合") + } + + var ( + err error + match bool + patternList = []string{`[0-9]+`, `[a-z]+`, `[A-Z]+`, `[!@#%]+`} //, `[~!@#$%^&*?_-]+`} + matchAccount = 0 + tips = []string{"缺少数字", "缺少小写字母", "缺少大写字母", "缺少'!@#%'"} + locktips = make([]string, 0) + ) + + for idx, pattern := range patternList { + match, err = regexp.MatchString(pattern, password) + if err != nil { + logger.Warn("regex match string err, reg_str: %s, err: %v", pattern, err) + return errors.New("密码强度不够") + } + + if match { + matchAccount++ + } else { + locktips = append(locktips, tips[idx]) + } + } + + if matchAccount < 3 { + return fmt.Errorf("密码强度不够, 可能 %s", strings.Join(locktips, ", ")) + } + + return nil +} diff --git a/pkg/tool/password_test.go b/pkg/tool/password_test.go new file mode 100644 index 0000000..9aadac1 --- /dev/null +++ b/pkg/tool/password_test.go @@ -0,0 +1,20 @@ +package tool + +import "testing" + +func TestEncPassword(t *testing.T) { + password := "123456" + + result := EncryptPassword(password, RandomString(8), 50000) + + t.Logf("sum => %s", result) +} + +func TestPassword(t *testing.T) { + p := "wahaha@123" + p = NewPassword(p) + t.Logf("password => %s", p) + + result := ComparePassword("wahaha@123", p) + t.Logf("compare result => %v", result) +} diff --git a/pkg/tool/random.go b/pkg/tool/random.go new file mode 100644 index 0000000..a5f1b2d --- /dev/null +++ b/pkg/tool/random.go @@ -0,0 +1,75 @@ +package tool + +import ( + "crypto/rand" + "math/big" + mrand "math/rand" +) + +var ( + letters = []byte("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") + letterNum = []byte("0123456789") + letterLow = []byte("abcdefghijklmnopqrstuvwxyz") + letterCap = []byte("ABCDEFGHIJKLMNOPQRSTUVWXYZ") + letterSyb = []byte("!@#$%^&*()_+-=") + adjectives = []string{ + "开心的", "灿烂的", "温暖的", "阳光的", "活泼的", + "聪明的", "优雅的", "幸运的", "甜蜜的", "勇敢的", + "宁静的", "热情的", "温柔的", "幽默的", "坚强的", + "迷人的", "神奇的", "快乐的", "健康的", "自由的", + "梦幻的", "勤劳的", "真诚的", "浪漫的", "自信的", + } + + plants = []string{ + "苹果", "香蕉", "橘子", "葡萄", "草莓", + "西瓜", "樱桃", "菠萝", "柠檬", "蜜桃", + "蓝莓", "芒果", "石榴", "甜瓜", "雪梨", + "番茄", "南瓜", "土豆", "青椒", "洋葱", + "黄瓜", "萝卜", "豌豆", "玉米", "蘑菇", + "菠菜", "茄子", "芹菜", "莲藕", "西兰花", + } +) + +func RandomInt(max int64) int64 { + num, _ := rand.Int(rand.Reader, big.NewInt(max)) + return num.Int64() +} + +func RandomString(length int) string { + result := make([]byte, length) + for i := 0; i < length; i++ { + num, _ := rand.Int(rand.Reader, big.NewInt(int64(len(letters)))) + result[i] = letters[num.Int64()] + } + return string(result) +} + +func RandomPassword(length int, withSymbol bool) string { + result := make([]byte, length) + kind := 3 + if withSymbol { + kind++ + } + + for i := 0; i < length; i++ { + switch i % kind { + case 0: + num, _ := rand.Int(rand.Reader, big.NewInt(int64(len(letterNum)))) + result[i] = letterNum[num.Int64()] + case 1: + num, _ := rand.Int(rand.Reader, big.NewInt(int64(len(letterLow)))) + result[i] = letterLow[num.Int64()] + case 2: + num, _ := rand.Int(rand.Reader, big.NewInt(int64(len(letterCap)))) + result[i] = letterCap[num.Int64()] + case 3: + num, _ := rand.Int(rand.Reader, big.NewInt(int64(len(letterSyb)))) + result[i] = letterSyb[num.Int64()] + } + } + return string(result) +} + +func RandomName() string { + return adjectives[mrand.Intn(len(adjectives))] + plants[mrand.Intn(len(plants))] +} diff --git a/pkg/tool/string.go b/pkg/tool/string.go new file mode 100644 index 0000000..c0e3b79 --- /dev/null +++ b/pkg/tool/string.go @@ -0,0 +1,11 @@ +package tool + +import "unsafe" + +func BytesToString(b []byte) string { + return unsafe.String(unsafe.SliceData(b), len(b)) +} + +func StringToBytes(s string) []byte { + return unsafe.Slice(unsafe.StringData(s), len(s)) +} diff --git a/pkg/tool/table.go b/pkg/tool/table.go new file mode 100644 index 0000000..c5b5044 --- /dev/null +++ b/pkg/tool/table.go @@ -0,0 +1,124 @@ +package tool + +import ( + "encoding/json" + "fmt" + "gitea.loveuer.com/yizhisec/packages/logger" + "github.com/jedib0t/go-pretty/v6/table" + "io" + "os" + "reflect" + "strings" +) + +func TablePrinter(data any, writers ...io.Writer) { + var w io.Writer = os.Stdout + if len(writers) > 0 && writers[0] != nil { + w = writers[0] + } + + t := table.NewWriter() + structPrinter(t, "", data) + _, _ = fmt.Fprintln(w, t.Render()) +} + +func structPrinter(w table.Writer, prefix string, item any) { +Start: + rv := reflect.ValueOf(item) + if rv.IsZero() { + return + } + + for rv.Type().Kind() == reflect.Pointer { + rv = rv.Elem() + } + + switch rv.Type().Kind() { + case reflect.Invalid, + reflect.Uintptr, + reflect.Chan, + reflect.Func, + reflect.UnsafePointer: + case reflect.Bool, + reflect.Int, + reflect.Int8, + reflect.Int16, + reflect.Int32, + reflect.Int64, + reflect.Uint, + reflect.Uint8, + reflect.Uint16, + reflect.Uint32, + reflect.Uint64, + reflect.Float32, + reflect.Float64, + reflect.Complex64, + reflect.Complex128, + reflect.Interface: + w.AppendRow(table.Row{strings.TrimPrefix(prefix, "."), rv.Interface()}) + case reflect.String: + val := rv.String() + if len(val) <= 160 { + w.AppendRow(table.Row{strings.TrimPrefix(prefix, "."), val}) + return + } + + w.AppendRow(table.Row{strings.TrimPrefix(prefix, "."), val[0:64] + "..." + val[len(val)-64:]}) + case reflect.Array, reflect.Slice: + for i := 0; i < rv.Len(); i++ { + p := strings.Join([]string{prefix, fmt.Sprintf("[%d]", i)}, ".") + structPrinter(w, p, rv.Index(i).Interface()) + } + case reflect.Map: + for _, k := range rv.MapKeys() { + structPrinter(w, fmt.Sprintf("%s.{%v}", prefix, k), rv.MapIndex(k).Interface()) + } + case reflect.Pointer: + goto Start + case reflect.Struct: + for i := 0; i < rv.NumField(); i++ { + p := fmt.Sprintf("%s.%s", prefix, rv.Type().Field(i).Name) + field := rv.Field(i) + + //log.Debug("TablePrinter: prefix: %s, field: %v", p, rv.Field(i)) + + if !field.CanInterface() { + return + } + + structPrinter(w, p, field.Interface()) + } + } +} + +func TableMapPrinter(data []byte) { + m := make(map[string]any) + if err := json.Unmarshal(data, &m); err != nil { + logger.Warn(err.Error()) + return + } + + t := table.NewWriter() + addRow(t, "", m) + fmt.Println(t.Render()) +} + +func addRow(w table.Writer, prefix string, m any) { + rv := reflect.ValueOf(m) + switch rv.Type().Kind() { + case reflect.Map: + for _, k := range rv.MapKeys() { + key := k.String() + if prefix != "" { + key = strings.Join([]string{prefix, k.String()}, ".") + } + addRow(w, key, rv.MapIndex(k).Interface()) + } + case reflect.Slice, reflect.Array: + for i := 0; i < rv.Len(); i++ { + addRow(w, fmt.Sprintf("%s[%d]", prefix, i), rv.Index(i).Interface()) + } + default: + w.AppendRow(table.Row{prefix, m}) + } +} diff --git a/pkg/tool/tools.go b/pkg/tool/tools.go new file mode 100644 index 0000000..ff99948 --- /dev/null +++ b/pkg/tool/tools.go @@ -0,0 +1,73 @@ +package tool + +import ( + "fmt" + "math" +) + +func Min[T ~int | ~uint | ~int8 | ~uint8 | ~int16 | ~uint16 | ~int32 | ~uint32 | ~int64 | ~uint64 | ~float32 | ~float64](a, b T) T { + if a <= b { + return a + } + + return b +} + +func Mins[T ~int | ~uint | ~int8 | ~uint8 | ~int16 | ~uint16 | ~int32 | ~uint32 | ~int64 | ~uint64 | ~float32 | ~float64](vals ...T) T { + var val T + + if len(vals) == 0 { + return val + } + + val = vals[0] + + for _, item := range vals[1:] { + if item < val { + val = item + } + } + + return val +} + +func Max[T ~int | ~uint | ~int8 | ~uint8 | ~int16 | ~uint16 | ~int32 | ~uint32 | ~int64 | ~uint64 | ~float32 | ~float64](a, b T) T { + if a >= b { + return a + } + + return b +} + +func Maxs[T ~int | ~uint | ~int8 | ~uint8 | ~int16 | ~uint16 | ~int32 | ~uint32 | ~int64 | ~uint64 | ~float32 | ~float64](vals ...T) T { + var val T + + if len(vals) == 0 { + return val + } + + for _, item := range vals { + if item > val { + val = item + } + } + + return val +} + +func Sum[T ~int | ~uint | ~int8 | ~uint8 | ~int16 | ~uint16 | ~int32 | ~uint32 | ~int64 | ~uint64 | ~float32 | ~float64](vals ...T) T { + var sum T = 0 + for i := range vals { + sum += vals[i] + } + return sum +} + +func Percent(val, minVal, maxVal, minPercent, maxPercent float64) string { + return fmt.Sprintf( + "%d%%", + int(math.Round( + ((val-minVal)/(maxVal-minVal)*(maxPercent-minPercent)+minPercent)*100, + )), + ) +} diff --git a/pkg/tool/tools_test.go b/pkg/tool/tools_test.go new file mode 100644 index 0000000..3622e76 --- /dev/null +++ b/pkg/tool/tools_test.go @@ -0,0 +1,70 @@ +package tool + +import "testing" + +func TestPercent(t *testing.T) { + type args struct { + val float64 + minVal float64 + maxVal float64 + minPercent float64 + maxPercent float64 + } + tests := []struct { + name string + args args + want string + }{ + { + name: "case 1", + args: args{ + val: 0.5, + minVal: 0, + maxVal: 1, + minPercent: 0, + maxPercent: 1, + }, + want: "50%", + }, + { + name: "case 2", + args: args{ + val: 0.3, + minVal: 0.1, + maxVal: 0.6, + minPercent: 0, + maxPercent: 1, + }, + want: "40%", + }, + { + name: "case 3", + args: args{ + val: 700, + minVal: 700, + maxVal: 766, + minPercent: 0.1, + maxPercent: 0.7, + }, + want: "10%", + }, + { + name: "case 4", + args: args{ + val: 766, + minVal: 700, + maxVal: 766, + minPercent: 0.1, + maxPercent: 0.7, + }, + want: "70%", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := Percent(tt.args.val, tt.args.minVal, tt.args.maxVal, tt.args.minPercent, tt.args.maxPercent); got != tt.want { + t.Errorf("Percent() = %v, want %v", got, tt.want) + } + }) + } +}