Go-kratos 框架商城微服务实战之用户服务 (三) 实现 RPC

Go-kratos 框架微服务商城实战之用户服务 (三)

这篇主要编写第一篇写的用户服务的 rpc 接口。文章写的不清晰的地方可通过 GitHub 源码 查看, 也感谢您指出不足之处。

注:横排 … 为代码省略,为了保持文章的篇幅简洁,我会将一些不必要的代码使用横排的 . 来代替,如果你在复制本文代码块的时候,切记不要将 . 也一同复制进去。

新增 RPC 接口

注:这里的目录指的是 kratos-shop/service/user/api/user/v1/ , 根目录指代的是 user 目录

编辑 user.proto 文件

...


service User{
  rpc CreateUser(CreateUserInfo) returns (UserInfoResponse){}; // 创建用户
  rpc GetUserList(PageInfo) returns (UserListResponse){}; // 用户列表
  rpc GetUserByMobile(MobileRequest) returns (UserInfoResponse){}; // 通过 mobile 查询用户
  rpc GetUserById(IdRequest) returns (UserInfoResponse){}; // 通过 Id 查询用户
  rpc UpdateUser(UpdateUserInfo) returns (google.protobuf.Empty){}; // 更新用户
  rpc CheckPassword(PasswordCheckInfo) returns (CheckResponse){}; // 验证用户密码
}

...

// 用户列表
message UserListResponse{
  int32 total = 1;
  repeated UserInfoResponse data = 2;
}

// 分页
message PageInfo{
  uint32 pn = 1;
  uint32 pSize = 2;
}

message MobileRequest{
  string mobile = 1;
}

message IdRequest{
  int64 id = 1;
}

message  UpdateUserInfo{
  int64 id = 1;
  string nickName = 2;
  string gender = 3;
  uint64 birthday = 4;
}

message PasswordCheckInfo{
  string password = 1;
  string encryptedPassword = 2;
}

message CheckResponse{
  bool success = 1;
}

重新生成 proto ,根目录执行 make api 命令

实现接口

注:这里是一次性把 proto 定义的 rpc 方法全部实现了,并没有分开编写

实现 RPC service 接口

注:这里的目录指的是 kratos-shop/service/user/internal/service

修改 user.go 文件

package service
...

// GetUserList .
func (u *UserService) GetUserList(ctx context.Context, req *v1.PageInfo) (*v1.UserListResponse, error) {
    list, total, err := u.uc.List(ctx, int(req.Pn), int(req.PSize))
    if err != nil {
        return nil, err
    }
    rsp := &v1.UserListResponse{}
    rsp.Total = int32(total)

    for _, user := range list {
        userInfoRsp := UserResponse(user)
        rsp.Data = append(rsp.Data, &userInfoRsp)
    }

    return rsp, nil
}

func UserResponse(user *biz.User) v1.UserInfoResponse {
    userInfoRsp := v1.UserInfoResponse{
        Id:       user.ID,
        Mobile:   user.Mobile,
        Password: user.Password,
        NickName: user.NickName,
        Gender:   user.Gender,
        Role:     int32(user.Role),
    }
    if user.Birthday != nil {
        userInfoRsp.Birthday = uint64(user.Birthday.Unix())
    }
    return userInfoRsp
}

// GetUserByMobile .
func (u *UserService) GetUserByMobile(ctx context.Context, req *v1.MobileRequest) (*v1.UserInfoResponse, error) {
    user, err := u.uc.UserByMobile(ctx, req.Mobile)
    if err != nil {
        return nil, err
    }
    rsp := UserResponse(user)
    return &rsp, nil
}

// UpdateUser .
func (u *UserService) UpdateUser(ctx context.Context, req *v1.UpdateUserInfo) (*emptypb.Empty, error) {
    birthDay := time.Unix(int64(req.Birthday), 0)
    user, err := u.uc.UpdateUser(ctx, &biz.User{
        ID:       req.Id,
        Gender:   req.Gender,
        Birthday: &birthDay,
        NickName: req.NickName,
    })

    if err != nil {
        return nil, err
    }

    if user == false {
        return nil, err
    }

    return &empty.Empty{}, nil
}

// CheckPassword .
func (u *UserService) CheckPassword(ctx context.Context, req *v1.PasswordCheckInfo) (*v1.CheckResponse, error) {
    check, err := u.uc.CheckPassword(ctx, req.Password, req.EncryptedPassword)
    if err != nil {
        return nil, err
    }
    return &v1.CheckResponse{Success: check}, nil
}

// GetUserById .
func (u *UserService) GetUserById(ctx context.Context, req *v1.IdRequest) (*v1.UserInfoResponse, error) {
    user, err := u.uc.UserById(ctx, req.Id)
    if err != nil {
        return nil, err
    }
    rsp := UserResponse(user)
    return &rsp, nil
}

实现 biz 层方法

注:这里的目录指的是 kratos-shop/service/user/internal/biz ,实现 service 调用的方法并声明好 repo 接口方法,repo 声明的方法需要在 data 层去实现

修改 user.go 文件

package biz

....
//go:generate mockgen -destination=../mocks/mrepo/user.go -package=mrepo . UserRepo
type UserRepo interface {
    CreateUser(context.Context, *User) (*User, error)
    ListUser(ctx context.Context, pageNum, pageSize int) ([]*User, int, error)
    UserByMobile(ctx context.Context, mobile string) (*User, error)
    GetUserById(ctx context.Context, id int64) (*User, error)
    UpdateUser(context.Context, *User) (bool, error)
    CheckPassword(ctx context.Context, password, encryptedPassword string) (bool, error)
}

...


func (uc *UserUsecase) List(ctx context.Context, pageNum, pageSize int) ([]*User, int, error) {
    return uc.repo.ListUser(ctx, pageNum, pageSize)
}

func (uc *UserUsecase) UserByMobile(ctx context.Context, mobile string) (*User, error) {
    return uc.repo.UserByMobile(ctx, mobile)
}

func (uc *UserUsecase) UpdateUser(ctx context.Context, user *User) (bool, error) {
    return uc.repo.UpdateUser(ctx, user)
}

func (uc *UserUsecase) CheckPassword(ctx context.Context, password, encryptedPassword string) (bool, error) {
    return uc.repo.CheckPassword(ctx, password, encryptedPassword)
}

func (uc *UserUsecase) UserById(ctx context.Context, id int64) (*User, error) {
    return uc.repo.GetUserById(ctx, id)
}

实现 data 层方法

注:这里的目录指的是 kratos-shop/service/user/internal/data ,实现 biz 层定义 repo interface 接口方法,具体去操作数据库

修改 user.go 文件

// ListUser .
func (r *userRepo) ListUser(ctx context.Context, pageNum, pageSize int) ([]*biz.User, int, error) {
    var users []User
    result := r.data.db.Find(&users)
    if result.Error != nil {
        return nil, 0, result.Error
    }
    total := int(result.RowsAffected)
    r.data.db.Scopes(paginate(pageNum, pageSize)).Find(&users)
    rv := make([]*biz.User, 0)
    for _, u := range users {
        rv = append(rv, &biz.User{
            ID:       u.ID,
            Mobile:   u.Mobile,
            Password: u.Password,
            NickName: u.NickName,
            Gender:   u.Gender,
            Role:     u.Role,
            Birthday: u.Birthday,
        })
    }
    return rv, total, nil
}

// paginate 分页
func paginate(page, pageSize int) func(db *gorm.DB) *gorm.DB {
    return func(db *gorm.DB) *gorm.DB {
        if page == 0 {
            page = 1
        }

        switch {
        case pageSize > 100:
            pageSize = 100
        case pageSize <= 0:
            pageSize = 10
        }

        offset := (page - 1) * pageSize
        return db.Offset(offset).Limit(pageSize)
    }
}

// UserByMobile .
func (r *userRepo) UserByMobile(ctx context.Context, mobile string) (*biz.User, error) {
    var user User
    result := r.data.db.Where(&User{Mobile: mobile}).First(&user)
    if result.Error != nil {
        return nil, result.Error
    }

    if result.RowsAffected == 0 {
        return nil, status.Errorf(codes.NotFound, "用户不存在")
    }
    re := modelToResponse(user)
    return &re, nil
}

// UpdateUser .
func (r *userRepo) UpdateUser(ctx context.Context, user *biz.User) (bool, error) {
    var userInfo User
    result := r.data.db.Where(&User{ID: user.ID}).First(&userInfo)
    if result.RowsAffected == 0 {
        return false, status.Errorf(codes.NotFound, "用户不存在")
    }

    userInfo.NickName = user.NickName
    userInfo.Birthday = user.Birthday
    userInfo.Gender = user.Gender

    res := r.data.db.Save(&userInfo)
    if res.Error != nil {
        return false, status.Errorf(codes.Internal, res.Error.Error())
    }

    return true, nil
}

// CheckPassword .
func (r *userRepo) CheckPassword(ctx context.Context, psd, encryptedPassword string) (bool, error) {
    options := &password.Options{SaltLen: 16, Iterations: 10000, KeyLen: 32, HashFunction: sha512.New}
    passwordInfo := strings.Split(encryptedPassword, "$")
    check := password.Verify(psd, passwordInfo[2], passwordInfo[3], options)
    return check, nil
}

// GetUserById .
func (r *userRepo) GetUserById(ctx context.Context, Id int64) (*biz.User, error) {
    var user User
    result := r.data.db.Where(&User{ID: Id}).First(&user)
    if result.Error != nil {
        return nil, result.Error
    }

    if result.RowsAffected == 0 {
        return nil, status.Errorf(codes.NotFound, "用户不存在")
    }
    re := modelToResponse(user)
    return &re, nil
}

编写测试代码

编写 data 层的测试代码

service/user/internal/data 目录

user_test.go 文件新增内容:

package data_test

import (
   . "github.com/onsi/ginkgo"
   . "github.com/onsi/gomega"
   "time"
   "user/internal/biz"
   "user/internal/data"
   "user/internal/testdata"
)

var _ = Describe("User", func() {
   var ro biz.UserRepo
   var uD *biz.User
   BeforeEach(func() {
       ro = data.NewUserRepo(Db, nil)
       // 这里你可以不引入外部组装好的数据,可以在这里直接写
       uD = testdata.User()
   })
   // 设置 It 块来添加单个规格
   It("CreateUser", func() {
       u, err := ro.CreateUser(ctx, uD)
       Ω(err).ShouldNot(HaveOccurred())
       // 组装的数据 mobile 为 13509876789
       Ω(u.Mobile).Should(Equal("13509876789")) // 手机号应该为创建的时候写入的手机号
   })
   // 设置 It 块来添加单个规格
   It("ListUser", func() {
       user, total, err := ro.ListUser(ctx, 1, 10)
       Ω(err).ShouldNot(HaveOccurred()) // 获取列表不应该出现错误
       Ω(user).ShouldNot(BeEmpty())     // 结果不应该为空
       Ω(total).Should(Equal(1))        // 总数应该为 1,因为上面只创建了一条
       Ω(len(user)).Should(Equal(1))
       Ω(user[0].Mobile).Should(Equal("13509876789"))
   })

   // 设置 It 块来添加单个规格
   It("UpdateUser", func() {
       birthDay := time.Unix(int64(693646426), 0)
       uD.NickName = "gyl"
       uD.Birthday = &birthDay
       uD.Gender = "female"
       user, err := ro.UpdateUser(ctx, uD)
       Ω(err).ShouldNot(HaveOccurred()) // 更新不应该出现错误
       Ω(user).Should(BeTrue())         // 结果应该为 true
   })

   It("CheckPassword", func() {
       p1 := "admin"
       encryptedPassword := "$pbkdf2-sha512$5p7doUNIS9I5mvhA$b18171ff58b04c02ed70ea4f39bda036029c107294bce83301a02fb53a1bcae0"
       password, err := ro.CheckPassword(ctx, p1, encryptedPassword)
       Ω(err).ShouldNot(HaveOccurred()) // 密码验证通过
       Ω(password).Should(BeTrue())     // 结果应该为true

       encryptedPassword1 := "$pbkdf2-sha512$5p7doUNIS9I5mvhA$b18171ff58b04c02ed70ea4f39"
       password1, err := ro.CheckPassword(ctx, p1, encryptedPassword1)
       if err != nil {
           return
       }
       Ω(err).ShouldNot(HaveOccurred())
       Ω(password1).Should(BeFalse()) // 密码验证不通过
   })
})

执行 go test 命令,测试 user_test.go

可以看到测试全部通过。这一篇并么有写 biz 的测试,可以自己完善一下 biz 的测试方法。具体执行测试的方法,可看上一篇。

结束语

至此 user 服务的一些接口基本全部写完了,再有关于用户表信息的需求,可以按照此篇进行增加。下一篇先不完善用户地址之类的需求接口了,先去写 HTTP API 端去来调用这个 user 服务,到时候就把整个服务的基本雏形完成了。

感谢您的耐心阅读,动动手指点个 star 吧。笔芯

感谢您的耐心阅读,动动手指点个赞吧。

本作品采用《CC 协议》,转载必须注明作者和本文链接
微信搜索:上帝喜爱笨人
讨论数量: 3

file

按文章来有些方法没有,去github对比找了一下,比如 modelToResponse

还有testdata文件夹没有数据没有提到,应该是mock的数据

2年前 评论
Aliliin (楼主) 2年前

biz.User 中Birthday字段 应该改为*time.Time

1年前 评论

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!