From 4f1aa63b1c9005b115d0a1e3fa2f92c39a4b9976 Mon Sep 17 00:00:00 2001 From: dark Date: Sun, 13 Jul 2025 05:39:36 +0800 Subject: [PATCH] api --- .cursorrules | 5 + .gitignore | 1 + backend/go.mod | 14 +- backend/go.sum | 22 +- backend/modd.conf | 7 + backend/readme.md | 126 ++++++ backend/usercenter/api/apis/user.api | 50 +++ .../usercenter/api/etc/usercenter-api.yaml | 23 ++ .../{orm => api/internal}/AutoMigrate.go | 11 +- .../usercenter/api/internal/config/config.go | 26 ++ .../handler/health/usercenter_ping_handler.go | 21 + .../usercenter/api/internal/handler/routes.go | 49 +++ .../handler/user/user_login_handler.go | 29 ++ .../handler/user/user_logout_handler.go | 38 ++ .../handler/user/user_register_handler.go | 28 ++ .../logic/health/usercenter_ping_logic.go | 34 ++ .../internal/logic/user/user_login_logic.go | 93 +++++ .../internal/logic/user/user_logout_logic.go | 85 ++++ .../logic/user/user_register_logic.go | 147 +++++++ .../api/internal/svc/service_context.go | 33 ++ .../usercenter/api/internal/types/types.go | 38 ++ backend/usercenter/api/usercenter.api | 23 ++ backend/usercenter/api/usercenter.go | 31 ++ backend/usercenter/cmd/migrate.go | 7 - .../usercenter/internal/logic/loginlogic.go | 30 -- .../internal/logic/registerlogic.go | 30 -- .../usercenter/internal/svc/servicecontext.go | 13 - backend/usercenter/orm/base.go | 6 +- backend/usercenter/orm/userType.go | 26 +- .../appendonlydir/appendonly.aof.1.base.rdb | Bin 88 -> 0 bytes .../appendonlydir/appendonly.aof.manifest | 2 - backend/usercenter/redis/data/dump.rdb | Bin 88 -> 0 bytes .../usercenter/{ => rpc}/etc/usercenter.yaml | 13 +- .../{ => rpc}/internal/config/config.go | 15 +- .../internal/logic/changepasswordlogic.go | 4 +- .../internal/logic/getprofilelogic.go | 4 +- .../internal/logic/getuserpermissionslogic.go | 4 +- .../internal/logic/getuserroleslogic.go | 4 +- .../rpc/internal/logic/loginlogic.go | 61 +++ .../{ => rpc}/internal/logic/pinglogic.go | 8 +- .../rpc/internal/logic/registerlogic.go | 86 +++++ .../internal/logic/updateprofilelogic.go | 4 +- .../internal/server/usercenterserver.go | 6 +- .../rpc/internal/svc/servicecontext.go | 39 ++ .../{ => rpc}/pb/usercenter/usercenter.pb.go | 0 .../pb/usercenter/usercenter_grpc.pb.go | 0 backend/usercenter/{ => rpc}/usercenter.go | 8 +- backend/usercenter/{ => rpc}/usercenter.proto | 0 .../{ => rpc}/usercenterclient/usercenter.go | 2 +- backend/utils/README.md | 362 ++++++++++++++++++ backend/utils/bcrypt.go | 119 ++++++ backend/utils/bcrypt_test.go | 166 ++++++++ backend/utils/jwt.go | 223 +++++++++++ backend/utils/jwt_test.go | 327 ++++++++++++++++ docker-compose.yml | 29 ++ nginx/conf.d/gateway.conf | 14 + .../log/access.log | 0 nginx/log/error.log | 159 ++++++++ nginx/log/jsj2025.com_access.log | 0 nginx/log/jsj2025.com_error.log | 0 60 files changed, 2570 insertions(+), 135 deletions(-) create mode 100644 .cursorrules create mode 100644 backend/modd.conf create mode 100644 backend/readme.md create mode 100644 backend/usercenter/api/apis/user.api create mode 100644 backend/usercenter/api/etc/usercenter-api.yaml rename backend/usercenter/{orm => api/internal}/AutoMigrate.go (61%) create mode 100644 backend/usercenter/api/internal/config/config.go create mode 100644 backend/usercenter/api/internal/handler/health/usercenter_ping_handler.go create mode 100644 backend/usercenter/api/internal/handler/routes.go create mode 100644 backend/usercenter/api/internal/handler/user/user_login_handler.go create mode 100644 backend/usercenter/api/internal/handler/user/user_logout_handler.go create mode 100644 backend/usercenter/api/internal/handler/user/user_register_handler.go create mode 100644 backend/usercenter/api/internal/logic/health/usercenter_ping_logic.go create mode 100644 backend/usercenter/api/internal/logic/user/user_login_logic.go create mode 100644 backend/usercenter/api/internal/logic/user/user_logout_logic.go create mode 100644 backend/usercenter/api/internal/logic/user/user_register_logic.go create mode 100644 backend/usercenter/api/internal/svc/service_context.go create mode 100644 backend/usercenter/api/internal/types/types.go create mode 100644 backend/usercenter/api/usercenter.api create mode 100644 backend/usercenter/api/usercenter.go delete mode 100644 backend/usercenter/cmd/migrate.go delete mode 100644 backend/usercenter/internal/logic/loginlogic.go delete mode 100644 backend/usercenter/internal/logic/registerlogic.go delete mode 100644 backend/usercenter/internal/svc/servicecontext.go delete mode 100644 backend/usercenter/redis/data/appendonlydir/appendonly.aof.1.base.rdb delete mode 100644 backend/usercenter/redis/data/appendonlydir/appendonly.aof.manifest delete mode 100644 backend/usercenter/redis/data/dump.rdb rename backend/usercenter/{ => rpc}/etc/usercenter.yaml (55%) rename backend/usercenter/{ => rpc}/internal/config/config.go (71%) rename backend/usercenter/{ => rpc}/internal/logic/changepasswordlogic.go (88%) rename backend/usercenter/{ => rpc}/internal/logic/getprofilelogic.go (88%) rename backend/usercenter/{ => rpc}/internal/logic/getuserpermissionslogic.go (88%) rename backend/usercenter/{ => rpc}/internal/logic/getuserroleslogic.go (88%) create mode 100644 backend/usercenter/rpc/internal/logic/loginlogic.go rename backend/usercenter/{ => rpc}/internal/logic/pinglogic.go (78%) create mode 100644 backend/usercenter/rpc/internal/logic/registerlogic.go rename backend/usercenter/{ => rpc}/internal/logic/updateprofilelogic.go (88%) rename backend/usercenter/{ => rpc}/internal/server/usercenterserver.go (94%) create mode 100644 backend/usercenter/rpc/internal/svc/servicecontext.go rename backend/usercenter/{ => rpc}/pb/usercenter/usercenter.pb.go (100%) rename backend/usercenter/{ => rpc}/pb/usercenter/usercenter_grpc.pb.go (100%) rename backend/usercenter/{ => rpc}/usercenter.go (82%) rename backend/usercenter/{ => rpc}/usercenter.proto (100%) rename backend/usercenter/{ => rpc}/usercenterclient/usercenter.go (99%) create mode 100644 backend/utils/README.md create mode 100644 backend/utils/bcrypt.go create mode 100644 backend/utils/bcrypt_test.go create mode 100644 backend/utils/jwt.go create mode 100644 backend/utils/jwt_test.go create mode 100644 nginx/conf.d/gateway.conf rename backend/usercenter/redis/data/appendonlydir/appendonly.aof.1.incr.aof => nginx/log/access.log (100%) create mode 100644 nginx/log/error.log create mode 100644 nginx/log/jsj2025.com_access.log create mode 100644 nginx/log/jsj2025.com_error.log diff --git a/.cursorrules b/.cursorrules new file mode 100644 index 0000000..434fbb3 --- /dev/null +++ b/.cursorrules @@ -0,0 +1,5 @@ +use context7 +后端技术栈: +go-zero +gorm + diff --git a/.gitignore b/.gitignore index 03dc0c4..433938d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ /etcd /redis +*.exe \ No newline at end of file diff --git a/backend/go.mod b/backend/go.mod index 0ba47e6..565bb9c 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -5,7 +5,12 @@ go 1.23.0 toolchain go1.23.11 require ( + github.com/golang-jwt/jwt/v5 v5.2.2 + github.com/redis/go-redis/v9 v9.10.0 + github.com/spf13/cast v1.9.2 + github.com/stretchr/testify v1.10.0 github.com/zeromicro/go-zero v1.8.4 + golang.org/x/crypto v0.40.0 google.golang.org/grpc v1.73.0 google.golang.org/protobuf v1.36.6 gorm.io/driver/mysql v1.6.0 @@ -30,6 +35,7 @@ require ( github.com/go-openapi/swag v0.22.4 // indirect github.com/go-sql-driver/mysql v1.9.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang-jwt/jwt/v4 v4.5.2 // indirect github.com/golang/mock v1.6.0 // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/google/gnostic-models v0.6.8 // indirect @@ -52,11 +58,11 @@ require ( github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/openzipkin/zipkin-go v0.4.3 // indirect github.com/pelletier/go-toml/v2 v2.2.2 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect github.com/prometheus/client_golang v1.21.1 // indirect github.com/prometheus/client_model v0.6.1 // indirect github.com/prometheus/common v0.62.0 // indirect github.com/prometheus/procfs v0.15.1 // indirect - github.com/redis/go-redis/v9 v9.10.0 // indirect github.com/spaolacci/murmur3 v1.1.0 // indirect go.etcd.io/etcd/api/v3 v3.5.15 // indirect go.etcd.io/etcd/client/pkg/v3 v3.5.15 // indirect @@ -77,10 +83,10 @@ require ( go.uber.org/automaxprocs v1.6.0 // indirect go.uber.org/multierr v1.9.0 // indirect go.uber.org/zap v1.24.0 // indirect - golang.org/x/net v0.38.0 // indirect + golang.org/x/net v0.41.0 // indirect golang.org/x/oauth2 v0.28.0 // indirect - golang.org/x/sys v0.31.0 // indirect - golang.org/x/term v0.30.0 // indirect + golang.org/x/sys v0.34.0 // indirect + golang.org/x/term v0.33.0 // indirect golang.org/x/text v0.27.0 // indirect golang.org/x/time v0.10.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20250324211829-b45e905df463 // indirect diff --git a/backend/go.sum b/backend/go.sum index 9262a08..59592fe 100644 --- a/backend/go.sum +++ b/backend/go.sum @@ -28,6 +28,8 @@ github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxER github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= 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/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= @@ -48,6 +50,10 @@ github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4 github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI= +github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8= +github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= @@ -135,6 +141,8 @@ github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spf13/cast v1.9.2 h1:SsGfm7M8QOFtEzumm7UZrZdLLquNdzFYfIbEXntcFbE= +github.com/spf13/cast v1.9.2/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -202,6 +210,8 @@ go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg= 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.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM= +golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= @@ -210,8 +220,8 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= -golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= -golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= +golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw= +golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA= golang.org/x/oauth2 v0.28.0 h1:CrgCKl8PPAVtLnU3c+EDw6x11699EWlsDeWNWKdIOkc= golang.org/x/oauth2 v0.28.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -226,11 +236,11 @@ golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= -golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA= +golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y= -golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g= +golang.org/x/term v0.33.0 h1:NuFncQrRcaRvVmgRkvM3j/F00gWIAlcmlB8ACEKmGIg= +golang.org/x/term v0.33.0/go.mod h1:s18+ql9tYWp1IfpV9DmCtQDDSRBUjKaw9M1eAv5UeF0= 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.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4= diff --git a/backend/modd.conf b/backend/modd.conf new file mode 100644 index 0000000..f3fafcc --- /dev/null +++ b/backend/modd.conf @@ -0,0 +1,7 @@ +# modd.conf 文件用于配置 modd 工具 +# backend 后台监听文件变化,自动重启服务 +# 1. usercenter 服务 +usercenter/*{ + prep: go build -o usercenter/usercenter -v usercenter/usercenter.go + daemon +sigkill: ./usercenter/usercenter -f usercenter/etc/usercenter.yaml +} diff --git a/backend/readme.md b/backend/readme.md new file mode 100644 index 0000000..5bbc204 --- /dev/null +++ b/backend/readme.md @@ -0,0 +1,126 @@ +# backend 说明 + +## 主要包 + +- go-zero 框架 + +```bash +go get github.com/zeromicro/go-zero +``` + +- gorm 数据库 + +```bash +go get gorm.io/gorm +``` + +- redis 缓存 + +```bash +go get github.com/redis/go-redis/v9 +go get github.com/redis/go-redis/extra/redisotel/v9 +``` + +## 工具包 + +- bcrypt 密码加密 + +```bash +go get golang.org/x/crypto/bcrypt +``` + +- jwt 认证 + +```bash +go get github.com/golang-jwt/jwt/v4 +``` + +- zap 日志 + +```bash +go get go.uber.org/zap +``` + +- viper 配置 + +```bash +go get github.com/spf13/viper +``` + +- copier 数据拷贝 + +```bash +go get github.com/jinzhu/copier/v2 +``` + +- strconv 字符串转换 + +```bash +go get strconv +``` + +- time 时间 + +```bash +go get time +``` + +- errors 错误 + +```bash +go get errors +``` + +- regexp 正则 + +```bash +go get regexp +``` + +- context 上下文 + +```bash +go get context +``` + +- fmt 格式化 + +```bash +go get fmt +``` + +- cast 类型转换 + +```bash +go get github.com/spf13/cast +``` + +- carbon 时间 + +```bash +go get github.com/golang-module/carbon/v2 +``` + +- base64Captcha 验证码 + +```bash +go get github.com/mojocn/base64Captcha +``` + +## 常用命令 + +- 生成 api 文件 + +```bash +goctl api new xxx -style go_zero +// 指定仓库地址,注意goctl版本 +goctl api new xxx -remote https://gitea.gxxhygroup.com/dark/goctl184.git -style go_zero +``` + +- api 生成 go 文件 + +```bash +goctl api go -api usercenter.api -dir . -style go_zero +// 指定仓库地址,注意goctl版本 +goctl api go -api usercenter.api -dir . -remote https://gitea.gxxhygroup.com/dark/goctl184.git -style go_zero +``` diff --git a/backend/usercenter/api/apis/user.api b/backend/usercenter/api/apis/user.api new file mode 100644 index 0000000..9ae4871 --- /dev/null +++ b/backend/usercenter/api/apis/user.api @@ -0,0 +1,50 @@ +// 用户管理 +syntax = "v1" + +type RegisterRequest { + Username string `json:"username"` + Password string `json:"password"` + Email string `json:"email,optional"` + Phone string `json:"phone"` + Nickname string `json:"nickname,optional"` + Avatar string `json:"avatar,optional"` + Role string `json:"role,optional"` + Status string `json:"status,optional"` +} + +type LoginRequest { + Identity string `json:"identity"` + Password string `json:"password"` +} + +type LogoutRequest { + Token string `json:"token"` +} + +type UsersResponse { + Code int `json:"code"` + Message string `json:"message"` + Data interface{} `json:"data"` + Success bool `json:"success"` +} + +@server ( + // jwt: Auth + // middleware: CheckPermission + group: user +) +// 用户管理 +service usercenter-api { + @doc "用户注册" + @handler UserRegisterHandler + post /register (RegisterRequest) returns (UsersResponse) + + @doc "用户登录" + @handler UserLoginHandler + post /login (LoginRequest) returns (UsersResponse) + + @doc "用户登出" + @handler UserLogoutHandler + post /logout (LogoutRequest) returns (UsersResponse) +} + diff --git a/backend/usercenter/api/etc/usercenter-api.yaml b/backend/usercenter/api/etc/usercenter-api.yaml new file mode 100644 index 0000000..290e483 --- /dev/null +++ b/backend/usercenter/api/etc/usercenter-api.yaml @@ -0,0 +1,23 @@ +Name: usercenter-api +Host: 0.0.0.0 +Port: 8889 +MySQL: + Host: 127.0.0.1 + Port: 3306 + User: root + Password: root + DBName: usercenter + MaxIdleConns: 10 + MaxOpenConns: 100 +Redis: + Host: 127.0.0.1 + Port: 6379 + Type: node + Pass: xhy.dev + Tls: false + TkDB: 1 + CasheDb: 0 +Auth: + AccessSecret: dev.gxxhygroup.com + AccessExpire: 604800 +TkStore: true diff --git a/backend/usercenter/orm/AutoMigrate.go b/backend/usercenter/api/internal/AutoMigrate.go similarity index 61% rename from backend/usercenter/orm/AutoMigrate.go rename to backend/usercenter/api/internal/AutoMigrate.go index fbe24f9..debd4cc 100644 --- a/backend/usercenter/orm/AutoMigrate.go +++ b/backend/usercenter/api/internal/AutoMigrate.go @@ -6,7 +6,8 @@ package orm import ( - "backend/usercenter/internal/config" + "backend/usercenter/api/internal/config" + "backend/usercenter/orm" "flag" "fmt" @@ -15,19 +16,21 @@ import ( "gorm.io/gorm" ) -var configFile = flag.String("f", "etc/usercenter.yaml", "the config file") +var configFile = flag.String("yaml", "etc/usercenter.yaml", "the config file") func AutoMigrate() { flag.Parse() var c config.Config conf.MustLoad(*configFile, &c) - db, err := gorm.Open(mysql.Open(c.MySQL.DSN()), &gorm.Config{}) + dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=True&loc=Local", c.MySQL.User, c.MySQL.Password, c.MySQL.Host, c.MySQL.Port, c.MySQL.DBName) + + db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{}) if err != nil { panic(err) } // 自动迁移 User 表 fmt.Println("迁移User表") - if err := db.AutoMigrate(&User{}); err != nil { + if err := db.AutoMigrate(&orm.User{}); err != nil { panic(err) } fmt.Println("迁移User表完成") diff --git a/backend/usercenter/api/internal/config/config.go b/backend/usercenter/api/internal/config/config.go new file mode 100644 index 0000000..79ecb07 --- /dev/null +++ b/backend/usercenter/api/internal/config/config.go @@ -0,0 +1,26 @@ +package config + +import "github.com/zeromicro/go-zero/rest" + +type Config struct { + rest.RestConf + MySQL struct { + Host string + Port int + User string + Password string + DBName string + } + Redis struct { + Host string + Port int + Pass string + TkDB int + CasheDb int + } + Auth struct { + AccessSecret string + AccessExpire int64 + } + TkStore bool +} diff --git a/backend/usercenter/api/internal/handler/health/usercenter_ping_handler.go b/backend/usercenter/api/internal/handler/health/usercenter_ping_handler.go new file mode 100644 index 0000000..8d97581 --- /dev/null +++ b/backend/usercenter/api/internal/handler/health/usercenter_ping_handler.go @@ -0,0 +1,21 @@ +package health + +import ( + "net/http" + + "backend/usercenter/api/internal/logic/health" + "backend/usercenter/api/internal/svc" + "github.com/zeromicro/go-zero/rest/httpx" +) + +func UsercenterPingHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + l := health.NewUsercenterPingLogic(r.Context(), svcCtx) + resp, err := l.UsercenterPing() + if err != nil { + httpx.ErrorCtx(r.Context(), w, err) + } else { + httpx.OkJsonCtx(r.Context(), w, resp) + } + } +} diff --git a/backend/usercenter/api/internal/handler/routes.go b/backend/usercenter/api/internal/handler/routes.go new file mode 100644 index 0000000..a05e8ba --- /dev/null +++ b/backend/usercenter/api/internal/handler/routes.go @@ -0,0 +1,49 @@ +// Code generated by goctl. DO NOT EDIT. +// goctl 1.8.4 + +package handler + +import ( + "net/http" + + health "backend/usercenter/api/internal/handler/health" + user "backend/usercenter/api/internal/handler/user" + "backend/usercenter/api/internal/svc" + + "github.com/zeromicro/go-zero/rest" +) + +func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) { + server.AddRoutes( + []rest.Route{ + { + Method: http.MethodGet, + Path: "/ping", + Handler: health.UsercenterPingHandler(serverCtx), + }, + }, + ) + + server.AddRoutes( + []rest.Route{ + { + // 用户登录 + Method: http.MethodPost, + Path: "/login", + Handler: user.UserLoginHandler(serverCtx), + }, + { + // 用户登出 + Method: http.MethodPost, + Path: "/logout", + Handler: user.UserLogoutHandler(serverCtx), + }, + { + // 用户注册 + Method: http.MethodPost, + Path: "/register", + Handler: user.UserRegisterHandler(serverCtx), + }, + }, + ) +} diff --git a/backend/usercenter/api/internal/handler/user/user_login_handler.go b/backend/usercenter/api/internal/handler/user/user_login_handler.go new file mode 100644 index 0000000..a0469e7 --- /dev/null +++ b/backend/usercenter/api/internal/handler/user/user_login_handler.go @@ -0,0 +1,29 @@ +package user + +import ( + "net/http" + + "backend/usercenter/api/internal/logic/user" + "backend/usercenter/api/internal/svc" + "backend/usercenter/api/internal/types" + "github.com/zeromicro/go-zero/rest/httpx" +) + +// 用户登录 +func UserLoginHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.LoginRequest + if err := httpx.Parse(r, &req); err != nil { + httpx.ErrorCtx(r.Context(), w, err) + return + } + + l := user.NewUserLoginLogic(r.Context(), svcCtx) + resp, err := l.UserLogin(&req) + if err != nil { + httpx.ErrorCtx(r.Context(), w, err) + } else { + httpx.OkJsonCtx(r.Context(), w, resp) + } + } +} diff --git a/backend/usercenter/api/internal/handler/user/user_logout_handler.go b/backend/usercenter/api/internal/handler/user/user_logout_handler.go new file mode 100644 index 0000000..940d29d --- /dev/null +++ b/backend/usercenter/api/internal/handler/user/user_logout_handler.go @@ -0,0 +1,38 @@ +package user + +import ( + "context" + "net/http" + + "backend/usercenter/api/internal/logic/user" + "backend/usercenter/api/internal/svc" + "backend/usercenter/api/internal/types" + + "github.com/zeromicro/go-zero/rest/httpx" +) + +// 用户登出 +func UserLogoutHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.LogoutRequest + if err := httpx.Parse(r, &req); err != nil { + httpx.ErrorCtx(r.Context(), w, err) + return + } + // 获取请求头里的token + token := r.Header.Get("Authorization") + // 如果有token, 使用context传值到logic中 + if token != "" { + ctx := context.WithValue(r.Context(), "token", token) + r = r.WithContext(ctx) + } + + l := user.NewUserLogoutLogic(r.Context(), svcCtx) + resp, err := l.UserLogout(&req) + if err != nil { + httpx.ErrorCtx(r.Context(), w, err) + } else { + httpx.OkJsonCtx(r.Context(), w, resp) + } + } +} diff --git a/backend/usercenter/api/internal/handler/user/user_register_handler.go b/backend/usercenter/api/internal/handler/user/user_register_handler.go new file mode 100644 index 0000000..28788c5 --- /dev/null +++ b/backend/usercenter/api/internal/handler/user/user_register_handler.go @@ -0,0 +1,28 @@ +package user + +import ( + "net/http" + + "backend/usercenter/api/internal/logic/user" + "backend/usercenter/api/internal/svc" + "backend/usercenter/api/internal/types" + "github.com/zeromicro/go-zero/rest/httpx" +) + +func UserRegisterHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.RegisterRequest + if err := httpx.Parse(r, &req); err != nil { + httpx.ErrorCtx(r.Context(), w, err) + return + } + + l := user.NewUserRegisterLogic(r.Context(), svcCtx) + resp, err := l.UserRegister(&req) + if err != nil { + httpx.ErrorCtx(r.Context(), w, err) + } else { + httpx.OkJsonCtx(r.Context(), w, resp) + } + } +} diff --git a/backend/usercenter/api/internal/logic/health/usercenter_ping_logic.go b/backend/usercenter/api/internal/logic/health/usercenter_ping_logic.go new file mode 100644 index 0000000..76d6a31 --- /dev/null +++ b/backend/usercenter/api/internal/logic/health/usercenter_ping_logic.go @@ -0,0 +1,34 @@ +package health + +import ( + "context" + + "backend/usercenter/api/internal/svc" + "backend/usercenter/api/internal/types" + + "github.com/zeromicro/go-zero/core/logx" + // github.com/spf13/cast // 类型转换 + // github.com/golang-module/carbon/v2 // 日期时间处理 + // github.com/jinzhu/copier/v2 // 结构体复制 + // orm 目录下的 orm 包 +) + +type UsercenterPingLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewUsercenterPingLogic(ctx context.Context, svcCtx *svc.ServiceContext) *UsercenterPingLogic { + return &UsercenterPingLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *UsercenterPingLogic) UsercenterPing() (resp *types.CommonResponse, err error) { + // todo: add your logic here and delete this line + + return +} diff --git a/backend/usercenter/api/internal/logic/user/user_login_logic.go b/backend/usercenter/api/internal/logic/user/user_login_logic.go new file mode 100644 index 0000000..0fc950c --- /dev/null +++ b/backend/usercenter/api/internal/logic/user/user_login_logic.go @@ -0,0 +1,93 @@ +package user + +import ( + "context" + + "backend/usercenter/api/internal/svc" + "backend/usercenter/api/internal/types" + "backend/usercenter/orm" + "backend/utils" + + "github.com/zeromicro/go-zero/core/logx" + "gorm.io/gorm" + // github.com/spf13/cast // 类型转换 + // github.com/golang-module/carbon/v2 // 日期时间处理 + // github.com/jinzhu/copier/v2 // 结构体复制 + // orm 目录下的 orm 包 +) + +type UserLoginLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// 用户登录 +func NewUserLoginLogic(ctx context.Context, svcCtx *svc.ServiceContext) *UserLoginLogic { + return &UserLoginLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *UserLoginLogic) UserLogin(req *types.LoginRequest) (resp *types.UsersResponse, err error) { + // 查找用户名或者手机号 + user := new(orm.User) + result := l.svcCtx.Db.Where("name = ? or phone = ?", req.Identity, req.Identity).First(user) + if result.Error != nil && result.Error != gorm.ErrRecordNotFound { + return &types.UsersResponse{ + Success: false, + Message: result.Error.Error(), + Data: nil, + Code: 200, + }, nil + } + if result.Error == gorm.ErrRecordNotFound { + return &types.UsersResponse{ + Success: false, + Message: "用户名或者手机号不存在", + Data: nil, + Code: 200, + }, nil + } + + // 检查密码 + password := req.Password + if password == "" { + return &types.UsersResponse{ + Success: false, + Message: "密码不能为空", + Data: nil, + Code: 200, + }, nil + } + + // 检查密码是否正确 + if !utils.CheckPassword(user.Password, req.Password) { + return &types.UsersResponse{ + Success: false, + Message: "密码错误", + Data: nil, + Code: 200, + }, nil + } + + // 生成token + jwtUtil := utils.NewJWTUtil(l.svcCtx.Config.Auth.AccessSecret, l.svcCtx.Config.Auth.AccessExpire, l.svcCtx.RedisClient, l.svcCtx.Config.TkStore) + token, err := jwtUtil.GenerateToken(l.ctx, int64(user.ID), user.Name) + if err != nil { + return &types.UsersResponse{ + Success: false, + Message: err.Error(), + Data: nil, + Code: 200, + }, nil + } + return &types.UsersResponse{ + Success: true, + Message: "登录成功", + Data: token, + Code: 200, + }, nil +} diff --git a/backend/usercenter/api/internal/logic/user/user_logout_logic.go b/backend/usercenter/api/internal/logic/user/user_logout_logic.go new file mode 100644 index 0000000..e042b2d --- /dev/null +++ b/backend/usercenter/api/internal/logic/user/user_logout_logic.go @@ -0,0 +1,85 @@ +package user + +import ( + "backend/usercenter/api/internal/svc" + "backend/usercenter/api/internal/types" + "context" + "fmt" + "regexp" + "strings" + + "github.com/zeromicro/go-zero/core/logx" + // github.com/spf13/cast // 类型转换 + // github.com/golang-module/carbon/v2 // 日期时间处理 + // github.com/jinzhu/copier/v2 // 结构体复制 + // orm 目录下的 orm 包 +) + +type UserLogoutLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// 用户登出 +func NewUserLogoutLogic(ctx context.Context, svcCtx *svc.ServiceContext) *UserLogoutLogic { + return &UserLogoutLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *UserLogoutLogic) UserLogout(req *types.LogoutRequest) (resp *types.UsersResponse, err error) { + // 获取请求头里的token + // 从 context 中获取请求头里的 token + token := l.ctx.Value("token").(string) + fmt.Println(token) + if token == "" { + return &types.UsersResponse{ + Success: false, + Message: "未获取到token", + Data: nil, + Code: 200, + }, nil + } + re := regexp.MustCompile(`(?i)^bearer\s*`) + token = re.ReplaceAllString(strings.TrimSpace(token), "") + fmt.Println(token) + // 查找token是否存在 + exists, err := l.svcCtx.RedisClient.Exists(l.ctx, token).Result() + if err != nil { + return &types.UsersResponse{ + Success: false, + Message: "登出失败", + Data: err.Error(), + Code: 200, + }, nil + } + if exists == 0 { + return &types.UsersResponse{ + Success: false, + Message: "token不存在", + Data: nil, + Code: 200, + }, nil + } + + // 删除token + err = l.svcCtx.RedisClient.Del(l.ctx, token).Err() + if err != nil { + return &types.UsersResponse{ + Success: false, + Message: "登出失败", + Data: err.Error(), + Code: 200, + }, nil + } + + return &types.UsersResponse{ + Success: true, + Message: "登出成功", + Data: nil, + Code: 200, + }, nil +} diff --git a/backend/usercenter/api/internal/logic/user/user_register_logic.go b/backend/usercenter/api/internal/logic/user/user_register_logic.go new file mode 100644 index 0000000..ff0c034 --- /dev/null +++ b/backend/usercenter/api/internal/logic/user/user_register_logic.go @@ -0,0 +1,147 @@ +package user + +import ( + "context" + "regexp" + + "backend/usercenter/api/internal/svc" + "backend/usercenter/api/internal/types" + "backend/usercenter/orm" + "backend/utils" + + "github.com/zeromicro/go-zero/core/logx" + "gorm.io/gorm" + // github.com/spf13/cast // 类型转换 + // github.com/golang-module/carbon/v2 // 日期时间处理 + // github.com/jinzhu/copier/v2 // 结构体复制 + // orm 目录下的 orm 包 +) + +type UserRegisterLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewUserRegisterLogic(ctx context.Context, svcCtx *svc.ServiceContext) *UserRegisterLogic { + return &UserRegisterLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *UserRegisterLogic) UserRegister(req *types.RegisterRequest) (resp *types.UsersResponse, err error) { + // 1. 检查用户名是否符合要求 + if len(req.Username) < 4 || len(req.Username) > 16 { + return &types.UsersResponse{ + Success: false, + Message: "用户名长度不能小于4位或大于16位", + Data: nil, + Code: 200, + }, nil + } + + // 1.1 正则检查用户名是否符合要求 + usernameRegex := regexp.MustCompile(`^[a-zA-Z0-9]+$`) + if !usernameRegex.MatchString(req.Username) { + return &types.UsersResponse{ + Success: false, + Message: "用户名只能包含字母和数字", + Data: nil, + Code: 200, + }, nil + } + + // 1. 检查用户名,手机号是否存在 + user := new(orm.User) + result := l.svcCtx.Db.Where("name = ? or phone = ?", req.Username, req.Phone).First(user) + if result.Error != nil && result.Error != gorm.ErrRecordNotFound { + return &types.UsersResponse{ + Success: false, + Message: result.Error.Error(), + Data: nil, + Code: 200, + }, nil + } + if result.RowsAffected > 0 { + return &types.UsersResponse{ + Success: false, + Message: "用户名或手机号已存在", + Data: nil, + Code: 200, + }, nil + } + + // 2. 检查密码是否符合要求 + if len(req.Password) < 6 || len(req.Password) > 16 { + return &types.UsersResponse{ + Success: false, + Message: "密码长度不能小于6位或大于16位", + Data: nil, + Code: 200, + }, nil + } + + // 2.1 检查手机号是否符合要求 + if len(req.Phone) != 11 { + return &types.UsersResponse{ + Success: false, + Message: "手机号长度不正确", + Data: nil, + Code: 200, + }, nil + } + // 2.2 正则检查手机号是否符合要求 + phoneRegex := regexp.MustCompile(`^1[3-9]\d{9}$`) + if !phoneRegex.MatchString(req.Phone) { + return &types.UsersResponse{ + Success: false, + Message: "手机号格式不正确", + Data: nil, + Code: 200, + }, nil + } + + // 3. 加密密码,使用utils.Bcrypt.GenerateFromPassword + hashedPassword, err := utils.HashPassword(req.Password) + if err != nil { + return &types.UsersResponse{ + Success: false, + Message: err.Error(), + Data: nil, + Code: 200, + }, nil + } + user.Password = string(hashedPassword) + // 如果其他字段有值,则赋值 + if req.Email != "" { + user.Email = req.Email + } + + // 4. 创建用户 + user.Name = req.Username + user.Password = hashedPassword + user.Phone = req.Phone + + // 4. 创建用户 + result = l.svcCtx.Db.Create(user) + if result.Error != nil { + return &types.UsersResponse{ + Success: false, + Message: result.Error.Error(), + Data: nil, + Code: 200, + }, nil + } + + // 5. 返回用户信息 + resp = &types.UsersResponse{ + Success: true, + Message: "注册成功", + Data: user.ID, + Code: 200, + } + + return +} diff --git a/backend/usercenter/api/internal/svc/service_context.go b/backend/usercenter/api/internal/svc/service_context.go new file mode 100644 index 0000000..65b313c --- /dev/null +++ b/backend/usercenter/api/internal/svc/service_context.go @@ -0,0 +1,33 @@ +package svc + +import ( + "backend/usercenter/api/internal/config" + "fmt" + + "github.com/redis/go-redis/v9" + "gorm.io/driver/mysql" + "gorm.io/gorm" +) + +type ServiceContext struct { + Config config.Config + Db *gorm.DB + RedisClient *redis.Client +} + +func NewServiceContext(c config.Config) *ServiceContext { + dns := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=True&loc=Local", c.MySQL.User, c.MySQL.Password, c.MySQL.Host, c.MySQL.Port, c.MySQL.DBName) + db, err := gorm.Open(mysql.Open(dns), &gorm.Config{}) + if err != nil { + panic(err) + } + return &ServiceContext{ + Config: c, + Db: db, + RedisClient: redis.NewClient(&redis.Options{ + Addr: fmt.Sprintf("%s:%d", c.Redis.Host, c.Redis.Port), + Password: c.Redis.Pass, + DB: c.Redis.TkDB, + }), + } +} diff --git a/backend/usercenter/api/internal/types/types.go b/backend/usercenter/api/internal/types/types.go new file mode 100644 index 0000000..aa1b787 --- /dev/null +++ b/backend/usercenter/api/internal/types/types.go @@ -0,0 +1,38 @@ +// Code generated by goctl. DO NOT EDIT. +// goctl 1.8.4 + +package types + +type CommonResponse struct { + Code int `json:"code"` + Message string `json:"message"` + Data interface{} `json:"data"` + Success bool `json:"success"` +} + +type LoginRequest struct { + Identity string `json:"identity"` + Password string `json:"password"` +} + +type LogoutRequest struct { + Token string `json:"token"` +} + +type RegisterRequest struct { + Username string `json:"username"` + Password string `json:"password"` + Email string `json:"email,optional"` + Phone string `json:"phone"` + Nickname string `json:"nickname,optional"` + Avatar string `json:"avatar,optional"` + Role string `json:"role,optional"` + Status string `json:"status,optional"` +} + +type UsersResponse struct { + Code int `json:"code"` + Message string `json:"message"` + Data interface{} `json:"data"` + Success bool `json:"success"` +} diff --git a/backend/usercenter/api/usercenter.api b/backend/usercenter/api/usercenter.api new file mode 100644 index 0000000..19529d4 --- /dev/null +++ b/backend/usercenter/api/usercenter.api @@ -0,0 +1,23 @@ +syntax = "v1" + +// 用户管理 +import "apis/user.api" + +// 公共响应 +type CommonResponse { + Code int `json:"code"` + Message string `json:"message"` + Data interface{} `json:"data"` + Success bool `json:"success"` +} + +@server ( + // jwt: Auth + // middleware: CheckPermission + group: health +) +service usercenter-api { + @handler UsercenterPingHandler + get /ping returns (CommonResponse) +} + diff --git a/backend/usercenter/api/usercenter.go b/backend/usercenter/api/usercenter.go new file mode 100644 index 0000000..bd1ad70 --- /dev/null +++ b/backend/usercenter/api/usercenter.go @@ -0,0 +1,31 @@ +package main + +import ( + "flag" + "fmt" + + "backend/usercenter/api/internal/config" + "backend/usercenter/api/internal/handler" + "backend/usercenter/api/internal/svc" + + "github.com/zeromicro/go-zero/core/conf" + "github.com/zeromicro/go-zero/rest" +) + +var configFile = flag.String("f", "etc/usercenter-api.yaml", "the config file") + +func main() { + flag.Parse() + + var c config.Config + conf.MustLoad(*configFile, &c) + + server := rest.MustNewServer(c.RestConf) + defer server.Stop() + + ctx := svc.NewServiceContext(c) + handler.RegisterHandlers(server, ctx) + + fmt.Printf("Starting server at %s:%d...\n", c.Host, c.Port) + server.Start() +} diff --git a/backend/usercenter/cmd/migrate.go b/backend/usercenter/cmd/migrate.go deleted file mode 100644 index 9db950c..0000000 --- a/backend/usercenter/cmd/migrate.go +++ /dev/null @@ -1,7 +0,0 @@ -package main - -import "backend/usercenter/orm" - -func main() { - orm.AutoMigrate() -} diff --git a/backend/usercenter/internal/logic/loginlogic.go b/backend/usercenter/internal/logic/loginlogic.go deleted file mode 100644 index eecf6dd..0000000 --- a/backend/usercenter/internal/logic/loginlogic.go +++ /dev/null @@ -1,30 +0,0 @@ -package logic - -import ( - "context" - - "backend/usercenter/internal/svc" - "backend/usercenter/pb/usercenter" - - "github.com/zeromicro/go-zero/core/logx" -) - -type LoginLogic struct { - ctx context.Context - svcCtx *svc.ServiceContext - logx.Logger -} - -func NewLoginLogic(ctx context.Context, svcCtx *svc.ServiceContext) *LoginLogic { - return &LoginLogic{ - ctx: ctx, - svcCtx: svcCtx, - Logger: logx.WithContext(ctx), - } -} - -func (l *LoginLogic) Login(in *usercenter.LoginRequest) (*usercenter.LoginResponse, error) { - // todo: add your logic here and delete this line - - return &usercenter.LoginResponse{}, nil -} diff --git a/backend/usercenter/internal/logic/registerlogic.go b/backend/usercenter/internal/logic/registerlogic.go deleted file mode 100644 index a37a625..0000000 --- a/backend/usercenter/internal/logic/registerlogic.go +++ /dev/null @@ -1,30 +0,0 @@ -package logic - -import ( - "context" - - "backend/usercenter/internal/svc" - "backend/usercenter/pb/usercenter" - - "github.com/zeromicro/go-zero/core/logx" -) - -type RegisterLogic struct { - ctx context.Context - svcCtx *svc.ServiceContext - logx.Logger -} - -func NewRegisterLogic(ctx context.Context, svcCtx *svc.ServiceContext) *RegisterLogic { - return &RegisterLogic{ - ctx: ctx, - svcCtx: svcCtx, - Logger: logx.WithContext(ctx), - } -} - -func (l *RegisterLogic) Register(in *usercenter.RegisterRequest) (*usercenter.RegisterResponse, error) { - // todo: add your logic here and delete this line - - return &usercenter.RegisterResponse{}, nil -} diff --git a/backend/usercenter/internal/svc/servicecontext.go b/backend/usercenter/internal/svc/servicecontext.go deleted file mode 100644 index f6ff5d4..0000000 --- a/backend/usercenter/internal/svc/servicecontext.go +++ /dev/null @@ -1,13 +0,0 @@ -package svc - -import "backend/usercenter/internal/config" - -type ServiceContext struct { - Config config.Config -} - -func NewServiceContext(c config.Config) *ServiceContext { - return &ServiceContext{ - Config: c, - } -} diff --git a/backend/usercenter/orm/base.go b/backend/usercenter/orm/base.go index 72369b8..8dcaac8 100644 --- a/backend/usercenter/orm/base.go +++ b/backend/usercenter/orm/base.go @@ -3,9 +3,9 @@ package orm import "time" type BaseModel struct { - CreatedAt time.Time `gorm:"autoCreateTime:milli"` - UpdatedAt time.Time `gorm:"autoUpdateTime:milli"` - DeletedAt time.Time `gorm:"index"` + CreatedAt time.Time `gorm:"autoCreateTime:milli"` + UpdatedAt time.Time `gorm:"autoUpdateTime:milli"` + DeletedAt *time.Time `gorm:"index"` // 使用指针类型支持软删除 CreatedBy string UpdatedBy string DeletedBy string diff --git a/backend/usercenter/orm/userType.go b/backend/usercenter/orm/userType.go index d0ea9c6..a9f19ea 100644 --- a/backend/usercenter/orm/userType.go +++ b/backend/usercenter/orm/userType.go @@ -6,17 +6,17 @@ import ( ) type User struct { - ID uint `gorm:"primaryKey"` - Name string // 用户名 - Email string // 邮箱 - Age uint8 // 年龄 - Birthday time.Time // 生日 - Phone string // 手机号 - Password string // 密码 - Status int // 状态 - Role int // 角色 - Avatar string // 头像 - Introduction string // 简介 - Department string // 部门 - BaseModel // 基础模型 包含CreatedAt、UpdatedAt、DeletedAt、CreatedBy、UpdatedBy、DeletedBy + ID uint `gorm:"primaryKey"` + Name string // 用户名 + Email string // 邮箱 + Age uint8 // 年龄 + Birthday *time.Time // 生日 (使用指针类型允许NULL值) + Phone string // 手机号 + Password string // 密码 + Status int // 状态 + Role int // 角色 + Avatar string // 头像 + Introduction string // 简介 + Department string // 部门 + BaseModel // 基础模型 包含CreatedAt、UpdatedAt、DeletedAt、CreatedBy、UpdatedBy、DeletedBy } diff --git a/backend/usercenter/redis/data/appendonlydir/appendonly.aof.1.base.rdb b/backend/usercenter/redis/data/appendonlydir/appendonly.aof.1.base.rdb deleted file mode 100644 index 9171b15fae3e9e7d81556e9cfdd12f5b508df740..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 88 zcmWG?b@2=~FfcUw#aWb^l3A= 0 { + return nil, errors.New("用户已存在") + } + + // 2. 检查手机号是否存在 + res = l.svcCtx.UsercenterDB.First(user, "phone = ?", in.Mobile) + if res.Error != nil && res.Error != gorm.ErrRecordNotFound { + return nil, errors.New(res.Error.Error()) + } + if res.RowsAffected > 0 { + return nil, errors.New("手机号已存在") + } + // 3. 检查用户名是否符合要求 + if len(in.Username) < 4 || len(in.Username) > 16 { + return nil, errors.New("用户名长度必须在4-16之间") + } + // 4. 检查密码是否符合要求 + if len(in.Password) < 6 || len(in.Password) > 16 { + return nil, errors.New("密码长度必须在6-16之间") + } + // 5. 检查手机号是否符合要求 + if len(in.Mobile) != 11 { + return nil, errors.New("手机号长度必须为11位") + } + // 6. 正则检查手机号 + if !regexp.MustCompile(`^1[3-9]\d{9}$`).MatchString(in.Mobile) { + return nil, errors.New("手机号格式不正确") + } + // 检查完成,创建用户 + // 密码加盐, utils.bcrypt.HashPassword + salt, err := utils.HashPassword(in.Password) + if err != nil { + return nil, errors.New(err.Error()) + } + user.Name = in.Username + user.Phone = in.Mobile + user.Password = salt + + res = l.svcCtx.UsercenterDB.Create(user) + if res.Error != nil { + return nil, errors.New(res.Error.Error()) + } + // 创建成功,返回成功 + return &usercenter.RegisterResponse{ + UserId: strconv.FormatUint(uint64(user.ID), 10), + Message: "注册成功", + }, nil +} diff --git a/backend/usercenter/internal/logic/updateprofilelogic.go b/backend/usercenter/rpc/internal/logic/updateprofilelogic.go similarity index 88% rename from backend/usercenter/internal/logic/updateprofilelogic.go rename to backend/usercenter/rpc/internal/logic/updateprofilelogic.go index 6d6822f..f82c83d 100644 --- a/backend/usercenter/internal/logic/updateprofilelogic.go +++ b/backend/usercenter/rpc/internal/logic/updateprofilelogic.go @@ -3,8 +3,8 @@ package logic import ( "context" - "backend/usercenter/internal/svc" - "backend/usercenter/pb/usercenter" + "backend/usercenter/rpc/internal/svc" + "backend/usercenter/rpc/pb/usercenter" "github.com/zeromicro/go-zero/core/logx" ) diff --git a/backend/usercenter/internal/server/usercenterserver.go b/backend/usercenter/rpc/internal/server/usercenterserver.go similarity index 94% rename from backend/usercenter/internal/server/usercenterserver.go rename to backend/usercenter/rpc/internal/server/usercenterserver.go index 3ac5a4c..bcf250d 100644 --- a/backend/usercenter/internal/server/usercenterserver.go +++ b/backend/usercenter/rpc/internal/server/usercenterserver.go @@ -7,9 +7,9 @@ package server import ( "context" - "backend/usercenter/internal/logic" - "backend/usercenter/internal/svc" - "backend/usercenter/pb/usercenter" + "backend/usercenter/rpc/internal/logic" + "backend/usercenter/rpc/internal/svc" + "backend/usercenter/rpc/pb/usercenter" ) type UsercenterServer struct { diff --git a/backend/usercenter/rpc/internal/svc/servicecontext.go b/backend/usercenter/rpc/internal/svc/servicecontext.go new file mode 100644 index 0000000..418e6c8 --- /dev/null +++ b/backend/usercenter/rpc/internal/svc/servicecontext.go @@ -0,0 +1,39 @@ +package svc + +import ( + // 引入配置文件 + "backend/usercenter/rpc/internal/config" + // 引入GORM + "gorm.io/driver/mysql" + "gorm.io/gorm" + + // 引入redis + "fmt" + + "github.com/redis/go-redis/v9" +) + +// 服务上下文 +type ServiceContext struct { + Config config.Config + UsercenterDB *gorm.DB + RedisClient *redis.Client // 注意类型 +} + +func NewServiceContext(c config.Config) *ServiceContext { + usercenterDB, err := gorm.Open(mysql.Open(c.MySQL.DSN()), &gorm.Config{}) + if err != nil { + panic(err) + } + redisClient := redis.NewClient(&redis.Options{ + Addr: fmt.Sprintf("%s:%d", c.RedisGo.Host, c.RedisGo.Port), + Password: c.RedisGo.Pass, // 或 Password,取决于你的 config.go/yaml + DB: c.RedisGo.DB, + }) + + return &ServiceContext{ + Config: c, + UsercenterDB: usercenterDB, + RedisClient: redisClient, + } +} diff --git a/backend/usercenter/pb/usercenter/usercenter.pb.go b/backend/usercenter/rpc/pb/usercenter/usercenter.pb.go similarity index 100% rename from backend/usercenter/pb/usercenter/usercenter.pb.go rename to backend/usercenter/rpc/pb/usercenter/usercenter.pb.go diff --git a/backend/usercenter/pb/usercenter/usercenter_grpc.pb.go b/backend/usercenter/rpc/pb/usercenter/usercenter_grpc.pb.go similarity index 100% rename from backend/usercenter/pb/usercenter/usercenter_grpc.pb.go rename to backend/usercenter/rpc/pb/usercenter/usercenter_grpc.pb.go diff --git a/backend/usercenter/usercenter.go b/backend/usercenter/rpc/usercenter.go similarity index 82% rename from backend/usercenter/usercenter.go rename to backend/usercenter/rpc/usercenter.go index e422a18..7aae5c9 100644 --- a/backend/usercenter/usercenter.go +++ b/backend/usercenter/rpc/usercenter.go @@ -4,10 +4,10 @@ import ( "flag" "fmt" - "backend/usercenter/internal/config" - "backend/usercenter/internal/server" - "backend/usercenter/internal/svc" - "backend/usercenter/pb/usercenter" + "backend/usercenter/rpc/internal/config" + "backend/usercenter/rpc/internal/server" + "backend/usercenter/rpc/internal/svc" + "backend/usercenter/rpc/pb/usercenter" "github.com/zeromicro/go-zero/core/conf" "github.com/zeromicro/go-zero/core/service" diff --git a/backend/usercenter/usercenter.proto b/backend/usercenter/rpc/usercenter.proto similarity index 100% rename from backend/usercenter/usercenter.proto rename to backend/usercenter/rpc/usercenter.proto diff --git a/backend/usercenter/usercenterclient/usercenter.go b/backend/usercenter/rpc/usercenterclient/usercenter.go similarity index 99% rename from backend/usercenter/usercenterclient/usercenter.go rename to backend/usercenter/rpc/usercenterclient/usercenter.go index 92d088d..0e65691 100644 --- a/backend/usercenter/usercenterclient/usercenter.go +++ b/backend/usercenter/rpc/usercenterclient/usercenter.go @@ -7,7 +7,7 @@ package usercenterclient import ( "context" - "backend/usercenter/pb/usercenter" + "backend/usercenter/rpc/pb/usercenter" "github.com/zeromicro/go-zero/zrpc" "google.golang.org/grpc" diff --git a/backend/utils/README.md b/backend/utils/README.md new file mode 100644 index 0000000..06b926e --- /dev/null +++ b/backend/utils/README.md @@ -0,0 +1,362 @@ +# JWT 工具使用说明 + +这是一个基于 Go-Zero 框架的 JWT 工具,支持 token 的生成、验证、刷新和管理功能,并将 token 存储在 Redis 中。 + +## 功能特性 + +- ✅ JWT token 生成和验证 +- ✅ Redis 存储管理 +- ✅ Token 刷新机制 +- ✅ 用户多 token 管理 +- ✅ 批量 token 操作 +- ✅ 完整的错误处理 +- ✅ 性能优化 +- ✅ 完整的测试覆盖 + +## 依赖说明 + +```bash +# 主要依赖 +go get github.com/golang-jwt/jwt/v5 +go get github.com/redis/go-redis/v9 + +# 测试依赖 +go get github.com/stretchr/testify +``` + +## 配置说明 + +### 1. 配置文件 (usercenter.yaml) + +```yaml +Auth: + AccessSecret: "your-secret-key" + AccessExpire: 604800 # 7天过期时间(秒) +``` + +### 2. 配置结构体 (config.go) + +```go +type Auth struct { + AccessSecret string // JWT 密钥 + AccessExpire int64 // JWT 过期时间(秒) +} +``` + +## 基本使用方法 + +### 1. 初始化 JWT 工具 + +```go +import ( + "backend/utils" + "github.com/redis/go-redis/v9" +) + +// 创建 Redis 客户端 +redisClient := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", + DB: 0, +}) + +// 创建 JWT 工具实例 +jwtUtil := utils.NewJWTUtil( + "your-secret-key", // 访问密钥 + 7*24*60*60, // 过期时间(秒) + redisClient, // Redis 客户端 +) +``` + +### 2. 生成 Token + +```go +ctx := context.Background() +userID := int64(123) +username := "john_doe" + +token, err := jwtUtil.GenerateToken(ctx, userID, username) +if err != nil { + log.Printf("生成 token 失败: %v", err) + return +} + +fmt.Printf("生成的 token: %s\n", token) +``` + +### 3. 验证 Token + +```go +claims, err := jwtUtil.ValidateToken(ctx, token) +if err != nil { + log.Printf("验证 token 失败: %v", err) + return +} + +fmt.Printf("用户ID: %d, 用户名: %s\n", claims.UserID, claims.Username) +``` + +### 4. 刷新 Token + +```go +newToken, err := jwtUtil.RefreshToken(ctx, oldToken) +if err != nil { + log.Printf("刷新 token 失败: %v", err) + return +} + +fmt.Printf("新的 token: %s\n", newToken) +``` + +### 5. 删除 Token (登出) + +```go +err := jwtUtil.DeleteToken(ctx, token) +if err != nil { + log.Printf("删除 token 失败: %v", err) + return +} + +fmt.Println("登出成功") +``` + +## 高级功能 + +### 1. 管理用户多个 Token + +```go +// 获取用户所有 token +tokens, err := jwtUtil.GetUserTokens(ctx, userID) +if err != nil { + log.Printf("获取用户 token 失败: %v", err) + return +} + +fmt.Printf("用户拥有 %d 个 token\n", len(tokens)) + +// 删除用户所有 token (强制登出) +err = jwtUtil.DeleteAllUserTokens(ctx, userID) +if err != nil { + log.Printf("删除用户所有 token 失败: %v", err) + return +} +``` + +### 2. 检查 Token 是否存在 + +```go +exists, err := jwtUtil.TokenExists(ctx, token) +if err != nil { + log.Printf("检查 token 失败: %v", err) + return +} + +if exists { + fmt.Println("Token 有效") +} else { + fmt.Println("Token 不存在或已过期") +} +``` + +### 3. 解析 Token (不验证签名) + +```go +claims, err := jwtUtil.ParseTokenUnverified(token) +if err != nil { + log.Printf("解析 token 失败: %v", err) + return +} + +fmt.Printf("Token 中的用户ID: %d\n", claims.UserID) +``` + +## 在 Go-Zero 服务中集成 + +### 1. 更新服务上下文 + +```go +// internal/svc/servicecontext.go +type ServiceContext struct { + Config config.Config + JWTUtil *utils.JWTUtil + // ... 其他字段 +} + +func NewServiceContext(c config.Config) *ServiceContext { + // 创建 Redis 客户端 + redisClient := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + DB: 0, + }) + + // 创建 JWT 工具 + jwtUtil := utils.NewJWTUtil( + c.Auth.AccessSecret, + c.Auth.AccessExpire, + redisClient, + ) + + return &ServiceContext{ + Config: c, + JWTUtil: jwtUtil, + } +} +``` + +### 2. 在登录逻辑中使用 + +```go +// internal/logic/loginlogic.go +func (l *LoginLogic) Login(in *pb.LoginRequest) (*pb.LoginResponse, error) { + // 验证用户凭证... + + // 生成 JWT token + token, err := l.svcCtx.JWTUtil.GenerateToken( + l.ctx, + user.ID, + user.Username, + ) + if err != nil { + return nil, err + } + + return &pb.LoginResponse{ + Token: token, + User: user, + }, nil +} +``` + +### 3. 在需要认证的逻辑中使用 + +```go +// internal/logic/getprofilelogic.go +func (l *GetProfileLogic) GetProfile(in *pb.GetProfileRequest) (*pb.GetProfileResponse, error) { + // 验证 token + claims, err := l.svcCtx.JWTUtil.ValidateToken(l.ctx, in.Token) + if err != nil { + return nil, errors.New("无效的 token") + } + + // 使用 claims 中的用户信息 + userID := claims.UserID + + // 获取用户信息... + + return &pb.GetProfileResponse{ + User: user, + }, nil +} +``` + +## 中间件示例 + +```go +// JWT 认证中间件 +func JWTAuthMiddleware(jwtUtil *utils.JWTUtil) func(next http.HandlerFunc) http.HandlerFunc { + return func(next http.HandlerFunc) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + // 从 Header 中获取 token + tokenString := r.Header.Get("Authorization") + if tokenString == "" { + http.Error(w, "缺少 token", http.StatusUnauthorized) + return + } + + // 去掉 "Bearer " 前缀 + if strings.HasPrefix(tokenString, "Bearer ") { + tokenString = tokenString[7:] + } + + // 验证 token + claims, err := jwtUtil.ValidateToken(r.Context(), tokenString) + if err != nil { + http.Error(w, "无效的 token", http.StatusUnauthorized) + return + } + + // 将用户信息存储到上下文中 + ctx := context.WithValue(r.Context(), "userID", claims.UserID) + ctx = context.WithValue(ctx, "username", claims.Username) + + // 继续处理请求 + next(w, r.WithContext(ctx)) + } + } +} +``` + +## 错误处理 + +JWT 工具返回的错误信息包括: + +- `生成 token 失败`: token 生成过程中的错误 +- `存储 token 到 Redis 失败`: Redis 存储错误 +- `解析 token 失败`: token 格式错误或签名验证失败 +- `无效的 token`: token 无效或已过期 +- `token 已过期或不存在`: Redis 中不存在该 token +- `token 中的用户ID与 Redis 中的不匹配`: 数据不一致错误 + +## 性能优化建议 + +1. **Redis 连接池**: 使用 Redis 连接池避免频繁创建连接 +2. **Token 缓存**: 对于高频访问的 token,可以考虑内存缓存 +3. **批量操作**: 使用批量 Redis 操作提高性能 +4. **异步删除**: 过期 token 的清理可以异步进行 + +## 安全建议 + +1. **密钥管理**: 使用强密钥并定期轮换 +2. **HTTPS**: 始终在 HTTPS 环境中传输 token +3. **过期时间**: 设置合理的 token 过期时间 +4. **Redis 安全**: 确保 Redis 访问安全 +5. **日志记录**: 记录认证相关的操作日志 + +## 测试 + +运行单元测试: + +```bash +cd backend/utils +go test -v +``` + +运行基准测试: + +```bash +go test -bench=. +``` + +## 常见问题 + +### Q: 如何处理 token 过期? + +A: 使用 `RefreshToken` 方法刷新 token,或者重新登录获取新的 token。 + +### Q: 如何实现强制登出? + +A: 使用 `DeleteAllUserTokens` 方法删除用户的所有 token。 + +### Q: Redis 连接失败怎么处理? + +A: 检查 Redis 服务是否正常运行,并确保连接参数正确。 + +### Q: Token 验证失败的常见原因? + +A: + +- Token 格式错误 +- 签名密钥不匹配 +- Token 已过期 +- Redis 中不存在该 token +- 网络连接问题 + +## 更新日志 + +### v1.0.0 + +- 基础 JWT 功能实现 +- Redis 存储支持 +- Token 刷新机制 +- 用户多 token 管理 +- 完整的测试覆盖 diff --git a/backend/utils/bcrypt.go b/backend/utils/bcrypt.go new file mode 100644 index 0000000..cc4f65e --- /dev/null +++ b/backend/utils/bcrypt.go @@ -0,0 +1,119 @@ +// 密码加密 +package utils + +import ( + "errors" + + "golang.org/x/crypto/bcrypt" +) + +const ( + // DefaultCost 默认加密成本,平衡安全性和性能 + DefaultCost = bcrypt.DefaultCost // 通常是10 + // MinCost 最小加密成本 + MinCost = bcrypt.MinCost // 4 + // MaxCost 最大加密成本 + MaxCost = bcrypt.MaxCost // 31 +) + +var ( + // ErrPasswordTooLong 密码过长错误 + ErrPasswordTooLong = errors.New("password is too long") + // ErrPasswordEmpty 密码为空错误 + ErrPasswordEmpty = errors.New("password cannot be empty") + // ErrInvalidCost 加密成本无效错误 + ErrInvalidCost = errors.New("invalid cost value") +) + +// HashPassword 使用bcrypt对密码进行加密 +// password: 原始密码 +// 返回加密后的密码哈希值和错误信息 +func HashPassword(password string) (string, error) { + return HashPasswordWithCost(password, DefaultCost) +} + +// HashPasswordWithCost 使用指定成本对密码进行加密 +// password: 原始密码 +// cost: 加密成本 (4-31),成本越高越安全但速度越慢 +// 返回加密后的密码哈希值和错误信息 +func HashPasswordWithCost(password string, cost int) (string, error) { + // 验证输入 + if password == "" { + return "", ErrPasswordEmpty + } + + // bcrypt有72字节的限制 + if len(password) > 72 { + return "", ErrPasswordTooLong + } + + // 验证成本值 + if cost < MinCost || cost > MaxCost { + return "", ErrInvalidCost + } + + // 生成密码哈希 + hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), cost) + if err != nil { + return "", err + } + + return string(hashedPassword), nil +} + +// CheckPassword 验证密码是否正确 +// hashedPassword: 存储的密码哈希值 +// password: 用户输入的原始密码 +// 返回验证结果 +func CheckPassword(hashedPassword, password string) bool { + // 验证输入 + if hashedPassword == "" || password == "" { + return false + } + + // 比较密码 + err := bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(password)) + return err == nil +} + +// CheckPasswordWithError 验证密码是否正确,返回详细错误信息 +// hashedPassword: 存储的密码哈希值 +// password: 用户输入的原始密码 +// 返回验证结果和错误信息 +func CheckPasswordWithError(hashedPassword, password string) error { + // 验证输入 + if hashedPassword == "" { + return errors.New("hashed password cannot be empty") + } + if password == "" { + return ErrPasswordEmpty + } + + // 比较密码 + return bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(password)) +} + +// GetCost 获取密码哈希的加密成本 +// hashedPassword: 密码哈希值 +// 返回加密成本和错误信息 +func GetCost(hashedPassword string) (int, error) { + if hashedPassword == "" { + return 0, errors.New("hashed password cannot be empty") + } + + cost, err := bcrypt.Cost([]byte(hashedPassword)) + return cost, err +} + +// NeedsRehash 检查密码是否需要重新哈希(当前成本低于推荐成本时) +// hashedPassword: 密码哈希值 +// preferredCost: 推荐的加密成本 +// 返回是否需要重新哈希 +func NeedsRehash(hashedPassword string, preferredCost int) (bool, error) { + currentCost, err := GetCost(hashedPassword) + if err != nil { + return false, err + } + + return currentCost < preferredCost, nil +} diff --git a/backend/utils/bcrypt_test.go b/backend/utils/bcrypt_test.go new file mode 100644 index 0000000..450c2f9 --- /dev/null +++ b/backend/utils/bcrypt_test.go @@ -0,0 +1,166 @@ +package utils + +import ( + "testing" +) + +func TestHashPassword(t *testing.T) { + password := "testpassword123" + + // 测试基本加密功能 + hashedPassword, err := HashPassword(password) + if err != nil { + t.Fatalf("HashPassword failed: %v", err) + } + + if hashedPassword == "" { + t.Fatal("HashedPassword should not be empty") + } + + if hashedPassword == password { + t.Fatal("HashedPassword should not equal original password") + } +} + +func TestHashPasswordWithCost(t *testing.T) { + password := "testpassword123" + cost := 12 + + // 测试指定成本的加密 + hashedPassword, err := HashPasswordWithCost(password, cost) + if err != nil { + t.Fatalf("HashPasswordWithCost failed: %v", err) + } + + // 验证成本 + actualCost, err := GetCost(hashedPassword) + if err != nil { + t.Fatalf("GetCost failed: %v", err) + } + + if actualCost != cost { + t.Fatalf("Expected cost %d, got %d", cost, actualCost) + } +} + +func TestCheckPassword(t *testing.T) { + password := "testpassword123" + wrongPassword := "wrongpassword" + + // 加密密码 + hashedPassword, err := HashPassword(password) + if err != nil { + t.Fatalf("HashPassword failed: %v", err) + } + + // 测试正确密码验证 + if !CheckPassword(hashedPassword, password) { + t.Fatal("CheckPassword should return true for correct password") + } + + // 测试错误密码验证 + if CheckPassword(hashedPassword, wrongPassword) { + t.Fatal("CheckPassword should return false for wrong password") + } +} + +func TestCheckPasswordWithError(t *testing.T) { + password := "testpassword123" + wrongPassword := "wrongpassword" + + // 加密密码 + hashedPassword, err := HashPassword(password) + if err != nil { + t.Fatalf("HashPassword failed: %v", err) + } + + // 测试正确密码验证 + err = CheckPasswordWithError(hashedPassword, password) + if err != nil { + t.Fatalf("CheckPasswordWithError should return nil for correct password, got: %v", err) + } + + // 测试错误密码验证 + err = CheckPasswordWithError(hashedPassword, wrongPassword) + if err == nil { + t.Fatal("CheckPasswordWithError should return error for wrong password") + } +} + +func TestPasswordValidation(t *testing.T) { + // 测试空密码 + _, err := HashPassword("") + if err != ErrPasswordEmpty { + t.Fatalf("Expected ErrPasswordEmpty, got: %v", err) + } + + // 测试过长密码 (> 72 bytes) + longPassword := make([]byte, 73) + for i := range longPassword { + longPassword[i] = 'a' + } + + _, err = HashPassword(string(longPassword)) + if err != ErrPasswordTooLong { + t.Fatalf("Expected ErrPasswordTooLong, got: %v", err) + } + + // 测试无效成本 + _, err = HashPasswordWithCost("test", 3) // 低于 MinCost + if err != ErrInvalidCost { + t.Fatalf("Expected ErrInvalidCost for low cost, got: %v", err) + } + + _, err = HashPasswordWithCost("test", 32) // 高于 MaxCost + if err != ErrInvalidCost { + t.Fatalf("Expected ErrInvalidCost for high cost, got: %v", err) + } +} + +func TestNeedsRehash(t *testing.T) { + password := "testpassword123" + + // 使用较低成本加密 + lowCostPassword, err := HashPasswordWithCost(password, 8) + if err != nil { + t.Fatalf("HashPasswordWithCost failed: %v", err) + } + + // 检查是否需要重新哈希 + needsRehash, err := NeedsRehash(lowCostPassword, 12) + if err != nil { + t.Fatalf("NeedsRehash failed: %v", err) + } + + if !needsRehash { + t.Fatal("Should need rehash when current cost is lower than preferred cost") + } + + // 使用相同成本检查 + needsRehash, err = NeedsRehash(lowCostPassword, 8) + if err != nil { + t.Fatalf("NeedsRehash failed: %v", err) + } + + if needsRehash { + t.Fatal("Should not need rehash when current cost equals preferred cost") + } +} + +func TestEmptyInputs(t *testing.T) { + // 测试空哈希密码 + if CheckPassword("", "password") { + t.Fatal("CheckPassword should return false for empty hash") + } + + // 测试空输入密码 + if CheckPassword("hash", "") { + t.Fatal("CheckPassword should return false for empty password") + } + + // 测试 GetCost 的空输入 + _, err := GetCost("") + if err == nil { + t.Fatal("GetCost should return error for empty hash") + } +} diff --git a/backend/utils/jwt.go b/backend/utils/jwt.go new file mode 100644 index 0000000..bc092e1 --- /dev/null +++ b/backend/utils/jwt.go @@ -0,0 +1,223 @@ +// 生成token,保存到redis,返回token,redis的key是token,value是user_id +// 过期时间由etc下 yaml文件配置 Auth.AccessExpire确定 +// 秘钥由etc下 yaml文件配置 Auth.AccessSecret确定 + +package utils + +import ( + "context" + "errors" + "fmt" + "strconv" + "time" + + "github.com/golang-jwt/jwt/v5" + "github.com/google/uuid" + "github.com/redis/go-redis/v9" +) + +// JWT 自定义声明 +type JWTClaims struct { + UserID int64 `json:"user_id"` + Username string `json:"username"` + ExString string `json:"ex_string"` + jwt.RegisteredClaims +} + +// JWT 工具结构体 +type JWTUtil struct { + AccessSecret string + AccessExpire int64 + RedisClient *redis.Client + TkStore bool +} + +// 创建新的 JWT 工具实例 +func NewJWTUtil(accessSecret string, accessExpire int64, redisClient *redis.Client, tkStore bool) *JWTUtil { + return &JWTUtil{ + AccessSecret: accessSecret, + AccessExpire: accessExpire, + RedisClient: redisClient, + TkStore: tkStore, + } +} + +// 生成 JWT token +func (j *JWTUtil) GenerateToken(ctx context.Context, userID int64, username string) (string, error) { + // 创建声明 + claims := &JWTClaims{ + UserID: userID, + Username: username, + RegisteredClaims: jwt.RegisteredClaims{ + ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Second * time.Duration(j.AccessExpire))), + IssuedAt: jwt.NewNumericDate(time.Now()), + NotBefore: jwt.NewNumericDate(time.Now()), + Issuer: "usercenter", + Subject: strconv.FormatInt(userID, 10), + }, + ExString: uuid.New().String(), + } + + // 使用 HS256 算法生成 token + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + tokenString, err := token.SignedString([]byte(j.AccessSecret)) + if err != nil { + return "", fmt.Errorf("生成 token 失败: %w", err) + } + + // 将 token 存储到 Redis,key 是 token,value 是 user_id + if j.TkStore { + err = j.RedisClient.Set(ctx, tokenString, userID, time.Duration(j.AccessExpire)*time.Second).Err() + if err != nil { + return "", fmt.Errorf("存储 token 到 Redis 失败: %w", err) + } + } + return tokenString, nil +} + +// 验证 JWT token +func (j *JWTUtil) ValidateToken(ctx context.Context, tokenString string) (*JWTClaims, error) { + // 解析 token + token, err := jwt.ParseWithClaims(tokenString, &JWTClaims{}, func(token *jwt.Token) (interface{}, error) { + // 验证签名方法 + if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { + return nil, fmt.Errorf("意外的签名方法: %v", token.Header["alg"]) + } + return []byte(j.AccessSecret), nil + }) + + if err != nil { + return nil, fmt.Errorf("解析 token 失败: %w", err) + } + + // 验证 token 是否有效 + if !token.Valid { + return nil, errors.New("无效的 token") + } + + // 获取声明 + claims, ok := token.Claims.(*JWTClaims) + if !ok { + return nil, errors.New("无法解析 token 声明") + } + + // 验证 token 是否在 Redis 中存在 + userID, err := j.RedisClient.Get(ctx, tokenString).Result() + if err != nil { + if err == redis.Nil { + return nil, errors.New("token 已过期或不存在") + } + return nil, fmt.Errorf("从 Redis 获取 token 失败: %w", err) + } + + // 验证 Redis 中的 user_id 是否与 token 中的一致 + redisUserID, err := strconv.ParseInt(userID, 10, 64) + if err != nil { + return nil, fmt.Errorf("解析 Redis 中的 user_id 失败: %w", err) + } + + if redisUserID != claims.UserID { + return nil, errors.New("token 中的用户ID与 Redis 中的不匹配") + } + + return claims, nil +} + +// 刷新 token +func (j *JWTUtil) RefreshToken(ctx context.Context, tokenString string) (string, error) { + // 先验证当前 token + claims, err := j.ValidateToken(ctx, tokenString) + if err != nil { + return "", fmt.Errorf("验证旧 token 失败: %w", err) + } + + // 删除旧的 token + err = j.RedisClient.Del(ctx, tokenString).Err() + if err != nil { + return "", fmt.Errorf("删除旧 token 失败: %w", err) + } + + // 生成新的 token + return j.GenerateToken(ctx, claims.UserID, claims.Username) +} + +// 删除 token(登出) +func (j *JWTUtil) DeleteToken(ctx context.Context, tokenString string) error { + // 从 Redis 中删除 token + err := j.RedisClient.Del(ctx, tokenString).Err() + if err != nil { + return fmt.Errorf("从 Redis 删除 token 失败: %w", err) + } + return nil +} + +// 检查 token 是否存在 +func (j *JWTUtil) TokenExists(ctx context.Context, tokenString string) (bool, error) { + exists, err := j.RedisClient.Exists(ctx, tokenString).Result() + if err != nil { + return false, fmt.Errorf("检查 token 是否存在失败: %w", err) + } + return exists == 1, nil +} + +// 解析 token(不验证签名,用于获取基本信息) +func (j *JWTUtil) ParseTokenUnverified(tokenString string) (*JWTClaims, error) { + token, _, err := new(jwt.Parser).ParseUnverified(tokenString, &JWTClaims{}) + if err != nil { + return nil, fmt.Errorf("解析 token 失败: %w", err) + } + + claims, ok := token.Claims.(*JWTClaims) + if !ok { + return nil, errors.New("无法解析 token 声明") + } + + return claims, nil +} + +// 获取用户所有有效的 token(通过用户ID前缀搜索) +func (j *JWTUtil) GetUserTokens(ctx context.Context, userID int64) ([]string, error) { + // 使用 SCAN 命令搜索包含用户ID的 token + var tokens []string + iter := j.RedisClient.Scan(ctx, 0, "*", 0).Iterator() + + for iter.Next(ctx) { + key := iter.Val() + // 获取该 key 对应的 value + value, err := j.RedisClient.Get(ctx, key).Result() + if err != nil { + continue + } + + // 检查 value 是否是目标用户ID + if value == strconv.FormatInt(userID, 10) { + tokens = append(tokens, key) + } + } + + if err := iter.Err(); err != nil { + return nil, fmt.Errorf("搜索用户 token 失败: %w", err) + } + + return tokens, nil +} + +// 删除用户所有 token(强制登出) +func (j *JWTUtil) DeleteAllUserTokens(ctx context.Context, userID int64) error { + tokens, err := j.GetUserTokens(ctx, userID) + if err != nil { + return fmt.Errorf("获取用户 token 失败: %w", err) + } + + if len(tokens) == 0 { + return nil + } + + // 批量删除 token + err = j.RedisClient.Del(ctx, tokens...).Err() + if err != nil { + return fmt.Errorf("批量删除用户 token 失败: %w", err) + } + + return nil +} diff --git a/backend/utils/jwt_test.go b/backend/utils/jwt_test.go new file mode 100644 index 0000000..91483ec --- /dev/null +++ b/backend/utils/jwt_test.go @@ -0,0 +1,327 @@ +package utils + +import ( + "context" + "testing" + "time" + + "github.com/redis/go-redis/v9" + "github.com/stretchr/testify/assert" +) + +// 测试用的 Redis 客户端 +func setupTestRedis() *redis.Client { + return redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", + DB: 1, // 使用测试数据库 + }) +} + +// 测试 JWT 工具的基本功能 +func TestJWTUtil_BasicOperations(t *testing.T) { + redisClient := setupTestRedis() + defer redisClient.Close() + + // 清理测试数据 + redisClient.FlushDB(context.Background()) + + jwtUtil := NewJWTUtil("test-secret", 3600, redisClient) + ctx := context.Background() + + userID := int64(123) + username := "testuser" + + // 测试生成 token + token, err := jwtUtil.GenerateToken(ctx, userID, username) + assert.NoError(t, err) + assert.NotEmpty(t, token) + + // 测试验证 token + claims, err := jwtUtil.ValidateToken(ctx, token) + assert.NoError(t, err) + assert.Equal(t, userID, claims.UserID) + assert.Equal(t, username, claims.Username) + + // 测试 token 是否存在 + exists, err := jwtUtil.TokenExists(ctx, token) + assert.NoError(t, err) + assert.True(t, exists) + + // 测试解析 token(不验证签名) + unverifiedClaims, err := jwtUtil.ParseTokenUnverified(token) + assert.NoError(t, err) + assert.Equal(t, userID, unverifiedClaims.UserID) + assert.Equal(t, username, unverifiedClaims.Username) + + // 测试删除 token + err = jwtUtil.DeleteToken(ctx, token) + assert.NoError(t, err) + + // 验证 token 已被删除 + exists, err = jwtUtil.TokenExists(ctx, token) + assert.NoError(t, err) + assert.False(t, exists) +} + +// 测试 token 刷新功能 +func TestJWTUtil_RefreshToken(t *testing.T) { + redisClient := setupTestRedis() + defer redisClient.Close() + + redisClient.FlushDB(context.Background()) + + jwtUtil := NewJWTUtil("test-secret", 3600, redisClient) + ctx := context.Background() + + userID := int64(456) + username := "refreshuser" + + // 生成原始 token + oldToken, err := jwtUtil.GenerateToken(ctx, userID, username) + assert.NoError(t, err) + + // 刷新 token + newToken, err := jwtUtil.RefreshToken(ctx, oldToken) + assert.NoError(t, err) + assert.NotEmpty(t, newToken) + assert.NotEqual(t, oldToken, newToken) + + // 验证新 token + claims, err := jwtUtil.ValidateToken(ctx, newToken) + assert.NoError(t, err) + assert.Equal(t, userID, claims.UserID) + assert.Equal(t, username, claims.Username) + + // 验证旧 token 已失效 + _, err = jwtUtil.ValidateToken(ctx, oldToken) + assert.Error(t, err) +} + +// 测试获取和删除用户所有 token +func TestJWTUtil_UserTokens(t *testing.T) { + redisClient := setupTestRedis() + defer redisClient.Close() + + redisClient.FlushDB(context.Background()) + + jwtUtil := NewJWTUtil("test-secret", 3600, redisClient) + ctx := context.Background() + + userID := int64(789) + username := "multiuser" + + // 生成多个 token + var tokens []string + for i := 0; i < 3; i++ { + token, err := jwtUtil.GenerateToken(ctx, userID, username) + assert.NoError(t, err) + tokens = append(tokens, token) + } + + // 获取用户所有 token + userTokens, err := jwtUtil.GetUserTokens(ctx, userID) + assert.NoError(t, err) + assert.Equal(t, 3, len(userTokens)) + + // 删除用户所有 token + err = jwtUtil.DeleteAllUserTokens(ctx, userID) + assert.NoError(t, err) + + // 验证所有 token 已被删除 + for _, token := range tokens { + exists, err := jwtUtil.TokenExists(ctx, token) + assert.NoError(t, err) + assert.False(t, exists) + } +} + +// 测试无效 token +func TestJWTUtil_InvalidToken(t *testing.T) { + redisClient := setupTestRedis() + defer redisClient.Close() + + redisClient.FlushDB(context.Background()) + + jwtUtil := NewJWTUtil("test-secret", 3600, redisClient) + ctx := context.Background() + + // 测试无效 token + _, err := jwtUtil.ValidateToken(ctx, "invalid-token") + assert.Error(t, err) + + // 测试空 token + _, err = jwtUtil.ValidateToken(ctx, "") + assert.Error(t, err) +} + +// 测试过期 token +func TestJWTUtil_ExpiredToken(t *testing.T) { + redisClient := setupTestRedis() + defer redisClient.Close() + + redisClient.FlushDB(context.Background()) + + // 创建一个很短过期时间的 JWT 工具 + jwtUtil := NewJWTUtil("test-secret", 1, redisClient) // 1秒过期 + ctx := context.Background() + + userID := int64(999) + username := "expireduser" + + // 生成 token + token, err := jwtUtil.GenerateToken(ctx, userID, username) + assert.NoError(t, err) + + // 立即验证应该成功 + claims, err := jwtUtil.ValidateToken(ctx, token) + assert.NoError(t, err) + assert.Equal(t, userID, claims.UserID) + + // 等待 token 过期 + time.Sleep(2 * time.Second) + + // 验证过期 token 应该失败 + _, err = jwtUtil.ValidateToken(ctx, token) + assert.Error(t, err) +} + +// 测试不同签名密钥 +func TestJWTUtil_DifferentSecret(t *testing.T) { + redisClient := setupTestRedis() + defer redisClient.Close() + + redisClient.FlushDB(context.Background()) + + jwtUtil1 := NewJWTUtil("secret1", 3600, redisClient) + jwtUtil2 := NewJWTUtil("secret2", 3600, redisClient) + ctx := context.Background() + + userID := int64(111) + username := "secretuser" + + // 用第一个工具生成 token + token, err := jwtUtil1.GenerateToken(ctx, userID, username) + assert.NoError(t, err) + + // 用第一个工具验证应该成功 + claims, err := jwtUtil1.ValidateToken(ctx, token) + assert.NoError(t, err) + assert.Equal(t, userID, claims.UserID) + + // 用第二个工具验证应该失败 + _, err = jwtUtil2.ValidateToken(ctx, token) + assert.Error(t, err) +} + +// 基准测试:生成 token +func BenchmarkJWTUtil_GenerateToken(b *testing.B) { + redisClient := setupTestRedis() + defer redisClient.Close() + + redisClient.FlushDB(context.Background()) + + jwtUtil := NewJWTUtil("bench-secret", 3600, redisClient) + ctx := context.Background() + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, err := jwtUtil.GenerateToken(ctx, int64(i), "benchuser") + if err != nil { + b.Fatal(err) + } + } +} + +// 基准测试:验证 token +func BenchmarkJWTUtil_ValidateToken(b *testing.B) { + redisClient := setupTestRedis() + defer redisClient.Close() + + redisClient.FlushDB(context.Background()) + + jwtUtil := NewJWTUtil("bench-secret", 3600, redisClient) + ctx := context.Background() + + // 预先生成一些 token + tokens := make([]string, 1000) + for i := 0; i < 1000; i++ { + token, err := jwtUtil.GenerateToken(ctx, int64(i), "benchuser") + if err != nil { + b.Fatal(err) + } + tokens[i] = token + } + + b.ResetTimer() + for i := 0; i < b.N; i++ { + tokenIndex := i % len(tokens) + _, err := jwtUtil.ValidateToken(ctx, tokens[tokenIndex]) + if err != nil { + b.Fatal(err) + } + } +} + +// 示例:如何在 HTTP 处理器中使用 +func ExampleJWTUtil_HTTPHandler() { + redisClient := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + DB: 0, + }) + defer redisClient.Close() + + jwtUtil := NewJWTUtil("your-secret", 3600, redisClient) + ctx := context.Background() + + // 模拟登录处理 + handleLogin := func(userID int64, username string) (string, error) { + token, err := jwtUtil.GenerateToken(ctx, userID, username) + if err != nil { + return "", err + } + return token, nil + } + + // 模拟需要认证的处理 + handleProtected := func(tokenString string) (*JWTClaims, error) { + claims, err := jwtUtil.ValidateToken(ctx, tokenString) + if err != nil { + return nil, err + } + return claims, nil + } + + // 模拟登出处理 + handleLogout := func(tokenString string) error { + return jwtUtil.DeleteToken(ctx, tokenString) + } + + // 示例使用 + userID := int64(123) + username := "exampleuser" + + // 登录 + token, err := handleLogin(userID, username) + if err != nil { + panic(err) + } + + // 访问保护的资源 + claims, err := handleProtected(token) + if err != nil { + panic(err) + } + + // 登出 + err = handleLogout(token) + if err != nil { + panic(err) + } + + // 输出结果 + t := &testing.T{} + assert.NotEmpty(t, token) + assert.Equal(t, userID, claims.UserID) + assert.Equal(t, username, claims.Username) +} diff --git a/docker-compose.yml b/docker-compose.yml index a417f90..ecb0b40 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -16,6 +16,9 @@ services: - 6379:6379 volumes: - ./redis/data:/data + environment: + - TZ=Asia/Shanghai + - REDIS_PASSWORD=123456 command: redis-server --appendonly yes ## etcd 注册中心 etcd: @@ -27,3 +30,29 @@ services: - ./etcd/data:/bitnami environment: ALLOW_NONE_AUTHENTICATION: "yes" + ## modd 工具 + # backend: + # image: harbor.gxxhygroup.com/comm/gomodd:v1.23.0 + # container_name: gojsj2025-backend + # environment: + # # 时区上海 - Timezone Shanghai + # TZ: Asia/Shanghai + # GOPROXY: https://goproxy.cn,direct + # working_dir: /go/jsj2025 + # volumes: + # - ./backend:/go/jsj2025 + # privileged: true + # restart: always + ## 网关 + nginx-gateway: + image: nginx:1.21.5 + container_name: nginx-gateway + restart: always + privileged: true + environment: + - TZ=Asia/Shanghai + ports: + - 8888:8081 + volumes: + - ./nginx/conf.d:/etc/nginx/conf.d + - ./nginx/log:/var/log/nginx diff --git a/nginx/conf.d/gateway.conf b/nginx/conf.d/gateway.conf new file mode 100644 index 0000000..4913411 --- /dev/null +++ b/nginx/conf.d/gateway.conf @@ -0,0 +1,14 @@ +server{ + listen 8888; + access_log /var/log/nginx/jsj2025.com_access.log; + error_log /var/log/nginx/jsj2025.com_error.log; + # 用户中心 + location ~ /usercenter/ { + proxy_set_header Host $http_host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header REMOTE-HOST $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_pass http://backend:8080; + } + +} \ No newline at end of file diff --git a/backend/usercenter/redis/data/appendonlydir/appendonly.aof.1.incr.aof b/nginx/log/access.log similarity index 100% rename from backend/usercenter/redis/data/appendonlydir/appendonly.aof.1.incr.aof rename to nginx/log/access.log diff --git a/nginx/log/error.log b/nginx/log/error.log new file mode 100644 index 0000000..605c0bc --- /dev/null +++ b/nginx/log/error.log @@ -0,0 +1,159 @@ +2025/07/11 05:07:41 [emerg] 1#1: host not found in upstream "usercenter" in /etc/nginx/conf.d/gateway.conf:11 +2025/07/11 05:07:43 [emerg] 1#1: host not found in upstream "usercenter" in /etc/nginx/conf.d/gateway.conf:11 +2025/07/11 05:07:46 [emerg] 1#1: host not found in upstream "usercenter" in /etc/nginx/conf.d/gateway.conf:11 +2025/07/11 05:07:49 [emerg] 1#1: host not found in upstream "usercenter" in /etc/nginx/conf.d/gateway.conf:11 +2025/07/11 05:07:51 [emerg] 1#1: host not found in upstream "usercenter" in /etc/nginx/conf.d/gateway.conf:11 +2025/07/11 05:07:55 [emerg] 1#1: host not found in upstream "usercenter" in /etc/nginx/conf.d/gateway.conf:11 +2025/07/11 05:08:00 [emerg] 1#1: host not found in upstream "usercenter" in /etc/nginx/conf.d/gateway.conf:11 +2025/07/11 05:08:08 [emerg] 1#1: host not found in upstream "usercenter" in /etc/nginx/conf.d/gateway.conf:11 +2025/07/11 05:08:23 [emerg] 1#1: host not found in upstream "usercenter" in /etc/nginx/conf.d/gateway.conf:11 +2025/07/11 05:08:50 [emerg] 1#1: host not found in upstream "usercenter" in /etc/nginx/conf.d/gateway.conf:11 +2025/07/11 05:09:43 [emerg] 1#1: host not found in upstream "usercenter" in /etc/nginx/conf.d/gateway.conf:11 +2025/07/11 05:10:06 [notice] 1#1: using the "epoll" event method +2025/07/11 05:10:06 [notice] 1#1: nginx/1.21.5 +2025/07/11 05:10:06 [notice] 1#1: built by gcc 10.2.1 20210110 (Debian 10.2.1-6) +2025/07/11 05:10:06 [notice] 1#1: OS: Linux 5.15.167.4-microsoft-standard-WSL2 +2025/07/11 05:10:06 [notice] 1#1: getrlimit(RLIMIT_NOFILE): 1048576:1048576 +2025/07/11 05:10:06 [notice] 1#1: start worker processes +2025/07/11 05:10:06 [notice] 1#1: start worker process 22 +2025/07/11 05:10:06 [notice] 1#1: start worker process 23 +2025/07/11 05:10:06 [notice] 1#1: start worker process 24 +2025/07/11 05:10:06 [notice] 1#1: start worker process 25 +2025/07/11 05:10:06 [notice] 1#1: start worker process 26 +2025/07/11 05:10:06 [notice] 1#1: start worker process 27 +2025/07/11 05:10:06 [notice] 1#1: start worker process 28 +2025/07/11 05:10:06 [notice] 1#1: start worker process 29 +2025/07/11 05:10:06 [notice] 1#1: start worker process 30 +2025/07/11 05:10:06 [notice] 1#1: start worker process 31 +2025/07/11 05:10:06 [notice] 1#1: start worker process 32 +2025/07/11 05:10:06 [notice] 1#1: start worker process 33 +2025/07/11 05:10:06 [notice] 1#1: start worker process 34 +2025/07/11 05:10:06 [notice] 1#1: start worker process 35 +2025/07/11 05:10:06 [notice] 1#1: start worker process 36 +2025/07/11 05:10:06 [notice] 1#1: start worker process 37 +2025/07/11 05:10:06 [notice] 1#1: start worker process 38 +2025/07/11 05:10:06 [notice] 1#1: start worker process 39 +2025/07/11 05:10:06 [notice] 1#1: start worker process 40 +2025/07/11 05:10:06 [notice] 1#1: start worker process 41 +2025/07/11 05:10:22 [notice] 1#1: signal 3 (SIGQUIT) received, shutting down +2025/07/11 05:10:22 [notice] 23#23: gracefully shutting down +2025/07/11 05:10:22 [notice] 34#34: gracefully shutting down +2025/07/11 05:10:22 [notice] 22#22: gracefully shutting down +2025/07/11 05:10:22 [notice] 24#24: gracefully shutting down +2025/07/11 05:10:22 [notice] 25#25: gracefully shutting down +2025/07/11 05:10:22 [notice] 28#28: gracefully shutting down +2025/07/11 05:10:22 [notice] 26#26: gracefully shutting down +2025/07/11 05:10:22 [notice] 27#27: gracefully shutting down +2025/07/11 05:10:22 [notice] 29#29: gracefully shutting down +2025/07/11 05:10:22 [notice] 31#31: gracefully shutting down +2025/07/11 05:10:22 [notice] 30#30: gracefully shutting down +2025/07/11 05:10:22 [notice] 33#33: gracefully shutting down +2025/07/11 05:10:22 [notice] 32#32: gracefully shutting down +2025/07/11 05:10:22 [notice] 35#35: gracefully shutting down +2025/07/11 05:10:22 [notice] 36#36: gracefully shutting down +2025/07/11 05:10:22 [notice] 23#23: exiting +2025/07/11 05:10:22 [notice] 38#38: gracefully shutting down +2025/07/11 05:10:22 [notice] 37#37: gracefully shutting down +2025/07/11 05:10:22 [notice] 39#39: gracefully shutting down +2025/07/11 05:10:22 [notice] 40#40: gracefully shutting down +2025/07/11 05:10:22 [notice] 41#41: gracefully shutting down +2025/07/11 05:10:22 [notice] 34#34: exiting +2025/07/11 05:10:22 [notice] 22#22: exiting +2025/07/11 05:10:22 [notice] 24#24: exiting +2025/07/11 05:10:22 [notice] 25#25: exiting +2025/07/11 05:10:22 [notice] 28#28: exiting +2025/07/11 05:10:22 [notice] 26#26: exiting +2025/07/11 05:10:22 [notice] 27#27: exiting +2025/07/11 05:10:22 [notice] 29#29: exiting +2025/07/11 05:10:22 [notice] 31#31: exiting +2025/07/11 05:10:22 [notice] 30#30: exiting +2025/07/11 05:10:22 [notice] 33#33: exiting +2025/07/11 05:10:22 [notice] 32#32: exiting +2025/07/11 05:10:22 [notice] 35#35: exiting +2025/07/11 05:10:22 [notice] 36#36: exiting +2025/07/11 05:10:22 [notice] 23#23: exit +2025/07/11 05:10:22 [notice] 38#38: exiting +2025/07/11 05:10:22 [notice] 37#37: exiting +2025/07/11 05:10:22 [notice] 39#39: exiting +2025/07/11 05:10:22 [notice] 40#40: exiting +2025/07/11 05:10:22 [notice] 41#41: exiting +2025/07/11 05:10:22 [notice] 34#34: exit +2025/07/11 05:10:22 [notice] 22#22: exit +2025/07/11 05:10:22 [notice] 24#24: exit +2025/07/11 05:10:22 [notice] 25#25: exit +2025/07/11 05:10:22 [notice] 28#28: exit +2025/07/11 05:10:22 [notice] 26#26: exit +2025/07/11 05:10:22 [notice] 27#27: exit +2025/07/11 05:10:22 [notice] 29#29: exit +2025/07/11 05:10:22 [notice] 31#31: exit +2025/07/11 05:10:22 [notice] 30#30: exit +2025/07/11 05:10:22 [notice] 33#33: exit +2025/07/11 05:10:22 [notice] 32#32: exit +2025/07/11 05:10:22 [notice] 35#35: exit +2025/07/11 05:10:22 [notice] 36#36: exit +2025/07/11 05:10:22 [notice] 38#38: exit +2025/07/11 05:10:22 [notice] 37#37: exit +2025/07/11 05:10:22 [notice] 39#39: exit +2025/07/11 05:10:22 [notice] 40#40: exit +2025/07/11 05:10:22 [notice] 41#41: exit +2025/07/11 05:10:22 [notice] 1#1: signal 17 (SIGCHLD) received from 41 +2025/07/11 05:10:22 [notice] 1#1: worker process 22 exited with code 0 +2025/07/11 05:10:22 [notice] 1#1: worker process 25 exited with code 0 +2025/07/11 05:10:22 [notice] 1#1: worker process 41 exited with code 0 +2025/07/11 05:10:22 [notice] 1#1: signal 29 (SIGIO) received +2025/07/11 05:10:22 [notice] 1#1: signal 17 (SIGCHLD) received from 38 +2025/07/11 05:10:22 [notice] 1#1: worker process 23 exited with code 0 +2025/07/11 05:10:22 [notice] 1#1: worker process 27 exited with code 0 +2025/07/11 05:10:22 [notice] 1#1: worker process 29 exited with code 0 +2025/07/11 05:10:22 [notice] 1#1: worker process 30 exited with code 0 +2025/07/11 05:10:22 [notice] 1#1: worker process 32 exited with code 0 +2025/07/11 05:10:22 [notice] 1#1: worker process 35 exited with code 0 +2025/07/11 05:10:22 [notice] 1#1: worker process 38 exited with code 0 +2025/07/11 05:10:22 [notice] 1#1: worker process 40 exited with code 0 +2025/07/11 05:10:22 [notice] 1#1: signal 29 (SIGIO) received +2025/07/11 05:10:22 [notice] 1#1: signal 17 (SIGCHLD) received from 37 +2025/07/11 05:10:22 [notice] 1#1: worker process 26 exited with code 0 +2025/07/11 05:10:22 [notice] 1#1: worker process 37 exited with code 0 +2025/07/11 05:10:22 [notice] 1#1: signal 29 (SIGIO) received +2025/07/11 05:10:22 [notice] 1#1: signal 17 (SIGCHLD) received from 24 +2025/07/11 05:10:22 [notice] 1#1: worker process 24 exited with code 0 +2025/07/11 05:10:22 [notice] 1#1: signal 29 (SIGIO) received +2025/07/11 05:10:22 [notice] 1#1: signal 17 (SIGCHLD) received from 34 +2025/07/11 05:10:22 [notice] 1#1: worker process 28 exited with code 0 +2025/07/11 05:10:22 [notice] 1#1: worker process 33 exited with code 0 +2025/07/11 05:10:22 [notice] 1#1: worker process 34 exited with code 0 +2025/07/11 05:10:22 [notice] 1#1: signal 29 (SIGIO) received +2025/07/11 05:10:22 [notice] 1#1: signal 17 (SIGCHLD) received from 39 +2025/07/11 05:10:22 [notice] 1#1: worker process 31 exited with code 0 +2025/07/11 05:10:22 [notice] 1#1: worker process 39 exited with code 0 +2025/07/11 05:10:22 [notice] 1#1: signal 29 (SIGIO) received +2025/07/11 05:10:22 [notice] 1#1: signal 17 (SIGCHLD) received from 31 +2025/07/11 05:10:22 [notice] 1#1: signal 17 (SIGCHLD) received from 36 +2025/07/11 05:10:22 [notice] 1#1: worker process 36 exited with code 0 +2025/07/11 05:10:22 [notice] 1#1: exit +2025/07/11 05:10:23 [notice] 1#1: using the "epoll" event method +2025/07/11 05:10:23 [notice] 1#1: nginx/1.21.5 +2025/07/11 05:10:23 [notice] 1#1: built by gcc 10.2.1 20210110 (Debian 10.2.1-6) +2025/07/11 05:10:23 [notice] 1#1: OS: Linux 5.15.167.4-microsoft-standard-WSL2 +2025/07/11 05:10:23 [notice] 1#1: getrlimit(RLIMIT_NOFILE): 1048576:1048576 +2025/07/11 05:10:23 [notice] 1#1: start worker processes +2025/07/11 05:10:23 [notice] 1#1: start worker process 22 +2025/07/11 05:10:23 [notice] 1#1: start worker process 23 +2025/07/11 05:10:23 [notice] 1#1: start worker process 24 +2025/07/11 05:10:23 [notice] 1#1: start worker process 25 +2025/07/11 05:10:23 [notice] 1#1: start worker process 26 +2025/07/11 05:10:23 [notice] 1#1: start worker process 27 +2025/07/11 05:10:23 [notice] 1#1: start worker process 28 +2025/07/11 05:10:23 [notice] 1#1: start worker process 29 +2025/07/11 05:10:23 [notice] 1#1: start worker process 30 +2025/07/11 05:10:23 [notice] 1#1: start worker process 31 +2025/07/11 05:10:23 [notice] 1#1: start worker process 32 +2025/07/11 05:10:23 [notice] 1#1: start worker process 33 +2025/07/11 05:10:23 [notice] 1#1: start worker process 34 +2025/07/11 05:10:23 [notice] 1#1: start worker process 35 +2025/07/11 05:10:23 [notice] 1#1: start worker process 36 +2025/07/11 05:10:23 [notice] 1#1: start worker process 37 +2025/07/11 05:10:23 [notice] 1#1: start worker process 38 +2025/07/11 05:10:23 [notice] 1#1: start worker process 39 +2025/07/11 05:10:23 [notice] 1#1: start worker process 40 +2025/07/11 05:10:23 [notice] 1#1: start worker process 41 diff --git a/nginx/log/jsj2025.com_access.log b/nginx/log/jsj2025.com_access.log new file mode 100644 index 0000000..e69de29 diff --git a/nginx/log/jsj2025.com_error.log b/nginx/log/jsj2025.com_error.log new file mode 100644 index 0000000..e69de29