清水泥沙

go-micro开发运维实践(实现用户CURD服务)

定义protobuf,生成代码

修改proto/user/user.proto

syntax = "proto3";

package micro.service.user;
option go_package = "proto/user";

service UserService {
  rpc Pagination(PaginationRequest) returns(PaginationResponse){}
  rpc Get(GetRequest) returns(UserResponse){}
  rpc Create(CreateRequest) returns(UserResponse){}
  rpc Update(UpdateRequest) returns(UserResponse){}
  rpc Delete(DeleteRequest) returns(UserResponse){}
}

message User{
  uint64 id = 1;
  string name = 3;
  string email = 4;
  string real_name = 6;
  string avatar = 7;
  string create_at = 9;
  string update_at = 10;
}

//UserResponse 单个用户响应
message UserResponse{
  User user = 1;
}

//PaginationResponse 用户分页数据响应
message PaginationResponse{
  repeated User users = 1;
  uint64 total = 2;
}

//PaginationRequest 用户分页请求
message PaginationRequest{
  uint64 page = 1;
  uint32 perPage = 2;
}

//GetRequest 获取单个用户请求
message GetRequest{
  uint64 id = 1;
}

//CreateRequest 创建用户请求
message CreateRequest{
  string name = 1;
  string password = 2;
  string email = 3;
  string real_name = 4;
  string avatar = 5;
}

//UpdateRequest 更新用户请求
message UpdateRequest{
  uint64 id = 1;
  string name = 2;
  string email = 3;
  string real_name = 4;
  string avatar = 6;
}

//DeleteRequest 删除用户请求
message DeleteRequest{
  uint64 id = 1;
}

执行生成命令

make命令能帮我们执行在makefile中预定义好的命令,在开发当中能给我们带来便利。

make proto

没有make命令可以直接复制makefile中的proto命令执行

protoc --proto_path=. --micro_out=${MODIFY}:. --go_out=${MODIFY}:. proto/user/user.proto

封装用户数据库交互层

封装分页工具

我们日常开发中在页面上经常需要获取一些分页数据,在多个微服务中如果每个都要实现分页代码代码必定会造成大量的冗余,所以我们这里需要对分页代码进行一些封装。打开我们项目的common项目

创建分页工具包目录

mkdir -p pkg/pagination
touch pkg/pagination/pagination.go

编写分页工具代码

package pagination

import (
	"gorm.io/gorm"
	"math"
)

// Page 单个分页元素
type Page struct {
	// 链接
	URL string
	// 页码
	Number uint64
}

// ViewData 同视图渲染的数据
type ViewData struct {
	// 是否需要显示分页
	HasPages bool

	// 下一页
	Next    Page
	HasNext bool

	// 上一页
	Prev    Page
	HasPrev bool

	Current Page

	// 数据库的内容总数量
	TotalCount uint64
	// 总页数
	TotalPage uint64
}

// Pagination 分页对象
type Pagination struct {
	PerPage uint32
	Page    uint64
	Count   uint64
	DB      *gorm.DB
}

// New 分页对象构建器
// db —— GORM 查询句柄,用以查询数据集和获取数据总数
// page —— page
// perPage —— 每页条数,传参为小于或者等于 0 时为默认值  10
func New(db *gorm.DB, page uint64, perPage uint32) *Pagination {
	// 默认每页数量
	if perPage <= 0 {
		perPage = 10
	}

	// 实例对象
	p := &Pagination{
		DB:      db,
		PerPage: perPage,
		Page:    page,
		Count:   0,
	}

	// 设置当前页码
	p.SetPage(page)

	return p
}

// Paging 返回渲染分页所需的数据
func (p *Pagination) Paging() ViewData {

	return ViewData{
		HasPages: p.HasPages(),

		Next:    p.NewPage(p.NextPage()),
		HasNext: p.HasNext(),

		Prev:    p.NewPage(p.PrevPage()),
		HasPrev: p.HasPrev(),

		Current:   p.NewPage(p.CurrentPage()),
		TotalPage: p.TotalPage(),

		TotalCount: p.Count,
	}
}

// NewPage 设置当前页
func (p Pagination) NewPage(page uint64) Page {
	return Page{
		Number: page,
	}
}

// SetPage 设置当前页
func (p *Pagination) SetPage(page uint64) {
	if page <= 0 {
		page = 1
	}

	p.Page = page
}

// CurrentPage 返回当前页码
func (p Pagination) CurrentPage() uint64 {
	totalPage := p.TotalPage()
	if totalPage == 0 {
		return 0
	}

	if p.Page > totalPage {
		return totalPage
	}

	return p.Page
}

// Results 返回请求数据,请注意 data 参数必须为 GROM 模型的 Slice 对象
func (p Pagination) Results(data interface{}) error {
	var err error
	var offset uint64
	page := p.CurrentPage()
	if page == 0 {
		return err
	}

	if page > 1 {
		offset = (page - 1) * uint64(p.PerPage)
	}

	return p.DB.Debug().Limit(int(p.PerPage)).Offset(int(offset)).Find(data).Error
}

// TotalCount 返回的是数据库里的条数
func (p *Pagination) TotalCount() uint64 {
	if p.Count == 0 {
		var count int64
		if err := p.DB.Count(&count).Error; err != nil {
			return 0
		}
		p.Count = uint64(count)
	}

	return p.Count
}

// HasPages 总页数大于 1 时会返回 true
func (p *Pagination) HasPages() bool {
	n := p.TotalCount()
	return n > uint64(p.PerPage)
}

// HasNext returns true if current page is not the last page
func (p Pagination) HasNext() bool {
	totalPage := p.TotalPage()
	if totalPage == 0 {
		return false
	}

	page := p.CurrentPage()
	if page == 0 {
		return false
	}

	return page < totalPage
}

// PrevPage 前一页码,0 意味着这就是第一页
func (p Pagination) PrevPage() uint64 {
	hasPrev := p.HasPrev()

	if !hasPrev {
		return 0
	}

	page := p.CurrentPage()
	if page == 0 {
		return 0
	}

	return page - 1
}

// NextPage 下一页码,0 的话就是最后一页
func (p Pagination) NextPage() uint64 {
	hasNext := p.HasNext()
	if !hasNext {
		return 0
	}

	page := p.CurrentPage()
	if page == 0 {
		return 0
	}

	return page + 1
}

// HasPrev 如果当前页不为第一页,就返回 true
func (p Pagination) HasPrev() bool {
	page := p.CurrentPage()
	if page == 0 {
		return false
	}

	return page > 1
}

// TotalPage 返回总页数
func (p Pagination) TotalPage() uint64 {
	count := p.TotalCount()
	if count == 0 {
		return 0
	}

	nums := int64(math.Ceil(float64(count) / float64(p.PerPage)))
	if nums == 0 {
		nums = 1
	}

	return uint64(nums)
}

编写用户仓库代码

创建用户仓库代码目录

mkdir -p pkg/repo
touch pkg/repo/user.go

编写仓库代码

package repo

import (
	baseDb "github.com/869413421/micro-service/common/pkg/db"
	"github.com/869413421/micro-service/common/pkg/pagination"
	"github.com/869413421/micro-service/user/pkg/model"
	"gorm.io/gorm"
)

// UserRepositoryInterface 用户CURD仓库接口
type UserRepositoryInterface interface {
	GetFirst(where map[string]interface{}) (*model.User, error)
	GetByID(uint642 uint64) (*model.User, error)
	GetByEmail(email string) (*model.User, error)
	Pagination(page uint64, perPage uint32) (users []model.User, viewData pagination.ViewData, err error)
}

// UserRepository 用户仓库
type UserRepository struct {
	Db *gorm.DB
}

// NewUserRepository 初始化仓库
func NewUserRepository() UserRepositoryInterface {
	db := baseDb.GetDB()
	return &UserRepository{Db: db}
}

// GetByID 根据ID获取用户
func (repo UserRepository) GetByID(id uint64) (*model.User, error) {
	user := &model.User{}
	err := repo.Db.First(&user, id).Error
	return user, err
}

// Pagination 获取分页数据
func (repo UserRepository) Pagination(page uint64, perPage uint32) (users []model.User, viewData pagination.ViewData, err error) {
	//1.初始化分页实例
	DB := repo.Db.Model(model.User{}).Order("created_at desc")
	_pager := pagination.New(DB, page, perPage)

	// 2. 获取分页构建数据
	viewData = _pager.Paging()

	// 3. 获取数据
	_pager.Results(&users)

	return users, viewData, nil
}

// GetByEmail 根据email获取用户
func (repo UserRepository) GetByEmail(email string) (*model.User, error) {
	user := &model.User{}
	err := repo.Db.Where("email = ?", email).First(&user).Error
	return user, err
}

// GetFirst 根据自定义条件获取用户
func (repo UserRepository) GetFirst(where map[string]interface{}) (*model.User, error) {
	user := &model.User{}
	for key, val := range where {
		repo.Db.Where(key+"=?", val)
	}
	err := repo.Db.First(&user).Error
	return user, err
}

根据依赖倒置原则,我们定义了一个用户抽象的接口,然后编写了接口的实现细节。这种方式能使我们上层模块(即调用用户仓库的类),不再依赖下层(即实现的代码UserRepository)。当后续我们的业务改动,只需要重新实现UserRepositoryInterface就可以直接对实现细节进行替换,在开发中我们应该遵循抽象不应该依赖细节,细节应该依赖抽象的方式来实现功能。

修改model/user.go

package model

import (
	db "github.com/869413421/micro-service/common/pkg/db"
	pb "github.com/869413421/micro-service/user/proto/user"
)

// User 用户模型
type User struct {
	db.BaseModel
	Name     string `gorm:"column:name;type:varchar(255);not null;unique;default:''" valid:"name"`
	Email    string `gorm:"column:email;type:varchar(255) not null;unique;default:''" valid:"email"`
	RealName string `gorm:"column:real_name;type:varchar(255);not null;default:''" valid:"realName"`
	Avatar   string `gorm:"column:avatar;type:varchar(255);not null;default:''" valid:"avatar"`
	Status   int    `gorm:"column:status;type:tinyint(1);not null;default:0" `
	Password string `gorm:"column:password;type:varchar(255) not null;;default:''" valid:"password"`
}

// ToORM protobuf转换为orm
func ToORM(protoUser *pb.User) *User {
	user := &User{}
	user.ID = protoUser.Id
	user.Email = protoUser.Email
	user.Name = protoUser.Name
	user.Avatar = protoUser.Avatar
	user.RealName = protoUser.RealName
	return user
}

// ToProtobuf orm转换为protobuf
func (model *User) ToProtobuf() *pb.User {
	user := &pb.User{}
	user.Id = model.ID
	user.Email = model.Email
	user.Name = model.Name
	user.Avatar = model.Avatar
	user.CreateAt = model.CreatedAtDate()
	user.UpdateAt = model.UpdatedAtDate()
	user.RealName = model.RealName
	return user
}

// Store 创建用户
func (model *User) Store() (err error) {
	result := db.GetDB().Create(&model)
	err = result.Error
	if err != nil {
		return err
	}
	return nil
}

// Update 更新用户
func (model *User) Update() (rowsAffected int64, err error) {
	result := db.GetDB().Save(&model)
	err = result.Error
	if err != nil {
		return 0, err
	}
	rowsAffected = result.RowsAffected
	return
}

// Delete 删除用户
func (model User) Delete() (rowsAffected int64, err error) {
	result := db.GetDB().Delete(&model)
	err = result.Error
	if err != nil {
		return
	}
	rowsAffected = result.RowsAffected
	return
}

添加模型事件,加密用户密码

在储存用户到数据库时,我们的密码不应该以明文的方式进行存储,我们这里利用gorm提供的模型事件,在用户信息进入数据库之前,对密码进行一次加密再存储。

打开common项目,封装一个加密工具包,把加密相关的工具方法放到这个目录下

mkdir -p pkg/password
touch pkg/password/password.go
package password

import (
	"crypto/md5"
	"encoding/hex"
	"golang.org/x/crypto/bcrypt"
)

// Hash hash加密
func Hash(password string) (string, error) {
	bytes, err := bcrypt.GenerateFromPassword([]byte(password), 14)
	if err != nil {
		return "", err
	}

	return string(bytes), nil
}

//CheckHash 检查密码是否与hash值匹配
func CheckHash(password string, hash string) bool {
	err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
	return err == nil
}

// IsHashed 检查是否已经加密过
func IsHashed(str string) bool {
	return len(str) == 60
}

// Md5Str 获取一个md5加密字符串
func Md5Str(str string) string {
	h := md5.New()
	h.Write([]byte(str))
	return hex.EncodeToString(h.Sum(nil))
}

执行go mod tidy下载加密相关包返回user项目,编写模型事件代码

touch pkg/model/user_hooks.go
package model

import (
	"github.com/869413421/micro-service/common/pkg/password"
	"gorm.io/gorm"
)

// BeforeSave 保存前模型事件
func (model *User) BeforeSave(tx *gorm.DB) (err error) {
	//1.如果密码没加密,进行一次加密
	if !password.IsHashed(model.Password) {
		model.Password, err = password.Hash(model.Password)
		if err!=nil{
			return err
		}
	}
	return nil
}

实现服务处理

前面我们对rpc接口进行了定义并且生成了相对应的通讯代码。我们只是整个服务经行了声明,但并没有对服务进行实现。打开user项目

修改handler/user.go

package handler

import (
	"context"
	"github.com/869413421/micro-service/common/pkg/types"
	"github.com/869413421/micro-service/user/pkg/model"
	"github.com/869413421/micro-service/user/pkg/repo"
	pb "github.com/869413421/micro-service/user/proto/user"
	"github.com/micro/go-micro/v2/errors"
	"gorm.io/gorm"
)

//UserServiceHandler 用户服务处理器
type UserServiceHandler struct {
	UserRepo repo.UserRepositoryInterface
}

// NewUserServiceHandler 用户服务初始化
func NewUserServiceHandler() *UserServiceHandler {
	return &UserServiceHandler{
		UserRepo: repo.NewUserRepository(),
	}
}

// Pagination 分页
func (srv *UserServiceHandler) Pagination(ctx context.Context, req *pb.PaginationRequest, rsp *pb.PaginationResponse) error {
	// 1.查找分页数据
	users, pagerData, err := srv.UserRepo.Pagination(req.Page, req.PerPage)
	if err != nil {
		return errors.InternalServerError("user.Pagination.Pagination.Error", err.Error())
	}

	// 2.构造用户列表
	userItems := make([]*pb.User, len(users))
	for index, user := range users {
		userItem := user.ToProtobuf()
		userItems[index] = userItem
	}

	// 3.返回用户信息
	rsp.Users = userItems
	rsp.Total = pagerData.TotalCount
	return nil
}

// Get 根据ID获取数据
func (srv *UserServiceHandler) Get(ctx context.Context, req *pb.GetRequest, rsp *pb.UserResponse) error {
	// 1.查找用户
	user, err := srv.UserRepo.GetByID(req.GetId())
	if err != nil && err != gorm.ErrRecordNotFound {
		return err
	}
	if err == gorm.ErrRecordNotFound {
		return errors.BadRequest("User.GetByID", "user not found")
	}

	// 2.返回用户信息
	rsp.User = user.ToProtobuf()
	return nil
}

// Create 创建用户
func (srv *UserServiceHandler) Create(ctx context.Context, req *pb.CreateRequest, rsp *pb.UserResponse) error {
	// 1.填充提交信息
	user := &model.User{}
	types.Fill(user, req)

	// 2.创建用户
	err := user.Store()
	if err != nil {
		return err
	}

	// 3.返回用户信息
	rsp.User = user.ToProtobuf()
	return nil
}

// Update 更新用户信息
func (srv *UserServiceHandler) Update(ctx context.Context, req *pb.UpdateRequest, rsp *pb.UserResponse) error {
	// 1.获取用户
	id := req.Id
	_user, err := srv.UserRepo.GetByID(id)
	if err != nil && err != gorm.ErrRecordNotFound {
		return err
	}
	if err == gorm.ErrRecordNotFound {
		return errors.NotFound("User.Update.GetUserByID.Error", "user not found ,check you request id")
	}

	// 2.验证提交信息
	types.Fill(_user, req)

	// 3.更新用户
	rowsAffected, err := _user.Update()
	if rowsAffected == 0 || err != nil {
		return errors.InternalServerError("User.Update.Update.Error", err.Error())
	}

	// 4.返回更新信息
	rsp.User = _user.ToProtobuf()
	return nil
}

// Delete 删除用户
func (srv *UserServiceHandler) Delete(ctx context.Context, req *pb.DeleteRequest, rsp *pb.UserResponse) error {
	// 1.获取用户
	id := req.Id
	_user, err := srv.UserRepo.GetByID(id)
	if err != nil && err != gorm.ErrRecordNotFound {
		return err
	}
	if err == gorm.ErrRecordNotFound {
		return errors.NotFound("User.Delete.GetUserByID.Error", "user not found ,check you request id")
	}

	// 2.删除用户
	rowsAffected, err := _user.Delete()
	if err != nil {
		return errors.InternalServerError("User.Delete.Delete.Error", err.Error())
	}
	if rowsAffected == 0 {
		return errors.BadRequest("User.Delete.Delete.Fail", "update fail")
	}

	// 3.返回更新信息
	rsp.User = _user.ToProtobuf()
	return nil
}

修改main.go

package main

import (
	"github.com/869413421/micro-service/common/pkg/db"
	"github.com/869413421/micro-service/user/handler"
	"github.com/869413421/micro-service/user/pkg/model"
	"github.com/micro/go-micro/v2"
	log "github.com/micro/go-micro/v2/logger"

	proto "github.com/869413421/micro-service/user/proto/user"
)

func main() {

	// 1.准备数据库连接,并且执行数据库迁移
	db := db.GetDB()
	db.AutoMigrate(&model.User{})

	// 2.创建服务
	service := micro.NewService(
		micro.Name("micro.service.user"),
		micro.Version("v1"),
	)

	// 3.初始化服务
	service.Init()

	// 4.注册服务处理器
	proto.RegisterUserServiceHandler(service.Server(),handler.NewUserServiceHandler())

	// 5.运行服务
	if err := service.Run(); err != nil {
		log.Fatal(err)
	}
}

编译服务

go mod download
make build

测试用户服务是否正常运行

重启服务

docker-compose restart micro-user-service

检查注册的服务方法

打开http://127.0.0.1:8082/service/micro.service.userimage.png可以看到实现的rpc接口已经注册

测试接口

点击client,选择相关服务以及方法,输入请求参数,对接口进行测试image.png可以看到返回了正常信息,顺便检查数据库用户密码是否加密image.png依次对其他接口进行测试,保证编写的代码正常运行,提交代码到github