我们通过一个系列文章跟大家详细展示一个 go-zero 微服务实例,整个系列分十三篇文章,目录结构如下:
- go-zero 实战 - 服务划分与项目创建
- go-zero 实战 - User API Gateway
- go-zero 实战 - User Login
- go-zero 实战 - User Register
- go-zero 实战 - User Userinfo
- go-zero 实战 - Food API Gateway
- go-zero 实战 - Food Search
- go-zero 实战 - Food AddFood
- go-zero 实战 - Food DeleteFood
- go-zero 实战 - Food Foodlist
- go-zero 实战进阶 - rpc 服务
- go-zero 实战进阶 - 用户管理 rpc 服务
- go-zero 实战进阶 - 食材管理 rpc 服务
期望通过本系列文章带你在本地利用 go-zero 快速开发一个《食谱指南》系统,让你快速上手微服务。
在上一篇 《go-zero 实战 - User API Gateway》 中,我们通过定义 user.api 文件中的内容对 API 类型进行了声明,并通过 goctl api 命令,一键快速生成一个 api 服务,如果仅仅是启动一个 go-zero 的 api 演示项目,你甚至都不用编码,就可以完成一个 api 服务开发及正常运行。
本篇将在上一篇的基础上完成以下工作内容:
- 创建本地数据库表
user,并定义表结构,以便存储用户基本信息; - 使用 goctl model 自动生成 mysql CRUD 代码;
- 添加用户登录接口的逻辑。
创建本地数据库表 user,并定义表结构
在本机 mysql 中创建 foodguides 数据库,并新建 user 表。
CREATE TABLE `user` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '用户Id',
`name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '用户名称',
`password` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '用户密码',
`email` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci 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 `name_index` (`name`),
UNIQUE KEY `email_index` (`email`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
执行如下 SQL 语句新增一条数据:
INSERT INTO `foodguides`.`user`(`id`, `name`, `password`, `email`) VALUES (1, 'RenPanPan', '3a1b7b6dba4af2887d91d2dab0090e9f7c8a4da5d6b17883ff346e026ec27213', 'renpanpan1990@163.com');
使用 goctl model 生成 mysql CRUD 代码
在 model 下新建 user.sql 文件,将上述创建 user 表的 SQL 语句复制到 user.sql 文件中。执行命令 goctl model 命令生成 CRUD 代码。
$ cd usermanage/model
$ goctl model mysql ddl --src user.sql --dir .
Done.
当你看到 Done. 输出则代表生成成功了,接下来我们来看一下生成的代码内容:
➜ model:
# 列出当前目录下的文件
$ ls
user.sql usermodel.go usermodel_gen.go vars.go
# 查看目录树
$ tree
.
├── user.sql
├── usermodel.go // CRUD 代码
├── usermodel_gen.go
└── vars.go // 定义常量和变量
编写 user login 服务
修改 user-api.yaml 配置文件
编辑 usermanage/api/etc 下的 user-api.yaml 文件,修改服务地址,端口号为 0.0.0.0:8888,Mysql 服务配置,Auth 验证配置。
Name: user-api
Host: 0.0.0.0
Port: 8888
Mysql:
DataSource: root:123456@tcp(127.0.0.1:9528)/foodguides?charset=utf8mb4&parseTime=True&loc=Local
Auth:
AccessSecret: ad879037-d3fd-tghj-112d-6bfc35d54b7d
AccessExpire: 86400
Salt: HWVOFkGgPTryzICwd7qnJaZR9KQ2i8xe
Tips
添加服务配置实例化
$ vim usermanage/api/internal/config/config.go
package config
import "github.com/zeromicro/go-zero/rest"
type Config struct {
rest.RestConf
MySql struct {
DataSource string
}
Auth struct {
AccessSecret string
AccessExpire int64
}
Salt string
}
注册服务上下文依赖
$ vim usermanage/api/internal/svc/serviceContext.go
package svc
import (
"FoodGuides/service/usermanage/api/internal/config"
"FoodGuides/service/usermanage/model"
"github.com/zeromicro/go-zero/core/stores/sqlx"
)
type ServiceContext struct {
Config config.Config
UserModel model.UserModel
}
func NewServiceContext(c config.Config) *ServiceContext {
return &ServiceContext{
Config: c,
UserModel: model.NewUserModel(sqlx.NewMysql(c.MySql.DataSource)),
}
}
添加用户登录逻辑 Login
添加 JWT 工具
在根目录 common 下新建 jwtx 工具库,用于生成用户 token。
$ vim common/jwtx/jwt.go
package jwtx
import "github.com/golang-jwt/jwt/v4"
// GetJwtToken
// @secretKey: JWT 加解密密钥
// @iat: 时间戳
// @seconds: 过期时间,单位秒
// @uid: 用户 ID
func GetJwtToken(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))
}
GetJwtToken 方法的实现可参考 官网示例。
添加密码加密工具
在根目录 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))
}
添加用户登录逻辑
用户登录流程:过邮箱查询判断用户是否是注册用户,如果是注册用户,需要将用户输入的密码进行加密与数据库中用户加密密码进行对比验证。当两者相等时,即认为用户登录成功,我们将使用用户信息生成对应的 token 以及 token 的有效期。
$ vim usermanage/api/internal/logic/loginlogic.go
package logic
import (
"FoodGuides/common/cryptx"
"FoodGuides/common/jwtx"
"FoodGuides/service/usermanage/model"
"context"
"errors"
"time"
"FoodGuides/service/usermanage/api/internal/svc"
"FoodGuides/service/usermanage/api/internal/types"
"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) (*types.LoginResponse, error) {
// 查询用户是否存在
res, err := l.svcCtx.UserModel.FindOneByEmail(l.ctx, req.Email)
if err != nil {
if err == model.ErrNotFound {
return nil, errors.New("用户不存在")
}
return nil, err
}
// 判断密码是否正确
password := cryptx.PasswordEncrypt(l.svcCtx.Config.Salt, req.Password)
if password != res.Password {
return nil, errors.New("密码错误")
}
now := time.Now().Unix()
accessExpire := l.svcCtx.Config.Auth.AccessExpire
jwtToken, err := jwtx.GetJwtToken(l.svcCtx.Config.Auth.AccessSecret, now, accessExpire, res.Id)
if err != nil {
return nil, err
}
token := types.JwtToken{
AccessToken: jwtToken,
AccessExpire: now + accessExpire,
RefreshAfter: now + accessExpire/2,
}
response := types.UserReply{
Id: res.Id,
Username: res.Name,
Email: res.Email,
JwtToken: token,
}
return &types.LoginResponse{UserReply: response}, nil
}
启动服务
运行如下命令以启动 user api 服务, 运行成功后,user api 则运行在本机的 8888 端口:
➜ service:
$ go run usermanage/api/user.go -f usermanage/api/etc/user-api.yaml
Starting server at 0.0.0.0:8888...
我们用 Postman 尝试请求 /users/login 接口:
- 在
Postman的Body选项中选择raw,并设置JSON内容为{"email": "renpanpan1990@163.com","password": "809161"}。 - 点击发送请求按钮,有如下截图的响应说明接口运行正常。
修改 Response 返回格式
我希望客户端接口请求数据的返回格式是这样子的:
{
"code": 1,
"msg": "登录成功",
"data": {
"id": 1,
"username": "RenPanPan",
"email": "renpanpan1990@163.com",
"accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2OTc2OTk3NDQsImlhdCI6MTY5NzYxMzM0NCwidWlkIjoxfQ.KF7woznVmjuPz85o1vYXnNTRvvnlZXASn7N9kWRL0nA",
"accessExpire": 1697699744,
"refreshAfter": 1697656544
}
}
目前如果需要实现这种格式响应,有 2 种做法:
- 自定义响应格式
- 使用
go-zero扩展包来实现
我们用做法 1 来演示一下,做法 2 可参考官方文档的演示实例 HTTP 扩展。
- 在根目录
common下新建response工具库,用来统一请求响应返回格式
$ vim common/responsex/response.go
package responsex
type HttpResponse struct {
Code int `json:"code"`
Message string `json:"msg"`
Data interface{} `json:"data"`
}
func SuccessResponse(resData interface{}, message string) HttpResponse {
return HttpResponse{Code: 1, Message: message, Data: resData}
}
func FailureResponse(resData interface{}, message string, code int) HttpResponse {
return HttpResponse{Code: code, Message: message, Data: resData}
}
- 修改
loginhandler.go文件
$ vim usermanage/api/internal/handler/loginhandler.go
package handler
import (
"FoodGuides/common/responsex"
"net/http"
"FoodGuides/service/usermanage/api/internal/logic"
"FoodGuides/service/usermanage/api/internal/svc"
"FoodGuides/service/usermanage/api/internal/types"
"github.com/zeromicro/go-zero/rest/httpx"
)
func LoginHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req types.LoginRequest
if err := httpx.Parse(r, &req); err != nil {
// httpx.ErrorCtx(r.Context(), w, err)
httpx.OkJson(w, responsex.FailureResponse(nil, err.Error(), 1000))
return
}
l := logic.NewLoginLogic(r.Context(), svcCtx)
resp, err := l.Login(&req)
if err != nil {
// httpx.ErrorCtx(r.Context(), w, err)
httpx.OkJson(w, responsex.FailureResponse(nil, err.Error(), 1000))
} else {
// httpx.OkJsonCtx(r.Context(), w, resp)
httpx.OkJson(w, responsex.SuccessResponse(resp, "登录成功"))
}
}
}
- 重启服务,并发起
/users/login请求
发现请求响应已变成我们想要的格式。至此,我们就完成了用户登录接口的搭建,下一篇我们将采用相同的方法搭建用户注册接口。