go-zero使用gorm-gen实现了基本的单体服务的增删改查

2,215 阅读9分钟

github地址

目录

目的
数据库表设计
使用gorm gen
使用gorm gen测试
修改项目的api文件等配置
jwt用户登录
错误处理
创建用户
异常处理

目的

完成基于go-zero单体服务

尽量展示开发过程遇到的问题,并解决,不会为了排版就把问题提前,什么时候遇到什么时候展示解决。

数据库表设计

  1. 基于GoDockerDev启动mysql,redis
// 创建compose目录
mkdir dev_compose

// 下载GoDockerDev
git clone https://github.com/timzzx/GoDockerDev.git

cd GoDockerDev/

// 启动docker-compose
docker-compose up -d

// 显示
root@tdev:/home/code/dev_compose/GoDockerDev# docker-compose up -d
[+] Running 2/2
 ⠿ Container godockerdev-redis-1  Started                                                    1.1s
 ⠿ Container godockerdev-mysql-1  Started                                                    1.0s

创建bk数据库并且创建user表

CREATE TABLE `user` (
  `id` bigint(20) unsigned zerofill NOT NULL AUTO_INCREMENT COMMENT '用户表主键',
  `name` varchar(128) NOT NULL COMMENT '用户名',
  `password` varchar(64) NOT NULL COMMENT '密码',
  `status` int(11) NOT NULL DEFAULT '1' COMMENT '是否有效1.有效 2.无效',
  `ctime` int(11) NOT NULL DEFAULT '0' COMMENT '创建时间',
  `utime` int(11) DEFAULT NULL COMMENT '更新时间',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表'

使用gorm gen

安装生成

// 安装Gen Tool
go install gorm.io/gen/tools/gentool@latest

// 报错了
cgo: C compiler "gcc" not found: exec: "gcc": executable file not found in $PATH

// 安装Ubuntu开发包(省事)
sudo apt install build-essential

// 再次安装Gen Tool
go install gorm.io/gen/tools/gentool@latest

cd tapi/
mkdir bkmodel

// 生成gen
gentool -dsn "root:123456@tcp(192.168.1.13:3306)/bk?charset=utf8mb4&parseTime=True&loc=Local" -outPath "./bkmodel/dao/query"

// 更新依赖
go mod tidy

注意 -outPath "./bkmodel/dao/query" 能改变的只能是bkmodel这个目录,后面的是固定的。

测试表增加一个字段,增加一张表,重新执行命令后新的表和字段都会生成。bkmodel下所有文件都不要修改直接使用就好

创建makefile

# 命令
gen:
	gentool -dsn "root:123456@tcp(192.168.1.13:3306)/bk?charset=utf8mb4&parseTime=True&loc=Local" -outPath "./bkmodel/dao/query"

make gen运行展示

root@tdev:/home/code/tapi# make gen
gentool -dsn "root:123456@tcp(192.168.1.13:3306)/bk?charset=utf8mb4&parseTime=True&loc=Local" -outPath "./bkmodel/dao/query"
2023/02/11 07:19:53 got 6 columns from table <user>
2023/02/11 07:19:53 Start generating code.
2023/02/11 07:19:53 generate model file(table <user> -> {model.User}): /home/code/tapi/bkmodel/dao/model/user.gen.go
2023/02/11 07:19:53 generate query file: /home/code/tapi/bkmodel/dao/query/user.gen.go
2023/02/11 07:19:53 generate query file: /home/code/tapi/bkmodel/dao/query/gen.go
2023/02/11 07:19:53 Generate code done.

时区有问题改一下

sudo timedatectl set-timezone Asia/Shanghai

// 检查
root@tdev:/home/code/tapi# sudo timedatectl set-timezone Asia/Shanghai
root@tdev:/home/code/tapi# timedatectl
               Local time: Sat 2023-02-11 15:31:07 CST
           Universal time: Sat 2023-02-11 07:31:07 UTC
                 RTC time: Sat 2023-02-11 07:31:07
                Time zone: Asia/Shanghai (CST, +0800)
System clock synchronized: yes
              NTP service: active
          RTC in local TZ: no

go-zero引入gorm gen

说明一下,go-zero的orm封装的比较简单,虽然带cahce的封装,不过这个功能对于后台来说不需要,反而后台涉及统计sql比较复杂,所以改用gorm。

增加make命令,编辑makefile

gen_api:
	goctl api go -api project.api -dir ./
dev:
	go run user.go -f etc/user.yaml

运行项目看看

go run user.go -f etc/user.yaml
Starting server at 0.0.0.0:8888...

增加Mysql配置,修改etc/user.yaml

Name: User
Host: 0.0.0.0
Port: 8888

Mysql:
  DataSource: root:123456@tcp(192.168.1.13:3306)/bk?charset=utf8mb4&parseTime=true&loc=Asia%2FShanghai

修改tapi/internal/config/config.go

package config

import "github.com/zeromicro/go-zero/rest"

type Config struct {
	rest.RestConf

	Mysql struct {
		DataSource string
	}
}

修改tapi/internal/svc/servicecontext.go

package svc

import (
	"tapi/bkmodel/dao/query"
	"tapi/internal/config"

	"gorm.io/driver/mysql"
	"gorm.io/gorm"
)

type ServiceContext struct {
	Config config.Config

	BkModel *query.Query
}

func NewServiceContext(c config.Config) *ServiceContext {
	db, _ := gorm.Open(mysql.Open(c.Mysql.DataSource), &gorm.Config{})
	return &ServiceContext{
		Config:  c,
		BkModel: query.Use(db),
	}
}

修改tapi/internal/logic/loginlogic.go

package logic

import (
	"context"

	"tapi/internal/svc"
	"tapi/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) (resp *types.LoginResponse, err error) {
	// todo: add your logic here and delete this line
	table := l.svcCtx.BkModel.User
	user, err := table.WithContext(l.ctx).Where(table.Name.Eq(req.Name)).Debug().First()

	if err != nil {

		return &types.LoginResponse{
			Code: 500,
			Msg:  err.Error(),
		}, nil
	}

	return &types.LoginResponse{
		Code: 200,
		Msg:  user.Name,
	}, nil
}

go-zero引入gorm gen测试

插入一条数据

INSERT INTO `user` VALUES ('1', 'tim', '123456', '1', '0', '0');

运行项目

make dev

// 返回
go run user.go -f etc/user.yaml
Starting server at 0.0.0.0:8888...

打开postman访问

1.png name 发送tim11 是个错误的,所以返回没有记录。

2.png 正确返回

debug选项要注意一下

// debug在这里,去掉就不会输出sql语句注意一下
user, err := table.WithContext(l.ctx).Where(table.Name.Eq(req.Name)).Debug().First()

// console 输出正确的信息
2023/02/11 17:15:07 /home/code/tapi/bkmodel/dao/query/user.gen.go:234
[0.784ms] [rows:1] SELECT * FROM `user` WHERE `user`.`name` = 'tim' ORDER BY `user`.`id` LIMIT 1

// console 输出错误的信息
2023/02/11 17:11:32 /home/code/tapi/bkmodel/dao/query/user.gen.go:234 record not found
[0.908ms] [rows:0] SELECT * FROM `user` WHERE `user`.`name` = 'tim11' ORDER BY `user`.`id` LIMIT 1

修改项目的api文件等配置

tapi项目的启动go文件为user.go,不是很合理。所以处理一下。

修改project.api

type (
	LoginRequest {
		Name     string `form:"name"`
		Password string `form:"password"`
	}
	LoginResponse {
		Code int64  `json:"code"`
		Msg  string `json:"msg"`
	}
)

service Backend {
	@handler Login
	post /api/user/login(LoginRequest) returns (LoginResponse)
}

修改makefile

# 命令
help:
	@echo 'Usage:'
	@echo '     db 生成sql执行代码'
	@echo '     api 根据api文件生成go-zero api代码'
	@echo '     dev 运行'
db:
	gentool -dsn 'root:123456@tcp(192.168.1.13:3306)/bk?charset=utf8mb4&parseTime=True&loc=Local' -outPath './bkmodel/dao/query'
api:
	goctl api go -api project.api -dir ./ -style gozero
dev:
	go run backend.go -f etc/backend.yaml

删除

user.go

/etc/user.api

internal/handle/下所有的

internal/logic/所有

执行 make api

重新生成好后 目前项目目录

root@tdev:/home/code/tapi# tree
.
├── backend.go
├── bkmodel
│   └── dao
│       ├── model
│       │   └── user.gen.go
│       └── query
│           ├── gen.go
│           └── user.gen.go
├── etc
│   ├── backend.yaml
├── go.mod
├── go.sum
├── internal
│   ├── config
│   │   └── config.go
│   ├── handler
│   │   ├── loginhandler.go
│   │   └── routes.go
│   ├── logic
│   │   └── loginlogic.go
│   ├── svc
│   │   └── servicecontext.go
│   └── types
│       └── types.go
├── makefile
└── project.api

11 directories, 16 files

jwt用户登录

修改etc/backend.yaml

Name: Backend
Host: 0.0.0.0
Port: 8888

Mysql:
  DataSource: root:123456@tcp(192.168.1.13:3306)/bk?charset=utf8mb4&parseTime=true&loc=Asia%2FShanghai

# 增加jwt参数
Auth: 
  AccessSecret: uOvKLmVfztaXGpNYd4Z0I1SiT7MweJhl
  AccessExpire: 86400

修改config/config.go

package config

import "github.com/zeromicro/go-zero/rest"

type Config struct {
	rest.RestConf
    // 增加jwt验证
	Auth struct {
		AccessSecret string
		AccessExpire int64
	}

	Mysql struct {
		DataSource string
	}
}

修改 backend.api

syntax = "v1"

info(
	title: "tapi"
	desc: "接口"
	author: "tim"
	version: 1.0
)

type (
	LoginRequest {
		Name     string `form:"name"`
		Password string `form:"password"`
	}
	LoginResponse {
		Code  int64  `json:"code"`
		Msg   string `json:"msg"`
		Token string `json:"token,optional"`
	}

	UserInfo {
		Id    int64  `json:"id"`
		Name  string `json:"name"`
		Ctime int64  `json:"ctime"`
		Utime int64  `json:"utime"`
	}
	UserInfoRequest {
	}
	UserInfoResponse {
		Code int64    `json:"code"`
		Msg  string   `json:"msg"`
		Data UserInfo `json:"data,optional"`
	}
)

service Backend {
	@handler Login
	post /api/login(LoginRequest) returns (LoginResponse)
}

@server(
	jwt: Auth // 开启auth验证
)

service Backend {
	@handler UserInfo
	post /api/user/info(UserInfoRequest) returns (UserInfoResponse)
}

执行 make api 生成代码

创建目录和文件common/jwtx/jwt.go

package jwtx

import "github.com/golang-jwt/jwt/v4"

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))
}

修改internal/logic/loginlogic.go

package logic

import (
	"context"
	"time"

	"tapi/common/jwtx"
	"tapi/internal/svc"
	"tapi/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) (resp *types.LoginResponse, err error) {
	// user表
	table := l.svcCtx.BkModel.User
	// 查询用户
	user, err := table.WithContext(l.ctx).Where(table.Name.Eq(req.Name)).Debug().First()
	if err != nil {
		return &types.LoginResponse{
			Code: 500,
			Msg:  err.Error(),
		}, nil
	}

	// 判断密码是否正确
	if user.Password != req.Password {
		return &types.LoginResponse{
			Code: 500,
			Msg:  "密码错误",
		}, nil
	}

	// 获取accessToken
	now := time.Now().Unix()
	accessExpire := l.svcCtx.Config.Auth.AccessExpire

	accessToken, err := jwtx.GetToken(l.svcCtx.Config.Auth.AccessSecret, now, accessExpire, user.ID)
	if err != nil {
		return &types.LoginResponse{
			Code: 500,
			Msg:  err.Error(),
		}, nil
	}

	return &types.LoginResponse{
		Code:  200,
		Token: accessToken,
		Msg:   "成功",
	}, nil
}

测试访问一下

3.png

修改internal/logic/userinfologic.go

package logic

import (
	"context"
	"encoding/json"

	"tapi/internal/svc"
	"tapi/internal/types"

	"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(req *types.UserInfoRequest) (resp *types.UserInfoResponse, err error) {
	// 获取token中的uid,具体自行查看go-zero的文档和源码,access的验证框架已经实现,我们只需要配置Auth的对应参数
	uid, _ := l.ctx.Value("uid").(json.Number).Int64()
	table := l.svcCtx.BkModel.User
	user, err := table.WithContext(l.ctx).Where(table.ID.Eq(uid)).First()

	if err != nil {
		return &types.UserInfoResponse{
			Code: 500,
			Msg:  err.Error(),
		}, nil
	}

	return &types.UserInfoResponse{
		Code: 200,
		Msg:  "成功",
		Data: types.UserInfo{
			Id:    user.ID,
			Name:  user.Name,
			Ctime: int64(user.Ctime),
			Utime: int64(user.Utime),
		},
	}, nil
}

测试

4.png

错误处理

编辑product.api

syntax = "v1"

info(
	title: "tapi"
	desc: "接口"
	author: "tim"
	version: 1.0
)

type (

	// 错误   增加这个结构
	CodeErrorResponse {
		Code int64  `json:"code"`
		Msg  string `json:"msg"`
	}

	// 登录请求
	LoginRequest {
		Name     string `form:"name"`
		Password string `form:"password"`
	}
	// 登录返回
	LoginResponse {
		Code  int64  `json:"code"`
		Msg   string `json:"msg"`
		Token string `json:"token,optional"`
	}

	// 用户数据
	UserInfo {
		Id    int64  `json:"id"`
		Name  string `json:"name"`
		Ctime int64  `json:"ctime"`
		Utime int64  `json:"utime"`
	}

	UserInfoRequest {
	}
	UserInfoResponse {
		Code int64    `json:"code"`
		Msg  string   `json:"msg"`
		Data UserInfo `json:"data,optional"`
	}
)

service Backend {
	@handler Login
	post /api/login(LoginRequest) returns (LoginResponse)
}

@server(
	jwt: Auth // 开启auth验证
)

service Backend {
	@handler UserInfo
	post /api/user/info(UserInfoRequest) returns (UserInfoResponse)
}

运行 make api 生成代码

修改 backend.go

package main

import (
	"context"
	"flag"
	"fmt"
	"net/http"

	"tapi/internal/config"
	"tapi/internal/handler"
	"tapi/internal/svc"
	"tapi/internal/types"

	"github.com/zeromicro/go-zero/core/conf"
	"github.com/zeromicro/go-zero/rest"
	"github.com/zeromicro/go-zero/rest/httpx"
)

var configFile = flag.String("f", "etc/backend.yaml", "the config file")

func main() {
	flag.Parse()

	var c config.Config
	conf.MustLoad(*configFile, &c)

	server := rest.MustNewServer(c.RestConf)
	defer server.Stop()

	ctx := svc.NewServiceContext(c)
	handler.RegisterHandlers(server, ctx)
	// 全局错误处理 增加这段代码
	httpx.SetErrorHandlerCtx(func(ctx context.Context, err error) (int, interface{}) {
		fmt.Println(err.Error())
		return http.StatusOK, &types.CodeErrorResponse{
			Code: 500,
			Msg:  err.Error(),
		}
	})

	fmt.Printf("Starting server at %s:%d...\n", c.Host, c.Port)
	server.Start()
}

启动服务 make dev

5.png

go-zero的自定义错误,有点麻烦,后台暂时用不上,这里就用最简单的处理方式。处理方式自行决定。

创建用户

修改 project.api

syntax = "v1"

info(
	title: "tapi"
	desc: "接口"
	author: "tim"
	version: 1.0
)

type (

	// 错误
	CodeErrorResponse {
		Code int64  `json:"code"`
		Msg  string `json:"msg"`
	}

	// 登录请求
	LoginRequest {
		Name     string `form:"name"`
		Password string `form:"password"`
	}
	// 登录返回
	LoginResponse {
		Code  int64  `json:"code"`
		Msg   string `json:"msg"`
		Token string `json:"token,optional"`
	}

	// 用户数据
	UserInfo {
		Id    int64  `json:"id"`
		Name  string `json:"name"`
		Ctime int64  `json:"ctime"`
		Utime int64  `json:"utime"`
	}

	UserInfoRequest {
	}
	UserInfoResponse {
		Code int64    `json:"code"`
		Msg  string   `json:"msg"`
		Data UserInfo `json:"data,optional"`
	}

	// 创建用户
	UserAddRequest {
		Name     string `form:"name"`
		Password string `form:"password"`
	}
	UserAddResponse {
		Code int64  `json:"code"`
		Msg  string `json:"msg"`
	}
)

service Backend {
	// 登录
	@handler Login
	post /api/login(LoginRequest) returns (LoginResponse)
}

@server(
	jwt: Auth // 开启auth验证
)

service Backend {
	// 用户信息
	@handler UserInfo
	post /api/user/info(UserInfoRequest) returns (UserInfoResponse)
	
	// 创建用户
	@handler UserAdd
	post /api/user/add(UserAddRequest) returns (UserAddResponse)
	
}

运行 make api

修改 internal/logic/useraddlogic.go

package logic

import (
	"context"
	"time"

	"tapi/bkmodel/dao/model"
	"tapi/internal/svc"
	"tapi/internal/types"

	"github.com/zeromicro/go-zero/core/logx"
)

type UserAddLogic struct {
	logx.Logger
	ctx    context.Context
	svcCtx *svc.ServiceContext
}

func NewUserAddLogic(ctx context.Context, svcCtx *svc.ServiceContext) *UserAddLogic {
	return &UserAddLogic{
		Logger: logx.WithContext(ctx),
		ctx:    ctx,
		svcCtx: svcCtx,
	}
}

func (l *UserAddLogic) UserAdd(req *types.UserAddRequest) (resp *types.UserAddResponse, err error) {

	table := l.svcCtx.BkModel.User
	// 查询用户是否存在
	u, err := table.WithContext(l.ctx).Where(table.Name.Eq(req.Name)).First()
	if err != nil {
		return &types.UserAddResponse{
			Code: 500,
			Msg:  err.Error(),
		}, nil
	}
	if u.Name == req.Name {
		return &types.UserAddResponse{
			Code: 500,
			Msg:  "用户已存在",
		}, nil
	}

	// 新建用户
	currTime := time.Now().Unix()
	user := model.User{
		Name:     req.Name,
		Password: req.Password,
		Status:   1,
		Utime:    int32(currTime),
		Ctime:    int32(currTime),
	}

	err = table.WithContext(l.ctx).Create(&user)
	if err != nil {
		return &types.UserAddResponse{
			Code: 500,
			Msg:  err.Error(),
		}, nil
	}
	return &types.UserAddResponse{
		Code: 200,
		Msg:  "成功",
	}, nil
}

运行 make dev

测试

上面代码测试返回为空数据,dlv调试发现

if u.Name == req.Name 这句中u为nil

修改成

if u != nil && u.Name == req.Name

重新运行 make dev 再测试OK

6.png

异常处理

上面创建用户出现了异常,go-zero捕获了,改变了http的code,这种方式对于接口不是很合理,所以改造了rcover中间件

修改backend.go

package main

import (
	"context"
	"flag"
	"fmt"
	"log"
	"net/http"
	"runtime/debug"

	"tapi/internal/config"
	"tapi/internal/handler"
	"tapi/internal/svc"
	"tapi/internal/types"

	"github.com/zeromicro/go-zero/core/conf"
	"github.com/zeromicro/go-zero/rest"
	"github.com/zeromicro/go-zero/rest/httpx"
)

var configFile = flag.String("f", "etc/backend.yaml", "the config file")

func main() {
	flag.Parse()

	var c config.Config
	conf.MustLoad(*configFile, &c)

	server := rest.MustNewServer(c.RestConf)
	defer server.Stop()

	ctx := svc.NewServiceContext(c)
	handler.RegisterHandlers(server, ctx)

	httpx.SetErrorHandlerCtx(func(ctx context.Context, err error) (int, interface{}) {
		fmt.Println(err.Error())
		return http.StatusOK, &types.CodeErrorResponse{
			Code: 500,
			Msg:  err.Error(),
		}
	})

	// 全局recover中间件
	server.Use(func(next http.HandlerFunc) http.HandlerFunc {
		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			defer func() {
				if result := recover(); result != nil {
					log.Println(fmt.Sprintf("%v\n%s", result, debug.Stack()))
					httpx.OkJson(w, &types.CodeErrorResponse{
						Code: 500,
						Msg:  "服务器错误", //string(debug.Stack()),
					})
				}
			}()

			next.ServeHTTP(w, r)
		})
	})

	fmt.Printf("Starting server at %s:%d...\n", c.Host, c.Port)
	server.Start()
}

上面改造的方式参考了go-zero/rest/handler/recoverhandler.go,这种改造方式不确定是否正确,暂时先解决了遇到的问题,后续再研究go-zero源码