├── .gitignore ├── .idea ├── .gitignore ├── TikTok.iml └── modules.xml ├── LICENSE ├── README.md ├── config ├── config.go ├── doc.go ├── douyin.sql ├── nginx.conf └── vsftpd.conf ├── controller ├── commentController.go ├── doc.go ├── followController.go ├── likeController.go ├── userController.go └── videoController.go ├── dao ├── commentDao.go ├── commentDao_test.go ├── doc.go ├── followDao.go ├── followDao_test.go ├── initDao.go ├── initDao_test.go ├── likeDao.go ├── likeDao_test.go ├── userDao.go ├── userDao_test.go ├── videoDao.go └── videoDao_test.go ├── document └── sensitiveDict.txt ├── go.mod ├── go.sum ├── images ├── 1.0.png ├── 1.2.png ├── 1.png ├── 10.png ├── 11.png ├── 2.png ├── 3.png ├── 4.png ├── 5.png ├── 6.png ├── 7.png ├── 8.png ├── 9.png ├── awards.jpg ├── awards1.jpg ├── framework.jpg ├── future.png ├── logo1.png ├── logo2.png ├── mysql.jpg ├── rabbitmq.jpg ├── recommend.jpg ├── redis.jpg └── video.png ├── main.go ├── middleware ├── ffmpeg │ ├── doc.go │ └── ffmpeg.go ├── ftp │ ├── doc.go │ └── ftp.go ├── jwt │ ├── auth.go │ ├── authBody.go │ └── doc.go ├── rabbitmq │ ├── commentMQ.go │ ├── doc.go │ ├── followMQ.go │ ├── likeMQ.go │ └── rabbitMQ.go └── redis │ ├── doc.go │ └── redis.go ├── router.go ├── service ├── commentService.go ├── commentServiceImpl.go ├── commentServiceImpl_test.go ├── commentSub.go ├── doc.go ├── followService.go ├── followServiceImpl.go ├── followServiceImpl_test.go ├── followSub.go ├── likeService.go ├── likeServiceImpl.go ├── likeServiceImpl_test.go ├── likeSub.go ├── userService.go ├── userServiceImpl.go ├── userServiceImpl_test.go ├── userSub.go ├── videoService.go ├── videoServiceImpl.go ├── videoServiceImpl_test.go └── videoSub.go └── util └── sensitiveFilter.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Dependency directories (remove the comment below to include it) 15 | # vendor/ 16 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Datasource local storage ignored files 5 | /dataSources/ 6 | /dataSources.local.xml 7 | # Editor-based HTTP Client requests 8 | /httpRequests/ 9 | -------------------------------------------------------------------------------- /.idea/TikTok.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # tiktok 2 | 3 | 4 | 5 | ![GitHub Repo stars](https://img.shields.io/github/stars/HammerCloth/tiktok?style=plastic) 6 | ![GitHub watchers](https://img.shields.io/github/watchers/HammerCloth/tiktok?style=plastic) 7 | ![GitHub forks](https://img.shields.io/github/forks/HammerCloth/tiktok?style=plastic) 8 | ![GitHub contributors](https://img.shields.io/github/contributors/HammerCloth/tiktok) 9 | [![MIT License][license-shield]][license-url] 10 | 11 | 12 | 13 |
14 | 15 |

16 | 17 | Logo 18 | 19 | 20 |

抖音简洁版

21 |

22 |
23 | 探索本项目的文档 » 24 |
25 |
26 |

27 |

28 | 29 | **Attention:** We always welcome contributors to the project. Before adding your contribution, please carefully read our [Git 分支管理规范](https://ypbg9olvt2.feishu.cn/docs/doccnTMRmh7YgMwL2PgZ5moWUsd)和[注释规范](https://juejin.cn/post/7096881555246678046)。 30 | 31 | ## 目录 32 | - [荣誉展示](#荣誉展示) 33 | - [上手指南](#上手指南) 34 | - [开发前的配置要求](#开发前的配置要求) 35 | - [安装步骤](#安装步骤) 36 | - [演示界面](#演示界面) 37 | - [演示视频](#演示视频) 38 | - [文件目录说明](#文件目录说明) 39 | - [开发的整体设计](#开发的整体设计) 40 | - [整体的架构图](#整体的架构图) 41 | - [数据库的设计](#数据库的设计) 42 | - [Redis架构的设计](#Redis架构的设计) 43 | - [RabbitMQ架构的设计](#RabbitMQ架构的设计) 44 | - [服务模块的设计](#服务模块的设计) 45 | - [视频模块的设计](#视频模块的设计) 46 | - [点赞模块的设计](#点赞模块的设计) 47 | - [关注模块的设计](#关注模块的设计) 48 | - [用户模块的设计](#用户模块的设计) 49 | - [评论模块的设计](#评论模块的设计) 50 | - [性能测试](#性能测试) 51 | - [编译项目到linux](#编译项目到linux) 52 | - [使用到的技术](#使用到的技术) 53 | - [未来展望](#未来展望) 54 | - [分布式服务](#分布式服务) 55 | - [推荐视频展望](#推荐视频展望) 56 | - [如何参与开源项目](#如何参与开源项目) 57 | - [版本控制](#版本控制) 58 | - [贡献者](#贡献者) 59 | - [鸣谢](#鸣谢) 60 | 61 | ### 荣誉展示 62 | 63 | Logo 64 | Logo 65 | 66 | 67 | ### 上手指南 68 | 69 | #### 开发前的配置要求 70 | 71 | 1. go 1.18.1 72 | 2. MySQL(数据库sql文件在config包中) 73 | 3. 搭建Redis、RabbitMQ环境 74 | 4. 配置静态资源服务器:安装Nginx、vsftpd、ffmpeg(相关配置文件在config包中) 75 | 5. [最新版抖音客户端软件](https://pan.baidu.com/s/1kXjvYWH12uhvFBARRMBCGg?pwd=6cos) 76 | 77 | 78 | 79 | #### 安装步骤 80 | 1. 下载源码 81 | 2. 配置SSH、FTP、Redis、静态服务器地址等相关参数 82 | 3. 启动服务 83 | 4. 在客户端配置相关地址服务端地址即可 84 | 85 | ```sh 86 | git clone https://github.com/HammerCloth/tiktok.git 87 | ``` 88 | #### 演示界面 89 | **基础功能演示** 90 | 91 | 92 | Logo 93 | Logo 94 | Logo 95 | Logo 96 | 97 | 98 | **拓展功能演示** 99 | 100 | 101 | Logo 102 | Logo 103 | Logo 104 | Logo 105 | 106 | 107 | **设置服务端地址** 108 | 109 | 110 | Logo 111 | Logo 112 | Logo 113 | 114 | 115 | #### 演示视频 116 | [![Watch the video](images/video.png)](http://43.138.25.60/tiktok.mp4) 117 | 118 | ### 文件目录说明 119 | 120 | ``` 121 | tiktok 122 | ├── /.idea/ 123 | ├── /config/ 配置文件包 124 | ├── /controller/ 控制器包 125 | ├── /dao/ 数据库访问 126 | ├── /document/ 敏感词词库 127 | ├── /images/ 图片引用 128 | ├── /middleware/ 中间件 129 | │ ├── ffmpeg/ 视频截图 130 | │ ├── ftp/ 文件服务器 131 | │ ├── jwt/ 鉴权 132 | │ ├── rabbitmq/ 消息队列 133 | │ ├── redis/ 缓存 134 | ├── /service/ 服务层 135 | ├── /util/ 工具 136 | ├── .gitignore 137 | ├── /go.mod/ 138 | ├── LICENSE 139 | ├── main.go 140 | ├── README.md 141 | └── router.go 142 | ``` 143 | 144 | ### 开发的整体设计 145 | #### 整体的架构图 146 |

147 | 148 | Logo 149 | 150 | 151 | #### 数据库的设计 152 |

153 | 154 | Logo 155 | 156 |

157 | 158 | #### Redis架构的设计 159 |

160 | 161 | Logo 162 | 163 |

164 | 165 | #### RabbitMQ架构的设计 166 |

167 | 168 | Logo 169 | 170 |

171 | 172 | #### 服务模块的设计 173 | 174 | ###### 视频模块的设计 175 | 视频模块包括视频Feed流获取、视频投稿和获取用户投稿列表。 176 | 详情请阅读[视频模块设计说明](https://bytedancecampus1.feishu.cn/docs/doccntmcunjHSMzVUNEhGbxjxJh) 查阅为该模块的详细设计。 177 | 178 | ###### 点赞模块的设计 179 | 点赞模块包括点赞视频、取消赞视频和获取点赞列表。 180 | 详情请阅读[点赞模块设计说明](https://bytedancecampus1.feishu.cn/docs/doccn13iJgTIAebIPpMiRqb0Hwb) 查阅为该模块的详细设计。 181 | 182 | ###### 关注模块的设计 183 | 关注模块包括关注、取关、获取关注列表、获取粉丝列表四个基本功能。 184 | 详情请阅读[关注模块的设计说明](https://bytedancecampus1.feishu.cn/docs/doccnOsdm29SufPJkDfRs7tLHgx) 查阅为该模块的详细设计。 185 | 186 | ###### 用户模块的设计 187 | 用户与安全模块包括用户注册、用户登录和用户信息三个部分 188 | 详情请阅读[用户模块的设计说明](https://bytedancecampus1.feishu.cn/docs/doccn1vusmV9oN1ukTCyLpbJ46f) 查阅为该模块的详细设计。 189 | 190 | ###### 评论模块的设计 191 | 评论模块包括发表评论、删除评论和查看评论。 192 | 详情阅读[评论模块的设计说明](https://bytedancecampus1.feishu.cn/docs/doccnDqfcZJW4tTD409NGlYfvCb) 查阅为该模块的详细设计。 193 | 194 | ### 性能测试 195 | 通过命令 go tool pprof -http=:6060 "http://localhost:8080/debug/pprof/profile?seconds=120" 生成了两个版本的火焰图,左图为v1.0,右图为v1.2版本,通过对比两张详细火焰图,优化后的相同方法调用时间更短(添加了相应的中间件) 196 | 197 |

198 | 199 | Logo 200 | Logo 201 | 202 | 203 | 同时包含各个接口的压力测试,详情请阅读[压测报告](https://bytedancecampus1.feishu.cn/docs/doccnoDHHJ84k94G1I2TxHj9Udh) 获得具体的压力数据。 204 | ### 编译项目到linux 205 | ```shell 206 | CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build ./ 207 | ``` 208 | 209 | ### 使用到的技术 210 | 框架相关: 211 | - [Gin](https://gin-gonic.com/docs/) 212 | - [Gorm](https://gorm.io/docs/) 213 | 214 | 服务器相关: 215 | - [Nginx](https://www.nginx-cn.net/) 216 | - [vsftpd](https://www.linuxfromscratch.org/blfs/view/svn/server/vsftpd.html) 217 | - [ffmpeg](https://ffmpeg.org/documentation.html) 218 | - [goftp](http://t.zoukankan.com/lvdongjie-p-9554849.html) 219 | 220 | 中间件相关: 221 | - [Redis](https://redis.io/docs/) 222 | - [RabbitMQ](https://www.rabbitmq.com/documentation.html) 223 | 224 | 数据库: 225 | - [MySQL](https://dev.mysql.com/doc/) 226 | 227 | ### 未来展望 228 | #### 分布式服务 229 | 利用dubbogo来完成分布式,貔貅作为网关,Nacos作为注册中心,将五个模块分别布置到不同的服务器上,以rpc调用的方式来调用当前模块依赖其他模块的方法,做到分布式处理与解耦。 230 |

231 | 232 | Logo 233 | 234 |

235 | 236 | #### 推荐视频展望 237 | 队伍创新推荐算法 238 |

239 | 240 | Logo 241 | 242 |

243 | 244 | 详情请阅读[视频推荐展望](https://bytedancecampus1.feishu.cn/docx/doxcnVIK62rWgR0iE49UAI2wB0b) 245 | 246 | ### 如何参与开源项目 247 | 248 | 贡献使开源社区成为一个学习、激励和创造的绝佳场所。你所作的任何贡献都是**非常感谢**的。 249 | 250 | 1. Fork the Project 251 | 2. Create your Feature Branch (`git checkout -b feature/AmazingFeature`) 252 | 3. Commit your Changes (`git commit -m 'Add some AmazingFeature'`) 253 | 4. Push to the Branch (`git push origin feature/AmazingFeature`) 254 | 5. Open a Pull Request 255 | 256 | ### 版本控制 257 | 258 | 该项目使用Git进行版本管理。您可以在repository参看当前可用版本。 259 | 260 | ### 贡献者 261 | - 司一雄 邮箱:18552541076@163.com 262 | - 刘宗舟 邮箱:1245314855@qq.com 263 | - 蒋宇栋 邮箱:jiangyudong123@qq.com 264 | - 李思源 邮箱:yuanlaisini_002@qq.com 265 | - 李林森 邮箱:1412837463@qq.com 266 | 267 | *您也可以查阅仓库为该项目做出贡献的开发者。* 268 | 269 | ### 版权说明 270 | 271 | 该项目签署了MIT 授权许可,详情请参阅 [LICENSE.txt](https://github.com/shaojintian/Best_README_template/blob/master/LICENSE.txt) 272 | 273 | ### 鸣谢 274 | 275 | - [字节跳动后端青训营](https://youthcamp.bytedance.com/) 276 | 277 | 278 | 279 | [license-shield]: https://img.shields.io/github/license/mrxuexi/tiktok.svg?style=flat-square 280 | 281 | [license-url]: https://github.com/mrxuexi/tiktok/blob/master/LICENSE.txt 282 | -------------------------------------------------------------------------------- /config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import "time" 4 | 5 | // Secret 密钥 6 | var Secret = "tiktok" 7 | 8 | // OneDayOfHours 时间 9 | var OneDayOfHours = 60 * 60 * 24 10 | var OneMinute = 60 * 1 11 | var OneMonth = 60 * 60 * 24 * 30 12 | var OneYear = 365 * 60 * 60 * 24 13 | var ExpireTime = time.Hour * 48 // 设置Redis数据热度消散时间。 14 | 15 | // VideoCount 每次获取视频流的数量 16 | const VideoCount = 5 17 | 18 | // ConConfig ftp服务器地址 19 | const ConConfig = "43.138.25.60:21" 20 | const FtpUser = "ftpuser" 21 | const FtpPsw = "424193726" 22 | const HeartbeatTime = 2 * 60 23 | 24 | // PlayUrlPrefix 存储的图片和视频的链接 25 | const PlayUrlPrefix = "http://43.138.25.60/" 26 | const CoverUrlPrefix = "http://43.138.25.60/images/" 27 | 28 | // HostSSH SSH配置 29 | const HostSSH = "43.138.25.60" 30 | const UserSSH = "ftpuser" 31 | const PasswordSSH = "424193726" 32 | const TypeSSH = "password" 33 | const PortSSH = 22 34 | const MaxMsgCount = 100 35 | const SSHHeartbeatTime = 10 * 60 36 | 37 | const ValidComment = 0 //评论状态:有效 38 | const InvalidComment = 1 //评论状态:取消 39 | const DateTime = "2006-01-02 15:04:05" 40 | 41 | //const ChanCapacity = 10 //chan管道容量,暂时没定 42 | 43 | const IsLike = 0 //点赞的状态 44 | const Unlike = 1 //取消赞的状态 45 | const LikeAction = 1 //点赞的行为 46 | const Attempts = 3 //操作数据库的最大尝试次数 47 | 48 | const DefaultRedisValue = -1 //redis中key对应的预设值,防脏读 49 | -------------------------------------------------------------------------------- /config/doc.go: -------------------------------------------------------------------------------- 1 | // Package config 2 | // @Description 用于保存一些全局的变量和一些配置 3 | // @Author 创建人siyixiong; 4 | // @Update 20220513 (创建/修改时间); 5 | package config 6 | -------------------------------------------------------------------------------- /config/douyin.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Navicat Premium Data Transfer 3 | 4 | Source Server : 抖音 5 | Source Server Type : MySQL 6 | Source Server Version : 50650 7 | Source Host : 43.138.25.60:3306 8 | Source Schema : douyin 9 | 10 | Target Server Type : MySQL 11 | Target Server Version : 50650 12 | File Encoding : 65001 13 | 14 | Date: 04/06/2022 23:43:31 15 | */ 16 | 17 | SET NAMES utf8mb4; 18 | SET FOREIGN_KEY_CHECKS = 0; 19 | 20 | -- ---------------------------- 21 | -- Table structure for comments 22 | -- ---------------------------- 23 | DROP TABLE IF EXISTS `comments`; 24 | CREATE TABLE `comments` ( 25 | `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '评论id,自增主键', 26 | `user_id` bigint(20) NOT NULL COMMENT '评论发布用户id', 27 | `video_id` bigint(20) NOT NULL COMMENT '评论视频id', 28 | `comment_text` varchar(255) NOT NULL COMMENT '评论内容', 29 | `create_date` datetime NOT NULL COMMENT '评论发布时间', 30 | `cancel` tinyint(4) NOT NULL DEFAULT '0' COMMENT '默认评论发布为0,取消后为1', 31 | PRIMARY KEY (`id`), 32 | KEY `videoIdIdx` (`video_id`) USING BTREE COMMENT '评论列表使用视频id作为索引-方便查看视频下的评论列表' 33 | ) ENGINE=InnoDB AUTO_INCREMENT=1206 DEFAULT CHARSET=utf8 COMMENT='评论表'; 34 | 35 | -- ---------------------------- 36 | -- Table structure for follows 37 | -- ---------------------------- 38 | DROP TABLE IF EXISTS `follows`; 39 | CREATE TABLE `follows` ( 40 | `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '自增主键', 41 | `user_id` bigint(20) NOT NULL COMMENT '用户id', 42 | `follower_id` bigint(20) NOT NULL COMMENT '关注的用户', 43 | `cancel` tinyint(4) NOT NULL DEFAULT '0' COMMENT '默认关注为0,取消关注为1', 44 | PRIMARY KEY (`id`), 45 | UNIQUE KEY `userIdToFollowerIdIdx` (`user_id`,`follower_id`) USING BTREE, 46 | KEY `FollowerIdIdx` (`follower_id`) USING BTREE 47 | ) ENGINE=InnoDB AUTO_INCREMENT=1096 DEFAULT CHARSET=utf8 COMMENT='关注表'; 48 | 49 | -- ---------------------------- 50 | -- Table structure for likes 51 | -- ---------------------------- 52 | DROP TABLE IF EXISTS `likes`; 53 | CREATE TABLE `likes` ( 54 | `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '自增主键', 55 | `user_id` bigint(20) NOT NULL COMMENT '点赞用户id', 56 | `video_id` bigint(20) NOT NULL COMMENT '被点赞的视频id', 57 | `cancel` tinyint(4) NOT NULL DEFAULT '0' COMMENT '默认点赞为0,取消赞为1', 58 | PRIMARY KEY (`id`), 59 | UNIQUE KEY `userIdtoVideoIdIdx` (`user_id`,`video_id`) USING BTREE, 60 | KEY `userIdIdx` (`user_id`) USING BTREE, 61 | KEY `videoIdx` (`video_id`) USING BTREE 62 | ) ENGINE=InnoDB AUTO_INCREMENT=1229 DEFAULT CHARSET=utf8 COMMENT='点赞表'; 63 | 64 | -- ---------------------------- 65 | -- Table structure for users 66 | -- ---------------------------- 67 | DROP TABLE IF EXISTS `users`; 68 | CREATE TABLE `users` ( 69 | `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '用户id,自增主键', 70 | `name` varchar(255) NOT NULL COMMENT '用户名', 71 | `password` varchar(255) NOT NULL COMMENT '用户密码', 72 | PRIMARY KEY (`id`), 73 | KEY `name_password_idx` (`name`,`password`) USING BTREE 74 | ) ENGINE=InnoDB AUTO_INCREMENT=20044 DEFAULT CHARSET=utf8 COMMENT='用户表'; 75 | 76 | -- ---------------------------- 77 | -- Table structure for videos 78 | -- ---------------------------- 79 | DROP TABLE IF EXISTS `videos`; 80 | CREATE TABLE `videos` ( 81 | `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '自增主键,视频唯一id', 82 | `author_id` bigint(20) NOT NULL COMMENT '视频作者id', 83 | `play_url` varchar(255) NOT NULL COMMENT '播放url', 84 | `cover_url` varchar(255) NOT NULL COMMENT '封面url', 85 | `publish_time` datetime NOT NULL COMMENT '发布时间戳', 86 | `title` varchar(255) DEFAULT NULL COMMENT '视频名称', 87 | PRIMARY KEY (`id`), 88 | KEY `time` (`publish_time`) USING BTREE, 89 | KEY `author` (`author_id`) USING BTREE 90 | ) ENGINE=InnoDB AUTO_INCREMENT=115 DEFAULT CHARSET=utf8 COMMENT='\r\n视频表'; 91 | 92 | -- ---------------------------- 93 | -- Procedure structure for addFollowRelation 94 | -- ---------------------------- 95 | DROP PROCEDURE IF EXISTS `addFollowRelation`; 96 | delimiter ;; 97 | CREATE PROCEDURE `addFollowRelation`(IN user_id int,IN follower_id int) 98 | BEGIN 99 | #Routine body goes here... 100 | # 声明记录个数变量。 101 | DECLARE cnt INT DEFAULT 0; 102 | # 获取记录个数变量。 103 | SELECT COUNT(1) FROM follows f where f.user_id = user_id AND f.follower_id = follower_id INTO cnt; 104 | # 判断是否已经存在该记录,并做出相应的插入关系、更新关系动作。 105 | # 插入操作。 106 | IF cnt = 0 THEN 107 | INSERT INTO follows(`user_id`,`follower_id`) VALUES(user_id,follower_id); 108 | END IF; 109 | # 更新操作 110 | IF cnt != 0 THEN 111 | UPDATE follows f SET f.cancel = 0 WHERE f.user_id = user_id AND f.follower_id = follower_id; 112 | END IF; 113 | END 114 | ;; 115 | delimiter ; 116 | 117 | -- ---------------------------- 118 | -- Procedure structure for delFollowRelation 119 | -- ---------------------------- 120 | DROP PROCEDURE IF EXISTS `delFollowRelation`; 121 | delimiter ;; 122 | CREATE PROCEDURE `delFollowRelation`(IN `user_id` int,IN `follower_id` int) 123 | BEGIN 124 | #Routine body goes here... 125 | # 定义记录个数变量,记录是否存在此关系,默认没有关系。 126 | DECLARE cnt INT DEFAULT 0; 127 | # 查看是否之前有关系。 128 | SELECT COUNT(1) FROM follows f WHERE f.user_id = user_id AND f.follower_id = follower_id INTO cnt; 129 | # 有关系,则需要update cancel = 1,使其关系无效。 130 | IF cnt = 1 THEN 131 | UPDATE follows f SET f.cancel = 1 WHERE f.user_id = user_id AND f.follower_id = follower_id; 132 | END IF; 133 | END 134 | ;; 135 | delimiter ; 136 | 137 | SET FOREIGN_KEY_CHECKS = 1; 138 | -------------------------------------------------------------------------------- /config/nginx.conf: -------------------------------------------------------------------------------- 1 | # 服务器nginx配置 2 | # 已经配置好图片服务器,视频服务器 3 | #user www www; 4 | worker_processes auto; 5 | #error_log /www/wwwlogs/nginx_error.log crit; 6 | pid /www/server/nginx/logs/nginx.pid; 7 | worker_rlimit_nofile 51200; 8 | 9 | events 10 | { 11 | use epoll; 12 | worker_connections 51200; 13 | multi_accept on; 14 | } 15 | 16 | http 17 | { 18 | include mime.types; 19 | #include luawaf.conf; 20 | 21 | #include proxy.conf; 22 | 23 | default_type application/octet-stream; 24 | 25 | #server_names_hash_bucket_size 512; 26 | #client_header_buffer_size 32k; 27 | #large_client_header_buffers 4 32k; 28 | #client_max_body_size 50m; 29 | 30 | sendfile on; 31 | tcp_nopush on; 32 | 33 | keepalive_timeout 60; 34 | 35 | tcp_nodelay on; 36 | autoindex on; #开启nginx目录浏览功能 37 | autoindex_exact_size off; #文件大小从KB开始显示 38 | autoindex_localtime on; #显示文件修改时间为服务器本地时间 39 | 40 | fastcgi_connect_timeout 300; 41 | fastcgi_send_timeout 300; 42 | fastcgi_read_timeout 300; 43 | fastcgi_buffer_size 64k; 44 | fastcgi_buffers 4 64k; 45 | fastcgi_busy_buffers_size 128k; 46 | fastcgi_temp_file_write_size 256k; 47 | fastcgi_intercept_errors on; 48 | gzip on; 49 | gzip_min_length 1k; 50 | gzip_buffers 4 16k; 51 | gzip_http_version 1.1; 52 | gzip_comp_level 2; 53 | gzip_types text/plain application/javascript application/x-javascript text/javascript text/css application/xml; 54 | gzip_vary on; 55 | gzip_proxied expired no-cache no-store private auth; 56 | gzip_disable "MSIE [1-6]\."; 57 | 58 | limit_conn_zone $binary_remote_addr zone=perip:10m; 59 | limit_conn_zone $server_name zone=perserver:10m; 60 | 61 | server_tokens off; 62 | access_log off; 63 | 64 | server 65 | { 66 | listen 80; 67 | # server_name video; 68 | index index.html; 69 | root /home/ftpuser/video; 70 | 71 | error_page 404 /404.html; 72 | #include enable-php.conf; 73 | 74 | location / { 75 | root html; 76 | index index.html index.htm; 77 | } 78 | #http://43.138.25.60/1.mp4 79 | location ~ .*\.mp4$ { 80 | mp4; 81 | } 82 | #http://43.138.25.60/images/a.jpg 83 | location /images{ 84 | root /home/ftpuser; 85 | } 86 | access_log /www/wwwlogs/access.log; 87 | } 88 | # include /www/server/panel/vhost/nginx/*.conf; 89 | } 90 | -------------------------------------------------------------------------------- /config/vsftpd.conf: -------------------------------------------------------------------------------- 1 | # Example config file /etc/vsftpd/vsftpd.conf 2 | # 3 | # The default compiled in settings are fairly paranoid. This sample file 4 | # loosens things up a bit, to make the ftp daemon more usable. 5 | # Please see vsftpd.conf.5 for all compiled in defaults. 6 | # 7 | # READ THIS: This example file is NOT an exhaustive list of vsftpd options. 8 | # Please read the vsftpd.conf.5 manual page to get a full idea of vsftpd's 9 | # capabilities. 10 | # 11 | # Allow anonymous FTP? (Beware - allowed by default if you comment this out). 12 | anonymous_enable=NO 13 | # 14 | # Uncomment this to allow local users to log in. 15 | # When SELinux is enforcing check for SE bool ftp_home_dir 16 | local_enable=YES 17 | # 18 | # Uncomment this to enable any form of FTP write command. 19 | write_enable=YES 20 | # 21 | # Default umask for local users is 077. You may wish to change this to 022, 22 | # if your users expect that (022 is used by most other ftpd's) 23 | local_umask=022 24 | # 25 | # Uncomment this to allow the anonymous FTP user to upload files. This only 26 | # has an effect if the above global write enable is activated. Also, you will 27 | # obviously need to create a directory writable by the FTP user. 28 | # When SELinux is enforcing check for SE bool allow_ftpd_anon_write, allow_ftpd_full_access 29 | #anon_upload_enable=YES 30 | # 31 | # Uncomment this if you want the anonymous FTP user to be able to create 32 | # new directories. 33 | #anon_mkdir_write_enable=YES 34 | # 35 | # Activate directory messages - messages given to remote users when they 36 | # go into a certain directory. 37 | dirmessage_enable=YES 38 | # 39 | # Activate logging of uploads/downloads. 40 | xferlog_enable=YES 41 | # 42 | # Make sure PORT transfer connections originate from port 20 (ftp-data). 43 | connect_from_port_20=YES 44 | # You may override where the log file goes if you like. The default is shown 45 | # below. 46 | #xferlog_file=/var/log/xferlog 47 | # 48 | # If you want, you can have your log file in standard ftpd xferlog format. 49 | # Note that the default log file location is /var/log/xferlog in this case. 50 | xferlog_std_format=YES 51 | # 52 | # You may change the default value for timing out an idle session. 53 | #idle_session_timeout=600 54 | # 55 | # You may change the default value for timing out a data connection. 56 | #data_connection_timeout=120 57 | # 58 | # It is recommended that you define on your system a unique user which the 59 | # ftp server can use as a totally isolated and unprivileged user. 60 | #nopriv_user=ftpsecure 61 | # 62 | # Enable this and the server will recognise asynchronous ABOR requests. Not 63 | # recommended for security (the code is non-trivial). Not enabling it, 64 | # however, may confuse older FTP clients. 65 | #async_abor_enable=YES 66 | # 67 | # By default the server will pretend to allow ASCII mode but in fact ignore 68 | # the request. Turn on the below options to have the server actually do ASCII 69 | # mangling on files when in ASCII mode. The vsftpd.conf(5) man page explains 70 | # the behaviour when these options are disabled. 71 | # Beware that on some FTP servers, ASCII support allows a denial of service 72 | # attack (DoS) via the command "SIZE /big/file" in ASCII mode. vsftpd 73 | # predicted this attack and has always been safe, reporting the size of the 74 | # raw file. 75 | # ASCII mangling is a horrible feature of the protocol. 76 | #ascii_upload_enable=YES 77 | #ascii_download_enable=YES 78 | # 79 | # You may fully customise the login banner string: 80 | #ftpd_banner=Welcome to blah FTP service. 81 | # 82 | # You may specify a file of disallowed anonymous e-mail addresses. Apparently 83 | # useful for combatting certain DoS attacks. 84 | #deny_email_enable=YES 85 | # (default follows) 86 | #banned_email_file=/etc/vsftpd/banned_emails 87 | # 88 | # You may specify an explicit list of local users to chroot() to their home 89 | # directory. If chroot_local_user is YES, then this list becomes a list of 90 | # users to NOT chroot(). 91 | # (Warning! chroot'ing can be very dangerous. If using chroot, make sure that 92 | # the user does not have write access to the top level directory within the 93 | # chroot) 94 | #chroot_local_user=YES 95 | #chroot_list_enable=YES 96 | # (default follows) 97 | #chroot_list_file=/etc/vsftpd/chroot_list 98 | # 99 | # You may activate the "-R" option to the builtin ls. This is disabled by 100 | # default to avoid remote users being able to cause excessive I/O on large 101 | # sites. However, some broken FTP clients such as "ncftp" and "mirror" assume 102 | # the presence of the "-R" option, so there is a strong case for enabling it. 103 | #ls_recurse_enable=YES 104 | # 105 | # When "listen" directive is enabled, vsftpd runs in standalone mode and 106 | # listens on IPv4 sockets. This directive cannot be used in conjunction 107 | # with the listen_ipv6 directive. 108 | listen=YES 109 | # 110 | # This directive enables listening on IPv6 sockets. By default, listening 111 | # on the IPv6 "any" address (::) will accept connections from both IPv6 112 | # and IPv4 clients. It is not necessary to listen on *both* IPv4 and IPv6 113 | # sockets. If you want that (perhaps because you want to listen on specific 114 | # addresses) then you must run two copies of vsftpd with two configuration 115 | # files. 116 | # Make sure, that one of the listen options is commented !! 117 | listen_ipv6=NO 118 | 119 | pam_service_name=vsftpd 120 | userlist_enable=YES 121 | tcp_wrappers=YES 122 | #仅允许user_list文件中的用户访问FTP服务 123 | userlist_deny=NO 124 | #被动模式端口范围 125 | pasv_min_port=30000 126 | pasv_max_port=30999 127 | 128 | -------------------------------------------------------------------------------- /controller/commentController.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "TikTok/dao" 5 | "TikTok/service" 6 | "TikTok/util" 7 | "github.com/gin-gonic/gin" 8 | "log" 9 | "net/http" 10 | "strconv" 11 | "time" 12 | ) 13 | 14 | // CommentListResponse 15 | // 评论列表返回参数 16 | type CommentListResponse struct { 17 | StatusCode int32 `json:"status_code"` 18 | StatusMsg string `json:"status_msg,omitempty"` 19 | CommentList []service.CommentInfo `json:"comment_list,omitempty"` 20 | } 21 | 22 | // CommentActionResponse 23 | // 发表评论返回参数 24 | type CommentActionResponse struct { 25 | StatusCode int32 `json:"status_code"` 26 | StatusMsg string `json:"status_msg,omitempty"` 27 | Comment service.CommentInfo `json:"comment"` 28 | } 29 | 30 | // CommentAction 31 | // 发表 or 删除评论 comment/action/ 32 | func CommentAction(c *gin.Context) { 33 | log.Println("CommentController-Comment_Action: running") //函数已运行 34 | //获取userId 35 | id, _ := c.Get("userId") 36 | userid, _ := id.(string) 37 | userId, err := strconv.ParseInt(userid, 10, 64) 38 | log.Printf("err:%v", err) 39 | log.Printf("userId:%v", userId) 40 | //错误处理 41 | if err != nil { 42 | c.JSON(http.StatusOK, CommentActionResponse{ 43 | StatusCode: -1, 44 | StatusMsg: "comment userId json invalid", 45 | }) 46 | log.Println("CommentController-Comment_Action: return comment userId json invalid") //函数返回userId无效 47 | return 48 | } 49 | //获取videoId 50 | videoId, err := strconv.ParseInt(c.Query("video_id"), 10, 64) 51 | //错误处理 52 | if err != nil { 53 | c.JSON(http.StatusOK, CommentActionResponse{ 54 | StatusCode: -1, 55 | StatusMsg: "comment videoId json invalid", 56 | }) 57 | log.Println("CommentController-Comment_Action: return comment videoId json invalid") //函数返回视频id无效 58 | return 59 | } 60 | //获取操作类型 61 | actionType, err := strconv.ParseInt(c.Query("action_type"), 10, 32) 62 | //错误处理 63 | if err != nil || actionType < 1 || actionType > 2 { 64 | c.JSON(http.StatusOK, CommentActionResponse{ 65 | StatusCode: -1, 66 | StatusMsg: "comment actionType json invalid", 67 | }) 68 | log.Println("CommentController-Comment_Action: return actionType json invalid") //评论类型数据无效 69 | return 70 | } 71 | //调用service层评论函数 72 | commentService := new(service.CommentServiceImpl) 73 | if actionType == 1 { //actionType为1,则进行发表评论操作 74 | content := c.Query("comment_text") 75 | // 垃圾评论过滤。 76 | content = util.Filter.Replace(content, '#') 77 | // find, _ := util.Filter.FindIn(content) 78 | /*if find { 79 | log.Println("垃圾评论") 80 | c.JSON(http.StatusOK, CommentActionResponse{ 81 | StatusCode: -1, 82 | StatusMsg: "垃圾评论", 83 | }) 84 | return 85 | content = "*****" 86 | } 87 | */ 88 | //发表评论数据准备 89 | var sendComment dao.Comment 90 | sendComment.UserId = userId 91 | sendComment.VideoId = videoId 92 | sendComment.CommentText = content 93 | timeNow := time.Now() 94 | sendComment.CreateDate = timeNow 95 | //发表评论 96 | commentInfo, err := commentService.Send(sendComment) 97 | //发表评论失败 98 | if err != nil { 99 | c.JSON(http.StatusOK, CommentActionResponse{ 100 | StatusCode: -1, 101 | StatusMsg: "send comment failed", 102 | }) 103 | log.Println("CommentController-Comment_Action: return send comment failed") //发表失败 104 | return 105 | } 106 | 107 | //发表评论成功: 108 | //返回结果 109 | c.JSON(http.StatusOK, CommentActionResponse{ 110 | StatusCode: 0, 111 | StatusMsg: "send comment success", 112 | Comment: commentInfo, 113 | }) 114 | log.Println("CommentController-Comment_Action: return Send success") //发表评论成功,返回正确信息 115 | return 116 | } else { //actionType为2,则进行删除评论操作 117 | //获取要删除的评论的id 118 | commentId, err := strconv.ParseInt(c.Query("comment_id"), 10, 64) 119 | if err != nil { 120 | c.JSON(http.StatusOK, CommentActionResponse{ 121 | StatusCode: -1, 122 | StatusMsg: "delete commentId invalid", 123 | }) 124 | log.Println("CommentController-Comment_Action: return commentId invalid") //评论id格式错误 125 | return 126 | } 127 | //删除评论操作 128 | err = commentService.DelComment(commentId) 129 | if err != nil { //删除评论失败 130 | str := err.Error() 131 | c.JSON(http.StatusOK, CommentActionResponse{ 132 | StatusCode: -1, 133 | StatusMsg: str, 134 | }) 135 | log.Println("CommentController-Comment_Action: return delete comment failed") //删除失败 136 | return 137 | } 138 | //删除评论成功 139 | c.JSON(http.StatusOK, CommentActionResponse{ 140 | StatusCode: 0, 141 | StatusMsg: "delete comment success", 142 | }) 143 | 144 | log.Println("CommentController-Comment_Action: return delete success") //函数执行成功,返回正确信息 145 | return 146 | } 147 | } 148 | 149 | // CommentList 150 | // 查看评论列表 comment/list/ 151 | func CommentList(c *gin.Context) { 152 | log.Println("CommentController-Comment_List: running") //函数已运行 153 | //获取userId 154 | id, _ := c.Get("userId") 155 | userid, _ := id.(string) 156 | userId, err := strconv.ParseInt(userid, 10, 64) 157 | //log.Printf("err:%v", err) 158 | //log.Printf("userId:%v", userId) 159 | 160 | //获取videoId 161 | videoId, err := strconv.ParseInt(c.Query("video_id"), 10, 64) 162 | //错误处理 163 | if err != nil { 164 | c.JSON(http.StatusOK, Response{ 165 | StatusCode: -1, 166 | StatusMsg: "comment videoId json invalid", 167 | }) 168 | log.Println("CommentController-Comment_List: return videoId json invalid") //视频id格式有误 169 | return 170 | } 171 | log.Printf("videoId:%v", videoId) 172 | 173 | //调用service层评论函数 174 | commentService := new(service.CommentServiceImpl) 175 | commentList, err := commentService.GetList(videoId, userId) 176 | //commentList, err := commentService.GetListFromRedis(videoId, userId) 177 | if err != nil { //获取评论列表失败 178 | c.JSON(http.StatusOK, CommentListResponse{ 179 | StatusCode: -1, 180 | StatusMsg: err.Error(), 181 | }) 182 | log.Println("CommentController-Comment_List: return list false") //查询列表失败 183 | return 184 | } 185 | 186 | //获取评论列表成功 187 | c.JSON(http.StatusOK, CommentListResponse{ 188 | StatusCode: 0, 189 | StatusMsg: "get comment list success", 190 | CommentList: commentList, 191 | }) 192 | log.Println("CommentController-Comment_List: return success") //成功返回列表 193 | return 194 | } 195 | -------------------------------------------------------------------------------- /controller/doc.go: -------------------------------------------------------------------------------- 1 | // Package controller 2 | // @Description 分层结构的controller包 3 | // @Author 创建人siyixiong; 4 | // @Update 20220513 (创建/修改时间); 5 | package controller 6 | -------------------------------------------------------------------------------- /controller/followController.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "TikTok/service" 5 | "fmt" 6 | "github.com/gin-gonic/gin" 7 | "log" 8 | "net/http" 9 | "strconv" 10 | ) 11 | 12 | // RelationActionResp 关注和取消关注需要返回结构。 13 | type RelationActionResp struct { 14 | Response 15 | } 16 | 17 | // FollowingResp 获取关注列表需要返回的结构。 18 | type FollowingResp struct { 19 | Response 20 | UserList []service.User `json:"user_list,omitempty"` 21 | } 22 | 23 | // FollowersResp 获取粉丝列表需要返回的结构。 24 | type FollowersResp struct { 25 | Response 26 | // 必须大写,才能序列化 27 | UserList []service.User `json:"user_list,omitempty"` 28 | } 29 | 30 | // RelationAction 处理关注和取消关注请求。 31 | func RelationAction(c *gin.Context) { 32 | userId, err1 := strconv.ParseInt(c.GetString("userId"), 10, 64) 33 | toUserId, err2 := strconv.ParseInt(c.Query("to_user_id"), 10, 64) 34 | actionType, err3 := strconv.ParseInt(c.Query("action_type"), 10, 64) 35 | // fmt.Println(userId, toUserId, actionType) 36 | // 传入参数格式有问题。 37 | if nil != err1 || nil != err2 || nil != err3 || actionType < 1 || actionType > 2 { 38 | fmt.Printf("fail") 39 | c.JSON(http.StatusOK, RelationActionResp{ 40 | Response{ 41 | StatusCode: -1, 42 | StatusMsg: "用户id格式错误", 43 | }, 44 | }) 45 | return 46 | } 47 | // 正常处理 48 | fsi := service.NewFSIInstance() 49 | switch { 50 | // 关注 51 | case 1 == actionType: 52 | go fsi.AddFollowRelation(userId, toUserId) 53 | // 取关 54 | case 2 == actionType: 55 | go fsi.DeleteFollowRelation(userId, toUserId) 56 | } 57 | log.Println("关注、取关成功。") 58 | c.JSON(http.StatusOK, RelationActionResp{ 59 | Response{ 60 | StatusCode: 0, 61 | StatusMsg: "OK", 62 | }, 63 | }) 64 | } 65 | 66 | // GetFollowing 处理获取关注列表请求。 67 | func GetFollowing(c *gin.Context) { 68 | userId, err := strconv.ParseInt(c.Query("user_id"), 10, 64) 69 | // 用户id解析出错。 70 | if nil != err { 71 | c.JSON(http.StatusOK, FollowingResp{ 72 | Response: Response{ 73 | StatusCode: -1, 74 | StatusMsg: "用户id格式错误。", 75 | }, 76 | UserList: nil, 77 | }) 78 | return 79 | } 80 | // 正常获取关注列表 81 | fsi := service.NewFSIInstance() 82 | users, err := fsi.GetFollowing(userId) 83 | // 获取关注列表时出错。 84 | if err != nil { 85 | c.JSON(http.StatusOK, FollowingResp{ 86 | Response: Response{ 87 | StatusCode: -1, 88 | StatusMsg: "获取关注列表时出错。", 89 | }, 90 | UserList: nil, 91 | }) 92 | return 93 | } 94 | // 成功获取到关注列表。 95 | log.Println("获取关注列表成功。") 96 | c.JSON(http.StatusOK, FollowingResp{ 97 | UserList: users, 98 | Response: Response{ 99 | StatusCode: 0, 100 | StatusMsg: "OK", 101 | }, 102 | }) 103 | } 104 | 105 | // GetFollowers 处理获取关注列表请求 106 | func GetFollowers(c *gin.Context) { 107 | userId, err := strconv.ParseInt(c.Query("user_id"), 10, 64) 108 | // 用户id解析出错。 109 | if nil != err { 110 | c.JSON(http.StatusOK, FollowersResp{ 111 | Response: Response{ 112 | StatusCode: -1, 113 | StatusMsg: "用户id格式错误。", 114 | }, 115 | UserList: nil, 116 | }) 117 | return 118 | } 119 | // 正常获取粉丝列表 120 | fsi := service.NewFSIInstance() 121 | users, err := fsi.GetFollowers(userId) 122 | // 获取关注列表时出错。 123 | if err != nil { 124 | c.JSON(http.StatusOK, FollowersResp{ 125 | Response: Response{ 126 | StatusCode: -1, 127 | StatusMsg: "获取粉丝列表时出错。", 128 | }, 129 | UserList: nil, 130 | }) 131 | return 132 | } 133 | // 成功获取到粉丝列表。 134 | //log.Println("获取粉丝列表成功。") 135 | c.JSON(http.StatusOK, FollowersResp{ 136 | Response: Response{ 137 | StatusCode: 0, 138 | StatusMsg: "OK", 139 | }, 140 | UserList: users, 141 | }) 142 | } 143 | -------------------------------------------------------------------------------- /controller/likeController.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "TikTok/service" 5 | "github.com/gin-gonic/gin" 6 | "log" 7 | "net/http" 8 | "strconv" 9 | ) 10 | 11 | type likeResponse struct { 12 | StatusCode int32 `json:"status_code"` 13 | StatusMsg string `json:"status_msg,omitempty"` 14 | } 15 | 16 | type GetFavouriteListResponse struct { 17 | StatusCode int32 `json:"status_code"` 18 | StatusMsg string `json:"status_msg,omitempty"` 19 | VideoList []service.Video `json:"video_list,omitempty"` 20 | } 21 | 22 | // FavoriteAction 点赞或者取消赞操作; 23 | func FavoriteAction(c *gin.Context) { 24 | strUserId := c.GetString("userId") 25 | userId, _ := strconv.ParseInt(strUserId, 10, 64) 26 | strVideoId := c.Query("video_id") 27 | videoId, _ := strconv.ParseInt(strVideoId, 10, 64) 28 | strActionType := c.Query("action_type") 29 | actionType, _ := strconv.ParseInt(strActionType, 10, 64) 30 | like := new(service.LikeServiceImpl) 31 | //获取点赞或者取消赞操作的错误信息 32 | err := like.FavouriteAction(userId, videoId, int32(actionType)) 33 | if err == nil { 34 | log.Printf("方法like.FavouriteAction(userid, videoId, int32(actiontype) 成功") 35 | c.JSON(http.StatusOK, likeResponse{ 36 | StatusCode: 0, 37 | StatusMsg: "favourite action success", 38 | }) 39 | } else { 40 | log.Printf("方法like.FavouriteAction(userid, videoId, int32(actiontype) 失败:%v", err) 41 | c.JSON(http.StatusOK, likeResponse{ 42 | StatusCode: 1, 43 | StatusMsg: "favourite action fail", 44 | }) 45 | } 46 | } 47 | 48 | // GetFavouriteList 获取点赞列表; 49 | func GetFavouriteList(c *gin.Context) { 50 | strUserId := c.Query("user_id") 51 | strCurId := c.GetString("userId") 52 | userId, _ := strconv.ParseInt(strUserId, 10, 64) 53 | curId, _ := strconv.ParseInt(strCurId, 10, 64) 54 | like := GetVideo() 55 | videos, err := like.GetFavouriteList(userId, curId) 56 | if err == nil { 57 | log.Printf("方法like.GetFavouriteList(userid) 成功") 58 | c.JSON(http.StatusOK, GetFavouriteListResponse{ 59 | StatusCode: 0, 60 | StatusMsg: "get favouriteList success", 61 | VideoList: videos, 62 | }) 63 | } else { 64 | log.Printf("方法like.GetFavouriteList(userid) 失败:%v", err) 65 | c.JSON(http.StatusOK, GetFavouriteListResponse{ 66 | StatusCode: 1, 67 | StatusMsg: "get favouriteList fail ", 68 | }) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /controller/userController.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "TikTok/dao" 5 | "TikTok/service" 6 | "github.com/gin-gonic/gin" 7 | "log" 8 | "net/http" 9 | "strconv" 10 | ) 11 | 12 | type Response struct { 13 | StatusCode int32 `json:"status_code"` 14 | StatusMsg string `json:"status_msg,omitempty"` 15 | } 16 | 17 | type UserLoginResponse struct { 18 | Response 19 | UserId int64 `json:"user_id,omitempty"` 20 | Token string `json:"token"` 21 | } 22 | 23 | type UserResponse struct { 24 | Response 25 | User service.User `json:"user"` 26 | } 27 | 28 | // Register POST douyin/user/register/ 用户注册 29 | func Register(c *gin.Context) { 30 | username := c.Query("username") 31 | password := c.Query("password") 32 | 33 | usi := service.UserServiceImpl{} 34 | 35 | u := usi.GetTableUserByUsername(username) 36 | if username == u.Name { 37 | c.JSON(http.StatusOK, UserLoginResponse{ 38 | Response: Response{StatusCode: 1, StatusMsg: "User already exist"}, 39 | }) 40 | } else { 41 | newUser := dao.TableUser{ 42 | Name: username, 43 | Password: service.EnCoder(password), 44 | } 45 | if usi.InsertTableUser(&newUser) != true { 46 | println("Insert Data Fail") 47 | } 48 | u := usi.GetTableUserByUsername(username) 49 | token := service.GenerateToken(username) 50 | log.Println("注册返回的id: ", u.Id) 51 | c.JSON(http.StatusOK, UserLoginResponse{ 52 | Response: Response{StatusCode: 0}, 53 | UserId: u.Id, 54 | Token: token, 55 | }) 56 | } 57 | } 58 | 59 | // Login POST douyin/user/login/ 用户登录 60 | func Login(c *gin.Context) { 61 | username := c.Query("username") 62 | password := c.Query("password") 63 | encoderPassword := service.EnCoder(password) 64 | println(encoderPassword) 65 | 66 | usi := service.UserServiceImpl{} 67 | 68 | u := usi.GetTableUserByUsername(username) 69 | 70 | if encoderPassword == u.Password { 71 | token := service.GenerateToken(username) 72 | c.JSON(http.StatusOK, UserLoginResponse{ 73 | Response: Response{StatusCode: 0}, 74 | UserId: u.Id, 75 | Token: token, 76 | }) 77 | } else { 78 | c.JSON(http.StatusOK, UserLoginResponse{ 79 | Response: Response{StatusCode: 1, StatusMsg: "Username or Password Error"}, 80 | }) 81 | } 82 | } 83 | 84 | // UserInfo GET douyin/user/ 用户信息 85 | func UserInfo(c *gin.Context) { 86 | user_id := c.Query("user_id") 87 | id, _ := strconv.ParseInt(user_id, 10, 64) 88 | 89 | usi := service.UserServiceImpl{ 90 | FollowService: &service.FollowServiceImp{}, 91 | LikeService: &service.LikeServiceImpl{}, 92 | } 93 | 94 | if u, err := usi.GetUserById(id); err != nil { 95 | c.JSON(http.StatusOK, UserResponse{ 96 | Response: Response{StatusCode: 1, StatusMsg: "User Doesn't Exist"}, 97 | }) 98 | } else { 99 | c.JSON(http.StatusOK, UserResponse{ 100 | Response: Response{StatusCode: 0}, 101 | User: u, 102 | }) 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /controller/videoController.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "TikTok/service" 5 | "github.com/gin-gonic/gin" 6 | "log" 7 | "net/http" 8 | "strconv" 9 | "time" 10 | ) 11 | 12 | type FeedResponse struct { 13 | Response 14 | VideoList []service.Video `json:"video_list"` 15 | NextTime int64 `json:"next_time"` 16 | } 17 | 18 | type VideoListResponse struct { 19 | Response 20 | VideoList []service.Video `json:"video_list"` 21 | } 22 | 23 | // Feed /feed/ 24 | func Feed(c *gin.Context) { 25 | inputTime := c.Query("latest_time") 26 | log.Printf("传入的时间" + inputTime) 27 | var lastTime time.Time 28 | if inputTime != "0" { 29 | me, _ := strconv.ParseInt(inputTime, 10, 64) 30 | lastTime = time.Unix(me, 0) 31 | } else { 32 | lastTime = time.Now() 33 | } 34 | log.Printf("获取到时间戳%v", lastTime) 35 | userId, _ := strconv.ParseInt(c.GetString("userId"), 10, 64) 36 | log.Printf("获取到用户id:%v\n", userId) 37 | videoService := GetVideo() 38 | feed, nextTime, err := videoService.Feed(lastTime, userId) 39 | if err != nil { 40 | log.Printf("方法videoService.Feed(lastTime, userId) 失败:%v", err) 41 | c.JSON(http.StatusOK, FeedResponse{ 42 | Response: Response{StatusCode: 1, StatusMsg: "获取视频流失败"}, 43 | }) 44 | return 45 | } 46 | log.Printf("方法videoService.Feed(lastTime, userId) 成功") 47 | c.JSON(http.StatusOK, FeedResponse{ 48 | Response: Response{StatusCode: 0}, 49 | VideoList: feed, 50 | NextTime: nextTime.Unix(), 51 | }) 52 | } 53 | 54 | // Publish /publish/action/ 55 | func Publish(c *gin.Context) { 56 | data, err := c.FormFile("data") 57 | userId, _ := strconv.ParseInt(c.GetString("userId"), 10, 64) 58 | log.Printf("获取到用户id:%v\n", userId) 59 | title := c.PostForm("title") 60 | log.Printf("获取到视频title:%v\n", title) 61 | if err != nil { 62 | log.Printf("获取视频流失败:%v", err) 63 | c.JSON(http.StatusOK, Response{ 64 | StatusCode: 1, 65 | StatusMsg: err.Error(), 66 | }) 67 | return 68 | } 69 | 70 | videoService := GetVideo() 71 | err = videoService.Publish(data, userId, title) 72 | if err != nil { 73 | log.Printf("方法videoService.Publish(data, userId) 失败:%v", err) 74 | c.JSON(http.StatusOK, Response{ 75 | StatusCode: 1, 76 | StatusMsg: err.Error(), 77 | }) 78 | return 79 | } 80 | log.Printf("方法videoService.Publish(data, userId) 成功") 81 | 82 | c.JSON(http.StatusOK, Response{ 83 | StatusCode: 0, 84 | StatusMsg: "uploaded successfully", 85 | }) 86 | } 87 | 88 | // PublishList /publish/list/ 89 | func PublishList(c *gin.Context) { 90 | user_Id, _ := c.GetQuery("user_id") 91 | userId, _ := strconv.ParseInt(user_Id, 10, 64) 92 | log.Printf("获取到用户id:%v\n", userId) 93 | curId, _ := strconv.ParseInt(c.GetString("userId"), 10, 64) 94 | log.Printf("获取到当前用户id:%v\n", curId) 95 | videoService := GetVideo() 96 | list, err := videoService.List(userId, curId) 97 | if err != nil { 98 | log.Printf("调用videoService.List(%v)出现错误:%v\n", userId, err) 99 | c.JSON(http.StatusOK, VideoListResponse{ 100 | Response: Response{StatusCode: 1, StatusMsg: "获取视频列表失败"}, 101 | }) 102 | return 103 | } 104 | log.Printf("调用videoService.List(%v)成功", userId) 105 | c.JSON(http.StatusOK, VideoListResponse{ 106 | Response: Response{StatusCode: 0}, 107 | VideoList: list, 108 | }) 109 | } 110 | 111 | // GetVideo 拼装videoService 112 | func GetVideo() service.VideoServiceImpl { 113 | var userService service.UserServiceImpl 114 | var followService service.FollowServiceImp 115 | var videoService service.VideoServiceImpl 116 | var likeService service.LikeServiceImpl 117 | var commentService service.CommentServiceImpl 118 | userService.FollowService = &followService 119 | userService.LikeService = &likeService 120 | followService.UserService = &userService 121 | likeService.VideoService = &videoService 122 | commentService.UserService = &userService 123 | videoService.CommentService = &commentService 124 | videoService.LikeService = &likeService 125 | videoService.UserService = &userService 126 | return videoService 127 | } 128 | -------------------------------------------------------------------------------- /dao/commentDao.go: -------------------------------------------------------------------------------- 1 | package dao 2 | 3 | import ( 4 | "TikTok/config" 5 | "errors" 6 | "log" 7 | "time" 8 | ) 9 | 10 | // Comment 11 | // 评论信息-数据库中的结构体-dao层使用 12 | type Comment struct { 13 | Id int64 //评论id 14 | UserId int64 //评论用户id 15 | VideoId int64 //视频id 16 | CommentText string //评论内容 17 | CreateDate time.Time //评论发布的日期mm-dd 18 | Cancel int32 //取消评论为1,发布评论为0 19 | } 20 | 21 | // TableName 修改表名映射 22 | func (Comment) TableName() string { 23 | return "comments" 24 | } 25 | 26 | // Count 27 | // 1、使用video id 查询Comment数量 28 | func Count(videoId int64) (int64, error) { 29 | log.Println("CommentDao-Count: running") //函数已运行 30 | //Init() 31 | var count int64 32 | //数据库中查询评论数量 33 | err := Db.Model(Comment{}).Where(map[string]interface{}{"video_id": videoId, "cancel": config.ValidComment}).Count(&count).Error 34 | if err != nil { 35 | log.Println("CommentDao-Count: return count failed") //函数返回提示错误信息 36 | return -1, errors.New("find comments count failed") 37 | } 38 | log.Println("CommentDao-Count: return count success") //函数执行成功,返回正确信息 39 | return count, nil 40 | } 41 | 42 | //CommentIdList 根据视频id获取评论id 列表 43 | func CommentIdList(videoId int64) ([]string, error) { 44 | var commentIdList []string 45 | err := Db.Model(Comment{}).Select("id").Where("video_id = ?", videoId).Find(&commentIdList).Error 46 | if err != nil { 47 | log.Println("CommentIdList:", err) 48 | return nil, err 49 | } 50 | return commentIdList, nil 51 | } 52 | 53 | // InsertComment 54 | // 2、发表评论 55 | func InsertComment(comment Comment) (Comment, error) { 56 | log.Println("CommentDao-InsertComment: running") //函数已运行 57 | //数据库中插入一条评论信息 58 | err := Db.Model(Comment{}).Create(&comment).Error 59 | if err != nil { 60 | log.Println("CommentDao-InsertComment: return create comment failed") //函数返回提示错误信息 61 | return Comment{}, errors.New("create comment failed") 62 | } 63 | log.Println("CommentDao-InsertComment: return success") //函数执行成功,返回正确信息 64 | return comment, nil 65 | } 66 | 67 | // DeleteComment 68 | // 3、删除评论,传入评论id 69 | func DeleteComment(id int64) error { 70 | log.Println("CommentDao-DeleteComment: running") //函数已运行 71 | var commentInfo Comment 72 | //先查询是否有此评论 73 | result := Db.Model(Comment{}).Where(map[string]interface{}{"id": id, "cancel": config.ValidComment}).First(&commentInfo) 74 | if result.RowsAffected == 0 { //查询到此评论数量为0则返回无此评论 75 | log.Println("CommentDao-DeleteComment: return del comment is not exist") //函数返回提示错误信息 76 | return errors.New("del comment is not exist") 77 | } 78 | //数据库中删除评论-更新评论状态为-1 79 | err := Db.Model(Comment{}).Where("id = ?", id).Update("cancel", config.InvalidComment).Error 80 | if err != nil { 81 | log.Println("CommentDao-DeleteComment: return del comment failed") //函数返回提示错误信息 82 | return errors.New("del comment failed") 83 | } 84 | log.Println("CommentDao-DeleteComment: return success") //函数执行成功,返回正确信息 85 | return nil 86 | } 87 | 88 | // GetCommentList 89 | // 4.根据视频id查询所属评论全部列表信息 90 | func GetCommentList(videoId int64) ([]Comment, error) { 91 | log.Println("CommentDao-GetCommentList: running") //函数已运行 92 | //数据库中查询评论信息list 93 | var commentList []Comment 94 | result := Db.Model(Comment{}).Where(map[string]interface{}{"video_id": videoId, "cancel": config.ValidComment}). 95 | Order("create_date desc").Find(&commentList) 96 | //若此视频没有评论信息,返回空列表,不报错 97 | if result.RowsAffected == 0 { 98 | log.Println("CommentDao-GetCommentList: return there are no comments") //函数返回提示无评论 99 | return nil, nil 100 | } 101 | //若获取评论列表出错 102 | if result.Error != nil { 103 | log.Println(result.Error.Error()) 104 | log.Println("CommentDao-GetCommentList: return get comment list failed") //函数返回提示获取评论错误 105 | return commentList, errors.New("get comment list failed") 106 | } 107 | log.Println("CommentDao-GetCommentList: return commentList success") //函数执行成功,返回正确信息 108 | return commentList, nil 109 | } 110 | -------------------------------------------------------------------------------- /dao/commentDao_test.go: -------------------------------------------------------------------------------- 1 | package dao 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | "time" 7 | ) 8 | 9 | //1、使用video id 查询Comment数量 的测试函数 10 | func TestCountComment(t *testing.T) { 11 | Init() 12 | count, err := Count(14) 13 | fmt.Printf("%v\n", count) 14 | fmt.Printf("[%v]", err) 15 | } 16 | 17 | //2、发表评论 的测试函数 18 | func TestInsertComment(t *testing.T) { 19 | Init() 20 | nowTime := time.Now() //评论时间记录 21 | comment := Comment{ 22 | UserId: 20008, 23 | VideoId: 1, 24 | CommentText: "user20008commentVideo1-2", 25 | CreateDate: nowTime, 26 | Cancel: 0, 27 | } 28 | comList, err := InsertComment(comment) 29 | fmt.Printf("[comList:%v][err:%v]", comList, err) 30 | } 31 | 32 | //3、删除评论 的测试函数 33 | func TestDelComment(t *testing.T) { 34 | Init() 35 | err := DeleteComment(int64(8)) 36 | fmt.Printf("[%v]", err) 37 | } 38 | 39 | //4.根据视频id查询所属评论全部列表信息 的测试函数 40 | func TestCommentList(t *testing.T) { 41 | Init() 42 | list, err := GetCommentList(int64(1)) 43 | fmt.Printf("%v\n", list) 44 | fmt.Printf("[%v]", err) 45 | } 46 | -------------------------------------------------------------------------------- /dao/doc.go: -------------------------------------------------------------------------------- 1 | // Package dao, 2 | // @Description 分层结构的dao包,本包内用来编写数据库读写的相关代码, 3 | // @Author 创建人siyixiong, 4 | // @Update 20220513 (创建/修改时间), 5 | package dao 6 | -------------------------------------------------------------------------------- /dao/followDao.go: -------------------------------------------------------------------------------- 1 | package dao 2 | 3 | import ( 4 | "log" 5 | "sync" 6 | ) 7 | 8 | // Follow 用户关系结构,对应用户关系表。 9 | type Follow struct { 10 | Id int64 11 | UserId int64 12 | FollowerId int64 13 | Cancel int8 14 | } 15 | 16 | // TableName 设置Follow结构体对应数据库表名。 17 | func (Follow) TableName() string { 18 | return "follows" 19 | } 20 | 21 | // FollowDao 把dao层看成整体,把dao的curd封装在一个结构体中。 22 | type FollowDao struct { 23 | } 24 | 25 | var ( 26 | followDao *FollowDao //操作该dao层crud的结构体变量。 27 | followOnce sync.Once //单例限定,去限定申请一个followDao结构体变量。 28 | ) 29 | 30 | // NewFollowDaoInstance 生成并返回followDao的单例对象。 31 | func NewFollowDaoInstance() *FollowDao { 32 | followOnce.Do( 33 | func() { 34 | followDao = &FollowDao{} 35 | }) 36 | return followDao 37 | } 38 | 39 | /* 40 | 下面为FollowDao的成员方法,即crud逻辑。 41 | */ 42 | 43 | // FindRelation 给定当前用户和目标用户id,查询follow表中相应的记录。 44 | func (*FollowDao) FindRelation(userId int64, targetId int64) (*Follow, error) { 45 | // follow变量用于后续存储数据库查出来的用户关系。 46 | follow := Follow{} 47 | //当查询出现错误时,日志打印err msg,并return err. 48 | if err := Db. 49 | Where("user_id = ?", targetId). 50 | Where("follower_id = ?", userId). 51 | Where("cancel = ?", 0). 52 | Take(&follow).Error; nil != err { 53 | // 当没查到数据时,gorm也会报错。 54 | if "record not found" == err.Error() { 55 | return nil, nil 56 | } 57 | log.Println(err.Error()) 58 | return nil, err 59 | } 60 | //正常情况,返回取到的值和空err. 61 | return &follow, nil 62 | } 63 | 64 | // GetFollowerCnt 给定当前用户id,查询follow表中该用户的粉丝数。 65 | func (*FollowDao) GetFollowerCnt(userId int64) (int64, error) { 66 | // 用于存储当前用户粉丝数的变量 67 | var cnt int64 68 | // 当查询出现错误的情况,日志打印err msg,并返回err. 69 | if err := Db. 70 | Model(Follow{}). 71 | Where("user_id = ?", userId). 72 | Where("cancel = ?", 0). 73 | Count(&cnt).Error; nil != err { 74 | log.Println(err.Error()) 75 | return 0, err 76 | } 77 | // 正常情况,返回取到的粉丝数。 78 | return cnt, nil 79 | } 80 | 81 | // GetFollowingCnt 给定当前用户id,查询follow表中该用户关注了多少人。 82 | func (*FollowDao) GetFollowingCnt(userId int64) (int64, error) { 83 | // 用于存储当前用户关注了多少人。 84 | var cnt int64 85 | // 查询出错,日志打印err msg,并return err 86 | if err := Db.Model(Follow{}). 87 | Where("follower_id = ?", userId). 88 | Where("cancel = ?", 0). 89 | Count(&cnt).Error; nil != err { 90 | log.Println(err.Error()) 91 | return 0, err 92 | } 93 | // 查询成功,返回人数。 94 | return cnt, nil 95 | } 96 | 97 | // InsertFollowRelation 给定用户和目标对象id,插入其关注关系。 98 | func (*FollowDao) InsertFollowRelation(userId int64, targetId int64) (bool, error) { 99 | // 生成需要插入的关系结构体。 100 | follow := Follow{ 101 | UserId: userId, 102 | FollowerId: targetId, 103 | Cancel: 0, 104 | } 105 | // 插入失败,返回err. 106 | if err := Db.Select("UserId", "FollowerId", "Cancel").Create(&follow).Error; nil != err { 107 | log.Println(err.Error()) 108 | return false, err 109 | } 110 | // 插入成功 111 | return true, nil 112 | } 113 | 114 | // FindEverFollowing 给定当前用户和目标用户id,查看曾经是否有关注关系。 115 | func (*FollowDao) FindEverFollowing(userId int64, targetId int64) (*Follow, error) { 116 | // 用于存储查出来的关注关系。 117 | follow := Follow{} 118 | //当查询出现错误时,日志打印err msg,并return err. 119 | if err := Db. 120 | Where("user_id = ?", userId). 121 | Where("follower_id = ?", targetId). 122 | Where("cancel = ? or cancel = ?", 0, 1). 123 | Take(&follow).Error; nil != err { 124 | // 当没查到记录报错时,不当做错误处理。 125 | if "record not found" == err.Error() { 126 | return nil, nil 127 | } 128 | log.Println(err.Error()) 129 | return nil, err 130 | } 131 | //正常情况,返回取到的关系和空err. 132 | return &follow, nil 133 | } 134 | 135 | // UpdateFollowRelation 给定用户和目标用户的id,更新他们的关系为取消关注或再次关注。 136 | func (*FollowDao) UpdateFollowRelation(userId int64, targetId int64, cancel int8) (bool, error) { 137 | // 更新失败,返回错误。 138 | if err := Db.Model(Follow{}). 139 | Where("user_id = ?", userId). 140 | Where("follower_id = ?", targetId). 141 | Update("cancel", cancel).Error; nil != err { 142 | // 更新失败,打印错误日志。 143 | log.Println(err.Error()) 144 | return false, err 145 | } 146 | // 更新成功。 147 | return true, nil 148 | } 149 | 150 | // GetFollowingIds 给定用户id,查询他关注了哪些人的id。 151 | func (*FollowDao) GetFollowingIds(userId int64) ([]int64, error) { 152 | var ids []int64 153 | if err := Db. 154 | Model(Follow{}). 155 | Where("follower_id = ?", userId). 156 | Pluck("user_id", &ids).Error; nil != err { 157 | // 没有关注任何人,但是不能算错。 158 | if "record not found" == err.Error() { 159 | return nil, nil 160 | } 161 | // 查询出错。 162 | log.Println(err.Error()) 163 | return nil, err 164 | } 165 | // 查询成功。 166 | return ids, nil 167 | } 168 | 169 | // GetFollowersIds 给定用户id,查询他关注了哪些人的id。 170 | func (*FollowDao) GetFollowersIds(userId int64) ([]int64, error) { 171 | var ids []int64 172 | if err := Db. 173 | Model(Follow{}). 174 | Where("user_id = ?", userId). 175 | Where("cancel = ?", 0). 176 | Pluck("follower_id", &ids).Error; nil != err { 177 | // 没有粉丝,但是不能算错。 178 | if "record not found" == err.Error() { 179 | return nil, nil 180 | } 181 | // 查询出错。 182 | log.Println(err.Error()) 183 | return nil, err 184 | } 185 | // 查询成功。 186 | return ids, nil 187 | } 188 | -------------------------------------------------------------------------------- /dao/followDao_test.go: -------------------------------------------------------------------------------- 1 | package dao 2 | -------------------------------------------------------------------------------- /dao/initDao.go: -------------------------------------------------------------------------------- 1 | package dao 2 | 3 | import ( 4 | "gorm.io/driver/mysql" 5 | "gorm.io/gorm" 6 | "gorm.io/gorm/logger" 7 | "log" 8 | "os" 9 | "time" 10 | ) 11 | 12 | var Db *gorm.DB 13 | 14 | func Init() { 15 | newLogger := logger.New( 16 | log.New(os.Stdout, "\r\n", log.LstdFlags), // io writer 17 | logger.Config{ 18 | SlowThreshold: time.Second, // 慢 SQL 阈值 19 | LogLevel: logger.Error, // Log level 20 | Colorful: true, // 彩色打印 21 | }, 22 | ) 23 | var err error 24 | dsn := "douyin:zjqxy@tcp(43.138.25.60:3306)/douyin?charset=utf8mb4&parseTime=True&loc=Local" 25 | //想要正确的处理time.Time,需要带上 parseTime 参数, 26 | //要支持完整的UTF-8编码,需要将 charset=utf8 更改为 charset=utf8mb4 27 | Db, err = gorm.Open(mysql.Open(dsn), &gorm.Config{ 28 | Logger: newLogger, 29 | }) 30 | if err != nil { 31 | log.Panicln("err:", err.Error()) 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /dao/initDao_test.go: -------------------------------------------------------------------------------- 1 | package dao 2 | -------------------------------------------------------------------------------- /dao/likeDao.go: -------------------------------------------------------------------------------- 1 | package dao 2 | 3 | import ( 4 | "TikTok/config" 5 | "errors" 6 | "log" 7 | ) 8 | 9 | // Like 表的结构。 10 | type Like struct { 11 | Id int64 //自增主键 12 | UserId int64 //点赞用户id 13 | VideoId int64 //视频id 14 | Cancel int8 //是否点赞,0为点赞,1为取消赞 15 | } 16 | 17 | // TableName 修改表名映射 18 | func (Like) TableName() string { 19 | return "likes" 20 | } 21 | 22 | // GetLikeUserIdList 根据videoId获取点赞userId 23 | func GetLikeUserIdList(videoId int64) ([]int64, error) { 24 | var likeUserIdList []int64 //存所有该视频点赞用户id; 25 | //查询likes表对应视频id点赞用户,返回查询结果 26 | err := Db.Model(Like{}).Where(map[string]interface{}{"video_id": videoId, "cancel": config.IsLike}). 27 | Pluck("user_id", &likeUserIdList).Error 28 | //查询过程出现错误,返回默认值0,并输出错误信息 29 | if err != nil { 30 | log.Println(err.Error()) 31 | return nil, errors.New("get likeUserIdList failed") 32 | } else { 33 | //没查询到或者查询到结果,返回数量以及无报错 34 | return likeUserIdList, nil 35 | } 36 | } 37 | 38 | // UpdateLike 根据userId,videoId,actionType点赞或者取消赞 39 | func UpdateLike(userId int64, videoId int64, actionType int32) error { 40 | //更新当前用户观看视频的点赞状态“cancel”,返回错误结果 41 | err := Db.Model(Like{}).Where(map[string]interface{}{"user_id": userId, "video_id": videoId}). 42 | Update("cancel", actionType).Error 43 | //如果出现错误,返回更新数据库失败 44 | if err != nil { 45 | log.Println(err.Error()) 46 | return errors.New("update data fail") 47 | } 48 | //更新操作成功 49 | return nil 50 | } 51 | 52 | // InsertLike 插入点赞数据 53 | func InsertLike(likeData Like) error { 54 | //创建点赞数据,默认为点赞,cancel为0,返回错误结果 55 | err := Db.Model(Like{}).Create(&likeData).Error 56 | //如果有错误结果,返回插入失败 57 | if err != nil { 58 | log.Println(err.Error()) 59 | return errors.New("insert data fail") 60 | } 61 | return nil 62 | } 63 | 64 | // GetLikeInfo 根据userId,videoId查询点赞信息 65 | func GetLikeInfo(userId int64, videoId int64) (Like, error) { 66 | //创建一条空like结构体,用来存储查询到的信息 67 | var likeInfo Like 68 | //根据userid,videoId查询是否有该条信息,如果有,存储在likeInfo,返回查询结果 69 | err := Db.Model(Like{}).Where(map[string]interface{}{"user_id": userId, "video_id": videoId}). 70 | First(&likeInfo).Error 71 | if err != nil { 72 | //查询数据为0,打印"can't find data",返回空结构体,这时候就应该要考虑是否插入这条数据了 73 | if "record not found" == err.Error() { 74 | log.Println("can't find data") 75 | return Like{}, nil 76 | } else { 77 | //如果查询数据库失败,返回获取likeInfo信息失败 78 | log.Println(err.Error()) 79 | return likeInfo, errors.New("get likeInfo failed") 80 | } 81 | } 82 | return likeInfo, nil 83 | } 84 | 85 | // GetLikeVideoIdList 根据userId查询所属点赞全部videoId 86 | func GetLikeVideoIdList(userId int64) ([]int64, error) { 87 | var likeVideoIdList []int64 88 | err := Db.Model(Like{}).Where(map[string]interface{}{"user_id": userId, "cancel": config.IsLike}). 89 | Pluck("video_id", &likeVideoIdList).Error 90 | if err != nil { 91 | //查询数据为0,返回空likeVideoIdList切片,以及返回无错误 92 | if "record not found" == err.Error() { 93 | log.Println("there are no likeVideoId") 94 | return likeVideoIdList, nil 95 | } else { 96 | //如果查询数据库失败,返回获取likeVideoIdList失败 97 | log.Println(err.Error()) 98 | return likeVideoIdList, errors.New("get likeVideoIdList failed") 99 | } 100 | } 101 | return likeVideoIdList, nil 102 | } 103 | -------------------------------------------------------------------------------- /dao/likeDao_test.go: -------------------------------------------------------------------------------- 1 | package dao 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | ) 7 | 8 | func TestGetLikeUserIdList(t *testing.T) { 9 | Init() 10 | list, err := GetLikeUserIdList(54) 11 | fmt.Printf("%v", list) 12 | fmt.Printf("%v", err) 13 | } 14 | 15 | func TestUpdateLike(t *testing.T) { 16 | Init() 17 | err := UpdateLike(3, 54, 0) 18 | fmt.Printf("%v", err) 19 | } 20 | 21 | func TestInsertLike(t *testing.T) { 22 | Init() 23 | err := InsertLike(Like{ 24 | UserId: 20003, 25 | VideoId: 71, 26 | Cancel: 0, 27 | }) 28 | fmt.Printf("%v", err) 29 | } 30 | 31 | func TestGetLikeInfo(t *testing.T) { 32 | Init() 33 | likeInfo, err := GetLikeInfo(3, 71) 34 | fmt.Printf("%v", likeInfo) 35 | fmt.Printf("%v", err) 36 | } 37 | 38 | func TestGetLikeVideoIdList(t *testing.T) { 39 | Init() 40 | videoIdList, err := GetLikeVideoIdList(3) 41 | fmt.Printf("%v", videoIdList) 42 | fmt.Printf("%v", err) 43 | } 44 | -------------------------------------------------------------------------------- /dao/userDao.go: -------------------------------------------------------------------------------- 1 | package dao 2 | 3 | import ( 4 | "log" 5 | ) 6 | 7 | // TableUser 对应数据库User表结构的结构体 8 | type TableUser struct { 9 | Id int64 10 | Name string 11 | Password string 12 | } 13 | 14 | // TableName 修改表名映射 15 | func (tableUser TableUser) TableName() string { 16 | return "users" 17 | } 18 | 19 | // GetTableUserList 获取全部TableUser对象 20 | func GetTableUserList() ([]TableUser, error) { 21 | tableUsers := []TableUser{} 22 | if err := Db.Find(&tableUsers).Error; err != nil { 23 | log.Println(err.Error()) 24 | return tableUsers, err 25 | } 26 | return tableUsers, nil 27 | } 28 | 29 | // GetTableUserByUsername 根据username获得TableUser对象 30 | func GetTableUserByUsername(name string) (TableUser, error) { 31 | tableUser := TableUser{} 32 | if err := Db.Where("name = ?", name).First(&tableUser).Error; err != nil { 33 | log.Println(err.Error()) 34 | return tableUser, err 35 | } 36 | return tableUser, nil 37 | } 38 | 39 | // GetTableUserById 根据user_id获得TableUser对象 40 | func GetTableUserById(id int64) (TableUser, error) { 41 | tableUser := TableUser{} 42 | if err := Db.Where("id = ?", id).First(&tableUser).Error; err != nil { 43 | log.Println(err.Error()) 44 | return tableUser, err 45 | } 46 | return tableUser, nil 47 | } 48 | 49 | // InsertTableUser 将tableUser插入表内 50 | func InsertTableUser(tableUser *TableUser) bool { 51 | if err := Db.Create(&tableUser).Error; err != nil { 52 | log.Println(err.Error()) 53 | return false 54 | } 55 | return true 56 | } 57 | -------------------------------------------------------------------------------- /dao/userDao_test.go: -------------------------------------------------------------------------------- 1 | package dao 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | ) 7 | 8 | func TestGetTableUserList(t *testing.T) { 9 | list, err := GetTableUserList() 10 | fmt.Printf("%v", list) 11 | fmt.Printf("%v", err) 12 | } 13 | 14 | func TestGetTableUserByUsername(t *testing.T) { 15 | list, err := GetTableUserByUsername("test") 16 | fmt.Printf("%v", list) 17 | fmt.Printf("%v", err) 18 | } 19 | 20 | func TestGetTableUserById(t *testing.T) { 21 | list, err := GetTableUserById(int64(4)) 22 | fmt.Printf("%v", list) 23 | fmt.Printf("%v", err) 24 | } 25 | 26 | func TestInsertTableUser(t *testing.T) { 27 | tu := &TableUser{ 28 | Id: 5, 29 | Name: "a", 30 | Password: "111111", 31 | } 32 | list := InsertTableUser(tu) 33 | fmt.Printf("%v", list) 34 | } 35 | -------------------------------------------------------------------------------- /dao/videoDao.go: -------------------------------------------------------------------------------- 1 | package dao 2 | 3 | import ( 4 | "TikTok/config" 5 | "TikTok/middleware/ftp" 6 | "io" 7 | "log" 8 | "time" 9 | ) 10 | 11 | type TableVideo struct { 12 | Id int64 `json:"id"` 13 | AuthorId int64 14 | PlayUrl string `json:"play_url"` 15 | CoverUrl string `json:"cover_url"` 16 | PublishTime time.Time 17 | Title string `json:"title"` //视频名,5.23添加 18 | } 19 | 20 | // TableName 21 | // 将TableVideo映射到videos, 22 | // 这样我结构体到名字就不需要是Video了,防止和我Service层到结构体名字冲突 23 | func (TableVideo) TableName() string { 24 | return "videos" 25 | } 26 | 27 | // GetVideosByAuthorId 28 | // 根据作者的id来查询对应数据库数据,并TableVideo返回切片 29 | func GetVideosByAuthorId(authorId int64) ([]TableVideo, error) { 30 | //建立结果集接收 31 | var data []TableVideo 32 | //初始化db 33 | //Init() 34 | result := Db.Where(&TableVideo{AuthorId: authorId}).Find(&data) 35 | //如果出现问题,返回对应到空,并且返回error 36 | if result.Error != nil { 37 | return nil, result.Error 38 | } 39 | return data, nil 40 | } 41 | 42 | // GetVideoByVideoId 43 | // 依据VideoId来获得视频信息 44 | func GetVideoByVideoId(videoId int64) (TableVideo, error) { 45 | var tableVideo TableVideo 46 | tableVideo.Id = videoId 47 | //Init() 48 | result := Db.First(&tableVideo) 49 | if result.Error != nil { 50 | return tableVideo, result.Error 51 | } 52 | return tableVideo, nil 53 | 54 | } 55 | 56 | // GetVideosByLastTime 57 | // 依据一个时间,来获取这个时间之前的一些视频 58 | func GetVideosByLastTime(lastTime time.Time) ([]TableVideo, error) { 59 | videos := make([]TableVideo, config.VideoCount) 60 | result := Db.Where("publish_time 0 { //在缓存中有此值,则找出来删除,然后返回 117 | vid, err1 := redis.RdbCVid.Get(redis.Ctx, strconv.FormatInt(commentId, 10)).Result() 118 | if err1 != nil { //没找到,返回err 119 | log.Println("redis find CV err:", err1) 120 | } 121 | //删除,两个redis都要删除 122 | del1, err2 := redis.RdbCVid.Del(redis.Ctx, strconv.FormatInt(commentId, 10)).Result() 123 | if err2 != nil { 124 | log.Println(err2) 125 | } 126 | del2, err3 := redis.RdbVCid.SRem(redis.Ctx, vid, strconv.FormatInt(commentId, 10)).Result() 127 | if err3 != nil { 128 | log.Println(err3) 129 | } 130 | log.Println("del comment in Redis success:", del1, del2) //del1、del2代表删除了几条数据 131 | 132 | /* 133 | //由于多协程会造成数据库读取压力大,因此使用chan来减少压力 134 | commentChan := make(chan int64) 135 | //协程删除数据库中的值 136 | go func() { 137 | commentChan <- commentId 138 | close(commentChan) 139 | //err := dao.DeleteComment(commentId) 140 | //if err != nil { 141 | // log.Println(err) 142 | //} 143 | }() 144 | go func() { 145 | for cId := range commentChan { 146 | err := dao.DeleteComment(cId) 147 | if err != nil { 148 | log.Println(err) 149 | } 150 | } 151 | }()*/ 152 | 153 | //使用mq进行数据库中评论的删除-评论状态更新 154 | //评论id传入消息队列 155 | rabbitmq.RmqCommentDel.Publish(strconv.FormatInt(commentId, 10)) 156 | return nil 157 | } 158 | //不在内存中,则直接走数据库删除 159 | return dao.DeleteComment(commentId) 160 | } 161 | 162 | // GetList 163 | // 4、查看评论列表-返回评论list 164 | func (c CommentServiceImpl) GetList(videoId int64, userId int64) ([]CommentInfo, error) { 165 | log.Println("CommentService-GetList: running") //函数已运行 166 | /* 167 | //法一、使用SQL语句查询评论列表及用户信息,嵌套user信息。且导致提高耦合性。 168 | //1.查找CommentData结构体的信息 169 | commentData := make([]CommentData, 1) 170 | result := dao.Db.Raw("select T.cid id,T.user_id user_id,T.`name`,T.follow_count,T.follower_count,"+ 171 | "\nif(f.cancel is null,'false','true') is_follow,"+ 172 | "\nT.comment_text content,T.create_date"+ 173 | "\nfrom follows f right join\n("+ 174 | "\n\tselect cid,vid,id user_id,`name`,comment_text,create_date,"+ 175 | "\n\tcount(if(tag = 'follower' and cancel is not null,1,null)) follower_count,"+ 176 | "\n\tcount(if(tag = 'follow' and cancel is not null,1,null)) follow_count"+ 177 | "\n\tfrom\n\t("+ 178 | "\n\t\tselect c.id cid,u.id,c.video_id vid,`name`,f.cancel,comment_text,create_date,'follower' tag"+ 179 | "\n\t\tfrom comments c join users u on c.user_id = u.id and c.cancel = 0"+ 180 | "\n\t\tleft join follows f on u.id = f.user_id and f.cancel = 0"+ 181 | "\n\t\tunion all"+ 182 | "\n\t\tselect c.id cid,u.id,c.video_id vid,`name`,f.cancel,comment_text,create_date,'follow' tag"+ 183 | "\n\t\tfrom comments c join users u on c.user_id = u.id and c.cancel = 0"+ 184 | "\n\t\tleft join follows f on u.id = f.follower_id and f.cancel = 0"+ 185 | "\n\t\t) T\n\t\tgroup by cid,vid,id,`name`,comment_text,create_date"+ 186 | "\n) T on f.follower_id = T.user_id and f.cancel = 0 and f.user_id = ?"+ 187 | "\nwhere vid = ? group by cid order by create_date desc", userId, videoId).Scan(&commentData) 188 | 189 | err := result.Error 190 | 191 | if nil != err { 192 | log.Println("CommentService-GetList: sql error") //sql查询出错 193 | return nil, err 194 | } 195 | //当前有0条评论 196 | if result.RowsAffected == 0 { 197 | return nil, nil 198 | } 199 | //2.拼接 200 | commentInfoList := make([]CommentInfo, 0, len(commentData)) 201 | for _, comment := range commentData { 202 | userData := User{ 203 | Id: comment.Id, 204 | Name: comment.Name, 205 | FollowCount: comment.FollowCount, 206 | FollowerCount: comment.FollowerCount, 207 | IsFollow: comment.IsFollow, 208 | } 209 | _commentInfo := CommentInfo{ 210 | Id: comment.Id, 211 | UserInfo: userData, 212 | Content: comment.Content, 213 | CreateDate: comment.CreateDate.Format(config.DateTime), 214 | } 215 | //3.组装list 216 | commentInfoList = append(commentInfoList, _commentInfo) 217 | } 218 | //-----------------------法一结束-------------------------- 219 | */ 220 | 221 | //法二:调用dao,先查评论,再循环查用户信息: 222 | //1.先查询评论列表信息 223 | commentList, err := dao.GetCommentList(videoId) 224 | if err != nil { 225 | log.Println("CommentService-GetList: return err: " + err.Error()) //函数返回提示错误信息 226 | return nil, err 227 | } 228 | //当前有0条评论 229 | if commentList == nil { 230 | return nil, nil 231 | } 232 | 233 | //提前定义好切片长度 234 | commentInfoList := make([]CommentInfo, len(commentList)) 235 | 236 | wg := &sync.WaitGroup{} 237 | wg.Add(len(commentList)) 238 | idx := 0 239 | for _, comment := range commentList { 240 | //2.调用方法组装评论信息,再append 241 | var commentData CommentInfo 242 | //将评论信息进行组装,添加想要的信息,插入从数据库中查到的数据 243 | go func(comment dao.Comment) { 244 | oneComment(&commentData, &comment, userId) 245 | //3.组装list 246 | //commentInfoList = append(commentInfoList, commentData) 247 | commentInfoList[idx] = commentData 248 | idx = idx + 1 249 | wg.Done() 250 | }(comment) 251 | } 252 | wg.Wait() 253 | //评论排序-按照主键排序 254 | sort.Sort(CommentSlice(commentInfoList)) 255 | //------------------------法二结束---------------------------- 256 | 257 | //协程查询redis中是否有此记录,无则将评论id切片存入redis 258 | go func() { 259 | //1.先在缓存中查此视频是否已有评论列表 260 | cnt, err1 := redis.RdbVCid.SCard(redis.Ctx, strconv.FormatInt(videoId, 10)).Result() 261 | if err1 != nil { //若查询缓存出错,则打印log 262 | //return 0, err 263 | log.Println("count from redis error:", err) 264 | } 265 | //2.缓存中查到了数量大于0,则说明数据正常,不用更新缓存 266 | if cnt > 0 { 267 | return 268 | } 269 | //3.缓存中数据不正确,更新缓存: 270 | //先在redis中存储一个-1 值,防止脏读 271 | _, _err := redis.RdbVCid.SAdd(redis.Ctx, strconv.Itoa(int(videoId)), config.DefaultRedisValue).Result() 272 | if _err != nil { //若存储redis失败,则直接返回 273 | log.Println("redis save one vId - cId 0 failed") 274 | return 275 | } 276 | //设置key值过期时间 277 | _, err2 := redis.RdbVCid.Expire(redis.Ctx, strconv.Itoa(int(videoId)), 278 | time.Duration(config.OneMonth)*time.Second).Result() 279 | if err2 != nil { 280 | log.Println("redis save one vId - cId expire failed") 281 | } 282 | //将评论id循环存入redis 283 | for _, _comment := range commentInfoList { 284 | insertRedisVideoCommentId(strconv.Itoa(int(videoId)), strconv.Itoa(int(_comment.Id))) 285 | } 286 | log.Println("comment list save ids in redis") 287 | }() 288 | 289 | log.Println("CommentService-GetList: return list success") //函数执行成功,返回正确信息 290 | return commentInfoList, nil 291 | } 292 | 293 | //在redis中存储video_id对应的comment_id 、 comment_id对应的video_id 294 | func insertRedisVideoCommentId(videoId string, commentId string) { 295 | //在redis-RdbVCid中存储video_id对应的comment_id 296 | _, err := redis.RdbVCid.SAdd(redis.Ctx, videoId, commentId).Result() 297 | if err != nil { //若存储redis失败-有err,则直接删除key 298 | log.Println("redis save send: vId - cId failed, key deleted") 299 | redis.RdbVCid.Del(redis.Ctx, videoId) 300 | return 301 | } 302 | //在redis-RdbCVid中存储comment_id对应的video_id 303 | _, err = redis.RdbCVid.Set(redis.Ctx, commentId, videoId, 0).Result() 304 | if err != nil { 305 | log.Println("redis save one cId - vId failed") 306 | } 307 | } 308 | 309 | //此函数用于给一个评论赋值:评论信息+用户信息 填充 310 | func oneComment(comment *CommentInfo, com *dao.Comment, userId int64) { 311 | var wg sync.WaitGroup 312 | wg.Add(1) 313 | //根据评论用户id和当前用户id,查询评论用户信息 314 | impl := UserServiceImpl{ 315 | FollowService: &FollowServiceImp{}, 316 | } 317 | var err error 318 | comment.Id = com.Id 319 | comment.Content = com.CommentText 320 | comment.CreateDate = com.CreateDate.Format(config.DateTime) 321 | comment.UserInfo, err = impl.GetUserByIdWithCurId(com.UserId, userId) 322 | if err != nil { 323 | log.Println("CommentService-GetList: GetUserByIdWithCurId return err: " + err.Error()) //函数返回提示错误信息 324 | } 325 | wg.Done() 326 | wg.Wait() 327 | } 328 | 329 | // CommentSlice 此变量以及以下三个函数都是做排序-准备工作 330 | type CommentSlice []CommentInfo 331 | 332 | func (a CommentSlice) Len() int { //重写Len()方法 333 | return len(a) 334 | } 335 | func (a CommentSlice) Swap(i, j int) { //重写Swap()方法 336 | a[i], a[j] = a[j], a[i] 337 | } 338 | func (a CommentSlice) Less(i, j int) bool { //重写Less()方法 339 | return a[i].Id > a[j].Id 340 | } 341 | -------------------------------------------------------------------------------- /service/commentServiceImpl_test.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "TikTok/dao" 5 | "fmt" 6 | "testing" 7 | ) 8 | 9 | func TestCountComment(t *testing.T) { 10 | dao.Init() 11 | impl := CommentServiceImpl{} 12 | count, err := impl.CountFromVideoId(1) 13 | if err != nil { 14 | fmt.Println("count err:", err) 15 | } 16 | fmt.Println(count) 17 | } 18 | -------------------------------------------------------------------------------- /service/commentSub.go: -------------------------------------------------------------------------------- 1 | package service 2 | -------------------------------------------------------------------------------- /service/doc.go: -------------------------------------------------------------------------------- 1 | // Package service 2 | // @Description 本包内分包定义不同模块的service接口和实现 3 | // @Author 创建人siyixiong; 4 | // @Update 20220513 (创建/修改时间); 5 | package service 6 | -------------------------------------------------------------------------------- /service/followService.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | // FollowService 定义用户关系接口以及用户关系中的各种方法 4 | type FollowService interface { 5 | /* 6 | 一、其他同学需要调用的业务方法。 7 | */ 8 | // IsFollowing 根据当前用户id和目标用户id来判断当前用户是否关注了目标用户 9 | IsFollowing(userId int64, targetId int64) (bool, error) 10 | // GetFollowerCnt 根据用户id来查询用户被多少其他用户关注 11 | GetFollowerCnt(userId int64) (int64, error) 12 | // GetFollowingCnt 根据用户id来查询用户关注了多少其它用户 13 | GetFollowingCnt(userId int64) (int64, error) 14 | /* 15 | 二、直接request需要的业务方法 16 | */ 17 | // AddFollowRelation 当前用户关注目标用户 18 | AddFollowRelation(userId int64, targetId int64) (bool, error) 19 | // DeleteFollowRelation 当前用户取消对目标用户的关注 20 | DeleteFollowRelation(userId int64, targetId int64) (bool, error) 21 | // GetFollowing 获取当前用户的关注列表 22 | GetFollowing(userId int64) ([]User, error) 23 | // GetFollowers 获取当前用户的粉丝列表 24 | GetFollowers(userId int64) ([]User, error) 25 | } 26 | -------------------------------------------------------------------------------- /service/followServiceImpl.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "TikTok/config" 5 | "TikTok/dao" 6 | "TikTok/middleware/rabbitmq" 7 | "TikTok/middleware/redis" 8 | "log" 9 | "strconv" 10 | "strings" 11 | "sync" 12 | "time" 13 | ) 14 | 15 | // FollowServiceImp 该结构体继承FollowService接口。 16 | type FollowServiceImp struct { 17 | UserService 18 | } 19 | 20 | var ( 21 | followServiceImp *FollowServiceImp //controller层通过该实例变量调用service的所有业务方法。 22 | followServiceOnce sync.Once //限定该service对象为单例,节约内存。 23 | ) 24 | 25 | // NewFSIInstance 生成并返回FollowServiceImp结构体单例变量。 26 | func NewFSIInstance() *FollowServiceImp { 27 | followServiceOnce.Do( 28 | func() { 29 | followServiceImp = &FollowServiceImp{ 30 | UserService: &UserServiceImpl{ 31 | // 存在我调userService中,userService又要调我。 32 | FollowService: &FollowServiceImp{}, 33 | }, 34 | } 35 | }) 36 | return followServiceImp 37 | } 38 | 39 | // IsFollowing 给定当前用户和目标用户id,判断是否存在关注关系。 40 | func (*FollowServiceImp) IsFollowing(userId int64, targetId int64) (bool, error) { 41 | // 先查Redis里面是否有此关系。 42 | if flag, err := redis.RdbFollowingPart.SIsMember(redis.Ctx, strconv.Itoa(int(userId)), targetId).Result(); flag { 43 | // 重现设置过期时间。 44 | redis.RdbFollowingPart.Expire(redis.Ctx, strconv.Itoa(int(userId)), config.ExpireTime) 45 | return true, err 46 | } 47 | // SQL 查询。 48 | relation, err := dao.NewFollowDaoInstance().FindRelation(userId, targetId) 49 | 50 | if nil != err { 51 | return false, err 52 | } 53 | if nil == relation { 54 | return false, nil 55 | } 56 | // 存在此关系,将其注入Redis中。 57 | go addRelationToRedis(int(userId), int(targetId)) 58 | 59 | return true, nil 60 | } 61 | func addRelationToRedis(userId int, targetId int) { 62 | // 第一次存入时,给该key添加一个-1为key,防止脏数据的写入。当然set可以去重,直接加,便于CPU。 63 | redis.RdbFollowingPart.SAdd(redis.Ctx, strconv.Itoa(int(userId)), -1) 64 | // 将查询到的关注关系注入Redis. 65 | redis.RdbFollowingPart.SAdd(redis.Ctx, strconv.Itoa(int(userId)), targetId) 66 | // 更新过期时间。 67 | redis.RdbFollowingPart.Expire(redis.Ctx, strconv.Itoa(int(userId)), config.ExpireTime) 68 | } 69 | 70 | // GetFollowerCnt 给定当前用户id,查询其粉丝数量。 71 | func (*FollowServiceImp) GetFollowerCnt(userId int64) (int64, error) { 72 | // 查Redis中是否已经存在。 73 | if cnt, err := redis.RdbFollowers.SCard(redis.Ctx, strconv.Itoa(int(userId))).Result(); cnt > 0 { 74 | // 更新过期时间。 75 | redis.RdbFollowers.Expire(redis.Ctx, strconv.Itoa(int(userId)), config.ExpireTime) 76 | return cnt - 1, err 77 | } 78 | // SQL中查询。 79 | ids, err := dao.NewFollowDaoInstance().GetFollowersIds(userId) 80 | if nil != err { 81 | return 0, err 82 | } 83 | // 将数据存入Redis. 84 | // 更新followers 和 followingPart 85 | go addFollowersToRedis(int(userId), ids) 86 | 87 | return int64(len(ids)), err 88 | } 89 | func addFollowersToRedis(userId int, ids []int64) { 90 | redis.RdbFollowers.SAdd(redis.Ctx, strconv.Itoa(userId), -1) 91 | for i, id := range ids { 92 | redis.RdbFollowers.SAdd(redis.Ctx, strconv.Itoa(userId), id) 93 | redis.RdbFollowingPart.SAdd(redis.Ctx, strconv.Itoa(int(id)), userId) 94 | redis.RdbFollowingPart.SAdd(redis.Ctx, strconv.Itoa(int(id)), -1) 95 | // 更新部分关注者的时间 96 | redis.RdbFollowingPart.Expire(redis.Ctx, strconv.Itoa(int(id)), 97 | config.ExpireTime+time.Duration((i%10)<<8)) 98 | } 99 | // 更新followers的过期时间。 100 | redis.RdbFollowers.Expire(redis.Ctx, strconv.Itoa(userId), config.ExpireTime) 101 | 102 | } 103 | 104 | // GetFollowingCnt 给定当前用户id,查询其关注者数量。 105 | func (*FollowServiceImp) GetFollowingCnt(userId int64) (int64, error) { 106 | // 查看Redis中是否有关注数。 107 | if cnt, err := redis.RdbFollowing.SCard(redis.Ctx, strconv.Itoa(int(userId))).Result(); cnt > 0 { 108 | // 更新过期时间。 109 | redis.RdbFollowing.Expire(redis.Ctx, strconv.Itoa(int(userId)), config.ExpireTime) 110 | return cnt - 1, err 111 | } 112 | // 用SQL查询。 113 | ids, err := dao.NewFollowDaoInstance().GetFollowingIds(userId) 114 | 115 | if nil != err { 116 | return 0, err 117 | } 118 | // 更新Redis中的followers和followPart 119 | go addFollowingToRedis(int(userId), ids) 120 | 121 | return int64(len(ids)), err 122 | } 123 | func addFollowingToRedis(userId int, ids []int64) { 124 | redis.RdbFollowing.SAdd(redis.Ctx, strconv.Itoa(userId), -1) 125 | for i, id := range ids { 126 | redis.RdbFollowing.SAdd(redis.Ctx, strconv.Itoa(userId), id) 127 | redis.RdbFollowingPart.SAdd(redis.Ctx, strconv.Itoa(userId), id) 128 | redis.RdbFollowingPart.SAdd(redis.Ctx, strconv.Itoa(userId), -1) 129 | // 更新过期时间 130 | redis.RdbFollowingPart.Expire(redis.Ctx, strconv.Itoa(userId), 131 | config.ExpireTime+time.Duration((i%10)<<8)) 132 | } 133 | // 更新following的过期时间 134 | redis.RdbFollowing.Expire(redis.Ctx, strconv.Itoa(userId), config.ExpireTime) 135 | } 136 | 137 | // AddFollowRelation 给定当前用户和目标对象id,添加他们之间的关注关系。 138 | func (*FollowServiceImp) AddFollowRelation(userId int64, targetId int64) (bool, error) { 139 | // 加信息打入消息队列。 140 | sb := strings.Builder{} 141 | sb.WriteString(strconv.Itoa(int(userId))) 142 | sb.WriteString(" ") 143 | sb.WriteString(strconv.Itoa(int(targetId))) 144 | rabbitmq.RmqFollowAdd.Publish(sb.String()) 145 | // 记录日志 146 | log.Println("消息打入成功。") 147 | // 更新redis信息。 148 | return updateRedisWithAdd(userId, targetId) 149 | /*followDao := dao.NewFollowDaoInstance() 150 | follow, err := followDao.FindEverFollowing(targetId, userId) 151 | // 寻找SQL 出错。 152 | if nil != err { 153 | return false, err 154 | } 155 | // 曾经关注过,只需要update一下cancel即可。 156 | if nil != follow { 157 | _, err := followDao.UpdateFollowRelation(targetId, userId, 0) 158 | // update 出错。 159 | if nil != err { 160 | return false, err 161 | } 162 | // update 成功。 163 | return true, nil 164 | } 165 | // 曾经没有关注过,需要插入一条关注关系。 166 | _, err = followDao.InsertFollowRelation(targetId, userId) 167 | if nil != err { 168 | // insert 出错 169 | return false, err 170 | } 171 | // insert 成功。 172 | return true, nil*/ 173 | } 174 | 175 | // 添加关注时,设置Redis 176 | func updateRedisWithAdd(userId int64, targetId int64) (bool, error) { 177 | /* 178 | 1-Redis是否存在followers_targetId. 179 | 2-Redis是否存在following_userId. 180 | 3-Redis是否存在following_part_userId. 181 | */ 182 | // step1 183 | targetIdStr := strconv.Itoa(int(targetId)) 184 | if cnt, _ := redis.RdbFollowers.SCard(redis.Ctx, targetIdStr).Result(); 0 != cnt { 185 | redis.RdbFollowers.SAdd(redis.Ctx, targetIdStr, userId) 186 | redis.RdbFollowers.Expire(redis.Ctx, targetIdStr, config.ExpireTime) 187 | } 188 | // step2 189 | followingUserIdStr := strconv.Itoa(int(userId)) 190 | if cnt, _ := redis.RdbFollowing.SCard(redis.Ctx, followingUserIdStr).Result(); 0 != cnt { 191 | redis.RdbFollowing.SAdd(redis.Ctx, followingUserIdStr, targetId) 192 | redis.RdbFollowing.Expire(redis.Ctx, followingUserIdStr, config.ExpireTime) 193 | } 194 | // step3 195 | followingPartUserIdStr := followingUserIdStr 196 | redis.RdbFollowingPart.SAdd(redis.Ctx, followingPartUserIdStr, targetId) 197 | // 可能是第一次给改用户加followingPart的关注者,需要加上-1防止脏读。 198 | redis.RdbFollowingPart.SAdd(redis.Ctx, followingPartUserIdStr, -1) 199 | redis.RdbFollowingPart.Expire(redis.Ctx, followingPartUserIdStr, config.ExpireTime) 200 | return true, nil 201 | } 202 | 203 | // DeleteFollowRelation 给定当前用户和目标用户id,删除其关注关系。 204 | func (*FollowServiceImp) DeleteFollowRelation(userId int64, targetId int64) (bool, error) { 205 | // 加信息打入消息队列。 206 | sb := strings.Builder{} 207 | sb.WriteString(strconv.Itoa(int(userId))) 208 | sb.WriteString(" ") 209 | sb.WriteString(strconv.Itoa(int(targetId))) 210 | rabbitmq.RmqFollowDel.Publish(sb.String()) 211 | // 记录日志 212 | log.Println("消息打入成功。") 213 | // 更新redis信息。 214 | return updateRedisWithDel(userId, targetId) 215 | /*followDao := dao.NewFollowDaoInstance() 216 | follow, err := followDao.FindEverFollowing(targetId, userId) 217 | // 寻找 SQL 出错。 218 | if nil != err { 219 | return false, err 220 | } 221 | // 曾经关注过,只需要update一下cancel即可。 222 | if nil != follow { 223 | _, err := followDao.UpdateFollowRelation(targetId, userId, 1) 224 | // update 出错。 225 | if nil != err { 226 | return false, err 227 | } 228 | // update 成功。 229 | return true, nil 230 | } 231 | // 没有关注关系 232 | return false, nil*/ 233 | } 234 | 235 | // 当取关时,更新redis里的信息 236 | func updateRedisWithDel(userId int64, targetId int64) (bool, error) { 237 | /* 238 | 1-Redis是否存在followers_targetId. 239 | 2-Redis是否存在following_userId. 240 | 2-Redis是否存在following_part_userId. 241 | */ 242 | // step1 243 | targetIdStr := strconv.Itoa(int(targetId)) 244 | if cnt, _ := redis.RdbFollowers.SCard(redis.Ctx, targetIdStr).Result(); 0 != cnt { 245 | redis.RdbFollowers.SRem(redis.Ctx, targetIdStr, userId) 246 | redis.RdbFollowers.Expire(redis.Ctx, targetIdStr, config.ExpireTime) 247 | } 248 | // step2 249 | followingIdStr := strconv.Itoa(int(userId)) 250 | if cnt, _ := redis.RdbFollowing.SCard(redis.Ctx, followingIdStr).Result(); 0 != cnt { 251 | redis.RdbFollowing.SRem(redis.Ctx, followingIdStr, targetId) 252 | redis.RdbFollowing.Expire(redis.Ctx, followingIdStr, config.ExpireTime) 253 | } 254 | // step3 255 | followingPartUserIdStr := followingIdStr 256 | if cnt, _ := redis.RdbFollowingPart.Exists(redis.Ctx, followingPartUserIdStr).Result(); 0 != cnt { 257 | redis.RdbFollowingPart.SRem(redis.Ctx, followingPartUserIdStr, targetId) 258 | redis.RdbFollowingPart.Expire(redis.Ctx, followingPartUserIdStr, config.ExpireTime) 259 | } 260 | return true, nil 261 | } 262 | 263 | // GetFollowing 根据当前用户id来查询他的关注者列表。 264 | func (f *FollowServiceImp) getFollowing(userId int64) ([]User, error) { 265 | // 获取关注对象的id数组。 266 | ids, err := dao.NewFollowDaoInstance().GetFollowingIds(userId) 267 | // 查询出错 268 | if nil != err { 269 | return nil, err 270 | } 271 | // 没得关注者 272 | if nil == ids { 273 | return nil, nil 274 | } 275 | // 根据每个id来查询用户信息。 276 | len := len(ids) 277 | if len > 0 { 278 | len -= 1 279 | } 280 | var wg sync.WaitGroup 281 | wg.Add(len) 282 | users := make([]User, len) 283 | i, j := 0, 0 284 | for ; i < len; j++ { 285 | if ids[j] == -1 { 286 | continue 287 | } 288 | go func(i int, idx int64) { 289 | defer wg.Done() 290 | users[i], _ = f.GetUserByIdWithCurId(idx, userId) 291 | }(i, ids[i]) 292 | i++ 293 | } 294 | wg.Wait() 295 | // 返回关注对象列表。 296 | return users, nil 297 | } 298 | 299 | // GetFollowing 根据当前用户id来查询他的关注者列表。 300 | func (f *FollowServiceImp) GetFollowing(userId int64) ([]User, error) { 301 | return getFollowing(userId) 302 | /*// 先查Redis,看是否有全部关注信息。 303 | followingIdStr := strconv.Itoa(int(userId)) 304 | if cnt, _ := middleware.RdbFollowers.SCard(middleware.Ctx, followingIdStr).Result(); 0 == cnt { 305 | users, _ := f.getFollowing(userId) 306 | 307 | go setRedisFollowing(userId, users) 308 | 309 | return users, nil 310 | } 311 | // Redis中有。 312 | UserIdStr := strconv.Itoa(int(userId)) 313 | userIds, _ := middleware.RdbFollowing.SMembers(middleware.Ctx, UserIdStr).Result() 314 | len := len(userIds) 315 | if len > 0 { 316 | len -= 1 317 | } 318 | users := make([]User, len) 319 | wg := sync.WaitGroup{} 320 | wg.Add(len) 321 | i, j := 0, 0 322 | for ; i < len; j++ { 323 | idx, _ := strconv.Atoi(userIds[j]) 324 | if idx == -1 { 325 | continue 326 | } 327 | go func(i int, idx int) { 328 | defer wg.Done() 329 | users[i], _ = f.GetUserByIdWithCurId(int64(idx), userId) 330 | }(i, idx) 331 | 332 | i++ 333 | } 334 | wg.Wait() 335 | log.Println("从Redis中查询到所有关注者。") 336 | return users, nil*/ 337 | } 338 | 339 | // 设置Redis关于所有关注的信息。 340 | func setRedisFollowing(userId int64, users []User) { 341 | /* 342 | 1-设置following_userId的所有关注id。 343 | 2-设置following_part_id关注信息。 344 | */ 345 | // 加上-1防止脏读 346 | followingIdStr := strconv.Itoa(int(userId)) 347 | redis.RdbFollowing.SAdd(redis.Ctx, followingIdStr, -1) 348 | // 设置过期时间 349 | redis.RdbFollowing.Expire(redis.Ctx, followingIdStr, config.ExpireTime) 350 | for i, user := range users { 351 | redis.RdbFollowing.SAdd(redis.Ctx, followingIdStr, user.Id) 352 | 353 | redis.RdbFollowingPart.SAdd(redis.Ctx, followingIdStr, user.Id) 354 | redis.RdbFollowingPart.SAdd(redis.Ctx, followingIdStr, -1) 355 | // 随机设置过期时间 356 | redis.RdbFollowingPart.Expire(redis.Ctx, followingIdStr, config.ExpireTime+ 357 | time.Duration((i%10)<<8)) 358 | } 359 | } 360 | 361 | // 从数据库查所有关注用户信息。 362 | func getFollowing(userId int64) ([]User, error) { 363 | users := make([]User, 1) 364 | // 查询出错。 365 | if err := dao.Db.Raw("select id,`name`,"+ 366 | "\ncount(if(tag = 'follower' and cancel is not null,1,null)) follower_count,"+ 367 | "\ncount(if(tag = 'follow' and cancel is not null,1,null)) follow_count,"+ 368 | "\n 'true' is_follow\nfrom\n("+ 369 | "\nselect f1.follower_id fid,u.id,`name`,f2.cancel,'follower' tag"+ 370 | "\nfrom follows f1 join users u on f1.user_id = u.id and f1.cancel = 0"+ 371 | "\nleft join follows f2 on u.id = f2.user_id and f2.cancel = 0\n\tunion all"+ 372 | "\nselect f1.follower_id fid,u.id,`name`,f2.cancel,'follow' tag"+ 373 | "\nfrom follows f1 join users u on f1.user_id = u.id and f1.cancel = 0"+ 374 | "\nleft join follows f2 on u.id = f2.follower_id and f2.cancel = 0\n) T"+ 375 | "\nwhere fid = ? group by fid,id,`name`", userId).Scan(&users).Error; nil != err { 376 | return nil, err 377 | } 378 | // 返回关注对象列表。 379 | return users, nil 380 | } 381 | 382 | // GetFollowers 根据当前用户id来查询他的粉丝列表。 383 | 384 | func (f *FollowServiceImp) getFollowers(userId int64) ([]User, error) { 385 | // 获取粉丝的id数组。 386 | ids, err := dao.NewFollowDaoInstance().GetFollowersIds(userId) 387 | // 查询出错 388 | if nil != err { 389 | return nil, err 390 | } 391 | // 没得粉丝 392 | if nil == ids { 393 | return nil, nil 394 | } 395 | // 根据每个id来查询用户信息。 396 | return f.getUserById(ids, userId) 397 | } 398 | func (f *FollowServiceImp) getUserById(ids []int64, userId int64) ([]User, error) { 399 | len := len(ids) 400 | if len > 0 { 401 | len -= 1 402 | } 403 | users := make([]User, len) 404 | var wg sync.WaitGroup 405 | wg.Add(len) 406 | i, j := 0, 0 407 | for ; i < len; j++ { 408 | // 越过-1 409 | if ids[j] == -1 { 410 | continue 411 | } 412 | //开启协程来查。 413 | go func(i int, idx int64) { 414 | defer wg.Done() 415 | users[i], _ = f.GetUserByIdWithCurId(idx, userId) 416 | }(i, ids[i]) 417 | i++ 418 | } 419 | wg.Wait() 420 | // 返回粉丝列表。 421 | return users, nil 422 | } 423 | 424 | // GetFollowers 根据当前用户id来查询他的粉丝列表。 425 | func (f *FollowServiceImp) GetFollowers(userId int64) ([]User, error) { 426 | return getFollowers(userId) 427 | /*// 先查Redis,看是否有全部粉丝信息。 428 | followersIdStr := strconv.Itoa(int(userId)) 429 | if cnt, _ := middleware.RdbFollowers.SCard(middleware.Ctx, followersIdStr).Result(); 0 == cnt { 430 | users, _ := f.getFollowers(userId) 431 | 432 | go setRedisFollowers(userId, users) 433 | 434 | return users, nil 435 | } 436 | // Redis中有。 437 | // 先更新有效期。 438 | middleware.RdbFollowers.Expire(middleware.Ctx, followersIdStr, config.ExpireTime) 439 | userIds, _ := middleware.RdbFollowers.SMembers(middleware.Ctx, followersIdStr).Result() 440 | len := len(userIds) 441 | if len > 0 { 442 | len -= 1 443 | } 444 | users := make([]User, len) 445 | var wg sync.WaitGroup 446 | wg.Add(len) 447 | i, j := 0, 0 448 | for ; i < len; j++ { 449 | idx, _ := strconv.Atoi(userIds[j]) 450 | if idx == -1 { 451 | continue 452 | } 453 | go func(i int, idx int) { 454 | defer wg.Done() 455 | users[i], _ = f.GetUserByIdWithCurId(int64(idx), userId) 456 | }(i, idx) 457 | i++ 458 | } 459 | wg.Wait() 460 | return users, nil*/ 461 | } 462 | 463 | // 从数据库查所有粉丝信息。 464 | func getFollowers(userId int64) ([]User, error) { 465 | users := make([]User, 1) 466 | 467 | if err := dao.Db.Raw("select T.id,T.name,T.follow_cnt follow_count,T.follower_cnt follower_count,if(f.cancel is null,'false','true') is_follow"+ 468 | "\nfrom follows f right join"+ 469 | "\n(select fid,id,`name`,"+ 470 | "\ncount(if(tag = 'follower' and cancel is not null,1,null)) follower_cnt,"+ 471 | "\ncount(if(tag = 'follow' and cancel is not null,1,null)) follow_cnt"+ 472 | "\nfrom("+ 473 | "\nselect f1.user_id fid,u.id,`name`,f2.cancel,'follower' tag"+ 474 | "\nfrom follows f1 join users u on f1.follower_id = u.id and f1.cancel = 0"+ 475 | "\nleft join follows f2 on u.id = f2.user_id and f2.cancel = 0"+ 476 | "\nunion all"+ 477 | "\nselect f1.user_id fid,u.id,`name`,f2.cancel,'follow' tag"+ 478 | "\nfrom follows f1 join users u on f1.follower_id = u.id and f1.cancel = 0"+ 479 | "\nleft join follows f2 on u.id = f2.follower_id and f2.cancel = 0"+ 480 | "\n) T group by fid,id,`name`"+ 481 | "\n) T on f.user_id = T.id and f.follower_id = T.fid and f.cancel = 0 where fid = ?", userId). 482 | Scan(&users).Error; nil != err { 483 | // 查询出错。 484 | return nil, err 485 | } 486 | // 查询成功。 487 | return users, nil 488 | } 489 | 490 | // 设置Redis关于所有粉丝的信息 491 | func setRedisFollowers(userId int64, users []User) { 492 | /* 493 | 1-设置followers_userId的所有粉丝id。 494 | 2-设置following_part_id关注信息。 495 | */ 496 | // 加上-1防止脏读。 497 | followersIdStr := strconv.Itoa(int(userId)) 498 | redis.RdbFollowers.SAdd(redis.Ctx, followersIdStr, -1) 499 | // 设置过期时间 500 | redis.RdbFollowers.Expire(redis.Ctx, followersIdStr, config.ExpireTime) 501 | for i, user := range users { 502 | redis.RdbFollowers.SAdd(redis.Ctx, followersIdStr, user.Id) 503 | 504 | userUserIdStr := strconv.Itoa(int(user.Id)) 505 | redis.RdbFollowingPart.SAdd(redis.Ctx, userUserIdStr, userId) 506 | redis.RdbFollowingPart.SAdd(redis.Ctx, userUserIdStr, -1) 507 | // 随机更新过期时间 508 | redis.RdbFollowingPart.Expire(redis.Ctx, userUserIdStr, config.ExpireTime+ 509 | time.Duration((i%10)<<8)) 510 | 511 | if user.IsFollow { 512 | redis.RdbFollowingPart.SAdd(redis.Ctx, followersIdStr, user.Id) 513 | redis.RdbFollowingPart.SAdd(redis.Ctx, followersIdStr, -1) 514 | redis.RdbFollowingPart.Expire(redis.Ctx, followersIdStr, config.ExpireTime+ 515 | time.Duration((i%10)<<8)) 516 | } 517 | } 518 | } 519 | -------------------------------------------------------------------------------- /service/followServiceImpl_test.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | ) 7 | 8 | func TestIsFollow(t *testing.T) { 9 | isFollow, err := NewFSIInstance().IsFollowing(1, 2) 10 | if nil != err { 11 | t.Errorf("IsFollow() error = %v", err) 12 | } 13 | if false == isFollow { 14 | fmt.Println("不存在该关系") 15 | } 16 | fmt.Println("存在该关系") 17 | } 18 | -------------------------------------------------------------------------------- /service/followSub.go: -------------------------------------------------------------------------------- 1 | package service 2 | -------------------------------------------------------------------------------- /service/likeService.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | // LikeService 定义点赞状态和点赞数量 4 | type LikeService interface { 5 | /* 6 | 1.其他模块(video)需要使用的业务方法。 7 | */ 8 | //IsFavorite 根据当前视频id判断是否点赞了该视频。 9 | IsFavourite(videoId int64, userId int64) (bool, error) 10 | //FavouriteCount 根据当前视频id获取当前视频点赞数量。 11 | FavouriteCount(videoId int64) (int64, error) 12 | //TotalFavourite 根据userId获取这个用户总共被点赞数量 13 | TotalFavourite(userId int64) (int64, error) 14 | //FavouriteVideoCount 根据userId获取这个用户点赞视频数量 15 | FavouriteVideoCount(userId int64) (int64, error) 16 | /* 17 | 2.request需要实现的功能 18 | */ 19 | //当前用户对视频的点赞操作 ,并把这个行为更新到like表中。 20 | //当前操作行为,1点赞,2取消点赞。 21 | FavouriteAction(userId int64, videoId int64, actionType int32) error 22 | // GetFavouriteList 获取当前用户的所有点赞视频,调用videoService的方法 23 | GetFavouriteList(userId int64, curId int64) ([]Video, error) 24 | } 25 | -------------------------------------------------------------------------------- /service/likeServiceImpl.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "TikTok/config" 5 | "TikTok/dao" 6 | "TikTok/middleware/rabbitmq" 7 | "TikTok/middleware/redis" 8 | "errors" 9 | "log" 10 | "strconv" 11 | "strings" 12 | "sync" 13 | "time" 14 | ) 15 | 16 | type LikeServiceImpl struct { 17 | VideoService 18 | UserService 19 | } 20 | 21 | //IsFavourite 根据userId,videoId查询点赞状态 这边可以快一点,通过查询两个Redis DB; 22 | //step1:查询Redis LikeUserId(key:strUserId)是否已经加载过此信息,通过是否存在value:videoId 判断点赞状态; 23 | //step2:如LikeUserId没有对应信息,查询LikeVideoId(key:strVideoId)是否已经加载过此信息,通过是否存在value:userId 判断点赞状态; 24 | //step3:LikeUserId LikeVideoId中都没有对应key,维护LikeUserId对应key,并通过查询key:strUserId中是否存在value:videoId 判断点赞状态; 25 | func (like *LikeServiceImpl) IsFavourite(videoId int64, userId int64) (bool, error) { 26 | //将int64 userId转换为 string strUserId 27 | strUserId := strconv.FormatInt(userId, 10) 28 | //将int64 videoId转换为 string strVideoId 29 | strVideoId := strconv.FormatInt(videoId, 10) 30 | //step1:查询Redis LikeUserId,key:strUserId中是否存在value:videoId,key中存在value 返回true,不存在返回false 31 | if n, err := redis.RdbLikeUserId.Exists(redis.Ctx, strUserId).Result(); n > 0 { 32 | //如果有问题,说明查询redis失败,返回默认false,返回错误信息 33 | if err != nil { 34 | log.Printf("方法:IsFavourite RedisLikeUserId query key失败:%v", err) 35 | return false, err 36 | } 37 | exist, err1 := redis.RdbLikeUserId.SIsMember(redis.Ctx, strUserId, videoId).Result() 38 | //如果有问题,说明查询redis失败,返回默认false,返回错误信息 39 | if err1 != nil { 40 | log.Printf("方法:IsFavourite RedisLikeUserId query value失败:%v", err1) 41 | return false, err1 42 | } 43 | log.Printf("方法:IsFavourite RedisLikeUserId query value成功") 44 | return exist, nil 45 | } else { //step2:LikeUserId不存在key,查询Redis LikeVideoId,key中存在value 返回true,不存在返回false 46 | if n, err := redis.RdbLikeVideoId.Exists(redis.Ctx, strVideoId).Result(); n > 0 { 47 | //如果有问题,说明查询redis失败,返回默认false,返回错误信息 48 | if err != nil { 49 | log.Printf("方法:IsFavourite RedisLikeVideoId query key失败:%v", err) 50 | return false, err 51 | } 52 | exist, err1 := redis.RdbLikeVideoId.SIsMember(redis.Ctx, strVideoId, userId).Result() 53 | //如果有问题,说明查询redis失败,返回默认false,返回错误信息 54 | if err1 != nil { 55 | log.Printf("方法:IsFavourite RedisLikeVideoId query value失败:%v", err1) 56 | return false, err1 57 | } 58 | log.Printf("方法:IsFavourite RedisLikeVideoId query value成功") 59 | return exist, nil 60 | } else { 61 | //key:strUserId,加入value:DefaultRedisValue,过期才会删,防止删最后一个数据的时候数据库还没更新完出现脏读,或者数据库操作失败造成的脏读 62 | if _, err := redis.RdbLikeUserId.SAdd(redis.Ctx, strUserId, config.DefaultRedisValue).Result(); err != nil { 63 | log.Printf("方法:IsFavourite RedisLikeUserId add value失败") 64 | redis.RdbLikeUserId.Del(redis.Ctx, strUserId) 65 | return false, err 66 | } 67 | //给键值设置有效期,类似于gc机制 68 | _, err := redis.RdbLikeUserId.Expire(redis.Ctx, strUserId, 69 | time.Duration(config.OneMonth)*time.Second).Result() 70 | if err != nil { 71 | log.Printf("方法:IsFavourite RedisLikeUserId 设置有效期失败") 72 | redis.RdbLikeUserId.Del(redis.Ctx, strUserId) 73 | return false, err 74 | } 75 | //step3:LikeUserId LikeVideoId中都没有对应key,通过userId查询likes表,返回所有点赞videoId,并维护到Redis LikeUserId(key:strUserId) 76 | videoIdList, err1 := dao.GetLikeVideoIdList(userId) 77 | //如果有问题,说明查询数据库失败,返回默认false,返回错误信息:"get likeVideoIdList failed" 78 | if err1 != nil { 79 | log.Printf(err1.Error()) 80 | return false, err1 81 | } 82 | //维护Redis LikeUserId(key:strUserId),遍历videoIdList加入 83 | for _, likeVideoId := range videoIdList { 84 | redis.RdbLikeUserId.SAdd(redis.Ctx, strUserId, likeVideoId) 85 | } 86 | //查询Redis LikeUserId,key:strUserId中是否存在value:videoId,存在返回true,不存在返回false 87 | exist, err2 := redis.RdbLikeUserId.SIsMember(redis.Ctx, strUserId, videoId).Result() 88 | //如果有问题,说明操作redis失败,返回默认false,返回错误信息 89 | if err2 != nil { 90 | log.Printf("方法:IsFavourite RedisLikeUserId query value失败:%v", err2) 91 | return false, err2 92 | } 93 | log.Printf("方法:IsFavourite RedisLikeUserId query value成功") 94 | return exist, nil 95 | } 96 | } 97 | } 98 | 99 | //FavouriteCount 根据videoId获取对应点赞数量; 100 | //step1:查询Redis LikeVideoId(key:strVideoId)是否已经加载过此信息,通过set集合中userId个数,获取点赞数量; 101 | //step2:LikeVideoId中都没有对应key,维护LikeVideoId对应key,再通过set集合中userId个数,获取点赞数量; 102 | func (like *LikeServiceImpl) FavouriteCount(videoId int64) (int64, error) { 103 | //将int64 videoId转换为 string strVideoId 104 | strVideoId := strconv.FormatInt(videoId, 10) 105 | //step1 如果key:strVideoId存在 则计算集合中userId个数 106 | if n, err := redis.RdbLikeVideoId.Exists(redis.Ctx, strVideoId).Result(); n > 0 { 107 | //如果有问题,说明查询redis失败,返回默认false,返回错误信息 108 | if err != nil { 109 | log.Printf("方法:FavouriteCount RedisLikeVideoId query key失败:%v", err) 110 | return 0, err 111 | } 112 | //获取集合中userId个数 113 | count, err1 := redis.RdbLikeVideoId.SCard(redis.Ctx, strVideoId).Result() 114 | //如果有问题,说明操作redis失败,返回默认0,返回错误信息 115 | if err1 != nil { 116 | log.Printf("方法:FavouriteCount RedisLikeVideoId query count 失败:%v", err1) 117 | return 0, err1 118 | } 119 | log.Printf("方法:FavouriteCount RedisLikeVideoId query count 成功") 120 | return count - 1, nil //去掉DefaultRedisValue 121 | } else { 122 | //key:strVideoId,加入value:DefaultRedisValue,过期才会删,防止删最后一个数据的时候数据库还没更新完出现脏读,或者数据库操作失败造成的脏读 123 | if _, err := redis.RdbLikeVideoId.SAdd(redis.Ctx, strVideoId, config.DefaultRedisValue).Result(); err != nil { 124 | log.Printf("方法:FavouriteCount RedisLikeVideoId add value失败") 125 | redis.RdbLikeVideoId.Del(redis.Ctx, strVideoId) 126 | return 0, err 127 | } 128 | //给键值设置有效期,类似于gc机制 129 | _, err := redis.RdbLikeVideoId.Expire(redis.Ctx, strVideoId, 130 | time.Duration(config.OneMonth)*time.Second).Result() 131 | if err != nil { 132 | log.Printf("方法:FavouriteCount RedisLikeVideoId 设置有效期失败") 133 | redis.RdbLikeVideoId.Del(redis.Ctx, strVideoId) 134 | return 0, err 135 | } 136 | //如果Redis LikeVideoId不存在此key,通过videoId查询likes表,返回所有点赞userId,并维护到Redis LikeVideoId(key:strVideoId) 137 | //再通过set集合中userId个数,获取点赞数量 138 | userIdList, err1 := dao.GetLikeUserIdList(videoId) 139 | //如果有问题,说明查询数据库失败,返回默认0,返回错误信息:"get likeUserIdList failed" 140 | if err1 != nil { 141 | log.Printf(err1.Error()) 142 | return 0, err1 143 | } 144 | //维护Redis LikeVideoId(key:strVideoId),遍历userIdList加入 145 | for _, likeUserId := range userIdList { 146 | redis.RdbLikeVideoId.SAdd(redis.Ctx, strVideoId, likeUserId) 147 | } 148 | //再通过set集合中userId个数,获取点赞数量 149 | count, err2 := redis.RdbLikeVideoId.SCard(redis.Ctx, strVideoId).Result() 150 | //如果有问题,说明操作redis失败,返回默认0,返回错误信息 151 | if err2 != nil { 152 | log.Printf("方法:FavouriteCount RedisLikeVideoId query count 失败:%v", err2) 153 | return 0, err2 154 | } 155 | log.Printf("方法:FavouriteCount RedisLikeVideoId query count 成功") 156 | return count - 1, nil //去掉DefaultRedisValue 157 | } 158 | } 159 | 160 | // FavouriteAction 根据userId,videoId,actionType对视频进行点赞或者取消赞操作; 161 | //step1: 维护Redis LikeUserId(key:strUserId),添加或者删除value:videoId,LikeVideoId(key:strVideoId),添加或者删除value:userId; 162 | //step2:更新数据库likes表; 163 | func (like *LikeServiceImpl) FavouriteAction(userId int64, videoId int64, actionType int32) error { 164 | //将int64 videoId转换为 string strVideoId 165 | strUserId := strconv.FormatInt(userId, 10) 166 | //将int64 videoId转换为 string strVideoId 167 | strVideoId := strconv.FormatInt(videoId, 10) 168 | //将要操作数据库likes表的信息打入消息队列RmqLikeAdd或者RmqLikeDel 169 | //拼接打入信息 170 | sb := strings.Builder{} 171 | sb.WriteString(strUserId) 172 | sb.WriteString(" ") 173 | sb.WriteString(strVideoId) 174 | 175 | //step1:维护Redis LikeUserId、LikeVideoId; 176 | //执行点赞操作维护 177 | if actionType == config.LikeAction { 178 | //查询Redis LikeUserId(key:strUserId)是否已经加载过此信息 179 | if n, err := redis.RdbLikeUserId.Exists(redis.Ctx, strUserId).Result(); n > 0 { 180 | //如果有问题,说明查询redis失败,返回错误信息 181 | if err != nil { 182 | log.Printf("方法:FavouriteAction RedisLikeUserId query key失败:%v", err) 183 | return err 184 | } //如果加载过此信息key:strUserId,则加入value:videoId 185 | //如果redis LikeUserId 添加失败,数据库操作成功,会有脏数据,所以只有redis操作成功才执行数据库likes表操作 186 | if _, err1 := redis.RdbLikeUserId.SAdd(redis.Ctx, strUserId, videoId).Result(); err1 != nil { 187 | log.Printf("方法:FavouriteAction RedisLikeUserId add value失败:%v", err1) 188 | return err1 189 | } else { 190 | //如果数据库操作失败了,redis是正确数据,客户端显示的是点赞成功,不会影响后续结果 191 | //只有当该用户取消所有点赞视频的时候redis才会重新加载数据库信息,这时候因为取消赞了必然和数据库信息一致 192 | //同样这条信息消费成功与否也不重要,因为redis是正确信息,理由如上 193 | rabbitmq.RmqLikeAdd.Publish(sb.String()) 194 | } 195 | } else { //如果不存在,则维护Redis LikeUserId 新建key:strUserId,设置过期时间,加入DefaultRedisValue, 196 | //key:strUserId,加入value:DefaultRedisValue,过期才会删,防止删最后一个数据的时候数据库还没更新完出现脏读,或者数据库操作失败造成的脏读 197 | //通过userId查询likes表,返回所有点赞videoId,加入key:strUserId集合中, 198 | //再加入当前videoId,再更新likes表此条数据 199 | if _, err := redis.RdbLikeUserId.SAdd(redis.Ctx, strUserId, config.DefaultRedisValue).Result(); err != nil { 200 | log.Printf("方法:FavouriteAction RedisLikeUserId add value失败") 201 | redis.RdbLikeUserId.Del(redis.Ctx, strUserId) 202 | return err 203 | } 204 | //给键值设置有效期,类似于gc机制 205 | _, err := redis.RdbLikeUserId.Expire(redis.Ctx, strUserId, 206 | time.Duration(config.OneMonth)*time.Second).Result() 207 | if err != nil { 208 | log.Printf("方法:FavouriteAction RedisLikeUserId 设置有效期失败") 209 | redis.RdbLikeUserId.Del(redis.Ctx, strUserId) 210 | return err 211 | } 212 | videoIdList, err1 := dao.GetLikeVideoIdList(userId) 213 | //如果有问题,说明查询失败,返回错误信息:"get likeVideoIdList failed" 214 | if err1 != nil { 215 | return err1 216 | } 217 | //遍历videoIdList,添加进key的集合中,若失败,删除key,并返回错误信息,这么做的原因是防止脏读, 218 | //保证redis与mysql数据一致性 219 | for _, likeVideoId := range videoIdList { 220 | if _, err1 := redis.RdbLikeUserId.SAdd(redis.Ctx, strUserId, likeVideoId).Result(); err1 != nil { 221 | log.Printf("方法:FavouriteAction RedisLikeUserId add value失败") 222 | redis.RdbLikeUserId.Del(redis.Ctx, strUserId) 223 | return err1 224 | } 225 | } 226 | //这样操作理由同上 227 | if _, err2 := redis.RdbLikeUserId.SAdd(redis.Ctx, strUserId, videoId).Result(); err2 != nil { 228 | log.Printf("方法:FavouriteAction RedisLikeUserId add value失败:%v", err2) 229 | return err2 230 | } else { 231 | rabbitmq.RmqLikeAdd.Publish(sb.String()) 232 | } 233 | } 234 | //查询Redis LikeVideoId(key:strVideoId)是否已经加载过此信息 235 | if n, err := redis.RdbLikeVideoId.Exists(redis.Ctx, strVideoId).Result(); n > 0 { 236 | //如果有问题,说明查询redis失败,返回错误信息 237 | if err != nil { 238 | log.Printf("方法:FavouriteAction RedisLikeVideoId query key失败:%v", err) 239 | return err 240 | } //如果加载过此信息key:strVideoId,则加入value:userId 241 | //如果redis LikeVideoId 添加失败,返回错误信息 242 | if _, err1 := redis.RdbLikeVideoId.SAdd(redis.Ctx, strVideoId, userId).Result(); err1 != nil { 243 | log.Printf("方法:FavouriteAction RedisLikeVideoId add value失败:%v", err1) 244 | return err1 245 | } 246 | } else { //如果不存在,则维护Redis LikeVideoId 新建key:strVideoId,设置有效期,加入DefaultRedisValue 247 | //通过videoId查询likes表,返回所有点赞userId,加入key:strVideoId集合中, 248 | //再加入当前userId,再更新likes表此条数据 249 | //key:strVideoId,加入value:DefaultRedisValue,过期才会删,防止删最后一个数据的时候数据库还没更新完出现脏读,或者数据库操作失败造成的脏读 250 | if _, err := redis.RdbLikeVideoId.SAdd(redis.Ctx, strVideoId, config.DefaultRedisValue).Result(); err != nil { 251 | log.Printf("方法:FavouriteAction RedisLikeVideoId add value失败") 252 | redis.RdbLikeVideoId.Del(redis.Ctx, strVideoId) 253 | return err 254 | } 255 | //给键值设置有效期,类似于gc机制 256 | _, err := redis.RdbLikeVideoId.Expire(redis.Ctx, strVideoId, 257 | time.Duration(config.OneMonth)*time.Second).Result() 258 | if err != nil { 259 | log.Printf("方法:FavouriteAction RedisLikeVideoId 设置有效期失败") 260 | redis.RdbLikeVideoId.Del(redis.Ctx, strVideoId) 261 | return err 262 | } 263 | userIdList, err1 := dao.GetLikeUserIdList(videoId) 264 | //如果有问题,说明查询失败,返回错误信息:"get likeUserIdList failed" 265 | if err1 != nil { 266 | return err1 267 | } 268 | //遍历userIdList,添加进key的集合中,若失败,删除key,并返回错误信息,这么做的原因是防止脏读, 269 | //保证redis与mysql数据一致性 270 | for _, likeUserId := range userIdList { 271 | if _, err1 := redis.RdbLikeVideoId.SAdd(redis.Ctx, strVideoId, likeUserId).Result(); err1 != nil { 272 | log.Printf("方法:FavouriteAction RedisLikeVideoId add value失败") 273 | redis.RdbLikeVideoId.Del(redis.Ctx, strVideoId) 274 | return err1 275 | } 276 | } 277 | //这样操作理由同上 278 | if _, err2 := redis.RdbLikeVideoId.SAdd(redis.Ctx, strVideoId, userId).Result(); err2 != nil { 279 | log.Printf("方法:FavouriteAction RedisLikeVideoId add value失败:%v", err2) 280 | return err2 281 | } 282 | } 283 | } else { //执行取消赞操作维护 284 | //查询Redis LikeUserId(key:strUserId)是否已经加载过此信息 285 | if n, err := redis.RdbLikeUserId.Exists(redis.Ctx, strUserId).Result(); n > 0 { 286 | //如果有问题,说明查询redis失败,返回错误信息 287 | if err != nil { 288 | log.Printf("方法:FavouriteAction RedisLikeUserId query key失败:%v", err) 289 | return err 290 | } //防止出现redis数据不一致情况,当redis删除操作成功,才执行数据库更新操作 291 | if _, err1 := redis.RdbLikeUserId.SRem(redis.Ctx, strUserId, videoId).Result(); err1 != nil { 292 | log.Printf("方法:FavouriteAction RedisLikeUserId del value失败:%v", err1) 293 | return err1 294 | } else { 295 | //后续数据库的操作,可以在mq里设置若执行数据库更新操作失败,重新消费该信息 296 | rabbitmq.RmqLikeDel.Publish(sb.String()) 297 | } 298 | } else { //如果不存在,则维护Redis LikeUserId 新建key:strUserId,加入value:DefaultRedisValue,过期才会删,防止删最后一个数据的时候数据库 299 | // 还没更新完出现脏读,或者数据库操作失败造成的脏读 300 | //通过userId查询likes表,返回所有点赞videoId,加入key:strUserId集合中, 301 | //再删除当前videoId,再更新likes表此条数据 302 | //key:strUserId,加入value:DefaultRedisValue,过期才会删,防止删最后一个数据的时候数据库还没更新完出现脏读,或者数据库操作失败造成的脏读 303 | if _, err := redis.RdbLikeUserId.SAdd(redis.Ctx, strUserId, config.DefaultRedisValue).Result(); err != nil { 304 | log.Printf("方法:FavouriteAction RedisLikeUserId add value失败") 305 | redis.RdbLikeUserId.Del(redis.Ctx, strUserId) 306 | return err 307 | } 308 | //给键值设置有效期,类似于gc机制 309 | _, err := redis.RdbLikeUserId.Expire(redis.Ctx, strUserId, 310 | time.Duration(config.OneMonth)*time.Second).Result() 311 | if err != nil { 312 | log.Printf("方法:FavouriteAction RedisLikeUserId 设置有效期失败") 313 | redis.RdbLikeUserId.Del(redis.Ctx, strUserId) 314 | return err 315 | } 316 | videoIdList, err1 := dao.GetLikeVideoIdList(userId) 317 | //如果有问题,说明查询失败,返回错误信息:"get likeVideoIdList failed" 318 | if err1 != nil { 319 | return err1 320 | } 321 | //遍历videoIdList,添加进key的集合中,若失败,删除key,并返回错误信息,这么做的原因是防止脏读, 322 | //保证redis与mysql 数据原子性 323 | for _, likeVideoId := range videoIdList { 324 | if _, err1 := redis.RdbLikeUserId.SAdd(redis.Ctx, strUserId, likeVideoId).Result(); err1 != nil { 325 | log.Printf("方法:FavouriteAction RedisLikeUserId add value失败") 326 | redis.RdbLikeUserId.Del(redis.Ctx, strUserId) 327 | return err1 328 | } 329 | } 330 | //这样操作理由同上 331 | if _, err2 := redis.RdbLikeUserId.SRem(redis.Ctx, strUserId, videoId).Result(); err2 != nil { 332 | log.Printf("方法:FavouriteAction RedisLikeUserId del value失败:%v", err2) 333 | return err2 334 | } else { 335 | rabbitmq.RmqLikeDel.Publish(sb.String()) 336 | } 337 | } 338 | 339 | //查询Redis LikeVideoId(key:strVideoId)是否已经加载过此信息 340 | if n, err := redis.RdbLikeVideoId.Exists(redis.Ctx, strVideoId).Result(); n > 0 { 341 | //如果有问题,说明查询redis失败,返回错误信息 342 | if err != nil { 343 | log.Printf("方法:FavouriteAction RedisLikeVideoId query key失败:%v", err) 344 | return err 345 | } //如果加载过此信息key:strVideoId,则删除value:userId 346 | //如果redis LikeVideoId 删除失败,返回错误信息 347 | if _, err1 := redis.RdbLikeVideoId.SRem(redis.Ctx, strVideoId, userId).Result(); err1 != nil { 348 | log.Printf("方法:FavouriteAction RedisLikeVideoId del value失败:%v", err1) 349 | return err1 350 | } 351 | } else { //如果不存在,则维护Redis LikeVideoId 新建key:strVideoId,加入value:DefaultRedisValue,过期才会删,防止删最后一个数据的时候数据库 352 | // 还没更新完出现脏读,或者数据库操作失败造成的脏读 353 | //通过videoId查询likes表,返回所有点赞userId,加入key:strVideoId集合中, 354 | //再删除当前userId,再更新likes表此条数据 355 | //key:strVideoId,加入value:DefaultRedisValue,过期才会删,防止删最后一个数据的时候数据库还没更新完出现脏读,或者数据库操作失败造成的脏读 356 | if _, err := redis.RdbLikeVideoId.SAdd(redis.Ctx, strVideoId, config.DefaultRedisValue).Result(); err != nil { 357 | log.Printf("方法:FavouriteAction RedisLikeVideoId add value失败") 358 | redis.RdbLikeVideoId.Del(redis.Ctx, strVideoId) 359 | return err 360 | } 361 | //给键值设置有效期,类似于gc机制 362 | _, err := redis.RdbLikeVideoId.Expire(redis.Ctx, strVideoId, 363 | time.Duration(config.OneMonth)*time.Second).Result() 364 | if err != nil { 365 | log.Printf("方法:FavouriteAction RedisLikeVideoId 设置有效期失败") 366 | redis.RdbLikeVideoId.Del(redis.Ctx, strVideoId) 367 | return err 368 | } 369 | 370 | userIdList, err1 := dao.GetLikeUserIdList(videoId) 371 | //如果有问题,说明查询失败,返回错误信息:"get likeUserIdList failed" 372 | if err1 != nil { 373 | redis.RdbLikeVideoId.Del(redis.Ctx, strVideoId) 374 | return err1 375 | } 376 | //遍历userIdList,添加进key的集合中,若失败,删除key,并返回错误信息,这么做的原因是防止脏读, 377 | //保证redis与mysql数据一致性 378 | for _, likeUserId := range userIdList { 379 | if _, err1 := redis.RdbLikeVideoId.SAdd(redis.Ctx, strVideoId, likeUserId).Result(); err1 != nil { 380 | log.Printf("方法:FavouriteAction RedisLikeVideoId add value失败") 381 | redis.RdbLikeVideoId.Del(redis.Ctx, strVideoId) 382 | return err1 383 | } 384 | } 385 | //这样操作理由同上 386 | if _, err2 := redis.RdbLikeVideoId.SRem(redis.Ctx, strVideoId, userId).Result(); err2 != nil { 387 | log.Printf("方法:FavouriteAction RedisLikeVideoId del value失败:%v", err2) 388 | return err2 389 | } 390 | } 391 | } 392 | return nil 393 | } 394 | 395 | //GetFavouriteList 根据userId,curId(当前用户Id),返回userId的点赞列表; 396 | //step1:查询Redis LikeUserId(key:strUserId)是否已经加载过此信息,获取集合中全部videoId,并添加到点赞列表集合中; 397 | //step2:LikeUserId中都没有对应key,维护LikeUserId对应key,同时添加到点赞列表集合中; 398 | func (like *LikeServiceImpl) GetFavouriteList(userId int64, curId int64) ([]Video, error) { 399 | //将int64 userId转换为 string strUserId 400 | strUserId := strconv.FormatInt(userId, 10) 401 | //step1:查询Redis LikeUserId,如果key:strUserId存在,则获取集合中全部videoId 402 | if n, err := redis.RdbLikeUserId.Exists(redis.Ctx, strUserId).Result(); n > 0 { 403 | //如果有问题,说明查询redis失败,返回默认nil,返回错误信息 404 | if err != nil { 405 | log.Printf("方法:GetFavouriteList RedisLikeVideoId query key失败:%v", err) 406 | return nil, err 407 | } 408 | //获取集合中全部videoId 409 | videoIdList, err1 := redis.RdbLikeUserId.SMembers(redis.Ctx, strUserId).Result() 410 | //如果有问题,说明查询redis失败,返回默认nil,返回错误信息 411 | if err1 != nil { 412 | log.Printf("方法:GetFavouriteList RedisLikeVideoId get values失败:%v", err1) 413 | return nil, err1 414 | } 415 | //提前开辟点赞列表空间 416 | favoriteVideoList := new([]Video) 417 | //采用协程并发将Video类型对象添加到集合中去 418 | i := len(videoIdList) - 1 //去掉DefaultRedisValue 419 | if i == 0 { 420 | return *favoriteVideoList, nil 421 | } 422 | var wg sync.WaitGroup 423 | wg.Add(i) 424 | for j := 0; j <= i; j++ { 425 | //将string videoId转换为 int64 VideoId 426 | videoId, _ := strconv.ParseInt(videoIdList[j], 10, 64) 427 | if videoId == config.DefaultRedisValue { 428 | continue 429 | } 430 | go like.addFavouriteVideoList(videoId, curId, favoriteVideoList, &wg) 431 | } 432 | wg.Wait() 433 | return *favoriteVideoList, nil 434 | } else { //如果Redis LikeUserId不存在此key,通过userId查询likes表,返回所有点赞videoId,并维护到Redis LikeUserId(key:strUserId) 435 | //key:strUserId,加入value:DefaultRedisValue,过期才会删,防止删最后一个数据的时候数据库还没更新完出现脏读,或者数据库操作失败造成的脏读 436 | if _, err := redis.RdbLikeUserId.SAdd(redis.Ctx, strUserId, config.DefaultRedisValue).Result(); err != nil { 437 | log.Printf("方法:GetFavouriteList RedisLikeUserId add value失败") 438 | redis.RdbLikeUserId.Del(redis.Ctx, strUserId) 439 | return nil, err 440 | } 441 | //给键值设置有效期,类似于gc机制 442 | _, err := redis.RdbLikeUserId.Expire(redis.Ctx, strUserId, 443 | time.Duration(config.OneMonth)*time.Second).Result() 444 | if err != nil { 445 | log.Printf("方法:GetFavouriteList RedisLikeUserId 设置有效期失败") 446 | redis.RdbLikeUserId.Del(redis.Ctx, strUserId) 447 | return nil, err 448 | } 449 | videoIdList, err1 := dao.GetLikeVideoIdList(userId) 450 | //如果有问题,说明查询数据库失败,返回nil和错误信息:"get likeVideoIdList failed" 451 | if err1 != nil { 452 | log.Println(err1.Error()) 453 | redis.RdbLikeUserId.Del(redis.Ctx, strUserId) 454 | return nil, err1 455 | } 456 | //遍历videoIdList,添加进key的集合中,若失败,删除key,并返回错误信息,这么做的原因是防止脏读, 457 | //保证redis与mysql数据一致性 458 | for _, likeVideoId := range videoIdList { 459 | if _, err2 := redis.RdbLikeUserId.SAdd(redis.Ctx, strUserId, likeVideoId).Result(); err2 != nil { 460 | log.Printf("方法:GetFavouriteList RedisLikeUserId add value失败") 461 | redis.RdbLikeUserId.Del(redis.Ctx, strUserId) 462 | return nil, err2 463 | } 464 | } 465 | //提前开辟点赞列表空间 466 | favoriteVideoList := new([]Video) 467 | //采用协程并发将Video类型对象添加到集合中去 468 | i := len(videoIdList) - 1 //去掉DefaultRedisValue 469 | if i == 0 { 470 | return *favoriteVideoList, nil 471 | } 472 | var wg sync.WaitGroup 473 | wg.Add(i) 474 | for j := 0; j <= i; j++ { 475 | if videoIdList[j] == config.DefaultRedisValue { 476 | continue 477 | } 478 | go like.addFavouriteVideoList(videoIdList[j], curId, favoriteVideoList, &wg) 479 | } 480 | wg.Wait() 481 | return *favoriteVideoList, nil 482 | } 483 | } 484 | 485 | //addFavouriteVideoList 根据videoId,登录用户curId,添加视频对象到点赞列表空间 486 | func (like *LikeServiceImpl) addFavouriteVideoList(videoId int64, curId int64, favoriteVideoList *[]Video, wg *sync.WaitGroup) { 487 | defer wg.Done() 488 | //调用videoService接口,GetVideo:根据videoId,当前用户id:curId,返回Video类型对象 489 | video, err := like.GetVideo(videoId, curId) 490 | //如果没有获取这个video_id的视频,视频可能被删除了,打印异常,并且不加入此视频 491 | if err != nil { 492 | log.Println(errors.New("this favourite video is miss")) 493 | return 494 | } 495 | //将Video类型对象添加到集合中去 496 | *favoriteVideoList = append(*favoriteVideoList, video) 497 | } 498 | 499 | //TotalFavourite 根据userId获取这个用户总共被点赞数量 500 | func (like *LikeServiceImpl) TotalFavourite(userId int64) (int64, error) { 501 | //根据userId获取这个用户的发布视频列表信息 502 | videoIdList, err := like.GetVideoIdList(userId) 503 | if err != nil { 504 | log.Printf(err.Error()) 505 | return 0, err 506 | } 507 | var sum int64 //该用户的总被点赞数 508 | //提前开辟空间,存取每个视频的点赞数 509 | videoLikeCountList := new([]int64) 510 | //采用协程并发将对应videoId的点赞数添加到集合中去 511 | i := len(videoIdList) 512 | var wg sync.WaitGroup 513 | wg.Add(i) 514 | for j := 0; j < i; j++ { 515 | go like.addVideoLikeCount(videoIdList[j], videoLikeCountList, &wg) 516 | } 517 | wg.Wait() 518 | //遍历累加,求总被点赞数 519 | for _, count := range *videoLikeCountList { 520 | sum += count 521 | } 522 | return sum, nil 523 | } 524 | 525 | //FavouriteVideoCount 根据userId获取这个用户点赞视频数量 526 | func (like *LikeServiceImpl) FavouriteVideoCount(userId int64) (int64, error) { 527 | //将int64 userId转换为 string strUserId 528 | strUserId := strconv.FormatInt(userId, 10) 529 | //step1:查询Redis LikeUserId,如果key:strUserId存在,则获取集合中元素个数 530 | if n, err := redis.RdbLikeUserId.Exists(redis.Ctx, strUserId).Result(); n > 0 { 531 | //如果有问题,说明查询redis失败,返回默认0,返回错误信息 532 | if err != nil { 533 | log.Printf("方法:FavouriteVideoCount RdbLikeUserId query key失败:%v", err) 534 | return 0, err 535 | } else { 536 | count, err1 := redis.RdbLikeUserId.SCard(redis.Ctx, strUserId).Result() 537 | //如果有问题,说明操作redis失败,返回默认0,返回错误信息 538 | if err1 != nil { 539 | log.Printf("方法:FavouriteVideoCount RdbLikeUserId query count 失败:%v", err1) 540 | return 0, err1 541 | } 542 | log.Printf("方法:FavouriteVideoCount RdbLikeUserId query count 成功") 543 | return count - 1, nil //去掉DefaultRedisValue 544 | 545 | } 546 | } else { //如果Redis LikeUserId不存在此key,通过userId查询likes表,返回所有点赞videoId,并维护到Redis LikeUserId(key:strUserId) 547 | //再通过set集合中userId个数,获取点赞数量 548 | //key:strUserId,加入value:DefaultRedisValue,过期才会删,防止删最后一个数据的时候数据库还没更新完出现脏读,或者数据库操作失败造成的脏读 549 | if _, err := redis.RdbLikeUserId.SAdd(redis.Ctx, strUserId, config.DefaultRedisValue).Result(); err != nil { 550 | log.Printf("方法:FavouriteVideoCount RedisLikeUserId add value失败") 551 | redis.RdbLikeUserId.Del(redis.Ctx, strUserId) 552 | return 0, err 553 | } 554 | //给键值设置有效期,类似于gc机制 555 | _, err := redis.RdbLikeUserId.Expire(redis.Ctx, strUserId, 556 | time.Duration(config.OneMonth)*time.Second).Result() 557 | if err != nil { 558 | log.Printf("方法:FavouriteVideoCount RedisLikeUserId 设置有效期失败") 559 | redis.RdbLikeUserId.Del(redis.Ctx, strUserId) 560 | return 0, err 561 | } 562 | videoIdList, err1 := dao.GetLikeVideoIdList(userId) 563 | //如果有问题,说明查询数据库失败,返回默认0,返回错误信息:"get likeVideoIdList failed" 564 | if err1 != nil { 565 | log.Printf(err1.Error()) 566 | return 0, err1 567 | } 568 | //维护Redis LikeUserId(key:strUserId),遍历videoIdList加入 569 | for _, likeVideoId := range videoIdList { 570 | if _, err1 := redis.RdbLikeUserId.SAdd(redis.Ctx, strUserId, likeVideoId).Result(); err1 != nil { 571 | log.Printf("方法:FavouriteVideoCount RedisLikeUserId add value失败") 572 | redis.RdbLikeUserId.Del(redis.Ctx, strUserId) 573 | return 0, err1 574 | } 575 | } 576 | //再通过set集合中videoId个数,获取点赞数量 577 | count, err2 := redis.RdbLikeUserId.SCard(redis.Ctx, strUserId).Result() 578 | //如果有问题,说明操作redis失败,返回默认0,返回错误信息 579 | if err2 != nil { 580 | log.Printf("方法:FavouriteVideoCount RdbLikeUserId query count 失败:%v", err2) 581 | return 0, err2 582 | } 583 | log.Printf("方法:FavouriteVideoCount RdbLikeUserId query count 成功") 584 | return count - 1, nil //去掉DefaultRedisValue 585 | } 586 | } 587 | 588 | //addVideoLikeCount 根据videoId,将该视频点赞数加入对应提前开辟好的空间内 589 | func (like *LikeServiceImpl) addVideoLikeCount(videoId int64, videoLikeCountList *[]int64, wg *sync.WaitGroup) { 590 | defer wg.Done() 591 | //调用FavouriteCount:根据videoId,获取点赞数 592 | count, err := like.FavouriteCount(videoId) 593 | if err != nil { 594 | //如果有错误,输出错误信息,并不加入该视频点赞数 595 | log.Printf(err.Error()) 596 | return 597 | } 598 | *videoLikeCountList = append(*videoLikeCountList, count) 599 | } 600 | 601 | //GetLikeService 解决likeService调videoService,videoService调userService,useService调likeService循环依赖的问题 602 | func GetLikeService() LikeServiceImpl { 603 | var userService UserServiceImpl 604 | var videoService VideoServiceImpl 605 | var likeService LikeServiceImpl 606 | userService.LikeService = &likeService 607 | likeService.VideoService = &videoService 608 | videoService.UserService = &userService 609 | return likeService 610 | } 611 | -------------------------------------------------------------------------------- /service/likeServiceImpl_test.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "TikTok/dao" 5 | "TikTok/middleware/ffmpeg" 6 | "TikTok/middleware/rabbitmq" 7 | "TikTok/middleware/redis" 8 | "fmt" 9 | "testing" 10 | ) 11 | 12 | func TestIsFavourite(t *testing.T) { 13 | // 初始化数据库 14 | dao.Init() 15 | // 初始化FTP服务器链接 16 | dao.InitFTP() 17 | // 初始化SSH 18 | ffmpeg.InitSSH() 19 | 20 | // 初始化redis-DB0的连接,follow选择的DB0. 21 | redis.InitRedis() 22 | // 初始化rabbitMQ。 23 | rabbitmq.InitRabbitMQ() 24 | // 初始化Follow的相关消息队列,并开启消费。 25 | rabbitmq.InitFollowRabbitMQ() 26 | // 初始化Like的相关消息队列,并开启消费。 27 | impl := LikeServiceImpl{} 28 | isFavourite, _ := impl.IsFavourite(666, 3) 29 | fmt.Printf("%v", isFavourite) 30 | } 31 | 32 | func TestFavouriteCount(t *testing.T) { 33 | // 初始化数据库 34 | dao.Init() 35 | // 初始化FTP服务器链接 36 | dao.InitFTP() 37 | // 初始化SSH 38 | ffmpeg.InitSSH() 39 | 40 | // 初始化redis-DB0的连接,follow选择的DB0. 41 | redis.InitRedis() 42 | // 初始化rabbitMQ。 43 | rabbitmq.InitRabbitMQ() 44 | // 初始化Follow的相关消息队列,并开启消费。 45 | rabbitmq.InitFollowRabbitMQ() 46 | // 初始化Like的相关消息队列,并开启消费。 47 | impl := LikeServiceImpl{} 48 | count, _ := impl.FavouriteCount(666) 49 | fmt.Printf("%v", count) 50 | } 51 | 52 | func TestTotalFavourite(t *testing.T) { 53 | // 初始化数据库 54 | dao.Init() 55 | // 初始化FTP服务器链接 56 | dao.InitFTP() 57 | // 初始化SSH 58 | ffmpeg.InitSSH() 59 | 60 | // 初始化redis-DB0的连接,follow选择的DB0. 61 | redis.InitRedis() 62 | // 初始化rabbitMQ。 63 | rabbitmq.InitRabbitMQ() 64 | // 初始化Follow的相关消息队列,并开启消费。 65 | rabbitmq.InitFollowRabbitMQ() 66 | // 初始化Like的相关消息队列,并开启消费。 67 | rabbitmq.InitLikeRabbitMQ() 68 | impl := LikeServiceImpl{} 69 | count, _ := impl.TotalFavourite(3) 70 | fmt.Printf("%v", count) 71 | } 72 | -------------------------------------------------------------------------------- /service/likeSub.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import "TikTok/dao" 4 | 5 | type LikeSub struct { 6 | } 7 | 8 | func (like *LikeSub) GetVideo(videoId int64, userId int64) (Video, error) { 9 | if videoId%2 == 1 { 10 | return Video{ 11 | TableVideo: dao.TableVideo{ 12 | Id: 1, 13 | AuthorId: 1, 14 | PlayUrl: "www.baidu.com", 15 | CoverUrl: "www.baidu.com", 16 | }, 17 | Author: User{ 18 | Id: 1, 19 | Name: "lzz", 20 | FollowCount: 12, 21 | FollowerCount: 13, 22 | IsFollow: true, 23 | }, 24 | FavoriteCount: 2, 25 | CommentCount: 3, 26 | IsFavorite: true, 27 | }, nil 28 | } else { 29 | return Video{ 30 | TableVideo: dao.TableVideo{ 31 | Id: 2, 32 | AuthorId: 3, 33 | PlayUrl: "www.baidu.com", 34 | CoverUrl: "www.baidu11.com", 35 | }, 36 | Author: User{ 37 | Id: 2, 38 | Name: "lzz", 39 | FollowCount: 12, 40 | FollowerCount: 13, 41 | IsFollow: true, 42 | }, 43 | FavoriteCount: 2, 44 | CommentCount: 3, 45 | IsFavorite: true, 46 | }, nil 47 | } 48 | } 49 | 50 | func (like *LikeSub) GetVideoIdList(userId int64) ([]int64, error) { 51 | videoList := []int64{51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 52 | 66, 67, 68, 69, 70, 71, 72, 73, 74, 75} 53 | return videoList, nil 54 | } 55 | -------------------------------------------------------------------------------- /service/userService.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import "TikTok/dao" 4 | 5 | type UserService interface { 6 | /* 7 | 个人使用 8 | */ 9 | // GetTableUserList 获得全部TableUser对象 10 | GetTableUserList() []dao.TableUser 11 | 12 | // GetTableUserByUsername 根据username获得TableUser对象 13 | GetTableUserByUsername(name string) dao.TableUser 14 | 15 | // GetTableUserById 根据user_id获得TableUser对象 16 | GetTableUserById(id int64) dao.TableUser 17 | 18 | // InsertTableUser 将tableUser插入表内 19 | InsertTableUser(tableUser *dao.TableUser) bool 20 | /* 21 | 他人使用 22 | */ 23 | // GetUserById 未登录情况下,根据user_id获得User对象 24 | GetUserById(id int64) (User, error) 25 | 26 | // GetUserByIdWithCurId 已登录(curID)情况下,根据user_id获得User对象 27 | GetUserByIdWithCurId(id int64, curId int64) (User, error) 28 | 29 | // 根据token返回id 30 | // 接口:auth中间件,解析完token,将userid放入context 31 | //(调用方法:直接在context内拿参数"userId"的值) fmt.Printf("userInfo: %v\n", c.GetString("userId")) 32 | } 33 | 34 | // User 最终封装后,controller返回的User结构体 35 | type User struct { 36 | Id int64 `json:"id,omitempty"` 37 | Name string `json:"name,omitempty"` 38 | FollowCount int64 `json:"follow_count"` 39 | FollowerCount int64 `json:"follower_count"` 40 | IsFollow bool `json:"is_follow"` 41 | TotalFavorited int64 `json:"total_favorited,omitempty"` 42 | FavoriteCount int64 `json:"favorite_count,omitempty"` 43 | } 44 | -------------------------------------------------------------------------------- /service/userServiceImpl.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "TikTok/config" 5 | "TikTok/dao" 6 | "crypto/hmac" 7 | "crypto/sha256" 8 | "encoding/hex" 9 | "fmt" 10 | "github.com/dgrijalva/jwt-go" 11 | "log" 12 | "strconv" 13 | "time" 14 | ) 15 | 16 | type UserServiceImpl struct { 17 | FollowService 18 | LikeService 19 | } 20 | 21 | // GetTableUserList 获得全部TableUser对象 22 | func (usi *UserServiceImpl) GetTableUserList() []dao.TableUser { 23 | tableUsers, err := dao.GetTableUserList() 24 | if err != nil { 25 | log.Println("Err:", err.Error()) 26 | return tableUsers 27 | } 28 | return tableUsers 29 | } 30 | 31 | // GetTableUserByUsername 根据username获得TableUser对象 32 | func (usi *UserServiceImpl) GetTableUserByUsername(name string) dao.TableUser { 33 | tableUser, err := dao.GetTableUserByUsername(name) 34 | if err != nil { 35 | log.Println("Err:", err.Error()) 36 | log.Println("User Not Found") 37 | return tableUser 38 | } 39 | log.Println("Query User Success") 40 | return tableUser 41 | } 42 | 43 | // GetTableUserById 根据user_id获得TableUser对象 44 | func (usi *UserServiceImpl) GetTableUserById(id int64) dao.TableUser { 45 | tableUser, err := dao.GetTableUserById(id) 46 | if err != nil { 47 | log.Println("Err:", err.Error()) 48 | log.Println("User Not Found") 49 | return tableUser 50 | } 51 | log.Println("Query User Success") 52 | return tableUser 53 | } 54 | 55 | // InsertTableUser 将tableUser插入表内 56 | func (usi *UserServiceImpl) InsertTableUser(tableUser *dao.TableUser) bool { 57 | flag := dao.InsertTableUser(tableUser) 58 | if flag == false { 59 | log.Println("插入失败") 60 | return false 61 | } 62 | return true 63 | } 64 | 65 | // GetUserById 未登录情况下,根据user_id获得User对象 66 | func (usi *UserServiceImpl) GetUserById(id int64) (User, error) { 67 | user := User{ 68 | Id: 0, 69 | Name: "", 70 | FollowCount: 0, 71 | FollowerCount: 0, 72 | IsFollow: false, 73 | TotalFavorited: 0, 74 | FavoriteCount: 0, 75 | } 76 | tableUser, err := dao.GetTableUserById(id) 77 | if err != nil { 78 | log.Println("Err:", err.Error()) 79 | log.Println("User Not Found") 80 | return user, err 81 | } 82 | log.Println("Query User Success") 83 | followCount, _ := usi.GetFollowingCnt(id) 84 | if err != nil { 85 | log.Println("Err:", err.Error()) 86 | } 87 | followerCount, _ := usi.GetFollowerCnt(id) 88 | if err != nil { 89 | log.Println("Err:", err.Error()) 90 | } 91 | u := GetLikeService() //解决循环依赖 92 | totalFavorited, _ := u.TotalFavourite(id) 93 | favoritedCount, _ := u.FavouriteVideoCount(id) 94 | user = User{ 95 | Id: id, 96 | Name: tableUser.Name, 97 | FollowCount: followCount, 98 | FollowerCount: followerCount, 99 | IsFollow: false, 100 | TotalFavorited: totalFavorited, 101 | FavoriteCount: favoritedCount, 102 | } 103 | return user, nil 104 | } 105 | 106 | // GetUserByIdWithCurId 已登录(curID)情况下,根据user_id获得User对象 107 | func (usi *UserServiceImpl) GetUserByIdWithCurId(id int64, curId int64) (User, error) { 108 | user := User{ 109 | Id: 0, 110 | Name: "", 111 | FollowCount: 0, 112 | FollowerCount: 0, 113 | IsFollow: false, 114 | TotalFavorited: 0, 115 | FavoriteCount: 0, 116 | } 117 | tableUser, err := dao.GetTableUserById(id) 118 | if err != nil { 119 | log.Println("Err:", err.Error()) 120 | log.Println("User Not Found") 121 | return user, err 122 | } 123 | log.Println("Query User Success") 124 | followCount, err := usi.GetFollowingCnt(id) 125 | if err != nil { 126 | log.Println("Err:", err.Error()) 127 | } 128 | followerCount, err := usi.GetFollowerCnt(id) 129 | if err != nil { 130 | log.Println("Err:", err.Error()) 131 | } 132 | isfollow, err := usi.IsFollowing(curId, id) 133 | if err != nil { 134 | log.Println("Err:", err.Error()) 135 | } 136 | u := GetLikeService() //解决循环依赖 137 | totalFavorited, _ := u.TotalFavourite(id) 138 | favoritedCount, _ := u.FavouriteVideoCount(id) 139 | user = User{ 140 | Id: id, 141 | Name: tableUser.Name, 142 | FollowCount: followCount, 143 | FollowerCount: followerCount, 144 | IsFollow: isfollow, 145 | TotalFavorited: totalFavorited, 146 | FavoriteCount: favoritedCount, 147 | } 148 | return user, nil 149 | } 150 | 151 | // GenerateToken 根据username生成一个token 152 | func GenerateToken(username string) string { 153 | u := UserService.GetTableUserByUsername(new(UserServiceImpl), username) 154 | fmt.Printf("generatetoken: %v\n", u) 155 | token := NewToken(u) 156 | println(token) 157 | return token 158 | } 159 | 160 | // NewToken 根据信息创建token 161 | func NewToken(u dao.TableUser) string { 162 | expiresTime := time.Now().Unix() + int64(config.OneDayOfHours) 163 | fmt.Printf("expiresTime: %v\n", expiresTime) 164 | id64 := u.Id 165 | fmt.Printf("id: %v\n", strconv.FormatInt(id64, 10)) 166 | claims := jwt.StandardClaims{ 167 | Audience: u.Name, 168 | ExpiresAt: expiresTime, 169 | Id: strconv.FormatInt(id64, 10), 170 | IssuedAt: time.Now().Unix(), 171 | Issuer: "tiktok", 172 | NotBefore: time.Now().Unix(), 173 | Subject: "token", 174 | } 175 | var jwtSecret = []byte(config.Secret) 176 | tokenClaims := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) 177 | if token, err := tokenClaims.SignedString(jwtSecret); err == nil { 178 | token = "Bearer " + token 179 | println("generate token success!\n") 180 | return token 181 | } else { 182 | println("generate token fail\n") 183 | return "fail" 184 | } 185 | } 186 | 187 | // EnCoder 密码加密 188 | func EnCoder(password string) string { 189 | h := hmac.New(sha256.New, []byte(password)) 190 | sha := hex.EncodeToString(h.Sum(nil)) 191 | fmt.Println("Result: " + sha) 192 | return sha 193 | } 194 | -------------------------------------------------------------------------------- /service/userServiceImpl_test.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "TikTok/dao" 5 | "fmt" 6 | "testing" 7 | ) 8 | 9 | func TestGetTableUserList(t *testing.T) { 10 | impl := UserServiceImpl{} 11 | list := impl.GetTableUserList() 12 | fmt.Printf("%v", list) 13 | } 14 | 15 | func TestGetTableUserByUsername(t *testing.T) { 16 | impl := UserServiceImpl{} 17 | list := impl.GetTableUserByUsername("test") 18 | fmt.Printf("%v", list) 19 | } 20 | 21 | func TestGetTableUserById(t *testing.T) { 22 | impl := UserServiceImpl{} 23 | list := impl.GetTableUserById(int64(4)) 24 | fmt.Printf("%v", list) 25 | } 26 | 27 | func TestInsertTableUser(t *testing.T) { 28 | impl := UserServiceImpl{} 29 | user := &dao.TableUser{ 30 | Id: 20000, 31 | Name: "qaq", 32 | Password: "111111", 33 | } 34 | list := impl.InsertTableUser(user) 35 | fmt.Printf("%v", list) 36 | } 37 | 38 | func TestGetUserById(t *testing.T) { 39 | impl := UserServiceImpl{ 40 | FollowService: &FollowServiceImp{}, 41 | LikeService: &LikeServiceImpl{}, 42 | } 43 | list, _ := impl.GetUserById(int64(4)) 44 | fmt.Printf("%v", list) 45 | } 46 | 47 | func TestGetUserByIdWithCurId(t *testing.T) { 48 | impl := UserServiceImpl{ 49 | FollowService: &FollowServiceImp{}, 50 | } 51 | list, _ := impl.GetUserByIdWithCurId(int64(482), int64(130)) 52 | fmt.Printf("%v", list) 53 | } 54 | -------------------------------------------------------------------------------- /service/userSub.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | // 如果要实现用户登录所有功能,将下方代码恢复 4 | /*type FollowServiceImpl struct { 5 | } 6 | 7 | func (fsi *FollowServiceImpl) IsFollowing(userId int64, targetId int64) (bool, error) { 8 | return true, nil 9 | } 10 | 11 | func (fsi *FollowServiceImpl) GetFollowingCnt(userId int64) (int64, error) { 12 | return int64(1), nil 13 | } 14 | 15 | func (fsi *FollowServiceImpl) GetFollowerCnt(userId int64) (int64, error) { 16 | return int64(1), nil 17 | } 18 | 19 | func (fsi *FollowServiceImpl) AddFollowRelation(userId int64, targetId int64) (bool, error) { 20 | return true, nil 21 | } 22 | 23 | func (fsi *FollowServiceImpl) DeleteFollowRelation(userId int64, targetId int64) (bool, error) { 24 | return true, nil 25 | } 26 | 27 | func (fsi *FollowServiceImpl) GetFollowing(userId int64) ([]User, error) { 28 | return nil, nil 29 | } 30 | 31 | func (fsi *FollowServiceImpl) GetFollowers(userId int64) ([]User, error) { 32 | return nil, nil 33 | }*/ 34 | -------------------------------------------------------------------------------- /service/videoService.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "TikTok/dao" 5 | "mime/multipart" 6 | "time" 7 | ) 8 | 9 | type Video struct { 10 | dao.TableVideo 11 | Author User `json:"author"` 12 | FavoriteCount int64 `json:"favorite_count"` 13 | CommentCount int64 `json:"comment_count"` 14 | IsFavorite bool `json:"is_favorite"` 15 | } 16 | 17 | type VideoService interface { 18 | // Feed 19 | // 通过传入时间戳,当前用户的id,返回对应的视频切片数组,以及视频数组中最早的发布时间 20 | Feed(lastTime time.Time, userId int64) ([]Video, time.Time, error) 21 | 22 | // GetVideo 23 | // 传入视频id获得对应的视频对象 24 | GetVideo(videoId int64, userId int64) (Video, error) 25 | 26 | // Publish 27 | // 将传入的视频流保存在文件服务器中,并存储在mysql表中 28 | // 5.23 加入title 29 | Publish(data *multipart.FileHeader, userId int64, title string) error 30 | 31 | // List 32 | // 通过userId来查询对应用户发布的视频,并返回对应的视频切片数组 33 | List(userId int64, curId int64) ([]Video, error) 34 | 35 | // GetVideoIdList 36 | // 通过一个作者id,返回该用户发布的视频id切片数组 37 | GetVideoIdList(userId int64) ([]int64, error) 38 | } 39 | -------------------------------------------------------------------------------- /service/videoServiceImpl.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "TikTok/config" 5 | "TikTok/dao" 6 | "TikTok/middleware/ffmpeg" 7 | "github.com/satori/go.uuid" 8 | "log" 9 | "mime/multipart" 10 | "sync" 11 | "time" 12 | ) 13 | 14 | type VideoServiceImpl struct { 15 | UserService 16 | LikeService 17 | CommentService 18 | } 19 | 20 | // Feed 21 | // 通过传入时间戳,当前用户的id,返回对应的视频数组,以及视频数组中最早的发布时间 22 | // 获取视频数组大小是可以控制的,在config中的videoCount变量 23 | func (videoService VideoServiceImpl) Feed(lastTime time.Time, userId int64) ([]Video, time.Time, error) { 24 | //创建对应返回视频的切片数组,提前将切片的容量设置好,可以减少切片扩容的性能 25 | videos := make([]Video, 0, config.VideoCount) 26 | //根据传入的时间,获得传入时间前n个视频,可以通过config.videoCount来控制 27 | tableVideos, err := dao.GetVideosByLastTime(lastTime) 28 | if err != nil { 29 | log.Printf("方法dao.GetVideosByLastTime(lastTime) 失败:%v", err) 30 | return nil, time.Time{}, err 31 | } 32 | log.Printf("方法dao.GetVideosByLastTime(lastTime) 成功:%v", tableVideos) 33 | //将数据通过copyVideos进行处理,在拷贝的过程中对数据进行组装 34 | err = videoService.copyVideos(&videos, &tableVideos, userId) 35 | if err != nil { 36 | log.Printf("方法videoService.copyVideos(&videos, &tableVideos, userId) 失败:%v", err) 37 | return nil, time.Time{}, err 38 | } 39 | log.Printf("方法videoService.copyVideos(&videos, &tableVideos, userId) 成功") 40 | //返回数据,同时获得视频中最早的时间返回 41 | return videos, tableVideos[len(tableVideos)-1].PublishTime, nil 42 | } 43 | 44 | // GetVideo 45 | // 传入视频id获得对应的视频对象,注意还需要传入当前登录用户id 46 | func (videoService *VideoServiceImpl) GetVideo(videoId int64, userId int64) (Video, error) { 47 | //初始化video对象 48 | var video Video 49 | 50 | //从数据库中查询数据,如果查询不到数据,就直接失败返回,后续流程就不需要执行了 51 | data, err := dao.GetVideoByVideoId(videoId) 52 | if err != nil { 53 | log.Printf("方法dao.GetVideoByVideoId(videoId) 失败:%v", err) 54 | return video, err 55 | } else { 56 | log.Printf("方法dao.GetVideoByVideoId(videoId) 成功") 57 | } 58 | 59 | //插入从数据库中查到的数据 60 | videoService.creatVideo(&video, &data, userId) 61 | return video, nil 62 | } 63 | 64 | // Publish 65 | // 将传入的视频流保存在文件服务器中,并存储在mysql表中 66 | func (videoService *VideoServiceImpl) Publish(data *multipart.FileHeader, userId int64, title string) error { 67 | //将视频流上传到视频服务器,保存视频链接 68 | file, err := data.Open() 69 | if err != nil { 70 | log.Printf("方法data.Open() 失败%v", err) 71 | return err 72 | } 73 | log.Printf("方法data.Open() 成功") 74 | //生成一个uuid作为视频的名字 75 | videoName := uuid.NewV4().String() 76 | log.Printf("生成视频名称%v", videoName) 77 | err = dao.VideoFTP(file, videoName) 78 | if err != nil { 79 | log.Printf("方法dao.VideoFTP(file, videoName) 失败%v", err) 80 | return err 81 | } 82 | log.Printf("方法dao.VideoFTP(file, videoName) 成功") 83 | defer file.Close() 84 | //在服务器上执行ffmpeg 从视频流中获取第一帧截图,并上传图片服务器,保存图片链接 85 | imageName := uuid.NewV4().String() 86 | //向队列中添加消息 87 | ffmpeg.Ffchan <- ffmpeg.Ffmsg{ 88 | videoName, 89 | imageName, 90 | } 91 | //组装并持久化 92 | err = dao.Save(videoName, imageName, userId, title) 93 | if err != nil { 94 | log.Printf("方法dao.Save(videoName, imageName, userId) 失败%v", err) 95 | return err 96 | } 97 | log.Printf("方法dao.Save(videoName, imageName, userId) 成功") 98 | return nil 99 | } 100 | 101 | // List 102 | // 通过userId来查询对应用户发布的视频,并返回对应的视频数组 103 | func (videoService *VideoServiceImpl) List(userId int64, curId int64) ([]Video, error) { 104 | //依据用户id查询所有的视频,获取视频列表 105 | data, err := dao.GetVideosByAuthorId(userId) 106 | if err != nil { 107 | log.Printf("方法dao.GetVideosByAuthorId(%v)失败:%v", userId, err) 108 | return nil, err 109 | } 110 | log.Printf("方法dao.GetVideosByAuthorId(%v)成功:%v", userId, data) 111 | //提前定义好切片长度 112 | result := make([]Video, 0, len(data)) 113 | //调用拷贝方法,将数据进行转换 114 | err = videoService.copyVideos(&result, &data, curId) 115 | if err != nil { 116 | log.Printf("方法videoService.copyVideos(&result, &data, %v)失败:%v", userId, err) 117 | return nil, err 118 | } 119 | //如果数据没有问题,则直接返回 120 | return result, nil 121 | } 122 | 123 | // 该方法可以将数据进行拷贝和转换,并从其他方法获取对应的数据 124 | func (videoService *VideoServiceImpl) copyVideos(result *[]Video, data *[]dao.TableVideo, userId int64) error { 125 | for _, temp := range *data { 126 | var video Video 127 | //将video进行组装,添加想要的信息,插入从数据库中查到的数据 128 | videoService.creatVideo(&video, &temp, userId) 129 | *result = append(*result, video) 130 | } 131 | return nil 132 | } 133 | 134 | //将video进行组装,添加想要的信息,插入从数据库中查到的数据 135 | func (videoService *VideoServiceImpl) creatVideo(video *Video, data *dao.TableVideo, userId int64) { 136 | //建立协程组,当这一组的携程全部完成后,才会结束本方法 137 | var wg sync.WaitGroup 138 | wg.Add(4) 139 | var err error 140 | video.TableVideo = *data 141 | //插入Author,这里需要将视频的发布者和当前登录的用户传入,才能正确获得isFollow, 142 | //如果出现错误,不能直接返回失败,将默认值返回,保证稳定 143 | go func() { 144 | video.Author, err = videoService.GetUserByIdWithCurId(data.AuthorId, userId) 145 | if err != nil { 146 | log.Printf("方法videoService.GetUserByIdWithCurId(data.AuthorId, userId) 失败:%v", err) 147 | } else { 148 | log.Printf("方法videoService.GetUserByIdWithCurId(data.AuthorId, userId) 成功") 149 | } 150 | wg.Done() 151 | }() 152 | 153 | //插入点赞数量,同上所示,不将nil直接向上返回,数据没有就算了,给一个默认就行了 154 | go func() { 155 | video.FavoriteCount, err = videoService.FavouriteCount(data.Id) 156 | if err != nil { 157 | log.Printf("方法videoService.FavouriteCount(data.ID) 失败:%v", err) 158 | } else { 159 | log.Printf("方法videoService.FavouriteCount(data.ID) 成功") 160 | } 161 | wg.Done() 162 | }() 163 | 164 | //获取该视屏的评论数字 165 | go func() { 166 | video.CommentCount, err = videoService.CountFromVideoId(data.Id) 167 | if err != nil { 168 | log.Printf("方法videoService.CountFromVideoId(data.ID) 失败:%v", err) 169 | } else { 170 | log.Printf("方法videoService.CountFromVideoId(data.ID) 成功") 171 | } 172 | wg.Done() 173 | }() 174 | 175 | //获取当前用户是否点赞了该视频 176 | go func() { 177 | video.IsFavorite, err = videoService.IsFavourite(video.Id, userId) 178 | if err != nil { 179 | log.Printf("方法videoService.IsFavourit(video.Id, userId) 失败:%v", err) 180 | } else { 181 | log.Printf("方法videoService.IsFavourit(video.Id, userId) 成功") 182 | } 183 | wg.Done() 184 | }() 185 | 186 | wg.Wait() 187 | } 188 | 189 | // GetVideoIdList 190 | // 通过一个作者id,返回该用户发布的视频id切片数组 191 | func (videoService *VideoServiceImpl) GetVideoIdList(authorId int64) ([]int64, error) { 192 | //直接调用dao层方法获取id即可 193 | id, err := dao.GetVideoIdsByAuthorId(authorId) 194 | if err != nil { 195 | log.Printf("方法dao.GetVideoIdsByAuthorId(%v) 失败:%v", authorId, err) 196 | return nil, err 197 | } else { 198 | log.Printf("方法dao.GetVideoIdsByAuthorId(%v) 成功", authorId) 199 | } 200 | return id, nil 201 | } 202 | -------------------------------------------------------------------------------- /service/videoServiceImpl_test.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "TikTok/dao" 5 | "fmt" 6 | "testing" 7 | "time" 8 | ) 9 | 10 | func getVideoService2() VideoService { 11 | dao.Init() 12 | var userService UserServiceImpl 13 | var followService FollowServiceImp 14 | var videoService VideoServiceImpl 15 | var likeService LikeServiceImpl 16 | var commentService CommentServiceImpl 17 | userService.FollowService = &followService 18 | followService.UserService = &userService 19 | likeService.VideoService = &videoService 20 | commentService.UserService = &userService 21 | videoService.CommentService = &commentService 22 | videoService.LikeService = &likeService 23 | videoService.UserService = &userService 24 | return &videoService 25 | } 26 | 27 | func TestList(t *testing.T) { 28 | videoService := getVideoService2() 29 | list, err := videoService.List(3, 2) 30 | if err != nil { 31 | return 32 | } 33 | for _, video := range list { 34 | fmt.Println(video) 35 | } 36 | 37 | } 38 | 39 | func TestGetVideo(t *testing.T) { 40 | videoService := getVideoService2() 41 | video, err := videoService.GetVideo(1, 2) 42 | if err != nil { 43 | return 44 | } 45 | fmt.Println(video) 46 | } 47 | 48 | func TestFeed(t *testing.T) { 49 | videoService := getVideoService2() 50 | feed, t2, err := videoService.Feed(time.Now(), 2) 51 | if err != nil { 52 | return 53 | } 54 | for _, video := range feed { 55 | fmt.Println(video) 56 | } 57 | fmt.Println(t2) 58 | } 59 | -------------------------------------------------------------------------------- /service/videoSub.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "TikTok/dao" 5 | ) 6 | 7 | type VideoSub struct { 8 | } 9 | 10 | func (vs VideoSub) Send(comment dao.Comment) error { 11 | //TODO implement me 12 | panic("implement me") 13 | } 14 | 15 | func (vs VideoSub) CountFromVideoId(id int64) (int64, error) { 16 | return 4, nil 17 | } 18 | 19 | func (vs VideoSub) DelComment(id int64) error { 20 | //TODO implement me 21 | panic("implement me") 22 | } 23 | 24 | func (vs VideoSub) GetList(vedioId int64, userId int64) ([]CommentInfo, error) { 25 | //TODO implement me 26 | panic("implement me") 27 | } 28 | 29 | func (vs VideoSub) IsFollowing(userId int64, targetId int64) (bool, error) { 30 | //TODO implement me 31 | panic("implement me") 32 | } 33 | 34 | func (vs VideoSub) GetFollowerCnt(userId int64) (int64, error) { 35 | //TODO implement me 36 | panic("implement me") 37 | } 38 | 39 | func (vs VideoSub) GetFollowingCnt(userId int64) (int64, error) { 40 | //TODO implement me 41 | panic("implement me") 42 | } 43 | 44 | func (vs VideoSub) AddFollowRelation(userId int64, targetId int64) (bool, error) { 45 | //TODO implement me 46 | panic("implement me") 47 | } 48 | 49 | func (vs VideoSub) DeleteFollowRelation(userId int64, targetId int64) (bool, error) { 50 | //TODO implement me 51 | panic("implement me") 52 | } 53 | 54 | func (vs VideoSub) GetFollowing(userId int64) ([]User, error) { 55 | //TODO implement me 56 | panic("implement me") 57 | } 58 | 59 | func (vs VideoSub) GetFollowers(userId int64) ([]User, error) { 60 | //TODO implement me 61 | panic("implement me") 62 | } 63 | 64 | func (vs VideoSub) CheckCommentString() string { 65 | //TODO implement me 66 | panic("implement me") 67 | } 68 | 69 | func (vs VideoSub) IsFavourit(videoId int64, userId int64) (bool, error) { 70 | return true, nil 71 | } 72 | 73 | func (vs VideoSub) FavouriteCount(videoId int64) (int64, error) { 74 | return 3, nil 75 | } 76 | 77 | func (vs VideoSub) FavouriteAction(userId int64, videoId int64, action_type int32) error { 78 | //TODO implement me 79 | panic("implement me") 80 | } 81 | 82 | func (vs VideoSub) GetFavouriteList(userId int64) ([]Video, error) { 83 | //TODO implement me 84 | panic("implement me") 85 | } 86 | 87 | func (vs VideoSub) GetTableUserList() []dao.TableUser { 88 | //TODO implement me 89 | panic("implement me") 90 | } 91 | 92 | func (vs VideoSub) GetTableUserByUsername(name string) dao.TableUser { 93 | //TODO implement me 94 | panic("implement me") 95 | } 96 | 97 | func (vs VideoSub) GetTableUserById(id int64) dao.TableUser { 98 | //TODO implement me 99 | panic("implement me") 100 | } 101 | 102 | func (vs VideoSub) InsertTableUser(tableUser *dao.TableUser) bool { 103 | //TODO implement me 104 | panic("implement me") 105 | } 106 | 107 | func (vs VideoSub) GetUserById(id int64) (User, error) { 108 | //TODO implement me 109 | panic("implement me") 110 | } 111 | 112 | func (vs VideoSub) GetUserByIdWithCurId(id int64, curId int64) (User, error) { 113 | var user User 114 | return user, nil 115 | } 116 | -------------------------------------------------------------------------------- /util/sensitiveFilter.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "github.com/importcjj/sensitive" 5 | "log" 6 | ) 7 | 8 | var Filter *sensitive.Filter 9 | 10 | const WordDictPath = "./document/sensitiveDict.txt" 11 | 12 | func InitFilter() { 13 | Filter = sensitive.New() 14 | err := Filter.LoadWordDict(WordDictPath) 15 | if err != nil { 16 | log.Println("InitFilter Fail,Err=" + err.Error()) 17 | } 18 | } 19 | --------------------------------------------------------------------------------