本章节内容均在 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 写的是真难受