Gin+Gorm实现简单备忘录

视频链接:https://www.bilibili.com/video/BV1GT4y1R7tX

Github地址:https://github.com/CocaineCong/TodoList

自己做了一遍的Gin+Gorm备忘录项目:https://github.com/Unicorn-acc/Golang-Project/tree/main/GoProject_todolist

此项目使用Gin+Gorm ,基于RESTful API实现的一个备忘录

规范是非常重要的,此项目非常适合小白入门学习web开发

接口文档内容:

项目主要功能介绍

  • 用户注册登录 ( jwt-go鉴权 )
  • 新增/删除/修改/查询 备忘录
  • 存储每条备忘录的浏览次数
  • 分页功能

一、项目初始化

创建项目文件的方式看gin部分:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 初始化一个目录
D:\go\project>mkdir ginlearn
D:\go\project>cd ginlearn
// 1. 这是目录是工作区,初始化工作区 go work init
D:\go\project\ginlearn>go work init
// 2. 创建对应模块,比如这边创建helloworld模块
D:\go\project\ginlearn>mkdir helloworld
D:\go\project\ginlearn>cd helloworld
// 3. 模块初始化 go mod init name (name要求:example.com/m)
D:\go\project\ginlearn\helloworld>go mod init test.com/helloworld
go: creating new go.mod: module test.com/helloworld
D:\go\project\ginlearn\helloworld>cd ..
// 4. 在工作区目录将helloworld模块加入到工作区里
D:\go\project\ginlearn>go work use ./helloworld

1
2
3
4
5
6
7
8
9
10
11
12
TodoList/
├── api
├── cache
├── conf
├── middleware
├── model
├── pkg
│ ├── e
│ └── util
├── routes
├── serializer
└── service
  • api : 用于定义接口函数
  • cache : 放置redis缓存
  • conf : 用于存储配置文件
  • middleware : 应用中间件
  • model : 应用数据库模型
  • pkg/e : 封装错误码
  • pkg/logging : 日志打印
  • pkg/util : 工具函数
  • routes : 路由逻辑处理
  • serializer : 将数据序列化为 json 的函数
  • service : 接口函数的实现

二、项目配置和数据库连接

一些依赖的导入

1
2
3
4
// 导入读取ini的组件:go get gopkg.in/ini.v1
// 导入gorm:go get gorm.io/gorm
// 导入gin: go get github.com/gin-gonic/gin
// 安装MySQL驱动 :go get gorm.io/driver/mysql

一、配置文件:conf/config.ini

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# debug开发模式,release生产模式
[service]
AppMode = debug
HttpPort = :3000
# 运行端口号 3000端口

[redis]
RedisDb = redis
RedisAddr = 127.0.0.1:6379
# redis ip地址和端口号
RedisPw =
# redis 密码
RedisDbName = 2
# redis 名字

[mysql]
Db = mysql
DbHost = 127.0.0.1
# mysql ip地址
DbPort = 3306
# mysql 端口号
DbUser = root
# mysql 用户名
DbPassWord = 123456
# mysql 密码
DbName = todolist
# mysql 名字

conf.go中读取配置文件到变量中,在Init()函数中进行读取

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
// config.conf.go
package config

import (
"gopkg.in/ini.v1"
"log"
"strings"
"todoList.com/todoList/model"
)

// 将配置文件内容读取出来
var (
AppMode string
HttpPort string

RedisDb string
RedisAddr string
RedisPw string
RedisDbName string

Db string
DbHost string
DbPort string
DbUser string
DbPassWord string
DbName string
)

func Init() {
// go get gopkg.in/ini.v1 ; 记得要在todolist包下
// 加载文件
file, err := ini.Load("./config/config.ini")
if err != nil {
log.Println("配置文件读取错误,请检查文件路径", err)
}
LoadServer(file)
LoadMysql(file) // 读取Mysql配置文件
// 将Mysql配置文件传给model.init, 让它去做数据库的连接
// 导入gorm:go get github.com/jinzhu/gorm
path := strings.Join([]string{DbUser, ":", DbPassWord, "@tcp(", DbHost, ":", DbPort, ")/", DbName, "?charset=utf8mb4&parseTime=true"}, "")
model.DataBase(path)
}

// 从file中加载文件信息
func LoadServer(file *ini.File) {
/* config.ini:
# debug开发模式,release生产模式
[service]
AppMode = debug
HttpPort = :3000
*/
// 这句话的意思是选中[service]中的AppMode信息,然后转为string类型赋值给变量
AppMode = file.Section("service").Key("AppMode").String()
HttpPort = file.Section("service").Key("HttpPort").String()
}

func LoadMysql(file *ini.File) {
Db = file.Section("mysql").Key("Db").String()
DbHost = file.Section("mysql").Key("DbHost").String()
DbPort = file.Section("mysql").Key("DbPort").String()
DbUser = file.Section("mysql").Key("DbUser").String()
DbPassWord = file.Section("mysql").Key("DbPassWord").String()
DbName = file.Section("mysql").Key("DbName").String()
}

读取到Mysql的配置文件后,调用model/init.go进行数据库的连接

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
package model

import (
"fmt"
"gorm.io/driver/mysql"
"gorm.io/gorm"
"gorm.io/gorm/logger"
"gorm.io/gorm/schema"
"log"
"time"
)

var DB *gorm.DB

// 根据配置文件进行Mysql连接
func DataBase(conn string) {
fmt.Println(conn)

db, err := gorm.Open(mysql.New(mysql.Config{
DSN: conn, // DSN data source name
DefaultStringSize: 256, // string 类型字段的默认长度
DisableDatetimePrecision: true, // 禁用 datetime 精度,MySQL 5.6 之前的数据库不支持
DontSupportRenameIndex: true, // 重命名索引时采用删除并新建的方式,MySQL 5.7 之前的数据库和 MariaDB 不支持重命名索引
DontSupportRenameColumn: true, // 用 `change` 重命名列,MySQL 8 之前的数据库和 MariaDB 不支持重命名列
SkipInitializeWithVersion: false, // 根据版本自动配置
}), &gorm.Config{ // &gorm.Config添加额外配置
// 打印日志
Logger: logger.Default.LogMode(logger.Info),
NamingStrategy: schema.NamingStrategy{
SingularTable: true, // 表明不加s
},
})
if err != nil {
panic("数据库连接错误")
}
log.Println("数据库连接成功")
// 设置数据库参数
mysqldb, _ := db.DB()
mysqldb.SetMaxIdleConns(20) // 设置连接池
mysqldb.SetMaxOpenConns(100) //最大连接数
mysqldb.SetConnMaxLifetime(time.Second * 30) // 设置最大连接时间
DB = db
migration()
}

三、数据库建表

建立两张表:UserTask

1
2
3
4
5
6
7
8
9
10
package model

import "gorm.io/gorm"

// User 用户模型
type User struct {
gorm.Model // Model会一共ID,CreateAt, UpdateAt, DeleteAt
UserName string `gorm:"unique"`
PasswordDigest string // 存储的是密文,也就是加密后的密码
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package model

import "gorm.io/gorm"

// 任务模型
type Task struct {
gorm.Model
User User `gorm:"ForeignKey:Uid"`
Uid uint `gorm:"not null"`
Title string `gorm:"index;not null"`
Status int `gorm:"default:0"` // 备忘录状态:0 未完成 ; 1 已完成
Content string `gorm:"type:longtext"` // 内容
StartTime int64 // 备忘录开始时间
EndTime int64 `gorm:"default:0"` // 备忘录完成时间
}

新建magirate.go实现自动迁移模式

1
2
3
4
5
6
7
8
9
10
11
12
13
package model

import "log"

func migration() {
// 自动迁移模式
err := DB.Set("gorm:table_options", "charset=utf8mb4").
AutoMigrate(&User{}, &Task{})
if err != nil {
log.Println("表迁移失败")
return
}
}

然后在model.init.go中创建完DB后进行调用迁移方法

四、用户注册与登录

依赖配置:

1
2
3
4
5
6
// 导入session相关的依赖
go get github.com/gin-contrib/sessions
// 导入对用户密码加密的包
go get golang.org/x/crypto/bcrypt
// 导入生成JWT令牌的包
go get github.com/dgrijalva/jwt-go

1、用户注册功能

①先编写全局路由

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// routes.routes.go
package routes

import (
"github.com/gin-contrib/sessions"
"github.com/gin-contrib/sessions/cookie"
"github.com/gin-gonic/gin"
"todoList.com/todoList/api"
)

// 路由配置
func NewRouter() *gin.Engine {
r := gin.Default() //生成了一个WSGI应用程序实例
store := cookie.NewStore([]byte("something-very-secret"))
r.Use(sessions.Sessions("mysession", store))
// 编写分组路由
v1 := r.Group("/api/v1")
{
v1.POST("user/register", api.UserRegister)
}
return r
}

②然后再api包下建一个user.go的Controller层处理这个请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package api

import (
"github.com/gin-gonic/gin"
"todoList.com/todoList/service"
)

// 接收的是gin的上下文:*gin.Context
func UserRegister(ctx *gin.Context) {
//相当于创建了一个UserRegisterService对象,调用这个对象中的Register方法。
var userRegisterService service.UserService
// shouldBind:上下文对UserService对象进行了绑定
if err := ctx.ShouldBind(&userRegisterService); err == nil {
// 绑定没有失败的话,调用service中的方法进行注册
res := userRegisterService.Register()
ctx.JSON(200, res)
} else {
ctx.JSON(400, err)
}
}

③再service.go中实现这个被调用的方法userRegisterService.Register()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
package service

import (
"todoList.com/todoList/model"
"todoList.com/todoList/serializer"
)

// UserService 用户注册服务(用于gin上下文进行参数绑定的类)
type UserService struct {
UserName string `form:"user_name" json:"user_name" binding:"required,min=3,max=15" example:"FanOne"`
Password string `form:"password" json:"password" binding:"required,min=5,max=16" example:"FanOne666"`
}

// 用户注册的方法
// 返回值是json,所以调用了序列化包下的通用接口
func (service *UserService) Register() serializer.Response {
var user model.User
var count int64
// 根据绑定的用户名查找数据库是否有相同的,取一个放到user中
model.DB.Model(&model.User{}).Where("user_name = ?", service.UserName).
First(&user).Count(&count)
if count == 1 {
// 说明已经存在该用户名了,返回注册失败
return serializer.Response{
Status: 400,
Msg: "数据库已存在该用户名!",
}
}
user.UserName = service.UserName
// 对用户的密码进行加密
if err := user.SetPassword(service.Password); err != nil {
return serializer.Response{ // 用户密码加密错误,返回注册失败
Status: 400,
Msg: "用户名密码加密错误" + err.Error(),
}
}

// 往表中插入用户信息
if err := model.DB.Create(&user).Error; err != nil {
return serializer.Response{ // 插入数据库错误,返回注册失败
Status: 400,
Msg: "数据库操作错误" + err.Error(),
}
}
// 返回注册成功响应
return serializer.Response{
Status: 200,
Msg: "用户注册成功!",
}
}

返回类型是一个通用返回类Response

1
2
3
4
5
6
7
8
9
10
11
// serializer.common.go
package serializer

// 基础序列化器
type Response struct {
Status int `json:"status"`
Data interface{} `json:"data"`
Msg string `json:"msg"`
Error string `json:"error"`
}

中间涉及密码的加密user.SetPassword(service.Password):写在Model.user.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 设置密码
func (user *User) SetPassword(password string) error {
// 传入类型是[]byte, 和一个加密难度
bytes, err := bcrypt.GenerateFromPassword([]byte(password), 12)
if err != nil {
return err
}
user.PasswordDigest = string(bytes)
return nil
}

// 校验密码
func (user *User) CheckPassword(password string) bool {
err := bcrypt.CompareHashAndPassword([]byte(user.PasswordDigest), []byte(password))
return err == nil
}

④最后在main.go主函数中启动gin服务,并用postman进行测试

1
2
3
4
5
6
7
8
9
10
11
12
13
package main

import (
"todoList.com/todoList/config"
"todoList.com/todoList/routes"
)

func main() {
config.Init()
r := routes.NewRouter()
_ = r.Run(config.HttpPort) // 运行在 配置文件设置的端口上
}

运行结果


2、用户登录

和上面用户注册流程一样

①完善路由

1
v1.POST("user/login", api.UserLogin)

api.user.go中实现路由方法

1
2
3
4
5
6
7
8
9
10
func UserLogin(ctx *gin.Context) {
var userLogin service.UserService
// 将上下文数据传到用户类中
if err := ctx.ShouldBind(&userLogin); err == nil {
res := userLogin.Login()
ctx.JSON(200, res)
} else {
ctx.JSON(400, err)
}
}

service.user.go中实现用户登录方法userLogin.Login()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
func (service *UserService) Login() serializer.Response {
var user model.User
// 1. 在数据库中查看有没有用户
if err := model.DB.Where("user_name = ?", service.UserName).
First(&user).Error; err != nil {
// 当 First、Last、Take 方法找不到记录时,GORM 会返回 ErrRecordNotFound 错误,
// 可以通过对比gorm.ErrRecordNotFound进行判断,或者使用Find和Limit的组合进行查询。
if err == gorm.ErrRecordNotFound {
return serializer.Response{
Status: 400,
Msg: "用户不存在,请先注册",
}
}
// 如果不是用户不存在,是其他不可抗拒的因素导致的错误
return serializer.Response{
Status: 400,
Msg: "数据库错误",
}
}
// 2. 用户存在,验证密码是否想用
if user.CheckPassword(service.Password) == false {
return serializer.Response{
Status: 400,
Msg: "密码错误",
}
}
// 3. 用户验证通过
// 发一个token给浏览器,为了其他功能需要身份验证所给请前端存储的
// 例如:创建一个备忘录,就需要知道这是哪一个用户创建的备忘录
token, err := util.GenerateToken(user.ID, service.UserName, 0)
if err != nil {
return serializer.Response{
Status: 500,
Msg: "Token签名分发错误",
}
}
return serializer.Response{
Status: 200,
Data: serializer.TokenData{User: serializer.BuildUser(user), Token: token},
Msg: "登录成功",
}
}

中间涉及token的创建和返回Response中绑定token

①token的生成与验证

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
// util.utils.go
package util

import (
"github.com/dgrijalva/jwt-go"
"time"
)

var JWTsecret = []byte("ABAB")

// 加密的数据(签名)
type Claims struct {
Id uint `json:"id"`
Username string `json:"username"`
Authority int `json:"authority"`
jwt.StandardClaims
}

// GenerateToken 签发用户Token
func GenerateToken(id uint, username string, authority int) (string, error) {
// 设置token过期时间
nowTime := time.Now()
expireTime := nowTime.Add(24 * time.Hour)
claims := Claims{
Id: id,
Username: username,
Authority: authority,
StandardClaims: jwt.StandardClaims{
ExpiresAt: expireTime.Unix(),
Issuer: "to-do-list",
},
}
tokenClaims := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
token, err := tokenClaims.SignedString(JWTsecret)
return token, err
}

// ParseToken 验证用户token
func ParseToken(token string) (*Claims, error) {
tokenClaims, err := jwt.ParseWithClaims(token, &Claims{}, func(token *jwt.Token) (interface{}, error) {
return JWTsecret, nil
})
if tokenClaims != nil {
if claims, ok := tokenClaims.Claims.(*Claims); ok && tokenClaims.Valid {
return claims, nil
}
}
return nil, err
}

②返回结果中绑定token

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//Data:   serializer.TokenData{User: serializer.BuildUser(user), Token: token},
// serializer.user.go
package serializer

import "todoList.com/todoList/model"

type User struct {
ID uint `json:"id" form:"id" example:"1"` // 用户ID
UserName string `json:"user_name" form:"user_name" example:"FanOne"` // 用户名
CreateAt int64 `json:"create_at" form:"create_at"` // 创建
}

// BuildUser 序列化用户
func BuildUser(user model.User) User {
return User{
ID: user.ID,
UserName: user.UserName,
CreateAt: user.CreatedAt.Unix(),
}
}

运行结果

五、JWT中间件

随着前后端分离的发展,以及数据中心的建立,越来越多的公司会创建一个中心服务器,服务于各种产品线。

而这些产品线上的产品,它们可能有着各种终端设备,包括但不仅限于浏览器、桌面应用、移动端应用、平板应用、甚至智能家居

实际上,不同的产品线通常有自己的服务器,产品内部的数据一般和自己的服务器交互。

但中心服务器仍然有必要存在,因为同一家公司的产品总是会存在共享的数据,比如用户数据

这些设备与中心服务器之间会进行http通信

一般来说,中心服务器至少承担着认证和授权的功能,例如登录:各种设备发送消息到中心服务器,然后中心服务器响应一个身份令牌

当这种结构出现后,就出现一个问题:它们之间还能使用传统的cookie方式传递令牌信息吗?

其实,也是可以的,因为cookie在传输中无非是一个消息头而已,只不过浏览器对这个消息头有特殊处理罢了。

但浏览器之外的设备肯定不喜欢cookie,因为浏览器有着对cookie完善的管理机制,但是在其他设备上,就需要开发者自己手动处理了

jwt的出现就是为了解决这个问题


jwt全称Json Web Token,强行翻译过来就是json格式的互联网令牌

它要解决的问题,就是为多种终端设备,提供统一的、安全的令牌格式

jwt只是一个令牌格式而已,你可以把它存储到cookie,也可以存储到localstorage,没有任何限制!

同样的,对于传输,你可以使用任何传输方式来传输jwt,一般来说,我们会使用消息头来传输它

比如,当登录成功后,服务器可以给客户端响应一个jwt:

1
2
3
4
5
6
7
HTTP/1.1 200 OK
...
set-cookie:token=jwt令牌
authorization:jwt令牌
...

{..., token:jwt令牌}

可以看到,jwt令牌可以出现在响应的任何一个地方,客户端和服务器自行约定即可。

当然,它也可以出现在响应的多个地方,比如为了充分利用浏览器的cookie,同时为了照顾其他设备,也可以让jwt出现在set-cookie和authorization或body中,尽管这会增加额外的传输量。

当客户端拿到令牌后,它要做的只有一件事:存储它。

你可以存储到任何位置,比如手机文件、PC文件、localstorage、cookie

当后续请求发生时,你只需要将它作为请求的一部分发送到服务器即可。

虽然jwt没有明确要求应该如何附带到请求中,但通常我们会使用如下的格式:

1
2
3
4
GET /api/resources HTTP/1.1
...
authorization: bearer jwt令牌
...

引入JWT模块,要实现用户身份认证

① 在routes.routes.go中进行请求的拦截

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package routes

import (
"github.com/gin-contrib/sessions"
"github.com/gin-contrib/sessions/cookie"
"github.com/gin-gonic/gin"
"todoList.com/todoList/api"
"todoList.com/todoList/middleware"
)

// 路由配置
func NewRouter() *gin.Engine {
r := gin.Default() //生成了一个WSGI应用程序实例
store := cookie.NewStore([]byte("something-very-secret"))
r.Use(sessions.Sessions("mysession", store))
// 编写分组路由
v1 := r.Group("/api/v1")
{
v1.POST("user/register", api.UserRegister)
v1.POST("user/login", api.UserLogin)
/////////////////////////////////////////
authed := v1.Group("/")
// 进行权限验证,验证通过才有权限进行访问下面的路由
authed.Use(middleware.JWT())
{
authed.POST("task", api.CreateTask)
}
}
return r
}

JWT()实现的具体方法:验证Token

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// middleware.JWT()
package middleware

import (
"github.com/gin-gonic/gin"
"time"
"todoList.com/todoList/pkg/util"
)

func JWT() gin.HandlerFunc {
return func(c *gin.Context) {
var code int
token := c.GetHeader("Authorization")
if token == "" {
code = 400 // 返回400, 因为传入的token不对
} else {
// 对token进行解析
claim, err := util.ParseToken(token)
if err != nil {
code = 403 // forbid, token解析有误,无权限,是假的
} else if time.Now().Unix() > claim.ExpiresAt {
code = 401 // Token无效,已经国企了
}
}
if code != 200 {
c.JSON(400, gin.H{
"status": code,
"msg": "Token解析错误",
})
c.Abort()
return
}
c.Next()
}
}

六、创建备忘录(增)

创建过程和user的创建过程相同

① routers.routers.go

1
2
3
4
authed.Use(middleware.JWT()) // 进行权限验证,验证通过才有权限进行访问下面的路由
{
authed.POST("task", api.CreateTask)
}

api.tasks.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package api

import (
"github.com/gin-gonic/gin"
"log"
"todoList.com/todoList/pkg/util"
"todoList.com/todoList/service"
)

func CreateTask(c *gin.Context) {
var createTaskservice service.CreateTaskService
// 进行身份的验证
claim, _ := util.ParseToken(c.GetHeader("Authorization"))

if err := c.ShouldBind(&createTaskservice); err == nil {
res := createTaskservice.Create(claim.Id)
c.JSON(200, res)
} else {
c.JSON(400, err)
log.Println("createTask err : ", err)
}
}

service.task.go创建一个接收参数的服务类,然后实现Create方法

这里犯了一个错误: 在给结构体属性加tag的时候json:"title":中间不能有空格,也就是tag部分不能是灰色的,不然属性就赋值不上,而且err还是为nil

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
package service

import (
"time"
"todoList.com/todoList/model"
"todoList.com/todoList/serializer"
)

type CreateTaskService struct {
Title string `json:"title" form:"title"`
Content string `json:"content" form:"content"`
Status int `json:"status" form:"status"` // 0未做 1已做
}

func (service *CreateTaskService) Create(id uint) serializer.Response {
var user model.User
model.DB.First(&user, id)
task := model.Task{
User: user,
Uid: user.ID,
Title: service.Title,
Content: service.Content,
Status: 0,
StartTime: time.Now().Unix(),
}
code := 200
err := model.DB.Create(&task).Error
if err != nil {
code = 500
return serializer.Response{
Status: code,
Msg: "创建备忘录失败",
Error: err.Error(),
}
}
return serializer.Response{
Status: code,
Data: serializer.BuildTask(task),
Msg: "创建备忘录成功",
}
}

运行结果

  • 请求头中要设置Authorization:{{token}},其中tokenpostman中设置的环境变量,是用户登录成功后返回给浏览器的token

错误的结果:titlecontent没有内容,gin框架没有成功对结构体进行赋值


七、展示一条备忘录(查)

① 建立路由:routes.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
func NewRouter() *gin.Engine {
r := gin.Default() //生成了一个WSGI应用程序实例
store := cookie.NewStore([]byte("something-very-secret"))
r.Use(sessions.Sessions("mysession", store))
// 编写分组路由
v1 := r.Group("/api/v1")
{
v1.POST("user/register", api.UserRegister)
v1.POST("user/login", api.UserLogin)
authed := v1.Group("/")
authed.Use(middleware.JWT()) // 进行权限验证,验证通过才有权限进行访问下面的路由
{
authed.POST("task", api.CreateTask)
//////////////////////////
// 这一条
//////////////////////////
authed.GET("task/:id", api.GetTaskById)
}
}
return r
}

②实现处理路由的方法 api.tasks.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func GetTaskById(c *gin.Context) {
var GetTaskservice service.ShowTaskService
// 进行身份的验证
claim, _ := util.ParseToken(c.GetHeader("Authorization"))

if err := c.ShouldBind(&GetTaskservice); err == nil {
// claim.Id是token中取到的用户id, c.Param("id")是前端传递的参数:备忘录id
res := GetTaskservice.GetById(claim.Id, c.Param("id"))
c.JSON(200, res)
} else {
c.JSON(400, err)
log.Println("createTask err : ", err)
}
}

③实现一个GetTaskservice服务类,然后定义该类的方法,该类中没有任何属性(因为是get请求)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
type ShowTaskService struct {
// Get请求,因此这个服务内容是空的
}

func (service *ShowTaskService) GetById(uid uint, tid string) serializer.Response {
// 1.查询到备忘录信息
var task model.Task
code := 200
err := model.DB.First(&task, tid).Error
if err != nil {
code = 500
return serializer.Response{
Status: code,
Msg: "查询备忘录失败",
}
}
// 如果查询的备忘录不是当前用户的,返回查询失败
if task.Uid != uid {
code = 500
return serializer.Response{
Status: code,
Msg: "未查询到当前用户的备忘录信息",
}
}
return serializer.Response{
Status: code,
Data: serializer.BuildTask(task),
Msg: "查询成功",
}
}

涉及到一个序列化 Data: serializer.BuildTask(task),

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package serializer

import "todoList.com/todoList/model"

type Task struct {
ID uint `json:"id" example:"1"` // 任务ID
Title string `json:"title" example:"吃饭"` // 题目
Content string `json:"content" example:"睡觉"` // 内容
// View uint64 `json:"view" example:"32"` // 浏览量
Status int `json:"status" example:"0"` // 状态(0未完成,1已完成)
CreatedAt int64 `json:"created_at"`
StartTime int64 `json:"start_time"`
EndTime int64 `json:"end_time"`
}

func BuildTask(item model.Task) Task {
return Task{
ID: item.ID,
Title: item.Title,
Content: item.Content,
Status: item.Status,
// View: item.View(),
CreatedAt: item.CreatedAt.Unix(),
StartTime: item.StartTime,
EndTime: item.EndTime,
}
}

八、展示所有的备忘录(查)

routes.go写路由

1
authed.GET("tasks", api.GetAllTaskById)

api.tasks.go 写处理函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func GetAllTaskById(c *gin.Context) {
var getListTaskService service.GetListTaskService
// 进行身份的验证
claim, _ := util.ParseToken(c.GetHeader("Authorization"))
// 绑定pagesize 和 pagenum 参数
if err := c.ShouldBind(&getListTaskService); err == nil {
res := getListTaskService.GetAllById(claim.Id)
c.JSON(200, res)
} else {
c.JSON(400, err)
log.Println("GetAllTaskById err : ", err)
}
}

③ service.task.go 实现查询服务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
type GetListTaskService struct {
PageNum int `json:"page_num" form:"page_num"`
PageSize int `json:"page_size" form:"page_size"`
}

// 展示用户的所有备忘录
func (service *GetListTaskService) GetAllById(uid uint) serializer.Response {
var tasks []model.Task
var count int64 = 0
if service.PageSize == 0 {
service.PageSize = 15 // 如果传来的页面大小是0,默认为15
}
// 多表查询
// 先找到是哪一个user,聚合函数查看一共多少条,然后进行分页操作
model.DB.Model(&model.Task{}).Preload("User").Where("uid = ?", uid).Count(&count).
Limit(service.PageSize).Offset((service.PageNum - 1) * service.PageSize).Find(&tasks)
// 返回带数量的列表响应
return serializer.BuildListResponse(serializer.BuildTasks(tasks), uint(count))
}

涉及到带数量的列表响应

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//serializer.BuildListResponse:
// BulidListResponse 带有总数的列表构建器
func BuildListResponse(items interface{}, total uint) Response {
return Response{
Status: 200,
Data: DataList{
Item: items,
Total: total,
},
Msg: "ok",
}
}

// serializer.BuildTasks(tasks)
func BuildTasks(items []model.Task) (tasks []Task) {
for _, item := range items {
task := BuildTask(item)
tasks = append(tasks, task)
}
return tasks
}

运行结果


九、更新备忘录(改)

和前面相同的步骤

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
//routes.go
authed.PUT("task/:id", api.UpdateTaskById)

// api.tasks.go
func UpdateTaskById(c *gin.Context) {
var updateTaskservice service.UpdateTaskService
if err := c.ShouldBind(&updateTaskservice); err == nil {
res := updateTaskservice.UpdateById(c.Param("id"))
c.JSON(200, res)
} else {
c.JSON(400, err)
log.Println("createTask err : ", err)
}
}

// service.task.go
type UpdateTaskService struct {
Title string `json:"title" form:"title"`
Content string `json:"content" form:"content"`
Status int `json:"status" form:"status"` // 0未做 1已做
}

func (service *UpdateTaskService) UpdateById(id string) serializer.Response {
var task model.Task
model.DB.First(&task, id)
task.Content = service.Content
task.Title = service.Title
task.Status = service.Status
err := model.DB.Save(&task).Error
if err != nil {
return serializer.Response{
Status: 400,
Msg: "更新失败",
}
}
return serializer.Response{
Status: 200,
Msg: "更新成功",
}
}

执行结果:


十、查询备忘录(查)

和查询单条备忘录差不多,主要这便是模糊查询

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// routes.go
authed.POST("search", api.SearchTask)

// api.task.go
func SearchTask(c *gin.Context) {
var searchTaskservice service.SearchTaskService
// 进行身份的验证
claim, _ := util.ParseToken(c.GetHeader("Authorization"))

if err := c.ShouldBind(&searchTaskservice); err == nil {
// claim.Id是token中取到的用户id, c.Param("id")是前端传递的参数:备忘录id
res := searchTaskservice.Search(claim.Id)
c.JSON(200, res)
} else {
c.JSON(400, err)
log.Println("SearchTask err : ", err)
}
}

// service.tasks.go
type SearchTaskService struct {
Info string `json:"info" form:"info"`
PageNum int `json:"page_num" form:"page_num"`
PageSize int `json:"page_size" form:"page_size"`
}

func (service *SearchTaskService) Search(id uint) serializer.Response {
if service.PageSize == 0 {
service.PageSize = 15
}
var tasks []model.Task
var count int64
//1. 先预加载用户
model.DB.Model(&model.Task{}).Preload("User").Where("uid = ?", id).
// 在用户的基础上搜索内容或者标题
Where("title Like ? OR content like ?", "%"+service.Info+"%", "%"+service.Info+"%").
// 计数 + 分页后赋值到tasks里
Count(&count).Limit(service.PageSize).Offset((service.PageNum - 1) * service.PageSize).Find(&tasks)
return serializer.BuildListResponse(serializer.BuildTasks(tasks), uint(count))
}

执行结果:


十一、删除备忘录(删)

和前面一样

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
// 路由配置
func NewRouter() *gin.Engine {
r := gin.Default() //生成了一个WSGI应用程序实例
store := cookie.NewStore([]byte("something-very-secret"))
r.Use(sessions.Sessions("mysession", store))
// 编写分组路由
v1 := r.Group("/api/v1")
{
v1.POST("user/register", api.UserRegister)
v1.POST("user/login", api.UserLogin)
authed := v1.Group("/")
authed.Use(middleware.JWT()) // 进行权限验证,验证通过才有权限进行访问下面的路由
{
authed.POST("task", api.CreateTask)
authed.GET("task/:id", api.GetTaskById)
authed.GET("tasks", api.GetAllTaskById)
authed.PUT("task/:id", api.UpdateTaskById)
authed.POST("search", api.SearchTask)
authed.DELETE("task/:id", api.DeleteTask)
}
}
return r
}


//api.task.go
func DeleteTask(c *gin.Context) {
var deleteTaskservice service.DeleteTaskService

if err := c.ShouldBind(&deleteTaskservice); err == nil {
res := deleteTaskservice.Delete(c.Param("id"))
c.JSON(200, res)
} else {
c.JSON(400, err)
log.Println("DeleteTask err : ", err)
}

}

// service.task.go
func (service *DeleteTaskService) Delete(id string) serializer.Response {
var task model.Task
err := model.DB.Delete(&task, id).Error
if err != nil {
return serializer.Response{
Status: 400,
Msg: "删除失败",
}
}
return serializer.Response{
Status: 200,
Msg: "删除成功",
}
}