Gin(六):文件的上传

https://juejin.im/post/5d368f3be51d4510aa0115e1

本文首发于 ISLAND

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

?新增用户头像

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

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

userHandler.go

  1. func UserProfile(context *gin.Context) {
  2. id := context.Query("id")
  3. var user model.UserModel
  4. i, err := strconv.Atoi(id)
  5. u, e := user.QueryById(i)
  6. if e != nil || err != nil {
  7. context.HTML(http.StatusOK, "error.tmpl", gin.H{
  8. "error": e,
  9. })
  10. }
  11. context.HTML(http.StatusOK, "user_profile.tmpl", gin.H{
  12. "user": u,
  13. })
  14. }

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

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

  1. type UserModel struct {
  2. Id int `form:"id"`
  3. Email string `form:"email" binding:"email"`
  4. Password string `form:"password" `
  5. Avatar sql.NullString
  6. }

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

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

修改后数据库

  1. create table user
  2. (
  3. id int auto_increment
  4. primary key,
  5. email varchar(30) not null,
  6. password varchar(40) not null,
  7. avatar varchar(100) null
  8. )
  9. comment '用户表';

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

?️‍获取用户信息

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

  1. func (user *UserModel) QueryById(id int) (UserModel, error) {
  2. u := UserModel{}
  3. row := initDB.Db.QueryRow("select * from user where id = ?;", id)
  4. e := row.Scan(&u.Id, &u.Email, &u.Password, &u.Avatar)
  5. if e != nil {
  6. log.Panicln(e)
  7. }
  8. return u, e
  9. }

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

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

  1. userRouter.GET("/profile/", handler.UserProfile)

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

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

  1. template
  2. |
  3. |-error.tmpl
  4. |-header.tmpl
  5. |-index.tmpl
  6. |-login.tmpl
  7. |-nav.tmpl
  8. |-user_profile.tmpl

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

index.tmpl

  1. {{template "header"}}
  2. <header>
  3. {{template "nav" .}}
  4. </header>
  5. <main>
  6. </main>

header.tmpl

  1. {{ define "header" }}
  2. <!doctype html>
  3. <html lang="en">
  4. <head>
  5. <meta charset="UTF-8">
  6. <meta name="viewport"
  7. content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
  8. <meta http-equiv="X-UA-Compatible" content="ie=edge">
  9. <link rel="stylesheet" href="/statics/css/bootstrap.min.css">
  10. <link rel="stylesheet" href="/statics/css/bootstrap-grid.min.css">
  11. <link rel="stylesheet" href="/statics/css/bootstrap-reboot.min.css">
  12. <script src="/statics/js/jquery.min.js" rel="script"></script>
  13. <script src="/statics/js/Popper.js" rel="script"></script>
  14. <script rel="script" src="/statics/js/bootstrap.bundle.js"></script>
  15. <title>Gin Hello</title>
  16. </head>
  17. {{end}}

nav.tmpl

  1. {{ if .email }}
  2. <ul class="navbar-nav ">
  3. <li class="nav-item">
  4. <a class="nav-link" href="/user/profile?id={{.id}}">{{ .email }}</a>
  5. </li>
  6. </ul>
  7. {{ else }}
  8. <ul class="navbar-nav ">
  9. <li class="nav-item">
  10. <a class="nav-link" data-toggle="modal" data-target="#login-modal">登录</a>
  11. </li>
  12. <li class="nav-item">
  13. <a class="nav-link" data-toggle="modal" data-target="#register-modal">注册</a>
  14. </li>
  15. </ul>
  16. {{end}}

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

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

user_profile.tmpl

  1. {{template "header"}}
  2. {{template "nav"}}
  3. <div class="container">
  4. <div class="row">
  5. <div class="col-sm">
  6. <div>
  7. <img src="{{ .user.Avatar.String }}" alt="avatar" class="rounded-circle">
  8. <div class="col-sm">
  9. <form method="post" action="/user/update" enctype="multipart/form-data">
  10. <div class="form-group" hidden>
  11. <label for="user-id">id</label>
  12. <input type="text" id="user-id"
  13. name="id"
  14. value="{{ .user.Id }}">
  15. <div class="form-group">
  16. <label for="user-email">Email</label>
  17. <input type="email" class="form-control" id="user-email" aria-describedby="emailHelp"
  18. name="email"
  19. readonly
  20. placeholder="Enter email"
  21. value="{{ .user.Email }}">
  22. <div class="form-group">
  23. <label for="user-password">密码</label>
  24. <input type="password" class="form-control" id="user-password" placeholder="密码" name="password"
  25. value="{{.user.Password}}">
  26. <div class="form-group">
  27. <label for="user-avatar">上传头像</label>
  28. <input type="file" class="form-control-file" id="user-avatar" name="avatar-file">
  29. <button type="submit" class="btn btn-primary">保存</button>
  30. </form>
  31. <div class="col-sm">

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

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

?上传头像

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

userHandler.go 添加 UpdateUserProfile 方法

  1. func UpdateUserProfile(context *gin.Context) {
  2. var user model.UserModel
  3. if err := context.ShouldBind(&user); err != nil {
  4. context.HTML(http.StatusOK, "error.tmpl", gin.H{
  5. "error": err.Error(),
  6. })
  7. log.Panicln("绑定发生错误 ", err.Error())
  8. }
  9. file, e := context.FormFile("avatar-file")
  10. if e != nil {
  11. context.HTML(http.StatusOK, "error.tmpl", gin.H{
  12. "error": e,
  13. })
  14. log.Panicln("文件上传错误", e.Error())
  15. }
  16. }

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

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

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

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

  1. package utils
  2. import (
  3. "log"
  4. "os"
  5. "os/exec"
  6. "strings"
  7. )
  8. func RootPath() string {
  9. s, err := exec.LookPath(os.Args[0])
  10. if err != nil {
  11. log.Panicln("发生错误",err.Error())
  12. }
  13. i := strings.LastIndex(s, "\\")
  14. path := s[0 : i+1]
  15. return path
  16. }

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

  1. // 省略部分代码
  2. path := utils.RootPath()
  3. path = path + "avatar\\"
  4. e = os.MkdirAll(path, os.ModePerm)
  5. if e != nil {
  6. context.HTML(http.StatusOK, "error.tmpl", gin.H{
  7. "error": e,
  8. })
  9. log.Panicln("无法创建文件夹", e.Error())
  10. }
  11. fileName := strconv.FormatInt(time.Now().Unix(), 10) + file.Filename
  12. e = context.SaveUploadedFile(file, path+fileName)
  13. if e != nil {
  14. context.HTML(http.StatusOK, "error.tmpl", gin.H{
  15. "error": e,
  16. })
  17. log.Panicln("无法保存文件", e.Error())
  18. }

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

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

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

initRouter.go

  1. router.StaticFS("/avatar", http.Dir(utils.RootPath()+"avatar/"))

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

完善我们最后的代码

  1. avatarUrl := "http://localhost:8080/avatar/" + fileName
  2. user.Avatar = sql.NullString{String: avatarUrl}
  3. e = user.Update(user.Id)
  4. if e != nil {
  5. context.HTML(http.StatusOK, "error.tmpl", gin.H{
  6. "error": e,
  7. })
  8. log.Panicln("数据无法更新", e.Error())
  9. }
  10. context.Redirect(http.StatusMovedPermanently, "/user/profile?id="+strconv.Itoa(user.Id))

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

最后效果

✍总结

通过本章节的学习 再次使用到了数据库的一些操作,同时也对文件上传进行了学习,并且完善了页面对于静态文件的显示。

?‍?本章节代码

https://github.com/youngxhui/GinHello/tree/gin_upload_file

历史文章

Gin(一):Hello
Gin(二):路由Router
Gin(三):模板tmpl
Gin(四):表单提交校验和模型绑定
Gin(五):连接MySQL
Gin(六):文件的上传
Gin(七):中间件的使用和定义 Gin(八):Cookie的使用

ft_authoradmin  ft_create_time2019-07-31 14:44
 ft_update_time2019-07-31 14:45