我们通过一个系列文章跟大家详细展示一个 go-zero 微服务示例,整个系列分十篇文章,目录结构如下:
期望通过本系列带你在本机利用 Docker 环境利用 go-zero 快速开发一个商城系统,让你快速上手微服务。
完整示例代码:https://github.com/nivin-studio/go-zero-mall
首先,我们来更新一下上篇文章中的服务拆分图片,由于微信公众号手机和电脑端不同步,导致美化的图片没有跟大家见面,特此补上,如图:
$ cd mall/service/user
$ vim model/user.sql
CREATE TABLE `user` ( `id` bigint unsigned NOT NULL AUTO_INCREMENT, `name` varchar(255) NOT NULL DEFAULT '' COMMENT '用户姓名', `gender` tinyint(3) unsigned NOT NULL DEFAULT '0' COMMENT '用户性别', `mobile` varchar(255) NOT NULL DEFAULT '' COMMENT '用户电话', `password` varchar(255) NOT NULL DEFAULT '' COMMENT '用户密码', `create_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP, `update_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `idx_mobile_unique` (`mobile`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
$ goctl model mysql ddl -src ./model/user.sql -dir ./model -c
$ vim api/user.api
type ( // 用户登录 LoginRequest { Mobile string `json:"mobile"` Password string `json:"password"` } LoginResponse { AccessToken string `json:"accessToken"` AccessExpire int64 `json:"accessExpire"` } // 用户登录 // 用户注册 RegisterRequest { Name string `json:"name"` Gender int64 `json:"gender"` Mobile string `json:"mobile"` Password string `json:"password"` } RegisterResponse { Id int64 `json:"id"` Name string `json:"name"` Gender int64 `json:"gender"` Mobile string `json:"mobile"` } // 用户注册 // 用户信息 UserInfoResponse { Id int64 `json:"id"` Name string `json:"name"` Gender int64 `json:"gender"` Mobile string `json:"mobile"` } // 用户信息 ) service User { @handler Login post /api/user/login(LoginRequest) returns (LoginResponse) @handler Register post /api/user/register(RegisterRequest) returns (RegisterResponse) } @server( jwt: Auth ) service User { @handler UserInfo post /api/user/userinfo() returns (UserInfoResponse) }
$ goctl api go -api ./api/user.api -dir ./api
$ vim rpc/user.proto
syntax = "proto3"; package userclient; option go_package = "user"; // 用户登录 message LoginRequest { string Mobile = 1; string Password = 2; } message LoginResponse { int64 Id = 1; string Name = 2; int64 Gender = 3; string Mobile = 4; } // 用户登录 // 用户注册 message RegisterRequest { string Name = 1; int64 Gender = 2; string Mobile = 3; string Password = 4; } message RegisterResponse { int64 Id = 1; string Name = 2; int64 Gender = 3; string Mobile = 4; } // 用户注册 // 用户信息 message UserInfoRequest { int64 Id = 1; } message UserInfoResponse { int64 Id = 1; string Name = 2; int64 Gender = 3; string Mobile = 4; } // 用户信息 service User { rpc Login(LoginRequest) returns(LoginResponse); rpc Register(RegisterRequest) returns(RegisterResponse); rpc UserInfo(UserInfoRequest) returns(UserInfoResponse); }
$ goctl rpc proto -src ./rpc/user.proto -dir ./rpc
添加下载依赖包
回到 mall
项目根目录执行如下命令:
$ go mod tidy
$ vim rpc/etc/user.yaml
Etcd
服务配置,Mysql
服务配置,CacheRedis
服务配置Name: user.rpc ListenOn: 0.0.0.0:9000 Etcd: Hosts: - etcd:2379 Key: user.rpc Mysql: DataSource: root:123456@tcp(mysql:3306)/mall?charset=utf8mb4&parseTime=true&loc=Asia%2FShanghai CacheRedis: - Host: redis:6379 Type: node Pass:
Mysql
服务配置,CacheRedis
服务配置的实例化$ vim rpc/internal/config/config.go
package config import ( "github.com/tal-tech/go-zero/core/stores/cache" "github.com/tal-tech/go-zero/zrpc" ) type Config struct { zrpc.RpcServerConf Mysql struct { DataSource string } CacheRedis cache.CacheConf }
user model
的依赖$ vim rpc/internal/svc/servicecontext.go
package svc import ( "mall/service/user/model" "mall/service/user/rpc/internal/config" "github.com/tal-tech/go-zero/core/stores/sqlx" ) type ServiceContext struct { Config config.Config UserModel model.UserModel } func NewServiceContext(c config.Config) *ServiceContext { conn := sqlx.NewMysql(c.Mysql.DataSource) return &ServiceContext{ Config: c, UserModel: model.NewUserModel(conn, c.CacheRedis), } }
添加密码加密工具
在根目录 common
新建 crypt
工具库,此工具方法主要用于密码的加密处理。
$ vim common/cryptx/crypt.go
package cryptx import ( "fmt" "golang.org/x/crypto/scrypt" ) func PasswordEncrypt(salt, password string) string { dk, _ := scrypt.Key([]byte(password), []byte(salt), 32768, 8, 1, 32) return fmt.Sprintf("%x", string(dk)) }
Salt
配置$ vim rpc/etc/user.yaml
Name: user.rpc ListenOn: 0.0.0.0:9000 ... Salt: HWVOFkGgPTryzICwd7qnJaZR9KQ2i8xe
$ vim rpc/internal/config/config.go
package config import ( "github.com/tal-tech/go-zero/core/stores/cache" "github.com/tal-tech/go-zero/zrpc" ) type Config struct { ... Salt string }
添加用户注册逻辑
用户注册流程,先判断注册手机号是否已经被注册,手机号未被注册,将用户信息写入数据库,用户密码需要进行加密存储。
$ vim rpc/internal/logic/registerlogic.go
package logic import ( "context" "mall/common/cryptx" "mall/service/user/model" "mall/service/user/rpc/internal/svc" "mall/service/user/rpc/user" "github.com/tal-tech/go-zero/core/logx" "google.golang.org/grpc/status" ) type RegisterLogic struct { ctx context.Context svcCtx *svc.ServiceContext logx.Logger } func NewRegisterLogic(ctx context.Context, svcCtx *svc.ServiceContext) *RegisterLogic { return &RegisterLogic{ ctx: ctx, svcCtx: svcCtx, Logger: logx.WithContext(ctx), } } func (l *RegisterLogic) Register(in *user.RegisterRequest) (*user.RegisterResponse, error) { // 判断手机号是否已经注册 _, err := l.svcCtx.UserModel.FindOneByMobile(in.Mobile) if err == nil { return nil, status.Error(100, "该用户已存在") } if err == model.ErrNotFound { newUser := model.User{ Name: in.Name, Gender: in.Gender, Mobile: in.Mobile, Password: cryptx.PasswordEncrypt(l.svcCtx.Config.Salt, in.Password), } res, err := l.svcCtx.UserModel.Insert(&newUser) if err != nil { return nil, status.Error(500, err.Error()) } newUser.Id, err = res.LastInsertId() if err != nil { return nil, status.Error(500, err.Error()) } return &user.RegisterResponse{ Id: newUser.Id, Name: newUser.Name, Gender: newUser.Gender, Mobile: newUser.Mobile, }, nil } return nil, status.Error(500, err.Error()) }
用户登录流程,通过手机号查询判断用户是否是注册用户,如果是注册用户,需要将用户输入的密码进行加密与数据库中用户加密密码进行对比验证。
$ vim rpc/internal/logic/loginlogic.go
package logic import ( "context" "mall/common/cryptx" "mall/service/user/model" "mall/service/user/rpc/internal/svc" "mall/service/user/rpc/user" "github.com/tal-tech/go-zero/core/logx" "google.golang.org/grpc/status" ) type LoginLogic struct { ctx context.Context svcCtx *svc.ServiceContext logx.Logger } func NewLoginLogic(ctx context.Context, svcCtx *svc.ServiceContext) *LoginLogic { return &LoginLogic{ ctx: ctx, svcCtx: svcCtx, Logger: logx.WithContext(ctx), } } func (l *LoginLogic) Login(in *user.LoginRequest) (*user.LoginResponse, error) { // 查询用户是否存在 res, err := l.svcCtx.UserModel.FindOneByMobile(in.Mobile) if err != nil { if err == model.ErrNotFound { return nil, status.Error(100, "用户不存在") } return nil, status.Error(500, err.Error()) } // 判断密码是否正确 password := cryptx.PasswordEncrypt(l.svcCtx.Config.Salt, in.Password) if password != res.Password { return nil, status.Error(100, "密码错误") } return &user.LoginResponse{ Id: res.Id, Name: res.Name, Gender: res.Gender, Mobile: res.Mobile, }, nil }
$ vim rpc/internal/logic/userinfologic.go
package logic import ( "context" "mall/service/user/model" "mall/service/user/rpc/internal/svc" "mall/service/user/rpc/user" "github.com/tal-tech/go-zero/core/logx" "google.golang.org/grpc/status" ) type UserInfoLogic struct { ctx context.Context svcCtx *svc.ServiceContext logx.Logger } func NewUserInfoLogic(ctx context.Context, svcCtx *svc.ServiceContext) *UserInfoLogic { return &UserInfoLogic{ ctx: ctx, svcCtx: svcCtx, Logger: logx.WithContext(ctx), } } func (l *UserInfoLogic) UserInfo(in *user.UserInfoRequest) (*user.UserInfoResponse, error) { // 查询用户是否存在 res, err := l.svcCtx.UserModel.FindOne(in.Id) if err != nil { if err == model.ErrNotFound { return nil, status.Error(100, "用户不存在") } return nil, status.Error(500, err.Error()) } return &user.UserInfoResponse{ Id: res.Id, Name: res.Name, Gender: res.Gender, Mobile: res.Mobile, }, nil }
$ vim api/etc/user.yaml
Mysql
服务配置,CacheRedis
服务配置,Auth
验证配置Name: User Host: 0.0.0.0 Port: 8000 Mysql: DataSource: root:123456@tcp(mysql:3306)/mall?charset=utf8mb4&parseTime=true&loc=Asia%2FShanghai CacheRedis: - Host: redis:6379 Pass: Type: node Auth: AccessSecret: uOvKLmVfztaXGpNYd4Z0I1SiT7MweJhl AccessExpire: 86400
user rpc
服务配置$ vim api/etc/user.yaml
Name: User Host: 0.0.0.0 Port: 8000 ...... UserRpc: Etcd: Hosts: - etcd:2379 Key: user.rpc
user rpc
服务配置的实例化$ vim api/internal/config/config.go
package config import ( "github.com/tal-tech/go-zero/rest" "github.com/tal-tech/go-zero/zrpc" ) type Config struct { rest.RestConf Auth struct { AccessSecret string AccessExpire int64 } UserRpc zrpc.RpcClientConf }
user rpc
的依赖$ vim api/internal/svc/servicecontext.go
package svc import ( "mall/service/user/api/internal/config" "mall/service/user/rpc/userclient" "github.com/tal-tech/go-zero/zrpc" ) type ServiceContext struct { Config config.Config UserRpc userclient.User } func NewServiceContext(c config.Config) *ServiceContext { return &ServiceContext{ Config: c, UserRpc: userclient.NewUser(zrpc.MustNewClient(c.UserRpc)), } }
$ vim api/internal/logic/registerlogic.go
package logic import ( "context" "mall/service/user/api/internal/svc" "mall/service/user/api/internal/types" "mall/service/user/rpc/userclient" "github.com/tal-tech/go-zero/core/logx" ) type RegisterLogic struct { logx.Logger ctx context.Context svcCtx *svc.ServiceContext } func NewRegisterLogic(ctx context.Context, svcCtx *svc.ServiceContext) RegisterLogic { return RegisterLogic{ Logger: logx.WithContext(ctx), ctx: ctx, svcCtx: svcCtx, } } func (l *RegisterLogic) Register(req types.RegisterRequest) (resp *types.RegisterResponse, err error) { res, err := l.svcCtx.UserRpc.Register(l.ctx, &userclient.RegisterRequest{ Name: req.Name, Gender: req.Gender, Mobile: req.Mobile, Password: req.Password, }) if err != nil { return nil, err } return &types.RegisterResponse{ Id: res.Id, Name: res.Name, Gender: res.Gender, Mobile: res.Mobile, }, nil }
添加 JWT
工具
在根目录 common
新建 jwtx
工具库,用于生成用户 token
。
$ vim common/jwtx/jwt.go
package jwtx import "github.com/golang-jwt/jwt" func GetToken(secretKey string, iat, seconds, uid int64) (string, error) { claims := make(jwt.MapClaims) claims["exp"] = iat + seconds claims["iat"] = iat claims["uid"] = uid token := jwt.New(jwt.SigningMethodHS256) token.Claims = claims return token.SignedString([]byte(secretKey)) }
添加用户登录逻辑
通过调用 user rpc
服务进行登录验证,登录成功后,使用用户信息生成对应的 token
以及 token
的有效期。
$ vim api/internal/logic/loginlogic.go
package logic import ( "context" "time" "mall/common/jwtx" "mall/service/user/api/internal/svc" "mall/service/user/api/internal/types" "mall/service/user/rpc/userclient" "github.com/tal-tech/go-zero/core/logx" ) type LoginLogic struct { logx.Logger ctx context.Context svcCtx *svc.ServiceContext } func NewLoginLogic(ctx context.Context, svcCtx *svc.ServiceContext) LoginLogic { return LoginLogic{ Logger: logx.WithContext(ctx), ctx: ctx, svcCtx: svcCtx, } } func (l *LoginLogic) Login(req types.LoginRequest) (resp *types.LoginResponse, err error) { res, err := l.svcCtx.UserRpc.Login(l.ctx, &userclient.LoginRequest{ Mobile: req.Mobile, Password: req.Password, }) if err != nil { return nil, err } now := time.Now().Unix() accessExpire := l.svcCtx.Config.Auth.AccessExpire accessToken, err := jwtx.GetToken(l.svcCtx.Config.Auth.AccessSecret, now, accessExpire, res.Id) if err != nil { return nil, err } return &types.LoginResponse{ AccessToken: accessToken, AccessExpire: now + accessExpire, }, nil }
$ vim api/internal/logic/userinfologic.go
package logic import ( "context" "encoding/json" "mall/service/user/api/internal/svc" "mall/service/user/api/internal/types" "mall/service/user/rpc/userclient" "github.com/tal-tech/go-zero/core/logx" ) type UserInfoLogic struct { logx.Logger ctx context.Context svcCtx *svc.ServiceContext } func NewUserInfoLogic(ctx context.Context, svcCtx *svc.ServiceContext) UserInfoLogic { return UserInfoLogic{ Logger: logx.WithContext(ctx), ctx: ctx, svcCtx: svcCtx, } } func (l *UserInfoLogic) UserInfo() (resp *types.UserInfoResponse, err error) { uid, _ := l.ctx.Value("uid").(json.Number).Int64() res, err := l.svcCtx.UserRpc.UserInfo(l.ctx, &userclient.UserInfoRequest{ Id: uid, }) if err != nil { return nil, err } return &types.UserInfoResponse{ Id: res.Id, Name: res.Name, Gender: res.Gender, Mobile: res.Mobile, }, nil }
通过
l.ctx.Value("uid")
可获取 jwt token 中自定义的参数
提示:启动服务需要在
golang
容器中启动
$ cd mall/service/user/rpc $ go run user.go -f etc/user.yaml Starting rpc server at 127.0.0.1:9000...
提示:启动服务需要在
golang
容器中启动
$ cd mall/service/user/api $ go run user.go -f etc/user.yaml Starting server at 0.0.0.0:8000...
https://github.com/zeromicro/go-zero
https://gitee.com/kevwan/go-zero
欢迎使用 go-zero
并 star 支持我们!
关注『微服务实践』公众号并点击 交流群 获取社区群二维码。
|