├── .idea ├── .gitignore ├── Tiny-TikTok.iml ├── inspectionProfiles │ └── Project_Default.xml ├── modules.xml ├── sqldialects.xml └── vcs.xml ├── README.md ├── api_router ├── cmd │ └── main.go ├── configs │ ├── config.go │ └── config.yaml ├── discovery │ └── resolver.go ├── go.mod ├── go.sum ├── internal │ ├── handler │ │ ├── comment.go │ │ ├── erros.go │ │ ├── favorite.go │ │ ├── follow.go │ │ ├── message.go │ │ ├── user.go │ │ └── video.go │ └── service │ │ ├── pb │ │ ├── socialService.proto │ │ ├── userService.proto │ │ └── videoService.proto │ │ ├── socialService.pb.go │ │ ├── socialService_grpc.pb.go │ │ ├── userService.pb.go │ │ ├── userService_grpc.pb.go │ │ ├── videoService.pb.go │ │ └── videoService_grpc.pb.go ├── logs │ ├── 2023-08-22.log │ ├── 2023-08-22.log1800 │ ├── 2023-08-27.log │ ├── 2023-08-27.log1500 │ ├── 2023-08-27.log1800 │ ├── 2023-08-28.log │ ├── 2023-08-28.log1200 │ ├── 2023-09-04.log │ ├── 2023-09-04.log0900 │ └── 2023-09-04.log1500 ├── pkg │ ├── auth │ │ └── jwt.go │ ├── logger │ │ ├── log.go │ │ └── log_test.go │ ├── res │ │ └── response.go │ └── wrapper │ │ └── hystrix.go └── router │ ├── middleware │ ├── error.go │ ├── jwt.go │ └── serve.go │ └── router.go ├── docs ├── README.md ├── image │ ├── 总体设计图.png │ ├── 技术架构图.png │ ├── 数据库设计图.png │ ├── 获奖证书.png │ └── 项目获奖证书.png └── mysql │ └── tables.sql ├── social_service ├── cmd │ └── main.go ├── config │ ├── config.go │ ├── config.yaml │ └── config_test.go ├── discovery │ └── autoRegister.go ├── go.mod ├── go.sum ├── internal │ ├── handler │ │ └── social.go │ ├── model │ │ ├── follow.go │ │ ├── follow_test.go │ │ ├── init.go │ │ ├── init_test.go │ │ ├── message.go │ │ ├── message_test.go │ │ └── migration.go │ └── service │ │ ├── pb │ │ └── socialService.proto │ │ ├── socialService.pb.go │ │ └── socialService_grpc.pb.go └── pkg │ └── cache │ ├── redis.go │ ├── redisAutoSync.go │ └── redis_test.go ├── user_service ├── cmd │ └── main.go ├── config │ ├── config.go │ ├── config.yaml │ └── config_test.go ├── discovery │ └── autoRegister.go ├── go.mod ├── go.sum ├── internal │ ├── handler │ │ └── user.go │ ├── model │ │ ├── init.go │ │ ├── init_test.go │ │ ├── migration.go │ │ ├── user.go │ │ └── user_test.go │ └── service │ │ ├── pb │ │ └── userService.proto │ │ ├── userService.pb.go │ │ └── userService_grpc.pb.go └── pkg │ ├── cache │ ├── redis.go │ └── redis_test.go │ └── encryption │ └── encrypt.go ├── utils ├── etcd │ ├── discovery.go │ └── register.go ├── exception │ ├── statusCode.go │ └── statusMsg.go ├── go.mod ├── go.sum └── snowFlake │ ├── snow.go │ └── snow_test.go └── video_service ├── cmd └── main.go ├── config ├── config.go └── config.yaml ├── discovery └── autoRegister.go ├── go.mod ├── go.sum ├── internal ├── handler │ ├── comment.go │ ├── favorite.go │ ├── video.go │ └── video_test.go ├── model │ ├── comment.go │ ├── favorite.go │ ├── init.go │ ├── init_test.go │ ├── migration.go │ ├── video.go │ └── video_test.go └── service │ ├── pb │ └── videoService.proto │ ├── videoService.pb.go │ └── videoService_grpc.pb.go ├── pkg ├── cache │ ├── redis.go │ └── redis_test.go ├── cut │ ├── ffmpeg.go │ └── ffmpeg_test.go └── mq │ ├── rabbitmq.go │ └── rabbitmq_test.go └── third_party ├── oss.go └── oss_test.go /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Editor-based HTTP Client requests 5 | /httpRequests/ 6 | # Datasource local storage ignored files 7 | /dataSources/ 8 | /dataSources.local.xml 9 | -------------------------------------------------------------------------------- /.idea/Tiny-TikTok.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 18 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/sqldialects.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

Tiny-TikTok

2 | 3 |

4 | 5 | Static Badge 6 | 7 | 8 | Static Badge 9 | 10 | 11 | Static Badge 12 | 13 | 14 | Static Badge 15 | 16 | 17 | Static Badge 18 | 19 |

20 | 21 | ## 1 项目介绍 22 | 23 | 简易抖音项目后端实现,使用 **Gin** 作为web框架,**MySQL** 作为数据存储并使用 Gorm 操作数据库。整个项目分为用户服务、视频服务、社交服务,使用 **Etcd** 作为注册中心,**Grpc** 进行服务之间的通信。 采用 **Redis** 作为缓存,提高读写效率;使用消息中间件 **RabbitMQ**,达到上游服务和下游服务的解耦。 24 | 25 | ![image](https://github.com/marinezz/Tiny-TikTok/blob/main/docs/image/%E9%A1%B9%E7%9B%AE%E8%8E%B7%E5%A5%96%E8%AF%81%E4%B9%A6.png?raw=true) 26 | 27 | **简单写一个总结:** 28 | 29 | 青训营结束也过去了一段时间,这次获得了**三等奖**,还算比较满意的结果(自己离还有不少差距,心服口服)。本来两个人完成的这个项目,自己也不是主力golang选手,一路跌跌撞撞,也将近用了完整的三四周时间。 30 | 31 | 收获最大的有两个点。一是只有思维的摩擦,才会有创造,比如缓存redis的设计,一开始只想放在后端和数据库之间,简单降低数据库的压力,仅此而已。但是后面在思考的时候,面对不同的场景,采用不同的数据结构,以及如何保证数据的一致性等等。第二个就是实践,只有真正去写代码了,才能意识到很多问题,理论上能滔滔不绝的说出来,但是实践出来还是有很大的差距,一些细节根本在想的时候抓不住。 32 | 33 | 最后总结一下自己的优势与不足,也算给后续想参加青训的同志们的一些简易吧。本项目对比其余项目,可能(自我感觉)优于其它人的,就是测试比较完整。对于redis、MQ的使用,这些大家基本都会去使用,但是使用的时候一定要往深度去思考,比如redis中数据的一致性(这个问题在最后答辩的时候都会问,有的人会考虑掉或者做得不够好)。然后就是广度,这是我们明显缺失的,对于前面的我最佩服的几个项目,从技术选型,到团队合作(一些团队合作工作),项目部署(自动化部署),服务监控,日志等都几乎完整的实现。而我们部署靠手动,监控只统一处理了日志,还有很大很大的差距。还有技术选型有一点点小问题,因为我参加过上一届青训,但是那次没项目就纯看视频课(没啥用),所以这次的课没有咋看,这次是教了Hertz和kitex的,所以可能会给出一种没好好听课的感觉吧~ 34 | 35 | **如果对你有帮助,右上角 Satr 走起 !!!** 36 | 37 | ### 1.1 目录介绍 38 | 39 | **第一部分:项目简介** 40 | 41 | **第二部分:项目概览。包括整个项目的技术选择、整体设计、目录结构设计与数据库设计** 42 | 43 | **第三部分:项目详细设计。包括对项目的思考以及一些关键点的设计思路** 44 | 45 | **第四部分:项目测试** 46 | 47 | **第五部分:启动说明。拿到整个项目,应该如何让它跑起来** 48 | 49 |
50 | 51 | ### 1.2 分支介绍 52 | 53 | 本项目的分支是项目的几个迭代版本,可以根据新老版本查看,由简到难: 54 | 55 | * **main分支**:最新版本,仍在迭代中。修复一些小bug,加入消息队列RabbitMQ 56 | 57 | * **1.0分支**:最老的分支,项目完整的启动,最简单的版本 58 | 59 | * **2.0分支**:在1.0的基础上优化缓存,索引。 60 | 61 |
62 | 63 | ## 2 项目概览 64 | 65 | ### 2.1 技术选型与设计 66 | 67 | ![image](https://github.com/marinezz/Tiny-TikTok/blob/main/docs/image/技术架构图.png) 68 | 69 | * **Gin**:Web框架。高性能、轻量级、简洁,被广泛用于构建RESTful API、网站和其它HTTP服务 70 | 71 | * **JWT**:用于身份的验证和授权,具有跨平台、无状态的优点,不需要再会话中保存任何信息,减轻服务器的负担 72 | 73 | * **Hystrix**:服务熔断,防止由于单个服务的故障导致整个系统的崩溃 74 | 75 | * **Etcd + grpc**:etcd实现服务注册和服务发现,grpc负责通信,构建健壮的分布式系统 76 | 77 | * **OSS**:对象存储服务器,用于存储和管理非结构化数据,比如图片、视频等 78 | 79 | * **FFmpeg**:多媒体开源工具,本项目用于上传视频时封面的截取 80 | 81 | * **Gorm**:ORM库,用于操作数据库 82 | 83 | * **MySQL**:关系型数据库,用于存储结构化数据 84 | 85 | * **Redis**:键值存储数据库,以内存作为数据存储介质,提供高性能读写 86 | 87 | * **RabbitMQ**:消息中间件,用于程序之间传递消息,支持消息的发布和订阅,支持消息异步传递,实现解耦和异步处理 88 | 89 |
90 | 91 | ### 2.2 总体设计 92 | 93 | ![image](https://github.com/marinezz/Tiny-TikTok/blob/main/docs/image/%E6%80%BB%E4%BD%93%E8%AE%BE%E8%AE%A1%E5%9B%BE.png) 94 | 95 | * 请求到达服务器前会对token进行校验 96 | * 通过Api_Router对外暴露接口,进入服务,网关微服务对其它服务进行服务熔断和服务限流 97 | * 各个服务先注册进入ETCD,api_router对各个服务进行调用,组装信息返回给服务端 98 | * api_router通过gprc实现服务之间的通讯 99 | * 服务操作数据库,将信息返回给上一层 100 | 101 |
102 | 103 | ### 2.3 项目结构设计 104 | 105 | * **总目录结构** 106 | 107 | ```bash 108 | ├─api_router # 路由网关 109 | ├─docs # 项目文档 110 | ├─social_service # 社交服务 111 | ├─user_service # 用户服务 112 | ├─utils # 工具函数包 113 | └─video_service # 视频服务 114 | ``` 115 | 116 |
117 | 118 | * **路由网关** 119 | 120 | ```bash 121 | ├─api_router 122 | │ ├─cmd 123 | │ ├─config # 项目配置文件 124 | │ ├─discovery # 服务注册与发现 125 | │ ├─internal 126 | │ │ ├─handler 127 | │ │ └─service 128 | │ │ └─pb 129 | │ ├─pkg 130 | │ │ ├─auth 131 | │ │ └─res 132 | │ └─router # 路由和中间件 133 | │ └─middleware 134 | ``` 135 | 136 | ​ **/cmd**:一个项目可以有很多个组件,吧main函数所在文件夹同一放在/cmd目录下 137 | 138 | ​ **/internal**:存放私有应用代码。handler类似三层架构中的控制层,service类似服务层,路由不用操作数据库,所以没有持久层 139 | 140 | ​ **/pkg**:存放可以被外部使用的代码库。auth中存放token鉴权,res存放对服务端的统一返回 141 | 142 |
143 | 144 | * **具体服务** 145 | 146 | ```bash 147 | ├─user_service 148 | │ ├─cmd 149 | │ ├─config 150 | │ ├─discovery 151 | │ ├─internal 152 | │ │ ├─handler 153 | │ │ ├─model 154 | │ │ └─service # 持久化层 155 | │ │ └─pb 156 | │ └─pkg 157 | │ └─encryption # 密码加密 158 | ``` 159 | 160 |
161 | 162 | * **工具函数包** 163 | 164 | ```bash 165 | ├─utils 166 | │ ├─etcd # etcd服务注册与发现组件 167 | │ ├─exception 168 | │ └─snowFlake # 雪花算法生成ID 169 | ``` 170 | 171 |
172 | 173 | ### 2.4 数据库设计 174 | 175 | ![image](https://github.com/marinezz/Tiny-TikTok/blob/main/docs/image/数据库设计图.png) 176 | 177 | **用户表**:用于存储用户名称、密码、头像、背景、用户简介信息,以由雪花算法生成的分布式id作为主键(其余表的ID同理),密码由bcrypt函数进行加密。 178 | 179 | **视频表**:用于存储视频的作者、标题、封面路径、视频路径、获赞数量以及评论数量,以视频作者的id关联用户表。 180 | 181 | **评论表**:用于存储视频的评论信息、评论创建时间、评论状态,通过用户id以及视频id关联用户表和视频表,通过评论状态作为软删除判断当前评论是否存在。 182 | 183 | **消息表**:用于存放用户发送的消息以及消息的创建时间,通过用户id关联用户表,记录消息的发生者和消息的接收者 184 | 185 | **关注表**:用户存放用户的关注信息,通过用户id关联用户表获取关注者和被关注者 186 | 187 | **点赞表**:用于存放视频的点赞信息,通过用户id关联用户表获取点赞的用户,视频id关联视频表获取被点赞的视频 188 | 189 |
190 | 191 | ## 3 详细设计 192 | 193 | 本部分包含:**认证鉴权**、**分布式唯一ID**、**密码加密**、**数据库操作**、**视频上传**、**日志打印**、**服务熔断**、**统一错误处理**、**高性能读写**、**异步解耦**的设计 194 | 195 | 详细设计参考答辩文档第三部分:[说明文档](https://v1rwxew1bdp.feishu.cn/docx/ATJPdobcOouDDLxVHsycpANMnig?from=from_copylink) 196 | 197 |
198 | 199 | ## 4 测试 200 | 201 | 本部分包含:单元测试、接口测试(功能测试)、性能测试(压力测试) 202 | 203 | 测试详情查看测试文档:[测试报告](https://v1rwxew1bdp.feishu.cn/docx/F1aQdSY6AoIzeLx6B8tcVQhgnud?from=from_copylink) 204 | 205 |
206 | 207 | ## 5 启动说明 208 | 209 | ### 5.1 启动之前 210 | 211 | 项目启动之前,了解各个组件的版本,一直觉得版本是一个很大的问题,先搞清楚每一个组件版本,后续才能有条不紊的进行 212 | 213 | | 名称 | 版本 | 作用 | 214 | | :----------: | :---: | :------------------: | 215 | | **Go** | 1.19 | | 216 | | **MySQL** | 8.0 | 关系型数据库 | 217 | | **Etcd** | 3.5.1 | 注册中心 | 218 | | **Redis** | 5.0.7 | 缓存 | 219 | | **RabbitMQ** | 3.7.4 | 消息中间件,异步解耦 | 220 | | **FFmpeg** | 6.0 | 视频剪切为封面 | 221 | | **Protoc** | 3.15 | 生成pb文件 | 222 | 223 | 确保每一个组件安装成功,并将ffmpeg和protoc配置进入环境变量 224 | 225 | 226 | 227 | ### 5.2 拉取项目 228 | 229 | 保证安装了git,直接克隆到本地 230 | 231 | ```bash 232 | git clone https://github.com/marinezz/Tiny-TikTok.git 233 | ``` 234 | 235 |
236 | 237 | ### 5.3 拉取依赖 238 | 239 | 将所有需要的依赖拉取到本地,进入每一个文件,拉取依赖。以api_router为例(其余文件操作相同): 240 | 241 | ```bash 242 | cd api_router/ 243 | go mod tidy 244 | ``` 245 | 246 |
247 | 248 | ### 5.4 启动项目 249 | 250 | **第一步**:启动etcd。确保etcd注册中心启动成功。如果不放心可以下载ectdkeeper(参考网络),后续也可以参看服务是否注册进入注册中心 251 | 252 | **第二步**:建立数据库。建立名为tiny_tiktok的数据库,不用建表,启动时gorm会自动建表 253 | 254 | **第三步**:修改配置文件。参考配置文件example,根据自己的实际情况修改配置文件 255 | 256 | **第四步**:正式启动项目。到每个文件的cmd文件中启动项目,或者使用命令 `go run main.go` 257 | 258 | **第五步**:项目启动成功 259 | 260 |
261 | 262 | ## 6 总结与展望 263 | 264 | 项目的从0到有,经过了一个月的时间,在开发过程中遇到了很多问题,也解决很多问题,得到了很多收获。在不断的实际操作中,才能有新的收获,新的想法。截止青训营的结束,现在还是有很多想法的存在,我们也会把自己的想法付诸实现,完完整整做出自己满意的项目。 265 | 266 | 267 | 268 | 我们还会继续ing..... 269 | 270 | 271 | 272 | **如果对你有帮助的话,希望您不要吝啬你得star哦!!** 273 | 274 | ![image](https://github.com/marinezz/Tiny-TikTok/blob/main/docs/image/%E8%8E%B7%E5%A5%96%E8%AF%81%E4%B9%A6.png?raw=true) 275 | -------------------------------------------------------------------------------- /api_router/cmd/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | config "api_router/configs" 5 | "api_router/discovery" 6 | "api_router/pkg/logger" 7 | "api_router/router" 8 | "github.com/spf13/viper" 9 | "net/http" 10 | "time" 11 | ) 12 | 13 | func main() { 14 | config.InitConfig() 15 | resolver := discovery.Resolver() 16 | r := router.InitRouter(resolver) 17 | server := &http.Server{ 18 | Addr: viper.GetString("server.port"), 19 | Handler: r, 20 | ReadTimeout: 10 * time.Second, 21 | WriteTimeout: 10 * time.Second, 22 | MaxHeaderBytes: 1 << 20, 23 | } 24 | 25 | err := server.ListenAndServe() 26 | if err != nil { 27 | logger.Log.Fatal("启动失败...") 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /api_router/configs/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "github.com/spf13/viper" 5 | "path" 6 | "runtime" 7 | "strings" 8 | ) 9 | 10 | // InitConfig 读取配置文件 11 | func InitConfig() { 12 | _, filePath, _, _ := runtime.Caller(0) 13 | 14 | currentDir := path.Dir(filePath) 15 | 16 | viper.SetConfigName("config") 17 | viper.SetConfigType("yaml") 18 | viper.AddConfigPath(currentDir) 19 | 20 | if err := viper.ReadInConfig(); err != nil { 21 | panic(err) 22 | } 23 | 24 | } 25 | 26 | // DbDnsInit 拼接链接数据库的DNS 27 | func DbDnsInit() string { 28 | host := viper.GetString("mysql.host") 29 | port := viper.GetString("mysql.port") 30 | username := viper.GetString("mysql.username") 31 | password := viper.GetString("mysql.password") 32 | database := viper.GetString("mysql.database") 33 | 34 | InitConfig() 35 | dns := strings.Join([]string{username, ":", password, "@tcp(", host, ":", port, ")/", database, "?charset=utf8&parseTime=True&loc=Local"}, "") 36 | 37 | return dns 38 | } 39 | -------------------------------------------------------------------------------- /api_router/configs/config.yaml: -------------------------------------------------------------------------------- 1 | server: 2 | name: api_router # 服务名称 3 | port: :10000 4 | address: 127.0.0.1:10000 # 服务地址 5 | jwtSecret: 123456 # jwt密码 6 | 7 | 8 | mysql: 9 | driver: mysql 10 | host: 127.0.0.1 11 | port: 3306 12 | username: root 13 | password: 123456 14 | database: tiny_tiktok 15 | 16 | 17 | etcd: 18 | address: 127.0.0.1:2379 -------------------------------------------------------------------------------- /api_router/discovery/resolver.go: -------------------------------------------------------------------------------- 1 | // 服务发现,发现所有的服务,返回一个map 2 | 3 | package discovery 4 | 5 | import ( 6 | "api_router/internal/service" 7 | "api_router/pkg/logger" 8 | "api_router/pkg/wrapper" 9 | "github.com/spf13/viper" 10 | "google.golang.org/grpc" 11 | "google.golang.org/grpc/credentials/insecure" 12 | "utils/etcd" 13 | ) 14 | 15 | func Resolver() map[string]interface{} { 16 | serveInstance := make(map[string]interface{}) 17 | 18 | etcdAddress := viper.GetString("etcd.address") 19 | serviceDiscovery, err := etcd.NewServiceDiscovery([]string{etcdAddress}) 20 | if err != nil { 21 | logger.Log.Fatal(err) 22 | } 23 | defer serviceDiscovery.Close() 24 | 25 | // 获取用户服务实例 26 | err = serviceDiscovery.ServiceDiscovery("user_service") 27 | if err != nil { 28 | logger.Log.Fatal(err) 29 | } 30 | userServiceAddr, _ := serviceDiscovery.GetService("user_service") 31 | userConn, err := grpc.Dial(userServiceAddr, grpc.WithTransportCredentials(insecure.NewCredentials())) 32 | if err != nil { 33 | logger.Log.Fatal(err) 34 | } 35 | userClient := service.NewUserServiceClient(userConn) 36 | logger.Log.Info("获取用户服务实例--成功--") 37 | serveInstance["user_service"] = userClient 38 | 39 | // 获取视频服务实例 40 | err = serviceDiscovery.ServiceDiscovery("video_service") 41 | if err != nil { 42 | logger.Log.Fatal(err) 43 | } 44 | videoServiceAddr, _ := serviceDiscovery.GetService("video_service") 45 | videoConn, err := grpc.Dial(videoServiceAddr, grpc.WithTransportCredentials(insecure.NewCredentials())) 46 | if err != nil { 47 | logger.Log.Fatal(err) 48 | } 49 | 50 | videoClient := service.NewVideoServiceClient(videoConn) 51 | logger.Log.Info("获取视频服务实例--成功--") 52 | serveInstance["video_service"] = videoClient 53 | 54 | // 获取社交服务实例 55 | err = serviceDiscovery.ServiceDiscovery("social_service") 56 | if err != nil { 57 | logger.Log.Fatal(err) 58 | } 59 | socialServiceAddr, _ := serviceDiscovery.GetService("social_service") 60 | socialConn, err := grpc.Dial(socialServiceAddr, grpc.WithTransportCredentials(insecure.NewCredentials())) 61 | if err != nil { 62 | logger.Log.Fatal(err) 63 | } 64 | 65 | socialClient := service.NewSocialServiceClient(socialConn) 66 | logger.Log.Info("获取社交服务实例--成功--") 67 | serveInstance["social_service"] = socialClient 68 | 69 | wrapper.NewWrapper("user_service") 70 | wrapper.NewWrapper("video_service") 71 | wrapper.NewWrapper("social_service") 72 | 73 | return serveInstance 74 | } 75 | -------------------------------------------------------------------------------- /api_router/go.mod: -------------------------------------------------------------------------------- 1 | module api_router 2 | 3 | go 1.19 4 | 5 | require ( 6 | github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5 7 | github.com/dgrijalva/jwt-go v3.2.0+incompatible 8 | github.com/gin-gonic/gin v1.9.1 9 | github.com/lestrrat-go/file-rotatelogs v2.4.0+incompatible 10 | github.com/pkg/errors v0.9.1 11 | github.com/rabbitmq/amqp091-go v1.8.1 12 | github.com/sirupsen/logrus v1.9.3 13 | github.com/spf13/viper v1.16.0 14 | google.golang.org/grpc v1.55.0 15 | google.golang.org/protobuf v1.30.0 16 | utils v0.0.0 17 | ) 18 | 19 | replace utils => ../utils 20 | 21 | require ( 22 | github.com/bytedance/sonic v1.9.1 // indirect 23 | github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect 24 | github.com/coreos/go-semver v0.3.0 // indirect 25 | github.com/coreos/go-systemd/v22 v22.3.2 // indirect 26 | github.com/fsnotify/fsnotify v1.6.0 // indirect 27 | github.com/gabriel-vasile/mimetype v1.4.2 // indirect 28 | github.com/gin-contrib/sse v0.1.0 // indirect 29 | github.com/go-playground/locales v0.14.1 // indirect 30 | github.com/go-playground/universal-translator v0.18.1 // indirect 31 | github.com/go-playground/validator/v10 v10.14.0 // indirect 32 | github.com/goccy/go-json v0.10.2 // indirect 33 | github.com/gogo/protobuf v1.3.2 // indirect 34 | github.com/golang/protobuf v1.5.3 // indirect 35 | github.com/hashicorp/hcl v1.0.0 // indirect 36 | github.com/jonboulle/clockwork v0.4.0 // indirect 37 | github.com/json-iterator/go v1.1.12 // indirect 38 | github.com/klauspost/cpuid/v2 v2.2.4 // indirect 39 | github.com/leodido/go-urn v1.2.4 // indirect 40 | github.com/lestrrat-go/strftime v1.0.6 // indirect 41 | github.com/magiconair/properties v1.8.7 // indirect 42 | github.com/mattn/go-isatty v0.0.19 // indirect 43 | github.com/mitchellh/mapstructure v1.5.0 // indirect 44 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 45 | github.com/modern-go/reflect2 v1.0.2 // indirect 46 | github.com/pelletier/go-toml/v2 v2.0.8 // indirect 47 | github.com/smartystreets/goconvey v1.8.1 // indirect 48 | github.com/spf13/afero v1.9.5 // indirect 49 | github.com/spf13/cast v1.5.1 // indirect 50 | github.com/spf13/jwalterweatherman v1.1.0 // indirect 51 | github.com/spf13/pflag v1.0.5 // indirect 52 | github.com/subosito/gotenv v1.4.2 // indirect 53 | github.com/twitchyliquid64/golang-asm v0.15.1 // indirect 54 | github.com/ugorji/go/codec v1.2.11 // indirect 55 | go.etcd.io/etcd/api/v3 v3.5.9 // indirect 56 | go.etcd.io/etcd/client/pkg/v3 v3.5.9 // indirect 57 | go.etcd.io/etcd/client/v3 v3.5.9 // indirect 58 | go.uber.org/atomic v1.9.0 // indirect 59 | go.uber.org/multierr v1.8.0 // indirect 60 | go.uber.org/zap v1.21.0 // indirect 61 | golang.org/x/arch v0.3.0 // indirect 62 | golang.org/x/crypto v0.9.0 // indirect 63 | golang.org/x/net v0.10.0 // indirect 64 | golang.org/x/sys v0.8.0 // indirect 65 | golang.org/x/text v0.9.0 // indirect 66 | google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect 67 | gopkg.in/ini.v1 v1.67.0 // indirect 68 | gopkg.in/yaml.v3 v3.0.1 // indirect 69 | ) 70 | -------------------------------------------------------------------------------- /api_router/internal/handler/comment.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "api_router/internal/service" 5 | "api_router/pkg/res" 6 | "context" 7 | "github.com/gin-gonic/gin" 8 | "net/http" 9 | "strconv" 10 | "utils/exception" 11 | ) 12 | 13 | // CommentAction 评论操作 14 | func CommentAction(ctx *gin.Context) { 15 | var commentActionReq service.CommentActionRequest 16 | 17 | userId, _ := ctx.Get("user_id") 18 | commentActionReq.UserId, _ = userId.(int64) 19 | 20 | videoId := ctx.PostForm("video_id") 21 | if videoId == "" { 22 | videoId = ctx.Query("video_id") 23 | } 24 | commentActionReq.VideoId, _ = strconv.ParseInt(videoId, 10, 64) 25 | 26 | actionType := ctx.PostForm("action_type") 27 | if actionType == "" { 28 | actionType = ctx.Query("action_type") 29 | } 30 | actionTypeValue, _ := strconv.Atoi(actionType) 31 | commentActionReq.ActionType = int64(actionTypeValue) 32 | 33 | // 评论操作 34 | if commentActionReq.ActionType == 1 { 35 | commentText := ctx.PostForm("comment_text") 36 | if commentText == "" { 37 | commentText = ctx.Query("comment_text") 38 | } 39 | commentActionReq.CommentText = commentText 40 | } else if commentActionReq.ActionType == 2 { 41 | commentId := ctx.PostForm("comment_id") 42 | if commentId == "" { 43 | commentId = ctx.Query("comment_id") 44 | } 45 | commentActionReq.CommentId, _ = strconv.ParseInt(commentId, 10, 64) 46 | } else { 47 | r := res.FavoriteActionResponse{ 48 | StatusCode: exception.ErrOperate, 49 | StatusMsg: exception.GetMsg(exception.ErrOperate), 50 | } 51 | 52 | ctx.JSON(http.StatusOK, r) 53 | return 54 | } 55 | 56 | videoServiceClient := ctx.Keys["video_service"].(service.VideoServiceClient) 57 | videoServiceResp, err := videoServiceClient.CommentAction(context.Background(), &commentActionReq) 58 | if err != nil { 59 | PanicIfCommentError(err) 60 | } 61 | 62 | if actionTypeValue == 1 { 63 | // 构建用户信息 64 | userIds := []int64{userId.(int64)} 65 | userInfos := GetUserInfo(userIds, ctx) 66 | 67 | r := res.CommentActionResponse{ 68 | StatusCode: videoServiceResp.StatusCode, 69 | StatusMsg: videoServiceResp.StatusMsg, 70 | Comment: BuildComment(videoServiceResp.Comment, userInfos[0]), 71 | } 72 | 73 | ctx.JSON(http.StatusOK, r) 74 | } 75 | // 如果是删除评论的操作 76 | if actionTypeValue == 2 { 77 | r := res.CommentDeleteResponse{ 78 | StatusCode: videoServiceResp.StatusCode, 79 | StatusMsg: videoServiceResp.StatusMsg, 80 | } 81 | 82 | ctx.JSON(http.StatusOK, r) 83 | } 84 | } 85 | 86 | func CommentList(ctx *gin.Context) { 87 | var commentListReq service.CommentListRequest 88 | 89 | videoIdStr := ctx.Query("video_id") 90 | videoId, _ := strconv.ParseInt(videoIdStr, 10, 64) 91 | 92 | commentListReq.VideoId = videoId 93 | 94 | videoServiceClient := ctx.Keys["video_service"].(service.VideoServiceClient) 95 | commentListResp, err := videoServiceClient.CommentList(context.Background(), &commentListReq) 96 | if err != nil { 97 | PanicIfCommentError(err) 98 | } 99 | 100 | // 找到所有的用户Id 101 | var userIds []int64 102 | for _, comment := range commentListResp.CommentList { 103 | userIds = append(userIds, comment.UserId) 104 | } 105 | 106 | userInfos := GetUserInfo(userIds, ctx) 107 | 108 | commentList := BuildCommentList(commentListResp.CommentList, userInfos) 109 | 110 | r := res.CommentListResponse{ 111 | StatusCode: commentListResp.StatusCode, 112 | StatusMsg: commentListResp.StatusMsg, 113 | Comments: commentList, 114 | } 115 | 116 | ctx.JSON(http.StatusOK, r) 117 | } 118 | 119 | func BuildComment(comment *service.Comment, userInfo res.User) res.Comment { 120 | 121 | return res.Comment{ 122 | Id: comment.Id, 123 | User: userInfo, 124 | Content: comment.Content, 125 | CreateDate: comment.CreateDate, 126 | } 127 | } 128 | 129 | func BuildCommentList(comments []*service.Comment, userInfos []res.User) []res.Comment { 130 | var commentList []res.Comment 131 | 132 | for i, comment := range comments { 133 | commentList = append(commentList, res.Comment{ 134 | Id: comment.Id, 135 | User: userInfos[i], 136 | Content: comment.Content, 137 | CreateDate: comment.CreateDate, 138 | }) 139 | } 140 | 141 | return commentList 142 | } 143 | -------------------------------------------------------------------------------- /api_router/internal/handler/erros.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "api_router/pkg/logger" 5 | "github.com/pkg/errors" 6 | ) 7 | 8 | // PanicIfVideoError 视频错误处理 9 | func PanicIfVideoError(err error) { 10 | if err != nil { 11 | err = errors.New("videoService--error--" + err.Error()) 12 | logger.Log.Info(err) 13 | panic(err) 14 | } 15 | } 16 | 17 | // PanicIfUserError 用户错误处理 18 | func PanicIfUserError(err error) { 19 | if err != nil { 20 | err = errors.New("UserService--error" + err.Error()) 21 | logger.Log.Info(err) 22 | panic(err) 23 | } 24 | } 25 | 26 | // PanicIfMessageError 消息错误处理 27 | func PanicIfMessageError(err error) { 28 | if err != nil { 29 | err = errors.New("MessageService--error" + err.Error()) 30 | logger.Log.Info(err) 31 | panic(err) 32 | } 33 | } 34 | 35 | // PanicIfFollowError 关注错误处理 36 | func PanicIfFollowError(err error) { 37 | if err != nil { 38 | err = errors.New("FollowService--error" + err.Error()) 39 | logger.Log.Info(err) 40 | panic(err) 41 | } 42 | } 43 | 44 | // PanicIfFavoriteError 喜欢错误处理 45 | func PanicIfFavoriteError(err error) { 46 | if err != nil { 47 | err = errors.New("FavoriteService--error" + err.Error()) 48 | logger.Log.Info(err) 49 | panic(err) 50 | } 51 | } 52 | 53 | // PanicIfCommentError 评论错误处理 54 | func PanicIfCommentError(err error) { 55 | if err != nil { 56 | err = errors.New("CommentService--error" + err.Error()) 57 | logger.Log.Info(err) 58 | panic(err) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /api_router/internal/handler/favorite.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "api_router/internal/service" 5 | "api_router/pkg/res" 6 | "context" 7 | "github.com/gin-gonic/gin" 8 | "net/http" 9 | "strconv" 10 | "utils/exception" 11 | ) 12 | 13 | // FavoriteAction 喜欢操作 14 | func FavoriteAction(ctx *gin.Context) { 15 | var favoriteActionReq service.FavoriteActionRequest 16 | 17 | userId, _ := ctx.Get("user_id") 18 | favoriteActionReq.UserId, _ = userId.(int64) 19 | // string转int64 20 | videoId := ctx.PostForm("video_id") 21 | if videoId == "" { 22 | videoId = ctx.Query("video_id") 23 | } 24 | favoriteActionReq.VideoId, _ = strconv.ParseInt(videoId, 10, 64) 25 | 26 | actionType := ctx.PostForm("action_type") 27 | if actionType == "" { 28 | actionType = ctx.Query("action_type") 29 | } 30 | actionTypeValue, _ := strconv.Atoi(actionType) 31 | 32 | // 异常操作 33 | if actionTypeValue == 1 || actionTypeValue == 2 { 34 | favoriteActionReq.ActionType = int64(actionTypeValue) 35 | 36 | videoServiceClient := ctx.Keys["video_service"].(service.VideoServiceClient) 37 | videoServiceResp, err := videoServiceClient.FavoriteAction(context.Background(), &favoriteActionReq) 38 | if err != nil { 39 | PanicIfFavoriteError(err) 40 | } 41 | 42 | r := res.FavoriteActionResponse{ 43 | StatusCode: videoServiceResp.StatusCode, 44 | StatusMsg: videoServiceResp.StatusMsg, 45 | } 46 | 47 | ctx.JSON(http.StatusOK, r) 48 | } else { 49 | r := res.FavoriteActionResponse{ 50 | StatusCode: exception.ErrOperate, 51 | StatusMsg: exception.GetMsg(exception.ErrOperate), 52 | } 53 | 54 | ctx.JSON(http.StatusOK, r) 55 | } 56 | } 57 | 58 | func FavoriteList(ctx *gin.Context) { 59 | var favoriteListReq service.FavoriteListRequest 60 | 61 | userIdStr := ctx.Query("user_id") 62 | userId, _ := strconv.ParseInt(userIdStr, 10, 64) 63 | 64 | favoriteListReq.UserId = userId 65 | 66 | videoServiceClient := ctx.Keys["video_service"].(service.VideoServiceClient) 67 | favoriteListResp, err := videoServiceClient.FavoriteList(context.Background(), &favoriteListReq) 68 | if err != nil { 69 | PanicIfFavoriteError(err) 70 | } 71 | 72 | // 找到所有的用户Id 73 | var userIds []int64 74 | for _, video := range favoriteListResp.VideoList { 75 | userIds = append(userIds, video.AuthId) 76 | } 77 | 78 | // 找到所有的用户信息 79 | userInfos := GetUserInfo(userIds, ctx) 80 | 81 | list := BuildVideoList(favoriteListResp.VideoList, userInfos) 82 | 83 | r := res.VideoListResponse{ 84 | StatusCode: favoriteListResp.StatusCode, 85 | StatusMsg: favoriteListResp.StatusMsg, 86 | VideoList: list, 87 | } 88 | 89 | ctx.JSON(http.StatusOK, r) 90 | } 91 | -------------------------------------------------------------------------------- /api_router/internal/handler/follow.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "api_router/internal/service" 5 | "api_router/pkg/res" 6 | "context" 7 | "github.com/gin-gonic/gin" 8 | "net/http" 9 | "strconv" 10 | "utils/exception" 11 | ) 12 | 13 | type Follow struct { 14 | IsFollow bool 15 | FollowCount int64 16 | FollowerCount int64 17 | } 18 | 19 | func FollowAction(ctx *gin.Context) { 20 | var followAction service.FollowRequest 21 | userId, _ := ctx.Get("user_id") 22 | followAction.UserId, _ = userId.(int64) 23 | toUserId := ctx.Query("to_user_id") 24 | followAction.ToUserId, _ = strconv.ParseInt(toUserId, 10, 64) 25 | actionType := ctx.Query("action_type") 26 | actionTypeInt64, _ := strconv.ParseInt(actionType, 10, 32) 27 | followAction.ActionType = int32(actionTypeInt64) 28 | 29 | if actionTypeInt64 != 1 && actionTypeInt64 != 2 { 30 | r := res.FavoriteActionResponse{ 31 | StatusCode: exception.ErrOperate, 32 | StatusMsg: exception.GetMsg(exception.ErrOperate), 33 | } 34 | 35 | ctx.JSON(http.StatusOK, r) 36 | return 37 | } 38 | 39 | socialServiceClient := ctx.Keys["social_service"].(service.SocialServiceClient) 40 | socialResp, err := socialServiceClient.FollowAction(context.Background(), &followAction) 41 | if err != nil { 42 | PanicIfFollowError(err) 43 | } 44 | 45 | r := res.FollowActionResponse{ 46 | StatusCode: socialResp.StatusCode, 47 | StatusMsg: socialResp.StatusMsg, 48 | } 49 | 50 | ctx.JSON(http.StatusOK, r) 51 | } 52 | 53 | func GetFollowList(ctx *gin.Context) { 54 | var followList service.FollowListRequest 55 | userId := ctx.Query("user_id") 56 | followList.UserId, _ = strconv.ParseInt(userId, 10, 64) 57 | 58 | socialServiceClient := ctx.Keys["social_service"].(service.SocialServiceClient) 59 | socialResp, err := socialServiceClient.GetFollowList(context.Background(), &followList) 60 | if err != nil { 61 | PanicIfFollowError(err) 62 | } 63 | 64 | r := res.FollowListResponse{ 65 | StatusCode: socialResp.StatusCode, 66 | StatusMsg: socialResp.StatusMsg, 67 | UserList: GetUserInfo(socialResp.UserId, ctx), 68 | } 69 | ctx.JSON(http.StatusOK, r) 70 | } 71 | 72 | func GetFollowerList(ctx *gin.Context) { 73 | var followerList service.FollowListRequest 74 | userId := ctx.Query("user_id") 75 | followerList.UserId, _ = strconv.ParseInt(userId, 10, 64) 76 | 77 | socialServiceClient := ctx.Keys["social_service"].(service.SocialServiceClient) 78 | socialResp, err := socialServiceClient.GetFollowerList(context.Background(), &followerList) 79 | if err != nil { 80 | PanicIfFollowError(err) 81 | } 82 | 83 | r := res.FollowListResponse{ 84 | StatusCode: socialResp.StatusCode, 85 | StatusMsg: socialResp.StatusMsg, 86 | UserList: GetUserInfo(socialResp.UserId, ctx), 87 | } 88 | ctx.JSON(http.StatusOK, r) 89 | } 90 | 91 | func GetFriendList(ctx *gin.Context) { 92 | var friendList service.FollowListRequest 93 | userId := ctx.Query("user_id") 94 | friendList.UserId, _ = strconv.ParseInt(userId, 10, 64) 95 | 96 | socialServiceClient := ctx.Keys["social_service"].(service.SocialServiceClient) 97 | socialResp, err := socialServiceClient.GetFriendList(context.Background(), &friendList) 98 | if err != nil { 99 | PanicIfFollowError(err) 100 | } 101 | 102 | r := res.FollowListResponse{ 103 | StatusCode: socialResp.StatusCode, 104 | StatusMsg: socialResp.StatusMsg, 105 | UserList: GetUserInfo(socialResp.UserId, ctx), 106 | } 107 | ctx.JSON(http.StatusOK, r) 108 | } 109 | -------------------------------------------------------------------------------- /api_router/internal/handler/message.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "api_router/internal/service" 5 | "api_router/pkg/res" 6 | "context" 7 | "github.com/gin-gonic/gin" 8 | "net/http" 9 | "strconv" 10 | "utils/exception" 11 | ) 12 | 13 | func PostMessage(ctx *gin.Context) { 14 | var postMessage service.PostMessageRequest 15 | userId, _ := ctx.Get("user_id") 16 | postMessage.UserId, _ = userId.(int64) 17 | toUserId := ctx.Query("to_user_id") 18 | postMessage.ToUserId, _ = strconv.ParseInt(toUserId, 10, 64) 19 | actionType := ctx.Query("action_type") 20 | actionTypeInt64, _ := strconv.ParseInt(actionType, 10, 32) 21 | 22 | if actionTypeInt64 != 1 { 23 | r := res.FavoriteActionResponse{ 24 | StatusCode: exception.ErrOperate, 25 | StatusMsg: exception.GetMsg(exception.ErrOperate), 26 | } 27 | 28 | ctx.JSON(http.StatusOK, r) 29 | return 30 | } 31 | 32 | postMessage.ActionType = int32(actionTypeInt64) 33 | content := ctx.Query("content") 34 | postMessage.Content = content 35 | 36 | socialServiceClient := ctx.Keys["social_service"].(service.SocialServiceClient) 37 | socialResp, err := socialServiceClient.PostMessage(context.Background(), &postMessage) 38 | if err != nil { 39 | PanicIfMessageError(err) 40 | } 41 | 42 | r := res.PostMessageResponse{ 43 | StatusCode: socialResp.StatusCode, 44 | StatusMsg: socialResp.StatusMsg, 45 | } 46 | ctx.JSON(http.StatusOK, r) 47 | } 48 | 49 | func GetMessage(ctx *gin.Context) { 50 | var getMessage service.GetMessageRequest 51 | userId, _ := ctx.Get("user_id") 52 | getMessage.UserId, _ = userId.(int64) 53 | toUserId := ctx.Query("to_user_id") 54 | getMessage.ToUserId, _ = strconv.ParseInt(toUserId, 10, 64) 55 | PreMsgTime := ctx.Query("pre_msg_time") 56 | getMessage.PreMsgTime, _ = strconv.ParseInt(PreMsgTime, 10, 64) 57 | 58 | socialServiceClient := ctx.Keys["social_service"].(service.SocialServiceClient) 59 | socialResp, err := socialServiceClient.GetMessage(context.Background(), &getMessage) 60 | 61 | if err != nil { 62 | PanicIfMessageError(err) 63 | } 64 | 65 | r := new(res.GetMessageResponse) 66 | r.StatusCode = socialResp.StatusCode 67 | r.StatusMsg = socialResp.StatusMsg 68 | for _, message := range socialResp.Message { 69 | messageResp := res.Message{ 70 | Id: message.Id, 71 | ToUserId: message.ToUserId, 72 | FromUserID: message.UserId, 73 | Content: message.Content, 74 | CreateTime: message.CreatedAt, 75 | } 76 | r.MessageList = append(r.MessageList, messageResp) 77 | } 78 | 79 | ctx.JSON(http.StatusOK, r) 80 | } 81 | -------------------------------------------------------------------------------- /api_router/internal/handler/user.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "api_router/internal/service" 5 | "api_router/pkg/auth" 6 | "api_router/pkg/res" 7 | "context" 8 | "github.com/gin-gonic/gin" 9 | "net/http" 10 | "strconv" 11 | "sync" 12 | "utils/exception" 13 | ) 14 | 15 | // UserRegister 用户注册 16 | func UserRegister(ctx *gin.Context) { 17 | var userReq service.UserRequest 18 | 19 | userName := ctx.PostForm("username") 20 | if userName == "" { 21 | userName = ctx.Query("username") 22 | } 23 | userReq.Username = userName 24 | 25 | passWord := ctx.PostForm("username") 26 | if passWord == "" { 27 | passWord = ctx.Query("password") 28 | } 29 | userReq.Password = passWord 30 | 31 | userServiceClient := ctx.Keys["user_service"].(service.UserServiceClient) 32 | userResp, err := userServiceClient.UserRegister(context.Background(), &userReq) 33 | if err != nil { 34 | PanicIfUserError(err) 35 | } 36 | 37 | token := "" 38 | if userResp.UserId != 0 { 39 | token, _ = auth.GenerateToken(userResp.UserId) 40 | } 41 | 42 | r := res.UserResponse{ 43 | StatusCode: userResp.StatusCode, 44 | StatusMsg: userResp.StatusMsg, 45 | UserId: userResp.UserId, 46 | Token: token, 47 | } 48 | 49 | ctx.JSON(http.StatusOK, r) 50 | } 51 | 52 | // UserLogin 用户登录 53 | func UserLogin(ctx *gin.Context) { 54 | var userReq service.UserRequest 55 | //err := ctx.Bind(&userReq) 56 | //if err != nil { 57 | // PanicIfUserError(err) 58 | //} 59 | userName := ctx.Query("username") 60 | if userName == "" { 61 | userName = ctx.PostForm("username") 62 | } 63 | userReq.Username = userName 64 | 65 | passWord := ctx.Query("password") 66 | if userName == "" { 67 | passWord = ctx.PostForm("password") 68 | } 69 | userReq.Password = passWord 70 | 71 | userServiceClient := ctx.Keys["user_service"].(service.UserServiceClient) 72 | userResp, err := userServiceClient.UserLogin(context.Background(), &userReq) 73 | if err != nil { 74 | PanicIfUserError(err) 75 | } 76 | 77 | token := "" 78 | if userResp.UserId != 0 { 79 | token, _ = auth.GenerateToken(userResp.UserId) 80 | } 81 | 82 | r := res.UserResponse{ 83 | StatusCode: userResp.StatusCode, 84 | StatusMsg: userResp.StatusMsg, 85 | UserId: userResp.UserId, 86 | Token: token, 87 | } 88 | 89 | ctx.JSON(http.StatusOK, r) 90 | } 91 | 92 | // UserInfo 用户信息列表 93 | func UserInfo(ctx *gin.Context) { 94 | var userIds []int64 95 | 96 | // jwt中间件会解析token,然后把user_id放入context中,所以用两种方式都可以获取到user_id 97 | userIdStr := ctx.Query("user_id") 98 | userId, _ := strconv.ParseInt(userIdStr, 10, 64) 99 | 100 | userIds = append(userIds, userId) 101 | 102 | r := res.UserInfoResponse{ 103 | StatusCode: exception.SUCCESS, 104 | StatusMsg: exception.GetMsg(exception.SUCCESS), 105 | User: GetUserInfo(userIds, ctx)[0], 106 | } 107 | 108 | ctx.JSON(http.StatusOK, r) 109 | } 110 | 111 | // GetUserInfo 根据用户id,去调取三个服务,拼接出所有的用户信息 112 | func GetUserInfo(userIds []int64, ctx *gin.Context) (userInfos []res.User) { 113 | var err error 114 | // 构建三个服务的请求 115 | var userInfoReq service.UserInfoRequest 116 | var countInfoReq service.CountRequest 117 | var followInfoReq service.FollowInfoRequest 118 | 119 | userInfoReq.UserIds = userIds 120 | countInfoReq.UserIds = userIds 121 | followInfoReq.ToUserId = userIds 122 | 123 | // 创建接收三个响应 124 | var userResp *service.UserInfoResponse 125 | var countInfoResp *service.CountResponse 126 | var followInfoResp *service.FollowInfoResponse 127 | 128 | // 分别去调用三个服务 129 | var wg sync.WaitGroup 130 | wg.Add(3) 131 | go func() { 132 | defer wg.Done() 133 | userServiceClient := ctx.Keys["user_service"].(service.UserServiceClient) 134 | userResp, err = userServiceClient.UserInfo(context.Background(), &userInfoReq) 135 | if err != nil { 136 | PanicIfUserError(err) 137 | } 138 | }() 139 | 140 | go func() { 141 | defer wg.Done() 142 | videoServiceClient := ctx.Keys["video_service"].(service.VideoServiceClient) 143 | countInfoResp, err = videoServiceClient.CountInfo(context.Background(), &countInfoReq) 144 | if err != nil { 145 | PanicIfVideoError(err) 146 | } 147 | }() 148 | 149 | go func() { 150 | defer wg.Done() 151 | 152 | // 拿到当前用户的id,在is_follow找对应关系 153 | userIdStr, _ := ctx.Get("user_id") 154 | userId, _ := userIdStr.(int64) 155 | followInfoReq.UserId = userId 156 | socialServiceClient := ctx.Keys["social_service"].(service.SocialServiceClient) 157 | followInfoResp, err = socialServiceClient.GetFollowInfo(context.Background(), &followInfoReq) 158 | if err != nil { 159 | PanicIfFollowError(err) 160 | } 161 | }() 162 | wg.Wait() 163 | 164 | // 构建信息userResp.Users[0], countInfoResp.Counts[0]) 165 | for index, _ := range userIds { 166 | userInfos = append(userInfos, BuildUser(userResp.Users[index], countInfoResp.Counts[index], followInfoResp.FollowInfo[index])) 167 | } 168 | 169 | return userInfos 170 | } 171 | 172 | // BuildUser 构建用户信息 173 | func BuildUser(user *service.User, count *service.Count, follow *service.FollowInfo) res.User { 174 | return res.User{ 175 | Id: user.Id, 176 | Name: user.Name, 177 | 178 | FollowCount: follow.FollowCount, 179 | FollowerCount: follow.FollowerCount, 180 | IsFollow: follow.IsFollow, 181 | 182 | Avatar: user.Avatar, 183 | BackgroundImage: user.BackgroundImage, 184 | Signature: user.Signature, 185 | 186 | TotalFavorited: strconv.FormatInt(count.TotalFavorited, 10), // 将int64转换成string 187 | WorkCount: count.WorkCount, 188 | FavoriteCount: count.FavoriteCount, 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /api_router/internal/handler/video.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "api_router/internal/service" 5 | "api_router/pkg/res" 6 | "context" 7 | 8 | "github.com/gin-gonic/gin" 9 | "io" 10 | 11 | "net/http" 12 | "strconv" 13 | ) 14 | 15 | // Feed 视频流 16 | func Feed(ctx *gin.Context) { 17 | var feedReq service.FeedRequest 18 | 19 | // 判断是否带有参数 20 | token := ctx.Query("token") 21 | if token == "" { 22 | feedReq.UserId = -1 23 | } else { 24 | userId, _ := ctx.Get("user_id") 25 | feedReq.UserId, _ = userId.(int64) 26 | } 27 | 28 | latestTime := ctx.Query("latest_time") 29 | if latestTime == "" || latestTime == "0" { 30 | feedReq.LatestTime = -1 31 | } else { 32 | timePoint, _ := strconv.ParseInt(latestTime, 10, 64) 33 | feedReq.LatestTime = timePoint 34 | } 35 | 36 | videoServiceClient := ctx.Keys["video_service"].(service.VideoServiceClient) 37 | feedResp, err := videoServiceClient.Feed(context.Background(), &feedReq) 38 | if err != nil { 39 | PanicIfVideoError(err) 40 | } 41 | 42 | var userIds []int64 43 | for _, video := range feedResp.VideoList { 44 | userIds = append(userIds, video.AuthId) 45 | } 46 | 47 | // 找到所有的用户信息 48 | userInfos := GetUserInfo(userIds, ctx) 49 | 50 | list := BuildVideoList(feedResp.VideoList, userInfos) 51 | 52 | r := res.FeedResponse{ 53 | StatusCode: feedResp.StatusCode, 54 | StatusMsg: feedResp.StatusMsg, 55 | NextTime: feedResp.NextTime, 56 | VideoList: list, 57 | } 58 | 59 | ctx.JSON(http.StatusOK, r) 60 | } 61 | 62 | // PublishAction 发布视频 63 | func PublishAction(ctx *gin.Context) { 64 | var publishActionReq service.PublishActionRequest 65 | 66 | userId, _ := ctx.Get("user_id") 67 | publishActionReq.UserId = userId.(int64) 68 | 69 | publishActionReq.Title = ctx.PostForm("title") 70 | 71 | formFile, _ := ctx.FormFile("data") 72 | file, err := formFile.Open() 73 | if err != nil { 74 | PanicIfVideoError(err) 75 | } 76 | defer file.Close() 77 | buf, err := io.ReadAll(file) // 将文件读取到字节切片buf中 78 | if err != nil { 79 | PanicIfVideoError(err) 80 | } 81 | publishActionReq.Data = buf 82 | 83 | videoServiceClient := ctx.Keys["video_service"].(service.VideoServiceClient) 84 | videoServiceResp, err := videoServiceClient.PublishAction(context.Background(), &publishActionReq) 85 | if err != nil { 86 | PanicIfVideoError(err) 87 | } 88 | 89 | r := res.PublishActionResponse{ 90 | StatusCode: videoServiceResp.StatusCode, 91 | StatusMsg: videoServiceResp.StatusMsg, 92 | } 93 | 94 | ctx.JSON(http.StatusOK, r) 95 | } 96 | 97 | // PublishList 发布列表 98 | func PublishList(ctx *gin.Context) { 99 | var pulishListReq service.PublishListRequest 100 | 101 | userIdStr := ctx.Query("user_id") 102 | userId, _ := strconv.ParseInt(userIdStr, 10, 64) 103 | 104 | pulishListReq.UserId = userId 105 | 106 | videoServiceClient := ctx.Keys["video_service"].(service.VideoServiceClient) 107 | publishListResp, err := videoServiceClient.PublishList(context.Background(), &pulishListReq) 108 | if err != nil { 109 | PanicIfVideoError(err) 110 | } 111 | 112 | var userIds []int64 113 | for _, video := range publishListResp.VideoList { 114 | userIds = append(userIds, video.AuthId) 115 | } 116 | 117 | // 找到所有的用户信息 118 | userInfos := GetUserInfo(userIds, ctx) 119 | 120 | list := BuildVideoList(publishListResp.VideoList, userInfos) 121 | 122 | r := res.VideoListResponse{ 123 | StatusCode: publishListResp.StatusCode, 124 | StatusMsg: publishListResp.StatusMsg, 125 | VideoList: list, 126 | } 127 | 128 | ctx.JSON(http.StatusOK, r) 129 | } 130 | 131 | // BuildVideoList 构建视频列表 132 | func BuildVideoList(videos []*service.Video, userInfos []res.User) []res.Video { 133 | 134 | var videoList []res.Video 135 | 136 | for i, video := range videos { 137 | videoList = append(videoList, res.Video{ 138 | Id: video.Id, 139 | Author: userInfos[i], 140 | PlayUrl: video.PlayUrl, 141 | CoverUrl: video.CoverUrl, 142 | FavoriteCount: video.FavoriteCount, 143 | CommentCount: video.CommentCount, 144 | IsFavorite: video.IsFavorite, 145 | Title: video.Title, 146 | }) 147 | } 148 | 149 | return videoList 150 | } 151 | -------------------------------------------------------------------------------- /api_router/internal/service/pb/socialService.proto: -------------------------------------------------------------------------------- 1 | syntax="proto3"; 2 | package pb; 3 | option go_package = "../;service"; // 此处格式为<生成的文件存放位置;生成文件的包名> 4 | 5 | // protoc -I internal/service/pb --go_out=./internal/service/ --go_opt=paths=source_relative --go-grpc_out=./internal/service/ --go-grpc_opt=paths=source_relative internal/service/pb/*.proto 6 | // 或者分开使用 7 | // protoc -I internal/service/pb/ --go_out=./ internal/service/pb/*.proto 8 | // protoc -I internal/service/pb/ --go-grpc_out=./ internal/service/pb/*.proto 9 | 10 | message FollowRequest{ 11 | // @gotags:json:"user_id" form:"user_id" uri:"user_id" 12 | int64 UserId = 1; 13 | // @gotags:json:"to_user_id" form:"to_user_id" uri:"to_user_id" 14 | int64 ToUserId = 2; 15 | // @gotags:json:"action_type" form:"action_type" uri:"action_type" 16 | int32 ActionType = 3; 17 | } 18 | 19 | message FollowResponse{ 20 | // @gotags:json:"status_code" form:"status_code" uri:"status_code" 21 | int32 StatusCode = 1; 22 | // @gotags:json:"status_msg" form:"status_msg" uri:"status_msg" 23 | string StatusMsg = 2; 24 | } 25 | 26 | message FollowListRequest{ 27 | // @gotags:json:"user_id" form:"user_id" uri:"user_id" 28 | int64 UserId = 1; 29 | } 30 | 31 | message FollowListResponse{ 32 | // @gotags:json:"status_code" form:"status_code" uri:"status_code" 33 | int32 StatusCode = 1; 34 | // @gotags:json:"status_msg" form:"status_msg" uri:"status_msg" 35 | string StatusMsg = 2; 36 | // @gotags:json:"user_list" form:"user_list" uri:"user_list" 37 | repeated int64 UserId = 3; 38 | } 39 | 40 | message FollowInfoRequest{ 41 | // @gotags:json:"user_id" form:"user_id" uri:"user_id" 42 | int64 UserId = 1; 43 | // @gotags:json:"to_user_id" form:"to_user_id" uri:"to_user_id" 44 | repeated int64 ToUserId = 2; 45 | } 46 | 47 | message FollowInfo{ 48 | // @gotags:json:"is_follow" form:"is_follow" uri:"is_follow" 49 | bool IsFollow = 1; 50 | // @gotags:json:"follow_count" form:"follow_count" uri:"follow_count" 51 | int64 FollowCount = 2; 52 | // @gotags:json:"follower_count" form:"follower_count" uri:"follower_count" 53 | int64 FollowerCount = 3; 54 | // @gotags:json:"to_user_id" form:"to_user_id" uri:"to_user_id" 55 | int64 ToUserId = 4; 56 | } 57 | 58 | message FollowInfoResponse{ 59 | // @gotags:json:"status_code" form:"status_code" uri:"status_code" 60 | int32 StatusCode = 1; 61 | // @gotags:json:"status_msg" form:"status_msg" uri:"status_msg" 62 | string StatusMsg = 2; 63 | // @gotags:json:"follow_info" form:"follow_info" uri:"follow_info" 64 | repeated FollowInfo FollowInfo = 3; 65 | } 66 | 67 | message PostMessageRequest{ 68 | // @gotags:json:"user_id" form:"user_id" uri:"user_id" 69 | int64 UserId = 1; 70 | // @gotags:json:"to_user_id" form:"to_user_id" uri:"to_user_id" 71 | int64 ToUserId = 2; 72 | // @gotags:json:"action_type" form:"action_type" uri:"action_type" 73 | int32 ActionType = 3; 74 | // @gotags:json:"content" form:"content" uri:"content" 75 | string Content = 4; 76 | } 77 | 78 | message PostMessageResponse{ 79 | // @gotags:json:"status_code" form:"status_code" uri:"status_code" 80 | int32 StatusCode = 1; 81 | // @gotags:json:"status_msg" form:"status_msg" uri:"status_msg" 82 | string StatusMsg = 2; 83 | } 84 | 85 | message GetMessageRequest{ 86 | // @gotags:json:"user_id" form:"user_id" uri:"user_id" 87 | int64 UserId = 1; 88 | // @gotags:json:"to_user_id" form:"to_user_id" uri:"to_user_id" 89 | int64 ToUserId = 2; 90 | // @gotags:json:"pre_msg_time" form:"pre_msg_time" uri:"pre_msg_time" 91 | int64 preMsgTime=3; 92 | } 93 | 94 | message Message{ 95 | // @gotags:json:"id" form:"id" uri:"id" 96 | int64 Id = 1; 97 | // @gotags:json:"user_id" form:"user_id" uri:"user_id" 98 | int64 UserId = 2; 99 | // @gotags:json:"to_user_id" form:"to_user_id" uri:"to_user_id" 100 | int64 ToUserId = 3; 101 | // @gotags:json:"content" form:"content" uri:"content" 102 | string Content = 4; 103 | // @gotags:json:"created_at" form:"created_at" uri:"created_at" 104 | int64 CreatedAt = 5; 105 | } 106 | 107 | message GetMessageResponse{ 108 | // @gotags:json:"status_code" form:"status_code" uri:"status_code" 109 | int32 StatusCode = 1; 110 | // @gotags:json:"status_msg" form:"status_msg" uri:"status_msg" 111 | string StatusMsg = 2; 112 | // @gotags:json:"message_list" form:"message_list" uri:"message_list" 113 | repeated Message message = 3; 114 | } 115 | 116 | service SocialService{ 117 | // 关注服务 118 | rpc FollowAction(FollowRequest) returns(FollowResponse); 119 | rpc GetFollowList(FollowListRequest) returns(FollowListResponse); 120 | rpc GetFollowerList(FollowListRequest) returns(FollowListResponse); 121 | rpc GetFriendList(FollowListRequest) returns(FollowListResponse); 122 | rpc GetFollowInfo(FollowInfoRequest) returns(FollowInfoResponse); 123 | 124 | // 消息服务 125 | rpc PostMessage(PostMessageRequest) returns(PostMessageResponse); 126 | rpc GetMessage(GetMessageRequest) returns(GetMessageResponse); 127 | 128 | } -------------------------------------------------------------------------------- /api_router/internal/service/pb/userService.proto: -------------------------------------------------------------------------------- 1 | syntax="proto3"; 2 | package pb; 3 | option go_package = "../;service"; // 此处格式为<生成的文件存放位置;生成文件的包名> 4 | 5 | // protoc -I internal/service/pb --go_out=./internal/service/ --go_opt=paths=source_relative --go-grpc_out=./internal/service/ --go-grpc_opt=paths=source_relative internal/service/pb/*.proto 6 | // 或者分开使用 7 | // protoc -I internal/service/pb/ --go_out=./ internal/service/pb/*.proto 8 | // protoc -I internal/service/pb/ --go-grpc_out=./ internal/service/pb/*.proto 9 | 10 | message UserRequest{ 11 | // @gotags:json:"username" form:"username" uri:"username" 12 | string Username = 1; // 账号 13 | // @gotags:json:"password" form:"password" uri:"password" 14 | string Password = 2; // 密码 15 | } 16 | 17 | message UserResponse{ 18 | // @gotags:json:"status_code" form:"status_code" uri:"status_code" 19 | int64 StatusCode = 1; 20 | // @gotags:json:"status_msg" form:"status_msg" uri:"status_msg" 21 | string StatusMsg = 2; 22 | // @gotags:json:"user_id" form:"user_id" uri:"user_id" 23 | int64 UserId = 3; // 用户id 24 | } 25 | 26 | message User { 27 | // @gotags:json:"id" form:"id" uri:"id" 28 | int64 Id = 1; 29 | // @gotags:json:"name" form:"name" uri:"name" 30 | string Name = 2; 31 | // @gotags:json:"avatar" form:"avatar" uri:"avatar" 32 | string Avatar = 3; // 用户头像 33 | // @gotags:json:"background_image" form:"background_image" uri:"background_image" 34 | string BackgroundImage = 4; // 用户背景图 35 | // @gotags:json:"signature" form:"signature" uri:"signature" 36 | string Signature = 5; // 用户签名 37 | } 38 | 39 | message UserInfoRequest { 40 | // @gotags:json:"user_ids" form:"user_ids" uri:"user_ids" 41 | repeated int64 UserIds = 1; // 传入一个userId切片 42 | } 43 | 44 | message UserInfoResponse { 45 | // @gotags:json:"status_code" form:"status_code" uri:"status_code" 46 | int64 StatusCode = 1; 47 | // @gotags:json:"status_msg" form:"status_msg" uri:"status_msg" 48 | string StatusMsg = 2; 49 | // @gotags:json:"users" form:"users" uri:"users" 50 | repeated User Users = 3; // 用户信息 51 | } 52 | 53 | 54 | service UserService{ 55 | rpc UserRegister(UserRequest) returns(UserResponse); 56 | rpc UserLogin(UserRequest) returns(UserResponse); 57 | rpc UserInfo(UserInfoRequest) returns(UserInfoResponse); 58 | } -------------------------------------------------------------------------------- /api_router/internal/service/pb/videoService.proto: -------------------------------------------------------------------------------- 1 | syntax="proto3"; 2 | package pb; 3 | option go_package = "../;service"; // 此处格式为<生成的文件存放位置;生成文件的包名> 4 | 5 | 6 | message Video { 7 | // @gotags:json:"id" form:"id" uri:"id" 8 | int64 Id = 1; 9 | // @gotags:json:"auth_id" form:"auth_id" uri:"auth_id" 10 | int64 AuthId = 2; 11 | // @gotags:json:"play_url" form:"play_rul" uri:"play_url" 12 | string PlayUrl = 3; // 视频播放地址 13 | // @gotasgs:json:"cover_url" form:"cover_url" uri:"cover_url" 14 | string CoverUrl = 4; 15 | // @gotags:json:"favorite_count" form:"favorite_count" uri:"favorite_count" 16 | int64 FavoriteCount = 5; 17 | // @gotags:json:"comment_count" form:"comment_count" uri:"comment_count" 18 | int64 CommentCount = 6; 19 | // @gotags:json:"is_favorite" form:"is_favorite" uri:"is_favorite" 20 | bool IsFavorite = 7; 21 | // @gotags:json:"title" form:"title" uri:"title" 22 | string Title = 8; 23 | } 24 | 25 | message Comment { 26 | // @gotags:json:"id" form:"id" uri:"id" 27 | int64 Id = 1; 28 | // @gatags:json:"user_id" form:"user_id" uri:"user_id" 29 | int64 UserId = 2; 30 | // @gotags:json:"content" form:"content" uri:"content" 31 | string Content = 3; // 评论内容 32 | // @gotags:json:"create_date" form:"create_date" uri:"create_date" 33 | string CreateDate = 4; 34 | } 35 | 36 | // 视频流 37 | message FeedRequest { 38 | // @gotags:json:"latest_time" form:"latest_time" uri:"latest_time" 39 | int64 LatestTime = 1; // 可选参数,限制返回视频的最新投稿时间戳,精确到秒,不填表示当前时间 40 | // @gotags:json:"user_id" form:"user_id" uri:"user_id" 41 | int64 UserId = 2; 42 | } 43 | 44 | message FeedResponse { 45 | // @gotags:json:"status_code" form:"status_code" uri:"status_code" 46 | int64 StatusCode = 1; 47 | // @gotags:json:"status_msg" form:"status_msg" uri:"status_msg" 48 | string StatusMsg = 2; 49 | // @gotags:json:"video_list" form:"video_list" uri:"video_list" 50 | repeated Video VideoList = 3; 51 | // @gotags:json:"next_time" form:"next_time" uri:"next_time" 52 | int64 NextTime = 4; 53 | } 54 | 55 | // 发布视频 56 | message PublishActionRequest { 57 | // @gotags:json:"user_id" form:"user_id" uri:"user_id" 58 | int64 UserId = 1; 59 | // @gotags:json:"data" form:"data" uri:"data" 60 | bytes Data = 2; 61 | // @gotags:json:"title" form:"title" uri:"title" 62 | string Title = 3; 63 | } 64 | 65 | message PublishActionResponse { 66 | // @gotags:json:"status_code" form:"status_code" uri:"status_code" 67 | int64 StatusCode = 1; 68 | // @gotags:json:"status_msg" form:"status_msg" uri:"status_msg" 69 | string StatusMsg = 2; 70 | } 71 | 72 | // 发布列表 73 | message PublishListRequest { 74 | // @gotags:json:"user_id" form:"user_id" uri:"user_id" 75 | int64 UserId = 1; 76 | } 77 | 78 | message PublishListResponse { 79 | // @gotags:json:"status_code" form:"status_code" uri:"status_code" 80 | int64 StatusCode = 1; 81 | // @gotags:json:"status_msg" form:"status_msg" uri:"status_msg" 82 | string StatusMsg = 2; 83 | // @gotags:json:"video_list" form:"video_list" uri:"video_list" 84 | repeated Video VideoList = 3; 85 | } 86 | 87 | // 赞操作 88 | message FavoriteActionRequest { 89 | // @gotags:json:"user_id" form:"user_id" uri:"user_id" 90 | int64 UserId = 1; 91 | // @gotags:json:"video_id" form:"video_id" uri:"video_id" 92 | int64 VideoId = 2; 93 | // @gotags:json:"action_type" form:"action_type" uri:"action_type" 94 | int64 ActionType = 3; // 1-点赞,2-取消点赞 95 | } 96 | 97 | message FavoriteActionResponse { 98 | // @gotags:json:"status_code" form:"status_code" uri:"status_code" 99 | int64 StatusCode = 1; 100 | // @gotags:json:"status_msg" form:"status_msg" uri:"status_msg" 101 | string StatusMsg = 2; 102 | } 103 | 104 | 105 | // 喜欢列表 106 | message FavoriteListRequest { 107 | // @gotags:json:"user_id" form:"user_id" uri:"user_id" 108 | int64 UserId = 1; 109 | } 110 | 111 | message FavoriteListResponse { 112 | // @gotags:json:"status_code" form:"status_code" uri:"status_code" 113 | int64 StatusCode = 1; 114 | // @gotags:json:"status_msg" form:"status_msg" uri:"status_msg" 115 | string StatusMsg = 2; 116 | // @gotags:json:"video_list" form:"video_list" uri:"video_list" 117 | repeated Video VideoList = 3; 118 | } 119 | 120 | 121 | // 评论操作 122 | message CommentActionRequest { 123 | // @gotags:json:"user_id" form:"user_id" uri:"user_id" 124 | int64 UserId = 1; 125 | // @gotags:json:"video_id" form:"video_id" uri:"video_id" 126 | int64 VideoId = 2; 127 | // @gotags:json:"action_type" form:"action_type" uri:"action_type" 128 | int64 ActionType = 3; //1-发布评论,2-删除评论 129 | // @gotags:json:"comment_text" form:"comment_text" uri:"comment_text" 130 | string CommentText = 4; // 可选,用户填写的评论内容,在action_type=1的时候使用 131 | // @gotags:json:"comment_id" form:"comment_id" uri:"comment_id" 132 | int64 CommentId = 5; // 可选,要删除的评论id,在action_type=2的时候使用 133 | } 134 | 135 | message CommentActionResponse { 136 | // @gotags:json:"status_code" form:"status_code" uri:"status_code" 137 | int64 StatusCode = 1; 138 | // @gotags:json:"status_msg" form:"status_msg" uri:"status_msg" 139 | string StatusMsg = 2; 140 | // @gotags:json:"comment" form:"comment" uri:"comment" 141 | Comment Comment = 3; 142 | } 143 | 144 | // 评论列表 145 | message CommentListRequest { 146 | // @gotags:json:"video_id" form:"video_id" uri:"video_id" 147 | int64 VideoId = 1; 148 | } 149 | 150 | message CommentListResponse { 151 | // @gotags:json:"status_code" form:"status_code" uri:"status_code" 152 | int64 StatusCode = 1; 153 | // @gotags:json:"status_msg" form:"status_msg" uri:"status_msg" 154 | string StatusMsg = 2; 155 | // @gotags:json:"comment_list" form:"comment_list" uri:"comment_list" 156 | repeated Comment CommentList = 3; 157 | } 158 | 159 | // 视频计数 160 | message Count { 161 | // @gotags:json:"total_favorited" form:"total_favorited" uri:"total_favorited" 162 | int64 TotalFavorited = 1; // 获赞数量 163 | // @gotags:json:"work_count" form:"work_count" uri:"work_count" 164 | int64 WorkCount = 2; // 作品数量 165 | // @gotags:json:"favorite_count" form:"favorite_count" uri:"favorite_count" 166 | int64 FavoriteCount = 3; // 喜欢数量 167 | } 168 | 169 | // 视频总计数请求 170 | message CountRequest { 171 | // @gotags:json:"user_id" form:"user_id" uri:"user_id" 172 | repeated int64 UserIds = 1; // 传入一个userId切片 173 | } 174 | 175 | message CountResponse { 176 | // @gotags:json:"status_code" form:"status_code" uri:"status_code" 177 | int64 StatusCode = 1; 178 | // @gotags:json:"status_msg" form:"status_msg" uri:"status_msg" 179 | string StatusMsg = 2; 180 | // @gotags:json:"counts" form:"counts" uri:"counts" 181 | repeated Count counts = 3; 182 | } 183 | 184 | service VideoService { 185 | rpc Feed(FeedRequest) returns(FeedResponse); 186 | rpc PublishAction(PublishActionRequest) returns (PublishActionResponse); 187 | rpc PublishList(PublishListRequest) returns(PublishListResponse); 188 | 189 | rpc FavoriteAction(FavoriteActionRequest) returns(FavoriteActionResponse); 190 | rpc FavoriteList(FavoriteListRequest) returns(FavoriteListResponse); 191 | 192 | rpc CommentAction(CommentActionRequest) returns(CommentActionResponse); 193 | rpc CommentList(CommentListRequest) returns(CommentListResponse); 194 | 195 | // 根据user_id切片,返回计数信息 196 | rpc CountInfo(CountRequest) returns(CountResponse); 197 | } 198 | 199 | -------------------------------------------------------------------------------- /api_router/internal/service/userService_grpc.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-go-grpc. DO NOT EDIT. 2 | // versions: 3 | // - protoc-gen-go-grpc v1.3.0 4 | // - protoc v3.15.5 5 | // source: userService.proto 6 | 7 | package service 8 | 9 | import ( 10 | context "context" 11 | grpc "google.golang.org/grpc" 12 | codes "google.golang.org/grpc/codes" 13 | status "google.golang.org/grpc/status" 14 | ) 15 | 16 | // This is a compile-time assertion to ensure that this generated file 17 | // is compatible with the grpc package it is being compiled against. 18 | // Requires gRPC-Go v1.32.0 or later. 19 | const _ = grpc.SupportPackageIsVersion7 20 | 21 | const ( 22 | UserService_UserRegister_FullMethodName = "/pb.UserService/UserRegister" 23 | UserService_UserLogin_FullMethodName = "/pb.UserService/UserLogin" 24 | UserService_UserInfo_FullMethodName = "/pb.UserService/UserInfo" 25 | ) 26 | 27 | // UserServiceClient is the client API for UserService service. 28 | // 29 | // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. 30 | type UserServiceClient interface { 31 | UserRegister(ctx context.Context, in *UserRequest, opts ...grpc.CallOption) (*UserResponse, error) 32 | UserLogin(ctx context.Context, in *UserRequest, opts ...grpc.CallOption) (*UserResponse, error) 33 | UserInfo(ctx context.Context, in *UserInfoRequest, opts ...grpc.CallOption) (*UserInfoResponse, error) 34 | } 35 | 36 | type userServiceClient struct { 37 | cc grpc.ClientConnInterface 38 | } 39 | 40 | func NewUserServiceClient(cc grpc.ClientConnInterface) UserServiceClient { 41 | return &userServiceClient{cc} 42 | } 43 | 44 | func (c *userServiceClient) UserRegister(ctx context.Context, in *UserRequest, opts ...grpc.CallOption) (*UserResponse, error) { 45 | out := new(UserResponse) 46 | err := c.cc.Invoke(ctx, UserService_UserRegister_FullMethodName, in, out, opts...) 47 | if err != nil { 48 | return nil, err 49 | } 50 | return out, nil 51 | } 52 | 53 | func (c *userServiceClient) UserLogin(ctx context.Context, in *UserRequest, opts ...grpc.CallOption) (*UserResponse, error) { 54 | out := new(UserResponse) 55 | err := c.cc.Invoke(ctx, UserService_UserLogin_FullMethodName, in, out, opts...) 56 | if err != nil { 57 | return nil, err 58 | } 59 | return out, nil 60 | } 61 | 62 | func (c *userServiceClient) UserInfo(ctx context.Context, in *UserInfoRequest, opts ...grpc.CallOption) (*UserInfoResponse, error) { 63 | out := new(UserInfoResponse) 64 | err := c.cc.Invoke(ctx, UserService_UserInfo_FullMethodName, in, out, opts...) 65 | if err != nil { 66 | return nil, err 67 | } 68 | return out, nil 69 | } 70 | 71 | // UserServiceServer is the server API for UserService service. 72 | // All implementations must embed UnimplementedUserServiceServer 73 | // for forward compatibility 74 | type UserServiceServer interface { 75 | UserRegister(context.Context, *UserRequest) (*UserResponse, error) 76 | UserLogin(context.Context, *UserRequest) (*UserResponse, error) 77 | UserInfo(context.Context, *UserInfoRequest) (*UserInfoResponse, error) 78 | mustEmbedUnimplementedUserServiceServer() 79 | } 80 | 81 | // UnimplementedUserServiceServer must be embedded to have forward compatible implementations. 82 | type UnimplementedUserServiceServer struct { 83 | } 84 | 85 | func (UnimplementedUserServiceServer) UserRegister(context.Context, *UserRequest) (*UserResponse, error) { 86 | return nil, status.Errorf(codes.Unimplemented, "method UserRegister not implemented") 87 | } 88 | func (UnimplementedUserServiceServer) UserLogin(context.Context, *UserRequest) (*UserResponse, error) { 89 | return nil, status.Errorf(codes.Unimplemented, "method UserLogin not implemented") 90 | } 91 | func (UnimplementedUserServiceServer) UserInfo(context.Context, *UserInfoRequest) (*UserInfoResponse, error) { 92 | return nil, status.Errorf(codes.Unimplemented, "method UserInfo not implemented") 93 | } 94 | func (UnimplementedUserServiceServer) mustEmbedUnimplementedUserServiceServer() {} 95 | 96 | // UnsafeUserServiceServer may be embedded to opt out of forward compatibility for this service. 97 | // Use of this interface is not recommended, as added methods to UserServiceServer will 98 | // result in compilation errors. 99 | type UnsafeUserServiceServer interface { 100 | mustEmbedUnimplementedUserServiceServer() 101 | } 102 | 103 | func RegisterUserServiceServer(s grpc.ServiceRegistrar, srv UserServiceServer) { 104 | s.RegisterService(&UserService_ServiceDesc, srv) 105 | } 106 | 107 | func _UserService_UserRegister_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 108 | in := new(UserRequest) 109 | if err := dec(in); err != nil { 110 | return nil, err 111 | } 112 | if interceptor == nil { 113 | return srv.(UserServiceServer).UserRegister(ctx, in) 114 | } 115 | info := &grpc.UnaryServerInfo{ 116 | Server: srv, 117 | FullMethod: UserService_UserRegister_FullMethodName, 118 | } 119 | handler := func(ctx context.Context, req interface{}) (interface{}, error) { 120 | return srv.(UserServiceServer).UserRegister(ctx, req.(*UserRequest)) 121 | } 122 | return interceptor(ctx, in, info, handler) 123 | } 124 | 125 | func _UserService_UserLogin_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 126 | in := new(UserRequest) 127 | if err := dec(in); err != nil { 128 | return nil, err 129 | } 130 | if interceptor == nil { 131 | return srv.(UserServiceServer).UserLogin(ctx, in) 132 | } 133 | info := &grpc.UnaryServerInfo{ 134 | Server: srv, 135 | FullMethod: UserService_UserLogin_FullMethodName, 136 | } 137 | handler := func(ctx context.Context, req interface{}) (interface{}, error) { 138 | return srv.(UserServiceServer).UserLogin(ctx, req.(*UserRequest)) 139 | } 140 | return interceptor(ctx, in, info, handler) 141 | } 142 | 143 | func _UserService_UserInfo_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 144 | in := new(UserInfoRequest) 145 | if err := dec(in); err != nil { 146 | return nil, err 147 | } 148 | if interceptor == nil { 149 | return srv.(UserServiceServer).UserInfo(ctx, in) 150 | } 151 | info := &grpc.UnaryServerInfo{ 152 | Server: srv, 153 | FullMethod: UserService_UserInfo_FullMethodName, 154 | } 155 | handler := func(ctx context.Context, req interface{}) (interface{}, error) { 156 | return srv.(UserServiceServer).UserInfo(ctx, req.(*UserInfoRequest)) 157 | } 158 | return interceptor(ctx, in, info, handler) 159 | } 160 | 161 | // UserService_ServiceDesc is the grpc.ServiceDesc for UserService service. 162 | // It's only intended for direct use with grpc.RegisterService, 163 | // and not to be introspected or modified (even as a copy) 164 | var UserService_ServiceDesc = grpc.ServiceDesc{ 165 | ServiceName: "pb.UserService", 166 | HandlerType: (*UserServiceServer)(nil), 167 | Methods: []grpc.MethodDesc{ 168 | { 169 | MethodName: "UserRegister", 170 | Handler: _UserService_UserRegister_Handler, 171 | }, 172 | { 173 | MethodName: "UserLogin", 174 | Handler: _UserService_UserLogin_Handler, 175 | }, 176 | { 177 | MethodName: "UserInfo", 178 | Handler: _UserService_UserInfo_Handler, 179 | }, 180 | }, 181 | Streams: []grpc.StreamDesc{}, 182 | Metadata: "userService.proto", 183 | } 184 | -------------------------------------------------------------------------------- /api_router/logs/2023-08-22.log: -------------------------------------------------------------------------------- 1 | 2023-08-22.log1800 -------------------------------------------------------------------------------- /api_router/logs/2023-08-22.log1800: -------------------------------------------------------------------------------- 1 | time="2023-08-22T19:15:10+08:00" level=info msg=222 2 | time="2023-08-22T19:15:28+08:00" level=info msg=222 3 | time="2023-08-22T19:52:58+08:00" level=info msg="获取用户服务实例--成功--" 4 | time="2023-08-22T19:52:58+08:00" level=info msg="获取视频服务实例--成功--" 5 | time="2023-08-22T19:52:58+08:00" level=info msg="获取社交服务实例--成功--" 6 | time="2023-08-22T19:55:13+08:00" level=info msg="获取用户服务实例--成功--" 7 | time="2023-08-22T19:55:13+08:00" level=info msg="获取视频服务实例--成功--" 8 | time="2023-08-22T19:55:13+08:00" level=info msg="获取社交服务实例--成功--" 9 | time="2023-08-22T19:55:13+08:00" level=info msg="请求过快,服务熔断" 10 | time="2023-08-22T19:55:13+08:00" level=info msg="请求太快拉..." 11 | time="2023-08-22T19:58:00+08:00" level=info msg="获取用户服务实例--成功--" 12 | time="2023-08-22T19:58:00+08:00" level=info msg="获取视频服务实例--成功--" 13 | time="2023-08-22T19:58:00+08:00" level=info msg="获取社交服务实例--成功--" 14 | time="2023-08-22T19:58:00+08:00" level=info msg="请求过快,服务熔断user_service" 15 | time="2023-08-22T19:58:00+08:00" level=info msg="请求太快拉..." 16 | time="2023-08-22T20:06:27+08:00" level=info msg="获取用户服务实例--成功--" 17 | time="2023-08-22T20:06:27+08:00" level=info msg="获取视频服务实例--成功--" 18 | time="2023-08-22T20:06:27+08:00" level=info msg="获取社交服务实例--成功--" 19 | time="2023-08-22T20:06:27+08:00" level=info msg="请求过快---user_service服务熔断" 20 | time="2023-08-22T20:07:35+08:00" level=info msg="获取用户服务实例--成功--" 21 | time="2023-08-22T20:07:35+08:00" level=info msg="获取视频服务实例--成功--" 22 | time="2023-08-22T20:07:35+08:00" level=info msg="获取社交服务实例--成功--" 23 | time="2023-08-22T20:07:35+08:00" level=info msg="请求过快---user_service服务熔断" 24 | time="2023-08-22T20:07:35+08:00" level=info msg="请求过快---video_service服务熔断" 25 | time="2023-08-22T20:07:35+08:00" level=info msg="请求过快---social_service服务熔断" 26 | -------------------------------------------------------------------------------- /api_router/logs/2023-08-27.log: -------------------------------------------------------------------------------- 1 | 2023-08-27.log1500 -------------------------------------------------------------------------------- /api_router/logs/2023-08-27.log1500: -------------------------------------------------------------------------------- 1 | time="2023-08-27T15:09:23+08:00" level=fatal msg="failed to build resolver: passthrough: received empty target in Build()" 2 | time="2023-08-27T15:09:35+08:00" level=info msg="获取用户服务实例--成功--" 3 | time="2023-08-27T15:09:35+08:00" level=info msg="获取视频服务实例--成功--" 4 | time="2023-08-27T15:09:35+08:00" level=info msg="获取社交服务实例--成功--" 5 | time="2023-08-27T15:09:35+08:00" level=info msg="请求过快---user_service服务熔断" 6 | time="2023-08-27T15:09:35+08:00" level=info msg="请求过快---video_service服务熔断" 7 | time="2023-08-27T15:09:35+08:00" level=info msg="请求过快---social_service服务熔断" 8 | time="2023-08-27T15:12:57+08:00" level=info msg="获取用户服务实例--成功--" 9 | time="2023-08-27T15:12:57+08:00" level=info msg="获取视频服务实例--成功--" 10 | time="2023-08-27T15:12:57+08:00" level=info msg="获取社交服务实例--成功--" 11 | time="2023-08-27T15:12:57+08:00" level=info msg="请求过快---user_service服务熔断" 12 | time="2023-08-27T15:12:57+08:00" level=info msg="请求过快---video_service服务熔断" 13 | time="2023-08-27T15:12:57+08:00" level=info msg="请求过快---social_service服务熔断" 14 | -------------------------------------------------------------------------------- /api_router/logs/2023-08-27.log1800: -------------------------------------------------------------------------------- 1 | time="2023-08-27T20:40:40+08:00" level=fatal msg="failed to build resolver: passthrough: received empty target in Build()" 2 | time="2023-08-27T20:40:46+08:00" level=fatal msg="failed to build resolver: passthrough: received empty target in Build()" 3 | time="2023-08-27T20:41:12+08:00" level=info msg="获取用户服务实例--成功--" 4 | time="2023-08-27T20:41:12+08:00" level=info msg="获取视频服务实例--成功--" 5 | time="2023-08-27T20:41:12+08:00" level=info msg="获取社交服务实例--成功--" 6 | time="2023-08-27T20:41:12+08:00" level=info msg="请求过快---user_service服务熔断" 7 | time="2023-08-27T20:41:12+08:00" level=info msg="请求过快---video_service服务熔断" 8 | time="2023-08-27T20:41:12+08:00" level=info msg="请求过快---social_service服务熔断" 9 | time="2023-08-27T20:53:22+08:00" level=info msg="获取用户服务实例--成功--" 10 | time="2023-08-27T20:53:22+08:00" level=info msg="获取视频服务实例--成功--" 11 | time="2023-08-27T20:53:22+08:00" level=info msg="获取社交服务实例--成功--" 12 | time="2023-08-27T20:53:22+08:00" level=info msg="请求过快---user_service服务熔断" 13 | time="2023-08-27T20:53:22+08:00" level=info msg="请求过快---video_service服务熔断" 14 | time="2023-08-27T20:53:22+08:00" level=info msg="请求过快---social_service服务熔断" 15 | -------------------------------------------------------------------------------- /api_router/logs/2023-08-28.log: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marinezz/Tiny-TikTok/d78369dabfa86fd1d59dce7140d0f7e9afaf70b8/api_router/logs/2023-08-28.log -------------------------------------------------------------------------------- /api_router/logs/2023-08-28.log1200: -------------------------------------------------------------------------------- 1 | time="2023-08-28T13:30:56+08:00" level=info msg="获取用户服务实例--成功--" 2 | time="2023-08-28T13:30:56+08:00" level=info msg="获取视频服务实例--成功--" 3 | time="2023-08-28T13:30:56+08:00" level=info msg="获取社交服务实例--成功--" 4 | time="2023-08-28T13:30:56+08:00" level=info msg="请求过快---user_service服务熔断" 5 | time="2023-08-28T13:30:56+08:00" level=info msg="请求过快---video_service服务熔断" 6 | time="2023-08-28T13:30:56+08:00" level=info msg="请求过快---social_service服务熔断" 7 | time="2023-08-28T13:35:17+08:00" level=info msg="获取用户服务实例--成功--" 8 | time="2023-08-28T13:35:17+08:00" level=info msg="获取视频服务实例--成功--" 9 | time="2023-08-28T13:35:17+08:00" level=info msg="获取社交服务实例--成功--" 10 | time="2023-08-28T13:35:17+08:00" level=info msg="请求过快---user_service服务熔断" 11 | time="2023-08-28T13:35:17+08:00" level=info msg="请求过快---video_service服务熔断" 12 | time="2023-08-28T13:35:17+08:00" level=info msg="请求过快---social_service服务熔断" 13 | time="2023-08-28T13:36:50+08:00" level=info msg="获取用户服务实例--成功--" 14 | time="2023-08-28T13:36:50+08:00" level=info msg="获取视频服务实例--成功--" 15 | time="2023-08-28T13:36:50+08:00" level=info msg="获取社交服务实例--成功--" 16 | time="2023-08-28T13:36:50+08:00" level=info msg="请求过快---user_service服务熔断" 17 | time="2023-08-28T13:36:50+08:00" level=info msg="请求过快---video_service服务熔断" 18 | time="2023-08-28T13:36:50+08:00" level=info msg="请求过快---social_service服务熔断" 19 | time="2023-08-28T13:38:38+08:00" level=info msg="获取用户服务实例--成功--" 20 | time="2023-08-28T13:38:38+08:00" level=info msg="获取视频服务实例--成功--" 21 | time="2023-08-28T13:38:38+08:00" level=info msg="获取社交服务实例--成功--" 22 | time="2023-08-28T13:38:38+08:00" level=info msg="请求过快---user_service服务熔断" 23 | time="2023-08-28T13:38:38+08:00" level=info msg="请求过快---video_service服务熔断" 24 | time="2023-08-28T13:38:38+08:00" level=info msg="请求过快---social_service服务熔断" 25 | time="2023-08-28T13:42:57+08:00" level=info msg="获取用户服务实例--成功--" 26 | time="2023-08-28T13:42:57+08:00" level=info msg="获取视频服务实例--成功--" 27 | time="2023-08-28T13:42:57+08:00" level=info msg="获取社交服务实例--成功--" 28 | time="2023-08-28T13:42:57+08:00" level=info msg="请求过快---user_service服务熔断" 29 | time="2023-08-28T13:42:57+08:00" level=info msg="请求过快---video_service服务熔断" 30 | time="2023-08-28T13:42:57+08:00" level=info msg="请求过快---social_service服务熔断" 31 | time="2023-08-28T13:57:53+08:00" level=info msg="获取用户服务实例--成功--" 32 | time="2023-08-28T13:57:53+08:00" level=info msg="获取视频服务实例--成功--" 33 | time="2023-08-28T13:57:53+08:00" level=info msg="获取社交服务实例--成功--" 34 | time="2023-08-28T13:57:53+08:00" level=info msg="请求过快---user_service服务熔断" 35 | time="2023-08-28T13:57:53+08:00" level=info msg="请求过快---video_service服务熔断" 36 | time="2023-08-28T13:57:53+08:00" level=info msg="请求过快---social_service服务熔断" 37 | -------------------------------------------------------------------------------- /api_router/logs/2023-09-04.log: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marinezz/Tiny-TikTok/d78369dabfa86fd1d59dce7140d0f7e9afaf70b8/api_router/logs/2023-09-04.log -------------------------------------------------------------------------------- /api_router/logs/2023-09-04.log0900: -------------------------------------------------------------------------------- 1 | time="2023-09-04T11:16:56+08:00" level=info msg="获取用户服务实例--成功--" 2 | time="2023-09-04T11:16:56+08:00" level=info msg="获取视频服务实例--成功--" 3 | time="2023-09-04T11:16:56+08:00" level=info msg="获取社交服务实例--成功--" 4 | time="2023-09-04T11:16:56+08:00" level=info msg="请求过快---user_service服务熔断" 5 | time="2023-09-04T11:16:56+08:00" level=info msg="请求过快---video_service服务熔断" 6 | time="2023-09-04T11:16:56+08:00" level=info msg="请求过快---social_service服务熔断" 7 | time="2023-09-04T11:19:39+08:00" level=info msg="FollowService--errorrpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:9108->127.0.0.1:10003: wsarecv: An existing connection was forcibly closed by the remote host." 8 | time="2023-09-04T11:23:12+08:00" level=info msg="FollowService--errorrpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:14985->127.0.0.1:10003: wsarecv: An existing connection was forcibly closed by the remote host." 9 | time="2023-09-04T11:31:04+08:00" level=info msg="FollowService--errorrpc error: code = Unavailable desc = connection error: desc = \"transport: Error while dialing: dial tcp 127.0.0.1:10003: connectex: No connection could be made because the target machine actively refused it.\"" 10 | -------------------------------------------------------------------------------- /api_router/logs/2023-09-04.log1500: -------------------------------------------------------------------------------- 1 | time="2023-09-04T15:16:00+08:00" level=info msg="获取用户服务实例--成功--" 2 | time="2023-09-04T15:16:00+08:00" level=info msg="获取视频服务实例--成功--" 3 | time="2023-09-04T15:16:00+08:00" level=info msg="获取社交服务实例--成功--" 4 | time="2023-09-04T15:16:00+08:00" level=info msg="请求过快---user_service服务熔断" 5 | time="2023-09-04T15:16:00+08:00" level=info msg="请求过快---video_service服务熔断" 6 | time="2023-09-04T15:16:00+08:00" level=info msg="请求过快---social_service服务熔断" 7 | -------------------------------------------------------------------------------- /api_router/pkg/auth/jwt.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | "errors" 5 | "github.com/dgrijalva/jwt-go" 6 | "github.com/spf13/viper" 7 | "time" 8 | ) 9 | 10 | type Claims struct { 11 | UserId int64 `json:"user_id"` 12 | jwt.StandardClaims 13 | } 14 | 15 | const TokenExpireDuration = time.Hour * 24 * 30 // 设置过期时间 16 | 17 | var Secret = []byte(viper.GetString("server.jwtSecret")) // 设置密码,配置文件中读取 18 | 19 | // GenerateToken 签发Token 20 | func GenerateToken(userId int64) (string, error) { 21 | now := time.Now() 22 | // 创建一个自己的声明 23 | claims := Claims{ 24 | UserId: userId, 25 | StandardClaims: jwt.StandardClaims{ 26 | ExpiresAt: now.Add(TokenExpireDuration).Unix(), // 过期时间: 当前时间 + 过期时间 27 | Issuer: "admin", // 签发人 28 | }, 29 | } 30 | // 创建签名对象 31 | tokenClaims := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) 32 | // 获取token 33 | token, err := tokenClaims.SignedString(Secret) 34 | 35 | return token, err 36 | } 37 | 38 | // ParseToken 解析token 39 | func ParseToken(token string) (*Claims, error) { 40 | tokenClaims, err := jwt.ParseWithClaims(token, &Claims{}, func(token *jwt.Token) (interface{}, error) { 41 | return Secret, nil 42 | }) 43 | if err != nil { 44 | return nil, err 45 | } 46 | 47 | if tokenClaims != nil { 48 | if claims, ok := tokenClaims.Claims.(*Claims); ok && tokenClaims.Valid { 49 | return claims, nil 50 | } 51 | } 52 | return nil, errors.New("invalid token") 53 | } 54 | -------------------------------------------------------------------------------- /api_router/pkg/logger/log.go: -------------------------------------------------------------------------------- 1 | package logger 2 | 3 | import ( 4 | rotatelogs "github.com/lestrrat-go/file-rotatelogs" 5 | "github.com/sirupsen/logrus" 6 | "log" 7 | "os" 8 | "path" 9 | "path/filepath" 10 | "runtime" 11 | "time" 12 | ) 13 | 14 | var Log *logrus.Logger 15 | 16 | func init() { 17 | // 如果实例存在则不用新建 18 | if Log != nil { 19 | fileName := getFileDir() 20 | witter := rotateLog(fileName) 21 | Log.Out = witter 22 | return 23 | } 24 | 25 | logger := logrus.New() 26 | fileName := getFileDir() 27 | witter := rotateLog(fileName) 28 | 29 | // 设置输出文件 30 | logger.Out = witter 31 | // 设置日志级别 32 | logger.SetLevel(logrus.DebugLevel) 33 | 34 | Log = logger 35 | } 36 | 37 | // 获取日志输出路径 38 | func getFileDir() string { 39 | now := time.Now() 40 | // 获取指定路径 41 | _, filePath, _, _ := runtime.Caller(0) 42 | logsPath := filepath.Join(filePath, "..", "..", "..", "logs") 43 | 44 | // 文件名称 45 | logFileName := now.Format("2006-01-02") + ".log" 46 | fileName := path.Join(logsPath, logFileName) 47 | 48 | // 查看文件是否存在,不存在则创建 49 | if _, err := os.Stat(fileName); err != nil { 50 | if _, err := os.Create(fileName); err != nil { 51 | log.Println(err.Error()) 52 | } 53 | } 54 | 55 | return fileName 56 | } 57 | 58 | // 日志本地文件分割 59 | func rotateLog(fileName string) *rotatelogs.RotateLogs { 60 | witter, _ := rotatelogs.New( 61 | fileName+"%H%M", 62 | rotatelogs.WithLinkName(fileName), 63 | // 日志最长保留时间 64 | rotatelogs.WithMaxAge(time.Duration(12)*time.Hour), 65 | // 日志轮转的时间间隔 66 | rotatelogs.WithRotationTime(time.Duration(3)*time.Hour), 67 | ) 68 | 69 | return witter 70 | } 71 | -------------------------------------------------------------------------------- /api_router/pkg/logger/log_test.go: -------------------------------------------------------------------------------- 1 | package logger 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestLog(t *testing.T) { 8 | 9 | //_, filePath, _, _ := runtime.Caller(0) 10 | //join := filepath.Join(filePath, "..", "..", "..", "logs") 11 | //print(join) 12 | 13 | Log.Info("222") 14 | } 15 | -------------------------------------------------------------------------------- /api_router/pkg/res/response.go: -------------------------------------------------------------------------------- 1 | package res 2 | 3 | type User struct { 4 | // 用户服务 5 | Id int64 `json:"id"` 6 | Name string `json:"name"` 7 | // 社交服务 8 | FollowCount int64 `json:"follow_count"` 9 | FollowerCount int64 `json:"follower_count"` 10 | IsFollow bool `json:"is_follow"` 11 | // 用户服务 12 | Avatar string `json:"avatar"` 13 | BackgroundImage string `json:"background_image"` 14 | Signature string `json:"signature"` 15 | // 视频服务 16 | TotalFavorited string `json:"total_favorited"` // 获赞数量 17 | WorkCount int64 `json:"work_count"` // 作品数量 18 | FavoriteCount int64 `json:"favorite_count"` // 喜欢数量 19 | } 20 | 21 | type Comment struct { 22 | Id int64 `json:"id"` // 评论id 23 | User User `json:"user"` 24 | Content string `json:"content"` // 评论内容 25 | CreateDate string `json:"create_date"` // 评论发布日期,格式 mm-dd 26 | } 27 | 28 | type Video struct { 29 | Id int64 `json:"id"` 30 | Author User `json:"author"` 31 | PlayUrl string `json:"play_url"` 32 | CoverUrl string `json:"cover_url"` 33 | FavoriteCount int64 `json:"favorite_count"` 34 | CommentCount int64 `json:"comment_count"` 35 | IsFavorite bool `json:"is_favorite"` 36 | Title string `json:"title"` 37 | } 38 | 39 | type FeedResponse struct { 40 | StatusCode int64 `json:"status_code"` 41 | NextTime int64 `json:"next_time"` 42 | StatusMsg string `json:"status_msg"` 43 | VideoList []Video `json:"video_list"` 44 | } 45 | 46 | type UserResponse struct { 47 | StatusCode int64 `json:"status_code"` 48 | StatusMsg string `json:"status_msg,omitempty"` 49 | UserId int64 `json:"user_id"` 50 | Token string `json:"token"` 51 | } 52 | 53 | type UserInfoResponse struct { 54 | StatusCode int64 `json:"status_code"` 55 | StatusMsg string `json:"status_msg,omitempty"` 56 | User User `json:"user"` 57 | } 58 | 59 | type VideoListResponse struct { 60 | StatusCode int64 `json:"status_code"` // 状态码,0-成功,其他值-失败 61 | StatusMsg string `json:"status_msg"` // 返回状态描述 62 | VideoList []Video `json:"video_list"` // 用户点赞视频列表 63 | } 64 | 65 | type PublishActionResponse struct { 66 | StatusCode int64 `json:"status_code"` 67 | StatusMsg string `json:"status_msg,omitempty"` 68 | } 69 | 70 | type FavoriteActionResponse struct { 71 | StatusCode int64 `json:"status_code"` 72 | StatusMsg string `json:"status_msg"` 73 | } 74 | 75 | type CommentActionResponse struct { 76 | StatusCode int64 `json:"status_code"` 77 | StatusMsg string `json:"status_msg"` 78 | Comment Comment `json:"comment"` // 评论成功返回评论内容,不需要重新拉取整个列表 79 | } 80 | 81 | type CommentDeleteResponse struct { 82 | StatusCode int64 `json:"status_code"` 83 | StatusMsg string `json:"status_msg"` 84 | } 85 | 86 | type CommentListResponse struct { 87 | StatusCode int64 `json:"status_code"` 88 | StatusMsg string `json:"status_msg"` 89 | Comments []Comment `json:"comment_list"` 90 | } 91 | 92 | type FollowActionResponse struct { 93 | StatusCode int32 `json:"status_code"` // 状态码,0-成功,其他值-失败 94 | StatusMsg string `json:"status_msg"` // 返回状态描述 95 | } 96 | 97 | type FollowListResponse struct { 98 | StatusCode int32 `json:"status_code"` // 状态码,0-成功,其他值-失败 99 | StatusMsg string `json:"status_msg"` // 返回状态描述 100 | UserList []User `json:"user_list"` 101 | } 102 | 103 | type PostMessageResponse struct { 104 | StatusCode int32 `json:"status_code"` // 状态码,0-成功,其他值-失败 105 | StatusMsg string `json:"status_msg"` // 返回状态描述 106 | } 107 | 108 | type Message struct { 109 | Id int64 `json:"id"` 110 | ToUserId int64 `json:"to_user_id"` 111 | FromUserID int64 `json:"from_user_id"` 112 | Content string `json:"content"` 113 | CreateTime int64 `json:"create_time"` 114 | } 115 | 116 | type GetMessageResponse struct { 117 | StatusCode int32 `json:"status_code"` // 状态码,0-成功,其他值-失败 118 | StatusMsg string `json:"status_msg"` // 返回状态描述 119 | MessageList []Message `json:"message_list"` 120 | } 121 | -------------------------------------------------------------------------------- /api_router/pkg/wrapper/hystrix.go: -------------------------------------------------------------------------------- 1 | package wrapper 2 | 3 | import ( 4 | "api_router/pkg/logger" 5 | "errors" 6 | "fmt" 7 | "github.com/afex/hystrix-go/hystrix" 8 | ) 9 | 10 | func NewWrapper(name string) { 11 | // 设置 Hystrix 配置 12 | hystrix.ConfigureCommand("my_command", hystrix.CommandConfig{ 13 | Timeout: 1000, // 超时时间(毫秒) 14 | MaxConcurrentRequests: 10, // 最大并发请求数 15 | RequestVolumeThreshold: 5, // 触发熔断的最小请求数,大于这个值才开始做熔断检测 16 | SleepWindow: 5000, // 熔断后休眠时间(毫秒) 17 | ErrorPercentThreshold: 20, // 错误率阈值 18 | }) 19 | 20 | // 使用 Hystrix 执行命令 21 | err := hystrix.Do(name, func() error { 22 | return errors.New("服务熔断") 23 | }, nil) 24 | if err != nil { 25 | logger.Log.Info("请求过快---" + name + "服务熔断") 26 | fmt.Println("err", err) 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /api_router/router/middleware/error.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "fmt" 5 | "github.com/gin-gonic/gin" 6 | "net/http" 7 | "utils/exception" 8 | ) 9 | 10 | // ErrorMiddleWare 错误处理中间件,捕获panic抛出异常 11 | func ErrorMiddleWare() gin.HandlerFunc { 12 | return func(c *gin.Context) { 13 | defer func() { 14 | r := recover() 15 | if r != nil { 16 | c.JSON(http.StatusOK, gin.H{ 17 | "status_code": exception.ERROR, 18 | // 打印具体错误 19 | "status_msg": fmt.Sprintf("%s", r), 20 | }) 21 | // 中断 22 | c.Abort() 23 | } 24 | }() 25 | c.Next() 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /api_router/router/middleware/jwt.go: -------------------------------------------------------------------------------- 1 | // Package middleware token校验中间件 2 | package middleware 3 | 4 | import ( 5 | "api_router/pkg/auth" 6 | "github.com/gin-gonic/gin" 7 | "net/http" 8 | "time" 9 | "utils/exception" 10 | ) 11 | 12 | func JWTMiddleware() gin.HandlerFunc { 13 | return func(c *gin.Context) { 14 | var code int 15 | code = exception.SUCCESS 16 | // token 可能在query中也可能在postForm中 17 | token := c.Query("token") 18 | if token == "" { 19 | token = c.PostForm("token") 20 | } 21 | // token不存在 22 | if token == "" { 23 | code = exception.RequestERROR 24 | } 25 | 26 | // 验证token(验证不通过,或者超时) 27 | claims, err := auth.ParseToken(token) 28 | if err != nil { 29 | code = exception.UnAuth 30 | } else if time.Now().Unix() > claims.ExpiresAt { 31 | code = exception.TokenTimeOut 32 | } 33 | 34 | if code != exception.SUCCESS { 35 | c.JSON(http.StatusOK, gin.H{ 36 | "StatusCode": code, 37 | "StatusMsg": exception.GetMsg(code), 38 | }) 39 | c.Abort() 40 | return 41 | } 42 | c.Set("user_id", claims.UserId) 43 | c.Next() 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /api_router/router/middleware/serve.go: -------------------------------------------------------------------------------- 1 | // 服务中间件,接收服务实例,并保存到context.Key中 2 | 3 | package middleware 4 | 5 | import ( 6 | "github.com/gin-gonic/gin" 7 | ) 8 | 9 | func ServeMiddleware(serveInstance map[string]interface{}) gin.HandlerFunc { 10 | return func(c *gin.Context) { 11 | // 如果直接复制,浅拷贝会导致map冲突 12 | c.Keys = make(map[string]interface{}) 13 | for key, value := range serveInstance { 14 | c.Keys[key] = value 15 | } 16 | c.Next() 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /api_router/router/router.go: -------------------------------------------------------------------------------- 1 | // Package router 路由 2 | package router 3 | 4 | import ( 5 | "api_router/internal/handler" 6 | "api_router/router/middleware" 7 | "github.com/gin-gonic/gin" 8 | "net/http" 9 | ) 10 | 11 | func InitRouter(serveInstance map[string]interface{}) *gin.Engine { 12 | r := gin.Default() 13 | 14 | r.Use(middleware.ServeMiddleware(serveInstance), middleware.ErrorMiddleWare()) 15 | // 测试 16 | r.GET("/ping", func(c *gin.Context) { 17 | c.JSON(http.StatusOK, gin.H{ 18 | "message": "ok", 19 | }) 20 | }) 21 | baseGroup := r.Group("/douyin") 22 | { 23 | // 视频流 24 | baseGroup.GET("/feed/", handler.Feed) 25 | 26 | // 用户 27 | baseGroup.POST("/user/register/", handler.UserRegister) 28 | baseGroup.POST("/user/login/", handler.UserLogin) 29 | baseGroup.GET("/user/", middleware.JWTMiddleware(), handler.UserInfo) 30 | 31 | // 视频 32 | publishGroup := baseGroup.Group("/publish") 33 | publishGroup.Use(middleware.JWTMiddleware()) 34 | { 35 | publishGroup.POST("/action/", handler.PublishAction) 36 | publishGroup.GET("/list/", handler.PublishList) 37 | } 38 | favoriteGroup := baseGroup.Group("favorite") 39 | favoriteGroup.Use(middleware.JWTMiddleware()) 40 | { 41 | favoriteGroup.POST("action/", handler.FavoriteAction) 42 | favoriteGroup.GET("list/", handler.FavoriteList) 43 | } 44 | commentGroup := baseGroup.Group("/comment") 45 | commentGroup.Use(middleware.JWTMiddleware()) 46 | { 47 | commentGroup.POST("/action/", handler.CommentAction) 48 | commentGroup.GET("/list/", handler.CommentList) 49 | } 50 | // 社交 51 | relationGroup := baseGroup.Group("/relation") 52 | relationGroup.Use(middleware.JWTMiddleware()) 53 | { 54 | relationGroup.POST("/action/", handler.FollowAction) 55 | relationGroup.GET("/follow/list/", handler.GetFollowList) 56 | relationGroup.GET("/follower/list/", handler.GetFollowerList) 57 | relationGroup.GET("/friend/list/", handler.GetFriendList) 58 | } 59 | messageGroup := baseGroup.Group("/message") 60 | messageGroup.Use(middleware.JWTMiddleware()) 61 | { 62 | messageGroup.POST("/action/", handler.PostMessage) 63 | messageGroup.GET("/chat/", handler.GetMessage) 64 | } 65 | } 66 | return r 67 | } 68 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Tiny-TikTok 2 | ## 1 项目介绍 3 | 4 | ### 1.1 概述 5 | 6 | 简易抖音项目后端实现 7 | 8 | ### 1.2 成员介绍 9 | 10 | Ben 11 | 12 | Marine 13 | 14 | ## 2 项目概览 15 | 16 | ### 2.1 技术选型与设计 17 | [img](https://v1rwxew1bdp.feishu.cn/space/api/box/stream/download/asynccode/?code=MmJlMjRhMzJjZDc0OGNjYTIyY2JhOGQzMGQ2YmM1OGRfT2NKdmZWYkVnalpxbHBmeUtCTE1HUnB3djY2SEtUYW5fVG9rZW46U2kxbGJQOVE3b0hOWnB4M1p4bWNJZ2pKbjliXzE2OTE3NTA4MTg6MTY5MTc1NDQxOF9WNA) 18 | 19 | * Gin:Web框架。高性能、轻量级、简洁,被广泛用于构建RESTful API、网站和其它HTTP服务 20 | 21 | * **JWT:**用于身份的验证和授权,具有跨平台、无状态的优点,不需要再会话中保存任何信息,减轻服务器的负担 22 | 23 | * **Hystrix:**服务熔断,防止由于单个服务的故障导致整个系统的崩溃 24 | 25 | * **Etcd + grpc:**etcd实现服务注册和服务发现,grpc负责通信,构建健壮的分布式系统 26 | 27 | * **OSS:**对象存储服务器,用于存储和管理非结构化数据,比如图片、视频等 28 | 29 | * **FFmpeg:**多媒体开源工具,本项目用于上传视频时封面的截取 30 | 31 | * **Gorm:**ORM库,用于操作数据库 32 | 33 | * **MySQL:**关系型数据库,用于存储结构化数据 34 | 35 | * **Redis:**键值存储数据库,以内存作为数据存储介质,提供高性能读写 36 | 37 | * **RabbitMQ:**消息中间件,用于程序之间传递消息,支持消息的发布和订阅,支持消息异步传递,实现解耦和异步处理 38 | 39 | 40 | 41 | ### 2.2 总体设计 42 | 43 | ![img](https://v1rwxew1bdp.feishu.cn/space/api/box/stream/download/asynccode/?code=ZGE3ODFmZDU2YmNiM2VhMTdjZmQ1NWUxNDY1MzlkYWFfOFVQajI1WWJSeG4yalJ6UG1aSkpQYnhGVUdvVVk0TVdfVG9rZW46T1M0RmJFVFpob0lqUjR4cndvWmNKZTRPbjdiXzE2OTE3NTA4MzY6MTY5MTc1NDQzNl9WNA) 44 | 45 | * 请求到达服务器前会对token进行校验 46 | * 通过Api_Router对外暴露接口,进入服务,网关微服务对其它服务进行服务熔断和服务限流 47 | * 各个服务先注册进入ETCD,api_router对各个服务进行调用,组装信息返回给服务端 48 | * api_router通过gprc实现服务之间的通讯 49 | * 服务操作数据库,将信息返回给上一层 50 | 51 | 52 | 53 | ### 2.3 项目结构设计 54 | 55 | * **总目录结构** 56 | 57 | ```bash 58 | ├─api_router # 路由网关 59 | ├─docs # 项目文档 60 | ├─social_service # 社交服务 61 | ├─user_service # 用户服务 62 | ├─utils # 工具函数包 63 | └─video_service # 视频服务 64 | ``` 65 | 66 | 67 | 68 | * **路由网关** 69 | 70 | ```bash 71 | ├─api_router 72 | │ ├─cmd 73 | │ ├─config # 项目配置文件 74 | │ ├─discovery # 服务注册与发现 75 | │ ├─internal 76 | │ │ ├─handler 77 | │ │ └─service 78 | │ │ └─pb 79 | │ ├─pkg 80 | │ │ ├─auth 81 | │ │ └─res 82 | │ └─router # 路由和中间件 83 | │ └─middleware 84 | ``` 85 | 86 | ​ **/cmd:**一个项目可以有很多个组件,吧main函数所在文件夹同一放在/cmd目录下 87 | 88 | ​ **/internal:**存放私有应用代码。handler类似三层架构中的控制层,service类似服务层,路由不用操作数据库,所以没有持久层 89 | 90 | ​ **/pkg:**存放可以被外部使用的代码库。auth中存放token鉴权,res存放对服务端的统一返回 91 | 92 | 93 | 94 | * **具体服务** 95 | 96 | ```bash 97 | ├─user_service 98 | │ ├─cmd 99 | │ ├─config 100 | │ ├─discovery 101 | │ ├─internal 102 | │ │ ├─handler 103 | │ │ ├─model 104 | │ │ └─service # 持久化层 105 | │ │ └─pb 106 | │ └─pkg 107 | │ └─encryption # 密码加密 108 | ``` 109 | 110 | 111 | 112 | * **工具函数包** 113 | 114 | ```bash 115 | ├─utils 116 | │ ├─etcd # etcd服务注册与发现组件 117 | │ ├─exception 118 | │ └─snowFlake # 雪花算法生成ID 119 | ``` 120 | 121 | 122 | 123 | ### 2.4 数据库设计 124 | 125 | ![img](https://v1rwxew1bdp.feishu.cn/space/api/box/stream/download/asynccode/?code=NjQ1NDIzMDQ3YTUwY2U0ZDllNzA5ODc5YjEyMTIyZDNfa0NKQlVCMEZkaWpNMWRJYVdsWEFONlVJb0JaeExLdTFfVG9rZW46VXRMVGI3Qjg1b1VKUjN4T3JNRWN3bkxIbjNrXzE2OTE3NTA4NDY6MTY5MTc1NDQ0Nl9WNA) 126 | 127 | **用户表:**用于存储用户名称、密码、头像、背景、用户简介信息,以由雪花算法生成的分布式id作为主键(其余表的ID同理),密码由bcrypt函数进行加密。 128 | 129 | **视频表:**用于存储视频的作者、标题、封面路径、视频路径、获赞数量以及评论数量,以视频作者的id关联用户表。 130 | 131 | **评论表:**用于存储视频的评论信息、评论创建时间、评论状态,通过用户id以及视频id关联用户表和视频表,通过评论状态作为软删除判断当前评论是否存在。 132 | 133 | **消息表:**用于存放用户发送的消息以及消息的创建时间,通过用户id关联用户表,记录消息的发生者和消息的接收者 134 | 135 | **关注表:**用户存放用户的关注信息,通过用户id关联用户表获取关注者和被关注者 136 | 137 | **点赞表:**用于存放视频的点赞信息,通过用户id关联用户表获取点赞的用户,视频id关联视频表获取被点赞的视频 138 | 139 | 140 | 141 | ## 3 详细设计 142 | 143 | ## 4 测试 144 | 145 | ## 5 项目演示 146 | 147 | ## 6 总结 148 | 149 | 150 | 151 | 152 | 153 | -------------------------------------------------------------------------------- /docs/image/总体设计图.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marinezz/Tiny-TikTok/d78369dabfa86fd1d59dce7140d0f7e9afaf70b8/docs/image/总体设计图.png -------------------------------------------------------------------------------- /docs/image/技术架构图.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marinezz/Tiny-TikTok/d78369dabfa86fd1d59dce7140d0f7e9afaf70b8/docs/image/技术架构图.png -------------------------------------------------------------------------------- /docs/image/数据库设计图.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marinezz/Tiny-TikTok/d78369dabfa86fd1d59dce7140d0f7e9afaf70b8/docs/image/数据库设计图.png -------------------------------------------------------------------------------- /docs/image/获奖证书.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marinezz/Tiny-TikTok/d78369dabfa86fd1d59dce7140d0f7e9afaf70b8/docs/image/获奖证书.png -------------------------------------------------------------------------------- /docs/image/项目获奖证书.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marinezz/Tiny-TikTok/d78369dabfa86fd1d59dce7140d0f7e9afaf70b8/docs/image/项目获奖证书.png -------------------------------------------------------------------------------- /docs/mysql/tables.sql: -------------------------------------------------------------------------------- 1 | /* 2 | 用户表 3 | */ 4 | CREATE TABLE `user` ( 5 | `id` bigint NOT NULL AUTO_INCREMENT, 6 | `user_name` varchar(256) DEFAULT NULL, 7 | `pass_word` varchar(256) NOT NULL, 8 | `avatar` varchar(256) DEFAULT NULL, 9 | `background_image` varchar(256) DEFAULT NULL, 10 | `signature` varchar(256) DEFAULT '该用户还没有简介', 11 | PRIMARY KEY (`id`), 12 | UNIQUE KEY `user_name` (`user_name`) 13 | ) ENGINE=InnoDB AUTO_INCREMENT=812575311663105 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci 14 | 15 | 16 | /* 17 | 视频表 18 | */ 19 | CREATE TABLE `video` ( 20 | `id` bigint NOT NULL AUTO_INCREMENT, 21 | `auth_id` bigint DEFAULT NULL, 22 | `title` varchar(256) DEFAULT NULL, 23 | `cover_url` varchar(256) DEFAULT NULL, 24 | `play_url` varchar(256) DEFAULT NULL, 25 | `favorite_count` bigint DEFAULT '0', 26 | `comment_count` bigint DEFAULT '0', 27 | `creat_at` datetime DEFAULT NULL, 28 | PRIMARY KEY (`id`) 29 | ) ENGINE=InnoDB AUTO_INCREMENT=2276964627783681 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci 30 | 31 | 32 | /* 33 | 消息表 34 | */ 35 | CREATE TABLE `message` ( 36 | `id` bigint NOT NULL AUTO_INCREMENT, 37 | `user_id` bigint DEFAULT NULL, 38 | `to_user_id` bigint DEFAULT NULL, 39 | `message` varchar(256) DEFAULT NULL, 40 | `created_at` varchar(256) DEFAULT NULL, 41 | PRIMARY KEY (`id`) 42 | ) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci 43 | 44 | 45 | /* 46 | 评论表 47 | */ 48 | CREATE TABLE `comment` ( 49 | `id` bigint NOT NULL AUTO_INCREMENT, 50 | `user_id` bigint DEFAULT NULL, 51 | `video_id` bigint DEFAULT NULL, 52 | `creat_at` datetime DEFAULT NULL, 53 | `comment_status` tinyint(1) DEFAULT NULL, 54 | PRIMARY KEY (`id`) 55 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci 56 | 57 | 58 | /* 59 | 点赞表 60 | */ 61 | CREATE TABLE `favorite` ( 62 | `id` bigint NOT NULL AUTO_INCREMENT, 63 | `user_id` bigint DEFAULT NULL, 64 | `video_id` bigint DEFAULT NULL, 65 | `is_favorite` tinyint(1) DEFAULT NULL, 66 | PRIMARY KEY (`id`) 67 | ) ENGINE=InnoDB AUTO_INCREMENT=2277278458191873 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci 68 | 69 | 70 | /* 71 | 关注表 72 | */ 73 | CREATE TABLE `follow` ( 74 | `id` bigint NOT NULL AUTO_INCREMENT, 75 | `user_id` bigint DEFAULT NULL, 76 | `to_user_id` bigint DEFAULT NULL, 77 | `is_follow` int DEFAULT (2), 78 | PRIMARY KEY (`id`) 79 | ) ENGINE=InnoDB AUTO_INCREMENT=7638289369411586 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci 80 | -------------------------------------------------------------------------------- /social_service/cmd/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "social/config" 5 | "social/discovery" 6 | "social/internal/model" 7 | "social/pkg/cache" 8 | ) 9 | 10 | func main() { 11 | config.InitConfig() 12 | model.InitDb() 13 | cache.InitRedis() 14 | go cache.TimerSync() 15 | discovery.AutoRegister() 16 | } 17 | -------------------------------------------------------------------------------- /social_service/config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "github.com/spf13/viper" 5 | "path" 6 | "runtime" 7 | "strings" 8 | ) 9 | 10 | // Config 数据结构 11 | type Config struct { 12 | Mysql struct { 13 | Host string 14 | Port string 15 | Username string 16 | Password string 17 | Database string 18 | } 19 | } 20 | 21 | // ConfigData 配置数据变量 22 | var ConfigData *Config 23 | 24 | // InitConfig 读取配置文件 25 | func InitConfig() { 26 | _, filePath, _, _ := runtime.Caller(0) 27 | 28 | currentDir := path.Dir(filePath) 29 | 30 | viper.SetConfigName("config") 31 | viper.SetConfigType("yaml") 32 | viper.AddConfigPath(currentDir) 33 | 34 | if err := viper.ReadInConfig(); err != nil { 35 | panic(err) 36 | } 37 | 38 | } 39 | 40 | // DbDnsInit 拼接链接数据库的DNS 41 | func DbDnsInit() string { 42 | host := viper.GetString("mysql.host") 43 | port := viper.GetString("mysql.port") 44 | username := viper.GetString("mysql.username") 45 | password := viper.GetString("mysql.password") 46 | database := viper.GetString("mysql.database") 47 | 48 | InitConfig() 49 | dns := strings.Join([]string{username, ":", password, "@tcp(", host, ":", port, ")/", database, "?charset=utf8&parseTime=True&loc=Local"}, "") 50 | 51 | return dns 52 | } 53 | -------------------------------------------------------------------------------- /social_service/config/config.yaml: -------------------------------------------------------------------------------- 1 | server: 2 | name: social_service # 服务名称 3 | address: 127.0.0.1:10003 # 服务地址 4 | 5 | 6 | mysql: 7 | driver: mysql 8 | host: 127.0.0.1 9 | port: 3306 10 | username: root 11 | password: 123456 12 | database: tiny_tiktok 13 | 14 | 15 | etcd: 16 | address: 127.0.0.1:2379 17 | 18 | redis: 19 | address: 127.0.0.1:6379 20 | password: 123456 -------------------------------------------------------------------------------- /social_service/config/config_test.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "fmt" 5 | "github.com/spf13/viper" 6 | "path" 7 | "runtime" 8 | "testing" 9 | ) 10 | 11 | func TestInitConfig(t *testing.T) { 12 | _, filePath, _, _ := runtime.Caller(0) 13 | 14 | currentDir := path.Dir(filePath) 15 | 16 | fmt.Println(currentDir) 17 | } 18 | 19 | func TestDbDnsInit(t *testing.T) { 20 | InitConfig() 21 | fmt.Printf("host is : %v \n", viper.GetString("mysql.host")) 22 | dns := DbDnsInit() 23 | fmt.Printf("dns is : %v", dns) 24 | } 25 | -------------------------------------------------------------------------------- /social_service/discovery/autoRegister.go: -------------------------------------------------------------------------------- 1 | package discovery 2 | 3 | import ( 4 | "github.com/spf13/viper" 5 | "google.golang.org/grpc" 6 | "log" 7 | "net" 8 | "social/internal/handler" 9 | "social/internal/service" 10 | "utils/etcd" 11 | ) 12 | 13 | // AutoRegister etcd自动注册 14 | func AutoRegister() { 15 | etcdAddress := viper.GetString("etcd.address") 16 | etcdRegister, err := etcd.NewEtcdRegister(etcdAddress) 17 | 18 | if err != nil { 19 | log.Fatal(err) 20 | } 21 | 22 | serviceName := viper.GetString("server.name") 23 | serviceAddress := viper.GetString("server.address") 24 | err = etcdRegister.ServiceRegister(serviceName, serviceAddress, 30) 25 | if err != nil { 26 | log.Fatal(err) 27 | } 28 | 29 | listener, err := net.Listen("tcp", serviceAddress) 30 | if err != nil { 31 | log.Fatal(err) 32 | } 33 | 34 | server := grpc.NewServer() 35 | service.RegisterSocialServiceServer(server, handler.NewSocialService()) 36 | 37 | err = server.Serve(listener) 38 | if err != nil { 39 | log.Fatal(err) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /social_service/go.mod: -------------------------------------------------------------------------------- 1 | module social 2 | 3 | go 1.19 4 | 5 | require ( 6 | github.com/gin-gonic/gin v1.9.1 7 | github.com/go-redis/redis/v8 v8.11.5 8 | github.com/spf13/viper v1.16.0 9 | golang.org/x/sync v0.1.0 10 | google.golang.org/grpc v1.57.0 11 | google.golang.org/protobuf v1.31.0 12 | gorm.io/driver/mysql v1.5.1 13 | gorm.io/gorm v1.25.3 14 | utils v0.0.0 15 | ) 16 | 17 | replace utils => ../utils 18 | 19 | require ( 20 | github.com/bytedance/sonic v1.9.1 // indirect 21 | github.com/cespare/xxhash/v2 v2.2.0 // indirect 22 | github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect 23 | github.com/coreos/go-semver v0.3.0 // indirect 24 | github.com/coreos/go-systemd/v22 v22.3.2 // indirect 25 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect 26 | github.com/fsnotify/fsnotify v1.6.0 // indirect 27 | github.com/gabriel-vasile/mimetype v1.4.2 // indirect 28 | github.com/gin-contrib/sse v0.1.0 // indirect 29 | github.com/go-playground/locales v0.14.1 // indirect 30 | github.com/go-playground/universal-translator v0.18.1 // indirect 31 | github.com/go-playground/validator/v10 v10.14.0 // indirect 32 | github.com/go-sql-driver/mysql v1.7.0 // indirect 33 | github.com/goccy/go-json v0.10.2 // indirect 34 | github.com/gogo/protobuf v1.3.2 // indirect 35 | github.com/golang/protobuf v1.5.3 // indirect 36 | github.com/hashicorp/hcl v1.0.0 // indirect 37 | github.com/jinzhu/inflection v1.0.0 // indirect 38 | github.com/jinzhu/now v1.1.5 // indirect 39 | github.com/json-iterator/go v1.1.12 // indirect 40 | github.com/klauspost/cpuid/v2 v2.2.4 // indirect 41 | github.com/leodido/go-urn v1.2.4 // indirect 42 | github.com/magiconair/properties v1.8.7 // indirect 43 | github.com/mattn/go-isatty v0.0.19 // indirect 44 | github.com/mitchellh/mapstructure v1.5.0 // indirect 45 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 46 | github.com/modern-go/reflect2 v1.0.2 // indirect 47 | github.com/pelletier/go-toml/v2 v2.0.8 // indirect 48 | github.com/robfig/cron/v3 v3.0.1 // indirect 49 | github.com/spf13/afero v1.9.5 // indirect 50 | github.com/spf13/cast v1.5.1 // indirect 51 | github.com/spf13/jwalterweatherman v1.1.0 // indirect 52 | github.com/spf13/pflag v1.0.5 // indirect 53 | github.com/subosito/gotenv v1.4.2 // indirect 54 | github.com/twitchyliquid64/golang-asm v0.15.1 // indirect 55 | github.com/ugorji/go/codec v1.2.11 // indirect 56 | go.etcd.io/etcd/api/v3 v3.5.9 // indirect 57 | go.etcd.io/etcd/client/pkg/v3 v3.5.9 // indirect 58 | go.etcd.io/etcd/client/v3 v3.5.9 // indirect 59 | go.uber.org/atomic v1.9.0 // indirect 60 | go.uber.org/multierr v1.8.0 // indirect 61 | go.uber.org/zap v1.21.0 // indirect 62 | golang.org/x/arch v0.3.0 // indirect 63 | golang.org/x/crypto v0.12.0 // indirect 64 | golang.org/x/net v0.14.0 // indirect 65 | golang.org/x/sys v0.11.0 // indirect 66 | golang.org/x/text v0.12.0 // indirect 67 | google.golang.org/genproto v0.0.0-20230803162519-f966b187b2e5 // indirect 68 | google.golang.org/genproto/googleapis/api v0.0.0-20230726155614-23370e0ffb3e // indirect 69 | google.golang.org/genproto/googleapis/rpc v0.0.0-20230815205213-6bfd019c3878 // indirect 70 | gopkg.in/ini.v1 v1.67.0 // indirect 71 | gopkg.in/yaml.v3 v3.0.1 // indirect 72 | ) 73 | -------------------------------------------------------------------------------- /social_service/internal/handler/social.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "context" 5 | "social/internal/model" 6 | "social/internal/service" 7 | "social/pkg/cache" 8 | "utils/exception" 9 | ) 10 | 11 | type SocialService struct { 12 | service.UnimplementedSocialServiceServer // 版本兼容问题 13 | } 14 | 15 | func NewSocialService() *SocialService { 16 | return &SocialService{} 17 | } 18 | 19 | // FollowAction 关注服务 20 | func (*SocialService) FollowAction(ctx context.Context, req *service.FollowRequest) (resp *service.FollowResponse, err error) { 21 | resp = new(service.FollowResponse) 22 | 23 | if req.UserId == req.ToUserId { 24 | resp.StatusCode = exception.FollowSelfErr 25 | resp.StatusMsg = exception.GetMsg(exception.FollowSelfErr) 26 | return resp, nil 27 | } 28 | 29 | resp.StatusCode = exception.ERROR 30 | resp.StatusMsg = exception.GetMsg(exception.ERROR) 31 | 32 | /* mysql操作 33 | 34 | follow := model.Follow{ 35 | UserId: req.UserId, 36 | ToUserId: req.ToUserId, 37 | IsFollow: req.ActionType, 38 | } 39 | 40 | err = model.GetFollowInstance().FollowAction(&follow) 41 | */ 42 | 43 | // redis操作 44 | err = cache.FollowAction(req.UserId, req.ToUserId, req.ActionType) 45 | 46 | if err != nil { 47 | return resp, nil 48 | } 49 | resp.StatusCode = exception.SUCCESS 50 | resp.StatusMsg = exception.GetMsg(exception.SUCCESS) 51 | return resp, nil 52 | } 53 | 54 | func (*SocialService) GetFollowList(ctx context.Context, req *service.FollowListRequest) (resp *service.FollowListResponse, err error) { 55 | resp = new(service.FollowListResponse) 56 | resp.StatusCode = exception.ERROR 57 | resp.StatusMsg = exception.GetMsg(exception.ERROR) 58 | 59 | // mysql操作 60 | // err = model.GetFollowInstance().GetFollowList(req.UserId, &resp.UserId) 61 | 62 | // redis操作 63 | err = cache.GetFollowList(req.UserId, &resp.UserId) 64 | 65 | if err != nil { 66 | return resp, nil 67 | } 68 | resp.StatusCode = exception.SUCCESS 69 | resp.StatusMsg = exception.GetMsg(exception.SUCCESS) 70 | return resp, nil 71 | } 72 | 73 | func (*SocialService) GetFollowerList(ctx context.Context, req *service.FollowListRequest) (resp *service.FollowListResponse, err error) { 74 | resp = new(service.FollowListResponse) 75 | resp.StatusCode = exception.ERROR 76 | resp.StatusMsg = exception.GetMsg(exception.ERROR) 77 | 78 | // mysql操作 79 | // err = model.GetFollowInstance().GetFollowerList(req.UserId, &resp.UserId) 80 | 81 | // redis操作 82 | err = cache.GetFollowerList(req.UserId, &resp.UserId) 83 | if err != nil { 84 | return resp, nil 85 | } 86 | resp.StatusCode = exception.SUCCESS 87 | resp.StatusMsg = exception.GetMsg(exception.SUCCESS) 88 | return resp, nil 89 | } 90 | 91 | func (*SocialService) GetFriendList(ctx context.Context, req *service.FollowListRequest) (resp *service.FollowListResponse, err error) { 92 | resp = new(service.FollowListResponse) 93 | resp.StatusCode = exception.ERROR 94 | resp.StatusMsg = exception.GetMsg(exception.ERROR) 95 | 96 | // mysql 97 | // err = model.GetFollowInstance().GetFriendList(req.UserId, &resp.UserId) 98 | 99 | // mysql 100 | err = cache.GetFriendList(req.UserId, &resp.UserId) 101 | 102 | if err != nil { 103 | return resp, nil 104 | } 105 | resp.StatusCode = exception.SUCCESS 106 | resp.StatusMsg = exception.GetMsg(exception.SUCCESS) 107 | return resp, nil 108 | } 109 | 110 | func (*SocialService) GetFollowInfo(ctx context.Context, req *service.FollowInfoRequest) (resp *service.FollowInfoResponse, err error) { 111 | resp = new(service.FollowInfoResponse) 112 | for _, toUserId := range req.ToUserId { 113 | /* mysql 114 | res1, err1 := model.GetFollowInstance().IsFollow(req.UserId, toUserId) 115 | cnt2, err2 := model.GetFollowInstance().GetFollowCount(toUserId) 116 | cnt3, err3 := model.GetFollowInstance().GetFollowerCount(toUserId) 117 | */ 118 | res1, err1 := cache.IsFollow(req.UserId, toUserId) 119 | cnt2, err2 := cache.GetFollowCount(toUserId) 120 | cnt3, err3 := cache.GetFollowerCount(toUserId) 121 | if err1 != nil || err2 != nil || err3 != nil { 122 | resp.StatusCode = exception.ERROR 123 | resp.StatusMsg = exception.GetMsg(exception.ERROR) 124 | return resp, nil 125 | } 126 | resp.FollowInfo = append(resp.FollowInfo, &service.FollowInfo{ 127 | IsFollow: res1, 128 | FollowCount: cnt2, 129 | FollowerCount: cnt3, 130 | ToUserId: toUserId, 131 | }) 132 | } 133 | return resp, nil 134 | } 135 | 136 | // PostMessage 消息服务 137 | func (*SocialService) PostMessage(ctx context.Context, req *service.PostMessageRequest) (resp *service.PostMessageResponse, err error) { 138 | resp = new(service.PostMessageResponse) 139 | message := model.Message{ 140 | UserId: req.UserId, 141 | ToUserId: req.ToUserId, 142 | Message: req.Content, 143 | } 144 | err = model.GetMessageInstance().PostMessage(&message) 145 | if err != nil { 146 | resp.StatusCode = exception.ERROR 147 | resp.StatusMsg = exception.GetMsg(exception.ERROR) 148 | return resp, nil 149 | } 150 | resp.StatusCode = exception.SUCCESS 151 | resp.StatusMsg = exception.GetMsg(exception.SUCCESS) 152 | return resp, nil 153 | } 154 | 155 | func (*SocialService) GetMessage(ctx context.Context, req *service.GetMessageRequest) (resp *service.GetMessageResponse, err error) { 156 | resp = new(service.GetMessageResponse) 157 | var messages []model.Message 158 | err = model.GetMessageInstance().GetMessage(req.UserId, req.ToUserId, req.PreMsgTime, &messages) 159 | if err != nil { 160 | resp.StatusCode = exception.ERROR 161 | resp.StatusMsg = exception.GetMsg(exception.ERROR) 162 | return resp, nil 163 | } 164 | 165 | for _, message := range messages { 166 | resp.Message = append(resp.Message, &service.Message{ 167 | Id: message.Id, 168 | UserId: message.UserId, 169 | ToUserId: message.ToUserId, 170 | Content: message.Message, 171 | CreatedAt: message.CreatedAt, 172 | }) 173 | } 174 | resp.StatusCode = exception.SUCCESS 175 | resp.StatusMsg = exception.GetMsg(exception.SUCCESS) 176 | return resp, nil 177 | } 178 | -------------------------------------------------------------------------------- /social_service/internal/model/follow.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "gorm.io/gorm" 7 | "log" 8 | "strconv" 9 | "sync" 10 | "utils/snowFlake" 11 | ) 12 | 13 | type Follow struct { 14 | Id int64 `gorm:"primary_key"` 15 | UserId int64 16 | ToUserId int64 17 | IsFollow int32 `gorm:"default:(2)"` 18 | } 19 | 20 | type FollowModel struct { 21 | } 22 | 23 | var followModel *FollowModel 24 | var followOnce sync.Once // 单例模式 25 | 26 | // GetFollowInstance 获取单例实例 27 | func GetFollowInstance() *FollowModel { 28 | followOnce.Do( 29 | func() { 30 | followModel = &FollowModel{} 31 | }, 32 | ) 33 | return followModel 34 | } 35 | 36 | // FollowAction 更新关注状态 37 | func (*FollowModel) FollowAction(follow *Follow) error { 38 | isFollow := follow.IsFollow 39 | if err := DB.Where(&Follow{UserId: follow.UserId, ToUserId: follow.ToUserId}).First(&follow).Error; err != nil { 40 | if errors.Is(err, gorm.ErrRecordNotFound) { 41 | flake, _ := snowFlake.NewSnowFlake(7, 3) 42 | follow.Id = flake.NextId() 43 | err = DB.Create(&follow).Error // create new record from newUser 44 | } 45 | if err != nil { 46 | return err 47 | } 48 | } 49 | if follow.Id != 0 && isFollow != follow.IsFollow { 50 | err := DB.Model(&Follow{}).Where(&follow).Update("is_follow", isFollow).Error 51 | if err != nil { 52 | return err 53 | } 54 | } 55 | return nil 56 | } 57 | 58 | func (*FollowModel) IsFollow(UserId int64, ToUserId int64) (bool, error) { 59 | follow := Follow{UserId: UserId, ToUserId: ToUserId} 60 | err := DB.Where(&follow).First(&follow).Error 61 | if errors.Is(err, gorm.ErrRecordNotFound) { 62 | return false, nil 63 | } else if err != nil { 64 | return false, err 65 | } 66 | if follow.IsFollow == 1 { 67 | return true, nil 68 | } 69 | return false, nil 70 | } 71 | 72 | func (*FollowModel) GetFollowList(reqUser int64, UserId *[]int64) error { 73 | var follows []Follow 74 | if err := DB.Where(&Follow{UserId: reqUser, IsFollow: 1}).Find(&follows).Error; err != nil { 75 | return err 76 | } 77 | 78 | for _, follow := range follows { 79 | *UserId = append(*UserId, follow.ToUserId) 80 | } 81 | return nil 82 | } 83 | 84 | func (*FollowModel) GetFollowerList(reqUser int64, UserId *[]int64) error { 85 | var follows []Follow 86 | if err := DB.Where(&Follow{ToUserId: reqUser, IsFollow: 1}).Find(&follows).Error; err != nil { 87 | return err 88 | } 89 | 90 | for _, follow := range follows { 91 | *UserId = append(*UserId, follow.UserId) 92 | } 93 | return nil 94 | } 95 | 96 | func (*FollowModel) GetFriendList(reqUser int64, UserId *[]int64) error { 97 | 98 | if err := DB.Raw("select a.to_user_id from follow as a inner join follow as b on a.user_id = b.to_user_id and a.to_user_id = b.user_id and a.is_follow = 1 and b.is_follow = 1 and a.user_id = ?", reqUser).Scan(UserId).Error; err != nil { 99 | return err 100 | } 101 | 102 | return nil 103 | } 104 | 105 | func (*FollowModel) GetFollowCount(reqUser int64) (int64, error) { 106 | var cnt int64 107 | 108 | if err := DB.Model(&Follow{}).Where(&Follow{UserId: reqUser, IsFollow: 1}).Count(&cnt).Error; err != nil { 109 | return 0, err 110 | } 111 | 112 | return cnt, nil 113 | } 114 | 115 | func (*FollowModel) GetFollowerCount(reqUser int64) (int64, error) { 116 | var cnt int64 117 | if err := DB.Model(&Follow{}).Where(&Follow{ToUserId: reqUser, IsFollow: 1}).Count(&cnt).Error; err != nil { 118 | return 0, err 119 | } 120 | return cnt, nil 121 | } 122 | 123 | func (*FollowModel) RedisToMysql(data string) error { 124 | // 先将所有follow置为2 125 | if err := DB.Model(&Follow{}).Where("is_follow = ?", 1).Update("is_follow", 2).Error; err != nil { 126 | return err 127 | } 128 | // 转换data为map 129 | var follows map[string][]string 130 | if err := json.Unmarshal([]byte(data), &follows); err != nil { 131 | return err 132 | } 133 | // 对每一个关注进行操作 134 | var wg sync.WaitGroup 135 | var errs []error 136 | 137 | for userId, toUserIds := range follows { 138 | userid, _ := strconv.ParseInt(userId, 10, 64) 139 | for _, toUserId := range toUserIds { 140 | touserid, _ := strconv.ParseInt(toUserId, 10, 64) 141 | wg.Add(1) 142 | go func(userId int64, toUserId int64) { 143 | defer wg.Done() 144 | follow := Follow{UserId: userId, ToUserId: toUserId, IsFollow: 1} 145 | err := GetFollowInstance().FollowAction(&follow) 146 | if err != nil { 147 | errs = append(errs, err) 148 | } 149 | }(userid, touserid) 150 | 151 | } 152 | } 153 | 154 | wg.Wait() 155 | 156 | log.Println(errs) 157 | 158 | return nil 159 | } 160 | -------------------------------------------------------------------------------- /social_service/internal/model/follow_test.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | ) 7 | 8 | func TestFollowModel_FollowAction(t *testing.T) { 9 | InitDb() 10 | follow := Follow{ 11 | UserId: 1, 12 | ToUserId: 2, 13 | IsFollow: 2, 14 | } 15 | err := GetFollowInstance().FollowAction(&follow) 16 | fmt.Println(err) 17 | } 18 | 19 | func TestFollowModel_IsFollow(t *testing.T) { 20 | InitDb() 21 | res, err := GetFollowInstance().IsFollow(5155644223918080, 5155317378584576) 22 | if err != nil { 23 | panic(err) 24 | } 25 | fmt.Println(res) 26 | } 27 | 28 | func TestFollowModel_GetFollowList(t *testing.T) { 29 | InitDb() 30 | var UserId []int64 31 | var reqUser int64 32 | reqUser = 1 33 | if err := GetFollowInstance().GetFollowList(reqUser, &UserId); err != nil { 34 | panic(nil) 35 | } 36 | fmt.Printf("%d的关注有%#v\n", reqUser, UserId) 37 | } 38 | 39 | func TestFollowModel_GetFollowerList(t *testing.T) { 40 | InitDb() 41 | var UserId []int64 42 | var reqUser int64 43 | reqUser = 2 44 | if err := GetFollowInstance().GetFollowerList(reqUser, &UserId); err != nil { 45 | panic(nil) 46 | } 47 | fmt.Printf("%d的粉丝有%#v\n", reqUser, UserId) 48 | } 49 | 50 | func TestFollowModel_GetFriendList(t *testing.T) { 51 | InitDb() 52 | var UserId []int64 53 | var reqUser int64 54 | reqUser = 2 55 | if err := GetFollowInstance().GetFriendList(reqUser, &UserId); err != nil { 56 | panic(nil) 57 | } 58 | fmt.Printf("%d的好友有%#v\n", reqUser, UserId) 59 | } 60 | 61 | func TestFollowModel_GetFollowCount(t *testing.T) { 62 | InitDb() 63 | var cnt int64 64 | var reqUser int64 65 | reqUser = 1 66 | cnt, err := GetFollowInstance().GetFollowCount(reqUser) 67 | if err != nil { 68 | panic(nil) 69 | } 70 | fmt.Printf("%d的关注有%d个\n", reqUser, cnt) 71 | } 72 | 73 | func TestFollowModel_GetFollowerCount(t *testing.T) { 74 | InitDb() 75 | var cnt int64 76 | var reqUser int64 77 | reqUser = 2 78 | cnt, err := GetFollowInstance().GetFollowerCount(reqUser) 79 | if err != nil { 80 | panic(nil) 81 | } 82 | fmt.Printf("%d的粉丝有%d个\n", reqUser, cnt) 83 | } 84 | -------------------------------------------------------------------------------- /social_service/internal/model/init.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "gorm.io/driver/mysql" 6 | "gorm.io/gorm" 7 | "gorm.io/gorm/logger" 8 | "gorm.io/gorm/schema" 9 | "social/config" 10 | "time" 11 | ) 12 | 13 | // DB 创建数据库单例 14 | var DB *gorm.DB 15 | 16 | // InitDb 初始化数据库 17 | func InitDb() { 18 | config.InitConfig() 19 | dns := config.DbDnsInit() 20 | Database(dns) 21 | } 22 | 23 | // Database 初始花数据库链接 24 | func Database(connString string) { 25 | var ormLogger logger.Interface 26 | 27 | // gin中有debug 、 release 、 test 三种模式 28 | // 不指定默认以debug形式启动 29 | // 开发用debug模式、 上线用release模式 30 | // 指定方式 : gin.SetMode(gin.ReleaseMode) 31 | 32 | // 设置gorm的日志模式,可以打印原生SQL语句 33 | if gin.Mode() == "debug" { 34 | ormLogger = logger.Default.LogMode(logger.Info) 35 | } else { 36 | ormLogger = logger.Default 37 | } 38 | db, err := gorm.Open(mysql.New(mysql.Config{ 39 | DSN: connString, // DSN data source name 40 | DefaultStringSize: 256, // string 类型字段的默认长度 41 | DisableDatetimePrecision: true, // 禁用 datetime 精度,MySQL 5.6 之前的数据库不支持 42 | DontSupportRenameIndex: true, // 重命名索引时采用删除并新建的方式,MySQL 5.7 之前的数据库和 MariaDB 不支持重命名索引 43 | DontSupportRenameColumn: true, // 用 `change` 重命名列,MySQL 8 之前的数据库和 MariaDB 不支持重命名列 44 | SkipInitializeWithVersion: false, // 根据当前 MySQL 版本自动配置 45 | }), &gorm.Config{ 46 | Logger: ormLogger, 47 | NamingStrategy: schema.NamingStrategy{ 48 | SingularTable: true, // 默认不加负数 49 | }, 50 | }) 51 | 52 | if err != nil { 53 | panic(err) 54 | } 55 | 56 | sqlDB, _ := db.DB() 57 | 58 | // 设置连接池 59 | sqlDB.SetMaxIdleConns(20) // 空闲时候的最大连接数 60 | sqlDB.SetMaxOpenConns(100) // 打开时候的最大连接数 61 | sqlDB.SetConnMaxLifetime(time.Second * 20) // 超时时间 62 | DB = db 63 | 64 | migration() 65 | } 66 | -------------------------------------------------------------------------------- /social_service/internal/model/init_test.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "fmt" 5 | "github.com/spf13/viper" 6 | "social/config" 7 | "testing" 8 | ) 9 | 10 | func TestInitDb(t *testing.T) { 11 | config.InitConfig() 12 | dns := config.DbDnsInit() 13 | fmt.Print(dns) 14 | config.InitConfig() 15 | fmt.Printf("host is: %v \n", viper.GetString("mysql.host")) 16 | } 17 | -------------------------------------------------------------------------------- /social_service/internal/model/message.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "sync" 5 | "time" 6 | "utils/snowFlake" 7 | ) 8 | 9 | type Message struct { 10 | Id int64 `gorm:"primary_key"` 11 | UserId int64 12 | ToUserId int64 13 | Message string 14 | CreatedAt int64 15 | } 16 | 17 | type MessageModel struct { 18 | } 19 | 20 | var messageModel *MessageModel 21 | var messageOnce sync.Once // 单例模式 22 | 23 | // GetMessageInstance 获取单例实例 24 | func GetMessageInstance() *MessageModel { 25 | messageOnce.Do( 26 | func() { 27 | messageModel = &MessageModel{} 28 | }, 29 | ) 30 | return messageModel 31 | } 32 | 33 | func getCurrentTime() (createTime int64) { 34 | createTime = time.Now().Unix() 35 | return 36 | } 37 | 38 | func (*MessageModel) PostMessage(message *Message) error { 39 | message.CreatedAt = getCurrentTime() 40 | flake, _ := snowFlake.NewSnowFlake(7, 3) 41 | message.Id = flake.NextId() 42 | err := DB.Create(&message).Error 43 | return err 44 | } 45 | 46 | func (*MessageModel) GetMessage(UserId int64, ToUserID int64, PreMsgTime int64, messages *[]Message) error { 47 | err := DB.Model(&Message{}).Where(DB.Model(&Message{}).Where(&Message{UserId: UserId, ToUserId: ToUserID}). 48 | Or(&Message{UserId: ToUserID, ToUserId: UserId})).Where("created_at > ?", PreMsgTime). 49 | Order("created_at").Find(messages).Error 50 | return err 51 | } 52 | -------------------------------------------------------------------------------- /social_service/internal/model/message_test.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | ) 7 | 8 | func TestMessageModel_GetMessage(t *testing.T) { 9 | InitDb() 10 | var messages []Message 11 | err := GetMessageInstance().GetMessage(5155317378584576, 5155644223918080, 16, &messages) 12 | if err != nil { 13 | panic(err) 14 | } 15 | fmt.Printf("%#v\n", messages) 16 | } 17 | 18 | func TestMessageModel_PostMessage(t *testing.T) { 19 | InitDb() 20 | err := GetMessageInstance().PostMessage(&Message{ 21 | UserId: 1, 22 | ToUserId: 2, 23 | Message: "你好", 24 | }) 25 | if err != nil { 26 | panic(err) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /social_service/internal/model/migration.go: -------------------------------------------------------------------------------- 1 | // Package model 数据库自动迁移 2 | package model 3 | 4 | import "log" 5 | 6 | func migration() { 7 | // 自动迁移 8 | err := DB.Set("gorm:table_options", "charset=utf8mb4").AutoMigrate(&Follow{}) 9 | // Todo 判断error 写入日志 10 | if err != nil { 11 | log.Print("err") 12 | } 13 | 14 | err = DB.Set("gorm:table_options", "charset=utf8mb4").AutoMigrate(&Message{}) 15 | // Todo 判断error 写入日志 16 | if err != nil { 17 | log.Print("err") 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /social_service/internal/service/pb/socialService.proto: -------------------------------------------------------------------------------- 1 | syntax="proto3"; 2 | package pb; 3 | option go_package = "../;service"; // 此处格式为<生成的文件存放位置;生成文件的包名> 4 | 5 | // protoc -I internal/service/pb --go_out=./internal/service/ --go_opt=paths=source_relative --go-grpc_out=./internal/service/ --go-grpc_opt=paths=source_relative internal/service/pb/*.proto 6 | // 或者分开使用 7 | // protoc -I internal/service/pb/ --go_out=./ internal/service/pb/*.proto 8 | // protoc -I internal/service/pb/ --go-grpc_out=./ internal/service/pb/*.proto 9 | 10 | message FollowRequest{ 11 | // @gotags:json:"user_id" form:"user_id" uri:"user_id" 12 | int64 UserId = 1; 13 | // @gotags:json:"to_user_id" form:"to_user_id" uri:"to_user_id" 14 | int64 ToUserId = 2; 15 | // @gotags:json:"action_type" form:"action_type" uri:"action_type" 16 | int32 ActionType = 3; 17 | } 18 | 19 | message FollowResponse{ 20 | // @gotags:json:"status_code" form:"status_code" uri:"status_code" 21 | int32 StatusCode = 1; 22 | // @gotags:json:"status_msg" form:"status_msg" uri:"status_msg" 23 | string StatusMsg = 2; 24 | } 25 | 26 | message FollowListRequest{ 27 | // @gotags:json:"user_id" form:"user_id" uri:"user_id" 28 | int64 UserId = 1; 29 | } 30 | 31 | message FollowListResponse{ 32 | // @gotags:json:"status_code" form:"status_code" uri:"status_code" 33 | int32 StatusCode = 1; 34 | // @gotags:json:"status_msg" form:"status_msg" uri:"status_msg" 35 | string StatusMsg = 2; 36 | // @gotags:json:"user_list" form:"user_list" uri:"user_list" 37 | repeated int64 UserId = 3; 38 | } 39 | 40 | message FollowInfoRequest{ 41 | // @gotags:json:"user_id" form:"user_id" uri:"user_id" 42 | int64 UserId = 1; 43 | // @gotags:json:"to_user_id" form:"to_user_id" uri:"to_user_id" 44 | repeated int64 ToUserId = 2; 45 | } 46 | 47 | message FollowInfo{ 48 | // @gotags:json:"is_follow" form:"is_follow" uri:"is_follow" 49 | bool IsFollow = 1; 50 | // @gotags:json:"follow_count" form:"follow_count" uri:"follow_count" 51 | int64 FollowCount = 2; 52 | // @gotags:json:"follower_count" form:"follower_count" uri:"follower_count" 53 | int64 FollowerCount = 3; 54 | // @gotags:json:"to_user_id" form:"to_user_id" uri:"to_user_id" 55 | int64 ToUserId = 4; 56 | } 57 | 58 | message FollowInfoResponse{ 59 | // @gotags:json:"status_code" form:"status_code" uri:"status_code" 60 | int32 StatusCode = 1; 61 | // @gotags:json:"status_msg" form:"status_msg" uri:"status_msg" 62 | string StatusMsg = 2; 63 | // @gotags:json:"follow_info" form:"follow_info" uri:"follow_info" 64 | repeated FollowInfo FollowInfo = 3; 65 | } 66 | 67 | message PostMessageRequest{ 68 | // @gotags:json:"user_id" form:"user_id" uri:"user_id" 69 | int64 UserId = 1; 70 | // @gotags:json:"to_user_id" form:"to_user_id" uri:"to_user_id" 71 | int64 ToUserId = 2; 72 | // @gotags:json:"action_type" form:"action_type" uri:"action_type" 73 | int32 ActionType = 3; 74 | // @gotags:json:"content" form:"content" uri:"content" 75 | string Content = 4; 76 | } 77 | 78 | message PostMessageResponse{ 79 | // @gotags:json:"status_code" form:"status_code" uri:"status_code" 80 | int32 StatusCode = 1; 81 | // @gotags:json:"status_msg" form:"status_msg" uri:"status_msg" 82 | string StatusMsg = 2; 83 | } 84 | 85 | message GetMessageRequest{ 86 | // @gotags:json:"user_id" form:"user_id" uri:"user_id" 87 | int64 UserId = 1; 88 | // @gotags:json:"to_user_id" form:"to_user_id" uri:"to_user_id" 89 | int64 ToUserId = 2; 90 | // @gotags:json:"pre_msg_time" form:"pre_msg_time" uri:"pre_msg_time" 91 | int64 preMsgTime=3; 92 | } 93 | 94 | message Message{ 95 | // @gotags:json:"id" form:"id" uri:"id" 96 | int64 Id = 1; 97 | // @gotags:json:"user_id" form:"user_id" uri:"user_id" 98 | int64 UserId = 2; 99 | // @gotags:json:"to_user_id" form:"to_user_id" uri:"to_user_id" 100 | int64 ToUserId = 3; 101 | // @gotags:json:"content" form:"content" uri:"content" 102 | string Content = 4; 103 | // @gotags:json:"created_at" form:"created_at" uri:"created_at" 104 | int64 CreatedAt = 5; 105 | } 106 | 107 | message GetMessageResponse{ 108 | // @gotags:json:"status_code" form:"status_code" uri:"status_code" 109 | int32 StatusCode = 1; 110 | // @gotags:json:"status_msg" form:"status_msg" uri:"status_msg" 111 | string StatusMsg = 2; 112 | // @gotags:json:"message_list" form:"message_list" uri:"message_list" 113 | repeated Message message = 3; 114 | } 115 | 116 | service SocialService{ 117 | // 关注服务 118 | rpc FollowAction(FollowRequest) returns(FollowResponse); 119 | rpc GetFollowList(FollowListRequest) returns(FollowListResponse); 120 | rpc GetFollowerList(FollowListRequest) returns(FollowListResponse); 121 | rpc GetFriendList(FollowListRequest) returns(FollowListResponse); 122 | rpc GetFollowInfo(FollowInfoRequest) returns(FollowInfoResponse); 123 | 124 | // 消息服务 125 | rpc PostMessage(PostMessageRequest) returns(PostMessageResponse); 126 | rpc GetMessage(GetMessageRequest) returns(GetMessageResponse); 127 | 128 | } -------------------------------------------------------------------------------- /social_service/pkg/cache/redis.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "context" 5 | "github.com/go-redis/redis/v8" 6 | "github.com/spf13/viper" 7 | "strconv" 8 | ) 9 | 10 | var Redis *redis.Client 11 | var ctx = context.Background() 12 | 13 | // InitRedis 连接redis 14 | func InitRedis() { 15 | addr := viper.GetString("redis.address") 16 | Redis = redis.NewClient(&redis.Options{ 17 | Addr: addr, 18 | DB: 0, // 存入DB0 19 | }) 20 | _, err := Redis.Ping(context.Background()).Result() 21 | if err != nil { 22 | panic(err) 23 | } 24 | } 25 | 26 | func GenerateFollowKey(UserId int64) string { 27 | return "TINYTIKTOK:FOLLOW:" + strconv.FormatInt(UserId, 10) 28 | } 29 | 30 | func GenerateFollowerKey(UserId int64) string { 31 | return "TINYTIKTOK:FOLLOWER:" + strconv.FormatInt(UserId, 10) 32 | } 33 | 34 | func GenerateFriendKey(UserId int64) string { 35 | return "TINYTIKTOK:FRIEND:" + strconv.FormatInt(UserId, 10) 36 | } 37 | 38 | func IsFollow(UserId int64, ToUserId int64) (bool, error) { 39 | result, err := Redis.SIsMember(ctx, GenerateFollowKey(UserId), ToUserId).Result() 40 | return result, err 41 | } 42 | 43 | func FollowAction(UserId int64, ToUserId int64, ActionType int32) error { 44 | // 先查询关注的状态 45 | result, err := Redis.SIsMember(ctx, GenerateFollowKey(UserId), ToUserId).Result() 46 | if err != nil { 47 | return err 48 | } 49 | // 获取操作类型 50 | action := true 51 | if ActionType == 2 { 52 | action = false 53 | } 54 | // 判断操作是否需要执行 55 | if result == action { 56 | return nil 57 | } 58 | // 执行操作 59 | pipe := Redis.TxPipeline() 60 | defer pipe.Close() 61 | if action { 62 | pipe.SAdd(ctx, GenerateFollowKey(UserId), ToUserId) 63 | pipe.SAdd(ctx, GenerateFollowerKey(ToUserId), UserId) 64 | _, err := pipe.Exec(ctx) 65 | if err != nil { 66 | return err 67 | } 68 | } else { 69 | pipe.SRem(ctx, GenerateFollowKey(UserId), ToUserId) 70 | pipe.SRem(ctx, GenerateFollowerKey(ToUserId), UserId) 71 | _, err := pipe.Exec(ctx) 72 | if err != nil { 73 | return err 74 | } 75 | } 76 | return nil 77 | } 78 | 79 | func GetFollowList(reqUser int64, UserId *[]int64) error { 80 | result, err := Redis.SMembers(ctx, GenerateFollowKey(reqUser)).Result() 81 | if err != nil { 82 | return err 83 | } 84 | for _, r := range result { 85 | r64, _ := strconv.ParseInt(r, 10, 64) 86 | *UserId = append(*UserId, r64) 87 | } 88 | return nil 89 | } 90 | 91 | func GetFollowerList(reqUser int64, UserId *[]int64) error { 92 | result, err := Redis.SMembers(ctx, GenerateFollowerKey(reqUser)).Result() 93 | if err != nil { 94 | return err 95 | } 96 | for _, r := range result { 97 | r64, _ := strconv.ParseInt(r, 10, 64) 98 | *UserId = append(*UserId, r64) 99 | } 100 | return nil 101 | } 102 | 103 | func GetFriendList(reqUser int64, UserId *[]int64) error { 104 | _, err := Redis.Do(ctx, "SINTERSTORE", GenerateFriendKey(reqUser), GenerateFollowKey(reqUser), GenerateFollowerKey(reqUser)).Result() 105 | if err != nil { 106 | return err 107 | } 108 | result, err := Redis.SMembers(ctx, GenerateFriendKey(reqUser)).Result() 109 | if err != nil { 110 | return err 111 | } 112 | for _, r := range result { 113 | r64, _ := strconv.ParseInt(r, 10, 64) 114 | *UserId = append(*UserId, r64) 115 | } 116 | return nil 117 | } 118 | 119 | func GetFollowCount(reqUser int64) (int64, error) { 120 | return Redis.SCard(ctx, GenerateFollowKey(reqUser)).Result() 121 | } 122 | 123 | func GetFollowerCount(reqUser int64) (int64, error) { 124 | return Redis.SCard(ctx, GenerateFollowerKey(reqUser)).Result() 125 | } 126 | -------------------------------------------------------------------------------- /social_service/pkg/cache/redisAutoSync.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "github.com/go-redis/redis/v8" 7 | "github.com/robfig/cron/v3" 8 | "golang.org/x/sync/errgroup" 9 | "log" 10 | "os" 11 | "social/internal/model" 12 | "time" 13 | ) 14 | 15 | // getAllKeys 获取所有"FOLLOW"表项的key值 16 | func getAllKeys(keys *[]string) { 17 | // 使用正则表达式来匹配键 18 | pattern := "TINYTIKTOK:FOLLOW:*" // 替换为您的正则表达式 19 | 20 | // 初始化SCAN迭代器 21 | iter := Redis.Scan(ctx, 0, pattern, 0).Iterator() 22 | 23 | // 用于并发处理的函数 24 | handleKey := func(key string) error { 25 | *keys = append(*keys, key) 26 | return nil 27 | } 28 | 29 | // 并发处理匹配的键 30 | var g errgroup.Group 31 | for iter.Next(ctx) { 32 | key := iter.Val() 33 | g.Go(func() error { 34 | return handleKey(key) 35 | }) 36 | } 37 | 38 | if err := g.Wait(); err != nil { 39 | panic(err) 40 | } 41 | } 42 | 43 | func getAllValueByKeys(keys []string) string { 44 | // 创建管道并将所有的 查询列表操作加入pipe 45 | cmds, err := Redis.Pipelined(ctx, func(pipe redis.Pipeliner) error { 46 | for _, key := range keys { 47 | pipe.SMembers(ctx, key) 48 | } 49 | return nil 50 | }) 51 | if err != nil { 52 | panic(err) 53 | } 54 | 55 | followInfo := make(map[string][]string) 56 | for index, cmd := range cmds { 57 | followInfo[keys[index][18:]] = cmd.(*redis.StringSliceCmd).Val() 58 | } 59 | marshal, err := json.Marshal(followInfo) 60 | if err != nil { 61 | return "" 62 | } 63 | return string(marshal) 64 | 65 | } 66 | 67 | /* 68 | func MysqlToRedis() error { 69 | var errs []error 70 | // 所有数据,每次取100条 71 | page := 1 72 | pageSize := 100 73 | for { 74 | var follows []Follow 75 | offset := (page - 1) * pageSize 76 | DB.Limit(pageSize).Offset(offset).Find(&follows) 77 | 78 | // 处理查询结果 79 | if len(follows) == 0 { 80 | break // 没有更多记录可获取,退出循环 81 | } 82 | 83 | for _, follow := range follows { 84 | err := cache.FollowAction(follow.UserId, follow.ToUserId, 1) 85 | errs = append(errs, err) 86 | } 87 | 88 | page++ 89 | } 90 | 91 | log.Println(errs) 92 | return nil 93 | */ 94 | func AutoSync() { 95 | // 先匹配所有follow的key 96 | var keys []string 97 | getAllKeys(&keys) 98 | res := getAllValueByKeys(keys) 99 | err := model.GetFollowInstance().RedisToMysql(res) 100 | if err != nil { 101 | panic(err) 102 | } 103 | } 104 | 105 | func TimerSync() { 106 | c := cron.New(cron.WithSeconds(), cron.WithChain(cron.SkipIfStillRunning(cron.DefaultLogger)), cron.WithLogger( 107 | cron.VerbosePrintfLogger(log.New(os.Stdout, "cron: ", log.LstdFlags)))) 108 | 109 | _, err := c.AddFunc("0 */10 * * * *", func() { 110 | fmt.Println(time.Now(), " 开始同步到mysql数据库") 111 | AutoSync() 112 | }) 113 | if err != nil { 114 | panic(err) 115 | } 116 | 117 | c.Start() 118 | select {} 119 | } 120 | -------------------------------------------------------------------------------- /social_service/pkg/cache/redis_test.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "fmt" 5 | "social/internal/model" 6 | "testing" 7 | ) 8 | 9 | func TestFollowAction(t *testing.T) { 10 | InitRedis() 11 | err := FollowAction(3, 4, 1) 12 | fmt.Println(err) 13 | } 14 | 15 | func TestGetFriendListList(t *testing.T) { 16 | InitRedis() 17 | var UserId []int64 18 | err := GetFollowList(1, &UserId) 19 | fmt.Printf("%#v", UserId) 20 | fmt.Println(err) 21 | } 22 | 23 | func TestGetFollowCount(t *testing.T) { 24 | InitRedis() 25 | fmt.Println(GetFollowCount(100)) 26 | } 27 | 28 | func TestAutoSync(t *testing.T) { 29 | InitRedis() 30 | model.InitDb() 31 | AutoSync() 32 | } 33 | -------------------------------------------------------------------------------- /user_service/cmd/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "user/config" 5 | "user/discovery" 6 | "user/internal/model" 7 | "user/pkg/cache" 8 | ) 9 | 10 | func main() { 11 | config.InitConfig() // 初始话配置文件 12 | cache.InitRedis() // 初始化redis 13 | model.InitDb() // 初始化数据库 14 | discovery.AutoRegister() // 自动注册 15 | } 16 | -------------------------------------------------------------------------------- /user_service/config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "github.com/spf13/viper" 5 | "path" 6 | "runtime" 7 | "strings" 8 | ) 9 | 10 | // Config 数据结构 11 | type Config struct { 12 | Mysql struct { 13 | Host string 14 | Port string 15 | Username string 16 | Password string 17 | Database string 18 | } 19 | } 20 | 21 | // ConfigData 配置数据变量 22 | var ConfigData *Config 23 | 24 | // InitConfig 读取配置文件 25 | func InitConfig() { 26 | _, filePath, _, _ := runtime.Caller(0) 27 | 28 | currentDir := path.Dir(filePath) 29 | 30 | viper.SetConfigName("config") 31 | viper.SetConfigType("yaml") 32 | viper.AddConfigPath(currentDir) 33 | 34 | if err := viper.ReadInConfig(); err != nil { 35 | panic(err) 36 | } 37 | 38 | } 39 | 40 | // DbDnsInit 拼接链接数据库的DNS 41 | func DbDnsInit() string { 42 | host := viper.GetString("mysql.host") 43 | port := viper.GetString("mysql.port") 44 | username := viper.GetString("mysql.username") 45 | password := viper.GetString("mysql.password") 46 | database := viper.GetString("mysql.database") 47 | 48 | InitConfig() 49 | dns := strings.Join([]string{username, ":", password, "@tcp(", host, ":", port, ")/", database, "?charset=utf8&parseTime=True&loc=Local"}, "") 50 | 51 | return dns 52 | } 53 | 54 | func RedisURLInit() string { 55 | password := viper.GetString("redis.password") 56 | address := viper.GetString("redis.address") 57 | database := viper.GetString("redis.database") 58 | 59 | InitConfig() 60 | RedisURL := "redis://" + password + "@" + address + "/" + database 61 | 62 | return RedisURL 63 | } 64 | -------------------------------------------------------------------------------- /user_service/config/config.yaml: -------------------------------------------------------------------------------- 1 | server: 2 | name: user_service # 服务名称 3 | address: 127.0.0.1:10001 # 服务地址 4 | 5 | 6 | mysql: 7 | driver: mysql 8 | host: 127.0.0.1 9 | port: 3306 10 | username: root 11 | password: 123456 12 | database: tiny_tiktok 13 | 14 | 15 | etcd: 16 | address: 127.0.0.1:2379 17 | 18 | redis: 19 | address: 127.0.0.1:6379 20 | -------------------------------------------------------------------------------- /user_service/config/config_test.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "fmt" 5 | "github.com/spf13/viper" 6 | "path" 7 | "runtime" 8 | "testing" 9 | ) 10 | 11 | func TestInitConfig(t *testing.T) { 12 | _, filePath, _, _ := runtime.Caller(0) 13 | 14 | currentDir := path.Dir(filePath) 15 | 16 | fmt.Println(currentDir) 17 | } 18 | 19 | func TestDbDnsInit(t *testing.T) { 20 | InitConfig() 21 | fmt.Printf("host is : %v \n", viper.GetString("mysql.host")) 22 | dns := DbDnsInit() 23 | fmt.Printf("dns is : %v", dns) 24 | } 25 | -------------------------------------------------------------------------------- /user_service/discovery/autoRegister.go: -------------------------------------------------------------------------------- 1 | package discovery 2 | 3 | import ( 4 | "github.com/spf13/viper" 5 | "google.golang.org/grpc" 6 | "log" 7 | "net" 8 | "user/internal/handler" 9 | "user/internal/service" 10 | "utils/etcd" 11 | ) 12 | 13 | // AutoRegister etcd自动注册 14 | func AutoRegister() { 15 | etcdAddress := viper.GetString("etcd.address") 16 | etcdRegister, err := etcd.NewEtcdRegister(etcdAddress) 17 | 18 | if err != nil { 19 | log.Fatal(err) 20 | } 21 | 22 | serviceName := viper.GetString("server.name") 23 | serviceAddress := viper.GetString("server.address") 24 | err = etcdRegister.ServiceRegister(serviceName, serviceAddress, 30) 25 | if err != nil { 26 | log.Fatal(err) 27 | } 28 | 29 | listener, err := net.Listen("tcp", serviceAddress) 30 | if err != nil { 31 | log.Fatal(err) 32 | } 33 | 34 | server := grpc.NewServer() 35 | service.RegisterUserServiceServer(server, handler.NewUserService()) 36 | 37 | err = server.Serve(listener) 38 | if err != nil { 39 | log.Fatal(err) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /user_service/go.mod: -------------------------------------------------------------------------------- 1 | module user 2 | 3 | go 1.19 4 | 5 | require ( 6 | github.com/gin-gonic/gin v1.9.1 7 | github.com/spf13/viper v1.16.0 8 | go.etcd.io/etcd/api/v3 v3.5.9 9 | go.etcd.io/etcd/client/v3 v3.5.9 10 | golang.org/x/crypto v0.9.0 11 | google.golang.org/grpc v1.57.0 12 | google.golang.org/protobuf v1.31.0 13 | gorm.io/driver/mysql v1.5.1 14 | gorm.io/gorm v1.25.2 15 | utils v0.0.0 16 | ) 17 | 18 | replace utils => ../utils 19 | 20 | require ( 21 | github.com/bytedance/sonic v1.9.1 // indirect 22 | github.com/cespare/xxhash/v2 v2.2.0 // indirect 23 | github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect 24 | github.com/coreos/go-semver v0.3.0 // indirect 25 | github.com/coreos/go-systemd/v22 v22.3.2 // indirect 26 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect 27 | github.com/fsnotify/fsnotify v1.6.0 // indirect 28 | github.com/gabriel-vasile/mimetype v1.4.2 // indirect 29 | github.com/gin-contrib/sse v0.1.0 // indirect 30 | github.com/go-playground/locales v0.14.1 // indirect 31 | github.com/go-playground/universal-translator v0.18.1 // indirect 32 | github.com/go-playground/validator/v10 v10.14.0 // indirect 33 | github.com/go-redis/redis/v8 v8.11.5 // indirect 34 | github.com/go-sql-driver/mysql v1.7.0 // indirect 35 | github.com/goccy/go-json v0.10.2 // indirect 36 | github.com/gogo/protobuf v1.3.2 // indirect 37 | github.com/golang/protobuf v1.5.3 // indirect 38 | github.com/hashicorp/hcl v1.0.0 // indirect 39 | github.com/jinzhu/inflection v1.0.0 // indirect 40 | github.com/jinzhu/now v1.1.5 // indirect 41 | github.com/json-iterator/go v1.1.12 // indirect 42 | github.com/klauspost/cpuid/v2 v2.2.4 // indirect 43 | github.com/leodido/go-urn v1.2.4 // indirect 44 | github.com/magiconair/properties v1.8.7 // indirect 45 | github.com/mattn/go-isatty v0.0.19 // indirect 46 | github.com/mitchellh/mapstructure v1.5.0 // indirect 47 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 48 | github.com/modern-go/reflect2 v1.0.2 // indirect 49 | github.com/pelletier/go-toml/v2 v2.0.8 // indirect 50 | github.com/sirupsen/logrus v1.9.3 // indirect 51 | github.com/spf13/afero v1.9.5 // indirect 52 | github.com/spf13/cast v1.5.1 // indirect 53 | github.com/spf13/jwalterweatherman v1.1.0 // indirect 54 | github.com/spf13/pflag v1.0.5 // indirect 55 | github.com/subosito/gotenv v1.4.2 // indirect 56 | github.com/twitchyliquid64/golang-asm v0.15.1 // indirect 57 | github.com/ugorji/go/codec v1.2.11 // indirect 58 | go.etcd.io/etcd/client/pkg/v3 v3.5.9 // indirect 59 | go.uber.org/atomic v1.9.0 // indirect 60 | go.uber.org/multierr v1.8.0 // indirect 61 | go.uber.org/zap v1.21.0 // indirect 62 | golang.org/x/arch v0.3.0 // indirect 63 | golang.org/x/net v0.10.0 // indirect 64 | golang.org/x/sys v0.8.0 // indirect 65 | golang.org/x/text v0.9.0 // indirect 66 | google.golang.org/genproto v0.0.0-20230526161137-0005af68ea54 // indirect 67 | google.golang.org/genproto/googleapis/api v0.0.0-20230525234035-dd9d682886f9 // indirect 68 | google.golang.org/genproto/googleapis/rpc v0.0.0-20230525234030-28d5490b6b19 // indirect 69 | gopkg.in/ini.v1 v1.67.0 // indirect 70 | gopkg.in/yaml.v3 v3.0.1 // indirect 71 | ) 72 | -------------------------------------------------------------------------------- /user_service/internal/handler/user.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "strconv" 8 | "time" 9 | "user/internal/model" 10 | "user/internal/service" 11 | "user/pkg/cache" 12 | "utils/exception" 13 | ) 14 | 15 | type UserService struct { 16 | service.UnimplementedUserServiceServer // 版本兼容问题 17 | } 18 | 19 | func NewUserService() *UserService { 20 | return &UserService{} 21 | } 22 | 23 | func (*UserService) UserRegister(ctx context.Context, req *service.UserRequest) (resp *service.UserResponse, err error) { 24 | resp = new(service.UserResponse) 25 | var user model.User 26 | 27 | // 检查用户是否已经存在 28 | if exist := model.GetInstance().CheckUserExist(req.Username); !exist { 29 | resp.StatusCode = exception.UserExist 30 | resp.StatusMsg = exception.GetMsg(exception.UserExist) 31 | return resp, err 32 | } 33 | 34 | user.UserName = req.Username 35 | user.PassWord = req.Password 36 | 37 | // 创建用户 38 | err = model.GetInstance().Create(&user) 39 | if err != nil { 40 | resp.StatusCode = exception.DataErr 41 | resp.StatusMsg = exception.GetMsg(exception.DataErr) 42 | return resp, err 43 | } 44 | 45 | // 查询出ID 46 | userName, err := model.GetInstance().FindUserByName(user.UserName) 47 | if err != nil { 48 | resp.StatusCode = exception.UserUnExist 49 | resp.StatusMsg = exception.GetMsg(exception.UserUnExist) 50 | return resp, err 51 | } 52 | 53 | resp.StatusCode = exception.SUCCESS 54 | resp.StatusMsg = exception.GetMsg(exception.SUCCESS) 55 | resp.UserId = userName.Id 56 | 57 | return resp, nil 58 | } 59 | 60 | func (*UserService) UserLogin(ctx context.Context, req *service.UserRequest) (resp *service.UserResponse, err error) { 61 | resp = new(service.UserResponse) 62 | 63 | // 检查用户是否存在 64 | if exist := model.GetInstance().CheckUserExist(req.Username); exist { 65 | resp.StatusCode = exception.UserUnExist 66 | resp.StatusMsg = exception.GetMsg(exception.UserUnExist) 67 | return resp, err 68 | } 69 | 70 | // 检查密码是否正确 71 | user, err := model.GetInstance().FindUserByName(req.Username) 72 | if ok := model.GetInstance().CheckPassWord(req.Password, user.PassWord); !ok { 73 | resp.StatusCode = exception.PasswordError 74 | resp.StatusMsg = exception.GetMsg(exception.PasswordError) 75 | return resp, err 76 | } 77 | 78 | resp.StatusCode = exception.SUCCESS 79 | resp.StatusMsg = exception.GetMsg(exception.SUCCESS) 80 | resp.UserId = user.Id 81 | 82 | return resp, nil 83 | } 84 | 85 | // UserInfo 用户信息 86 | func (*UserService) UserInfo(ctx context.Context, req *service.UserInfoRequest) (resp *service.UserInfoResponse, err error) { 87 | resp = new(service.UserInfoResponse) 88 | 89 | // 根据userId切片查询user信息 90 | userIds := req.UserIds 91 | 92 | for _, userId := range userIds { 93 | // 查看缓存是否存在 需要的信息 94 | var user *model.User 95 | key := fmt.Sprintf("%s:%s:%s", "user", "info", strconv.FormatInt(userId, 10)) 96 | 97 | exists, err := cache.Redis.Exists(cache.Ctx, key).Result() 98 | if err != nil { 99 | return nil, fmt.Errorf("缓存错误:%v", err) 100 | } 101 | 102 | if exists > 0 { 103 | // 查询缓存 104 | userString, err := cache.Redis.Get(cache.Ctx, key).Result() 105 | if err != nil { 106 | return nil, fmt.Errorf("缓存错误:%v", err) 107 | } 108 | err = json.Unmarshal([]byte(userString), &user) 109 | if err != nil { 110 | return nil, err 111 | } 112 | } else { 113 | // 查询数据库 114 | user, err = model.GetInstance().FindUserById(userId) 115 | if err != nil { 116 | resp.StatusCode = exception.UserUnExist 117 | resp.StatusMsg = exception.GetMsg(exception.UserUnExist) 118 | return resp, err 119 | } 120 | 121 | // 将查询结果放入缓存中 122 | userJson, _ := json.Marshal(&user) 123 | err = cache.Redis.Set(cache.Ctx, key, userJson, 12*time.Hour).Err() 124 | if err != nil { 125 | return nil, fmt.Errorf("缓存错误:%v", err) 126 | } 127 | 128 | } 129 | resp.Users = append(resp.Users, BuildUser(user)) 130 | } 131 | 132 | resp.StatusCode = exception.SUCCESS 133 | resp.StatusMsg = exception.GetMsg(exception.SUCCESS) 134 | 135 | return resp, nil 136 | } 137 | 138 | func BuildUser(u *model.User) *service.User { 139 | user := service.User{ 140 | Id: u.Id, 141 | Name: u.UserName, 142 | Avatar: u.Avatar, 143 | BackgroundImage: u.BackgroundImage, 144 | Signature: u.Signature, 145 | } 146 | return &user 147 | } 148 | -------------------------------------------------------------------------------- /user_service/internal/model/init.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "gorm.io/driver/mysql" 6 | "gorm.io/gorm" 7 | "gorm.io/gorm/logger" 8 | "gorm.io/gorm/schema" 9 | "time" 10 | "user/config" 11 | ) 12 | 13 | // DB 创建数据库单例 14 | var DB *gorm.DB 15 | 16 | // InitDb 初始化数据库 17 | func InitDb() { 18 | config.InitConfig() 19 | dns := config.DbDnsInit() 20 | Database(dns) 21 | } 22 | 23 | // Database 初始花数据库链接 24 | func Database(connString string) { 25 | var ormLogger logger.Interface 26 | 27 | // gin中有debug 、 release 、 test 三种模式 28 | // 不指定默认以debug形式启动 29 | // 开发用debug模式、 上线用release模式 30 | // 指定方式 : gin.SetMode(gin.ReleaseMode) 31 | 32 | // 设置gorm的日志模式,可以打印原生SQL语句 33 | if gin.Mode() == "debug" { 34 | ormLogger = logger.Default.LogMode(logger.Info) 35 | } else { 36 | ormLogger = logger.Default 37 | } 38 | db, err := gorm.Open(mysql.New(mysql.Config{ 39 | DSN: connString, // DSN data source name 40 | DefaultStringSize: 256, // string 类型字段的默认长度 41 | DisableDatetimePrecision: true, // 禁用 datetime 精度,MySQL 5.6 之前的数据库不支持 42 | DontSupportRenameIndex: true, // 重命名索引时采用删除并新建的方式,MySQL 5.7 之前的数据库和 MariaDB 不支持重命名索引 43 | DontSupportRenameColumn: true, // 用 `change` 重命名列,MySQL 8 之前的数据库和 MariaDB 不支持重命名列 44 | SkipInitializeWithVersion: false, // 根据当前 MySQL 版本自动配置 45 | }), &gorm.Config{ 46 | Logger: ormLogger, 47 | NamingStrategy: schema.NamingStrategy{ 48 | SingularTable: true, // 默认不加负数 49 | }, 50 | }) 51 | 52 | if err != nil { 53 | panic(err) 54 | } 55 | 56 | sqlDB, _ := db.DB() 57 | 58 | // 设置连接池 59 | sqlDB.SetMaxIdleConns(20) // 空闲时候的最大连接数 60 | sqlDB.SetMaxOpenConns(100) // 打开时候的最大连接数 61 | sqlDB.SetConnMaxLifetime(time.Second * 20) // 超时时间 62 | DB = db 63 | 64 | migration() 65 | } 66 | -------------------------------------------------------------------------------- /user_service/internal/model/init_test.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "fmt" 5 | "github.com/spf13/viper" 6 | "testing" 7 | "user/config" 8 | ) 9 | 10 | func TestInitDb(t *testing.T) { 11 | config.InitConfig() 12 | dns := config.DbDnsInit() 13 | fmt.Print(dns) 14 | config.InitConfig() 15 | fmt.Printf("host is: %v \n", viper.GetString("mysql.host")) 16 | } 17 | -------------------------------------------------------------------------------- /user_service/internal/model/migration.go: -------------------------------------------------------------------------------- 1 | // Package model 数据库自动迁移 2 | package model 3 | 4 | import "log" 5 | 6 | func migration() { 7 | // 自动迁移 8 | err := DB.Set("gorm:table_options", "charset=utf8mb4").AutoMigrate(&User{}) 9 | 10 | if err != nil { 11 | log.Print("err") 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /user_service/internal/model/user.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "gorm.io/gorm" 5 | "sync" 6 | "user/pkg/encryption" 7 | "utils/snowFlake" 8 | ) 9 | 10 | type User struct { 11 | Id int64 `gorm:"primary_key"` 12 | UserName string `gorm:"unique"` 13 | PassWord string `gorm:"notnull"` 14 | Avatar string `gorm:"default:http://tiny-tiktok.oss-cn-chengdu.aliyuncs.com/%E9%BB%98%E8%AE%A4%E5%A4%B4%E5%83%8F.png"` // 用户头像 15 | BackgroundImage string `gorm:"default:http://tiny-tiktok.oss-cn-chengdu.aliyuncs.com/%E9%BB%98%E8%AE%A4%E8%83%8C%E6%99%AF.png"` // 用户背景 // 用户首页顶部图 16 | Signature string `gorm:"default:该用户还没有简介"` // 个人简介 17 | } 18 | 19 | type UserModel struct { 20 | } 21 | 22 | var userModel *UserModel 23 | var userOnce sync.Once // 单例模式 24 | 25 | // GetInstance 获取单例实例 26 | func GetInstance() *UserModel { 27 | userOnce.Do( 28 | func() { 29 | userModel = &UserModel{} 30 | }, 31 | ) 32 | return userModel 33 | } 34 | 35 | // Create 创建用户 36 | func (*UserModel) Create(user *User) error { 37 | flake, _ := snowFlake.NewSnowFlake(7, 1) 38 | user.Id = flake.NextId() 39 | user.PassWord = encryption.HashPassword(user.PassWord) 40 | DB.Create(&user) 41 | return nil 42 | } 43 | 44 | // FindUserByName 根据用户名称查找用户,并返回对象 45 | func (*UserModel) FindUserByName(username string) (*User, error) { 46 | user := User{} 47 | res := DB.Where("user_name=?", username).First(&user) 48 | if res.Error != nil { 49 | return nil, res.Error 50 | } 51 | return &user, nil 52 | } 53 | 54 | func (*UserModel) FindUserById(userid int64) (*User, error) { 55 | user := User{} 56 | res := DB.Where("id=?", userid).First(&user) 57 | if res.Error != nil { 58 | return nil, res.Error 59 | } 60 | 61 | return &user, nil 62 | } 63 | 64 | // CheckUserExist 检查User是否存在(已经被注册过了) 65 | func (*UserModel) CheckUserExist(username string) bool { 66 | user := User{} 67 | err := DB.Where("user_name=?", username).First(&user).Error 68 | if err == gorm.ErrRecordNotFound { 69 | return true 70 | } 71 | 72 | return false 73 | } 74 | 75 | // CheckPassWord 检查密码是否正确 76 | func (*UserModel) CheckPassWord(password string, storePassword string) bool { 77 | return encryption.VerifyPasswordWithHash(password, storePassword) 78 | } 79 | -------------------------------------------------------------------------------- /user_service/internal/model/user_test.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | ) 7 | 8 | func TestUser_Create(t *testing.T) { 9 | InitDb() 10 | user := &User{ 11 | UserName: "张三", 12 | PassWord: "123456", 13 | } 14 | GetInstance().Create(user) 15 | } 16 | 17 | func TestUserModel_FindUserByName(t *testing.T) { 18 | InitDb() 19 | user, _ := GetInstance().FindUserByName("ben") 20 | fmt.Print(user) 21 | } 22 | 23 | func TestUserModel_CheckUserExist(t *testing.T) { 24 | InitDb() 25 | exist := GetInstance().CheckUserExist("lisi") 26 | fmt.Println(exist) 27 | } 28 | 29 | func TestUserModel_FindUserById(t *testing.T) { 30 | InitDb() 31 | user, _ := GetInstance().FindUserById(4391185549234176) 32 | fmt.Println(user) 33 | } 34 | -------------------------------------------------------------------------------- /user_service/internal/service/pb/userService.proto: -------------------------------------------------------------------------------- 1 | syntax="proto3"; 2 | package pb; 3 | option go_package = "../;service"; // 此处格式为<生成的文件存放位置;生成文件的包名> 4 | 5 | // protoc -I internal/service/pb --go_out=./internal/service/ --go_opt=paths=source_relative --go-grpc_out=./internal/service/ --go-grpc_opt=paths=source_relative internal/service/pb/*.proto 6 | // 或者分开使用 7 | // protoc -I internal/service/pb/ --go_out=./ internal/service/pb/*.proto 8 | // protoc -I internal/service/pb/ --go-grpc_out=./ internal/service/pb/*.proto 9 | 10 | message UserRequest{ 11 | // @gotags:json:"username" form:"username" uri:"username" 12 | string Username = 1; // 账号 13 | // @gotags:json:"password" form:"password" uri:"password" 14 | string Password = 2; // 密码 15 | } 16 | 17 | message UserResponse{ 18 | // @gotags:json:"status_code" form:"status_code" uri:"status_code" 19 | int64 StatusCode = 1; 20 | // @gotags:json:"status_msg" form:"status_msg" uri:"status_msg" 21 | string StatusMsg = 2; 22 | // @gotags:json:"user_id" form:"user_id" uri:"user_id" 23 | int64 UserId = 3; // 用户id 24 | } 25 | 26 | message User { 27 | // @gotags:json:"id" form:"id" uri:"id" 28 | int64 Id = 1; 29 | // @gotags:json:"name" form:"name" uri:"name" 30 | string Name = 2; 31 | // @gotags:json:"avatar" form:"avatar" uri:"avatar" 32 | string Avatar = 3; // 用户头像 33 | // @gotags:json:"background_image" form:"background_image" uri:"background_image" 34 | string BackgroundImage = 4; // 用户背景图 35 | // @gotags:json:"signature" form:"signature" uri:"signature" 36 | string Signature = 5; // 用户签名 37 | } 38 | 39 | message UserInfoRequest { 40 | // @gotags:json:"user_ids" form:"user_ids" uri:"user_ids" 41 | repeated int64 UserIds = 1; // 传入一个userId切片 42 | } 43 | 44 | message UserInfoResponse { 45 | // @gotags:json:"status_code" form:"status_code" uri:"status_code" 46 | int64 StatusCode = 1; 47 | // @gotags:json:"status_msg" form:"status_msg" uri:"status_msg" 48 | string StatusMsg = 2; 49 | // @gotags:json:"users" form:"users" uri:"users" 50 | repeated User Users = 3; // 用户信息 51 | } 52 | 53 | 54 | service UserService{ 55 | rpc UserRegister(UserRequest) returns(UserResponse); 56 | rpc UserLogin(UserRequest) returns(UserResponse); 57 | rpc UserInfo(UserInfoRequest) returns(UserInfoResponse); 58 | } -------------------------------------------------------------------------------- /user_service/internal/service/userService_grpc.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-go-grpc. DO NOT EDIT. 2 | // versions: 3 | // - protoc-gen-go-grpc v1.3.0 4 | // - protoc v3.15.5 5 | // source: userService.proto 6 | 7 | package service 8 | 9 | import ( 10 | context "context" 11 | grpc "google.golang.org/grpc" 12 | codes "google.golang.org/grpc/codes" 13 | status "google.golang.org/grpc/status" 14 | ) 15 | 16 | // This is a compile-time assertion to ensure that this generated file 17 | // is compatible with the grpc package it is being compiled against. 18 | // Requires gRPC-Go v1.32.0 or later. 19 | const _ = grpc.SupportPackageIsVersion7 20 | 21 | const ( 22 | UserService_UserRegister_FullMethodName = "/pb.UserService/UserRegister" 23 | UserService_UserLogin_FullMethodName = "/pb.UserService/UserLogin" 24 | UserService_UserInfo_FullMethodName = "/pb.UserService/UserInfo" 25 | ) 26 | 27 | // UserServiceClient is the client API for UserService service. 28 | // 29 | // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. 30 | type UserServiceClient interface { 31 | UserRegister(ctx context.Context, in *UserRequest, opts ...grpc.CallOption) (*UserResponse, error) 32 | UserLogin(ctx context.Context, in *UserRequest, opts ...grpc.CallOption) (*UserResponse, error) 33 | UserInfo(ctx context.Context, in *UserInfoRequest, opts ...grpc.CallOption) (*UserInfoResponse, error) 34 | } 35 | 36 | type userServiceClient struct { 37 | cc grpc.ClientConnInterface 38 | } 39 | 40 | func NewUserServiceClient(cc grpc.ClientConnInterface) UserServiceClient { 41 | return &userServiceClient{cc} 42 | } 43 | 44 | func (c *userServiceClient) UserRegister(ctx context.Context, in *UserRequest, opts ...grpc.CallOption) (*UserResponse, error) { 45 | out := new(UserResponse) 46 | err := c.cc.Invoke(ctx, UserService_UserRegister_FullMethodName, in, out, opts...) 47 | if err != nil { 48 | return nil, err 49 | } 50 | return out, nil 51 | } 52 | 53 | func (c *userServiceClient) UserLogin(ctx context.Context, in *UserRequest, opts ...grpc.CallOption) (*UserResponse, error) { 54 | out := new(UserResponse) 55 | err := c.cc.Invoke(ctx, UserService_UserLogin_FullMethodName, in, out, opts...) 56 | if err != nil { 57 | return nil, err 58 | } 59 | return out, nil 60 | } 61 | 62 | func (c *userServiceClient) UserInfo(ctx context.Context, in *UserInfoRequest, opts ...grpc.CallOption) (*UserInfoResponse, error) { 63 | out := new(UserInfoResponse) 64 | err := c.cc.Invoke(ctx, UserService_UserInfo_FullMethodName, in, out, opts...) 65 | if err != nil { 66 | return nil, err 67 | } 68 | return out, nil 69 | } 70 | 71 | // UserServiceServer is the server API for UserService service. 72 | // All implementations must embed UnimplementedUserServiceServer 73 | // for forward compatibility 74 | type UserServiceServer interface { 75 | UserRegister(context.Context, *UserRequest) (*UserResponse, error) 76 | UserLogin(context.Context, *UserRequest) (*UserResponse, error) 77 | UserInfo(context.Context, *UserInfoRequest) (*UserInfoResponse, error) 78 | mustEmbedUnimplementedUserServiceServer() 79 | } 80 | 81 | // UnimplementedUserServiceServer must be embedded to have forward compatible implementations. 82 | type UnimplementedUserServiceServer struct { 83 | } 84 | 85 | func (UnimplementedUserServiceServer) UserRegister(context.Context, *UserRequest) (*UserResponse, error) { 86 | return nil, status.Errorf(codes.Unimplemented, "method UserRegister not implemented") 87 | } 88 | func (UnimplementedUserServiceServer) UserLogin(context.Context, *UserRequest) (*UserResponse, error) { 89 | return nil, status.Errorf(codes.Unimplemented, "method UserLogin not implemented") 90 | } 91 | func (UnimplementedUserServiceServer) UserInfo(context.Context, *UserInfoRequest) (*UserInfoResponse, error) { 92 | return nil, status.Errorf(codes.Unimplemented, "method UserInfo not implemented") 93 | } 94 | func (UnimplementedUserServiceServer) mustEmbedUnimplementedUserServiceServer() {} 95 | 96 | // UnsafeUserServiceServer may be embedded to opt out of forward compatibility for this service. 97 | // Use of this interface is not recommended, as added methods to UserServiceServer will 98 | // result in compilation errors. 99 | type UnsafeUserServiceServer interface { 100 | mustEmbedUnimplementedUserServiceServer() 101 | } 102 | 103 | func RegisterUserServiceServer(s grpc.ServiceRegistrar, srv UserServiceServer) { 104 | s.RegisterService(&UserService_ServiceDesc, srv) 105 | } 106 | 107 | func _UserService_UserRegister_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 108 | in := new(UserRequest) 109 | if err := dec(in); err != nil { 110 | return nil, err 111 | } 112 | if interceptor == nil { 113 | return srv.(UserServiceServer).UserRegister(ctx, in) 114 | } 115 | info := &grpc.UnaryServerInfo{ 116 | Server: srv, 117 | FullMethod: UserService_UserRegister_FullMethodName, 118 | } 119 | handler := func(ctx context.Context, req interface{}) (interface{}, error) { 120 | return srv.(UserServiceServer).UserRegister(ctx, req.(*UserRequest)) 121 | } 122 | return interceptor(ctx, in, info, handler) 123 | } 124 | 125 | func _UserService_UserLogin_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 126 | in := new(UserRequest) 127 | if err := dec(in); err != nil { 128 | return nil, err 129 | } 130 | if interceptor == nil { 131 | return srv.(UserServiceServer).UserLogin(ctx, in) 132 | } 133 | info := &grpc.UnaryServerInfo{ 134 | Server: srv, 135 | FullMethod: UserService_UserLogin_FullMethodName, 136 | } 137 | handler := func(ctx context.Context, req interface{}) (interface{}, error) { 138 | return srv.(UserServiceServer).UserLogin(ctx, req.(*UserRequest)) 139 | } 140 | return interceptor(ctx, in, info, handler) 141 | } 142 | 143 | func _UserService_UserInfo_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 144 | in := new(UserInfoRequest) 145 | if err := dec(in); err != nil { 146 | return nil, err 147 | } 148 | if interceptor == nil { 149 | return srv.(UserServiceServer).UserInfo(ctx, in) 150 | } 151 | info := &grpc.UnaryServerInfo{ 152 | Server: srv, 153 | FullMethod: UserService_UserInfo_FullMethodName, 154 | } 155 | handler := func(ctx context.Context, req interface{}) (interface{}, error) { 156 | return srv.(UserServiceServer).UserInfo(ctx, req.(*UserInfoRequest)) 157 | } 158 | return interceptor(ctx, in, info, handler) 159 | } 160 | 161 | // UserService_ServiceDesc is the grpc.ServiceDesc for UserService service. 162 | // It's only intended for direct use with grpc.RegisterService, 163 | // and not to be introspected or modified (even as a copy) 164 | var UserService_ServiceDesc = grpc.ServiceDesc{ 165 | ServiceName: "pb.UserService", 166 | HandlerType: (*UserServiceServer)(nil), 167 | Methods: []grpc.MethodDesc{ 168 | { 169 | MethodName: "UserRegister", 170 | Handler: _UserService_UserRegister_Handler, 171 | }, 172 | { 173 | MethodName: "UserLogin", 174 | Handler: _UserService_UserLogin_Handler, 175 | }, 176 | { 177 | MethodName: "UserInfo", 178 | Handler: _UserService_UserInfo_Handler, 179 | }, 180 | }, 181 | Streams: []grpc.StreamDesc{}, 182 | Metadata: "userService.proto", 183 | } 184 | -------------------------------------------------------------------------------- /user_service/pkg/cache/redis.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "context" 5 | "github.com/go-redis/redis/v8" 6 | "github.com/spf13/viper" 7 | ) 8 | 9 | var Redis *redis.Client 10 | 11 | var Ctx = context.Background() 12 | 13 | // InitRedis 连接redis 14 | func InitRedis() { 15 | addr := viper.GetString("redis.address") 16 | Redis = redis.NewClient(&redis.Options{ 17 | Addr: addr, 18 | Password: "", 19 | DB: 0, // 存入DB0 20 | }) 21 | } 22 | -------------------------------------------------------------------------------- /user_service/pkg/cache/redis_test.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | ) 7 | 8 | func TestInitRedis(t *testing.T) { 9 | InitRedis() 10 | Redis.Set(context.Background(), "K2", "myValue", 0) 11 | } 12 | -------------------------------------------------------------------------------- /user_service/pkg/encryption/encrypt.go: -------------------------------------------------------------------------------- 1 | // 密码加密 2 | 3 | package encryption 4 | 5 | import "golang.org/x/crypto/bcrypt" 6 | 7 | const PassWordCost = 12 // 加密难度,相当与hash被迭代2^12次。默认是10 ,推荐使用12 8 | 9 | func HashPassword(password string) string { 10 | hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), PassWordCost) 11 | if err != nil { 12 | return "" 13 | } 14 | return string(hashedPassword) 15 | } 16 | 17 | // VerifyPasswordWithHash 方法验证传入的密码与数据库中的密码哈希是否匹配 18 | func VerifyPasswordWithHash(inputPassword string, storedHash string) bool { 19 | // 将数据库中的密码哈希字符串解析为哈希字节 20 | storedHashBytes := []byte(storedHash) 21 | 22 | // 使用 bcrypt 提供的 CompareHashAndPassword 方法比较密码 23 | err := bcrypt.CompareHashAndPassword(storedHashBytes, []byte(inputPassword)) 24 | return err == nil 25 | } 26 | -------------------------------------------------------------------------------- /utils/etcd/discovery.go: -------------------------------------------------------------------------------- 1 | package etcd 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "go.etcd.io/etcd/api/v3/mvccpb" 7 | clientv3 "go.etcd.io/etcd/client/v3" 8 | "log" 9 | "sync" 10 | "time" 11 | ) 12 | 13 | // EtcdDiscovery 服务发现 14 | type EtcdDiscovery struct { 15 | cli *clientv3.Client // etcd连接 16 | serviceMap map[string]string // 服务列表(k-v列表) 17 | lock sync.RWMutex // 读写互斥锁 18 | } 19 | 20 | func NewServiceDiscovery(endpoints []string) (*EtcdDiscovery, error) { 21 | // 创建etcdClient对象 22 | cli, err := clientv3.New(clientv3.Config{ 23 | Endpoints: endpoints, 24 | DialTimeout: 5 * time.Second, 25 | }) 26 | 27 | if err != nil { 28 | return nil, err 29 | } 30 | 31 | return &EtcdDiscovery{ 32 | cli: cli, 33 | serviceMap: make(map[string]string), // 初始化kvMap 34 | }, nil 35 | } 36 | 37 | // ServiceDiscovery 读取etcd的服务并开启协程监听kv变化 38 | func (e *EtcdDiscovery) ServiceDiscovery(prefix string) error { 39 | // 根据服务名称的前缀,获取所有的注册服务 40 | resp, err := e.cli.Get(context.Background(), prefix, clientv3.WithPrefix()) 41 | if err != nil { 42 | return err 43 | } 44 | 45 | // 遍历key-value存储到本地map 46 | for _, kv := range resp.Kvs { 47 | e.putService(string(kv.Key), string(kv.Value)) 48 | } 49 | 50 | // 开启监听协程,监听prefix的变化 51 | go func() { 52 | watchRespChan := e.cli.Watch(context.Background(), prefix, clientv3.WithPrefix()) 53 | log.Printf("watching prefix:%s now...", prefix) 54 | for watchResp := range watchRespChan { 55 | for _, event := range watchResp.Events { 56 | switch event.Type { 57 | case mvccpb.PUT: // 发生了修改或者新增 58 | e.putService(string(event.Kv.Key), string(event.Kv.Value)) // ServiceMap中进行相应的修改或新增 59 | case mvccpb.DELETE: //发生了删除 60 | e.delService(string(event.Kv.Key)) // ServiceMap中进行相应的删除 61 | } 62 | } 63 | } 64 | }() 65 | 66 | return nil 67 | } 68 | 69 | // SetService 新增或修改本地服务 70 | func (s *EtcdDiscovery) putService(key, val string) { 71 | s.lock.Lock() 72 | s.serviceMap[key] = val 73 | s.lock.Unlock() 74 | log.Println("put key :", key, "val:", val) 75 | } 76 | 77 | // DelService 删除本地服务 78 | func (s *EtcdDiscovery) delService(key string) { 79 | s.lock.Lock() 80 | delete(s.serviceMap, key) 81 | s.lock.Unlock() 82 | log.Println("del key:", key) 83 | } 84 | 85 | // GetService 获取本地服务 86 | func (s *EtcdDiscovery) GetService(serviceName string) (string, error) { 87 | s.lock.RLock() 88 | serviceAddr, ok := s.serviceMap[serviceName] 89 | s.lock.RUnlock() 90 | if !ok { 91 | return "", fmt.Errorf("can not get serviceAddr") 92 | } 93 | return serviceAddr, nil 94 | } 95 | 96 | // Close 关闭服务 97 | func (e *EtcdDiscovery) Close() error { 98 | return e.cli.Close() 99 | } 100 | -------------------------------------------------------------------------------- /utils/etcd/register.go: -------------------------------------------------------------------------------- 1 | package etcd 2 | 3 | import ( 4 | "context" 5 | clientv3 "go.etcd.io/etcd/client/v3" 6 | "log" 7 | "time" 8 | ) 9 | 10 | // EtcdRegister 服务注册 11 | type EtcdRegister struct { 12 | etcdCli *clientv3.Client // etcdClient对象 13 | leaseId clientv3.LeaseID // 租约id 14 | } 15 | 16 | // CreateLease 创建租约。expire表示有效期(s) 17 | func (e *EtcdRegister) CreateLease(expire int64) error { 18 | 19 | lease, err := e.etcdCli.Grant(context.Background(), expire) 20 | if err != nil { 21 | log.Println(err.Error()) 22 | return err 23 | } 24 | 25 | e.leaseId = lease.ID // 记录生成的租约Id 26 | return nil 27 | } 28 | 29 | // BindLease 绑定租约。将租约与对应的key-value绑定 30 | func (e *EtcdRegister) BindLease(key string, value string) error { 31 | 32 | res, err := e.etcdCli.Put(context.Background(), key, value, clientv3.WithLease(e.leaseId)) 33 | if err != nil { 34 | log.Println(err.Error()) 35 | return err 36 | } 37 | 38 | log.Printf("bind lease success %v \n", res) 39 | return nil 40 | } 41 | 42 | // KeepAlive 获取续约通道 并 持续续租 43 | func (e *EtcdRegister) KeepAlive() error { 44 | keepAliveChan, err := e.etcdCli.KeepAlive(context.Background(), e.leaseId) 45 | 46 | if err != nil { 47 | log.Println(err.Error()) 48 | return err 49 | } 50 | 51 | go func(keepAliveChan <-chan *clientv3.LeaseKeepAliveResponse) { 52 | for { 53 | select { 54 | case resp := <-keepAliveChan: 55 | log.Printf("续约成功...leaseID=%d", resp.ID) 56 | } 57 | } 58 | }(keepAliveChan) 59 | 60 | return nil 61 | } 62 | 63 | // Close 关闭服务 64 | func (e *EtcdRegister) Close() error { 65 | log.Printf("close...\n") 66 | // 撤销租约 67 | e.etcdCli.Revoke(context.Background(), e.leaseId) 68 | return e.etcdCli.Close() 69 | } 70 | 71 | // NewEtcdRegister 初始化etcd服务注册对象 72 | func NewEtcdRegister(etcdServerAddr string) (*EtcdRegister, error) { 73 | 74 | client, err := clientv3.New(clientv3.Config{ 75 | Endpoints: []string{etcdServerAddr}, 76 | DialTimeout: 3 * time.Second, 77 | }) 78 | 79 | if err != nil { 80 | log.Println(err.Error()) 81 | return nil, err 82 | } 83 | 84 | e := &EtcdRegister{ 85 | etcdCli: client, 86 | } 87 | return e, nil 88 | } 89 | 90 | // ServiceRegister 服务注册。expire表示过期时间,serviceName和serviceAddr分别是服务名与服务地址 91 | func (e *EtcdRegister) ServiceRegister(serviceName, serviceAddr string, expire int64) (err error) { 92 | 93 | // 创建租约 94 | err = e.CreateLease(expire) 95 | if err != nil { 96 | return err 97 | } 98 | 99 | // 将租约与k-v绑定 100 | err = e.BindLease(serviceName, serviceAddr) 101 | if err != nil { 102 | return err 103 | } 104 | 105 | // 持续续租 106 | err = e.KeepAlive() 107 | return err 108 | } 109 | -------------------------------------------------------------------------------- /utils/exception/statusCode.go: -------------------------------------------------------------------------------- 1 | package exception 2 | 3 | //* 错误码: 4 | //* 四位组成 5 | //* 1. 1开头代表用户端错误 6 | //* 2. 2开头代表当前系统异常 7 | //* 3. 3开头代表第三方服务异常 8 | //* 4. 4开头若无法确定具体错误,选择宏观错误 9 | //* 5. 大的错误类间的步长间距预留100 10 | 11 | const ( 12 | SUCCESS = 0 // 常规的返回成功 13 | ERROR = -1 // 常规返回失败 14 | 15 | RequestERROR = 1000 // token相关 16 | UnAuth = 1001 17 | TokenTimeOut = 1002 18 | 19 | ErrOperate = 1200 // 异常操作 20 | 21 | DataErr = 2000 // 数据创建 22 | CacheErr = 2001 // 缓存异常 23 | 24 | UserExist = 2100 // 用户相关 25 | UserUnExist = 2101 26 | PasswordError = 2102 27 | 28 | VideoUnExist = 2200 // 视频相关 29 | VideoUploadErr = 2201 30 | VideoFavoriteErr = 2203 31 | UserNoVideo = 2204 32 | 33 | FavoriteErr = 2300 // 点赞相关 34 | CancelFavoriteErr = 2301 35 | VideoNoFavorite = 2302 36 | UserNoFavorite = 2303 37 | 38 | CommentErr = 2400 // 评论失败 39 | CommentUnExist = 2401 // 评论相关 40 | CommentDeleteErr = 2402 41 | 42 | FollowSelfErr = 2500 // 关注相关 43 | ) 44 | -------------------------------------------------------------------------------- /utils/exception/statusMsg.go: -------------------------------------------------------------------------------- 1 | package exception 2 | 3 | var Msg = map[int]string{ 4 | 5 | SUCCESS: "OK", 6 | ERROR: "fail", 7 | 8 | RequestERROR: "请求有误,用户未登录", 9 | UnAuth: "Token未经授权", 10 | TokenTimeOut: "Token授权已过期", 11 | 12 | ErrOperate: "错误操作", 13 | 14 | DataErr: "数据创建错误", 15 | CacheErr: "Redis异常", 16 | 17 | UserExist: "用户已经存在", 18 | UserUnExist: "用户不存在,请注册", 19 | PasswordError: "密码错误", 20 | 21 | VideoUnExist: "视频信息不存在", 22 | VideoUploadErr: "视频上传失败", 23 | VideoFavoriteErr: "视频点赞失败", 24 | UserNoVideo: "用户没有发布作品", 25 | 26 | FavoriteErr: "点赞失败", 27 | CancelFavoriteErr: "取消点赞失败", 28 | VideoNoFavorite: "视频没有点赞", 29 | UserNoFavorite: "用户没有点赞", 30 | 31 | CommentErr: "评论失败", 32 | CommentUnExist: "评论不存在", 33 | CommentDeleteErr: "评论删除失败", 34 | 35 | FollowSelfErr: "关注自己", 36 | } 37 | 38 | // GetMsg 根据状态码获取对应信息 39 | func GetMsg(code int) string { 40 | msg, ok := Msg[code] 41 | if ok { 42 | return msg 43 | } 44 | 45 | return Msg[ERROR] 46 | } 47 | -------------------------------------------------------------------------------- /utils/go.mod: -------------------------------------------------------------------------------- 1 | module utils 2 | 3 | go 1.19 4 | 5 | require ( 6 | go.etcd.io/etcd/api/v3 v3.5.9 7 | go.etcd.io/etcd/client/v3 v3.5.9 8 | ) 9 | 10 | require ( 11 | github.com/coreos/go-semver v0.3.0 // indirect 12 | github.com/coreos/go-systemd/v22 v22.3.2 // indirect 13 | github.com/gogo/protobuf v1.3.2 // indirect 14 | github.com/golang/protobuf v1.5.2 // indirect 15 | go.etcd.io/etcd/client/pkg/v3 v3.5.9 // indirect 16 | go.uber.org/atomic v1.7.0 // indirect 17 | go.uber.org/multierr v1.6.0 // indirect 18 | go.uber.org/zap v1.17.0 // indirect 19 | golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4 // indirect 20 | golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40 // indirect 21 | golang.org/x/text v0.3.5 // indirect 22 | google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c // indirect 23 | google.golang.org/grpc v1.41.0 // indirect 24 | google.golang.org/protobuf v1.26.0 // indirect 25 | ) 26 | -------------------------------------------------------------------------------- /utils/snowFlake/snow.go: -------------------------------------------------------------------------------- 1 | package snowFlake 2 | 3 | import ( 4 | "errors" 5 | "sync" 6 | "time" 7 | ) 8 | 9 | const ( 10 | // 当前时间 2023-08-07 10:47:09 毫秒级别 11 | epoch int64 = 1691376429015 12 | // 序列号位数 13 | numberBit int8 = 12 14 | // 服务ID位数 15 | serveIdBit int8 = 5 16 | // 机器ID位数 17 | machineIdBit int8 = 5 18 | // 服务ID的偏移量 19 | serveIdShift int8 = numberBit 20 | // 机器ID的偏移量 21 | machineIdShift int8 = numberBit + serveIdBit 22 | // 时间戳的偏移量 23 | timestampShift int8 = numberBit + serveIdBit + machineIdBit 24 | // 服务ID的最大值 31 25 | serverIdMax int64 = -1 ^ (-1 << serveIdBit) 26 | // 机器ID的最大值 31 27 | machineIdMax int64 = -1 ^ (-1 << machineIdBit) 28 | // 序列号的最大值 4095 29 | numberMax int64 = -1 ^ (-1 << numberBit) 30 | ) 31 | 32 | type SnowFlake struct { 33 | // 每次生产一个id都是一个原子操作 34 | lock sync.Mutex 35 | // 时间戳、机器ID、服务ID、序列号 36 | timestamp int64 37 | machineId int64 38 | serveId int64 39 | number int64 40 | } 41 | 42 | // NewSnowFlake 构造函数,传入机器ID和服务ID 43 | func NewSnowFlake(machineId int64, serveId int64) (*SnowFlake, error) { 44 | if machineId < 0 || machineId > machineIdMax { 45 | return nil, errors.New("mechineId超出限制") 46 | } 47 | if serveId < 0 || serveId > serverIdMax { 48 | return nil, errors.New("serveId超出限制") 49 | } 50 | return &SnowFlake{ 51 | timestamp: 0, 52 | machineId: machineId, 53 | serveId: serveId, 54 | number: 0, 55 | }, nil 56 | } 57 | 58 | func (snow *SnowFlake) NextId() int64 { 59 | // 原子操作 60 | snow.lock.Lock() 61 | defer snow.lock.Unlock() 62 | 63 | // 获取当前时间戳 64 | now := time.Now().UnixMilli() 65 | 66 | // 如果时间戳还是当前时间错,则序列号增加 67 | if now == snow.timestamp { 68 | snow.number++ 69 | // 如果超过了序列号的最大值,则更新时间戳 70 | if snow.number > numberMax { 71 | for now <= snow.timestamp { 72 | now = time.Now().UnixMilli() 73 | } 74 | } 75 | } else { 76 | snow.number = 0 77 | snow.timestamp = now 78 | } 79 | 80 | // 拼接最后的结果,将不同不服的数值移到指定位置 81 | id := (snow.timestamp-epoch)< ../utils 20 | 21 | require ( 22 | github.com/bytedance/sonic v1.9.1 // indirect 23 | github.com/cespare/xxhash/v2 v2.2.0 // indirect 24 | github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect 25 | github.com/coreos/go-semver v0.3.0 // indirect 26 | github.com/coreos/go-systemd/v22 v22.3.2 // indirect 27 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect 28 | github.com/fsnotify/fsnotify v1.6.0 // indirect 29 | github.com/gabriel-vasile/mimetype v1.4.2 // indirect 30 | github.com/gin-contrib/sse v0.1.0 // indirect 31 | github.com/go-playground/locales v0.14.1 // indirect 32 | github.com/go-playground/universal-translator v0.18.1 // indirect 33 | github.com/go-playground/validator/v10 v10.14.0 // indirect 34 | github.com/go-sql-driver/mysql v1.7.0 // indirect 35 | github.com/goccy/go-json v0.10.2 // indirect 36 | github.com/gogo/protobuf v1.3.2 // indirect 37 | github.com/golang/protobuf v1.5.3 // indirect 38 | github.com/hashicorp/hcl v1.0.0 // indirect 39 | github.com/jinzhu/inflection v1.0.0 // indirect 40 | github.com/jinzhu/now v1.1.5 // indirect 41 | github.com/json-iterator/go v1.1.12 // indirect 42 | github.com/klauspost/cpuid/v2 v2.2.4 // indirect 43 | github.com/leodido/go-urn v1.2.4 // indirect 44 | github.com/magiconair/properties v1.8.7 // indirect 45 | github.com/mattn/go-isatty v0.0.19 // indirect 46 | github.com/mitchellh/mapstructure v1.5.0 // indirect 47 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 48 | github.com/modern-go/reflect2 v1.0.2 // indirect 49 | github.com/pelletier/go-toml/v2 v2.0.8 // indirect 50 | github.com/spf13/afero v1.9.5 // indirect 51 | github.com/spf13/cast v1.5.1 // indirect 52 | github.com/spf13/jwalterweatherman v1.1.0 // indirect 53 | github.com/spf13/pflag v1.0.5 // indirect 54 | github.com/subosito/gotenv v1.4.2 // indirect 55 | github.com/twitchyliquid64/golang-asm v0.15.1 // indirect 56 | github.com/ugorji/go/codec v1.2.11 // indirect 57 | go.etcd.io/etcd/api/v3 v3.5.9 // indirect 58 | go.etcd.io/etcd/client/pkg/v3 v3.5.9 // indirect 59 | go.etcd.io/etcd/client/v3 v3.5.9 // indirect 60 | go.uber.org/atomic v1.9.0 // indirect 61 | go.uber.org/multierr v1.8.0 // indirect 62 | go.uber.org/zap v1.21.0 // indirect 63 | golang.org/x/arch v0.3.0 // indirect 64 | golang.org/x/crypto v0.9.0 // indirect 65 | golang.org/x/net v0.10.0 // indirect 66 | golang.org/x/sys v0.8.0 // indirect 67 | golang.org/x/text v0.9.0 // indirect 68 | golang.org/x/time v0.1.0 // indirect 69 | google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect 70 | gopkg.in/ini.v1 v1.67.0 // indirect 71 | gopkg.in/yaml.v3 v3.0.1 // indirect 72 | ) 73 | -------------------------------------------------------------------------------- /video_service/internal/handler/comment.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "github.com/go-redis/redis/v8" 8 | "log" 9 | "strconv" 10 | "time" 11 | "utils/exception" 12 | "video/internal/model" 13 | "video/internal/service" 14 | "video/pkg/cache" 15 | ) 16 | 17 | // CommentAction 评论操作 18 | func (*VideoService) CommentAction(ctx context.Context, req *service.CommentActionRequest) (resp *service.CommentActionResponse, err error) { 19 | resp = new(service.CommentActionResponse) 20 | key := fmt.Sprintf("%s:%s:%s", "video", "comment_list", strconv.FormatInt(req.VideoId, 10)) 21 | comment := model.Comment{ 22 | UserId: req.UserId, 23 | VideoId: req.VideoId, 24 | Content: req.CommentText, 25 | } 26 | action := req.ActionType 27 | 28 | time := time.Now() 29 | 30 | // 发布评论 31 | if action == 1 { 32 | comment.CreatAt = time 33 | 34 | // 事务中执行创建操作 35 | tx := model.DB.Begin() 36 | id, err := model.GetCommentInstance().CreateComment(tx, &comment) 37 | if err != nil { 38 | resp.StatusCode = exception.CommentErr 39 | resp.StatusMsg = exception.GetMsg(exception.CommentErr) 40 | resp.Comment = nil 41 | return resp, err 42 | } 43 | comment.Id = id 44 | comment.CommentStatus = true 45 | commentJson, _ := json.Marshal(comment) 46 | 47 | // 存入缓存中 48 | member := redis.Z{ 49 | Score: float64(time.Unix()), 50 | Member: commentJson, 51 | } 52 | 53 | err = cache.Redis.ZAdd(cache.Ctx, key, &member).Err() 54 | if err != nil { 55 | tx.Rollback() 56 | return nil, fmt.Errorf("缓存错误:%v", err) 57 | } 58 | 59 | tx.Commit() 60 | 61 | commentResp := &service.Comment{ 62 | Id: id, 63 | Content: req.CommentText, 64 | // 将Time.time转换成字符串形式,格式为mm-dd 65 | CreateDate: time.Format("01-02"), 66 | } 67 | 68 | // 将评论返回 69 | resp.StatusCode = exception.SUCCESS 70 | resp.StatusMsg = exception.GetMsg(exception.SUCCESS) 71 | resp.Comment = commentResp 72 | 73 | return resp, nil 74 | } 75 | 76 | // 删除评论 77 | tx := model.DB.Begin() 78 | commentInstance, err := model.GetCommentInstance().GetComment(tx, req.CommentId) 79 | commentMarshal, _ := json.Marshal(commentInstance) 80 | 81 | err = model.GetCommentInstance().DeleteComment(req.CommentId) 82 | if err != nil { 83 | resp.StatusCode = exception.CommentDeleteErr 84 | resp.StatusMsg = exception.GetMsg(exception.CommentDeleteErr) 85 | resp.Comment = nil 86 | 87 | return resp, err 88 | } 89 | log.Print(commentMarshal) 90 | 91 | // 删除缓存 92 | count, err := cache.Redis.ZRem(cache.Ctx, key, string(commentMarshal)).Result() 93 | log.Printf("删除了: %v 条记录", count) 94 | if err != nil { 95 | tx.Rollback() 96 | return nil, fmt.Errorf("缓存错误:%v", err) 97 | } 98 | 99 | tx.Commit() 100 | 101 | resp.StatusCode = exception.SUCCESS 102 | resp.StatusMsg = exception.GetMsg(exception.SUCCESS) 103 | resp.Comment = nil 104 | 105 | return resp, nil 106 | } 107 | 108 | // CommentList 评论列表 109 | func (*VideoService) CommentList(ctx context.Context, req *service.CommentListRequest) (resp *service.CommentListResponse, err error) { 110 | resp = new(service.CommentListResponse) 111 | var comments []model.Comment 112 | key := fmt.Sprintf("%s:%s:%s", "video", "comment_list", strconv.FormatInt(req.VideoId, 10)) 113 | 114 | exist, err := cache.Redis.Exists(cache.Ctx, key).Result() 115 | if err != nil { 116 | log.Print(4) 117 | return nil, fmt.Errorf("缓存错误:%v", err) 118 | } 119 | 120 | if exist == 0 { 121 | err := buildCommentCache(req.VideoId) 122 | if err != nil { 123 | log.Print(3) 124 | return nil, fmt.Errorf("缓存错误:%v", err) 125 | } 126 | } 127 | 128 | // 查询缓存 129 | commentsString, err := cache.Redis.ZRevRange(cache.Ctx, key, 0, -1).Result() 130 | if err != nil { 131 | log.Print(5) 132 | return nil, fmt.Errorf("缓存错误:%v", err) 133 | } 134 | 135 | for _, commentString := range commentsString { 136 | var comment model.Comment 137 | err := json.Unmarshal([]byte(commentString), &comment) 138 | if err != nil { 139 | return nil, err 140 | } 141 | comments = append(comments, comment) 142 | } 143 | 144 | resp.StatusCode = exception.SUCCESS 145 | resp.StatusMsg = exception.GetMsg(exception.SUCCESS) 146 | resp.CommentList = BuildComments(comments) 147 | 148 | return resp, nil 149 | } 150 | 151 | func BuildComments(comments []model.Comment) []*service.Comment { 152 | var commentresp []*service.Comment 153 | 154 | for _, comment := range comments { 155 | commentresp = append(commentresp, &service.Comment{ 156 | Id: comment.Id, 157 | UserId: comment.UserId, 158 | Content: comment.Content, 159 | CreateDate: comment.CreatAt.Format("01-02"), 160 | }) 161 | } 162 | 163 | return commentresp 164 | } 165 | 166 | // 构建评论列表缓存 167 | func buildCommentCache(videoId int64) error { 168 | key := fmt.Sprintf("%s:%s:%s", "video", "comment_list", strconv.FormatInt(videoId, 10)) 169 | 170 | comments, err := model.GetCommentInstance().CommentList(videoId) 171 | if err != nil { 172 | return err 173 | } 174 | 175 | var zMembers []*redis.Z 176 | for _, comment := range comments { 177 | commentJSON, err := json.Marshal(comment) 178 | if err != nil { 179 | fmt.Println("Error encoding comment:", err) 180 | continue 181 | } 182 | zMembers = append(zMembers, &redis.Z{ 183 | Score: float64(comment.CreatAt.Unix()), 184 | Member: commentJSON, 185 | }) 186 | } 187 | 188 | err = cache.Redis.ZAdd(cache.Ctx, key, zMembers...).Err() 189 | if err != nil { 190 | return err 191 | } 192 | 193 | return nil 194 | } 195 | 196 | // 通过缓存查看视频得评论数量 197 | func getCommentCount(videoId int64) int64 { 198 | key := fmt.Sprintf("%s:%s:%s", "video", "comment_list", strconv.FormatInt(videoId, 10)) 199 | 200 | exists, err := cache.Redis.Exists(cache.Ctx, key).Result() 201 | if err != nil { 202 | log.Print(err) 203 | } 204 | 205 | if exists == 0 { 206 | err := buildCommentCache(videoId) 207 | if err != nil { 208 | log.Print(err) 209 | } 210 | } 211 | 212 | count, err := cache.Redis.ZCard(cache.Ctx, key).Result() 213 | if err != nil { 214 | log.Print(err) 215 | } 216 | 217 | return count 218 | } 219 | -------------------------------------------------------------------------------- /video_service/internal/handler/video_test.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "github.com/go-redis/redis/v8" 7 | "log" 8 | "testing" 9 | "time" 10 | "video/config" 11 | "video/internal/model" 12 | "video/pkg/cache" 13 | ) 14 | 15 | func TestVideoService_Feed(t *testing.T) { 16 | timePoint := time.Unix(1693130905305/1000, 0) 17 | fmt.Print(timePoint) 18 | } 19 | 20 | func TestCache(t *testing.T) { 21 | model.InitDb() 22 | cache.InitRedis() 23 | key := "video:comment_list:4395719587667968" 24 | tx := model.DB.Begin() 25 | comment, _ := model.GetCommentInstance().GetComment(tx, 5146154783088640) 26 | commentJson, _ := json.Marshal(comment) 27 | 28 | removedCount, err := cache.Redis.ZRem(cache.Ctx, key, string(commentJson)).Result() 29 | if err != nil { 30 | fmt.Println("Redis操作出错:", err) 31 | } else { 32 | fmt.Printf("删除了 %d 个匹配的成员\n", removedCount) 33 | } 34 | } 35 | 36 | func TestFavoriteCache(t *testing.T) { 37 | model.InitDb() 38 | cache.InitRedis() 39 | 40 | key := "video:favorite_video:4410007823982592" 41 | 42 | _, err := cache.Redis.Del(cache.Ctx, key).Result() 43 | if err != nil { 44 | fmt.Println("Error deleting existing Set:", err) 45 | return 46 | } 47 | 48 | _, err = cache.Redis.SAdd(cache.Ctx, key, "tempMember").Result() 49 | if err != nil { 50 | fmt.Println("Error adding member to Set:", err) 51 | return 52 | } 53 | 54 | _, err = cache.Redis.Expire(cache.Ctx, key, 12*time.Hour).Result() 55 | if err != nil { 56 | fmt.Println("Error setting expiration time:", err) 57 | return 58 | } 59 | 60 | // 检查 Set 是否存在 61 | exists, err := cache.Redis.Exists(cache.Ctx, key).Result() 62 | if err != nil { 63 | fmt.Println("Error checking Set existence:", err) 64 | return 65 | } 66 | 67 | if exists == 1 { 68 | fmt.Println("Set exists") 69 | } else { 70 | fmt.Println("Set does not exist") 71 | } 72 | 73 | // 删除临时成员并检查是否删除成功 74 | removedCount, err := cache.Redis.SRem(cache.Ctx, key, "tempMember").Result() 75 | if err != nil { 76 | fmt.Println("Error removing temporary member:", err) 77 | return 78 | } 79 | 80 | if removedCount == 1 { 81 | fmt.Println("Successfully removed temporary member") 82 | } else { 83 | fmt.Println("Temporary member not found in Set") 84 | return 85 | } 86 | 87 | // 检查 Set 是否存在 88 | exists, err = cache.Redis.Exists(cache.Ctx, key).Result() 89 | if err != nil { 90 | fmt.Println("Error checking Set existence:", err) 91 | return 92 | } 93 | 94 | if exists == 1 { 95 | fmt.Println("Set exists") 96 | } else { 97 | fmt.Println("Set does not exist") 98 | } 99 | } 100 | 101 | func TestSet(t *testing.T) { 102 | model.InitDb() 103 | cache.InitRedis() 104 | 105 | key := "video:favorite_video:4410007823982592" 106 | 107 | // 先添加临时成员 108 | _, err := cache.Redis.SAdd(cache.Ctx, key, "tempMember").Result() 109 | if err != nil { 110 | fmt.Println("Error adding member to Set:", err) 111 | return 112 | } 113 | 114 | // 检查 Set 是否存在 115 | exists, err := cache.Redis.Exists(cache.Ctx, key).Result() 116 | if err != nil { 117 | fmt.Println("Error checking Set existence:", err) 118 | return 119 | } 120 | 121 | if exists == 1 { 122 | fmt.Println("Set exists") 123 | } else { 124 | fmt.Println("Set does not exist") 125 | return 126 | } 127 | 128 | // 删除临时成员并检查是否删除成功 129 | removedCount, err := cache.Redis.SRem(cache.Ctx, key, "tempMember").Result() 130 | if err != nil { 131 | fmt.Println("Error removing temporary member:", err) 132 | return 133 | } 134 | 135 | if removedCount == 1 { 136 | fmt.Println("Successfully removed temporary member") 137 | } else { 138 | fmt.Println("Temporary member not found in Set") 139 | return 140 | } 141 | 142 | // 再次检查 Set 是否存在 143 | exists, err = cache.Redis.Exists(cache.Ctx, key).Result() 144 | if err != nil { 145 | fmt.Println("Error checking Set existence:", err) 146 | return 147 | } 148 | 149 | if exists == 1 { 150 | fmt.Println("Set exists") 151 | } else { 152 | fmt.Println("Set does not exist") 153 | } 154 | 155 | } 156 | 157 | func TestZset(t *testing.T) { 158 | model.InitDb() 159 | cache.InitRedis() 160 | key := "video:comment_list:4396317246627840" 161 | 162 | emptyMember := "empty_member" 163 | z := redis.Z{ 164 | Member: emptyMember, 165 | Score: 0, 166 | } 167 | count, err := cache.Redis.ZAdd(cache.Ctx, key, &z).Result() 168 | if err != nil { 169 | log.Print(err) 170 | } 171 | log.Print("创建数量", count) 172 | 173 | // 立即删除空成员 174 | count, err = cache.Redis.ZRem(cache.Ctx, key, emptyMember).Result() 175 | if err != nil { 176 | log.Print(err) 177 | } 178 | log.Print("删除数量", count) 179 | 180 | // 检查ZSET是否为空 181 | length, err := cache.Redis.ZCard(cache.Ctx, key).Result() 182 | if err != nil { 183 | log.Print(err) 184 | } 185 | 186 | if length == 0 { 187 | // ZSET为空 188 | println("ZSET为空") 189 | } else { 190 | // ZSET不为空 191 | println("ZSET不为空") 192 | } 193 | } 194 | 195 | func TestMQ(t *testing.T) { 196 | config.InitConfig() // 初始话配置文件 197 | model.InitDb() // 初始化数据库 198 | cache.InitRedis() // 初始化缓 199 | PublishVideo() 200 | } 201 | -------------------------------------------------------------------------------- /video_service/internal/model/comment.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "errors" 5 | "gorm.io/gorm" 6 | "sync" 7 | "time" 8 | "utils/snowFlake" 9 | ) 10 | 11 | type Comment struct { 12 | Id int64 `gorm:"primaryKey"` 13 | UserId int64 14 | VideoId int64 15 | Content string `gorm:"default:(-)"` // 评论内容 16 | CreatAt time.Time 17 | CommentStatus bool `gorm:"default:(-)"` 18 | } 19 | 20 | type CommentModel struct { 21 | } 22 | 23 | var commentModel *CommentModel 24 | var commentOnce sync.Once 25 | 26 | // GetCommentInstance 拿到单例实例 27 | func GetCommentInstance() *CommentModel { 28 | commentOnce.Do( 29 | func() { 30 | commentModel = &CommentModel{} 31 | }) 32 | return commentModel 33 | } 34 | 35 | // CreateComment 新增评论 36 | func (*CommentModel) CreateComment(tx *gorm.DB, comment *Comment) (id int64, err error) { 37 | flake, _ := snowFlake.NewSnowFlake(7, 2) 38 | comment.Id = flake.NextId() 39 | comment.CommentStatus = true 40 | comment.CreatAt = time.Now() 41 | 42 | result := tx.Create(&comment) 43 | if result.Error != nil { 44 | return -1, result.Error 45 | } 46 | 47 | return comment.Id, nil 48 | } 49 | 50 | // DeleteComment 删除评论 51 | func (*CommentModel) DeleteComment(commentId int64) error { 52 | var comment Comment 53 | result := DB.First(&comment, commentId) 54 | if result.Error != nil { 55 | return result.Error 56 | } 57 | if comment.CommentStatus == false { 58 | return errors.New("评论不存在!!") 59 | } 60 | 61 | comment.CommentStatus = false 62 | result = DB.Save(&comment) 63 | if result.Error != nil { 64 | return result.Error 65 | } 66 | return nil 67 | } 68 | 69 | // CommentList 根据视频id找到所有的评论 70 | func (*CommentModel) CommentList(videoId int64) ([]Comment, error) { 71 | var comments []Comment 72 | 73 | result := DB.Table("comment"). 74 | Where("video_id = ? AND comment_status = ?", videoId, true). 75 | Find(&comments) 76 | if result.Error != nil { 77 | return nil, result.Error 78 | } 79 | 80 | return comments, nil 81 | } 82 | 83 | // GetComment 根据评论ID找到具体评论 84 | func (*CommentModel) GetComment(tx *gorm.DB, commentId int64) (Comment, error) { 85 | comment := Comment{} 86 | 87 | result := tx.Table("comment"). 88 | Where("id = ?", commentId). 89 | First(&comment) 90 | if result.Error != nil { 91 | return comment, result.Error 92 | } 93 | 94 | return comment, nil 95 | } 96 | -------------------------------------------------------------------------------- /video_service/internal/model/favorite.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "errors" 5 | "gorm.io/gorm" 6 | "sync" 7 | "utils/snowFlake" 8 | ) 9 | 10 | type Favorite struct { 11 | Id int64 `gorm:"primaryKey"` 12 | UserId int64 13 | VideoId int64 14 | IsFavorite bool `gorm:"default:true"` 15 | } 16 | 17 | type FavoriteModel struct { 18 | } 19 | 20 | var favoriteModel *FavoriteModel 21 | var favoriteOnce sync.Once 22 | 23 | func GetFavoriteInstance() *FavoriteModel { 24 | favoriteOnce.Do( 25 | func() { 26 | favoriteModel = &FavoriteModel{} 27 | }) 28 | return favoriteModel 29 | } 30 | 31 | // AddFavorite 创建点赞 32 | func (*FavoriteModel) AddFavorite(tx *gorm.DB, favorite *Favorite) error { 33 | result := tx.Where("user_id=? AND video_id=?", favorite.UserId, favorite.VideoId).First(&favorite) 34 | // 发生除没找到记录的其它错误 35 | if result.Error != nil && !errors.Is(result.Error, gorm.ErrRecordNotFound) { 36 | return result.Error 37 | } 38 | 39 | // 如果找到了记录,更新is_favorite置为0 40 | if result.RowsAffected > 0 { 41 | favorite.IsFavorite = true 42 | result = tx.Save(&favorite) 43 | if result.Error != nil { 44 | return result.Error 45 | } 46 | } else { 47 | flake, _ := snowFlake.NewSnowFlake(7, 2) 48 | favorite.Id = flake.NextId() 49 | result = tx.Create(&favorite) 50 | if result.Error != nil { 51 | return result.Error 52 | } 53 | } 54 | 55 | return nil 56 | } 57 | 58 | // IsFavorite 根据用户id和视频id获取点赞状态 59 | func (*FavoriteModel) IsFavorite(userId int64, videoId int64) (bool, error) { 60 | var isFavorite bool 61 | 62 | result := DB.Table("favorite"). 63 | Where("user_id = ? AND video_id = ?", userId, videoId). 64 | Pluck("is_favorite", &isFavorite) 65 | if result.Error != nil { 66 | return true, result.Error 67 | } 68 | 69 | return isFavorite, nil 70 | } 71 | 72 | // DeleteFavorite 删除点赞 73 | func (*FavoriteModel) DeleteFavorite(tx *gorm.DB, favorite *Favorite) error { 74 | result := tx.Where("user_id=? AND video_id=?", favorite.UserId, favorite.VideoId).First(&favorite) 75 | // 发生除没找到记录的其它错误 76 | if result.Error != nil && !errors.Is(result.Error, gorm.ErrRecordNotFound) { 77 | return result.Error 78 | } 79 | // 如果找到了记录,更新is_favorite置为0 80 | if result.RowsAffected > 0 { 81 | favorite.IsFavorite = false 82 | result = tx.Save(&favorite) 83 | if result.Error != nil { 84 | return result.Error 85 | } 86 | } 87 | 88 | return nil 89 | } 90 | 91 | // FavoriteVideoList 根据用户Id获取所有喜欢的视频id 92 | func (*FavoriteModel) FavoriteVideoList(userId int64) ([]int64, error) { 93 | var videoIds []int64 94 | 95 | result := DB.Table("favorite"). 96 | Where("user_id = ? AND is_favorite = ?", userId, true). 97 | Pluck("video_id", &videoIds) 98 | if result.Error != nil { 99 | return nil, result.Error 100 | } 101 | 102 | return videoIds, nil 103 | } 104 | 105 | // GetFavoriteCount 获取喜欢数量 106 | func (*FavoriteModel) GetFavoriteCount(userId int64) (int64, error) { 107 | var count int64 108 | 109 | DB.Table("favorite"). 110 | Where("user_id=? AND is_favorite=?", userId, true). 111 | Count(&count) 112 | 113 | return count, nil 114 | } 115 | 116 | // GetVideoFavoriteCount 获取视频货站数量 117 | func (*FavoriteModel) GetVideoFavoriteCount(videoId int64) (int64, error) { 118 | var count int64 119 | 120 | DB.Table("favorite"). 121 | Where("video_id=? AND is_favorite=?", videoId, true). 122 | Count(&count) 123 | 124 | return count, nil 125 | } 126 | 127 | // FavoriteUserList 根据视频找到所有点赞用户的id 128 | func (*FavoriteModel) FavoriteUserList(videoId int64) ([]int64, error) { 129 | var userIds []int64 130 | 131 | result := DB.Table("favorite"). 132 | Where("video_id = ? AND is_favorite = ?", videoId, true). 133 | Pluck("user_id", &userIds) 134 | if result.Error != nil { 135 | return nil, result.Error 136 | } 137 | 138 | return userIds, nil 139 | } 140 | -------------------------------------------------------------------------------- /video_service/internal/model/init.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "gorm.io/driver/mysql" 6 | "gorm.io/gorm" 7 | "gorm.io/gorm/logger" 8 | "gorm.io/gorm/schema" 9 | "time" 10 | "video/config" 11 | ) 12 | 13 | // DB 创建数据库单例 14 | var DB *gorm.DB 15 | 16 | // InitDb 初始化数据库 17 | func InitDb() { 18 | config.InitConfig() 19 | dns := config.DbDnsInit() 20 | Database(dns) 21 | } 22 | 23 | // Database 初始花数据库链接 24 | func Database(connString string) { 25 | var ormLogger logger.Interface 26 | 27 | // gin中有debug 、 release 、 test 三种模式 28 | // 不指定默认以debug形式启动 29 | // 开发用debug模式、 上线用release模式 30 | // 指定方式 : gin.SetMode(gin.ReleaseMode) 31 | 32 | // 设置gorm的日志模式,可以打印原生SQL语句 33 | if gin.Mode() == "debug" { 34 | ormLogger = logger.Default.LogMode(logger.Info) 35 | } else { 36 | ormLogger = logger.Default 37 | } 38 | db, err := gorm.Open(mysql.New(mysql.Config{ 39 | DSN: connString, // DSN data source name 40 | DefaultStringSize: 256, // string 类型字段的默认长度 41 | DisableDatetimePrecision: true, // 禁用 datetime 精度,MySQL 5.6 之前的数据库不支持 42 | DontSupportRenameIndex: true, // 重命名索引时采用删除并新建的方式,MySQL 5.7 之前的数据库和 MariaDB 不支持重命名索引 43 | DontSupportRenameColumn: true, // 用 `change` 重命名列,MySQL 8 之前的数据库和 MariaDB 不支持重命名列 44 | SkipInitializeWithVersion: false, // 根据当前 MySQL 版本自动配置 45 | }), &gorm.Config{ 46 | Logger: ormLogger, 47 | NamingStrategy: schema.NamingStrategy{ 48 | SingularTable: true, // 默认不加负数 49 | }, 50 | }) 51 | 52 | if err != nil { 53 | panic(err) 54 | } 55 | 56 | sqlDB, _ := db.DB() 57 | 58 | // 设置连接池 59 | sqlDB.SetMaxIdleConns(20) // 空闲时候的最大连接数 60 | sqlDB.SetMaxOpenConns(100) // 打开时候的最大连接数 61 | sqlDB.SetConnMaxLifetime(time.Second * 20) // 超时时间 62 | DB = db 63 | 64 | migration() 65 | } 66 | -------------------------------------------------------------------------------- /video_service/internal/model/init_test.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "fmt" 5 | "github.com/spf13/viper" 6 | "testing" 7 | "video/config" 8 | ) 9 | 10 | func TestInitDb(t *testing.T) { 11 | config.InitConfig() 12 | dns := config.DbDnsInit() 13 | fmt.Print(dns) 14 | config.InitConfig() 15 | fmt.Printf("host is: %v \n", viper.GetString("mysql.host")) 16 | } 17 | -------------------------------------------------------------------------------- /video_service/internal/model/migration.go: -------------------------------------------------------------------------------- 1 | // Package model 数据库自动迁移 2 | package model 3 | 4 | import "log" 5 | 6 | func migration() { 7 | // 自动迁移 8 | err := DB.Set("gorm:table_options", "charset=utf8mb4").AutoMigrate(&Video{}, &Comment{}, &Favorite{}) 9 | if err != nil { 10 | log.Print("err") 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /video_service/internal/model/video.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "gorm.io/gorm" 5 | "sync" 6 | "time" 7 | "utils/snowFlake" 8 | ) 9 | 10 | type Video struct { 11 | Id int64 `gorm:"primary_key"` 12 | AuthId int64 13 | Title string 14 | CoverUrl string `gorm:"default:(-)"` 15 | PlayUrl string `gorm:"default:(-)"` 16 | //FavoriteCount int64 `gorm:"default:0"` 17 | //CommentCount int64 `gorm:"default:0"` 18 | CreatAt time.Time 19 | } 20 | 21 | type VideoModel struct { 22 | } 23 | 24 | var videoMedel *VideoModel 25 | var videoOnce sync.Once // 单例模式 26 | 27 | // GetVideoInstance 获取单例的实例 28 | func GetVideoInstance() *VideoModel { 29 | videoOnce.Do( 30 | func() { 31 | videoMedel = &VideoModel{} 32 | }) 33 | return videoMedel 34 | } 35 | 36 | // Create 创建视频信息 37 | func (*VideoModel) Create(video *Video) error { 38 | // 服务2 39 | flake, _ := snowFlake.NewSnowFlake(7, 2) 40 | video.Id = flake.NextId() 41 | 42 | DB.Create(&video) 43 | 44 | return nil 45 | } 46 | 47 | // DeleteVideoByUrl 删除视频 48 | func (v *VideoModel) DeleteVideoByUrl(videoUrl string) error { 49 | var video Video 50 | if err := DB.Where("play_url = ?", videoUrl).First(&video).Error; err != nil { 51 | return err 52 | } 53 | 54 | // 删除找到的记录 55 | if err := DB.Delete(&video).Error; err != nil { 56 | return err 57 | } 58 | 59 | return nil 60 | } 61 | 62 | // GetVideoByTime 根据创建时间获取视频 63 | func (*VideoModel) GetVideoByTime(timePoint time.Time) ([]Video, error) { 64 | var videos []Video 65 | 66 | result := DB.Table("video"). 67 | Where("creat_at < ?", timePoint). 68 | Order("creat_at DESC"). 69 | Limit(30). 70 | Find(&videos) 71 | if result.Error != nil { 72 | return nil, result.Error 73 | } 74 | 75 | // 查询不到数据,就返回当前时间最新的30条数据 76 | if len(videos) == 0 { 77 | timePoint = time.Now() 78 | result := DB.Table("video"). 79 | Where("creat_at < ?", timePoint). 80 | Order("creat_at DESC"). 81 | Limit(30). 82 | Find(&videos) 83 | if result.Error != nil { 84 | return nil, result.Error 85 | } 86 | return videos, nil 87 | } 88 | 89 | return videos, nil 90 | } 91 | 92 | // GetVideoList 根据视频Id获取视频列表 93 | func (*VideoModel) GetVideoList(videoIds []int64) ([]Video, error) { 94 | var videos []Video 95 | 96 | result := DB.Table("video"). 97 | Where("id IN ?", videoIds). 98 | Find(&videos) 99 | if result.Error != nil { 100 | return nil, result.Error 101 | } 102 | 103 | return videos, nil 104 | } 105 | 106 | // GetVideoListByUser 根据用户的id找到视频列表 107 | func (*VideoModel) GetVideoListByUser(userId int64) ([]Video, error) { 108 | var videos []Video 109 | 110 | result := DB.Table("video"). 111 | Where("auth_id = ?", userId). 112 | Find(&videos) 113 | if result.Error != nil { 114 | return nil, result.Error 115 | } 116 | 117 | return videos, nil 118 | } 119 | 120 | // AddFavoriteCount 喜欢记录 + 1 121 | func (*VideoModel) AddFavoriteCount(tx *gorm.DB, videoId int64) error { 122 | result := tx.Model(&Video{}).Where("id = ?", videoId). 123 | Update("favorite_count", gorm.Expr("favorite_count + ?", 1)) 124 | if result.Error != nil { 125 | return result.Error 126 | } 127 | 128 | return nil 129 | } 130 | 131 | // DeleteFavoriteCount 喜欢记录 - 1 132 | func (*VideoModel) DeleteFavoriteCount(tx *gorm.DB, videoId int64) error { 133 | result := tx.Model(&Video{}).Where("id = ?", videoId). 134 | Update("favorite_count", gorm.Expr("favorite_count - ?", 1)) 135 | if result.Error != nil { 136 | return result.Error 137 | } 138 | 139 | return nil 140 | } 141 | 142 | // AddCommentCount 视频评论数量 + 1 143 | func (*VideoModel) AddCommentCount(tx *gorm.DB, videoId int64) error { 144 | result := tx.Model(&Video{}). 145 | Where("id = ?", videoId). 146 | Update("comment_count", gorm.Expr("comment_count + ?", 1)) 147 | if result.Error != nil { 148 | return result.Error 149 | } 150 | 151 | return nil 152 | } 153 | 154 | // DeleteCommentCount 视频评论数量 - 1 155 | func (*VideoModel) DeleteCommentCount(tx *gorm.DB, videoId int64) error { 156 | result := tx.Model(&Video{}). 157 | Where("id = ?", videoId). 158 | Update("comment_count", gorm.Expr("comment_count - ?", 1)) 159 | if result.Error != nil { 160 | return result.Error 161 | } 162 | 163 | return nil 164 | } 165 | 166 | // GetFavoritedCount 获取用户的获赞数量 167 | func (*VideoModel) GetFavoritedCount(userId int64) (int64, error) { 168 | var count int64 169 | 170 | DB.Table("video"). 171 | Where("auth_id=?", userId). 172 | Select("SUM(favorite_count) as count"). 173 | Pluck("count", &count) 174 | 175 | return count, nil 176 | } 177 | 178 | // GetWorkCount 获取用户的作品数量 179 | func (*VideoModel) GetWorkCount(userId int64) (int64, error) { 180 | var count int64 181 | DB.Table("video"). 182 | Where("auth_id=?", userId). 183 | Count(&count) 184 | 185 | return count, nil 186 | } 187 | -------------------------------------------------------------------------------- /video_service/internal/model/video_test.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "strconv" 7 | "testing" 8 | "time" 9 | ) 10 | 11 | // 测试视频喜欢记录 + 1 12 | func TestVideoModel_AddFavoriteCount(t *testing.T) { 13 | InitDb() 14 | tx := DB.Begin() 15 | GetVideoInstance().AddFavoriteCount(tx, 1949953677991936) 16 | } 17 | 18 | // 测试视频喜欢记录 - 1 19 | func TestFavoriteModel_DeleteFavorite2(t *testing.T) { 20 | InitDb() 21 | tx := DB.Begin() 22 | GetVideoInstance().DeleteFavoriteCount(tx, 1949953677991936) 23 | } 24 | 25 | // 测试新增喜欢记录 26 | func TestFavoriteModel_AddFavorite(t *testing.T) { 27 | InitDb() 28 | tx := DB.Begin() 29 | favorite := Favorite{ 30 | UserId: 123, 31 | VideoId: 456, 32 | } 33 | GetFavoriteInstance().AddFavorite(tx, &favorite) 34 | } 35 | 36 | // 测试软删除喜欢记录 37 | func TestFavoriteModel_DeleteFavorite(t *testing.T) { 38 | InitDb() 39 | tx := DB.Begin() 40 | favorite := Favorite{ 41 | UserId: 123, 42 | VideoId: 456, 43 | } 44 | GetFavoriteInstance().DeleteFavorite(tx, &favorite) 45 | } 46 | 47 | // 测试创建评论 48 | func TestCommentModel_CreateComment(t *testing.T) { 49 | InitDb() 50 | tx := DB.Begin() 51 | comment := Comment{ 52 | UserId: 111, 53 | VideoId: 222, 54 | Content: "喜欢", 55 | } 56 | GetCommentInstance().CreateComment(tx, &comment) 57 | tx.Commit() 58 | } 59 | 60 | // 测试删除评论 61 | func TestCommentModel_DeleteComment(t *testing.T) { 62 | InitDb() 63 | GetCommentInstance().DeleteComment(8361782507610112) 64 | 65 | } 66 | 67 | // 测试删除评论 68 | func TestCommentModel_CommentList(t *testing.T) { 69 | InitDb() 70 | commentList, _ := GetCommentInstance().CommentList(4395719587667968) 71 | fmt.Print(commentList) 72 | } 73 | 74 | func TestTime(t *testing.T) { 75 | currentTime := time.Now() 76 | fmt.Println(currentTime) 77 | timeString := currentTime.Format("2006-01-02 15:04:05") 78 | fmt.Println("Formatted time:", timeString) 79 | } 80 | 81 | // 测试用户获赞数量 82 | func TestVideoModel_GetFavoritedCount(t *testing.T) { 83 | InitDb() 84 | count, _ := GetVideoInstance().GetFavoritedCount(812575311663104) 85 | fmt.Println(count) 86 | } 87 | 88 | // 统计作品数量 89 | func TestVideoModel_GetWorkCount(t *testing.T) { 90 | InitDb() 91 | count, _ := GetVideoInstance().GetWorkCount(812575311663104) 92 | fmt.Println(count) 93 | } 94 | 95 | // 统计喜欢数量 96 | func TestFavorite_GetFavoriteCount(t *testing.T) { 97 | InitDb() 98 | count, _ := GetFavoriteInstance().GetFavoriteCount(812575311663104) 99 | fmt.Println(count) 100 | } 101 | 102 | // 找到喜欢的视频id 103 | func TestFavoriteModel_FavoriteVideoList(t *testing.T) { 104 | InitDb() 105 | list, _ := GetFavoriteInstance().FavoriteVideoList(812575311663104) 106 | 107 | videoList, _ := GetVideoInstance().GetVideoList(list) 108 | fmt.Print(list) 109 | fmt.Print(videoList) 110 | } 111 | 112 | func TestFavoriteModel_IsFavorite(t *testing.T) { 113 | InitDb() 114 | favorite, _ := GetFavoriteInstance().IsFavorite(812575311663104, 2276964627783680) 115 | fmt.Print(favorite) 116 | } 117 | 118 | // 根据时间查找视频列表 119 | func TestVideoModel_GetVideoByTime(t *testing.T) { 120 | InitDb() 121 | videos, _ := GetVideoInstance().GetVideoByTime(time.Now()) 122 | fmt.Print(videos) 123 | } 124 | 125 | func TestFmt(t *testing.T) { 126 | userId := int64(111) 127 | key := fmt.Sprintf("%s:%s:%s", "user", "info", strconv.FormatInt(userId, 10)) 128 | print(key) 129 | } 130 | 131 | func TestFavoriteModel_FavoriteUserList(t *testing.T) { 132 | InitDb() 133 | list, _ := GetFavoriteInstance().FavoriteUserList(4396360053694464) 134 | fmt.Println(list) 135 | } 136 | 137 | func TestCommentModel_GetComment(t *testing.T) { 138 | InitDb() 139 | tx := DB.Begin() 140 | comment, _ := commentModel.GetComment(tx, 4419719369990144) 141 | log.Print(comment) 142 | } 143 | -------------------------------------------------------------------------------- /video_service/internal/service/pb/videoService.proto: -------------------------------------------------------------------------------- 1 | syntax="proto3"; 2 | package pb; 3 | option go_package = "../;service"; // 此处格式为<生成的文件存放位置;生成文件的包名> 4 | 5 | 6 | message Video { 7 | // @gotags:json:"id" form:"id" uri:"id" 8 | int64 Id = 1; 9 | // @gotags:json:"auth_id" form:"auth_id" uri:"auth_id" 10 | int64 AuthId = 2; 11 | // @gotags:json:"play_url" form:"play_rul" uri:"play_url" 12 | string PlayUrl = 3; // 视频播放地址 13 | // @gotasgs:json:"cover_url" form:"cover_url" uri:"cover_url" 14 | string CoverUrl = 4; 15 | // @gotags:json:"favorite_count" form:"favorite_count" uri:"favorite_count" 16 | int64 FavoriteCount = 5; 17 | // @gotags:json:"comment_count" form:"comment_count" uri:"comment_count" 18 | int64 CommentCount = 6; 19 | // @gotags:json:"is_favorite" form:"is_favorite" uri:"is_favorite" 20 | bool IsFavorite = 7; 21 | // @gotags:json:"title" form:"title" uri:"title" 22 | string Title = 8; 23 | } 24 | 25 | message Comment { 26 | // @gotags:json:"id" form:"id" uri:"id" 27 | int64 Id = 1; 28 | // @gatags:json:"user_id" form:"user_id" uri:"user_id" 29 | int64 UserId = 2; 30 | // @gotags:json:"content" form:"content" uri:"content" 31 | string Content = 3; // 评论内容 32 | // @gotags:json:"create_date" form:"create_date" uri:"create_date" 33 | string CreateDate = 4; 34 | } 35 | 36 | // 视频流 37 | message FeedRequest { 38 | // @gotags:json:"latest_time" form:"latest_time" uri:"latest_time" 39 | int64 LatestTime = 1; // 可选参数,限制返回视频的最新投稿时间戳,精确到秒,不填表示当前时间 40 | // @gotags:json:"user_id" form:"user_id" uri:"user_id" 41 | int64 UserId = 2; 42 | } 43 | 44 | message FeedResponse { 45 | // @gotags:json:"status_code" form:"status_code" uri:"status_code" 46 | int64 StatusCode = 1; 47 | // @gotags:json:"status_msg" form:"status_msg" uri:"status_msg" 48 | string StatusMsg = 2; 49 | // @gotags:json:"video_list" form:"video_list" uri:"video_list" 50 | repeated Video VideoList = 3; 51 | // @gotags:json:"next_time" form:"next_time" uri:"next_time" 52 | int64 NextTime = 4; 53 | } 54 | 55 | // 发布视频 56 | message PublishActionRequest { 57 | // @gotags:json:"user_id" form:"user_id" uri:"user_id" 58 | int64 UserId = 1; 59 | // @gotags:json:"data" form:"data" uri:"data" 60 | bytes Data = 2; 61 | // @gotags:json:"title" form:"title" uri:"title" 62 | string Title = 3; 63 | } 64 | 65 | message PublishActionResponse { 66 | // @gotags:json:"status_code" form:"status_code" uri:"status_code" 67 | int64 StatusCode = 1; 68 | // @gotags:json:"status_msg" form:"status_msg" uri:"status_msg" 69 | string StatusMsg = 2; 70 | } 71 | 72 | // 发布列表 73 | message PublishListRequest { 74 | // @gotags:json:"user_id" form:"user_id" uri:"user_id" 75 | int64 UserId = 1; 76 | } 77 | 78 | message PublishListResponse { 79 | // @gotags:json:"status_code" form:"status_code" uri:"status_code" 80 | int64 StatusCode = 1; 81 | // @gotags:json:"status_msg" form:"status_msg" uri:"status_msg" 82 | string StatusMsg = 2; 83 | // @gotags:json:"video_list" form:"video_list" uri:"video_list" 84 | repeated Video VideoList = 3; 85 | } 86 | 87 | // 赞操作 88 | message FavoriteActionRequest { 89 | // @gotags:json:"user_id" form:"user_id" uri:"user_id" 90 | int64 UserId = 1; 91 | // @gotags:json:"video_id" form:"video_id" uri:"video_id" 92 | int64 VideoId = 2; 93 | // @gotags:json:"action_type" form:"action_type" uri:"action_type" 94 | int64 ActionType = 3; // 1-点赞,2-取消点赞 95 | } 96 | 97 | message FavoriteActionResponse { 98 | // @gotags:json:"status_code" form:"status_code" uri:"status_code" 99 | int64 StatusCode = 1; 100 | // @gotags:json:"status_msg" form:"status_msg" uri:"status_msg" 101 | string StatusMsg = 2; 102 | } 103 | 104 | 105 | // 喜欢列表 106 | message FavoriteListRequest { 107 | // @gotags:json:"user_id" form:"user_id" uri:"user_id" 108 | int64 UserId = 1; 109 | } 110 | 111 | message FavoriteListResponse { 112 | // @gotags:json:"status_code" form:"status_code" uri:"status_code" 113 | int64 StatusCode = 1; 114 | // @gotags:json:"status_msg" form:"status_msg" uri:"status_msg" 115 | string StatusMsg = 2; 116 | // @gotags:json:"video_list" form:"video_list" uri:"video_list" 117 | repeated Video VideoList = 3; 118 | } 119 | 120 | 121 | // 评论操作 122 | message CommentActionRequest { 123 | // @gotags:json:"user_id" form:"user_id" uri:"user_id" 124 | int64 UserId = 1; 125 | // @gotags:json:"video_id" form:"video_id" uri:"video_id" 126 | int64 VideoId = 2; 127 | // @gotags:json:"action_type" form:"action_type" uri:"action_type" 128 | int64 ActionType = 3; //1-发布评论,2-删除评论 129 | // @gotags:json:"comment_text" form:"comment_text" uri:"comment_text" 130 | string CommentText = 4; // 可选,用户填写的评论内容,在action_type=1的时候使用 131 | // @gotags:json:"comment_id" form:"comment_id" uri:"comment_id" 132 | int64 CommentId = 5; // 可选,要删除的评论id,在action_type=2的时候使用 133 | } 134 | 135 | message CommentActionResponse { 136 | // @gotags:json:"status_code" form:"status_code" uri:"status_code" 137 | int64 StatusCode = 1; 138 | // @gotags:json:"status_msg" form:"status_msg" uri:"status_msg" 139 | string StatusMsg = 2; 140 | // @gotags:json:"comment" form:"comment" uri:"comment" 141 | Comment Comment = 3; 142 | } 143 | 144 | // 评论列表 145 | message CommentListRequest { 146 | // @gotags:json:"video_id" form:"video_id" uri:"video_id" 147 | int64 VideoId = 1; 148 | } 149 | 150 | message CommentListResponse { 151 | // @gotags:json:"status_code" form:"status_code" uri:"status_code" 152 | int64 StatusCode = 1; 153 | // @gotags:json:"status_msg" form:"status_msg" uri:"status_msg" 154 | string StatusMsg = 2; 155 | // @gotags:json:"comment_list" form:"comment_list" uri:"comment_list" 156 | repeated Comment CommentList = 3; 157 | } 158 | 159 | // 视频计数 160 | message Count { 161 | // @gotags:json:"total_favorited" form:"total_favorited" uri:"total_favorited" 162 | int64 TotalFavorited = 1; // 获赞数量 163 | // @gotags:json:"work_count" form:"work_count" uri:"work_count" 164 | int64 WorkCount = 2; // 作品数量 165 | // @gotags:json:"favorite_count" form:"favorite_count" uri:"favorite_count" 166 | int64 FavoriteCount = 3; // 喜欢数量 167 | } 168 | 169 | // 视频总计数请求 170 | message CountRequest { 171 | // @gotags:json:"user_id" form:"user_id" uri:"user_id" 172 | repeated int64 UserIds = 1; // 传入一个userId切片 173 | } 174 | 175 | message CountResponse { 176 | // @gotags:json:"status_code" form:"status_code" uri:"status_code" 177 | int64 StatusCode = 1; 178 | // @gotags:json:"status_msg" form:"status_msg" uri:"status_msg" 179 | string StatusMsg = 2; 180 | // @gotags:json:"counts" form:"counts" uri:"counts" 181 | repeated Count counts = 3; 182 | } 183 | 184 | service VideoService { 185 | rpc Feed(FeedRequest) returns(FeedResponse); 186 | rpc PublishAction(PublishActionRequest) returns (PublishActionResponse); 187 | rpc PublishList(PublishListRequest) returns(PublishListResponse); 188 | 189 | rpc FavoriteAction(FavoriteActionRequest) returns(FavoriteActionResponse); 190 | rpc FavoriteList(FavoriteListRequest) returns(FavoriteListResponse); 191 | 192 | rpc CommentAction(CommentActionRequest) returns(CommentActionResponse); 193 | rpc CommentList(CommentListRequest) returns(CommentListResponse); 194 | 195 | // 根据user_id切片,返回计数信息 196 | rpc CountInfo(CountRequest) returns(CountResponse); 197 | } 198 | 199 | -------------------------------------------------------------------------------- /video_service/pkg/cache/redis.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "context" 5 | "github.com/go-redis/redis/v8" 6 | "github.com/spf13/viper" 7 | ) 8 | 9 | var Redis *redis.Client 10 | 11 | var Ctx = context.Background() 12 | 13 | // InitRedis 连接redis 14 | func InitRedis() { 15 | addr := viper.GetString("redis.address") 16 | Redis = redis.NewClient(&redis.Options{ 17 | Addr: addr, 18 | Password: "", 19 | DB: 0, // 存入DB0 20 | }) 21 | } 22 | -------------------------------------------------------------------------------- /video_service/pkg/cache/redis_test.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | ) 7 | 8 | func TestInitRedis(t *testing.T) { 9 | InitRedis() 10 | Redis.Set(context.Background(), "K2", "myValue", 0) 11 | } 12 | -------------------------------------------------------------------------------- /video_service/pkg/cut/ffmpeg.go: -------------------------------------------------------------------------------- 1 | package cut 2 | 3 | import ( 4 | "bytes" 5 | "os" 6 | "os/exec" 7 | ) 8 | 9 | // Cover 截取视频为图片 10 | func Cover(videoURL string, timeOffset string) ([]byte, error) { 11 | cmd := exec.Command( 12 | "ffmpeg", "-i", videoURL, "-ss", timeOffset, "-vframes", "1", "-q:v", "2", "-f", "image2", "pipe:1", 13 | ) 14 | cmd.Stderr = os.Stderr 15 | 16 | var outputBuffer bytes.Buffer 17 | cmd.Stdout = &outputBuffer 18 | 19 | err := cmd.Run() 20 | if err != nil { 21 | return nil, err 22 | } 23 | 24 | return outputBuffer.Bytes(), nil 25 | } 26 | -------------------------------------------------------------------------------- /video_service/pkg/cut/ffmpeg_test.go: -------------------------------------------------------------------------------- 1 | package cut 2 | 3 | import ( 4 | "testing" 5 | "video/config" 6 | "video/third_party" 7 | ) 8 | 9 | func TestCover(t *testing.T) { 10 | config.InitConfig() 11 | videoURL := "http://tiny-tiktok.oss-cn-chengdu.aliyuncs.com/video1.mp4" 12 | 13 | imageBytes, _ := Cover(videoURL, "00:00:05") 14 | 15 | third_party.Upload("output.jpg", imageBytes) 16 | } 17 | -------------------------------------------------------------------------------- /video_service/pkg/mq/rabbitmq.go: -------------------------------------------------------------------------------- 1 | package mq 2 | 3 | import ( 4 | amqp "github.com/rabbitmq/amqp091-go" 5 | "log" 6 | "video/config" 7 | ) 8 | 9 | func failOnError(err error, msg string) { 10 | if err != nil { 11 | log.Printf("%s: %s", msg, err) 12 | } 13 | } 14 | 15 | func InitMQ() *amqp.Connection { 16 | // 连接到RabbitMQ服务器 17 | url := config.InitRabbitMQUrl() 18 | conn, err := amqp.Dial(url) 19 | failOnError(err, "Failed to connect to RabbitMQ") 20 | 21 | return conn 22 | } 23 | 24 | // ConsumeMessage 消费消息 25 | func ConsumeMessage() { 26 | 27 | } 28 | -------------------------------------------------------------------------------- /video_service/pkg/mq/rabbitmq_test.go: -------------------------------------------------------------------------------- 1 | package mq 2 | 3 | import ( 4 | "context" 5 | amqp "github.com/rabbitmq/amqp091-go" 6 | "log" 7 | "strconv" 8 | "testing" 9 | "time" 10 | "video/config" 11 | ) 12 | 13 | func TestMQServer(t *testing.T) { 14 | conn, err := amqp.Dial("amqp://guest:guest@localhost:5672/") 15 | failOnError(err, "Failed to connect to RabbitMQ") 16 | defer conn.Close() 17 | 18 | ch, err := conn.Channel() 19 | failOnError(err, "Failed to open a channel") 20 | defer ch.Close() 21 | 22 | q, err := ch.QueueDeclare( 23 | "rpc_queue", // name 24 | false, // durable 25 | false, // delete when unused 26 | false, // exclusive 27 | false, // no-wait 28 | nil, // arguments 29 | ) 30 | failOnError(err, "Failed to declare a queue") 31 | 32 | err = ch.Qos( 33 | 1, // prefetch count 34 | 0, // prefetch size 35 | false, // global 36 | ) 37 | failOnError(err, "Failed to set QoS") 38 | 39 | msgs, err := ch.Consume( 40 | q.Name, // queue 41 | "", // consumer 42 | false, // auto-ack 43 | false, // exclusive 44 | false, // no-local 45 | false, // no-wait 46 | nil, // args 47 | ) 48 | failOnError(err, "Failed to register a consumer") 49 | 50 | var forever chan struct{} 51 | 52 | go func() { 53 | ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) 54 | defer cancel() 55 | for d := range msgs { 56 | n, err := strconv.Atoi(string(d.Body)) 57 | failOnError(err, "Failed to convert body to integer") 58 | 59 | log.Printf(" [.] fib(%d)", n) 60 | response := fib(n) 61 | 62 | err = ch.PublishWithContext(ctx, 63 | "", // exchange 64 | d.ReplyTo, // routing key 65 | false, // mandatory 66 | false, // immediate 67 | amqp.Publishing{ 68 | ContentType: "text/plain", 69 | CorrelationId: d.CorrelationId, 70 | Body: []byte(strconv.Itoa(response)), 71 | }) 72 | failOnError(err, "Failed to publish a message") 73 | 74 | d.Ack(false) 75 | } 76 | }() 77 | 78 | log.Printf(" [*] Awaiting RPC requests") 79 | <-forever 80 | } 81 | 82 | func TestUrl(t *testing.T) { 83 | config.InitConfig() 84 | url := config.InitRabbitMQUrl() 85 | log.Printf(url) 86 | } 87 | -------------------------------------------------------------------------------- /video_service/third_party/oss.go: -------------------------------------------------------------------------------- 1 | package third_party 2 | 3 | import ( 4 | "bytes" 5 | "github.com/aliyun/aliyun-oss-go-sdk/oss" 6 | "github.com/spf13/viper" 7 | ) 8 | 9 | // 创建存储空间 10 | func getBucket() (*oss.Bucket, error) { 11 | bucketName := viper.GetString("oss.bucketName") 12 | endPoint := viper.GetString("oss.endpoint") 13 | accessKeyId := viper.GetString("oss.accessKeyId") 14 | accessKeySecret := viper.GetString("oss.accessKeySecret") 15 | 16 | // 创建OSSClient实例 17 | client, err := oss.New(endPoint, accessKeyId, accessKeySecret) 18 | if err != nil { 19 | return nil, err 20 | } 21 | // 获取存储空间 22 | bucket, err := client.Bucket(bucketName) 23 | if err != nil { 24 | return nil, err 25 | } 26 | return bucket, nil 27 | } 28 | 29 | // Upload 上传 30 | func Upload(fileDir string, fileBytes []byte) error { 31 | bucket, err := getBucket() 32 | if err != nil { 33 | return err 34 | } 35 | 36 | // 上传文件 37 | file := bytes.NewReader(fileBytes) 38 | err = bucket.PutObject(fileDir, file) 39 | return err 40 | } 41 | 42 | // Delete 删除 43 | func Delete(fileDir string) error { 44 | bucket, err := getBucket() 45 | if err != nil { 46 | return err 47 | } 48 | 49 | // 上传文件 50 | err = bucket.DeleteObject(fileDir) 51 | return err 52 | } 53 | -------------------------------------------------------------------------------- /video_service/third_party/oss_test.go: -------------------------------------------------------------------------------- 1 | package third_party 2 | 3 | import ( 4 | "io/ioutil" 5 | "testing" 6 | "video/config" 7 | ) 8 | 9 | func TestUpload(t *testing.T) { 10 | 11 | config.InitConfig() 12 | filePath := "D:\\Project\\video\\output_image.jpg" 13 | fileByte, _ := ioutil.ReadFile(filePath) 14 | Upload("hello", fileByte) 15 | } 16 | --------------------------------------------------------------------------------