golang

用fiber写一个restful的博客后端

2021-01-21  本文已影响0人  thepoy

前言

Go的web框架有很多,fiber与gin、beego等的区别在于使用的是fasthttp,而不是标准库net/httpfasthttp的性能可以达到net/http的 10 倍,再加上标准 库net/http的作者已经离开了Go团队,标准库的性能在很长的一段时间内,可能都不会有提升。

1 博客的结构

本文写的是一个简单的个人博客,所以只要完成博客的基本功能就可以了。

1.1 用户

虽然是个人博客,但可以扩展出用户注册的功能来(万一有热心网友想在这个博客里注册账号发表文章呢?),这个注册的功能更多的还是用来发表评论/提出疑问。

1.1.1 需要实现的基本功能

1.1.2 数据库里用户表的基本信息

1.1.3 代码

1.1.3.1 数据库users表模型
// 使用gorm连接和管理数据库
// models/models.go
type User struct {
    gorm.Model
    Username      string     `json:"username" gorm:"type:varchar(20);not null;unique"`
    Password      string     `json:"password" gorm:"not null;type:varchar(256)"`
    Email         string     `json:"email" gorm:"type:varchar(60);not null;unique"`
    Phone         string     `json:"phone" gorm:"type:varchar(20);not null;unique"`
    IsAdmin       bool       `json:"is_admin" gorm:"type=boolean;not null;defult=false"`
    IsWriteOff    bool       `json:"is_write_off" gorm:"type=boolean;not null;defult=false"`
    IsVerified    bool       `json:"is_verify" gorm:"type=boolean;not null;defult=false"`
    RegisterIP    string     `json:"register_ip" gorm:"type:varchar(15);not null"`
    LastLoginTime *time.Time `json:"last_login_time"`
    LastLoginIP   string     `json:"last_login_ip" gorm:"type:varchar(15)"`
    Blogs         []Blog  // 见 1.2,user与blog为一对多的关系
}
1.1.3.2 注册

注册时,要考虑到几点:

  1. 验证前端传来的注册信息
  2. 验证邮件的发送
  3. 以密文保存密码
  4. 是普通用户还是管理员用户
  5. 唯一字段重复时的错误响应
  6. 注册成功的响应

注册的函数:

// apis/user.go
func Register(c *fiber.Ctx) error {
    var registerJSON models.RegisterJSON
    if err := c.BodyParser(&registerJSON); err != nil {
        return utils.ErrorJSON(c, fiber.StatusForbidden, err)
    }

    if err := registerJSON.Validate(); err != nil {
        return utils.ErrorJSON(c, fiber.StatusForbidden, err)
    }

    user := registerJSON.User
    user.IsAdmin = utils.IsAdmin(user.Username)
    user.RegisterIP = c.IP()

    user.IsVerified = utils.ValidateCodeIsValid(registerJSON.Email, registerJSON.ValidateCode)

    db := utils.GetDB()

    password, err := utils.GeneratePassword(user.Password)
    if err != nil {
        return utils.ErrorJSON(c, fiber.StatusForbidden, err)
    }
    user.Password = password

    result := db.Create(&user)
    if result.Error != nil {
        db.Rollback()
        errStr := result.Error.Error()
        if strings.Contains(errStr, "Error 1062: Duplicate entry") {
            err := utils.DatabaseExistError(errStr)
            return utils.ErrorJSON(c, fiber.StatusForbidden, err)
        }

    }

    return c.Status(fiber.StatusCreated).JSON(fiber.Map{
        "status":      fiber.StatusCreated,
        "msg":         "Account registration is successful",
        "username":    user.Username,
        "user_id":     user.ID,
        "is_verified": user.IsVerified,
    })
}

// models/json.go
type RegisterJSON struct {
    User
    ValidateCode string `json:"validate_code"`
}

func (rj RegisterJSON) Validate() error {
        // validation验证使用的是ozzo-validation
    return validation.ValidateStruct(&rj,
        validation.Field(&rj.Username, validation.Required, validation.Length(3, 20)),
        validation.Field(&rj.Password, validation.Required, validation.Length(8, 40)),
        validation.Field(&rj.Email, validation.Required, is.Email, validation.Length(5, 40)),
        // 手机号的正则规则待改进
        validation.Field(&rj.Phone,
            validation.Required,
            validation.Match(regexp.MustCompile("^1[0-9]{10}$")).Error("must be a string with 11 digits"),
        ),
        validation.Field(&rj.ValidateCode, validation.Required, validation.Length(6, 6)),
    )
}

// utils/user.go
func IsAdmin(username string) bool {
    for _, u := range blogConfig.Admins {
        if username == u {
            return true
        }
    }
    return false
}

func ValidateCodeIsValid(email, code string) bool {
    conn := redisPool.Get()
    defer conn.Close()

    res, err := redis.String(conn.Do("Get", email))
    if err != nil {
        return false
    }

    if code != res {
        return false
    }

    // 验证成功后,不管是否成功删除,都执行一次删除操作
    go func() {
        conn.Do("DEL", email)
    }()

    return true
}

func GeneratePassword(pwd string) (string, error) {
    hash, err := bcrypt.GenerateFromPassword([]byte(pwd), bcrypt.DefaultCost)
    if err != nil {
        return "", err
    }
    return string(hash), nil
}

func DatabaseExistError(errStr string) error {
    errInfo := strings.Split(errStr, "'")

    errCol := strings.Split(errInfo[3], ".")
    var res string
    if strings.Contains(errInfo[3], ".") {
        res = fmt.Sprintf("The `%s` you entered alreadyd exists: [ %s ]", errCol[1], errInfo[1])
    } else {
        res = fmt.Sprintf("The `%s` you entered alreadyd exists: [ %s ]", errCol[0], errInfo[1])
    }
    return errors.New(res)
}

// utils/common.go
func ErrorJSON(c *fiber.Ctx, statusCode int, err error) error {
    return c.Status(statusCode).JSON(&models.ErrorResponse{
        Error: err.Error(),
    })
}
上一篇下一篇

猜你喜欢

热点阅读