定义protobuf,生成代码

修改proto/user/user.proto

  1. syntax = "proto3";
  2. package micro.service.user;
  3. option go_package = "proto/user";
  4. service UserService {
  5. rpc Pagination(PaginationRequest) returns(PaginationResponse){}
  6. rpc Get(GetRequest) returns(UserResponse){}
  7. rpc Create(CreateRequest) returns(UserResponse){}
  8. rpc Update(UpdateRequest) returns(UserResponse){}
  9. rpc Delete(DeleteRequest) returns(UserResponse){}
  10. }
  11. message User{
  12. uint64 id = 1;
  13. string name = 3;
  14. string email = 4;
  15. string real_name = 6;
  16. string avatar = 7;
  17. string create_at = 9;
  18. string update_at = 10;
  19. }
  20. //UserResponse 单个用户响应
  21. message UserResponse{
  22. User user = 1;
  23. }
  24. //PaginationResponse 用户分页数据响应
  25. message PaginationResponse{
  26. repeated User users = 1;
  27. uint64 total = 2;
  28. }
  29. //PaginationRequest 用户分页请求
  30. message PaginationRequest{
  31. uint64 page = 1;
  32. uint32 perPage = 2;
  33. }
  34. //GetRequest 获取单个用户请求
  35. message GetRequest{
  36. uint64 id = 1;
  37. }
  38. //CreateRequest 创建用户请求
  39. message CreateRequest{
  40. string name = 1;
  41. string password = 2;
  42. string email = 3;
  43. string real_name = 4;
  44. string avatar = 5;
  45. }
  46. //UpdateRequest 更新用户请求
  47. message UpdateRequest{
  48. uint64 id = 1;
  49. string name = 2;
  50. string email = 3;
  51. string real_name = 4;
  52. string avatar = 6;
  53. }
  54. //DeleteRequest 删除用户请求
  55. message DeleteRequest{
  56. uint64 id = 1;
  57. }

执行生成命令

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

  1. make proto

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

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

封装用户数据库交互层

封装分页工具

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

创建分页工具包目录

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

编写分页工具代码

  1. package pagination
  2. import (
  3. "gorm.io/gorm"
  4. "math"
  5. )
  6. // Page 单个分页元素
  7. type Page struct {
  8. // 链接
  9. URL string
  10. // 页码
  11. Number uint64
  12. }
  13. // ViewData 同视图渲染的数据
  14. type ViewData struct {
  15. // 是否需要显示分页
  16. HasPages bool
  17. // 下一页
  18. Next Page
  19. HasNext bool
  20. // 上一页
  21. Prev Page
  22. HasPrev bool
  23. Current Page
  24. // 数据库的内容总数量
  25. TotalCount uint64
  26. // 总页数
  27. TotalPage uint64
  28. }
  29. // Pagination 分页对象
  30. type Pagination struct {
  31. PerPage uint32
  32. Page uint64
  33. Count uint64
  34. DB *gorm.DB
  35. }
  36. // New 分页对象构建器
  37. // db —— GORM 查询句柄,用以查询数据集和获取数据总数
  38. // page —— page
  39. // perPage —— 每页条数,传参为小于或者等于 0 时为默认值 10
  40. func New(db *gorm.DB, page uint64, perPage uint32) *Pagination {
  41. // 默认每页数量
  42. if perPage <= 0 {
  43. perPage = 10
  44. }
  45. // 实例对象
  46. p := &Pagination{
  47. DB: db,
  48. PerPage: perPage,
  49. Page: page,
  50. Count: 0,
  51. }
  52. // 设置当前页码
  53. p.SetPage(page)
  54. return p
  55. }
  56. // Paging 返回渲染分页所需的数据
  57. func (p *Pagination) Paging() ViewData {
  58. return ViewData{
  59. HasPages: p.HasPages(),
  60. Next: p.NewPage(p.NextPage()),
  61. HasNext: p.HasNext(),
  62. Prev: p.NewPage(p.PrevPage()),
  63. HasPrev: p.HasPrev(),
  64. Current: p.NewPage(p.CurrentPage()),
  65. TotalPage: p.TotalPage(),
  66. TotalCount: p.Count,
  67. }
  68. }
  69. // NewPage 设置当前页
  70. func (p Pagination) NewPage(page uint64) Page {
  71. return Page{
  72. Number: page,
  73. }
  74. }
  75. // SetPage 设置当前页
  76. func (p *Pagination) SetPage(page uint64) {
  77. if page <= 0 {
  78. page = 1
  79. }
  80. p.Page = page
  81. }
  82. // CurrentPage 返回当前页码
  83. func (p Pagination) CurrentPage() uint64 {
  84. totalPage := p.TotalPage()
  85. if totalPage == 0 {
  86. return 0
  87. }
  88. if p.Page > totalPage {
  89. return totalPage
  90. }
  91. return p.Page
  92. }
  93. // Results 返回请求数据,请注意 data 参数必须为 GROM 模型的 Slice 对象
  94. func (p Pagination) Results(data interface{}) error {
  95. var err error
  96. var offset uint64
  97. page := p.CurrentPage()
  98. if page == 0 {
  99. return err
  100. }
  101. if page > 1 {
  102. offset = (page - 1) * uint64(p.PerPage)
  103. }
  104. return p.DB.Debug().Limit(int(p.PerPage)).Offset(int(offset)).Find(data).Error
  105. }
  106. // TotalCount 返回的是数据库里的条数
  107. func (p *Pagination) TotalCount() uint64 {
  108. if p.Count == 0 {
  109. var count int64
  110. if err := p.DB.Count(&count).Error; err != nil {
  111. return 0
  112. }
  113. p.Count = uint64(count)
  114. }
  115. return p.Count
  116. }
  117. // HasPages 总页数大于 1 时会返回 true
  118. func (p *Pagination) HasPages() bool {
  119. n := p.TotalCount()
  120. return n > uint64(p.PerPage)
  121. }
  122. // HasNext returns true if current page is not the last page
  123. func (p Pagination) HasNext() bool {
  124. totalPage := p.TotalPage()
  125. if totalPage == 0 {
  126. return false
  127. }
  128. page := p.CurrentPage()
  129. if page == 0 {
  130. return false
  131. }
  132. return page < totalPage
  133. }
  134. // PrevPage 前一页码,0 意味着这就是第一页
  135. func (p Pagination) PrevPage() uint64 {
  136. hasPrev := p.HasPrev()
  137. if !hasPrev {
  138. return 0
  139. }
  140. page := p.CurrentPage()
  141. if page == 0 {
  142. return 0
  143. }
  144. return page - 1
  145. }
  146. // NextPage 下一页码,0 的话就是最后一页
  147. func (p Pagination) NextPage() uint64 {
  148. hasNext := p.HasNext()
  149. if !hasNext {
  150. return 0
  151. }
  152. page := p.CurrentPage()
  153. if page == 0 {
  154. return 0
  155. }
  156. return page + 1
  157. }
  158. // HasPrev 如果当前页不为第一页,就返回 true
  159. func (p Pagination) HasPrev() bool {
  160. page := p.CurrentPage()
  161. if page == 0 {
  162. return false
  163. }
  164. return page > 1
  165. }
  166. // TotalPage 返回总页数
  167. func (p Pagination) TotalPage() uint64 {
  168. count := p.TotalCount()
  169. if count == 0 {
  170. return 0
  171. }
  172. nums := int64(math.Ceil(float64(count) / float64(p.PerPage)))
  173. if nums == 0 {
  174. nums = 1
  175. }
  176. return uint64(nums)
  177. }

编写用户仓库代码

创建用户仓库代码目录

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

编写仓库代码

  1. package repo
  2. import (
  3. baseDb "github.com/869413421/micro-service/common/pkg/db"
  4. "github.com/869413421/micro-service/common/pkg/pagination"
  5. "github.com/869413421/micro-service/user/pkg/model"
  6. "gorm.io/gorm"
  7. )
  8. // UserRepositoryInterface 用户CURD仓库接口
  9. type UserRepositoryInterface interface {
  10. GetFirst(where map[string]interface{}) (*model.User, error)
  11. GetByID(uint642 uint64) (*model.User, error)
  12. GetByEmail(email string) (*model.User, error)
  13. Pagination(page uint64, perPage uint32) (users []model.User, viewData pagination.ViewData, err error)
  14. }
  15. // UserRepository 用户仓库
  16. type UserRepository struct {
  17. Db *gorm.DB
  18. }
  19. // NewUserRepository 初始化仓库
  20. func NewUserRepository() UserRepositoryInterface {
  21. db := baseDb.GetDB()
  22. return &UserRepository{Db: db}
  23. }
  24. // GetByID 根据ID获取用户
  25. func (repo UserRepository) GetByID(id uint64) (*model.User, error) {
  26. user := &model.User{}
  27. err := repo.Db.First(&user, id).Error
  28. return user, err
  29. }
  30. // Pagination 获取分页数据
  31. func (repo UserRepository) Pagination(page uint64, perPage uint32) (users []model.User, viewData pagination.ViewData, err error) {
  32. //1.初始化分页实例
  33. DB := repo.Db.Model(model.User{}).Order("created_at desc")
  34. _pager := pagination.New(DB, page, perPage)
  35. // 2. 获取分页构建数据
  36. viewData = _pager.Paging()
  37. // 3. 获取数据
  38. _pager.Results(&users)
  39. return users, viewData, nil
  40. }
  41. // GetByEmail 根据email获取用户
  42. func (repo UserRepository) GetByEmail(email string) (*model.User, error) {
  43. user := &model.User{}
  44. err := repo.Db.Where("email = ?", email).First(&user).Error
  45. return user, err
  46. }
  47. // GetFirst 根据自定义条件获取用户
  48. func (repo UserRepository) GetFirst(where map[string]interface{}) (*model.User, error) {
  49. user := &model.User{}
  50. for key, val := range where {
  51. repo.Db.Where(key+"=?", val)
  52. }
  53. err := repo.Db.First(&user).Error
  54. return user, err
  55. }

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

修改model/user.go

  1. package model
  2. import (
  3. db "github.com/869413421/micro-service/common/pkg/db"
  4. pb "github.com/869413421/micro-service/user/proto/user"
  5. )
  6. // User 用户模型
  7. type User struct {
  8. db.BaseModel
  9. Name string `gorm:"column:name;type:varchar(255);not null;unique;default:''" valid:"name"`
  10. Email string `gorm:"column:email;type:varchar(255) not null;unique;default:''" valid:"email"`
  11. RealName string `gorm:"column:real_name;type:varchar(255);not null;default:''" valid:"realName"`
  12. Avatar string `gorm:"column:avatar;type:varchar(255);not null;default:''" valid:"avatar"`
  13. Status int `gorm:"column:status;type:tinyint(1);not null;default:0" `
  14. Password string `gorm:"column:password;type:varchar(255) not null;;default:''" valid:"password"`
  15. }
  16. // ToORM protobuf转换为orm
  17. func ToORM(protoUser *pb.User) *User {
  18. user := &User{}
  19. user.ID = protoUser.Id
  20. user.Email = protoUser.Email
  21. user.Name = protoUser.Name
  22. user.Avatar = protoUser.Avatar
  23. user.RealName = protoUser.RealName
  24. return user
  25. }
  26. // ToProtobuf orm转换为protobuf
  27. func (model *User) ToProtobuf() *pb.User {
  28. user := &pb.User{}
  29. user.Id = model.ID
  30. user.Email = model.Email
  31. user.Name = model.Name
  32. user.Avatar = model.Avatar
  33. user.CreateAt = model.CreatedAtDate()
  34. user.UpdateAt = model.UpdatedAtDate()
  35. user.RealName = model.RealName
  36. return user
  37. }
  38. // Store 创建用户
  39. func (model *User) Store() (err error) {
  40. result := db.GetDB().Create(&model)
  41. err = result.Error
  42. if err != nil {
  43. return err
  44. }
  45. return nil
  46. }
  47. // Update 更新用户
  48. func (model *User) Update() (rowsAffected int64, err error) {
  49. result := db.GetDB().Save(&model)
  50. err = result.Error
  51. if err != nil {
  52. return 0, err
  53. }
  54. rowsAffected = result.RowsAffected
  55. return
  56. }
  57. // Delete 删除用户
  58. func (model User) Delete() (rowsAffected int64, err error) {
  59. result := db.GetDB().Delete(&model)
  60. err = result.Error
  61. if err != nil {
  62. return
  63. }
  64. rowsAffected = result.RowsAffected
  65. return
  66. }

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

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

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

  1. mkdir -p pkg/password
  2. touch pkg/password/password.go
  1. package password
  2. import (
  3. "crypto/md5"
  4. "encoding/hex"
  5. "golang.org/x/crypto/bcrypt"
  6. )
  7. // Hash hash加密
  8. func Hash(password string) (string, error) {
  9. bytes, err := bcrypt.GenerateFromPassword([]byte(password), 14)
  10. if err != nil {
  11. return "", err
  12. }
  13. return string(bytes), nil
  14. }
  15. //CheckHash 检查密码是否与hash值匹配
  16. func CheckHash(password string, hash string) bool {
  17. err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
  18. return err == nil
  19. }
  20. // IsHashed 检查是否已经加密过
  21. func IsHashed(str string) bool {
  22. return len(str) == 60
  23. }
  24. // Md5Str 获取一个md5加密字符串
  25. func Md5Str(str string) string {
  26. h := md5.New()
  27. h.Write([]byte(str))
  28. return hex.EncodeToString(h.Sum(nil))
  29. }

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

  1. touch pkg/model/user_hooks.go
  1. package model
  2. import (
  3. "github.com/869413421/micro-service/common/pkg/password"
  4. "gorm.io/gorm"
  5. )
  6. // BeforeSave 保存前模型事件
  7. func (model *User) BeforeSave(tx *gorm.DB) (err error) {
  8. //1.如果密码没加密,进行一次加密
  9. if !password.IsHashed(model.Password) {
  10. model.Password, err = password.Hash(model.Password)
  11. if err!=nil{
  12. return err
  13. }
  14. }
  15. return nil
  16. }

实现服务处理

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

修改handler/user.go

  1. package handler
  2. import (
  3. "context"
  4. "github.com/869413421/micro-service/common/pkg/types"
  5. "github.com/869413421/micro-service/user/pkg/model"
  6. "github.com/869413421/micro-service/user/pkg/repo"
  7. pb "github.com/869413421/micro-service/user/proto/user"
  8. "github.com/micro/go-micro/v2/errors"
  9. "gorm.io/gorm"
  10. )
  11. //UserServiceHandler 用户服务处理器
  12. type UserServiceHandler struct {
  13. UserRepo repo.UserRepositoryInterface
  14. }
  15. // NewUserServiceHandler 用户服务初始化
  16. func NewUserServiceHandler() *UserServiceHandler {
  17. return &UserServiceHandler{
  18. UserRepo: repo.NewUserRepository(),
  19. }
  20. }
  21. // Pagination 分页
  22. func (srv *UserServiceHandler) Pagination(ctx context.Context, req *pb.PaginationRequest, rsp *pb.PaginationResponse) error {
  23. // 1.查找分页数据
  24. users, pagerData, err := srv.UserRepo.Pagination(req.Page, req.PerPage)
  25. if err != nil {
  26. return errors.InternalServerError("user.Pagination.Pagination.Error", err.Error())
  27. }
  28. // 2.构造用户列表
  29. userItems := make([]*pb.User, len(users))
  30. for index, user := range users {
  31. userItem := user.ToProtobuf()
  32. userItems[index] = userItem
  33. }
  34. // 3.返回用户信息
  35. rsp.Users = userItems
  36. rsp.Total = pagerData.TotalCount
  37. return nil
  38. }
  39. // Get 根据ID获取数据
  40. func (srv *UserServiceHandler) Get(ctx context.Context, req *pb.GetRequest, rsp *pb.UserResponse) error {
  41. // 1.查找用户
  42. user, err := srv.UserRepo.GetByID(req.GetId())
  43. if err != nil && err != gorm.ErrRecordNotFound {
  44. return err
  45. }
  46. if err == gorm.ErrRecordNotFound {
  47. return errors.BadRequest("User.GetByID", "user not found")
  48. }
  49. // 2.返回用户信息
  50. rsp.User = user.ToProtobuf()
  51. return nil
  52. }
  53. // Create 创建用户
  54. func (srv *UserServiceHandler) Create(ctx context.Context, req *pb.CreateRequest, rsp *pb.UserResponse) error {
  55. // 1.填充提交信息
  56. user := &model.User{}
  57. types.Fill(user, req)
  58. // 2.创建用户
  59. err := user.Store()
  60. if err != nil {
  61. return err
  62. }
  63. // 3.返回用户信息
  64. rsp.User = user.ToProtobuf()
  65. return nil
  66. }
  67. // Update 更新用户信息
  68. func (srv *UserServiceHandler) Update(ctx context.Context, req *pb.UpdateRequest, rsp *pb.UserResponse) error {
  69. // 1.获取用户
  70. id := req.Id
  71. _user, err := srv.UserRepo.GetByID(id)
  72. if err != nil && err != gorm.ErrRecordNotFound {
  73. return err
  74. }
  75. if err == gorm.ErrRecordNotFound {
  76. return errors.NotFound("User.Update.GetUserByID.Error", "user not found ,check you request id")
  77. }
  78. // 2.验证提交信息
  79. types.Fill(_user, req)
  80. // 3.更新用户
  81. rowsAffected, err := _user.Update()
  82. if rowsAffected == 0 || err != nil {
  83. return errors.InternalServerError("User.Update.Update.Error", err.Error())
  84. }
  85. // 4.返回更新信息
  86. rsp.User = _user.ToProtobuf()
  87. return nil
  88. }
  89. // Delete 删除用户
  90. func (srv *UserServiceHandler) Delete(ctx context.Context, req *pb.DeleteRequest, rsp *pb.UserResponse) error {
  91. // 1.获取用户
  92. id := req.Id
  93. _user, err := srv.UserRepo.GetByID(id)
  94. if err != nil && err != gorm.ErrRecordNotFound {
  95. return err
  96. }
  97. if err == gorm.ErrRecordNotFound {
  98. return errors.NotFound("User.Delete.GetUserByID.Error", "user not found ,check you request id")
  99. }
  100. // 2.删除用户
  101. rowsAffected, err := _user.Delete()
  102. if err != nil {
  103. return errors.InternalServerError("User.Delete.Delete.Error", err.Error())
  104. }
  105. if rowsAffected == 0 {
  106. return errors.BadRequest("User.Delete.Delete.Fail", "update fail")
  107. }
  108. // 3.返回更新信息
  109. rsp.User = _user.ToProtobuf()
  110. return nil
  111. }

修改main.go

  1. package main
  2. import (
  3. "github.com/869413421/micro-service/common/pkg/db"
  4. "github.com/869413421/micro-service/user/handler"
  5. "github.com/869413421/micro-service/user/pkg/model"
  6. "github.com/micro/go-micro/v2"
  7. log "github.com/micro/go-micro/v2/logger"
  8. proto "github.com/869413421/micro-service/user/proto/user"
  9. )
  10. func main() {
  11. // 1.准备数据库连接,并且执行数据库迁移
  12. db := db.GetDB()
  13. db.AutoMigrate(&model.User{})
  14. // 2.创建服务
  15. service := micro.NewService(
  16. micro.Name("micro.service.user"),
  17. micro.Version("v1"),
  18. )
  19. // 3.初始化服务
  20. service.Init()
  21. // 4.注册服务处理器
  22. proto.RegisterUserServiceHandler(service.Server(),handler.NewUserServiceHandler())
  23. // 5.运行服务
  24. if err := service.Run(); err != nil {
  25. log.Fatal(err)
  26. }
  27. }

编译服务

  1. go mod download
  2. make build

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

重启服务

  1. docker-compose restart micro-user-service

检查注册的服务方法

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

测试接口

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