同学们好!成功登录《我家租房网》系统的用户,可以点击位于搜索页面右上角的用户名,进入用户页面。点击用户页面中的“我的房源”,会打开我的房源页面。点击我的房源页面中的“发布新房源”,会打开发布新房源页面。用户在填写完房源信息表单后,点击“发布房源信息”,系统后台将把房源信息存入数据库。这节课我们将实现发布房源的功能。
在实现发布房源功能之前,我们需要先为前端服务器,添加一个用于获取房源的路由和控制器,目前先不做具体实现,仅向浏览器响应“无数据”即可。随后我们将实现支持发布房源的后端微服务,再在前端服务器中添加路由处理函数并远程调用后端微服务,最后再将前后端连在一起做完整的功能测试。
为前端服务器添加对获取房源业务的支持,响应“无数据”即可,具体功能待后续课程继续完善。
这里我们要为获取房源添加一条路由,GET方法结合/api/v1.0/user/houses路径,处理函数名为GetHouses。
在front-end工程目录下的main.go文件中添加一条路由:
1...
2func main() {
3 ...
4 // 路由匹配
5 ...
6 apiv10.GET("/user/houses", controller.GetHouses)
7 ...
8}
9...
这里我们调用了路由对象的GET方法,为获取房源添加了一条路由,将GET方法结合/user/houses路径,路由到controller包的GetHouses函数。将GetHouses函数定义在controller包里是因为该函数的主要任务是执行业务逻辑,属于MVC中的C,即控制器层的部分。
当然,在controller包里真的得有GetHouses函数。为此,我们在front-end工程目录下的controller子目录中创建一个名为house.go的文件,在该文件中定义GetHouses函数:
x1package controller
2
3import (
4 "github.com/gin-gonic/gin"
5 "iHome/front-end/utils"
6 "net/http"
7)
8
9// 获取房源
10func GetHouses(ctx *gin.Context) {
11 rsp := make(map[string]interface{})
12 rsp["errno"] = utils.ERROR_NO_DATA
13 rsp["errmsg"] = utils.StrError(rsp["errno"].(string))
14 ctx.JSON(http.StatusOK, rsp)
15}
house.go中的代码依然隶属于controller的包。在GetHouse函数里,我们创建了一个字符串到空接口的映射,并为其添加了两个键值对,一个表示错误代码,另一个表示错误描述。最后通过Gin框架调用该函数时传入的上下文对象的JSON方法,构造响应报文,响应头中的状态码为200,响应体为一个JSON字符串,源于对映射的序列化。
下面我们来实现支持发布房源的后端服务器。
第一步是创建特定的微服务。
在创建微服务之前,我们可以先执行这条SQL语句,将每一种设施的ID和名称插入到设施表中。创建微服务的过程还是和先前一样,直接在back-end工程目录下执行go-micro命令,子命令为new,参数为service,微服务名为PublishHouse。
接着,需要修改一下go-micro为我们自动生成的接口描述。在back-end工程目录下的PublishHouse微服务目录中,有一个名为proto的子目录,这就是存放接口描述脚本及其Go代码文件的地方。现阶段里面只有一个名为PublishHouse.proto的ProtoBuf脚本文件。我们需要对该文件做一些修改:
xxxxxxxxxx
281...
2message CallRequest {
3 string username = 1;
4 string title = 2;
5 string price = 3;
6 string areaId = 4;
7 string address = 5;
8 string roomCount = 6;
9 string acreage = 7;
10 string unit = 8;
11 string capacity = 9;
12 string beds = 10;
13 string deposit = 11;
14 string minDays = 12;
15 string maxDays = 13;
16 repeated string facilities = 14;
17}
18
19message House {
20 string house_id = 1;
21}
22
23message CallResponse {
24 string errno = 1;
25 string errmsg = 2;
26 House data = 3;
27}
28...
这段代码描述的是,前端服务器和后端微服务之间的数据交换格式。CallRequest表示前端提供给后端的请求数据,其中包含13个字符串和一个字符串列表,它们分别是房主用户名、标题、价格、归属地区ID、地址、房间数、面积、房间配置、容纳人数、床铺配置、押金、最少入住天数、最多入住天数,以及设施ID列表。CallResponse表示后端返回给前端的响应数据,其中包含错误代码、错误描述和房屋信息。房屋信息中只含有一个字段,即房屋ID。
有了用ProtoBuf语言描述的远程调用接口,我们还需要把它编译成基于Go语言的程序代码。在PublishHouse微服务目录中执行make命令,并携带四个目标参数,它们是init、proto、update和tidy。
在PublishHouse微服务目录下,创建一个名为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}
修改PublishHouse微服务目录中的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:9010"),
13 )
14 ...
15}
16...
程序启动伊始,即完成对数据库的初始化。其中包括两个动作,其一是连接ihomedb数据库并获得连接池对象,其二是在该数据库中创建表。当然,如果这些表已经存在,则不再创建。接着,在服务器对象的初始化部分,指示其注册到Consul服务器,同时绑定本机的9010端口。
接下来,我们为PublishHouse微服务编写与数据持久化有关的代码。
打开PublishHouse微服务目录下,model子目录中的mysql.go文件,在其中添加有关将房源信息保存到MySQL数据库中的代码:
xxxxxxxxxx
421...
2// 将房源信息保存到MySQL数据库中
3func SaveHouse(username string, areaId uint, title string,
4 address string, roomCount int, acreage int, price int, unit string,
5 capacity int, beds string, deposit int, minDays int, maxDays int,
6 facilities []uint) (uint, error) {
7 var user User
8 err := db.Select("id").Where("name=?", username).First(&user).Error
9 if err != nil {
10 return 0, err
11 }
12
13 house := House{
14 User_id: user.ID,
15 Area_id: areaId,
16 Title: title,
17 Address: address,
18 Room_count: roomCount,
19 Acreage: acreage,
20 Price: price,
21 Unit: unit,
22 Capacity: capacity,
23 Beds: beds,
24 Deposit: deposit,
25 Min_days: minDays,
26 Max_days: maxDays,
27 }
28
29 for _, id := range facilities {
30 var facility Facility
31 err = db.Where("id=?", id).First(&facility).Error
32 if err != nil {
33 return 0, err
34 }
35
36 house.Facilities = append(house.Facilities, &facility)
37 }
38
39 err = db.Create(&house).Error
40 return house.ID, err
41}
42...
定义名为SaveHouse的函数,用于将房源信息保存到MySQL数据库中。该函数接收房主用户名、归属地区ID、标题、地址、房间数、面积、价格、房间配置、容纳人数、床铺配置、押金、最少入住天数、最多入住天数,以及设施ID列表等共计14个参数。以用户名为条件,查询用户表中特定记录的用户ID字段。创建并初始化表示房屋表记录的房屋结构体变量。遍历设施ID列表参数中的每一个设施ID,以此为条件从设施表中查询特定的设施记录,将表示该记录的设施结构体变量,添加到房屋结构体变量的设施集切片中。最后通过数据库连接池对象的Create方法,将房屋结构体变量所表示的房屋记录插入到房屋表中,并返回自动生成的房屋ID和错误对象。
在完成PublishHouse微服务模型层的所有开发工作之后,我们将着手编写用于处理发布房源业务的代码。
打开PublishHouse微服务目录下,handler子目录中的PublishHouse.go文件,在其中的Call方法里,添加与发布房源有关的操作:
xxxxxxxxxx
361...
2// 发布房源
3func (e *PublishHouse) Call(ctx context.Context, req *pb.CallRequest, rsp *pb.CallResponse) error {
4 logger.Infof("Received PublishHouse.Call request: %v", req)
5
6 // 将房源信息保存到MySQL数据库中
7 areaId, _ := strconv.Atoi(req.AreaId)
8 roomCount, _ := strconv.Atoi(req.RoomCount)
9 acreage, _ := strconv.Atoi(req.Acreage)
10 price, _ := strconv.Atoi(req.Price)
11 capacity, _ := strconv.Atoi(req.Capacity)
12 deposit, _ := strconv.Atoi(req.Deposit)
13 minDays, _ := strconv.Atoi(req.MinDays)
14 maxDays, _ := strconv.Atoi(req.MaxDays)
15 var facilities []uint
16 for _, facility := range req.Facilities {
17 id, _ := strconv.Atoi(facility)
18 facilities = append(facilities, uint(id))
19 }
20 houseId, err := model.SaveHouse(req.Username, uint(areaId),
21 req.Title, req.Address, roomCount, acreage, price, req.Unit,
22 capacity, req.Beds, deposit, minDays, maxDays, facilities)
23 if err != nil {
24 logger.Error(err)
25 rsp.Errno = utils.ERROR_DATABASE
26 rsp.Errmsg = utils.StrError(rsp.Errno)
27 return nil
28 }
29
30 rsp.Data = &pb.House{HouseId: strconv.Itoa(int(houseId))}
31
32 rsp.Errno = utils.ERROR_OK
33 rsp.Errmsg = utils.StrError(rsp.Errno)
34 return nil
35}
36...
这里首先将请求对象中字符串形式的归属地区ID、房间数、面积、价格、容纳人数、押金、最少入住天数、最多入住天数,以及设施ID列表,转换为整型及整型切片,保存到对应的局部变量中。调用模型层的SaveHouse函数,将源自请求对象的房源信息,如房主用户名、归属地区ID、标题、地址、房间数、面积、价格、房间配置、容纳人数、床铺配置、押金、最少入住天数、最多入住天数,以及设施ID列表等,保存到MySQL数据库中。若成功,则将该函数返回的房屋ID,转换成字符串后,填入响应对象的特定字段中。
在完成PublishHouse后端服务器的开发后,我们需要为前端服务器添加一条路由,并在路由处理函数中完成对后端微服务远程方法的调用。
前端和后端共用同一套接口描述。
因此,这里我们将PublishHouse微服务目录下proto子目录中的所有文件,原封不动地复制到front-end工程目录proto子目录下的PublishHouse目录中。
这里我们要为发布房源添加一条路由,POST方法结合/api/v1.0/houses路径,处理函数名为PublishHouse。
在front-end工程目录下的main.go文件中添加一条路由:
xxxxxxxxxx
91...
2func main() {
3 ...
4 // 路由匹配
5 ...
6 apiv10.POST("/houses", controller.PublishHouse)
7 ...
8}
9...
这里我们调用了路由对象的POST方法,为发布房源添加了一条路由,将POST方法结合/houses路径,路由到controller包的PublishHouse函数。将PublishHouse函数定义在controller包里是因为该函数的主要任务是执行业务逻辑,属于MVC中的C,即控制器层的部分。
当然,在controller包里真的得有PublishHouse函数。为此,我们打开front-end工程目录下controller子目录中的house.go文件。在该文件的import部分增加一行,同时定义PublishHouse函数:
xxxxxxxxxx
581...
2import (
3 ...
4 pbPublishHouse "iHome/front-end/proto/PublishHouse"
5 ...
6)
7...
8// 发布房源
9func PublishHouse(ctx *gin.Context) {
10 // 从Session中读取用户名
11 username := model.ReadUsername(ctx)
12
13 // 获取房源信息
14 req := struct {
15 Title string `json:"title"`
16 Price string `json:"price"`
17 AreaId string `json:"area_id"`
18 Address string `json:"address"`
19 RoomCount string `json:"room_count"`
20 Acreage string `json:"acreage"`
21 Unit string `json:"unit"`
22 Capacity string `json:"capacity"`
23 Beds string `json:"beds"`
24 Deposit string `json:"deposit"`
25 MinDays string `json:"min_days"`
26 MaxDays string `json:"max_days"`
27 Facilities []string `json:"facility"`
28 }{}
29 ctx.Bind(&req)
30
31 // 调用微服务
32 srv := micro.NewService(micro.Registry(consul.NewRegistry()))
33 srv.Init()
34 c := pbPublishHouse.NewPublishHouseService("publishhouse", srv.Client())
35 rsp, err := c.Call(context.Background(), &pbPublishHouse.CallRequest{
36 Username: username,
37 Title: req.Title,
38 Price: req.Price,
39 AreaId: req.AreaId,
40 Address: req.Address,
41 RoomCount: req.RoomCount,
42 Acreage: req.Acreage,
43 Unit: req.Unit,
44 Capacity: req.Capacity,
45 Beds: req.Beds,
46 Deposit: req.Deposit,
47 MinDays: req.MinDays,
48 MaxDays: req.MaxDays,
49 Facilities: req.Facilities,
50 })
51 if err != nil {
52 fmt.Println(err)
53 return
54 }
55
56 ctx.JSON(http.StatusOK, rsp)
57}
58...
这段代码首先从Session中读取用户名,接着对请求包体中的JSON字符串做反序列化,得到所发布房源的标题、价格、归属地区ID、地址、房间数、面积、房间配置、容纳人数、床铺配置、押金、最少入住天数、最多入住天数,以及设施ID列表等信息。通过Consul服务发现,获取有关名为publishhouse的微服务的信息,远程调用其中的PublishHouse对象的Call方法。该方法以包含房主用户名和房源信息的请求对象为参数,并返回包含错误代码、错误描述和房屋ID的响应对象。最后将该响应对象序列化为一个JSON字符串,编码到HTTP响应中,回传给浏览器。
至此,我们已经完成发布房源的全部开发工作。下面我们将对这部分功能进行测试。
启动Consul服务器、GetAreas、UserLogin、GetUser、PublishHouse后端微服务和前端服务器。通过登录页面登录系统,点击位于搜索页面右上角的用户名,进入用户页面,点击“我的房源”,进入我的房源页面,点击“发布新房源”,进入发布新房源页面,填写房源信息表单,点击“发布房源信息”。进入MySQL控制台,检查ihomedb数据库houses表中的房屋信息。
谢谢大家,我们下节课再见!