├── api ├── README.md └── user │ └── v1 │ ├── user.proto │ └── user_grpc.pb.go ├── internal ├── model │ ├── README.md │ ├── init.go │ └── user_info.go ├── cache │ ├── README.md │ ├── cache.go │ ├── user_token_cache.go │ ├── user_cache_test.go │ └── user_cache.go ├── repository │ ├── README.md │ ├── repository.go │ ├── main_test.go │ ├── user_repo_test.go │ └── user_repo.go ├── service │ ├── README.md │ ├── service.go │ ├── verify_code_svc.go │ ├── user_svc_test.go │ └── user_svc.go ├── server │ ├── server.go │ ├── http.go │ └── grpc.go ├── ecode │ ├── README.md │ └── user.go ├── handler │ └── ping.go ├── types │ └── user.go ├── tasks │ ├── task.go │ └── email_welcome.go ├── routers │ └── router.go └── mocks │ ├── user_cache_mock.go │ └── user_repo_mock.go ├── cmd ├── consumer │ └── main.go ├── server │ ├── wire.go │ ├── main.go │ └── wire_gen.go └── cron │ └── main.go ├── third_party ├── validate │ └── README.md └── google │ ├── api │ ├── annotations.proto │ ├── httpbody.proto │ └── http.proto │ └── protobuf │ ├── empty.proto │ └── any.proto ├── config ├── prod │ ├── trace.yaml │ ├── cron.yaml │ ├── redis.yaml │ ├── app.yaml │ ├── database.yaml │ ├── registry.yaml │ ├── logger.yaml │ ├── nginx_api.conf │ ├── README.md │ └── config.yaml ├── docker │ ├── trace.yaml │ ├── redis.yaml │ ├── app.yaml │ ├── registry.yaml │ ├── logger.yaml │ ├── nginx_api.conf │ ├── database.yaml │ ├── README.md │ └── config.yaml ├── dev │ ├── trace.yaml │ ├── cron.yaml │ ├── redis.yaml │ ├── app.yaml │ ├── registry.yaml │ ├── logger.yaml │ ├── database.yaml │ └── config.yaml ├── nginx_api.conf └── README.md ├── openapi.yaml ├── deploy ├── k8s │ ├── go-service.yaml │ ├── go-ingress.yaml │ ├── go-deployment.yaml │ └── ingress-controller.yaml ├── mysql │ ├── mysql-configmap.yaml │ └── deployment-service.yaml ├── docker_image.sh ├── deploy.sh ├── README.md ├── redis │ ├── redis-config.yaml │ └── deployment-service.yaml └── docker │ └── Dockerfile ├── .gitignore ├── LICENSE ├── scripts └── admin.sh ├── README.md ├── coverage.txt ├── Makefile └── go.mod /api/README.md: -------------------------------------------------------------------------------- 1 | # proto 2 | 3 | proto 文件存放位置 -------------------------------------------------------------------------------- /internal/model/README.md: -------------------------------------------------------------------------------- 1 | # Models 2 | 3 | 数据模型层 -------------------------------------------------------------------------------- /internal/cache/README.md: -------------------------------------------------------------------------------- 1 | # cache 2 | 3 | cache 文件存放位置 -------------------------------------------------------------------------------- /cmd/consumer/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | func main() { 4 | 5 | } 6 | -------------------------------------------------------------------------------- /internal/repository/README.md: -------------------------------------------------------------------------------- 1 | # Repository 2 | 3 | 数据访问层: 包含数据库、cache、RPC、HTTP请求等 -------------------------------------------------------------------------------- /third_party/validate/README.md: -------------------------------------------------------------------------------- 1 | # protoc-gen-validate (PGV) 2 | 3 | * https://github.com/envoyproxy/protoc-gen-validate -------------------------------------------------------------------------------- /internal/service/README.md: -------------------------------------------------------------------------------- 1 | # Service 2 | 3 | 业务逻辑层 4 | 5 | ## Reference 6 | 7 | - 多设备登录登出 https://juejin.cn/post/7276396424526348303 -------------------------------------------------------------------------------- /config/prod/trace.yaml: -------------------------------------------------------------------------------- 1 | ServiceName: "eagle" 2 | LocalAgentHostPort: "127.0.0.1:6831" 3 | CollectorEndpoint: "http://localhost:14268/api/traces" -------------------------------------------------------------------------------- /config/docker/trace.yaml: -------------------------------------------------------------------------------- 1 | ServiceName: "user-svc" 2 | LocalAgentHostPort: "user-svc:6831" 3 | CollectorEndpoint: "http://user-svc:14268/api/traces" 4 | -------------------------------------------------------------------------------- /config/dev/trace.yaml: -------------------------------------------------------------------------------- 1 | ServiceName: "user-svc" 2 | LocalAgentHostPort: "192.168.31.178:6831" 3 | CollectorEndpoint: "http://192.168.31.178:14268/api/traces" -------------------------------------------------------------------------------- /internal/server/server.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import "github.com/google/wire" 4 | 5 | // ProviderSet is server providers. 6 | var ProviderSet = wire.NewSet(NewGRPCServer) 7 | -------------------------------------------------------------------------------- /internal/service/service.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import "github.com/google/wire" 4 | 5 | // ProviderSet is service providers. 6 | var ProviderSet = wire.NewSet(NewUserServiceServer) 7 | -------------------------------------------------------------------------------- /config/dev/cron.yaml: -------------------------------------------------------------------------------- 1 | Addr: 127.0.0.1:6379 2 | Password: "" 3 | DB: 0 4 | MinIdleConn: 200 5 | DialTimeout: 60s 6 | ReadTimeout: 500ms 7 | WriteTimeout: 500ms 8 | PoolSize: 100 9 | PoolTimeout: 240s 10 | Concurrency: 10 -------------------------------------------------------------------------------- /config/prod/cron.yaml: -------------------------------------------------------------------------------- 1 | Addr: 127.0.0.1:6379 2 | Password: "" 3 | DB: 0 4 | MinIdleConn: 200 5 | DialTimeout: 60s 6 | ReadTimeout: 500ms 7 | WriteTimeout: 500ms 8 | PoolSize: 100 9 | PoolTimeout: 240s 10 | Concurrency: 10 -------------------------------------------------------------------------------- /internal/cache/cache.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "github.com/go-eagle/eagle/pkg/redis" 5 | "github.com/google/wire" 6 | ) 7 | 8 | // ProviderSet is cache providers. 9 | var ProviderSet = wire.NewSet(redis.Init, NewUserCache) 10 | -------------------------------------------------------------------------------- /config/dev/redis.yaml: -------------------------------------------------------------------------------- 1 | default: 2 | Addr: 127.0.0.1:6379 3 | Password: "" 4 | DB: 0 5 | MinIdleConn: 200 6 | DialTimeout: 60s 7 | ReadTimeout: 500ms 8 | WriteTimeout: 500ms 9 | PoolSize: 100 10 | PoolTimeout: 240s 11 | EnableTrace: true -------------------------------------------------------------------------------- /config/docker/redis.yaml: -------------------------------------------------------------------------------- 1 | default: 2 | Addr: redis:6379 3 | Password: "" 4 | DB: 0 5 | MinIdleConn: 200 6 | DialTimeout: 60s 7 | ReadTimeout: 500ms 8 | WriteTimeout: 500ms 9 | PoolSize: 100 10 | PoolTimeout: 240s 11 | EnableTrace: true -------------------------------------------------------------------------------- /openapi.yaml: -------------------------------------------------------------------------------- 1 | # Generated with protoc-gen-openapi 2 | # https://github.com/google/gnostic/tree/master/cmd/protoc-gen-openapi 3 | 4 | openapi: 3.0.3 5 | info: 6 | title: "" 7 | version: 0.0.1 8 | paths: {} 9 | components: 10 | schemas: {} 11 | -------------------------------------------------------------------------------- /config/prod/redis.yaml: -------------------------------------------------------------------------------- 1 | default: 2 | Addr: redis-svc:6379 3 | Password: "" 4 | DB: 0 5 | MinIdleConn: 200 6 | DialTimeout: 60s 7 | ReadTimeout: 500ms 8 | WriteTimeout: 500ms 9 | PoolSize: 100 10 | PoolTimeout: 240s 11 | EnableTrace: true -------------------------------------------------------------------------------- /internal/repository/repository.go: -------------------------------------------------------------------------------- 1 | package repository 2 | 3 | import ( 4 | "github.com/go-microservice/user-service/internal/model" 5 | "github.com/google/wire" 6 | ) 7 | 8 | // ProviderSet is repo providers. 9 | var ProviderSet = wire.NewSet(model.Init, NewUser) 10 | -------------------------------------------------------------------------------- /deploy/k8s/go-service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: user-svc 5 | labels: 6 | app: user-service 7 | spec: 8 | ports: 9 | - name: user-svc-port 10 | port: 9001 11 | protocol: TCP 12 | #nodePort: 30002 13 | targetPort: 8080 14 | 15 | type: ClusterIP 16 | #type: NodePort 17 | selector: 18 | app: user-service -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | .idea 15 | .DS_Store 16 | 17 | # Dependency directories (remove the comment below to include it) 18 | vendor/ 19 | log 20 | user-service 21 | cron 22 | -------------------------------------------------------------------------------- /internal/ecode/README.md: -------------------------------------------------------------------------------- 1 | # 业务错误码定义 2 | 3 | > 公共错误码已经在 `github.com/go-eagle/eagle/pkg/errcode` 包中,可以直接使用 4 | 5 | 业务的错误码可以根据模块按文件进行定义 6 | 7 | 使用时公共错误码 以 `errno.开头`,业务错误码以 `ecode.开头` 8 | 9 | ## Demo 10 | 11 | ```go 12 | // 公共错误码 13 | import "github.com/go-eagle/eagle/pkg/errcode" 14 | ... 15 | errno.InternalServerError 16 | 17 | // 业务错误码 18 | import "github.com/go-eagle/eagle/internal/ecode" 19 | ... 20 | ecode.ErrUserNotFound 21 | ``` -------------------------------------------------------------------------------- /internal/handler/ping.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | 6 | "github.com/go-eagle/eagle/pkg/app" 7 | "github.com/go-eagle/eagle/pkg/log" 8 | ) 9 | 10 | // Ping ping 11 | // @Summary ping 12 | // @Description ping 13 | // @Tags system 14 | // @Accept json 15 | // @Produce json 16 | // @Router /ping [get] 17 | func Ping(c *gin.Context) { 18 | log.Info("Get function called.") 19 | 20 | app.Success(c, gin.H{}) 21 | } 22 | -------------------------------------------------------------------------------- /config/docker/app.yaml: -------------------------------------------------------------------------------- 1 | Name: user-svc 2 | Version: 1.0.0 3 | PprofPort: :5555 4 | Mode: debug # debug, release, test 5 | JwtSecret: JWT_SECRET 6 | JwtTimeout: 86400 7 | CookieName: jwt-token 8 | SSL: true 9 | CtxDefaultTimeout: 12 10 | CSRF: true 11 | Debug: false 12 | EnableTrace: false 13 | EnablePprof: true 14 | 15 | HTTP: 16 | Addr: :8081 17 | ReadTimeout: 3s 18 | WriteTimeout: 3s 19 | GRPC: 20 | Addr: :9091 21 | ReadTimeout: 5s 22 | WriteTimeout: 5s 23 | -------------------------------------------------------------------------------- /config/dev/app.yaml: -------------------------------------------------------------------------------- 1 | Name: user-svc 2 | Version: 1.0.0 3 | PprofPort: :5555 4 | Mode: debug # debug, release, test 5 | JwtSecret: JWT_SECRET 6 | JwtTimeout: 86400 7 | CookieName: jwt-token 8 | SSL: true 9 | CtxDefaultTimeout: 12 10 | CSRF: true 11 | Debug: false 12 | EnableTrace: false 13 | EnablePprof: true 14 | 15 | HTTP: 16 | Addr: :8081 17 | ReadTimeout: 3s 18 | WriteTimeout: 3s 19 | GRPC: 20 | Addr: :9091 21 | Timeout: 3s 22 | ReadTimeout: 5s 23 | WriteTimeout: 5s 24 | -------------------------------------------------------------------------------- /config/prod/app.yaml: -------------------------------------------------------------------------------- 1 | Name: user-svc 2 | Version: 1.0.0 3 | PprofPort: :5555 4 | Mode: debug # debug, release, test 5 | JwtSecret: JWT_SECRET 6 | JwtTimeout: 86400 7 | CookieName: jwt-token 8 | SSL: true 9 | CtxDefaultTimeout: 12 10 | CSRF: true 11 | Debug: false 12 | EnableTrace: true 13 | EnablePprof: true 14 | 15 | HTTP: 16 | Addr: :8081 17 | ReadTimeout: 3s 18 | WriteTimeout: 3s 19 | GRPC: 20 | Addr: :9091 21 | Timeout: 3s 22 | ReadTimeout: 5s 23 | WriteTimeout: 5s 24 | -------------------------------------------------------------------------------- /config/docker/registry.yaml: -------------------------------------------------------------------------------- 1 | etcd: 2 | Endpoints: 3 | - "etcd:2379" 4 | ConnectTimeout: 5s 5 | BasicAuth: 6 | UserName: 7 | Password: 8 | Secure: false 9 | AutoSyncInterval: 1s 10 | TTL: 11 | consul: 12 | Addr: "consul:8500" 13 | Scheme: http 14 | Datacenter: 15 | WaitTime: 5s 16 | nacos: 17 | Addr: "nacos" 18 | Port: 8848 19 | NamespaceId: public 20 | TimeoutMs: 5000 21 | LogDir: 22 | CacheDir: 23 | LogLevel: warn # debug,info,warn,error, default is info 24 | -------------------------------------------------------------------------------- /config/prod/database.yaml: -------------------------------------------------------------------------------- 1 | Name: mysql # 数据库名称 2 | Addr: mysql-svc:3306 # 如果是 docker,可以替换为 对应的服务名称,eg: db:3306 3 | UserName: root 4 | Password: root 5 | ShowLog: true # 是否打印所有SQL日志 6 | MaxIdleConn: 10 # 最大闲置的连接数,0意味着使用默认的大小2, 小于0表示不使用连接池 7 | MaxOpenConn: 60 # 最大打开的连接数, 需要小于数据库配置中的max_connections数 8 | ConnMaxLifeTime: 4h # 单个连接最大存活时间,建议设置比数据库超时时长(wait_timeout)稍小一些 9 | #SlowThreshold: 0 # 慢查询阈值,设置后只打印慢查询日志,默认为200ms -------------------------------------------------------------------------------- /config/dev/registry.yaml: -------------------------------------------------------------------------------- 1 | etcd: 2 | Endpoints: 3 | - "127.0.0.1:2379" 4 | ConnectTimeout: 5s 5 | BasicAuth: 6 | UserName: 7 | Password: 8 | Secure: false 9 | AutoSyncInterval: 1s 10 | TTL: 11 | consul: 12 | Addr: "127.0.0.1:8500" 13 | Scheme: http 14 | Datacenter: 15 | WaitTime: 5s 16 | nacos: 17 | Addr: "127.0.0.1" 18 | Port: 8848 19 | NamespaceId: public 20 | TimeoutMs: 5000 21 | LogDir: 22 | CacheDir: 23 | LogLevel: warn # debug,info,warn,error, default is info 24 | -------------------------------------------------------------------------------- /config/prod/registry.yaml: -------------------------------------------------------------------------------- 1 | etcd: 2 | Endpoints: 3 | - "127.0.0.1:2379" 4 | ConnectTimeout: 5s 5 | BasicAuth: 6 | UserName: 7 | Password: 8 | Secure: false 9 | AutoSyncInterval: 1s 10 | TTL: 11 | consul: 12 | Addr: "127.0.0.1:8500" 13 | Scheme: http 14 | Datacenter: 15 | WaitTime: 5s 16 | nacos: 17 | Addr: "127.0.0.1" 18 | Port: 8848 19 | NamespaceId: public 20 | TimeoutMs: 5000 21 | LogDir: 22 | CacheDir: 23 | LogLevel: warn # debug,info,warn,error, default is info 24 | -------------------------------------------------------------------------------- /config/docker/logger.yaml: -------------------------------------------------------------------------------- 1 | Development: false 2 | DisableCaller: false 3 | DisableStacktrace: false 4 | Encoding: json # json or console 5 | Level: info # 日志级别,INFO, WARN, ERROR 6 | Name: eagle 7 | Writers: console # 有2个可选项:file,console 选择file会将日志记录到logger_file指定的日志文件中,选择console会将日志输出到标准输出,当然也可以两者同时选择 8 | LoggerFile: /tmp/log/eagle.log 9 | LoggerWarnFile: /tmp/log/eagle.wf.log 10 | LoggerErrorFile: /tmp/log/eagle.err.log 11 | LogRollingPolicy: daily 12 | LogRotateDate: 1 13 | LogRotateSize: 1 14 | LogBackupCount: 7 -------------------------------------------------------------------------------- /config/dev/logger.yaml: -------------------------------------------------------------------------------- 1 | Development: false 2 | DisableCaller: false 3 | DisableStacktrace: false 4 | Encoding: json # json or console 5 | Level: info # 日志级别,INFO, WARN, ERROR 6 | Name: user-svc 7 | Writers: console # 有2个可选项:file,console 选择file会将日志记录到logger_file指定的日志文件中,选择console会将日志输出到标准输出,当然也可以两者同时选择 8 | LoggerFile: /tmp/log/eagle.log 9 | LoggerWarnFile: /tmp/log/eagle.wf.log 10 | LoggerErrorFile: /tmp/log/eagle.err.log 11 | LogRollingPolicy: daily 12 | LogRotateDate: 1 13 | LogRotateSize: 1 14 | LogBackupCount: 7 -------------------------------------------------------------------------------- /config/prod/logger.yaml: -------------------------------------------------------------------------------- 1 | Development: false 2 | DisableCaller: false 3 | DisableStacktrace: false 4 | Encoding: json # json or console 5 | Level: info # 日志级别,INFO, WARN, ERROR 6 | Name: user-svc 7 | Writers: console # 有2个可选项:file,console 选择file会将日志记录到logger_file指定的日志文件中,选择console会将日志输出到标准输出,当然也可以两者同时选择 8 | LoggerFile: /data/logs/eagle.log 9 | LoggerWarnFile: /data/logs/eagle.wf.log 10 | LoggerErrorFile: /data/logs/eagle.err.log 11 | LogRollingPolicy: daily 12 | LogRotateDate: 1 13 | LogRotateSize: 1 14 | LogBackupCount: 7 -------------------------------------------------------------------------------- /internal/server/http.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "github.com/go-eagle/eagle/pkg/app" 5 | "github.com/go-eagle/eagle/pkg/transport/http" 6 | 7 | "github.com/go-microservice/user-service/internal/routers" 8 | ) 9 | 10 | // NewHTTPServer creates a HTTP server 11 | func NewHTTPServer(c *app.ServerConfig) *http.Server { 12 | router := routers.NewRouter() 13 | 14 | srv := http.NewServer( 15 | http.WithAddress(c.Addr), 16 | http.WithReadTimeout(c.ReadTimeout), 17 | http.WithWriteTimeout(c.WriteTimeout), 18 | ) 19 | 20 | srv.Handler = router 21 | 22 | return srv 23 | } 24 | -------------------------------------------------------------------------------- /config/nginx_api.conf: -------------------------------------------------------------------------------- 1 | upstream web { 2 | server app:8080; 3 | } 4 | 5 | server { 6 | listen 80; 7 | server_name localhost; 8 | 9 | location / { 10 | # https://stackoverflow.com/questions/42720618/docker-nginx-stopped-emerg-11-host-not-found-in-upstream 11 | resolver 127.0.0.1; 12 | 13 | proxy_set_header Host $http_host; 14 | proxy_set_header X-Forwarded-Host $http_host; 15 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 16 | proxy_set_header X-Real-IP $remote_addr; 17 | 18 | client_max_body_size 5m; 19 | 20 | proxy_pass http://web; 21 | } 22 | 23 | } -------------------------------------------------------------------------------- /config/prod/nginx_api.conf: -------------------------------------------------------------------------------- 1 | upstream web { 2 | server app:8080; 3 | } 4 | 5 | server { 6 | listen 80; 7 | server_name localhost; 8 | 9 | location / { 10 | # https://stackoverflow.com/questions/42720618/docker-nginx-stopped-emerg-11-host-not-found-in-upstream 11 | resolver 127.0.0.1; 12 | 13 | proxy_set_header Host $http_host; 14 | proxy_set_header X-Forwarded-Host $http_host; 15 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 16 | proxy_set_header X-Real-IP $remote_addr; 17 | 18 | client_max_body_size 5m; 19 | 20 | proxy_pass http://web; 21 | } 22 | 23 | } -------------------------------------------------------------------------------- /config/docker/nginx_api.conf: -------------------------------------------------------------------------------- 1 | upstream web { 2 | server app:8080; 3 | } 4 | 5 | server { 6 | listen 80; 7 | server_name localhost; 8 | 9 | location / { 10 | # https://stackoverflow.com/questions/42720618/docker-nginx-stopped-emerg-11-host-not-found-in-upstream 11 | resolver 127.0.0.1; 12 | 13 | proxy_set_header Host $http_host; 14 | proxy_set_header X-Forwarded-Host $http_host; 15 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 16 | proxy_set_header X-Real-IP $remote_addr; 17 | 18 | client_max_body_size 5m; 19 | 20 | proxy_pass http://web; 21 | } 22 | 23 | } -------------------------------------------------------------------------------- /deploy/mysql/mysql-configmap.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: mysql-db-config 5 | namespace: default 6 | labels: 7 | app: mysql-db-config 8 | data: 9 | my.cnf: | 10 | [client] 11 | default-character-set=utf8mb4 12 | [mysql] 13 | default-character-set=utf8mb4 14 | [mysqld] 15 | character-set-server = utf8mb4 16 | collation-server = utf8mb4_unicode_ci 17 | init_connect='SET NAMES utf8mb4' 18 | skip-character-set-client-handshake = true 19 | max_connections=2000 20 | secure_file_priv=/var/lib/mysql 21 | datadir=/var/lib/mysql 22 | bind-address=0.0.0.0 23 | symbolic-links=0 -------------------------------------------------------------------------------- /deploy/docker_image.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | tag=$1 4 | IMAGE_NAME="user-service" 5 | NAMESPACE="go-microservices" 6 | REGISTRY="registry.cn-hangzhou.aliyuncs.com" 7 | 8 | if [[ -z "$tag" ]]; then 9 | echo "tag is empty" 10 | echo "usage: sh $0 " 11 | exit 1 12 | fi 13 | 14 | # build image 15 | echo "1. build docker image" 16 | docker build -t ${REGISTRY}/${NAMESPACE}/${IMAGE_NAME}:${tag} -t ${REGISTRY}/${NAMESPACE}/${IMAGE_NAME}:latest -f deploy/docker/Dockerfile . 17 | 18 | # docker push new-repo:tagname 19 | echo "2. push docker image to remote hub" 20 | docker push ${REGISTRY}/${NAMESPACE}/${IMAGE_NAME}:${tag} 21 | 22 | echo "Done. push docker image success." 23 | -------------------------------------------------------------------------------- /config/dev/database.yaml: -------------------------------------------------------------------------------- 1 | default: 2 | Driver: mysql # 数据库驱动,目前支持 mysql, postgres 3 | Name: eagle # 数据库名称 4 | Addr: localhost:3306 # 如果是 docker,可以替换为 对应的服务名称,eg: db:3306 5 | UserName: root 6 | Password: 123456 7 | ShowLog: true # 是否打印所有SQL日志 8 | MaxIdleConn: 10 # 最大闲置的连接数,0意味着使用默认的大小2, 小于0表示不使用连接池 9 | MaxOpenConn: 60 # 最大打开的连接数, 需要小于数据库配置中的max_connections数 10 | Timeout: 3s # 数据库连接超时时间, 如果是 PostgreSQL 不需要加入单位 11 | ReadTimeout: 3s # 数据库去读超时时间, 0代表不限制,如果是PostgreSQL, 3000代表3s 12 | WriteTimeout: 3s # 数据库写入超时时间, 0代表不限制,如果是PostgreSQL, 不会使用该字段的值 13 | ConnMaxLifeTime: 4h # 单个连接最大存活时间,建议设置比数据库超时时长(wait_timeout)稍小一些 14 | SlowThreshold: 500ms # 慢查询阈值,设置后只打印慢查询日志,默认为200ms 15 | -------------------------------------------------------------------------------- /config/docker/database.yaml: -------------------------------------------------------------------------------- 1 | default: 2 | Driver: mysql # 数据库驱动,目前支持 mysql, postgres 3 | Name: eagle # 数据库名称 4 | Addr: mysql:3306 # 如果是 docker,可以替换为 对应的服务名称,eg: db:3306 5 | UserName: root 6 | Password: 123456 7 | ShowLog: true # 是否打印所有SQL日志 8 | MaxIdleConn: 10 # 最大闲置的连接数,0意味着使用默认的大小2, 小于0表示不使用连接池 9 | MaxOpenConn: 60 # 最大打开的连接数, 需要小于数据库配置中的max_connections数 10 | Timeout: 3s # 数据库连接超时时间, 如果是 PostgreSQL 不需要加入单位 11 | ReadTimeout: 3s # 数据库去读超时时间, 0代表不限制,如果是PostgreSQL, 3000代表3s 12 | WriteTimeout: 3s # 数据库写入超时时间, 0代表不限制,如果是PostgreSQL, 不会使用该字段的值 13 | ConnMaxLifeTime: 4h # 单个连接最大存活时间,建议设置比数据库超时时长(wait_timeout)稍小一些 14 | SlowThreshold: 500ms # 慢查询阈值,设置后只打印慢查询日志,默认为200ms 15 | -------------------------------------------------------------------------------- /internal/types/user.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | // User include user base info and user profile 4 | type User struct { 5 | Id int64 `json:"id"` 6 | Username string `json:"username"` 7 | Phone string `json:"phone"` 8 | Email string `json:"email"` 9 | Password string `json:"password"` 10 | LoginAt int64 `json:"login_at"` // login time for last times 11 | Status int32 `json:"status"` 12 | Nickname string `json:"nickname"` 13 | Avatar string `json:"avatar"` 14 | Gender string `json:"gender"` 15 | Birthday string `json:"birthday"` 16 | Bio string `json:"bio"` 17 | CreatedAt int64 `json:"created_at"` 18 | UpdatedAt int64 `json:"updated_at"` 19 | } 20 | -------------------------------------------------------------------------------- /config/README.md: -------------------------------------------------------------------------------- 1 | # conf 2 | 3 | 默认从 config.local.yaml 加载配置, 可以通过下面命令生成: 4 | 5 | ```bash 6 | cp config.sample.yaml config.local.yaml 7 | ``` 8 | 9 | 如果是多环境可以设定不同的配置文件,比如: 10 | - config.local.yaml 本地开发环境 11 | - config.dev.yaml 开发环境 12 | - config.test.yaml 测试环境 13 | - config.pre.yaml 预发布环境 14 | - config.prod.yaml 线上环境 15 | 16 | 生成开发环境配置文件: 17 | 18 | ```bash 19 | cp config.sample.yaml config.dev.yaml 20 | ``` 21 | 22 | 以上配置是可以提交到Git等代码仓库的,`config.local.yaml` 配置文件默认不会被提交。 23 | 24 | 使用本地配置文件来运行程序,命令如下: 25 | 26 | ```bash 27 | # 本地启动 28 | # 也可以直接 ./eagle 29 | ./eagle -c config/config.local.yaml 30 | # 开发环境启动 31 | ./eagle -c config/config.dev.yaml 32 | # 线上环境启动 33 | ./eagle -c config/config.prod.yaml 34 | 35 | ``` 36 | -------------------------------------------------------------------------------- /config/prod/README.md: -------------------------------------------------------------------------------- 1 | # conf 2 | 3 | 默认从 config.local.yaml 加载配置, 可以通过下面命令生成: 4 | 5 | ```bash 6 | cp config.sample.yaml config.local.yaml 7 | ``` 8 | 9 | 如果是多环境可以设定不同的配置文件,比如: 10 | - config.local.yaml 本地开发环境 11 | - config.dev.yaml 开发环境 12 | - config.test.yaml 测试环境 13 | - config.pre.yaml 预发布环境 14 | - config.prod.yaml 线上环境 15 | 16 | 生成开发环境配置文件: 17 | 18 | ```bash 19 | cp config.sample.yaml config.dev.yaml 20 | ``` 21 | 22 | 以上配置是可以提交到Git等代码仓库的,`config.local.yaml` 配置文件默认不会被提交。 23 | 24 | 使用本地配置文件来运行程序,命令如下: 25 | 26 | ```bash 27 | # 本地启动 28 | # 也可以直接 ./eagle 29 | ./eagle -c config/config.local.yaml 30 | # 开发环境启动 31 | ./eagle -c config/config.dev.yaml 32 | # 线上环境启动 33 | ./eagle -c config/config.prod.yaml 34 | 35 | ``` 36 | -------------------------------------------------------------------------------- /config/docker/README.md: -------------------------------------------------------------------------------- 1 | # conf 2 | 3 | 默认从 config.local.yaml 加载配置, 可以通过下面命令生成: 4 | 5 | ```bash 6 | cp config.sample.yaml config.local.yaml 7 | ``` 8 | 9 | 如果是多环境可以设定不同的配置文件,比如: 10 | - config.local.yaml 本地开发环境 11 | - config.dev.yaml 开发环境 12 | - config.test.yaml 测试环境 13 | - config.pre.yaml 预发布环境 14 | - config.prod.yaml 线上环境 15 | 16 | 生成开发环境配置文件: 17 | 18 | ```bash 19 | cp config.sample.yaml config.dev.yaml 20 | ``` 21 | 22 | 以上配置是可以提交到Git等代码仓库的,`config.local.yaml` 配置文件默认不会被提交。 23 | 24 | 使用本地配置文件来运行程序,命令如下: 25 | 26 | ```bash 27 | # 本地启动 28 | # 也可以直接 ./eagle 29 | ./eagle -c config/config.local.yaml 30 | # 开发环境启动 31 | ./eagle -c config/config.dev.yaml 32 | # 线上环境启动 33 | ./eagle -c config/config.prod.yaml 34 | 35 | ``` 36 | -------------------------------------------------------------------------------- /internal/server/grpc.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "github.com/go-eagle/eagle/pkg/app" 5 | "github.com/go-eagle/eagle/pkg/transport/grpc" 6 | 7 | v1 "github.com/go-microservice/user-service/api/user/v1" 8 | "github.com/go-microservice/user-service/internal/service" 9 | ) 10 | 11 | // NewGRPCServer creates a gRPC server 12 | func NewGRPCServer(cfg *app.ServerConfig, svc *service.UserServiceServer) *grpc.Server { 13 | 14 | grpcServer := grpc.NewServer( 15 | grpc.Network("tcp"), 16 | grpc.Address(cfg.Addr), 17 | grpc.Timeout(cfg.WriteTimeout), 18 | grpc.EnableLog(), 19 | grpc.EnableTracing(), 20 | ) 21 | 22 | // register biz service 23 | v1.RegisterUserServiceServer(grpcServer, svc) 24 | 25 | return grpcServer 26 | } 27 | -------------------------------------------------------------------------------- /internal/model/init.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "gorm.io/gorm" 5 | 6 | "github.com/go-eagle/eagle/pkg/storage/orm" 7 | ) 8 | 9 | var ( 10 | ErrRecordNotFound = gorm.ErrRecordNotFound 11 | ) 12 | 13 | var ( 14 | // DB define a gloabl db 15 | DB *gorm.DB 16 | ) 17 | 18 | // Init init db 19 | func Init() (*gorm.DB, func(), error) { 20 | err := orm.New([]string{"default"}...) 21 | if err != nil { 22 | return nil, nil, err 23 | } 24 | 25 | // get first db 26 | DB, err := orm.GetDB("default") 27 | if err != nil { 28 | return nil, nil, err 29 | } 30 | sqlDB, err := DB.DB() 31 | if err != nil { 32 | return nil, nil, err 33 | } 34 | 35 | // here you can add second or more db, and remember to add close to below cleanFunc 36 | // ... 37 | 38 | cleanFunc := func() { 39 | sqlDB.Close() 40 | } 41 | return DB, cleanFunc, nil 42 | } 43 | -------------------------------------------------------------------------------- /deploy/deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | tag=$1 4 | IMAGE_NAME="user-service" 5 | NAMESPACE="go-microservices" 6 | REGISTRY="registry.cn-hangzhou.aliyuncs.com" 7 | 8 | # 指定docker hub 9 | 10 | if [[ -z "$tag" ]]; then 11 | echo "tag is empty" 12 | echo "usage: sh $0 " 13 | exit 1 14 | fi 15 | 16 | # build image 17 | echo "1. build docker image" 18 | docker build -t ${REGISTRY}/${NAMESPACE}/${IMAGE_NAME}:${tag} -t ${REGISTRY}/${NAMESPACE}/${IMAGE_NAME}:latest -f deploy/docker/Dockerfile . 19 | 20 | # docker push new-repo:tagname 21 | echo "2. push docker image to hub" 22 | docker push ${REGISTRY}/${NAMESPACE}/${IMAGE_NAME}:${tag} 23 | 24 | # deploy k8s deployment 25 | echo "3. deploy k8s deployment" 26 | kubectl apply -f k8s/go-deployment.yaml 27 | 28 | # deploy k8s service 29 | echo "4. deploy k8s service" 30 | kubectl apply -f k8s/go-service.yaml 31 | 32 | echo "Done. deploy success." 33 | -------------------------------------------------------------------------------- /deploy/k8s/go-ingress.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: networking.k8s.io/v1 2 | kind: Ingress 3 | metadata: 4 | name: user-ingress 5 | annotations: 6 | nginx.org/lb-method: round_robin 7 | #kubernetes.io/ingress.class: "nginx" # 指定 Ingress Controller 的类型 8 | #nginx.ingress.kubernetes.io/use-regex: "true" # 指定我们的 rules 的 path 可以使用正则表达式 9 | #nginx.ingress.kubernetes.io/rewrite-target: /$1 10 | spec: 11 | ingressClassName: ngx-ink 12 | rules: 13 | - host: user-app.dev # 需要在host中进行绑定,ip为(minikube ip) 14 | http: 15 | paths: 16 | - path: / 17 | pathType: Prefix 18 | backend: 19 | service: 20 | name: user-svc 21 | port: 22 | number: 8080 23 | 24 | --- 25 | # Ingress Class 26 | apiVersion: networking.k8s.io/v1 27 | kind: IngressClass 28 | metadata: 29 | name: ngx-ink 30 | 31 | spec: 32 | controller: nginx.org/ingress-controller 33 | --- 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 1024casts 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /third_party/google/api/annotations.proto: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015, Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | syntax = "proto3"; 16 | 17 | package google.api; 18 | 19 | import "google/api/http.proto"; 20 | import "google/protobuf/descriptor.proto"; 21 | 22 | option go_package = "google.golang.org/genproto/googleapis/api/annotations;annotations"; 23 | option java_multiple_files = true; 24 | option java_outer_classname = "AnnotationsProto"; 25 | option java_package = "com.google.api"; 26 | option objc_class_prefix = "GAPI"; 27 | 28 | extend google.protobuf.MethodOptions { 29 | // See `HttpRule`. 30 | HttpRule http = 72295728; 31 | } -------------------------------------------------------------------------------- /deploy/k8s/go-deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: # Deployment的元数据 4 | name: user-service-dep 5 | labels: 6 | app: user-service 7 | spec: 8 | replicas: 1 # ReplicaSet部分的定义 9 | selector: 10 | matchLabels: 11 | app: user-service 12 | strategy: 13 | type: RollingUpdate 14 | rollingUpdate: 15 | maxSurge: 1 16 | maxUnavailable: 1 17 | template: # Pod 模板的定义 18 | metadata: 19 | labels: 20 | app: user-service 21 | spec: # Pod里容器相关的定义 22 | containers: 23 | - name: user-service 24 | image: qloog/user-service:v0.0.29 25 | imagePullPolicy: Always 26 | resources: 27 | limits: 28 | memory: "256Mi" 29 | cpu: "300m" # 0.3核,1000m = 1核心 30 | ports: 31 | - containerPort: 8080 32 | - containerPort: 9090 33 | readinessProbe: # 就绪探针 34 | exec: 35 | command: [ "/bin/grpc_health_probe", "-addr=:9090" ] 36 | initialDelaySeconds: 5 37 | livenessProbe: # 存活探针 38 | exec: 39 | command: [ "/bin/grpc_health_probe", "-addr=:9090" ] 40 | initialDelaySeconds: 10 41 | -------------------------------------------------------------------------------- /internal/tasks/task.go: -------------------------------------------------------------------------------- 1 | package tasks 2 | 3 | import ( 4 | "os" 5 | "sync" 6 | "time" 7 | 8 | "github.com/go-eagle/eagle/pkg/config" 9 | 10 | "github.com/hibiken/asynq" 11 | ) 12 | 13 | const ( 14 | // queue name 15 | QueueCritical = "critical" 16 | QueueDefault = "default" 17 | QueueLow = "low" 18 | ) 19 | 20 | var ( 21 | client *asynq.Client 22 | once sync.Once 23 | ) 24 | 25 | type Config struct { 26 | Addr string 27 | Password string 28 | DB int 29 | MinIdleConn int 30 | DialTimeout time.Duration 31 | ReadTimeout time.Duration 32 | WriteTimeout time.Duration 33 | PoolSize int 34 | PoolTimeout time.Duration 35 | Concurrency int //并发数 36 | } 37 | 38 | func GetClient() *asynq.Client { 39 | once.Do(func() { 40 | c := config.New("config", config.WithEnv(os.Getenv("APP_ENV"))) 41 | var cfg Config 42 | if err := c.Load("cron", &cfg); err != nil { 43 | panic(err) 44 | } 45 | client = asynq.NewClient(asynq.RedisClientOpt{ 46 | Addr: cfg.Addr, 47 | Password: cfg.Password, 48 | DB: cfg.DB, 49 | DialTimeout: cfg.DialTimeout, 50 | ReadTimeout: cfg.ReadTimeout, 51 | WriteTimeout: cfg.WriteTimeout, 52 | PoolSize: cfg.PoolSize, 53 | }) 54 | }) 55 | return client 56 | } 57 | -------------------------------------------------------------------------------- /internal/model/user_info.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "errors" 5 | 6 | "gorm.io/gorm" 7 | ) 8 | 9 | // UserModel define a user base info struct. 10 | type UserModel struct { 11 | ID int64 `gorm:"primary_key;AUTO_INCREMENT;column:id" json:"id"` 12 | Username string `gorm:"column:username" json:"username"` 13 | Nickname string `gorm:"column:nickname" json:"nickname"` 14 | Phone string `gorm:"column:phone" json:"phone"` 15 | Email string `gorm:"column:email" json:"email"` 16 | Password string `gorm:"column:password" json:"password"` 17 | Avatar string `gorm:"column:avatar" json:"avatar"` 18 | Gender string `gorm:"column:gender" json:"gender"` 19 | Birthday string `gorm:"column:birthday" json:"birthday"` 20 | Bio string `gorm:"column:bio" json:"bio"` 21 | LoginAt int64 `gorm:"column:login_at" json:"login_at"` // login time for last times 22 | Status int32 `gorm:"column:status" json:"status"` 23 | CreatedAt int64 `gorm:"column:created_at" json:"created_at"` 24 | UpdatedAt int64 `gorm:"column:updated_at" json:"updated_at"` 25 | } 26 | 27 | // TableName 表名 28 | func (u *UserModel) TableName() string { 29 | return "user_info" 30 | } 31 | 32 | func (u *UserModel) BeforeCreate(tx *gorm.DB) (err error) { 33 | if u.Username == "admin" || u.Username == "root" || u.Username == "administrator" { 34 | return errors.New("invalid name") 35 | } 36 | return 37 | } 38 | -------------------------------------------------------------------------------- /deploy/mysql/deployment-service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: mysql-svc 5 | spec: 6 | type: NodePort 7 | ports: 8 | - port: 3306 9 | nodePort: 30306 10 | targetPort: mysql 11 | selector: 12 | app: mysql 13 | 14 | --- 15 | 16 | apiVersion: apps/v1 17 | kind: Deployment 18 | metadata: 19 | name: mysql 20 | spec: 21 | selector: 22 | matchLabels: 23 | app: mysql 24 | strategy: 25 | type: Recreate 26 | template: 27 | metadata: 28 | labels: 29 | app: mysql 30 | spec: 31 | containers: 32 | - image: mysql:5.7 33 | name: mysql 34 | env: 35 | - name: MYSQL_ROOT_PASSWORD 36 | value: root 37 | - name: MYSQL_USER 38 | value: mysql 39 | - name: MYSQL_PASSWORD 40 | value: mysql 41 | ports: 42 | - containerPort: 3306 43 | name: mysql 44 | volumeMounts: 45 | - name: mysql-persistent-storage 46 | mountPath: /var/lib/mysql 47 | - name: mysql-config 48 | mountPath: /etc/mysql/conf.d/my.cnf 49 | subPath: my.cnf 50 | volumes: 51 | - name: mysql-persistent-storage 52 | emptyDir: {} 53 | - name: mysql-config 54 | configMap: 55 | name: mysql-db-config 56 | items: 57 | - key: my.cnf 58 | path: my.cnf -------------------------------------------------------------------------------- /internal/ecode/user.go: -------------------------------------------------------------------------------- 1 | package ecode 2 | 3 | import ( 4 | "github.com/go-eagle/eagle/pkg/errcode" 5 | "google.golang.org/grpc/codes" 6 | ) 7 | 8 | //nolint: golint 9 | var ( 10 | // common errors 11 | ErrInternalError = errcode.New(10000, "Internal error") 12 | ErrInvalidArgument = errcode.New(10001, "Invalid argument") 13 | ErrNotFound = errcode.New(10003, "Not found") 14 | ErrAccessDenied = errcode.New(10006, "Access denied") 15 | ErrCanceled = errcode.New(codes.Canceled, "RPC request is canceled") 16 | 17 | // user grpc errors 18 | ErrUserIsExist = errcode.New(20100, "The user already exists.") 19 | ErrUserNotFound = errcode.New(20101, "The user was not found.") 20 | ErrPasswordIncorrect = errcode.New(20102, "账号或密码错误") 21 | ErrAreaCodeEmpty = errcode.New(20103, "手机区号不能为空") 22 | ErrPhoneEmpty = errcode.New(20104, "手机号不能为空") 23 | ErrGenVCode = errcode.New(20105, "生成验证码错误") 24 | ErrSendSMS = errcode.New(20106, "发送短信错误") 25 | ErrSendSMSTooMany = errcode.New(20107, "已超出当日限制,请明天再试") 26 | ErrVerifyCode = errcode.New(20108, "验证码错误") 27 | ErrEmailOrPassword = errcode.New(20109, "邮箱或密码错误") 28 | ErrTwicePasswordNotMatch = errcode.New(20110, "两次密码输入不一致") 29 | ErrRegisterFailed = errcode.New(20111, "注册失败") 30 | ErrToken = errcode.New(20112, "Gen token error") 31 | ErrEncrypt = errcode.New(20113, "Encrypting the user password error") 32 | ) 33 | -------------------------------------------------------------------------------- /deploy/README.md: -------------------------------------------------------------------------------- 1 | 2 | ## K8s 环境搭建 3 | 4 | ```bash 5 | # 部署mysql 6 | kubectl apply -f deploy/mysql/mysql-configmap.yaml 7 | kubectl apply -f deploy/mysql/deployment-service.yaml 8 | 9 | # 部署redis 10 | kubectl apply -f deploy/redis/redis-config.yaml 11 | kubectl apply -f deploy/redis/deployment-service.yaml 12 | 13 | # 部署应用 14 | kubectl apply -f deploy/k8s/go-deployment.yaml 15 | kubectl apply -f deploy/k8s/go-service.yaml 16 | kubectl apply -f deploy/k8s/go-inigress.yaml 17 | 18 | # 安装 Nginx Ingress Controller 19 | kubectl apply -f common/ns-and-sa.yaml 20 | kubectl apply -f rbac/rbac.yaml 21 | kubectl apply -f common/nginx-config.yaml 22 | kubectl apply -f common/default-server-secret.yaml 23 | 24 | # 查看 ingress 配置是否正常 25 | kubectl describe ing user-ingress 26 | ``` 27 | 28 | ## 本地docker环境 29 | 30 | ### MySQL 31 | 32 | 搭建本地 MySQL 33 | 34 | ```bash 35 | # 启动 36 | docker run -it --name mysql -p 3306:3306 -e MYSQL_ROOT_PASSWORD=123456 mysql:5.6 37 | 38 | # 查看 39 | docker ps 40 | 41 | # 进入容器 42 | docker exec -it mysql bash 43 | 44 | # 登录mysql 45 | mysql -u root -p 46 | 47 | # 授权root用户远程登录 48 | GRANT ALL PRIVILEGES ON *.* TO 'root'@'%' IDENTIFIED BY '123456'; 49 | FLUSH PRIVILEGES; 50 | 51 | # 退出容器 52 | exit 53 | 54 | # 在宿主机登录 55 | mysql -h127.0.0.1 -uroot -p123456 56 | 57 | ``` 58 | 59 | ### Redis 60 | 61 | 搭建本地 Redis 62 | 63 | ```bash 64 | # 启动 65 | docker run -it --name redis -p 6379:6379 redis:6.0 66 | ``` 67 | 68 | ### 启动应用 69 | 70 | ```bash 71 | # 启动应用 72 | docker run --rm --link=mysql --link=redis -it -p 8080:8080 user-service:v0.0.21 73 | 74 | # 检测服务是否正常 75 | ➜ ~ curl localhost:8080/ping 76 | 77 | # 输出如下说明正常 78 | {"code":0,"message":"Ok","data":{}} 79 | ``` -------------------------------------------------------------------------------- /internal/routers/router.go: -------------------------------------------------------------------------------- 1 | package routers 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "github.com/go-eagle/eagle/pkg/app" 6 | "github.com/go-eagle/eagle/pkg/middleware" 7 | "github.com/prometheus/client_golang/prometheus/promhttp" 8 | ginSwagger "github.com/swaggo/gin-swagger" //nolint: goimports 9 | "github.com/swaggo/gin-swagger/swaggerFiles" 10 | 11 | "github.com/go-microservice/user-service/internal/handler" 12 | ) 13 | 14 | // Load loads the middlewares, routes, handlers. 15 | func NewRouter() *gin.Engine { 16 | g := gin.New() 17 | // 使用中间件 18 | g.Use(middleware.NoCache) 19 | g.Use(middleware.Options) 20 | g.Use(middleware.Secure) 21 | g.Use(middleware.Logging()) 22 | g.Use(middleware.RequestID()) 23 | g.Use(middleware.Metrics(app.Conf.Name)) 24 | g.Use(middleware.Tracing(app.Conf.Name)) 25 | 26 | // 404 Handler. 27 | g.NoRoute(app.RouteNotFound) 28 | g.NoMethod(app.RouteNotFound) 29 | 30 | // swagger api docs 31 | g.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler)) 32 | // pprof router 性能分析路由 33 | // 默认关闭,开发环境下可以打开 34 | // 访问方式: HOST/debug/pprof 35 | // 通过 HOST/debug/pprof/profile 生成profile 36 | // 查看分析图 go tool pprof -http=:5000 profile 37 | // see: https://github.com/gin-contrib/pprof 38 | // pprof.Register(g) 39 | 40 | // HealthCheck 健康检查路由 41 | g.GET("/health", app.HealthCheck) 42 | // metrics router 可以在 prometheus 中进行监控 43 | // 通过 grafana 可视化查看 prometheus 的监控数据,使用插件6671查看 44 | g.GET("/metrics", gin.WrapH(promhttp.Handler())) 45 | g.GET("/ping", handler.Ping) 46 | 47 | // v1 router 48 | apiV1 := g.Group("/v1") 49 | apiV1.Use() 50 | { 51 | // here to add biz router 52 | } 53 | 54 | return g 55 | } 56 | -------------------------------------------------------------------------------- /deploy/redis/redis-config.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: redis-config 5 | data: 6 | redis.conf: | 7 | protected-mode no 8 | port 6379 9 | tcp-backlog 511 10 | timeout 0 11 | tcp-keepalive 300 12 | daemonize no 13 | supervised no 14 | pidfile /var/run/redis_6379.pid 15 | loglevel notice 16 | logfile "" 17 | databases 16 18 | always-show-logo yes 19 | save 900 1 20 | save 300 10 21 | save 60 10000 22 | stop-writes-on-bgsave-error yes 23 | rdbcompression yes 24 | rdbchecksum yes 25 | dbfilename dump.rdb 26 | dir /data 27 | slave-serve-stale-data yes 28 | slave-read-only yes 29 | repl-diskless-sync no 30 | repl-diskless-sync-delay 5 31 | repl-disable-tcp-nodelay no 32 | slave-priority 100 33 | lazyfree-lazy-eviction no 34 | lazyfree-lazy-expire no 35 | lazyfree-lazy-server-del no 36 | slave-lazy-flush no 37 | appendonly no 38 | appendfilename "appendonly.aof" 39 | appendfsync everysec 40 | no-appendfsync-on-rewrite no 41 | auto-aof-rewrite-percentage 100 42 | auto-aof-rewrite-min-size 64mb 43 | aof-load-truncated yes 44 | aof-use-rdb-preamble no 45 | lua-time-limit 5000 46 | slowlog-log-slower-than 10000 47 | slowlog-max-len 128 48 | latency-monitor-threshold 0 49 | notify-keyspace-events Ex 50 | hash-max-ziplist-entries 512 51 | hash-max-ziplist-value 64 52 | list-max-ziplist-size -2 53 | list-compress-depth 0 54 | set-max-intset-entries 512 55 | zset-max-ziplist-entries 128 56 | zset-max-ziplist-value 64 57 | hll-sparse-max-bytes 3000 58 | activerehashing yes 59 | client-output-buffer-limit normal 0 0 0 60 | client-output-buffer-limit slave 256mb 64mb 60 61 | client-output-buffer-limit pubsub 32mb 8mb 60 62 | hz 10 63 | aof-rewrite-incremental-fsync yes -------------------------------------------------------------------------------- /internal/tasks/email_welcome.go: -------------------------------------------------------------------------------- 1 | package tasks 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | 8 | "github.com/hibiken/asynq" 9 | 10 | "github.com/go-eagle/eagle/pkg/log" 11 | "github.com/pkg/errors" 12 | ) 13 | 14 | const ( 15 | TypeEmailWelcome = "email:welcome" 16 | ) 17 | 18 | type EmailWelcomePayload struct { 19 | Username string 20 | } 21 | 22 | //---------------------------------------------- 23 | // Write a function NewXXXTask to create a task. 24 | // A task consists of a type and a payload. 25 | //---------------------------------------------- 26 | 27 | func NewEmailWelcomeTask(username string) error { 28 | payload, err := json.Marshal(EmailWelcomePayload{Username: username}) 29 | if err != nil { 30 | return errors.Wrapf(err, "[tasks] json marshal error, name: %s", TypeEmailWelcome) 31 | } 32 | task := asynq.NewTask(TypeEmailWelcome, payload) 33 | info, err := GetClient().Enqueue(task) 34 | if err != nil { 35 | return errors.Wrapf(err, "[tasks] Enqueue task error, name: %s", TypeEmailWelcome) 36 | } 37 | 38 | log.Infof("[tasks] welcome task, id: %s, queue_name: %s, type: %s", info.ID, info.Queue, info.Type) 39 | 40 | return nil 41 | } 42 | 43 | //--------------------------------------------------------------- 44 | // Write a function HandleXXXTask to handle the input task. 45 | // Note that it satisfies the asynq.HandlerFunc interface. 46 | // 47 | // Handler doesn't need to be a function. You can define a type 48 | // that satisfies asynq.Handler interface. See examples below. 49 | //--------------------------------------------------------------- 50 | 51 | func HandleEmailWelcomeTask(ctx context.Context, t *asynq.Task) error { 52 | var p EmailWelcomePayload 53 | if err := json.Unmarshal(t.Payload(), &p); err != nil { 54 | return fmt.Errorf("json.Unmarshal failed: %v: %w", err, asynq.SkipRetry) 55 | } 56 | log.Infof("Sending Email to User: username=%s", p.Username) 57 | // Email delivery code ... 58 | return nil 59 | } 60 | -------------------------------------------------------------------------------- /internal/repository/main_test.go: -------------------------------------------------------------------------------- 1 | package repository 2 | 3 | import ( 4 | "database/sql" 5 | "log" 6 | "os" 7 | "testing" 8 | "time" 9 | 10 | "github.com/go-microservice/user-service/internal/model" 11 | 12 | sqlmock "github.com/DATA-DOG/go-sqlmock" 13 | "github.com/alicebob/miniredis/v2" 14 | "github.com/redis/go-redis/v9" 15 | "gorm.io/driver/mysql" 16 | "gorm.io/gorm" 17 | 18 | "github.com/go-microservice/user-service/internal/cache" 19 | ) 20 | 21 | var ( 22 | mockDB *gorm.DB 23 | mock sqlmock.Sqlmock 24 | sqlDB *sql.DB 25 | testCache cache.UserCache 26 | testRepo UserRepo 27 | 28 | data *model.UserModel 29 | ) 30 | 31 | func TestMain(m *testing.M) { 32 | var err error 33 | // mocks db 34 | sqlDB, mock, err = sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual)) 35 | if err != nil { 36 | log.Fatalf("mock db, err: %v", err) 37 | } 38 | _ = mock 39 | 40 | mockDB, err = gorm.Open(mysql.New( 41 | mysql.Config{ 42 | SkipInitializeWithVersion: true, 43 | DriverName: "mysql", 44 | Conn: sqlDB, 45 | }), &gorm.Config{}) 46 | if err != nil { 47 | log.Fatalf("cannot connect to gorm, err: %v", err) 48 | } 49 | 50 | redisServer := mockRedis() 51 | redisClient := redis.NewClient(&redis.Options{Addr: redisServer.Addr()}) 52 | 53 | testCache = cache.NewUserCache(redisClient) 54 | 55 | testRepo = NewUser(mockDB, nil) 56 | 57 | data = &model.UserModel{ 58 | ID: 1, 59 | Username: "test-username", 60 | Nickname: "nickname", 61 | Phone: "12345678", 62 | Email: "test@test.com", 63 | Password: "123456", 64 | Avatar: "", 65 | Gender: "", 66 | Birthday: "2022-11-10", 67 | Bio: "ok", 68 | LoginAt: time.Now().Unix(), 69 | Status: 0, 70 | CreatedAt: time.Now().Unix(), 71 | UpdatedAt: time.Now().Unix(), 72 | } 73 | 74 | os.Exit(m.Run()) 75 | } 76 | 77 | func mockRedis() *miniredis.Miniredis { 78 | s, err := miniredis.Run() 79 | if err != nil { 80 | panic(err) 81 | } 82 | return s 83 | } 84 | -------------------------------------------------------------------------------- /scripts/admin.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | #!/bin/bash 4 | 5 | server=$1 6 | base_dir=$PWD 7 | interval=2 8 | timeout=180 9 | 10 | # 命令行参数,需要手动指定 11 | args="" 12 | 13 | function findPid() { 14 | echo -n `ps -ef -u $UID|grep "${base_dir}/${server} ${args}"|egrep -v 'grep|admin.sh'|awk '{print $2}'` 15 | } 16 | 17 | function waitProcess() { 18 | i=0 19 | while (($i<${timeout})) 20 | do 21 | if [ "`findPid`" == "" -a "$1" == "stop" ];then 22 | break 23 | fi 24 | 25 | if [ "`findPid`" != "" -a "$1" == "start" ];then 26 | break 27 | fi 28 | 29 | echo waiting to $1 ... 30 | 31 | sleep 1 32 | 33 | ((i++)) 34 | done 35 | 36 | if [ "$i" -ge "${timeout}" -a "$1" == "stop" ];then 37 | echo "waiting timeout(${timeout}s), send SIGKILL signal." 38 | kill -9 `findPid` 39 | fi 40 | sleep 1 41 | } 42 | 43 | 44 | function start() 45 | { 46 | if [ "`findPid`" != "" ];then 47 | echo "${server} already running" 48 | exit 0 49 | fi 50 | 51 | nohup ${base_dir}/${server} ${args} &>/dev/null & 52 | 53 | waitProcess start 54 | 55 | # check status 56 | if [ "`findPid`" == "" ];then 57 | echo "${server} start failed" 58 | exit 1 59 | fi 60 | } 61 | 62 | function status() 63 | { 64 | if [ "`findPid`" != "" ];then 65 | echo ${server} is running 66 | else 67 | echo ${server} is not running 68 | fi 69 | } 70 | 71 | function stop() 72 | { 73 | if [ "`findPid`" != "" ];then 74 | echo "send SIGKILL signal to `findPid`" 75 | kill `findPid` 76 | fi 77 | 78 | waitProcess stop 79 | 80 | if [ "`findPid`" != "" ];then 81 | echo "${server} stop failed after ${${timeout}}s" 82 | exit 1 83 | fi 84 | } 85 | 86 | case "$2" in 87 | 'start') 88 | start 89 | ;; 90 | 'stop') 91 | stop 92 | ;; 93 | 'status') 94 | status 95 | ;; 96 | 'restart') 97 | stop && start 98 | ;; 99 | *) 100 | echo "usage: $0 {start|stop|restart|status}" 101 | exit 0 102 | ;; 103 | esac -------------------------------------------------------------------------------- /deploy/docker/Dockerfile: -------------------------------------------------------------------------------- 1 | # Build stage 2 | FROM golang:1.22-alpine3.20 AS builder 3 | 4 | # The latest alpine images don't have some tools like (`git` and `bash`). 5 | # Adding git, bash and openssh to the image 6 | RUN apk add --no-cache git make bash ca-certificates tzdata \ 7 | --repository http://mirrors.aliyun.com/alpine/v3.11/community \ 8 | --repository http://mirrors.aliyun.com/alpine/v3.11/main 9 | 10 | RUN GRPC_HEALTH_PROBE_VERSION=v0.3.0 && \ 11 | wget -qO/go/bin/grpc_health_probe \ 12 | https://github.com/grpc-ecosystem/grpc-health-probe/releases/download/${GRPC_HEALTH_PROBE_VERSION}/grpc_health_probe-linux-amd64 && \ 13 | chmod +x /go/bin/grpc_health_probe 14 | 15 | # 镜像设置必要的环境变量 16 | ENV GO111MODULE=on \ 17 | CGO_ENABLED=0 \ 18 | GOOS=linux \ 19 | GOARCH=amd64 \ 20 | GOPROXY="https://goproxy.cn,direct" \ 21 | TZ=Asia/Shanghai \ 22 | APP_ENV=docker 23 | 24 | # 移动到工作目录 25 | WORKDIR /go/src/user-service 26 | 27 | # 复制项目中的 go.mod 和 go.sum文件并下载依赖信息 28 | COPY go.mod . 29 | COPY go.sum . 30 | 31 | # 将代码复制到容器中 32 | COPY . . 33 | COPY config ./config 34 | 35 | # Build the Go app 36 | #RUN make build 37 | RUN make build 38 | 39 | # Runtime stage 40 | FROM alpine:3.20 41 | 42 | WORKDIR /bin 43 | 44 | # 从builder镜像中把 /build 拷贝到当前目录 45 | COPY --from=builder /go/src/user-service/bin/user-service /bin/user-service 46 | COPY --from=builder /go/src/user-service/config /data/conf/user-service/config 47 | COPY --from=builder /go/bin/grpc_health_probe /bin/grpc_health_probe 48 | 49 | 50 | RUN apk update \ 51 | && apk add --no-cache curl jq \ 52 | && rm -rf /var/cache/apk/* \ 53 | && mkdir -p /data/logs/ 54 | 55 | # Expose port 8080 to the outside world 56 | EXPOSE 8080 57 | EXPOSE 9090 58 | 59 | # 需要运行的命令 60 | ENTRYPOINT ["/bin/user-service", "-c", "/data/conf/user-service/config"] 61 | 62 | # 1. build image: docker build -t qloog/user-service:v1.0.0 -f deploy/docker/Dockerfile . 63 | # 2. start: docker run --rm -it -p 8080:8080 user-service:v1.0.0 64 | # 3. test: curl -i http://localhost:8080/health 65 | # 4. tag: docker tag user-service:v1.0.0 qloog/user-service:v1.0.0 66 | # 5. push tag: docker push qloog/user-service:v1.0.0 67 | 68 | -------------------------------------------------------------------------------- /cmd/server/wire.go: -------------------------------------------------------------------------------- 1 | //go:build wireinject 2 | // +build wireinject 3 | 4 | package main 5 | 6 | import ( 7 | "log" 8 | 9 | eagle "github.com/go-eagle/eagle/pkg/app" 10 | "github.com/go-eagle/eagle/pkg/client/consulclient" 11 | "github.com/go-eagle/eagle/pkg/client/etcdclient" 12 | "github.com/go-eagle/eagle/pkg/client/nacosclient" 13 | logger "github.com/go-eagle/eagle/pkg/log" 14 | "github.com/go-eagle/eagle/pkg/registry" 15 | "github.com/go-eagle/eagle/pkg/registry/consul" 16 | "github.com/go-eagle/eagle/pkg/registry/etcd" 17 | "github.com/go-eagle/eagle/pkg/registry/nacos" 18 | "github.com/go-eagle/eagle/pkg/transport/grpc" 19 | "github.com/go-microservice/user-service/internal/cache" 20 | "github.com/go-microservice/user-service/internal/repository" 21 | "github.com/go-microservice/user-service/internal/server" 22 | "github.com/go-microservice/user-service/internal/service" 23 | "github.com/google/wire" 24 | ) 25 | 26 | func InitApp(cfg *eagle.Config, config *eagle.ServerConfig) (*eagle.App, func(), error) { 27 | wire.Build(server.ProviderSet, service.ProviderSet, repository.ProviderSet, cache.ProviderSet, newApp) 28 | return &eagle.App{}, nil, nil 29 | } 30 | 31 | func newApp(cfg *eagle.Config, gs *grpc.Server) *eagle.App { 32 | return eagle.New( 33 | eagle.WithName(cfg.Name), 34 | eagle.WithVersion(cfg.Version), 35 | eagle.WithLogger(logger.GetLogger()), 36 | eagle.WithServer( 37 | // init HTTP server 38 | server.NewHTTPServer(&cfg.HTTP), 39 | // init gRPC server 40 | gs, 41 | ), 42 | eagle.WithRegistry(getConsulRegistry()), 43 | ) 44 | } 45 | 46 | // create a etcd register 47 | func getEtcdRegistry() registry.Registry { 48 | client, err := etcdclient.New() 49 | if err != nil { 50 | log.Fatal(err) 51 | } 52 | return etcd.New(client.Client) 53 | } 54 | 55 | // create a consul register 56 | func getConsulRegistry() registry.Registry { 57 | client, err := consulclient.New() 58 | if err != nil { 59 | panic(err) 60 | } 61 | return consul.New(client) 62 | } 63 | 64 | // create a nacos register 65 | func getNacosRegistry() registry.Registry { 66 | client, err := nacosclient.New() 67 | if err != nil { 68 | panic(err) 69 | } 70 | return nacos.New(client) 71 | } 72 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # user service 2 | 3 | ## feature 4 | 5 | - 注册 6 | - 登录 7 | - 获取用户信息 8 | - 批量获取用户信息 9 | - 更新用户信息 10 | - 更新用户密码 11 | 12 | ## 开发流程 13 | 14 | > 以下命令执行都是在根目录下执行 15 | 16 | ### 1. 编写 proto 文件 17 | 18 | 可以使用 `eagle proto add api/user/v1/user.proto` 的方式新建proto文件 19 | 20 | 执行后,会在对应的目录下生成一个 proto 文件,里面直接写业务定义就可以了。 21 | 22 | ### 2. 生成对应的pb文件 23 | 24 | 有两种方式可以生成: 25 | 26 | `make grpc` 27 | 或 28 | `eagle proto client api/user/v1/user.proto` 29 | 30 | 都会生成两个文件 `api/user/v1/user.pb.go` 和 `api/user/v1/user_grpc.pb.go` 31 | 32 | ### 3. 生成对应的server文件 33 | 34 | `eagle proto server api/user/v1/user.proto` 35 | 36 | 执行该命令后,会在 `internal/service` 下 多一个 `user_grpc.go` 文件。 37 | 38 | ### 4. 编写业务逻辑 39 | 40 | 在 `internal/service/user_svc.go` 里直接写业务逻辑接口。 41 | 42 | ### 5. 转换service输出到pb 43 | 44 | 在 `internal/service/user_grpc.go` 中将 `user_grpc.go` 转为pb输出的方式。 45 | 46 | ### 6. 将业务注册到gRPC server 中 47 | 48 | 在 `internal/server/grpc.go` 中新增 `v1.RegisterUserServiceServer(grpcServer, service.NewUserService())` 49 | 50 | ## 运行 51 | 52 | 在根目录下执行以下命令 53 | 54 | ```bash 55 | # 运行 56 | make run 57 | 58 | # 生成 docker 竞相,v1.0.0 对应仓库打的上线tag 59 | make GIT_TAG=v1.0.0 docker 60 | ` 61 | 62 | grpc即可正常启动 63 | 64 | ## 调试 65 | 66 | ### 安装 grpcurl 67 | 68 | ```bash 69 | go install github.com/fullstorydev/grpcurl/cmd/grpcurl@latest 70 | ``` 71 | 72 | ### 返回结果 73 | 74 | 正常返回 75 | 76 | ```bash 77 | ➜ ~ grpcurl -plaintext -d '{"email":"12345678@cc.com","username":"admin8","password":"123456"}' localhost:9090 user.v1.UserService/Register 78 | { 79 | "username": "admin8" 80 | } 81 | ``` 82 | 83 | 异常返回 84 | 85 | ```bash 86 | ➜ ~ grpcurl -plaintext -d '{"email":"1234567@cc.com","username":"admin7","password":"123456"}' localhost:9090 user.v1.UserService/Register 87 | ERROR: 88 | Code: Code(20100) 89 | Message: The user already exists. 90 | Details: 91 | 1) {"@type":"type.googleapis.com/micro.user.v1.RegisterRequest","email":"1234567@cc.com","password":"123456","username":"admin7"} 92 | ``` 93 | 94 | ## 启动异步任务 95 | 96 | ```bash 97 | # 直接运行 98 | go run cmd/cron/main.go -c=config -e=dev 99 | 100 | # 编译 101 | go build -o cron cmd/cron/main.go 102 | 103 | # 运行 104 | ./cron -c=config -e=env 105 | 或者 106 | APP_ENV ./cron -c=config 107 | ``` 108 | -------------------------------------------------------------------------------- /deploy/redis/deployment-service.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: Service # Type of Kubernetes resource 4 | metadata: 5 | name: redis-svc # Name of the Kubernetes resource 6 | labels: # Labels that will be applied to this resource 7 | app: redis 8 | role: master 9 | tier: backend 10 | spec: 11 | type: NodePort 12 | ports: 13 | - port: 6379 # Map incoming connections on port 6379 to the target port 6379 of the Pod 14 | targetPort: 6379 15 | nodePort: 31379 16 | selector: # Map any Pod with the specified labels to this service 17 | app: redis 18 | role: master 19 | tier: backend 20 | --- 21 | apiVersion: apps/v1 22 | kind: Deployment 23 | metadata: 24 | name: redis # Unique name for the deployment 25 | labels: 26 | app: redis # Labels to be applied to this deployment 27 | spec: 28 | selector: 29 | matchLabels: # This deployment applies to the Pods matching these labels 30 | app: redis 31 | role: master 32 | tier: backend 33 | replicas: 1 # Run a single pod in the deployment 34 | template: # Template for the pods that will be created by this deployment 35 | metadata: 36 | labels: # Labels to be applied to the Pods in this deployment 37 | app: redis 38 | role: master 39 | tier: backend 40 | spec: # Spec for the container which will be run inside the Pod. 41 | containers: 42 | - name: redis 43 | image: redis:6.0 44 | resources: 45 | requests: 46 | cpu: 100m 47 | memory: 100Mi 48 | # command: ["redis-server","/etc/redis/redis.conf"] 49 | command: 50 | - redis-server 51 | - /etc/redis/redis.conf 52 | ports: 53 | - containerPort: 6379 54 | volumeMounts: 55 | - name: redis-config 56 | mountPath: /etc/redis/redis.conf 57 | subPath: redis.conf 58 | - name: redis-storage 59 | mountPath: /data 60 | volumes: 61 | - name: redis-storage 62 | emptyDir: {} 63 | - name: redis-config 64 | configMap: 65 | name: redis-config 66 | items: 67 | - key: redis.conf 68 | path: redis.conf 69 | -------------------------------------------------------------------------------- /cmd/cron/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "log" 7 | "os" 8 | 9 | eagle "github.com/go-eagle/eagle/pkg/app" 10 | "github.com/go-eagle/eagle/pkg/config" 11 | logger "github.com/go-eagle/eagle/pkg/log" 12 | "github.com/go-eagle/eagle/pkg/redis" 13 | v "github.com/go-eagle/eagle/pkg/version" 14 | "github.com/go-microservice/user-service/internal/model" 15 | "github.com/spf13/pflag" 16 | 17 | "github.com/go-microservice/user-service/internal/tasks" 18 | 19 | "github.com/hibiken/asynq" 20 | ) 21 | 22 | var ( 23 | cfgDir = pflag.StringP("config dir", "c", "config", "config path.") 24 | env = pflag.StringP("env name", "e", "dev", "env var name.") 25 | version = pflag.BoolP("version", "v", false, "show version info.") 26 | ) 27 | 28 | func init() { 29 | pflag.Parse() 30 | if *version { 31 | ver := v.Get() 32 | marshaled, err := json.MarshalIndent(&ver, "", " ") 33 | if err != nil { 34 | fmt.Printf("%v\n", err) 35 | os.Exit(1) 36 | } 37 | 38 | fmt.Println(string(marshaled)) 39 | return 40 | } 41 | 42 | // init config 43 | c := config.New(*cfgDir, config.WithEnv(*env)) 44 | var cfg eagle.Config 45 | if err := c.Load("app", &cfg); err != nil { 46 | panic(err) 47 | } 48 | // set global 49 | eagle.Conf = &cfg 50 | 51 | // -------------- init resource ------------- 52 | logger.Init() 53 | // init db 54 | model.Init() 55 | // init redis 56 | redis.Init() 57 | } 58 | 59 | func main() { 60 | // load config 61 | c := config.New(*cfgDir, config.WithEnv(*env)) 62 | var cfg tasks.Config 63 | if err := c.Load("cron", &cfg); err != nil { 64 | panic(err) 65 | } 66 | 67 | // -------------- Run worker server ------------ 68 | srv := asynq.NewServer( 69 | asynq.RedisClientOpt{Addr: cfg.Addr}, 70 | asynq.Config{ 71 | // Specify how many concurrent workers to use 72 | Concurrency: cfg.Concurrency, 73 | // Optionally specify multiple queues with different priority. 74 | Queues: map[string]int{ 75 | tasks.QueueCritical: 6, 76 | tasks.QueueDefault: 3, 77 | tasks.QueueLow: 1, 78 | }, 79 | // See the godoc for other configuration options 80 | }, 81 | ) 82 | 83 | // mux maps a type to a handler 84 | mux := asynq.NewServeMux() 85 | // register handlers... 86 | mux.HandleFunc(tasks.TypeEmailWelcome, tasks.HandleEmailWelcomeTask) 87 | 88 | if err := srv.Run(mux); err != nil { 89 | log.Fatalf("could not run server: %v", err) 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /cmd/server/main.go: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * ____ __ 4 | * / __/__ ____ _/ /__ 5 | * / _// _ `/ _ `/ / -_) 6 | * /___/\_,_/\_, /_/\__/ 7 | * /___/ 8 | * 9 | * 10 | * generate by http://patorjk.com/software/taag/#p=display&f=Small%20Slant&t=Eagle 11 | */ 12 | package main 13 | 14 | import ( 15 | "encoding/json" 16 | "fmt" 17 | "log" 18 | "net/http" 19 | "os" 20 | 21 | "github.com/gin-gonic/gin" 22 | eagle "github.com/go-eagle/eagle/pkg/app" 23 | "github.com/go-eagle/eagle/pkg/config" 24 | logger "github.com/go-eagle/eagle/pkg/log" 25 | "github.com/go-eagle/eagle/pkg/trace" 26 | v "github.com/go-eagle/eagle/pkg/version" 27 | "github.com/spf13/pflag" 28 | _ "go.uber.org/automaxprocs" 29 | ) 30 | 31 | var ( 32 | cfgDir = pflag.StringP("config dir", "c", "config", "config path.") 33 | env = pflag.StringP("env name", "e", "", "env var name.") 34 | version = pflag.BoolP("version", "v", false, "show version info.") 35 | ) 36 | 37 | // @title eagle docs api 38 | // @version 1.0 39 | // @description eagle demo 40 | 41 | // @host localhost:8080 42 | // @BasePath /v1 43 | func main() { 44 | pflag.Parse() 45 | if *version { 46 | ver := v.Get() 47 | marshaled, err := json.MarshalIndent(&ver, "", " ") 48 | if err != nil { 49 | fmt.Printf("%v\n", err) 50 | os.Exit(1) 51 | } 52 | 53 | fmt.Println(string(marshaled)) 54 | return 55 | } 56 | 57 | // init config 58 | c := config.New(*cfgDir, config.WithEnv(*env)) 59 | var cfg eagle.Config 60 | if err := c.Load("app", &cfg); err != nil { 61 | panic(err) 62 | } 63 | // set global 64 | eagle.Conf = &cfg 65 | 66 | // -------------- init resource ------------- 67 | logger.Init() 68 | 69 | gin.SetMode(cfg.Mode) 70 | 71 | if cfg.EnableTrace { 72 | var traceCfg trace.Config 73 | var err error 74 | err = config.Load("trace", &traceCfg) 75 | _, err = trace.InitTracerProvider(traceCfg.ServiceName, traceCfg.CollectorEndpoint) 76 | if err != nil { 77 | panic(err) 78 | } 79 | } 80 | 81 | // init pprof server 82 | go func() { 83 | fmt.Printf("Listening and serving PProf HTTP on %s\n", cfg.PprofPort) 84 | if err := http.ListenAndServe(cfg.PprofPort, http.DefaultServeMux); err != nil && err != http.ErrServerClosed { 85 | log.Fatalf("listen ListenAndServe for PProf, err: %s", err.Error()) 86 | } 87 | }() 88 | 89 | // start app 90 | app, cleanup, err := InitApp(&cfg, &cfg.GRPC) 91 | defer cleanup() 92 | if err != nil { 93 | panic(err) 94 | } 95 | if err := app.Run(); err != nil { 96 | panic(err) 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /third_party/google/protobuf/empty.proto: -------------------------------------------------------------------------------- 1 | // Protocol Buffers - Google's data interchange format 2 | // Copyright 2008 Google Inc. All rights reserved. 3 | // https://developers.google.com/protocol-buffers/ 4 | // 5 | // Redistribution and use in source and binary forms, with or without 6 | // modification, are permitted provided that the following conditions are 7 | // met: 8 | // 9 | // * Redistributions of source code must retain the above copyright 10 | // notice, this list of conditions and the following disclaimer. 11 | // * Redistributions in binary form must reproduce the above 12 | // copyright notice, this list of conditions and the following disclaimer 13 | // in the documentation and/or other materials provided with the 14 | // distribution. 15 | // * Neither the name of Google Inc. nor the names of its 16 | // contributors may be used to endorse or promote products derived from 17 | // this software without specific prior written permission. 18 | // 19 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 20 | // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 21 | // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 22 | // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 23 | // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 24 | // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 25 | // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 26 | // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 27 | // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 28 | // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | 31 | syntax = "proto3"; 32 | 33 | package google.protobuf; 34 | 35 | option csharp_namespace = "Google.Protobuf.WellKnownTypes"; 36 | option go_package = "google.golang.org/protobuf/types/known/emptypb"; 37 | option java_package = "com.google.protobuf"; 38 | option java_outer_classname = "EmptyProto"; 39 | option java_multiple_files = true; 40 | option objc_class_prefix = "GPB"; 41 | option cc_enable_arenas = true; 42 | 43 | // A generic empty message that you can re-use to avoid defining duplicated 44 | // empty messages in your APIs. A typical example is to use it as the request 45 | // or the response type of an API method. For instance: 46 | // 47 | // service Foo { 48 | // rpc Bar(google.protobuf.Empty) returns (google.protobuf.Empty); 49 | // } 50 | // 51 | // The JSON representation for `Empty` is empty JSON object `{}`. 52 | message Empty {} -------------------------------------------------------------------------------- /internal/cache/user_token_cache.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | //go:generate mockgen -source=internal/cache/user_token_cache.go -destination=internal/mock/user_token_cache_mock.go -package mock 4 | 5 | import ( 6 | "context" 7 | "errors" 8 | "fmt" 9 | "time" 10 | 11 | "github.com/go-eagle/eagle/pkg/cache" 12 | "github.com/go-eagle/eagle/pkg/encoding" 13 | "github.com/go-eagle/eagle/pkg/log" 14 | "github.com/go-eagle/eagle/pkg/redis" 15 | ) 16 | 17 | const ( 18 | // PrefixUserTokenCacheKey cache prefix 19 | PrefixUserTokenCacheKey = "user:token:%d" 20 | UserTokenExpireTime = 24 * time.Hour * 30 21 | ) 22 | 23 | // UserToken define cache interface 24 | type UserTokenCache interface { 25 | SetUserTokenCache(ctx context.Context, id int64, token string, duration time.Duration) error 26 | GetUserTokenCache(ctx context.Context, id int64) (token string, err error) 27 | DelUserTokenCache(ctx context.Context, id int64) error 28 | } 29 | 30 | // userTokenCache define cache struct 31 | type userTokenCache struct { 32 | cache cache.Cache 33 | } 34 | 35 | // NewUserTokenCache new a cache 36 | func NewUserTokenCache() UserTokenCache { 37 | jsonEncoding := encoding.JSONEncoding{} 38 | cachePrefix := "" 39 | return &userTokenCache{ 40 | cache: cache.NewRedisCache(redis.RedisClient, cachePrefix, jsonEncoding, func() interface{} { 41 | return "" 42 | }), 43 | } 44 | } 45 | 46 | // GetUserTokenCacheKey get cache key 47 | func (c *userTokenCache) GetUserTokenCacheKey(id int64) string { 48 | return fmt.Sprintf(PrefixUserTokenCacheKey, id) 49 | } 50 | 51 | // SetUserTokenCache write to cache 52 | func (c *userTokenCache) SetUserTokenCache(ctx context.Context, id int64, token string, duration time.Duration) error { 53 | if len(token) == 0 || id == 0 { 54 | return nil 55 | } 56 | cacheKey := c.GetUserTokenCacheKey(id) 57 | err := c.cache.Set(ctx, cacheKey, &token, duration) 58 | if err != nil { 59 | return err 60 | } 61 | return nil 62 | } 63 | 64 | // GetUserTokenCache get from cache 65 | func (c *userTokenCache) GetUserTokenCache(ctx context.Context, id int64) (token string, err error) { 66 | cacheKey := c.GetUserTokenCacheKey(id) 67 | err = c.cache.Get(ctx, cacheKey, &token) 68 | if err != nil && !errors.Is(err, redis.ErrRedisNotFound) { 69 | log.WithContext(ctx).Warnf("get err from redis, err: %+v", err) 70 | return "", err 71 | } 72 | return token, nil 73 | } 74 | 75 | // DelUserTokenCache delete cache 76 | func (c *userTokenCache) DelUserTokenCache(ctx context.Context, id int64) error { 77 | cacheKey := c.GetUserTokenCacheKey(id) 78 | err := c.cache.Del(ctx, cacheKey) 79 | if err != nil { 80 | return err 81 | } 82 | return nil 83 | } 84 | -------------------------------------------------------------------------------- /cmd/server/wire_gen.go: -------------------------------------------------------------------------------- 1 | // Code generated by Wire. DO NOT EDIT. 2 | 3 | //go:generate go run -mod=mod github.com/google/wire/cmd/wire 4 | //go:build !wireinject 5 | // +build !wireinject 6 | 7 | package main 8 | 9 | import ( 10 | "github.com/go-eagle/eagle/pkg/app" 11 | "github.com/go-eagle/eagle/pkg/client/consulclient" 12 | "github.com/go-eagle/eagle/pkg/client/etcdclient" 13 | "github.com/go-eagle/eagle/pkg/client/nacosclient" 14 | "github.com/go-eagle/eagle/pkg/log" 15 | "github.com/go-eagle/eagle/pkg/redis" 16 | "github.com/go-eagle/eagle/pkg/registry" 17 | "github.com/go-eagle/eagle/pkg/registry/consul" 18 | "github.com/go-eagle/eagle/pkg/registry/etcd" 19 | "github.com/go-eagle/eagle/pkg/registry/nacos" 20 | "github.com/go-eagle/eagle/pkg/transport/grpc" 21 | "github.com/go-microservice/user-service/internal/cache" 22 | "github.com/go-microservice/user-service/internal/model" 23 | "github.com/go-microservice/user-service/internal/repository" 24 | "github.com/go-microservice/user-service/internal/server" 25 | "github.com/go-microservice/user-service/internal/service" 26 | log2 "log" 27 | ) 28 | 29 | import ( 30 | _ "go.uber.org/automaxprocs" 31 | ) 32 | 33 | // Injectors from wire.go: 34 | 35 | func InitApp(cfg *app.Config, config *app.ServerConfig) (*app.App, func(), error) { 36 | db, cleanup, err := model.Init() 37 | if err != nil { 38 | return nil, nil, err 39 | } 40 | client, cleanup2, err := redis.Init() 41 | if err != nil { 42 | cleanup() 43 | return nil, nil, err 44 | } 45 | userCache := cache.NewUserCache(client) 46 | userRepo := repository.NewUser(db, userCache) 47 | userServiceServer := service.NewUserServiceServer(userRepo) 48 | grpcServer := server.NewGRPCServer(config, userServiceServer) 49 | appApp := newApp(cfg, grpcServer) 50 | return appApp, func() { 51 | cleanup2() 52 | cleanup() 53 | }, nil 54 | } 55 | 56 | // wire.go: 57 | 58 | func newApp(cfg *app.Config, gs *grpc.Server) *app.App { 59 | return app.New(app.WithName(cfg.Name), app.WithVersion(cfg.Version), app.WithLogger(log.GetLogger()), app.WithServer(server.NewHTTPServer(&cfg.HTTP), gs), app.WithRegistry(getConsulRegistry()), 60 | ) 61 | } 62 | 63 | // create a etcd register 64 | func getEtcdRegistry() registry.Registry { 65 | client, err := etcdclient.New() 66 | if err != nil { 67 | log2.Fatal(err) 68 | } 69 | return etcd.New(client.Client) 70 | } 71 | 72 | // create a consul register 73 | func getConsulRegistry() registry.Registry { 74 | client, err := consulclient.New() 75 | if err != nil { 76 | panic(err) 77 | } 78 | return consul.New(client) 79 | } 80 | 81 | // create a nacos register 82 | func getNacosRegistry() registry.Registry { 83 | client, err := nacosclient.New() 84 | if err != nil { 85 | panic(err) 86 | } 87 | return nacos.New(client) 88 | } 89 | -------------------------------------------------------------------------------- /internal/cache/user_cache_test.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | "time" 7 | 8 | "github.com/stretchr/testify/assert" 9 | 10 | "github.com/alicebob/miniredis/v2" 11 | "github.com/go-redis/redis/v8" 12 | 13 | "github.com/go-microservice/user-service/internal/model" 14 | ) 15 | 16 | var ( 17 | redisServer *miniredis.Miniredis 18 | redisClient *redis.Client 19 | testData = &model.UserModel{ID: 1, Username: "test"} 20 | uc UserCache 21 | ) 22 | 23 | func setup() { 24 | redisServer = mockRedis() 25 | redisClient = redis.NewClient(&redis.Options{Addr: redisServer.Addr()}) 26 | uc = NewUserCache(redisClient) 27 | } 28 | 29 | func teardown() { 30 | redisServer.Close() 31 | } 32 | 33 | func mockRedis() *miniredis.Miniredis { 34 | s, err := miniredis.Run() 35 | if err != nil { 36 | panic(err) 37 | } 38 | return s 39 | } 40 | 41 | func Test_userCache_SetUserCache(t *testing.T) { 42 | setup() 43 | defer teardown() 44 | 45 | var id int64 46 | ctx := context.Background() 47 | id = 1 48 | err := uc.SetUserCache(ctx, id, testData, time.Hour) 49 | if err != nil { 50 | t.Fatal(err) 51 | } 52 | } 53 | 54 | func Test_userCache_GetUserCache(t *testing.T) { 55 | setup() 56 | defer teardown() 57 | 58 | var id int64 59 | ctx := context.Background() 60 | id = 1 61 | err := uc.SetUserCache(ctx, id, testData, time.Hour) 62 | if err != nil { 63 | t.Fatal(err) 64 | } 65 | 66 | act, err := uc.GetUserCache(ctx, id) 67 | if err != nil { 68 | t.Fatal(err) 69 | } 70 | assert.Equal(t, testData, act) 71 | } 72 | 73 | func Test_userCache_MultiGetUserCache(t *testing.T) { 74 | setup() 75 | defer teardown() 76 | 77 | ctx := context.Background() 78 | testData := []*model.UserModel{ 79 | {ID: 1}, 80 | {ID: 2}, 81 | } 82 | err := uc.MultiSetUserCache(ctx, testData, time.Hour) 83 | if err != nil { 84 | t.Fatal(err) 85 | } 86 | 87 | expected := make(map[string]*model.UserModel) 88 | expected["user:1"] = &model.UserModel{ID: 1} 89 | expected["user:2"] = &model.UserModel{ID: 2} 90 | 91 | act, err := uc.MultiGetUserCache(ctx, []int64{1, 2}) 92 | if err != nil { 93 | t.Fatal(err) 94 | } 95 | assert.Equal(t, expected, act) 96 | } 97 | 98 | func Test_userCache_MultiSetUserCache(t *testing.T) { 99 | setup() 100 | defer teardown() 101 | 102 | ctx := context.Background() 103 | testData := []*model.UserModel{ 104 | {ID: 1}, 105 | {ID: 2}, 106 | } 107 | err := uc.MultiSetUserCache(ctx, testData, time.Hour) 108 | if err != nil { 109 | t.Fatal(err) 110 | } 111 | } 112 | 113 | func Test_userCache_DelUserCache(t *testing.T) { 114 | setup() 115 | defer teardown() 116 | 117 | var id int64 118 | ctx := context.Background() 119 | id = 1 120 | err := uc.DelUserCache(ctx, id) 121 | if err != nil { 122 | t.Fatal(err) 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /third_party/google/api/httpbody.proto: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | syntax = "proto3"; 16 | 17 | package google.api; 18 | 19 | import "google/protobuf/any.proto"; 20 | 21 | option cc_enable_arenas = true; 22 | option go_package = "google.golang.org/genproto/googleapis/api/httpbody;httpbody"; 23 | option java_multiple_files = true; 24 | option java_outer_classname = "HttpBodyProto"; 25 | option java_package = "com.google.api"; 26 | option objc_class_prefix = "GAPI"; 27 | 28 | // Message that represents an arbitrary HTTP body. It should only be used for 29 | // payload formats that can't be represented as JSON, such as raw binary or 30 | // an HTML page. 31 | // 32 | // 33 | // This message can be used both in streaming and non-streaming API methods in 34 | // the request as well as the response. 35 | // 36 | // It can be used as a top-level request field, which is convenient if one 37 | // wants to extract parameters from either the URL or HTTP template into the 38 | // request fields and also want access to the raw HTTP body. 39 | // 40 | // Example: 41 | // 42 | // message GetResourceRequest { 43 | // // A unique request id. 44 | // string request_id = 1; 45 | // 46 | // // The raw HTTP body is bound to this field. 47 | // google.api.HttpBody http_body = 2; 48 | // } 49 | // 50 | // service ResourceService { 51 | // rpc GetResource(GetResourceRequest) returns (google.api.HttpBody); 52 | // rpc UpdateResource(google.api.HttpBody) returns 53 | // (google.protobuf.Empty); 54 | // } 55 | // 56 | // Example with streaming methods: 57 | // 58 | // service CaldavService { 59 | // rpc GetCalendar(stream google.api.HttpBody) 60 | // returns (stream google.api.HttpBody); 61 | // rpc UpdateCalendar(stream google.api.HttpBody) 62 | // returns (stream google.api.HttpBody); 63 | // } 64 | // 65 | // Use of this type only changes how the request and response bodies are 66 | // handled, all other features will continue to work unchanged. 67 | message HttpBody { 68 | // The HTTP Content-Type header value specifying the content type of the body. 69 | string content_type = 1; 70 | 71 | // The HTTP request/response body as raw binary. 72 | bytes data = 2; 73 | 74 | // Application specific response metadata. Must be set in the first response 75 | // for streaming APIs. 76 | repeated google.protobuf.Any extensions = 3; 77 | } -------------------------------------------------------------------------------- /internal/service/verify_code_svc.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "math/rand" 7 | "strconv" 8 | "time" 9 | 10 | "github.com/go-eagle/eagle/pkg/log" 11 | "github.com/go-eagle/eagle/pkg/redis" 12 | "github.com/pkg/errors" 13 | ) 14 | 15 | const ( 16 | verifyCodeRedisKey = "user_svc:verify:code:%s" // 验证码key 17 | maxDurationTime = 10 * time.Minute // 验证码有效期 18 | ) 19 | 20 | // phone white list 21 | var phoneWhiteLit = []string{ 22 | "13000001111", 23 | } 24 | 25 | // VerifyCodeService define a interface 26 | type VerifyCodeService interface { 27 | GenVerifyCode(ctx context.Context, phone string) (int, error) 28 | CheckVerifyCode(ctx context.Context, phone string, vCode string) bool 29 | GetVerifyCode(ctx context.Context, phone string) (vCode string, err error) 30 | } 31 | 32 | type verifyCodeService struct { 33 | } 34 | 35 | var _ VerifyCodeService = (*verifyCodeService)(nil) 36 | 37 | func newVerifyCodeService() *verifyCodeService { 38 | return &verifyCodeService{} 39 | } 40 | 41 | // GenLoginVCode 生成校验码 42 | func (s *verifyCodeService) GenVerifyCode(ctx context.Context, phone string) (int, error) { 43 | // step1: 生成随机数 44 | vCodeStr := fmt.Sprintf("%06v", rand.New(rand.NewSource(time.Now().UnixNano())).Int31n(1000000)) 45 | 46 | // step2: 写入到redis里 47 | // 使用set, key使用前缀+手机号 缓存10分钟) 48 | key := fmt.Sprintf(verifyCodeRedisKey, phone) 49 | err := redis.RedisClient.Set(ctx, key, vCodeStr, maxDurationTime).Err() 50 | if err != nil { 51 | return 0, errors.Wrap(err, "[vcode_svc] gen verify code from redis set err") 52 | } 53 | 54 | vCode, err := strconv.Atoi(vCodeStr) 55 | if err != nil { 56 | return 0, errors.Wrap(err, "[vcode_svc] string convert int err") 57 | } 58 | 59 | return vCode, nil 60 | } 61 | 62 | // isTestPhone add test phone number to here and avoid to verify 63 | func isTestPhone(phone string) bool { 64 | for _, val := range phoneWhiteLit { 65 | if val == phone { 66 | return true 67 | } 68 | } 69 | return false 70 | } 71 | 72 | // CheckLoginVCode check phone if correct 73 | func (s *verifyCodeService) CheckVerifyCode(ctx context.Context, phone string, vCode string) bool { 74 | if isTestPhone(phone) { 75 | return true 76 | } 77 | 78 | oldVCode, err := s.GetVerifyCode(ctx, phone) 79 | if err != nil { 80 | log.Warnf("[vcode_svc] get verify code err, %v", err) 81 | return false 82 | } 83 | 84 | if vCode != oldVCode { 85 | return false 86 | } 87 | 88 | return true 89 | } 90 | 91 | // GetLoginVCode get verify code 92 | func (s *verifyCodeService) GetVerifyCode(ctx context.Context, phone string) (vCode string, err error) { 93 | key := fmt.Sprintf(verifyCodeRedisKey, phone) 94 | vCode, err = redis.RedisClient.Get(ctx, key).Result() 95 | if err == redis.ErrRedisNotFound { 96 | return 97 | } else if err != nil { 98 | return "", errors.Wrap(err, "[vcode_svc] redis get verify code err") 99 | } 100 | 101 | return vCode, nil 102 | } 103 | -------------------------------------------------------------------------------- /internal/repository/user_repo_test.go: -------------------------------------------------------------------------------- 1 | package repository 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | "time" 7 | 8 | sqlmock "github.com/DATA-DOG/go-sqlmock" 9 | "github.com/golang/mock/gomock" 10 | "github.com/stretchr/testify/assert" 11 | 12 | "github.com/go-microservice/user-service/internal/mocks" 13 | "github.com/go-microservice/user-service/internal/model" 14 | ) 15 | 16 | func Test_userRepo_CreateUser(t *testing.T) { 17 | ctl := gomock.NewController(t) 18 | defer ctl.Finish() 19 | 20 | ctx := context.Background() 21 | data := &model.UserModel{ 22 | Username: "test-username", 23 | CreatedAt: time.Now().Unix(), 24 | UpdatedAt: time.Now().Unix(), 25 | } 26 | mock.ExpectBegin() 27 | mock.ExpectExec("INSERT INTO `user_info` (`username`,`nickname`,`phone`,`email`,`password`,`avatar`,`gender`,`birthday`,`bio`,`login_at`,`status`,`created_at`,`updated_at`) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)"). 28 | WithArgs( 29 | data.Username, 30 | data.Nickname, 31 | data.Phone, 32 | data.Email, 33 | data.Password, 34 | data.Avatar, 35 | data.Gender, 36 | data.Birthday, 37 | data.Bio, 38 | data.LoginAt, 39 | data.Status, 40 | data.CreatedAt, 41 | data.UpdatedAt, 42 | ). 43 | WillReturnResult(sqlmock.NewResult(1, 1)) 44 | mock.ExpectCommit() 45 | 46 | user := NewUser(mockDB, nil) 47 | ret, err := user.CreateUser(ctx, data) 48 | assert.NoError(t, err) 49 | assert.NotNil(t, ret) 50 | } 51 | 52 | func Test_userRepo_GetUser(t *testing.T) { 53 | ctl := gomock.NewController(t) 54 | defer ctl.Finish() 55 | 56 | var id int64 57 | ctx := context.Background() 58 | id = 1 59 | 60 | mockCache := mocks.NewMockUserCache(ctl) 61 | mockCache.EXPECT().GetUserCache(ctx, id).Return(&model.UserModel{ID: id}, nil).Times(1) 62 | 63 | mock.ExpectQuery("SELECT * FROM user_info WHERE id = ?"). 64 | WithArgs(data.Username). 65 | WillReturnRows( 66 | sqlmock.NewRows([]string{`id`, `username`, `nickname`, `phone`, `email`, `password`, `avatar`, `gender`, 67 | `birthday`, `bio`, `login_at`, `status`, `created_at`, `updated_at`}). 68 | AddRow(1, data.Username, data.Nickname, data.Phone, data.Email, data.Password, data.Avatar, 69 | data.Gender, data.Birthday, data.Bio, data.LoginAt, data.Status, data.CreatedAt, data.UpdatedAt, 70 | ), 71 | ) 72 | 73 | user := NewUser(mockDB, mockCache) 74 | ret, err := user.GetUser(ctx, id) 75 | assert.NoError(t, err) 76 | assert.NotNil(t, ret) 77 | assert.Equal(t, id, ret.ID) 78 | } 79 | 80 | func Test_userRepo_GetUserByUsername(t *testing.T) { 81 | ctl := gomock.NewController(t) 82 | defer ctl.Finish() 83 | 84 | ctx := context.Background() 85 | 86 | mock.ExpectQuery("SELECT * FROM user_info WHERE username = ?"). 87 | WithArgs(data.Username). 88 | WillReturnRows( 89 | sqlmock.NewRows([]string{`id`, `username`, `nickname`, `phone`, `email`, `password`, `avatar`, `gender`, 90 | `birthday`, `bio`, `login_at`, `status`, `created_at`, `updated_at`}). 91 | AddRow(1, data.Username, data.Nickname, data.Phone, data.Email, data.Password, data.Avatar, 92 | data.Gender, data.Birthday, data.Bio, data.LoginAt, data.Status, data.CreatedAt, data.UpdatedAt, 93 | ), 94 | ) 95 | 96 | user := NewUser(mockDB, nil) 97 | ret, err := user.GetUserByUsername(ctx, data.Username) 98 | assert.NoError(t, err) 99 | assert.NotNil(t, ret) 100 | assert.Equal(t, data.Username, ret.Username) 101 | } 102 | -------------------------------------------------------------------------------- /config/dev/config.yaml: -------------------------------------------------------------------------------- 1 | app: 2 | Name: eagle 3 | Version: 1.0.0 4 | PprofPort: :5555 5 | Mode: debug # debug, release, test 6 | JwtSecret: JWT_SECRET 7 | JwtTimeout: 86400 8 | CookieName: jwt-token 9 | SSL: true 10 | CtxDefaultTimeout: 12 11 | CSRF: true 12 | Debug: false 13 | 14 | Http: 15 | Addr: :8080 16 | ReadTimeout: 3s 17 | WriteTimeout: 3s 18 | Grpc: 19 | Addr: :9090 20 | ReadTimeout: 5s 21 | WriteTimeout: 5s 22 | 23 | logger: 24 | Development: false 25 | DisableCaller: false 26 | DisableStacktrace: false 27 | Encoding: json # json or console 28 | Level: info # 日志级别,INFO, WARN, ERROR 29 | Name: eagle 30 | Writers: console # 有2个可选项:file,console 选择file会将日志记录到logger_file指定的日志文件中,选择console会将日志输出到标准输出,当然也可以两者同时选择 31 | LoggerFile: /tmp/log/eagle.log 32 | LoggerWarnFile: /tmp/log/eagle.wf.log 33 | LoggerErrorFile: /tmp/log/eagle.err.log 34 | LogRollingPolicy: daily 35 | LogRotateDate: 1 36 | LogRotateSize: 1 37 | LogBackupCount: 7 38 | 39 | orm: 40 | Name: eagle # 数据库名称 41 | Addr: localhost:3306 # 如果是 docker,可以替换为 对应的服务名称,eg: db:3306 42 | UserName: root 43 | Password: 123456 44 | ShowLog: true # 是否打印所有SQL日志 45 | MaxIdleConn: 10 # 最大闲置的连接数,0意味着使用默认的大小2, 小于0表示不使用连接池 46 | MaxOpenConn: 60 # 最大打开的连接数, 需要小于数据库配置中的max_connections数 47 | ConnMaxLifeTime: 4h # 单个连接最大存活时间,建议设置比数据库超时时长(wait_timeout)稍小一些 48 | SlowThreshold: 0 # 慢查询阈值,设置后只打印慢查询日志,默认为500ms 49 | 50 | mysql: 51 | Dsn: "root:123456@tcp(localhost:3306)/eagle?timeout=2s&readTimeout=5s&writeTimeout=5s&parseTime=true&loc=Local&charset=utf8,utf8mb4" 52 | ShowLog: true # 是否打印SQL日志 53 | MaxIdleConn: 10 # 最大闲置的连接数,0意味着使用默认的大小2, 小于0表示不使用连接池 54 | MaxOpenConn: 60 # 最大打开的连接数, 需要小于数据库配置中的max_connections数 55 | ConnMaxLifeTime: 4000 # 单个连接最大存活时间,建议设置比数据库超时时长(wait_timeout)稍小一些 56 | QueryTimeout: 200 57 | ExecTimeout: 200 58 | TranTimeout: 200 59 | Braker: # 熔断器配置 60 | window: 3s 61 | sleep: 100ms 62 | bucket: 100 63 | ratio: 0.5 64 | request: 100 65 | 66 | 67 | redis: 68 | Addr: 127.0.0.1:6379 69 | Password: "" 70 | DB: 0 71 | MinIdleConn: 200 72 | DialTimeout: 60s 73 | ReadTimeout: 500ms 74 | WriteTimeout: 500ms 75 | PoolSize: 100 76 | PoolTimeout: 240s 77 | IsTrace: true 78 | 79 | email: 80 | Host: SMTP_HOST # SMTP地址 81 | Port: 25 # 端口 82 | Username: USER # 用户名 83 | Password: PASSWORD # 密码 84 | Name: eagle # 发送者名称 85 | Address: SEND_EMAIL # 发送者邮箱 86 | ReplyTo: EMAIL # 回复地址 87 | KeepAlive: 30 # 连接保持时长 88 | 89 | web: 90 | Name: eagle 91 | Domain: http://eagle.com 92 | Secret: abcdefg 93 | Static: /data/static 94 | 95 | cookie: 96 | Name: jwt-token 97 | MaxAge: 86400 98 | Secure: false 99 | HttpOnly: true 100 | Domain: http://eagle.com 101 | Secret: abcdefg 102 | 103 | qiniu: 104 | AccessKey: ACCESS_KEY 105 | SecretKey: SECRET_KEY 106 | CdnURL: http://cdn.eagle.com 107 | SignatureID: signature_id # 短信签名id 108 | TemplateID: template_id # 模板id 109 | 110 | metrics: 111 | Url: 0.0.0.0:7070 112 | ServiceName: api 113 | 114 | MongoDB: 115 | URI: "mongodb://localhost:27017" 116 | User: "admin" 117 | Password: "admin" 118 | DB: "eagle" 119 | -------------------------------------------------------------------------------- /config/docker/config.yaml: -------------------------------------------------------------------------------- 1 | app: 2 | Name: eagle 3 | Version: 1.0.0 4 | PprofPort: :5555 5 | Mode: debug # debug, release, test 6 | JwtSecret: JWT_SECRET 7 | JwtTimeout: 86400 8 | CookieName: jwt-token 9 | SSL: true 10 | CtxDefaultTimeout: 12 11 | CSRF: true 12 | Debug: false 13 | 14 | Http: 15 | Addr: :8080 16 | ReadTimeout: 3s 17 | WriteTimeout: 3s 18 | Grpc: 19 | Addr: :9090 20 | ReadTimeout: 5s 21 | WriteTimeout: 5s 22 | 23 | logger: 24 | Development: false 25 | DisableCaller: false 26 | DisableStacktrace: false 27 | Encoding: json # json or console 28 | Level: info # 日志级别,INFO, WARN, ERROR 29 | Name: eagle 30 | Writers: console # 有2个可选项:file,console 选择file会将日志记录到logger_file指定的日志文件中,选择console会将日志输出到标准输出,当然也可以两者同时选择 31 | LoggerFile: /tmp/log/eagle.log 32 | LoggerWarnFile: /tmp/log/eagle.wf.log 33 | LoggerErrorFile: /tmp/log/eagle.err.log 34 | LogRollingPolicy: daily 35 | LogRotateDate: 1 36 | LogRotateSize: 1 37 | LogBackupCount: 7 38 | 39 | orm: 40 | Name: eagle # 数据库名称 41 | Addr: localhost:3306 # 如果是 docker,可以替换为 对应的服务名称,eg: db:3306 42 | UserName: root 43 | Password: 123456 44 | ShowLog: true # 是否打印所有SQL日志 45 | MaxIdleConn: 10 # 最大闲置的连接数,0意味着使用默认的大小2, 小于0表示不使用连接池 46 | MaxOpenConn: 60 # 最大打开的连接数, 需要小于数据库配置中的max_connections数 47 | ConnMaxLifeTime: 4h # 单个连接最大存活时间,建议设置比数据库超时时长(wait_timeout)稍小一些 48 | SlowThreshold: 0 # 慢查询阈值,设置后只打印慢查询日志,默认为500ms 49 | 50 | mysql: 51 | Dsn: "root:123456@tcp(localhost:3306)/eagle?timeout=2s&readTimeout=5s&writeTimeout=5s&parseTime=true&loc=Local&charset=utf8,utf8mb4" 52 | ShowLog: true # 是否打印SQL日志 53 | MaxIdleConn: 10 # 最大闲置的连接数,0意味着使用默认的大小2, 小于0表示不使用连接池 54 | MaxOpenConn: 60 # 最大打开的连接数, 需要小于数据库配置中的max_connections数 55 | ConnMaxLifeTime: 4000 # 单个连接最大存活时间,建议设置比数据库超时时长(wait_timeout)稍小一些 56 | QueryTimeout: 200 57 | ExecTimeout: 200 58 | TranTimeout: 200 59 | Braker: # 熔断器配置 60 | window: 3s 61 | sleep: 100ms 62 | bucket: 100 63 | ratio: 0.5 64 | request: 100 65 | 66 | 67 | redis: 68 | Addr: 127.0.0.1:6379 69 | Password: "" 70 | DB: 0 71 | MinIdleConn: 200 72 | DialTimeout: 60s 73 | ReadTimeout: 500ms 74 | WriteTimeout: 500ms 75 | PoolSize: 100 76 | PoolTimeout: 240s 77 | IsTrace: true 78 | 79 | email: 80 | Host: SMTP_HOST # SMTP地址 81 | Port: 25 # 端口 82 | Username: USER # 用户名 83 | Password: PASSWORD # 密码 84 | Name: eagle # 发送者名称 85 | Address: SEND_EMAIL # 发送者邮箱 86 | ReplyTo: EMAIL # 回复地址 87 | KeepAlive: 30 # 连接保持时长 88 | 89 | web: 90 | Name: eagle 91 | Domain: http://eagle.com 92 | Secret: abcdefg 93 | Static: /data/static 94 | 95 | cookie: 96 | Name: jwt-token 97 | MaxAge: 86400 98 | Secure: false 99 | HttpOnly: true 100 | Domain: http://eagle.com 101 | Secret: abcdefg 102 | 103 | qiniu: 104 | AccessKey: ACCESS_KEY 105 | SecretKey: SECRET_KEY 106 | CdnURL: http://cdn.eagle.com 107 | SignatureID: signature_id # 短信签名id 108 | TemplateID: template_id # 模板id 109 | 110 | metrics: 111 | Url: 0.0.0.0:7070 112 | ServiceName: api 113 | 114 | MongoDB: 115 | URI: "mongodb://localhost:27017" 116 | User: "admin" 117 | Password: "admin" 118 | DB: "eagle" 119 | -------------------------------------------------------------------------------- /config/prod/config.yaml: -------------------------------------------------------------------------------- 1 | app: 2 | Name: eagle 3 | Version: 1.0.0 4 | PprofPort: :5555 5 | Mode: debug # debug, release, test 6 | JwtSecret: JWT_SECRET 7 | JwtTimeout: 86400 8 | CookieName: jwt-token 9 | SSL: true 10 | CtxDefaultTimeout: 12 11 | CSRF: true 12 | Debug: false 13 | 14 | Http: 15 | Addr: :8080 16 | ReadTimeout: 3s 17 | WriteTimeout: 3s 18 | Grpc: 19 | Addr: :9090 20 | ReadTimeout: 5s 21 | WriteTimeout: 5s 22 | 23 | logger: 24 | Development: false 25 | DisableCaller: false 26 | DisableStacktrace: false 27 | Encoding: json # json or console 28 | Level: info # 日志级别,INFO, WARN, ERROR 29 | Name: eagle 30 | Writers: console # 有2个可选项:file,console 选择file会将日志记录到logger_file指定的日志文件中,选择console会将日志输出到标准输出,当然也可以两者同时选择 31 | LoggerFile: /tmp/log/eagle.log 32 | LoggerWarnFile: /tmp/log/eagle.wf.log 33 | LoggerErrorFile: /tmp/log/eagle.err.log 34 | LogRollingPolicy: daily 35 | LogRotateDate: 1 36 | LogRotateSize: 1 37 | LogBackupCount: 7 38 | 39 | orm: 40 | Name: eagle # 数据库名称 41 | Addr: localhost:3306 # 如果是 docker,可以替换为 对应的服务名称,eg: db:3306 42 | UserName: root 43 | Password: 123456 44 | ShowLog: true # 是否打印所有SQL日志 45 | MaxIdleConn: 10 # 最大闲置的连接数,0意味着使用默认的大小2, 小于0表示不使用连接池 46 | MaxOpenConn: 60 # 最大打开的连接数, 需要小于数据库配置中的max_connections数 47 | ConnMaxLifeTime: 4h # 单个连接最大存活时间,建议设置比数据库超时时长(wait_timeout)稍小一些 48 | SlowThreshold: 0 # 慢查询阈值,设置后只打印慢查询日志,默认为500ms 49 | 50 | mysql: 51 | Dsn: "root:123456@tcp(localhost:3306)/eagle?timeout=2s&readTimeout=5s&writeTimeout=5s&parseTime=true&loc=Local&charset=utf8,utf8mb4" 52 | ShowLog: true # 是否打印SQL日志 53 | MaxIdleConn: 10 # 最大闲置的连接数,0意味着使用默认的大小2, 小于0表示不使用连接池 54 | MaxOpenConn: 60 # 最大打开的连接数, 需要小于数据库配置中的max_connections数 55 | ConnMaxLifeTime: 4000 # 单个连接最大存活时间,建议设置比数据库超时时长(wait_timeout)稍小一些 56 | QueryTimeout: 200 57 | ExecTimeout: 200 58 | TranTimeout: 200 59 | Braker: # 熔断器配置 60 | window: 3s 61 | sleep: 100ms 62 | bucket: 100 63 | ratio: 0.5 64 | request: 100 65 | 66 | 67 | redis: 68 | Addr: 127.0.0.1:6379 69 | Password: "" 70 | DB: 0 71 | MinIdleConn: 200 72 | DialTimeout: 60s 73 | ReadTimeout: 500ms 74 | WriteTimeout: 500ms 75 | PoolSize: 100 76 | PoolTimeout: 240s 77 | IsTrace: true 78 | 79 | email: 80 | Host: SMTP_HOST # SMTP地址 81 | Port: 25 # 端口 82 | Username: USER # 用户名 83 | Password: PASSWORD # 密码 84 | Name: eagle # 发送者名称 85 | Address: SEND_EMAIL # 发送者邮箱 86 | ReplyTo: EMAIL # 回复地址 87 | KeepAlive: 30 # 连接保持时长 88 | 89 | web: 90 | Name: eagle 91 | Domain: http://eagle.com 92 | Secret: abcdefg 93 | Static: /data/static 94 | 95 | cookie: 96 | Name: jwt-token 97 | MaxAge: 86400 98 | Secure: false 99 | HttpOnly: true 100 | Domain: http://eagle.com 101 | Secret: abcdefg 102 | 103 | qiniu: 104 | AccessKey: ACCESS_KEY 105 | SecretKey: SECRET_KEY 106 | CdnURL: http://cdn.eagle.com 107 | SignatureID: signature_id # 短信签名id 108 | TemplateID: template_id # 模板id 109 | 110 | metrics: 111 | Url: 0.0.0.0:7070 112 | ServiceName: api 113 | 114 | MongoDB: 115 | URI: "mongodb://localhost:27017" 116 | User: "admin" 117 | Password: "admin" 118 | DB: "eagle" 119 | -------------------------------------------------------------------------------- /deploy/k8s/ingress-controller.yaml: -------------------------------------------------------------------------------- 1 | # https://docs.nginx.com/nginx-ingress-controller/ 2 | 3 | # metadata: name 4 | # matchLabels -> app 5 | # - -ingress-class=ngx-ink 6 | 7 | # kubectl port-forward -n nginx-ingress ngx-kic-dep-5956f8f878-89s4z 8080:80 & 8 | # curl 127.1:8080/nginx-health 9 | # kubectl port-forward -n nginx-ingress ngx-kic-dep-5956f8f878-89s4z 8081:8081 & 10 | # curl 127.1:8081/nginx-ready 11 | # kubectl port-forward -n nginx-ingress ngx-kic-dep-5956f8f878-89s4z 8088:8080 & 12 | # curl 127.1:8088/stub_status 13 | 14 | # in cluster: 15 | # curl --resolve ngx.test:80:10.10.2.17 ngx.test 16 | 17 | # kubectl port-forward -n nginx-ingress ngx-kic-dep-5956f8f878-fk2vw 8080:80 & 18 | # curl --resolve ngx.test:8080:127.0.0.1 ngx.test:8080 19 | 20 | apiVersion: apps/v1 21 | kind: Deployment 22 | metadata: 23 | name: ngx-kic-dep 24 | namespace: nginx-ingress 25 | 26 | spec: 27 | replicas: 1 28 | selector: 29 | matchLabels: 30 | app: ngx-kic-dep 31 | 32 | template: 33 | metadata: 34 | labels: 35 | app: ngx-kic-dep 36 | #annotations: 37 | #prometheus.io/scrape: "true" 38 | #prometheus.io/port: "9113" 39 | #prometheus.io/scheme: http 40 | spec: 41 | serviceAccountName: nginx-ingress 42 | 43 | hostNetwork: true 44 | dnsPolicy: ClusterFirstWithHostNet 45 | 46 | containers: 47 | #- image: nginx/nginx-ingress:2.2.0 48 | - image: nginx/nginx-ingress:2.2-alpine 49 | imagePullPolicy: IfNotPresent 50 | name: nginx-ingress 51 | ports: 52 | - name: http 53 | containerPort: 80 54 | - name: https 55 | containerPort: 443 56 | - name: readiness-port 57 | containerPort: 8081 58 | - name: prometheus 59 | containerPort: 9113 60 | readinessProbe: 61 | httpGet: 62 | path: /nginx-ready 63 | port: readiness-port 64 | periodSeconds: 1 65 | securityContext: 66 | allowPrivilegeEscalation: true 67 | runAsUser: 101 #nginx 68 | capabilities: 69 | drop: 70 | - ALL 71 | add: 72 | - NET_BIND_SERVICE 73 | env: 74 | - name: POD_NAMESPACE 75 | valueFrom: 76 | fieldRef: 77 | fieldPath: metadata.namespace 78 | - name: POD_NAME 79 | valueFrom: 80 | fieldRef: 81 | fieldPath: metadata.name 82 | args: 83 | - -ingress-class=ngx-ink 84 | - -health-status 85 | - -ready-status 86 | - -nginx-status 87 | 88 | - -nginx-configmaps=$(POD_NAMESPACE)/nginx-config 89 | - -default-server-tls-secret=$(POD_NAMESPACE)/default-server-secret 90 | #- -v=3 # Enables extensive logging. Useful for troubleshooting. 91 | #- -report-ingress-status 92 | #- -external-service=nginx-ingress 93 | #- -enable-prometheus-metrics 94 | #- -global-configuration=$(POD_NAMESPACE)/nginx-configuration 95 | 96 | --- 97 | 98 | apiVersion: v1 99 | kind: Service 100 | metadata: 101 | name: ngx-kic-svc 102 | namespace: nginx-ingress 103 | 104 | spec: 105 | ports: 106 | - port: 80 107 | protocol: TCP 108 | targetPort: 80 109 | nodePort: 30080 110 | 111 | selector: 112 | app: ngx-kic-dep 113 | type: NodePort 114 | 115 | --- 116 | -------------------------------------------------------------------------------- /api/user/v1/user.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package user.v1; 4 | 5 | import "google/protobuf/empty.proto"; 6 | import "validate/validate.proto"; 7 | 8 | option go_package = "github.com/go-microservice/user-service/api/user/v1;v1"; 9 | 10 | // 用户服务 11 | service UserService { 12 | // auth 13 | // sign up 14 | rpc Register(RegisterRequest) returns (RegisterReply) {} 15 | // sign in 16 | rpc Login(LoginRequest) returns (LoginReply) {} 17 | // logout 18 | rpc Logout(LogoutRequest) returns (google.protobuf.Empty) {} 19 | 20 | // user 21 | rpc CreateUser(CreateUserRequest) returns(CreateUserReply) {} 22 | rpc GetUser(GetUserRequest) returns (GetUserReply) {} 23 | rpc BatchGetUsers(BatchGetUsersRequest) returns (BatchGetUsersReply) {} 24 | rpc UpdateUser(UpdateUserRequest) returns (UpdateUserReply) {} 25 | rpc UpdatePassword(UpdatePasswordRequest) returns (UpdatePasswordReply) {} 26 | } 27 | 28 | // 用户状态 29 | enum StatusType { 30 | NORMAL = 0; 31 | DELETE = 1; 32 | Ban = 2; 33 | } 34 | 35 | // 性别 36 | enum GenderType { 37 | UNKNOWN = 0; 38 | MALE = 1; 39 | FEMALE = 2; 40 | }; 41 | 42 | // user info 43 | message User { 44 | int64 id = 1; 45 | string username = 2; 46 | string email = 3; 47 | string phone = 4; 48 | int64 last_login_at = 5; 49 | StatusType status = 6; 50 | string nickname = 7; 51 | string avatar = 8; 52 | GenderType gender = 9; 53 | string birthday = 10; 54 | string bio = 11; 55 | int64 created_at = 12; 56 | int64 updated_at = 13; 57 | } 58 | 59 | message RegisterRequest { 60 | string username = 1 [(validate.rules).string.min_len = 6]; 61 | string email = 2 [(validate.rules).string.email = true]; 62 | string password = 3 [(validate.rules).string.min_len = 6]; 63 | } 64 | 65 | message RegisterReply { 66 | int64 id = 1; 67 | string username = 2; 68 | } 69 | 70 | message LoginRequest { 71 | string username = 1 [(validate.rules).string.min_len = 6]; 72 | string email = 2 [(validate.rules).string.email = true]; 73 | string password = 3 [(validate.rules).string.min_len = 6]; 74 | } 75 | 76 | message LoginReply { 77 | int64 id = 1; 78 | string access_token = 2; 79 | string refresh_token = 3; 80 | } 81 | 82 | message LogoutRequest { 83 | int64 id = 1 [(validate.rules).int64.gte = 1]; 84 | string access_token = 2 [(validate.rules).string.min_len = 20]; 85 | 86 | } 87 | 88 | message CreateUserRequest { 89 | string username = 1; 90 | string email = 2; 91 | string password = 3; 92 | } 93 | 94 | message CreateUserReply { 95 | int64 id = 1; 96 | string username = 2; 97 | string email = 3; 98 | } 99 | 100 | message UpdateUserRequest { 101 | int64 user_id = 1; 102 | string username = 2; 103 | string email = 3; 104 | string phone = 4; 105 | int64 login_at = 5; 106 | StatusType status = 6; 107 | string nickname = 7; 108 | string avatar = 8; 109 | GenderType gender = 9; 110 | string birthday = 10; 111 | string bio = 11; 112 | int64 updated_at = 13; 113 | } 114 | 115 | message UpdateUserReply { 116 | int64 user_id = 1; 117 | string username = 2; 118 | string email = 3; 119 | string phone = 4; 120 | int64 login_at = 5; 121 | StatusType status = 6; 122 | string nickname = 7; 123 | string avatar = 8; 124 | GenderType gender = 9; 125 | string birthday = 10; 126 | string bio = 11; 127 | int64 updated_at = 13; 128 | } 129 | 130 | message UpdatePasswordRequest { 131 | string id = 1; 132 | string password = 2; 133 | string new_password = 3; 134 | string confirm_password = 4; 135 | } 136 | 137 | message UpdatePasswordReply { 138 | 139 | } 140 | 141 | message GetUserRequest { 142 | int64 id = 1; 143 | } 144 | 145 | message GetUserReply { 146 | User user = 1; 147 | } 148 | 149 | message BatchGetUsersRequest { 150 | repeated int64 ids = 1; 151 | } 152 | 153 | message BatchGetUsersReply { 154 | repeated User users = 1; 155 | } -------------------------------------------------------------------------------- /internal/cache/user_cache.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | //go:generate mockgen -source=user_cache.go -destination=../../internal/mocks/user_cache_mock.go -package mocks 4 | 5 | import ( 6 | "context" 7 | "fmt" 8 | "time" 9 | 10 | "github.com/spf13/cast" 11 | 12 | "github.com/go-eagle/eagle/pkg/cache" 13 | "github.com/go-eagle/eagle/pkg/encoding" 14 | "github.com/go-eagle/eagle/pkg/log" 15 | "github.com/redis/go-redis/v9" 16 | 17 | "github.com/go-microservice/user-service/internal/model" 18 | ) 19 | 20 | const ( 21 | // PrefixUserCacheKey cache prefix 22 | PrefixUserCacheKey = "user:%d" 23 | ) 24 | 25 | type UserCache interface { 26 | SetUserCache(ctx context.Context, id int64, data *model.UserModel, duration time.Duration) error 27 | GetUserCache(ctx context.Context, id int64) (ret *model.UserModel, err error) 28 | MultiGetUserCache(ctx context.Context, ids []int64) (map[string]*model.UserModel, error) 29 | MultiSetUserCache(ctx context.Context, data []*model.UserModel, duration time.Duration) error 30 | DelUserCache(ctx context.Context, id int64) error 31 | SetCacheWithNotFound(ctx context.Context, id int64) error 32 | } 33 | 34 | // userCache define a cache struct 35 | type userCache struct { 36 | cache cache.Cache 37 | } 38 | 39 | // NewUserCache new a cache 40 | func NewUserCache(rdb *redis.Client) UserCache { 41 | jsonEncoding := encoding.JSONEncoding{} 42 | cachePrefix := "" 43 | return &userCache{ 44 | cache: cache.NewRedisCache(rdb, cachePrefix, jsonEncoding, func() interface{} { 45 | return &model.UserModel{} 46 | }), 47 | } 48 | } 49 | 50 | // GetUserCacheKey get cache key 51 | func (c *userCache) GetUserCacheKey(id int64) string { 52 | return fmt.Sprintf(PrefixUserCacheKey, id) 53 | } 54 | 55 | // SetUserCache write to cache 56 | func (c *userCache) SetUserCache(ctx context.Context, id int64, data *model.UserModel, duration time.Duration) error { 57 | if data == nil || id == 0 { 58 | return nil 59 | } 60 | cacheKey := c.GetUserCacheKey(id) 61 | err := c.cache.Set(ctx, cacheKey, data, duration) 62 | if err != nil { 63 | return err 64 | } 65 | return nil 66 | } 67 | 68 | // GetUserCache 获取cache 69 | func (c *userCache) GetUserCache(ctx context.Context, id int64) (ret *model.UserModel, err error) { 70 | var data *model.UserModel 71 | cacheKey := c.GetUserCacheKey(id) 72 | err = c.cache.Get(ctx, cacheKey, &data) 73 | if err != nil { 74 | log.WithContext(ctx).Warnf("get err from redis, err: %+v", err) 75 | return nil, err 76 | } 77 | return data, nil 78 | } 79 | 80 | // MultiGetUserCache 批量获取cache 81 | func (c *userCache) MultiGetUserCache(ctx context.Context, ids []int64) (map[string]*model.UserModel, error) { 82 | var keys []string 83 | for _, v := range ids { 84 | cacheKey := c.GetUserCacheKey(v) 85 | keys = append(keys, cacheKey) 86 | } 87 | 88 | // NOTE: 需要在这里make实例化,如果在返回参数里直接定义会报 nil map 89 | itemMap := make(map[string]*model.UserModel) 90 | err := c.cache.MultiGet(ctx, keys, itemMap) 91 | if err != nil { 92 | return nil, err 93 | } 94 | 95 | retMap := make(map[string]*model.UserModel) 96 | for _, v := range ids { 97 | val, ok := itemMap[c.GetUserCacheKey(v)] 98 | if ok { 99 | retMap[cast.ToString(v)] = val 100 | } 101 | } 102 | return retMap, nil 103 | } 104 | 105 | // MultiSetUserCache 批量设置cache 106 | func (c *userCache) MultiSetUserCache(ctx context.Context, data []*model.UserModel, duration time.Duration) error { 107 | valMap := make(map[string]interface{}) 108 | for _, v := range data { 109 | cacheKey := c.GetUserCacheKey(v.ID) 110 | valMap[cacheKey] = v 111 | } 112 | 113 | err := c.cache.MultiSet(ctx, valMap, duration) 114 | if err != nil { 115 | return err 116 | } 117 | return nil 118 | } 119 | 120 | // DelUserCache 删除cache 121 | func (c *userCache) DelUserCache(ctx context.Context, id int64) error { 122 | cacheKey := c.GetUserCacheKey(id) 123 | err := c.cache.Del(ctx, cacheKey) 124 | if err != nil { 125 | return err 126 | } 127 | return nil 128 | } 129 | 130 | // SetCacheWithNotFound set empty cache 131 | func (c *userCache) SetCacheWithNotFound(ctx context.Context, id int64) error { 132 | cacheKey := c.GetUserCacheKey(id) 133 | err := c.cache.SetCacheWithNotFound(ctx, cacheKey) 134 | if err != nil { 135 | return err 136 | } 137 | return nil 138 | } 139 | -------------------------------------------------------------------------------- /internal/service/user_svc_test.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "log" 8 | "net" 9 | "testing" 10 | 11 | "github.com/go-microservice/user-service/internal/model" 12 | 13 | "github.com/go-microservice/user-service/internal/mocks" 14 | 15 | "github.com/golang/mock/gomock" 16 | 17 | "google.golang.org/grpc/codes" 18 | "google.golang.org/grpc/status" 19 | 20 | "google.golang.org/grpc" 21 | "google.golang.org/grpc/test/bufconn" 22 | 23 | pb "github.com/go-microservice/user-service/api/user/v1" 24 | ) 25 | 26 | const ( 27 | addr = "" 28 | bufSize = 1024 * 1024 29 | ) 30 | 31 | var ( 32 | lis *bufconn.Listener 33 | ) 34 | 35 | // init create a gRPC server 36 | func initGRPCServer(t *testing.T) { 37 | lis = bufconn.Listen(bufSize) 38 | srv := grpc.NewServer() 39 | 40 | ctrl := gomock.NewController(t) 41 | defer ctrl.Finish() 42 | 43 | mockUserRepo := mocks.NewMockUserRepo(ctrl) 44 | 45 | pb.RegisterUserServiceServer(srv, &UserServiceServer{ 46 | repo: mockUserRepo, 47 | }) 48 | 49 | go func() { 50 | if err := srv.Serve(lis); err != nil { 51 | log.Fatalf("srv.Serve, err: %v", err) 52 | } 53 | }() 54 | } 55 | 56 | func dialer() func(context.Context, string) (net.Conn, error) { 57 | return func(context.Context, string) (net.Conn, error) { 58 | return lis.Dial() 59 | } 60 | } 61 | 62 | func TestUserServiceServer_GetUser(t *testing.T) { 63 | testCases := []struct { 64 | name string 65 | id int64 66 | res *pb.GetUserReply 67 | buildStubs func(mock *mocks.MockUserRepo) 68 | errCode codes.Code 69 | errMsg string 70 | }{ 71 | { 72 | name: "OK", 73 | id: 1, 74 | res: &pb.GetUserReply{User: &pb.User{Id: 1}}, 75 | buildStubs: func(mock *mocks.MockUserRepo) { 76 | mock.EXPECT().GetUser(gomock.Any(), int64(1)). 77 | Return(&model.UserModel{ID: 1}, nil).Times(1) 78 | }, 79 | errCode: codes.OK, 80 | errMsg: "", 81 | }, 82 | { 83 | name: "NotFound", 84 | id: 10, 85 | res: &pb.GetUserReply{User: &pb.User{Id: 0}}, 86 | buildStubs: func(mock *mocks.MockUserRepo) { 87 | mock.EXPECT().GetUser(gomock.Any(), int64(10)). 88 | Return(&model.UserModel{}, nil).Times(1) 89 | }, 90 | errCode: codes.NotFound, 91 | errMsg: "not found", 92 | }, 93 | { 94 | name: "InternalError", 95 | id: 2, 96 | res: &pb.GetUserReply{User: &pb.User{Id: 2}}, 97 | buildStubs: func(mock *mocks.MockUserRepo) { 98 | mock.EXPECT().GetUser(gomock.Any(), int64(2)). 99 | Return(&model.UserModel{}, errors.New("internal error")).Times(1) 100 | }, 101 | errCode: codes.Code(10000), 102 | errMsg: "Internal error", 103 | }, 104 | } 105 | 106 | for _, tc := range testCases { 107 | t.Run(tc.name, func(t *testing.T) { 108 | lis := bufconn.Listen(bufSize) 109 | ctrl := gomock.NewController(t) 110 | defer ctrl.Finish() 111 | 112 | mockUserRepo := mocks.NewMockUserRepo(ctrl) 113 | tc.buildStubs(mockUserRepo) 114 | 115 | srv := grpc.NewServer() 116 | pb.RegisterUserServiceServer(srv, &UserServiceServer{ 117 | repo: mockUserRepo, 118 | }) 119 | 120 | go func() { 121 | if err := srv.Serve(lis); err != nil { 122 | log.Fatalf("srv.Serve, err: %v", err) 123 | } 124 | }() 125 | 126 | dialer := func(context.Context, string) (net.Conn, error) { 127 | return lis.Dial() 128 | } 129 | 130 | ctx := context.Background() 131 | conn, err := grpc.DialContext(ctx, addr, grpc.WithContextDialer(dialer), grpc.WithInsecure()) 132 | if err != nil { 133 | log.Fatalf("grpc.DialContext, err: %v", err) 134 | } 135 | client := pb.NewUserServiceClient(conn) 136 | 137 | req := &pb.GetUserRequest{Id: tc.id} 138 | resp, err := client.GetUser(ctx, req) 139 | if err != nil { 140 | fmt.Println("~~~~~~~~~~~~~~~~", err) 141 | if er, ok := status.FromError(err); ok { 142 | if er.Code() != tc.errCode { 143 | t.Errorf("error code, expected: %d, received: %d", tc.errCode, er.Code()) 144 | } 145 | if er.Message() != tc.errMsg { 146 | t.Errorf("error message, expected: %s, received: %s", tc.errMsg, er.Message()) 147 | } 148 | } 149 | } 150 | if resp != nil { 151 | if resp.GetUser().Id != tc.res.GetUser().Id { 152 | t.Errorf("response, expected: %v, received: %v", tc.res.GetUser().Id, resp.GetUser().Id) 153 | } 154 | } 155 | }) 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /internal/mocks/user_cache_mock.go: -------------------------------------------------------------------------------- 1 | // Code generated by MockGen. DO NOT EDIT. 2 | // Source: user_cache.go 3 | 4 | // Package mocks is a generated GoMock package. 5 | package mocks 6 | 7 | import ( 8 | context "context" 9 | reflect "reflect" 10 | time "time" 11 | 12 | model "github.com/go-microservice/user-service/internal/model" 13 | gomock "github.com/golang/mock/gomock" 14 | ) 15 | 16 | // MockUserCache is a mock of UserCache interface. 17 | type MockUserCache struct { 18 | ctrl *gomock.Controller 19 | recorder *MockUserCacheMockRecorder 20 | } 21 | 22 | // MockUserCacheMockRecorder is the mock recorder for MockUserCache. 23 | type MockUserCacheMockRecorder struct { 24 | mock *MockUserCache 25 | } 26 | 27 | // NewMockUserCache creates a new mock instance. 28 | func NewMockUserCache(ctrl *gomock.Controller) *MockUserCache { 29 | mock := &MockUserCache{ctrl: ctrl} 30 | mock.recorder = &MockUserCacheMockRecorder{mock} 31 | return mock 32 | } 33 | 34 | // EXPECT returns an object that allows the caller to indicate expected use. 35 | func (m *MockUserCache) EXPECT() *MockUserCacheMockRecorder { 36 | return m.recorder 37 | } 38 | 39 | // DelUserCache mocks base method. 40 | func (m *MockUserCache) DelUserCache(ctx context.Context, id int64) error { 41 | m.ctrl.T.Helper() 42 | ret := m.ctrl.Call(m, "DelUserCache", ctx, id) 43 | ret0, _ := ret[0].(error) 44 | return ret0 45 | } 46 | 47 | // DelUserCache indicates an expected call of DelUserCache. 48 | func (mr *MockUserCacheMockRecorder) DelUserCache(ctx, id interface{}) *gomock.Call { 49 | mr.mock.ctrl.T.Helper() 50 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DelUserCache", reflect.TypeOf((*MockUserCache)(nil).DelUserCache), ctx, id) 51 | } 52 | 53 | // GetUserCache mocks base method. 54 | func (m *MockUserCache) GetUserCache(ctx context.Context, id int64) (*model.UserModel, error) { 55 | m.ctrl.T.Helper() 56 | ret := m.ctrl.Call(m, "GetUserCache", ctx, id) 57 | ret0, _ := ret[0].(*model.UserModel) 58 | ret1, _ := ret[1].(error) 59 | return ret0, ret1 60 | } 61 | 62 | // GetUserCache indicates an expected call of GetUserCache. 63 | func (mr *MockUserCacheMockRecorder) GetUserCache(ctx, id interface{}) *gomock.Call { 64 | mr.mock.ctrl.T.Helper() 65 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserCache", reflect.TypeOf((*MockUserCache)(nil).GetUserCache), ctx, id) 66 | } 67 | 68 | // MultiGetUserCache mocks base method. 69 | func (m *MockUserCache) MultiGetUserCache(ctx context.Context, ids []int64) (map[string]*model.UserModel, error) { 70 | m.ctrl.T.Helper() 71 | ret := m.ctrl.Call(m, "MultiGetUserCache", ctx, ids) 72 | ret0, _ := ret[0].(map[string]*model.UserModel) 73 | ret1, _ := ret[1].(error) 74 | return ret0, ret1 75 | } 76 | 77 | // MultiGetUserCache indicates an expected call of MultiGetUserCache. 78 | func (mr *MockUserCacheMockRecorder) MultiGetUserCache(ctx, ids interface{}) *gomock.Call { 79 | mr.mock.ctrl.T.Helper() 80 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "MultiGetUserCache", reflect.TypeOf((*MockUserCache)(nil).MultiGetUserCache), ctx, ids) 81 | } 82 | 83 | // MultiSetUserCache mocks base method. 84 | func (m *MockUserCache) MultiSetUserCache(ctx context.Context, data []*model.UserModel, duration time.Duration) error { 85 | m.ctrl.T.Helper() 86 | ret := m.ctrl.Call(m, "MultiSetUserCache", ctx, data, duration) 87 | ret0, _ := ret[0].(error) 88 | return ret0 89 | } 90 | 91 | // MultiSetUserCache indicates an expected call of MultiSetUserCache. 92 | func (mr *MockUserCacheMockRecorder) MultiSetUserCache(ctx, data, duration interface{}) *gomock.Call { 93 | mr.mock.ctrl.T.Helper() 94 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "MultiSetUserCache", reflect.TypeOf((*MockUserCache)(nil).MultiSetUserCache), ctx, data, duration) 95 | } 96 | 97 | // SetCacheWithNotFound mocks base method. 98 | func (m *MockUserCache) SetCacheWithNotFound(ctx context.Context, id int64) error { 99 | m.ctrl.T.Helper() 100 | ret := m.ctrl.Call(m, "SetCacheWithNotFound", ctx, id) 101 | ret0, _ := ret[0].(error) 102 | return ret0 103 | } 104 | 105 | // SetCacheWithNotFound indicates an expected call of SetCacheWithNotFound. 106 | func (mr *MockUserCacheMockRecorder) SetCacheWithNotFound(ctx, id interface{}) *gomock.Call { 107 | mr.mock.ctrl.T.Helper() 108 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetCacheWithNotFound", reflect.TypeOf((*MockUserCache)(nil).SetCacheWithNotFound), ctx, id) 109 | } 110 | 111 | // SetUserCache mocks base method. 112 | func (m *MockUserCache) SetUserCache(ctx context.Context, id int64, data *model.UserModel, duration time.Duration) error { 113 | m.ctrl.T.Helper() 114 | ret := m.ctrl.Call(m, "SetUserCache", ctx, id, data, duration) 115 | ret0, _ := ret[0].(error) 116 | return ret0 117 | } 118 | 119 | // SetUserCache indicates an expected call of SetUserCache. 120 | func (mr *MockUserCacheMockRecorder) SetUserCache(ctx, id, data, duration interface{}) *gomock.Call { 121 | mr.mock.ctrl.T.Helper() 122 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetUserCache", reflect.TypeOf((*MockUserCache)(nil).SetUserCache), ctx, id, data, duration) 123 | } 124 | -------------------------------------------------------------------------------- /internal/mocks/user_repo_mock.go: -------------------------------------------------------------------------------- 1 | // Code generated by MockGen. DO NOT EDIT. 2 | // Source: ./internal/./repository/user_repo_mock.go 3 | 4 | // Package mock_repository is a generated GoMock package. 5 | package mocks 6 | 7 | import ( 8 | context "context" 9 | reflect "reflect" 10 | 11 | model "github.com/go-microservice/user-service/internal/model" 12 | gomock "github.com/golang/mock/gomock" 13 | ) 14 | 15 | // MockUserRepo is a mocks of UserRepo interface. 16 | type MockUserRepo struct { 17 | ctrl *gomock.Controller 18 | recorder *MockUserRepoMockRecorder 19 | } 20 | 21 | // MockUserRepoMockRecorder is the mocks recorder for MockUserRepo. 22 | type MockUserRepoMockRecorder struct { 23 | mock *MockUserRepo 24 | } 25 | 26 | // NewMockUserRepo creates a new mocks instance. 27 | func NewMockUserRepo(ctrl *gomock.Controller) *MockUserRepo { 28 | mock := &MockUserRepo{ctrl: ctrl} 29 | mock.recorder = &MockUserRepoMockRecorder{mock} 30 | return mock 31 | } 32 | 33 | // EXPECT returns an object that allows the caller to indicate expected use. 34 | func (m *MockUserRepo) EXPECT() *MockUserRepoMockRecorder { 35 | return m.recorder 36 | } 37 | 38 | // BatchGetUser mocks base method. 39 | func (m *MockUserRepo) BatchGetUser(ctx context.Context, ids []int64) ([]*model.UserModel, error) { 40 | m.ctrl.T.Helper() 41 | ret := m.ctrl.Call(m, "BatchGetUser", ctx, ids) 42 | ret0, _ := ret[0].([]*model.UserModel) 43 | ret1, _ := ret[1].(error) 44 | return ret0, ret1 45 | } 46 | 47 | // BatchGetUser indicates an expected call of BatchGetUser. 48 | func (mr *MockUserRepoMockRecorder) BatchGetUser(ctx, ids interface{}) *gomock.Call { 49 | mr.mock.ctrl.T.Helper() 50 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BatchGetUser", reflect.TypeOf((*MockUserRepo)(nil).BatchGetUser), ctx, ids) 51 | } 52 | 53 | // CreateUser mocks base method. 54 | func (m *MockUserRepo) CreateUser(ctx context.Context, data *model.UserModel) (int64, error) { 55 | m.ctrl.T.Helper() 56 | ret := m.ctrl.Call(m, "CreateUser", ctx, data) 57 | ret0, _ := ret[0].(int64) 58 | ret1, _ := ret[1].(error) 59 | return ret0, ret1 60 | } 61 | 62 | // CreateUser indicates an expected call of CreateUser. 63 | func (mr *MockUserRepoMockRecorder) CreateUser(ctx, data interface{}) *gomock.Call { 64 | mr.mock.ctrl.T.Helper() 65 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateUser", reflect.TypeOf((*MockUserRepo)(nil).CreateUser), ctx, data) 66 | } 67 | 68 | // GetUser mocks base method. 69 | func (m *MockUserRepo) GetUser(ctx context.Context, id int64) (*model.UserModel, error) { 70 | m.ctrl.T.Helper() 71 | ret := m.ctrl.Call(m, "GetUser", ctx, id) 72 | ret0, _ := ret[0].(*model.UserModel) 73 | ret1, _ := ret[1].(error) 74 | return ret0, ret1 75 | } 76 | 77 | // GetUser indicates an expected call of GetUser. 78 | func (mr *MockUserRepoMockRecorder) GetUser(ctx, id interface{}) *gomock.Call { 79 | mr.mock.ctrl.T.Helper() 80 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUser", reflect.TypeOf((*MockUserRepo)(nil).GetUser), ctx, id) 81 | } 82 | 83 | // GetUserByEmail mocks base method. 84 | func (m *MockUserRepo) GetUserByEmail(ctx context.Context, email string) (*model.UserModel, error) { 85 | m.ctrl.T.Helper() 86 | ret := m.ctrl.Call(m, "GetUserByEmail", ctx, email) 87 | ret0, _ := ret[0].(*model.UserModel) 88 | ret1, _ := ret[1].(error) 89 | return ret0, ret1 90 | } 91 | 92 | // GetUserByEmail indicates an expected call of GetUserByEmail. 93 | func (mr *MockUserRepoMockRecorder) GetUserByEmail(ctx, email interface{}) *gomock.Call { 94 | mr.mock.ctrl.T.Helper() 95 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserByEmail", reflect.TypeOf((*MockUserRepo)(nil).GetUserByEmail), ctx, email) 96 | } 97 | 98 | // GetUserByPhone mocks base method. 99 | func (m *MockUserRepo) GetUserByPhone(ctx context.Context, phone string) (*model.UserModel, error) { 100 | m.ctrl.T.Helper() 101 | ret := m.ctrl.Call(m, "GetUserByPhone", ctx, phone) 102 | ret0, _ := ret[0].(*model.UserModel) 103 | ret1, _ := ret[1].(error) 104 | return ret0, ret1 105 | } 106 | 107 | // GetUserByPhone indicates an expected call of GetUserByPhone. 108 | func (mr *MockUserRepoMockRecorder) GetUserByPhone(ctx, phone interface{}) *gomock.Call { 109 | mr.mock.ctrl.T.Helper() 110 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserByPhone", reflect.TypeOf((*MockUserRepo)(nil).GetUserByPhone), ctx, phone) 111 | } 112 | 113 | // GetUserByUsername mocks base method. 114 | func (m *MockUserRepo) GetUserByUsername(ctx context.Context, username string) (*model.UserModel, error) { 115 | m.ctrl.T.Helper() 116 | ret := m.ctrl.Call(m, "GetUserByUsername", ctx, username) 117 | ret0, _ := ret[0].(*model.UserModel) 118 | ret1, _ := ret[1].(error) 119 | return ret0, ret1 120 | } 121 | 122 | // GetUserByUsername indicates an expected call of GetUserByUsername. 123 | func (mr *MockUserRepoMockRecorder) GetUserByUsername(ctx, username interface{}) *gomock.Call { 124 | mr.mock.ctrl.T.Helper() 125 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserByUsername", reflect.TypeOf((*MockUserRepo)(nil).GetUserByUsername), ctx, username) 126 | } 127 | 128 | // UpdateUser mocks base method. 129 | func (m *MockUserRepo) UpdateUser(ctx context.Context, id int64, data *model.UserModel) error { 130 | m.ctrl.T.Helper() 131 | ret := m.ctrl.Call(m, "UpdateUser", ctx, id, data) 132 | ret0, _ := ret[0].(error) 133 | return ret0 134 | } 135 | 136 | // UpdateUser indicates an expected call of UpdateUser. 137 | func (mr *MockUserRepoMockRecorder) UpdateUser(ctx, id, data interface{}) *gomock.Call { 138 | mr.mock.ctrl.T.Helper() 139 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateUser", reflect.TypeOf((*MockUserRepo)(nil).UpdateUser), ctx, id, data) 140 | } 141 | -------------------------------------------------------------------------------- /coverage.txt: -------------------------------------------------------------------------------- 1 | mode: atomic 2 | github.com/go-microservice/user-service/internal/repository/user_repo.go:48.59,54.2 1 1 3 | github.com/go-microservice/user-service/internal/repository/user_repo.go:57.97,59.16 2 0 4 | github.com/go-microservice/user-service/internal/repository/user_repo.go:63.2,63.21 1 0 5 | github.com/go-microservice/user-service/internal/repository/user_repo.go:59.16,61.3 1 0 6 | github.com/go-microservice/user-service/internal/repository/user_repo.go:67.91,69.16 2 0 7 | github.com/go-microservice/user-service/internal/repository/user_repo.go:72.2,73.16 2 0 8 | github.com/go-microservice/user-service/internal/repository/user_repo.go:78.2,80.12 2 0 9 | github.com/go-microservice/user-service/internal/repository/user_repo.go:69.16,71.3 1 0 10 | github.com/go-microservice/user-service/internal/repository/user_repo.go:73.16,75.3 1 0 11 | github.com/go-microservice/user-service/internal/repository/user_repo.go:84.93,87.16 2 1 12 | github.com/go-microservice/user-service/internal/repository/user_repo.go:90.2,90.17 1 1 13 | github.com/go-microservice/user-service/internal/repository/user_repo.go:95.2,97.50 3 0 14 | github.com/go-microservice/user-service/internal/repository/user_repo.go:101.2,101.17 1 0 15 | github.com/go-microservice/user-service/internal/repository/user_repo.go:108.2,108.18 1 0 16 | github.com/go-microservice/user-service/internal/repository/user_repo.go:87.16,89.3 1 0 17 | github.com/go-microservice/user-service/internal/repository/user_repo.go:90.17,92.3 1 1 18 | github.com/go-microservice/user-service/internal/repository/user_repo.go:97.50,99.3 1 0 19 | github.com/go-microservice/user-service/internal/repository/user_repo.go:101.17,103.17 2 0 20 | github.com/go-microservice/user-service/internal/repository/user_repo.go:103.17,105.4 1 0 21 | github.com/go-microservice/user-service/internal/repository/user_repo.go:111.110,114.16 3 0 22 | github.com/go-microservice/user-service/internal/repository/user_repo.go:118.2,118.18 1 0 23 | github.com/go-microservice/user-service/internal/repository/user_repo.go:114.16,116.3 1 0 24 | github.com/go-microservice/user-service/internal/repository/user_repo.go:121.104,124.16 3 0 25 | github.com/go-microservice/user-service/internal/repository/user_repo.go:128.2,128.18 1 0 26 | github.com/go-microservice/user-service/internal/repository/user_repo.go:124.16,126.3 1 0 27 | github.com/go-microservice/user-service/internal/repository/user_repo.go:131.104,134.16 3 0 28 | github.com/go-microservice/user-service/internal/repository/user_repo.go:138.2,138.18 1 0 29 | github.com/go-microservice/user-service/internal/repository/user_repo.go:134.16,136.3 1 0 30 | github.com/go-microservice/user-service/internal/repository/user_repo.go:142.103,145.16 3 0 31 | github.com/go-microservice/user-service/internal/repository/user_repo.go:148.2,149.24 2 0 32 | github.com/go-microservice/user-service/internal/repository/user_repo.go:158.2,158.23 1 0 33 | github.com/go-microservice/user-service/internal/repository/user_repo.go:176.2,176.17 1 0 34 | github.com/go-microservice/user-service/internal/repository/user_repo.go:145.16,147.3 1 0 35 | github.com/go-microservice/user-service/internal/repository/user_repo.go:149.24,151.10 2 0 36 | github.com/go-microservice/user-service/internal/repository/user_repo.go:155.3,155.26 1 0 37 | github.com/go-microservice/user-service/internal/repository/user_repo.go:151.10,153.12 2 0 38 | github.com/go-microservice/user-service/internal/repository/user_repo.go:158.23,162.17 4 0 39 | github.com/go-microservice/user-service/internal/repository/user_repo.go:166.3,166.26 1 0 40 | github.com/go-microservice/user-service/internal/repository/user_repo.go:162.17,165.4 1 0 41 | github.com/go-microservice/user-service/internal/repository/user_repo.go:166.26,169.18 3 0 42 | github.com/go-microservice/user-service/internal/repository/user_repo.go:169.18,172.5 1 0 43 | github.com/go-microservice/user-service/internal/cache/user_cache.go:37.48,41.81 3 5 44 | github.com/go-microservice/user-service/internal/cache/user_cache.go:41.81,43.4 1 2 45 | github.com/go-microservice/user-service/internal/cache/user_cache.go:48.54,50.2 1 10 46 | github.com/go-microservice/user-service/internal/cache/user_cache.go:53.118,54.28 1 2 47 | github.com/go-microservice/user-service/internal/cache/user_cache.go:57.2,59.16 3 2 48 | github.com/go-microservice/user-service/internal/cache/user_cache.go:62.2,62.12 1 2 49 | github.com/go-microservice/user-service/internal/cache/user_cache.go:54.28,56.3 1 0 50 | github.com/go-microservice/user-service/internal/cache/user_cache.go:59.16,61.3 1 0 51 | github.com/go-microservice/user-service/internal/cache/user_cache.go:66.99,70.16 4 1 52 | github.com/go-microservice/user-service/internal/cache/user_cache.go:74.2,74.18 1 1 53 | github.com/go-microservice/user-service/internal/cache/user_cache.go:70.16,73.3 2 0 54 | github.com/go-microservice/user-service/internal/cache/user_cache.go:78.110,80.24 2 1 55 | github.com/go-microservice/user-service/internal/cache/user_cache.go:86.2,88.16 3 1 56 | github.com/go-microservice/user-service/internal/cache/user_cache.go:91.2,91.20 1 1 57 | github.com/go-microservice/user-service/internal/cache/user_cache.go:80.24,83.3 2 2 58 | github.com/go-microservice/user-service/internal/cache/user_cache.go:88.16,90.3 1 0 59 | github.com/go-microservice/user-service/internal/cache/user_cache.go:95.115,97.25 2 2 60 | github.com/go-microservice/user-service/internal/cache/user_cache.go:102.2,103.16 2 2 61 | github.com/go-microservice/user-service/internal/cache/user_cache.go:106.2,106.12 1 2 62 | github.com/go-microservice/user-service/internal/cache/user_cache.go:97.25,100.3 2 4 63 | github.com/go-microservice/user-service/internal/cache/user_cache.go:103.16,105.3 1 0 64 | github.com/go-microservice/user-service/internal/cache/user_cache.go:110.71,113.16 3 1 65 | github.com/go-microservice/user-service/internal/cache/user_cache.go:116.2,116.12 1 1 66 | github.com/go-microservice/user-service/internal/cache/user_cache.go:113.16,115.3 1 0 67 | github.com/go-microservice/user-service/internal/cache/user_cache.go:120.79,123.16 3 0 68 | github.com/go-microservice/user-service/internal/cache/user_cache.go:126.2,126.12 1 0 69 | github.com/go-microservice/user-service/internal/cache/user_cache.go:123.16,125.3 1 0 70 | -------------------------------------------------------------------------------- /third_party/google/protobuf/any.proto: -------------------------------------------------------------------------------- 1 | // Protocol Buffers - Google's data interchange format 2 | // Copyright 2008 Google Inc. All rights reserved. 3 | // https://developers.google.com/protocol-buffers/ 4 | // 5 | // Redistribution and use in source and binary forms, with or without 6 | // modification, are permitted provided that the following conditions are 7 | // met: 8 | // 9 | // * Redistributions of source code must retain the above copyright 10 | // notice, this list of conditions and the following disclaimer. 11 | // * Redistributions in binary form must reproduce the above 12 | // copyright notice, this list of conditions and the following disclaimer 13 | // in the documentation and/or other materials provided with the 14 | // distribution. 15 | // * Neither the name of Google Inc. nor the names of its 16 | // contributors may be used to endorse or promote products derived from 17 | // this software without specific prior written permission. 18 | // 19 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 20 | // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 21 | // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 22 | // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 23 | // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 24 | // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 25 | // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 26 | // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 27 | // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 28 | // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | 31 | syntax = "proto3"; 32 | 33 | package google.protobuf; 34 | 35 | option csharp_namespace = "Google.Protobuf.WellKnownTypes"; 36 | option go_package = "google.golang.org/protobuf/types/known/anypb"; 37 | option java_package = "com.google.protobuf"; 38 | option java_outer_classname = "AnyProto"; 39 | option java_multiple_files = true; 40 | option objc_class_prefix = "GPB"; 41 | 42 | // `Any` contains an arbitrary serialized protocol buffer message along with a 43 | // URL that describes the type of the serialized message. 44 | // 45 | // Protobuf library provides support to pack/unpack Any values in the form 46 | // of utility functions or additional generated methods of the Any type. 47 | // 48 | // Example 1: Pack and unpack a message in C++. 49 | // 50 | // Foo foo = ...; 51 | // Any any; 52 | // any.PackFrom(foo); 53 | // ... 54 | // if (any.UnpackTo(&foo)) { 55 | // ... 56 | // } 57 | // 58 | // Example 2: Pack and unpack a message in Java. 59 | // 60 | // Foo foo = ...; 61 | // Any any = Any.pack(foo); 62 | // ... 63 | // if (any.is(Foo.class)) { 64 | // foo = any.unpack(Foo.class); 65 | // } 66 | // 67 | // Example 3: Pack and unpack a message in Python. 68 | // 69 | // foo = Foo(...) 70 | // any = Any() 71 | // any.Pack(foo) 72 | // ... 73 | // if any.Is(Foo.DESCRIPTOR): 74 | // any.Unpack(foo) 75 | // ... 76 | // 77 | // Example 4: Pack and unpack a message in Go 78 | // 79 | // foo := &pb.Foo{...} 80 | // any, err := anypb.New(foo) 81 | // if err != nil { 82 | // ... 83 | // } 84 | // ... 85 | // foo := &pb.Foo{} 86 | // if err := any.UnmarshalTo(foo); err != nil { 87 | // ... 88 | // } 89 | // 90 | // The pack methods provided by protobuf library will by default use 91 | // 'type.googleapis.com/full.type.name' as the type URL and the unpack 92 | // methods only use the fully qualified type name after the last '/' 93 | // in the type URL, for example "foo.bar.com/x/y.z" will yield type 94 | // name "y.z". 95 | // 96 | // 97 | // JSON 98 | // 99 | // The JSON representation of an `Any` value uses the regular 100 | // representation of the deserialized, embedded message, with an 101 | // additional field `@type` which contains the type URL. Example: 102 | // 103 | // package google.profile; 104 | // message Person { 105 | // string first_name = 1; 106 | // string last_name = 2; 107 | // } 108 | // 109 | // { 110 | // "@type": "type.googleapis.com/google.profile.Person", 111 | // "firstName": , 112 | // "lastName": 113 | // } 114 | // 115 | // If the embedded message type is well-known and has a custom JSON 116 | // representation, that representation will be embedded adding a field 117 | // `value` which holds the custom JSON in addition to the `@type` 118 | // field. Example (for message [google.protobuf.Duration][]): 119 | // 120 | // { 121 | // "@type": "type.googleapis.com/google.protobuf.Duration", 122 | // "value": "1.212s" 123 | // } 124 | // 125 | message Any { 126 | // A URL/resource name that uniquely identifies the type of the serialized 127 | // protocol buffer message. This string must contain at least 128 | // one "/" character. The last segment of the URL's path must represent 129 | // the fully qualified name of the type (as in 130 | // `path/google.protobuf.Duration`). The name should be in a canonical form 131 | // (e.g., leading "." is not accepted). 132 | // 133 | // In practice, teams usually precompile into the binary all types that they 134 | // expect it to use in the context of Any. However, for URLs which use the 135 | // scheme `http`, `https`, or no scheme, one can optionally set up a type 136 | // server that maps type URLs to message definitions as follows: 137 | // 138 | // * If no scheme is provided, `https` is assumed. 139 | // * An HTTP GET on the URL must yield a [google.protobuf.Type][] 140 | // value in binary format, or produce an error. 141 | // * Applications are allowed to cache lookup results based on the 142 | // URL, or have them precompiled into a binary to avoid any 143 | // lookup. Therefore, binary compatibility needs to be preserved 144 | // on changes to types. (Use versioned type names to manage 145 | // breaking changes.) 146 | // 147 | // Note: this functionality is not currently available in the official 148 | // protobuf release, and it is not used for type URLs beginning with 149 | // type.googleapis.com. 150 | // 151 | // Schemes other than `http`, `https` (or the empty scheme) might be 152 | // used with implementation specific semantics. 153 | // 154 | string type_url = 1; 155 | 156 | // Must be a valid serialized protocol buffer of the above specified type. 157 | bytes value = 2; 158 | } -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SHELL := /bin/bash 2 | BASEDIR = $(shell pwd) 3 | 4 | # make GIT_TAG=v1.0.0 build 5 | SERVICE_NAME?=user-service 6 | 7 | # build with version infos 8 | versionDir = "github.com/go-eagle/eagle/pkg/version" 9 | gitTag = $(shell if [ "`git describe --tags --abbrev=0 2>/dev/null`" != "" ];then git describe --tags --abbrev=0; else git log --pretty=format:'%h' -n 1; fi) 10 | buildDate = $(shell TZ=Asia/Shanghai date +%FT%T%z) 11 | gitCommit = $(shell git log --pretty=format:'%H' -n 1) 12 | gitTreeState = $(shell if git status|grep -q 'clean';then echo clean; else echo dirty; fi) 13 | 14 | ldflags="-w -X ${versionDir}.gitTag=${gitTag} -X ${versionDir}.buildDate=${buildDate} -X ${versionDir}.gitCommit=${gitCommit} -X ${versionDir}.gitTreeState=${gitTreeState}" 15 | 16 | PROJECT_NAME := "github.com/go-microservice/user-service" 17 | PKG := "$(PROJECT_NAME)" 18 | PKG_LIST := $(shell go list ${PKG}/... | grep -v /vendor/) 19 | GO_FILES := $(shell find . -name '*.go' | grep -v /vendor/ | grep -v _test.go) 20 | 21 | # proto 22 | APP_RELATIVE_PATH=$(shell a=`basename $$PWD` && echo $$b) 23 | API_PROTO_FILES=$(shell find api$(APP_RELATIVE_PATH) -name *.proto) 24 | 25 | # init environment variables 26 | export PATH := $(shell go env GOPATH)/bin:$(PATH) 27 | export GOPATH := $(shell go env GOPATH) 28 | export GO111MODULE := on 29 | 30 | # make make all 31 | .PHONY: all 32 | all: lint test build 33 | 34 | .PHONY: build 35 | # make build, Build the binary file 36 | build: 37 | GOOS=linux GOARCH=amd64 go build -v -ldflags ${ldflags} -o bin/$(SERVICE_NAME) cmd/server/main.go cmd/server/wire_gen.go 38 | 39 | .PHONY: run 40 | # make run, run current project 41 | run: wire 42 | go run cmd/server/main.go cmd/server/wire_gen.go 43 | 44 | .PHONY: wire 45 | # make wire, generate wire_gen.go 46 | wire: 47 | cd cmd/server && wire 48 | 49 | .PHONY: dep 50 | # make dep Get the dependencies 51 | dep: 52 | @go mod tidy 53 | 54 | .PHONY: fmt 55 | # make fmt 56 | fmt: 57 | @gofmt -s -w . 58 | 59 | .PHONY: golint 60 | # make golint 61 | golint: 62 | @if ! which golint &>/dev/null; then \ 63 | echo "Installing golint"; \ 64 | go get -u golang.org/x/lint/golint; \ 65 | fi 66 | @golint -set_exit_status ${PKG_LIST} 67 | 68 | .PHONY: lint 69 | # make lint 70 | lint: 71 | @if ! which golangci-lint &>/dev/null; then \ 72 | echo "Installing golangci-lint"; \ 73 | go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.43.0; \ 74 | fi 75 | ${GOPATH}/bin/golangci-lint run ./... 76 | 77 | .PHONY: test 78 | # make test 79 | test: vet 80 | @go test -short ${PKG_LIST} 81 | 82 | .PHONY: vet 83 | # make vet 84 | vet: 85 | go vet ./... | grep -v vendor;true 86 | 87 | .PHONY: cover 88 | # make cover 89 | cover: 90 | @go test -short -coverprofile=coverage.txt -covermode=atomic ${PKG_LIST} 91 | 92 | .PHONY: view-cover 93 | # make view-cover preview coverage 94 | view-cover: 95 | go tool cover -html=coverage.txt -o coverage.html 96 | 97 | .PHONY: docker 98 | # make docker 生成docker镜像, eg: make GIT_TAG=v1.0.0 docker 99 | docker: 100 | sh deploy/docker_image.sh $(GIT_TAG) 101 | 102 | .PHONY: deploy 103 | # make deploy deploy app to k8s 104 | deploy: 105 | sh deploy/deploy.sh 106 | 107 | .PHONY: clean 108 | # make clean 109 | clean: 110 | @-rm -vrf eagle 111 | @-rm -vrf cover.out 112 | @-rm -vrf coverage.txt 113 | @go mod tidy 114 | @echo "clean finished" 115 | 116 | .PHONY: graph 117 | # make graph 生成交互式的可视化Go程序调用图(会在浏览器自动打开) 118 | graph: 119 | @export GO111MODULE="on" 120 | @if ! which go-callvis &>/dev/null; then \ 121 | echo "downloading go-callvis"; \ 122 | go get -u github.com/ofabry/go-callvis; \ 123 | fi 124 | @echo "generating graph" 125 | @go-callvis github.com/go-eagle/eagle 126 | 127 | .PHONY: mockgen 128 | # make mockgen gen mock file 129 | mockgen: 130 | # mocken grpc client 131 | mockgen -destination="./internal/mocks/mock_user_grpc_client" -package=mocks -source="api/user/v1/user_grpc.pb.go UserServiceClient" 132 | # other 133 | cd ./internal && for file in `egrep -rnl "type.*?interface" ./repository | grep -v "_test" `; do \ 134 | echo $$file ; \ 135 | cd .. && mockgen -destination="./internal/mocks/$$file" -source="./internal/$$file" && cd ./internal ; \ 136 | done 137 | 138 | .PHONY: init 139 | # init env 140 | init: 141 | go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.27.1 142 | go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.2.0 143 | go install github.com/google/gnostic@latest 144 | go install github.com/google/gnostic/cmd/protoc-gen-openapi@latest 145 | go install github.com/pseudomuto/protoc-gen-doc/cmd/protoc-gen-doc@latest 146 | go install github.com/golang/mock/mockgen@latest 147 | go install github.com/favadi/protoc-go-inject-tag@latest 148 | go install github.com/envoyproxy/protoc-gen-validate 149 | 150 | .PHONY: proto 151 | # generate proto struct only 152 | proto: 153 | protoc --proto_path=. \ 154 | --proto_path=./third_party \ 155 | --go_out=. --go_opt=paths=source_relative \ 156 | --validate_out=lang=go,paths=source_relative:. \ 157 | $(API_PROTO_FILES) 158 | 159 | .PHONY: grpc 160 | # generate grpc code 161 | grpc: 162 | protoc --proto_path=. \ 163 | --proto_path=./third_party \ 164 | --go_out=. --go_opt=paths=source_relative \ 165 | --go-grpc_out=. --go-grpc_opt=paths=source_relative \ 166 | $(API_PROTO_FILES) 167 | 168 | .PHONY: openapi 169 | # generate openapi 170 | openapi: 171 | protoc --proto_path=. \ 172 | --proto_path=./third_party \ 173 | --openapi_out=. \ 174 | $(API_PROTO_FILES) 175 | 176 | .PHONY: doc 177 | # generate html or markdown doc 178 | doc: 179 | protoc --proto_path=. \ 180 | --proto_path=./third_party \ 181 | --doc_out=. \ 182 | --doc_opt=html,index.html \ 183 | $(API_PROTO_FILES) 184 | 185 | .PHONY: docs 186 | # gen swagger doc 187 | docs: 188 | @if ! which swag &>/dev/null; then \ 189 | echo "downloading swag"; \ 190 | go get -u github.com/swaggo/swag/cmd/swag; \ 191 | fi 192 | @swag init 193 | @mv docs/docs.go api/http 194 | @mv docs/swagger.json api/http 195 | @mv docs/swagger.yaml api/http 196 | @echo "gen-docs done" 197 | @echo "see docs by: http://localhost:8080/swagger/index.html" 198 | 199 | # show help 200 | help: 201 | @echo '' 202 | @echo 'Usage:' 203 | @echo ' make [target]' 204 | @echo '' 205 | @echo 'Targets:' 206 | @awk '/^[a-zA-Z\-\_0-9]+:/ { \ 207 | helpMessage = match(lastLine, /^# (.*)/); \ 208 | if (helpMessage) { \ 209 | helpCommand = substr($$1, 0, index($$1, ":")-1); \ 210 | helpMessage = substr(lastLine, RSTART + 2, RLENGTH); \ 211 | printf "\033[36m %-22s\033[0m %s\n", helpCommand,helpMessage; \ 212 | } \ 213 | } \ 214 | { lastLine = $$0 }' $(MAKEFILE_LIST) 215 | 216 | .DEFAULT_GOAL := all 217 | -------------------------------------------------------------------------------- /internal/repository/user_repo.go: -------------------------------------------------------------------------------- 1 | package repository 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "strings" 7 | "time" 8 | 9 | cacheBase "github.com/go-eagle/eagle/pkg/cache" 10 | "github.com/go-eagle/eagle/pkg/redis" 11 | 12 | "github.com/pkg/errors" 13 | "github.com/spf13/cast" 14 | "go.opentelemetry.io/otel" 15 | "go.opentelemetry.io/otel/trace" 16 | "golang.org/x/sync/singleflight" 17 | "gorm.io/gorm" 18 | 19 | "github.com/go-microservice/user-service/internal/cache" 20 | "github.com/go-microservice/user-service/internal/model" 21 | ) 22 | 23 | var ( 24 | _tableUserName = (&model.UserModel{}).TableName() 25 | _getUserSQL = "SELECT * FROM %s WHERE id = ?" 26 | _getUserByUsernameSQL = "SELECT * FROM %s WHERE username = ?" 27 | _getUserByEmailSQL = "SELECT * FROM %s WHERE email = ?" 28 | _getUserByPhoneSQL = "SELECT * FROM %s WHERE phone = ?" 29 | _batchGetUserSQL = "SELECT * FROM %s WHERE id IN (%s)" 30 | ) 31 | 32 | var ( 33 | g singleflight.Group 34 | ) 35 | 36 | var _ UserRepo = (*userRepo)(nil) 37 | 38 | // UserRepo define a repo interface 39 | type UserRepo interface { 40 | CreateUser(ctx context.Context, data *model.UserModel) (id int64, err error) 41 | UpdateUser(ctx context.Context, id int64, data *model.UserModel) error 42 | GetUser(ctx context.Context, id int64) (ret *model.UserModel, err error) 43 | GetUserByUsername(ctx context.Context, username string) (ret *model.UserModel, err error) 44 | GetUserByEmail(ctx context.Context, email string) (ret *model.UserModel, err error) 45 | GetUserByPhone(ctx context.Context, phone string) (ret *model.UserModel, err error) 46 | BatchGetUser(ctx context.Context, ids []int64) (ret []*model.UserModel, err error) 47 | } 48 | 49 | type userRepo struct { 50 | db *gorm.DB 51 | tracer trace.Tracer 52 | cache cache.UserCache 53 | } 54 | 55 | // NewUser new a repository and return 56 | func NewUser(db *gorm.DB, cache cache.UserCache) UserRepo { 57 | return &userRepo{ 58 | db: db, 59 | tracer: otel.Tracer("userRepo"), 60 | cache: cache, 61 | } 62 | } 63 | 64 | // CreateUser create a item 65 | func (r *userRepo) CreateUser(ctx context.Context, data *model.UserModel) (id int64, err error) { 66 | err = r.db.WithContext(ctx).Create(&data).Error 67 | if err != nil { 68 | return 0, errors.Wrap(err, "[repo] create User err") 69 | } 70 | 71 | return data.ID, nil 72 | } 73 | 74 | // UpdateUser update item 75 | func (r *userRepo) UpdateUser(ctx context.Context, id int64, data *model.UserModel) error { 76 | item, err := r.GetUser(ctx, id) 77 | if err != nil { 78 | return errors.Wrapf(err, "[repo] update User err: %v", err) 79 | } 80 | // 当使用 struct 更新时,默认情况下,GORM 只会更新非零值的字段 81 | // see: https://gorm.io/zh_CN/docs/update.html#%E6%9B%B4%E6%96%B0%E5%A4%9A%E5%88%97 82 | err = r.db.Model(&item).Updates(data).Error 83 | if err != nil { 84 | return err 85 | } 86 | 87 | // delete cache 88 | _ = r.cache.DelUserCache(ctx, id) 89 | 90 | return nil 91 | } 92 | 93 | // GetUser get a record by primary id 94 | func (r *userRepo) GetUser(ctx context.Context, id int64) (ret *model.UserModel, err error) { 95 | // read cache 96 | ret, err = r.cache.GetUserCache(ctx, id) 97 | if errors.Is(err, cacheBase.ErrPlaceholder) { 98 | return nil, model.ErrRecordNotFound 99 | } else if errors.Is(err, redis.ErrRedisNotFound) { 100 | // use sync/singleflight mode to get data 101 | // demo see: https://github.com/go-demo/singleflight-demo/blob/master/main.go 102 | // https://juejin.cn/post/6844904084445593613 103 | key := fmt.Sprintf("sf_get_user_%d", id) 104 | val, err, _ := g.Do(key, func() (interface{}, error) { 105 | data := new(model.UserModel) 106 | // get data from database 107 | err = r.db.WithContext(ctx).First(data, id).Error 108 | // if data is empty, set not found cache to prevent cache penetration(缓存穿透) 109 | if errors.Is(err, model.ErrRecordNotFound) { 110 | err = r.cache.SetCacheWithNotFound(ctx, id) 111 | if err != nil { 112 | return nil, err 113 | } 114 | return nil, model.ErrRecordNotFound 115 | } else if err != nil { 116 | return nil, errors.Wrapf(err, "[repo.user] query db err") 117 | } 118 | 119 | // set cache 120 | if data.ID > 0 { 121 | err = r.cache.SetUserCache(ctx, id, data, cacheBase.DefaultExpireTime) 122 | if err != nil { 123 | return nil, errors.Wrap(err, "[repo.user] SetUserBaseCache err") 124 | } 125 | return data, nil 126 | } 127 | return nil, model.ErrRecordNotFound 128 | }) 129 | if err != nil { 130 | return nil, err 131 | } 132 | if val != nil { 133 | ret = val.(*model.UserModel) 134 | return ret, nil 135 | } 136 | } else if err != nil { 137 | // fail fast, if cache error return, don't request to db 138 | return nil, err 139 | } 140 | 141 | return ret, nil 142 | } 143 | 144 | func (r *userRepo) GetUserByUsername(ctx context.Context, username string) (ret *model.UserModel, err error) { 145 | item := new(model.UserModel) 146 | err = r.db.WithContext(ctx).Raw(fmt.Sprintf(_getUserByUsernameSQL, _tableUserName), username).Scan(&item).Error 147 | if err != nil { 148 | return nil, err 149 | } 150 | 151 | return item, nil 152 | } 153 | 154 | func (r *userRepo) GetUserByEmail(ctx context.Context, email string) (ret *model.UserModel, err error) { 155 | item := new(model.UserModel) 156 | err = r.db.WithContext(ctx).Raw(fmt.Sprintf(_getUserByEmailSQL, _tableUserName), email).Scan(&item).Error 157 | if err != nil { 158 | return nil, err 159 | } 160 | 161 | return item, nil 162 | } 163 | 164 | func (r *userRepo) GetUserByPhone(ctx context.Context, phone string) (ret *model.UserModel, err error) { 165 | item := new(model.UserModel) 166 | err = r.db.WithContext(ctx).Raw(fmt.Sprintf(_getUserByPhoneSQL, _tableUserName), phone).Scan(&item).Error 167 | if err != nil { 168 | return 169 | } 170 | 171 | return item, nil 172 | } 173 | 174 | // BatchGetUser batch get items by primary id 175 | func (r *userRepo) BatchGetUser(ctx context.Context, ids []int64) (ret []*model.UserModel, err error) { 176 | itemMap, err := r.cache.MultiGetUserCache(ctx, ids) 177 | if err != nil { 178 | return nil, err 179 | } 180 | var missedID []int64 181 | for _, v := range ids { 182 | item, ok := itemMap[cast.ToString(v)] 183 | if !ok { 184 | missedID = append(missedID, v) 185 | continue 186 | } 187 | ret = append(ret, item) 188 | } 189 | // get missed data 190 | if len(missedID) > 0 { 191 | var missedData []*model.UserModel 192 | missedIDStr := cast.ToStringSlice(missedID) 193 | _sql := fmt.Sprintf(_batchGetUserSQL, _tableUserName, strings.Join(missedIDStr, ",")) 194 | err = r.db.WithContext(ctx).Raw(_sql).Scan(&missedData).Error 195 | if err != nil { 196 | return nil, err 197 | } 198 | if len(missedData) > 0 { 199 | ret = append(ret, missedData...) 200 | err = r.cache.MultiSetUserCache(ctx, missedData, 5*time.Minute) 201 | if err != nil { 202 | return nil, err 203 | } 204 | } 205 | } 206 | 207 | return ret, nil 208 | } 209 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/go-microservice/user-service 2 | 3 | go 1.22.3 4 | 5 | require ( 6 | github.com/DATA-DOG/go-sqlmock v1.5.0 7 | github.com/alicebob/miniredis/v2 v2.15.1 8 | github.com/envoyproxy/protoc-gen-validate v1.0.2 9 | github.com/gin-gonic/gin v1.9.0 10 | github.com/go-eagle/eagle v1.9.0 11 | github.com/go-redis/redis/v8 v8.11.4 12 | github.com/golang/mock v1.6.0 13 | github.com/google/wire v0.5.0 14 | github.com/hibiken/asynq v0.23.0 15 | github.com/jinzhu/copier v0.3.4 16 | github.com/pkg/errors v0.9.1 17 | github.com/prometheus/client_golang v1.14.0 18 | github.com/redis/go-redis/v9 v9.0.5 19 | github.com/spf13/cast v1.4.1 20 | github.com/spf13/pflag v1.0.5 21 | github.com/stretchr/testify v1.9.0 22 | github.com/swaggo/gin-swagger v1.2.0 23 | go.opentelemetry.io/otel v1.26.0 24 | go.opentelemetry.io/otel/trace v1.26.0 25 | go.uber.org/automaxprocs v1.5.1 26 | golang.org/x/sync v0.6.0 27 | google.golang.org/grpc v1.58.3 28 | google.golang.org/protobuf v1.33.0 29 | gorm.io/driver/mysql v1.5.2 30 | gorm.io/gorm v1.25.10 31 | ) 32 | 33 | require ( 34 | github.com/1024casts/gorm-opentelemetry v1.0.1-0.20210805144709-183269b54068 // indirect 35 | github.com/ClickHouse/ch-go v0.61.5 // indirect 36 | github.com/ClickHouse/clickhouse-go/v2 v2.23.2 // indirect 37 | github.com/KyleBanks/depth v1.2.1 // indirect 38 | github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a // indirect 39 | github.com/aliyun/alibaba-cloud-sdk-go v1.61.18 // indirect 40 | github.com/andybalholm/brotli v1.1.0 // indirect 41 | github.com/armon/go-metrics v0.3.10 // indirect 42 | github.com/beorn7/perks v1.0.1 // indirect 43 | github.com/buger/jsonparser v1.1.1 // indirect 44 | github.com/bytedance/sonic v1.8.0 // indirect 45 | github.com/cespare/xxhash/v2 v2.2.0 // indirect 46 | github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect 47 | github.com/coreos/go-semver v0.3.0 // indirect 48 | github.com/coreos/go-systemd/v22 v22.5.0 // indirect 49 | github.com/davecgh/go-spew v1.1.1 // indirect 50 | github.com/dgraph-io/ristretto v0.1.0 // indirect 51 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect 52 | github.com/dustin/go-humanize v1.0.1 // indirect 53 | github.com/fatih/color v1.13.0 // indirect 54 | github.com/fsnotify/fsnotify v1.6.0 // indirect 55 | github.com/gin-contrib/cors v1.3.1 // indirect 56 | github.com/gin-contrib/sse v0.1.0 // indirect 57 | github.com/go-errors/errors v1.0.1 // indirect 58 | github.com/go-faster/city v1.0.1 // indirect 59 | github.com/go-faster/errors v0.7.1 // indirect 60 | github.com/go-kratos/aegis v0.1.1 // indirect 61 | github.com/go-logr/logr v1.4.1 // indirect 62 | github.com/go-logr/stdr v1.2.2 // indirect 63 | github.com/go-ole/go-ole v1.2.6 // indirect 64 | github.com/go-openapi/jsonpointer v0.19.5 // indirect 65 | github.com/go-openapi/jsonreference v0.20.0 // indirect 66 | github.com/go-openapi/spec v0.20.3 // indirect 67 | github.com/go-openapi/swag v0.19.15 // indirect 68 | github.com/go-playground/locales v0.14.1 // indirect 69 | github.com/go-playground/universal-translator v0.18.1 // indirect 70 | github.com/go-playground/validator/v10 v10.11.2 // indirect 71 | github.com/go-sql-driver/mysql v1.7.0 // indirect 72 | github.com/goccy/go-json v0.10.0 // indirect 73 | github.com/gogo/protobuf v1.3.2 // indirect 74 | github.com/golang-jwt/jwt/v4 v4.4.2 // indirect 75 | github.com/golang/glog v1.1.0 // indirect 76 | github.com/golang/protobuf v1.5.3 // indirect 77 | github.com/golang/snappy v0.0.4 // indirect 78 | github.com/google/uuid v1.6.0 // indirect 79 | github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 // indirect 80 | github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 // indirect 81 | github.com/hashicorp/consul/api v1.11.0 // indirect 82 | github.com/hashicorp/go-cleanhttp v0.5.2 // indirect 83 | github.com/hashicorp/go-hclog v1.0.0 // indirect 84 | github.com/hashicorp/go-immutable-radix v1.3.1 // indirect 85 | github.com/hashicorp/go-rootcerts v1.0.2 // indirect 86 | github.com/hashicorp/go-version v1.6.0 // indirect 87 | github.com/hashicorp/golang-lru v0.5.4 // indirect 88 | github.com/hashicorp/hcl v1.0.0 // indirect 89 | github.com/hashicorp/serf v0.9.6 // indirect 90 | github.com/jackc/pgpassfile v1.0.0 // indirect 91 | github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect 92 | github.com/jackc/pgx/v5 v5.5.4 // indirect 93 | github.com/jackc/puddle/v2 v2.2.1 // indirect 94 | github.com/jinzhu/inflection v1.0.0 // indirect 95 | github.com/jinzhu/now v1.1.5 // indirect 96 | github.com/jmespath/go-jmespath v0.4.0 // indirect 97 | github.com/josharian/intern v1.0.0 // indirect 98 | github.com/json-iterator/go v1.1.12 // indirect 99 | github.com/klauspost/compress v1.17.8 // indirect 100 | github.com/klauspost/cpuid/v2 v2.0.9 // indirect 101 | github.com/leodido/go-urn v1.2.1 // indirect 102 | github.com/lestrrat-go/file-rotatelogs v2.4.0+incompatible // indirect 103 | github.com/lestrrat-go/strftime v1.0.5 // indirect 104 | github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect 105 | github.com/magiconair/properties v1.8.7 // indirect 106 | github.com/mailru/easyjson v0.7.7 // indirect 107 | github.com/mattn/go-colorable v0.1.12 // indirect 108 | github.com/mattn/go-isatty v0.0.17 // indirect 109 | github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect 110 | github.com/mitchellh/go-homedir v1.1.0 // indirect 111 | github.com/mitchellh/mapstructure v1.4.3 // indirect 112 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 113 | github.com/modern-go/reflect2 v1.0.2 // indirect 114 | github.com/nacos-group/nacos-sdk-go v1.1.1 // indirect 115 | github.com/paulmach/orb v0.11.1 // indirect 116 | github.com/pelletier/go-toml v1.9.5 // indirect 117 | github.com/pelletier/go-toml/v2 v2.0.6 // indirect 118 | github.com/pierrec/lz4/v4 v4.1.21 // indirect 119 | github.com/pmezard/go-difflib v1.0.0 // indirect 120 | github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect 121 | github.com/prometheus/client_model v0.4.0 // indirect 122 | github.com/prometheus/common v0.37.0 // indirect 123 | github.com/prometheus/procfs v0.8.0 // indirect 124 | github.com/qiniu/api.v7 v0.0.0-20190520053455-bea02cd22bf4 // indirect 125 | github.com/redis/go-redis/extra/rediscmd/v9 v9.0.5 // indirect 126 | github.com/redis/go-redis/extra/redisotel/v9 v9.0.5 // indirect 127 | github.com/robfig/cron/v3 v3.0.1 // indirect 128 | github.com/segmentio/asm v1.2.0 // indirect 129 | github.com/shirou/gopsutil/v3 v3.23.12 // indirect 130 | github.com/shoenig/go-m1cpu v0.1.6 // indirect 131 | github.com/shopspring/decimal v1.4.0 // indirect 132 | github.com/spf13/afero v1.9.2 // indirect 133 | github.com/spf13/jwalterweatherman v1.1.0 // indirect 134 | github.com/spf13/viper v1.10.0 // indirect 135 | github.com/subosito/gotenv v1.2.0 // indirect 136 | github.com/swaggo/swag v1.7.0 // indirect 137 | github.com/teris-io/shortid v0.0.0-20171029131806-771a37caa5cf // indirect 138 | github.com/tklauser/go-sysconf v0.3.12 // indirect 139 | github.com/tklauser/numcpus v0.6.1 // indirect 140 | github.com/toolkits/net v0.0.0-20160910085801-3f39ab6fe3ce // indirect 141 | github.com/twitchyliquid64/golang-asm v0.15.1 // indirect 142 | github.com/ugorji/go/codec v1.2.9 // indirect 143 | github.com/vearne/gin-timeout v0.1.0 // indirect 144 | github.com/vmihailenco/msgpack v4.0.4+incompatible // indirect 145 | github.com/willf/pad v0.0.0-20190207183901-eccfe5d84172 // indirect 146 | github.com/yuin/gopher-lua v0.0.0-20200816102855-ee81675732da // indirect 147 | github.com/yusufpapurcu/wmi v1.2.3 // indirect 148 | go.etcd.io/etcd/api/v3 v3.5.5 // indirect 149 | go.etcd.io/etcd/client/pkg/v3 v3.5.5 // indirect 150 | go.etcd.io/etcd/client/v3 v3.5.5 // indirect 151 | go.opentelemetry.io/contrib v0.22.0 // indirect 152 | go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.45.0 // indirect 153 | go.opentelemetry.io/contrib/propagators v0.22.0 // indirect 154 | go.opentelemetry.io/otel/exporters/jaeger v1.0.0-RC3 // indirect 155 | go.opentelemetry.io/otel/metric v1.26.0 // indirect 156 | go.opentelemetry.io/otel/sdk v1.26.0 // indirect 157 | go.uber.org/multierr v1.11.0 // indirect 158 | go.uber.org/zap v1.27.0 // indirect 159 | golang.org/x/arch v0.0.0-20210923205945-b76863e36670 // indirect 160 | golang.org/x/crypto v0.21.0 // indirect 161 | golang.org/x/net v0.23.0 // indirect 162 | golang.org/x/sys v0.19.0 // indirect 163 | golang.org/x/text v0.14.0 // indirect 164 | golang.org/x/time v0.3.0 // indirect 165 | golang.org/x/tools v0.14.0 // indirect 166 | google.golang.org/appengine v1.6.7 // indirect 167 | google.golang.org/genproto v0.0.0-20230711160842-782d3b101e98 // indirect 168 | google.golang.org/genproto/googleapis/api v0.0.0-20230711160842-782d3b101e98 // indirect 169 | google.golang.org/genproto/googleapis/rpc v0.0.0-20230711160842-782d3b101e98 // indirect 170 | gopkg.in/ini.v1 v1.66.2 // indirect 171 | gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect 172 | gopkg.in/yaml.v2 v2.4.0 // indirect 173 | gopkg.in/yaml.v3 v3.0.1 // indirect 174 | gorm.io/driver/clickhouse v0.6.1 // indirect 175 | gorm.io/driver/postgres v1.5.4 // indirect 176 | ) 177 | -------------------------------------------------------------------------------- /internal/service/user_svc.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "time" 7 | 8 | "github.com/go-eagle/eagle/pkg/app" 9 | "github.com/go-eagle/eagle/pkg/auth" 10 | "github.com/go-eagle/eagle/pkg/errcode" 11 | "github.com/jinzhu/copier" 12 | "github.com/spf13/cast" 13 | "google.golang.org/protobuf/types/known/emptypb" 14 | 15 | pb "github.com/go-microservice/user-service/api/user/v1" 16 | "github.com/go-microservice/user-service/internal/cache" 17 | "github.com/go-microservice/user-service/internal/ecode" 18 | "github.com/go-microservice/user-service/internal/model" 19 | "github.com/go-microservice/user-service/internal/repository" 20 | "github.com/go-microservice/user-service/internal/tasks" 21 | "github.com/go-microservice/user-service/internal/types" 22 | ) 23 | 24 | var ( 25 | _ pb.UserServiceServer = (*UserServiceServer)(nil) 26 | ) 27 | 28 | type UserServiceServer struct { 29 | pb.UnimplementedUserServiceServer 30 | 31 | repo repository.UserRepo 32 | } 33 | 34 | func NewUserServiceServer(repo repository.UserRepo) *UserServiceServer { 35 | return &UserServiceServer{ 36 | repo: repo, 37 | } 38 | } 39 | 40 | func (s *UserServiceServer) Register(ctx context.Context, req *pb.RegisterRequest) (*pb.RegisterReply, error) { 41 | err := req.Validate() 42 | if err != nil { 43 | return nil, ecode.ErrInvalidArgument.WithDetails(errcode.NewDetails(map[string]interface{}{ 44 | "msg": err.Error(), 45 | })).Status(req).Err() 46 | } 47 | 48 | var userBase *model.UserModel 49 | // check user is existed 50 | userBase, err = s.repo.GetUserByEmail(ctx, req.Email) 51 | if err != nil { 52 | return nil, ecode.ErrInternalError.WithDetails(errcode.NewDetails(map[string]interface{}{ 53 | "msg": err.Error(), 54 | })).Status(req).Err() 55 | } 56 | if userBase != nil && userBase.ID > 0 { 57 | return nil, ecode.ErrUserIsExist.Status(req).Err() 58 | } 59 | userBase, err = s.repo.GetUserByUsername(ctx, req.Username) 60 | if err != nil { 61 | return nil, ecode.ErrInternalError.WithDetails(errcode.NewDetails(map[string]interface{}{ 62 | "msg": err.Error(), 63 | })).Status(req).Err() 64 | } 65 | if userBase != nil && userBase.ID > 0 { 66 | return nil, ecode.ErrUserIsExist.Status(req).Err() 67 | } 68 | 69 | // gen a hash password 70 | pwd, err := auth.HashAndSalt(req.Password) 71 | if err != nil { 72 | return nil, errcode.ErrEncrypt 73 | } 74 | 75 | // create a new user 76 | user, err := newUser(req.Username, req.Email, pwd) 77 | if err != nil { 78 | return nil, ecode.ErrInternalError.WithDetails(errcode.NewDetails(map[string]interface{}{ 79 | "msg": err.Error(), 80 | })).Status(req).Err() 81 | } 82 | uid, err := s.repo.CreateUser(ctx, user) 83 | if err != nil { 84 | return nil, ecode.ErrInternalError.WithDetails(errcode.NewDetails(map[string]interface{}{ 85 | "msg": err.Error(), 86 | })).Status(req).Err() 87 | } 88 | 89 | // send welcome email 90 | err = tasks.NewEmailWelcomeTask(user.Username) 91 | if err != nil { 92 | return nil, ecode.ErrInternalError.WithDetails(errcode.NewDetails(map[string]interface{}{ 93 | "msg": err.Error(), 94 | })).Status(req).Err() 95 | } 96 | 97 | return &pb.RegisterReply{ 98 | Id: uid, 99 | Username: req.Username, 100 | }, nil 101 | } 102 | 103 | func newUser(username, email, password string) (*model.UserModel, error) { 104 | return &model.UserModel{ 105 | Username: username, 106 | Email: email, 107 | Password: password, 108 | Status: int32(pb.StatusType_NORMAL), 109 | CreatedAt: time.Now().Unix(), 110 | }, nil 111 | } 112 | 113 | func (s *UserServiceServer) Login(ctx context.Context, req *pb.LoginRequest) (*pb.LoginReply, error) { 114 | if len(req.Email) == 0 && len(req.Username) == 0 { 115 | return nil, ecode.ErrInvalidArgument.Status(req).Err() 116 | } 117 | 118 | // get user base info 119 | var ( 120 | user *model.UserModel 121 | err error 122 | ) 123 | if req.Email != "" { 124 | user, err = s.repo.GetUserByEmail(ctx, req.Email) 125 | if err != nil { 126 | return nil, ecode.ErrInternalError.WithDetails(errcode.NewDetails(map[string]interface{}{ 127 | "msg": err.Error(), 128 | })).Status(req).Err() 129 | } 130 | } 131 | if user == nil && len(req.Username) > 0 { 132 | user, err = s.repo.GetUserByUsername(ctx, req.Username) 133 | if err != nil { 134 | return nil, ecode.ErrInternalError.WithDetails(errcode.NewDetails(map[string]interface{}{ 135 | "msg": err.Error(), 136 | })).Status(req).Err() 137 | } 138 | } 139 | if user == nil || user.ID == 0 { 140 | return nil, ecode.ErrPasswordIncorrect.Status(req).Err() 141 | } 142 | 143 | if !auth.ComparePasswords(user.Password, req.Password) { 144 | return nil, ecode.ErrPasswordIncorrect.Status(req).Err() 145 | } 146 | 147 | // Sign the json web token. 148 | payload := map[string]interface{}{"user_id": user.ID, "username": user.Username} 149 | token, err := app.Sign(ctx, payload, app.Conf.JwtSecret, int64(cache.UserTokenExpireTime)) 150 | if err != nil { 151 | return nil, ecode.ErrToken.Status(req).Err() 152 | } 153 | 154 | // record token to redis 155 | err = cache.NewUserTokenCache().SetUserTokenCache(ctx, user.ID, token, cache.UserTokenExpireTime) 156 | if err != nil { 157 | return nil, ecode.ErrToken.Status(req).Err() 158 | } 159 | 160 | return &pb.LoginReply{ 161 | Id: user.ID, 162 | AccessToken: token, 163 | }, nil 164 | } 165 | 166 | func (s *UserServiceServer) Logout(ctx context.Context, req *pb.LogoutRequest) (*emptypb.Empty, error) { 167 | c := cache.NewUserTokenCache() 168 | // check token 169 | token, err := c.GetUserTokenCache(ctx, req.Id) 170 | if err != nil { 171 | return nil, ecode.ErrToken.Status(req).Err() 172 | } 173 | if token != req.AccessToken { 174 | return nil, ecode.ErrAccessDenied.Status(req).Err() 175 | } 176 | 177 | // delete token from cache 178 | err = c.DelUserTokenCache(ctx, req.GetId()) 179 | if err != nil { 180 | return nil, ecode.ErrInternalError.WithDetails(errcode.NewDetails(map[string]interface{}{ 181 | "msg": err.Error(), 182 | })).Status(req).Err() 183 | } 184 | 185 | // NOTE: don't set to nil 186 | return &emptypb.Empty{}, nil 187 | } 188 | 189 | func (s *UserServiceServer) CreateUser(ctx context.Context, req *pb.CreateUserRequest) (*pb.CreateUserReply, error) { 190 | // gen a hash password 191 | pwd, err := auth.HashAndSalt(req.Password) 192 | if err != nil { 193 | return nil, errcode.ErrEncrypt 194 | } 195 | 196 | // create a new user 197 | user, err := newUser(req.Username, req.Email, pwd) 198 | if err != nil { 199 | return nil, ecode.ErrInternalError.WithDetails(errcode.NewDetails(map[string]interface{}{ 200 | "msg": err.Error(), 201 | })).Status(req).Err() 202 | } 203 | id, err := s.repo.CreateUser(ctx, user) 204 | if err != nil { 205 | return nil, ecode.ErrInternalError.WithDetails(errcode.NewDetails(map[string]interface{}{ 206 | "msg": err.Error(), 207 | })).Status(req).Err() 208 | } 209 | 210 | return &pb.CreateUserReply{ 211 | Id: id, 212 | Username: req.Username, 213 | }, nil 214 | } 215 | 216 | func (s *UserServiceServer) UpdateUser(ctx context.Context, req *pb.UpdateUserRequest) (*pb.UpdateUserReply, error) { 217 | if req.UserId == 0 { 218 | return nil, ecode.ErrInvalidArgument.Status(req).Err() 219 | } 220 | 221 | user := &model.UserModel{ 222 | Nickname: req.Nickname, 223 | Phone: req.Phone, 224 | Email: req.Email, 225 | Avatar: req.Avatar, 226 | Gender: cast.ToString(req.Gender), 227 | Birthday: req.Birthday, 228 | Bio: req.Bio, 229 | Status: cast.ToInt32(req.Status), 230 | UpdatedAt: time.Now().Unix(), 231 | } 232 | err := s.repo.UpdateUser(ctx, req.UserId, user) 233 | if err != nil { 234 | return nil, ecode.ErrInternalError.WithDetails(errcode.NewDetails(map[string]interface{}{ 235 | "msg": err.Error(), 236 | })).Status(req).Err() 237 | } 238 | 239 | return &pb.UpdateUserReply{ 240 | UserId: req.UserId, 241 | Nickname: req.Nickname, 242 | Phone: req.Phone, 243 | Email: req.Email, 244 | Avatar: req.Avatar, 245 | Gender: req.Gender, 246 | Birthday: req.Birthday, 247 | Bio: req.Bio, 248 | Status: req.Status, 249 | UpdatedAt: time.Now().Unix(), 250 | }, nil 251 | } 252 | 253 | func (s *UserServiceServer) UpdatePassword(ctx context.Context, req *pb.UpdatePasswordRequest) (*pb.UpdatePasswordReply, error) { 254 | if len(req.Id) == 0 { 255 | return nil, ecode.ErrInvalidArgument.Status(req).Err() 256 | } 257 | if len(req.Password) == 0 || len(req.NewPassword) == 0 || len(req.ConfirmPassword) == 0 { 258 | return nil, ecode.ErrInvalidArgument.Status(req).Err() 259 | } 260 | if req.NewPassword != req.ConfirmPassword { 261 | return nil, ecode.ErrTwicePasswordNotMatch.Status(req).Err() 262 | } 263 | 264 | // get user base info 265 | var ( 266 | user *model.UserModel 267 | err error 268 | ) 269 | user, err = s.repo.GetUser(ctx, cast.ToInt64(req.Id)) 270 | if err != nil { 271 | return nil, ecode.ErrInternalError.WithDetails(errcode.NewDetails(map[string]interface{}{ 272 | "msg": err.Error(), 273 | })).Status(req).Err() 274 | } 275 | if user == nil || user.ID == 0 { 276 | return nil, ecode.ErrUserNotFound.Status(req).Err() 277 | } 278 | 279 | if !auth.ComparePasswords(user.Password, req.Password) { 280 | return nil, ecode.ErrPasswordIncorrect.Status(req).Err() 281 | } 282 | 283 | newPwd, err := auth.HashAndSalt(req.NewPassword) 284 | if err != nil { 285 | return nil, ecode.ErrEncrypt.Status(req).Err() 286 | } 287 | 288 | data := &model.UserModel{ 289 | Password: newPwd, 290 | UpdatedAt: time.Now().Unix(), 291 | } 292 | err = s.repo.UpdateUser(ctx, user.ID, data) 293 | if err != nil { 294 | return nil, ecode.ErrInternalError.WithDetails(errcode.NewDetails(map[string]interface{}{ 295 | "msg": err.Error(), 296 | })).Status(req).Err() 297 | } 298 | 299 | return &pb.UpdatePasswordReply{}, nil 300 | } 301 | func (s *UserServiceServer) GetUser(ctx context.Context, req *pb.GetUserRequest) (*pb.GetUserReply, error) { 302 | user, err := s.repo.GetUser(ctx, req.Id) 303 | if err != nil { 304 | if errors.Is(err, model.ErrRecordNotFound) { 305 | return nil, ecode.ErrUserNotFound.Status(req).Err() 306 | } 307 | return nil, ecode.ErrInternalError.WithDetails(errcode.NewDetails(map[string]interface{}{ 308 | "msg": err.Error(), 309 | })).Status(req).Err() 310 | } 311 | 312 | u, err := convertUser(user) 313 | if err != nil { 314 | return nil, ecode.ErrInternalError.WithDetails(errcode.NewDetails(map[string]interface{}{ 315 | "msg": err.Error(), 316 | })).Status(req).Err() 317 | } 318 | 319 | return &pb.GetUserReply{ 320 | User: u, 321 | }, nil 322 | } 323 | func (s *UserServiceServer) BatchGetUsers(ctx context.Context, req *pb.BatchGetUsersRequest) (*pb.BatchGetUsersReply, error) { 324 | // check rpc request if canceled 325 | if ctx.Err() == context.Canceled { 326 | return nil, ecode.ErrCanceled.Status(req).Err() 327 | } 328 | 329 | if len(req.GetIds()) == 0 { 330 | return nil, errors.New("ids is empty") 331 | } 332 | var ( 333 | ids []int64 334 | users []*pb.User 335 | ) 336 | ids = req.GetIds() 337 | 338 | // user base 339 | userBases, err := s.repo.BatchGetUser(ctx, ids) 340 | if err != nil { 341 | return nil, ecode.ErrInternalError.Status(req).Err() 342 | } 343 | userMap := make(map[int64]*model.UserModel, 0) 344 | for _, val := range userBases { 345 | userMap[val.ID] = val 346 | } 347 | 348 | // compose data 349 | for _, id := range ids { 350 | user, ok := userMap[id] 351 | if !ok { 352 | continue 353 | } 354 | u, err := convertUser(user) 355 | if err != nil { 356 | // record log 357 | continue 358 | } 359 | users = append(users, u) 360 | } 361 | 362 | return &pb.BatchGetUsersReply{ 363 | Users: users, 364 | }, nil 365 | } 366 | 367 | func convertUser(u *model.UserModel) (*pb.User, error) { 368 | if u == nil { 369 | return nil, nil 370 | } 371 | user := &types.User{ 372 | Id: u.ID, 373 | Username: u.Username, 374 | Phone: u.Phone, 375 | Email: u.Email, 376 | LoginAt: u.LoginAt, 377 | Status: u.Status, 378 | Nickname: u.Nickname, 379 | Avatar: u.Avatar, 380 | Gender: u.Gender, 381 | Birthday: u.Birthday, 382 | Bio: u.Bio, 383 | CreatedAt: u.CreatedAt, 384 | UpdatedAt: u.UpdatedAt, 385 | } 386 | 387 | // copy to pb.user 388 | pbUser := &pb.User{} 389 | err := copier.Copy(pbUser, &user) 390 | if err != nil { 391 | return nil, err 392 | } 393 | return pbUser, nil 394 | } 395 | -------------------------------------------------------------------------------- /api/user/v1/user_grpc.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-go-grpc. DO NOT EDIT. 2 | // versions: 3 | // - protoc-gen-go-grpc v1.2.0 4 | // - protoc v3.18.1 5 | // source: api/user/v1/user.proto 6 | 7 | package v1 8 | 9 | import ( 10 | context "context" 11 | grpc "google.golang.org/grpc" 12 | codes "google.golang.org/grpc/codes" 13 | status "google.golang.org/grpc/status" 14 | emptypb "google.golang.org/protobuf/types/known/emptypb" 15 | ) 16 | 17 | // This is a compile-time assertion to ensure that this generated file 18 | // is compatible with the grpc package it is being compiled against. 19 | // Requires gRPC-Go v1.32.0 or later. 20 | const _ = grpc.SupportPackageIsVersion7 21 | 22 | // UserServiceClient is the client API for UserService service. 23 | // 24 | // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. 25 | type UserServiceClient interface { 26 | // auth 27 | // sign up 28 | Register(ctx context.Context, in *RegisterRequest, opts ...grpc.CallOption) (*RegisterReply, error) 29 | // sign in 30 | Login(ctx context.Context, in *LoginRequest, opts ...grpc.CallOption) (*LoginReply, error) 31 | // logout 32 | Logout(ctx context.Context, in *LogoutRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) 33 | // user 34 | CreateUser(ctx context.Context, in *CreateUserRequest, opts ...grpc.CallOption) (*CreateUserReply, error) 35 | GetUser(ctx context.Context, in *GetUserRequest, opts ...grpc.CallOption) (*GetUserReply, error) 36 | BatchGetUsers(ctx context.Context, in *BatchGetUsersRequest, opts ...grpc.CallOption) (*BatchGetUsersReply, error) 37 | UpdateUser(ctx context.Context, in *UpdateUserRequest, opts ...grpc.CallOption) (*UpdateUserReply, error) 38 | UpdatePassword(ctx context.Context, in *UpdatePasswordRequest, opts ...grpc.CallOption) (*UpdatePasswordReply, error) 39 | } 40 | 41 | type userServiceClient struct { 42 | cc grpc.ClientConnInterface 43 | } 44 | 45 | func NewUserServiceClient(cc grpc.ClientConnInterface) UserServiceClient { 46 | return &userServiceClient{cc} 47 | } 48 | 49 | func (c *userServiceClient) Register(ctx context.Context, in *RegisterRequest, opts ...grpc.CallOption) (*RegisterReply, error) { 50 | out := new(RegisterReply) 51 | err := c.cc.Invoke(ctx, "/user.v1.UserService/Register", in, out, opts...) 52 | if err != nil { 53 | return nil, err 54 | } 55 | return out, nil 56 | } 57 | 58 | func (c *userServiceClient) Login(ctx context.Context, in *LoginRequest, opts ...grpc.CallOption) (*LoginReply, error) { 59 | out := new(LoginReply) 60 | err := c.cc.Invoke(ctx, "/user.v1.UserService/Login", in, out, opts...) 61 | if err != nil { 62 | return nil, err 63 | } 64 | return out, nil 65 | } 66 | 67 | func (c *userServiceClient) Logout(ctx context.Context, in *LogoutRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) { 68 | out := new(emptypb.Empty) 69 | err := c.cc.Invoke(ctx, "/user.v1.UserService/Logout", in, out, opts...) 70 | if err != nil { 71 | return nil, err 72 | } 73 | return out, nil 74 | } 75 | 76 | func (c *userServiceClient) CreateUser(ctx context.Context, in *CreateUserRequest, opts ...grpc.CallOption) (*CreateUserReply, error) { 77 | out := new(CreateUserReply) 78 | err := c.cc.Invoke(ctx, "/user.v1.UserService/CreateUser", in, out, opts...) 79 | if err != nil { 80 | return nil, err 81 | } 82 | return out, nil 83 | } 84 | 85 | func (c *userServiceClient) GetUser(ctx context.Context, in *GetUserRequest, opts ...grpc.CallOption) (*GetUserReply, error) { 86 | out := new(GetUserReply) 87 | err := c.cc.Invoke(ctx, "/user.v1.UserService/GetUser", in, out, opts...) 88 | if err != nil { 89 | return nil, err 90 | } 91 | return out, nil 92 | } 93 | 94 | func (c *userServiceClient) BatchGetUsers(ctx context.Context, in *BatchGetUsersRequest, opts ...grpc.CallOption) (*BatchGetUsersReply, error) { 95 | out := new(BatchGetUsersReply) 96 | err := c.cc.Invoke(ctx, "/user.v1.UserService/BatchGetUsers", in, out, opts...) 97 | if err != nil { 98 | return nil, err 99 | } 100 | return out, nil 101 | } 102 | 103 | func (c *userServiceClient) UpdateUser(ctx context.Context, in *UpdateUserRequest, opts ...grpc.CallOption) (*UpdateUserReply, error) { 104 | out := new(UpdateUserReply) 105 | err := c.cc.Invoke(ctx, "/user.v1.UserService/UpdateUser", in, out, opts...) 106 | if err != nil { 107 | return nil, err 108 | } 109 | return out, nil 110 | } 111 | 112 | func (c *userServiceClient) UpdatePassword(ctx context.Context, in *UpdatePasswordRequest, opts ...grpc.CallOption) (*UpdatePasswordReply, error) { 113 | out := new(UpdatePasswordReply) 114 | err := c.cc.Invoke(ctx, "/user.v1.UserService/UpdatePassword", in, out, opts...) 115 | if err != nil { 116 | return nil, err 117 | } 118 | return out, nil 119 | } 120 | 121 | // UserServiceServer is the server API for UserService service. 122 | // All implementations must embed UnimplementedUserServiceServer 123 | // for forward compatibility 124 | type UserServiceServer interface { 125 | // auth 126 | // sign up 127 | Register(context.Context, *RegisterRequest) (*RegisterReply, error) 128 | // sign in 129 | Login(context.Context, *LoginRequest) (*LoginReply, error) 130 | // logout 131 | Logout(context.Context, *LogoutRequest) (*emptypb.Empty, error) 132 | // user 133 | CreateUser(context.Context, *CreateUserRequest) (*CreateUserReply, error) 134 | GetUser(context.Context, *GetUserRequest) (*GetUserReply, error) 135 | BatchGetUsers(context.Context, *BatchGetUsersRequest) (*BatchGetUsersReply, error) 136 | UpdateUser(context.Context, *UpdateUserRequest) (*UpdateUserReply, error) 137 | UpdatePassword(context.Context, *UpdatePasswordRequest) (*UpdatePasswordReply, error) 138 | mustEmbedUnimplementedUserServiceServer() 139 | } 140 | 141 | // UnimplementedUserServiceServer must be embedded to have forward compatible implementations. 142 | type UnimplementedUserServiceServer struct { 143 | } 144 | 145 | func (UnimplementedUserServiceServer) Register(context.Context, *RegisterRequest) (*RegisterReply, error) { 146 | return nil, status.Errorf(codes.Unimplemented, "method Register not implemented") 147 | } 148 | func (UnimplementedUserServiceServer) Login(context.Context, *LoginRequest) (*LoginReply, error) { 149 | return nil, status.Errorf(codes.Unimplemented, "method Login not implemented") 150 | } 151 | func (UnimplementedUserServiceServer) Logout(context.Context, *LogoutRequest) (*emptypb.Empty, error) { 152 | return nil, status.Errorf(codes.Unimplemented, "method Logout not implemented") 153 | } 154 | func (UnimplementedUserServiceServer) CreateUser(context.Context, *CreateUserRequest) (*CreateUserReply, error) { 155 | return nil, status.Errorf(codes.Unimplemented, "method CreateUser not implemented") 156 | } 157 | func (UnimplementedUserServiceServer) GetUser(context.Context, *GetUserRequest) (*GetUserReply, error) { 158 | return nil, status.Errorf(codes.Unimplemented, "method GetUser not implemented") 159 | } 160 | func (UnimplementedUserServiceServer) BatchGetUsers(context.Context, *BatchGetUsersRequest) (*BatchGetUsersReply, error) { 161 | return nil, status.Errorf(codes.Unimplemented, "method BatchGetUsers not implemented") 162 | } 163 | func (UnimplementedUserServiceServer) UpdateUser(context.Context, *UpdateUserRequest) (*UpdateUserReply, error) { 164 | return nil, status.Errorf(codes.Unimplemented, "method UpdateUser not implemented") 165 | } 166 | func (UnimplementedUserServiceServer) UpdatePassword(context.Context, *UpdatePasswordRequest) (*UpdatePasswordReply, error) { 167 | return nil, status.Errorf(codes.Unimplemented, "method UpdatePassword not implemented") 168 | } 169 | func (UnimplementedUserServiceServer) mustEmbedUnimplementedUserServiceServer() {} 170 | 171 | // UnsafeUserServiceServer may be embedded to opt out of forward compatibility for this service. 172 | // Use of this interface is not recommended, as added methods to UserServiceServer will 173 | // result in compilation errors. 174 | type UnsafeUserServiceServer interface { 175 | mustEmbedUnimplementedUserServiceServer() 176 | } 177 | 178 | func RegisterUserServiceServer(s grpc.ServiceRegistrar, srv UserServiceServer) { 179 | s.RegisterService(&UserService_ServiceDesc, srv) 180 | } 181 | 182 | func _UserService_Register_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 183 | in := new(RegisterRequest) 184 | if err := dec(in); err != nil { 185 | return nil, err 186 | } 187 | if interceptor == nil { 188 | return srv.(UserServiceServer).Register(ctx, in) 189 | } 190 | info := &grpc.UnaryServerInfo{ 191 | Server: srv, 192 | FullMethod: "/user.v1.UserService/Register", 193 | } 194 | handler := func(ctx context.Context, req interface{}) (interface{}, error) { 195 | return srv.(UserServiceServer).Register(ctx, req.(*RegisterRequest)) 196 | } 197 | return interceptor(ctx, in, info, handler) 198 | } 199 | 200 | func _UserService_Login_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 201 | in := new(LoginRequest) 202 | if err := dec(in); err != nil { 203 | return nil, err 204 | } 205 | if interceptor == nil { 206 | return srv.(UserServiceServer).Login(ctx, in) 207 | } 208 | info := &grpc.UnaryServerInfo{ 209 | Server: srv, 210 | FullMethod: "/user.v1.UserService/Login", 211 | } 212 | handler := func(ctx context.Context, req interface{}) (interface{}, error) { 213 | return srv.(UserServiceServer).Login(ctx, req.(*LoginRequest)) 214 | } 215 | return interceptor(ctx, in, info, handler) 216 | } 217 | 218 | func _UserService_Logout_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 219 | in := new(LogoutRequest) 220 | if err := dec(in); err != nil { 221 | return nil, err 222 | } 223 | if interceptor == nil { 224 | return srv.(UserServiceServer).Logout(ctx, in) 225 | } 226 | info := &grpc.UnaryServerInfo{ 227 | Server: srv, 228 | FullMethod: "/user.v1.UserService/Logout", 229 | } 230 | handler := func(ctx context.Context, req interface{}) (interface{}, error) { 231 | return srv.(UserServiceServer).Logout(ctx, req.(*LogoutRequest)) 232 | } 233 | return interceptor(ctx, in, info, handler) 234 | } 235 | 236 | func _UserService_CreateUser_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 237 | in := new(CreateUserRequest) 238 | if err := dec(in); err != nil { 239 | return nil, err 240 | } 241 | if interceptor == nil { 242 | return srv.(UserServiceServer).CreateUser(ctx, in) 243 | } 244 | info := &grpc.UnaryServerInfo{ 245 | Server: srv, 246 | FullMethod: "/user.v1.UserService/CreateUser", 247 | } 248 | handler := func(ctx context.Context, req interface{}) (interface{}, error) { 249 | return srv.(UserServiceServer).CreateUser(ctx, req.(*CreateUserRequest)) 250 | } 251 | return interceptor(ctx, in, info, handler) 252 | } 253 | 254 | func _UserService_GetUser_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 255 | in := new(GetUserRequest) 256 | if err := dec(in); err != nil { 257 | return nil, err 258 | } 259 | if interceptor == nil { 260 | return srv.(UserServiceServer).GetUser(ctx, in) 261 | } 262 | info := &grpc.UnaryServerInfo{ 263 | Server: srv, 264 | FullMethod: "/user.v1.UserService/GetUser", 265 | } 266 | handler := func(ctx context.Context, req interface{}) (interface{}, error) { 267 | return srv.(UserServiceServer).GetUser(ctx, req.(*GetUserRequest)) 268 | } 269 | return interceptor(ctx, in, info, handler) 270 | } 271 | 272 | func _UserService_BatchGetUsers_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 273 | in := new(BatchGetUsersRequest) 274 | if err := dec(in); err != nil { 275 | return nil, err 276 | } 277 | if interceptor == nil { 278 | return srv.(UserServiceServer).BatchGetUsers(ctx, in) 279 | } 280 | info := &grpc.UnaryServerInfo{ 281 | Server: srv, 282 | FullMethod: "/user.v1.UserService/BatchGetUsers", 283 | } 284 | handler := func(ctx context.Context, req interface{}) (interface{}, error) { 285 | return srv.(UserServiceServer).BatchGetUsers(ctx, req.(*BatchGetUsersRequest)) 286 | } 287 | return interceptor(ctx, in, info, handler) 288 | } 289 | 290 | func _UserService_UpdateUser_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 291 | in := new(UpdateUserRequest) 292 | if err := dec(in); err != nil { 293 | return nil, err 294 | } 295 | if interceptor == nil { 296 | return srv.(UserServiceServer).UpdateUser(ctx, in) 297 | } 298 | info := &grpc.UnaryServerInfo{ 299 | Server: srv, 300 | FullMethod: "/user.v1.UserService/UpdateUser", 301 | } 302 | handler := func(ctx context.Context, req interface{}) (interface{}, error) { 303 | return srv.(UserServiceServer).UpdateUser(ctx, req.(*UpdateUserRequest)) 304 | } 305 | return interceptor(ctx, in, info, handler) 306 | } 307 | 308 | func _UserService_UpdatePassword_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 309 | in := new(UpdatePasswordRequest) 310 | if err := dec(in); err != nil { 311 | return nil, err 312 | } 313 | if interceptor == nil { 314 | return srv.(UserServiceServer).UpdatePassword(ctx, in) 315 | } 316 | info := &grpc.UnaryServerInfo{ 317 | Server: srv, 318 | FullMethod: "/user.v1.UserService/UpdatePassword", 319 | } 320 | handler := func(ctx context.Context, req interface{}) (interface{}, error) { 321 | return srv.(UserServiceServer).UpdatePassword(ctx, req.(*UpdatePasswordRequest)) 322 | } 323 | return interceptor(ctx, in, info, handler) 324 | } 325 | 326 | // UserService_ServiceDesc is the grpc.ServiceDesc for UserService service. 327 | // It's only intended for direct use with grpc.RegisterService, 328 | // and not to be introspected or modified (even as a copy) 329 | var UserService_ServiceDesc = grpc.ServiceDesc{ 330 | ServiceName: "user.v1.UserService", 331 | HandlerType: (*UserServiceServer)(nil), 332 | Methods: []grpc.MethodDesc{ 333 | { 334 | MethodName: "Register", 335 | Handler: _UserService_Register_Handler, 336 | }, 337 | { 338 | MethodName: "Login", 339 | Handler: _UserService_Login_Handler, 340 | }, 341 | { 342 | MethodName: "Logout", 343 | Handler: _UserService_Logout_Handler, 344 | }, 345 | { 346 | MethodName: "CreateUser", 347 | Handler: _UserService_CreateUser_Handler, 348 | }, 349 | { 350 | MethodName: "GetUser", 351 | Handler: _UserService_GetUser_Handler, 352 | }, 353 | { 354 | MethodName: "BatchGetUsers", 355 | Handler: _UserService_BatchGetUsers_Handler, 356 | }, 357 | { 358 | MethodName: "UpdateUser", 359 | Handler: _UserService_UpdateUser_Handler, 360 | }, 361 | { 362 | MethodName: "UpdatePassword", 363 | Handler: _UserService_UpdatePassword_Handler, 364 | }, 365 | }, 366 | Streams: []grpc.StreamDesc{}, 367 | Metadata: "api/user/v1/user.proto", 368 | } 369 | -------------------------------------------------------------------------------- /third_party/google/api/http.proto: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | syntax = "proto3"; 16 | 17 | package google.api; 18 | 19 | option cc_enable_arenas = true; 20 | option go_package = "google.golang.org/genproto/googleapis/api/annotations;annotations"; 21 | option java_multiple_files = true; 22 | option java_outer_classname = "HttpProto"; 23 | option java_package = "com.google.api"; 24 | option objc_class_prefix = "GAPI"; 25 | 26 | // Defines the HTTP configuration for an API service. It contains a list of 27 | // [HttpRule][google.api.HttpRule], each specifying the mapping of an RPC method 28 | // to one or more HTTP REST API methods. 29 | message Http { 30 | // A list of HTTP configuration rules that apply to individual API methods. 31 | // 32 | // **NOTE:** All service configuration rules follow "last one wins" order. 33 | repeated HttpRule rules = 1; 34 | 35 | // When set to true, URL path parameters will be fully URI-decoded except in 36 | // cases of single segment matches in reserved expansion, where "%2F" will be 37 | // left encoded. 38 | // 39 | // The default behavior is to not decode RFC 6570 reserved characters in multi 40 | // segment matches. 41 | bool fully_decode_reserved_expansion = 2; 42 | } 43 | 44 | // # gRPC Transcoding 45 | // 46 | // gRPC Transcoding is a feature for mapping between a gRPC method and one or 47 | // more HTTP REST endpoints. It allows developers to build a single API service 48 | // that supports both gRPC APIs and REST APIs. Many systems, including [Google 49 | // APIs](https://github.com/googleapis/googleapis), 50 | // [Cloud Endpoints](https://cloud.google.com/endpoints), [gRPC 51 | // Gateway](https://github.com/grpc-ecosystem/grpc-gateway), 52 | // and [Envoy](https://github.com/envoyproxy/envoy) proxy support this feature 53 | // and use it for large scale production services. 54 | // 55 | // `HttpRule` defines the schema of the gRPC/REST mapping. The mapping specifies 56 | // how different portions of the gRPC request message are mapped to the URL 57 | // path, URL query parameters, and HTTP request body. It also controls how the 58 | // gRPC response message is mapped to the HTTP response body. `HttpRule` is 59 | // typically specified as an `google.api.http` annotation on the gRPC method. 60 | // 61 | // Each mapping specifies a URL path template and an HTTP method. The path 62 | // template may refer to one or more fields in the gRPC request message, as long 63 | // as each field is a non-repeated field with a primitive (non-message) type. 64 | // The path template controls how fields of the request message are mapped to 65 | // the URL path. 66 | // 67 | // Example: 68 | // 69 | // service Messaging { 70 | // rpc GetMessage(GetMessageRequest) returns (Message) { 71 | // option (google.api.http) = { 72 | // get: "/v1/{name=messages/*}" 73 | // }; 74 | // } 75 | // } 76 | // message GetMessageRequest { 77 | // string name = 1; // Mapped to URL path. 78 | // } 79 | // message Message { 80 | // string text = 1; // The resource content. 81 | // } 82 | // 83 | // This enables an HTTP REST to gRPC mapping as below: 84 | // 85 | // HTTP | gRPC 86 | // -----|----- 87 | // `GET /v1/messages/123456` | `GetMessage(name: "messages/123456")` 88 | // 89 | // Any fields in the request message which are not bound by the path template 90 | // automatically become HTTP query parameters if there is no HTTP request body. 91 | // For example: 92 | // 93 | // service Messaging { 94 | // rpc GetMessage(GetMessageRequest) returns (Message) { 95 | // option (google.api.http) = { 96 | // get:"/v1/messages/{message_id}" 97 | // }; 98 | // } 99 | // } 100 | // message GetMessageRequest { 101 | // message SubMessage { 102 | // string subfield = 1; 103 | // } 104 | // string message_id = 1; // Mapped to URL path. 105 | // int64 revision = 2; // Mapped to URL query parameter `revision`. 106 | // SubMessage sub = 3; // Mapped to URL query parameter `sub.subfield`. 107 | // } 108 | // 109 | // This enables a HTTP JSON to RPC mapping as below: 110 | // 111 | // HTTP | gRPC 112 | // -----|----- 113 | // `GET /v1/messages/123456?revision=2&sub.subfield=foo` | 114 | // `GetMessage(message_id: "123456" revision: 2 sub: SubMessage(subfield: 115 | // "foo"))` 116 | // 117 | // Note that fields which are mapped to URL query parameters must have a 118 | // primitive type or a repeated primitive type or a non-repeated message type. 119 | // In the case of a repeated type, the parameter can be repeated in the URL 120 | // as `...?param=A¶m=B`. In the case of a message type, each field of the 121 | // message is mapped to a separate parameter, such as 122 | // `...?foo.a=A&foo.b=B&foo.c=C`. 123 | // 124 | // For HTTP methods that allow a request body, the `body` field 125 | // specifies the mapping. Consider a REST update method on the 126 | // message resource collection: 127 | // 128 | // service Messaging { 129 | // rpc UpdateMessage(UpdateMessageRequest) returns (Message) { 130 | // option (google.api.http) = { 131 | // patch: "/v1/messages/{message_id}" 132 | // body: "message" 133 | // }; 134 | // } 135 | // } 136 | // message UpdateMessageRequest { 137 | // string message_id = 1; // mapped to the URL 138 | // Message message = 2; // mapped to the body 139 | // } 140 | // 141 | // The following HTTP JSON to RPC mapping is enabled, where the 142 | // representation of the JSON in the request body is determined by 143 | // protos JSON encoding: 144 | // 145 | // HTTP | gRPC 146 | // -----|----- 147 | // `PATCH /v1/messages/123456 { "text": "Hi!" }` | `UpdateMessage(message_id: 148 | // "123456" message { text: "Hi!" })` 149 | // 150 | // The special name `*` can be used in the body mapping to define that 151 | // every field not bound by the path template should be mapped to the 152 | // request body. This enables the following alternative definition of 153 | // the update method: 154 | // 155 | // service Messaging { 156 | // rpc UpdateMessage(Message) returns (Message) { 157 | // option (google.api.http) = { 158 | // patch: "/v1/messages/{message_id}" 159 | // body: "*" 160 | // }; 161 | // } 162 | // } 163 | // message Message { 164 | // string message_id = 1; 165 | // string text = 2; 166 | // } 167 | // 168 | // 169 | // The following HTTP JSON to RPC mapping is enabled: 170 | // 171 | // HTTP | gRPC 172 | // -----|----- 173 | // `PATCH /v1/messages/123456 { "text": "Hi!" }` | `UpdateMessage(message_id: 174 | // "123456" text: "Hi!")` 175 | // 176 | // Note that when using `*` in the body mapping, it is not possible to 177 | // have HTTP parameters, as all fields not bound by the path end in 178 | // the body. This makes this option more rarely used in practice when 179 | // defining REST APIs. The common usage of `*` is in custom methods 180 | // which don't use the URL at all for transferring data. 181 | // 182 | // It is possible to define multiple HTTP methods for one RPC by using 183 | // the `additional_bindings` option. Example: 184 | // 185 | // service Messaging { 186 | // rpc GetMessage(GetMessageRequest) returns (Message) { 187 | // option (google.api.http) = { 188 | // get: "/v1/messages/{message_id}" 189 | // additional_bindings { 190 | // get: "/v1/users/{user_id}/messages/{message_id}" 191 | // } 192 | // }; 193 | // } 194 | // } 195 | // message GetMessageRequest { 196 | // string message_id = 1; 197 | // string user_id = 2; 198 | // } 199 | // 200 | // This enables the following two alternative HTTP JSON to RPC mappings: 201 | // 202 | // HTTP | gRPC 203 | // -----|----- 204 | // `GET /v1/messages/123456` | `GetMessage(message_id: "123456")` 205 | // `GET /v1/users/me/messages/123456` | `GetMessage(user_id: "me" message_id: 206 | // "123456")` 207 | // 208 | // ## Rules for HTTP mapping 209 | // 210 | // 1. Leaf request fields (recursive expansion nested messages in the request 211 | // message) are classified into three categories: 212 | // - Fields referred by the path template. They are passed via the URL path. 213 | // - Fields referred by the [HttpRule.body][google.api.HttpRule.body]. They are passed via the HTTP 214 | // request body. 215 | // - All other fields are passed via the URL query parameters, and the 216 | // parameter name is the field path in the request message. A repeated 217 | // field can be represented as multiple query parameters under the same 218 | // name. 219 | // 2. If [HttpRule.body][google.api.HttpRule.body] is "*", there is no URL query parameter, all fields 220 | // are passed via URL path and HTTP request body. 221 | // 3. If [HttpRule.body][google.api.HttpRule.body] is omitted, there is no HTTP request body, all 222 | // fields are passed via URL path and URL query parameters. 223 | // 224 | // ### Path template syntax 225 | // 226 | // Template = "/" Segments [ Verb ] ; 227 | // Segments = Segment { "/" Segment } ; 228 | // Segment = "*" | "**" | LITERAL | Variable ; 229 | // Variable = "{" FieldPath [ "=" Segments ] "}" ; 230 | // FieldPath = IDENT { "." IDENT } ; 231 | // Verb = ":" LITERAL ; 232 | // 233 | // The syntax `*` matches a single URL path segment. The syntax `**` matches 234 | // zero or more URL path segments, which must be the last part of the URL path 235 | // except the `Verb`. 236 | // 237 | // The syntax `Variable` matches part of the URL path as specified by its 238 | // template. A variable template must not contain other variables. If a variable 239 | // matches a single path segment, its template may be omitted, e.g. `{var}` 240 | // is equivalent to `{var=*}`. 241 | // 242 | // The syntax `LITERAL` matches literal text in the URL path. If the `LITERAL` 243 | // contains any reserved character, such characters should be percent-encoded 244 | // before the matching. 245 | // 246 | // If a variable contains exactly one path segment, such as `"{var}"` or 247 | // `"{var=*}"`, when such a variable is expanded into a URL path on the client 248 | // side, all characters except `[-_.~0-9a-zA-Z]` are percent-encoded. The 249 | // server side does the reverse decoding. Such variables show up in the 250 | // [Discovery 251 | // Document](https://developers.google.com/discovery/v1/reference/apis) as 252 | // `{var}`. 253 | // 254 | // If a variable contains multiple path segments, such as `"{var=foo/*}"` 255 | // or `"{var=**}"`, when such a variable is expanded into a URL path on the 256 | // client side, all characters except `[-_.~/0-9a-zA-Z]` are percent-encoded. 257 | // The server side does the reverse decoding, except "%2F" and "%2f" are left 258 | // unchanged. Such variables show up in the 259 | // [Discovery 260 | // Document](https://developers.google.com/discovery/v1/reference/apis) as 261 | // `{+var}`. 262 | // 263 | // ## Using gRPC API Service Configuration 264 | // 265 | // gRPC API Service Configuration (service config) is a configuration language 266 | // for configuring a gRPC service to become a user-facing product. The 267 | // service config is simply the YAML representation of the `google.api.Service` 268 | // proto message. 269 | // 270 | // As an alternative to annotating your proto file, you can configure gRPC 271 | // transcoding in your service config YAML files. You do this by specifying a 272 | // `HttpRule` that maps the gRPC method to a REST endpoint, achieving the same 273 | // effect as the proto annotation. This can be particularly useful if you 274 | // have a proto that is reused in multiple services. Note that any transcoding 275 | // specified in the service config will override any matching transcoding 276 | // configuration in the proto. 277 | // 278 | // Example: 279 | // 280 | // http: 281 | // rules: 282 | // # Selects a gRPC method and applies HttpRule to it. 283 | // - selector: example.v1.Messaging.GetMessage 284 | // get: /v1/messages/{message_id}/{sub.subfield} 285 | // 286 | // ## Special notes 287 | // 288 | // When gRPC Transcoding is used to map a gRPC to JSON REST endpoints, the 289 | // proto to JSON conversion must follow the [proto3 290 | // specification](https://developers.google.com/protocol-buffers/docs/proto3#json). 291 | // 292 | // While the single segment variable follows the semantics of 293 | // [RFC 6570](https://tools.ietf.org/html/rfc6570) Section 3.2.2 Simple String 294 | // Expansion, the multi segment variable **does not** follow RFC 6570 Section 295 | // 3.2.3 Reserved Expansion. The reason is that the Reserved Expansion 296 | // does not expand special characters like `?` and `#`, which would lead 297 | // to invalid URLs. As the result, gRPC Transcoding uses a custom encoding 298 | // for multi segment variables. 299 | // 300 | // The path variables **must not** refer to any repeated or mapped field, 301 | // because client libraries are not capable of handling such variable expansion. 302 | // 303 | // The path variables **must not** capture the leading "/" character. The reason 304 | // is that the most common use case "{var}" does not capture the leading "/" 305 | // character. For consistency, all path variables must share the same behavior. 306 | // 307 | // Repeated message fields must not be mapped to URL query parameters, because 308 | // no client library can support such complicated mapping. 309 | // 310 | // If an API needs to use a JSON array for request or response body, it can map 311 | // the request or response body to a repeated field. However, some gRPC 312 | // Transcoding implementations may not support this feature. 313 | message HttpRule { 314 | // Selects a method to which this rule applies. 315 | // 316 | // Refer to [selector][google.api.DocumentationRule.selector] for syntax details. 317 | string selector = 1; 318 | 319 | // Determines the URL pattern is matched by this rules. This pattern can be 320 | // used with any of the {get|put|post|delete|patch} methods. A custom method 321 | // can be defined using the 'custom' field. 322 | oneof pattern { 323 | // Maps to HTTP GET. Used for listing and getting information about 324 | // resources. 325 | string get = 2; 326 | 327 | // Maps to HTTP PUT. Used for replacing a resource. 328 | string put = 3; 329 | 330 | // Maps to HTTP POST. Used for creating a resource or performing an action. 331 | string post = 4; 332 | 333 | // Maps to HTTP DELETE. Used for deleting a resource. 334 | string delete = 5; 335 | 336 | // Maps to HTTP PATCH. Used for updating a resource. 337 | string patch = 6; 338 | 339 | // The custom pattern is used for specifying an HTTP method that is not 340 | // included in the `pattern` field, such as HEAD, or "*" to leave the 341 | // HTTP method unspecified for this rule. The wild-card rule is useful 342 | // for services that provide content to Web (HTML) clients. 343 | CustomHttpPattern custom = 8; 344 | } 345 | 346 | // The name of the request field whose value is mapped to the HTTP request 347 | // body, or `*` for mapping all request fields not captured by the path 348 | // pattern to the HTTP body, or omitted for not having any HTTP request body. 349 | // 350 | // NOTE: the referred field must be present at the top-level of the request 351 | // message type. 352 | string body = 7; 353 | 354 | // Optional. The name of the response field whose value is mapped to the HTTP 355 | // response body. When omitted, the entire response message will be used 356 | // as the HTTP response body. 357 | // 358 | // NOTE: The referred field must be present at the top-level of the response 359 | // message type. 360 | string response_body = 12; 361 | 362 | // Additional HTTP bindings for the selector. Nested bindings must 363 | // not contain an `additional_bindings` field themselves (that is, 364 | // the nesting may only be one level deep). 365 | repeated HttpRule additional_bindings = 11; 366 | } 367 | 368 | // A custom pattern is used for defining custom HTTP verb. 369 | message CustomHttpPattern { 370 | // The name of this custom HTTP verb. 371 | string kind = 1; 372 | 373 | // The path matched by this custom verb. 374 | string path = 2; 375 | } --------------------------------------------------------------------------------