Gin(六):文件的上传

本文首发于 ISLAND

之前使用了数据库做了简答的增加和查询功能,今天再次使用数据库完成一些其他功能,比如说头像的上传和显示。

:camera:新增用户头像

当用户登录完成后,页面右上角会显示当前用户的用户 email 。下面我们做点击 email 进入用户详情页,并可以修改信息。

先完善后端接口。通过用户的 id 来获取用户的详细信息,同时我们写了一个错误页 error.tmpl ,来进行错误信息的展示。

userHandler.go

func UserProfile(context *gin.Context) {
    id := context.Query("id")
    var user model.UserModel
    i, err := strconv.Atoi(id)
    u, e := user.QueryById(i)
    if e != nil || err != nil {
        context.HTML(http.StatusOK, "error.tmpl", gin.H{
            "error": e,
        })
    }
    context.HTML(http.StatusOK, "user_profile.tmpl", gin.H{
        "user": u,
    })
}
复制代码

代码中获取前端传递的id,通过 strconv.Atoi() 将String 类型转化为 int 类型。 user.QueryById() 方法是我们用来进行对id查询的方法。

在进行 QueryById 方法之前,我们要对 user 结构体和数据库进行一下简单的修改。

type UserModel struct {
    Id       int            `form:"id"`
    Email    string         `form:"email" binding:"email"`
    Password string         `form:"password" `
    Avatar   sql.NullString
}
复制代码

我们新增一行 Avatar ,类型为 sql.NullString 。为什么是 sql.NullString ?因为我们数据库中该字段初始时为 null ,而 string 类型是不可以接收 null 类型的,所以我们只能采用 NullString 来对 null 字符串进行处理。

同时,要对数据库进行添加,新增一列 avatar 字段。

修改后数据库

create table user
(
    id int auto_increment
        primary key,
    email varchar(30) not null,
    password varchar(40) not null,
    avatar varchar(100) null
)
comment '用户表';
复制代码

完成前期的修改工作,可以做剩下的事情。

️‍获取用户信息

userModel.go 中获取用户信息,完成 QueryById 方法

func (user *UserModel) QueryById(id int) (UserModel, error) {
    u := UserModel{}
    row := initDB.Db.QueryRow("select * from user where id = ?;", id)
    e := row.Scan(&u.Id, &u.Email, &u.Password, &u.Avatar)
    if e != nil {
        log.Panicln(e)
    }
    return u, e
}
复制代码

该方法基本和上一节的通过邮箱查询用户方法基本一致。

完成该方法后,就可以将我们的路由添加上。

userRouter.GET("/profile/", handler.UserProfile)
复制代码

此时就完成了后台的工作,剩下来就是对前端进行修改。

首先要重写划分一下前端的代码块。

template
|
|-error.tmpl
|-header.tmpl
|-index.tmpl
|-login.tmpl
|-nav.tmpl
|-user_profile.tmpl
复制代码

我们将原来 indexhead 标签部分的代码移动到 header 中,将 header 原有的代码移动到 nav.tmpl 中。

index.tmpl

{{template "header"}}
{{template "nav" .}}
复制代码

header.tmpl

{{ define "header" }}
    
    
    
        
        
        
        
        
        
        
        
        
        Gin Hello
    
{{end}}
复制代码

nav.tmpl

{{ if .email }}
    
{{ else }}
    
{{end}}
复制代码

通过路径 /user/profile?id={{ .id }}id 数据传递后台。

当数据获取成功后,会转跳到 user_profile.tmpl

user_profile.tmpl

{{template "header"}}
{{template "nav"}}
avatar
复制代码

该页面的 Email 不可编辑,密码可以修改,头像可以上传。

此时我们的页面就完成了。

上传头像

完成了基本页面,就要进行头像上传了。

userHandler.go 添加 UpdateUserProfile 方法

func UpdateUserProfile(context *gin.Context) {
    var user model.UserModel
    if err := context.ShouldBind(&user); err != nil {
        context.HTML(http.StatusOK, "error.tmpl", gin.H{
            "error": err.Error(),
        })
        log.Panicln("绑定发生错误 ", err.Error())
    }
    file, e := context.FormFile("avatar-file")
    if e != nil {
        context.HTML(http.StatusOK, "error.tmpl", gin.H{
            "error": e,
        })
        log.Panicln("文件上传错误", e.Error())
    }
}
复制代码

通过数据绑定来将 id email 和 密码进行绑定,然后通过 context.FormFile() 将文件数据进行获取。

文件数据可以获取,那么获取的文件应该进行保存。

先写一个工具类来获取我们的项目根路径。

新建一个 utils 文件夹, utils 中新建 pathUtils.go

package utils

import (
    "log"
    "os"
    "os/exec"
    "strings"
)

func RootPath() string {
    s, err := exec.LookPath(os.Args[0])
    if err != nil {
        log.Panicln("发生错误",err.Error())
    }
    i := strings.LastIndex(s, "\\")
    path := s[0 : i+1]
    return path
}
复制代码

编写工具类,方便我们日后对它直接使用。

// 省略部分代码
path := utils.RootPath()
path = path + "avatar\\"
e = os.MkdirAll(path, os.ModePerm)
if e != nil {
    context.HTML(http.StatusOK, "error.tmpl", gin.H{
        "error": e,
    })
    log.Panicln("无法创建文件夹", e.Error())
}
fileName := strconv.FormatInt(time.Now().Unix(), 10) + file.Filename
e = context.SaveUploadedFile(file, path+fileName)
if e != nil {
    context.HTML(http.StatusOK, "error.tmpl", gin.H{
        "error": e,
    })
    log.Panicln("无法保存文件", e.Error())
}
复制代码

通过获取当前时间来确保图片的唯一性,保障上传的图片不会因为重名而覆盖。

在这里要思考一个问题,既然我们上传了头像,就要在页面上进行展示,头像展示需要获取地址,那么如何获取图片保存后的地址?

在之前设置路由的时候,我们设置过一次静态文件目录,头像图片也属于静态文件,所以我们要对我们的上传目录再进行设置。

initRouter.go

router.StaticFS("/avatar", http.Dir(utils.RootPath()+"avatar/"))
复制代码

我们将我们上传的路径,映射为 /avatar 之后,我们就可以通过改路径进行访问资源。

完善我们最后的代码

avatarUrl := "http://localhost:8080/avatar/" + fileName
    user.Avatar = sql.NullString{String: avatarUrl}
    e = user.Update(user.Id)
    if e != nil {
        context.HTML(http.StatusOK, "error.tmpl", gin.H{
            "error": e,
        })
        log.Panicln("数据无法更新", e.Error())
    }
    context.Redirect(http.StatusMovedPermanently, "/user/profile?id="+strconv.Itoa(user.Id))
复制代码

当图片进行报错后我们再次重定向到 /user/profile 路由,这也页面上就会显示我们新的数据。