距离我们组开始做大项目已经过去一周了,期间经历了两位队员的离开,一位队员阳了,等等情况。一开始进度非常缓慢,技术选型也是各种曲折。没想到一个周末已经完成了几个跟用户相关的API的开发,用这篇文章记录一下,也算是给自己巩固一下知识。
技术选型
| 后端框架 | gin |
|---|---|
| 数据库模块 | gorm |
| 数据库 | MySQL |
目前阶段只用到这些。后续可能会用到的:缓存(Redis),消息队列(Redis或者RabbitMQ),对象存储(Amazon S3)。虽然这么短的时间内大概率我们来不及用到这些东西。
项目开发
项目初始化
在1024code的工作台新建一个代码空间。
因为默认的Golang代码空间已经提供了 go.mod ,就不需要 go mod init 命令来新建项目了。
安装所需的依赖包:
go get github.com/gin-gonic/gin
go get gorm.io/gorm
go get gorm.io/driver/mysql
我们可以直接编写代码,到时候直接 go mod tidy 就好了。
数据库
一开始大家在讨论要怎么部署数据库,既方便每个人本地测试,又可以有一个大家都能访问的数据库,存放一些公用的数据。
本地测试一开始是计划用 docker compose配置两个服务,一个是后端服务器,一个是MySQL。只要配置好了,大家直接 docker compose up 就好了,避免了大家用不同的系统导致的各种本地环境不一致的问题,而且方便快捷。
后来发现原来1024code上面提供了现成的数据库资源,包括MySQL,Redis,和PostgreSQL。优点是一键部署,而且把数据库连接所需的信息都设置在了环境变量里面,非常方便。我们就直接推翻了一开始的计划,改用1024code提供的MySQL。
连接数据库的代码:
// 连接到MySQL数据库
User := os.Getenv("MYSQL_USER")
Pass := os.Getenv("MYSQL_PASSWORD")
Host := os.Getenv("MYSQL_HOST")
Port := os.Getenv("MYSQL_PORT")
// dsn := "user:rootpassword@tcp(db:3306)/app?charset=utf8mb4&parseTime=True&loc=Local"
dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/mysql?charset=utf8mb4&parseTime=True&loc=Local", User, Pass, Host, Port)
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
// TODO: defer close db
if err != nil {
panic("failed to connect database")
}
直接从环境变量读取对应的值,然后生dsn,生成数据库连接。
表单
根据之前我们定义好的数据库表单,定义了如下的表单结构体。
// User 表示应用中的用户
type User struct {
gorm.Model
Username string `gorm:"unique"`
Password string
Profile UserProfile `gorm:"foreignKey:UserID"`
}
// UserProfile 表示用户的额外信息
type UserProfile struct {
gorm.Model
UserID uint
Avatar string
Background string
Signature string
FollowCount int
FollowerCount int
TotalFavorited int
WorkCount int
}
根据GORM文档的说明,如果声明了gorm.Model,GORM会为每个结构体自动添加CreatedAt,UpdatedAt,DeletedAt,等字段。
遵循写的越多要改的越多的原则,我们就先之定义跟用户相关的结构体,等后面开发API时需要用到其它的表的时候再来定义。
在主函数中加入
db.AutoMigrate(&models.User{}, &models.UserProfile{})
用来自动将定义好的表单结构体迁移成MySQL中的表单。
初始化路由
在主函数中初始化路由,并且将之前建立的数据库连接注册为中间件,存入上下位,方面API处理函数跟数据库交互。
r := gin.Default()
// 注册中间件将db实例传递给每个处理函数
r.Use(func(c *gin.Context) {
c.Set("db", db)
c.Next()
})
API开发
用户注册
POST请求,必需的参数有username和password。
最简单的用户名和密码的注册方式,要求用户名保证唯一,创建成功后返回用户id和权限Token。
API函数Register(c *gin.Context)的大致处理流程:
- 新建一个变量
var user models.User,通过c.PostForm("username")和c.PostForm("password")获取请求的用户名和密码,并赋值给user对应的字段 - 检查用户名和密码非空,且长度在6-25位之间。如果不满足要求,返回
HTTP 400 Bad Request响应码 - 如果条件满足,试图将用户数据参入数据库:
db.Create(&user)。注意我们不需要额外判断用户名是否重复,因为之前定义表单结构体的时候已经声明了用户名字段的属性有gorm:"unique",即唯一性。如果注册用户使用了已经存在的用户名,这里会报错,直接返回HTTP 400 Bad Request - 如果注册成功,返回
HTTP 201 Created表示创建成功。目前我们还没有加入Token鉴权的逻辑。
用户登录
POST请求,必需的参数有username和password。
类似地,通过用户名和密码进行登录,登录成功后返回用户 id 和权限 token。
API函数Login(c *gin.Context)的大致处理流程:
- 初始化两个
models.User变量,user和inputUser,前者是数据库中保存的用户,后者是通过API请求参数得到的输入用户 - 验证用户名密码非空,否则返回
HTTP 401 Unauthorized - 通过GORM检索数据库看看名为
username的记录是否存在,如果不存在,返回HTTP 401 Unauthorized - 验证
user和inputUser的密码是否匹配。这里用的是最基本的明文匹配,实际生产中至少应该使用哈希后的密码。 - 如果上述验证都通过,返回
HTTP 200 OK,表示验证通过,返回用户ID和Token。
用户信息查询
GET请求,必需的参数有用户的ID和token,用来获取某个用户的主页信息。
API函数GetUser(c *gin.Context)的大致处理流程:
- 获取请求的ID,并转换为
uint格式。在转换的过程中如果出现错误,说明用户请求的ID并不是一个数字,那肯定不是一个合法的ID,直接返回HTTP 400 Bad Request - 通过GORM从数据库查找这个ID的用户,如果出现
gorm.ErrRecordNotFound的错误,返回HTTP 404 Not Found - 如果找到了用户,返回该用户的主页信息。
配置路由
不要忘了配置路由,将路由的URL跟定义的API函数一一对应起来。
r.POST("/douyin/user/register/", user.Register)
r.GET("/douyin/user/", user.GetUser)
r.POST("/douyin/user/login/", user.Login)
最后还要让服务器运行起来,这里设置后台服务器监听8080端口的请求。
r.Run(":8080")