Go语言 基于gin框架从0开始构建一个bbs server(四)- 板块与发帖

596 阅读4分钟

github源码地址

帖子板块的设计

通常一个bbs 会分很多板块, 我们这里可以先设计一下 板块的表结构:

create table `community`
(
    `id`             int(11)                                 not null auto_increment,
    -- 板块id
    `community_id`   int(10) unsigned                        not null,
    -- 板块名称
    `community_name` varchar(128) collate utf8mb4_general_ci not null,
    -- 板块介绍
    `introduction`   varchar(256) collate utf8mb4_general_ci not null,
    `create_time`    timestamp                               not null default current_timestamp,
    `update_time`    timestamp                               not null default current_timestamp on update current_timestamp,
    primary key (`id`),
    unique key `idx_community_id` (`community_id`),
    unique key `idx_community_name` (`community_name`)
) engine = InnoDB
  default charset = utf8mb4
  collate = utf8mb4_general_ci;

改写路由

通常我们的api 上线之后 可能会做一些升级 比如v1 v2 之类的,所以我们可以改一下我们的路由注册

package route

import (
   "go_web_app/controllers"
   "go_web_app/logger"
   "go_web_app/middleware"
   "net/http"

   "go.uber.org/zap"

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

func Setup(mode string) *gin.Engine {
   if mode == gin.ReleaseMode {
      gin.SetMode(mode)
   }
   r := gin.New()
   // 最重要的就是这个日志库
   r.Use(logger.GinLogger(), logger.GinRecovery(true))
   
   //v1 版本的路由
   v1 := r.Group("/api/v1")

   // 注册
   v1.POST("/register", controllers.RegisterHandler)
   // 登录
   v1.POST("/login", controllers.LoginHandler)

   v1.GET("/community", controllers.CommunityHandler)

   //验证jwt机制
   v1.GET("/ping", middleware.JWTAuthMiddleWare(), func(context *gin.Context) {
      // 这里post man 模拟的 将token auth-token
      zap.L().Debug("ping", zap.String("ping-username", context.GetString("username")))
      controllers.ResponseSuccess(context, "pong")
   })

   r.GET("/", func(context *gin.Context) {
      context.String(http.StatusOK, "ok")
   })
   return r
}

这样以后接口升级 就不会迷茫了

完成板块list的 接口

这个比较简单了,我们还是按照 正常的mvc思路走

1 定义 我们的community model 2 定义 controller 3 logic 4 dao

type Community struct {
   Id          int64  `json:"id" db:"community_id"`
   Name        string `json:"name" db:"community_name"`
   Introdution string `json:"Introdution" db:"introdution"`
}
// CommunityHandler 板块
func CommunityHandler(c *gin.Context) {
   data, err := logic.GetCommunityList()
   if err != nil {
      zap.L().Error("GetCommunityList error", zap.Error(err))
      ResponseError(c, CodeServerBusy)
      return
   }
   ResponseSuccess(c, data)
}
func GetCommunityList() (communityList []*models.Community, err error) {
   return mysql.GetCommunityList()

}
package mysql

import (
   "database/sql"
   "go_web_app/models"

   "go.uber.org/zap"
)

func GetCommunityList() (communityList []*models.Community, err error) {
   sqlStr := "select community_id,community_name from community"
   err = db.Select(&communityList, sqlStr)
   if err != nil {
      // 空数据的时候 不算错误 只是没有板块而已
      if err == sql.ErrNoRows {
         zap.L().Warn("no community ")
         err = nil
      }
   }
   return

}

然后验验货

image.png

板块详情

有了前面的基础,我们再依葫芦画瓢, 完成一个接口,给定一个id 返回对应的 板块详情

类似:

image.png

先定义路由:

v1.GET("/community/:id", controllers.CommunityDetailHandler)

然后 定义controller

这里要注意 要做下 类型转换,取出对应的id 参数值

func CommunityDetailHandler(c *gin.Context) {
   communityIdStr := c.Param("id")
   communityId, err := strconv.ParseInt(communityIdStr, 10, 64)
   // 校验参数是否正确
   if err != nil {
      zap.L().Error("GetCommunityListDetail error", zap.Error(err))
      ResponseError(c, CodeInvalidParam)
      return
   }
   data, err := logic.GetCommunityById(communityId)
   if err != nil {
      zap.L().Error("GetCommunityListDetail error", zap.Error(err))
      ResponseError(c, CodeServerBusy)
      return
   }
   ResponseSuccess(c, data)
}

最后完成我们的logic与dao

func GetCommunityById(id int64) (model *models.Community, err error) {
   return mysql.GetCommunityById(id)

}
func GetCommunityById(id int64) (community *models.Community, err error) {
   community = new(models.Community)
   sqlStr := "select community_id,community_name,introduction,create_time " +
      "from community where community_id=?"
   err = db.Get(community, sqlStr, id)
   if err != nil {
      // 空数据的时候 不算错误 只是没有板块而已
      if err == sql.ErrNoRows {
         zap.L().Warn("no community ")
         err = nil
      }
   }
   return community, err

}

帖子

有了板块的概念和用户的概念,我们就可以定义我们的帖子了

drop table if exists `post`;
create table `post`
(
    `id`           bigint(20)                               not null auto_increment,
    `post_id`      bigint(20)                               not null comment '帖子id',
    `title`        varchar(128) collate utf8mb4_general_ci  not null comment '标题',
    `content`      varchar(8192) collate utf8mb4_general_ci not null comment '内容',
    `author_id`    bigint(20)                               not null comment '作者id',
    `community_id` bigint(20)                               not null default '1' comment '板块id',
    `status`       tinyint(4)                               not null default '1' comment '帖子状态',
    `create_time`  timestamp                                not null default current_timestamp comment '创建时间',
    `update_time`  timestamp                                not null default current_timestamp on update current_timestamp comment '更新时间',
    primary key (`id`),
    -- 唯一索引
    unique key `idx_post_id` (`post_id`),
    -- 普通索引
    key `idx_author_id` (`author_id`),
    key `idx_community_id` (`community_id`)
) engine = InnoDB
  default charset = utf8mb4
  collate = utf8mb4_general_ci;

同样的 也要定义帖子的struct:

type Post struct {
   Status      int32     `json:"status" db:"status"`
   CommunityId int64     `json:"community_id" db:"community_id"`
   Id          int64     `json:"id" db:"post_id"`
   AuthorId    int64     `json:"author_id" db:"author_id"`
   Title       string    `json:"title" db:"title"`
   Content     string    `json:"content" db:"content"`
   CreateTime  time.Time `json:"create_time" db:"create_time"`
}

内存对齐 优化

image.png

上面看到 struct的定义 有个特点,int和int的在一起,string和string的摆放在一起,那么这么做有什么好处呢?

package models

import (
   "fmt"
   "testing"
   "unsafe"
)

type P1 struct {
   a int8
   b string
   c int8
}

type P2 struct {
   a int8
   c int8
   b string
}

func TestPP(t *testing.T) {
   p1 := P1{
      a: 1,
      b: "b",
      c: 2,
   }

   p2 := P2{
      a: 1,
      c: 2,
      b: "b",
   }

   fmt.Println("p1:", unsafe.Sizeof(p1))
   fmt.Println("p2:", unsafe.Sizeof(p2))

}

image.png

明显看出来 p1的内存大小是比p2要大一些的

在这里暂且就不细说内存对齐的概念,我们只要知道 定义struct的时候 尽量把同一类型的放在一起 即可

发帖逻辑

有了上述的基础 我们要做发帖的逻辑 就很简单了

  1. 首先做参数验证
  2. 通过中间件获取发帖人的userId
  3. 雪花算法生成 帖子id
  4. 插入数据库
  5. 发帖成功返回帖子id

首先改写我们的struct 添加 binding tag

type Post struct {
   Status      int32     `json:"status" db:"status"`
   CommunityId int64     `json:"community_id" db:"community_id" binding:"required"`
   Id          int64     `json:"id" db:"post_id"`
   AuthorId    int64     `json:"author_id" db:"author_id"`
   Title       string    `json:"title" db:"title" binding:"required" `
   Content     string    `json:"content" db:"content" binding:"required" `
   CreateTime  time.Time `json:"create_time" db:"create_time"`
}

然后 写一下controller 层

func CreatePostHandler(c *gin.Context) {
   // 获取参数和参数校验
   p := new(models.Post)
   // 这里只能校验下 是否是标准的json格式 之类的 比较简单
   if err := c.ShouldBindJSON(p); err != nil {
      zap.L().Error("CreatePostHandler with invalid param", zap.Error(err))
      // 因为有的错误 比如json格式不对的错误 是不属于validator错误的 自然无法翻译,所以这里要做类型判断
      errs, ok := err.(validator.ValidationErrors)
      if !ok {
         ResponseError(c, CodeInvalidParam)
      } else {
         ResponseErrorWithMsg(c, CodeInvalidParam, removeTopStruct(errs.Translate(trans)))
      }
      return
   }
   // 取不到userId 则提示需要登录
   authorId, err := getCurrentUserId(c)
   if err != nil {
      ResponseError(c, CodeNoLogin)
      return
   }
   p.AuthorId = authorId
   msg, err := logic.CreatePost(p)
   zap.L().Info("CreatePostHandlerSuccess", zap.String("postId", msg))
   if err != nil {
      ResponseError(c, CodeServerBusy)
      return
   }
   ResponseSuccess(c, msg)

   // 创建帖子
   // 返回响应
}

logic层

// chuan
func CreatePost(post *models.Post) (msg string, err error) {
   // 雪花算法 生成帖子id
   post.Id = snowflake.GenId()
   zap.L().Debug("createPostLogic", zap.Int64("postId", post.Id))
   err = mysql.InsertPost(post)
   if err != nil {
      return "failed", err
   }
   //发表帖子成功时 要把帖子id 回给 请求方
   return strconv.FormatInt(post.Id, 10), nil
}

最后就是dao层

func InsertPost(post *models.Post) error {

   sqlstr := `insert into post(post_id,title,content,author_id,community_id) values(?,?,?,?,?)`
   _, err := db.Exec(sqlstr, post.Id, post.Title, post.Content, post.AuthorId, post.CommunityId)
   if err != nil {
      zap.L().Error("InsertPost dn error", zap.Error(err))
      return err
   }
   return nil
}

最后看下效果:

image.png