├── public
├── favicon.ico
├── images
│ ├── 1_1.png
│ ├── 1_2.png
│ ├── 2_1.png
│ ├── 3_1.png
│ ├── 3_2.png
│ ├── 4_1.png
│ ├── 4_2.png
│ ├── 5_1.png
│ ├── 5_2.png
│ └── 6_1.png
├── videos
│ ├── 1_1.mp4
│ ├── 1_2.mp4
│ ├── 2_1.mp4
│ ├── 3_1.mp4
│ ├── 3_2.mp4
│ ├── 4_1.mp4
│ ├── 4_2.mp4
│ ├── 5_1.mp4
│ ├── 5_2.mp4
│ └── 6_1.mp4
└── Initdata
│ ├── avatar
│ ├── 1.png
│ ├── 2.png
│ ├── 3.png
│ ├── 4.png
│ ├── 5.png
│ ├── 6.png
│ └── default.png
│ └── background
│ ├── 1.png
│ ├── 2.png
│ ├── 3.png
│ ├── 4.png
│ ├── 5.png
│ ├── 6.png
│ └── default.png
├── asset
├── images
│ ├── logo.jpg
│ ├── show.png
│ ├── bottom.jpg
│ ├── database.png
│ └── architecture.png
└── document
│ ├── Middleware.md
│ ├── Layered_Architecture.md
│ └── Database_Design.md
├── config.yaml
├── utils
├── ffmpeg.go
├── encrypt.go
├── startTime.go
└── videoPublish.go
├── models
├── relation_model.go
├── favorite_model.go
├── message_model.go
├── comment_model.go
├── video_model.go
├── user_model.go
└── dao.go
├── config
├── config.go
└── default_settings.go
├── .gitignore
├── setup
├── models_init.go
├── gorm_init.go
├── config_init.go
└── sampledata_init.go
├── controllers
├── video
│ ├── publish.go
│ ├── publishList.go
│ └── feed.go
├── user
│ ├── register.go
│ ├── user.go
│ └── login.go
├── message
│ ├── messageAction.go
│ └── messageChat.go
├── relation
│ ├── relationAction.go
│ ├── friendList.go
│ ├── followlist.go
│ └── followerList.go
├── favorite
│ ├── favoriteAction.go
│ └── favoriteList.go
├── response
│ ├── shared_resp.go
│ ├── favorite_resp.go
│ ├── message_resp.go
│ ├── comment_resp.go
│ ├── video_resp.go
│ ├── relation_resp.go
│ └── user_resp.go
└── comment
│ ├── commentAction.go
│ └── commentlist.go
├── main.go
├── LICENSE
├── services
├── message_service.go
├── user_service.go
├── comment_service.go
├── video_service.go
├── favorite_service.go
└── relation_service.go
├── router.go
├── test
├── test_helpers.go
├── social_api_test.go
├── base_api_test.go
└── interact_api_test.go
├── middlewares
└── jwt.go
├── go.mod
├── README.md
└── go.sum
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zheng-yi-yi/SimpleDouyin/HEAD/public/favicon.ico
--------------------------------------------------------------------------------
/asset/images/logo.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zheng-yi-yi/SimpleDouyin/HEAD/asset/images/logo.jpg
--------------------------------------------------------------------------------
/asset/images/show.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zheng-yi-yi/SimpleDouyin/HEAD/asset/images/show.png
--------------------------------------------------------------------------------
/public/images/1_1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zheng-yi-yi/SimpleDouyin/HEAD/public/images/1_1.png
--------------------------------------------------------------------------------
/public/images/1_2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zheng-yi-yi/SimpleDouyin/HEAD/public/images/1_2.png
--------------------------------------------------------------------------------
/public/images/2_1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zheng-yi-yi/SimpleDouyin/HEAD/public/images/2_1.png
--------------------------------------------------------------------------------
/public/images/3_1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zheng-yi-yi/SimpleDouyin/HEAD/public/images/3_1.png
--------------------------------------------------------------------------------
/public/images/3_2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zheng-yi-yi/SimpleDouyin/HEAD/public/images/3_2.png
--------------------------------------------------------------------------------
/public/images/4_1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zheng-yi-yi/SimpleDouyin/HEAD/public/images/4_1.png
--------------------------------------------------------------------------------
/public/images/4_2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zheng-yi-yi/SimpleDouyin/HEAD/public/images/4_2.png
--------------------------------------------------------------------------------
/public/images/5_1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zheng-yi-yi/SimpleDouyin/HEAD/public/images/5_1.png
--------------------------------------------------------------------------------
/public/images/5_2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zheng-yi-yi/SimpleDouyin/HEAD/public/images/5_2.png
--------------------------------------------------------------------------------
/public/images/6_1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zheng-yi-yi/SimpleDouyin/HEAD/public/images/6_1.png
--------------------------------------------------------------------------------
/public/videos/1_1.mp4:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zheng-yi-yi/SimpleDouyin/HEAD/public/videos/1_1.mp4
--------------------------------------------------------------------------------
/public/videos/1_2.mp4:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zheng-yi-yi/SimpleDouyin/HEAD/public/videos/1_2.mp4
--------------------------------------------------------------------------------
/public/videos/2_1.mp4:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zheng-yi-yi/SimpleDouyin/HEAD/public/videos/2_1.mp4
--------------------------------------------------------------------------------
/public/videos/3_1.mp4:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zheng-yi-yi/SimpleDouyin/HEAD/public/videos/3_1.mp4
--------------------------------------------------------------------------------
/public/videos/3_2.mp4:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zheng-yi-yi/SimpleDouyin/HEAD/public/videos/3_2.mp4
--------------------------------------------------------------------------------
/public/videos/4_1.mp4:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zheng-yi-yi/SimpleDouyin/HEAD/public/videos/4_1.mp4
--------------------------------------------------------------------------------
/public/videos/4_2.mp4:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zheng-yi-yi/SimpleDouyin/HEAD/public/videos/4_2.mp4
--------------------------------------------------------------------------------
/public/videos/5_1.mp4:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zheng-yi-yi/SimpleDouyin/HEAD/public/videos/5_1.mp4
--------------------------------------------------------------------------------
/public/videos/5_2.mp4:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zheng-yi-yi/SimpleDouyin/HEAD/public/videos/5_2.mp4
--------------------------------------------------------------------------------
/public/videos/6_1.mp4:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zheng-yi-yi/SimpleDouyin/HEAD/public/videos/6_1.mp4
--------------------------------------------------------------------------------
/asset/images/bottom.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zheng-yi-yi/SimpleDouyin/HEAD/asset/images/bottom.jpg
--------------------------------------------------------------------------------
/asset/images/database.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zheng-yi-yi/SimpleDouyin/HEAD/asset/images/database.png
--------------------------------------------------------------------------------
/asset/images/architecture.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zheng-yi-yi/SimpleDouyin/HEAD/asset/images/architecture.png
--------------------------------------------------------------------------------
/public/Initdata/avatar/1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zheng-yi-yi/SimpleDouyin/HEAD/public/Initdata/avatar/1.png
--------------------------------------------------------------------------------
/public/Initdata/avatar/2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zheng-yi-yi/SimpleDouyin/HEAD/public/Initdata/avatar/2.png
--------------------------------------------------------------------------------
/public/Initdata/avatar/3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zheng-yi-yi/SimpleDouyin/HEAD/public/Initdata/avatar/3.png
--------------------------------------------------------------------------------
/public/Initdata/avatar/4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zheng-yi-yi/SimpleDouyin/HEAD/public/Initdata/avatar/4.png
--------------------------------------------------------------------------------
/public/Initdata/avatar/5.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zheng-yi-yi/SimpleDouyin/HEAD/public/Initdata/avatar/5.png
--------------------------------------------------------------------------------
/public/Initdata/avatar/6.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zheng-yi-yi/SimpleDouyin/HEAD/public/Initdata/avatar/6.png
--------------------------------------------------------------------------------
/public/Initdata/avatar/default.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zheng-yi-yi/SimpleDouyin/HEAD/public/Initdata/avatar/default.png
--------------------------------------------------------------------------------
/public/Initdata/background/1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zheng-yi-yi/SimpleDouyin/HEAD/public/Initdata/background/1.png
--------------------------------------------------------------------------------
/public/Initdata/background/2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zheng-yi-yi/SimpleDouyin/HEAD/public/Initdata/background/2.png
--------------------------------------------------------------------------------
/public/Initdata/background/3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zheng-yi-yi/SimpleDouyin/HEAD/public/Initdata/background/3.png
--------------------------------------------------------------------------------
/public/Initdata/background/4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zheng-yi-yi/SimpleDouyin/HEAD/public/Initdata/background/4.png
--------------------------------------------------------------------------------
/public/Initdata/background/5.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zheng-yi-yi/SimpleDouyin/HEAD/public/Initdata/background/5.png
--------------------------------------------------------------------------------
/public/Initdata/background/6.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zheng-yi-yi/SimpleDouyin/HEAD/public/Initdata/background/6.png
--------------------------------------------------------------------------------
/public/Initdata/background/default.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zheng-yi-yi/SimpleDouyin/HEAD/public/Initdata/background/default.png
--------------------------------------------------------------------------------
/config.yaml:
--------------------------------------------------------------------------------
1 | # MySQL 配置
2 | mysql:
3 | # 主机
4 | host: 127.0.0.1
5 | # 端口
6 | port: 3306
7 | # 用户名(如 root)
8 | username: root
9 | # 密码
10 | password:
11 | # 数据库名称
12 | db_name: douyin
13 |
--------------------------------------------------------------------------------
/utils/ffmpeg.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import (
4 | "os/exec"
5 | )
6 |
7 | func Ffmpeg(videoPath, outputPath string) error {
8 | cmd := "ffmpeg -i " + videoPath + " -f image2 -t 0.001 " + outputPath
9 | err := exec.Command("cmd", "/c", cmd).Run()
10 | return err
11 | }
12 |
--------------------------------------------------------------------------------
/models/relation_model.go:
--------------------------------------------------------------------------------
1 | package models
2 |
3 | // Relation, 关注关系表
4 | type Relation struct {
5 | ID uint `gorm:"primarykey"`
6 | FromUserId uint `gorm:"not null; comment: 用户id; type:INT"`
7 | ToUserId uint `gorm:"not null; comment: 关注的用户; type:INT"`
8 | Cancel uint `gorm:"not null; comment: 默认关注为0,取消关注为1; type:INT"`
9 | }
10 |
--------------------------------------------------------------------------------
/models/favorite_model.go:
--------------------------------------------------------------------------------
1 | package models
2 |
3 | // Favorite, 点赞表
4 | type Favorite struct {
5 | ID uint `gorm:"primarykey"`
6 | UserId uint `gorm:"not null; comment:用户ID; type:INT"`
7 | VideoId uint `gorm:"not null; comment:视频ID; type:INT"`
8 | // 定义外键关系
9 | User User `gorm:"foreignKey:UserId; references:ID; comment:点赞用户的信息"`
10 | Video Video `gorm:"foreignKey:VideoId; references:ID; comment:点赞视频的信息"`
11 | }
12 |
--------------------------------------------------------------------------------
/models/message_model.go:
--------------------------------------------------------------------------------
1 | package models
2 |
3 | import (
4 | "time"
5 | )
6 |
7 | // Message, 消息表
8 | type Message struct {
9 | ID uint `gorm:"primaryKey comment:消息id"`
10 | FromUserID uint `gorm:"not null comment:消息发送者id; type:INT"`
11 | ToUserID uint `gorm:"not null comment:消息接收者id; type:INT"`
12 | Content string `gorm:"not null comment:消息内容; type:TEXT"`
13 | CreateTime time.Time `gorm:"not null comment:消息发送时间; type:DATETIME"`
14 | }
15 |
--------------------------------------------------------------------------------
/config/config.go:
--------------------------------------------------------------------------------
1 | package config
2 |
3 | import "gorm.io/gorm"
4 |
5 | const (
6 | LOCAL_IP_ADDRESS = "xxx.xxx.xxx.xxx" // 填入本机 IP 地址
7 | VIDEO_STREAM_BATCH_SIZE = 30 // 每次获取视频流的数量限制
8 | DATETIME_FORMAT = "2006-01-02 15:04:05" // 固定的时间格式
9 | SHORT_DATE_FORMAT = "01-02" // 短日期格式的字符串
10 | AUTH_KEY = "Douyin" // JWT 密钥
11 | )
12 |
13 | var (
14 | Database *gorm.DB // Database: 全局变量,用于保存数据库连接实例
15 | GormConfig *gorm.Config // GormConfig: 全局变量,用于保存 GORM 库的配置选项
16 | )
17 |
--------------------------------------------------------------------------------
/utils/encrypt.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import (
4 | "golang.org/x/crypto/bcrypt"
5 | )
6 |
7 | const CustomCost int = 8
8 |
9 | // EncryptPassword 生成密码哈希,使用自定义的哈希迭代成本(cost)
10 | func EncryptPassword(password string) ([]byte, error) {
11 | return bcrypt.GenerateFromPassword([]byte(password), CustomCost)
12 | }
13 |
14 | // CheckPasswordValidity 将加密后的密码和用户提供的密码进行比较
15 | func CheckPasswordValidity(encryptedPassword, userPassword string) bool {
16 | err := bcrypt.CompareHashAndPassword([]byte(encryptedPassword), []byte(userPassword))
17 | return err == nil
18 | }
19 |
--------------------------------------------------------------------------------
/utils/startTime.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import (
4 | "strconv"
5 | "time"
6 |
7 | "github.com/zheng-yi-yi/simpledouyin/config"
8 | )
9 |
10 | // 返回一个格式化的时间字符串。
11 | func CalculateStartTime(lastTimestamp string) string {
12 | if lastTimestamp != "" {
13 | timestamp, err := strconv.ParseInt(lastTimestamp, 10, 64)
14 | if err == nil {
15 | if timestamp > 1000000000000 {
16 | timestamp /= 1000
17 | }
18 | return time.Unix(timestamp, 0).Format(config.DATETIME_FORMAT)
19 | }
20 | }
21 | return time.Now().Format(config.DATETIME_FORMAT)
22 | }
23 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # If you prefer the allow list template instead of the deny list, see community template:
2 | # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
3 | #
4 | # Binaries for programs and plugins
5 | *.exe
6 | *.exe~
7 | *.dll
8 | *.so
9 | *.dylib
10 |
11 | # Test binary, built with `go test -c`
12 | *.test
13 |
14 | # Output of the go coverage tool, specifically when used with LiteIDE
15 | *.out
16 |
17 | # Dependency directories (remove the comment below to include it)
18 | # vendor/
19 |
20 | # Go workspace file
21 | go.work
22 |
23 | .idea
--------------------------------------------------------------------------------
/models/comment_model.go:
--------------------------------------------------------------------------------
1 | package models
2 |
3 | import "time"
4 |
5 | // Comment. 评论表
6 | type Comment struct {
7 | ID uint `gorm:"primarykey; comment:评论id"`
8 | UserId uint `gorm:"not null; comment:发布评论的用户id; type:INT"`
9 | VideoId uint `gorm:"not null; comment:评论所属视频id; type:INT"`
10 | Content string `gorm:"not null; comment:评论内容; type:VARCHAR(255)"`
11 | CreatedAt time.Time `gorm:"not null; comment:评论发布日期; type:DATETIME"`
12 | // 定义外键关系
13 | User User `gorm:"foreignKey:UserId; references:ID; comment:评论所属用户"`
14 | Video Video `gorm:"foreignKey:VideoId; references:ID; comment:评论所属视频"`
15 | }
16 |
--------------------------------------------------------------------------------
/setup/models_init.go:
--------------------------------------------------------------------------------
1 | package setup
2 |
3 | import (
4 | "github.com/zheng-yi-yi/simpledouyin/models"
5 | "gorm.io/gorm"
6 | )
7 |
8 | // CreateTable :自动创建(或更新)数据库表格,并初始化表格数据
9 | func CreateTable(db *gorm.DB) error {
10 | err := db.AutoMigrate(
11 | models.User{},
12 | models.Video{},
13 | models.Favorite{},
14 | models.Comment{},
15 | models.Relation{},
16 | models.Message{},
17 | )
18 | if err != nil {
19 | return err
20 | }
21 | tableInit(db)
22 | return nil
23 | }
24 |
25 | // 表格-样例数据初始化
26 | func tableInit(db *gorm.DB) {
27 | initUsers(db)
28 | initVideos(db)
29 | initFavorites(db)
30 | initComments(db)
31 | initRelations(db)
32 | initMessages(db)
33 | }
34 |
--------------------------------------------------------------------------------
/controllers/video/publish.go:
--------------------------------------------------------------------------------
1 | package video
2 |
3 | import (
4 | "github.com/gin-gonic/gin"
5 | "github.com/zheng-yi-yi/simpledouyin/controllers/response"
6 | )
7 |
8 | // Publish , 处理视频投稿发布请求。
9 | func Publish(c *gin.Context) {
10 | // 获取上传的视频文件
11 | file, err := c.FormFile("data")
12 | if err != nil {
13 | response.VideoFileAccessError(c) // 视频获取失败
14 | return
15 | }
16 | // 当前用户id
17 | userId := c.Value("userID").(uint)
18 | // 获取视频标题
19 | title := c.PostForm("title")
20 | // 保存视频
21 | err = VideoService.SaveVideoFile(c, file, userId, title)
22 | if err != nil {
23 | response.VideoFileSaveFailure(c) // 视频保存失败
24 | return
25 | }
26 | // 视频发布成功
27 | response.PostVideoSuccessful(c)
28 | }
29 |
--------------------------------------------------------------------------------
/controllers/user/register.go:
--------------------------------------------------------------------------------
1 | package user
2 |
3 | import (
4 | "github.com/gin-gonic/gin"
5 | "github.com/zheng-yi-yi/simpledouyin/controllers/response"
6 | "github.com/zheng-yi-yi/simpledouyin/middlewares"
7 | )
8 |
9 | // Register , 处理用户的注册请求
10 | func Register(c *gin.Context) {
11 | // 获取请求参数
12 | username := c.Query("username")
13 | password := c.Query("password")
14 | // 用户注册
15 | user, err := UserService.Register(username, password)
16 | if err != nil {
17 | // 用户注册失败
18 | response.RegisterUserFailure(c)
19 | return
20 | }
21 | // 生成 token
22 | token, err := middlewares.GenerateToken(user.ID, username, password)
23 | if err != nil {
24 | // token 生成失败
25 | response.RegisterTokenError(c)
26 | return
27 | }
28 | // 用户注册成功
29 | response.RegisterUserComplete(c, int32(user.ID), token)
30 | }
31 |
--------------------------------------------------------------------------------
/models/video_model.go:
--------------------------------------------------------------------------------
1 | package models
2 |
3 | import (
4 | "time"
5 | )
6 |
7 | // Video, 视频表
8 | type Video struct {
9 | ID uint `gorm:"primarykey"`
10 | UserId uint `gorm:"not null; comment:作者ID type:INT"`
11 | PlayUrl string `gorm:"not null; comment:视频播放地址; type:VARCHAR(255)"`
12 | CoverUrl string `gorm:"not null; comment:视频封面地址; type:VARCHAR(255)"`
13 | FavoriteCount int64 `gorm:"not null; comment:点赞数量; type:BIGINT"`
14 | CommentCount int64 `gorm:"not null; comment:视频的评论总数; type:BIGINT"`
15 | Description string `gorm:"not null; comment:视频描述; type:TEXT"`
16 | CreatedAt time.Time `gorm:"not null; comment:视频发布日期; type:DATETIME"`
17 | // 定义外键关系
18 | User User `gorm:"foreignKey:UserId; references:ID; comment:作者信息"`
19 | }
20 |
--------------------------------------------------------------------------------
/controllers/user/user.go:
--------------------------------------------------------------------------------
1 | package user
2 |
3 | import (
4 | "strconv"
5 |
6 | "github.com/gin-gonic/gin"
7 | "github.com/zheng-yi-yi/simpledouyin/controllers/response"
8 | "github.com/zheng-yi-yi/simpledouyin/models"
9 | )
10 |
11 | // 获取用户信息
12 | func UserInfo(c *gin.Context) {
13 | // 当前登录的用户
14 | userId := c.Value("userID").(uint)
15 | // 要获取用户信息的用户id
16 | query_user_id, err := strconv.ParseUint(c.Query("user_id"), 10, 64)
17 | if err != nil {
18 | // 参数类型转换失败
19 | response.UserInfoConversionError(c)
20 | return
21 | }
22 | // 获取指定用户的信息
23 | userInfo, getUserInfoErr := models.FetchData(uint(query_user_id))
24 | if getUserInfoErr != nil {
25 | // 用户信息获取失败
26 | response.GetUserInfoFailure(c)
27 | return
28 | }
29 | // 构建用户信息的响应
30 | response.UserInfoComplete(c, userInfo, userId, query_user_id)
31 | }
32 |
--------------------------------------------------------------------------------
/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "log"
5 |
6 | "github.com/gin-gonic/gin"
7 | "github.com/zheng-yi-yi/simpledouyin/config"
8 | "github.com/zheng-yi-yi/simpledouyin/setup"
9 | )
10 |
11 | func main() {
12 | Init() // 初始化所有配置
13 | r := gin.Default() // 创建一个 HTTP 服务器的实例,并使用默认的配置选项进行初始化
14 | initRouter(r) // 初始化路由规则
15 | r.Run() // 监听客户端请求
16 | }
17 |
18 | func Init() {
19 | // 初始化 config.yaml 配置文件
20 | setup.InitConfig()
21 | // 初始化 GORM 配置
22 | config.GormConfig = setup.InitGormConfig()
23 | // 初始化数据库连接
24 | config.Database = setup.InitGorm()
25 | if config.Database != nil {
26 | if err := setup.CreateTable(config.Database); err != nil {
27 | log.Printf("无法创建或更新表: %v", err)
28 | }
29 | log.Println("数据库表初始化成功!")
30 | } else {
31 | log.Fatalf("数据库连接为空,无法创建或更新表,请检查...")
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/controllers/user/login.go:
--------------------------------------------------------------------------------
1 | package user
2 |
3 | import (
4 | "github.com/gin-gonic/gin"
5 | "github.com/zheng-yi-yi/simpledouyin/controllers/response"
6 | "github.com/zheng-yi-yi/simpledouyin/middlewares"
7 | "github.com/zheng-yi-yi/simpledouyin/services"
8 | )
9 |
10 | var UserService services.UserService
11 |
12 | // Login , 用户登录
13 | func Login(c *gin.Context) {
14 | // 获取请求参数
15 | username := c.Query("username")
16 | password := c.Query("password")
17 | // 用户登录
18 | user, err := UserService.Login(username, password)
19 | if err != nil {
20 | // 用户登录失败
21 | response.UserLoginFailure(c)
22 | return
23 | }
24 | // 生成 token
25 | token, err := middlewares.GenerateToken(user.ID, username, password)
26 | if err != nil {
27 | // 用户登录时 token 生成失败
28 | response.UserLoginTokenError(c)
29 | return
30 | }
31 | // 用户登录成功
32 | response.UserLoginComplete(c, int32(user.ID), token)
33 | }
34 |
--------------------------------------------------------------------------------
/controllers/message/messageAction.go:
--------------------------------------------------------------------------------
1 | package message
2 |
3 | import (
4 | "strconv"
5 |
6 | "github.com/gin-gonic/gin"
7 | "github.com/zheng-yi-yi/simpledouyin/controllers/response"
8 | "github.com/zheng-yi-yi/simpledouyin/services"
9 | )
10 |
11 | var MessageService services.MessageService
12 |
13 | // MessageAction ,发送消息
14 | func MessageAction(c *gin.Context) {
15 | // 当前登录的用户
16 | from_user_id := c.Value("userID").(uint)
17 | // 要查询的对方用户id
18 | to_user_id, err := strconv.ParseUint(c.Query("to_user_id"), 10, 64)
19 | if err != nil {
20 | response.UserIdConversionError(c) // 用户id参数类型转换失败
21 | return
22 | }
23 | action_type := c.Query("action_type")
24 | // 发送信息
25 | if action_type == "1" {
26 | content := c.Query("content") // 消息内容
27 | if err := MessageService.AddMessage(uint(from_user_id), content, uint(to_user_id)); err != nil {
28 | response.MessageActionError(c) // 消息发送失败
29 | return
30 | }
31 | response.MessageActionSucceeded(c) // 消息发送成功
32 | return
33 | }
34 | response.UnsuccessfulAction(c) // 非法操作
35 | }
36 |
--------------------------------------------------------------------------------
/models/user_model.go:
--------------------------------------------------------------------------------
1 | package models
2 |
3 | // User, 用户表
4 | type User struct {
5 | ID uint `gorm:"primarykey"`
6 | UserName string `gorm:"not null; comment:用户名; type:VARCHAR(255)"`
7 | PassWord string `gorm:"not null; comment:密码; type:VARCHAR(255)"`
8 | FollowCount int `gorm:"not null; comment:关注总数; type:INT"`
9 | FollowerCount int `gorm:"not null; comment:粉丝总数; type:INT"`
10 | FavoriteCount int64 `gorm:"not null; comment:喜欢数; type:BIGINT"`
11 | TotalFavorited string `gorm:"not null; comment:获赞数量; type:VARCHAR(255)"`
12 | WorkCount int64 `gorm:"not null; comment:作品数; type:BIGINT"`
13 | Avatar string `gorm:"not null; comment:用户头像; type:VARCHAR(255)"`
14 | BackgroundImage string `gorm:"not null; comment:顶部图; type:VARCHAR(255)"`
15 | Signature string `gorm:"not null; comment:个人简介; type:TEXT"`
16 | // 定义外键关系
17 | Video []Video `gorm:"foreignKey:UserId; references:ID; comment:视频信息"`
18 | Comment []Comment `gorm:"foreignKey:UserId; references:ID; comment:评论信息"`
19 | }
20 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2023 zheng-yi-yi
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy
4 | of this software and associated documentation files (the "Software"), to deal
5 | in the Software without restriction, including without limitation the rights
6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 | copies of the Software, and to permit persons to whom the Software is
8 | furnished to do so, subject to the following conditions:
9 |
10 | The above copyright notice and this permission notice shall be included in all
11 | copies or substantial portions of the Software.
12 |
13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19 | SOFTWARE.
20 |
--------------------------------------------------------------------------------
/services/message_service.go:
--------------------------------------------------------------------------------
1 | package services
2 |
3 | import (
4 | "time"
5 |
6 | "github.com/zheng-yi-yi/simpledouyin/config"
7 | "github.com/zheng-yi-yi/simpledouyin/models"
8 | )
9 |
10 | type MessageService struct {
11 | }
12 |
13 | func (ms *MessageService) AddMessage(fromUserID uint, content string, toUserID uint) error {
14 | // 创建消息结构体
15 | message := models.Message{
16 | FromUserID: fromUserID,
17 | ToUserID: toUserID,
18 | Content: content,
19 | CreateTime: time.Now(),
20 | }
21 | // 将消息插入到数据库
22 | if err := config.Database.Create(&message).Error; err != nil {
23 | return err
24 | }
25 |
26 | return nil
27 | }
28 |
29 | func (MessageService *MessageService) GetMessageListWithTime(fromUserID, toUserID uint, preMsgTime time.Time) ([]models.Message, error) {
30 | var messages []models.Message
31 |
32 | // 查询消息记录,基于 pre_msg_time 进行查询
33 | err := config.Database.Where("((from_user_id = ? AND to_user_id = ?) OR (from_user_id = ? AND to_user_id = ?)) AND create_time > ?",
34 | fromUserID, toUserID, toUserID, fromUserID, preMsgTime).
35 | Order("create_time").
36 | Find(&messages).Error
37 |
38 | if err != nil {
39 | return nil, err
40 | }
41 |
42 | return messages, nil
43 | }
44 |
--------------------------------------------------------------------------------
/controllers/relation/relationAction.go:
--------------------------------------------------------------------------------
1 | package relation
2 |
3 | import (
4 | "strconv"
5 |
6 | "github.com/gin-gonic/gin"
7 | "github.com/zheng-yi-yi/simpledouyin/controllers/response"
8 | )
9 |
10 | // RelationAction , 关注操作
11 | func RelationAction(c *gin.Context) {
12 | // 当前登录的用户
13 | formUserId := c.Value("userID").(uint)
14 | // 要查询的对方用户id
15 | toUserId, err := strconv.ParseUint(c.Query("to_user_id"), 10, 64)
16 | if err != nil {
17 | response.ToUserIdConversionError(c)
18 | return
19 | }
20 | actionType := c.Query("action_type")
21 | if toUserId == uint64(formUserId) {
22 | response.UnsuccessfulAction(c) // 操作失败(自己不用关注或取关自己)
23 | return
24 | }
25 | switch actionType {
26 | case "1":
27 | //关注操作
28 | err := RelationService.FollowUser(uint(formUserId), uint(toUserId))
29 | if err != nil {
30 | response.RelationActionError(c) // 关注失败
31 | }
32 | response.RelationActionSucceeded(c) // 关注成功
33 | return
34 | case "2":
35 | //取消关注操作
36 | err := RelationService.CancelFollowUser(uint(formUserId), uint(toUserId))
37 | if err != nil {
38 | response.CancelRelationError(c) // 取关失败
39 | }
40 | response.CancelRelationSucceeded(c) // 取关成功
41 | return
42 | default:
43 | response.UnsuccessfulAction(c) // 操作失败
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/controllers/favorite/favoriteAction.go:
--------------------------------------------------------------------------------
1 | package favorite
2 |
3 | import (
4 | "strconv"
5 |
6 | "github.com/gin-gonic/gin"
7 | "github.com/zheng-yi-yi/simpledouyin/controllers/response"
8 | "github.com/zheng-yi-yi/simpledouyin/services"
9 | )
10 |
11 | var FavoriteService services.FavoriteService
12 |
13 | // FavoriteAction ,登录用户对视频的点赞和取消点赞操作
14 | func FavoriteAction(c *gin.Context) {
15 | video_id, err := strconv.ParseUint(c.Query("video_id"), 10, 64)
16 | if err != nil {
17 | // 视频id参数类型转换失败
18 | response.VideoIdConversionError(c)
19 | return
20 | }
21 | userId := c.Value("userID").(uint)
22 | actionType := c.Query("action_type")
23 | // 判断操作是否合法
24 | switch actionType {
25 | case "1":
26 | // 点赞操作
27 | err := FavoriteService.AddLike(userId, uint(video_id))
28 | if err != nil {
29 | // 点赞失败
30 | response.LikeActionError(c)
31 | return
32 | }
33 | // 点赞成功
34 | response.LikeActionSucceeded(c)
35 | return
36 | case "2":
37 | // 取消赞操作
38 | err := FavoriteService.CancelLike(userId, uint(video_id))
39 | if err != nil {
40 | // 取消赞失败
41 | response.CancelLikeError(c)
42 | return
43 | }
44 | // 取消赞成功
45 | response.UnlikeActionSucceeded(c)
46 | return
47 | default:
48 | // 操作不合法
49 | response.UnsuccessfulAction(c)
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/setup/gorm_init.go:
--------------------------------------------------------------------------------
1 | package setup
2 |
3 | import (
4 | "github.com/zheng-yi-yi/simpledouyin/config"
5 | "gorm.io/driver/mysql"
6 | "gorm.io/gorm"
7 | "gorm.io/gorm/schema"
8 | )
9 |
10 | // 初始化 GORM 配置,返回一个 GORM 配置的指针
11 | func InitGormConfig() *gorm.Config {
12 | // 配置 GORM 库的行为
13 | gormConfig := &gorm.Config{
14 | SkipDefaultTransaction: false,
15 | NamingStrategy: &schema.NamingStrategy{
16 | TablePrefix: "dy_",
17 | SingularTable: false,
18 | NameReplacer: nil,
19 | NoLowerCase: false,
20 | },
21 | PrepareStmt: true,
22 | }
23 | return gormConfig
24 | }
25 |
26 | // 初始化数据库连接:返回一个与 MySQL 数据库连接的 GORM 数据库对象。
27 | func InitGorm() *gorm.DB {
28 | mysqlconf := Config.Mysql
29 | mysqlConfig := mysql.Config{
30 | DriverName: "mysql",
31 | ServerVersion: "",
32 | DSN: mysqlconf.Dsn(),
33 | Conn: nil,
34 | SkipInitializeWithVersion: false,
35 | DefaultStringSize: 0,
36 | DefaultDatetimePrecision: nil,
37 | DisableDatetimePrecision: false,
38 | DontSupportRenameIndex: false,
39 | DontSupportRenameColumn: false,
40 | DontSupportForShareClause: false,
41 | }
42 | // 创建数据库连接
43 | db, err := gorm.Open(mysql.New(mysqlConfig), config.GormConfig)
44 | if err != nil {
45 | panic(err)
46 | }
47 | return db
48 | }
49 |
--------------------------------------------------------------------------------
/controllers/relation/friendList.go:
--------------------------------------------------------------------------------
1 | package relation
2 |
3 | import (
4 | "strconv"
5 |
6 | "github.com/gin-gonic/gin"
7 | "github.com/zheng-yi-yi/simpledouyin/controllers/response"
8 | "github.com/zheng-yi-yi/simpledouyin/models"
9 | )
10 |
11 | // FriendList , 获取好友列表
12 | func FriendList(c *gin.Context) {
13 | // 当前登录的用户
14 | userId := c.Value("userID").(uint)
15 | // 要查询的用户id
16 | query_user_id, err := strconv.ParseUint(c.Query("user_id"), 10, 64)
17 | if err != nil {
18 | response.UserIdConversionError(c) // 用户id参数类型转换失败
19 | return
20 | }
21 | // 获取好友列表
22 | users, err := RelationService.GetFriendsList(uint(query_user_id))
23 | if err != nil {
24 | response.GetFriendListError(c) // 好友列表获取失败
25 | return
26 | }
27 | var relationUsers []response.User
28 | for _, user := range users {
29 | relationUser := response.User{
30 | Id: int64(user.ID),
31 | Name: user.UserName,
32 | Avatar: user.Avatar,
33 | Signature: user.Signature,
34 | FollowCount: int64(user.FollowCount),
35 | FollowerCount: int64(user.FollowCount),
36 | IsFollow: models.IsFollow(uint(userId), user.ID),
37 | Background: user.BackgroundImage,
38 | TotalFavorited: user.TotalFavorited,
39 | WorkCount: user.WorkCount,
40 | FavoriteCount: user.FavoriteCount,
41 | }
42 | relationUsers = append(relationUsers, relationUser)
43 | }
44 | response.GetFriendListSucceeded(c, relationUsers) // 好友列表获取成功
45 | }
46 |
--------------------------------------------------------------------------------
/controllers/relation/followlist.go:
--------------------------------------------------------------------------------
1 | package relation
2 |
3 | import (
4 | "strconv"
5 |
6 | "github.com/gin-gonic/gin"
7 | "github.com/zheng-yi-yi/simpledouyin/controllers/response"
8 | "github.com/zheng-yi-yi/simpledouyin/models"
9 | )
10 |
11 | // FollowList ,关注列表
12 | func FollowList(c *gin.Context) {
13 | // 当前登录的用户
14 | userId := c.Value("userID").(uint)
15 | // 要查询的用户id
16 | query_user_id, err := strconv.ParseUint(c.Query("user_id"), 10, 64)
17 | if err != nil {
18 | response.UserIdConversionError(c) // 用户id参数类型转换失败
19 | return
20 | }
21 | // 查询用户关注的所有用户
22 | users, err := RelationService.GetFllowList(uint(query_user_id))
23 | if err != nil {
24 | // 关注列表获取失败
25 | response.GetFollowListError(c)
26 | return
27 | }
28 | var relationUsers []response.User
29 | for _, user := range users {
30 | relationUser := response.User{
31 | Id: int64(user.ID),
32 | Name: user.UserName,
33 | Avatar: user.Avatar,
34 | Signature: user.Signature,
35 | FollowCount: int64(user.FollowCount),
36 | FollowerCount: int64(user.FollowCount),
37 | IsFollow: models.IsFollow(uint(userId), user.ID),
38 | Background: user.BackgroundImage,
39 | TotalFavorited: user.TotalFavorited,
40 | WorkCount: user.WorkCount,
41 | FavoriteCount: user.FavoriteCount,
42 | }
43 | relationUsers = append(relationUsers, relationUser)
44 | }
45 | // 关注列表获取成功
46 | response.GetFollowListSucceeded(c, relationUsers)
47 | }
48 |
--------------------------------------------------------------------------------
/controllers/message/messageChat.go:
--------------------------------------------------------------------------------
1 | package message
2 |
3 | import (
4 | "strconv"
5 | "time"
6 |
7 | "github.com/gin-gonic/gin"
8 | "github.com/zheng-yi-yi/simpledouyin/controllers/response"
9 | )
10 |
11 | // MessageChat ,获取聊天记录
12 | func MessageChat(c *gin.Context) {
13 | // 当前登录的用户
14 | from_user_id := c.Value("userID").(uint)
15 | // 获取对方用户id
16 | to_user_id, err := strconv.ParseUint(c.Query("to_user_id"), 10, 64)
17 | if err != nil {
18 | response.ToUserIdConversionError(c) // 对方用户id参数类型转换失败
19 | return
20 | }
21 | // 获取pre_msg_time
22 | msgTime := time.Now()
23 | pre_msg_time := c.Query("pre_msg_time")
24 | if pre_msg_time != "" {
25 | preMsgTimeUnix, err := strconv.ParseInt(pre_msg_time, 10, 64)
26 | if err != nil {
27 | response.PreMsgTimeConversionError(c)
28 | return
29 | }
30 | msgTime = time.Unix(0, preMsgTimeUnix*int64(time.Millisecond))
31 | }
32 |
33 | // 获取消息列表
34 | messages, err := MessageService.GetMessageListWithTime(from_user_id, uint(to_user_id), msgTime)
35 | if err != nil {
36 | response.GetMessageChatError(c) // 聊天记录获取失败
37 | return
38 | }
39 | messageList := make([]response.Message, 0, len(messages))
40 | for _, message := range messages {
41 | messageList = append(messageList, response.Message{
42 | ID: int64(message.ID),
43 | ToUserID: int64(message.ToUserID),
44 | FromUserID: int64(message.FromUserID),
45 | Content: message.Content,
46 | CreateTime: message.CreateTime.Unix(),
47 | })
48 | }
49 | response.GetMessageChatSucceeded(c, messageList) // 聊天记录获取成功
50 | }
51 |
--------------------------------------------------------------------------------
/controllers/relation/followerList.go:
--------------------------------------------------------------------------------
1 | package relation
2 |
3 | import (
4 | "strconv"
5 |
6 | "github.com/gin-gonic/gin"
7 | "github.com/zheng-yi-yi/simpledouyin/controllers/response"
8 | "github.com/zheng-yi-yi/simpledouyin/models"
9 | "github.com/zheng-yi-yi/simpledouyin/services"
10 | )
11 |
12 | var RelationService services.RelationService
13 |
14 | // FollowerList , 拉取粉丝用户列表
15 | func FollowerList(c *gin.Context) {
16 | // 当前登录的用户
17 | userId := c.Value("userID").(uint)
18 | // 要查询的用户id
19 | query_user_id, err := strconv.ParseUint(c.Query("user_id"), 10, 64)
20 | if err != nil {
21 | response.UserIdConversionError(c) // 用户id参数类型转换失败
22 | return
23 | }
24 | // 获取粉丝列表
25 | users, err := RelationService.GetFollowerList(uint(query_user_id))
26 | if err != nil {
27 | response.GetFollowerListError(c) // 粉丝列表获取失败
28 | return
29 | }
30 | var relaUserList []response.User
31 | for _, user := range users {
32 | RelationUser := response.User{
33 | Id: int64(user.ID),
34 | Name: user.UserName,
35 | Avatar: user.Avatar,
36 | Signature: user.Signature,
37 | FollowCount: int64(user.FollowCount),
38 | FollowerCount: int64(user.FollowCount),
39 | IsFollow: models.IsFollow(uint(userId), user.ID),
40 | Background: user.BackgroundImage,
41 | TotalFavorited: user.TotalFavorited,
42 | WorkCount: user.WorkCount,
43 | FavoriteCount: user.FavoriteCount,
44 | }
45 | relaUserList = append(relaUserList, RelationUser)
46 | }
47 | response.GetFollowerListSucceeded(c, relaUserList) // 粉丝列表获取成功
48 | }
49 |
--------------------------------------------------------------------------------
/controllers/response/shared_resp.go:
--------------------------------------------------------------------------------
1 | package response
2 |
3 | import (
4 | "net/http"
5 |
6 | "github.com/gin-gonic/gin"
7 | )
8 |
9 | type Response struct {
10 | StatusCode int32 `json:"status_code"` // 状态码,0-成功,其他值-失败
11 | StatusMsg string `json:"status_msg"` // 返回状态描述
12 | }
13 |
14 | // UnsuccessfulAction ,操作失败
15 | func UnsuccessfulAction(c *gin.Context) {
16 | c.JSON(http.StatusBadRequest, Response{
17 | StatusCode: 1,
18 | StatusMsg: ",操作失败",
19 | })
20 | }
21 |
22 | // VideoIdConversionError , 视频id参数类型转换失败
23 | func VideoIdConversionError(c *gin.Context) {
24 | c.JSON(http.StatusBadRequest, Response{
25 | StatusCode: 1,
26 | StatusMsg: "视频id参数类型转换失败",
27 | })
28 | }
29 |
30 | // CommentIdConversionError ,评论id参数类型转换失败
31 | func CommentIdConversionError(c *gin.Context) {
32 | c.JSON(http.StatusBadRequest, Response{
33 | StatusCode: 1,
34 | StatusMsg: "评论id参数类型转换失败",
35 | })
36 | }
37 |
38 | // UserIdConversionError , 用户id参数类型转换失败
39 | func UserIdConversionError(c *gin.Context) {
40 | c.JSON(http.StatusBadRequest, Response{
41 | StatusCode: 1,
42 | StatusMsg: "用户id参数类型转换失败",
43 | })
44 | }
45 |
46 | // ToUserIdConversionError , 对方用户id参数类型转换失败
47 | func ToUserIdConversionError(c *gin.Context) {
48 | c.JSON(http.StatusBadRequest, Response{
49 | StatusCode: 1,
50 | StatusMsg: "对方用户id参数类型转换失败",
51 | })
52 | }
53 |
54 | // PreMsgTimeConversionError , pre_msg_time 参数类型转换失败
55 | func PreMsgTimeConversionError(c *gin.Context) {
56 | c.JSON(http.StatusBadRequest, Response{
57 | StatusCode: 1,
58 | StatusMsg: "pre_msg_time参数类型转换失败",
59 | })
60 | }
61 |
--------------------------------------------------------------------------------
/setup/config_init.go:
--------------------------------------------------------------------------------
1 | package setup
2 |
3 | import (
4 | "fmt"
5 | "log"
6 |
7 | "github.com/fsnotify/fsnotify"
8 | "github.com/spf13/viper"
9 | )
10 |
11 | // MysqlConfig , 存储 MySQL 数据库的配置信息
12 | type MysqlConfig struct {
13 | Host string `yaml:"host"` // 主机
14 | Port string `yaml:"port"` // 端口
15 | Username string `yaml:"username"` // 用户名
16 | Password string `yaml:"password"` // 密码
17 | Dbname string `mapstructure:"db_name"` // 数据库名
18 | }
19 |
20 | // Dsn , 返回数据库所需的参数字符串
21 | func (m *MysqlConfig) Dsn() string {
22 | return m.Username + ":" + m.Password + "@tcp(" + m.Host + ":" + m.Port + ")/" + m.Dbname + "?charset=utf8mb4&parseTime=True&loc=Local"
23 | }
24 |
25 | type Conf struct {
26 | Mysql *MysqlConfig // MysqlConfig 结构体定义了 MySQL 数据库的配置信息
27 | }
28 |
29 | var Config *Conf // Config 指向一个包含配置信息的结构体
30 |
31 | // 解析根目录下的配置文件 config.yaml,解析后的配置统一通过 setupConfig 获取
32 | func InitConfig() {
33 | v := viper.New()
34 | v.SetConfigFile("config.yaml")
35 | v.SetConfigType("yaml")
36 | err := v.ReadInConfig()
37 | if err != nil {
38 | log.Printf("Error opening the configuration file: %v", err)
39 | return
40 | }
41 | // 尝试将配置文件的内容解析到 config.Config 变量中。
42 | if err := v.Unmarshal(&Config); err != nil {
43 | log.Printf("Configuration file reading failed: %v", err)
44 | return
45 | }
46 | fmt.Printf("配置文件读取结果:%v", *Config)
47 | viper.WatchConfig()
48 | viper.OnConfigChange(func(in fsnotify.Event) {
49 | if err := v.Unmarshal(&Config); err != nil {
50 | log.Printf("Configuration file reading failed: %v", err)
51 | return
52 | }
53 | })
54 | }
55 |
--------------------------------------------------------------------------------
/controllers/comment/commentAction.go:
--------------------------------------------------------------------------------
1 | package comment
2 |
3 | import (
4 | "strconv"
5 |
6 | "github.com/gin-gonic/gin"
7 | "github.com/zheng-yi-yi/simpledouyin/controllers/response"
8 | "github.com/zheng-yi-yi/simpledouyin/services"
9 | )
10 |
11 | var CommentService services.CommentService
12 |
13 | // CommentAction 处理评论操作的请求
14 | func CommentAction(c *gin.Context) {
15 | // 获取当前登录的用户id
16 | userId := c.Value("userID").(uint)
17 | // 评论所属的视频id
18 | video_id, err := strconv.ParseInt(c.Query("video_id"), 10, 64)
19 | if err != nil {
20 | // 视频 id 参数类型转换失败
21 | response.VideoIdConversionError(c)
22 | return
23 | }
24 | // 操作类型
25 | action_type := c.Query("action_type")
26 | switch action_type {
27 | case "1":
28 | // 发布评论
29 | content := c.Query("comment_text")
30 | commentResp, err := CommentService.CreateComment(uint(video_id), content, userId)
31 | if err != nil {
32 | // 评论保存失败
33 | response.CommentSaveFailed(c)
34 | return
35 | }
36 | // 评论添加成功
37 | response.CommentAddSuccess(c, commentResp)
38 | return
39 | case "2":
40 | // 删除评论
41 | comment_id_str := c.Query("comment_id")
42 | comment_id, err := strconv.ParseInt(comment_id_str, 10, 64)
43 | if err != nil {
44 | // 评论id参数类型转换失败
45 | response.CommentIdConversionError(c)
46 | return
47 | }
48 | err = CommentService.DeleteCommentById(userId, uint(video_id), uint(comment_id))
49 | if err != nil {
50 | // 评论删除失败
51 | response.CommentDelFailed(c)
52 | return
53 | }
54 | // 评论删除成功
55 | response.CommentDelSuccess(c)
56 | return
57 | default:
58 | // 非法操作
59 | response.UnsuccessfulAction(c)
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/controllers/response/favorite_resp.go:
--------------------------------------------------------------------------------
1 | package response
2 |
3 | import (
4 | "net/http"
5 |
6 | "github.com/gin-gonic/gin"
7 | )
8 |
9 | // 点赞操作响应
10 | type FavoriteResponse struct {
11 | StatusCode int64 `json:"status_code"` // 状态码,0-成功,其他值-失败
12 | StatusMsg string `json:"status_msg"` // 返回状态描述
13 | }
14 |
15 | // LikeActionSucceeded , 点赞成功
16 | func LikeActionSucceeded(c *gin.Context) {
17 | c.JSON(http.StatusOK, FavoriteResponse{
18 | StatusCode: 0,
19 | StatusMsg: "点赞成功",
20 | })
21 | }
22 |
23 | // LikeActionError , 点赞失败
24 | func LikeActionError(c *gin.Context) {
25 | c.JSON(http.StatusInternalServerError, FavoriteResponse{
26 | StatusCode: 1,
27 | StatusMsg: "点赞失败",
28 | })
29 | }
30 |
31 | // UnlikeActionSucceeded , 取消赞成功
32 | func UnlikeActionSucceeded(c *gin.Context) {
33 | c.JSON(http.StatusOK, FavoriteResponse{
34 | StatusCode: 0,
35 | StatusMsg: "取消赞成功",
36 | })
37 | }
38 |
39 | // CancelLikeError , 取消赞失败
40 | func CancelLikeError(c *gin.Context) {
41 | c.JSON(http.StatusInternalServerError, FavoriteResponse{
42 | StatusCode: 1,
43 | StatusMsg: "取消赞失败",
44 | })
45 | }
46 |
47 | // 喜欢列表响应
48 | type FavoriteListResponse struct {
49 | StatusCode string `json:"status_code"` // 状态码,0-成功,其他值-失败
50 | StatusMsg string `json:"status_msg"` // 返回状态描述
51 | VideoList []Video `json:"video_list,omitempty"` // 用户点赞视频列表
52 | }
53 |
54 | // GetFavoriteListSucceeded , 视频列表获取成功
55 | func GetFavoriteListSucceeded(c *gin.Context, videoList []Video) {
56 | c.JSON(http.StatusOK, FavoriteListResponse{
57 | StatusCode: "0",
58 | StatusMsg: "视频列表获取成功",
59 | VideoList: videoList,
60 | })
61 | }
62 |
--------------------------------------------------------------------------------
/config/default_settings.go:
--------------------------------------------------------------------------------
1 | package config
2 |
3 | const (
4 | // 资源路径
5 | SERVER_RESOURCES = "http://" + LOCAL_IP_ADDRESS + ":8080/public/"
6 | DEFAULT_AVATAR_URL = SERVER_RESOURCES + "Initdata/avatar/"
7 | DEFAULT_BG_IMAGE_URL = SERVER_RESOURCES + "Initdata/background/"
8 |
9 | // 默认用户信息
10 | DEFAULT_USER_AVATAR_URL = DEFAULT_AVATAR_URL + "default.png" // 默认头像地址
11 | DEFAULT_USER_BG_IMAGE_URL = DEFAULT_BG_IMAGE_URL + "default.png" // 默认背景图地址
12 | DEFAULT_USER_BIO = "这个人很懒,什么也没有留下......" // 默认简介内容
13 |
14 | // 用户 1
15 | USER1_AVATAR_URL = DEFAULT_AVATAR_URL + "1.png"
16 | USER1_BACKGROUND_IMAGE_URL = DEFAULT_BG_IMAGE_URL + "1.png"
17 | USER1_PROFILE_DESCRIPTION = "明确地爱,直接地厌恶,真诚地喜欢,站在太阳下的坦荡,大声无愧地称赞自己"
18 |
19 | // 用户 2
20 | USER2_AVATAR_URL = DEFAULT_AVATAR_URL + "2.png"
21 | USER2_BACKGROUND_IMAGE_URL = DEFAULT_BG_IMAGE_URL + "2.png"
22 | USER2_PROFILE_DESCRIPTION = "发呆业务爱好者"
23 |
24 | // 用户 3
25 | USER3_AVATAR_URL = DEFAULT_AVATAR_URL + "3.png"
26 | USER3_BACKGROUND_IMAGE_URL = DEFAULT_BG_IMAGE_URL + "3.png"
27 | USER3_PROFILE_DESCRIPTION = "这里介绍不了我"
28 |
29 | // 用户 4
30 | USER4_AVATAR_URL = DEFAULT_AVATAR_URL + "4.png"
31 | USER4_BACKGROUND_IMAGE_URL = DEFAULT_BG_IMAGE_URL + "4.png"
32 | USER4_PROFILE_DESCRIPTION = "有时候词不达意 但我很高兴遇见你"
33 |
34 | // 用户 5
35 | USER5_AVATAR_URL = DEFAULT_AVATAR_URL + "5.png"
36 | USER5_BACKGROUND_IMAGE_URL = DEFAULT_BG_IMAGE_URL + "5.png"
37 | USER5_PROFILE_DESCRIPTION = "周末就是将生活调回自己喜欢的频道"
38 |
39 | // 用户 6
40 | USER6_AVATAR_URL = DEFAULT_AVATAR_URL + "6.png"
41 | USER6_BACKGROUND_IMAGE_URL = DEFAULT_BG_IMAGE_URL + "6.png"
42 | USER6_PROFILE_DESCRIPTION = "麻麻说简介太长就会有笨蛋跟着念"
43 | )
44 |
--------------------------------------------------------------------------------
/controllers/response/message_resp.go:
--------------------------------------------------------------------------------
1 | package response
2 |
3 | import (
4 | "net/http"
5 |
6 | "github.com/gin-gonic/gin"
7 | )
8 |
9 | // 发送消息响应
10 | type MessageActionResponse struct {
11 | StatusCode int64 `json:"status_code"` // 状态码,0-成功,其他值-失败
12 | StatusMsg string `json:"status_msg"` // 返回状态描述
13 | }
14 |
15 | // MessageActionSucceeded , 发送消息成功
16 | func MessageActionSucceeded(c *gin.Context) {
17 | c.JSON(http.StatusOK, MessageActionResponse{
18 | StatusCode: 0,
19 | StatusMsg: "发送消息成功",
20 | })
21 | }
22 |
23 | // MessageActionError , 发送消息失败
24 | func MessageActionError(c *gin.Context) {
25 | c.JSON(http.StatusInternalServerError, MessageActionResponse{
26 | StatusCode: 1,
27 | StatusMsg: "发送消息失败",
28 | })
29 | }
30 |
31 | type Message struct {
32 | Content string `json:"content"` // 消息内容
33 | CreateTime int64 `json:"create_time"` // 消息发送时间 yyyy-MM-dd HH:MM:ss
34 | FromUserID int64 `json:"from_user_id"` // 消息发送者id
35 | ID int64 `json:"id"` // 消息id
36 | ToUserID int64 `json:"to_user_id"` // 消息接收者id
37 | }
38 |
39 | // 聊天记录响应
40 | type MessageChatResponse struct {
41 | StatusCode string `json:"status_code"` // 状态码,0-成功,其他值-失败
42 | StatusMsg string `json:"status_msg"` // 返回状态描述
43 | MessageList []Message `json:"message_list,omitempty"` // 用户列表
44 |
45 | }
46 |
47 | // GetMessageChatSucceeded , 聊天记录获取成功
48 | func GetMessageChatSucceeded(c *gin.Context, messageList []Message) {
49 | c.JSON(http.StatusOK, MessageChatResponse{
50 | StatusCode: "0",
51 | StatusMsg: "聊天记录获取成功",
52 | MessageList: messageList,
53 | })
54 | }
55 |
56 | // GetMessageChatError , 聊天记录获取失败
57 | func GetMessageChatError(c *gin.Context) {
58 | c.JSON(http.StatusInternalServerError, MessageChatResponse{
59 | StatusCode: "0",
60 | StatusMsg: "聊天记录获取失败",
61 | })
62 | }
63 |
--------------------------------------------------------------------------------
/services/user_service.go:
--------------------------------------------------------------------------------
1 | package services
2 |
3 | import (
4 | "errors"
5 |
6 | "github.com/zheng-yi-yi/simpledouyin/config"
7 | "github.com/zheng-yi-yi/simpledouyin/models"
8 | "github.com/zheng-yi-yi/simpledouyin/utils"
9 | "gorm.io/gorm"
10 | )
11 |
12 | type UserService struct {
13 | }
14 |
15 | // 用户注册服务
16 | func (userService *UserService) Register(username, password string) (models.User, error) {
17 | var user models.User
18 | err := config.Database.Where("user_name = ?", username).First(&user).Error
19 | if err == nil {
20 | return models.User{}, errors.New("用户名已注册")
21 | }
22 |
23 | // 加密用户密码
24 | encryptedPassword, err := utils.EncryptPassword(password)
25 | if err != nil {
26 | return models.User{}, err
27 | }
28 |
29 | user = models.User{
30 | UserName: username,
31 | PassWord: string(encryptedPassword), // 存储加密后的密码
32 | FollowCount: 0,
33 | FollowerCount: 0,
34 | FavoriteCount: 0,
35 | Avatar: config.DEFAULT_USER_AVATAR_URL,
36 | BackgroundImage: config.DEFAULT_USER_BG_IMAGE_URL,
37 | Signature: config.DEFAULT_USER_BIO,
38 | TotalFavorited: "0",
39 | WorkCount: 0,
40 | }
41 | err = config.Database.Create(&user).Error
42 | if err != nil {
43 | return models.User{}, err
44 | }
45 | return user, nil
46 | }
47 |
48 | // 用户登录服务
49 | func (userService *UserService) Login(username, password string) (models.User, error) {
50 | var user models.User
51 | err := config.Database.Where("user_name = ?", username).First(&user).Error
52 | if err == gorm.ErrRecordNotFound {
53 | return models.User{}, errors.New("用户名不存在")
54 | } else if err != nil {
55 | return models.User{}, err
56 | }
57 |
58 | // 验证用户密码
59 | if utils.CheckPasswordValidity(user.PassWord, password) {
60 | return user, nil
61 | } else {
62 | return models.User{}, errors.New("用户密码错误")
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/controllers/comment/commentlist.go:
--------------------------------------------------------------------------------
1 | package comment
2 |
3 | import (
4 | "strconv"
5 |
6 | "github.com/gin-gonic/gin"
7 | "github.com/zheng-yi-yi/simpledouyin/config"
8 | "github.com/zheng-yi-yi/simpledouyin/controllers/response"
9 | "github.com/zheng-yi-yi/simpledouyin/models"
10 | )
11 |
12 | // 评论列表,已删除的评论和已注销用户的评论不会展示
13 | func CommentList(c *gin.Context) {
14 | // 当前登录的用户
15 | userId := c.Value("userID").(uint)
16 | // 要查询的视频id
17 | videoId, err := strconv.ParseUint(c.Query("video_id"), 10, 64)
18 | if err != nil {
19 | // 视频id参数类型转换失败
20 | response.VideoIdConversionError(c)
21 | return
22 | }
23 | comments, err := CommentService.GetVideoComment(uint(videoId))
24 | if err != nil {
25 | // 评论列表获取失败
26 | response.GetCommentListFailed(c)
27 | return
28 | }
29 | commentList := make([]response.Comment, 0, len(comments))
30 | for _, comment := range comments {
31 | userInfo, getUserInfoErr := models.FetchData(comment.UserId)
32 | if getUserInfoErr != nil {
33 | continue
34 | }
35 | commentList = append(commentList, response.Comment{
36 | Id: int64(comment.ID),
37 | User: response.User{
38 | Id: int64(userInfo.ID),
39 | Name: userInfo.UserName,
40 | FollowCount: int64(userInfo.FollowCount),
41 | FollowerCount: int64(userInfo.FollowerCount),
42 | IsFollow: models.IsFollow(userId, userInfo.ID),
43 | Avatar: userInfo.Avatar,
44 | Background: userInfo.BackgroundImage,
45 | Signature: userInfo.Signature,
46 | TotalFavorited: userInfo.TotalFavorited,
47 | WorkCount: userInfo.WorkCount,
48 | FavoriteCount: userInfo.FavoriteCount,
49 | },
50 | Content: comment.Content,
51 | CreateDate: comment.CreatedAt.Format(config.SHORT_DATE_FORMAT),
52 | })
53 | }
54 | // 评论列表获取成功
55 | response.GetCommentListSuccess(c, commentList)
56 | }
57 |
--------------------------------------------------------------------------------
/controllers/response/comment_resp.go:
--------------------------------------------------------------------------------
1 | package response
2 |
3 | import (
4 | "net/http"
5 |
6 | "github.com/gin-gonic/gin"
7 | )
8 |
9 | type Comment struct {
10 | Id int64 `json:"id"` // 评论id
11 | User User `json:"user"` // 评论用户
12 | Content string `json:"content"` // 评论内容
13 | CreateDate string `json:"create_date"` // 评论发布日期,格式 mm-dd
14 | }
15 |
16 | // 评论操作响应
17 | type CommentActionResponse struct {
18 | StatusCode int32 `json:"status_code"` // 状态码,0-成功,其他值-失败
19 | StatusMsg string `json:"status_msg"` // 返回状态描述
20 | Comment Comment `json:"comment,omitempty"` // 评论成功返回评论内容,不需要重新拉取整个列表
21 | }
22 |
23 | // CommentAddSuccess , 评论添加成功
24 | func CommentAddSuccess(c *gin.Context, comment Comment) {
25 | c.JSON(http.StatusOK, CommentActionResponse{
26 | StatusCode: 0,
27 | StatusMsg: "评论添加成功",
28 | Comment: comment,
29 | })
30 | }
31 |
32 | // CommentSaveFailed , 评论保存失败
33 | func CommentSaveFailed(c *gin.Context) {
34 | c.JSON(http.StatusInternalServerError, CommentActionResponse{
35 | StatusCode: 1,
36 | StatusMsg: "评论保存失败",
37 | })
38 | }
39 |
40 | // CommentDelSuccess , 评论删除成功
41 | func CommentDelSuccess(c *gin.Context) {
42 | c.JSON(http.StatusOK, CommentActionResponse{
43 | StatusCode: 0,
44 | StatusMsg: "评论删除成功",
45 | })
46 | }
47 |
48 | // CommentDelFailed , 评论删除失败
49 | func CommentDelFailed(c *gin.Context) {
50 | c.JSON(http.StatusInternalServerError, CommentActionResponse{
51 | StatusCode: 1,
52 | StatusMsg: "评论删除失败",
53 | })
54 | }
55 |
56 | // 评论列表响应
57 | type CommentListResponse struct {
58 | StatusCode int64 `json:"status_code"` // 状态码,0-成功,其他值-失败
59 | StatusMsg string `json:"status_msg"` // 返回状态描述
60 | CommentList []Comment `json:"comment_list,omitempty"` // 评论列表
61 | }
62 |
63 | // GetCommentListSuccess , 评论列表获取成功
64 | func GetCommentListSuccess(c *gin.Context, commentList []Comment) {
65 | c.JSON(http.StatusOK, CommentListResponse{
66 | StatusCode: 0,
67 | StatusMsg: "评论列表获取成功",
68 | CommentList: commentList,
69 | })
70 | }
71 |
72 | // GetCommentListFailed , 评论列表获取失败
73 | func GetCommentListFailed(c *gin.Context) {
74 | c.JSON(http.StatusInternalServerError, CommentListResponse{
75 | StatusCode: 1,
76 | StatusMsg: "评论列表获取失败",
77 | })
78 | }
79 |
--------------------------------------------------------------------------------
/controllers/video/publishList.go:
--------------------------------------------------------------------------------
1 | package video
2 |
3 | import (
4 | "strconv"
5 |
6 | "github.com/gin-gonic/gin"
7 | "github.com/zheng-yi-yi/simpledouyin/config"
8 | "github.com/zheng-yi-yi/simpledouyin/controllers/response"
9 | "github.com/zheng-yi-yi/simpledouyin/models"
10 | )
11 |
12 | // 获取用户发布视频列表的处理函数
13 | func PublishList(c *gin.Context) {
14 | // 当前用户
15 | userId := c.Value("userID").(uint)
16 | // 要查询的用户id
17 | query_user_id, err := strconv.ParseUint(c.Query("user_id"), 10, 64)
18 | if err != nil {
19 | // 用户id参数类型转换失败
20 | response.UserIdConversionError(c)
21 | return
22 | }
23 | // 获取用户发布的视频列表。
24 | userPublishList := VideoService.UserPublishList(uint(query_user_id))
25 | // 如果用户发布列表为空,返回一个空的成功响应。
26 | if len(userPublishList) == 0 {
27 | response.GetPublishListSuccess(c, []response.Video{})
28 | return
29 | }
30 | // 创建视频列表:遍历用户发布列表,将每个视频对象映射到一个新的视频对象,同时进行一些字段的转换和处理。
31 | videoList := make([]response.Video, 0, len(userPublishList))
32 | for i := 0; i < len(userPublishList); i++ {
33 | videoList = append(videoList, response.Video{
34 | Id: int64(userPublishList[i].ID),
35 | Author: response.User{
36 | Id: int64(userPublishList[i].User.ID),
37 | Name: userPublishList[i].User.UserName,
38 | FollowCount: int64(userPublishList[i].User.FollowCount),
39 | FollowerCount: int64(userPublishList[i].User.FollowerCount),
40 | IsFollow: models.IsFollow(userId, uint(query_user_id)),
41 | Avatar: userPublishList[i].User.Avatar,
42 | Background: userPublishList[i].User.BackgroundImage,
43 | Signature: userPublishList[i].User.Signature,
44 | TotalFavorited: userPublishList[i].User.TotalFavorited,
45 | WorkCount: userPublishList[i].User.WorkCount,
46 | FavoriteCount: userPublishList[i].User.FavoriteCount,
47 | },
48 | PlayUrl: config.SERVER_RESOURCES + userPublishList[i].PlayUrl,
49 | CoverUrl: config.SERVER_RESOURCES + userPublishList[i].CoverUrl,
50 | FavoriteCount: userPublishList[i].FavoriteCount,
51 | CommentCount: userPublishList[i].CommentCount,
52 | IsFavorite: models.IsFavorite(userId, uint(query_user_id)),
53 | Title: userPublishList[i].Description,
54 | })
55 | }
56 | // 最后,返回带有视频列表的成功响应。
57 | response.GetPublishListSuccess(c, videoList)
58 | }
59 |
--------------------------------------------------------------------------------
/router.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "github.com/gin-gonic/gin"
5 | "github.com/zheng-yi-yi/simpledouyin/controllers/comment"
6 | "github.com/zheng-yi-yi/simpledouyin/controllers/favorite"
7 | "github.com/zheng-yi-yi/simpledouyin/controllers/message"
8 | "github.com/zheng-yi-yi/simpledouyin/controllers/relation"
9 | "github.com/zheng-yi-yi/simpledouyin/controllers/user"
10 | "github.com/zheng-yi-yi/simpledouyin/controllers/video"
11 | "github.com/zheng-yi-yi/simpledouyin/middlewares"
12 | )
13 |
14 | // route initialization function
15 | func initRouter(r *gin.Engine) {
16 | // basicSetup
17 | r.Static("/public", "./public")
18 | r.StaticFile("/favicon.ico", "./public/favicon.ico")
19 | apiRouter := r.Group("/douyin")
20 | // fundamental features:
21 | apiRouter.GET("/feed/", video.Feed) // 视频流
22 | apiRouter.POST("/user/register/", user.Register) // 用户注册
23 | apiRouter.POST("/user/login/", user.Login) // 用户登录
24 | apiRouter.GET("/user/", middlewares.Auth(), user.UserInfo) // 用户信息
25 | apiRouter.POST("/publish/action/", middlewares.UserPublishAuth(), video.Publish) // 视频投稿
26 | apiRouter.GET("/publish/list/", middlewares.Auth(), video.PublishList) // 发布列表
27 | // Extended Feature 1: Interactivity
28 | apiRouter.POST("/favorite/action/", middlewares.Auth(), favorite.FavoriteAction) // 点赞操作
29 | apiRouter.GET("/favorite/list/", middlewares.Auth(), favorite.FavoriteList) // 喜欢列表
30 | apiRouter.POST("/comment/action/", middlewares.Auth(), comment.CommentAction) // 评论操作
31 | apiRouter.GET("/comment/list/", middlewares.Auth(), comment.CommentList) // 评论列表
32 | // Extended Feature 2: Social
33 | apiRouter.POST("/relation/action/", middlewares.Auth(), relation.RelationAction) // 关注操作
34 | apiRouter.GET("/relation/follow/list/", middlewares.Auth(), relation.FollowList) // 关注列表
35 | apiRouter.GET("/relation/follower/list/", middlewares.Auth(), relation.FollowerList) // 粉丝列表
36 | apiRouter.GET("/relation/friend/list/", middlewares.Auth(), relation.FriendList) // 好友列表
37 | apiRouter.POST("/message/action/", middlewares.Auth(), message.MessageAction) // 发送消息
38 | apiRouter.GET("/message/chat/", middlewares.Auth(), message.MessageChat) // 聊天记录
39 | }
40 |
--------------------------------------------------------------------------------
/services/comment_service.go:
--------------------------------------------------------------------------------
1 | package services
2 |
3 | import (
4 | "time"
5 |
6 | "github.com/zheng-yi-yi/simpledouyin/config"
7 | "github.com/zheng-yi-yi/simpledouyin/controllers/response"
8 | "github.com/zheng-yi-yi/simpledouyin/models"
9 | )
10 |
11 | type CommentService struct {
12 | }
13 |
14 | // CreateComment: 创建新评论
15 | func (s *CommentService) CreateComment(video_id uint, content string, user_id uint) (response.Comment, error) {
16 | // 新建一条新评论记录
17 | comment := models.Comment{
18 | VideoId: video_id,
19 | Content: content,
20 | UserId: user_id,
21 | CreatedAt: time.Now(),
22 | }
23 | result := config.Database.Create(&comment)
24 | if result.Error != nil {
25 | return response.Comment{}, result.Error
26 | }
27 |
28 | // 如果添加评论成功,则增加该视频的评论总数
29 | if err := models.IncrementCommentCount(uint(video_id)); err != nil {
30 | return response.Comment{}, err
31 | }
32 |
33 | // 评论成功则返回评论内容(响应)
34 | userInfo, _ := models.FetchData(user_id)
35 | NewComment := response.Comment{
36 | Id: int64(comment.ID),
37 | User: response.User{
38 | Id: int64(userInfo.ID),
39 | Name: userInfo.UserName,
40 | Avatar: userInfo.Avatar,
41 | },
42 | Content: content, // 评论内容
43 | CreateDate: time.Now().Format(config.SHORT_DATE_FORMAT), // 评论发布日期,格式 mm-dd
44 | }
45 | return NewComment, result.Error
46 | }
47 |
48 | // 获取所有未删除的评论
49 | func (s *CommentService) GetVideoComment(video_id uint) ([]models.Comment, error) {
50 | var commentList []models.Comment
51 | if err := config.Database.Where("video_id=?", video_id).Find(&commentList).Error; err != nil {
52 | return nil, err
53 | }
54 | return commentList, nil
55 | }
56 |
57 | // 根据相应的评论获取ID
58 | func (s *CommentService) GetCommentById(comment_id int64) models.Comment {
59 | var comment models.Comment
60 | config.Database.Where("id = ?", uint(comment_id)).First(&comment)
61 | return comment
62 | }
63 |
64 | // 根据用户ID、视频ID和评论ID,定位并删除对应评论
65 | func (s *CommentService) DeleteCommentById(userId, videoId, commentId uint) error {
66 | // 删除评论
67 | result := config.Database.Where("user_id=? and video_id=? and id=?", userId, videoId, commentId).Delete(&models.Comment{})
68 | if result.Error != nil {
69 | return result.Error
70 | }
71 | // 成功删除评论后,就减少该视频的评论总数
72 | if err := models.DecreaseCommentCount(uint(videoId)); err != nil {
73 | return err
74 | }
75 | return nil
76 | }
77 |
--------------------------------------------------------------------------------
/controllers/favorite/favoriteList.go:
--------------------------------------------------------------------------------
1 | package favorite
2 |
3 | import (
4 | "strconv"
5 |
6 | "github.com/gin-gonic/gin"
7 | "github.com/zheng-yi-yi/simpledouyin/config"
8 | "github.com/zheng-yi-yi/simpledouyin/controllers/response"
9 | "github.com/zheng-yi-yi/simpledouyin/controllers/video"
10 | "github.com/zheng-yi-yi/simpledouyin/models"
11 | )
12 |
13 | // FavoriteList , 获取用户的所有点赞视频
14 | func FavoriteList(c *gin.Context) {
15 | // 要查询的用户id
16 | query_user_id, err := strconv.ParseUint(c.Query("user_id"), 10, 64)
17 | if err != nil {
18 | // 用户id参数类型转换失败
19 | response.UserIdConversionError(c)
20 | return
21 | }
22 | // 根据要查询的用户id,取出该用户点赞的所有视频ID
23 | videoIds, err := FavoriteService.GetFavoriteList(uint(query_user_id))
24 | if err != nil {
25 | response.UnsuccessfulAction(c)
26 | return
27 | }
28 | // 根据点赞过的视频ID 取出所有对应的视频信息
29 | videoInfoList := video.VideoService.GetVideoInfoByIds(videoIds)
30 | // 如果点赞列表长度为0,则返回空的成功响应
31 | if len(videoInfoList) == 0 {
32 | // 视频列表获取成功
33 | response.GetFavoriteListSucceeded(c, []response.Video{})
34 | return
35 | }
36 | videoList := make([]response.Video, 0, len(videoInfoList))
37 | for i := 0; i < len(videoInfoList); i++ {
38 | videoList = append(videoList, response.Video{
39 | Id: int64(videoInfoList[i].ID),
40 | Author: response.User{
41 | Id: int64(videoInfoList[i].User.ID),
42 | Name: videoInfoList[i].User.UserName,
43 | FollowCount: int64(videoInfoList[i].User.FollowCount),
44 | FollowerCount: int64(videoInfoList[i].User.FollowerCount),
45 | IsFollow: models.IsFollow(uint(query_user_id), videoInfoList[i].User.ID),
46 | Avatar: videoInfoList[i].User.Avatar,
47 | Background: videoInfoList[i].User.BackgroundImage,
48 | Signature: videoInfoList[i].User.Signature,
49 | TotalFavorited: videoInfoList[i].User.TotalFavorited,
50 | WorkCount: videoInfoList[i].User.WorkCount,
51 | FavoriteCount: videoInfoList[i].User.FavoriteCount,
52 | },
53 | PlayUrl: config.SERVER_RESOURCES + videoInfoList[i].PlayUrl,
54 | CoverUrl: config.SERVER_RESOURCES + videoInfoList[i].CoverUrl,
55 | FavoriteCount: videoInfoList[i].FavoriteCount,
56 | CommentCount: videoInfoList[i].CommentCount,
57 | IsFavorite: models.IsFavorite(uint(query_user_id), videoInfoList[i].ID),
58 | Title: videoInfoList[i].Description,
59 | })
60 | }
61 | // 获取视频列表成功
62 | response.GetFavoriteListSucceeded(c, videoList)
63 | }
64 |
--------------------------------------------------------------------------------
/test/test_helpers.go:
--------------------------------------------------------------------------------
1 | package test
2 |
3 | import (
4 | "fmt"
5 | "math/rand"
6 | "net/http"
7 | "testing"
8 | "time"
9 |
10 | "github.com/gavv/httpexpect/v2"
11 | "github.com/zheng-yi-yi/simpledouyin/config"
12 | )
13 |
14 | var serverAddr = "http://" + config.LOCAL_IP_ADDRESS + ":8080" // 服务器地址
15 |
16 | // newExpect , 创建并返回一个新的 httpexpect.Expect 实例,用于进行 HTTP 请求和断言
17 | func newExpect(t *testing.T) *httpexpect.Expect {
18 | return httpexpect.WithConfig(httpexpect.Config{
19 | Client: http.DefaultClient,
20 | BaseURL: serverAddr,
21 | Reporter: httpexpect.NewAssertReporter(t),
22 | Printers: []httpexpect.Printer{
23 | httpexpect.NewCompactPrinter(t),
24 | },
25 | })
26 | }
27 |
28 | // getTestUserIdAndToken , 获取测试用户的并获取其ID以及token
29 | func getTestUserIdAndToken(username string, password string, e *httpexpect.Expect) (int, string) {
30 | registerResp := e.POST("/douyin/user/register/").
31 | WithQuery("username", username).WithQuery("password", password).
32 | WithFormField("username", username).WithFormField("password", password).
33 | Expect().
34 | Status(http.StatusOK).
35 | JSON().Object()
36 |
37 | userId := 0
38 | token := registerResp.Value("token").String().Raw()
39 |
40 | if len(token) == 0 {
41 | loginResp := e.POST("/douyin/user/login/").
42 | WithQuery("username", username).WithQuery("password", password).
43 | WithFormField("username", username).WithFormField("password", password).
44 | Expect().
45 | Status(http.StatusOK).
46 | JSON().Object()
47 |
48 | loginToken := loginResp.Value("token").String()
49 | loginToken.Length().Gt(0)
50 | token = loginToken.Raw()
51 |
52 | userId = int(loginResp.Value("user_id").Number().Raw())
53 | } else {
54 | userId = int(registerResp.Value("user_id").Number().Raw())
55 | }
56 | return userId, token
57 | }
58 |
59 | // generateRandomUsername , 生成随机用户名
60 | func generateRandomUsername() string {
61 | // 定义可用字符集合
62 | characters := "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
63 | source := rand.NewSource(time.Now().UnixNano())
64 | randGen := rand.New(source)
65 | // 生成随机8个字符的用户名
66 | username := make([]byte, 8)
67 | for i := 0; i < 8; i++ {
68 | username[i] = characters[randGen.Intn(len(characters))]
69 | }
70 | return string(username)
71 | }
72 |
73 | // generateRandomPassword , 生成随机六位数密码
74 | func generateRandomPassword() string {
75 | source := rand.NewSource(time.Now().UnixNano())
76 | randGen := rand.New(source)
77 | password := fmt.Sprintf("%06d", randGen.Intn(1000000))
78 | return password
79 | }
80 |
--------------------------------------------------------------------------------
/services/video_service.go:
--------------------------------------------------------------------------------
1 | package services
2 |
3 | import (
4 | "log"
5 | "mime/multipart"
6 | "path/filepath"
7 |
8 | "github.com/gin-gonic/gin"
9 | "github.com/zheng-yi-yi/simpledouyin/config"
10 | "github.com/zheng-yi-yi/simpledouyin/models"
11 | "github.com/zheng-yi-yi/simpledouyin/utils"
12 | )
13 |
14 | type VideoService struct {
15 | }
16 |
17 | // 获取视频Feed
18 | func (videoService *VideoService) Feed(startTime string) *[]models.Video {
19 | var videoList *[]models.Video
20 | config.Database.
21 | Where("created_at <= ?", startTime).
22 | Preload("User").
23 | Order("created_at DESC").
24 | Limit(config.VIDEO_STREAM_BATCH_SIZE).
25 | Find(&videoList)
26 | return videoList
27 | }
28 |
29 | // 获取指定用户发布的视频列表
30 | func (videoService *VideoService) UserPublishList(userId uint) []*models.Video {
31 | var videoList []*models.Video
32 | config.Database.
33 | Where("user_id = ?", userId).
34 | Preload("User").
35 | Find(&videoList)
36 | return videoList
37 | }
38 |
39 | // GetVideoInfoByIds ,根据点赞过的视频ID ,取出所有对应的视频信息
40 | func (videoService *VideoService) GetVideoInfoByIds(videoIds []uint) []*models.Video {
41 | var videoList []*models.Video
42 | config.Database.Where("id IN ?", videoIds).Preload("User").Find(&videoList)
43 | return videoList
44 | }
45 |
46 | // 用户投稿视频
47 | func (videoService *VideoService) SaveVideoFile(c *gin.Context, file *multipart.FileHeader, userId uint, title string) error {
48 | // 获取视频文件目标路径
49 | videoDst := utils.GetVideoDst(file, userId)
50 | // 保存上传的视频文件到目标路径
51 | if err := c.SaveUploadedFile(file, videoDst); err != nil {
52 | // 视频文件保存失败
53 | log.Printf("视频文件保存失败: %s", err)
54 | return err
55 | }
56 |
57 | // 获取视频文件和封面图片文件的本地路径
58 | videoPath := utils.GetVideoPath(file, userId)
59 | coverPath := utils.GetCoverPath(file, userId)
60 |
61 | // 使用 Ffmpeg 函数生成封面图片
62 | utils.Ffmpeg(videoPath, coverPath)
63 |
64 | // 生成 playUrl 与 coverUrl
65 | fileExt := filepath.Ext(file.Filename)
66 | playUrl := "videos/" + utils.GetVideoName(userId) + fileExt
67 | coverUrl := "images/" + utils.GetCoverName(userId)
68 | // 创建视频记录
69 | video := models.Video{
70 | UserId: userId,
71 | PlayUrl: playUrl,
72 | CoverUrl: coverUrl,
73 | Description: title,
74 | }
75 | err := config.Database.Create(&video).Error
76 | if err != nil {
77 | log.Printf("视频数据保存失败: %s", err)
78 | return err
79 | }
80 | // 成功创建视频后,调用 IncrementWorkCount 函数
81 | if err := models.IncrementWorkCount(userId); err != nil {
82 | log.Printf("用户作品数添加失败: %s", err)
83 | return err
84 | }
85 | return nil
86 | }
87 |
--------------------------------------------------------------------------------
/controllers/response/video_resp.go:
--------------------------------------------------------------------------------
1 | package response
2 |
3 | import (
4 | "net/http"
5 |
6 | "github.com/gin-gonic/gin"
7 | )
8 |
9 | type Video struct {
10 | Id int64 `json:"id,omitempty"` // 视频唯一标识
11 | Author User `json:"author"` // 视频作者信息
12 | CommentCount int64 `json:"comment_count"` // 视频的评论总数
13 | CoverUrl string `json:"cover_url"` // 视频封面地址
14 | PlayUrl string `json:"play_url"` // 视频播放地址
15 | FavoriteCount int64 `json:"favorite_count"` // 视频的点赞总数
16 | IsFavorite bool `json:"is_favorite"` // true-已点赞,false-未点赞
17 | Title string `json:"title"` // 视频标题
18 | }
19 |
20 | type VideoList []Video
21 |
22 | // 视频流响应
23 | type FeedResponse struct {
24 | StatusCode int32 `json:"status_code"` // 状态码,0-成功,其他值-失败
25 | StatusMsg string `json:"status_msg"` // 返回状态描述
26 | VideoList []Video `json:"video_list,omitempty"` // 视频猎豹
27 | NextTime int64 `json:"next_time,omitempty"` // 本次返回的视频中,发布最早的时间,作为下次请求时的latest_time
28 | }
29 |
30 | // 获取视频流成功
31 | func GetFeedSuccess(c *gin.Context, nextTime int64, videoList []Video) {
32 | c.JSON(http.StatusOK, FeedResponse{
33 | StatusCode: 0,
34 | StatusMsg: "成功获取视频流",
35 | VideoList: videoList,
36 | NextTime: nextTime,
37 | })
38 | }
39 |
40 | // 视频投稿响应
41 | type PostVideoResponse struct {
42 | StatusCode int32 `json:"status_code"` // 状态码,0-成功,其他值-失败
43 | StatusMsg string `json:"status_msg"` // 返回状态描述
44 | }
45 |
46 | // VideoFileAccessError , 视频文件获取失败
47 | func VideoFileAccessError(c *gin.Context) {
48 | c.JSON(http.StatusInternalServerError, PostVideoResponse{
49 | StatusCode: 1,
50 | StatusMsg: "视频文件获取失败",
51 | })
52 | }
53 |
54 | // VideoFileSaveFailure , 视频文件保存失败
55 | func VideoFileSaveFailure(c *gin.Context) {
56 | c.JSON(http.StatusInternalServerError, PostVideoResponse{
57 | StatusCode: 1,
58 | StatusMsg: "视频文件保存失败",
59 | })
60 | }
61 |
62 | // PostVideoSuccessful , 视频发布成功
63 | func PostVideoSuccessful(c *gin.Context) {
64 | c.JSON(http.StatusOK, PostVideoResponse{
65 | StatusCode: 0,
66 | StatusMsg: "视频发布成功",
67 | })
68 | }
69 |
70 | // 发布列表响应
71 | type VideoListResponse struct {
72 | StatusCode int32 `json:"status_code"` // 状态码,0-成功,其他值-失败
73 | StatusMsg string `json:"status_msg"` // 返回状态描述
74 | VideoList []Video `json:"video_list,omitempty"` // 用户发布的视频列表
75 | }
76 |
77 | // GetPublishListSuccess , 视频列表获取成功
78 | func GetPublishListSuccess(c *gin.Context, videoList []Video) {
79 | c.JSON(http.StatusOK, VideoListResponse{
80 | StatusCode: 0,
81 | StatusMsg: "视频列表获取成功",
82 | VideoList: videoList,
83 | })
84 | }
85 |
--------------------------------------------------------------------------------
/utils/videoPublish.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import (
4 | "fmt"
5 | "mime/multipart"
6 | "os"
7 | "path/filepath"
8 |
9 | "github.com/zheng-yi-yi/simpledouyin/models"
10 | )
11 |
12 | // GetVideoDst , 根据视频文件和用户ID获取视频目标路径
13 | func GetVideoDst(file *multipart.FileHeader, userId uint) string {
14 | // 获取视频名称
15 | videoName := GetVideoName(userId)
16 |
17 | // 获取文件扩展名
18 | fileExt := filepath.Ext(file.Filename)
19 |
20 | // 根据视频名称和文件扩展名生成新的文件名
21 | filename := videoName + fileExt
22 |
23 | // 将文件名与视频文件目录路径合并得到完整的视频目标路径
24 | videoDst := filepath.Join("./public/videos/", filename)
25 |
26 | // 返回视频目标路径
27 | return videoDst
28 | }
29 |
30 | // GetVideoPath , 根据视频文件和用户ID获取视频文件路径
31 | func GetVideoPath(file *multipart.FileHeader, userId uint) string {
32 | // 获取当前工作目录
33 | pwd, getPwdErr := os.Getwd()
34 | if getPwdErr != nil {
35 | fmt.Println(getPwdErr.Error())
36 | return ""
37 | }
38 |
39 | // 获取视频目标路径
40 | videoDst := GetVideoDst(file, userId)
41 |
42 | // 将当前工作目录与视频目标路径合并得到完整的视频文件路径
43 | videoPath := filepath.Join(pwd, videoDst)
44 |
45 | // 返回视频文件路径
46 | return videoPath
47 | }
48 |
49 | // GetCoverDst , 根据文件信息和用户ID生成封面图片文件的目标路径。
50 | func GetCoverDst(file *multipart.FileHeader, userId uint) string {
51 | // 调用 GetCoverName 函数,根据用户ID生成封面图片文件名
52 | coverName := GetCoverName(userId)
53 |
54 | // 如果封面图片文件名为空
55 | if coverName == "" {
56 | return "" // 返回空字符串表示无法生成封面图片文件目标路径
57 | }
58 |
59 | // 使用格式化字符串生成封面图片文件目标路径,格式为 "./public/images/文件名"
60 | coverDst := filepath.Join("./public/images/", coverName)
61 |
62 | // 返回生成的封面图片文件目标路径
63 | return coverDst
64 | }
65 |
66 | // GetCoverPath , 根据视频文件和用户ID获取封面图片文件路径
67 | func GetCoverPath(file *multipart.FileHeader, userId uint) string {
68 | // 获取当前工作目录
69 | pwd, getPwdErr := os.Getwd()
70 | if getPwdErr != nil {
71 | fmt.Println(getPwdErr.Error())
72 | return ""
73 | }
74 |
75 | // 获取封面图片目标路径
76 | coverDst := GetCoverDst(file, userId)
77 |
78 | // 将当前工作目录与封面图片目标路径合并得到完整的封面图片文件路径
79 | coverPath := filepath.Join(pwd, coverDst)
80 |
81 | // 返回封面图片文件路径
82 | return coverPath
83 | }
84 |
85 | // GetVideoName , 根据用户ID获取视频名称
86 | func GetVideoName(userId uint) string {
87 | // 调用 models 包中的 GetVideoCount 函数,获取用户的视频数量和可能的错误
88 | VideoCount, err := models.GetVideoCount(userId)
89 |
90 | // 如果获取视频数量时发生错误
91 | if err != nil {
92 | VideoCount = 0 // 将视频数量设置为0
93 | }
94 |
95 | // 使用格式化字符串生成视频文件名,格式为 "用户ID_视频数量+1"
96 | filename := fmt.Sprintf("%d_%d", userId, VideoCount+1)
97 |
98 | // 返回生成的视频文件名
99 | return filename
100 | }
101 |
102 | // GetCoverName , 根据用户ID生成封面图片名称
103 | func GetCoverName(userId uint) string {
104 | // 调用 models 包中的 GetVideoCount 函数,获取用户的视频数量和可能的错误
105 | VideoCount, err := models.GetVideoCount(userId)
106 |
107 | // 如果获取视频数量时发生错误
108 | if err != nil {
109 | VideoCount = 0
110 | }
111 |
112 | // 使用格式化字符串生成封面图片文件名,格式为 "用户ID_视频数量+1.png"
113 | coverName := fmt.Sprintf("%d_%d.png", userId, VideoCount+1)
114 |
115 | // 返回生成的封面图片文件名
116 | return coverName
117 | }
118 |
--------------------------------------------------------------------------------
/services/favorite_service.go:
--------------------------------------------------------------------------------
1 | package services
2 |
3 | import (
4 | "github.com/zheng-yi-yi/simpledouyin/config"
5 | "github.com/zheng-yi-yi/simpledouyin/models"
6 | "gorm.io/gorm"
7 | )
8 |
9 | type FavoriteService struct {
10 | }
11 |
12 | // IsFavorite ,判断用户是否点赞了视频
13 | func (s *FavoriteService) IsFavorite(userId, videoId uint) bool {
14 | // 创建一个 Favorite 结构体实例,用于存储查询结果
15 | var favorite models.Favorite
16 |
17 | // 在数据库中查找匹配的点赞记录
18 | result := config.Database.Where("user_id = ? AND video_id = ?", userId, videoId).First(&favorite)
19 |
20 | // 检查是否找到匹配的点赞记录
21 | return result.Error == nil
22 | }
23 |
24 | // AddLike ,点赞操作
25 | func (s *FavoriteService) AddLike(userId, videoId uint) error {
26 | // 获取数据库连接实例
27 | db := config.Database
28 |
29 | // 新建一条点赞记录
30 | favorite := models.Favorite{
31 | UserId: userId,
32 | VideoId: videoId,
33 | }
34 | result := db.Create(&favorite)
35 | if result.Error != nil {
36 | return result.Error
37 | }
38 |
39 | // 将该视频的获赞总数加一
40 | if err := models.IncrementVideoLikeCount(uint(videoId)); err != nil {
41 | return err
42 | }
43 |
44 | // 将本次点赞用户的点赞数加一
45 | if err := models.IncrementUserLikeCount(userId); err != nil {
46 | return err
47 | }
48 |
49 | // 根据该视频id来获取其作者id
50 | author_id, err := models.GetAuthorIDForVideo(uint(videoId))
51 | if err != nil {
52 | return err
53 | }
54 |
55 | // 将视频作者的获赞总数加一
56 | if err := models.IncrementAuthorTotalFavorited(author_id); err != nil {
57 | return err
58 | }
59 |
60 | return nil
61 | }
62 |
63 | // CancelLike ,取消点赞操作
64 | func (s *FavoriteService) CancelLike(userId, videoId uint) error {
65 | // 获取数据库连接实例
66 | db := config.Database
67 |
68 | // 查找要删除的现有点赞关系
69 | var existingFavorite models.Favorite
70 | result := db.Where("user_id = ? AND video_id = ?", userId, videoId).First(&existingFavorite)
71 | if result.Error != nil {
72 | if result.Error == gorm.ErrRecordNotFound {
73 | // 如果点赞关系不存在,则无错误地返回
74 | return nil
75 | }
76 | return result.Error
77 | }
78 |
79 | // 删除现有的点赞关系
80 | result = db.Delete(&existingFavorite)
81 | if result.Error != nil {
82 | return result.Error
83 | }
84 |
85 | // 成功取消点赞后,调用 DecrementUserLikeCount 函数,将用户的点赞数减一
86 | if err := models.DecrementUserLikeCount(userId); err != nil {
87 | return err
88 | }
89 |
90 | // 成功取消点赞后,调用 DecrementVideoLikeCount 函数,将视频的获赞数加一
91 | if err := models.DecrementVideoLikeCount(uint(videoId)); err != nil {
92 | return err
93 | }
94 |
95 | // 获取该取消赞的视频作者id
96 | author_id, err := models.GetAuthorIDForVideo(uint(videoId))
97 | if err != nil {
98 | return err
99 | }
100 |
101 | // 将视频作者的获赞总数减一
102 | if err := models.DecrementAuthorTotalFavorited(author_id); err != nil {
103 | return err
104 | }
105 |
106 | return nil // 操作成功,返回 nil 表示没有错误
107 | }
108 |
109 | // GetFavoriteList ,根据用户ID取出该用户点赞的所有视频ID
110 | func (s *FavoriteService) GetFavoriteList(userId uint) ([]uint, error) {
111 | var favorites []models.Favorite
112 |
113 | // 查询该用户点赞的所有记录
114 | result := config.Database.Where("user_id = ?", userId).Find(&favorites)
115 | if result.Error != nil {
116 | return nil, result.Error
117 | }
118 |
119 | // 提取视频ID并放入列表
120 | var videoIDs []uint
121 | for _, favorite := range favorites {
122 | videoIDs = append(videoIDs, favorite.VideoId)
123 | }
124 |
125 | return videoIDs, nil
126 | }
127 |
--------------------------------------------------------------------------------
/controllers/response/relation_resp.go:
--------------------------------------------------------------------------------
1 | package response
2 |
3 | import (
4 | "net/http"
5 |
6 | "github.com/gin-gonic/gin"
7 | )
8 |
9 | // 关注操作响应
10 | type RelationActionResponse struct {
11 | StatusCode int64 `json:"status_code"` // 状态码,0-成功,其他值-失败
12 | StatusMsg string `json:"status_msg"` // 返回状态描述
13 | }
14 |
15 | // RelationActionSucceeded , 关注成功
16 | func RelationActionSucceeded(c *gin.Context) {
17 | c.JSON(http.StatusOK, RelationActionResponse{
18 | StatusCode: 0,
19 | StatusMsg: "关注成功",
20 | })
21 | }
22 |
23 | // RelationActionError , 关注失败
24 | func RelationActionError(c *gin.Context) {
25 | c.JSON(http.StatusInternalServerError, RelationActionResponse{
26 | StatusCode: 1,
27 | StatusMsg: "关注失败",
28 | })
29 | }
30 |
31 | // CancelRelationSucceeded , 取关成功
32 | func CancelRelationSucceeded(c *gin.Context) {
33 | c.JSON(http.StatusOK, RelationActionResponse{
34 | StatusCode: 0,
35 | StatusMsg: "取关成功",
36 | })
37 | }
38 |
39 | // CancelRelationError , 取关失败
40 | func CancelRelationError(c *gin.Context) {
41 | c.JSON(http.StatusInternalServerError, RelationActionResponse{
42 | StatusCode: 1,
43 | StatusMsg: "取关失败",
44 | })
45 | }
46 |
47 | // 关注列表响应
48 | type FollowListResponse struct {
49 | StatusCode string `json:"status_code"` // 状态码,0-成功,其他值-失败
50 | StatusMsg string `json:"status_msg"` // 返回状态描述
51 | UserList []User `json:"user_list,omitempty"` // 用户信息列表
52 | }
53 |
54 | // GetFollowListSucceeded , 关注列表获取成功
55 | func GetFollowListSucceeded(c *gin.Context, userList []User) {
56 | c.JSON(http.StatusOK, FollowListResponse{
57 | StatusCode: "0",
58 | StatusMsg: "关注列表获取成功",
59 | UserList: userList,
60 | })
61 | }
62 |
63 | // GetFollowListError , 关注列表获取失败
64 | func GetFollowListError(c *gin.Context) {
65 | c.JSON(http.StatusInternalServerError, FollowListResponse{
66 | StatusCode: "1",
67 | StatusMsg: "关注列表获取失败",
68 | })
69 | }
70 |
71 | // 粉丝列表响应
72 | type FollowerListResponse struct {
73 | StatusCode string `json:"status_code"` // 状态码,0-成功,其他值-失败
74 | StatusMsg string `json:"status_msg"` // 返回状态描述
75 | UserList []User `json:"user_list,omitempty"` // 用户列表
76 | }
77 |
78 | // GetFollowerListSucceeded , 粉丝列表获取成功
79 | func GetFollowerListSucceeded(c *gin.Context, userList []User) {
80 | c.JSON(http.StatusOK, FollowerListResponse{
81 | StatusCode: "0",
82 | StatusMsg: "粉丝列表获取成功",
83 | UserList: userList,
84 | })
85 | }
86 |
87 | // GetFollowerListError , 粉丝列表获取失败
88 | func GetFollowerListError(c *gin.Context) {
89 | c.JSON(http.StatusInternalServerError, FollowerListResponse{
90 | StatusCode: "1",
91 | StatusMsg: "粉丝列表获取失败",
92 | })
93 | }
94 |
95 | // 好友列表响应
96 | type FriendListResponse struct {
97 | StatusCode string `json:"status_code"` // 状态码,0-成功,其他值-失败
98 | StatusMsg string `json:"status_msg"` // 返回状态描述
99 | UserList []User `json:"user_list,omitempty"` // 用户列表
100 | }
101 |
102 | // GetFriendListSucceeded , 好友列表获取成功
103 | func GetFriendListSucceeded(c *gin.Context, userList []User) {
104 | c.JSON(http.StatusOK, FriendListResponse{
105 | StatusCode: "0",
106 | StatusMsg: "粉丝列表获取成功",
107 | UserList: userList,
108 | })
109 | }
110 |
111 | // GetFriendListError , 好友列表获取失败
112 | func GetFriendListError(c *gin.Context) {
113 | c.JSON(http.StatusInternalServerError, FriendListResponse{
114 | StatusCode: "1",
115 | StatusMsg: "粉丝列表获取失败",
116 | })
117 | }
118 |
--------------------------------------------------------------------------------
/middlewares/jwt.go:
--------------------------------------------------------------------------------
1 | package middlewares
2 |
3 | import (
4 | "log"
5 | "net/http"
6 | "time"
7 |
8 | "github.com/gin-gonic/gin"
9 | "github.com/golang-jwt/jwt/v5"
10 | "github.com/zheng-yi-yi/simpledouyin/config"
11 | )
12 |
13 | // 密钥
14 | var JwtKey = []byte(config.AUTH_KEY)
15 |
16 | // 一些常量
17 | const (
18 | ISSUE = "SimpleDouyin"
19 | CheckFailed = "校验失败"
20 | ParseTokenFailed = "解析token失败"
21 | GetTokenFailed = "获取token失败"
22 | )
23 |
24 | // MyClaims,定义 JWT 的声明
25 | type MyClaims struct {
26 | UserID uint
27 | Username string
28 | Password string
29 | jwt.RegisteredClaims
30 | }
31 |
32 | // 鉴权失败响应
33 | type AuthFailResponse struct {
34 | StatusCode int64 `json:"status_code"` // 状态码,0-成功,其他值-失败
35 | StatusMsg string `json:"status_msg"` // 返回状态描述
36 | }
37 |
38 | // GenerateToken , 生成token
39 | func GenerateToken(userId uint, username, password string) (string, error) {
40 | claims := MyClaims{
41 | userId,
42 | username, // 用户名
43 | password, // 密码
44 | jwt.RegisteredClaims{
45 | ExpiresAt: jwt.NewNumericDate(time.Now().Add(24 * time.Hour)), // 过期时间(通常相对于当前时间设置)
46 | IssuedAt: jwt.NewNumericDate(time.Now()), // 令牌的发行时间(即创建时间)
47 | NotBefore: jwt.NewNumericDate(time.Now()), // 令牌生效时间(在此时间之前令牌无效)
48 | Issuer: ISSUE, // 创建此令牌的实体
49 | },
50 | }
51 | token, err := jwt.NewWithClaims(jwt.SigningMethodHS256, claims).SignedString(JwtKey)
52 | if err != nil {
53 | return "", err
54 | }
55 | return token, nil
56 | }
57 |
58 | // Auth , 验证 JWT token,并将其中的用户信息解析后存储在 Gin 的上下文中
59 | func Auth() gin.HandlerFunc {
60 | return func(c *gin.Context) {
61 | // 获取token
62 | tokenString := c.Query("token")
63 | if len(tokenString) == 0 {
64 | c.Set("userID", uint(0))
65 | c.Next()
66 | return
67 | }
68 | // 解析 token
69 | token, err := jwt.ParseWithClaims(tokenString, &MyClaims{}, func(token *jwt.Token) (interface{}, error) {
70 | return JwtKey, nil
71 | })
72 | if err != nil {
73 | c.JSON(http.StatusInternalServerError, AuthFailResponse{StatusCode: 1, StatusMsg: ParseTokenFailed})
74 | c.Abort()
75 | return
76 | }
77 | // 校验鉴权的声明
78 | if token != nil {
79 | claims, ok := token.Claims.(*MyClaims)
80 | if ok && token.Valid {
81 | c.Set("userID", claims.UserID)
82 | c.Next()
83 | return
84 | }
85 | }
86 | c.JSON(http.StatusInternalServerError, AuthFailResponse{StatusCode: 1, StatusMsg: CheckFailed})
87 | c.Abort()
88 | }
89 | }
90 |
91 | // UserPublishAuth , 用户发布视频前进行用户验证
92 | // 由于客户端提交表单数据时,
93 | // 包含的token字段是在请求体中以 multipart/form-data 的方式进行传递
94 | // 因此需要使用 c.PostForm 方法来获取表单数据中的字段值
95 | func UserPublishAuth() gin.HandlerFunc {
96 | return func(c *gin.Context) {
97 | // 获取token
98 | tokenString := c.PostForm("token")
99 | if len(tokenString) == 0 {
100 | c.Set("userID", uint(0))
101 | c.Next()
102 | return
103 | }
104 | // 解析 token
105 | token, err := jwt.ParseWithClaims(tokenString, &MyClaims{}, func(token *jwt.Token) (interface{}, error) {
106 | return JwtKey, nil
107 | })
108 | if err != nil {
109 | log.Println("token解析失败")
110 | c.JSON(http.StatusBadRequest, AuthFailResponse{StatusCode: 1, StatusMsg: ParseTokenFailed})
111 | c.Abort()
112 | return
113 | }
114 | // 校验鉴权的声明
115 | if token != nil {
116 | claims, ok := token.Claims.(*MyClaims)
117 | if ok && token.Valid {
118 | c.Set("userID", claims.UserID)
119 | c.Next()
120 | return
121 | }
122 | }
123 | log.Println("校验鉴权失败")
124 | c.JSON(http.StatusBadRequest, AuthFailResponse{StatusCode: 1, StatusMsg: CheckFailed})
125 | c.Abort()
126 | }
127 | }
128 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/zheng-yi-yi/simpledouyin
2 |
3 | go 1.20
4 |
5 | require (
6 | github.com/fsnotify/fsnotify v1.6.0
7 | github.com/gavv/httpexpect/v2 v2.15.0
8 | github.com/gin-gonic/gin v1.9.1
9 | github.com/golang-jwt/jwt/v5 v5.0.0
10 | github.com/spf13/viper v1.16.0
11 | github.com/stretchr/testify v1.8.3
12 | golang.org/x/crypto v0.9.0
13 | gorm.io/driver/mysql v1.5.1
14 | gorm.io/gorm v1.25.2
15 | )
16 |
17 | require (
18 | github.com/ajg/form v1.5.1 // indirect
19 | github.com/andybalholm/brotli v1.0.4 // indirect
20 | github.com/bytedance/sonic v1.9.1 // indirect
21 | github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
22 | github.com/davecgh/go-spew v1.1.1 // indirect
23 | github.com/fatih/color v1.13.0 // indirect
24 | github.com/fatih/structs v1.1.0 // indirect
25 | github.com/gabriel-vasile/mimetype v1.4.2 // indirect
26 | github.com/gin-contrib/sse v0.1.0 // indirect
27 | github.com/go-playground/locales v0.14.1 // indirect
28 | github.com/go-playground/universal-translator v0.18.1 // indirect
29 | github.com/go-playground/validator/v10 v10.14.0 // indirect
30 | github.com/go-sql-driver/mysql v1.7.0 // indirect
31 | github.com/gobwas/glob v0.2.3 // indirect
32 | github.com/goccy/go-json v0.10.2 // indirect
33 | github.com/google/go-querystring v1.1.0 // indirect
34 | github.com/gorilla/websocket v1.4.2 // indirect
35 | github.com/hashicorp/hcl v1.0.0 // indirect
36 | github.com/hpcloud/tail v1.0.0 // indirect
37 | github.com/imkira/go-interpol v1.1.0 // indirect
38 | github.com/jinzhu/inflection v1.0.0 // indirect
39 | github.com/jinzhu/now v1.1.5 // indirect
40 | github.com/json-iterator/go v1.1.12 // indirect
41 | github.com/klauspost/compress v1.15.0 // indirect
42 | github.com/klauspost/cpuid/v2 v2.2.4 // indirect
43 | github.com/leodido/go-urn v1.2.4 // indirect
44 | github.com/magiconair/properties v1.8.7 // indirect
45 | github.com/mattn/go-colorable v0.1.13 // indirect
46 | github.com/mattn/go-isatty v0.0.19 // indirect
47 | github.com/mitchellh/go-wordwrap v1.0.1 // indirect
48 | github.com/mitchellh/mapstructure v1.5.0 // indirect
49 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
50 | github.com/modern-go/reflect2 v1.0.2 // indirect
51 | github.com/pelletier/go-toml/v2 v2.0.8 // indirect
52 | github.com/pmezard/go-difflib v1.0.0 // indirect
53 | github.com/sanity-io/litter v1.5.5 // indirect
54 | github.com/sergi/go-diff v1.0.0 // indirect
55 | github.com/spf13/afero v1.9.5 // indirect
56 | github.com/spf13/cast v1.5.1 // indirect
57 | github.com/spf13/jwalterweatherman v1.1.0 // indirect
58 | github.com/spf13/pflag v1.0.5 // indirect
59 | github.com/subosito/gotenv v1.4.2 // indirect
60 | github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
61 | github.com/ugorji/go/codec v1.2.11 // indirect
62 | github.com/valyala/bytebufferpool v1.0.0 // indirect
63 | github.com/valyala/fasthttp v1.34.0 // indirect
64 | github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
65 | github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
66 | github.com/xeipuuv/gojsonschema v1.2.0 // indirect
67 | github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0 // indirect
68 | github.com/yudai/gojsondiff v1.0.0 // indirect
69 | github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 // indirect
70 | golang.org/x/arch v0.3.0 // indirect
71 | golang.org/x/net v0.10.0 // indirect
72 | golang.org/x/sys v0.8.0 // indirect
73 | golang.org/x/text v0.9.0 // indirect
74 | google.golang.org/protobuf v1.30.0 // indirect
75 | gopkg.in/fsnotify.v1 v1.4.7 // indirect
76 | gopkg.in/ini.v1 v1.67.0 // indirect
77 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
78 | gopkg.in/yaml.v3 v3.0.1 // indirect
79 | moul.io/http2curl/v2 v2.3.0 // indirect
80 | )
81 |
--------------------------------------------------------------------------------
/test/social_api_test.go:
--------------------------------------------------------------------------------
1 | package test
2 |
3 | import (
4 | "net/http"
5 | "testing"
6 |
7 | "github.com/stretchr/testify/assert"
8 | )
9 |
10 | // 测试 “关系” 模块
11 | func TestRelation(t *testing.T) {
12 |
13 | e := newExpect(t)
14 |
15 | // 获取测试用户A的用户 ID 和 token
16 | userIdA, tokenA := getTestUserIdAndToken("RelationTestA", "114589", e)
17 | // 获取测试用户B的用户 ID 和令牌
18 | userIdB, tokenB := getTestUserIdAndToken("RelationTestB", "223657", e)
19 |
20 | // 发送 POST 请求来执行关注操作,将用户 A 关注用户 B
21 | relationResp := e.POST("/douyin/relation/action/").
22 | WithQuery("token", tokenA).WithQuery("to_user_id", userIdB).WithQuery("action_type", 1).
23 | WithFormField("token", tokenA).WithFormField("to_user_id", userIdB).WithFormField("action_type", 1).
24 | Expect().
25 | Status(http.StatusOK).
26 | JSON().Object()
27 | relationResp.Value("status_code").Number().IsEqual(0) // 确保关注操作成功
28 |
29 | // 发送 GET 请求来获取用户 A 的关注列表
30 | followListResp := e.GET("/douyin/relation/follow/list/").
31 | WithQuery("token", tokenA).WithQuery("user_id", userIdA).
32 | WithFormField("token", tokenA).WithFormField("user_id", userIdA).
33 | Expect().
34 | Status(http.StatusOK).
35 | JSON().Object()
36 | followListResp.Value("status_code").String().IsEqual("0") // 确保获取关注列表成功
37 |
38 | // 检查关注列表中是否包含测试用户 B
39 | containTestUserB := false
40 | for _, element := range followListResp.Value("user_list").Array().Iter() {
41 | user := element.Object()
42 | user.ContainsKey("id") // 确保用户对象包含 ID
43 | if int(user.Value("id").Number().Raw()) == userIdB {
44 | containTestUserB = true
45 | }
46 | }
47 | // 使用断言确保测试用户 B 在关注列表中
48 | assert.True(t, containTestUserB, "关注测试用户失败")
49 |
50 | // 发送 GET 请求来获取用户 B 的粉丝列表
51 | followerListResp := e.GET("/douyin/relation/follower/list/").
52 | WithQuery("token", tokenB).WithQuery("user_id", userIdB).
53 | WithFormField("token", tokenB).WithFormField("user_id", userIdB).
54 | Expect().
55 | Status(http.StatusOK).
56 | JSON().Object()
57 | followerListResp.Value("status_code").String().IsEqual("0") // 确保获取粉丝列表成功
58 |
59 | // 检查粉丝列表中是否包含测试用户 A
60 | containTestUserA := false
61 | for _, element := range followerListResp.Value("user_list").Array().Iter() {
62 | user := element.Object()
63 | user.ContainsKey("id") // 确保用户对象包含 ID
64 | if int(user.Value("id").Number().Raw()) == userIdA {
65 | containTestUserA = true
66 | }
67 | }
68 | // 使用断言确保测试用户 A 在粉丝列表中
69 | assert.True(t, containTestUserA, "关注者测试用户失败")
70 | }
71 |
72 | // 测试 “消息” 模块
73 | func TestChat(t *testing.T) {
74 |
75 | e := newExpect(t)
76 |
77 | // 获取测试用户A的用户 ID 和 token
78 | userIdA, tokenA := getTestUserIdAndToken("ChatTestA", "881246", e)
79 | // 获取测试用户B的用户 ID 和令牌
80 | userIdB, tokenB := getTestUserIdAndToken("ChatTestB", "958831", e)
81 |
82 | // 发送 POST 请求来发送消息给用户 B
83 | messageResp := e.POST("/douyin/message/action/").
84 | WithQuery("token", tokenA).WithQuery("to_user_id", userIdB).WithQuery("action_type", 1).WithQuery("content", "Send to UserB").
85 | WithFormField("token", tokenA).WithFormField("to_user_id", userIdB).WithFormField("action_type", 1).WithQuery("content", "Send to UserB").
86 | Expect().
87 | Status(http.StatusOK).
88 | JSON().Object()
89 | messageResp.Value("status_code").Number().IsEqual(0) // 确保发送消息操作成功
90 |
91 | // 发送 GET 请求来获取用户 A 和用户 B 之间的聊天消息列表
92 | chatResp := e.GET("/douyin/message/chat/").
93 | WithQuery("token", tokenA).WithQuery("to_user_id", userIdB).
94 | WithFormField("token", tokenA).WithFormField("to_user_id", userIdB).
95 | Expect().
96 | Status(http.StatusOK).
97 | JSON().Object()
98 | chatResp.Value("status_code").String().IsEqual("0") // 确保获取聊天消息列表成功
99 | chatResp.Value("message_list").Array().NotEmpty() // 确保聊天消息列表不为空
100 |
101 | // 发送 GET 请求来获取用户 B 和用户 A 之间的聊天消息列表
102 | chatResp = e.GET("/douyin/message/chat/").
103 | WithQuery("token", tokenB).WithQuery("to_user_id", userIdA).
104 | WithFormField("token", tokenB).WithFormField("to_user_id", userIdA).
105 | Expect().
106 | Status(http.StatusOK).
107 | JSON().Object()
108 | chatResp.Value("status_code").String().IsEqual("0") // 确保获取聊天消息列表成功
109 | chatResp.Value("message_list").Array().NotEmpty() // 确保聊天消息列表不为空
110 |
111 | }
112 |
--------------------------------------------------------------------------------
/test/base_api_test.go:
--------------------------------------------------------------------------------
1 | package test
2 |
3 | import (
4 | "net/http"
5 | "testing"
6 | )
7 |
8 | // 测试 “视频流” 功能
9 | func TestFeed(t *testing.T) {
10 | e := newExpect(t)
11 | feedResp := e.GET("/douyin/feed/").Expect().Status(http.StatusOK).JSON().Object()
12 | feedResp.Value("status_code").Number().IsEqual(0)
13 | feedResp.Value("video_list").Array().Length().Gt(0)
14 | for _, element := range feedResp.Value("video_list").Array().Iter() {
15 | video := element.Object()
16 | video.ContainsKey("id")
17 | video.ContainsKey("author")
18 | video.Value("play_url").String().NotEmpty()
19 | video.Value("cover_url").String().NotEmpty()
20 | }
21 | }
22 |
23 | // 测试 "用户" 模块
24 | func TestUserAction(t *testing.T) {
25 |
26 | e := newExpect(t)
27 |
28 | // 随机得到一个用户名以及密码
29 | var registerName = generateRandomUsername()
30 | var registerPwd = generateRandomPassword()
31 |
32 | // 注册用户并验证响应
33 | registerResp := e.POST("/douyin/user/register/").
34 | WithQuery("username", registerName).
35 | WithQuery("password", registerPwd).
36 | WithFormField("username", registerName).
37 | WithFormField("password", registerPwd).
38 | Expect().
39 | // 确保HTTP状态码为200
40 | Status(http.StatusOK).
41 | // 解析响应为JSON对象
42 | JSON().Object()
43 | // 验证注册响应中的字段是否符合预期
44 | registerResp.Value("status_code").Number().IsEqual(0)
45 | registerResp.Value("user_id").Number().Gt(0)
46 | registerResp.Value("token").String().Length().Gt(0)
47 |
48 | // 登录用户并验证响应
49 | loginResp := e.POST("/douyin/user/login/").
50 | WithQuery("username", registerName).
51 | WithQuery("password", registerPwd).
52 | WithFormField("username", registerName).
53 | WithFormField("password", registerPwd).
54 | Expect().
55 | // 确保HTTP状态码为200
56 | Status(http.StatusOK).
57 | // 解析响应为JSON对象
58 | JSON().Object()
59 | // 验证登录响应中的字段
60 | loginResp.Value("status_code").Number().IsEqual(0)
61 | loginResp.Value("user_id").Number().Gt(0)
62 | loginResp.Value("token").String().Length().Gt(0)
63 |
64 | // 获取登录后的 token 和 用户 id。
65 | token := loginResp.Value("token").String().Raw()
66 | UserId := loginResp.Value("user_id").Number().Raw()
67 |
68 | // 获取用户信息并验证响应
69 | userResp := e.GET("/douyin/user/").
70 | WithQuery("token", token).
71 | WithQuery("user_id", UserId).
72 | Expect().
73 | // 确保HTTP状态码为200
74 | Status(http.StatusOK).
75 | // 解析响应为JSON对象
76 | JSON().Object()
77 | // 验证用户信息响应中的字段
78 | userResp.Value("status_code").Number().IsEqual(0)
79 | userInfo := userResp.Value("user").Object()
80 | userInfo.NotEmpty()
81 | userInfo.Value("id").Number().Gt(0)
82 | userInfo.Value("name").String().Length().Gt(0)
83 | }
84 |
85 | // 测试 “视频投稿、发布列表” 功能
86 | func TestPublishAndList(t *testing.T) {
87 | e := newExpect(t)
88 |
89 | // 随机得到一个用户名以及密码
90 | var publish_username = generateRandomUsername()
91 | var publish_password = generateRandomPassword()
92 |
93 | // 获取测试用户的 userId 和 token
94 | userId, token := getTestUserIdAndToken(publish_username, publish_password, e)
95 |
96 | // 发布视频请求并进行断言
97 | publishResp := e.POST("/douyin/publish/action/").
98 | WithMultipart().
99 | WithFile("data", "../public/videos/1_1.mp4"). // 添加要上传的视频文件路径
100 | WithFormField("token", token).
101 | WithFormField("title", "用户视频上传测试"). // 设置视频标题
102 | Expect().
103 | Status(http.StatusOK).
104 | JSON().Object()
105 |
106 | // 验证发布响应的状态码
107 | publishResp.Value("status_code").Number().IsEqual(0)
108 |
109 | // 获取发布列表请求并进行断言
110 | publishListResp := e.GET("/douyin/publish/list/").
111 | WithQuery("user_id", userId).WithQuery("token", token).
112 | Expect().
113 | Status(http.StatusOK).
114 | JSON().Object()
115 |
116 | // 验证发布列表响应的状态码
117 | publishListResp.Value("status_code").Number().IsEqual(0)
118 |
119 | // 验证视频列表不为空并遍历每个视频元素
120 | publishListResp.Value("video_list").Array().Length().Gt(0)
121 |
122 | for _, element := range publishListResp.Value("video_list").Array().Iter() {
123 | video := element.Object()
124 |
125 | // 验证视频对象中是否包含指定字段
126 | video.ContainsKey("id")
127 | video.ContainsKey("author")
128 |
129 | // 验证视频播放链接和封面链接不为空
130 | video.Value("play_url").String().NotEmpty()
131 | video.Value("cover_url").String().NotEmpty()
132 | }
133 | }
134 |
--------------------------------------------------------------------------------
/models/dao.go:
--------------------------------------------------------------------------------
1 | package models
2 |
3 | import (
4 | "github.com/zheng-yi-yi/simpledouyin/config"
5 | "gorm.io/gorm"
6 | )
7 |
8 | // 判断登录用户是否关注了视频作者
9 | func IsFollow(fromUserId, toUserId uint) bool {
10 | // 查找存在的关注关系
11 | var existingRelation Relation
12 | if err := config.Database.Where("from_user_id = ? AND to_user_id = ? AND cancel = 0", fromUserId, toUserId).First(&existingRelation).Error; err != nil {
13 | return false
14 | }
15 | return true
16 | }
17 |
18 | // 判断用户是否点赞当前视频
19 | func IsFavorite(userId, videoId uint) bool {
20 | // 创建一个 Favorite 结构体实例,用于存储查询结果
21 | var favorite Favorite
22 | // 在数据库中查找匹配的点赞记录
23 | result := config.Database.Where("user_id = ? AND video_id = ?", userId, videoId).First(&favorite)
24 | // 检查是否找到匹配的点赞记录
25 | return result.Error == nil
26 | }
27 |
28 | // IncrementWorkCount 增加用户的作品数。
29 | func IncrementWorkCount(userID uint) error {
30 | return config.Database.Model(&User{}).Where("id = ?", userID).Update("work_count", gorm.Expr("work_count + ?", 1)).Error
31 | }
32 |
33 | // IncrementUserLikeCount 增加用户的点赞数。
34 | func IncrementUserLikeCount(userID uint) error {
35 | return config.Database.Model(&User{}).Where("id = ?", userID).Update("favorite_count", gorm.Expr("favorite_count + ?", 1)).Error
36 | }
37 |
38 | // IncrementAuthorTotalFavorited 根据视频作者的id来将其获赞总数加一
39 | func IncrementAuthorTotalFavorited(userID uint) error {
40 | return config.Database.Model(&User{}).Where("id = ?", userID).Update("total_favorited", gorm.Expr("total_favorited + ?", 1)).Error
41 | }
42 |
43 | // DecrementAuthorTotalFavorited 根据视频作者的id来将其获赞总数减一
44 | func DecrementAuthorTotalFavorited(userID uint) error {
45 | return config.Database.Model(&User{}).Where("id = ?", userID).Update("total_favorited", gorm.Expr("total_favorited - ?", 1)).Error
46 | }
47 |
48 | // DecrementUserLikeCount 减少用户的点赞数。
49 | func DecrementUserLikeCount(userID uint) error {
50 | return config.Database.Model(&User{}).Where("id = ?", userID).Update("favorite_count", gorm.Expr("favorite_count - ?", 1)).Error
51 | }
52 |
53 | // GetVideoCount 根据用户ID查询其拥有的视频数量。
54 | func GetVideoCount(userId uint) (int64, error) {
55 | var videoCount int64
56 | result := config.Database.Model(&Video{}).Where("user_id = ?", userId).Count(&videoCount)
57 | if result.Error != nil {
58 | return 0, result.Error
59 | }
60 | return videoCount, nil
61 | }
62 |
63 | // IncrementCommentCount 增加指定视频的评论数(加一)
64 | func IncrementCommentCount(videoID uint) error {
65 | result := config.Database.Model(&Video{}).Where("id = ?", videoID).Update("comment_count", gorm.Expr("comment_count + ?", 1))
66 | if result.Error != nil {
67 | return result.Error
68 | }
69 | return nil
70 | }
71 |
72 | // DecreaseCommentCount 减少指定视频的评论数(减一)
73 | func DecreaseCommentCount(videoID uint) error {
74 | result := config.Database.Model(&Video{}).Where("id = ?", videoID).Update("comment_count", gorm.Expr("comment_count - ?", 1))
75 | if result.Error != nil {
76 | return result.Error
77 | }
78 | return nil
79 | }
80 |
81 | // GetAuthorIDForVideo 根据视频ID获取视频的作者ID
82 | func GetAuthorIDForVideo(videoID uint) (uint, error) {
83 | var authorID uint
84 | if err := config.Database.Model(&Video{}).Select("user_id").Where("id = ?", videoID).Scan(&authorID).Error; err != nil {
85 | return 0, err
86 | }
87 | return authorID, nil
88 | }
89 |
90 | // IncrementVideoLikeCount 增加视频获赞数
91 | func IncrementVideoLikeCount(videoID uint) error {
92 | result := config.Database.Model(&Video{}).Where("id = ?", videoID).Update("favorite_count", gorm.Expr("favorite_count + ?", 1))
93 | if result.Error != nil {
94 | return result.Error
95 | }
96 | return nil
97 | }
98 |
99 | // DecrementVideoLikeCount 减少视频获赞数
100 | func DecrementVideoLikeCount(videoID uint) error {
101 | result := config.Database.Model(&Video{}).Where("id = ?", videoID).Update("favorite_count", gorm.Expr("favorite_count - ?", 1))
102 | if result.Error != nil {
103 | return result.Error
104 | }
105 | return nil
106 | }
107 |
108 | // FetchData , 通过用户ID获取用户信息
109 | func FetchData(userId uint) (User, error) {
110 | var user User
111 | err := config.Database.Where("id = ?", userId).First(&user).Error
112 | if err != nil {
113 | return User{}, err
114 | }
115 | return user, nil
116 | }
117 |
--------------------------------------------------------------------------------
/controllers/video/feed.go:
--------------------------------------------------------------------------------
1 | package video
2 |
3 | import (
4 | "net/http"
5 | "time"
6 |
7 | "github.com/gin-gonic/gin"
8 | "github.com/golang-jwt/jwt/v5"
9 | "github.com/zheng-yi-yi/simpledouyin/config"
10 | "github.com/zheng-yi-yi/simpledouyin/controllers/response"
11 | "github.com/zheng-yi-yi/simpledouyin/middlewares"
12 | "github.com/zheng-yi-yi/simpledouyin/models"
13 | "github.com/zheng-yi-yi/simpledouyin/services"
14 | "github.com/zheng-yi-yi/simpledouyin/utils"
15 | )
16 |
17 | var VideoService services.VideoService
18 |
19 | // Feed , 获取视频流
20 | func Feed(c *gin.Context) {
21 | tokenString := c.Query("token")
22 | if len(tokenString) == 0 {
23 | // 未登录状态下的视频流获取
24 | NoLoginAccess(c)
25 | return
26 | }
27 | // 解析 token
28 | token, err := jwt.ParseWithClaims(tokenString, &middlewares.MyClaims{}, func(token *jwt.Token) (interface{}, error) {
29 | return middlewares.JwtKey, nil
30 | })
31 | if err != nil {
32 | c.JSON(http.StatusInternalServerError, middlewares.AuthFailResponse{StatusCode: 1, StatusMsg: middlewares.ParseTokenFailed})
33 | c.Abort()
34 | return
35 | }
36 | // 校验鉴权的声明
37 | claims, ok := token.Claims.(*middlewares.MyClaims)
38 | if ok && token.Valid {
39 | // 已登录状态下的视频流获取
40 | LoginAccess(c, claims.UserID)
41 | return
42 | }
43 | c.JSON(http.StatusInternalServerError, middlewares.AuthFailResponse{StatusCode: 1, StatusMsg: middlewares.CheckFailed})
44 | }
45 |
46 | // 未登录时的视频流获取
47 | func NoLoginAccess(c *gin.Context) {
48 | startTime := utils.CalculateStartTime(c.Query("latest_time"))
49 | feedVideo := *VideoService.Feed(startTime)
50 | lenFeedVideoList := len(feedVideo)
51 | // 空数据处理
52 | if lenFeedVideoList == 0 {
53 | response.GetFeedSuccess(c, time.Now().Unix(), []response.Video{})
54 | return
55 | }
56 | nextTime := feedVideo[lenFeedVideoList-1].CreatedAt.Unix()
57 | videoList := make([]response.Video, 0, lenFeedVideoList)
58 | for _, video := range feedVideo {
59 | videoList = append(videoList,
60 | response.Video{
61 | Id: int64(video.ID),
62 | Author: response.User{
63 | Id: int64(video.User.ID),
64 | Name: video.User.UserName,
65 | FollowCount: int64(video.User.FollowCount),
66 | FollowerCount: int64(video.User.FollowerCount),
67 | Avatar: video.User.Avatar,
68 | Background: video.User.BackgroundImage,
69 | Signature: video.User.Signature,
70 | TotalFavorited: video.User.TotalFavorited,
71 | WorkCount: video.User.WorkCount,
72 | FavoriteCount: video.User.FavoriteCount,
73 | },
74 | PlayUrl: config.SERVER_RESOURCES + video.PlayUrl,
75 | CoverUrl: config.SERVER_RESOURCES + video.CoverUrl,
76 | FavoriteCount: video.FavoriteCount,
77 | CommentCount: video.CommentCount,
78 | Title: video.Description,
79 | })
80 | }
81 | response.GetFeedSuccess(c, nextTime, videoList)
82 | }
83 |
84 | // 未登录时的视频流获取
85 | func LoginAccess(c *gin.Context, userId uint) {
86 | startTime := utils.CalculateStartTime(c.Query("latest_time"))
87 | feedVideo := *VideoService.Feed(startTime)
88 | lenFeedVideoList := len(feedVideo)
89 | if lenFeedVideoList == 0 {
90 | // 空数据处理
91 | response.GetFeedSuccess(c, time.Now().Unix(), []response.Video{})
92 | return
93 | }
94 | nextTime := feedVideo[lenFeedVideoList-1].CreatedAt.Unix()
95 | videoList := make([]response.Video, 0, lenFeedVideoList)
96 | for _, video := range feedVideo {
97 | videoList = append(videoList,
98 | response.Video{
99 | Id: int64(video.ID),
100 | Author: response.User{
101 | Id: int64(video.User.ID),
102 | Name: video.User.UserName,
103 | FollowCount: int64(video.User.FollowCount),
104 | FollowerCount: int64(video.User.FollowerCount),
105 | IsFollow: models.IsFollow(userId, video.User.ID),
106 | Avatar: video.User.Avatar,
107 | Background: video.User.BackgroundImage,
108 | Signature: video.User.Signature,
109 | TotalFavorited: video.User.TotalFavorited,
110 | WorkCount: video.User.WorkCount,
111 | FavoriteCount: video.User.FavoriteCount,
112 | },
113 | PlayUrl: config.SERVER_RESOURCES + video.PlayUrl,
114 | CoverUrl: config.SERVER_RESOURCES + video.CoverUrl,
115 | FavoriteCount: video.FavoriteCount,
116 | CommentCount: video.CommentCount,
117 | IsFavorite: models.IsFavorite(userId, video.ID),
118 | Title: video.Description,
119 | })
120 | }
121 | response.GetFeedSuccess(c, nextTime, videoList)
122 | }
123 |
--------------------------------------------------------------------------------
/controllers/response/user_resp.go:
--------------------------------------------------------------------------------
1 | package response
2 |
3 | import (
4 | "net/http"
5 |
6 | "github.com/gin-gonic/gin"
7 | "github.com/zheng-yi-yi/simpledouyin/models"
8 | )
9 |
10 | type User struct {
11 | Id int64 `json:"id,omitempty"` // 用户id
12 | Name string `json:"name,omitempty"` // 用户名称
13 | FollowCount int64 `json:"follow_count,omitempty"` // 关注总数
14 | FollowerCount int64 `json:"follower_count,omitempty"` // 粉丝总数
15 | IsFollow bool `json:"is_follow,omitempty"` // true-已关注,false-未关注
16 | Avatar string `json:"avatar,omitempty"` // 用户头像
17 | Background string `json:"background_image,omitempty"` // 用户个人页顶部大图
18 | Signature string `json:"signature,omitempty"` // 个人简介
19 | FavoriteCount int64 `json:"favorite_count"` // 喜欢数
20 | TotalFavorited string `json:"total_favorited"` // 获赞数量
21 | WorkCount int64 `json:"work_count"` // 作品数
22 | }
23 |
24 | // 用户注册响应
25 | type UserRegisterResponse struct {
26 | StatusCode int32 `json:"status_code"` // 状态码,0-成功,其他值-失败
27 | StatusMsg string `json:"status_msg"` // 返回状态描述
28 | UserId int32 `json:"user_id,omitempty"` // 用户鉴权token
29 | Token string `json:"token,omitempty"` // 用户id
30 | }
31 |
32 | // RegisterUserFailure , 用户注册失败
33 | func RegisterUserFailure(c *gin.Context) {
34 | c.JSON(http.StatusInternalServerError, UserRegisterResponse{
35 | StatusCode: 1,
36 | StatusMsg: "用户注册失败",
37 | })
38 | }
39 |
40 | // RegisterTokenError , 用户注册时token生成失败
41 | func RegisterTokenError(c *gin.Context) {
42 | c.JSON(http.StatusInternalServerError, UserRegisterResponse{
43 | StatusCode: 1,
44 | StatusMsg: "token生成失败",
45 | })
46 | }
47 |
48 | // RegisterUserComplete , 用户注册成功
49 | func RegisterUserComplete(c *gin.Context, userId int32, token string) {
50 | c.JSON(http.StatusOK, UserRegisterResponse{
51 | StatusCode: 0,
52 | StatusMsg: "用户注册成功",
53 | UserId: userId,
54 | Token: token,
55 | })
56 | }
57 |
58 | // 用户登录响应
59 | type UserLoginResponse struct {
60 | StatusCode int32 `json:"status_code"` // 状态码,0-成功,其他值-失败
61 | StatusMsg string `json:"status_msg"` // 返回状态描述
62 | UserId int32 `json:"user_id,omitempty"` // 用户鉴权token
63 | Token string `json:"token,omitempty"` // 用户id
64 | }
65 |
66 | // UserLoginFailure , 用户登录失败
67 | func UserLoginFailure(c *gin.Context) {
68 | c.JSON(http.StatusInternalServerError, UserLoginResponse{
69 | StatusCode: 1,
70 | StatusMsg: "用户登录失败",
71 | })
72 | }
73 |
74 | // UserLoginTokenError , 用户登录时token生成失败
75 | func UserLoginTokenError(c *gin.Context) {
76 | c.JSON(http.StatusInternalServerError, UserLoginResponse{
77 | StatusCode: 1,
78 | StatusMsg: "token生成失败",
79 | })
80 | }
81 |
82 | // UserLoginComplete , 用户登录成功
83 | func UserLoginComplete(c *gin.Context, userId int32, token string) {
84 | c.JSON(http.StatusOK, UserLoginResponse{
85 | StatusCode: 0,
86 | StatusMsg: "用户登录成功",
87 | UserId: userId,
88 | Token: token,
89 | })
90 | }
91 |
92 | // 用户信息响应
93 | type UserInfoResponse struct {
94 | StatusCode int32 `json:"status_code"` // 状态码,0-成功,其他值-失败
95 | StatusMsg string `json:"status_msg"` // 返回状态描述
96 | User User `json:"user,omitempty"` // 用户信息
97 | }
98 |
99 | // GetUserInfoFailure , 用户信息获取失败
100 | func GetUserInfoFailure(c *gin.Context) {
101 | c.JSON(http.StatusInternalServerError, UserInfoResponse{
102 | StatusCode: 1,
103 | StatusMsg: "用户信息获取失败",
104 | })
105 | }
106 |
107 | // UserInfoConversionError , 参数类型转换失败
108 | func UserInfoConversionError(c *gin.Context) {
109 | c.JSON(http.StatusInternalServerError, UserInfoResponse{
110 | StatusCode: 1,
111 | StatusMsg: "参数类型转换失败",
112 | })
113 | }
114 |
115 | // UserInfoComplete , 用户信息获取成功
116 | func UserInfoComplete(c *gin.Context, userInfo models.User, userId uint, query_user_id uint64) {
117 | c.JSON(http.StatusOK, UserInfoResponse{
118 | StatusCode: 0,
119 | StatusMsg: "用户信息获取成功",
120 | User: User{
121 | Id: int64(userInfo.ID),
122 | Name: userInfo.UserName,
123 | FollowCount: int64(userInfo.FollowCount),
124 | FollowerCount: int64(userInfo.FollowerCount),
125 | IsFollow: models.IsFollow(userId, uint(query_user_id)),
126 | Avatar: userInfo.Avatar,
127 | Background: userInfo.BackgroundImage,
128 | Signature: userInfo.Signature,
129 | TotalFavorited: userInfo.TotalFavorited,
130 | WorkCount: userInfo.WorkCount,
131 | FavoriteCount: userInfo.FavoriteCount,
132 | },
133 | })
134 | }
135 |
--------------------------------------------------------------------------------
/asset/document/Middleware.md:
--------------------------------------------------------------------------------
1 | # Middleware
2 |
3 | 本篇文档记录JWT(JSON Web Token)中间件的设计思路和实现过程。
4 |
5 | ---
6 |
7 | ## JWT 😊
8 |
9 | `JWT` 是一种用于在网络上安全传输信息的开放标准(RFC 7519),通常用于身份验证和信息交换。在本文中,我们将使用 `Golang` 和 `Gin` 框架来实现 `JWT` 中间件,用于验证用户的身份并将其信息存储在 `Gin` 的上下文中。
10 |
11 | ## 设计思路 💡
12 |
13 | ### (1)密钥管理
14 |
15 | 首先,我们需要定义一个密钥(JwtKey)来对JWT进行签名和验证。密钥应该是一个安全的字节序列,并且应该妥善保管。
16 |
17 | ```go
18 | var JwtKey = []byte("douyin")
19 | ```
20 |
21 | ### (2)JWT声明定义
22 |
23 | 我们需要定义 `JWT` 的声明,以便在生成和解析 `JWT` 时使用。在本项目中,我定义了 `MyClaims` 结构体,其中包括用户ID、用户名、密码以及一些标准的 `JWT` 声明(如过期时间、发行时间、发行者)。
24 |
25 | ```go
26 | type MyClaims struct {
27 | UserID uint
28 | Username string
29 | Password string
30 | jwt.RegisteredClaims
31 | }
32 | ```
33 |
34 | ### (3)生成JWT Token
35 |
36 | 要生成 `JWT Token`,我们使用 `GenerateToken` 函数,该函数接受用户ID、用户名和密码作为参数,并返回一个签名后的`JWT Token`。在生成过程中,我们设置了`JWT`的一些标准声明,如过期时间和发行者。
37 |
38 | ```go
39 | func GenerateToken(userId uint, username, password string) (string, error) {
40 | claims := MyClaims{
41 | userId,
42 | username,
43 | password,
44 | jwt.RegisteredClaims{
45 | ExpiresAt: jwt.NewNumericDate(time.Now().Add(24 * time.Hour)),
46 | IssuedAt: jwt.NewNumericDate(time.Now()),
47 | NotBefore: jwt.NewNumericDate(time.Now()),
48 | Issuer: "SimpleDouyin",
49 | },
50 | }
51 | token, err := jwt.NewWithClaims(jwt.SigningMethodHS256, claims).SignedString(JwtKey)
52 | if err != nil {
53 | return "", err
54 | }
55 | return token, nil
56 | }
57 | ```
58 |
59 | ### (4)鉴权中间件
60 |
61 | 我们定义了一个 `Auth` 中间件函数,用于验证 `JWT Token` 并将用户信息存储在 `Gin` 的上下文中。
62 |
63 | 该中间件首先获取请求中的 `Token`,然后尝试解析它。如果解析成功且 `Token` 有效,我们将 `用户ID` 存储在上下文中,以便后续处理函数使用。如果解析失败或 `Token无效` ,我们将返回鉴权失败的响应。
64 |
65 | ```go
66 | func Auth() gin.HandlerFunc {
67 | return func(c *gin.Context) {
68 | // 获取token
69 | tokenString := c.Query("token")
70 | if len(tokenString) == 0 {
71 | c.JSON(http.StatusBadRequest, AuthFailResponse{StatusCode: 1, StatusMsg: "无效token"})
72 | c.Abort()
73 | return
74 | }
75 | // 解析 token
76 | token, err := jwt.ParseWithClaims(tokenString, &MyClaims{}, func(token *jwt.Token) (interface{}, error) {
77 | return JwtKey, nil
78 | })
79 | if err != nil {
80 | c.JSON(http.StatusBadRequest, AuthFailResponse{StatusCode: 1, StatusMsg: "token解析失败"})
81 | c.Abort()
82 | return
83 | }
84 | // 校验鉴权的声明
85 | if token != nil {
86 | claims, ok := token.Claims.(*MyClaims)
87 | if ok && token.Valid {
88 | c.Set("userID", claims.UserID)
89 | c.Next()
90 | return
91 | }
92 | }
93 | c.JSON(http.StatusBadRequest, AuthFailResponse{StatusCode: 1, StatusMsg: "校验失败"})
94 | c.Abort()
95 | }
96 | }
97 | ```
98 |
99 | ### (5)用户发布视频前的鉴权
100 |
101 | 对于用户发布视频的鉴权,我们定义了 `UserPublishAuth` 中间件函数。
102 |
103 | 由于客户端提交表单数据时,`Token`字段是以 `multipart/form-data` 方式传递的,因此我们需要使用 `c.PostForm` 方法来获取 `Token` 值。鉴权逻辑与上述 `Auth` 中间件类似。
104 |
105 | ```go
106 | func UserPublishAuth() gin.HandlerFunc {
107 | return func(c *gin.Context) {
108 | // 获取token
109 | tokenString := c.PostForm("token")
110 | if len(tokenString) == 0 {
111 | c.JSON(http.StatusBadRequest, AuthFailResponse{StatusCode: 1, StatusMsg: "无效token"})
112 | c.Abort()
113 | return
114 | }
115 | // 解析 token
116 | token, err := jwt.ParseWithClaims(tokenString, &MyClaims{}, func(token *jwt.Token) (interface{}, error) {
117 | return JwtKey, nil
118 | })
119 | if err != nil {
120 | c.JSON(http.StatusBadRequest, AuthFailResponse{StatusCode: 1, StatusMsg: "token解析失败"})
121 | c.Abort()
122 | return
123 | }
124 | // 校验鉴权的声明
125 | if token != nil {
126 | claims, ok := token.Claims.(*MyClaims)
127 | if ok && token.Valid {
128 | c.Set("userID", claims.UserID)
129 | c.Next()
130 | return
131 | }
132 | }
133 | c.JSON(http.StatusBadRequest, AuthFailResponse{StatusCode: 1, StatusMsg: "校验失败"})
134 | c.Abort()
135 | }
136 | }
137 | ```
138 |
139 | ---
140 |
141 | ## 如何使用
142 |
143 | 当我们需要生成token时,可以这样:
144 |
145 | ```go
146 | // 生成Token
147 | token, err := GenerateToken(userId, username, password)
148 | if err != nil {
149 | // 处理错误
150 | } else {
151 | // 将Token发送给客户端
152 | }
153 | ```
154 |
155 | 用户鉴权的话,举一个例子:
156 |
157 | ```go
158 | router.POST("/publish/action/", UserPublishAuth(), func(c *gin.Context) {
159 | // 从上下文中获取用户ID
160 | userID := c.Value("userID")
161 | // 处理视频发布逻辑
162 | // ...
163 | })
164 | ```
165 |
166 | ---
167 |
168 | ## 参考资料
169 |
170 | 了解更多关于JWT的信息,请参考以下文档:
171 |
172 | - [JWT 官方文档](https://jwt.io/introduction)
173 | - [jwt-go 包文档](https://pkg.go.dev/github.com/golang-jwt/jwt/v5)
174 | - [RFC 7519 - JSON Web Token (JWT)](https://datatracker.ietf.org/doc/html/rfc7519)
--------------------------------------------------------------------------------
/test/interact_api_test.go:
--------------------------------------------------------------------------------
1 | package test
2 |
3 | import (
4 | "net/http"
5 | "testing"
6 |
7 | "github.com/stretchr/testify/assert"
8 | )
9 |
10 | // 测试 "点赞" 模块
11 | func TestFavorite(t *testing.T) {
12 | e := newExpect(t)
13 |
14 | // 发送 GET 请求来获取抖音动态信息
15 | feedResp := e.GET("/douyin/feed/").Expect().Status(http.StatusOK).JSON().Object()
16 | feedResp.Value("status_code").Number().IsEqual(0) // 确保响应状态码为 0,表示成功
17 | feedResp.Value("video_list").Array().Length().Gt(0) // 确保视频列表不为空
18 |
19 | // 获取第一个视频的 ID
20 | firstVideo := feedResp.Value("video_list").Array().Value(0).Object()
21 | videoId := firstVideo.Value("id").Number().Raw()
22 |
23 | // 随机得到一个用户名以及密码
24 | var favorite_username = generateRandomUsername()
25 | var favorite_password = generateRandomPassword()
26 |
27 | // 获取测试用户的用户 ID 和 token
28 | userId, token := getTestUserIdAndToken(favorite_username, favorite_password, e)
29 |
30 | // 发送 POST 请求来执行点赞操作
31 | favoriteResp := e.POST("/douyin/favorite/action/").
32 | WithQuery("token", token).WithQuery("video_id", videoId).WithQuery("action_type", 1).
33 | WithFormField("token", token).WithFormField("video_id", videoId).WithFormField("action_type", 1).
34 | Expect().
35 | Status(http.StatusOK).
36 | JSON().Object()
37 | favoriteResp.Value("status_code").Number().IsEqual(0) // 确保收藏操作成功
38 |
39 | // 发送 GET 请求来获取用户的点赞列表
40 | favoriteListResp := e.GET("/douyin/favorite/list/").
41 | WithQuery("token", token).WithQuery("user_id", userId).
42 | WithFormField("token", token).WithFormField("user_id", userId).
43 | Expect().
44 | Status(http.StatusOK).
45 | JSON().Object()
46 | favoriteListResp.Value("status_code").String().IsEqual("0") // 确保获取收藏列表成功
47 |
48 | // 遍历收藏列表中的每个视频并进行验证
49 | for _, element := range favoriteListResp.Value("video_list").Array().Iter() {
50 | video := element.Object()
51 | video.ContainsKey("id") // 确保视频对象包含 ID
52 | video.ContainsKey("author") // 确保视频对象包含作者信息
53 | video.Value("play_url").String().NotEmpty() // 确保播放链接不为空
54 | video.Value("cover_url").String().NotEmpty() // 确保封面链接不为空
55 | }
56 | }
57 |
58 | // 测试 "评论" 模块
59 | func TestComment(t *testing.T) {
60 |
61 | e := newExpect(t)
62 |
63 | // 发送 GET 请求来获取抖音视频流
64 | feedResp := e.GET("/douyin/feed/").Expect().Status(http.StatusOK).JSON().Object()
65 | feedResp.Value("status_code").Number().IsEqual(0) // 确保响应状态码为 0,表示成功
66 | feedResp.Value("video_list").Array().Length().Gt(0) // 确保视频列表不为空
67 |
68 | // 获取第一个视频的 ID
69 | firstVideo := feedResp.Value("video_list").Array().Value(0).Object()
70 | videoId := firstVideo.Value("id").Number().Raw()
71 |
72 | // 随机得到一个用户名以及密码
73 | var favorite_username = generateRandomUsername()
74 | var favorite_password = generateRandomPassword()
75 |
76 | // 获取测试用户的用户 ID 和 token
77 | _, token := getTestUserIdAndToken(favorite_username, favorite_password, e)
78 |
79 | // 发送 POST 请求来添加评论
80 | addCommentResp := e.POST("/douyin/comment/action/").
81 | WithQuery("token", token).WithQuery("video_id", videoId).WithQuery("action_type", 1).WithQuery("comment_text", "测试评论").
82 | WithFormField("token", token).WithFormField("video_id", videoId).WithFormField("action_type", 1).WithFormField("comment_text", "测试评论").
83 | Expect().
84 | Status(http.StatusOK).
85 | JSON().Object()
86 | addCommentResp.Value("status_code").Number().IsEqual(0) // 确保添加评论操作成功
87 | addCommentResp.Value("comment").Object().Value("id").Number().Gt(0) // 确保评论的 ID 大于 0
88 | commentId := int(addCommentResp.Value("comment").Object().Value("id").Number().Raw())
89 |
90 | // 发送 GET 请求来获取视频的评论列表
91 | commentListResp := e.GET("/douyin/comment/list/").
92 | WithQuery("token", token).WithQuery("video_id", videoId).
93 | WithFormField("token", token).WithFormField("video_id", videoId).
94 | Expect().
95 | Status(http.StatusOK).
96 | JSON().Object()
97 | commentListResp.Value("status_code").Number().IsEqual(0) // 确保获取评论列表成功
98 | containTestComment := false
99 | for _, element := range commentListResp.Value("comment_list").Array().Iter() {
100 | comment := element.Object()
101 | comment.ContainsKey("id") // 确保评论对象包含 ID
102 | comment.ContainsKey("user") // 确保评论对象包含用户信息
103 | comment.Value("content").String().NotEmpty() // 确保评论内容不为空
104 | comment.Value("create_date").String().NotEmpty() // 确保评论创建日期不为空
105 | if int(comment.Value("id").Number().Raw()) == commentId {
106 | containTestComment = true
107 | }
108 | }
109 |
110 | // 断言:确保测试评论在评论列表中
111 | assert.True(t, containTestComment, "无法在列表中找到测试评论")
112 |
113 | // 发送 POST 请求来删除评论
114 | delCommentResp := e.POST("/douyin/comment/action/").
115 | WithQuery("token", token).WithQuery("video_id", videoId).WithQuery("action_type", 2).WithQuery("comment_id", commentId).
116 | WithFormField("token", token).WithFormField("video_id", videoId).WithFormField("action_type", 2).WithFormField("comment_id", commentId).
117 | Expect().
118 | Status(http.StatusOK).
119 | JSON().Object()
120 | delCommentResp.Value("status_code").Number().IsEqual(0) // 确保删除评论操作成功
121 | }
122 |
--------------------------------------------------------------------------------
/services/relation_service.go:
--------------------------------------------------------------------------------
1 | package services
2 |
3 | import (
4 | "errors"
5 |
6 | "github.com/zheng-yi-yi/simpledouyin/config"
7 | "github.com/zheng-yi-yi/simpledouyin/models"
8 | "gorm.io/gorm"
9 | )
10 |
11 | type RelationService struct {
12 | }
13 |
14 | // 关注用户
15 | func (s *RelationService) FollowUser(fromUserId uint, toUserId uint) error {
16 | // 获取数据库连接的引用
17 | db := config.Database
18 | // 检查 fromUserId 和 toUserId 是否是有效的用户
19 | var fromUser, toUser models.User
20 | if err := db.First(&fromUser, fromUserId).Error; err != nil {
21 | return errors.New("未找到 fromUserId")
22 | }
23 | if err := db.First(&toUser, toUserId).Error; err != nil {
24 | return errors.New("未找到 toUserId")
25 | }
26 | // 查找已存在的关系,包括可能的取消关注情况
27 | var existingRelation models.Relation
28 | if err := db.Where("from_user_id = ? AND to_user_id = ?", fromUserId, toUserId).First(&existingRelation).Error; err == nil {
29 | // 如果已存在关系,检查是否是取消状态,如果是则重新激活
30 | if existingRelation.Cancel == 1 {
31 | // 更新为重新关注状态
32 | if err := db.Model(&existingRelation).Update("cancel", 0).Error; err != nil {
33 | return err
34 | }
35 | return nil
36 | }
37 | return errors.New("关系已经存在")
38 | }
39 | // 创建一个新的关注关系
40 | newRelation := models.Relation{
41 | FromUserId: fromUserId,
42 | ToUserId: toUserId,
43 | Cancel: 0, // 默认:未取消
44 | }
45 | // 将新的关系插入到数据库中
46 | if err := db.Create(&newRelation).Error; err != nil {
47 | return err
48 | }
49 | // 更新关注用户的 FollowCount
50 | if err := config.Database.Model(&models.User{}).Where("id = ?", fromUserId).UpdateColumn("follow_count", gorm.Expr("follow_count + ?", 1)).Error; err != nil {
51 | return err
52 | }
53 | // 更新被关注用户的 FollowerCount
54 | if err := config.Database.Model(&models.User{}).Where("id = ?", toUserId).UpdateColumn("follower_count", gorm.Expr("follower_count + ?", 1)).Error; err != nil {
55 | return err
56 | }
57 | return nil
58 | }
59 |
60 | // 取消关注用户
61 | func (s *RelationService) CancelFollowUser(fromUserId, toUserId uint) error {
62 | // 获取数据库连接的引用
63 | db := config.Database
64 | // 查找已存在的关注关系
65 | var existingRelation models.Relation
66 | if err := db.Where("from_user_id = ? AND to_user_id = ?", fromUserId, toUserId).First(&existingRelation).Error; err != nil {
67 | return errors.New("关注关系不存在")
68 | }
69 | // 检查关注关系的取消状态
70 | if existingRelation.Cancel == 1 {
71 | return errors.New("关注关系已经是取消状态")
72 | }
73 | // 更新关注关系为取消状态
74 | if err := db.Model(&existingRelation).Update("cancel", 1).Error; err != nil {
75 | return err
76 | }
77 | //更新关注用户的 FollowCount
78 | if err := config.Database.Model(&models.User{}).Where("id = ?", fromUserId).Update("follow_count", gorm.Expr("follow_count - 1")).Error; err != nil {
79 | return err
80 | }
81 | //更新被关注用户的 FollowerCount
82 | if err := config.Database.Model(&models.User{}).Where("id = ?", toUserId).Update("follower_count", gorm.Expr("follower_count - 1")).Error; err != nil {
83 | return err
84 | }
85 | return nil
86 | }
87 |
88 | // 获取关注列表
89 | func (s *RelationService) GetFllowList(userId uint) ([]models.User, error) {
90 | //通过relation表查询用户的所有关注用户Id
91 | var relation []models.Relation
92 | result := config.Database.Select("to_user_id").Where("from_user_id = ?", userId).Find(&relation)
93 | if result.Error != nil {
94 | return nil, result.Error
95 | }
96 | var toUserIds []uint64
97 | for _, rel := range relation {
98 | toUserIds = append(toUserIds, uint64(rel.ToUserId))
99 | }
100 | //再通过users表返回被关注的用户的详细信息
101 | var users []models.User
102 | result = config.Database.Where("id IN (?)", toUserIds).Find(&users)
103 | if result.Error != nil {
104 | return nil, result.Error
105 | }
106 | return users, nil
107 | }
108 |
109 | // 判断是否关注
110 | func (s *RelationService) IsFollow(fromUserId uint, toUserId uint) bool {
111 | // 获取数据库连接的引用
112 | db := config.Database
113 | // 查找存在的关注关系
114 | var existingRelation models.Relation
115 | if err := db.Where("from_user_id = ? AND to_user_id = ? AND cancel = 0", fromUserId, toUserId).First(&existingRelation).Error; err != nil {
116 | return false
117 | }
118 | return true
119 | }
120 |
121 | // 获取粉丝列表
122 | func (s *RelationService) GetFollowerList(userId uint) ([]models.User, error) {
123 | // 通过relation表查询关注了该用户的所有用户Id
124 | var relation []models.Relation
125 | result := config.Database.Select("from_user_id").Where("to_user_id = ? AND cancel = ?", userId, 0).Find(&relation)
126 | if result.Error != nil {
127 | return nil, result.Error
128 | }
129 | var followerUserIds []uint64
130 | for _, rel := range relation {
131 | followerUserIds = append(followerUserIds, uint64(rel.FromUserId))
132 | }
133 | // 再通过users表返回粉丝的详细信息
134 | var followers []models.User
135 | result = config.Database.Where("id IN (?)", followerUserIds).Find(&followers)
136 | if result.Error != nil {
137 | return nil, result.Error
138 | }
139 | return followers, nil
140 | }
141 |
142 | // GetFriendsList 通过用户ID获取好友用户列表
143 | func (s *RelationService) GetFriendsList(userID uint) ([]models.User, error) {
144 | // 通过relation表查询用户关注的好友
145 | var relations []models.Relation
146 | result := config.Database.Where("(from_user_id = ? OR to_user_id = ?) AND cancel = ?", userID, userID, 0).Find(&relations)
147 | if result.Error != nil {
148 | return nil, result.Error
149 | }
150 |
151 | friendUserIDs := make(map[uint]bool) // 使用 map 来存储好友的用户ID,避免重复
152 | for _, rel := range relations {
153 | if rel.FromUserId == userID {
154 | friendUserIDs[rel.ToUserId] = true
155 | } else {
156 | friendUserIDs[rel.FromUserId] = true
157 | }
158 | }
159 |
160 | // 获取好友用户的详细信息
161 | var friendUserList []models.User
162 | for friendID := range friendUserIDs {
163 | var user models.User
164 | result := config.Database.First(&user, friendID)
165 | if result.Error != nil {
166 | return nil, result.Error
167 | }
168 | friendUserList = append(friendUserList, user)
169 | }
170 |
171 | return friendUserList, nil
172 | }
173 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 | 
12 | 
13 | 
14 | 
15 |
16 |
25 |
26 |
27 |
28 | # ☁️ Introduction
29 |
30 | `SimpleDouyin`,一个基于 `Golang` 编写的短视频分享平台后端服务,以促进用户互动和内容分享,是第六届字节跳动青训营后端大项目的实现,涵盖了用户、视频、喜欢、关系、评论和聊天等功能模块。
31 |
32 |
39 |
40 | ---
41 |
42 | ## 🌟 Technology
43 |
44 |
45 |
46 |
47 |
48 | | Technology |
49 | Description |
50 |
51 |
52 | | Go |
53 | 静态强类型、编译型、并发型,并具有垃圾回收功能的编程语言 |
54 |
55 |
56 | | Gin |
57 | 构建 Web 服务和 API,处理 HTTP 请求和响应 |
58 |
59 |
60 | | MySQL |
61 | 存储各数据模块,以支持数据的持久化存储和管理 |
62 |
63 |
64 | | GORM |
65 | 用于进行数据库查询、插入、更新和删除操作 |
66 |
67 |
68 | | golang-jwt |
69 | 实现 JWT 鉴权,用于用户身份验证和授权 |
70 |
71 |
72 | | bcrypt |
73 | 对用户密码进行加密和验证,确保密码存储的安全性 |
74 |
75 |
76 | | Ffmpeg |
77 | 多媒体处理工具,用于视频文件的截图操作,生成封面 |
78 |
79 |
80 | | viper |
81 | 用于加载数据库连接信息、密钥、服务器端口等配置参数 |
82 |
83 |
84 | | httpexpect |
85 | 编写 HTTP 请求和响应的测试用例,验证各个 API 端点的正确性和一致性 |
86 |
87 |
88 |
89 |
90 |
91 | ---
92 |
93 | # 🚀 Feature module
94 |
95 | 本项目划分为六个模块,功能描述如下:
96 |
97 | 1. ✅用户模块
98 | - 用户注册:通过提供用户名和密码进行用户注册
99 | - 用户登录:已注册用户可以通过用户名和密码进行登录
100 | - 用户信息:获取登录用户的个人信息,如头像、昵称,关注数和粉丝数等
101 | 2. ✅视频模块
102 | - 视频浏览:不限制登录状态,用户可以刷视频
103 | - 视频上传:登录用户可以上传短视频,包括标题和视频文件
104 | - 发布列表:获取用户所有作品
105 | 3. ✅喜欢模块
106 | - 赞操作:用户可以对视频进行点赞操作或取消点赞
107 | - 喜欢列表:获取登录用户所有点赞视频
108 | 4. ✅关系模块
109 | - 关系操作:登录用户对其他用户进行关注操作或取关
110 | - 关注列表:获取登录用户的关注列表
111 | - 粉丝列表:获取登录用户的粉丝列表
112 | - 好友列表:获取所有关注登录用户的粉丝列表
113 | 5. ✅评论模块
114 | - 评论操作:登录用户可以对视频进行评论
115 | - 评论列表:查看视频的所有评论
116 | 6. ✅消息模块
117 | - 聊天记录:获取当前登录用户和指定用户的聊天记录
118 | - 发送消息:登录用户将消息发送给好友
119 |
120 | ---
121 |
122 | # 💻 Layered_Architecture
123 |
124 | 本项目的主体结构如下:
125 |
126 | ```bash
127 | SimpleDouyin/
128 | ├── config/ # 项目配置
129 | ├── setup/ # 项目初始化
130 | ├── controllers/ # 处理 HTTP 请求的控制器
131 | ├── services/ # 存放系统的业务逻辑代码
132 | ├── models/ # 存放数据模型以及与数据库交互的操作
133 | ├── public/ # 存放静态资源文件,包括视频文件和封面等
134 | ├── middlewares/ # 自定义中间件函数,用于在路由处理前后执行操作
135 | ├── utils/ # 工具函数:包括 encrypt加密操作和ffmpeg截图服务等
136 | ├── main.go # 项目入口:包含 HTTP 服务器初始化和启动代码
137 | └── router.go # 路由层:路由定义,映射 HTTP 请求到相应的处理函数
138 | ```
139 |
140 | 可以看到,`SimpleDouyin` 项目采用分层架构,代码按照路由层、控制层、服务层和持久层来划分,这样可以有效地将不同的功能逻辑分开,利于构建一个结构清晰、模块化、可扩展且易于维护的后端应用程序。
141 |
142 |
143 |

144 |
145 |
146 |
147 |
148 | 具体内容可查看 [分层架构设计思路开发文档](./asset/document/Layered_Architecture.md) 。
149 |
150 | # 🔍 Database design
151 |
152 | `SimpleDouyin` 项目在数据存储方面需要构建以下实体表:
153 |
154 |
155 |

156 |
157 |
158 | 具体内容可查看 [数据库设计思路开发文档](./asset/document/Database_Design.md) 。
159 |
160 | # ⌨️ How to use
161 |
162 | 下面是一些简单的步骤,帮助你快速启动项目并在本地运行起来。
163 |
164 |
165 | 点击查看 | 快速开始
166 |
167 | ## 前言
168 |
169 | 首先确保本地已经配置好了 `Go` 和 `MySQL` 的开发环境,并且已成功安装了 `FFmpeg` 工具(该工具用于视频上传后的封面截图服务)。
170 |
171 | > 本项目是在 `Windows` 系统上进行开发的,有关 `Go` 开发环境的具体配置步骤可以自行查询。关于 `FFmpeg` 工具的安装,请[访问 FFmpeg 官方网站](https://ffmpeg.org/) 自行下载安装。
172 | ## 克隆项目
173 |
174 | ```git
175 | git clone https://github.com/zheng-yi-yi/SimpleDouyin.git
176 | ```
177 |
178 | ## 进入项目根目录
179 |
180 | ```bash
181 | cd SimpleDouyin
182 | ```
183 |
184 | ## 安装依赖
185 |
186 | 使用以下命令安装项目所需的依赖:
187 |
188 | ```go
189 | go mod tidy
190 | ```
191 |
192 | ## 修改配置
193 |
194 | 打开项目根目录下的 `config.yaml` 文件:
195 |
196 | - 填入: `username`
197 | - 填入:`password`
198 |
199 | 接着修改 `config` 目录下的 `config.go` 文件:
200 |
201 | - 将 `LOCAL_IP_ADDRESS` 的值修改为本机 `IP` 地址。
202 |
203 |
204 |
205 | 如何查询本机 IP 地址 | 点击查看
206 |
207 |
208 |
209 | >
210 | > 1. 按 `Win` + `R` 打开运行窗口
211 | > 2. 输入 `cmd` 打开命令行窗口
212 | > 3. 输入 `ipconfig` 并回车,即可查到本机 IP 地址。
213 | > 4. 比如:`IPv4 地址 . . . . . . . . . . . . : 192.168.1.7`
214 |
215 |
216 |
217 |
218 | ## 添加数据库
219 |
220 | 使用以下命令创建数据库 `douyin`。
221 |
222 | ```sql
223 | CREATE DATABASE douyin
224 | ```
225 |
226 | ## 编译运行
227 |
228 | 编译项目:
229 |
230 | ```shell
231 | go build
232 | ```
233 |
234 | 编译后会生成一个可执行文件,运行:
235 |
236 | ```
237 | ./SimpleDouyin.exe
238 | ```
239 |
240 | ## APP
241 |
242 | 最后,在极简抖音 App 中,双击右下方的 **“我”** 打开高级设置 ,填入服务地址(本机地址),点击**保存并重启**后即可看见效果。
243 |
244 | > 比如本机 IP 地址为 `192.168.1.7`,那么在高级设置中就填入 `http://192.168.1.7:8080/`
245 |
246 |
247 |
248 | ---
249 |
250 | # 🎉 鸣谢
251 |
252 | [字节跳动 | 青训营](https://youthcamp.bytedance.com/)
253 |
254 |
--------------------------------------------------------------------------------
/asset/document/Layered_Architecture.md:
--------------------------------------------------------------------------------
1 | # Layered_Architecture
2 |
3 | 本项目的主体结构如下:
4 |
5 | ```bash
6 | SimpleDouyin/
7 | ├── config/ # 项目配置
8 | ├── setup/ # 项目初始化
9 | ├── controllers/ # 处理 HTTP 请求的控制器
10 | ├── services/ # 存放系统的业务逻辑代码
11 | ├── models/ # 存放数据模型以及与数据库交互的操作
12 | ├── public/ # 存放静态资源文件,包括视频文件和封面等
13 | ├── middlewares/ # 自定义中间件函数,用于在路由处理前后执行操作
14 | ├── utils/ # 工具函数:包括 encrypt加密操作和ffmpeg截图服务等
15 | ├── main.go # 项目入口:包含 HTTP 服务器初始化和启动代码
16 | └── router.go # 路由层:路由定义,映射 HTTP 请求到相应的处理函数
17 | ```
18 |
19 | 可以看到,`SimpleDouyin` 项目采用分层架构,代码按照路由层、控制层、服务层和持久层来划分,这样可以有效地将不同的功能逻辑分开,利于构建一个结构清晰、模块化、可扩展且易于维护的后端应用程序。
20 |
21 |
22 |

23 |
24 |
25 |
26 |
27 | 下面进行简单的分析与记录。
28 |
29 | ---
30 |
31 | ## Router
32 |
33 | **路由层**,负责路由的初始化以及定义应用程序的不同 `API` 端点,接收客户端请求,并分发映射到控制器方法中,起到了路由请求和处理方法之间的桥梁作用。
34 |
35 | > 这一层主要将不同的 HTTP 请求(GET、POST)映射到相应的控制器进行处理。
36 |
37 | 主体代码如下:
38 |
39 | ```go
40 | // route initialization function
41 | func initRouter(r *gin.Engine) {
42 | // basicSetup
43 | r.Static("/public", "./public")
44 | r.StaticFile("/favicon.ico", "./public/favicon.ico")
45 | apiRouter := r.Group("/douyin")
46 | // fundamental features:
47 | apiRouter.GET("/feed/", video.Feed) // 视频流
48 | apiRouter.POST("/user/register/", user.Register) // 用户注册
49 | apiRouter.POST("/user/login/", user.Login) // 用户登录
50 | apiRouter.GET("/user/", middlewares.Auth(), user.UserInfo) // 用户信息
51 | apiRouter.POST("/publish/action/", middlewares.UserPublishAuth(), video.Publish) // 视频投稿
52 | apiRouter.GET("/publish/list/", middlewares.Auth(), video.PublishList) // 发布列表
53 | // Extended Feature 1: Interactivity
54 | apiRouter.POST("/favorite/action/", middlewares.Auth(), favorite.FavoriteAction) // 点赞操作
55 | apiRouter.GET("/favorite/list/", middlewares.Auth(), favorite.FavoriteList) // 喜欢列表
56 | apiRouter.POST("/comment/action/", middlewares.Auth(), comment.CommentAction) // 评论操作
57 | apiRouter.GET("/comment/list/", middlewares.Auth(), comment.CommentList) // 评论列表
58 | // Extended Feature 2: Social
59 | apiRouter.POST("/relation/action/", middlewares.Auth(), relation.RelationAction) // 关注操作
60 | apiRouter.GET("/relation/follow/list/", middlewares.Auth(), relation.FollowList) // 关注列表
61 | apiRouter.GET("/relation/follower/list/", middlewares.Auth(), relation.FollowerList)// 粉丝列表
62 | apiRouter.GET("/relation/friend/list/", middlewares.Auth(), relation.FriendList) // 好友列表
63 | apiRouter.POST("/message/action/", middlewares.Auth(), message.MessageAction) // 发送消息
64 | apiRouter.GET("/message/chat/", middlewares.Auth(), message.MessageChat) // 聊天记录
65 | }
66 | ```
67 |
68 | 上述代码位于本项目根目录下的 `router.go` 文件中,`initRouter` 是一个基于 `Gin` 框架的路由初始化函数,它用于定义 `API` 端点和路由处理函数。它接受一个 `gin.Engine` 实例 `r` 作为参数。
69 |
70 | > `gin.Engine` 是 `Gin` 框架的核心,用于管理 `HTTP` 路由和中间件。
71 |
72 | 我们来记录一下:
73 |
74 | ```go
75 | r.Static("/public", "./public")
76 | r.StaticFile("/favicon.ico", "./public/favicon.ico")
77 | ```
78 |
79 | 这两行代码用于设置静态文件服务,`Static` 方法用于将指定目录下的文件提供给客户端,`StaticFile` 方法用于提供一个特定文件。
80 |
81 | ```go
82 | apiRouter := r.Group("/douyin")
83 | ```
84 |
85 | 这行代码定义了一个名为 `/douyin` 的路由组,将所有的 `API` 端点都添加到这个组中。这有助于组织和命名相关的 `API` 端点,使代码更具结构性。
86 |
87 | ```go
88 | apiRouter.GET("/feed/", video.Feed)
89 | // ...
90 | ```
91 |
92 | 接着,我们使用 `GET` 和 `POST` 方法定义了不同的 `API` 端点,以及它们对应的处理函数。
93 |
94 | 比如, `apiRouter.GET("/feed/", video.Feed)` 定义了一个 `GET` 请求的 `API` 端点 `/douyin/feed/`,并将其与 `video.Feed` 函数关联。这表示当客户端发出 `/douyin/feed/` 的 `GET` 请求时,将调用 `video.Feed` 处理函数来处理请求。
95 |
96 | 其他 `API` 端点的定义和关联方式与上述相似,每个端点都有对应的处理函数。
97 |
98 | ```go
99 | apiRouter.GET("/user/", middlewares.Auth(), user.UserInfo)
100 | // ...
101 | apiRouter.POST("/publish/action/", middlewares.UserPublishAuth(), video.Publish)
102 | ```
103 |
104 | 可以发现,这里我们使用了 `middlewares.Auth()` 和 `middlewares.UserPublishAuth()` 中间件函数,这是为了确保某些 `API` 端点的身份验证和授权。具体可查看项目根目录中 `middlewares` 包下的 `jwt.go` 文件。
105 |
106 | > 中间件是一种在处理请求之前或之后执行的函数,用于执行常见的操作,例如身份验证、日志记录等。详细可查看 [JWT设计思路](Middleware.md)
107 |
108 | ---
109 |
110 | ## Controllers
111 |
112 | **控制层**,接收来自路由的请求,并根据请求类型和参数来调用相应业务逻辑层中的方法,同时进行相应的错误处理和构建合适的 HTTP 响应。
113 |
114 | > 这一层将客户端请求与底层的业务逻辑隔离开来,提供统一的接口。
115 |
116 | 控制层的目录结构如下:
117 |
118 | ```bash
119 | controllers/
120 | ├── user/
121 | │ ├── register.go # 处理用户注册请求
122 | │ ├── login.go # 处理用户登录请求
123 | │ └── user.go # 处理获取用户信息请求
124 | ├── video/
125 | │ ├── feed.go # 处理获取视频流(feed)的请求
126 | │ ├── publish.go # 处理用户发布视频请求
127 | │ └── publishList.go # 处理获取用户发布视频列表请求
128 | ├── favorite/
129 | │ ├── favoriteAction.go # 处理赞操作(点赞或取消赞)请求
130 | │ └── favoriteList.go # 处理获取用户喜欢列表请求
131 | ├── comment/
132 | │ ├── commentAction.go # 处理评论操作(添加评论或删除评论)请求
133 | │ └── commentlist.go # 处理获取评论列表请求
134 | ├── relation/
135 | │ ├── relationAction.go # 处理关系操作(关注或取关)请求
136 | │ ├── followlist.go # 处理获取用户关注列表请求
137 | │ ├── followerList.go # 处理获取用户粉丝列表请求
138 | │ └── friendList.go # 处理获取用户好友列表请求
139 | ├── message/
140 | │ ├── messageAction.go # 处理发送消息请求
141 | │ └── messageChat.go # 处理获取聊天记录请求
142 | └── response/
143 | ├── shared_resp.go # 通用响应结构
144 | ├── user_resp.go # 与用户模块相关的响应结构
145 | ├── video_resp.go # 与视频模块相关的响应结构
146 | ├── comment_resp.go # 与评论模块相关的响应结构
147 | ├── favorite_resp.go # 与喜欢模块相关的响应结构
148 | ├── relation_resp.go # 与关系模块相关的响应结构
149 | └── message_resp.go # 与消息模块相关的响应结构
150 | ```
151 |
152 | 可以看到,这一层的主要目的是处理客户端的请求,并与底层的业务逻辑层隔离开来,提供统一的接口。其中主要任务包括:
153 |
154 | 1. 解析请求参数
155 | 2. 调用服务层方法
156 | 3. 处理相关错误
157 | 4. 构建HTTP响应
158 |
159 | 同时,我根据不同的模块,将代码组织成多个子目录,算是提高了代码的可维护性。
160 |
161 | 另外,控制层定义了响应结构体,用于构建HTTP响应的 JSON 数据,我将这部分代码都放在了 `controllers/response` 包下,也便于管理。
162 |
163 | > 响应结构体包括了状态码、状态消息以及相关的数据,具体可查看源码。
164 |
165 | 以上便是简单的控制层实现,包含了多个功能模块,每个模块负责不同的业务逻辑,但都遵循类似的设计模式和实现原则。
166 |
167 |
168 | ## Services
169 |
170 | **服务层**,处理应用程序功能的地方。这一层负责验证和处理数据,进行业务逻辑的编写,协调不同模块之间的交互,确保整个应用程序按预期工作。
171 |
172 | > 这一层可以调用数据库访问层的方法来读取或写入数据。
173 |
174 | 该层的目录结构如下:
175 |
176 | ```bash
177 | Services/
178 | ├── comment_service.go # 创建评论、获取视频的评论列表、根据评论ID获取评论等
179 | ├── favorite_service.go # 判断用户是否点赞、点赞和取消点赞操作、获取用户点赞列表等
180 | ├── message_service.go # 添加消息和获取消息列表
181 | ├── relation_service.go # 关注用户、取关用户、获取关注列表等
182 | ├── user_service.go # 用户注册与登录、获取用户信息等
183 | └── video_service.go # 获取视频Feed、获取用户发布的视频列表等
184 | ```
185 |
186 | 这一层代码主要是用于处理不同模块的业务逻辑,包括评论、点赞、消息、用户关系、用户注册和登录、视频处理等。
187 |
188 |
189 | ## Models
190 |
191 | **持久层**,这一层定义了数据模型(struct)、并处理与数据库的交互,包括创建、读取、更新和删除数据,将业务逻辑层与数据库之间的操作隔离开来。
192 |
193 | > 这一层主要负责与数据库进行交互
194 |
195 | 持久层的分析与记录可查看 [数据库设计思路与实现](Database_Design.md) 。
--------------------------------------------------------------------------------
/asset/document/Database_Design.md:
--------------------------------------------------------------------------------
1 | # Database design
2 |
3 | ## ER图
4 |
5 | `SimpleDouyin` 项目在数据存储方面需要构建以下实体表:
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 | User |
39 | 用户信息,包括用户名、密码、关注数、作品数等 |
40 |
41 |
42 | Video |
43 | 视频信息,包括作者ID、视频播放地址、视频获赞数量等 |
44 |
45 |
46 | Favorite |
47 | 点赞信息,包括用户ID,视频ID,点赞状态等。 |
48 |
49 |
50 | Relation |
51 | 关注信息,包括关注者ID,被关注者ID,关注是否取消等 |
52 |
53 |
54 | Comment |
55 | 评论信息,包括评论内容和发布时间,与用户和视频关联。 |
56 |
57 |
58 | Message |
59 | 聊天信息,包括发送者、接收者、消息内容和发送时间。 |
60 |
61 |
62 |
63 |
64 |
65 | ---
66 |
67 | # GORM 库的使用
68 |
69 |
70 | ## models包下的模型定义
71 |
72 | 以下是每个实体表的设计,包含了各个 `GORM` 模型定义。
73 |
74 | **User**:
75 |
76 | ```go
77 | // 用户表
78 | type User struct {
79 | ID uint `gorm:"primarykey"`
80 | UserName string `gorm:"not null; comment:用户名; type:VARCHAR(255)"`
81 | PassWord string `gorm:"not null; comment:密码; type:VARCHAR(255)"`
82 | FollowCount int `gorm:"not null; comment:关注总数; type:INT"`
83 | FollowerCount int `gorm:"not null; comment:粉丝总数; type:INT"`
84 | FavoriteCount int64 `gorm:"not null; comment:喜欢数; type:BIGINT"`
85 | TotalFavorited string `gorm:"not null; comment:获赞数量; type:VARCHAR(255)"`
86 | WorkCount int64 `gorm:"not null; comment:作品数; type:BIGINT"`
87 | Avatar string `gorm:"not null; comment:用户头像; type:VARCHAR(255)"`
88 | BackgroundImage string `gorm:"not null; comment:顶部图; type:VARCHAR(255)"`
89 | Signature string `gorm:"not null; comment:个人简介; type:TEXT"`
90 | // 定义外键关系
91 | Video []Video `gorm:"foreignKey:UserId; references:ID; comment:视频信息"`
92 | Comment []Comment `gorm:"foreignKey:UserId; references:ID; comment:评论信息"`
93 | }
94 | ```
95 |
96 | **Video**:
97 |
98 | ```go
99 | // Video, 视频表
100 | type Video struct {
101 | ID uint `gorm:"primarykey"`
102 | UserId uint `gorm:"not null; comment:作者ID type:INT"`
103 | PlayUrl string `gorm:"not null; comment:视频播放地址; type:VARCHAR(255)"`
104 | CoverUrl string `gorm:"not null; comment:视频封面地址; type:VARCHAR(255)"`
105 | FavoriteCount int64 `gorm:"not null; comment:点赞数量; type:BIGINT"`
106 | CommentCount int64 `gorm:"not null; comment:视频的评论总数; type:BIGINT"`
107 | Description string `gorm:"not null; comment:视频描述; type:TEXT"`
108 | CreatedAt time.Time `gorm:"not null; comment:视频发布日期; type:DATETIME"`
109 | // 定义外键关系
110 | User User `gorm:"foreignKey:UserId; references:ID; comment:作者信息"`
111 | }
112 | ```
113 |
114 | **Favorite**:
115 |
116 | ```go
117 | // Favorite, 点赞表
118 | type Favorite struct {
119 | ID uint `gorm:"primarykey"`
120 | UserId uint `gorm:"not null; comment:用户ID; type:INT"`
121 | VideoId uint `gorm:"not null; comment:视频ID; type:INT"`
122 | // 定义外键关系
123 | User User `gorm:"foreignKey:UserId; references:ID; comment:点赞用户的信息"`
124 | Video Video `gorm:"foreignKey:VideoId; references:ID; comment:点赞视频的信息"`
125 | }
126 | ```
127 |
128 | **Relation**:
129 |
130 | ```go
131 | // Relation, 关注关系表
132 | type Relation struct {
133 | ID uint `gorm:"primarykey"`
134 | FromUserId uint `gorm:"not null; comment: 用户id; type:INT"`
135 | ToUserId uint `gorm:"not null; comment: 关注的用户; type:INT"`
136 | Cancel uint `gorm:"not null; comment: 默认关注为0,取消关注为1; type:INT"`
137 | }
138 | ```
139 |
140 | **Comment**:
141 |
142 | ```go
143 | // Comment. 评论表
144 | type Comment struct {
145 | ID uint `gorm:"primarykey; comment:评论id"`
146 | UserId uint `gorm:"not null; comment:发布评论的用户id; type:INT"`
147 | VideoId uint `gorm:"not null; comment:评论所属视频id; type:INT"`
148 | Content string `gorm:"not null; comment:评论内容; type:VARCHAR(255)"`
149 | CreatedAt time.Time `gorm:"not null; comment:评论发布日期; type:DATETIME"`
150 | // 定义外键关系
151 | User User `gorm:"foreignKey:UserId; references:ID; comment:评论所属用户"`
152 | Video Video `gorm:"foreignKey:VideoId; references:ID; comment:评论所属视频"`
153 | }
154 | ```
155 |
156 | **Message**:
157 |
158 | ```go
159 | // Message, 消息表
160 | type Message struct {
161 | ID uint `gorm:"primaryKey comment:消息id"`
162 | FromUserID uint `gorm:"not null comment:消息发送者id; type:INT"`
163 | ToUserID uint `gorm:"not null comment:消息接收者id; type:INT"`
164 | Content string `gorm:"not null comment:消息内容; type:TEXT"`
165 | CreateTime time.Time `gorm:"not null comment:消息发送时间; type:DATETIME"`
166 | }
167 | ```
168 |
169 | ---
170 |
171 | ## AutoMigrate - 自动建表或更新表结构
172 |
173 | 本项目使用了 `GORM` 库进行自动创建数据库表,需要使用到 `AutoMigrate` 函数。
174 |
175 | 首先简单介绍一下 `AutoMigrate` 函数:
176 |
177 | `AutoMigrate` 函数是 `GORM` 中的一个重要功能,用于自动创建数据库表和更新表结构以匹配模型定义的更改。该函数通常在应用程序的启动时使用,以确保数据库表的结构与模型一致。函数签名如下:
178 |
179 | ```go
180 | func (db *DB) AutoMigrate(dst ...interface{}) error
181 | ```
182 |
183 | 参数说明:
184 |
185 | - `db`: `AutoMigrate` 方法是 GORM 的 `*gorm.DB` 类型的方法,表示数据库连接对象。
186 | - `dst`: 一个或多个要进行自动迁移的模型(结构体)的变量或指针。你可以传递多个模型作为参数,`AutoMigrate` 会依次创建或更新这些模型对应的数据库表。
187 |
188 | 举个例子:
189 |
190 | ```go
191 | import (
192 | "gorm.io/driver/mysql"
193 | "gorm.io/gorm"
194 | )
195 |
196 | func main() {
197 |
198 | // 数据库连接
199 | dsn := "user:password@tcp(database-host:port)/database-name?charset=utf8mb4&parseTime=True&loc=Local"
200 | db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
201 | if err != nil {
202 | panic("无法连接到数据库")
203 | }
204 |
205 | // 自动建表或更新表结构
206 | db.AutoMigrate(&User{})
207 | db.AutoMigrate(&Video{})
208 | db.AutoMigrate(&Favorite{})
209 | db.AutoMigrate(&Relation{})
210 | db.AutoMigrate(&Comment{})
211 | db.AutoMigrate(&Message{})
212 |
213 | // ......
214 | }
215 | ```
216 |
217 | 在上述示例中,我们首先连接到数据库,然后使用 `AutoMigrate` 函数传递了一系列模型(`User`、`Video` 等),这些模型的表结构会被创建或更新以匹配模型定义。
218 |
219 | ---
220 |
221 | ## dao.go
222 |
223 | 在 `models` 包下的`dao.go` 定义了基本的数据库操作函数,用于执行与用户、视频、点赞、评论等相关的数据库操作。它们可以实现各种与数据库交互的功能,如判断用户关注状态、点赞状态,增加或减少统计数据等。根据你的应用需求,你可以在这些基础函数的基础上进一步构建更复杂的功能和业务逻辑。
224 |
225 | ---
226 |
227 | ## 注意事项
228 |
229 | - 使用 `AutoMigrate` 时,请确保传递的模型结构与数据库表的当前状态匹配。如果你已经有了一个具有数据的数据库表,不要轻易更改字段名称或类型,以免造成数据丢失或不兼容。
230 | - 如果你的应用程序在生产环境中使用,建议在升级数据库表结构时小心谨慎,并备份重要数据。
231 | - `AutoMigrate` 只会自动创建或更新表结构,不会执行其他数据迁移任务,如数据导入、数据转换等。对于复杂的数据库迁移需求,你可能需要使用其他数据库迁移工具,如 `gormigrate`。
232 |
233 | ---
234 |
235 | ## 参考资料
236 |
237 | - [GORM官方文档](https://gorm.io/docs/) - `GORM` 官方文档提供了详细的使用指南、示例和 `API` 文档。
238 | - [GORM GitHub仓库](https://github.com/go-gorm/gorm) - `GORM` 的 `GitHub` 仓库包含了源代码、问题跟踪和社区讨论,可以查找最新的更新和问题解决方案。
--------------------------------------------------------------------------------
/setup/sampledata_init.go:
--------------------------------------------------------------------------------
1 | package setup
2 |
3 | import (
4 | "time"
5 |
6 | "github.com/zheng-yi-yi/simpledouyin/config"
7 | "github.com/zheng-yi-yi/simpledouyin/models"
8 | "gorm.io/gorm"
9 | )
10 |
11 | const testUserPassword = "$2a$16$qOVr7vIeEZcah73BCpocV.Sq/m70sK4YusyEIU73TrvEj8YKzH7aK" // 样例数据,测试用户的密码均为:123456
12 |
13 | // ========== 用户表-样例数据 ==========
14 |
15 | var initialUsers = []models.User{
16 | {
17 | UserName: "User1", // 用户名
18 | PassWord: testUserPassword, // 密码
19 | FollowCount: 3, // 关注总数
20 | FollowerCount: 5, // 粉丝总数
21 | FavoriteCount: 6, // 喜欢数
22 | Avatar: config.USER1_AVATAR_URL, // 用户头像
23 | BackgroundImage: config.USER1_BACKGROUND_IMAGE_URL, // 用户个人页顶部大图
24 | Signature: config.USER1_PROFILE_DESCRIPTION, // 个人简介
25 | TotalFavorited: "8", // 获赞数量
26 | WorkCount: 2, // 作品数
27 | },
28 | {
29 | UserName: "User2", // 用户名
30 | PassWord: testUserPassword, // 密码
31 | FollowCount: 3, // 关注总数
32 | FollowerCount: 3, // 粉丝总数
33 | FavoriteCount: 7, // 喜欢数
34 | Avatar: config.USER2_AVATAR_URL, // 用户头像
35 | BackgroundImage: config.USER2_BACKGROUND_IMAGE_URL, // 用户个人页顶部大图
36 | Signature: config.USER2_PROFILE_DESCRIPTION, // 个人简介
37 | TotalFavorited: "4", // 获赞数量
38 | WorkCount: 1, // 作品数
39 | },
40 | {
41 | UserName: "User3", // 用户名
42 | PassWord: testUserPassword, // 密码
43 | FollowCount: 4, // 关注总数
44 | FollowerCount: 3, // 粉丝总数
45 | FavoriteCount: 7, // 喜欢数
46 | Avatar: config.USER3_AVATAR_URL, // 用户头像
47 | BackgroundImage: config.USER3_BACKGROUND_IMAGE_URL, // 用户个人页顶部大图
48 | Signature: config.USER3_PROFILE_DESCRIPTION, // 个人简介
49 | TotalFavorited: "6", // 获赞数量
50 | WorkCount: 2, // 作品数
51 | },
52 | {
53 | UserName: "User4", // 用户名
54 | PassWord: testUserPassword, // 密码
55 | FollowCount: 4, // 关注总数
56 | FollowerCount: 4, // 粉丝总数
57 | FavoriteCount: 6, // 喜欢数
58 | Avatar: config.USER4_AVATAR_URL, // 用户头像
59 | BackgroundImage: config.USER4_BACKGROUND_IMAGE_URL, // 用户个人页顶部大图
60 | Signature: config.USER4_PROFILE_DESCRIPTION, // 个人简介
61 | TotalFavorited: "7", // 获赞数量
62 | WorkCount: 2, // 作品数
63 | },
64 | {
65 | UserName: "User5", // 用户名
66 | PassWord: testUserPassword, // 密码
67 | FollowCount: 3, // 关注总数
68 | FollowerCount: 3, // 粉丝总数
69 | FavoriteCount: 6, // 喜欢数
70 | Avatar: config.USER5_AVATAR_URL, // 用户头像
71 | BackgroundImage: config.USER5_BACKGROUND_IMAGE_URL, // 用户个人页顶部大图
72 | Signature: config.USER5_PROFILE_DESCRIPTION, // 个人简介
73 | TotalFavorited: "11", // 获赞数量
74 | WorkCount: 2, // 作品数
75 | },
76 | {
77 | UserName: "User6", // 用户名
78 | PassWord: testUserPassword, // 密码
79 | FollowCount: 4, // 关注总数
80 | FollowerCount: 3, // 粉丝总数
81 | FavoriteCount: 7, // 喜欢数
82 | Avatar: config.USER6_AVATAR_URL, // 用户头像
83 | BackgroundImage: config.USER6_BACKGROUND_IMAGE_URL, // 用户个人页顶部大图
84 | Signature: config.USER6_PROFILE_DESCRIPTION, // 个人简介
85 | TotalFavorited: "4", // 获赞数量
86 | WorkCount: 1, // 作品数
87 | },
88 | }
89 |
90 | func initUsers(db *gorm.DB) {
91 | var count int64
92 | db.Model(&models.User{}).Count(&count)
93 | if count == 0 {
94 | for _, user := range initialUsers {
95 | db.Create(&user)
96 | }
97 | }
98 | }
99 |
100 | // ========== 视频表-样例数据 ==========
101 |
102 | var initialVideos = []models.Video{
103 | {
104 | UserId: 1, // 用户id
105 | PlayUrl: "videos/1_1.mp4", // 视频地址
106 | CoverUrl: "images/1_1.png", // 封面图地址
107 | FavoriteCount: 5, // 点赞数量
108 | CommentCount: 3, // 评论数量
109 | Description: "回忆这把刀", // 视频标题
110 | CreatedAt: time.Now(),
111 | },
112 | {
113 | UserId: 5, // 用户id
114 | PlayUrl: "videos/5_1.mp4", // 视频地址
115 | CoverUrl: "images/5_1.png", // 封面图地址
116 | FavoriteCount: 5, // 点赞数量
117 | CommentCount: 3, // 评论数量
118 | Description: "而我独缺你一生的了解 #戏曲", // 视频标题
119 | CreatedAt: time.Now(),
120 | },
121 | {
122 | UserId: 3, // 用户id
123 | PlayUrl: "videos/3_1.mp4", // 视频地址
124 | CoverUrl: "images/3_1.png", // 封面图地址
125 | FavoriteCount: 4, // 点赞数量
126 | CommentCount: 2, // 评论数量
127 | Description: "哪有什么突然好想你 明明就心里一直有你.", // 视频标题
128 | CreatedAt: time.Now(),
129 | },
130 | {
131 | UserId: 6, // 用户id
132 | PlayUrl: "videos/6_1.mp4", // 视频地址
133 | CoverUrl: "images/6_1.png", // 封面图地址
134 | FavoriteCount: 4, // 点赞数量
135 | CommentCount: 3, // 评论数量
136 | Description: "好像做了一场短暂的山水梦", // 视频标题
137 | CreatedAt: time.Now(),
138 | },
139 | {
140 | UserId: 1, // 用户id
141 | PlayUrl: "videos/1_2.mp4", // 视频地址
142 | CoverUrl: "images/1_2.png", // 封面图地址
143 | FavoriteCount: 3, // 点赞数量
144 | CommentCount: 2, // 评论数量
145 | Description: "登昆仑兮食玉英,与天地兮同寿与日月兮齐光。#汉服之美在华夏", // 视频标题
146 | CreatedAt: time.Now(),
147 | },
148 | {
149 | UserId: 4, // 用户id
150 | PlayUrl: "videos/4_1.mp4", // 视频地址
151 | CoverUrl: "images/4_1.png", // 封面图地址
152 | FavoriteCount: 4, // 点赞数量
153 | CommentCount: 1, // 评论数量
154 | Description: "世上有无条件的爱吗?", // 视频标题
155 | CreatedAt: time.Now(),
156 | },
157 | {
158 | UserId: 4, // 用户id
159 | PlayUrl: "videos/4_2.mp4", // 视频地址
160 | CoverUrl: "images/4_2.png", // 封面图地址
161 | FavoriteCount: 3, // 点赞数量
162 | CommentCount: 3, // 评论数量
163 | Description: "五档 启动! #太阳神尼卡登场", // 视频标题
164 | CreatedAt: time.Now(),
165 | },
166 | {
167 | UserId: 2, // 用户id
168 | PlayUrl: "videos/2_1.mp4", // 视频地址
169 | CoverUrl: "images/2_1.png", // 封面图地址
170 | FavoriteCount: 3, // 点赞数量
171 | CommentCount: 2, // 评论数量
172 | Description: "那些仅凭半句就封神的诗句", // 视频标题
173 | CreatedAt: time.Now(),
174 | },
175 | {
176 | UserId: 3, // 用户id
177 | PlayUrl: "videos/3_2.mp4", // 视频地址
178 | CoverUrl: "images/3_2.png", // 封面图地址
179 | FavoriteCount: 2, // 点赞数量
180 | CommentCount: 1, // 评论数量
181 | Description: "落日沉溺于橘色的海,晚风沦陷于赤城的爱", // 视频标题
182 | CreatedAt: time.Now(),
183 | },
184 | {
185 | UserId: 5, // 用户id
186 | PlayUrl: "videos/5_2.mp4", // 视频地址
187 | CoverUrl: "images/5_2.png", // 封面图地址
188 | FavoriteCount: 6, // 点赞数量
189 | CommentCount: 4, // 评论数量
190 | Description: "文有太极安天下,武有八极定乾坤 #太极拳 #国风少年", // 视频标题
191 | CreatedAt: time.Now(),
192 | },
193 | }
194 |
195 | func initVideos(db *gorm.DB) {
196 | var count int64
197 | db.Model(&models.Video{}).Count(&count)
198 | if count == 0 {
199 | for _, video := range initialVideos {
200 | db.Create(&video)
201 | }
202 | }
203 | }
204 |
205 | // ========== 点赞表-样例数据 ==========
206 |
207 | var initialFavorites = []models.Favorite{
208 | {UserId: 2, VideoId: 1},
209 | {UserId: 3, VideoId: 1},
210 | {UserId: 4, VideoId: 1},
211 | {UserId: 5, VideoId: 1},
212 | {UserId: 6, VideoId: 1},
213 | {UserId: 1, VideoId: 2},
214 | {UserId: 5, VideoId: 2},
215 | {UserId: 4, VideoId: 2},
216 | {UserId: 6, VideoId: 2},
217 | {UserId: 2, VideoId: 3},
218 | {UserId: 4, VideoId: 3},
219 | {UserId: 6, VideoId: 3},
220 | {UserId: 1, VideoId: 4},
221 | {UserId: 2, VideoId: 4},
222 | {UserId: 3, VideoId: 4},
223 | {UserId: 5, VideoId: 4},
224 | {UserId: 6, VideoId: 4},
225 | {UserId: 1, VideoId: 5},
226 | {UserId: 6, VideoId: 5},
227 | {UserId: 3, VideoId: 5},
228 | {UserId: 4, VideoId: 5},
229 | {UserId: 2, VideoId: 6},
230 | {UserId: 1, VideoId: 6},
231 | {UserId: 3, VideoId: 6},
232 | {UserId: 4, VideoId: 6},
233 | {UserId: 6, VideoId: 7},
234 | {UserId: 1, VideoId: 7},
235 | {UserId: 2, VideoId: 7},
236 | {UserId: 2, VideoId: 8},
237 | {UserId: 3, VideoId: 8},
238 | {UserId: 5, VideoId: 8},
239 | {UserId: 3, VideoId: 9},
240 | {UserId: 5, VideoId: 9},
241 | {UserId: 1, VideoId: 10},
242 | {UserId: 2, VideoId: 10},
243 | {UserId: 3, VideoId: 10},
244 | {UserId: 4, VideoId: 10},
245 | {UserId: 5, VideoId: 10},
246 | {UserId: 6, VideoId: 10},
247 | }
248 |
249 | func initFavorites(db *gorm.DB) {
250 | var count int64
251 | db.Model(&models.Favorite{}).Count(&count)
252 | if count == 0 {
253 | for _, favorite := range initialFavorites {
254 | db.Create(&favorite)
255 | }
256 | }
257 | }
258 |
259 | // ========== 评论表-样例数据 ==========
260 |
261 | var initialComments = []models.Comment{
262 | {UserId: 2, VideoId: 1, CreatedAt: time.Now(), Content: "好啦好啦,都在歌单里啦"},
263 | {UserId: 4, VideoId: 1, CreatedAt: time.Now(), Content: "爷青回!!!"},
264 | {UserId: 6, VideoId: 1, CreatedAt: time.Now(), Content: "好活,当赏"},
265 | {UserId: 5, VideoId: 2, CreatedAt: time.Now(), Content: "这竟然是你唱的"},
266 | {UserId: 6, VideoId: 2, CreatedAt: time.Now(), Content: "这个戏腔绝了"},
267 | {UserId: 6, VideoId: 2, CreatedAt: time.Now(), Content: "兰亭序:“而我独缺你一生的了解”\n七里香:“你是我唯一想要的了解”"},
268 | {UserId: 2, VideoId: 3, CreatedAt: time.Now(), Content: "“不要因为廉价的新鲜感放弃长久的陪伴”"},
269 | {UserId: 6, VideoId: 3, CreatedAt: time.Now(), Content: "幸好思念无声"},
270 | {UserId: 2, VideoId: 4, CreatedAt: time.Now(), Content: "说走就走的旅行..."},
271 | {UserId: 1, VideoId: 4, CreatedAt: time.Now(), Content: "这首歌好像在哪里听过!"},
272 | {UserId: 5, VideoId: 4, CreatedAt: time.Now(), Content: "好久没出去玩了,走起"},
273 | {UserId: 3, VideoId: 5, CreatedAt: time.Now(), Content: "变装那一瞬间好高级"},
274 | {UserId: 1, VideoId: 5, CreatedAt: time.Now(), Content: "这服装好看!"},
275 | {UserId: 4, VideoId: 5, CreatedAt: time.Now(), Content: "这种变装是怎么做到的!"},
276 | {UserId: 1, VideoId: 6, CreatedAt: time.Now(), Content: "因世上的至爱是不计较条件..."},
277 | {UserId: 2, VideoId: 7, CreatedAt: time.Now(), Content: "年少的梦终将绽放于盛夏,解放之鼓必将响彻整个夏天"},
278 | {UserId: 6, VideoId: 7, CreatedAt: time.Now(), Content: "啧,怎么说呢......"},
279 | {UserId: 1, VideoId: 7, CreatedAt: time.Now(), Content: "好看"},
280 | {UserId: 3, VideoId: 8, CreatedAt: time.Now(), Content: "落霞与孤鹜齐飞,秋水共长天一色"},
281 | {UserId: 5, VideoId: 8, CreatedAt: time.Now(), Content: "为天地立心,为生民立命,为往圣继绝学,为万世开太平"},
282 | {UserId: 5, VideoId: 9, CreatedAt: time.Now(), Content: "夕阳洒在世界的尽头"},
283 | {UserId: 1, VideoId: 10, CreatedAt: time.Now(), Content: "运动孩多多少少有点不合群"},
284 | {UserId: 4, VideoId: 10, CreatedAt: time.Now(), Content: "这圆画的好圆"},
285 | {UserId: 3, VideoId: 10, CreatedAt: time.Now(), Content: "和我体育老师教的好像一样,但又不怎么一样"},
286 | {UserId: 2, VideoId: 10, CreatedAt: time.Now(), Content: "行云流水"},
287 | }
288 |
289 | func initComments(db *gorm.DB) {
290 | var count int64
291 | db.Model(&models.Comment{}).Count(&count)
292 | if count == 0 {
293 | for _, comment := range initialComments {
294 | db.Create(&comment)
295 | }
296 | }
297 | }
298 |
299 | // ========== 关注关系表-样例数据 ==========
300 |
301 | var initialRelations = []models.Relation{
302 | {FromUserId: 1, ToUserId: 4, Cancel: 0},
303 | {FromUserId: 1, ToUserId: 5, Cancel: 0},
304 | {FromUserId: 2, ToUserId: 1, Cancel: 0},
305 | {FromUserId: 2, ToUserId: 3, Cancel: 0},
306 | {FromUserId: 2, ToUserId: 6, Cancel: 0},
307 | {FromUserId: 3, ToUserId: 1, Cancel: 0},
308 | {FromUserId: 3, ToUserId: 5, Cancel: 0},
309 | {FromUserId: 3, ToUserId: 6, Cancel: 0},
310 | {FromUserId: 3, ToUserId: 4, Cancel: 0},
311 | {FromUserId: 4, ToUserId: 1, Cancel: 0},
312 | {FromUserId: 4, ToUserId: 3, Cancel: 0},
313 | {FromUserId: 4, ToUserId: 2, Cancel: 0},
314 | {FromUserId: 4, ToUserId: 5, Cancel: 0},
315 | {FromUserId: 5, ToUserId: 1, Cancel: 0},
316 | {FromUserId: 5, ToUserId: 4, Cancel: 0},
317 | {FromUserId: 5, ToUserId: 6, Cancel: 0},
318 | {FromUserId: 6, ToUserId: 1, Cancel: 0},
319 | {FromUserId: 6, ToUserId: 3, Cancel: 0},
320 | {FromUserId: 6, ToUserId: 2, Cancel: 0},
321 | {FromUserId: 6, ToUserId: 4, Cancel: 0},
322 | }
323 |
324 | func initRelations(db *gorm.DB) {
325 | var count int64
326 | db.Model(&models.Relation{}).Count(&count)
327 | if count == 0 {
328 | for _, relation := range initialRelations {
329 | db.Create(&relation)
330 | }
331 | }
332 | }
333 |
334 | // ========== 聊天记录表-样例数据 ==========
335 |
336 | var initialMessages = []models.Message{
337 | // 用户一 和 用户二 的初始对话
338 | {FromUserID: 1, ToUserID: 2, CreateTime: time.Now(), Content: "你会GO吗"},
339 | {FromUserID: 2, ToUserID: 1, CreateTime: time.Now(), Content: "会一点,怎么了"},
340 | {FromUserID: 1, ToUserID: 2, CreateTime: time.Now(), Content: "最近做Web应用,有点挑战。"},
341 | {FromUserID: 2, ToUserID: 1, CreateTime: time.Now(), Content: "嗯,Go在Web开发不错。遇到啥挑战了"},
342 | {FromUserID: 1, ToUserID: 2, CreateTime: time.Now(), Content: "处理并发和性能,用goroutines处理多任务,但调度和同步有问题"},
343 | {FromUserID: 2, ToUserID: 1, CreateTime: time.Now(), Content: "那你得注意竞态和内存错误"},
344 | {FromUserID: 1, ToUserID: 2, CreateTime: time.Now(), Content: "对,我用channels同步数据,但偶尔出bug,很纠结"},
345 | {FromUserID: 2, ToUserID: 1, CreateTime: time.Now(), Content: "可能是goroutine间通信问题,检查下channel用法,避免死锁"},
346 | {FromUserID: 1, ToUserID: 2, CreateTime: time.Now(), Content: "我去看一下"},
347 | {FromUserID: 2, ToUserID: 1, CreateTime: time.Now(), Content: "嗯,有问题再来讨论"},
348 | {FromUserID: 1, ToUserID: 2, CreateTime: time.Now(), Content: "好!"},
349 | // 用户二 和 用户三 的初始对话
350 | {FromUserID: 3, ToUserID: 2, CreateTime: time.Now(), Content: "周末有安排嘛"},
351 | {FromUserID: 2, ToUserID: 3, CreateTime: time.Now(), Content: "没,怎么说"},
352 | {FromUserID: 3, ToUserID: 2, CreateTime: time.Now(), Content: "打球哩,走起"},
353 | {FromUserID: 2, ToUserID: 3, CreateTime: time.Now(), Content: "几点"},
354 | {FromUserID: 3, ToUserID: 2, CreateTime: time.Now(), Content: "六点吧"},
355 | {FromUserID: 2, ToUserID: 3, CreateTime: time.Now(), Content: "行,你叫上他们,人多点"},
356 | {FromUserID: 3, ToUserID: 2, CreateTime: time.Now(), Content: "已经在约了"},
357 | {FromUserID: 2, ToUserID: 3, CreateTime: time.Now(), Content: "OK"},
358 | {FromUserID: 3, ToUserID: 2, CreateTime: time.Now(), Content: "你记得带球,我的放学校了"},
359 | {FromUserID: 2, ToUserID: 3, CreateTime: time.Now(), Content: "这,我也是..."},
360 | {FromUserID: 3, ToUserID: 2, CreateTime: time.Now(), Content: "那就让阿凯带球"},
361 | {FromUserID: 2, ToUserID: 3, CreateTime: time.Now(), Content: "ok,我去叫他"},
362 | }
363 |
364 | func initMessages(db *gorm.DB) {
365 | var count int64
366 | db.Model(&models.Message{}).Count(&count)
367 | if count == 0 {
368 | for _, message := range initialMessages {
369 | db.Create(&message)
370 | }
371 | }
372 | }
373 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
2 | cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
3 | cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
4 | cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
5 | cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
6 | cloud.google.com/go v0.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
7 | cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
8 | cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
9 | cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
10 | cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=
11 | cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
12 | cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=
13 | cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=
14 | cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
15 | cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
16 | cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
17 | cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI=
18 | cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk=
19 | cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY=
20 | cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
21 | cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
22 | cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
23 | cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
24 | cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
25 | cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
26 | cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
27 | cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
28 | cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
29 | cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
30 | cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
31 | cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=
32 | cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
33 | cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
34 | cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
35 | cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
36 | cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
37 | cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo=
38 | dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
39 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
40 | github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
41 | github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU=
42 | github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY=
43 | github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY=
44 | github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
45 | github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
46 | github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s=
47 | github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
48 | github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
49 | github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
50 | github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams=
51 | github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
52 | github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
53 | github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
54 | github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
55 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
56 | github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
57 | github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
58 | github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
59 | github.com/davecgh/go-spew v0.0.0-20161028175848-04cdfd42973b/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
60 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
61 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
62 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
63 | github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
64 | github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
65 | github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
66 | github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po=
67 | github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
68 | github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
69 | github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w=
70 | github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
71 | github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo=
72 | github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
73 | github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY=
74 | github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
75 | github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
76 | github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
77 | github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
78 | github.com/gavv/httpexpect/v2 v2.15.0 h1:CCnFk9of4l4ijUhnMxyoEpJsIIBKcuWIFLMwwGTZxNs=
79 | github.com/gavv/httpexpect/v2 v2.15.0/go.mod h1:7myOP3A3VyS4+qnA4cm8DAad8zMN+7zxDB80W9f8yIc=
80 | github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
81 | github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
82 | github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
83 | github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=
84 | github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
85 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
86 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
87 | github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
88 | github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
89 | github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
90 | github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
91 | github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
92 | github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js=
93 | github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
94 | github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc=
95 | github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
96 | github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
97 | github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
98 | github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
99 | github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
100 | github.com/golang-jwt/jwt/v5 v5.0.0 h1:1n1XNM9hk7O9mnQoNBGolZvzebBQ7p93ULHRc28XJUE=
101 | github.com/golang-jwt/jwt/v5 v5.0.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
102 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
103 | github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
104 | github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
105 | github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
106 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
107 | github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
108 | github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
109 | github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
110 | github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
111 | github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
112 | github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
113 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
114 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
115 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
116 | github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
117 | github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
118 | github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
119 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
120 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
121 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
122 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
123 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
124 | github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
125 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
126 | github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
127 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
128 | github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
129 | github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
130 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
131 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
132 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
133 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
134 | github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
135 | github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
136 | github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
137 | github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
138 | github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
139 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
140 | github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
141 | github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
142 | github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
143 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
144 | github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
145 | github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
146 | github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
147 | github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
148 | github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
149 | github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
150 | github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
151 | github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
152 | github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
153 | github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
154 | github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
155 | github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
156 | github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
157 | github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
158 | github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
159 | github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
160 | github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
161 | github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g=
162 | github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
163 | github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
164 | github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
165 | github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
166 | github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
167 | github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
168 | github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
169 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
170 | github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
171 | github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
172 | github.com/imkira/go-interpol v1.1.0 h1:KIiKr0VSG2CUW1hl1jpiyuzuJeKUUpC8iM1AIE7N1Vk=
173 | github.com/imkira/go-interpol v1.1.0/go.mod h1:z0h2/2T3XF8kyEPpRgJ3kmNv+C43p+I/CoI+jC3w2iA=
174 | github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
175 | github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
176 | github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
177 | github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
178 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
179 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
180 | github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
181 | github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
182 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
183 | github.com/klauspost/compress v1.15.0 h1:xqfchp4whNFxn5A4XFyyYtitiWI8Hy5EW59jEwcyL6U=
184 | github.com/klauspost/compress v1.15.0/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
185 | github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
186 | github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk=
187 | github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
188 | github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
189 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
190 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
191 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
192 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
193 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
194 | github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
195 | github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
196 | github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
197 | github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
198 | github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
199 | github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
200 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
201 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
202 | github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
203 | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
204 | github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
205 | github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
206 | github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0=
207 | github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0=
208 | github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
209 | github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
210 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
211 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
212 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
213 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
214 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
215 | github.com/onsi/ginkgo v1.10.1 h1:q/mM8GF/n0shIN8SaAZ0V+jnLPzen6WIVZdiwrRlMlo=
216 | github.com/onsi/gomega v1.7.0 h1:XPnZz8VVBHjVsy1vzJmRwIcSwiUO+JFfrv/xGiigmME=
217 | github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ=
218 | github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4=
219 | github.com/pkg/diff v0.0.0-20200914180035-5b29258ca4f7/go.mod h1:zO8QMzTeZd5cpnIkz/Gn6iK0jDfGicM1nynOkkPIl28=
220 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
221 | github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg=
222 | github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
223 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
224 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
225 | github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
226 | github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
227 | github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
228 | github.com/sanity-io/litter v1.5.5 h1:iE+sBxPBzoK6uaEP5Lt3fHNgpKcHXc/A2HGETy0uJQo=
229 | github.com/sanity-io/litter v1.5.5/go.mod h1:9gzJgR2i4ZpjZHsKvUXIRQVk7P+yM3e+jAF7bU2UI5U=
230 | github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ=
231 | github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
232 | github.com/spf13/afero v1.9.5 h1:stMpOSZFs//0Lv29HduCmli3GUfpFoF3Y1Q/aXj/wVM=
233 | github.com/spf13/afero v1.9.5/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ=
234 | github.com/spf13/cast v1.5.1 h1:R+kOtfhWQE6TVQzY+4D7wJLBgkdVasCEFxSUBYBYIlA=
235 | github.com/spf13/cast v1.5.1/go.mod h1:b9PdjNptOpzXr7Rq1q9gJML/2cdGQAo69NKzQ10KN48=
236 | github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk=
237 | github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
238 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
239 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
240 | github.com/spf13/viper v1.16.0 h1:rGGH0XDZhdUOryiDWjmIvUSWpbNqisK8Wk0Vyefw8hc=
241 | github.com/spf13/viper v1.16.0/go.mod h1:yg78JgCJcbrQOvV9YLXgkLaZqUidkY9K+Dd1FofRzQg=
242 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
243 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
244 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
245 | github.com/stretchr/testify v0.0.0-20161117074351-18a02ba4a312/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
246 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
247 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
248 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
249 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
250 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
251 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
252 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
253 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
254 | github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
255 | github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY=
256 | github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
257 | github.com/subosito/gotenv v1.4.2 h1:X1TuBLAMDFbaTAChgCBLu3DU3UPyELpnF2jjJ2cz/S8=
258 | github.com/subosito/gotenv v1.4.2/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0=
259 | github.com/tailscale/depaware v0.0.0-20210622194025-720c4b409502/go.mod h1:p9lPsd+cx33L3H9nNoecRRxPssFKUwwI50I3pZ0yT+8=
260 | github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
261 | github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
262 | github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
263 | github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
264 | github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
265 | github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
266 | github.com/valyala/fasthttp v1.34.0 h1:d3AAQJ2DRcxJYHm7OXNXtXt2as1vMDfxeIcFvhmGGm4=
267 | github.com/valyala/fasthttp v1.34.0/go.mod h1:epZA5N+7pY6ZaEKRmstzOuYJx9HI8DI1oaCGZpdH4h0=
268 | github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
269 | github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
270 | github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo=
271 | github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
272 | github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0=
273 | github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
274 | github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74=
275 | github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
276 | github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0 h1:6fRhSjgLCkTD3JnJxvaJ4Sj+TYblw757bqYgZaOq5ZY=
277 | github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0/go.mod h1:/LWChgwKmvncFJFHJ7Gvn9wZArjbV5/FppcK2fKk/tI=
278 | github.com/yudai/gojsondiff v1.0.0 h1:27cbfqXLVEJ1o8I6v3y9lg8Ydm53EKqHXAOMxEGlCOA=
279 | github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg=
280 | github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 h1:BHyfKlQyqbsFN5p3IfnEUduWvb9is428/nNb5L3U01M=
281 | github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM=
282 | github.com/yudai/pp v2.0.1+incompatible h1:Q4//iY4pNF6yPLZIigmvcl7k/bPgrcTPIFIcmawg5bI=
283 | github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
284 | github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
285 | github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
286 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
287 | go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
288 | go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
289 | go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
290 | go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
291 | go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
292 | go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
293 | golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
294 | golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k=
295 | golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
296 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
297 | golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
298 | golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
299 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
300 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
301 | golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
302 | golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
303 | golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
304 | golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g=
305 | golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0=
306 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
307 | golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
308 | golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
309 | golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
310 | golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
311 | golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
312 | golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
313 | golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
314 | golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
315 | golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
316 | golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
317 | golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
318 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
319 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
320 | golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
321 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
322 | golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
323 | golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
324 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
325 | golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
326 | golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
327 | golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
328 | golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
329 | golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
330 | golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
331 | golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
332 | golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
333 | golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
334 | golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
335 | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
336 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
337 | golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
338 | golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
339 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
340 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
341 | golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
342 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
343 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
344 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
345 | golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
346 | golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
347 | golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
348 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
349 | golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
350 | golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
351 | golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
352 | golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
353 | golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
354 | golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
355 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
356 | golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
357 | golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
358 | golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
359 | golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
360 | golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
361 | golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
362 | golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
363 | golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
364 | golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
365 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
366 | golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
367 | golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
368 | golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
369 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
370 | golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
371 | golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
372 | golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M=
373 | golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
374 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
375 | golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
376 | golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
377 | golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
378 | golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
379 | golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
380 | golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
381 | golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
382 | golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
383 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
384 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
385 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
386 | golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
387 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
388 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
389 | golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
390 | golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
391 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
392 | golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
393 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
394 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
395 | golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
396 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
397 | golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
398 | golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
399 | golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
400 | golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
401 | golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
402 | golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
403 | golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
404 | golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
405 | golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
406 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
407 | golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
408 | golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
409 | golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
410 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
411 | golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
412 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
413 | golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
414 | golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
415 | golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
416 | golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
417 | golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
418 | golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
419 | golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
420 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
421 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
422 | golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
423 | golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
424 | golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
425 | golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
426 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
427 | golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
428 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
429 | golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
430 | golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
431 | golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
432 | golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
433 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
434 | golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
435 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
436 | golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU=
437 | golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
438 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
439 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
440 | golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
441 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
442 | golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
443 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
444 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
445 | golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
446 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
447 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
448 | golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
449 | golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
450 | golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
451 | golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
452 | golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
453 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
454 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
455 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
456 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
457 | golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
458 | golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
459 | golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
460 | golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
461 | golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
462 | golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
463 | golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
464 | golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
465 | golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
466 | golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
467 | golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
468 | golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
469 | golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
470 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
471 | golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
472 | golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
473 | golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
474 | golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
475 | golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
476 | golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
477 | golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
478 | golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
479 | golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
480 | golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
481 | golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
482 | golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
483 | golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
484 | golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
485 | golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
486 | golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
487 | golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
488 | golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
489 | golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
490 | golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
491 | golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
492 | golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
493 | golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE=
494 | golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
495 | golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
496 | golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
497 | golang.org/x/tools v0.0.0-20201211185031-d93e913c1a58/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
498 | golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
499 | golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
500 | golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
501 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
502 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
503 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
504 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
505 | google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
506 | google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
507 | google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
508 | google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
509 | google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
510 | google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
511 | google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
512 | google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
513 | google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
514 | google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
515 | google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
516 | google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
517 | google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
518 | google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
519 | google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
520 | google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
521 | google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg=
522 | google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE=
523 | google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8=
524 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
525 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
526 | google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
527 | google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
528 | google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
529 | google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
530 | google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
531 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
532 | google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
533 | google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
534 | google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
535 | google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
536 | google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
537 | google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
538 | google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
539 | google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
540 | google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
541 | google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
542 | google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
543 | google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
544 | google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
545 | google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=
546 | google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
547 | google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
548 | google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
549 | google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
550 | google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
551 | google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
552 | google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
553 | google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
554 | google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
555 | google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
556 | google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
557 | google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
558 | google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
559 | google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
560 | google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
561 | google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
562 | google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
563 | google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
564 | google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
565 | google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
566 | google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
567 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
568 | google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
569 | google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
570 | google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
571 | google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
572 | google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
573 | google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
574 | google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
575 | google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=
576 | google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
577 | google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
578 | google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
579 | google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
580 | google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
581 | google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8=
582 | google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
583 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
584 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
585 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
586 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
587 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
588 | google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
589 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
590 | google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
591 | google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
592 | google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
593 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
594 | google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
595 | google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
596 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
597 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
598 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
599 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
600 | gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
601 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
602 | gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
603 | gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
604 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
605 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
606 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
607 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
608 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
609 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
610 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
611 | gorm.io/driver/mysql v1.5.1 h1:WUEH5VF9obL/lTtzjmML/5e6VfFR/788coz2uaVCAZw=
612 | gorm.io/driver/mysql v1.5.1/go.mod h1:Jo3Xu7mMhCyj8dlrb3WoCaRd1FhsVh+yMXb1jUInf5o=
613 | gorm.io/gorm v1.25.1/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=
614 | gorm.io/gorm v1.25.2 h1:gs1o6Vsa+oVKG/a9ElL3XgyGfghFfkKA2SInQaCyMho=
615 | gorm.io/gorm v1.25.2/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=
616 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
617 | honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
618 | honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
619 | honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
620 | honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
621 | honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
622 | honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
623 | moul.io/http2curl/v2 v2.3.0 h1:9r3JfDzWPcbIklMOs2TnIFzDYvfAZvjeavG6EzP7jYs=
624 | moul.io/http2curl/v2 v2.3.0/go.mod h1:RW4hyBjTWSYDOxapodpNEtX0g5Eb16sxklBqmd2RHcE=
625 | rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
626 | rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
627 | rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
628 | rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
629 |
--------------------------------------------------------------------------------