Appearance
租房网
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动词来实现资源的状态扭转:
比如: 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/
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:两个参数,第一个参数是资源路径,第二个参数是指定的文件夹,示例:
gorouter.Static("/assets", "./assets")
StaticFS:两个参数,第一个参数是资源路径,第二个参数是http.Dir(),示例代码如下:
gorouter.StaticFS("/more_static", http.Dir("my_file_system"))
StaticFile:俩个参数,第一个参数是资源路径,第二个参数是具体的文件
router.StaticFile("/favicon.ico", "./resources/favicon.ico")
运行项目,打开浏览器输入网址127.0.0.1:8080/home
,查看页面如下:
按f12打开开发者工具,然后再次刷新页面,结果如下:
这些报红的页面就是需要我们接着来实现的具体功能,让我们先从获取地域信息开始!
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业务流程图
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服务和获取地域信息服务,然后再打开我们页面,点击选择城区,显示如下:
地域信息就获取到了
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)
}
这时候再刷新页面,显示如下:
接着我们点击注册按钮,显示注册页面,页面如下:
2.4.2获取验证码图片
进入注册页面之后,在开发者工具中,会发现下面的问题:
验证码请求未实现,接着我们就来实现验证码服务。首先是创建服务,命令如下:
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)
代码中共有两种生成图片的方式,一种是生成带验证码的图片,并返回验证码,一种是指定验证码内容,生成图片。
运行这个代码, 在浏览器中访问,显示如下:
根据示例代码,来实现我们的生成验证码服务。具体实现如下:
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)
}
这时候重启项目,访问注册线面,显示如下:
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(®User)
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()
}