博客 / 詳情

返回

Gin + Gorm 實戰: 一小時完成一個簡單的問答社區後端服務

問答社區是一種常見的社交化應用,允許用户發佈問題、回答問題並相互交流。隨着互聯網的發展,問答社區已經成為人們獲取知識和分享經驗的重要平台。

本文將介紹如何使用 Gin 和 Gorm 構建一個簡單的問答社區。本社區包含以下功能:

  • 用户註冊和登錄
  • 問題發佈和回答
  • 問題列表和詳情
  • 答案列表和詳情
  • 用户信息和回答列表


數據庫設計

一共有users、questions、answers三個表,如下所示:

CREATE DATABASE IF NOT EXISTS qasys DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci;

create table qasys.users
(
    id         bigint unsigned auto_increment primary key,
    username   varchar(255) not null comment '用户名',
    password   varchar(255) not null comment '密碼',
    email      varchar(255) not null comment '郵箱',
    created_at datetime     null,
    updated_at datetime     null,
    deleted_at datetime     null,
    constraint email unique (email),
    constraint username unique (username)
);



create table qasys.questions
(
    id         bigint unsigned auto_increment primary key,
    user_id    int          not null,
    title      varchar(255) not null comment '標題',
    content    text         not null comment '問題詳情',
    created_at datetime     null,
    updated_at datetime     null,
    deleted_at datetime     null
);

create index questions_user_id_index on qasys.questions (user_id);


create table qasys.answers
(
    id          bigint unsigned auto_increment primary key,
    question_id int      not null comment '問題id',
    user_id     int      not null comment '用户id',
    content     text     not null comment '答案',
    created_at  datetime null,
    updated_at  datetime null,
    deleted_at  datetime null
);

create index answers_question_id_index on qasys.answers (question_id);
create index answers_user_id_index on qasys.answers (user_id);


準備環境

  1. 確保您的環境已經安裝了 Go 和 一個mysql服務,並把上面的表導入mysql。
  2. 安裝一個腳手架sponge(集成了gin+gorm),支持在windows、mac、linux環境下,點擊查看 安裝sponge説明。
  3. 安裝完成後打開終端,啓動sponge UI界面服務:
sponge run

在瀏覽器訪問 http://localhost:24631,進入sponge生成代碼的UI界面。


創建問答社區服務

進入sponge的UI界面:

  1. 點擊左邊菜單欄【SQL】-->【創建web服務】;
  2. 選擇數據庫mysql,填寫數據庫dsn,然後點擊按鈕獲取表名,選擇表名(可多選);
  3. 填寫其他參數,鼠標放在問號?位置可以查看參數説明;

填寫完參數後,點擊按鈕下載代碼生成web服務完整項目代碼,如下圖所示:

這是創建的web服務代碼目錄,已經包含了users, questions, answers三個表的CRUD api所有代碼,包含了Gin和Gorm的初始化和配置代碼,開箱即用。

.
├─ cmd
│   └─ qa
│       ├─ initial
│       └─ main.go
├─ configs
├─ deployments
│   ├─ binary
│   ├─ docker-compose
│   └─ kubernetes
├─ docs
├─ internal
│   ├─ cache
│   ├─ config
│   ├─ dao
│   ├─ ecode
│   ├─ handler
│   ├─ model
│   ├─ routers
│   ├─ server
│   └─ types
└─ scripts

解壓代碼文件,打開終端,切換到web服務代碼目錄,執行命令:

# 生成swagger文檔
make docs

# 編譯和運行服務
make run

在瀏覽器打開 http://localhost:8080/swagger/index.html,可以在頁面上進行增刪改查api測試,如下圖所示:

從上圖中可以看到,使用sponge生成服務已經完成了大部分api了,還有註冊登錄api、鑑權還沒實現,接下完成未實現的功能。


添加註冊登錄api

1. 定義請求參數和返回結果結構體

進入目錄internal/types,打開文件users_types.go,添加註冊和登錄的請求和返回結構體代碼:

// RegisterRequest login request params
type RegisterRequest struct {
    Email    string `json:"email" binding:"email"`    // 郵件
    Username string `json:"username" binding:"min=2"` // 用户名
    Password string `json:"password" binding:"min=6"` // 密碼
}

// RegisterRespond data
type RegisterRespond struct {
    Code int    `json:"code"` // return code
    Msg  string `json:"msg"`  // return information description
    Data struct {
        ID uint64 `json:"id"`
    } `json:"data"` // return data
}

// LoginRequest login request params
type LoginRequest struct {
    Username string `json:"username" binding:"min=2"` // 用户名
    Password string `json:"password" binding:"min=6"` // 密碼
}

// LoginRespond data
type LoginRespond struct {
    Code int    `json:"code"` // return code
    Msg  string `json:"msg"`  // return information description
    Data struct {
        ID uint64 `json:"id"`
        Token string `json:"token"`
    } `json:"data"` // return data
}


2. 定義錯誤碼

進入目錄internal/ecode,打開文件users_http.go,添加兩行行代碼,定義註冊和登錄錯誤碼:

var (
    usersNO       = 49
    usersName     = "users"
    usersBaseCode = errcode.HCode(usersNO)

    // ...
    ErrRegisterUsers       = errcode.NewError(usersBaseCode+10, "註冊失敗")
    ErrLoginUsers          = errcode.NewError(usersBaseCode+11, "登錄失敗")
    // for each error code added, add +1 to the previous error code
)


3. 定義handler函數

進入目錄internal/handler,打開文件users.go,添加註冊和登錄方法,並填寫swagger註解:

// Register 註冊
// @Summary 註冊
// @Description register
// @Tags auth
// @accept json
// @Produce json
// @Param data body types.RegisterRequest true "login information"
// @Success 200 {object} types.RegisterRespond{}
// @Router /api/v1/auth/register [post]
func (h *usersHandler) Register(c *gin.Context) {

}

// Login 登錄
// @Summary 登錄
// @Description login
// @Tags auth
// @accept json
// @Produce json
// @Param data body types.LoginRequest true "login information"
// @Success 200 {object} types.LoginRespond{}
// @Router /api/v1/teacher/login [post]
func (h *usersHandler) Login(c *gin.Context) {

}

然後把Register和Login方法添加到UsersHandler接口:

type UsersHandler interface {
    // ...
    Register(c *gin.Context)
    Login(c *gin.Context)
}


4. 註冊路由

進入目錄internal/routers,打開文件users.go,把Register和Login路由註冊進來:

func noAuthUsersRouter(group *gin.RouterGroup) {
    h := handler.NewUsersHandler()
    group.POST("/auth/register", h.Register)
    group.POST("/auth/login", h.Login)
}

然後把noAuthUsersRouter函數添加到routers.go的註冊路由函數下,如下所示:

func registerRouters(r *gin.Engine, groupPath string, routerFns []func(*gin.RouterGroup), handlers ...gin.HandlerFunc) {
    rg := r.Group(groupPath, handlers...)

    noAuthUsersRouter(rg)

    for _, fn := range routerFns {
        fn(rg)
    }
}


5. 編寫業務邏輯代碼

進入目錄internal/handler,打開文件users.go,編寫註冊和登錄的業務邏輯代碼:

func (h *usersHandler) Register(c *gin.Context) {
    req := &types.RegisterRequest{}
    err := c.ShouldBindJSON(req)
    if err != nil {
        logger.Warn("ShouldBindJSON error: ", logger.Err(err), middleware.GCtxRequestIDField(c))
        response.Error(c, ecode.InvalidParams)
        return
    }
    ctx := middleware.WrapCtx(c)

    password, err := gocrypto.HashAndSaltPassword(req.Password)
    if err != nil {
        logger.Error("gocrypto.HashAndSaltPassword error", logger.Err(err), middleware.CtxRequestIDField(ctx))
        response.Output(c, ecode.InternalServerError.ToHTTPCode())
        return
    }

    users := &model.Users{
        Username: req.Username,
        Password: password,
        Email:    req.Email,
    }

    err = h.iDao.Create(ctx, users)
    if err != nil {
        logger.Error("Create error", logger.Err(err), logger.Any("form", req), middleware.GCtxRequestIDField(c))
        response.Output(c, ecode.InternalServerError.ToHTTPCode())
        return
    }
    response.Success(c, gin.H{"id": users.ID})
}

func (h *usersHandler) Login(c *gin.Context) {
    req := &types.LoginRequest{}
    err := c.ShouldBindJSON(req)
    if err != nil {
        logger.Warn("ShouldBindJSON error: ", logger.Err(err), middleware.GCtxRequestIDField(c))
        response.Error(c, ecode.InvalidParams)
        return
    }
    ctx := middleware.WrapCtx(c)

    condition := &query.Conditions{
        Columns: []query.Column{
            {
                Name:  "username",
                Exp:   "=",
                Value: req.Username,
            },
        },
    }
    user, err := h.iDao.GetByCondition(ctx, condition)
    if err != nil {
        if errors.Is(err, model.ErrRecordNotFound) {
            logger.Warn("Login not found", logger.Err(err), logger.Any("form", req), middleware.GCtxRequestIDField(c))
            response.Error(c, ecode.ErrLoginUsers)
        } else {
            logger.Error("Login error", logger.Err(err), logger.Any("form", req), middleware.GCtxRequestIDField(c))
            response.Output(c, ecode.InternalServerError.ToHTTPCode())
        }
        return
    }

    // 驗證密碼
    if !gocrypto.VerifyPassword(req.Password, user.Password) {
        logger.Warn("password error", middleware.CtxRequestIDField(ctx))
        response.Error(c, ecode.ErrLoginUsers)
    }

    // 生成token
    token, err := jwt.GenerateToken(utils.Uint64ToStr(user.ID), user.Username)
    if err != nil {
        logger.Error("jwt.GenerateToken error", logger.Err(err), middleware.CtxRequestIDField(ctx))
        response.Output(c, ecode.InternalServerError.ToHTTPCode())
    }

    // TODO: 存儲token到緩存

    response.Success(c, gin.H{
        "id":    user.ID,
        "token": token,
    })
}

6. 開啓api鑑權

有了註冊和登錄api,其他api需要添加jwt鑑權,sponge生成的所有api默認是沒有加入jwt鑑權的,只需開啓即可,進入目錄internal/routers,分別打開questions.go,answers.go,users.go代碼,把默認註釋代碼去掉//反註釋,表示下面所有路由都開啓jwt鑑權,如下所示:

group.Use(middleware.Auth())

然後在需要鑑權的api的swagger註解中添加下面説明,這樣在swagger頁面請求api時,請求頭會帶上token,後端會從請求獲取token值並驗證token是否有效。

// @Security BearerAuth


7. 測試api

編寫完業務邏輯代碼後,在終端執行命令:

# 生成swagger文檔
make docs

# 編譯和運行服務
make run

在瀏覽器刷新 http://localhost:8080/swagger/index.html,在頁面上可以看到註冊和登錄api,在頁面測試註冊和登錄api,獲取到token之後,把Bearer token填寫到Authorize處,測試其他api是否可以正常調用。


8. 部署

默認支持部署到服務器、docker、k8s三種方式:

方式一:部署服務到遠程linux服務

# 如果需要更新服務,再次執行此命令
make deploy-binary USER=root PWD=123456 IP=192.168.1.10

方式二:部署到docker

# 構建鏡像
make image-build REPO_HOST=myRepo.com TAG=1.0

# 推送鏡像,這裏參數REPO_HOST和TAG,與構建鏡像的參數REPO_HOST和TAG一樣。
make image-push REPO_HOST=myRepo.com TAG=1.0

# 複製 deployments/docker-compose 目錄下的文件到目標服務器,修改鏡像地址後啓動服務
docker-compose up -d

方式三: 部署到k8s

# 構建鏡像
make image-build REPO_HOST=myRepo.com TAG=1.0

# 推送鏡像,這裏參數REPO_HOST和TAG,與構建鏡像的參數REPO_HOST和TAG一樣。
make image-push REPO_HOST=myRepo.com TAG=1.0

# 複製 deployments/kubernetes 目錄下的文件到目標服務器,修改鏡像地址後,按順序執行腳本
kubectl apply -f ./*namespace.yml
kubectl apply -f ./


總結

sponge集成了Gin 和 Gorm 強大的web後端服務開發框架,可以幫助開發者快速且輕鬆構建 RESTful API 服務,這是sponge github地址。

本文介紹瞭如何使用 Gin 和 Gorm 構建一個簡單的問答社區,問答社區包含了一些基本功能,可以作為基礎擴展到更復雜的應用。

user avatar jiangdaoyidezuoyeben 頭像
1 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.