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 协议》,转载必须注明作者和本文链接
推荐文章: