【笔记】基于Gin的后端笔记

目录

一、安装和基本概念

二、示例

三、测试

四、xorm库操作mysql

五、后端开发思路

六、常用工具

七、踩坑

一、安装和基本概念

1.在Goland控制台上输入以下语句安装gin

1
2
go get -u github.com/gin-gonic/gin
//加-u的区别是,如果本地的包版本太低会顺便更新,不加-u的话只要本地有包就会不采取任何措施

如果出现网络错误,就分别执行下述语句后再重新安装

1
2
3
4
# 设置goproxy.io代理
go env -w GOPROXY="https://goproxy.io"
# 设置GO111MOUDLE
go env -w GO111MODULE="on"

2.几个内置的中间件

  • gin.Logger(),用于日志
  • gin.Recovery(),用于恢复恐慌panic()保持程序运行,然后返回500

3.gin.Default()定义的默认使用了Recovery和Logger中间件

  • 而用gin.New()定义的则是不使用任何中间件
  • 中间件的使用语法如下
    1
    router.Use(gin.Logger())

4.gin与mysql的交互

  • 在Goland控制台上输入以下语句安装与mysql的驱动
    1
    go get github.com/go-sql-driver/mysql
  • 在Goland控制台上输入以下语句安装xorm
    1
    go get xorm.io/xorm

5.如果在ajax异步调用的中间页面response函数中返回gin.HTML()的话,并且前端会使用这个返回值时,会直接将这个html文件所有代码附加在使用这个返回值地方

二、示例

1.基础网页demo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package main

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

func main() {
router := gin.Default() //router是一个 *engine类型
router.GET("/get", func(c *gin.Context) {
c.JSON(200, gin.H{"message": "use get method"})
})
router.Run(":4000")
}
//运行后访问localhost:4000/get,会显示一个JSON页面内容是message: "use get method"
  • router := gin.Default()声明并创建名为router的路由
  • router.GET(“/get”, func(c *gin.Context) {
      c.JSON(200, gin.H{"message": "use get method"})
    
    })是访问触发时,c.JSON()返回一个状态码是200(200等价于http.StatusOK),响应内容是一个JSON格式字符串的响应
    • “.GET”表示用GET方式来处理访问”/get”的请求
    • func(c *gin.Context) {
      c.JSON(200, gin.H{“message”: “use get method”})
      }执行函数gin.Context,其内封装了request和response,其中c是变量名可以随意更改
      • 注意gin.H的H是一个map结构,不是结构体

2.不同的http请求格式示例

1
2
3
4
5
6
7
8
9
router := gin.Default()
router.GET("/get", func(c *gin.Context) { c.JSON(200, gin.H{"message": "use get method"}) })
router.POST("/post", func(c *gin.Context) { c.JSON(200, gin.H{"message": "use post method"}) })
router.PUT("/put", func(c *gin.Context) { c.JSON(200, gin.H{"message": "use put method"}) })
router.DELETE("/delete", func(c *gin.Context) { c.JSON(200, gin.H{"message": "use delete method"}) })
router.PATCH("/patch", func(c *gin.Context) { c.JSON(200, gin.H{"message": "use patch method"}) })
router.HEAD("/head", func(c *gin.Context) { c.JSON(200, gin.H{"message": "use head method"}) })
router.OPTIONS("/options", func(c *gin.Context) { c.JSON(200, gin.H{"message": "use options method"}) })
router.Run()

3.切换输出的格式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
router.GET("/json", func(c *gin.Context) {
c.JSON(200, gin.H{"message": "return json data"})
})

router.GET("/string", func(c *gin.Context) {
c.String(200, "message %s", "return string data")
})

router.GET("/yaml", func(c *gin.Context) {
arr := [][]string{
{"one", "two", "three"},
{"four", "five", "six"},
}
c.YAML(200, arr)
})

router.GET("/xml", func(c *gin.Context) {
person := struct { //声明一个匿名结构体
Name string
Age int
}{"Jane", 20}
c.XML(200, fmt.Sprintln(person))
})

4.获取api参数

  • 定义,:name即表示把出现在这个位置的字符赋值给name属性,最后用c.Params读取
    1
    2
    3
    router.GET("/user/:name/:age/:addr/:sex", func(c *gin.Context) {
    c.JSON(200, fmt.Sprintln(c.Params))
    })
  • 使用
    1
    2
    3
    4
    //若输入的url为http://localhost:8080/user/jane/20/beijing/female?id=999&height=170&wigth=100

    //输出
    "[{name jane} {age 20} {addr beijing} {sex female}]\n"
  • 获取某个指定值的写法
    1
    2
    3
    4
    router.GET("/user/:name/:age/:addr/:sex", func(c *gin.Context) {
    age := c.Param("age")
    c.JSON(200, age)
    })

5.获取url参数

  • 定义
    1
    2
    3
    4
    5
    6
    7
    router.GET("/user/:name/:age/:addr/:sex", func(c *gin.Context) {
    id := c.Query("id") //id不存在时返回空串
    //id := c.DefaultQuery("id", "001") //id不存在时返回001
    height := c.Query("height")
    wight := c.Query("wight")
    c.JSON(200, gin.H("height": height, "id": id, "wight": wight)}
    })
  • 使用
    1
    2
    3
    4
    //若输入的url为http://localhost:8080/user/jane/20/beijing/female?id=999&height=170&wigth=100

    //输出
    {"height":"170","id":"999","wight":"100"}

6.获取表单参数

  • 前端index.html代码为
    1
    2
    3
    4
    5
    <form action="http://127.0.0.1:8080/form" method="post">
    用户名:<input type="text" name="username" placeholder="请输入你的用户名"> <br>
    密码:<input type="password" name="password" placeholder="请输入你的密码"> <br>
    <input type="submit" value="提交">
    </form>
  • 后端用gin接受
    1
    2
    3
    4
    5
    6
    7
    router.POST("/form", func(c *gin.Context) {
    types := c.DefaultPostForm("type", "post")
    username := c.PostForm("username")
    password := c.PostForm("password")

    c.String(http.StatusOK, fmt.Sprintf("username:%s , password:%s , types:%s", username, password, types))
    })

7.输出html文件

  • 路由写法
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    package main

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

    func main() {
    router := gin.Default()
    router.LoadHTMLGlob("tem/*")
    router.GET("/index", func(c *gin.Context) {
    c.HTML(200, "index.html", gin.H{"title": "测试", "ce": "123456"})
    })
    router.Run()
    }
  • 根目录\tmp\index.html写法
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    <!DOCTYPE html>
    <html lang="en">
    <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>{{.title}}</title>
    </head>
    <body>
    fgkjdskjdsh{{.ce}}
    </body>
    </html>
  • 最终会在localhost:8080/index渲染成以下网页
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    <!DOCTYPE html>
    <html lang="en">
    <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>测试</title>
    </head>
    <body>
    fgkjdskjdsh123456
    </body>
    </html>

8.访问不同路径时的分离函数写法,下述代码无论是访问/demo、/test还是/aaaa都会去执行response,这种写法比之前的简洁多

1
2
3
4
5
6
7
8
9
10
func response(c *gin.Context) {
c.JSON(200, gin.H{"message": "hello world"})
}
func main() {
router := gin.Default()
router.GET("/demo", response)
router.GET("/test", response)
router.GET("/aaaa", response)
router.Run()
}

9.gin的前后端交互写法(以接受token为例)

  • 前端
    1
    2
    3
    4
    5
    6
    7
    8
    <script src="https://cdn.dingxiang-inc.com/fe/common/jquery/1.9.1/jquery.min.js"></script>

    $.ajax({
    type:"GET",
    url:"http://127.0.0.1:8080/index/bar",
    dataType:"text",
    data: { token: token}
    });
  • 后端
    1
    2
    3
    4
    5
    6
    7
    8
    9
    func main(){
    router := gin.Default()
    router.GET("/index/bar", response)
    }

    func response(c *gin.Context){
    token := c.Query("token") //从这里之后就可以用token的值了,注意是string类型
    .....
    }

10.测试时,显示字符串用法

1
2
3
4
5
6
func response(c *gin.Context){
str1 := "str01"
str2 := "str02"
c.String(200, str1 + str2 + "!")
}
//最终会在页面上显示str01str02!

10.与mysql连接(使用xorm)

  • 连接函数
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    func main(){
    orm1, err := OrmEngine()
    if err != nil {
    panic("创建orm错误")
    }
    }

    func OrmEngine() (*xorm.Engine, error) {
    //注册xorm引擎
    engine, err := xorm.NewEngine("mysql", "root:123456@tcp(127.0.0.1:3306)/test?charset=utf8")
    if err != nil {
    return nil, err
    }
    engine.ShowSQL(true)
    return engine, nil
    }


  • 增加操作
    1
    2
    3
    sql := "insert into biao01(name,kkk) values (?, ?)"
    orm1.Exec(sql, "Li", "4")
    //在表biao01中增加一个name=Li, kkk=4的记录
  • 删除操作
    1
    2
    3
    sql := "delete from biao01 where kkk = ?"
    orm1.Exec(sql, 4)
    //删除表biao01中的kkk==4的记录
  • 查询操作
    1
    2
    3
    4
    5
    6
    7
    str, err := orm1.QueryString("select name from biao01 where kkk = 1;")
    if err != nil {
    panic(err)
    }
    for _, name_map := range str {
    fmt.Println(name_map["name"])
    }
  • 更改操作
    1
    2
    sql := "update biao01 set name = ? where kkk = ?"
    orm1.Exec(sql, "wang", 4)

11.前后端加数据库交互demo

  • 前端
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    <!DOCTYPE html>
    <html lang="en">
    <head>
    <meta charset="UTF-8">
    <title>TEST</title>
    <script src="https://cdn.dingxiang-inc.com/fe/common/jquery/1.9.1/jquery.min.js"></script>
    </head>
    <body>

    <form action="#" method="GET" class="tm-call-to-action-form">
    <input
    type="text"
    id="question"
    value="初始值"

    style="width: 500px"
    required
    />
    </form>

    <button id="button1" onclick="myFunction()" class="btn btn-primary">
    查询
    </button>
    <strong><p id="ans_class" ></p></strong>
    <p id="ans" ></p>

    <script>
    function myFunction() {
    var question = document.getElementById('question').value;
    document.getElementById('button1').innerHTML = "正在查询,请稍等";
    fetchData(question);
    }

    function fetchData(question = "默认值") {
    $.ajax({
    type: "GET",
    url: "http://127.0.0.1:8080/qa_bar",
    dataType:"text",
    data: { Question: question},
    success: function (result) {
    document.getElementById('ans_class').innerHTML = "查询结果:";
    if (result != 'None'){
    document.getElementById('ans').innerHTML = result;
    }else{
    document.getElementById('ans').innerHTML = '不存在相关答案';
    }
    document.getElementById('button1').innerHTML = "查询";
    }
    });
    }
    </script>
    </body>
    </html>
  • 后端
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    //router.GET("/qa_bar", res_qa)

    func res_qa(c *gin.Context) {
    str, err := Orm.QueryString("select name from biao01 where kkk = 4;")
    if err != nil {
    panic(err)
    }
    name := str[0]["name"]
    question := c.Query("Question")
    temple := name + question
    c.String(200, temple)
    }

12.后端传json和前端接json案例

  • 后端
    1
    c.JSON(200, gin.H{"temple": temple})
  • 前端
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    $.ajax({
    type: "GET",
    url: "http://127.0.0.1:8080/qa_bar",
    dataType:"text",
    data: { Question: question},
    success: function (result) {
    var data = JSON.parse(result)
    document.getElementById('ans').innerHTML = data.temple; //意为把id==ans的<p></p>内容改为json里的temple
    }
    });

13.分页的实现逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
//controller层

type param struct {
Page int `form:"page" json:"page"`
Limit int `form:"limit" json:"limit"`
}
//这个意思是若param没有值,就把值默认设为10和1
request := &param{
Limit: 10,
Page: 1,
}
err := ctx.Bind(request)
if err != nil {
code.BuildReturn(ctx, 0, err.Error(), "")
return
}
var search map[string]interface{}
err = util.StructTo(&request, &search)
if err != nil {
code.BuildReturn(ctx, 0, err.Error(), "")
return
}

//mysql层
type searchStore struct {
Page int `json:"page"`
Limit int `json:"limit"`
}
var search searchStore
err := util.StructTo(&param, &search)
if err != nil {
return nil, 0, err
}
offset := (search.Page - 1) * search.Limit
......
err = session.Limit(search.Limit, offset).FindAndCount(&XX)

17.将数据输出为浏览器下载Excel文件写法

  • cotroller层写的响应函数
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    func (c *ControllerOrder) OrderTradeOutput(ctx *gin.Context) {
    type param struct {
    TradeStartTime int `form:"trade_start_time" json:"trade_start_time"`
    TradeEndTime int `form:"trade_end_time" json:"trade_end_time"`
    TradeType int `form:"trade_type" json:"trade_type"`
    Page int `form:"page" json:"page"`
    Limit int `form:"limit" json:"limit"`
    }
    request := &param{}
    err := ctx.Bind(request)
    if err != nil {
    output.OutputOrderTrade(ctx, nil)
    return
    }
    var search map[string]interface{}
    err = util.StructTo(&request, &search)
    if err != nil {
    output.OutputOrderTrade(ctx, nil)
    return
    }
    list, _, err := c.srv.Order().GetOrderTrade(ctx, search)
    if err != nil {
    output.OutputOrderTrade(ctx, nil)
    return
    }
    output.OutputOrderTrade(ctx, list)
    return
    }
  • out工具层写输出excel具体实现
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    //DownloadRoleInfoBo用于最终生成excel所以要按一定格式处理
    type DownloadRoleInfoBo struct {
    TradeTime string
    TradeType string
    TradeAmount string
    TradeUserTitle string
    TradeNo string
    }

    /*注意这里list里的元素要是{
    "trade_time": xx,
    "trade_type": xx,
    "trade_amount": xx,
    "trade_user_title": xx,
    "trade_no": xx,
    }
    */
    func OutputOrderTrade(context *gin.Context, list []map[string]interface{}) {
    for _, role := range list {
    res = append(res, &DownloadRoleInfoBo{
    TradeTime: role["trade_time"].(string),
    TradeType: role["trade_type"].(string),
    TradeAmount: role["trade_amount"].(string),
    TradeUserTitle: role["trade_user_title"].(string),
    TradeNo: role["trade_no"].(string),
    })
    }

    content := ToExcel([]string{`交易时间`, `交易类型`, `交易金额`, `交易人姓名`, `订单编号`}, res)
    ResponseXls(context, content, "流水数据")
    }

    // 生成io.ReadSeeker 参数 titleList 为Excel表头,dataList 为数据
    func ToExcel(titleList []string, dataList []interface{}) (content io.ReadSeeker) {
    // 生成一个新的文件
    file := xlsx.NewFile()
    // 添加sheet页
    sheet, _ := file.AddSheet("Sheet1")
    // 插入表头
    titleRow := sheet.AddRow()
    for _, v := range titleList {
    cell := titleRow.AddCell()
    cell.Value = v
    }
    // 插入内容
    for _, v := range dataList {
    row := sheet.AddRow()
    row.WriteStruct(v, -1)
    }

    var buffer bytes.Buffer
    _ = file.Write(&buffer)
    content = bytes.NewReader(buffer.Bytes())
    return
    }

    // 向前端返回Excel文件
    // 参数 content 为上面生成的io.ReadSeeker, fileTag 为返回前端的文件名
    func ResponseXls(c *gin.Context, content io.ReadSeeker, fileTag string) {
    fileName := fmt.Sprintf("%d%s%d%s%d%s%s.xlsx", time.Now().Year(), `-`, time.Now().Month(), `-`, time.Now().Day(), `-`, fileTag)
    //fileName := fmt.Sprintf("%s.xlsx", fileTag)
    c.Writer.Header().Add("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, fileName))
    c.Writer.Header().Add("Content-Type", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
    http.ServeContent(c.Writer, c.Request, fileName, time.Now(), content)
    }

18.后端使用前端POST传的json写法

  • 方法1用结构体接受
    1
    2
    3
    4
    5
    6
    7
    8
    9
    type param struct {
    Mobile string `form:"mobile" json:"mobile" binding:"required"`
    PassWord string `form:"pass_word" json:"pass_word" binding:"required"`
    BranchId int `form:"branch_id" json:"branch_id"`
    Token string `form:"token" json:"token"`
    }
    var request param
    ctx.Bind(&request) //ctx是*gin.Context类型
    token := request.Token
  • 方法2用map[string]interface{}接受
    1
    2
    3
    json := make(map[string]interface{}) //注意该结构接受的内容
    ctx.BindJSON(&json)
    token := json["token"].(string)

19.生成微信小程序对应页面二维码的响应函数(前端传一个path字符串)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
func (c *ControllerCourse) ProductCode(ctx *gin.Context) {
type param struct {
Path string `form:"path" json:"path" binding:"required"`
}
var request param
err := ctx.Bind(&request)
if err != nil {
code.BuildReturn(ctx, 0, err.Error(), "")
return
}
//Get请求获得access_token
appId := "wx80bdaa43787b1ee8"
appSecret := "139dbea7e33098841ce46e91c2b9dad2"
urlGet := "https://api.weixin.qq.com/cgi-bin/token" + "?grant_type=client_credential&appid=" + appId + "&secret=" + appSecret
var getRes responseGet
responseGet, _ := http.Get(urlGet)
err = json.NewDecoder(responseGet.Body).Decode(&getRes) //accessToken里存的是Get返回json
if err != nil {
fmt.Println(err)
return
}
accessToken := getRes.AccessToken
//post请求生成图片
urlPost := "https://api.weixin.qq.com/wxa/getwxacodeunlimit?access_token=" + accessToken
song := make(map[string]interface{})
song["page"] = request.Path
song["scene"] = "a"
bytesData, err := json.Marshal(song)
if err != nil {
fmt.Println(err.Error())
return
}
reader := bytes.NewReader(bytesData)
requestPost, err := http.NewRequest("POST", urlPost, reader)
if err != nil {
fmt.Println(err.Error())
return
}
requestPost.Header.Set("Content-Type", "application/json;charset=UTF-8")
client := http.Client{}
resp, err := client.Do(requestPost)
if err != nil {
fmt.Println(err.Error())
return
}
respBytes, err := ioutil.ReadAll(resp.Body)
if err != nil {
fmt.Println(err.Error())
return
}
//byte数组直接转成string,优化内存
str := (*string)(unsafe.Pointer(&respBytes))
fmt.Println(*str)
if err != nil {
code.BuildReturn(ctx, 0, err.Error(), "")
return
}
fmt.Println(len(respBytes))
//code.BuildReturn(ctx, 1, "获取二维码成功", respBytes)
ctx.Writer.WriteString(string(respBytes))
return
}

20.一个*gin Context类型的常用方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//ctx是*gin Context类型
//获取请求方法
reqMethod := ctx.Request.Method
//获取请求路由
reqUri := ctx.Request.RequestURI
// 请求IP
clientIP := ctx.ClientIP()
// 状态码
statusCode := ctx.Writer.Status()

//一个赋值给ctx的方法
ctx.Set("user_id", 123)
ctx.Set("branch_id", 1)
//后续就可以用Get方法获取存的这个值了
userId := ctx.Get("user_id")
branchId := ctx.Get("branch_id")

21.一个*gin.RouterGroup类型的常用方法

  • 由*gin.Engine.Group(“xx”)产生,也可以看作是一个*gin.Engine对象
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    //分组,此时app是127.0.0.1/qyk_back_api
    app := router.Group("/qyk_back_api")

    //再分组,有两个url:127.0.0.1/qyk_back_api/user/login和127.0.0.1/qyk_back_api/user/user_detail
    userRouter := app.Group("/user")
    {
    userRouter.POST("/login", userController.Login)
    userRouter.POST("/user_detail", userController.UserDetail)
    }

    //使用中间件,即路径前缀有127.0.0.1/qyk_back_api的都要先去调用一次auth.CORS()函数
    app.Use(auth.CORS())

22.处理时间

  • 例子:要求大于当前时间15分钟的订单全部更改状态
    1
    2
    d, _ := time.ParseDuration("-15m") //d为-15分钟
    pastTime := time.Now().Add(d).Format("2006-01-02 15:04:05") //意为在当前时间上加上-15分钟,然后根据"2006-01-02 15:04:05"的格式转换成string

23.在外部想调用xorm数据库

  • 需要store层的工厂方法生成出来的store.Factory
  • 然后就可以用以下方法直接调用mysql层的方法了
    1
    2
    mysqlStore, _ := mysql.GetMysqlFactory()
    orderList, err := mysqlStore.Order().GetListForPastTime(pastTime)

24.接受excel文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
type param struct {
File *multipart.FileHeader `form:"file" json:"file" binding:"required"`
}
type paramDev struct {
Name string `form:"name" json:"name"`
IdNo string `form:"id_no" json:"id_no"`
}
var request param
ctx.Bind(&request)
fileTemp, _ := request.File.Open()
fileBuffer := make([]byte, request.File.Size)
fileTemp.Read(fileBuffer)
file, _ := os.Create("导入文件缓存.xlsx")
defer file.Close()
file.Write(fileBuffer) //将接受的文件保存到本地

xlFile, err := xlsx.OpenFile("导入文件缓存.xlsx")
registerList := make([]map[string]interface{}, 0)
for _, sheet := range xlFile.Sheets {
for index, row := range sheet.Rows {
if index == 0 {
continue //去除表头
}
var temp paramDev
var str []string
for _, cell := range row.Cells {
str = append(str, cell.String())
}
temp.Name = str[0]
temp.IdNo = str[1]
var temple map[string]interface{}
util.StructTo(temp, &temple)
registerList = append(registerList, temple)
}
}

//清理缓存
file.Close()
os.Remove("导入文件缓存.xlsx")

# 这里有个excel的坑,如果在第21行增加了数据又删掉的话,会一直识别到21行而不是只识别有值的部分
# 最好用For row的循环中加if str[0] == "" && str[1] == ""{continue}来避坑

25.接受json表示的数组

  • 第一种:以string类型传过来
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    type param struct {
    List string `form:"list" json:"list" binding:"required"`
    }
    type paramDev struct {
    Name string `form:"name" json:"name"`
    IdNo string `form:"id_no" json:"id_no"`
    }
    var request param
    ctx.Bind(&request)

    //将List处理成每个成员符合json规范的[]string
    strTmp := strings.Split(request.List, "},")
    for index, element := range strTmp {
    var str string
    if index == 0 {
    str = element[1:]
    }
    if index == len(strTmp)-1 {
    str = element[:len(element)-1]
    } else {
    str += "}"
    }
    listTemple = append(listTemple, str)
    }

    for _, elementTemp := range listTemple {
    //string通过json映射转结构体
    var test paramDev
    bt := []byte(elementTemp)
    json.Unmarshal(bt, &test)

    //结构体通过json映射转map
    var element map[string]interface{}
    err = util.StructTo(test, &element) //util.StructTo是自己写的工具函数
  • 第二种:以array数组传过来
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    //result是[[{"id", "fullname"}, {...}]]格式的数据

    type resGet struct {
    Status int `json:"status"`
    Message string `json:"message"`
    DataVersion string `json:"data_version"`
    Result [][]param `json:"result"`
    }

    type param struct {
    Id string `json:"id"`
    Name string `json:"fullname"`
    }

    var getRes resGet
    allListUrl := "https://apis.map.qq.com/ws/district/v1/getchildren?key=" + config.Conf.UserWeb.TencentSecret
    responseGet, err := http.Get(allListUrl)
    json.NewDecoder(responseGet.Body).Decode(&getRes)
    for _, element := range getRes.Result[0] {
    fmt.Println(element.Name)
    }

26.设计模式在项目中的实现

  • 工厂模式实例
    • store层的store.go有工厂函数接口,负责生产每张数据库表的store对象
    • 每张数据库表的store对象也是一个接口,负责实现Get、Find的具体方法

27.gin框架设置和获取cookie的办法

  • 设置cookie
    1
    2
    3
    ctx.SetCookie("token", token, 1000, "/", "localhost", false, true)
    # ctx是*gin.context类型
    # 第一个参数为 cookie 名;第二个参数为 cookie 值;第三个参数为 cookie 有效时长,当 cookie 存在的时间超过设定时间时,cookie 就会失效,它就不再是我们有效的 cookie;第四个参数为 cookie 所在的目录;第五个为所在域,表示我们的 cookie 作用范围;第六个表示是否只能通过 https 访问;第七个表示 cookie 是否可以通过 js代码进行操作
  • 获取cookie的值
    1
    cookie, err := context.Cookie("token")

28.gin框架获取header的方法

  • 获得Header里的token
    1
    token := ctx.GetHeader("Authorization")

29.gin框架设置全局可用的键值对(绑定上下文)

1
2
ctx.Set("user_id", user.Id)
ctx.Set("branch_id", user.BranchId)

30.gin中间件

  • 使用,在router.go文件里加上
    1
    router.Use(MiddleWare())
  • 中间件.go的写法
    1
    2
    3
    4
    5
    6
    func MiddleWare() gin.HandlerFunc{
    return func(ctx *gin.Context){
    token := ctx.GetHeader("Authorization")
    ctx.Next()
    }
    }

31.jwt生成token

  • 生成token字符串
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    func GetToken(userId int, secret string) (string,error) {
    jwtKey = []byte(secret) //把密钥转化成[]byte类型,后面用来加密
    claim := &MyClaims{
    UserId: userId,
    StandardClaims:jwt.StandardClaims{
    IssuedAt: time.Now().Unix(), //签发时间
    Subject: "userToken", //标识名
    },
    }
    token := jwt.NewWithClaims(jwt.SigningMethodHS256, claim) //生成jwt.token对象,HS256是对称加密,RS256是非对称加密(需要用额外的公/私钥生成库生成对应的公/私钥)
    tokenString, err := token.SignedString(jwtKey) //用上述生成jwt.token对象和[]byte类型的secret加密生成token字符串
    if err != nil {
    return "", err
    }

    return tokenString, nil
    }
  • 解析token字符串
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    func ParseToken(tokenString string)(*MyClaims, error)  {
    claim := &MyClaims{}
    jwtKey = []byte(config.Conf.UserWeb.ApiSecret) //把密钥转化成[]byte类型,后面用来解密
    token,err := jwt.ParseWithClaims(tokenString,claim, func(token *jwt.Token) (interface{}, error) {
    return jwtKey,nil
    })
    if err == nil{
    return nil, err
    }
    if token == nil{
    return nil, errors.New("未获取到token")
    }
    if token.Valid == false{
    return nil, errors.New("token解析失败")
    }

    return claim,nil
    }
  • ExpiresAt表示的是过期时间,在结构体StandardClaims里声明下述属性意味着生成的token过期时间为5分钟,不设置默认是15天
    1
    ExpiresAt: nowTime.Add(time.Minute * 5).Unix(),

32.刷新token方案

  • 双token机制,一个access_token里存用户信息用来调用业务接口,一个refresh_token不存用户信息只用来做刷新的识别
    • 当access_token过期了就会用refresh_token去把两个token都刷新
    • 如果refresh_token也过期了那么就需要用户重新登录
    • 所以refresh_token一般需要设置过期时间是access_token的两倍,access_token是7天,refresh_token就是14天
    • 原因:为防止

三、测试

1.postman进行web并发响应测试

  • 新建一个collection
  • 输入测试的url和参数并保存进里面
  • 在collection右侧三角选择run collection
  • iterations是并发数,delay是延迟(建议设置0)

2.Postman传文件

  • Body下
  • form-data下
  • Key类型选file
  • Value选本地文件

3.Postman传数组

  • Body下
  • form-data下
  • Value值填以下格式数据
    1
    [{"name": "cupidatat enim ut dolore","id_no": "452525197305054416"},{"name": "cillum adipisicing Duis","id_no": "440126197310270915"}]

4.Postman传token

  • Headers下
  • 增添一个名为“Authorization”,值为token的项

四、xorm库操作mysql

1.获取所有的记录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
type QykBranch struct {
Id int `json:"id" xorm:"not null pk autoincr INT(11)"`
Title string `json:"title" xorm:"not null VARCHAR(100)"`
AddTime mytime.MyTime`json:"add_time" xorm:"not null created DATETIME"`
DelTime mytime.MyTime `json:"del_time" xorm:"not null DATETIME"`
UpdateTime mytime.MyTime `json:"update_time" xorm:"not null updated DATETIME"`
Del int `json:"del" xorm:"not null TINYINT(4)"`
}

type branches struct {
orm *xorm.Engine
}

func (b *branches) List(ctx context.Context) ([]*model.QykBranch, error) {
branchList := make([]*model.QykBranch, 0)
err := b.orm.Cols("id", "title").Find(&branchList)
//count, err := b.orm.FindAndCount(&branchList) //用这个的话count存的是一共返回的记录条数
if err != nil {
return nil, err
}
//用下面形式来转换成后端格式
data := make([]map[string]interface{}, 0)
for _, value := range branchList {
data = append(data, map[string]interface{}{
"id": value.Id,
"title": value.Title,
})
}
//用下面这种形式直接传到前端
context.JSON(200,gin.H{
"data":data,
})
}

2.指定从哪张表查的操作就是看传进去的model名

  • 例如下述model就对应mysql库里的qy_branch表,不需要任何多余操作,默认按这种规则对应的
    • 记住这个QykBranch其实就是xorm把mysql里的表转化成的结构体,是写死了的,每个属性对应得非常严密
      1
      2
      3
      4
      type QykBranch struct {
      Id int `json:"id" xorm:"not null pk autoincr INT(11)"`
      ...
      }
  • 举例使用,以下就表示从mysql库里的qy_branch表里找id和title
    1
    2
    branchList := make([]*model.QykBranch, 0)
    err := b.orm.Cols("id", "title").Find(&branchList)

3.一些常规思想

  • Get()方法一般要求传context.Context类型和一个int类型id进去,然后返回一个*model.QykBranch这种指针
    1
    2
    3
    qyk_kc, err := o.store.Course().Get(ctx, qyk_order_detail.KcId)
    //然后就可以可以用qyk_kc里面包含的值了
    str := qyk_kc.Title

4.Where的使用

  • 直接匹配
    1
    2
    session := o.orm.Where("del = ?", 0) //这里session后面可以看作是具有(del=0)的o.orm
    session.Where("pay_time >= ? and pay_time <= ?", search.OrderStartTime, search.OrderEndTime) //session此时可以看作是具有(del=0和符合pay_time条件)的o.orm
  • 带and和or的用法
    1
    session.Where("kc_start_time >= ? and kc_end_time <= ?", search.StartTime, search.EndTime)
  • 模糊匹配
    1
    2
    branch := strconv.Itoa(search.OrderNo) //strconv.Itoa()是把整型转换成字符串
    session.Where("id like ?", "%"+branch+"%")

5.In的使用

1
session.In("id", orderIdList) //即session是所有id存在orderIdList中的o.rom

6.Limit的使用

1
2
3
orderList := make([]*model.QykOrder, 0)
err := session.Limit(imit, offset).Find(&orderList)
//从第offset条记录开始,返回最多limit条记录

7.更新数据(坑:为0时传进去会默认不更新,需要用col+update额外指定更新一下)

1
2
3
4
5
6
7
func (m *messages) DoRead(ctx context.Context, id int) error {
temple := model.QykMessage{IsRead: 1}
m.orm.Where("id = ?", id).Update(&temple)
return nil
}
// 意为找出qyk_message表中id=id的记录,然后把其is_read字段值置为1(QykMessage里的IsRead属性自动对应qyk_message里的is_read属性)
// 即找到符合Where条件的那条记录,用&temple里的有数据的项去覆盖这条记录

8.string类型的时间可以直接与mysql数据库的datetime进行比较

9.增加数据

  • 先在model里赋值
    1
    2
    3
    4
    5
    6
    course := &model.QykKc{
    BranchId: branchId.(int),
    Title: request.CourseTitle,
    IsFull: 2,
    IsShelf: 2,
    }
  • 在mysql层用Insert()
    1
    _, err := c.orm.Insert(course)

10.返回升序排序asc和降序排序desc的记录列表

1
2
3
4
//按add_time从小到大排序(即时间靠后的排在前面)
session.Asc("add_time")
//按add_time从大到小排序
session.Desc("add_time")

11.给表起别名Alias

1
session.Alias("alias") .Where("alias.name = ?","u")

12.xorm反序列化,即根据mysql的表自动生成model

  • 执行以下命令新安装一个为xorm设计的cmd工具
    1
    go get github.com/go-xorm/cmd/xorm
  • 安装完成后进入$GOPATH/pkg/mod/github.com/go-xorm/cmd/xorm目录,然后在cmd命令行运行以下命令
    • 勿在ide的控制台运行,因为会有转义等命令格式错误
    • windows系统在当前文件夹打开cmd命令行,可以直接在文件路径上输入cmd然后回车
      1
      2
      3
      4
      5
      6
      xorm reverse mysql root:741852963@tcp(39.105.120.230:3306)/dy?charset=utf8 templates/goxorm

      # root是数据库用户名,741852963是数据库密码
      # 39.105.120.230:3306是数据库地址,本地的话用127.0.0.1
      # dy是数据库名字
      # 若找不到xorm的话,用./ xorm运行或者把$GOPATH/pkg/mod/github.com/go-xorm/cmd/xorm该路径加入到环境变量中
  • 最终得到一个model文件夹,里面存有根据数据库表格生成的model文件
  • 若要得到带json反射tag的model文件,要在D:\goworkplace\pkg\mod\github.com\go-xorm\cmd\xorm\templates\goxorm\config的genJson改成1
    1
    genJson=1
  • 若要满足在insert和update时自动更新时间
    • 先修改config的created=add_time、updated=update_time,这个add_time和update_time是你希望在inser和update自动更新的字段名
    • 再生成模型
  • 若出现生成model命令不识别的问题
    • 先在系统变量path里加一个D:\goworkplace(你的GOPATH路径)
    • 然后进到D:\goworkplace\pkg\mod\github.com\go-xorm\cmd\xorm目录,在该路径下拉起cmd命令行
    • 执行xorm reverse mysql 数据库管理员名:密码@(数据库ip地址:端口号)/数据库名?charset=utf8 templates/goxorm命令

13.xorm连接mysql的初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func GetMysqlFactory() (store.Factory, error) {
//获取配置信息
conf := config.Conf
mysql := conf.Mysql
//注册xorm引擎
engine, err := xorm.NewEngine(mysql.Driver, mysql.User+":"+mysql.PassWord+"@("+mysql.Host+":"+mysql.Port+")/"+mysql.DbName+
"?charset="+mysql.Charset+"&parseTime=true&loc=Local&charset=utf8mb4&collation=utf8mb4_unicode_ci")
if err != nil {
return nil, err
}
engine.ShowSQL(mysql.ShowSql)

mysqlFactory := &datastore{orm: engine}
return mysqlFactory, nil
}

14.Ditsinct()去重只会筛选出去重的字段,而GroupBy()去重会筛选出所有的字段

  • 即选种类数量用Distinct(),去重用GroupBy()
  • 实际中我们往往用distinct来返回不重复字段的条数,其原因是distinct只能返回他的目标字段,而无法返回其他字段

15.数据库里的decimal类型生成的模型类型是string

16.遍历筛选的用法

  • 情景:有一个group_id的[]int数组,要去表里找属性group_id在该数组里的记录
  • 写法
    • 接受传参
      1
      2
      3
      4
      5
      type searchServer struct {
      GroupId []int `json:"group_id"`
      }
      var param searchServer
      util.StructTo(&search, &param)
    • 筛选
      1
      2
      3
      4
      5
      6
      7
      8
      9
      if len(param.GroupId) != 0 {
      for key, i := range param.GroupId {
      if key == 0 { //这一步是为了防止出现Or()语句直接与前面的链接起来从而忽略掉group_id这个筛选条件
      session = session.Where("group_id like ?", "%"+"\""+strconv.Itoa(i)+"\""+"%")
      } else {
      session = session.Or("group_id like ?", "%"+"\""+strconv.Itoa(i)+"\""+"%")
      }
      }
      }

五、后端开发思路

1.项目的分布和作用

  • config文件夹:里面存用于配置的.json文件,方便改参数直接改这个就行
  • tool包:用于写自己用得顺手的小工具,用途不限(初始化配置、生成token等等)
  • router包:用于初始化路由和控制路由
  • controller包:用于写路由中调用的反应函数
    • 每个反应函数单独成一个文件,该文件就相当于是接口
  • models包:里面文件用于控制数据库行为

2.go工作文件夹

  • src放项目代码
  • pkg放下载的库

3.一个接口的实现步骤

  • 读前端写的API文档和产品写的原型图
  • 路由层router写路由(url路径)
  • controller层写方法:写构建返回前端的数据结构(srv)
  • service写声明和实现:调用store层的方法获取数据,构建并返回给controller对应的数据结构(store)
  • store层写声明,mysql层写实现:返回模型层的指针*

4.检查前端调用接口和前端数据

  • 用Google打开网站
  • 右键打开“检查”
  • 在network里就记录了最近调用的请求路径和数据

5.软件开发应具有的特性

  • 低耦合,高内聚:主要看类的内聚性是否高,耦合度是否低
  • 敏捷开发:敏捷开发并不追求前期完美的设计,而是力求在很短的周期内开发出产品的核心功能,尽早发布出可用的版本。然后在后续的生产周期内,按照新需求不断迭代升级,完善产品

6.数据表必备字段

  • id
  • add_time
  • update_time
  • del_time
  • del

7.MVC+store层概念(因为采用前后端分离,所有不用写View层)

  • controller层处理返回值(每个接口有一个)
  • service层调用store层方法进行筛选、组合字段等(每个接口有一个)
  • store层一般写Create、Get、Update、Find、Del方法就足够用(多个接口共用)
    • 运用到工厂模式,这里的store对象是由factor工厂生产出来的
  • model层相对于一个结构体的声明,用于辅助store层,xorm库可以用脚本根据mysql库自动生成model

8.中间件的常用

  • 出错使用
    1
    2
    ctx.Abort()
    return
  • 顺利执行完后结尾用(注意这里不能用return,因为还要走下一步路由)
    1
    ctx.Next()

9.数据库有需要存[]int的需求时,将其转换成[“12”,”13”]的字符串存进去,因为这种形式可以直接structto方法用一个[]string接受在里面后变成str[0] = “12”,str[1] = “13”,然后就可以分别对每个成员直接用strconv.Atoi()来转成int了

六、踩坑

1.在mysql/mysql.go文件中一定要强制导入这个包,不然xorm会报错不识别mysql引擎

1
_ "github.com/go-sql-driver/mysql"

2.报错Error 1292: Incorrect datetime value: ‘’ for column是因为老版本mysql的datetime类型字段可以为’’,而新版本mysql使用严格模式导致不行,解决办法是修改sql_mode字段来实现

  • 解决办法:进入服务器mysql环境依次执行以下语句
    1
    2
    3
    mysql> set session sql_mode='';

    mysql> set global sql_mode='';

3.要想保留两个int相除不损失精度的结果,应该用以下格式,先转float再除

1
satisfy = float32(satisfySum) / float32(userNumber)

4.excel的坑,如果在第21行增加了数据又删掉的话,会一直识别到21行而不是只识别有值的部分

  • 最好用For row的循环中加以下代码来避坑
    1
    2
    3
    if str[0] == "" && str[1] == "" && str[2] == "" {
    continue
    }

5.time.LoadLocation(“Asia/Shanghai”)在服务器上报错

  • 原因是放到容器docker里运行时无法获得LoadLocation解析用的辅助文件
  • 解决办法一:源代码改成以下代码
    1
    2
    3
    4
    tmp, err := time.LoadLocation("Asia/Shanghai")
    if err != nil {
    tmp = time.FixedZone("CST", 8*3600)
    }
  • 解决办法二:在dockefile文件加入以下代码
    1
    2
    3
    4
    5
    6
    7
    8
    9
    ... ...
    FROM golang:1.13-alpine
    ... ...
    FROM alpine
    ... ...
    COPY --from=0 /usr/local/go/lib/time/zoneinfo.zip /opt/zoneinfo.zip
    ... ...
    ENV ZONEINFO /opt/zoneinfo.zip
    ... ...

6.Error:Field validation for failed on the ‘required’绑定Bind报错

  • 原因是使用ctx.Bind(&request)时会根据前端请求的Content-Type来选择使用哪个解析,默认用form
  • 解决办法(改用json格式传):
    • 自测的话,需要选择postman的Body-raw-JSON来传
    • 前端的话,需要Content-Type改成application/json

7.直接复制别的项目文件过来时,记得在go.mod文件里改module的名字成当前项目名

8.用map[string]的时候记得直接初始化,不要只var声明

1
search := map[string]interface{}{}