【Go实战项目】- Todo List

412 阅读5分钟

项目介绍

本文档是博主在B站上发布的Go实战项目视频的配套文档。

视频地址:

项目地址

技术栈

  • Golang
  • Gin
  • Mysql
  • Redis
  • ...

表结构设计

image.png 项目只涉及到用户表和待做事项表。其实大家可以发现,按照常理来说,用户表和代做事项表应该是有外键关系的。我在设计这个表的时候没有给他们建立外键关系,没有什么任何特殊意义,只是出于个人习惯

// 用户表
type User struct {
   ID        int64          `json:"id" gorm:"column:id;primarykey"`
   Username  string         `json:"username" gorm:"column:username;unique"`
   Password  string         `json:"-" gorm:"column:password"`
   Email     string         `json:"email" gorm:"column:email;unique"`
   Avatar    string         `json:"avatar" gorm:"column:avatar"`
   CreatedAt time.Time      `json:"created_at" gorm:"column:created_at"`
   UpdatedAt time.Time      `json:"updated_at" gorm:"column:updated_at"`
   DeletedAt gorm.DeletedAt `json:"-" gorm:"column:deleted_at;index"` // 不需要序列化出去
}
// 任务表
type Todo struct {
   ID        int64          `json:"id" gorm:"column:id;primarykey"`
   UserId    int64          `json:"user_id" gorm:"column:user_id"`
   Content   string         `json:"content" gorm:"column:content"`
   Completed bool           `json:"completed" gorm:"column:completed;default:false"`
   CreatedAt time.Time      `json:"created_at" gorm:"column:created_at"`
   UpdatedAt time.Time      `json:"updated_at" gorm:"column:updated_at"`
   DeletedAt gorm.DeletedAt `json:"-" gorm:"column:deleted_at;index"` // 不需要序列化出去
}

项目需求

  • 用户模块
  1. 用户登录
  2. 用户注册
  3. 查询用户待做事项
  • 待做事项模块
  1. 新增待做事项
  2. 修改待做事项
  3. 单查待做事项
  4. 多查待做事项
  5. 完成待做事项
  • 通用模块
  1. 发送验证码

项目结构

- config // 配置信息
    - conf-dev.yaml
    - conf-pro.yaml
    - config.go
- constant // 静态数据
    - constant.go
    - errno.go
- handler // handler层
    - common
        - init.go
    - user
        - init.go
    - todo
        - init.go
- middlers // 中间件
    - auth.go
- model // 模型层
    - todo.go
    - user.go
- router // 路由层
    - common.go
    - init.go
    - todo.go
    - user.go
- service // 服务层
    - common
    - user
    - todo
- template // 模板文件
    - email.html
- utils // 工具
    - crypto.go
    - jwt.go
    - logger.go
    - verify_code.go
- main.go // 项目入口

上面是整个项目的文件结构。一个合理的项目分层能够大大提高开发的效率。我们用的是Gin框架作为web框架,大家都知道,Gin官方并没有提供一个项目结构,不像Python中的Django框架,Java中的Spring全家桶,所以很多东西都需要我们开发人员自行设计,这就导致了市面上有很多分层。我这边是借鉴了字节跳动的一个Demo级别的项目分层结构,并且加入些自己的扩展。

接口详解

下图是整个项目的请求流程

image.png 处于文章篇幅,我这边就不把每个接口的代码实现贴在这里了,大家自行去到我的GitHub仓库查看即可

1. 用户登录

  1. 校验请求参数
  2. 执行查询用户操作
  3. 成功-》返回用户ID和Token
  4. 失败-》返回失败信息

Handler层

package user

import (
   "github.com/borntodie-new/todo-list-backup/config"
   "github.com/borntodie-new/todo-list-backup/constant"
   "github.com/borntodie-new/todo-list-backup/utils"
   "github.com/gin-gonic/gin"
   "github.com/golang-jwt/jwt/v4"
   "net/http"
   "time"

   resp "github.com/borntodie-new/todo-list-backup/handler"
   service "github.com/borntodie-new/todo-list-backup/service/user"
)

type LoginRequest struct {
   Username string `json:"username" binding:"required"`
   Password string `json:"password" binding:"required"`
}

func (h *Handler) Login(ctx *gin.Context) {
   req := new(LoginRequest)
   if err := ctx.ShouldBindJSON(&req); err != nil {
      ctx.JSON(http.StatusOK, resp.RespFailed(constant.ParamErr))
      return
   }
   // handler code here
   user, err := service.RetrieveUser(req.Username, req.Password, ctx, h.db)
   if err != nil {
      ctx.JSON(http.StatusOK, resp.RespFailed(err))
      return
   }
   // signed token here
   conf := config.GetConfig()
   customJWT := utils.NewCustomJWT([]byte(config.GetConfig().JWTConfig.SigningKey))
   claims := &utils.Claims{
      ID:       user.ID,
      Username: user.Username,
      RegisteredClaims: jwt.RegisteredClaims{
         ExpiresAt: jwt.NewNumericDate(time.Now().
            Add(time.Duration(conf.JWTConfig.ExpireTime) * time.Hour)), // 签名生效时间
         NotBefore: jwt.NewNumericDate(time.Now()), // 签名生效时间
         IssuedAt:  jwt.NewNumericDate(time.Now()), // 签名生效时间
      },
   }
   token, err := customJWT.GenerateToken(claims)
   if err != nil {
      ctx.JSON(http.StatusOK, resp.RespFailed(err))
      return
   }
   ctx.JSON(http.StatusOK, resp.RespSuccessWithData(gin.H{
      "user_id": user.ID,
      "token":   token,
   }))
}

Service层

package user

import (
   "context"
   "gorm.io/gorm"

   "github.com/borntodie-new/todo-list-backup/constant"
   "github.com/borntodie-new/todo-list-backup/model"
   "github.com/borntodie-new/todo-list-backup/utils"
)

type RetrieveUserFlow struct {
   // global
   ctx context.Context
   db  *gorm.DB

   // request data
   Username string
   Password string

   // response data
   User *model.User
}

func NewRetrieveUserFlow(username, password string, ctx context.Context, db *gorm.DB) *RetrieveUserFlow {
   return &RetrieveUserFlow{
      ctx:      ctx,
      db:       db,
      Username: username,
      Password: password,
   }
}

func RetrieveUser(username, password string, ctx context.Context, db *gorm.DB) (*model.User, error) {
   return NewRetrieveUserFlow(username, password, ctx, db).Do()
}

func (f *RetrieveUserFlow) Do() (*model.User, error) {
   if err := f.checkParam(); err != nil {
      return nil, err
   }
   if err := f.prepareData(); err != nil {
      return nil, err
   }
   return f.User, nil
}

func (f *RetrieveUserFlow) checkParam() error {
   if f.Username == "" || f.Password == ""{
      return constant.ParamErr
   }
   return nil
}

func (f *RetrieveUserFlow) prepareData() error {
   instance, err := model.NewUserDao(f.ctx, f.db).RetrieveInstance(f.Username)
   if err != nil {
      return constant.ServiceErr
   }
   verify := utils.Default().Verify(f.Password, instance.Password)
   if !verify {
      return constant.UserPasswordErr
   }
   f.User = instance
   return nil
}

Model层

package model

import (
   "context"
   "gorm.io/gorm"
   "sync"
   "time"

   "github.com/borntodie-new/todo-list-backup/constant"
)

type User struct {
   ID        int64          `json:"id" gorm:"column:id;primarykey"`
   Username  string         `json:"username" gorm:"column:username;unique"`
   Password  string         `json:"-" gorm:"column:password"`
   Email     string         `json:"email" gorm:"column:email;unique"`
   Avatar    string         `json:"avatar" gorm:"column:avatar"`
   CreatedAt time.Time      `json:"created_at" gorm:"column:created_at"`
   UpdatedAt time.Time      `json:"updated_at" gorm:"column:updated_at"`
   DeletedAt gorm.DeletedAt `json:"deleted_at" gorm:"column:deleted_at;index"`
}

func (User) TableName() string {
   return constant.UserTableName
}

type UserDao struct {
   ctx context.Context
   db  *gorm.DB
}

var (
   userDao  *UserDao
   userOnce sync.Once
)

func NewUserDao(ctx context.Context, db *gorm.DB) *UserDao {
   userOnce.Do(func() {
      userDao = &UserDao{ctx: ctx, db: db}
   })
   return userDao
}
func (d *UserDao) RetrieveInstance(username string) (*User, error) {
   u := new(User)
   err := d.db.WithContext(d.ctx).Where("username = ?", username).Find(&u).Error
   return u, err
}

2. 用户注册

  1. 校验请求数据
  2. 校验邮箱验证码
  3. 执行新增用户操作
  4. 成功-》返回成功信息
  5. 失败-》返回失败信息

3. 查询用户待做事项

  1. 校验请求数据
  2. 执行查询用户操作**(查询用户+查询用户下所有的待做事项数据)**
  3. 成功-》返回成功信息
  4. 失败-》返回失败信息

4. 新增待做事项

  1. 中间件判断是否登录
  2. 校验请求数据
  3. 执行新增待做事项操作
  4. 成功-》返回成功数据
  5. 失败-》返回失败数据

5. 修改待做事项(修改待做事项内容)

  1. 中间件判断是否登录
  2. 校验请求数据
  3. 执行修改待做事项操作
  4. 成功-》返回成功数据
  5. 失败-》返回失败数据

6. 单查待做事项

  1. 中间件判断是否登录
  2. 校验请求数据
  3. 执行单查待做事项操作
  4. 成功-》返回成功数据
  5. 失败-》返回失败数据

7. 多查待做事项

  1. 中间件判断是否登录
  2. 校验请求数据
  3. 执行多查待做事项操作
  4. 成功-》返回成功数据
  5. 失败-》返回失败数据

8. 发送验证码

  1. 校验请求数据
  2. 执行发送验证码操作
  3. 保存验证码到Redis中

总结

整个项目还是比较简单的,还是有些设计思想在里面的,当然不是我想出来,而是从别人的代码中学到的,然后应用到这个项目中。比如

  1. 在查询用户信息和该用户下的代做事项记录的时候,使用到了协程,并且用WaitGroup保持数据的可用性
  2. 对于Handler层,使用到了接口,方便后期扩展
  3. 在Model层、Service层、Handler层都用到了单例模式,是通过Go内置的包,sync包实现的,主要是sync.Once实现

这个项目是一个后端项目,后期应该会有一个配套的前端页面出来。这几个礼拜学校的任务有点重,前端项目可能得等一阵子才能和大家见面了。希望大家能够期待下~