go doudou开发单体RESTful服务快速上手教程

目录
  • 引言
    • 需求清单
    • 学习目标
  • 开发环境准备
    • 安装go-doudou
    • 初始化工程
    • 定义接口
    • 生成代码
    • 启动服务
    • 数据库和表结构准备
    • 生成domain和dao层代码
  • 用户注册接口
    • 修改domain
    • PublicSignUp方法实现
    • Postman测试
  • 用户登录接口
    • PublicLogIn方法实现
    • Postman测试
  • 上传头像接口
    • 修改domain
    • 修改.env配置
    • JWT校验中间件
    • UploadAvatar方法实现
  • 下载头像接口
    • GetPublicDownloadAvatar方法实现
  • 用户详情接口
    • GetUser方法实现
    • Postman测试
  • 用户分页接口
    • 导入测试数据
    • PageUsers方法实现
    • Postman测试
    • 服务部署
  • 总结

引言

笔者2015年开始接触go语言并采用go语言从事web项目开发至今,先后用过beego、gin、grpc等框架。这些框架非常优秀,通过学习它们的源码,也学到了很多。笔者之前在公司一直是单打独斗,一个人就把前后端的活包了,用现成的框架其实也蛮好。只是后来带了团队,接了不少项目,开始接触和学习敏捷开发、项目管理等方面的理论和实践,发现前后端不同成员之间沟通和联调也是需要很多成本的,特别是如果前端同事完全不懂后端,后端同事完全不懂前端的情况下会遇到不少头疼的事。于是萌生了用go语言开发一套低代码的、易于快速开发的、同时方便前后端同事沟通和联调的微服务框架,这就是go-doudou微服务框架。go-doudou框架主要基于gorilla的mux路由库做RESTful接口的快速生成,基于hashicorp公司开源的memberlist库做服务注册与发现和故障检测,同时支持开发单体应用和微服务应用。本教程将通过一个用户管理服务的案例来分几篇文章介绍如何用go-doudou开发单体RESTful接口。

需求清单

  • 用户注册
  • 用户登录
  • 用户详情
  • 用户分页
  • 上传头像
  • 下载头像

学习目标

  • 用户详情、用户分页和上传头像需要采用jwt做权限校验
  • 用户注册、用户登录和下载头像接口可以公开访问,无须鉴权
  • 提供在线接口文档
  • 提供go语言客户端SDK
  • 提供mock接口实现
  • 实现真实业务逻辑
  • go-doudou内建的ddl表结构同步工具
  • go-doudou内建的dao层代码生成和使用

开发环境准备

  • docker环境: 推荐下载安装docker官方的desktop软件,官方安装文档地址
  • IDE:推荐goland,当然vscode也可以

安装go-doudou

  • 配置goproxy.cn代理,加速依赖下载
export GOPROXY=https://goproxy.cn,direct
  • 如果你用的go版本是1.16以下版本:
GO111MODULE=on  go get -v github.com/unionj-cloud/go-doudou@v0.8.6

如果你用的go是1.16及以上版本:

go get -v github.com/unionj-cloud/go-doudou@v0.8.6
  • goproxy.cn的同步会延迟一些,如果执行以上命令失败,可以关闭代理,科学上网
export GOPROXY=https://proxy.golang.org,direct
  • 以上办法都不行,可以直接克隆同步到gitee的源码,本地安装
git clone git@gitee.com:unionj-cloud/go-doudou.git

切到根路径下,执行命令:

go install
  • 执行命令go-doudou -v,如果输出如下内容,表示安装成功:
➜  ~ go-doudou -v
go-doudou version v0.8.6

初始化工程

执行命令:

go-doudou svc init usersvc

切到usersvc路径下,可以看到生成了如下文件结构:

➜  tutorials ll
total 0
drwxr-xr-x  9 wubin1989  staff   288B 10 24 20:05 usersvc
➜  tutorials cd usersvc
➜  usersvc git:(master) ✗ ll
total 24
-rw-r--r--  1 wubin1989  staff   707B 10 24 20:05 Dockerfile
-rw-r--r--  1 wubin1989  staff   439B 10 24 20:05 go.mod
-rw-r--r--  1 wubin1989  staff   247B 10 24 20:05 svc.go
drwxr-xr-x  3 wubin1989  staff    96B 10 24 20:05 vo
  • svc.go文件:做接口设计和定义
  • vo文件夹:定义接口入参和出参的结构体
  • Dockerfile:用于docker镜像打包

定义接口

我们打开svc.go文件看一下:

package service
import (
	"context"
	v3 "github.com/unionj-cloud/go-doudou/openapi/v3"
	"os"
	"usersvc/vo"
)
// Usersvc 用户管理服务
// 调用用户详情、用户分页和上传头像接口需要带上Bearer Token请求头
// 用户注册、用户登录和下载头像接口可以公开访问,无须鉴权
type Usersvc interface {
	// PageUsers 用户分页接口
	// 展示如何定义POST请求且Content-Type为application/json的接口
	PageUsers(ctx context.Context,
		// 分页请求参数
		query vo.PageQuery) (
		// 分页结果
		data vo.PageRet,
		// 错误信息
		err error)
	// GetUser 用户详情接口
	// 展示如何定义带查询字符串参数的GET请求接口
	GetUser(ctx context.Context,
		// 用户ID
		userId int) (
		// 用户详情
		data vo.UserVo,
		// 错误信息
		err error)
	// PublicSignUp 用户注册接口
	// 展示如何定义POST请求且Content-Type是application/x-www-form-urlencoded的接口
	PublicSignUp(ctx context.Context,
		// 用户名
		username string,
		// 密码
		password string,
		// 图形验证码
		code string,
	) (
		// 成功返回OK
		data string, err error)
	// PublicLogIn 用户登录接口
	// 展示如何鉴权并返回token
	PublicLogIn(ctx context.Context,
		// 用户名
		username string,
		// 密码
		password string) (
		// token
		data string, err error)
	// UploadAvatar 上传头像接口
	// 展示如何定义文件上传接口
	// 函数签名的入参里必须要有至少一个[]*v3.FileModel或者*v3.FileModel类型的参数
	UploadAvatar(ctx context.Context,
		// 用户头像
		avatar *v3.FileModel) (
		// 成功返回OK
		data string, err error)
	// GetPublicDownloadAvatar 下载头像接口
	// 展示如何定义文件下载接口
	// 函数签名的出参里必须有且只有一个*os.File类型的参数
	GetPublicDownloadAvatar(ctx context.Context,
		// 用户ID
		userId int) (
		// 文件二进制流
		data *os.File, err error)
}

以上代码里每个方法都有注释。请仔细阅读。接口定义支持文档注释,只支持go语言常见的//注释。这些注释会作为OpenAPI3.0规范里的description参数值导出到生成的json文档和go-doudou内建的在线文档里,下文会做演示。

生成代码

执行如下命令,即可生成启动一个服务所需的全部代码

go-doudou svc http --handler -c go --doc

解释一下命令中的flag参数:

  • --handler:表示需要生成http handler接口实现,就是把解析http请求参数和编码返回值的代码都生成出来
  • -c:表示生成服务接口的客户端SDK,目前只支持go。如果不需要生成客户端SDK,可以不设置这个flag,因为相对其他代码来说,生成过程比较耗时
  • --doc:表示生成OpenAPI3.0规范的json文档 这行命令是笔者常用的命令,推荐大家也这样使用。并且这行命令可以在每次修改了svc.go文件里的接口定义以后执行,可以增量的生成代码。规则是:
  • handler.go文件和OpenAPI3.0规范的json文档总是会重新生成
  • handlerimpl.go文件和svcimpl.go文件只会增量生成,不会修改现有代码
  • 其他文件都会先判断同名文件是否存在,如果存在就跳过

为了确保依赖都已经下载下来,最好再执行一下这个命令:

go mod tidy

我们来看一下此时的项目结构:

➜  usersvc git:(master) ✗ ll
total 296
-rw-r--r--  1 wubin1989  staff   707B 10 24 20:05 Dockerfile
drwxr-xr-x  3 wubin1989  staff    96B 10 24 23:10 client
drwxr-xr-x  3 wubin1989  staff    96B 10 24 23:10 cmd
drwxr-xr-x  3 wubin1989  staff    96B 10 24 23:10 config
drwxr-xr-x  3 wubin1989  staff    96B 10 24 23:10 db
-rw-r--r--  1 wubin1989  staff   514B 10 24 23:10 go.mod
-rw-r--r--  1 wubin1989  staff   115K 10 24 23:10 go.sum
-rw-r--r--  1 wubin1989  staff   1.7K 10 24 23:21 svc.go
-rw-r--r--  1 wubin1989  staff   1.6K 10 25 09:18 svcimpl.go
drwxr-xr-x  3 wubin1989  staff    96B 10 24 23:10 transport
-rwxr-xr-x  1 wubin1989  staff   5.9K 10 25 09:18 usersvc_openapi3.go
-rwxr-xr-x  1 wubin1989  staff   5.7K 10 25 09:18 usersvc_openapi3.json
drwxr-xr-x  3 wubin1989  staff    96B 10 24 23:07 vo
  • Dockerfile文件:用于打包docker镜像
  • client包:生成的go客户端代码
  • cmd包:里面有main.go文件,用于启动服务
  • config包:用于加载配置
  • db包:用于连接数据库
  • svc.go文件:设计接口
  • svcimpl.go文件:里面有mock的接口实现,后续在里面根据业务需求编写真实的业务逻辑
  • transport包:里面是http handler接口和实现,负责具体的接口入参解析和出参序列化
  • usersvc_openapi3.go文件:用于在线接口文档功能
  • usersvc_openapi3.json文件:遵循OpenAPI 3.0规范的接口文档
  • vo包:里面是接口的入参和出参结构体类型

启动服务

go-doudou svc run

我们可以看到如下输出:

➜  usersvc git:(master) ✗ go-doudou svc run                       
INFO[2021-12-28 22:39:35] Initializing logging reporter                
INFO[2021-12-28 22:39:35] ================ Registered Routes ================ 
INFO[2021-12-28 22:39:35] +----------------------+--------+-------------------------+ 
INFO[2021-12-28 22:39:35] |         NAME         | METHOD |         PATTERN         | 
INFO[2021-12-28 22:39:35] +----------------------+--------+-------------------------+ 
INFO[2021-12-28 22:39:35] | PageUsers            | POST   | /page/users             | 
INFO[2021-12-28 22:39:35] | User                 | GET    | /user                   | 
INFO[2021-12-28 22:39:35] | PublicSignUp         | POST   | /public/sign/up         | 
INFO[2021-12-28 22:39:35] | PublicLogIn          | POST   | /public/log/in          | 
INFO[2021-12-28 22:39:35] | UploadAvatar         | POST   | /upload/avatar          | 
INFO[2021-12-28 22:39:35] | PublicDownloadAvatar | GET    | /public/download/avatar | 
INFO[2021-12-28 22:39:35] | GetDoc               | GET    | /go-doudou/doc          | 
INFO[2021-12-28 22:39:35] | GetOpenAPI           | GET    | /go-doudou/openapi.json | 
INFO[2021-12-28 22:39:35] | Prometheus           | GET    | /go-doudou/prometheus   | 
INFO[2021-12-28 22:39:35] | GetRegistry          | GET    | /go-doudou/registry     | 
INFO[2021-12-28 22:39:35] +----------------------+--------+-------------------------+ 
INFO[2021-12-28 22:39:35] =================================================== 
INFO[2021-12-28 22:39:35] Started in 233.424µs                         
INFO[2021-12-28 22:39:35] Http server is listening on :6060

当出现"Http server is listening on :6060"时,表示服务已经启动,并且我们已经有了mock的服务接口实现。例如,我们可以执行如下命令请求/user接口,看看返回什么数据:

➜  usersvc git:(master) ✗ http http://localhost:6060/user
HTTP/1.1 200 OK
Content-Encoding: gzip
Content-Length: 109
Content-Type: application/json; charset=UTF-8
Date: Mon, 01 Nov 2021 15:21:10 GMT
Vary: Accept-Encoding
{
    "data": {
        "Dept": "ZkkCmcLU",
        "Id": -1941954111002502016,
        "Name": "aiMtQ",
        "Phone": "XMAqXf"
    }
}

此时你可能注意到返回的数据的字段名称是首字母大写的,这可能不是你想要的。在vo包下的vo.go文件里有一行go generate命令:

//go:generate go-doudou name --file $GOFILE

这行命令里用到了go-doudou框架内置的一个工具name。它可以根据指定的命名规则生成结构体字段后面的json标签。默认生成策略是首字母小写的驼峰命名策略,同时支持蛇形命名。未导出的字段会跳过,只修改导出字段的json标签。命令行执行命令:

go generate ./...

然后重启服务,请求/user接口,可以看到字段名称已经变成首字母小写的驼峰命名了。

➜  usersvc git:(master) ✗ http http://localhost:6060/user
HTTP/1.1 200 OK
Content-Encoding: gzip
Content-Length: 114
Content-Type: application/json; charset=UTF-8
Date: Tue, 02 Nov 2021 08:25:39 GMT
Vary: Accept-Encoding
{
    "data": {
        "dept": "wGAEEeveHp",
        "id": -816946940349962228,
        "name": "hquwOKl",
        "phone": "AriWmKYB"
    }
}

关于name工具的更多用法,请参考文档。 此时,因为vo包里的结构体修改了json标签,所以OpenAPI文档需要重新生成,否则在线文档里的字段名称还是修改前的。需要执行如下命令:

go-doudou svc http --doc

然后我们重启一下服务,在地址栏输入http://localhost:6060/go-doudou/doc, 再输入http basic用户名admin,密码admin,看一下在线文档是什么效果:

在线文档里的接口说明和参数说明都取自svc.go的接口方法注释和参数注释。

数据库和表结构准备

为了支持中文字符,需先在根目录下创建mysql配置文件my/custom.cnf,贴进去如下内容:

[client]
default-character-set=utf8mb4
[mysql]
default-character-set=utf8mb4
[mysqld]
character_set_server=utf8mb4
collation-server=utf8mb4_general_ci
default-authentication-plugin=mysql_native_password
init_connect='SET NAMES utf8mb4'

在根目录下创建数据库初始化脚本sqlscripts/init.sql,贴进去如下内容:

CREATE SCHEMA `tutorial` DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_general_ci;
CREATE TABLE `tutorial`.`t_user`
(
    `id`        INT         NOT NULL AUTO_INCREMENT,
    `username`  VARCHAR(45) NOT NULL COMMENT '用户名',
    `password`  VARCHAR(60) NOT NULL COMMENT '密码',
    `name`      VARCHAR(45) NOT NULL COMMENT '真实姓名',
    `phone`     VARCHAR(45) NOT NULL COMMENT '手机号',
    `dept`      VARCHAR(45) NOT NULL COMMENT '所属部门',
    `create_at` DATETIME    NULL DEFAULT current_timestamp,
    `update_at` DATETIME    NULL DEFAULT current_timestamp on update current_timestamp,
    `delete_at` DATETIME    NULL,
    PRIMARY KEY (`id`)
);
INSERT INTO tutorial.t_user (id, username, password, name, phone, dept, create_at, update_at, delete_at, avatar) VALUES (2, 'peter', '$2a$14$VaQLa/GbLAhRZvvTlgE8OOQgsBY4RDAJC5jkz13kjP9RlntdKBZVW', '张三丰', '13552053960', '技术部', '2021-12-28 06:41:00', '2021-12-28 14:59:20', null, 'out/wolf-wolves-snow-wolf-landscape-985ca149f06cd03b9f0ed8dfe326afdb.jpg');
INSERT INTO tutorial.t_user (id, username, password, name, phone, dept, create_at, update_at, delete_at, avatar) VALUES (4, 'john', '$2a$14$AKCs.u9vFUOCe5VwcmdfwOAkeiDtQYEgIB/nSU8/eemYwd91.qU.i', '李世民', '13552053961', '行政部', '2021-12-28 12:12:32', '2021-12-28 14:59:20', null, '');
INSERT INTO tutorial.t_user (id, username, password, name, phone, dept, create_at, update_at, delete_at, avatar) VALUES (5, 'lucy', '$2a$14$n0.l54axUqnKGagylQLu7ee.yDrtLubxzM1qmOaHK9Ft2P09YtQUS', '朱元璋', '13552053962', '销售部', '2021-12-28 12:13:17', '2021-12-28 14:59:20', null, '');
INSERT INTO tutorial.t_user (id, username, password, name, phone, dept, create_at, update_at, delete_at, avatar) VALUES (6, 'jack', '$2a$14$jFCwiZHcD7.DL/teao.Dl.HAFwk8wM2f1riH1fG2f52WYKqSiGZlC', '张无忌', '', '总裁办', '2021-12-28 12:14:19', '2021-12-28 14:59:20', null, '');

在根目录下创建docker-compose.yml文件,贴进入如下内容:

version: '3.9'
services:
  db:
    container_name: db
    image: mysql:5.7
    restart: always
    environment:
      MYSQL_ROOT_PASSWORD: 1234
    ports:
      - 3306:3306
    volumes:
      - $PWD/my:/etc/mysql/conf.d
      - $PWD/sqlscripts:/docker-entrypoint-initdb.d
    networks:
      - tutorial
networks:
  tutorial:
    driver: bridge

在根目录下执行docker compose命令,即可启动mysql数据库容器:

docker-compose -f docker-compose.yml up -d

可以通过docker ps命令查看正在运行的容器

➜  usersvc git:(master) ✗ docker ps
CONTAINER ID   IMAGE       COMMAND                  CREATED          STATUS          PORTS                                                  NAMES
df6af6362c41   mysql:5.7   "docker-entrypoint.s…"   13 minutes ago   Up 13 minutes   0.0.0.0:3306->3306/tcp, :::3306->3306/tcp, 33060/tcp   db

生成domain和dao层代码

因为我们初始化的schema名称是tutorial,所以我们先要把.env文件里的环境变量DB_SCHEMA的值改成tutorial

DB_SCHEMA=tutorial

执行如下命令,生成domain和dao层代码:

go-doudou ddl -r --dao --pre=t_

解释一下:

  • -r:表示从数据库表结构生成go结构体
  • --dao:表示生成dao层代码
  • --pre:表示表名称有前缀t_ 此时,你可以看到项目里多了两个目录:

具体用法请参考ddl文档 这里我们看一下dao/base.go文件里提供了哪些CRUD方法,后面实现具体业务逻辑的时候会用到:

package dao
import (
	"context"
	"github.com/unionj-cloud/go-doudou/ddl/query"
)
type Base interface {
	Insert(ctx context.Context, data interface{}) (int64, error)
	Upsert(ctx context.Context, data interface{}) (int64, error)
	UpsertNoneZero(ctx context.Context, data interface{}) (int64, error)
	DeleteMany(ctx context.Context, where query.Q) (int64, error)
	Update(ctx context.Context, data interface{}) (int64, error)
	UpdateNoneZero(ctx context.Context, data interface{}) (int64, error)
	UpdateMany(ctx context.Context, data interface{}, where query.Q) (int64, error)
	UpdateManyNoneZero(ctx context.Context, data interface{}, where query.Q) (int64, error)
	Get(ctx context.Context, id interface{}) (interface{}, error)
	SelectMany(ctx context.Context, where ...query.Q) (interface{}, error)
	CountMany(ctx context.Context, where ...query.Q) (int, error)
	PageMany(ctx context.Context, page query.Page, where ...query.Q) (query.PageRet, error)
}

再修改一下svcimpl.go文件的UsersvcImpl结构体

type UsersvcImpl struct {
	conf *config.Config
	db   *sqlx.DB
}

以及NewUsersvc方法

func NewUsersvc(conf *config.Config, db *sqlx.DB) Usersvc {
	return &UsersvcImpl{
		conf,
		db,
	}
}

生成的main方法里已经为我们注入了mysql连接实例,所以不用改

svc := service.NewUsersvc(conf, conn)

后面我们直接在接口实现里面调用UsersvcImpl结构体的db属性即可

用户注册接口

修改domain

因为通常来说用户名都必须是唯一的,所以我们需要改一下domain/user.go文件:

Username string     `dd:"type:varchar(45);extra:comment '用户名';unique"`

再执行ddl命令

go-doudou ddl --pre=t_

这行命令没有-r参数了,表示从go结构体更新到表结构。

PublicSignUp方法实现

要实现注册逻辑,我们需要先给dao层代码加一个方法CheckUsernameExists,判断一下传进来的用户名是否已经被注册。先改一下dao/userdao.go文件

package dao
import "context"
type UserDao interface {
	Base
	CheckUsernameExists(ctx context.Context, username string) (bool, error)
}

再新建一个文件dao/userdaoimplext.go文件,加入如下代码

package dao
import (
	"context"
	"github.com/unionj-cloud/go-doudou/ddl/query"
	"usersvc/domain"
)
func (receiver UserDaoImpl) CheckUsernameExists(ctx context.Context, username string) (bool, error) {
	many, err := receiver.SelectMany(ctx, query.C().Col("username").Eq(username))
	if err != nil {
		return false, err
	}
	users := many.([]domain.User)
	if len(users) > 0 {
		return true, nil
	}
	return false, nil
}

这样就实现了对生成的dao层代码的自定义扩展。以后如果user实体的字段新增或者减少,只需要删除userdaosql.go文件,再次执行go-doudou ddl --dao --pre=t_命令,重新生成userdaosql.go文件即可,已存在的dao层文件不会被修改。 然后就是SignUp方法的具体实现了

func (receiver *UsersvcImpl) PublicSignUp(ctx context.Context, username string, password string, code string) (data string, err error) {
	hashPassword, _ := lib.HashPassword(password)
	userDao := dao.NewUserDao(receiver.db)
	var exists bool
	exists, err = userDao.CheckUsernameExists(ctx, username)
	if err != nil {
		panic(err)
	}
	if exists {
		panic(lib.ErrUsernameExists)
	}
	_, err = userDao.Insert(ctx, domain.User{
		Username: username,
		Password: hashPassword,
	})
	if err != nil {
		panic(err)
	}
	return "OK", nil
}

遇到报错,可以直接panic,也可以return "", lib.ErrUsernameExists。因为已经加了ddhttp.Recover中间件,可以自动从panic里恢复,并返回错误信息给前端。需要注意的是,http状态码为500,不是200。只要从接口方法里返回了error类型的参数,生成的http handler代码里默认设置的http状态码就是500。如果想自定义修改默认生成的http handler里的代码,是完全可以的。当有接口定义新增或者修改的时候,再次执行命令go-doudou svc http --handler -c go --doc不会覆盖已存在的代码,只会增量生成代码。

Postman测试

测试一下接口,这是第一次请求

这是第二次请求

用户登录接口

PublicLogIn方法实现

func (receiver *UsersvcImpl) PublicLogIn(ctx context.Context, username string, password string) (data string, err error) {
	userDao := dao.NewUserDao(receiver.db)
	many, err := userDao.SelectMany(ctx, query.C().Col("username").Eq(username).And(query.C().Col("delete_at").IsNull()))
	if err != nil {
		return "", err
	}
	users := many.([]domain.User)
	if len(users) == 0 || !lib.CheckPasswordHash(password, users[0].Password) {
		panic(lib.ErrUsernameOrPasswordIncorrect)
	}
	now := time.Now()
	token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
		"userId": users[0].Id,
		"exp":    now.Add(10 * time.Minute).Unix(),
		//"iat":    now.Unix(),
		//"nbf":    now.Unix(),
	})
	return token.SignedString(receiver.conf.JWTConf.Secret)
}

这段代码的逻辑是先根据入参username查出来数据库中的用户,如果没查到或者密码不对,返回“用户名或密码错误”的报错,如果密码对了,则签发token返回。用的jwt库是golang-jwt/jwt

Postman测试

上传头像接口

修改domain

表里面少了一个avatar字段,现在我们加上:

Avatar   string     `dd:"type:varchar(255);extra:comment '用户头像'"`

因为是新增了字段,所以要先删除dao/userdaosql.go文件,再执行ddl命令

go-doudou ddl --dao --pre=t_

如果增删的字段比较多,涉及多个实体,可以通过如下命令一次删掉所有*sql.go文件,再重新生成

rm -rf dao/*sql.go

修改.env配置

加入三行配置。JWT_为前缀的是JWT token校验相关的配置。Biz_为前缀的是实际业务相关的配置。

JWT_SECRET=secret
JWT_IGNORE_URL=/public/sign/up,/public/log/in,/public/get/download/avatar,/public/**
BIZ_OUTPUT=out

JWT_IGNORE_URL的值设置成/public/**就可以了,我都列上,是想说明这里同时支持通配符匹配和完整匹配。 同时,config/config.go文件也需要相应的改动。当然也可以直接调用os.Getenv方法。

package config
import (
	"github.com/kelseyhightower/envconfig"
	"github.com/sirupsen/logrus"
)
type Config struct {
	DbConf  DbConfig
	JWTConf JWTConf
	BizConf BizConf
}
type BizConf struct {
	Output string
}
type JWTConf struct {
	Secret    []byte
	IgnoreUrl []string `split_words:"true"`
}
type DbConfig struct {
	Driver  string `default:"mysql"`
	Host    string `default:"localhost"`
	Port    string `default:"3306"`
	User    string
	Passwd  string
	Schema  string
	Charset string `default:"utf8mb4"`
}
func LoadFromEnv() *Config {
	var dbconf DbConfig
	err := envconfig.Process("db", &dbconf)
	if err != nil {
		logrus.Panicln("Error processing env", err)
	}
	var jwtConf JWTConf
	err = envconfig.Process("jwt", &jwtConf)
	if err != nil {
		logrus.Panicln("Error processing env", err)
	}
	var bizConf BizConf
	err = envconfig.Process("biz", &bizConf)
	if err != nil {
		logrus.Panicln("Error processing env", err)
	}
	return &Config{
		dbconf,
		jwtConf,
		bizConf,
	}
}

JWT校验中间件

因为go-doudou的http router采用的是gorilla/mux,所以与gorilla/mux的middleware是完全兼容的,自定义中间件的写法也是完全一样的。

package middleware
import (
	"context"
	"fmt"
	"github.com/dgrijalva/jwt-go"
	"github.com/gobwas/glob"
	"net/http"
	"os"
	"strings"
)
type ctxKey int
const userIdKey ctxKey = ctxKey(0)
func NewContext(ctx context.Context, id int) context.Context {
	return context.WithValue(ctx, userIdKey, id)
}
func FromContext(ctx context.Context) (int, bool) {
	userId, ok := ctx.Value(userIdKey).(int)
	return userId, ok
}
func Jwt(inner http.Handler) http.Handler {
	g := glob.MustCompile(fmt.Sprintf("{%s}", os.Getenv("JWT_IGNORE_URL")))
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if g.Match(r.RequestURI) {
			inner.ServeHTTP(w, r)
			return
		}
		authHeader := r.Header.Get("Authorization")
		tokenString := strings.TrimSpace(strings.TrimPrefix(authHeader, "Bearer "))
		token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
			if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
				return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"])
			}
			return []byte(os.Getenv("JWT_SECRET")), nil
		})
		if err != nil || !token.Valid {
			w.WriteHeader(401)
			w.Write([]byte("Unauthorised.\n"))
			return
		}
		claims := token.Claims.(jwt.MapClaims)
		if userId, exists := claims["userId"]; !exists {
			w.WriteHeader(401)
			w.Write([]byte("Unauthorised.\n"))
			return
		} else {
			inner.ServeHTTP(w, r.WithContext(NewContext(r.Context(), int(userId.(float64)))))
		}
	})
}

UploadAvatar方法实现

func (receiver *UsersvcImpl) UploadAvatar(ctx context.Context, avatar *v3.FileModel) (data string, err error) {
	defer avatar.Close()
	_ = os.MkdirAll(receiver.conf.BizConf.Output, os.ModePerm)
	out := filepath.Join(receiver.conf.BizConf.Output, avatar.Filename)
	var f *os.File
	f, err = os.OpenFile(out, os.O_WRONLY|os.O_CREATE, os.ModePerm)
	if err != nil {
		panic(err)
	}
	defer f.Close()
	_, err = io.Copy(f, avatar.Reader)
	if err != nil {
		panic(err)
	}
	userId, _ := middleware.FromContext(ctx)
	userDao := dao.NewUserDao(receiver.db)
	_, err = userDao.UpdateNoneZero(ctx, domain.User{
		Id:     userId,
		Avatar: out,
	})
	if err != nil {
		panic(err)
	}
	return "OK", nil
}

这里需要注意的是,defer avatar.Close()这行代码一定要尽早写上,这是释放文件描述符资源的代码。

下载头像接口

GetPublicDownloadAvatar方法实现

func (receiver *UsersvcImpl) GetPublicDownloadAvatar(ctx context.Context, userId int) (data *os.File, err error) {
	userDao := dao.NewUserDao(receiver.db)
	var get interface{}
	get, err = userDao.Get(ctx, userId)
	if err != nil {
		panic(err)
	}
	return os.Open(get.(domain.User).Avatar)
}

用户详情接口

GetUser方法实现

func (receiver *UsersvcImpl) GetUser(ctx context.Context, userId int) (data vo.UserVo, err error) {
	userDao := dao.NewUserDao(receiver.db)
	var get interface{}
	get, err = userDao.Get(ctx, userId)
	if err != nil {
		panic(err)
	}
	user := get.(domain.User)
	return vo.UserVo{
		Id:       user.Id,
		Username: user.Username,
		Name:     user.Name,
		Phone:    user.Phone,
		Dept:     user.Dept,
	}, nil
}

Postman测试

用户分页接口

导入测试数据

INSERT INTO tutorial.t_user (id, username, password, name, phone, dept, create_at, update_at, delete_at, avatar) VALUES (2, 'peter', '$2a$14$VaQLa/GbLAhRZvvTlgE8OOQgsBY4RDAJC5jkz13kjP9RlntdKBZVW', '张三丰', '13552053960', '技术部', '2021-12-28 06:41:00', '2021-12-28 14:59:20', null, 'out/wolf-wolves-snow-wolf-landscape-985ca149f06cd03b9f0ed8dfe326afdb.jpg');
INSERT INTO tutorial.t_user (id, username, password, name, phone, dept, create_at, update_at, delete_at, avatar) VALUES (4, 'john', '$2a$14$AKCs.u9vFUOCe5VwcmdfwOAkeiDtQYEgIB/nSU8/eemYwd91.qU.i', '李世民', '13552053961', '行政部', '2021-12-28 12:12:32', '2021-12-28 14:59:20', null, '');
INSERT INTO tutorial.t_user (id, username, password, name, phone, dept, create_at, update_at, delete_at, avatar) VALUES (5, 'lucy', '$2a$14$n0.l54axUqnKGagylQLu7ee.yDrtLubxzM1qmOaHK9Ft2P09YtQUS', '朱元璋', '13552053962', '销售部', '2021-12-28 12:13:17', '2021-12-28 14:59:20', null, '');
INSERT INTO tutorial.t_user (id, username, password, name, phone, dept, create_at, update_at, delete_at, avatar) VALUES (6, 'jack', '$2a$14$jFCwiZHcD7.DL/teao.Dl.HAFwk8wM2f1riH1fG2f52WYKqSiGZlC', '张无忌', '', '总裁办', '2021-12-28 12:14:19', '2021-12-28 14:59:20', null, '');

PageUsers方法实现

func (receiver *UsersvcImpl) PageUsers(ctx context.Context, pageQuery vo.PageQuery) (data vo.PageRet, err error) {
	userDao := dao.NewUserDao(receiver.db)
	var q query.Q
	q = query.C().Col("delete_at").IsNull()
	if stringutils.IsNotEmpty(pageQuery.Filter.Name) {
		q = q.And(query.C().Col("name").Like(fmt.Sprintf(`%s%%`, pageQuery.Filter.Name)))
	}
	if stringutils.IsNotEmpty(pageQuery.Filter.Dept) {
		q = q.And(query.C().Col("dept").Eq(pageQuery.Filter.Dept))
	}
	var page query.Page
	if len(pageQuery.Page.Orders) > 0 {
		for _, item := range pageQuery.Page.Orders {
			page = page.Order(query.Order{
				Col:  item.Col,
				Sort: sortenum.Sort(item.Sort),
			})
		}
	}
	if pageQuery.Page.PageNo == 0 {
		pageQuery.Page.PageNo = 1
	}
	page = page.Limit((pageQuery.Page.PageNo-1)*pageQuery.Page.Size, pageQuery.Page.Size)
	var ret query.PageRet
	ret, err = userDao.PageMany(ctx, page, q)
	if err != nil {
		panic(err)
	}
	var items []vo.UserVo
	for _, item := range ret.Items.([]domain.User) {
		var userVo vo.UserVo
		_ = copier.DeepCopy(item, &userVo)
		items = append(items, userVo)
	}
	data = vo.PageRet{
		Items:    items,
		PageNo:   ret.PageNo,
		PageSize: ret.PageSize,
		Total:    ret.Total,
		HasNext:  ret.HasNext,
	}
	return data, nil
}

Postman测试

服务部署

最后介绍一下docker-compose部署服务 首先修改Dockerfile

FROM golang:1.16.6-alpine AS builder
ENV GO111MODULE=on
ARG user
ENV HOST_USER=$user
ENV GOPROXY=https://goproxy.cn,direct
WORKDIR /repo
ADD go.mod .
ADD go.sum .
ADD . ./
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories
RUN apk add --no-cache bash tzdata
ENV TZ="Asia/Shanghai"
RUN go mod vendor
RUN export GDD_VER=$(go list -mod=vendor -m -f '{{ .Version }}' github.com/unionj-cloud/go-doudou) && \
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -v -ldflags="-X 'github.com/unionj-cloud/go-doudou/svc/config.BuildUser=$HOST_USER' -X 'github.com/unionj-cloud/go-doudou/svc/config.BuildTime=$(date)' -X 'github.com/unionj-cloud/go-doudou/svc/config.GddVer=$GDD_VER'" -mod vendor -o api cmd/main.go
ENTRYPOINT ["/repo/api"]

然后修改docker-compose.yml

version: '3.9'
services:
  db:
    container_name: db
    image: mysql:5.7
    restart: always
    environment:
      MYSQL_ROOT_PASSWORD: 1234
    ports:
      - 3306:3306
    volumes:
      - $PWD/my:/etc/mysql/conf.d
      - $PWD/sqlscripts:/docker-entrypoint-initdb.d
    networks:
      - tutorial
  usersvc:
    container_name: usersvc
    build:
      context: .
    environment:
      - GDD_BANNER=off
      - GDD_PORT=6060
      - DB_HOST=db
    expose:
      - "6060"
    ports:
      - "6060:6060"
    networks:
      - tutorial
    depends_on:
      - db
networks:
  tutorial:
    driver: bridge

最后执行命令

docker-compose -f docker-compose.yml up -d

如果usersvc容器没有启动成功,可能是因为db容器还没有完全启动,可以多执行几遍上面的命令。

总结

到这里,我们达到了全部的学习目标,也实现了需求清单中的全部接口。教程的全部源码都在这里

以上就是go doudou开发单体RESTful服务快速上手教程的详细内容,更多关于go doudou单体RESTful服务的资料请关注我们其它相关文章!

(0)

相关推荐

  • go doudou应用中使用注解示例详解

    目录 快速上手 准备 初始化工程 设计业务接口 生成代码 下载依赖 Auth中间件 修改main函数 启动服务 测试效果 注解实现原理 总结 快速上手 我们都知道go语言没有原生的注解,但是做业务开发有些时候没有注解确实不方便.go-doudou通过go语言标准库ast/parser实现了对注解的支持.b站配套视频教程地址:[golang] go-doudou微服务框架入门03-如何使用注解,如果喜欢看视频,可直接跟视频上手实践. 我们通过一个简单的基于go-doudou开发的服务来演示用法和效

  • Go gRPC进阶教程gRPC转换HTTP

    目录 前言 gRPC转成HTTP 编写和编译proto 服务端代码修改 使用postman测试 生成swagger文档 把swagger-ui转成Go代码,备用 对外提供swagger-ui 在swagger中配置bearer token 验证测试 总结 前言 我们通常把RPC用作内部通信,而使用Restful Api进行外部通信.为了避免写两套应用,我们使用grpc-gateway把gRPC转成HTTP.服务接收到HTTP请求后,grpc-gateway把它转成gRPC进行处理,然后以JSON

  • Go Grpc Gateway兼容HTTP协议文档自动生成网关

    目录 前言 一,grpc-gateway介绍 二,grpc-gateway环境准备 二,编写grpc-gateway服务 四,使用gateway生成swagger文档 五,性能对比 http -> go -> grpc -> go http -> go -> http -> grpc_gateway -> grpc -> go 六,总结 前言 调用,让客户端可以更具自身情况自由选择,服务端工作只需要做一份呢?还别说真还有一个准备好的轮子那就是今天的主角<

  • go doudou开发gRPC服务快速上手实现详解

    目录 引言 准备 安装go 安装gRPC编译器和插件 安装编译器protoc 安装插件 安装go-doudou 初始化项目 定义服务 生成代码 实现接口 测试服务 总结 引言 go-doudou从v2版本开始已经支持开发gRPC服务.开发流程跟v1版本是一致的,都是先在svc.go文件里的interface里定义方法,然后执行go-doudou代码生成命令生成代码,最后你再写自定义业务逻辑实现接口.go-doudou的学习曲线非常平滑,对新手极其友好,特别是具有其他编程语言开发背景的开发者,比如

  • Golang gRPC HTTP协议转换示例

    gRPC HTTP协议转换 正当有这个需求的时候,就看到了这个实现姿势.源自coreos的一篇博客,转载到了grpc官方博客gRPC with REST and Open APIs. etcd3改用grpc后为了兼容原来的api,同时要提供http/json方式的API,为了满足这个需求,要么开发两套API,要么实现一种转换机制,他们选择了后者,而我们选择跟随他们的脚步. 他们实现了一个协议转换的网关,对应github上的项目grpc-gateway,这个网关负责接收客户端请求,然后决定直接转发

  • Go gRPC服务proto数据验证进阶教程

    前言 上篇介绍了go-grpc-middleware的grpc_zap.grpc_auth和grpc_recovery使用,本篇将介绍grpc_validator,它可以对gRPC数据的输入和输出进行验证. 创建proto文件,添加验证规则 这里使用第三方插件go-proto-validators自动生成验证规则. go get github.com/mwitkow/go-proto-validators 1.新建simple.proto文件 syntax = "proto3"; pa

  • go doudou应用中使用枚举类型教程示例

    目录 go语言支持语法自己实现枚举类型 结构体类型示例 接口请求参数示例 go语言支持语法自己实现枚举类型 我们都知道go语言没有原生的枚举类型,但是做业务开发有些时候没有枚举类型确实不方便前后端联调.我们可以通过go语言支持的语法自己实现枚举类型.请看以下示例代码和注释说明: // 首先定义一个int类型别名,新类型名称就是枚举类型名称 type KeyboardLayout int // 然后定义若干常量,作为枚举值 // 第一个常量是默认值 const ( UNKNOWN Keyboard

  • Go gRPC服务进阶middleware使用教程

    目录 前言 go-grpc-middleware简介 grpc_zap日志记录 grpc_auth认证 grpc_recovery恢复 总结 前言 之前介绍了gRPC中TLS认证和自定义方法认证,最后还简单介绍了gRPC拦截器的使用.gRPC自身只能设置一个拦截器,所有逻辑都写一起会比较乱.本篇简单介绍go-grpc-middleware的使用,包括grpc_zap.grpc_auth和grpc_recovery. go-grpc-middleware简介 go-grpc-middleware封

  • Nginx服务快速入门教程

    一.Nginx介绍 1. 什么是Nginx? Nginx (engine x) 是一个高性能的HTTP和反向代理web服务器,同时也提供了IMAP/POP3/SMTP服务. 其特点是占有内存少,并发能力强,事实上nginx的并发能力在同类型的网页服务器中表现较好. 2. 为什么要使用Nginx? 随着当今互联网的迅速发展,单点服务器早已无法承载上万个乃至数十万个用户的持续访问.比如一台Tomcat服务器在理想状态下只能够可以承受住2000个左右的并发量,为了解决这个问题,就需要多台Tomcat服

  • Python程序语言快速上手教程

    本来打算从网上找一篇入门教程,但因为Python很少是程序员的第一次接触程序所学的语言,所以网上现有的教程多不是很基础,还是决定自己写下这些. 如果没有程序基础的话,可能会觉得本文涵盖的内容有点多.对照大学里面常教的C语言的教学速度,本文大约有四五个课时的内容:对照网上程序类的视频 教程,大致相当于两三个小时的内容:对于翻一本程序书籍,大约相当于翻一个小时书.也因此,如果有深入学习的打算的话,为了效率还是推荐看书. 如果暂时不能理解本文中的一些内容也没关系,因为都是一些经常会用到的基础知识,在实

  • Elasticsearch Join字段类型简单快速上手教程

    目录 概述 父子关系的限制 Global ordinals 父子文档 总结 阅读本文需要一定的Elasticsearch基础哦,本文深度有,但是不深 概述 Elasticsearch中Join数据类型的字段相信大家也都用过,也就是口中常谈的父子文档.在Elasticsearch中Join不能跨索引和分片,所以保存文档信息时要保证父子文档使用相同的路由参数来保证父文档与子文档保存在同一个索引的同一个分片,那么都有哪些限制呢? 父子关系的限制 每个索引中只能有一个关系字段 父文档与子文档必须在同一个

  • Go快速开发一个RESTful API服务

    目录 何时使用单体 RESTful 服务 商城单体 RESTful 服务 单体服务实现 Mall API 定义 账号模块 API 定义 订单模块 API 定义 商品模块 API 定义 生成单体服务 实现业务逻辑 总结 何时使用单体 RESTful 服务 对于很多初创公司来说,业务的早期我们更应该关注于业务价值的交付,而单体服务具有架构简单,部署简单,开发成本低等优点,可以帮助我们快速实现产品需求.我们在使用单体服务快速交付业务价值的同时,也需要为业务的发展预留可能性,所以我们一般会在单体服务中清

  • 使用SpringBoot开发Restful服务实现增删改查功能

    在去年的时候,在各种渠道中略微的了解了SpringBoot,在开发web项目的时候是如何的方便.快捷.但是当时并没有认真的去学习下,毕竟感觉自己在Struts和SpringMVC都用得不太熟练.不过在看了很多关于SpringBoot的介绍之后,并没有想象中的那么难,于是开始准备学习SpringBoot. 在闲暇之余的时候,看了下SpringBoot实战以及一些大神关于SpringBoot的博客之后,开始写起了我的第一个SpringBoot的项目.在能够对SpringBoot进行一些简单的开发Re

  • Java的JNI快速入门教程(推荐)

    1. JNI简介 JNI是Java Native Interface的英文缩写,意为Java本地接口. 问题来源:由于Java编写底层的应用较难实现,在一些实时性要求非常高的部分Java较难胜任(实时性要求高的地方目前还未涉及,实时性这类话题有待考究). 解决办法:Java使用JNI可以调用现有的本地库(C/C++开发任何和系统相关的程序和类库),极大地灵活Java的开发. 2. JNI快速学习教程 2.1 问题: 使用JNI写一段代码,实现string_Java_Test_helloworld

  • 无废话快速上手React路由开发

    安装 输入以下命令进行安装: // npm npm install react-router-dom // yarn yarn add react-router-dom react-router相关标签 react-router常用的组件有以下八个: import { BrowserRouter, HashRouter, Route, Redirect, Switch, Link, NavLink, withRouter, } from 'react-router-dom' 简单路由跳转 实现一

  • 5分钟快速上手Spring Boot

    概述 与一些动态语言(如Ruby.Groovy.Node.js)相比,Java开发显得异常笨重.接触过外包项目的朋友也有所了解,如果要开发一个小型项目,首选的编程语言并不是Java,而是PHP.为什么呢?因为开发起来快!目前很多大型互联网公司的早起编程语言都是类似PHP这种能够快速开发的语言. 既然问题出现了,那必然有解决问题的方案,SpringBoot做到了.SpringBoot是由Pivotal公司所属团队研发,该公司的企业宗旨为: 致力于"改变世界构造软件的方式(We are transf

  • NodeJS 基于 Dapr 构建云原生微服务应用快速入门教程

    目录 安装 Dapr CLI 本地环境中初始化 Dapr 运行初始化 CLI 命令 验证容器是否正在运行 验证组件目录是否已初始化 使用 Dapr API 运行 Dapr sidecar 保存状态 获取状态 删除状态 上手实战指南 1. 服务调用 示例仓库 运行 order-processor 服务 运行 checkout 服务 查看服务调用输出 2. 状态管理 操纵服务状态 查看 order-processor 输出 3. 发布和订阅 订阅 topic 发布 topic 查看发布/订阅输出 4

随机推荐