Blog

Gin

Begin

安装Gin:

go get -u github.com/gin-gonic/gin

例子:

package main

import (
	"fmt"
	"github.com/gin-gonic/gin"
	"log"
)
func main() {
	engine := gin.Default()
	engine.GET("/hello", func(context *gin.Context) {
		fmt.Println("请求路径", context.FullPath())
		context.Writer.Write([]byte("Hello, Gin\n"))
	})
	if err := engine.Run(":8000"); err!= nil {
		log.Fatal(err.Error())
	}
}

网络请求与路由处理

创建Engine

在gin框架中,Engine被定义成为一个结构体,Engine代表gin框架的一个结构体定义,其中包含了路由组、中间件、页面渲染接口、框架配置设置等相关内容。默认的Engine可以通过gin.Default进行创建,或者使用gin.New()同样可以创建。两种方式如下所示:

engine1 = gin.Default()
engine2 = gin.New()

gin.Default()和gin.New()的区别在于gin.Default也使用gin.New()创建engine实例,但是会默认使用Logger和Recovery中间件。

Logger是负责进行打印并输出日志的中间件,方便开发者进行程序调试;Recovery中间件的作用是如果程序执行过程中遇到panic中断了服务,则Recovery会恢复程序执行,并返回服务器500内部错误。通常情况下,我们使用默认的gin.Default创建Engine实例。

RESTful

REST与技术无关,代表的是一种软件架构风格,REST是Representational State Transfer的简称,中文翻译为“表征状态转移”或“表现层状态转化”。

简单来说,REST的含义就是客户端与Web服务器之间进行交互的时候,使用HTTP协议中的4个请求方法代表不同的动作。

  • GET用来获取资源
  • POST用来新建资源
  • PUT用来更新资源
  • DELETE用来删除资源。

只要API程序遵循了REST风格,那就可以称其为RESTful API。目前在前后端分离的架构中,前后端基本都是通过RESTful API来进行交互。

处理HTTP请求

在创建的engine实例中,包含很多方法可以直接处理不同类型的HTTP请求。

HTTP请求类型

http协议中一共定义了八种方法或者称之为类型来表明对请求网络资源(Request-URI)的不同的操作方式,分别是:OPTIONS、HEAD、GET、POST、PUT、DELETE、TRACE、CONNECT。

通用处理

engine中可以直接进行HTTP请求的处理,在engine中使用Handle方法进行http请求的处理。Handle方法包含三个参数,具体如下所示:

func (group *RouterGroup) Handle(httpMethod, relativePath string, handlers ...HandlerFunc) IRoutes
  • httpMethod:第一个参数表示要处理的HTTP的请求类型,是GET、POST、DELETE等8种请求类型中的一种。
  • relativePath:第二个参数表示要解析的接口,由开发者进行定义。
  • handlers:第三个参数是处理对应的请求的代码的定义。

例子:

package main

import (
	"fmt"
	"github.com/gin-gonic/gin"
)

func main() {
	engine := gin.Default()
	engine.Handle("GET", "/hello", func(context *gin.Context) {
		fmt.Println(context.FullPath())

		name := context.DefaultQuery("name", "hello")
		
		fmt.Println(name)
		context.Writer.Write([]byte("hello, " + name))
	})

	engine.Handle("POST", "/login", func(context *gin.Context) {
		fmt.Println(context.FullPath())

		username := context.PostForm("username")
		password := context.PostForm("password")
		
		fmt.Println(username, password)
		context.Writer.Write([]byte(username + "登陆成功!"))
	})
	
	engine.Run()
}

分类处理

除了engine中包含的通用的处理方法以外,engine还可以按类型进行直接解析。engine中包含有get方法、post方法、delete方法等与http请求类型对应的方法。

例子:

package main

import (
	"fmt"
	"github.com/gin-gonic/gin"
)

func main() {
	engine := gin.Default()

	engine.GET("/hello", func(context *gin.Context) {
		fmt.Println(context.FullPath())

		name := context.Query("name")
		fmt.Println(name)
		context.Writer.Write([]byte("hello, " + name))
	})

	engine.POST("/login", func(context *gin.Context) {
		fmt.Println(context.FullPath())

		username, exist := context.GetPostForm("username")
		if exist {
			fmt.Println(username)
		}
		password, exists := context.GetPostForm("password")
		if exists {
			fmt.Println(password)
		}

		context.Writer.Write([]byte(username + "登陆成功!"))
	})

	engine.DELETE("/user/:id", func(context *gin.Context) {
		id := context.Param("id")
		fmt.Println(id)
		context.Writer.Write([]byte("delete id = " + id))
	})
	engine.Run()
}

请求参数绑定与多数据格式处理

如果表单数据较多时,使用PostForm和GetPostForm一次获取一个表单数据,开发效率较慢。Gin框架提供给开发者表单实体绑定的功能,可以将表单数据与结构体绑定。

使用PostForm这种单个获取属性和字段的方式,代码量较多,需要一个一个属性进行获取。而表单数据的提交,往往对应着完整的数据结构体定义,其中对应着表单的输入项。gin框架提供了数据结构体和表单提交数据绑定的功能,提高表单数据获取的效率。

ShouldBindQuery

使用ShouldBindQuery可以实现Get方式的数据请求的绑定。具体实现如下:

package main

import (
	"fmt"
	"github.com/gin-gonic/gin"
	"log"
)

func main() {
	engine := gin.Default()
	engine.GET("/hello", func(context *gin.Context) {
		fmt.Println(context.FullPath())
		var student Student
		err := context.ShouldBindQuery(&student)
		if err != nil {
			log.Fatal(err.Error())
			return
		}
		fmt.Println(student.Name)
		fmt.Println(student.Classes)

		context.Writer.Write([]byte("hello, " + student.Name))
	})
	engine.Run()
}

type Student struct {
	Name string	`form:"name"`
	Classes string `form:"classes"`
}

ShouldBind

使用ShouldBind可以实现Post方式的提交数据的绑定工作。具体实现如下:

package main

import (
	"fmt"
	"github.com/gin-gonic/gin"
	"log"
)

func main() {
	engine := gin.Default()

	engine.POST("/login", func(context *gin.Context) {
		fmt.Println(context.FullPath())
		var register Register
		if err := context.ShouldBind(&register); err != nil {
			log.Fatal(err.Error())
			return
		}
		fmt.Println(register)
		context.Writer.Write([]byte("Name: " + register.UserName + "\nPhone: " + register.Phone))
	})

	engine.Run()
}

type Register struct {
	UserName string `form:"name"`
	Phone string `form:"phone"`
	Password string `form:"password"`
}

ShouldBindJson

当客户端使用Json格式进行数据提交时,可以采用ShouldBindJson对数据进行绑定并自动解析。具体实现如下:

package main

import (
	"fmt"
	"github.com/gin-gonic/gin"
	"log"
)

func main() {
	engine := gin.Default()
	engine.POST("/addStudent", func(context *gin.Context) {
		fmt.Println(context.FullPath())
		var person Person
		if err := context.BindJSON(&person); err != nil {
			log.Fatal(err.Error())
			return
		}
		fmt.Println(person)
		context.Writer.Write([]byte("add: " + person.Name))
	})
	engine.Run()
}

type Person struct {
	Name string `form:name`
	Sex string `form:sex`
	Age int `form:age`
}

多数据格式返回请求结果

一个完整的请求包含请求、处理请求和结果返回三个步骤,在服务器端对请求处理完成以后,会将结果返回给客户端。

在gin框架中,支持返回多种请求数据格式。

[]byte

engine.GET("/helloByte", func(context *gin.Context) {
    fullPath := context.FullPath()
    fmt.Println(fullPath)
    context.Writer.Write([]byte(fullPath))
})

string

engine.GET("/helloString", func(context *gin.Context) {
    fullPath := context.FullPath()
    fmt.Println(fullPath)
    context.Writer.WriteString(fullPath)
})

JSON

gin框架中的context包含的JSON方法可以将结构体类型的数据转换成JSON格式的结构化数据,然后返回给客户端。

map

engine.GET("/helloJson", func(context *gin.Context) {
    fullPath := context.FullPath()
    fmt.Println(fullPath)
    context.JSON(200, map[string]interface{}{
        "code":1,
        "msg":"OK",
        "data":fullPath,
    })
})

结构体

engine.GET("/helloStruct", func(context *gin.Context) {
    fullPath := context.FullPath()
    fmt.Println(fullPath)
    resp := Response{Code: 1, Msg: "OK", Data: fullPath}
    context.JSON(200, &resp)
})
...
type Response struct {
	Code int
	Msg string
	Data interface{}
}

HTML模板

package main

import (
	"fmt"
	"github.com/gin-gonic/gin"
	"net/http"
)

func main() {
	engine := gin.Default()
	engine.LoadHTMLGlob("./html/*")
	engine.Static("/img", "./img")
	engine.GET("/helloHtml", func(context *gin.Context) {
		fullPath := context.FullPath()
		fmt.Println(fullPath)
		context.HTML(http.StatusOK, "index.html", gin.H{
			"fullPath": fullPath,
			"title": "Hello, Gin",
		})
	})
	engine.Run()
}
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>{{.title}}</title>
</head>
<body>
<center>{{.fullPath}}</center>
<div align="center">
    <img src="../img/go.jpeg">
</div>
</body>
</html>

加载静态资源

在上面的index.html的基础上,添加一张img进行展示。需要将img所在的目录进行静态资源路径设置才可能会生效,如下所示:

engine.Static("/img", "./img")

在工程项目的根目录下创建img目录,用于存放静态的img资源。

同理,在项目开发时,一些静态的资源文件如html、js、css等可以通过静态资源文件设置的方式来进行设置。

使用路由组分类处理请求

背景

在实际的项目开发中,均是模块化开发。同一模块内的功能接口,往往会有相同的接口前缀。

例如在系统中有用户模块,用户有不同注册、登录、用户信息。

注册:http://localhost:9000/user/register
登录:http://localhost:9000/user/login
用户信息:http://localhost:9000/user/info
删除:http://localhost:9000/user/1001

类似这种接口前缀统一,均属于相同模块的功能接口。可以使用路由组进行分类处理。

Group

gin框架中可以使用路由组来实现对路由的分类。

路由组是router.Group中的一个方法,用于对请求进行分组。

package main

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

func main() {
	engine := gin.Default()
	routerGroup := engine.Group("/user")
	// http://localhost:8080/user/register
	routerGroup.POST("/register", regHandle)
	routerGroup.DELETE("/:id", delHandle)
	engine.Run()
}

func regHandle(context *gin.Context) {
	context.Writer.WriteString(context.FullPath())
}

func delHandle(context *gin.Context) {
	id := context.Param("id")
	context.Writer.WriteString(string(id))
}

中间件的编写和使用

中间件

在web应用服务中,完整的一个业务处理在技术上包含客户端操作、服务器端处理、返回处理结果给客户端三个步骤。

在实际的业务开发和处理中,会有更负责的业务和需求场景。一个完整的系统可能要包含鉴权认证、权限管理、安全检查、日志记录等多维度的系统支持。

鉴权认证、权限管理、安全检查、日志记录等这些保障和支持系统业务属于全系统的业务,和具体的系统业务没有关联,对于系统中的所有业务都适用。

由此,在业务开发过程中,为了更好的梳理系统架构,可以将上述描述所涉及的一些通用业务单独抽离并进行开发,然后以插件化的形式进行对接。这种方式既保证了系统功能的完整,同时又有效的将具体业务和系统功能进行解耦,并且,还可以达到灵活配置的目的。

这种通用业务独立开发并灵活配置使用的组件,一般称之为"中间件",因为其位于服务器和实际业务处理程序之间。其含义就是相当于在请求和具体的业务逻辑处理之间增加某些操作,这种以额外添加的方式不会影响编码效率,也不会侵入到框架中。中间件的位置和角色示意图如下图所示:

Gin的中间件

在gin中,中间件称之为middleware,中间件的类型定义如下所示:

// HandlerFunc defines the handler used by gin middleware as return value.
type HandlerFunc func(*Context)

HandlerFunc是一个函数类型,接收一个Context参数。用于编写程序处理函数并返回HandleFunc类型,作为中间件的定义。

中间件Use用法

使用gin.Default创建了gin引擎engins变量,其中,就使用了中间件。如下图所示:

func Default() *Engine {
	debugPrintWARNINGDefault()
	engine := New()
	engine.Use(Logger(), Recovery())
	return engine
}
//Log中间件
func Logger() HandlerFunc {
	return LoggerWithConfig(LoggerConfig{})
}
//Recovery中间件
func Recovery() HandlerFunc {
	return RecoveryWithWriter(DefaultErrorWriter)
}

在Default函数中,engine调用Use方法设置了Logger中间件和Recovery中间件。Use函数接收一个可变参数,类型为HandlerFunc,恰为中间件的类型。Use方法定义如下:

func (engine *Engine) Use(middleware ...HandlerFunc) IRoutes {
	engine.RouterGroup.Use(middleware...)
	engine.rebuild404Handlers()
	engine.rebuild405Handlers()
	return engine
}

自定义中间件

根据上文的介绍,可以自己定义实现一个特殊需求的中间件,中间件的类型是函数,有两条标准:

  • func函数
  • 返回值类型为HandlerFunc

比如,我们自定义一个自己的中间件。在前面所学的内容中,我们在处理请求时,为了方便代码调试,通常都将请求的一些信息打印出来。有了中间件以后,为了避免代码多次重复编写,使用统一的中间件来完成。定义一个名为RequestInfos的中间件,在该中间件中打印请求的path和method。具体代码实现如下所示:

package main

import (
	"fmt"
	"github.com/gin-gonic/gin"
)

func main() {
	engine := gin.Default()
	//全部路由都使用中间件
	//engine.Use(RequestInfos())
	engine.GET("/query", func(context *gin.Context) {
		context.JSON(200, map[string]interface{}{
			"code":1,
			"msg":context.FullPath(),
		})
	})
	//单个路由使用中间件,放在第二个参数
	engine.GET("/single", RequestInfos(), func(context *gin.Context) {
		context.Writer.WriteString("single")
	})
	engine.Run()
}

// RequestInfos 打印请求信息的中间件
func RequestInfos() gin.HandlerFunc {
	return func(context *gin.Context) {
		path := context.FullPath()
		method := context.Request.Method
		fmt.Println("请求path:", path)
		fmt.Println("请求method:", method)
	}
}

context.Next函数

在上文自定义的中间件RequestInfos中,打印了请求了请求的path和method,接着去执行了正常的业务处理函数。如果我们想输出业务处理结果的信息,该如何实现呢。答案是使用context.Next函数。

context.Next函数可以将中间件代码的执行顺序一分为二,Next函数调用之前的代码在请求处理之前之前,当程序执行到context.Next时,会中断向下执行,转而先去执行具体的业务逻辑,执行完业务逻辑处理函数之后,程序会再次回到context.Next处,继续执行中间件后续的代码。

例子:

package main

import (
	"fmt"
	"github.com/gin-gonic/gin"
)

func main() {
	engine := gin.Default()
	//全部路由都使用中间件
	engine.Use(RequestInfos())
	engine.GET("/query", func(context *gin.Context) {
		fmt.Println("处理接口请求。")
		context.JSON(404, map[string]interface{}{
			"code":1,
			"msg":context.FullPath(),
		})
	})
	engine.Run()
}

// RequestInfos 打印请求信息的中间件
func RequestInfos() gin.HandlerFunc {
	return func(context *gin.Context) {
		path := context.FullPath()
		method := context.Request.Method
		fmt.Println("请求path:", path)
		fmt.Println("请求method:", method)
		fmt.Println("状态码:", context.Writer.Status())

		context.Next()

		fmt.Println("状态码:", context.Writer.Status())
	}
}

输出:

请求path: /query
请求method: GET
状态码: 200
处理接口请求。
状态码: 404

访问和操作数据库

引入:

go get "github.com/go-sql-driver/mysql"

例子:

package main

import (
	"database/sql"
	"fmt"
	"log"
)
import _ "github.com/go-sql-driver/mysql"

func main() {
	//连接数据库
	connStr := "root:@tcp(127.0.0.1:3306)/mydb1"
	db, err := sql.Open("mysql", connStr)
	if err != nil {
		log.Fatal(err.Error())
		return
	}

	//创建数据库
	_, err = db.Exec(`create table person(
    id int primary key auto_increment,
    name varchar(20) not null,
    age int default 1);`)
	if err != nil {
		log.Fatal(err.Error())
		return
	} else {
		fmt.Println("创建数据库成功!")
	}

	//插入数据
	_, err = db.Exec("insert into person(name, age) values (?, ?);", "Tom", 18)
	if err != nil {
		log.Fatal(err.Error())
		return
	} else {
		fmt.Println("数据插入成功!")
	}

	//查询数据
	rows, err := db.Query("select id, name, age from person;")
	if err != nil {
		log.Fatal(err.Error())
		return
	}
	scan:
	if rows.Next() {
		person := new(Person)
		err = rows.Scan(&person.Id, &person.Name, &person.Age)
		if err != nil {
			log.Fatal(err.Error())
			return
		}
		fmt.Println(person)
		goto scan
	}
}

type Person struct {
	Id int
	Name string
	Age int
}

项目初始化

app.json:

{
  "app_name": "Cloud Restaurant",
  "app_mode": "debug",
  "app_host": "localhost",
  "app_port": "8000"
}

HelloController.go:

package controller

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

type HelloController struct {

}

func (hello *HelloController)Router(engine *gin.Engine) {
	engine.GET("/hello", hello.Hello)
}

func (hello *HelloController) Hello(context *gin.Context) {
	context.JSON(200, map[string]interface{}{
		"hello":"hello Cloud Restaurant",
	})
}

Config.go:

package tool

import (
	"bufio"
	"encoding/json"
	"os"
)

type Config struct {
	AppName string	`json:"app_name"`
	AppMode string	`json:"app_mode"`
	AppHost string	`json:"app_host"`
	AppPort	string	`json:"app_port"`
}

var _cfg *Config = nil

func ParseConfig(path string) (*Config, error) {
	file, err := os.Open(path)
	if err!= nil {
		panic(err)
	}
	defer file.Close()

	reader := bufio.NewReader(file)
	decoder := json.NewDecoder(reader)
	if err = decoder.Decode(&_cfg); err != nil {
		return nil, err
	}
	return _cfg, nil
}

main.go:

package main

import (
	"CloudRestaurant/controller"
	"CloudRestaurant/tool"
	"github.com/gin-gonic/gin"
)

func main() {
	config, err := tool.ParseConfig("./config/app.json")
	if err != nil {
		panic(err.Error())
	}

	app := gin.Default()
	registerRouter(app)
	app.Run(config.AppHost + ":" + config.AppPort)
}

//路由设置
func registerRouter(router *gin.Engine)  {
	new(controller.HelloController).Router(router)
}

项目前端介绍

目录说明

  • build:build目录是指定的项目编译目录,该项目的编译配置相关的操作,都在该目录中进行配置和指定。
  • config:config目录主要是对项目全局进行相关的配置,以及测试或者发布版本进行配置。
  • dist:所有的.vue页面编译后成为的js文件,都会输出到该目录中。
  • node_modules:该目录是nodejs项目所需要的依赖的第三方库的代码目录。由于该目录体积较大,在进行项目迁移或者项目拷贝时,可以将该目录删除,在项目的根目录中执行npm install命令会重新生成并下载所需要的第三方的代码库。
  • src:该目录是存放前端工程项目的源码目录。
  • static:该目录用于存放静态的文件,比如js、css文件等。
  • package.json:执行npm init命令会生成该文件,该文件是node项目的配置文件,其中包含项目的编译和第三方依赖库依赖信息等内容。

请求接口API

在shop-client前端项目的src目录下的api目录中,有两个js文件,分别为ajax.js文件和index.js文件。

  • ajax.js文件:该文件中封装了前端项目进行异步网络请求的方法ajax,该函数包含三个参数,分别是:url、data、type,表示请求路径,请求参数和请求方法。
  • index.js文件:在该文件中,引入了ajax.js文件和其中的ajax方法,定义了基础请求路径BASE_URL常量,此项目中的请求端口为8090,与后台服务器监听的端口一致。如果想自己修改服务器监听端口,要记得将前端代码BASE_URL常量中的端口也要一并修改。另外,在index.js文件中定义了本项目功能开发所需要的接口,供在.vue文件中进行使用。

常用命令

可以使用一些常用命令对项目进行编译、测试启动运行等操作。在项目根目录下的package.json目录文件中,可以找到有scripts标签配置,其中就定义和配置了不同功能的脚本命令。scripts标签配置内容如下所示:

...
"scripts": {
    "dev": "webpack-dev-server --inline --progress --config build/webpack.dev.conf.js",
    "start": "npm run dev",
    "lint": "eslint --ext .js,.vue src",
    "build": "node build/build.js"
}
...

按照上述的脚本命令配置,可以实现很多指令功能。 比如说,可以使用如下命令运行编译项目:

npm run build

上述命令会具体执行node build/build.js命令,用于根据build.js文件对项目进行编译。

另外,还可以使用如下命令运行nodejs项目:

npm start

因为在scripts脚本中配置了start命令,只有start命令可以省略run。当然,npm start会具体执行的指令是npm run dev,因此也可以通过npm run dev命令来运行项目。

集成第三方短信发送功能

申请阿里云服务

  1. 申请签名
  2. 申请模板
  3. 创建AccessKey和AccessKeySercet

集成

SDK

安装阿里云Go SDK:

go get github.com/aliyun/alibaba-cloud-sdk-go

tool/Config.go

type SmsConfig struct {
	SignName     string `json:"sign_name"`
	TemplateCode string `json:"template_code"`
	AppKey       string `json:"app_key"`
	AppSecret    string `json:"app_secret"`
	RegionId     string `json:"region_id"`
}

config/app.json

  "sms": {
    "sign_name": "",
    "template_code": "",
    "app_key": "",
    "app_secret": "",
    "region_id": "cn-hangzhou"
  },

controller/MemberController.go

func (mc *MemberController) Router(engine *gin.Engine) {
	engine.GET("/api/sendcode", mc.sendSmsCode)
}

func (mc *MemberController) sendSmsCode(context *gin.Context)  {
	phone, exist := context.GetQuery("phone")
	if !exist{
		context.JSON(200, map[string]interface{}{
			"code":0,
			"msg":"参数解析失败",
		})
		return
	}
	ms := service.MemberService{}
	isSend := ms.SendCode(phone)
	if isSend {
		context.JSON(200, map[string]interface{}{
			"code":1,
			"msg":"发送成功",
		})
		return
	}
	context.JSON(200, map[string]interface{}{
		"code":1,
		"msg":"发送失败",
	})
}

service/MemberService.go

参考API:https://api.aliyun.com/?spm=5176.12207334.0.0.54d71cbe3NE29f#/?product=Dysmsapi&version=2017-05-25&api=SendSms&params={%22RegionId%22:%22cn-hangzhou%22}&tab=DEMO&lang=GO

func (ms *MemberService) SendCode(phone string) bool {
	//产生一个验证码
	code := fmt.Sprintf("%06v", rand.New(rand.NewSource(time.Now().UnixNano())).Int31n(1000000))
	//调用阿里云短信接口发送短信
	smsConfig := tool.GetConfig().Sms
	client, err := dysmsapi.NewClientWithAccessKey(smsConfig.RegionId, smsConfig.AppKey, smsConfig.AppSecret)
	if err != nil {
		log.Fatal(err.Error())
		return false
	}

	request := dysmsapi.CreateSendSmsRequest()

	request.Scheme = "https"
	request.SignName = smsConfig.SignName
	request.TemplateCode = smsConfig.TemplateCode
	request.PhoneNumbers = phone

	par, err := json.Marshal(map[string]interface{}{
		"name": code,
	})

	request.TemplateParam = string(par)
	response, err := client.SendSms(request)

	fmt.Println( response)

	if err != nil {
		log.Fatal(err.Error())
		return false
	}
	if response.Code == "OK" {
		return true
	}
	return false
}

测试

GET请求以下地址:

http://localhost:8000/api/sendcode?phone=17700000000

如果出现如下错误:

missing go.sum entry for module providing package <package_name>

执行go mod tidy来整理依赖,这个命令会:

  • 删除不需要的依赖包
  • 下载新的依赖包
  • 更新go.sum

验证码保存到数据库

在用户接受到验证码以后,输入验证码进行登录,我们需要验证用户输入的验证码是否正确。

因此,我们需要将发送过的验证码通过持久化的方式保存下来,方便我们进行校验。我们选择通过数据库来存储用户手机验证码。

Xorm

在项目开发过程中,我们会使用一些成熟的框架来操作数据库。xorm就是一个比较流行的数据库操作orm框架。

安装xorm:

go get github.com/go-xorm/xorm

安装mysql驱动:

go get github.com/go-sql-driver/mysql

连接数据库

首先创建数据库:

create database cloudrestaurant;

tool/OrmEngine.go

package tool

import (
	"CloudRestaurant/model"
	"github.com/go-xorm/xorm"
)
import _ "github.com/go-sql-driver/mysql"

var DbEngine *Orm

type Orm struct {
	*xorm.Engine
}

func OrmEngine(cfg *Config) (*Orm, error) {
	database := cfg.Database
	conn := database.Username + ":" + database.Password + "@tcp(" + database.Host + ":" + database.Port + ")/" +database.DbName + "?charset=" + database.Charset
	engine, err := xorm.NewEngine(database.Driver, conn)
	if err != nil {
		return nil, err
	}
	engine.ShowSQL(database.ShowSql)

	//同步生成数据库表
	err = engine.Sync2(new(model.Member), new(model.SmsCode))
	if err != nil {
		return nil,err
	}

	orm :=  new(Orm)
	orm.Engine = engine
	DbEngine = orm
	return orm, nil
}

app.json

连接数据库有些参数需要自己指定,比如驱动类型,登录数据库的用户名,密码,数据库名等。将这些变量配置在app.json配置文件中。

...
  "database": {
    "driver": "mysql",
    "username": "root",
    "password": "",
    "host": "localhost",
    "port": "3306",
    "db_name": "cloudrestaurant",
    "charset": "utf8",
    "show_sql": true
  }
...

tool/Config.go

在Config结构体中添加对dtabase的解析。

...
type DatabaseConfig struct {
	Driver   string `json:"driver"`
	Username string `json:"username"`
	Password string `json:"password"`
	Host     string `json:"host"`
	Port     string `json:"port"`
	DbName   string `json:"db_name"`
	Charset  string `json:"charset"`
	ShowSql  bool   `json:"show_sql"`
}
...

model/SmsCode.go

要存储验证码,需要在数据库中创建表结构进行存储。我们可以创建SmsCode结构体,并通过tag设置数据库字段约束。

package model

type SmsCode struct {
	Id int64 `xorm:"pk autoincr" json:"id"`
	Phone string `xorm:"varchar(11)" json:"phone"`
	BizId string `xorm:"varchar(30)" json:"biz_id"`
	Code string `xorm:"varchar(6)" json:"code"`
	CreateTime int64 `xorm:"bigint" json:"create_time"`
}

dao/MemberDao.go

package dao

import (
	"CloudRestaurant/model"
	"CloudRestaurant/tool"
	"log"
)

type MemberDao struct {
	*tool.Orm
}

func (md *MemberDao) InsertCode(sms model.SmsCode) int64 {
	result, err := md.InsertOne(&sms)
	if err != nil {
		log.Fatal(err.Error())
	}
	return result
}

service/MemberService.go

将验证码数据保存到数据库中。

...
	if response.Code == "OK" {
		smsCode := model.SmsCode{Phone: phone, Code: code, BizId: response.BizId, CreateTime: time.Now().Unix()}
		memberDao := dao.MemberDao{tool.DbEngine}
		result := memberDao.InsertCode(smsCode)
		return result > 0
	}
...

main.go

package main

import (
	"CloudRestaurant/controller"
	"CloudRestaurant/tool"
	"github.com/gin-gonic/gin"
	"log"
)

func main() {
	config, err := tool.ParseConfig("./config/app.json")
	if err != nil {
		panic(err.Error())
	}

	_, err = tool.OrmEngine(config)
	if err != nil {
		log.Fatal(err.Error())
		return
	}

	app := gin.Default()
	registerRouter(app)
	app.Run(config.AppHost + ":" + config.AppPort)
}

//路由设置
func registerRouter(router *gin.Engine)  {
	new(controller.HelloController).Router(router)
	new(controller.MemberController).Router(router)
}

注册数据插入

dao/MemberDao.go

package dao

import (
	"CloudRestaurant/model"
	"CloudRestaurant/tool"
	"log"
)

type MemberDao struct {
	*tool.Orm
}

func (md *MemberDao) ValidateSmsCode(phone string, code string) *model.SmsCode {
	var sms model.SmsCode
	if _, err := md.Where("phone=? and code=?", phone, code).Get(&sms); err != nil {
		log.Fatal(err.Error())
	}
	return &sms
}

func (md *MemberDao) InsertCode(sms model.SmsCode) int64 {
	result, err := md.InsertOne(&sms)
	if err != nil {
		log.Fatal(err.Error())
	}
	return result
}

func (md *MemberDao) QueryByPhone(phone string) *model.Member {
	var member model.Member
	if _, err := md.Where("mobile=?", phone).Get(&member); err != nil {
		log.Fatal(err.Error())
	}
	return &member
}

func (md *MemberDao) InsertMember(member model.Member) int64 {
	result, err := md.InsertOne(&member)
	if err != nil {
		log.Fatal(err.Error())
	}
	return result
}

全局跨域请求处理设置

跨域访问

在浏览器中的任意一个页面地址,或者访问后台的api接口url,其实都包含几个相同的部分:

  1. 通信协议:又称protocol,有很多通信协议,比如http,tcp/ip协议等等。
  2. 主机:也就是常说的host。
  3. 端口:即服务所监听的端口号。
  4. 资源路径:端口号后面的内容即是路径。

当在一个页面中发起一个新的请求时,如果通信协议、主机和端口,这三部分内容中的任意一个与原页面的不相同,就被称之为跨域访问

如,在gin接口项目中,前端使用nodejs开发,运行在8080端口,我们访问的应用首页是:http://localhost:8080。 在使用gin框架开发的api项目中,服务端的监听端口为8090。

一个端口数8080,一个是8090,两者端口不同,因此按照规定,发生了跨域访问。

OPTIONS请求

前端vue开发的功能,使用axios发送POST登录请求。在请求时发生了跨域访问,因此浏览器为了安全起见,会首先发起一个请求测试一下此次访问是否安全,这种测试的请求类型为OPTIONS,又称之为options嗅探,同时在header中会带上origin,用来判断是否有跨域请求权限。

然后服务器相应Access-Control-Allow-Origin的值,该值会与浏览器的origin值进行匹配,如果能够匹配通过,则表示有跨域访问的权限。

跨域访问权限检查通过,会正式发送POST请求。

服务端设置跨域访问

可以在gin服务端,编写程序进行全局设置。通过中间件的方式设置全局跨域访问,用以返回Access-Control-Allow-Origin和浏览器进行匹配。

在服务端编写跨域访问中间件,如下:

// Cors 跨域访问:cross origin resource share
func Cors() gin.HandlerFunc {
	return func(context *gin.Context) {
		method := context.Request.Method
		origin := context.Request.Header.Get("Origin")
		var headerKeys []string
		for k, _ := range context.Request.Header {
			headerKeys = append(headerKeys, k)
		}
		headerStr := strings.Join(headerKeys, ",")
		if headerStr != "" {
			headerStr = fmt.Sprintf("access-control-allow-origin, access-control-allow-headers, %s", headerStr)
		} else {
			headerStr = "access-control-allow-origin, access-control-allow-headers"
		}

		if origin != "" {
			context.Writer.Header().Set("Access-Control-Allow-Origin", "*")
			context.Header("Access-Control-Allow-Origin", "*") // 设置允许访问所有域
			context.Header("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE, UPDATE")
			context.Header("Access-Control-Allow-Headers", "Authorization, Content-Length, X-CSRF-Token, Token,session,X_Requested_With,Accept, Origin, Host, Connection, Accept-Encoding, Accept-Language,DNT, X-CustomHeader, Keep-Alive, User-Agent, X-Requested-With, If-Modified-Since, Cache-Control, Content-Type, Pragma")
			context.Header("Access-Control-Expose-Headers", "Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers,Cache-Control,Content-Language,Content-Type,Expires,Last-Modified,Pragma,FooBar")
			context.Header("Access-Control-Max-Age", "172800")
			context.Header("Access-Control-Allow-Credentials", "false")
			context.Set("content-type", "application/json") // 设置返回格式是json
		}
		if method == "OPTIONS" {
			context.JSON(http.StatusOK, "Options Request!")
		}
		//处理请求
		context.Next()
	}
}

需要在设置路由之前启用中间件:

...
	//设置全局跨域访问
	app.Use(Cors())
	registerRouter(app)
	
	app.Run(config.AppHost + ":" + config.AppPort)
...

服务器设置好跨域访问以后,重新启动服务器api程序,并在浏览器端重新访问。可以看到正常发送了OPTIONS嗅探后,正常发送了POST请求。

验证码的生成和验证

原理和安装

验证码的使用流程和原理为:在服务器端负责生成图形化验证码,并以数据流的形式供前端访问获取,同时将生成的验证码存储到全局的缓存中,在本案例中,我们使用Redis作为全局缓存,并设置缓存失效时间。当用户使用用户名和密码进行登录时,进行验证码验证。验证通过即可继续进行登录。

安装开源的验证码生成库:

go get -u github.com/mojocn/base64Captcha

生成和验证

本项目中采用的验证码的生成库支持三种验证码,分别是:audio,character和digit。我们选择character类型。

tool/Captcha.go

定义Captcha.go文件,实现验证码的生成和验证码函数的定义。在进行验证码生成时,默认提供验证码的配置,并生成验证码后返回给客户端浏览器。

package tool

import (
	"github.com/gin-gonic/gin"
	"github.com/mojocn/base64Captcha"
	"image/color"
)

type CaptchaResult struct {
	Id           string `json:"id"`
	Base64Blob   string `json:"base_64_blob"`
	VertifyValue string `json:"vertify_value"`
}

// GenerateCaptchaHandler 生成验证码
func GenerateCaptchaHandler(ctx *gin.Context) {
	//图形验证码的默认配置
	parameters := base64Captcha.ConfigCharacter{
		Height:             30,
		Width:              100,
		Mode:               3,
		ComplexOfNoiseText: 0,
		ComplexOfNoiseDot:  0,
		IsUseSimpleFont:    true,
		IsShowHollowLine:   false,
		IsShowNoiseDot:     false,
		IsShowNoiseText:    false,
		IsShowSlimeLine:    false,
		IsShowSineLine:     false,
		CaptchaLen:         4,
		BgColor: &color.RGBA{
			R: 255,
			G: 255,
			B: 255,
			A: 255,
		},
	}

	captchaId, captchaInterfaceInstance := base64Captcha.GenerateCaptcha("", parameters)
	base64blob := base64Captcha.CaptchaWriteToBase64Encoding(captchaInterfaceInstance)

	captchaResult := CaptchaResult{Id: captchaId, Base64Blob: base64blob}

	// 设置json响应
	Success(ctx, map[string]interface{}{
		"captcha_result": captchaResult,
	})
}

func VertifyCaptcha(id string, value string) bool {
	return base64Captcha.VerifyCaptcha(id, value)
}

controller/MemberController.go

//添加路由
func (mc *MemberController) Router(engine *gin.Engine) {
	...
	engine.GET("/api/captcha", mc.captcha)
	engine.POST("/api/vertifycha", mc.vertifyCaptcha)
}

//校验验证码是否正确
func (mc *MemberController) vertifyCaptcha(context *gin.Context) {
	var captcha tool.CaptchaResult
	err := tool.Decode(context.Request.Body, &captcha)
	if err != nil {
		tool.Failed(context, "参数解析失败")
		return
	}
	result := tool.VertifyCaptcha(captcha.Id, captcha.VertifyValue)
	fmt.Println("登陆状态:", result)
}

func (mc *MemberController) captcha(context *gin.Context) {
	tool.GenerateCaptchaHandler(context)
}

缓存到Redis

安装库

在项目中使用redis,需要安装go-redis库,可以在https://github.com/go-redis/redis中查看如何下载go-redis和配置。

增加Redis配置

在配置文件app.json中新增redis配置:

...
"redis_config": {
    "addr": "127.0.0.1",
    "port": "6379",
    "password": "",
    "db": 0
}
...

同时,新增RedisConfig结构体定义,如下所示:

type RedisConfig struct {
	Addr string `json:"addr"`
	Port string `json:"port"`
	Password string `json:"password"`
	Db   int    `json:"db"`
}

Redis初始化操作

进行了redis配置以后,需要对redis进行初始化。

tool/RedisStore.go
package tool

import (
	"github.com/go-redis/redis"
	"github.com/mojocn/base64Captcha"
	"log"
	"time"
)

type RedisStore struct {
	client *redis.Client
}

var Redis_Store RedisStore

func InitRedisStore() *RedisStore {
	config := GetConfig().RedisConfig
	client := redis.NewClient(&redis.Options{
		Addr:     config.Addr + ":" + config.Port,
		Password: config.Password,
		DB:       config.Db,
	})
	Redis_Store = RedisStore{client: client}
	base64Captcha.SetCustomStore(&Redis_Store)

	return &Redis_Store
}

func (rs *RedisStore) Set(id string, value string) {
	if err := rs.client.Set(id, value, time.Minute*10).Err(); err != nil {
		log.Println(err)
	}
}

func (rs *RedisStore) Get(id string, clear bool) string {
	val, err := rs.client.Get(id).Result()
	if err != nil {
		log.Println(err)
		return ""
	}
	if clear {
		if err := rs.client.Del(id).Err(); err != nil {
			log.Println(err)
			return ""
		}
	}
	return val
}

对Redis进行初始化和定义完成以后,需要在main中调用一下初始化操作:

func main(){
    ...
	//初始化redis配置
	tool.InitRedisStore()	
    ...
}

用户名密码登陆

此处程序登陆注册存在问题。

Param/LoginParam.go

package param

type LoginParam struct {
	Name string `json:"name"`
	Password string `json:"password"`
	Id string `json:"id"`
	Value string `json:"value"`	//验证码输入值
}

controller/MemberController.go

engine.POST("/api/login_pwd", mc.nameLogin)
func (mc *MemberController) nameLogin(context *gin.Context) {
	//解析用户传递参数
	var loginParam param.LoginParam
	err := tool.Decode(context.Request.Body, &loginParam)
	fmt.Println(context.Request.Body)
	if err != nil {
		tool.Failed(context, "参数解析失败")
		return
	}
	//验证验证码
	validate := tool.VertifyCaptcha(loginParam.Id, loginParam.Value)
	if !validate {
		tool.Failed(context, "验证码错误")
		return
	}
	//登陆
	ms := service.MemberService{}
	member := ms.Login(loginParam.Name, loginParam.Password)
	if member.Id != 0 {
		tool.Success(context, &member)
		return
	}
	tool.Failed(context, "登陆失败")

}

service/MemberService.go

func (ms *MemberService) Login(name string, password string) *model.Member {
	//是否存在当前用户信息
	md := dao.MemberDao{tool.DbEngine}
	member := md.Query(name, password)
	if member.Id != 0 {
		return member
	}
	//不存在,注册
	user := model.Member{}
	user.UserName = name
	user.Password = tool.EncoderSha256(password)
	user.RegisterTime = time.Now().Unix()
	insertMember := md.InsertMember(user)
	user.Id = insertMember
	return &user
}

dao/MemberDao.go

涉及到操作数据库的两个方法分别是:Query和InsertMember方法。InsertMember方法之前已经编写过,只需要重新编写一个Query方法即可。

func  (md *MemberDao) Query(name string, password string) *model.Member {
	var member model.Member
	_, err := md.Where("user_name = ? and password = ?", name, tool.EncoderSha256(password)).Get(&member)
	if err != nil {
		log.Fatal(err.Error())
		return nil
	}
	return &member
}

tool/PasswordEncode.go

package tool

import (
	"crypto/md5"
	"crypto/sha256"
	"encoding/base64"
	"encoding/hex"
	"fmt"
	"io"
)

func EncoderSha256(data string) string {
	h := sha256.New()
	h.Write([]byte(data))
	sum := h.Sum(nil)
	//由于是十六进制表示,因此需要进行转换
	s := hex.EncodeToString(sum)
	return string(s)
}

func Md5(data string) string {
	w := md5.New()
	io.WriteString(w, data)
	bydate := w.Sum(nil)
	result := fmt.Sprintf("%x", bydate)
	return result
}

func Base64Encode(data string) string {
	return base64.StdEncoding.EncodeToString([]byte(data))
}

func Base64Decode(data string) ([]byte, error) {
	return base64.StdEncoding.DecodeString(data)
}

用户头像上传

此处程序返回用户ID存在问题。

本节开发上传一张图片到服务器,并保存成为用户的头像。

在文件上传过程中,后台服务器需要确认该头像文件是哪位用户上传的。前端在上传文件时,一并将用户id上传至服务器。服务器需要确认该用户是否已经登录,只有登录的用户才有权限上传。最通常的做法是通过session来获取用户是否已经登录,并进行权限判断。

Session功能集成

安装session库

go语言和gin框架都没有实现session库,可以通过第三方实现的session库来集成session功能。安装如下session库:

go get github.com/gin-contrib/sessions

初始化session

在项目中,集成session功能,首先要进行初始化。我们选择将session数据持久化保存到redis中,因此需要与redis相结合。

session功能初始化完成以后就可以使用了,session的使用主要有两个操作:set和get。在sessions库中,有对应的session.Set(key, value)和session.Get(key)方法来实现set和get操作。

为了方便session的set和get操作,在初始化完session后,另外封装session的set和get函数。

tool/SessionStore.go
package tool

import (
	"github.com/gin-contrib/sessions"
	"github.com/gin-contrib/sessions/redis"
	"github.com/gin-gonic/gin"
	"log"
)

func InitSession(engine *gin.Engine) {
	config := GetConfig().RedisConfig
	SessionStore, err := redis.NewStore(10, "tcp", config.Addr+":"+config.Port, config.Password, []byte("secret"))
	if err != nil {
		log.Fatal(err.Error())
	}
	engine.Use(sessions.Sessions("MySession", SessionStore))
}

func SetSess(ctx *gin.Context, key interface{}, value interface{}) error {
	session := sessions.Default(ctx)
	if session == nil {
		return nil
	}
	session.Set(key, value)
	return session.Save()
}

func GetSess(ctx *gin.Context, key interface{}) interface{} {
	session := sessions.Default(ctx)
	return session.Get(key)
}

用户登录添加session

cic 当用户进行登录,并登录成功后,选择将用户的信息保存在session中。在项目的需要登录才能使用的地方,可以进行权限的判断和拦截。

因此,在之前已经完成的登录功能方法中,添加登录操作成功后,将用户数据保存到sesion的操作。在MemberController中的nameLogin和smsLogin方法中,添加如下设置session的代码操作,具体调用如下所示:

...
//将用户信息保存到session中
sess, err := json.Marshal(member)
if err != nil {
	fmt.Println("json Marshal error!")
	return
}
if err := tool.SetSess(context, "user_16"+string(member.Id), sess); err != nil {
	tool.Failed(context, "session set  error")
	return
}
...

集成session操作

在项目的入口main.go文件的main函数中,通过中间件调用开启session集成。main函数修改如下:

...
//集成session
app.Use(tool.InitSession())
...

文件上传Contoller实现

在MemberController中,创建uploadAvator方法,用于实现用户头像上传的业务流程控制。该方法其实主要有几个步骤:第一步是获取到用户端上传的文件,接下来将文件保存到对应的目录中,因为要知道该文件对应的是哪位用户的数据,因此需要将文件路径更新到用户数据库中的对应记录中。

//头像文件上传
func (mc *MemberController) UploadAvator(context *gin.Context) {
	// 1.解析上传的参数:image-file , user-ID
	userID := context.PostForm("user_id")
	fmt.Println("userID >>", userID)
	file, err := context.FormFile("avatar")
	if err != nil {
		tool.Failed(context, "avator decode error")
		return
	}
	// 2.通过session判断用户是否已经登录
	sess := tool.GetSess(context, "user_"+userID)
	if sess == nil {
		tool.Failed(context, "get user session error")
		return
	}
	var member model.Member
	json.Unmarshal(sess.([]byte), &member) //将session的内容设置为member的内容
	// 3.将file保存到本地
	fileName := "./uploadfile/" + strconv.FormatInt(time.Now().Unix(), 10) + file.Filename
	if err := context.SaveUploadedFile(file, fileName); err != nil {
		tool.Failed(context, "save avator error")
		return
	}

	tool.Failed(context, "upload avator error")
	// 5.返回结果
}

Dao层实现

//更新用户头像信息
func (md *MemberDao) UpdateMemberAvatar(userID int64, fileName string) int64 {
	member := model.Member{Avatar: fileName}
	res, err := md.Where("id = ?", userID).Update(&member)
	if err != nil {
		fmt.Println(err.Error())
		return 0
	}
	return res
}

FastDFS分布式文件系统

在实际的开发中,涉及到文件上传的功能,往往单独搭建一个文件服务器用于文件的存储。因此我们接下来就搭建一个分布式的文件系统,并将已完成的文件上传功能进行优化,将文件存储到分布式文件系统中。

FastDFS介绍

目前已经有很多开源的分布式文件系统。而fastDFS正是其中一员,fastDFS是基于http协议的分布式文件系统,其设计理念是一切从简。也正是因为其简单至上的设计理念,使其运维和扩展变得更加简单,同时还具备高性能,高可靠,无中心以及免维护等特点。

FastDFS的特点

fastDFS分布式文件存储系统主要解决了海量数据的存储问题,利用fastDFS系统,特别适合系统中的中小文件的存储和在线服务。中小文件的范围大致为4KB ~ 500MB大小之间。

FastDFS的组件构成及工作原理

在fastDFS分布式文件存储系统中,由三中角色的组件组成,分别称之为:跟踪服务器(Tracker Server)、存储服务器(Storage Server)和客户端(Client)。

每个角色组件都有各自的作用和功能:

  • 存储服务器:即Storage Server。存储服务器顾名思义就是用于存储数据的服务器,主要提供存储容量和备份服务。存储服务器为了保证数据安全不丢失,会多台服务器组成一个组,又称group。同一个组内的服务器上的数据互为备份。
  • 跟踪服务器:英文称之为Tracker Server。跟踪服务器的主要作用是做调度工作,担负起均衡的作用;跟踪服务器主要负责管理所有的存储服务器,以及服务器的分组情况。存储服务器启动后会与跟踪服务器保持链接,并进行周期性信息同步。
  • 客户端:主要负责上传和下载文件数据。客户端所部署的机器就是实际项目部署所在的机器。

FastDFS文件上传过程

  1. Storage Server会定期的向Tracker Server发送自己的存储信息。
  2. 当Tracker Server Cluster中的Tracker Server不止一个时,各个Tracker之间的关系是对等的,因此客户端上传时可以选择任意一个Tracker。
  3. 当Tracker收到客户端上传文件的请求时,会为该文件分配一个可以存储文件的group,当选定了group后就要决定给客户端分配group中的哪一个storage server。
  4. 当分配好storage server后,客户端向storage发送写文件请求,storage将会为文件分配一个数据存储目录。
  5. 然后为文件分配一个fileid,最后根据以上的信息生成文件名存储文件。

FastDFS文件下载过程

  1. 在下载文件时,客户端可以选择任意tracker server。
  2. tracker发送下载请求给某个tracker,并携带着文件名信息。
  3. tracker从文件名中解析出文件所存储的group、文件大小、创建时间等信息,然后为该请求选择一个storage用来服务下载的请求。

集成到Gin

安装fastdfs的golang库

在项目中进行文件上传编程实现,需要安装一个go语言库,该库名称为fdfs_client,通过如下命令进行安装。

go get github.com/tedcy/fdfs_client

编写fdfs.conf配置文件

在fdfs_client库中,提供对文件的上传和下载方法,其中文件上传支持两种方式。

要使用文件上传功能方法,首先需要构造一个fdfsClient实例。如同我们前文讲的fastDFS的组件构成一样,client需要连接tracker服务器,然后进行上传。

在构造fdfsClient实例时,首先需要编写配置文件fdfs.conf,在fdfs.conf文件中进行配置选项的设置:

tracker_server=127.0.0.1:22122
http_port=http://127.0.0.1:80
maxConns=100

在fdfs.conf配置文件中,配置了三个选项,分别是:

  • tracker_server:跟踪服务器的ip和跟踪服务的端口
  • http_port:配置了nginx服务器后的http访问地址和端口
  • maxConns:最大连接数为100,默认值即可

在构造fdfsClient对象实例化时,会使用该文件。

文件上传编程实现

将文件上传功能作为全局的一个工具函数进行定义,实现文件上传功能,并返回保存后的文件的id。编程实现如下:

func UploadFile(fileName string) string {
	client, err := fdfs_client.NewClientWithConfig("./config/fastdfs.conf")
	defer client.Destory()

	if err != nil {
		fmt.Println(err.Error())
	}

	fileId, err := client.UploadByFilename(fileName)
	if err != nil {
		fmt.Println(err.Error())
	}
	return fileId
}

修改Controller文件上传方法

现在,已经接入了fastDFS文件系统,因此,对MemberController的uploadAvator方法进行修改。

修改思路:将客户端上传的文件,先保存在服务器目录下的uploadfile目录中,然后将文件的路径和名称作为参数传递到UploadFile函数中,进行上传。上传成功后,将保存到本地的uploadfile文件删除,并把保存到fastDFS系统的fileId更新到对应用户记录的数据库。最后拼接文件访问的全路径,返回给客户端。

用户信息查询

该获取用户信息的接口请求类型为GET类型,在该接口访问时,是根据当前登录用户的会话状态进行请求的。

所谓会话,其实就是客户端会存储登录用户信息的cookie。Cookie可以在服务端通过代码进行设置。

Cookie的设置

在用户调用登录功能时,完成cookie设置。

context.SetCookie("cookie_id", "login_success", 10*60, "/", "localohost", true, true)
  • cookie_id:第一个参数表示的要即将设置的cookie的名称。
  • login_success:第二个字段表示的是cookie里面设置的具体的值的内容。在实际的使用中,该值开发者可以自己生成。
  • 10*60:第三个值表示的是cookie的有效时间,当该值<0时,会立刻清除cookie,当该值>0时,表示cookie数据的有效期会持续n秒。
  • “/":该参数是一个可选项,表示cookie所在的目录。
  • “localhost”:该参数表示cookie所在的域,可以理解为cookie的有效范围。

Cookie中间件

在设置了Cookie值以后,可以在服务端编写中间件获取Cookie信息。服务端编写的cookie处理逻辑如下。

const CookieName = "cookie_user"
const CookieTimeLength = 10 * 60

func CookieAuth(context *gin.Context) (*http.Cookie, error) {
	cookie, err := context.Request.Cookie(CookieName)
	if err == nil {
		context.SetCookie(cookie.Name, cookie.Value, cookie.MaxAge, cookie.Path, cookie.Domain, cookie.Secure, cookie.HttpOnly)
	} else {
		return nil, err
	}
	return cookie, nil
}

在需要登录才能访问的接口处理中,调用如上函数进行用户权限判断,以此来判断用户是否已经登录。

项目总结


参考链接: