同学们好!成功登录《我家租房网》系统的用户,可以点击位于搜索页面右上角的用户名,进入用户页面。点击用户页面个人信息栏中的“修改”,会打开修改个人信息的页面,在这里可以上传头像也可以更新用户名。这节课我们将着手实现上传头像的功能。
我们将首先实现用于上传头像的后端微服务,再在前端服务器中添加路由处理函数并远程调用后端微服务,最后再将前后端连在一起做完整的功能测试,并就所发现的问题给出解决方案。后续课程将继续优化这部分功能。
首先我们来实现后端服务器。
第一步是创建特定的微服务。
创建微服务的过程还是和先前一样,直接在back-end工程目录下执行go-micro命令,子命令为new,参数为service,微服务名为UploadAvatar。
接着,需要修改一下go-micro为我们自动生成的接口描述。在back-end工程目录下的UploadAvatar微服务目录中,有一个名为proto的子目录,这就是存放接口描述脚本及其Go代码文件的地方。现阶段里面只有一个名为UploadAvatar.proto的ProtoBuf脚本文件。我们需要对该文件做一些修改:
x1...
2message CallRequest {
3 string username = 1;
4 bytes avatar = 2;
5 string suffix = 3;
6}
7
8message Avatar {
9 string avatar_url = 1;
10}
11
12message CallResponse {
13 string errno = 1;
14 string errmsg = 2;
15 Avatar data = 3;
16}
17...
这段代码描述的是,前端服务器和后端微服务之间的数据交换格式。CallRequest表示前端提供给后端的请求数据,其中包含字符串形式的用户名、字节序列形式的头像数据,和字符串形式的头像文件名后缀。CallResponse表示后端返回给前端的响应数据,其中包含错误代码、错误描述和头像信息。头像信息中只含有一个字段,即头像图片的URL。
有了用ProtoBuf语言描述的远程调用接口,我们还需要把它编译成基于Go语言的程序代码。在UploadAvatar微服务目录中执行make命令,并携带四个目标参数,它们是init、proto、update和tidy。
在UploadAvatar微服务目录下,创建一个名为model的子目录,表示该微服务的模型层。在该子目录中创建mysql.go文件,封装所有与操作MySQL数据库有关的代码。在实际编写数据库访问代码之前,我们可以先把之前为前端服务器编写的mysql.go文件中的内容原封不动地复制到这里:
xxxxxxxxxx
1011package model
2
3import (
4 _ "github.com/go-sql-driver/mysql"
5 "github.com/jinzhu/gorm"
6 "time"
7)
8
9// 用户表
10type User struct {
11 ID uint `json:"id"` // 用户ID
12 Name string `json:"name" gorm:"size:32;unique"` // 用户名
13 Password_hash string `json:"password_hash" gorm:"size:128"` // 密码哈希
14 Mobile string `json:"mobile" gorm:"size:11;unique"` // 手机号
15 Real_name string `json:"real_name" gorm:"size:32"` // 真实姓名
16 Id_card string `json:"id_card" gorm:"size:20"` // 身份证号
17 Avatar_url string `json:"avatar_url" gorm:"size:256"` // 头像URL
18 Houses []*House `json:"houses"` // 房屋集
19 Orders []*Order `json:"orders"` // 订单集
20}
21
22// 房屋表
23type House struct {
24 gorm.Model
25 User_id uint `json:"user_id"` // 房主用户ID
26 Area_id uint `json:"area_id"` // 归属地区ID
27 Title string `json:"title" gorm:"size:64"` // 标题
28 Address string `json:"address" gorm:"size:512"` // 地址
29 Room_count int `json:"room_count" gorm:"default:1"` // 房间数
30 Acreage int `json:"acreage" gorm:"default:0"` // 面积
31 Price int `json:"price"` // 价格
32 Unit string `json:"unit" gorm:"size:32;default:''"` // 房间配置
33 Capacity int `json:"capacity" gorm:"default:1"` // 容纳人数
34 Beds string `json:"beds" gorm:"size:64;default:''"` // 床铺配置
35 Deposit int `json:"deposit" gorm:"default:0"` // 押金
36 Min_days int `json:"min_days" gorm:"default:1"` // 最少入住天数
37 Max_days int `json:"max_days" gorm:"default:0"` // 最多入住天数
38 Order_count int `json:"order_count" gorm:"default:0"` // 预定订单数
39 Index_image_url string `json:"index_image_url" gorm:"size:256;default:''"` // 主图片URL
40 Facilities []*Facility `json:"facilities" gorm:"many2many:houses_facilities"` // 设施集
41 Images []*Image `json:"images"` // 图片集
42 Orders []*Order `json:"orders"` // 订单集
43}
44
45// 城区表
46type Area struct {
47 ID uint `json:"id"` // 城区ID
48 Name string `json:"name" gorm:"size:32"` // 城区名
49 Houses []*House `json:"houses"` // 房屋集
50}
51
52// 设施表
53type Facility struct {
54 ID uint `json:"id"` // 设施ID
55 Name string `json:"name" gorm:"size:32"` // 设施名
56 Houses []*House `json:"houses"` // 房屋集
57}
58
59// 图片表
60type Image struct {
61 ID uint `json:"id"` // 图片ID
62 Url string `json:"url" gorm:"size:256"` // 图片URL
63 House_id uint `json:"house_id"` // 所属房屋ID
64}
65
66// 订单表
67type Order struct {
68 gorm.Model
69 User_id uint `json:"user_id"` // 下单用户ID
70 House_id uint `json:"house_id"` // 预定房屋ID
71 Begin_date time.Time `json:"begin_date" gorm:"type:datetime"` // 预定起始日
72 End_date time.Time `json:"end_date" gorm:"type:datetime"` // 预定终止日
73 Days int `json:"days"` // 预定天数
74 House_price int `json:"house_price"` // 房屋价格
75 Amount int `json:"amount"` // 总金额
76 Status string `json:"status" gorm:"default:'WAIT_ACCEPT'"` // 状态
77 Comment string `json:"comment" gorm:"size:512"` // 评论
78 Credit bool `json:"credit"` // 征信良好
79}
80
81var db *gorm.DB // 数据库连接池
82
83// 初始化数据库
84func InitDB() error {
85 var err error
86
87 // 连接数据库
88 if db, err = gorm.Open("mysql",
89 "root:123456@tcp(127.0.0.1:3306)/ihomedb?"+
90 "charset=utf8&parseTime=True&loc=Local"); err == nil {
91 // 设置最大连接数和最大空闲连接数
92 db.DB().SetMaxOpenConns(100)
93 db.DB().SetMaxIdleConns(10)
94
95 // 创建表
96 return db.AutoMigrate(new(User), new(House),
97 new(Area), new(Facility), new(Image), new(Order)).Error
98 }
99
100 return err
101}
修改UploadAvatar微服务目录中的main.go文件,添加对InitDB函数的调用,将服务发现改为Consul,绑定IP地址和端口:
xxxxxxxxxx
161...
2func main() {
3 // 初始化数据库
4 if err := model.InitDB(); err != nil {
5 logger.Fatal(err)
6 }
7 ...
8 srv.Init(
9 micro.Name(service),
10 micro.Version(version),
11 micro.Registry(consul.NewRegistry()),
12 micro.Address("127.0.0.1:9008"),
13 )
14 ...
15}
16...
程序启动伊始,即完成对数据库的初始化。其中包括两个动作,其一是连接ihomedb数据库并获得连接池对象,其二是在该数据库中创建表。当然,如果这些表已经存在,则不再创建。接着,在服务器对象的初始化部分,指示其注册到Consul服务器,同时绑定本机的9008端口。
在完成UploadAvatar微服务模型层的所有开发工作之后,我们将着手编写用于处理上传头像业务的代码。
打开UploadAvatar微服务目录下,handler子目录中的UploadAvatar.go文件,在其中的Call方法里,添加与上传头像有关的操作:
xxxxxxxxxx
101...
2// 上传头像
3func (e *UploadAvatar) Call(ctx context.Context, req *pb.CallRequest, rsp *pb.CallResponse) error {
4 logger.Infof("Received UploadAvatar.Call request: %v", req)
5
6 fmt.Println(req.Username, len(req.Avatar), req.Suffix)
7
8 return nil
9}
10...
这里仅仅是简单地打印了包含在请求对象中的用户名、头像数据字节数,和头像文件名后缀。
在完成UploadAvatar后端服务器的开发后,我们需要为前端服务器添加一条路由,并在路由处理函数中完成对后端微服务远程方法的调用。
前端和后端共用同一套接口描述。
因此,这里我们将UploadAvatar微服务目录下proto子目录中的所有文件,原封不动地复制到front-end工程目录proto子目录下的UploadAvatar目录中。
这里我们要为上传头像添加一条路由,POST方法结合/api/v1.0/user/avatar路径,处理函数名为UploadAvatar。
在front-end工程目录下的main.go文件中添加一条路由:
xxxxxxxxxx
91...
2func main() {
3 ...
4 // 路由匹配
5 ...
6 apiv10.POST("/user/avatar", controller.UploadAvatar)
7 ...
8}
9...
这里我们调用了路由对象的POST方法,为上传头像添加了一条路由,将POST方法结合/user/avatar路径,路由到controller包的UploadAvatar函数。将UploadAvatar函数定义在controller包里是因为该函数的主要任务是执行业务逻辑,属于MVC中的C,即控制器层的部分。
当然,在controller包里真的得有UploadAvatar函数。为此,我们打开front-end工程目录下controller子目录中的user.go文件。在该文件的import部分增加一行,同时定义UploadAvatar函数:
xxxxxxxxxx
591...
2import (
3 ...
4 pbUploadAvatar "iHome/front-end/proto/UploadAvatar"
5 ...
6)
7...
8// 上传头像
9func UploadAvatar(ctx *gin.Context) {
10 response := make(map[string]string)
11
12 // 从Session中读取用户名
13 username := model.ReadUsername(ctx)
14
15 fileHeader, err := ctx.FormFile("avatar")
16 if err != nil {
17 fmt.Println(err)
18 response["errno"] = utils.ERROR_IO
19 response["errmsg"] = utils.StrError(response["errno"])
20 ctx.JSON(http.StatusOK, response)
21 return
22 }
23
24 file, err := fileHeader.Open()
25 if err != nil {
26 fmt.Println(err)
27 response["errno"] = utils.ERROR_IO
28 response["errmsg"] = utils.StrError(response["errno"])
29 ctx.JSON(http.StatusOK, response)
30 return
31 }
32
33 avatar := make([]byte, fileHeader.Size)
34 _, err = file.Read(avatar)
35 if err != nil {
36 fmt.Println(err)
37 response["errno"] = utils.ERROR_IO
38 response["errmsg"] = utils.StrError(response["errno"])
39 ctx.JSON(http.StatusOK, response)
40 return
41 }
42
43 // 调用微服务
44 srv := micro.NewService(micro.Registry(consul.NewRegistry()))
45 srv.Init()
46 c := pbUploadAvatar.NewUploadAvatarService("uploadavatar", srv.Client())
47 rsp, err := c.Call(context.Background(), &pbUploadAvatar.CallRequest{
48 Username: username,
49 Avatar: avatar,
50 Suffix: path.Ext(fileHeader.Filename),
51 })
52 if err != nil {
53 fmt.Println(err)
54 return
55 }
56
57 ctx.JSON(http.StatusOK, rsp)
58}
59...
这段代码首先从Session中读取用户名,接着从请求包体中根据键获取头像文件头对象,并通过该对象的Open方法获得头像文件对象。根据头像文件的大小分配足量的字节切片,将头像文件中的数据读到该切片中。通过Consul服务发现,获取有关名为uploadavatar的微服务的信息,远程调用其中的UploadAvatar对象的Call方法。该方法以包含用户名、头像数据和头像文件名后缀的请求对象为参数,并返回包含错误代码、错误描述和头像信息的响应对象。头像信息中包含头像图片的URL,浏览器将据此显示头像图片。最后将该响应对象序列化为一个JSON字符串,编码到HTTP响应中,回传给浏览器。
至此,我们已经完成上传头像的全部开发工作。下面我们将对这部分功能进行测试。
启动Consul服务器、UserLogin、GetUser、UploadAvatar后端微服务和前端服务器。通过登录页面登录系统,点击位于搜索页面右上角的用户名,进入用户页面,点击个人信息栏中的“修改”,进入修改个人信息的页面,点击“选择文件”,选择图片文件,点击“上传”。观察UploadAvatar后端微服务的日志输出,验证其远程方法的执行是否符合预期。
当然,截至目前,我们的UploadAvatar后端微服务并没有做什么实质性的工作,只是打印了请求对象中的用户名、头像数据字节数,和头像文件名后缀。该微服务也没有向前端返回任何有价值的响应,因此浏览器并没有显示出所上传的头像图片。在完善这部分功能之前,我们需要先思考几个问题。
第一个问题是,后端微服务在得到头像数据后,把它保存在什么地方?存在后端服务器本地,显然并不合适。因为作为一个分布式的服务器系统,执行不同业务的后端微服务,往往被部署在不同的服务器主机上,即使是执行相同业务的微服务,也存在多机冗余部署的情况。谁也无法保证,当前端服务器需要获取头像数据时,为其提供服务的一定是当初处理上传头像业务的那台服务器主机。更妥当的做法是,将头像数据以文件的形式,保存到某个现成的云平台上。象七牛云、阿里云、腾讯云等,都提供类似这种分布式文件存储服务。作为另一种选择,我们也可以自己搭建这样的云平台,FastDFS结合Nginx,就是一种低成本且高度可靠的解决方案。第二个问题是,包含头像数据的文件如何命名?数据完全不同的头像不能相互覆盖,而内容相同的头像也没必要保存多份。针对这类问题,FastDFS就可以很好地解决。这就是我们后面课程需要完成的工作。
谢谢大家,我们下节课再见!