dark 1 week ago
parent
commit
4f1aa63b1c
  1. 5
      .cursorrules
  2. 1
      .gitignore
  3. 14
      backend/go.mod
  4. 22
      backend/go.sum
  5. 7
      backend/modd.conf
  6. 126
      backend/readme.md
  7. 50
      backend/usercenter/api/apis/user.api
  8. 23
      backend/usercenter/api/etc/usercenter-api.yaml
  9. 11
      backend/usercenter/api/internal/AutoMigrate.go
  10. 26
      backend/usercenter/api/internal/config/config.go
  11. 21
      backend/usercenter/api/internal/handler/health/usercenter_ping_handler.go
  12. 49
      backend/usercenter/api/internal/handler/routes.go
  13. 29
      backend/usercenter/api/internal/handler/user/user_login_handler.go
  14. 38
      backend/usercenter/api/internal/handler/user/user_logout_handler.go
  15. 28
      backend/usercenter/api/internal/handler/user/user_register_handler.go
  16. 34
      backend/usercenter/api/internal/logic/health/usercenter_ping_logic.go
  17. 93
      backend/usercenter/api/internal/logic/user/user_login_logic.go
  18. 85
      backend/usercenter/api/internal/logic/user/user_logout_logic.go
  19. 147
      backend/usercenter/api/internal/logic/user/user_register_logic.go
  20. 33
      backend/usercenter/api/internal/svc/service_context.go
  21. 38
      backend/usercenter/api/internal/types/types.go
  22. 23
      backend/usercenter/api/usercenter.api
  23. 31
      backend/usercenter/api/usercenter.go
  24. 7
      backend/usercenter/cmd/migrate.go
  25. 30
      backend/usercenter/internal/logic/loginlogic.go
  26. 30
      backend/usercenter/internal/logic/registerlogic.go
  27. 13
      backend/usercenter/internal/svc/servicecontext.go
  28. 6
      backend/usercenter/orm/base.go
  29. 26
      backend/usercenter/orm/userType.go
  30. BIN
      backend/usercenter/redis/data/appendonlydir/appendonly.aof.1.base.rdb
  31. 2
      backend/usercenter/redis/data/appendonlydir/appendonly.aof.manifest
  32. BIN
      backend/usercenter/redis/data/dump.rdb
  33. 13
      backend/usercenter/rpc/etc/usercenter.yaml
  34. 15
      backend/usercenter/rpc/internal/config/config.go
  35. 4
      backend/usercenter/rpc/internal/logic/changepasswordlogic.go
  36. 4
      backend/usercenter/rpc/internal/logic/getprofilelogic.go
  37. 4
      backend/usercenter/rpc/internal/logic/getuserpermissionslogic.go
  38. 4
      backend/usercenter/rpc/internal/logic/getuserroleslogic.go
  39. 61
      backend/usercenter/rpc/internal/logic/loginlogic.go
  40. 8
      backend/usercenter/rpc/internal/logic/pinglogic.go
  41. 86
      backend/usercenter/rpc/internal/logic/registerlogic.go
  42. 4
      backend/usercenter/rpc/internal/logic/updateprofilelogic.go
  43. 6
      backend/usercenter/rpc/internal/server/usercenterserver.go
  44. 39
      backend/usercenter/rpc/internal/svc/servicecontext.go
  45. 0
      backend/usercenter/rpc/pb/usercenter/usercenter.pb.go
  46. 0
      backend/usercenter/rpc/pb/usercenter/usercenter_grpc.pb.go
  47. 8
      backend/usercenter/rpc/usercenter.go
  48. 0
      backend/usercenter/rpc/usercenter.proto
  49. 2
      backend/usercenter/rpc/usercenterclient/usercenter.go
  50. 362
      backend/utils/README.md
  51. 119
      backend/utils/bcrypt.go
  52. 166
      backend/utils/bcrypt_test.go
  53. 223
      backend/utils/jwt.go
  54. 327
      backend/utils/jwt_test.go
  55. 29
      docker-compose.yml
  56. 14
      nginx/conf.d/gateway.conf
  57. 0
      nginx/log/access.log
  58. 159
      nginx/log/error.log
  59. 0
      nginx/log/jsj2025.com_access.log
  60. 0
      nginx/log/jsj2025.com_error.log

5
.cursorrules

@ -0,0 +1,5 @@
use context7
后端技术栈:
go-zero
gorm

1
.gitignore

@ -1,2 +1,3 @@
/etcd
/redis
*.exe

14
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

22
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=

7
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
}

126
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
```

50
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)
}

23
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

11
backend/usercenter/orm/AutoMigrate.go → 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表完成")

26
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
}

21
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)
}
}
}

49
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),
},
},
)
}

29
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)
}
}
}

38
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)
}
}
}

28
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)
}
}
}

34
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
}

93
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
}

85
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
}

147
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
}

33
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,
}),
}
}

38
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"`
}

23
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)
}

31
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()
}

7
backend/usercenter/cmd/migrate.go

@ -1,7 +0,0 @@
package main
import "backend/usercenter/orm"
func main() {
orm.AutoMigrate()
}

30
backend/usercenter/internal/logic/loginlogic.go

@ -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
}

30
backend/usercenter/internal/logic/registerlogic.go

@ -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
}

13
backend/usercenter/internal/svc/servicecontext.go

@ -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,
}
}

6
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

26
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
}

BIN
backend/usercenter/redis/data/appendonlydir/appendonly.aof.1.base.rdb

Binary file not shown.

2
backend/usercenter/redis/data/appendonlydir/appendonly.aof.manifest

@ -1,2 +0,0 @@
file appendonly.aof.1.base.rdb seq 1 type b
file appendonly.aof.1.incr.aof seq 1 type i startoffset 0 endoffset 0

BIN
backend/usercenter/redis/data/dump.rdb

Binary file not shown.

13
backend/usercenter/etc/usercenter.yaml → backend/usercenter/rpc/etc/usercenter.yaml

@ -1,5 +1,6 @@
Name: usercenter.rpc
ListenOn: 0.0.0.0:8080
Mode: dev
Etcd:
Hosts:
- 127.0.0.1:2379
@ -12,4 +13,14 @@ MySQL:
DBName: usercenter
MaxIdleConns: 10
MaxOpenConns: 100
RedisGo:
Host: 127.0.0.1
Port: 6379
Type: node
Pass: xhy.dev
Tls: false
DB: 0
AuthRpc:
AccessSecret: dev.gxxhygroup.com
AccessExpire: 604800
TkStore: true

15
backend/usercenter/internal/config/config.go → backend/usercenter/rpc/internal/config/config.go

@ -22,7 +22,20 @@ func (m MySQLConf) DSN() string {
m.User, m.Password, m.Host, m.Port, m.DBName)
}
type RedisGoConf struct {
Host string
Port int
Pass string
DB int
}
type Config struct {
zrpc.RpcServerConf
MySQL MySQLConf
MySQL MySQLConf
RedisGo RedisGoConf
AuthRpc struct {
AccessSecret string
AccessExpire int64
}
TkStore bool
}

4
backend/usercenter/internal/logic/changepasswordlogic.go → backend/usercenter/rpc/internal/logic/changepasswordlogic.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"
)

4
backend/usercenter/internal/logic/getprofilelogic.go → backend/usercenter/rpc/internal/logic/getprofilelogic.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"
)

4
backend/usercenter/internal/logic/getuserpermissionslogic.go → backend/usercenter/rpc/internal/logic/getuserpermissionslogic.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"
)

4
backend/usercenter/internal/logic/getuserroleslogic.go → backend/usercenter/rpc/internal/logic/getuserroleslogic.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"
)

61
backend/usercenter/rpc/internal/logic/loginlogic.go

@ -0,0 +1,61 @@
package logic
import (
"context"
"errors"
"backend/usercenter/orm"
"backend/usercenter/rpc/internal/svc"
"backend/usercenter/rpc/pb/usercenter"
"backend/utils"
"github.com/spf13/cast"
"github.com/zeromicro/go-zero/core/logx"
"gorm.io/gorm"
)
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) {
// 1. 验证用户是否存在,支持用户名和手机号登录
user := new(orm.User)
result := l.svcCtx.UsercenterDB.Where("name = ? OR phone = ?", in.Identity, in.Identity).First(&user)
if result.Error != nil && result.Error != gorm.ErrRecordNotFound {
return nil, result.Error
}
if result.RowsAffected == 0 {
return nil, errors.New("用户不存在")
}
// 2. 验证密码,使用 utils.bcrypt.CheckPassword 工具类进行比较
if !utils.CheckPassword(user.Password, in.Password) {
return nil, errors.New("密码错误")
}
// 3. 生成token,使用 utils.GenerateToken 工具类生成
jwtUtil := utils.NewJWTUtil(l.svcCtx.Config.AuthRpc.AccessSecret, l.svcCtx.Config.AuthRpc.AccessExpire, l.svcCtx.RedisClient, l.svcCtx.Config.TkStore)
token, err := jwtUtil.GenerateToken(l.ctx, cast.ToInt64(user.ID), user.Name)
if err != nil {
return nil, err
}
// 4. 返回token
return &usercenter.LoginResponse{
UserId: cast.ToString(user.ID),
Token: token,
Message: "登录成功",
}, nil
}

8
backend/usercenter/internal/logic/pinglogic.go → backend/usercenter/rpc/internal/logic/pinglogic.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"
)
@ -26,5 +26,7 @@ func NewPingLogic(ctx context.Context, svcCtx *svc.ServiceContext) *PingLogic {
func (l *PingLogic) Ping(in *usercenter.Request) (*usercenter.Response, error) {
// todo: add your logic here and delete this line
return &usercenter.Response{}, nil
return &usercenter.Response{
Pong: "pong",
}, nil
}

86
backend/usercenter/rpc/internal/logic/registerlogic.go

@ -0,0 +1,86 @@
package logic
import (
"backend/usercenter/orm"
"backend/usercenter/rpc/internal/svc"
"backend/usercenter/rpc/pb/usercenter"
"backend/utils"
"context"
"errors"
"regexp"
"strconv"
"github.com/zeromicro/go-zero/core/logx"
"gorm.io/gorm"
)
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) {
// 1. 检查用户是否存在
user := new(orm.User)
res := l.svcCtx.UsercenterDB.First(user, "name = ?", in.Username)
// 有错误返回错误,找到数据返回找到
if res.Error != nil && res.Error != gorm.ErrRecordNotFound {
return nil, errors.New(res.Error.Error())
}
if res.RowsAffected > 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
}

4
backend/usercenter/internal/logic/updateprofilelogic.go → 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"
)

6
backend/usercenter/internal/server/usercenterserver.go → 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 {

39
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,
}
}

0
backend/usercenter/pb/usercenter/usercenter.pb.go → backend/usercenter/rpc/pb/usercenter/usercenter.pb.go

0
backend/usercenter/pb/usercenter/usercenter_grpc.pb.go → backend/usercenter/rpc/pb/usercenter/usercenter_grpc.pb.go

8
backend/usercenter/usercenter.go → 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"

0
backend/usercenter/usercenter.proto → backend/usercenter/rpc/usercenter.proto

2
backend/usercenter/usercenterclient/usercenter.go → 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"

362
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 管理
- 完整的测试覆盖

119
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
}

166
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")
}
}

223
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
}

327
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)
}

29
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

14
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;
}
}

0
backend/usercenter/redis/data/appendonlydir/appendonly.aof.1.incr.aof → nginx/log/access.log

159
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

0
nginx/log/jsj2025.com_access.log

0
nginx/log/jsj2025.com_error.log

Loading…
Cancel
Save