学习笔记:带你十天轻松搞定 Go 微服务系列(三)- 用户服务
1、学习课程
带你十天轻松搞定 Go 微服务系列(三)
2、生成 user model 模型
在昨天代码目录 xxx/gonivinck/code 下
| $ cd mall/service/user |
| $ vim model/user.sql |
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 |
有的人就会报错
| Command 'goctl' not found, did you mean: |
| |
| command 'doctl' from snap doctl (v1.69.0+git2.8af78470) |
| |
| See 'snap info <snapname>' for additional versions. |

解决方法:在宿主机未安装 goctl 是不能访问的使用的,我们进入 golang 容器使用就好了(后面有需要 goctl 都要进入容器使用 goctl )
1. 第一步
| $ docker ps | grep golang |
执行结果:

这里面列出所有在运行的容器信息,第一项就是CONTAINER ID,下面要用到
| CONTAINER ID IMAGE COMMAND ... PORTS NAMES |
2. 第二步
| $ docker exec -it container-id /bin/bash |
这里的 container-id 就换成上面 CONTAINER ID 的信息,我这里是 13258ded6d62
执行结果:

3. 第三步
| $ cd mall/service/user |
| $ goctl model mysql ddl -src ./model/user.sql -dir ./model -c |
当出现 done,就证明执行成功了
执行结果:

3、生成 user api 服务流程
3.1 创建 api 文件
3.2 编写 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) |
| } |
3.3 运行模板生成命令
| $ goctl api go -api ./api/user.api -dir ./api |
4、生成 user rpc 服务
创建 proto 文件
编写 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 项目根目录执行如下命令:
5、 编写 user rpc 服务
4.1 修改配置文件
修改 user.yaml 配置文件
修改服务监听地址,端口号为0.0.0.0:9000,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 |
4.2 添加 user model 依赖
添加 Mysql 服务配置,CacheRedis 服务配置的实例化
| $ vim rpc/internal/config/config.go |
| package config |
| |
| import ( |
| "github.com/zeromicro/go-zero/core/stores/cache" |
| "github.com/zeromicro/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/zeromicro/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), |
| } |
| } |
4.3 添加用户注册逻辑 Register
4.3.1 添加密码加密工具
在根目录 common 新建 crypt 工具库,此工具方法主要用于密码的加密处理。
| $ mkdir cryptx |
| $ cd cryptx |
| $ vim 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)) |
| } |
4.3.2 添加密码加密 Salt 配置
| Name: user.rpc |
| ListenOn: 0.0.0.0:9000 |
| |
| ... |
| |
| Salt: HWVOFkGgPTryzICwd7qnJaZR9KQ2i8xe |
| $ vim rpc/internal/config/config.go |
| package config |
| |
| import ( |
| "github.com/zeromicro/go-zero/core/stores/cache" |
| "github.com/zeromicro/go-zero/zrpc" |
| ) |
| |
| type Config struct { |
| ... |
| Salt string |
| } |
4.3.3 添加用户注册逻辑
用户注册流程,先判断注册手机号是否已经被注册,手机号未被注册,将用户信息写入数据库,用户密码需要进行加密存储。
| $ 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/zeromicro/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()) |
| } |
4.4 添加用户登录逻辑 Login
用户登录流程,通过手机号查询判断用户是否是注册用户,如果是注册用户,需要将用户输入的密码进行加密与数据库中用户加密密码进行对比验证。
| $ 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/zeromicro/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 |
| } |
4.5 添加用户信息逻辑 UserInfo
| $ 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/zeromicro/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 |
| } |
6、编写 user api 服务
6.1 修改配置文件
修改 user.yaml 配置文件
修改服务地址,端口号为0.0.0.0:8000,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 |
6.2 添加 user rpc 依赖
添加 user rpc 服务配置
| 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/zeromicro/go-zero/rest" |
| "github.com/zeromicro/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/zeromicro/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)), |
| } |
| } |
6.3 添加用户注册逻辑 Register
| $ 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/zeromicro/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 |
| } |
6.4 添加用户登录逻辑 Login
6.4.1添加 JWT 工具
在根目录 common 新建 jwtx 工具库,用于生成用户 token。
| 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)) |
| } |
6.4.2 添加用户登录逻辑
通过调用 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/zeromicro/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 |
| } |
6.5 添加用户信息逻辑 UserInfo
| $ 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/zeromicro/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 中自定义的参数
7、 启动 user rpc 服务
提示:启动服务需要在 golang 容器中启动
| $ cd mall/service/user/rpc |
| $ go run user.go -f etc/user.yaml |
可能会报下面的错
| internal/logic/loginlogic.go:6:3: package mall/common/cryptx imports golang.org/x/crypto/scrypt from implicitly required module; to add missing requirements, run: |
| go get golang.org/x/crypto@v0.0.0-20210920023735-84f357641f63 |

这是包没导全,进入 mall 目录 go mod tidy
一下,再重新
| $ go run user.go -f etc/user.yaml |
看到 Starting rpc server at 127.0.0.1:9000… 就是启动成功了

8、 启动 user api 服务
提示:启动服务需要在 golang 容器中启动,且前面的 rpc 服务不能关闭,不然会报错,如下所示:
| 2022/03/07 12:57:18 {"@timestamp":"2022-03-07T12:57:18.642+08:00","level":"error","content":"discovbuilder.go:33 bad resolver state"} |
| 2022/03/07 12:57:21 rpc dial: discov://etcd:2379/user.rpc, error: context deadline exceeded, make sure rpc service "user.rpc" is already started |
| exit status 1 |

陆续输入如下命令:
| $ cd mall/service/user/api |
| $ go run user.go -f etc/user.yaml |
看到 Starting server at 0.0.0.0:8000… 就是启动成功了

9、系列
学习笔记:带你十天轻松搞定 Go 微服务系列(一)- 环境搭建
学习笔记:带你十天轻松搞定 Go 微服务系列(二)- 服务拆分
学习笔记:带你十天轻松搞定 Go 微服务系列(三)- 用户服务
学习笔记:带你十天轻松搞定 Go 微服务系列(四)- 产品服务
学习笔记:带你十天轻松搞定 Go 微服务系列(五)- 订单服务
学习笔记:带你十天轻松搞定 Go 微服务系列(六)- 支付服务
学习笔记:带你十天轻松搞定 Go 微服务系列(七)- RPC 服务 Auth 验证
学习笔记:带你十天轻松搞定 Go 微服务系列(八)- 服务监控
学习笔记:带你十天轻松搞定 Go 微服务系列(九)- 链路追踪
学习笔记:带你十天轻松搞定 Go 微服务系列大结局(十)- 分布式事务
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步