Skip to content

租房网

1.REST详解

在进行开发前,这里面需要给大家介绍一个概念,REST

REST全称是Representational State Transfer,中文意思是表述(编者注:通常译为表征)性状态转移。

也有说全称是 Resource Representational State Transfer 资源表现状态转移

是Roy Thomas Fielding博士于2000年在他的博士论文中提出来的一种万维网软件架构风格,目的是便于不同软件/程序在网络(例如互联网)中互相传递信息。表现层状态转换是根基于超文本传输协议(HTTP)之上而确定的一组约束和属性,是一种设计提供万维网络服务的软件构建风格。符合或兼容于这种架构风格(简称为 REST 或 RESTful)的网络服务,允许客户端发出以统一资源标识符访问和操作网络资源的请求,而与预先定义好的无状态操作集一致化。因此表现层状态转换提供了在互联网络的计算系统之间,彼此资源可交互使用的协作性质(interoperability)。相对于其它种类的网络服务,例如 SOAP服务则是以本身所定义的操作集,来访问网络上的资源。

(摘自维基百科)

概念一般都比较晦涩难懂,我们看一下知乎大神给出的简洁版介绍:

1.REST不是"rest"这个单词,而是几个单词缩写。但即使那几个单词说出来,也无法理解在说什么 -_-!! (不是要贬低人,是我自己也理解困难);

2.REST描述的是在网络中client和server的一种交互形式;REST本身不实用,实用的是如何设计 RESTful API(REST风格的网络接口);

3.Server提供的RESTful API中,URL中只使用名词来指定资源,原则上不使用动词。“资源”是REST架构或者说整个网络处理的核心。比如:

URL全局资源定位符 locat restful

URI:全局资源标识符 identify

http://api.qc.com/v1/newsfeed: 获取某人的新鲜; http://api.qc.com/v1/friends: 获取某人的好友列表; http://api.qc.com/v1/profile: 获取某人的详细信息;

4 .用HTTP协议里的动词来实现资源的添加,修改,删除等操作。即通过HTTP动词来实现资源的状态扭转:

1565331217181

比如: DELETE http://api.qc.com/v1/friends: 删除某人的好友 (在http parameter指定好友id) POST http://api.qc.com/v1/friends: 添加好友 UPDATE http://api.qc.com/v1/profile: 更新个人资料

禁止使用: GET http://api.qc.com/v1/deleteFriend

Restful的好处

  • 轻量,直接基于http,不再需要任何别的诸如消息协议。get/post/put/delete为CRUD操作

  • 面向资源,一目了然,具有自解释性。

  • 数据描述简单,一般以xml,json做数据交换。

  • 无状态,在调用一个接口(访问、操作资源)的时候,可以不用考虑上下文,不用考虑当前状态,极大的降低了复杂度。

  • 简单、低耦合

在web开发中,restful是非常常见的设计。在我们接下来的开发过程中,路由尽量使用restful风格。

2.IHome开发

2.1创建项目

这里我们使用gin框架作为web框架进行开发,首先我们先来看一下gin的一个简单使用。

首先是gin的下载,下载命令如下:

shell
$ go get -u -v github.com/gin-gonic/gin

然后我们用简单的示例代码着手gin框架的学习使用,示例代码如下:

go
func main(){
	//初始化路由
	router := gin.Default()

	//路由匹配
	router.GET("/", func(context *gin.Context) {
		context.Writer.Write([]byte("Hello World"))
	})

	//开启监听
	router.Run(":8080")
}

非常简单就能实现一个web服务器的雏形,接着我们在项目开发中再详细介绍gin的使用

开发过程中可能有些内容介绍不够全面,如果想看详细文档可以在下面网址查看:

github原始地址:https://github.com/gin-gonic/gin

github补充示例地址:https://github.com/gin-gonic/examples/

gin中文文档:https://www.kancloud.cn/shuangdeyu/gin_book/949413

2.2开发前准备

2.2.1创建工具类文件夹

shell
#创建工具函数文件夹
$ mkdir utils
# 进入文件夹创建文件
$ cd utils
# 配置文件读取函数文件
$ vim config.go
# 错误码文件
$ vim error.go

错误码文件error.go内容如下:

go
package utils

const (
	RECODE_OK         = "0"
	RECODE_DBERR      = "4001"
	RECODE_NODATA     = "4002"
	RECODE_DATAEXIST  = "4003"
	RECODE_DATAERR    = "4004"
	
	RECODE_SESSIONERR = "4101"
	RECODE_LOGINERR   = "4102"
	RECODE_PARAMERR   = "4103"
	RECODE_USERONERR  = "4104"
	RECODE_ROLEERR    = "4105"
	RECODE_PWDERR     = "4106"
	RECODE_USERERR    = "4107"
	RECODE_SMSERR     = "4108"
	RECODE_MOBILEERR  = "4109"

	RECODE_REQERR     = "4201"
	RECODE_IPERR      = "4202"
	RECODE_THIRDERR   = "4301"
	RECODE_IOERR      = "4302"
	RECODE_SERVERERR  = "4500"
	RECODE_UNKNOWERR  = "4501"
)

var recodeText = map[string]string{
	RECODE_OK:         "成功",
	RECODE_DBERR:      "数据库查询错误",
	RECODE_NODATA:     "无数据",
	RECODE_DATAEXIST:  "数据已存在",
	RECODE_DATAERR:    "数据错误",
	RECODE_SESSIONERR: "用户未登录",
	RECODE_LOGINERR:   "用户登录失败",
	RECODE_PARAMERR:   "参数错误",
	RECODE_USERERR:    "用户不存在或未激活",
	RECODE_USERONERR:  "用户已经注册",
	RECODE_ROLEERR:    "用户身份错误",
	RECODE_PWDERR:     "密码错误",
	RECODE_REQERR:     "非法请求或请求次数受限",
	RECODE_IPERR:      "IP受限",
	RECODE_THIRDERR:   "第三方系统错误",
	RECODE_IOERR:      "文件读写错误",
	RECODE_SERVERERR:  "内部错误",
	RECODE_UNKNOWERR:  "未知错误",
	RECODE_SMSERR:     "短信失败",
	RECODE_MOBILEERR:  "手机号错误",
}

func RecodeText(code string) string {
	str, ok := recodeText[code]
	if ok {
		return str
	}
	return recodeText[RECODE_UNKNOWERR]
}

配置文件内容,等到我们写业务代码的时候继续补充。

2.2.2创建数据库和表

先创建一个文件夹,用来存放数据库文件:

shell
$ mkdir models
#创建数据库文件
$ vim models.go

数据库文件models.go文件内容如下:

go
package model

import (
	"time"
	"github.com/jinzhu/gorm"
	_ "github.com/jinzhu/gorm/dialects/mysql"
)

/* 用户 table_name = user */
type User struct {
	ID            int                                  //用户编号
	Name          string        `gorm:"size:32;unique"`           //用户名
	Password_hash string        `gorm:"size:128" `      //用户密码加密的
	Mobile        string        `gorm:"size:11;unique" ` //手机号
	Real_name     string        `gorm:"size:32" `      //真实姓名  实名认证
	Id_card       string        `gorm:"size:20" `        //身份证号  实名认证
	Avatar_url    string        `gorm:"size:256" `    //用户头像路径       通过fastdfs进行图片存储
	Houses        []*House         //用户发布的房屋信息  一个人多套房
	Orders        []*OrderHouse    //用户下的订单       一个人多次订单
}


/* 房屋信息 table_name = house */
type House struct {
	gorm.Model                     //房屋编号
	UserId          uint           //房屋主人的用户编号  与用户进行关联
	AreaId          uint           //归属地的区域编号   和地区表进行关联
	Title           string        `gorm:"size:64" `         //房屋标题
	Address         string        `gorm:"size:512"`         //地址
	Room_count      int           `gorm:"default:1" `       //房间数目
	Acreage         int           `gorm:"default:0" json:"acreage"`                   //房屋总面积
    Price           int           `json:"price"`
	Unit            string        `gorm:"size:32;default:''" json:"unit"`             //房屋单元,如 几室几厅
	Capacity        int           `gorm:"default:1" json:"capacity"`                   //房屋容纳的总人数
	Beds            string        `gorm:"size:64;default:''" json:"beds"`             //房屋床铺的配置
	Deposit         int           `gorm:"default:0" json:"deposit"`                   //押金
	Min_days        int           `gorm:"default:1" json:"min_days"`                   //最少入住的天数
	Max_days        int           `gorm:"default:0" json:"max_days"`                   //最多入住的天数 0表示不限制
	Order_count     int           `gorm:"default:0" json:"order_count"`               //预定完成的该房屋的订单数
	Index_image_url string        `gorm:"size:256;default:''" json:"index_image_url"` 			 //房屋主图片路径
	Facilities      []*Facility   `gorm:"many2many:house_facilities" json:"facilities"`                 //房屋设施   与设施表进行关联
	Images          []*HouseImage `json:"img_urls"`                      //房屋的图片   除主要图片之外的其他图片地址
	Orders          []*OrderHouse `json:"orders"`                        //房屋的订单    与房屋表进行管理
}

/* 区域信息 table_name = area */  //区域信息是需要我们手动添加到数据库中的
type Area struct {
	Id     int      `json:"aid"`                        //区域编号     1    2
	Name   string   `gorm:"size:32" json:"aname"`       //区域名字     昌平 海淀
	Houses []*House `json:"houses"` //区域所有的房屋   与房屋表进行关联
}

/* 设施信息 table_name = "facility"*/    //设施信息 需要我们提前手动添加的
type Facility struct {
	Id     int      `json:"fid"`     //设施编号
	Name   string   `gorm:"size:32"` //设施名字
	Houses []*House  //都有哪些房屋有此设施  与房屋表进行关联的
}

/* 房屋图片 table_name = "house_image"*/
type HouseImage struct {
	Id    int    `json:"house_image_id"`         //图片id
	Url   string `gorm:"size:256" json:"url"`    //图片url     存放我们房屋的图片
	HouseId uint `json:"house_id"` //图片所属房屋编号
}

/* 订单 table_name = order */
type OrderHouse struct {
	gorm.Model               //订单编号
	UserId        uint     `json:"user_id"`  //下单的用户编号   //与用户表进行关联
	HouseId       uint    `json:"house_id"` //预定的房间编号   //与房屋信息进行关联
	Begin_date  time.Time `gorm:"type:datetime"`          //预定的起始时间
	End_date    time.Time `gorm:"type:datetime"`          //预定的结束时间
	Days        int       //预定总天数
	House_price int       //房屋的单价
	Amount      int       //订单总金额
	Status      string    `gorm:"default:'WAIT_ACCEPT'"`   //订单状态
	Comment     string    `gorm:"size:512"`                //订单评论
	Credit      bool		//表示个人征信情况 true表示良好
}

func InitDb()(*gorm.DB,error){
    //sql.Open()
	db,err := gorm.Open("mysql","root:123456@tcp(127.0.0.1:3306)/test?charset=utf8")
	defer db.Close()
	if err == nil {
		db.SingularTable(true)
		db.AutoMigrate(new(User),new(House),new(Area),new(Facility),new(HouseImage),new(OrderHouse))
		/*db.DB().SetMaxIdleConns(10)
		db.DB().SetConnMaxLifetime(100)*/
		return db,nil
	}
	return nil ,err
}

这里我们使用gorm来创建数据库表

2.2.3导入前端页面并展示

把我们的html.zip拷贝过去,然后解压到对应目录。接着我们就可以开发具体业务了。

然后修改main.go文件,内容如下:

go
package main

import "github.com/gin-gonic/gin"

func main(){
	//初始化路由
	router := gin.Default()

	//映射静态资源
	router.Static("/home","html")
	

	//开启监听
	router.Run(":8080")
}

设置静态路径有三个方法,分别是:

Static:两个参数,第一个参数是资源路径,第二个参数是指定的文件夹,示例:

go
router.Static("/assets", "./assets")

StaticFS:两个参数,第一个参数是资源路径,第二个参数是http.Dir(),示例代码如下:

go
router.StaticFS("/more_static", http.Dir("my_file_system"))

StaticFile:俩个参数,第一个参数是资源路径,第二个参数是具体的文件

router.StaticFile("/favicon.ico", "./resources/favicon.ico")

运行项目,打开浏览器输入网址127.0.0.1:8080/home,查看页面如下:

1565488579627

按f12打开开发者工具,然后再次刷新页面,结果如下:

1565488723410

这些报红的页面就是需要我们接着来实现的具体功能,让我们先从获取地域信息开始!

2.3获取地域信息

2.3.1接口定义如下

json
#Request:
method: GET
url:api/v1.0/areas
#data:
no input data
#Response
#返回成功:
{
    "errno": "0",
    "errmsg":"OK",
    "data": [
    {"aid": 1, "aname": "东城区"}, 
    {"aid": 2, "aname": "西城区"}, 
    {"aid": 3, "aname": "通州区"}, 
    {"aid": 4, "aname": "顺义区"}] 
    //...
}
#注册失败:
{
    "errno": "400x",   //状态码
    "errmsg":"状态错误信息"
}

2.3.2业务流程图

1565332723582

2.3.3添加对应路由和业务处理

main.go页面进行修改,添加对应路由和处理函数,代码如下:

go
func main(){
	//初始化路由
	router := gin.Default()

	//映射静态资源
	router.Static("/home","html")

	//添加动态路由
	r1 := router.Group("api/v1.0")
	{
		r1.GET("/areas",controller.GetArea)
	}
	

	//开启监听
	router.Run(":8080")
}

在使用gin开发时,一般情况会按照模块把路由分为不同的组,然后在组内再做路由匹配。

使用路由匹配对应函数,函数格式必须是func(*gin.Context)

接着我们来实现对应的控制器方法,方法名需要参考开发文档,和开发文档保持一直。

2.3.4具体业务实现

这里我们把获取地域信息业务放到服务中实现,然后用远程调用。首先我们来创建一个服务:

shell
$ micro new --type srv protect/getArea

然后去修改proto文件并编译自动生成的proto文件:

修改后的proto文件如下:

protobuf
syntax = "proto3";

package go.micro.srv.getArea;

service GetArea {
	rpc Call(Request) returns (Response) {}
}

message Request {

}

message Response {
	string errno = 1;
	string errmsg = 2;
	repeated area data = 3;
}

message area{
	int32 aid = 1;
	string aname = 2;
}

根据接口文档定义的传入数据和传出数据定义protobuf格式,然后编译proto文件

shell
$ make proto

然后删除掉main.go中的无用代码。进handler.go中进行代码实现。

业务分析

从业务流程图可知,我们需要从redis中获取数据,获取到数据就把数据通过json打包,传回给前端,获取不到数据,就从mysql中拿数据,拿到数据之后再把用json序列化之后存到redis中,然后把数据返回给前端。

那我们首先从redis中获取数据,这里我们直接用redis连接池来实现。其实在我们调用的redis包中有redis连接池的接口,我们直接使用即可,redis连接池代码如下:

go
var RedisClient *redis.Pool


func InitRedis(){
	RedisClient = &redis.Pool{
        //设置redis连接池最大空闲链接数
		MaxIdle:utils.G_redis_maxidel,
        //设置redis连接池最大同时链接数
		MaxActive:utils.G_redis_maxactive,
        //设置redis连接池最大空闲时间
		IdleTimeout:utils.G_redis_idletimeout,
		//连接redis
		Dial: func() (redis.Conn, error) {
			conn,err :=redis.Dial("tcp",utils.G_redis_addr+utils.G_redis_port)
			if err != nil {

				return nil,err
			}
            //设置选中哪个数据库
			conn.Do("select",utils.G_redis_db)
			return conn,nil
		},
	}
}

从redis中获取数据,代码如下:

go
//连接redis
	redisPool:=model.InitRedis()
	client := redisPool.Get()

	defer client.Close()

	areaResp,err := redis.Bytes(client.Do("get","areas"))

校验是否能够获取到数据,如果获取不到,从mysql中获取代码,然后存储到redis中,具体代码如下:

go
	var areas []model.Area
	if len(areaResp) == 0{
		//第一次从mysql中获取数据 调用封装的函数
		areas,err = model.GetAllArea()
		if err != nil {
			resp.Errno = utils.RECODE_DBERR
			resp.Errmsg = utils.RecodeText(utils.RECODE_DBERR)
			return err
		}
		//对areas数据编码
		areaBytes ,err := json.Marshal(areas)
		if err !=nil{
			resp.Errno = utils.RECODE_DATAERR
			resp.Errno = utils.RecodeText(utils.RECODE_DATAERR)
			return err
		}

		//存储到redis中
		_,err = redis.String(client.Do("set","areas",areaBytes))
		if err != nil {
			resp.Errno = utils.RECODE_DATAERR
			resp.Errmsg = utils.RecodeText(utils.RECODE_DATAERR)
			return err
		}
	}else {
		err = json.Unmarshal(areaResp,&areas)
		if err != nil {
			resp.Errno = utils.RECODE_DATAERR
			resp.Errmsg = utils.RecodeText(utils.RECODE_DATAERR)
			return err
		}
	}

这里我们把mysql操作封装成函数,放到model包中。具体实现如下:

go
func GetAllArea(db*gorm.DB)([]Area,error){
	var areas []Area
	err := db.Find(&areas).Error
	if err != nil {
		return nil,err
	}
	return areas,nil
}

gorm获取数据方法非常多,常见的方法如下:

获取一条数据:db.First(),db.Last()

获取所有数据:db.Find()

指定查询条件:db.Where("name=?","xiaohong").Find()或者db.Find(user,"name = ?","xiaohong")

获取部分字段数据:db.Table("user").select("name","age").where("name=?","xiaohong").Scan(自定义结构体)

完整代码如下:

go
//初始化返回值
	resp.Errno = utils.RECODE_OK
	resp.Errmsg = utils.RecodeText(utils.RECODE_OK)

	//连接redis
	redisPool:=model.InitRedis()
	client := redisPool.Get()

	defer client.Close()

	areaResp,err := redis.Bytes(client.Do("get","areas"))

	var areas []model.Area
	if len(areaResp) == 0{
		//第一次从mysql中获取数据
		db,_ :=model.InitDb()
		areas,err = model.GetAllArea(db)
		if err != nil {
			resp.Errno = utils.RECODE_DBERR
			resp.Errmsg = utils.RecodeText(utils.RECODE_DBERR)
			return err
		}
		//对areas数据编码
		areaBytes ,err := json.Marshal(areas)
		if err !=nil{
			resp.Errno = utils.RECODE_DATAERR
			resp.Errno = utils.RecodeText(utils.RECODE_DATAERR)
			return err
		}

		//存储到redis中
		_,err = redis.String(client.Do("set","areas",areaBytes))
		if err != nil {
			resp.Errno = utils.RECODE_DATAERR
			resp.Errmsg = utils.RecodeText(utils.RECODE_DATAERR)
			return err
		}
	}else {
		err = json.Unmarshal(areaResp,&areas)
		if err != nil {
			resp.Errno = utils.RECODE_DATAERR
			resp.Errmsg = utils.RecodeText(utils.RECODE_DATAERR)
			return err
		}
	}

	for _,v := range areas{
		temp := getArea.Area{
			Aid:int32(v.Id),
			Aname:v.Name,
		}
		resp.Data = append(resp.Data,&temp)
	}

	return nil

2.3.5web端回调服务接口

现在服务接口写完了,我们可以在web端调用一下这个接口,调用的方法和我们以前用go-micro方法一样,具体代码如下:

go
func GetArea(ctx *gin.Context){
	//初始化service
	reg := consul.NewRegistry(func(options *registry.Options) {
		options.Addrs = []string{"127.0.0.1:8500"}
	})

	service := grpc.NewService(
		micro.Registry(reg),
	)

	//获取操作客户端
	grpcClient := getArea.NewGetAreaService("go.micro.srv.getArea",service.Client())
	//回调函数
	resp ,err:= grpcClient.Call(context.TODO(),&getArea.Request{})
	if err != nil {
		resp.Errno = utils.RECODE_DATAERR
		resp.Errmsg = utils.RecodeText(utils.RECODE_DATAERR)
	}

	ctx.JSON(200,resp)
}

开启web服务和获取地域信息服务,然后再打开我们页面,点击选择城区,显示如下:

1565529172641

地域信息就获取到了

2.4获取验证码业务实现

要显示注册页面,需要先把注册按钮显示出来,这里我们需要先给session请求回个假数据,然后再去实现注册相关业务。

2.4.1模拟回复session请求

根据开发文档我们知道session的请求是api/v1.0/session,失败的话返回数据为,errno和errmsg。

我们先在web端给对应路由做匹配,代码如下:

go
r1.GET("/session",controller.GetSession)

然后在控制器中实现GetSession函数,代码如下:

go
//获取session信息
func GetSession(ctx*gin.Context){
	resp := make(map[string]interface{})

	//初始化返回值
	resp["errno"] = utils.RECODE_SESSIONERR
	resp["errmsg"] = utils.RecodeText(utils.RECODE_SESSIONERR)

	//返回数据
	ctx.JSON(200,resp)
}

这时候再刷新页面,显示如下:

1565531933966

接着我们点击注册按钮,显示注册页面,页面如下:

1565531989744

2.4.2获取验证码图片

进入注册页面之后,在开发者工具中,会发现下面的问题:

1565532096094

验证码请求未实现,接着我们就来实现验证码服务。首先是创建服务,命令如下:

shell
$ micro new --type srv project/getImageCode

然后根据开发文档规定的请求参数和返回值修改proto文件并编译,修改之后的proto文件如下:

go
syntax = "proto3";

package go.micro.srv.getImageCode;

service GetImageCode {
	rpc Call(Request) returns (Response) {}
}

message Request {
	string uuid = 1;
}

message Response {
	string errno = 1;
	string errmsg = 2;
	string msg = 3;
}

2.4.3业务流程图

2.4.4具体业务实现

根据流程图分析可知,我们需要生成验证码图片,然后把验证码图片中的随机数和传入的uuid一起存到redis中。那我们先来看看图片验证码如何生成。我们这里选择的是github.com/afocus/captcha来实现验证码功能。

要使用这个包首先当然是去下载这个包,下载命令如下:

shell
$ go get -u -v github.com/afocus/captcha

然后我们来看一下这个包的一个简单示例代码,代码如下:

go
cap = captcha.New()
// 可以设置多个字体 或使用cap.AddFont("xx.ttf")追加
cap.SetFont("comic.ttf", "xxx.ttf")
// 设置验证码大小
cap.SetSize(128, 64)
// 设置干扰强度
cap.SetDisturbance(captcha.MEDIUM)
// 设置前景色 可以多个 随机替换文字颜色 默认黑色
cap.SetFrontColor(color.RGBA{255, 255, 255, 255})
// 设置背景色 可以多个 随机替换背景色 默认白色
cap.SetBkgColor(color.RGBA{255, 0, 0, 255}, color.RGBA{0, 0, 255, 255}, color.RGBA{0, 153, 0, 255})

//设置验证码个数,以及格式
img,str := cap.Create(4,captcha.NUM)

我们可以先来跑一下这个包给我们提供的示例代码,代码路径为:代码路径在github.com/afocus/captcha/examples/main.go,具体代码如下:

go
	cap = captcha.New()

	if err := cap.SetFont("comic.ttf"); err != nil {
		panic(err.Error())
	}

	/*
	   //We can load font not only from localfile, but also from any []byte slice
	   	fontContenrs, err := ioutil.ReadFile("comic.ttf")
	   	if err != nil {
	   		panic(err.Error())
	   	}

	   	err = cap.AddFontFromBytes(fontContenrs)
	   	if err != nil {
	   		panic(err.Error())
	   	}
	*/

	cap.SetSize(128, 64)
	cap.SetDisturbance(captcha.MEDIUM)
	cap.SetFrontColor(color.RGBA{255, 255, 255, 255})
	cap.SetBkgColor(color.RGBA{255, 0, 0, 255}, color.RGBA{0, 0, 255, 255}, color.RGBA{0, 153, 0, 255})

	http.HandleFunc("/r", func(w http.ResponseWriter, r *http.Request) {
		img, str := cap.Create(6, captcha.ALL)
		png.Encode(w, img)
		println(str)
	})

	http.HandleFunc("/c", func(w http.ResponseWriter, r *http.Request) {
		str := r.URL.RawQuery
		img := cap.CreateCustom(str)
		png.Encode(w, img)
	})

	http.ListenAndServe(":8086", nil)

代码中共有两种生成图片的方式,一种是生成带验证码的图片,并返回验证码,一种是指定验证码内容,生成图片。

运行这个代码, 在浏览器中访问,显示如下:

1565539693333

根据示例代码,来实现我们的生成验证码服务。具体实现如下:

go
	//获取验证码操作对象
	cap := captcha.New()

	//设置字体库
	if err := cap.SetFont("comic.ttf"); err != nil {
		panic(err.Error())
	}


	//设置验证码属性
	cap.SetSize(128, 64)
	cap.SetDisturbance(captcha.MEDIUM)
	cap.SetFrontColor(color.RGBA{255, 255, 255, 255})
	cap.SetBkgColor(color.RGBA{255, 0, 0, 255}, color.RGBA{0, 0, 255, 255}, color.RGBA{0, 153, 0, 255})

	//生成图片
	img, str := cap.Create(6, captcha.ALL)
	//获取到验证码,把验证码结合uuid写入redis中
	redisPool := model.InitRedis()
	redisConn := redisPool.Get()
	redisConn.Do("setex",req.Uuid,60 * 5,str)

	//把拿到的图片对象序列化成字节切片传递给web
	imgBuffer,_:=json.Marshal(img)
	rsp.Msg = imgBuffer

2.4.5web端接收数据并传递给浏览器

首先我们现在web端对路由做一个匹配,然后指定操作函数,代码如下:

go
r1.GET("/imagecode/:uuid",controller.GetImageCode)

然后我们来实现GetImageCode函数。首先我们需要获取请求传过来的数据,代码如下:

go
//获取数据
uuid := ctx.Param("uuid")

gin获取前端传递的数据有不同的方法,具体如下:

获取路径中的参数为:ctx.Param("name")

获取get请求传递的数据,方法为:ctx.Query("lastname") ,ctx.DefaultQuery("firstname", "Guest")

获取post请求传递的数据,方法为:ctx.PostForm("message"),c.DefaultPostForm("nick", "anonymous")

需要注意的是post这种获取方法必须是form data传递的才能获取到,如果传递的数据是json的话,我们就需要创建一个对应的结构体,给数据进行绑定,具体操作为:ctx.Bind()或者ctx.BindJSON()

然后我们调用服务,把uuid传递给服务,并接收数据,代码如下:

go
//链接服务并调用
	service := utils.GetGrpcService()
	grpcClent := getImageCode.NewGetImageCodeService("go.micro.srv.getImageCode",service.Client())
	resp,err:=grpcClent.Call(context.TODO(),&getImageCode.Request{
		Uuid:uuid,
	})

	if err != nil{
		resp.Errno = utils.RECODE_DATAERR
		resp.Errmsg = utils.RecodeText(utils.RECODE_DATAERR)
		ctx.JSON(500,resp)
		return
	}

这里我们把连接服务的代码封装成一个函数,直接使用即可。

得到数据之后对数据进行解析,代码如下:

go
//解析返回数据为image,传回前端
	var img captcha.Image
	json.Unmarshal(resp.Msg,&img)
	png.Encode(ctx.Writer,img)

全部代码如下:

go
//获取验证码图片
func GetImageCode(ctx*gin.Context){
	//获取数据
	uuid := ctx.Param("uuid")

	//链接服务并调用
	service := utils.GetGrpcService()
	grpcClent := getImageCode.NewGetImageCodeService("go.micro.srv.getImageCode",service.Client())
	resp,err:=grpcClent.Call(context.TODO(),&getImageCode.Request{
		Uuid:uuid,
	})

	if err != nil{
		resp.Errno = utils.RECODE_DATAERR
		resp.Errmsg = utils.RecodeText(utils.RECODE_DATAERR)
		ctx.JSON(500,resp)
		return
	}


	//解析返回数据为image,传回前端
	var img captcha.Image
	json.Unmarshal(resp.Msg,&img)
	png.Encode(ctx.Writer,img)
}

这时候重启项目,访问注册线面,显示如下:

1565542977581

2.5获取短信验证码

2.5.1接口定义

json
#Request:
method: GET
#111表示的是手机号
url:api/v1.0/smscode/:mobile
url:api/v1.0/smscode/111?  text=248484&id=9cd8faa9-5653-4f7c-b653-0a58a8a98c81
data:no input data
#Response
#返回成功:
{
    "errno": "0",   //状态码
    "errmsg":"ok"
}
#注册失败:
{
    "errno": "4001",   //状态码
    "errmsg":"状态错误信息"
}

2.5.2业务流程图

2.5.3具体业务实现

由业务流程图可知,前端会传递uuid和电话号码,首先需要获取一下验证码输入是否正确,如果正确就发送短信,并对短信内容进行缓存,成功返回ok。首先,我们先来创建短信服务,命令如下:

shell
$ micro new --type srv project/getSmscd

然后根据设计文档中的传入传出参数修改proto文件并编译自动生成的proto文件,修改后的proto文件如下:

protobuf
syntax = "proto3";

package go.micro.srv.getSmscd;

service GetSmscd {
	rpc Call(Request) returns (Response) {}
}


message Request {
	string phone = 1;
	string text = 2;
	string uuid = 3;
}

message Response {
	string errno = 1;
	string errmsg = 2;
}

然后编译生成对应接口,接着修改main.go文件,具体的和上面两个服务类似,不做重复介绍,重点是我们具体业务的实现。首先我们还是要先验证图片验证码输入是否正确,代码如下:

go
	//校验验证码输入是否正确
	redisConn := model.InitRedis().Get()
	defer redisConn.Close()
	code,err := redis.String(redisConn.Do("get",req.Uuid))
	if err != nil {
		rsp.Errno = utils.RECODE_DATAERR
		rsp.Errmsg = utils.RecodeText(utils.RECODE_DATAERR)
		return err
	}

	if code != req.Text{
		rsp.Errno = utils.RECODE_DATAERR
		rsp.Errmsg = utils.RecodeText(utils.RECODE_DATAERR)
		return err
	}

然后使用传输过来的电话号码进行短信发送,代码借鉴我们品优购的短信发送代码,内容如下:

go
//发送短信
	//初始化客户端  需要accessKey  需要开通申请
	client, err := sdk.NewClientWithAccessKey("default", "LTAI01Pc2yYrheUc", "7diQBKg04TxpykIWli4VCVJUZbqmYX")
	if err != nil {
		rsp.Errno = utils.RECODE_DATAERR
		rsp.Errmsg = utils.RecodeText(utils.RECODE_DATAERR)
		return err
	}
	//获取6位数随机码
	rnd := rand.New(rand.NewSource(time.Now().UnixNano()))
	vcode := fmt.Sprintf("%06d", rnd.Int31n(1000000))

	//初始化请求对象
	request := requests.NewCommonRequest()
	request.Method = "POST"//设置请求方法
	request.Scheme = "https" // https | http   //设置请求协议
	request.Domain = "dysmsapi.aliyuncs.com"  //域名
	request.Version = "2017-05-25"			//版本号
	request.ApiName = "SendSms"				//api名称
	request.QueryParams["PhoneNumbers"] = req.Phone  //需要发送的电话号码
	request.QueryParams["SignName"] = "品优购"    //签名名称   需要申请
	request.QueryParams["TemplateCode"] = "SMS_164275022"   //模板号   需要申请
	request.QueryParams["TemplateParam"] = `{"code":`+vcode+`}`   //发送短信验证码

	response, err := client.ProcessCommonRequest(request)  //发送短信
	if err != nil {
		rsp.Errno = utils.RECODE_DATAERR
		rsp.Errmsg = utils.RecodeText(utils.RECODE_DATAERR)
		return err
	}

发送成功之后把数据存储到redis中,并返回结果,具体代码如下:

go
//校验是否发送成功,成功之后存储到redis中,失败返回错误信息
	if response.IsSuccess(){
		redisConn.Do("setex",req.Phone+"_code",60 * 5,vcode)
		rsp.Errno = utils.RECODE_OK
		rsp.Errmsg = utils.RecodeText(utils.RECODE_OK)
		return nil
	}else {
		rsp.Errno = utils.RECODE_SMSERR
		rsp.Errmsg = utils.RecodeText(utils.RECODE_SMSERR)
		return errors.New("发送短信失败")
	}

全部代码如下:

go
	//校验验证码输入是否正确
	redisConn := model.InitRedis().Get()
	defer redisConn.Close()
	code,err := redis.String(redisConn.Do("get",req.Uuid))
	if err != nil {
		rsp.Errno = utils.RECODE_DATAERR
		rsp.Errmsg = utils.RecodeText(utils.RECODE_DATAERR)
		return err
	}

	if code != req.Text{
		rsp.Errno = utils.RECODE_DATAERR
		rsp.Errmsg = utils.RecodeText(utils.RECODE_DATAERR)
		return err
	}

	//发送短信
	//初始化客户端  需要accessKey  需要开通申请
	client, err := sdk.NewClientWithAccessKey("default", "LTAI01Pc2yYrheUc", "7diQBKg04TxpykIWli4VCVJUZbqmYX")
	if err != nil {
		rsp.Errno = utils.RECODE_DATAERR
		rsp.Errmsg = utils.RecodeText(utils.RECODE_DATAERR)
		return err
	}
	//获取6位数随机码
	rnd := rand.New(rand.NewSource(time.Now().UnixNano()))
	vcode := fmt.Sprintf("%06d", rnd.Int31n(1000000))

	//初始化请求对象
	request := requests.NewCommonRequest()
	request.Method = "POST"//设置请求方法
	request.Scheme = "https" // https | http   //设置请求协议
	request.Domain = "dysmsapi.aliyuncs.com"  //域名
	request.Version = "2017-05-25"			//版本号
	request.ApiName = "SendSms"				//api名称
	request.QueryParams["PhoneNumbers"] = req.Phone  //需要发送的电话号码
	request.QueryParams["SignName"] = "品优购"    //签名名称   需要申请
	request.QueryParams["TemplateCode"] = "SMS_164275022"   //模板号   需要申请
	request.QueryParams["TemplateParam"] = `{"code":`+vcode+`}`   //发送短信验证码

	response, err := client.ProcessCommonRequest(request)  //发送短信
	if err != nil {
		rsp.Errno = utils.RECODE_DATAERR
		rsp.Errmsg = utils.RecodeText(utils.RECODE_DATAERR)
		return err
	}

	//校验是否发送成功,成功之后存储到redis中,失败返回错误信息
	if response.IsSuccess(){
		redisConn.Do("setex",req.Phone+"_code",60 * 5,vcode)
		rsp.Errno = utils.RECODE_OK
		rsp.Errmsg = utils.RecodeText(utils.RECODE_OK)
		return nil
	}else {
		rsp.Errno = utils.RECODE_SMSERR
		rsp.Errmsg = utils.RecodeText(utils.RECODE_SMSERR)
		return errors.New("发送短信失败")
	}

2.5.4web端实现

后台服务实现之后我们接着来写一下web端代码,首先需要先匹配对应路由,代码如下:

go
r1.GET("/smscode/:mobile",controller.GetSmscd)

接着我们来实现具体的控制器函数。

还是我们的四步骤,先获取数据,代码如下:

go
//获取数据
	phone := ctx.Param("mobile")
	text := ctx.Query("text")
	uuid := ctx.Query("id")

然后校验数据:

go
//校验电话号码格式
	reg ,_:= regexp.Compile(`^1[3,4,5,7,8]\d{9}$`)
	result := reg.MatchString(phone)
	if !result{
		resp["errno"] = utils.RECODE_MOBILEERR
		resp["errmsg"] = utils.RecodeText(utils.RECODE_MOBILEERR)
		ctx.JSON(500,resp)
		return
	}
	//校验数据不能为空
	if text == "" || uuid == "" {
		resp["errno"] = utils.RECODE_DATAERR
		resp["errmsg"] = utils.RecodeText(utils.RECODE_DATAERR)
		ctx.JSON(500,resp)
		return
	}

然后远程调用,获取返回数据,具体代码如下:

go
//调用远程服务
	grpcService := utils.GetGrpcService()
	client := getSmscd.NewGetSmscdService("go.micro.srv.getSmscd",grpcService.Client())
	rsp,err:= client.Call(context.TODO(),&getSmscd.Request{
		Text:text,
		Uuid:uuid,
		Phone:phone,
	})

	if err != nil {
		fmt.Println(err)
		resp["errno"] = utils.RECODE_SMSERR
		resp["errmsg"] = utils.RecodeText(utils.RECODE_SMSERR)
		ctx.JSON(500,resp)
		return
	}

2.6发送注册信息服务

2.6.1接口定义

go
#Request:
method: POST
url:api/v1.0/users
#data:
{
    mobile: "123", //手机号
    password: "123", //密码
    sms_code: "123"   //短信验证码
}
#Response
#返回成功:
{
    "errno": "0",   //状态码
    "errmsg":"ok"
}
#注册失败:
{
    "errno": "4001",   //状态码
    "errmsg":"状态错误信息"
}

2.6.2业务流程图

2.6.3具体业务实现

创建服务,命令如下:

shell
$ micro new --type srv project/postRet

接着我们修改proto文件并编译,修改后的proto文件内容如下:

protobuf
syntax = "proto3";

package go.micro.srv.postRet;

service PostRet {
	rpc Call(Request) returns (Response) {}
}

message Request {
	string mobile = 1;
	string password = 2;
	string sms_code = 3;
}

message Response {
	string errno = 1;
	string errmsg = 2;
}

具体的业务代码如下:

go
//从redis中获取短信验证码
	redisConn := model.InitRedis().Get()
	code,err := redis.String(redisConn.Do("get",req.Mobile+"_code"))
	if err != nil {
		rsp.Errno = utils.RECODE_DATAERR
		rsp.Errmsg = utils.RecodeText(utils.RECODE_DATAERR)
		return err
	}
	//校验验证码是否正确
	if code != req.SmsCode{
		rsp.Errno = utils.RECODE_DATAERR
		rsp.Errmsg = utils.RecodeText(utils.RECODE_DATAERR)
		return errors.New("验证码不正确")
	}

	//把用户数据插入到mysql中
	db,err := model.InitDb()
	if err != nil {
		rsp.Errno = utils.RECODE_DBERR
		rsp.Errmsg = utils.RecodeText(utils.RECODE_DBERR)
		return errors.New("数据库连接错误")
	}
	var user model.User
	user.Name = req.Mobile
	user.Mobile = req.Mobile

	//对密码做md5加密
	m5 := md5.New()
	m5.Write([]byte(req.Password))
	m5Pwd := hex.EncodeToString(m5.Sum(nil))

	user.Password_hash = m5Pwd
	err = db.Create(&user).Error
	if err != nil {
		rsp.Errno = utils.RECODE_DBERR
		rsp.Errmsg = utils.RecodeText(utils.RECODE_DBERR)
		return err
	}

2.6.4session中间件使用

首先我们来看一下gin中间件具体是什么?其实gin中间件功能很类似我们beego中的路由过滤器。不过功能更强大。我们先来看一个自定义中间件的简单示例:

go
func Logger() gin.HandlerFunc {
	return func(c *gin.Context) {
		t := time.Now()
		// 在路由之前调用上面代码
		c.Next()
		//在路由之后调用
		latency := time.Since(t)
		fmt.Println(latency)
	}
}
func main() {
	r := gin.Default()
	r.Use(Logger())

	r.GET("/test", func(c *gin.Context) {
			c.Writer.Write([]byte("hello world"))
	})

	r.Run(":8080")
}

除了自定义中间件,gin还提供了一些框架中间件,地址为:https://github.com/gin-contrib/

这里我们选择使用session。首先我们需要下载session,命令如下:

shell
$ go get -u -v github.com/gin-contrib/sessions

然后我们来看一下sessions的简单使用。首先是做session的初始化,主要是做session容器的设置,代码如下:

go
store, _ := redis.NewStore(10, "tcp", "localhost:6379", "", []byte("secret"))
router.Use(sessions.Sessions("mysession", store))

然后我们就可以在下面的路由中去使用相应的sessions操作了,具体操作如下:

go
s := sessions.Default(ctx)
	s.Set("name","itcast")
	name := s.Get("name")
	s.Delete("name")

但是大家这时候发现,在服务中我们并没有使用gin的路由,也没有对应的gin.Context,所以在服务中实现session的添加比较复杂,我们可以把session操作放在web端来处理,具体操作我们接着往下看。

2.6.5web端实现

首先是做路由匹配,代码如下:

go
r1.POST("/users",controller.PostRet)

有请求之后,接着我们去实现对应控制器方法。首先是获取数据,代码如下:

go
//确定容器
	resp := make(map[string]interface{})
	//绑定数据
	var regUser RegisterUser
	err := ctx.Bind(&regUser)
	if err != nil {
		resp["errno"] = utils.RECODE_DATAERR
		resp["errmsg"] = utils.RecodeText(utils.RECODE_DATAERR)
		ctx.JSON(200,resp)
		return
	}

这里面需要注意,gin里面获取post上传的formdata数据使用postForm,但是如果获取json格式数据,需要去绑定对应结构体,所以这里面我们需要先创建一个和json数据对应的结构体,结构体结构如下:

go
type RegisterUser struct {
	Mobile string `json:"mobile"`
	Password string  `json:"password"`
	SmsCode string    `json:"sms_code"`
}

这样我们久能获取数据了,然后需要对数据进行校验,代码如下:

go
//校验数据
	if regUser.Mobile == "" || regUser.Password == "" || regUser.SmsCode == ""{
		resp["errno"] = utils.RECODE_DATAERR
		resp["errmsg"] = utils.RecodeText(utils.RECODE_DATAERR)
		ctx.JSON(200,resp)
		return
	}

	reg,_ := regexp.Compile(`^1[3,4,5,7,8]\d{9}$`)
	if !reg.MatchString(regUser.Mobile){
		resp["errno"] = utils.RECODE_DATAERR
		resp["errmsg"] = utils.RecodeText(utils.RECODE_DATAERR)
		ctx.JSON(200,resp)
		return
	}

然后进行远程服务调用:

go
//远程调用
	grpcService := utils.GetGrpcService()
	grpcClient := postRet.NewPostRetService("go.micro.srv.postRet",grpcService.Client())
	respData,err := grpcClient.Call(context.TODO(),&postRet.Request{Mobile:regUser.Mobile,Password:regUser.Password,SmsCode:regUser.SmsCode})
	if err != nil {
		resp["errno"] = utils.RECODE_DATAERR
		resp["errmsg"] = utils.RecodeText(utils.RECODE_DATAERR)
		ctx.JSON(200,resp)
		return
	}

根据返回值,设置session,代码如下:

go
	if respData.Errno == utils.RECODE_OK{
		s := sessions.Default(ctx)
		s.Set("userName",regUser.Mobile)
        s.Save()
	}