同学们好!成功登录《我家租房网》系统的用户,可以点击位于搜索页面右上角的用户名,进入用户页面。点击用户页面中的“我的房源”,会打开我的房源页面。在“发布新房源”的下面,会显示当前用户所发布的房源列表。点击房源列表中的房源条目,会显示房屋详情。这节课我们来实现获取房屋详情的功能。
我们将首先实现支持获取房屋详情的后端微服务,再在前端服务器中添加路由处理函数并远程调用后端微服务,最后再将前后端连在一起做完整的功能测试。
首先我们来实现后端服务器。
第一步是创建特定的微服务。
创建微服务的过程还是和先前一样,直接在back-end工程目录下执行go-micro命令,子命令为new,参数为service,微服务名为GetHouse。
接着,需要修改一下go-micro为我们自动生成的接口描述。在back-end工程目录下的GetHouse微服务目录中,有一个名为proto的子目录,这就是存放接口描述脚本及其Go代码文件的地方。现阶段里面只有一个名为GetHouse.proto的ProtoBuf脚本文件。我们需要对该文件做一些修改:
x1...
2message CallRequest {
3 string username = 1;
4 string houseId = 2;
5}
6
7message Comment {
8 string comment = 1;
9 string ctime = 2;
10 string user_name = 3;
11}
12
13message House {
14 uint64 acreage = 1;
15 string address = 2;
16 string beds = 3;
17 uint64 capacity = 4;
18 repeated Comment comments = 5;
19 uint64 deposit = 6;
20 repeated uint64 facilities = 7;
21 uint64 hid = 8;
22 repeated string img_urls = 9;
23 uint64 max_days = 10;
24 uint64 min_days = 11;
25 uint64 price = 12;
26 uint64 room_count = 13;
27 string title = 14;
28 string unit = 15;
29 string user_avatar = 16;
30 uint64 user_id = 17;
31 string user_name = 18;
32}
33
34message HouseOfUser {
35 House house = 1;
36 uint64 user_id = 2;
37}
38
39message CallResponse {
40 string errno = 1;
41 string errmsg = 2;
42 HouseOfUser data = 3;
43}
44...
这段代码描述的是,前端服务器和后端微服务之间的数据交换格式。CallRequest表示前端提供给后端的请求数据,其中包含两个字符串,用户名和房屋ID。CallResponse表示后端返回给前端的响应数据,其中包含错误代码、错误描述和房屋及浏览用户信息。房屋及浏览用户信息中包含房屋信息和浏览用户ID。房屋信息中包含面积、地址、床铺配置、容纳人数、评论列表、押金、设施ID列表、房屋ID、房屋图片URL列表、最多入住天数、最少入住天数、价格、房间数、标题、房间配置、房主用户头像URL、房主用户ID、房主用户名等共计十八项内容。其中评论列表中的每条评论均包含评论内容、发布时间和下单用户名等三项内容。
有了用ProtoBuf语言描述的远程调用接口,我们还需要把它编译成基于Go语言的程序代码。在GetHouse微服务目录中执行make命令,并携带四个目标参数,它们是init、proto、update和tidy。
在GetHouse微服务目录下,创建一个名为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}
修改GetHouse微服务目录中的main.go文件,添加对InitDB函数的调用,将服务发现改为Consul,绑定IP地址和端口:
xxxxxxxxxx
171...
2func main() {
3 ...
4 // 初始化数据库
5 if err := model.InitDB(); err != nil {
6 logger.Fatal(err)
7 }
8 ...
9 srv.Init(
10 micro.Name(service),
11 micro.Version(version),
12 micro.Registry(consul.NewRegistry()),
13 micro.Address("127.0.0.1:9013"),
14 )
15 ...
16}
17...
程序启动伊始,即完成对数据库的初始化。其中包括两个动作,其一是连接ihomedb数据库并获得连接池对象,其二是在该数据库中创建表。当然,如果这些表已经存在,则不再创建。接着,在服务器对象的初始化部分,指示其注册到Consul服务器,同时绑定本机的9013端口。
接下来,我们为GetHouse微服务编写与数据持久化有关的代码。
打开GetHouse微服务目录下,model子目录中的mysql.go文件,在其中添加有关从MySQL数据库中读取房屋详情、房主用户、浏览用户、房屋订单、下单用户、房屋设施和房屋副图的代码:
xxxxxxxxxx
501...
2// 从MySQL数据库中读取房屋详情
3func ReadHouse(houseId uint) (House, error) {
4 var house House
5 err := db.Where("id=?", houseId).First(&house).Error
6 return house, err
7}
8
9// 从MySQL数据库中读取房主用户
10func ReadOwner(house House) (User, error) {
11 var user User
12 err := db.Model(&house).Related(&user).Error
13 return user, err
14}
15
16// 从MySQL数据库中读取浏览用户
17func ReadBrowser(username string) (User, error) {
18 var user User
19 err := db.Where("name=?", username).First(&user).Error
20 return user, err
21}
22
23// 从MySQL数据库中读取房屋订单
24func ReadOrders(house House) ([]Order, error) {
25 var orders []Order
26 err := db.Model(&house).Related(&orders).Error
27 return orders, err
28}
29
30// 从MySQL数据库中读取下单用户
31func ReadOrderer(order Order) (User, error) {
32 var user User
33 err := db.Model(&order).Related(&user).Error
34 return user, err
35}
36
37// 从MySQL数据库中读取房屋设施
38func ReadFacilities(house House) ([]Facility, error) {
39 var facilities []Facility
40 err := db.Model(&house).Related(&facilities, "Facilities").Error
41 return facilities, err
42}
43
44// 从MySQL数据库中读取房屋副图
45func ReadImages(house House) ([]Image, error) {
46 var images []Image
47 err := db.Model(&house).Related(&images).Error
48 return images, err
49}
50...
定义名为ReadHouse的函数,用于从MySQL数据库中读取房屋详情。该函数以从参数传入的房屋ID为条件,查询房屋表中的特定记录,获得表示该记录的房屋结构体变量,连同错误对象,返回给函数的调用者。
定义名为ReadOwner的函数,用于从MySQL数据库中读取房主用户。该函数以从参数传入的房屋结构体变量为依据,基于房屋表的房主用户ID外键,和用户表的用户ID主键,在房屋表和用户表之间做一对一的关联查询,得到表示用户表中特定记录的用户结构体变量。最后将参数房屋的房主用户信息,连同错误对象,返回给函数的调用者。
定义名为ReadBrowser的函数,用于从MySQL数据库中读取浏览用户。该函数以从参数传入的浏览用户名为条件,查询用户表中的特定记录,获得表示该记录的用户结构体变量,连同错误对象,返回给函数的调用者。
定义名为ReadOrders的函数,用于从MySQL数据库中读取房屋订单。该函数以从参数传入的房屋结构体变量为依据,基于房屋表的房屋ID主键,和订单表的预定房屋ID外键,在房屋表和订单表之间做一对多的关联查询,得到表示订单表中多条记录的订单结构体切片。最后将参数房屋的所有订单信息,连同错误对象,返回给函数的调用者。
定义名为ReadOrderer的函数,用于从MySQL数据库中读取下单用户。该函数以从参数传入的订单结构体变量为依据,基于订单表的下单用户ID外键,和用户表的用户ID主键,在订单表和用户表之间做一对一的关联查询,得到表示用户表中特定记录的用户结构体变量。最后将参数订单的下单用户信息,连同错误对象,返回给函数的调用者。
定义名为ReadFacilities的函数,用于从MySQL数据库中读取房屋设施。该函数以从参数传入的房屋结构体变量为依据,基于房屋表的房屋ID主键,和设施表的设施ID主键,借助房屋设施中间表,在房屋表和设施表之间做一对多的关联查询,得到表示设施表中多条记录的设施结构体切片。最后将参数房屋的所有设施信息,连同错误对象,返回给函数的调用者。
定义名为ReadImages的函数,用于从MySQL数据库中读取房屋副图。该函数以从参数传入的房屋结构体变量为依据,基于房屋表的房屋ID主键,和图片表的所属房屋ID外键,在房屋表和图片表之间做一对多的关联查询,得到表示图片表中多条记录的图片结构体切片。最后将参数房屋的所有副图信息,连同错误对象,返回给函数的调用者。
在完成GetHouse微服务模型层的所有开发工作之后,我们将着手编写用于处理获取房屋详情业务的代码。
打开GetHouse微服务目录下,handler子目录中的GetHouse.go文件,在其中的Call方法里,添加与获取房屋详情有关的操作:
xxxxxxxxxx
1191...
2// 获取房屋详情
3func (e *GetHouse) Call(ctx context.Context, req *pb.CallRequest, rsp *pb.CallResponse) error {
4 logger.Infof("Received GetHouse.Call request: %v", req)
5
6 // 从MySQL数据库中读取房屋详情
7 houseId, _ := strconv.Atoi(req.HouseId)
8 house, err := model.ReadHouse(uint(houseId))
9 if err != nil {
10 logger.Error(err)
11 rsp.Errno = utils.ERROR_DATABASE
12 rsp.Errmsg = utils.StrError(rsp.Errno)
13 return nil
14 }
15
16 // 从MySQL数据库中读取房主用户
17 owner, err := model.ReadOwner(house)
18 if err != nil {
19 logger.Error(err)
20 rsp.Errno = utils.ERROR_DATABASE
21 rsp.Errmsg = utils.StrError(rsp.Errno)
22 return nil
23 }
24
25 // 从MySQL数据库中读取浏览用户
26 browser, err := model.ReadBrowser(req.Username)
27 if err != nil {
28 logger.Error(err)
29 rsp.Errno = utils.ERROR_DATABASE
30 rsp.Errmsg = utils.StrError(rsp.Errno)
31 return nil
32 }
33
34 rsp.Data = &pb.HouseOfUser{
35 House: &pb.House{
36 Acreage: uint64(house.Acreage),
37 Address: house.Address,
38 Beds: house.Beds,
39 Capacity: uint64(house.Capacity),
40 Deposit: uint64(house.Deposit),
41 Hid: uint64(house.ID),
42 MaxDays: uint64(house.Max_days),
43 MinDays: uint64(house.Min_days),
44 Price: uint64(house.Price),
45 RoomCount: uint64(house.Room_count),
46 Title: house.Title,
47 Unit: house.Unit,
48 UserAvatar: "http://192.168.0.111:8888/" + owner.Avatar_url,
49 UserId: uint64(owner.ID),
50 UserName: owner.Name,
51 },
52 UserId: uint64(browser.ID),
53 }
54
55 // 从MySQL数据库中读取房屋订单
56 orders, err := model.ReadOrders(house)
57 if err != nil {
58 logger.Error(err)
59 rsp.Errno = utils.ERROR_DATABASE
60 rsp.Errmsg = utils.StrError(rsp.Errno)
61 return nil
62 }
63
64 for _, order := range orders {
65 // 从MySQL数据库中读取下单用户
66 orderer, err := model.ReadOrderer(order)
67 if err != nil {
68 logger.Error(err)
69 rsp.Errno = utils.ERROR_DATABASE
70 rsp.Errmsg = utils.StrError(rsp.Errno)
71 return nil
72 }
73
74 rsp.Data.House.Comments = append(rsp.Data.House.Comments, &pb.Comment{
75 Comment: order.Comment,
76 Ctime: order.CreatedAt.Format("2006-01-02 15:04:05"),
77 UserName: orderer.Name,
78 })
79 }
80
81 // 从MySQL数据库中读取房屋设施
82 facilities, err := model.ReadFacilities(house)
83 if err != nil {
84 logger.Error(err)
85 rsp.Errno = utils.ERROR_DATABASE
86 rsp.Errmsg = utils.StrError(rsp.Errno)
87 return nil
88 }
89
90 for _, facility := range facilities {
91 rsp.Data.House.Facilities = append(
92 rsp.Data.House.Facilities, uint64(facility.ID))
93 }
94
95 // 房屋主图
96 if house.Index_image_url != "" {
97 rsp.Data.House.ImgUrls = append(rsp.Data.House.ImgUrls,
98 "http://192.168.0.111:8888/"+house.Index_image_url)
99 }
100
101 // 从MySQL数据库中读取房屋副图
102 images, err := model.ReadImages(house)
103 if err != nil {
104 logger.Error(err)
105 rsp.Errno = utils.ERROR_DATABASE
106 rsp.Errmsg = utils.StrError(rsp.Errno)
107 return nil
108 }
109
110 for _, image := range images {
111 rsp.Data.House.ImgUrls = append(rsp.Data.House.ImgUrls,
112 "http://192.168.0.111:8888/"+image.Url)
113 }
114
115 rsp.Errno = utils.ERROR_OK
116 rsp.Errmsg = utils.StrError(rsp.Errno)
117 return nil
118}
119...
这里首先调用模型层的ReadHouse函数,根据请求对象中的房屋ID,获取该房屋的详情信息。调用模型层的ReadOwner函数,获取该房屋的房主用户信息。调用模型层的ReadBrowser函数,根据请求对象中的用户名,获取浏览用户信息。将前面得到的房屋及浏览用户信息填入响应对象,其中有些字符串信息,需要先转换为无符号整数,URL信息,需要在文件凭证前面拼接Nginx服务器的地址和端口。调用模型层的ReadOrders函数,获取该房屋的所有订单信息,遍历其中的每一张订单,调用模型层的ReadOrderer函数,获取该订单的下单用户信息,将该订单的评论内容、发布时间和下单用户名,添加到响应对象的评论列表中。调用模型层的ReadFacilities函数,获取该房屋的所有设施信息,遍历其中的每一件设施,将其ID添加到响应对象的设施ID列表中。若该房屋存在主图,则将其文件凭证拼接到Nginx服务器的地址和端口之后,添加到响应对象的房屋图片URL列表中。最后调用模型层的ReadImages函数,获取该房屋的所有副图信息,遍历其中的每一幅副图,将其文件凭证拼接到Nginx服务器的地址和端口之后,添加到响应对象的房屋图片URL列表中。
在完成GetHouse后端服务器的开发后,我们需要为前端服务器添加一条路由,并在路由处理函数中完成对后端微服务远程方法的调用。
前端和后端共用同一套接口描述。
因此,这里我们将GetHouse微服务目录下proto子目录中的所有文件,原封不动地复制到front-end工程目录proto子目录下的GetHouse目录中。
这里我们要为获取房屋详情添加一条路由,GET方法结合/api/v1.0/houses/:id路径,处理函数名为GetHouse。
在front-end工程目录下的main.go文件中添加一条路由:
xxxxxxxxxx
91...
2func main() {
3 ...
4 // 路由匹配
5 ...
6 apiv10.GET("/houses/:id", controller.GetHouse)
7 ...
8}
9...
这里我们调用了路由对象的GET方法,为获取房屋详情添加了一条路由,将GET方法结合/houses/:id路径,路由到controller包的GetHouse函数。将GetHouse函数定义在controller包里是因为该函数的主要任务是执行业务逻辑,属于MVC中的C,即控制器层的部分。
当然,在controller包里真的得有GetHouse函数。为此,我们打开front-end工程目录下controller子目录中的house.go文件。在该文件的import部分增加一行,同时定义GetHouse函数:
xxxxxxxxxx
301...
2import (
3 ...
4 pbGetHouse "iHome/front-end/proto/GetHouse"
5 ...
6)
7...
8// 获取房屋详情
9func GetHouse(ctx *gin.Context) {
10 // 从Session中读取用户名
11 username := model.ReadUsername(ctx)
12
13 // 获取房屋ID
14 houseId := ctx.Param("id")
15
16 // 调用微服务
17 srv := micro.NewService(micro.Registry(consul.NewRegistry()))
18 srv.Init()
19 c := pbGetHouse.NewGetHouseService("gethouse", srv.Client())
20 rsp, err := c.Call(context.Background(), &pbGetHouse.CallRequest{
21 Username: username,
22 HouseId: houseId,
23 })
24 if err != nil {
25 fmt.Println(err)
26 return
27 }
28
29 ctx.JSON(http.StatusOK, rsp)
30}
这段代码首先从Session中读取用户名,接着调用了Gin框架调用此函数时传入的上下文对象的Param方法,取添加路由时放在请求路径中的占位符“id”为参数,接收其返回的房屋ID。通过Consul服务发现,获取有关名为gethouse的微服务的信息,远程调用其中的GetHouse对象的Call方法。该方法以包含用户名和房屋ID的请求对象为参数,并返回包含错误代码、错误描述和房屋及浏览用户信息的响应对象。最后将该响应对象序列化为一个JSON字符串,编码到HTTP响应中,回传给浏览器。
至此,我们已经完成获取房屋详情的全部开发工作。下面我们将对这部分功能进行测试。
在虚拟机中启动FastDFS和Nginx。启动Consul服务器、UserLogin、GetUser、GetHouses、GetHouse后端微服务和前端服务器。通过登录页面登录系统,点击位于搜索页面右上角的用户名,进入用户页面,点击“我的房源”,进入我的房源页面,当前用户已发布的房源列表,显示在“发布新房源”下方,点击房源列表中的房源条目,显示房屋详情。
谢谢大家,我们下节课再见!