全栈开发实战(一)——简易博客社区后端搭建
(一)项目准备
在项目开始前,首先确保你已安装好Go语言并配置好Go语言编辑器,同时安装好MySQL或其他数据库,其次,为了方便调试,可安装相应的接口测试工具和数据库可视化工具
本项目所使用的工具如下:
工具 | 说明 |
---|---|
GoLand | 编写并运行程序 |
Postman | 接口测试 |
Navicat Premiun | 数据库可视化 |
1. 创建项目
打开Goland,创建一个新的项目blog_server
为了更方便的import包,建议修改镜像路径如下:
GOPROXY=https://goproxy.io,direct
2. 模块安装
安装gin——一个golang的微框架
go get -u github.com/gin-gonic/gin
安装gorm——Golang语言中一款性能极好的ORM库
go get -u github.com/jinzhu/gorm
安装mysql驱动——用于操作数据库
go get github.com/go-sql-driver/mysql
安装jwt包——用于生成和验证token
go get github.com/dgrijalva/jwt-go
安装uuid包——用于生成id
go get -u -v github.com/satori/go.uuid
3. 创建数据库
从终端进入mysql,创建数据库blog
4. 创建静态文件目录
在项目文件夹下新建static文件夹,在该文件夹下新建images文件夹并放入一张png格式图片,将图片命名为default_avatar.png作为用户的初始头像(也可以是其他格式,请自行修改)
(二)登录注册接口
1. 用户模型
在项目文件夹下新建model文件夹,该文件夹存放项目的数据结构模型
我们先来构建用户结构体User,User继承gorm.Model,可自动添加id等基本字段。这里需要注意,为了实现收藏与关注功能,每个用户还有一个Collects和Following字段,用于保存收藏的文章编号和关注的用户编号,该字段的数据类型为数组
而UserInfo为部分的用户信息,便于将数据库的查询结果绑定到结构体上
新建user.go写入:
/* model/user.go */
type User struct {
gorm.Model
UserName string `gorm:"varchar(20);not null"`
PhoneNumber string `gorm:"varchar(20);not null;unique"`
Password string `gorm:"size:255;not null"`
Avatar string `gorm:"size:255;not null"`
Collects Array `gorm:"type:longtext"`
Following Array `gorm:"type:longtext"`
Fans int `gorm:"AUTO_INCREMENT"`
}
type UserInfo struct {
ID uint `json:"id"`
Avatar string `json:"avatar"`
UserName string `json:"userName"`
}
由于数据库本身无数组这一数据结构,我们需要自定义,并在数据存取时进行格式转换,即将数据存到数据库时,对数据进行处理,获得数据库支持的类型,而从数据库读取数据后,对其进行处理,获得Go类型的变量
新建array.go写入:
/* model/array.go */
type Array []string
// Scan 从数据库读取数据后,对其进行处理,获得Go类型的变量
func (m *Array) Scan(val interface{}) error {
s := val.([]uint8)
ss := strings.Split(string(s), "|")
*m = ss
return nil
}
// Value 将数据存到数据库时,对数据进行处理,获得数据库支持的类型
func (m Array) Value() (driver.Value, error) {
str := strings.Join(m, "|")
return str, nil
}
2. 连接数据库
在项目文件夹下新建common文件夹,该文件夹存放项目的一些通用功能
新建文件database.go,编写数据库初始化函数InitDB()与数据库数据获取函数GetDB()
/* common/database.go */
var DB *gorm.DB
// InitDB() 数据库初始化
func InitDB() *gorm.DB {
driverName := "mysql"
user := "root"
password := "你的Mysql root用户的密码"
host := "localhost"
port := "3306"
database := "blog"
charset := "utf8"
args := fmt.Sprintf("%s:%s@(%s:%s)/%s?charset=%s&parseTime=true",
user,
password,
host,
port,
database,
charset)
// 连接数据库
db, err := gorm.Open(driverName, args)
if err != nil {
panic("failed to open database: " + err.Error())
}
// 迁移数据表
db.AutoMigrate(&model.User{})
DB = db
return db
}
// 数据库信息获取
func GetDB() *gorm.DB {
return DB
}
3. 注册功能
在项目文件夹下新建cotroller文件夹,该文件夹存放主要的操作函数
新建UserController.go编写与用户有关的函数
我们先来编写注册函数Register(),用户注册函数的流程包括获取参数、数据验证、密码加密、创建用户、返回结果,在创建用户时,我们为用户写入一个默认的头像
/* controller/UserController.go */
// Register 注册
func Register(c *gin.Context) {
db := common.GetDB()
// 获取参数
var requestUser model.User
c.Bind(&requestUser)
userName := requestUser.UserName
phoneNumber := requestUser.PhoneNumber
password := requestUser.Password
// 数据验证
var user model.User
db.Where("phone_number = ?", phoneNumber).First(&user)
if user.ID != 0 {
c.JSON(http.StatusOK, gin.H{
"code": 422,
"msg": "用户已存在",
})
return
}
// 密码加密
hashedPassword, _ := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
// 创建用户
newUser := model.User{
UserName: userName,
PhoneNumber: phoneNumber,
Password: string(hashedPassword),
Avatar: "/images/default_avatar.png",
Collects: model.Array{},
Following: model.Array{},
Fans: 0,
}
db.Create(&newUser)
// 返回结果
c.JSON(http.StatusOK, gin.H{
"code": 200,
"msg": "注册成功",
})
}
4. 生成token
由于用户登录成功后我们需要为他发放一个token,我们先来编写token生成函数ReleaseToken()
在common文件夹下新建文件jwt.go,编写生成token的函数:
/* common/jwt.go */
// jwt加密密钥
var jwtKey = []byte("a_secret_key")
type Claims struct {
UserId uint
jwt.StandardClaims
}
// ReleaseToken 生成token
func ReleaseToken(user model.User) (string, error) {
// token的有效期
expirationTime := time.Now().Add(7 * 24 * time.Hour)
claims := &Claims{
// 自定义字段
UserId: user.ID,
// 标准字段
StandardClaims: jwt.StandardClaims{
// 过期时间
ExpiresAt: expirationTime.Unix(),
// 发放时间
IssuedAt: time.Now().Unix(),
},
}
// 使用jwt密钥生成token
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
tokenString, err := token.SignedString(jwtKey)
if err != nil {
return "", err
}
// 返回token
return tokenString, nil
}
5. 登录功能
我们接下来编写登录函数,用户登录函数的流程包括获取参数、数据验证、判断密码是否正确、发放token、返回结果,发放token时调用刚刚编写的ReleaseToke()
/* controller/UserController.go */
// Login 登录
func Login(c *gin.Context) {
db := common.GetDB()
// 获取参数
var requestUser model.User
c.Bind(&requestUser)
phoneNumber := requestUser.PhoneNumber
password := requestUser.Password
// 数据验证
var user model.User
db.Where("phone_number =?", phoneNumber).First(&user)
if user.ID == 0 {
c.JSON(http.StatusOK, gin.H{
"code": 422,
"msg": "用户不存在",
})
return
}
// 判断密码是否正确
if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password)); err != nil {
c.JSON(http.StatusOK, gin.H{
"code": 422,
"msg": "密码错误",
})
return
}
// 发放token
token, err := common.ReleaseToken(user)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"code": 500,
"msg": "系统异常",
})
return
}
// 返回结果
c.JSON(http.StatusOK, gin.H{
"code": 200,
"data": gin.H{"token": token},
"msg": "登录成功",
})
}
6. 解析token
前端接收到返回的token后会将其保存,当请求需要token验证的接口时再发送给后端,此时,后端就需要对token进行解析,识别出用户的身份
我们回到文件jwt.go,编写解析token的函数
/* common/jwt.go */
// ParseToken 解析token
func ParseToken(tokenString string) (*jwt.Token, *Claims, error) {
claims := &Claims{}
token, err := jwt.ParseWithClaims(tokenString, claims, func(token *jwt.Token) (i interface{}, err error) {
return jwtKey, nil
})
return token, claims, err
}
7. 中间件验证
接下来我们编写一个中间件,获取到前端请求中的token,调用ParseToken()对其进行解析,若token不合规范,该请求将会被抛弃,当token符合规范时才可以进行下一步操作
在项目文件夹下新建文件夹middleware,该文件夹存放项目所需的中间件
新建文件AuthMiddleware.go,编写中间件AuthMiddleware()
/* middl1e/AuthMiddleware.go */
func AuthMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
// 获取authorization header
tokenString := c.Request.Header.Get("Authorization")
// token为空
if tokenString == "" {
c.JSON(http.StatusOK, gin.H{
"code": 401,
"msg": "权限不足",
})
c.Abort()
return
}
// 非法token
if tokenString == "" || len(tokenString) < 7 || !strings.HasPrefix(tokenString, "Bearer") {
c.JSON(http.StatusUnauthorized, gin.H{
"code": 401,
"msg": "权限不足",
})
c.Abort()
return
}
// 提取token的有效部分
tokenString = tokenString[7:]
// 解析token
token, claims, err := common.ParseToken(tokenString)
// 非法token
if err != nil || !token.Valid {
c.JSON(http.StatusUnauthorized, gin.H{
"code": 401,
"msg": "权限不足",
})
c.Abort()
return
}
// 获取claims中的userId
userId := claims.UserId
DB := common.GetDB()
var user model.User
DB.Where("id =?", userId).First(&user)
// 将用户信息写入上下文便于读取
c.Set("user", user)
c.Next()
}
}
为了测试中间件,我们编写一个需要传token的函数,对前端发送的token进行解析并返回用户的部分信息
/* controller/UserController.go */
// GetInfo 登录后获取信息
func GetInfo(c *gin.Context) {
// 获取上下文中的用户信息
user, _ := c.Get("user")
// 返回用户信息
//response.Success(c, gin.H{"id": user.(model.User).ID, "avatar": user.(model.User).Avatar}, "登录获取信息成功")
c.JSON(http.StatusOK, gin.H{
"code": 200,
"data": gin.H{"id": user.(model.User).ID, "avatar": user.(model.User).Avatar},
"msg": "登录获取信息成功",
})
}
8. 编写路由
在项目文件夹下新建文件夹routes并新建文件routes.go,写入我们的路由
/* routes/rotues.go */
func CollectRoutes(r *gin.Engine) *gin.Engine {
// 允许跨域访问
r.Use(middleware.CORSMiddleware())
// 注册
r.POST("/register", controller.Register)
// 登录
r.POST("/login", controller.Login)
// 登录获取用户信息
r.GET("/user", middleware.AuthMiddleware(),controller.GetInfo)
return r
}
新建文件main.go文件,将package改为main并写入连接数据库、设置静态文件夹路径、创建并启动路由的相关语句
func main() {
// 获取初始化的数据库
db := common.InitDB()
// 延迟关闭数据库
defer db.Close()
// 创建路由引擎
r := gin.Default()
// 配置静态文件路径
r.StaticFS("/images", http.Dir("./static/images"))
// 启动路由
routes.CollectRoutes(r)
// 启动服务
panic(r.Run(":8080"))
}
这里有个小坑,记得在main.go中手动import mysql-driver
_ "github.com/go-sql-driver/mysql"
8. 接口测试
在控制台输入go run main.go
运行项目
打开Postman进行接口测试
注册接口测试
数据库新增了用户信息
登录接口测试
我们将token写入登录获取信息接口的头部,测试中间件
加上后端地址打开图片,测试静态文件目录设置是否正确
(三)图片上传接口
1. 上传图像功能
在controller文件夹下新建文件FileController.go,编写上传图像函数Upload,该函数接收前端传来的图片文件,保存于后端的静态文件夹并返回图片url
/* controller/FileController.go */
// Upload 上传图像
func Upload(c *gin.Context) {
file, header, err := c.Request.FormFile("file")
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"code": 500,
"msg": "格式错误",
})
return
}
filename := header.Filename
ext := path.Ext(filename)
// 用上传时间作为文件名
name := "image_" + time.Now().Format("20060102150405")
newFilename := name + ext
out, err := os.Create("static/images/" + newFilename)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"code": 500,
"msg": "创建错误",
})
return
}
defer out.Close()
_, err = io.Copy(out, file)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"code": 500,
"msg": "复制错误",
})
return
}
c.JSON(http.StatusOK, gin.H{
"code": 200,
"data": gin.H{"filePath": "/images/" + newFilename},
"msg": "上传成功",
})
}
2. 修改路由
在routes.go中添加图像上传的路由
/* model/category.go */
func CollectRoutes(r *gin.Engine) *gin.Engine {
// 注册
r.POST("/register", controller.Register)
// 登录
r.POST("/login", controller.Login)
// 登录获取用户信息
r.GET("/user", middleware.AuthMiddleware(),controller.GetInfo)
// 上传图像
r.POST("/upload", controller.Upload)
return r
}
3. 接口测试
测试图像上传接口
(四)分类查询接口
1. 分类表
为实现对文章进行分类,我们需要一个分类表(当然也可以写对分类增删查改的接口,不过我为了省事就跳过啦)
在数据库blog中新建表categories,添加字段id及category_name并插入一些数据,其中id为0定义为全部,不对文章进行分类
2. 分类模型
在model文件夹下新建category.go,构建分类模型
/* model/category.go */
type Category struct {
ID uint `json:"id" gorm:"type:char(36);primary_key;"`
CategoryName string `json:"name" gorm:"type:varchar(50);not null"`
}
3. 查询分类
在cotroller文件夹下新建文件CategoryController.go,编写查询全部分类的函数SearchCategory()和按分类id查询分类名的函数SearchCategoryName()
/* controller/CategoryController.go */
// SearchCategory 查询分类
func SearchCategory(c *gin.Context) {
db := common.GetDB()
var categories []model.Category
if err := db.Find(&categories).Error; err != nil {
response.Fail(c, nil, "查找失败")
return
}
response.Success(c, gin.H{"categories": categories}, "查找成功")
}
// SearchCategoryName 查询分类名
func SearchCategoryName(c *gin.Context) {
db := common.GetDB()
var category model.Category
// 获取path中的分类id
categoryId := c.Params.ByName("id")
if err := db.Where("id = ?", categoryId).First(&category).Error; err != nil {
response.Fail(c, nil, "分类不存在")
return
}
response.Success(c, gin.H{"categoryName": category.CategoryName}, "查找成功")
}
4. 修改路由
/* routes/routes.go */
func CollectRoutes(r *gin.Engine) *gin.Engine {
// 允许跨域访问
r.Use(middleware.CORSMiddleware())
// 注册
r.POST("/register", controller.Register)
// 登录
r.POST("/login", controller.Login)
// 登录获取用户信息
r.GET("/user", middleware.AuthMiddleware(),controller.GetInfo)
// 上传图像
r.POST("/upload", controller.Upload)
// 查询分类
r.GET("/category", controller.SearchCategory) // 查询分类
r.GET("/category/:id", controller.SearchCategoryName) // 查询分类名
return r
}
5. 测试接口
测试分类接口
(五)文章增删查改接口
1. 文章模型
在model文件夹下新建article.go文件夹,写入数据库存储的文章数据结构以及返回的文章数据结构,这里我们将使用uuid生成文章id
/* model/article.go */
type Article struct {
ID uuid.UUID `json:"id" gorm:"type:char(36);primary_key;"`
UserId uint `json:"user_id" gorm:"not null"`
CategoryId uint `json:"category_id" gorm:"not null"`
Title string `json:"title" gorm:"type:varchar(50);not null"`
Content string `json:"content" gorm:"type:text;not null"`
HeadImage string `json:"head_image"`
CreatedAt Time `json:"created_at" gorm:"type:timestamp"`
UpdatedAt Time `json:"updated_at" gorm:"type:timestamp"`
}
type ArticleInfo struct {
ID string `json:"id"`
CategoryId uint `json:"category_id"`
Title string `json:"title"`
Content string `json:"content"`
HeadImage string `json:"head_image"`
CreatedAt Time `json:"created_at"`
}
// BeforeCreate 在创建文章之前将id赋值
func (a *Article) BeforeCreate(s *gorm.Scope) error {
return s.SetColumn("ID", uuid.NewV4())
}
我们定义一个Time类型,将时间戳转化为实际时间
/* model/time.go */
const timeFormat = "2006-01-02 15:04:05"
const timezone = "Asia/Shanghai"
type Time time.Time
func (t Time) MarshalJSON() ([]byte, error) {
b := make([]byte, 0, len(timeFormat)+2)
b = append(b, '"')
b = time.Time(t).AppendFormat(b, timeFormat)
b = append(b, '"')
return b, nil
}
func (t *Time) UnmarshalJSON(data []byte) (err error) {
now, _ := time.ParseInLocation(`"`+timeFormat+`"`, string(data), time.Local)
*t = Time(now)
return
}
func (t Time) String() string {
return time.Time(t).Format(timeFormat)
}
func (t Time) local() time.Time {
loc, _ := time.LoadLocation(timezone)
return time.Time(t).In(loc)
}
func (t Time) Value() (driver.Value, error) {
var zeroTime time.Time
var ti = time.Time(t)
if ti.UnixNano() == zeroTime.UnixNano() {
return nil, nil
}
return ti, nil
}
func (t *Time) Scan(v interface{}) error {
value, ok := v.(time.Time)
if ok {
*t = Time(value)
return nil
}
return fmt.Errorf("can not convert %v to timestamp", v)
}
同时,在InitDB()中加入loc确定时区
/* common/database.go */
// InitDB() 数据库初始化
func InitDB() *gorm.DB {
driverName := "mysql"
user := "root"
password := "你的Mysql root用户的密码"
host := "localhost"
port := "3306"
database := "blog"
charset := "utf8"
loc := "Asia/Shanghai"
args := fmt.Sprintf("%s:%s@(%s:%s)/%s?charset=%s&parseTime=true&loc=%s",
user,
password,
host,
port,
database,
charset,
url.QueryEscape(loc))
// 连接数据库
db, err := gorm.Open(driverName, args)
if err != nil {
panic("failed to open database: " + err.Error())
}
// 迁移数据表
db.AutoMigrate(&model.User{})
DB = db
return db
}
为了方便,我们定义一个请求数据时的文章数据类型,保留文章增删查改操作的基本字段,以便后端接整个结构体并验证
在项目文件夹下新建文件夹vo,并新建文件article.go
/* vo/article.go */
type CreateArticleRequest struct {
// 加上binging用于表单验证
CategoryId uint `json:"category_id" binging:"required"`
Title string `json:"title" binging:"required"`
Content string `json:"content" binging:"required"`
HeadImage string `json:"head_image"`
}
2. 返回同一的响应格式
为减少代码量,我们为项目封装一个统一的失败与成功的返回格式
在项目文件夹下新建文件夹response并新建文件response.go,编写返回函数
/* response/response.go */
func Response(c *gin.Context, httpStatus int, code int, data gin.H, msg string) {
c.JSON(httpStatus, gin.H{"code": code, "data": data, "msg": msg})
}
// Success 成功
func Success(c *gin.Context, data gin.H, msg string) {
Response(c, http.StatusOK, 200, data, msg)
}
// Fail 失败
func Fail(c *gin.Context, data gin.H, msg string) {
Response(c, http.StatusOK, 400, data, msg)
}
2. 文章增删改查功能
在文件夹controller下新建文件ArticleController.go,编写文章增删改查的操作函数,其中List()函数返回关键字和分类查询的结果,count为满足关键字和分类的数据条数,便于前端进行分页
type ArticleController struct {
DB *gorm.DB
}
type IArticleController interface {
Create(c *gin.Context)
Update(c *gin.Context)
Delete(c *gin.Context)
Show(c *gin.Context)
List(c *gin.Context)
}
func (a ArticleController) Create(c *gin.Context) {
var articleRequest vo.CreateArticleRequest
// 数据验证
if err := c.ShouldBindJSON(&articleRequest); err != nil {
response.Fail(c, nil, "数据错误")
return
}
// 获取登录用户
user, _ := c.Get("user")
// 创建文章
article := model.Article{
UserId: user.(model.User).ID,
CategoryId: articleRequest.CategoryId,
Title: articleRequest.Title,
Content: articleRequest.Content,
HeadImage: articleRequest.HeadImage,
}
if err := a.DB.Create(&article).Error; err != nil {
response.Fail(c, nil, "发布失败")
return
}
response.Success(c, gin.H{"id": article.ID}, "发布成功")
}
func (a ArticleController) Update(c *gin.Context) {
var articleRequest vo.CreateArticleRequest
// 数据验证
if err := c.ShouldBindJSON(&articleRequest); err != nil {
response.Fail(c, nil, "数据错误")
return
}
// 获取path中的id
articleId := c.Params.ByName("id")
// 查找文章
var article model.Article
if a.DB.Where("id = ?", articleId).First(&article).RecordNotFound() {
response.Fail(c, nil, "文章不存在")
return
}
// 获取登录用户
user, _ := c.Get("user")
userId := user.(model.User).ID
if userId != article.UserId {
response.Fail(c, nil, "登录用户不正确")
return
}
// 更新文章
if err := a.DB.Model(&article).Update(articleRequest).Error; err != nil {
response.Fail(c, nil, "修改失败")
return
}
response.Success(c, nil, "修改成功")
}
func (a ArticleController) Delete(c *gin.Context) {
// 获取path中的id
articleId := c.Params.ByName("id")
// 查找文章
var article model.Article
if a.DB.Where("id = ?", articleId).First(&article).RecordNotFound() {
response.Fail(c, nil, "文章不存在")
return
}
// 获取登录用户
user, _ := c.Get("user")
userId := user.(model.User).ID
if userId != article.UserId {
response.Fail(c, nil, "登录用户不正确")
return
}
// 删除文章
if err := a.DB.Delete(&article).Error; err != nil {
response.Fail(c, nil, "删除失败")
return
}
response.Success(c, nil, "删除成功")
}
func (a ArticleController) Show(c *gin.Context) {
// 获取path中的id
articleId := c.Params.ByName("id")
// 查找文章
var article model.Article
if a.DB.Where("id = ?", articleId).First(&article).RecordNotFound() {
response.Fail(c, nil, "文章不存在")
return
}
// 展示文章详情
response.Success(c, gin.H{"article": article}, "查找成功")
}
func (a ArticleController) List(c *gin.Context) {
// 获取关键词、分类、分页参数
keyword := c.DefaultQuery("keyword", "")
categoryId := c.DefaultQuery("categoryId", "0")
pageNum, _ := strconv.Atoi(c.DefaultQuery("pageNum", "1"))
pageSize, _ := strconv.Atoi(c.DefaultQuery("pageSize", "5"))
var query []string
var args []string
// 若关键词存在
if keyword != "" {
query = append(query, "(title LIKE ? OR content LIKE ?)")
args = append(args, "%"+keyword+"%")
args = append(args, "%"+keyword+"%")
}
// 若分类存在
if categoryId != "0" {
query = append(query, "category_id = ?")
args = append(args, categoryId)
}
// 拼接字符串
var querystr string
if len(query) > 0 {
querystr = strings.Join(query, " AND ")
}
// 页面内容
var article []model.ArticleInfo
// 文章总数
var count int
// 查询文章
switch len(args) {
case 0:
a.DB.Table("articles").Select("id, category_id, title, LEFT(content,80) AS content, head_image, created_at").Order("created_at desc").Offset((pageNum - 1) * pageSize).Limit(pageSize).Find(&article)
a.DB.Model(model.Article{}).Count(&count)
case 1:
a.DB.Table("articles").Select("id, category_id, title, LEFT(content,80) AS content, head_image, created_at").Where(querystr, args[0]).Order("created_at desc").Offset((pageNum - 1) * pageSize).Limit(pageSize).Find(&article)
a.DB.Model(model.Article{}).Where(querystr, args[0]).Count(&count)
case 2:
a.DB.Table("articles").Select("id, category_id, title, LEFT(content,80) AS content, head_image, created_at").Where(querystr, args[0], args[1]).Order("created_at desc").Offset((pageNum - 1) * pageSize).Limit(pageSize).Find(&article)
a.DB.Model(model.Article{}).Where(querystr, args[0], args[1]).Count(&count)
case 3:
a.DB.Table("articles").Select("id, category_id, title, LEFT(content,80) AS content, head_image, created_at").Where(querystr, args[0], args[1], args[2]).Order("created_at desc").Offset((pageNum - 1) * pageSize).Limit(pageSize).Find(&article)
a.DB.Model(model.Article{}).Where(querystr, args[0], args[1], args[2]).Count(&count)
}
// 展示文章列表
response.Success(c, gin.H{"article": article, "count": count}, "查找成功")
}
func NewArticleController() IArticleController {
db := common.GetDB()
db.AutoMigrate(model.Article{})
return ArticleController{DB: db}
}
3. 修改路由
我们使用路由组简化路由的表达
/* routes/routes.go */
func CollectRoutes(r *gin.Engine) *gin.Engine {
// 允许跨域访问
r.Use(middleware.CORSMiddleware())
// 注册
r.POST("/register", controller.Register)
// 登录
r.POST("/login", controller.Login)
// 登录获取用户信息
r.GET("/user", middleware.AuthMiddleware(),controller.GetInfo)
// 上传图像
r.POST("/upload", controller.Upload)
// 查询分类
r.GET("/category", controller.SearchCategory) // 查询分类
r.GET("/category/:id", controller.SearchCategoryName) // 查询分类名
//用户文章的增删查改
articleRoutes := r.Group("/article")
articleController := controller.NewArticleController()
articleRoutes.POST("", middleware.AuthMiddleware(), articleController.Create) // 发布文章
articleRoutes.PUT(":id", middleware.AuthMiddleware(), articleController.Update) // 修改文章
articleRoutes.DELETE(":id", middleware.AuthMiddleware(), articleController.Delete) // 删除文章
articleRoutes.GET(":id", articleController.Show) // 查看文章
articleRoutes.POST("list", articleController.List)
return r
}
4. 接口测试
文章发布接口
文章修改接口
文章修改接口
文章关键字和分类分页查询
文章查看接口
(六)用户信息管理接口
1.获取简要信息
在显示文章时需要同时显示作者头像,因此我们写一个函数返回文章作者的头像、文章作者的ID以及当前登录用户的ID,以便判断文章的作者是否是登录用户
/* controller/UserController.go */
// GetBriefInfo 获取简要信息
func GetBriefInfo(c *gin.Context) {
db := common.GetDB()
// 获取path中的userId
userId := c.Params.ByName("id")
// 判断用户身份
user, _ := c.Get("user")
var curUser model.User
if userId == strconv.Itoa(int(user.(model.User).ID)) {
curUser = user.(model.User)
} else {
db.Where("id =?", userId).First(&curUser)
if curUser.ID == 0 {
response.Fail(c, nil, "用户不存在")
return
}
}
// 返回用户简要信息
response.Success(c, gin.H{"id": curUser.ID, "name": curUser.UserName, "avatar": curUser.Avatar, "loginId": user.(model.User).ID}, "查找成功")
}
2. 获取详细信息
在用户信息的详情页,需要展示用户的头像、用户名、文章列表、收藏夹、关注列表等信息,我们写一个函数返回上述信息
由于收藏夹、关注列表是自定义类型,我们需要将其转化为字符串数组才能使用IN查询
/* controller/UserController.go */
// GetDetailedInfo 获取详细信息
func GetDetailedInfo(c *gin.Context) {
db := common.GetDB()
// 获取path中的userId
userId := c.Params.ByName("id")
// 判断用户身份
user, _ := c.Get("user")
//var self bool
var curUser model.User
if userId == strconv.Itoa(int(user.(model.User).ID)) {
//self = true
curUser = user.(model.User)
} else {
//self = false
db.Where("id = ?", userId).First(&curUser)
if curUser.ID == 0 {
response.Fail(c, nil, "用户不存在")
return
}
}
// 返回用户详细信息
var articles, collects []model.ArticleInfo
var following []model.UserInfo
var collist, follist []string
collist = ToStringArray(curUser.Collects)
follist = ToStringArray(curUser.Following)
db.Table("articles").Select("id, category_id, title, LEFT(content,80) AS content, head_image, created_at").Where("user_id = ?", userId).Order("created_at desc").Find(&articles)
db.Table("articles").Select("id, category_id, title, LEFT(content,80) AS content, head_image, created_at").Where("id IN (?)", collist).Order("created_at desc").Find(&collects)
db.Table("users").Select("id, avatar, user_name").Where("id IN (?)", follist).Find(&following)
response.Success(c, gin.H{"id": curUser.ID, "name": curUser.UserName, "avatar": curUser.Avatar, "loginId": user.(model.User).ID, "articles": articles, "collects": collects, "following": following, "fans": curUser.Fans}, "查找成功")
}
// ToStringArray 将自定义类型转化为字符串数组
func ToStringArray(l []string) (a model.Array) {
for i := 0; i < len(a); i++ {
l = append(l, a[i])
}
return l
}
2. 修改信息功能
用户允许修改头像和用户名,实现方式如下:
/* controller/UserController.go */
// ModifyAvatar 修改头像
func ModifyAvatar(c *gin.Context) {
db := common.GetDB()
// 获取用户ID
user, _ := c.Get("user")
// 获取参数
var requestUser model.User
c.Bind(&requestUser)
avatar := requestUser.Avatar
// 查找用户
var curUser model.User
db.Where("id = ?", user.(model.User).ID).First(&curUser)
// 更新信息
if err := db.Model(&curUser).Update("avatar", avatar).Error; err != nil {
response.Fail(c, nil, "更新失败")
return
}
response.Success(c, nil, "更新成功")
}
// ModifyName 修改用户名
func ModifyName(c *gin.Context) {
db := common.GetDB()
// 获取用户ID
user, _ := c.Get("user")
// 获取参数
var requestUser model.User
c.Bind(&requestUser)
userName := requestUser.UserName
// 查找用户
var curUser model.User
db.Where("id = ?", user.(model.User).ID).First(&curUser)
// 更新信息
if err := db.Model(&curUser).Update("user_name", userName).Error; err != nil {
response.Fail(c, nil, "更新失败")
return
}
response.Success(c, nil, "更新成功")
}
3. 修改路由
我们将与用户信息有关的接口写成一个路由组
/* routes/routes.go */
func CollectRoutes(r *gin.Engine) *gin.Engine {
// 允许跨域访问
r.Use(middleware.CORSMiddleware())
// 注册
r.POST("/register", controller.Register)
// 登录
r.POST("/login", controller.Login)
// 上传图像
r.POST("/upload", controller.Upload)
// 用户信息管理
userRoutes := r.Group("/user")
userRoutes.Use(middleware.AuthMiddleware())
userRoutes.GET("", controller.GetInfo) // 验证用户
userRoutes.GET("briefInfo/:id", controller.GetBriefInfo) // 获取用户简要信息
userRoutes.GET("detailedInfo/:id", controller.GetDetailedInfo) // 获取用户详细信息
userRoutes.PUT("avatar/:id", controller.ModifyAvatar) // 修改头像
userRoutes.PUT("name/:id", controller.ModifyName) // 修改用户名
// 查询分类
r.GET("/category", controller.SearchCategory) // 查询分类
r.GET("/category/:id", controller.SearchCategoryName) // 查询分类名
//用户文章的增删查改
articleRoutes := r.Group("/article")
//articleRoutes.Use(middleware.AuthMiddleware())
articleController := controller.NewArticleController()
articleRoutes.POST("", middleware.AuthMiddleware(), articleController.Create) // 发布文章
articleRoutes.PUT(":id", middleware.AuthMiddleware(), articleController.Update) // 修改文章
articleRoutes.DELETE(":id", middleware.AuthMiddleware(), articleController.Delete) // 删除文章
articleRoutes.GET(":id", articleController.Show) // 查看文章
articleRoutes.POST("list", articleController.List) // 显示文章列表
return r
}
4. 接口测试
获取简要信息接口
获取详细信息接口
创建一个用户进行测试
修改头像接口
修改用户名接口
用户信息修改成功
(七)收藏关注接口
1. 收藏功能
我们来编写函数查询登录用户是否有收藏当前的文章,并编写函数实现将文章ID添加到用户收藏夹和移除用户收藏夹
/* routes/routes.go */
// Collects 查询收藏
func Collects(c *gin.Context) {
db := common.GetDB()
// 获取用户ID
user, _ := c.Get("user")
// 获取path中的id
id := c.Params.ByName("id")
var curUser model.User
db.Where("id = ?", user.(model.User).ID).First(&curUser)
// 判断是否已收藏
for i := 0; i < len(curUser.Collects); i++ {
if curUser.Collects[i] == id {
response.Success(c, gin.H{"collected": true, "index": i}, "查询成功")
return
}
}
response.Success(c, gin.H{"collected": false}, "查询成功")
}
// NewCollect 新增收藏
func NewCollect(c *gin.Context) {
db := common.GetDB()
// 获取用户ID
user, _ := c.Get("user")
// 获取path中的id
id := c.Params.ByName("id")
// 查找用户
var curUser model.User
db.Where("id = ?", user.(model.User).ID).First(&curUser)
var newCollects []string
newCollects = append(curUser.Collects, id)
// 更新收藏夹
if err := db.Model(&curUser).Update("collects", newCollects).Error; err != nil {
response.Fail(c, nil, "更新失败")
return
}
response.Success(c, nil, "更新成功")
}
// UnCollect 取消收藏
func UnCollect(c *gin.Context) {
db := common.GetDB()
// 获取用户ID
user, _ := c.Get("user")
// 获取path中的index
index, _ := strconv.Atoi(c.Params.ByName("index"))
// 查找用户
var curUser model.User
db.Where("id = ?", user.(model.User).ID).First(&curUser)
var newCollects []string
newCollects = append(curUser.Collects[:index], curUser.Collects[index+1:]...)
// 更新收藏夹
if err := db.Model(&curUser).Update("collects", newCollects).Error; err != nil {
response.Fail(c, nil, "更新失败")
return
}
response.Success(c, nil, "更新成功")
}
2. 关注功能
关注功能的实现与收藏功能的实现类似,但要修改文章作者的粉丝数
// Following 查询关注
func Following(c *gin.Context) {
db := common.GetDB()
// 获取用户ID
user, _ := c.Get("user")
// 获取path中的id
id := c.Params.ByName("id")
var curUser model.User
db.Where("id = ?", user.(model.User).ID).First(&curUser)
// 判断是否已关注
for i := 0; i < len(curUser.Following); i++ {
if curUser.Following[i] == id {
response.Success(c, gin.H{"followed": true, "index": i}, "查询成功")
return
}
}
response.Success(c, gin.H{"followed": false}, "查询成功")
}
// NewFollow 新增关注
func NewFollow(c *gin.Context) {
db := common.GetDB()
// 获取用户ID
user, _ := c.Get("user")
// 获取path中的id
id := c.Params.ByName("id")
// 查找用户
var curUser model.User
db.Where("id = ?", user.(model.User).ID).First(&curUser)
//var newFollowing []string
newFollowing := append(curUser.Following, id)
// 更新关注列表
if err := db.Model(&curUser).Update("following", newFollowing).Error; err != nil {
response.Fail(c, nil, "更新失败")
return
}
// 更新粉丝数
var followUser model.User
db.Where("id = ?", id).First(&followUser)
if err := db.Model(&followUser).Update("fans", followUser.Fans+1).Error; err != nil {
response.Fail(c, nil, "更新失败")
return
}
response.Success(c, nil, "更新成功")
}
// UnFollow 取消关注
func UnFollow(c *gin.Context) {
db := common.GetDB()
// 获取用户ID
user, _ := c.Get("user")
// 获取path中的index
index, _ := strconv.Atoi(c.Params.ByName("index"))
// 查找用户
var curUser model.User
db.Where("id = ?", user.(model.User).ID).First(&curUser)
//var newFollowing []string
newFollowing := append(curUser.Following[:index], curUser.Following[index+1:]...)
followId := curUser.Following[index]
// 更新关注列表
if err := db.Model(&curUser).Update("following", newFollowing).Error; err != nil {
response.Fail(c, nil, "更新失败")
return
}
// 更新粉丝数
var followUser model.User
db.Where("id = ?", followId).First(&followUser)
if err := db.Model(&followUser).Update("fans", followUser.Fans-1).Error; err != nil {
response.Fail(c, nil, "更新失败")
return
}
response.Success(c, nil, "更新成功")
}
3. 跨域请求中间件
写到这里,我们后端的所有接口已经完成,但在前后端交互时,我们还需解决跨域问题
接下来我们来编写一个跨域请求的中间件处理跨域请求
在middleware文件夹下新建文件CORSMiddleware.go
/* middleware/CORSMiddleware.go/ */
func CORSMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
c.Writer.Header().Set("Access-Control-Allow-Origin", "*")
c.Writer.Header().Set("Access-Control-Allow-Methods", "*")
c.Writer.Header().Set("Access-Control-Allow-Headers", "*")
c.Writer.Header().Set("Access-Control-Allow-Credentials", "true")
c.Writer.Header().Set("Access-Control-Max-Age", "86400")
if c.Request.Method == http.MethodOptions {
c.AbortWithStatus(200)
} else {
c.Next()
}
}
}
4. 修改路由
我们将处理跨域请求的中间件放在开头,同时也使用路由组完成收藏和关注的接口
/* routes/routes.go */
func CollectRoutes(r *gin.Engine) *gin.Engine {
// 允许跨域访问
r.Use(middleware.CORSMiddleware())
// 注册
r.POST("/register", controller.Register)
// 登录
r.POST("/login", controller.Login)
// 上传图像
r.POST("/upload", controller.Upload)
r.POST("/upload/rich_editor_upload", controller.RichEditorUpload)
// 用户信息管理
userRoutes := r.Group("/user")
userRoutes.Use(middleware.AuthMiddleware())
userRoutes.GET("", controller.GetInfo) // 验证用户
userRoutes.GET("briefInfo/:id", controller.GetBriefInfo) // 获取用户简要信息
userRoutes.GET("detailedInfo/:id", controller.GetDetailedInfo) // 获取用户详细信息
userRoutes.PUT("avatar/:id", controller.ModifyAvatar) // 修改头像
userRoutes.PUT("name/:id", controller.ModifyName) // 修改用户名
// 我的收藏
colRoutes := r.Group("/collects")
colRoutes.Use(middleware.AuthMiddleware())
colRoutes.GET(":id", controller.Collects) // 查询收藏
colRoutes.PUT("new/:id", controller.NewCollect) // 收藏
colRoutes.DELETE(":index", controller.UnCollect) // 取消收藏
// 我的关注
folRoutes := r.Group("/following")
folRoutes.Use(middleware.AuthMiddleware())
folRoutes.GET(":id", controller.Following) // 查询关注
folRoutes.PUT("new/:id", controller.NewFollow) // 关注
folRoutes.DELETE(":index", controller.UnFollow) // 取消关注
// 查询分类
r.GET("/category", controller.SearchCategory) // 查询分类
r.GET("/category/:id", controller.SearchCategoryName) // 查询分类名
//用户文章的增删查改
articleRoutes := r.Group("/article")
//articleRoutes.Use(middleware.AuthMiddleware())
articleController := controller.NewArticleController()
articleRoutes.POST("", middleware.AuthMiddleware(), articleController.Create) // 发布文章
articleRoutes.PUT(":id", middleware.AuthMiddleware(), articleController.Update) // 修改文章
articleRoutes.DELETE(":id", middleware.AuthMiddleware(), articleController.Delete) // 删除文章
articleRoutes.GET(":id", articleController.Show) // 查看文章
articleRoutes.POST("list", articleController.List) // 显示文章列表
return r
}
5. 接口测试
添加收藏
取消收藏
添加关注
取消关注
(九)总结
恭喜你已完成了后端的搭建,接下来将介绍前端的搭建过程~