本章节内容均在
user
目录下完成
准备模块框架
生成 api 代码
在 cmd/api
目录下新建一个 desc
目录编写 api 文件
syntax = "v1"
info(
title: "User API"
desc: "API for user"
author: "GuoChenxu"
email: "2269409349@qq.com"
version: "1.0"
)
// register
type (
RegisterReq {
Username string `form:"user_name"`
Password string `form:"password"`
}
RegisterResp {
Status int `json:"status"`
Data string `json:"data"`
Message string `json:"msg"`
Error string `json:"error"`
}
)
// login
type (
User {
Id int64 `json:"id"`
Username string `json:"user_name"`
CreateAt int64 `json:"create_at"`
}
Data {
User User `json:"user"`
Token string `json:"token"`
}
LoginReq {
Username string `form:"user_name"`
Password string `form:"password"`
}
LoginResp {
Status int `json:"status"`
Data Data `json:"data"`
Message string `json:"msg"`
Error string `json:"error"`
}
)
// api
@server(
prefix: api/v1/user
)
service user {
@doc "register"
@handler register
post /register (RegisterReq) returns (RegisterResp)
@doc "login"
@handler login
post /login (LoginReq) returns (LoginResp)
}
然后在 api
目录下生成代码文件
goctl api go --api .\desc\user.api --dir .\ --style=go_zero
生成 rpc 代码
同样地, 在 cmd/rpc
目录下新建 desc
目录, 编写 proto 文件, 这里就不分开写了
syntax = "proto3";
package pb;
option go_package = "./pb";
// 定义消息类型
message RegisterReq {
string Username = 1;
string Password = 2;
}
message RegisterResp {
int32 Status = 1;
string Data = 2;
string Message = 3;
string Error = 4;
}
message LoginReq {
string Username = 1;
string Password = 2;
}
message User {
int64 Id = 1;
string Username = 2;
int64 create_at = 3;
}
message Data {
User User = 1;
string Token = 2;
}
message LoginResp {
int32 Status = 1;
Data Data = 2;
string Message = 3;
string Error = 4;
}
message GenerateTokenReq {
int64 userId = 1;
}
message GenerateTokenResp {
string accessToken = 1;
int64 accessExpire = 2;
int64 refreshAfter = 3;
}
// 定义服务
service userrpc {
rpc Register(RegisterReq) returns(RegisterResp);
rpc Login(LoginReq) returns(LoginResp);
rpc GenerateToken(GenerateTokenReq) returns(GenerateTokenResp);
}
然后在 rpc
目录下生成代码
goctl rpc protoc .\desc\user.proto --go_out=.\ --go-grpc_out=.\ --zrpc_out=.\ --style=go_zero
生成 model 代码
在 model
目录下我们根据数据库中的 user
表生成代码文件, 这里我们使用一个 gorm 和 gozero 整合的库
, 同时我们也需要使用他的模板文件来生成代码
# --home 指定模板文件在本地的位置 (远程使用 --remote), 不填是使用 gozero 默认的模板
# --cache 是否启用缓存, 不填默认不启用
goctl model mysql datasource -url="root:101325@tcp(127.0.0.1:3306)/gtodolist" -table="user" --dir="./" --home="../template/gorm-gozero/1.4.2" --style=go_zero --cache=true
目前 model
目录有如下文件
user.sql
user_model.go
user_model_gen.go
vars.go
到目前为止我们 user
模块的代码就全部生成好了
修改配置文件
接下来我们要修改项目的配置文件
api
修改 etc/user.yaml
配置文件
Name: user
Host: 0.0.0.0
Port: 22301
Log:
Encoding: plain
UserRpcConfig:
Etcd:
Hosts:
- 127.0.0.1:2379
Key: user.rpc
# jwt验证
JwtAuth:
AccessSecret: gtodolist
AccessExpire: 31536000
internal/config/config.go
type Config struct {
rest.RestConf
JwtAuth struct {
AccessSecret string
AccessExpire int64
}
UserRpcConfig zrpc.RpcClientConf
}
internal/svc/service_context.go
type ServiceContext struct {
Config config.Config
UserRpcClient userrpc.Userrpc
}
func NewServiceContext(c config.Config) *ServiceContext {
return &ServiceContext{
Config: c,
UserRpcClient: userrpc.NewUserrpc(zrpc.MustNewClient(c.UserRpcConfig)),
}
}
rpc
同样是 etc/user.yaml
文件
Name: user.rpc
ListenOn: 0.0.0.0:22351
Etcd:
Hosts:
- 0.0.0.0:2379
Key: user.rpc
# 日志
Log:
Encoding: plain
# jwt验证
JwtAuth:
AccessSecret: gtodolist
AccessExpire: 31536000
# mysql
Mysql:
Path: 127.0.0.1
Port: 3306
Dbname: gtodolist
Username: root
Password: "101325"
MaxIdleConns: 10
MaxOpenConns: 10
LogZap: false
Config: parseTime=True&loc=Local
Cache:
- Host: 127.0.0.1:6379
Pass: "101325"
Redis:
Host: 127.0.0.1:6379
Pass: "101325"
Type: node
Key: user.rpc
internal/config/config.go
type Config struct {
zrpc.RpcServerConf
JwtAuth struct {
AccessSecret string
AccessExpire int64
}
Mysql gormc.Mysql
Cache cache.CacheConf
}
internal/svc/service_context.go
type ServiceContext struct {
Config config.Config
UserModel model.UserModel
}
func NewServiceContext(c config.Config) *ServiceContext {
db, err := gormc.ConnectMysql(c.Mysql)
if err != nil {
log.Fatal(err)
}
return &ServiceContext{
Config: c,
UserModel: model.NewUserModel(db, c.Cache),
}
}
编写核心逻辑
注意, 如果服务端返回的 err 不为空的话, 那第一个参数直接返回 nil
api
因为主要操作数据库的部分都在 rpc 中完成, 所以 api 中的逻辑比较简单, 主要就是调用 rpc 中的方法
register
internal\logic\register_logic.go
func (l *RegisterLogic) Register(req *types.RegisterReq) (resp *types.RegisterResp, err error) {
// 向 rpc 发送请求
registerResp, err := l.svcCtx.UserRpcClient.Register(l.ctx, &pb.RegisterReq{
Username: req.Username,
Password: req.Password,
})
// 出现错误
if err != nil {
return &types.RegisterResp{
Status: int(vo.ErrRequestParamError.GetErrCode()),
Data: vo.ErrRequestParamError.GetErrMsg(),
Message: err.Error(),
Error: err.Error(),
}, nil
}
resp = &types.RegisterResp{}
_ = copier.Copy(resp, registerResp)
return resp, err
}
login
internal\logic\login_logic.go
func (l *LoginLogic) Login(req *types.LoginReq) (resp *types.LoginResp, err error) {
loginResp, err := l.svcCtx.UserRpcClient.Login(l.ctx, &pb.LoginReq{
Username: req.Username,
Password: req.Password,
})
if err != nil {
return &types.LoginResp{
Status: int(vo.ErrRequestParamError.GetErrCode()),
Message: err.Error(),
Error: err.Error(),
}, nil
}
resp = &types.LoginResp{}
_ = copier.Copy(resp, loginResp)
return resp, err
}
rpc
register
internal\logic\register_logic
执行流程: 检查是否存在同名用户 (捕获错误) -> 创建用户 (捕获错误) -> 返回结果
func (l *RegisterLogic) Register(in *pb.RegisterReq) (*pb.RegisterResp, error) {
// 根据用户名查询用户是否存在
user, err := l.svcCtx.UserModel.FindOneByUsername(l.ctx, sql.NullString{
String: in.Username,
Valid: true,
})
// 查询出错
if err != nil && err != model.ErrNotFound {
return nil, errors.Wrap(vo.ErrDBerror, "数据库查询出错")
}
// 用户名已存在
if user != nil {
return nil, errors.Wrap(vo.ErrUserAlreadyRegisterError, "用户名已存在")
}
// 添加用户
username := sql.NullString{
String: in.Username,
Valid: true,
}
bp, _ := tool.BcryptByString(in.Password)
passwordDigest := sql.NullString{
String: bp,
Valid: true,
}
user = &model.User{
Username: username,
PasswordDigest: passwordDigest,
}
err = l.svcCtx.UserModel.Insert(l.ctx, nil, user)
// 注册失败
if err != nil {
return nil, errors.Wrap(vo.ErrDBerror, "数据库插入出错")
}
return &pb.RegisterResp{
Status: vo.OK,
Data: vo.SUCCESS,
Message: vo.SUCCESS,
Error: "",
}, nil
}
token
internal\logic\generate_token_logic
登录时我们需要返回一个 token, 所以我们在编写登录代码前先写一个生成 token 的服务
这个是通用的函数, 直接复制即可
func (l *GenerateTokenLogic) GenerateToken(in *pb.GenerateTokenReq) (*pb.GenerateTokenResp, error) {
now := time.Now().Unix()
accessExpire := l.svcCtx.Config.JwtAuth.AccessExpire
accessToken, err := l.getJwtToken(l.svcCtx.Config.JwtAuth.AccessSecret, now, accessExpire, in.UserId)
// 生成 token 失败
if err != nil {
return nil, errors.Wrapf(vo.ErrGenerateTokenError, "getJwtToken err userId:%d , err:%v", in.UserId, err)
}
return &pb.GenerateTokenResp{
AccessToken: accessToken,
AccessExpire: now + accessExpire,
RefreshAfter: now + accessExpire/2,
}, nil
}
func (l *GenerateTokenLogic) getJwtToken(secretKey string, iat, seconds, userId int64) (string, error) {
claims := make(jwt.MapClaims)
claims["exp"] = iat + seconds
claims["iat"] = iat
claims[ctxdata.CtxKeyJwtUserId] = userId
token := jwt.New(jwt.SigningMethodHS256)
token.Claims = claims
return token.SignedString([]byte(secretKey))
}
login
internal\logic\login_logic
执行流程: 查询用户看是否存在 (捕获错误) -> 匹配密码 -> 生成 token -> 返回结果
func (l *LoginLogic) Login(in *pb.LoginReq) (*pb.LoginResp, error) {
// 查询用户是否存在且密码正确
user, err := l.svcCtx.UserModel.FindOneByUsername(l.ctx, sql.NullString{
String: in.Username,
Valid: true,
})
if err != nil && err != model.ErrNotFound {
return nil, errors.Wrap(vo.ErrDBerror, "数据库查询出错")
}
if user == nil {
return nil, errors.Wrap(vo.ErrUserNoExistsError, "用户不存在")
}
if !tool.CheckPasswordHash(in.Password, user.PasswordDigest.String) {
return nil, errors.Wrap(vo.ErrUsernamePwdError, "密码匹配出错")
}
// 生成 token
genToken := NewGenerateTokenLogic(l.ctx, l.svcCtx)
tokenResp, err := genToken.GenerateToken(&userrpc.GenerateTokenReq{
UserId: user.Id,
})
if err != nil {
return nil, errors.Wrap(vo.ErrGenerateTokenError, "生成 token 失败")
}
return &pb.LoginResp{
Status: vo.OK,
Data: &pb.Data{
User: &pb.User{
Id: user.Id,
Username: user.Username.String,
CreateAt: user.CreatedAt.Unix(),
},
Token: tokenResp.AccessToken,
},
Message: vo.SUCCESS,
Error: "",
}, nil
}
用户的注册登录到这就基本结束了 不得不说, 这前端的 api 写的是真难受