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

本文最后更新于:2 年前

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

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

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

新增 RPC 接口

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

编辑 user.proto 文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
...


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 文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
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 文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
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 文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
// 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 文件新增内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
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 吧。笔芯

关注我获取更新