用fiber写一个restful的博客后端
2021-01-21 本文已影响0人
thepoy
前言
Go的web框架有很多,fiber与gin、beego等的区别在于使用的是fasthttp
,而不是标准库net/http
。fasthttp
的性能可以达到net/http
的 10 倍,再加上标准 库net/http
的作者已经离开了Go团队,标准库的性能在很长的一段时间内,可能都不会有提升。
1 博客的结构
本文写的是一个简单的个人博客,所以只要完成博客的基本功能就可以了。
1.1 用户
虽然是个人博客,但可以扩展出用户注册的功能来(万一有热心网友想在这个博客里注册账号发表文章呢?),这个注册的功能更多的还是用来发表评论/提出疑问。
1.1.1 需要实现的基本功能
- 注册
- 登录
- 退出
- 注销/软删除
- 删除/硬删除
1.1.2 数据库里用户表的基本信息
- id
- 用户名
- 密码(加密)
- 邮箱
- 手机号
- 是否为高级用户/管理员
- 是否已注销
- 是否已验证
- 注册的ip
- 注册的时间
- 更新的时间
- 注销的时间
- 最后一次登录的ip
- 最后一次登录的时间
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 注册
注册时,要考虑到几点:
- 验证前端传来的注册信息
- 验证邮件的发送
- 以密文保存密码
- 是普通用户还是管理员用户
- 唯一字段重复时的错误响应
- 注册成功的响应
注册的函数:
// apis/user.go
func Register(c *fiber.Ctx) error {
var registerJSON models.RegisterJSON
if err := c.BodyParser(®isterJSON); 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(),
})
}