Go语言 基于gin框架从0开始构建一个bbs server(一)-用户注册

1,336 阅读8分钟

建立user 表

建表语句如下:

create  table `user`(
                        `id` bigint(20) not null auto_increment,
                        `user_id` bigint(20) not null ,
    -- utf8mb4_general_ci 这个就是排序的时候有点速度上的优势 仅此而已
                        `username` varchar(64) collate utf8mb4_general_ci not null ,
                        `password` varchar(64) collate utf8mb4_general_ci not null ,
                        `email` varchar(64) collate utf8mb4_general_ci,
                        `gender` tinyint(4) not null default '0',
    -- 这个就是插入数据的时候 可以自动设置时间
                        `create_time` timestamp null default  current_timestamp,
    -- 更新数据饿的时候 时间戳也可以自动更新
                        `update_time` timestamp null default  current_timestamp on update current_timestamp,
                        primary key (`id`),
    -- 建立索引 加快存储速度 同时unique key 代表是唯一的 不能重复
                        unique key `idx_username` (`username`) using btree ,
                        unique key `idx_user_id` (`user_id`) using btree
) default charset=utf8mb4 collate=utf8mb4_general_ci

这里要说下为啥 不能用id 这个自增字段 来代表 用户id

主要是因为 如果你用自增字段来代表用户id 那么竞争对手 可以通过你这个id的值 来判断 你的用户规模

另外 如果后期涉及到分库分表的时候 这个id 就会重复 那肯定是不行的

当然 user_id 这个字段 也有人用uuid的,但是用uuid呢 涉及到排序 就不是很方便了。

分布式 id 生成器

这个东西的特点是 全局唯一,具有递增性,高可用性 以及高性能。

不仅仅可以用于 用户id 还可以 作为 例如 微博转发的评论消息,电商里面的订单号 等等,

我们都是需要先生成这个分布式id,然后再插入到db中。 而且这个id 往往还带有一些时间的信息,

这样即使我们后面对数据库 做分库分表也可以游刃有余

雪花算法

这个是推特开源的64位整数组成分布式id,性能高,且单机递增。这个算法的详细介绍 有兴趣的同学可以自行百度, 这里我们只是介绍一下,后面会用到。

绝大多数人 我们只需要 这64bit的 每个bit 代表啥含义即可

0 无意义

41 bit 时间戳

10 bit 机器id (分布式的机器id)

12 bit 序列号

在go中 使用雪花算法

我们这里主要是 单机应用 所以machineid 传1 就行

package main

import (
   "fmt"
   "time"

   "github.com/bwmarrin/snowflake"
)

var node *snowflake.Node

func GenId() int64 {
   return node.Generate().Int64()
}

func Init(startTime string, machineId int64) (err error) {
   var st time.Time
   st, err = time.Parse("2006-01-02", startTime)
   if err != nil {
      return err
   }
   fmt.Println("st:", st)
   snowflake.Epoch = st.UnixNano() / 1000000
   node, err = snowflake.NewNode(machineId)
   return err
}

func main() {
   if err := Init(time.Now().Format("2006-01-02"), 1); err != nil {
      fmt.Printf("init failed ,err:%v\n", err)
   }
   id := GenId()
   fmt.Println(id)
}

用户注册功能的逻辑梳理

image.png

这里就用下mvc

简单一点:

controller层 我们就负责一些 参数校验之类的功能

logic 层 当然就是主要的业务逻辑处理层 在这一层我们不可避免的会调用到 数据库

dao 层 这个就是把数据库操作都封装程一个个函数给logic 层面调用 即可

简单实现controller

接受前端传递的参数时,我们总要校验一下参数是否合法

首先 我们可以把参数 定义成一个结构体、

image.png

然后 就是在controller 层面 验证我的参数是否合法

func RegisterHandler(c *gin.Context) {
   // 获取参数和参数校验
   p := new(models.ParamRegister)
   // 这里只能校验下 是否是标准的json格式 之类的 比较简单
   if err := c.ShouldBindJSON(p); err != nil {
      zap.L().Error("RegisterHandler with invalid param", zap.Error(err))
      c.JSON(http.StatusOK, gin.H{
         "msg": "请求参数有误",
      })
      return
   }
   if len(p.UserName) == 0 {
      c.JSON(http.StatusOK, gin.H{
         "msg": "用户名不能为空",
      })
      return
   }
   if len(p.Password) == 0 {
      c.JSON(http.StatusOK, gin.H{
         "msg": "密码不能为空",
      })
      return
   }
   // 进行参数校验
   if p.RePassword != p.Password {
      c.JSON(http.StatusOK, gin.H{
         "msg": "密码和确认密码不同",
      })
      return
   }
   fmt.Println(p)
   // 业务处理
   logic.Register(p)
   // 返回响应
   c.JSON(http.StatusOK, "register success")
}

利用三方库 来做校验

上述的校验规则 其实在很多接口中都有用到,为了简答一点 我们可以利用 第三方库来做这件事 可以省略不少代码 github.com/go-playgrou…

gin实际上也已经内置了这个库 我们直接用就可以

type ParamRegister struct {
   UserName   string `json:"username" binding:"required"`
   Password   string `json:"password" binding:"required"`
   RePassword string `json:"re_password" binding:"required"`
}

比如我们加上了required 就会自动校验 这些参数 是不是为空 为空的话 在 ShouldBindJSON 就会直接报错了,可以省略我们不少工作

if err := c.ShouldBindJSON(p); err != nil {
   zap.L().Error("RegisterHandler with invalid param", zap.Error(err))
   c.JSON(http.StatusOK, gin.H{
      "msg": err.Error(),
   })
   return
}

这时候 其实也可以看到 error 可以正确的展示 错误的信息

image.png

错误信息 国际化

有时候我们的项目也会涉及到国际化,比如上述的错误信息是英文的,可以改的再好一点 让她直接是中文的

首先我们注册一下翻译器

package controllers

import (
   "fmt"

   "github.com/gin-gonic/gin/binding"
   "github.com/go-playground/locales/en"
   "github.com/go-playground/locales/zh"
   ut "github.com/go-playground/universal-translator"
   "github.com/go-playground/validator/v10"
   enTrans "github.com/go-playground/validator/v10/translations/en"
   zhTrans "github.com/go-playground/validator/v10/translations/zh"
)

// 全局翻译器
var trans ut.Translator

// InitTrans locale 指定你想要的翻译 环境
func InitTrans(locale string) (err error) {
   if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
      //中文
      zhT := zh.New()
      //英文
      enT := en.New()
      // 第一个参数 是备用的语言
      uni := ut.New(enT, zhT, enT)

      //local 一般会在前端的请求头中 定义Accept-Language
      var ok bool
      trans, ok = uni.GetTranslator(locale)
      if !ok {
         return fmt.Errorf("uni.GetTranslator failed:%s ", locale)
      }

      switch locale {
      case "en":
         err = enTrans.RegisterDefaultTranslations(v, trans)
      case "zh":
         err = zhTrans.RegisterDefaultTranslations(v, trans)
      default:
         // 默认是英文
         err = enTrans.RegisterDefaultTranslations(v, trans)
      }
      return err
   }
   return err
}

在main函数启动的时候 调用一下她

// 初始化validator的 trans为中文
if err := controllers.InitTrans("zh"); err != nil {
   fmt.Printf("init translation  failed:%s \n", err)
   return
}

在输出报错信息的时候我们也要修改一下代码:

// 这里只能校验下 是否是标准的json格式 之类的 比较简单
if err := c.ShouldBindJSON(p); err != nil {
   zap.L().Error("RegisterHandler with invalid param", zap.Error(err))
   // 因为有的错误 比如json格式不对的错误 是不属于validator错误的 自然无法翻译,所以这里要做类型判断
   errs, ok := err.(validator.ValidationErrors)
   if !ok {
      c.JSON(http.StatusOK, gin.H{
         "msg": err.Error(),
      })
   } else {
      c.JSON(http.StatusOK, gin.H{
         "msg": errs.Translate(trans),
      })
   }
   return
}

最后看下运行的结果:

image.png

报错信息修改

上述不管是英文的 还是中文的报错信息 其实都有一个问题就是 json给的字段 是下划线的,然后我们给的报错信息 却是我们定义的struct里面的 field的信息 比如上述的username 结果报错的时候是UserName 这个其实很不合理

这里我们修改下:

可以在validator 那里 做一个取tag的方法:

//注册一个获取jsonTag的自定义方法
v.RegisterTagNameFunc(func(field reflect.StructField) string {
   name := strings.SplitN(field.Tag.Get("json"), ",", 2)[0]
   if name == "-" {
      return ""
   }
   return name
})

image.png

这里结果就很明显了。

但是想一想 好像还是不够优雅,这里前面还有一个什么ParasmRegister 之类的结构体信息

要想办法 把这个也去掉

// 去除报错信息中的结构体信息
func removeTopStruct(fields map[string]string) map[string]string {
   res := map[string]string{}
   for field, err := range fields {
      // 这里 算法非常简单 就是遍历你的错误信息 然后把key值取出来 把.之前的信息去掉就行了
      res[field[strings.Index(field, ".")+1:]] = err
   }
   return res
}

最后输出错误信息的时候 call 一下这个方法 即可

if !ok {
   c.JSON(http.StatusOK, gin.H{
      "msg": err.Error(),
   })
} else {
   c.JSON(http.StatusOK, gin.H{
      "msg": removeTopStruct(errs.Translate(trans)),
   })
}

image.png

eq字段的添加

上述我们还漏掉了一个 密码和确认密码 必须相等的判断逻辑,之前是自己手动判断的逻辑 ,这里也用这个第三方库改一下,其实就是增加一个配置 就好

type ParamRegister struct {
   UserName   string `json:"username" binding:"required"`
   Password   string `json:"password" binding:"required"`
   // eqfield 指定必须相等的字段
   RePassword string `json:"re_password" binding:"required,eqfield=Password"`
}

image.png

validator也可以自定义一些 校验方法,这里就不再演示了。有兴趣的可以自行查阅相关文档

完成注册功能

有了前文的基础知识以后 我们就可以完成这个注册需求了

首先我们可以完善我们的logic层,主要就是判断用户名存在与否 然后决定是否插入数据库

func Register(register *models.ParamRegister) (err error) {
   // 判断用户是否存在
   err = mysql.CheckUserExist(register.UserName)
   if err != nil {
      // db 出错
      return err
   }
   // 生成userid
   userId := snowflake.GenId()
   // 构造一个User db对象
   user := models.User{
      UserId:   userId,
      Username: register.UserName,
      Password: register.Password,
   }
   // 保存数据库
   err = mysql.InsertUser(&user)
   if err != nil {
      return err
   }
   return
}

然后定义一下我们的db struct

package models

type User struct {
   UserId   int64  `db:"user_id"`
   Username string `db:"username"`
   Password string `db:"password"`
}

在dao中 我们完成对db的操作

package mysql

import (
   "crypto/md5"
   "encoding/hex"
   "errors"
   "go_web_app/models"

   "go.uber.org/zap"
)

const serect = "wuyue.com"

// dao层 其实就是将数据库操作 封装为函数 等待logic层 去调用她

func InsertUser(user *models.User) error {
   // 密码要加密保存
   user.Password = encryptPassword(user.Password)
   sqlstr := `insert into user(user_id,username,password) values(?,?,?)`
   _, err := db.Exec(sqlstr, user.UserId, user.Username, user.Password)
   if err != nil {
      zap.L().Error("InsertUser dn error", zap.Error(err))
      return err
   }
   return nil
}

// CheckUserExist 检查数据库是否有该用户名
func CheckUserExist(username string) error {
   sqlstr := `select count(user_id) from user where username = ?`
   var count int
   err := db.Get(&count, sqlstr, username)
   if err != nil {
      zap.L().Error("CheckUserExist dn error", zap.Error(err))
      return err
   }
   if count > 0 {
      return errors.New("用户已存在")
   }
   return nil
}

// 加密密码
func encryptPassword(password string) string {
   h := md5.New()
   h.Write([]byte(serect))
   return hex.EncodeToString(h.Sum([]byte(password)))
}

最后 完成我们的controller 即可:

package controllers

import (
   "go_web_app/logic"
   "go_web_app/models"
   "net/http"

   "github.com/go-playground/validator/v10"

   "go.uber.org/zap"

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

func RegisterHandler(c *gin.Context) {
   // 获取参数和参数校验
   p := new(models.ParamRegister)
   // 这里只能校验下 是否是标准的json格式 之类的 比较简单
   if err := c.ShouldBindJSON(p); err != nil {
      zap.L().Error("RegisterHandler with invalid param", zap.Error(err))
      // 因为有的错误 比如json格式不对的错误 是不属于validator错误的 自然无法翻译,所以这里要做类型判断
      errs, ok := err.(validator.ValidationErrors)
      if !ok {
         c.JSON(http.StatusOK, gin.H{
            "msg": err.Error(),
         })
      } else {
         c.JSON(http.StatusOK, gin.H{
            "msg": removeTopStruct(errs.Translate(trans)),
         })
      }
      return
   }
   // 业务处理
   err := logic.Register(p)
   if err != nil {
      c.JSON(http.StatusOK, gin.H{
         "msg": err.Error(),
      })
      return
   }
   // 返回响应
   c.JSON(http.StatusOK, "register success")
}

源码