Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 22 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,22 +1,32 @@
# simple-demo

## 抖音项目服务端简单示例
## 抖音项目服务端

具体功能内容参考飞书说明文档
### 点赞功能更新

工程无其他依赖,直接编译运行即可
使用Redis实现,数据暂时没有同步到MySQL的video_favorite,Redis我放的是服务器上了,不用改配置了

```shell
go build && ./simple-demo
```
#### 点赞操作

### 功能说明
在Redis中维护了两个set

接口功能不完善,仅作为示例
+ 以userId为key, value为此用户点赞的视频id集合;
+ 以videoId为key,value为点赞此视频的用户id集合

* 用户登录数据保存在内存中,单次运行过程中有效
* 视频上传后会保存到本地 public 目录中,访问时用 127.0.0.1:8080/static/video_name 即可
#### 点赞列表

### 测试数据
从Redis中查询用户点赞的视频id集合,再对MySQL进行批量查询

测试数据写在 demo_data.go 中,用于列表接口的 mock 测试
#### 其他更新

* 抽取配置文件config.yaml, 配置信息启动时加载到config目录下的对应的struct,包括MySQL、Redis、JWT的配置 [利用Viper实现,参考链接](https://www.topgoer.cn/docs/goday/goday-1crg2dneqeek8)
* 使用JWT生成token,在gin中添加了JWT中间件(middleware目录) [gin中使用jwt](https://www.cnblogs.com/supery007/p/13724121.html)
* initialize 目录下进行一些初始化,包括Config、MySQL、Redis、Router
* global 目录下存放一些公共变量,包括Config、MySQL的连接、Redis的连接
* utils 目录下抽取一些工具方法
* **`utils.GetUserId(c *gin.Context)` 可以获取token中的userId**
* **`utils.IsFavorite(userId, videoId) ` 判断用户是否点赞了该视频**
* service下存放业务操作,repository下直接操作数据库
* model下存放模型与表对应,model.response下存放返回的response结构
* 为了测试方便, 我在demo_data.go的根据model下的模型,定义了user1,user2,以及videos数据,登录接口直接利用user1.Id生成token并返回,用户信息接口也是直接返回user1的信息,视频流接口直接返回videos
* 数据库SQL我也放上来了,之前文档有更新,表结构有改动
17 changes: 17 additions & 0 deletions config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
mysql:
path: 127.0.0.1
port: 3306
config: charset=utf8&parseTime=True&loc=Local
db-name: douyin
username: root
password: 123456
max-idle-conns: 10
max-open-conns: 100
redis:
db: 0
addr: xiaozhang.fit:6379
password: "94264944htz"
jwt:
issuer: douyin
signing-key: af4ffea3-cc5c-4c57-9fbe-86d3c3c370f9
expires-time: 604800
7 changes: 7 additions & 0 deletions config/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package config

type Server struct {
Mysql Mysql `mapstructure:"mysql" yaml:"mysql"`
Redis Redis `mapstructure:"redis" yaml:"redis"`
JWT JWT `mapstructure:"jwt" yaml:"jwt"`
}
7 changes: 7 additions & 0 deletions config/jwt.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package config

type JWT struct {
SigningKey string `mapstructure:"signing-key" yaml:"signing-key"` // jwt签名
ExpiresTime int64 `mapstructure:"expires-time" yaml:"expires-time"` // 过期时间
Issuer string `mapstructure:"issuer" yaml:"issuer"` // 签发者
}
16 changes: 16 additions & 0 deletions config/mysql.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package config

type Mysql struct {
Path string `mapstructure:"path" yaml:"path"` // 服务器地址
Port string `mapstructure:"port" yaml:"port"` // 端口
Config string `mapstructure:"config" yaml:"config"` // 高级配置
Dbname string `mapstructure:"db-name" yaml:"db-name"` // 数据库名
Username string `mapstructure:"username" yaml:"username"` // 数据库用户名
Password string `mapstructure:"password" yaml:"password"` // 数据库密码
MaxIdleConns int `mapstructure:"max-idle-conns" yaml:"max-idle-conns"` // 空闲中的最大连接数
MaxOpenConns int `mapstructure:"max-open-conns" yaml:"max-open-conns"` // 打开到数据库的最大连接数
}

func (m *Mysql) Dsn() string {
return m.Username + ":" + m.Password + "@tcp(" + m.Path + ":" + m.Port + ")/" + m.Dbname + "?" + m.Config
}
7 changes: 7 additions & 0 deletions config/redis.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package config

type Redis struct {
DB int `mapstructure:"db" yaml:"db"` // redis的哪个数据库
Addr string `mapstructure:"addr" yaml:"addr"` // 服务器地址:端口
Password string `mapstructure:"password" yaml:"password"` // 密码
}
41 changes: 41 additions & 0 deletions controller/demo_data.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package controller

import "github.com/RaymondCode/simple-demo/model"

var DemoVideos = []Video{
{
Id: 1,
Expand Down Expand Up @@ -28,3 +30,42 @@ var DemoUser = User{
FollowerCount: 0,
IsFollow: false,
}

var user1 = model.User{
Id: 1,
Name: "zhangsan",
FollowCount: 0,
FollowerCount: 0,
IsFollow: false,
}

var user2 = model.User{
Id: 2,
Name: "lisi",
FollowCount: 0,
FollowerCount: 0,
IsFollow: false,
}

var videos = []model.Video{
{
Id: 1,
Author: user1,
PlayUrl: "https://www.w3schools.com/html/movie.mp4",
CoverUrl: "https://cdn.pixabay.com/photo/2016/03/27/18/10/bear-1283347_1280.jpg",
FavoriteCount: 0,
CommentCount: 0,
IsFavorite: false,
Title: "测试1",
},
{
Id: 2,
Author: user2,
PlayUrl: "https://www.w3schools.com/html/mov_bbb.mp4",
CoverUrl: "https://www.topgoer.cn/uploads/blog/202111/attach_16b79b08788038a8.jpg",
FavoriteCount: 0,
CommentCount: 0,
IsFavorite: false,
Title: "测试2",
},
}
32 changes: 20 additions & 12 deletions controller/favorite.go
Original file line number Diff line number Diff line change
@@ -1,27 +1,35 @@
package controller

import (
"github.com/RaymondCode/simple-demo/model/response"
"github.com/RaymondCode/simple-demo/service"
"github.com/RaymondCode/simple-demo/utils"
"github.com/gin-gonic/gin"
"net/http"
"strconv"
)

// FavoriteAction no practical effect, just check if token is valid
// FavoriteAction 点赞操作
func FavoriteAction(c *gin.Context) {
token := c.Query("token")
videoIdStr := c.Query("video_id")
actionType := c.Query("action_type")

if _, exist := usersLoginInfo[token]; exist {
c.JSON(http.StatusOK, Response{StatusCode: 0})
videoId, err := strconv.ParseInt(videoIdStr, 10, 64)
if err != nil {
response.FailWithMessage("video_id类型错误,无法转换为int", c)
}
if err := service.GroupApp.FavoriteService.FavoriteAction(actionType, utils.GetUserId(c), videoId); err != nil {
response.FailWithMessage(err.Error(), c)
} else {
c.JSON(http.StatusOK, Response{StatusCode: 1, StatusMsg: "User doesn't exist"})
response.Ok(c)
}
}

// FavoriteList all users have same favorite video list
func FavoriteList(c *gin.Context) {
c.JSON(http.StatusOK, VideoListResponse{
Response: Response{
StatusCode: 0,
},
VideoList: DemoVideos,
})
videoList, err := service.GroupApp.FavoriteService.FavoriteList(utils.GetUserId(c))
if err != nil {
response.FailWithMessage("点赞列表查询失败", c)
} else {
response.OkWithVideoList(videoList, "查询成功", c)
}
}
34 changes: 27 additions & 7 deletions controller/feed.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
package controller

import (
"strconv"

"github.com/RaymondCode/simple-demo/model/response"
"github.com/RaymondCode/simple-demo/service"
"github.com/gin-gonic/gin"
"net/http"
"time"
)

type FeedResponse struct {
Expand All @@ -14,9 +16,27 @@ type FeedResponse struct {

// Feed same demo video list for every request
func Feed(c *gin.Context) {
c.JSON(http.StatusOK, FeedResponse{
Response: Response{StatusCode: 0},
VideoList: DemoVideos,
NextTime: time.Now().Unix(),
})
//c.JSON(http.StatusOK, FeedResponse{
// Response: Response{StatusCode: 0},
// VideoList: DemoVideos,
// NextTime: time.Now().Unix(),
//})
var latestTime int64
if c.Query("latest_time") != "" {
t, err := strconv.ParseInt(c.Query("latest_time"), 10, 64)
if err != nil {
response.FailWithMessage("无效的latest_time参数", c)
}
latestTime = t

} else {
latestTime = -1
}

token := c.Query("token")
videos, err := service.GroupApp.FeedService.QueryFeed(latestTime, token)
if err != nil {
response.FailWithMessage("无法返回有效的videos", c)
}
response.OkWithVideoList(videos, "success", c)
}
57 changes: 30 additions & 27 deletions controller/relation.go
Original file line number Diff line number Diff line change
@@ -1,42 +1,45 @@
package controller

import (
"github.com/RaymondCode/simple-demo/model/response"
"github.com/RaymondCode/simple-demo/service"
"github.com/RaymondCode/simple-demo/utils"
"github.com/gin-gonic/gin"
"net/http"
"strconv"
)

type UserListResponse struct {
Response
UserList []User `json:"user_list"`
}

// RelationAction no practical effect, just check if token is valid
//首先判断用户是否有效,获取请求
func RelationAction(c *gin.Context) {
token := c.Query("token")

if _, exist := usersLoginInfo[token]; exist {
c.JSON(http.StatusOK, Response{StatusCode: 0})
} else {
c.JSON(http.StatusOK, Response{StatusCode: 1, StatusMsg: "User doesn't exist"})
toUserIdStr := c.Query("to_user_id")
toUserId, err := strconv.ParseInt(toUserIdStr, 10, 64)
if err != nil {
response.FailWithMessage("to_userId type is error", c)
return
}
actionType := c.Query("action_type")
if err := service.GroupApp.RelationService.RelationAction(utils.GetUserId(c), toUserId, actionType); err != nil {
response.FailWithMessage("RelationAction failed", c)
return
}
response.Ok(c)
}

// FollowList all users have same follow list
//获取关注列表
func FollowList(c *gin.Context) {
c.JSON(http.StatusOK, UserListResponse{
Response: Response{
StatusCode: 0,
},
UserList: []User{DemoUser},
})
userList, err := service.GroupApp.RelationService.FollowList(utils.GetUserId(c))
if err != nil {
response.FailWithMessage("FollowList failed", c)
return
}
response.OkWithUserList(userList, c)
}

// FollowerList all users have same follower list
//获取粉丝列表
func FollowerList(c *gin.Context) {
c.JSON(http.StatusOK, UserListResponse{
Response: Response{
StatusCode: 0,
},
UserList: []User{DemoUser},
})
userList, err := service.GroupApp.RelationService.FollowerList(utils.GetUserId(c))
if err != nil {
response.FailWithMessage("FollowerList failed", c)
return
}
response.OkWithUserList(userList, c)
}
Loading