0%

go-zero入门实践二

说明

  • 紧接上篇文章
  • 本篇文章开始接入mysql等具体的业务逻辑

项目结构

  • order 和 user 目录下分别新增model、api目录等

image-20230727101351298

单体服务

  • user为单体api服务,编写对外的增删改查接口
  • user/model目录下的sql脚本,自动生成model代码
1
2
3
4
5
6
7
8
9
10
CREATE TABLE `user` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`username` VARCHAR(50) NULL DEFAULT NULL COLLATE 'utf8_general_ci',
`password` TEXT NULL DEFAULT NULL COLLATE 'utf8_general_ci',
`gender` VARCHAR(50) NULL DEFAULT NULL COLLATE 'utf8_general_ci',
PRIMARY KEY (`id`) USING BTREE
)
COLLATE='utf8_general_ci'
ENGINE=InnoDB
;
  • 执行命令生成user下的model代码
1
E:\proj\gowork\study-gozero-demo\mall\user\model> goctl model mysql ddl -src user.sql -dir . -c 
  • 在user/model下面生成了对model中增删改查
1
2
3
4
5
6
7
E:\proj\gowork\study-gozero-demo\mall\user\model>ls
Mode LastWriteTime Length Name
---- ------------- ------ ----
-a---- 2023/7/27 10:05 338 user.sql
-a---- 2023/7/27 10:35 623 usermodel.go
-a---- 2023/7/27 10:35 3995 usermodel_gen.go
-a---- 2023/7/27 10:35 106 vars.
  • 发现usermodel_gen.go 中代码没有查询用户名和密码的函数,因此新增一个,同时新增一个查询所有用户的函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
type (
userModel interface {
Insert(ctx context.Context, data *User) (sql.Result, error)
FindOne(ctx context.Context, id int64) (*User, error)
// 新增的查询用户名函数
FindOneByUsername(ctx context.Context, username string) (*User, error)
// 查询所有的用户
FindAll(ctx context.Context, name string) ([]*User, error)
Update(ctx context.Context, data *User) error
Delete(ctx context.Context, id int64) error
}

.....
// 查询用户名,若存在则返回user的数据
func (m *defaultUserModel) FindOneByUsername(ctx context.Context, username string) (*User, error) {
userUsernameKey := fmt.Sprintf("%s%v", cacheUserIdPrefix, username)
var resp User
err := m.QueryRowIndexCtx(ctx, &resp, userUsernameKey, m.formatPrimary, func(ctx context.Context, conn sqlx.SqlConn, v interface{}) (i interface{}, e error) {
query := fmt.Sprintf("select %s from %s where `username` = ? limit 1", userRows, m.table)
if err := conn.QueryRowCtx(ctx, &resp, query, username); err != nil {
return nil, err
}
return resp.Id, nil
}, m.queryPrimary)
switch err {
case nil:
return &resp, nil
case sqlc.ErrNotFound:
return nil, ErrNotFound
default:
return nil, err
}
}

func (m *defaultUserModel) FindAll(ctx context.Context, username string) ([]*User, error) {
var resp []*User
var query string
var err error
if len(strings.TrimSpace(username)) > 0 {
query = fmt.Sprintf("select %s from %s where `username` = ?", userRows, m.table)
err = m.QueryRowsNoCacheCtx(ctx, &resp, query, username)

} else {
query = fmt.Sprintf("select %s from %s", userRows, m.table)
err = m.QueryRowsNoCacheCtx(ctx, &resp, query)
}
// fmt.Println("query=", query)
fmt.Println("query=", query, "error:", err)

switch err {
case nil:
return resp, nil
case sqlc.ErrNotFound:
return nil, ErrNotFound
default:
return nil, err
}
}

需要注意的是,moder_gen.go 文件是由 goctl 工具自动生成的,每次执行 goctl 命令时都会重新生成该文件。因此,在修改表结构或需要重新生成代码时,应该先备份自定义的代码,并注意保留所需的代码或修改

  • 修改user/api/user.api 内容
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
type (
User {
Id int64 `json:"id"`
Username string `json:"username"`
Gender string `json:"gender"`
Password string `json:"password"`
}

LoginReq {
Username string `json:"username"`
Password string `json:"password"`
}

LoginReply {
Id int64 `json:"id"`
Username string `json:"username"`
Gender string `json:"gender"`
// 下面三个为jwt鉴权需要用到
AccessToken string `json:"accessToken"`
AccessExpire int64 `json:"accessExpire"`
RefreshAfter int64 `json:"refreshAfter"`
}

UserAddReq {
Username string `json:"username"`
Password string `json:"password"`
Gender string `json:"gender"`

}
UserAddRes {
Id int64 `json:"id"`
Name string `json:"name"`
}
UserDelReq {
Id int64 `json:"id"`
}
UserDelRes {
Id int64 `json:"id"`
Username string `json:"username"`
}
UserEditReq {
Id int64 `json:"id"`
Username string `json:"username"`
Password string `json:"password"`
Gender string `json:"gender"`

}
UserEditRes {
Id int64 `json:"id"`
Username string `json:"username"`
}
UserQueryReq {
Username string `json:"username"`
}
UserQueryRes {
UserList []*User `json:"products"`
}
)

service user-api {
@handler login
post /user/login (LoginReq) returns (LoginReply)
@handler UserAdd
post /user/UserAdd (UserAddReq) returns (UserAddRes)
@handler UserDel
post /user/UserDel (UserDelReq) returns (UserDelRes)
@handler UserQuery
get /user/UserQuery (UserQueryReq) returns (UserQueryRes)
@handler UserEdit
post /user/UserEdit (UserEditReq) returns (UserEditRes)
}
  • 生成api服务
1
E:\proj\gowork\study-gozero-demo\mall\user\api> goctl api go -api user.api -dir . 

编写业务代码

  • 目录E:\proj\gowork\study-gozero-demo\mall\user\api\internal\config\config.go对添加mysql配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

package config

import (
"github.com/zeromicro/go-zero/core/stores/cache"
"github.com/zeromicro/go-zero/rest"
)

type Config struct {
rest.RestConf
// 添加mysql配置
Mysql struct {
DataSource string
}
// 使用缓存
CacheRedis cache.CacheConf
}

  • 修改E\proj\gowork\study-gozero-demo\mall\user\api\etc\user-api.yaml配置文件
1
2
3
4
5
6
7
8
9
Name: user-api
Host: 0.0.0.0
Port: 8888
Mysql:
DataSource: root:123456@tcp(127.0.0.1:3306)/go_zero?charset=utf8mb4&parseTime=true&loc=Asia%2FShanghai
CacheRedis:
- Host: 127.0.0.1:6379
Pass: ""
Type: node

本地环境你需要搭建好mysql和redis的环境

  • 完善依赖服务user/api/internal/svc/servicecontext.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package svc

import (
"study-gozero-demo/mall/user/api/internal/config"
"study-gozero-demo/mall/user/model"

"github.com/zeromicro/go-zero/core/stores/sqlx"
)

type ServiceContext struct {
Config config.Config
// 引入model层的增删改查
UserModel model.UserModel
}

func NewServiceContext(c config.Config) *ServiceContext {
// 初始化mysql连接
conn := sqlx.NewMysql(c.Mysql.DataSource)

return &ServiceContext{
Config: c,
// 调用user/model层代码
UserModel: model.NewUserModel(conn, c.CacheRedis),
}
}

  • 编写user/api/internal/logic 目录下的各接口信息

登录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
//loginlogic.go

func (l *LoginLogic) Login(req *types.LoginReq) (resp *types.LoginReply, err error) {
// todo: add your logic here and delete this line

if len(strings.TrimSpace(req.Username)) == 0 || len(strings.TrimSpace(req.Password)) == 0 {
return nil, errors.New("参数错误")
}

userInfo, err := l.svcCtx.UserModel.FindOneByUsername(l.ctx, req.Username)
switch err {
case nil:
case model.ErrNotFound:
return nil, errors.New("用户名不存在")
default:
return nil, err
}

if userInfo.Pwd != req.Password {
return nil, errors.New("用户密码不正确")
}
// 登录成功返回数据
return &types.LoginReply{
Id: userInfo.Id,
Username: userInfo.Username,
}, nil

}


新增用户

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// useraddlogic.go
func (l *UserAddLogic) UserAdd(req *types.UserAddReq) (resp *types.UserAddRes, err error)
if len(strings.TrimSpace(req.Username)) == 0 || len(strings.TrimSpace(req.Password)) == 0 {
return nil, errors.New("参数错误")
}

userInfo, err1 := l.svcCtx.UserModel.FindOneByUsername(l.ctx, req.Username)
fmt.Println("user_info", userInfo)
// 如果查到已存在用户名,则不能新增
if err1 == nil {
return nil, errors.New(req.Username + "用户已经存在")
}
// 组装结构体
var addReq model.User
errCopy := copier.Copy(&addReq, req)
if errCopy != nil {
return nil, errCopy
}

respd, err2 := l.svcCtx.UserModel.Insert(l.ctx, &addReq)
fmt.Println("------respd:", respd, "error:", err2)
if err2 != nil {
fmt.Println("insert into error:", err)
return nil, errors.New(err.Error())

}

// 登录成功返回数据
return &types.UserAddRes{
Name: req.Username,
}, nil
}

编辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// usereditlogic.go
func (l *UserEditLogic) UserEdit(req *types.UserEditReq) (resp *types.UserEditRes, err error) {

if len(strings.TrimSpace(req.Username)) == 0 || len(strings.TrimSpace(req.Password)) == 0 {
return nil, errors.New("参数错误")
}
if strings.TrimSpace(req.Username) == "admin" {
return nil, errors.New("此用户不许修改")

}

userInfo, _ := l.svcCtx.UserModel.FindOne(l.ctx, req.Id)
if userInfo == nil {
return nil, errors.New("用户不存在")
}
// 组装结构体
var editReq model.User
errCopy := copier.Copy(&editReq, req)
if errCopy != nil {
return nil, errCopy
}

err2 := l.svcCtx.UserModel.Update(l.ctx, &editReq)
if err2 != nil {
fmt.Println("update error:", err)
return nil, errors.New("更新错误")

}

// 返回数据
return &types.UserEditRes{
Username: req.Username,
Id: req.Id,
}, nil
}

  • 删除
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// userdellogic.go
func (l *UserDelLogic) UserDel(req *types.UserDelReq) (resp *types.UserDelRes, err error) {
// todo: add your logic here and delete this line

if req.Id == 23 {
return nil, errors.New("此用户不可删除")
}
userInfo, _ := l.svcCtx.UserModel.FindOne(l.ctx, req.Id)
if userInfo == nil {
return nil, errors.New("用户不存在")
}

err2 := l.svcCtx.UserModel.Delete(l.ctx, req.Id)
if err2 != nil {
fmt.Println("del error:", err)
return nil, errors.New("删除错误")

}

// 返回数据
return &types.UserDelRes{
Username: userInfo.Username.String,
Id: userInfo.Id,
}, nil
}
  • 查询
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func (l *UserQueryLogic) UserQuery(req *types.UserQueryReq) (resp *types.UserQueryRes, err error) {
// todo: add your logic here and delete this line

fmt.Println("username", req.Username)
res1, err := l.svcCtx.UserModel.FindAll(l.ctx, req.Username)
fmt.Println("res1:", res1, "err:", err)
if err != nil {
return nil, errors.New("查询错误")
}
var resList []*types.User
for _, u := range res1 {
var resVo types.User
_ = copier.Copy(&resVo, u)
resList = append(resList, &resVo)
}
return &types.UserQueryRes{UserList: resList}, nil

}

接口测试

  • 本地数据库中的user表,需要有如下密码

image-20230727151305396

  • 启动user/api的服务
1
E:\proj\gowork\study-gozero-demo\mall\user\api> go run .\user.go
  • python客户端进行测试
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import requests
data ={"username": "admin", "password": "123456"}
resp = requests.post("http://127.0.0.1:8888/user/login", json=data)
print(resp.text)

data ={"username": "admin1", "password": "1234561", "gender": "男"}
resp = requests.post("http://127.0.0.1:8888/user/UserAdd", json=data)
print(resp.text)

data ={"username": "admin551", "password": "12345776", "gender": "男1", "id": 24}
resp = requests.post("http://127.0.0.1:8888/user/UserEdit", json=data)
print(resp.text)


data ={"username": ""}
resp = requests.get("http://127.0.0.1:8888/user/UserQuery", json=data)
print(resp.text)


data ={"id": 23}
resp = requests.post("http://127.0.0.1:8888/user/UserDel", json=data)
print(resp.text)

业务鉴权jwt

  • jwt鉴权一般在api层使用,我们这次演示工程中分别在user api登录时生成jwt token,在order api查模拟新增时验证用户jwt token。

user api

  • 配置mall/user/api/internal/config/config.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package config

import (
"github.com/zeromicro/go-zero/core/stores/cache"
"github.com/zeromicro/go-zero/rest"
)

type Config struct {
rest.RestConf

// 添加mysql配置
Mysql struct {
DataSource string
}
// 使用缓存
CacheRedis cache.CacheConf
// jwt的配置字段
Auth struct {
AccessSecret string
AccessExpire int64
}
}
  • 配置/order/api/etc/api.yaml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
Name: user-api
Host: 0.0.0.0
Port: 8888
Mysql:
DataSource: root:123456@tcp(127.0.0.1:3306)/go_zero?charset=utf8mb4&parseTime=true&loc=Asia%2FShanghai
CacheRedis:
- Host: 127.0.0.1:6379
Pass: ""
Type: node
Auth:
# 生成jwt token的密钥 一般格式为udid
AccessSecret: 0c891c78-9415-ec96-deb8-3b658f9e57f3
# jwt token有效期,单位:秒,现在设置的为1小时
AccessExpire: 3600
  • 在user/api/internal/logic/loginlogic.go 中加上生成jwt内容的代码,并且在登录返回的数据返回jwt内容
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
func (l *LoginLogic) getJwtToken(secretKey string, iat, seconds, userId int64) (string, error) {
claims := make(jwt.MapClaims)
claims["exp"] = iat + seconds
claims["iat"] = iat
claims["userId"] = userId
token := jwt.New(jwt.SigningMethodHS256)
token.Claims = claims
return token.SignedString([]byte(secretKey))
}

func (l *LoginLogic) Login(req *types.LoginReq) (resp *types.LoginReply, err error) {
if len(strings.TrimSpace(req.Username)) == 0 || len(strings.TrimSpace(req.Password)) == 0 {
return nil, errors.New("参数错误")
}
userInfo, err := l.svcCtx.UserModel.FindOneByUsername(l.ctx, req.Username)
switch err {
case nil:
case model.ErrNotFound:
return nil, errors.New("用户名不存在")
default:
return nil, err
}
if userInfo.Password.String != req.Password {
return nil, errors.New("用户密码不正确")
}

// ---jwt-start---
now := time.Now().Unix()
accessExpire := l.svcCtx.Config.Auth.AccessExpire
jwtToken, err := l.getJwtToken(l.svcCtx.Config.Auth.AccessSecret, now, l.svcCtx.Config.Auth.AccessExpire, userInfo.Id)
if err != nil {
return nil, err
}
// ---jwt-end---

// 登录成功返回数据
return &types.LoginReply{
Id: userInfo.Id,
Username: userInfo.Username.String,
AccessToken: jwtToken,
AccessExpire: now + accessExpire,
RefreshAfter: now + accessExpire/2,
}, nil
}

order api

  • 使用jwt鉴权校验

  • 我们把之前的order/api目录内容清空

  • 编写mall/order/api/order.api

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
type (
OrderReq {
Id string `path:"id"`
}

OrderReply {
Id string `json:"id"`
ProductName string `json:"productname"`
}

OrderAddReq {
ProductName string `json:"productname"`
Price string `json:"price"`
Unit string `json:"unit"`
}

OrderAddRes {
Code int `json:"code"`
Messsage string `json:"message"`
}
)

// 在哪个服务头上加这个验证,就是需要jwt鉴权
@server(
jwt: Auth
)
service order {
@handler orderAdd
post /api/order/OrderAdd (OrderAddReq) returns (OrderAddRes)
}

service order {
@handler getOrder
get /api/order/get/:id (OrderReq) returns (OrderReply)
}
// 在哪个服务头上加这个验证,就是需要jwt鉴权
@server(
jwt: Auth
)
service order {
@handler orderAdd
post /api/order/OrderAdd (OrderAddReq) returns (OrderAddRes)
}

service order {
@handler getOrder
get /api/order/get/:id (OrderReq) returns (OrderReply)
}
  • 生成order.api代码
1
PS E:\proj\gowork\study-gozero-demo\mall\order\api>  goctl api go -api order.api -dir . 
  • 分别编写mall\order\api\internal\logic 下的测试代码

getorderlogic.go

1
2
3
4
5
6
7
8
func (l *GetOrderLogic) GetOrder(req *types.OrderReq) (resp *types.OrderReply, err error) {
fmt.Println("id=", req.Id)

return &types.OrderReply{
Id: req.Id,
ProductName: "test order",
}, nil
}

orderaddlogic.go

1
2
3
4
5
6
7
func (l *OrderAddLogic) OrderAdd(req *types.OrderAddReq) (resp *types.OrderAddRes, err error) {
fmt.Println("req=", req)
return &types.OrderAddRes{
Code: 1,
Messsage: "success",
}, nil
}
  • order/api/etc/order.yaml 端口修改和加上jwt的token
1
2
3
4
5
6
7
8
Name: order
Host: 0.0.0.0
Port: 8889
Auth:
# 生成jwt token的密钥 一般格式为udid
AccessSecret: 0c891c78-9415-ec96-deb8-3b658f9e57f3
# jwt token有效期,单位:秒,现在设置的为1小时
AccessExpire: 3600
  • 分别启动order/api/order.gouser/api/user.go

测试

1
2
3
4
5
6
7
8
9
resp = requests.get("http://127.0.0.1:8889/api/order/get/2")
print(resp.text)

data ={"productname": "西瓜", "price": "122", "unit": "个"}
resp = requests.post("http://127.0.0.1:8889/api/order/OrderAdd", json=data)
print(resp)

{"id":"2","productname":"test order"}
<Response [401]>
  • 发现OrderAdd 直接返回401

  • 我们把token加到头部

1
2
3
4
5
6
7
8
data ={"username": "admin", "password": "123456"}
resp = requests.post("http://127.0.0.1:8888/user/login", json=data)
data = json.loads(resp.text)
token = data["accessToken"]
resp = requests.post("http://127.0.0.1:8889/api/order/OrderAdd", headers=header, json=data1)
print(resp.text)

{"code":1,"message":"success"}

总结

  • 本次主要用单机服务实现了user api层结合mysql的增删改查,同时在order层加上了jwt的校验
  • 由于登录的数据缓存到了redis中,进行修改和删除时,可能出现删除不掉,因此可以在redis中删除可以
  • 下篇文章将使用为微服务,mysql,jwt等结合起来