项目介绍
本文档是博主在B站上发布的Go实战项目视频的配套文档。
视频地址:
技术栈
- Golang
- Gin
- Mysql
- Redis
- ...
表结构设计
项目只涉及到用户表和待做事项表。其实大家可以发现,按照常理来说,用户表和代做事项表应该是有外键关系的。我在设计这个表的时候没有给他们建立外键关系,没有什么任何特殊意义,只是出于个人习惯
// 用户表
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"` // 不需要序列化出去
}
项目需求
- 用户模块
- 用户登录
- 用户注册
- 查询用户待做事项
- 待做事项模块
- 新增待做事项
- 修改待做事项
- 单查待做事项
- 多查待做事项
- 完成待做事项
- 通用模块
- 发送验证码
项目结构
- 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级别的项目分层结构,并且加入些自己的扩展。
接口详解
下图是整个项目的请求流程
处于文章篇幅,我这边就不把每个接口的代码实现贴在这里了,大家自行去到我的GitHub仓库查看即可
1. 用户登录
- 校验请求参数
- 执行查询用户操作
- 成功-》返回用户ID和Token
- 失败-》返回失败信息
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. 用户注册
- 校验请求数据
- 校验邮箱验证码
- 执行新增用户操作
- 成功-》返回成功信息
- 失败-》返回失败信息
3. 查询用户待做事项
- 校验请求数据
- 执行查询用户操作**(查询用户+查询用户下所有的待做事项数据)**
- 成功-》返回成功信息
- 失败-》返回失败信息
4. 新增待做事项
- 中间件判断是否登录
- 校验请求数据
- 执行新增待做事项操作
- 成功-》返回成功数据
- 失败-》返回失败数据
5. 修改待做事项(修改待做事项内容)
- 中间件判断是否登录
- 校验请求数据
- 执行修改待做事项操作
- 成功-》返回成功数据
- 失败-》返回失败数据
6. 单查待做事项
- 中间件判断是否登录
- 校验请求数据
- 执行单查待做事项操作
- 成功-》返回成功数据
- 失败-》返回失败数据
7. 多查待做事项
- 中间件判断是否登录
- 校验请求数据
- 执行多查待做事项操作
- 成功-》返回成功数据
- 失败-》返回失败数据
8. 发送验证码
- 校验请求数据
- 执行发送验证码操作
- 保存验证码到Redis中
总结
整个项目还是比较简单的,还是有些设计思想在里面的,当然不是我想出来,而是从别人的代码中学到的,然后应用到这个项目中。比如
- 在查询用户信息和该用户下的代做事项记录的时候,使用到了协程,并且用WaitGroup保持数据的可用性
- 对于Handler层,使用到了接口,方便后期扩展
- 在Model层、Service层、Handler层都用到了单例模式,是通过Go内置的包,sync包实现的,主要是sync.Once实现
这个项目是一个后端项目,后期应该会有一个配套的前端页面出来。这几个礼拜学校的任务有点重,前端项目可能得等一阵子才能和大家见面了。希望大家能够期待下~